想象一座汽车工厂:厂长(CPU)一次只能处理一份文件,擅长做决策;车间里却站着成千上万名工人(GPU 核心),可以同时拧螺丝、装轮胎、焊车门。过去的前端世界只有一个“厂长”在 JavaScript 里干活,遇到大量重复计算就容易排队。WebGPU 就是新装的“厂区调度系统”,而 compute shader 是发给工人班组的工作卡——卡上写明每个线程从哪块货架取数、做什么运算、把结果放回哪个货位。用它做矩阵运算,就像把组装一辆车的任务拆给多个班组同时完成:一个班组负责结果矩阵的一个小方块。本文从 GPU 分工讲起,带你走通 WebGPU 的设备获取、缓冲区创建、WGSL 着色器编写、workgroup 调度,再到矩阵乘法的完整实现。

岗位关联

掌握 WebGPU compute shader,你就能在浏览器里直接调动 GPU 算力做 AI 推理和科学计算,从只会发请求的“前端搬运工”,变成能驾驭本机算力的“计算调度员”。

目录
  1. 可视化演示
  2. CPU与GPU分工
  3. WebGPU编程模型
  4. 缓冲区设计
  5. Workgroup调度
  6. 着色器编写
  7. 矩阵乘法实现
  8. 性能与应用
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

CPU与GPU分工

汽车工厂里,厂长(CPU)擅长做决策:审批订单、安排流水线、处理突发情况。可他一次只能处理一件事。如果让厂长亲自给一万辆车拧螺丝,工厂就会瘫痪。GPU 更像车间里成千上万的工人:单个工人不聪明,但大家同时动手,产量惊人。矩阵运算恰好是“拧螺丝”型任务——输出矩阵的每个元素只依赖输入矩阵的若干数值,互相之间没有先后依赖。

浏览器里的 JavaScript 一直扮演厂长。每写一个 for 循环,都像是在让厂长亲自跑腿。WebGPU 改变了分工方式:CPU 负责组织计算任务,GPU 负责批量执行。compute shader 就是一张写满操作步骤的工单,GPU 工人照着工单同时开工。前端开发者需要知道什么任务适合交给 GPU。

适合 GPU 的任务有三个特征:大量独立计算、算术强度高、访存规则。矩阵乘法完美命中。现代 GPU 含有数千个小核心,以 SIMT(单指令多线程)方式运行:一个指令派发给很多核心,对不同数据同时执行。理想情况下,加速比取决于能并行执行的线程数。

当然,实际工厂还要考虑工人往返货架的路程、班组长协调的损耗。数据搬运和线程开销会让加速比低于理论值,但把矩阵乘法这种大批量、低依赖的活交给 GPU,方向完全正确。

Sideal=TserialTparallel=Ntask×tunitNtask×tunit/Nthread=NthreadS_{\text{ideal}} = \frac{T_{\text{serial}}}{T_{\text{parallel}}} = \frac{N_{\text{task}} \times t_{\text{unit}}}{N_{\text{task}} \times t_{\text{unit}} / N_{\text{thread}}} = N_{\text{thread}}
这是理想加速比公式:假设任务能被均匀分给所有 GPU 线程,最终耗时就是串行耗时除以线程数。
  1. 串行总时间一个任务接一个任务做完全部工作所需的时间,等于任务数乘单个任务耗时。
    Tserial=Ntask×tunitT_{\text{serial}} = N_{\text{task}} \times t_{\text{unit}}
  2. 线程数量GPU 能同时运行的线程数,由 workgroup 大小和调度数量共同决定。
    Nthread=workgroup_size×workgroup_countN_{\text{thread}} = \text{workgroup\_size} \times \text{workgroup\_count}
  3. 并行总时间任务被均匀分给所有线程后,每个线程只做总任务量的一个分之一。
    Tparallel=Ntask×tunitNthreadT_{\text{parallel}} = \frac{N_{\text{task}} \times t_{\text{unit}}}{N_{\text{thread}}}
  4. 理想加速比把串行时间除以并行时间,得到线程数本身。
    Sideal=TserialTparallel=NthreadS_{\text{ideal}} = \frac{T_{\text{serial}}}{T_{\text{parallel}}} = N_{\text{thread}}

WebGPU编程模型

工厂光有工人不够,还要有管理制度。WebGPU 就是浏览器里的“厂区管理系统”。拿 WebGPU 做计算,先要通过 navigator.gpu.requestAdapter() 找到显卡(厂区),再用 requestDevice() 申请一个逻辑设备(生产许可证)。设备负责创建管线、缓冲区和纹理。

拿到设备后,需要把“工单内容”固定成可执行流程——compute pipeline(计算管线)。它由着色器模块(WGSL 代码)和管线布局组成:着色器模块描述工人动作,管线布局声明工人使用哪些货架。这是 GPU 能反复执行的标准工序。

实际调度的流程是:device.createCommandEncoder() 创建命令编码器(工单本),在编码器上 beginComputePass() 开启一轮计算,再在 pass 里依次 setPipeline、setBindGroup、dispatchWorkgroups,最后 end()。所有命令编码完毕后,queue.submit([encoder.finish()]) 把整个工单交给 GPU 队列。这个过程里 JavaScript 不参与逐元素计算,它只当调度员。

这套流程就像厂长把每道工序写进工单,再交给班组长分发。明白它,后续的缓冲区、着色器、调度才有安放位置。

threads=X×Y×Z×x×y×z\text{threads} = X \times Y \times Z \times x \times y \times z
总线程数等于“网格三个方向的班组数量”乘“每个班组三个方向的线程数量”。
  1. 网格维度dispatchWorkgroups(X, Y, Z) 指定三个方向上各放多少个班组。
    grid=(X,Y,Z)\text{grid} = (X, Y, Z)
  2. 班组大小@workgroup_size(x, y, z) 声明每个班组内有多少线程。
    workgroup_size=(x,y,z)\text{workgroup\_size} = (x, y, z)
  3. 总线程数把两组维度相乘,就得到本次调度可用的线程总数。
    threads=X×Y×Z×x×y×z\text{threads} = X \times Y \times Z \times x \times y \times z

缓冲区设计

工厂的原材料和成品不能放在厂长口袋里,要放在仓库货架上。WebGPU 中,GPU 能直接读写的是 GPUBuffer。矩阵 A、矩阵 B 是原料,矩阵 C 是成品,分别存放在不同的 storage buffer 里。

创建 GPUBuffer 时,要告诉 GPU 两个关键信息:大小和用途。大小用字节数表示;一个 f32(32 位浮点数)占 4 字节,所以 m×n 的矩阵需要 m*n*4 字节。用途用 usage 标志声明:STORAGE 表示着色器可读写,COPY_DST 表示可以从 CPU 写入数据,COPY_SRC 表示可以回读结果。

从 CPU 上传数据,常用 device.queue.writeBuffer(buffer, 0, data) 直接写入;想要 CPU 读取结果,则要 buffer.mapAsync() 把 GPU 内存临时映射到 CPU 地址空间。GPU 缓冲区有对齐要求,比如某些用法要求大小是 4 的倍数;当矩阵宽度不是 4 的倍数时,要在一行末尾补几个占位符。

货架空间备好,工人才能随时取料。缓冲区设计是 GPU 计算的第一个坑:空间不够或用途不对,后面的着色器寸步难行。

bytes=rows×cols×4\text{bytes} = \text{rows} \times \text{cols} \times 4
这是存储一个 Float32 矩阵所需字节数的公式:元素个数乘以每个元素 4 字节。
  1. 元素个数矩阵共有多少个数需要存储。
    Nelement=rows×colsN_{\text{element}} = \text{rows} \times \text{cols}
  2. 单元素字节数f32 类型占 4 个字节。
    sizeelement=4\text{size}_{\text{element}} = 4
  3. 总字节数元素个数与单个元素字节数相乘。
    bytes=Nelement×4\text{bytes} = N_{\text{element}} \times 4

Workgroup调度

车间里工人不能挤成一团,要分成班组。WebGPU 用 workgroup(工作组)组织线程:一个 workgroup 是 GPU 调度和共享内存的基本单位,相当于一个班;班组内部又包含若干个线程,相当于工人。

在 WGSL 中,@workgroup_size(8, 8, 1) 声明每个班组有 8×8 个线程。dispatchWorkgroups(4, 4, 1) 告诉 GPU 在 X、Y 方向各放 4 个班组。于是整个网格有 16 个班组、共 1024 个线程。每个线程需要知道自己算哪个位置,这就靠内置变量。

WGSL 提供 local_invocation_id(班组内编号)、workgroup_id(班组编号)和 global_invocation_id(全局线程编号)。矩阵运算中,通常用 global_invocation_id.x/y 映射到输出矩阵的行列。共享内存 var<workgroup> 允许班组内线程协作,把一块数据先搬进班组再反复使用,减少全局访存。

班组划分得好,工人少跑路;划分不好,班组之间还要互相等。矩阵乘法里的 tiling 就是把“班组”和“货架数据块”对齐。

global_id=workgroup_id×workgroup_size+local_id\text{global\_id} = \text{workgroup\_id} \times \text{workgroup\_size} + \text{local\_id}
全局线程编号等于“班组编号跳过前面的班组”再加“班组内偏移”。
  1. 班组编号workgroup_id 表示当前班组在整个网格中的位置。
    workgroup_id∈[0,grid_size)\text{workgroup\_id} \in [0, \text{grid\_size})
  2. 班组尺寸workgroup_size 表示一个班组在每个方向上有多少个线程。
    workgroup_size=(x,y,z)\text{workgroup\_size} = (x, y, z)
  3. 班组内偏移local_id 是线程在班组内部的局部坐标。
    local_id∈[0,workgroup_size)\text{local\_id} \in [0, \text{workgroup\_size})
  4. 全局编号先跳过前面的班组,再加上班组内偏移,得到全局唯一线程编号。
    global_id=workgroup_id×workgroup_size+local_id\text{global\_id} = \text{workgroup\_id} \times \text{workgroup\_size} + \text{local\_id}

着色器编写

工单上写什么,工人就干什么。WGSL(WebGPU Shading Language)就是 WebGPU 的工单语言。计算着色器用 @compute 标注入口函数,用 @workgroup_size 声明班组规模,用 @builtin(global_invocation_id) 拿到全局线程坐标。

数据从哪里读?通过绑定组(bind group)把 GPUBuffer 接到着色器上。例如 @group(0) @binding(0) var<storage, read> a: array<f32>; 表示第 0 组第 0 个资源是只读的存储缓冲区,里面是一串 f32。在 JS 端,用 setBindGroup 把实际缓冲区交给 GPU。

因为存储缓冲区是扁平的数组,二维矩阵在 GPU 眼中只是一段连续内存。我们要用线性索引 row * stride + col 找元素。stride 通常等于一行元素的个数(加上可能的 padding)。在矩阵乘法中,每个线程先算出自己的 row、col,再从绑定组里的 A、B 缓冲区取数。

工单写得清楚,工人就不会拿错零件。WGSL 的语法不复杂,关键是记住:着色器运行在 GPU 上,不能用 DOM,只有输入缓冲区和输出缓冲区。

index=row×stride+col\text{index} = \text{row} \times \text{stride} + \text{col}
这是把二维矩阵坐标变成一维数组下标的公式:先跳过 row 个整行,再向右偏移 col 个元素。
  1. 行号与列号线程从 gid.y 和 gid.x 拿到输出矩阵的行列。
    row=gid.y,col=gid.x\text{row} = \text{gid.y}, \quad \text{col} = \text{gid.x}
  2. 行距每行有多少个连续元素,可能包含 padding。
    stride=cols+padding\text{stride} = \text{cols} + \text{padding}
  3. 线性索引用行号乘行距,再加列号,得到元素在缓冲区中的位置。
    index=row×stride+col\text{index} = \text{row} \times \text{stride} + \text{col}
wgsl6 行
@compute @workgroup_size(8, 8)fn main(@builtin(global_invocation_id) gid : vec3<u32>) {  let row = gid.y;  let col = gid.x;  // 读取 A、B 并累加,写入 C}

矩阵乘法实现

现在开始真正的组装。假设要算 C = A × B,C 里每个格子都需要 A 的一行和 B 的一列做“点积”:把对应的数逐对相乘,再全部加起来。就像一个工人要跑到两个货架,分别取一排零件,按顺序配对拧紧。

朴素思路是每个线程负责 C 的一个元素:

这个代码正确,但每次循环都去全局内存取数,工人反复跑仓库,很慢。

优化的办法是分块(tiling):把 A、B 切成小方块,一个 workgroup 负责 C 的一个输出块。先把需要的数据块搬进 var<workgroup> 共享内存,班组内线程反复复用。这样每个元素只需从全局内存读一次,而不是每次乘法都读。实现时要处理边界:当矩阵维度不能被 tile 整除时,用条件判断避免越界。

班组把一块货直接搬到工位旁边,再也不用每次横穿仓库。这就是矩阵乘法在 GPU 上高效跑起来的核心秘密:并行计算 + 数据复用。

Ci,j=∑k=0K−1Ai,k×Bk,jC_{i,j} = \sum_{k=0}^{K-1} A_{i,k} \times B_{k,j}
C 中第 i 行第 j 列的值,等于 A 的第 i 行与 B 的第 j 列逐对相乘再求和。
  1. 输出坐标线程决定 (i,j),准备写 C 的第 i 行第 j 列。
    Ci,jC_{i,j}
  2. 累加变量每一步取 A 的一个元素和 B 的一个元素相乘,再加到总和里。
    sum←sum+Ai,k×Bk,j\text{sum} \gets \text{sum} + A_{i,k} \times B_{k,j}
  3. 求和区间k 从 0 遍历到 K-1,K 是 A 的列数、B 的行数。
    ∑k=0K−1\sum_{k=0}^{K-1}
  4. 完整公式所有 k 的乘积累加完,得到最终结果。
    Ci,j=∑k=0K−1Ai,k×Bk,jC_{i,j} = \sum_{k=0}^{K-1} A_{i,k} \times B_{k,j}
wgsl5 行
let sum = 0.0;for (var k = 0u; k < K; k = k + 1u) {  sum = sum + a[row * K + k] * b[k * N + col];}c[row * N + col] = sum;

性能与应用

工厂好不好,不看工人数量,看单位时间产出多少合格车。GPU 矩阵运算的性能,也要看吞吐量:每秒完成多少浮点运算,或者完成一次矩阵乘法用多少毫秒。在浏览器里可以用 performance.now() 记录提交前后,再通过回读结果估算整体延迟,也可以用 Chrome 的 WebGPU 面板做更深入分析。

除了测速,还要控制资源生命期。重复创建缓冲区和管线会像频繁招工再裁员一样浪费。WebGPU 的优势之一是可复用:创建一次 pipeline,每帧只更新缓冲区和绑定组。

这些能力让浏览器里的 AI 推理成为现实:神经网络全连接层、卷积层底层都是矩阵运算。输入图片先转成矩阵,再交给 GPU compute shader 算,结果可直接画到 canvas。科学计算里的流体模拟、粒子系统、数值求解同样适用。

把工厂从“有人干活”升级到“高效运转”,靠的是性能分析和资源复用。WebGPU 让前端不再只是展示页面,而是真正拥有本机算力。

throughput=tasktime\text{throughput} = \frac{\text{task}}{\text{time}}
吞吐量是衡量 GPU 计算效率的核心指标:单位时间内完成的任务量越多,工厂效率越高。
  1. 任务量矩阵乘法的浮点运算次数可以近似为两倍的 M×N×K。
    task=2×M×N×K\text{task} = 2 \times M \times N \times K
  2. 总耗时实际墙钟时间包含数据搬运、GPU 计算和结果回读。
    Ttotal=Ttransfer+Tcompute+TreadbackT_{\text{total}} = T_{\text{transfer}} + T_{\text{compute}} + T_{\text{readback}}
  3. 吞吐量任务量除以总耗时,得到每秒完成的任务数。
    throughput=taskTtotal\text{throughput} = \frac{\text{task}}{T_{\text{total}}}

AI 应用场景

浏览器端 AI 推理

阶段:模型推理前的张量预处理与矩阵计算

解决的问题:传统方案把图片归一化、缩放、卷积等矩阵运算放在 JavaScript 里做,主线程被大量循环阻塞,界面掉帧;如果上传服务器又会增加隐私风险和网络延迟。WebGPU 可以把这些矩阵运算放到 GPU,让 CPU 专心处理交互。

具体案例:一个前端 OCR 应用,用户截屏后先由 compute shader 把截图转成灰度矩阵、缩放到模型输入尺寸,再在本地完成矩阵乘加。结果可以直接用于后续推理,整个流程不出浏览器。

科学计算可视化

阶段:仿真计算与数值更新

解决的问题:流体模拟每帧都要更新大量速度场、密度场,本质是矩阵/网格运算。用 CPU 模拟只能支撑很小的网格;用服务端计算则延迟高。WebGPU compute shader 可以在显卡上并行更新每个网格点。

具体案例:一个网页版流体交互装置,把画布分成 256×256 的网格,每个线程负责一个格点,根据周围格点速度计算下一帧状态,再直接渲染到 canvas。用户拖动鼠标就能看到实时流体反应。

Python 演示

python36 行
import numpy as np def tiled_matmul(A, B, tile=4):    M, K = A.shape    K2, N = B.shape    assert K == K2    C = np.zeros((M, N), dtype=np.float32)     for i in range(0, M, tile):        for j in range(0, N, tile):            i_end = min(i + tile, M)            j_end = min(j + tile, N)            acc = np.zeros((i_end - i, j_end - j), dtype=np.float32)             for k in range(0, K, tile):                k_end = min(k + tile, K)                a_block = A[i:i_end, k:k_end]                b_block = B[k:k_end, j:j_end]                acc += a_block @ b_block             C[i:i_end, j:j_end] = acc     return C def main():    M, N, K = 64, 64, 64    A = np.random.rand(M, K).astype(np.float32)    B = np.random.rand(K, N).astype(np.float32)     C = tiled_matmul(A, B, tile=8)    C_ref = A @ B     print("max error:", np.max(np.abs(C - C_ref))) if __name__ == "__main__":    main()
目的:用 Python 模拟 GPU 分块矩阵乘法的核心逻辑,帮助你理解 WebGPU compute shader 里的 tile 思想与边界处理。
  1. C = np.zeros((M, N), dtype=np.float32)创建输出矩阵,相当于在 GPU 里分配结果缓冲区。
  2. for i in range(0, M, tile):模拟 dispatch 沿行方向划分的工作组网格。
  3. i_end = min(i + tile, M)边界处理,防止最后一组超出矩阵范围。
  4. acc = np.zeros((i_end - i, j_end - j), dtype=np.float32)每个工作组的局部累加器,相当于共享内存中的结果块。
  5. a_block = A[i:i_end, k:k_end]从全局“货架”上取 A 的一个数据块。
  6. acc += a_block @ b_block核心运算:分块累加,相当于 WGSL 里的循环累加。
  7. C[i:i_end, j:j_end] = acc把工作组算完的结果写回输出缓冲区。

常见误区

误区我一开始以为 WebGPU 就是 WebGL 的升级版,主要用来画更复杂的 3D 图形,跟矩阵运算关系不大。

正解WebGPU 确实包含渲染能力,但它同时提供独立的 compute pipeline,专门做通用并行计算。compute shader 不产出一个像素,而是直接读写缓冲区。你可以把 WebGL 想象成只负责“画画的班组”,而 WebGPU compute 是“不画画、只装配零件”的另一条生产线。

为何常见:GPU 历史上一开始就为图形服务,WebGL 的名字也让人默认新 API 只是图形增强。

误区我以为 compute shader 就是在 JavaScript 里写一个函数,然后让浏览器自动并行执行。

正解compute shader 必须用 WGSL 编写,编译后运行在 GPU 上,无法直接调用 DOM 或 console.log。JavaScript 只负责创建管线、设置绑定组、提交命令,真正的循环和算术发生在显卡里。就像厂长写工单,工人按工单干活,厂长不会自己去拧螺丝。

为何常见:前端开发者的日常语言是 JavaScript,看到“写一段逻辑”很容易套用 JS 的心智模型。

误区很多人觉得线程数越多越好,所以把 `@workgroup_size(256, 256)` 调到显卡能接受的上限。

正解workgroup 过大会消耗过多寄存器和工作组共享内存,导致 GPU 能同时驻留的班组变少,占用率反而下降。合适的 size 需要根据矩阵规模和硬件能力测试,通常 8×8、16×16 是常见起点。就像班组人太多会挤在工位前,反而降低产出。

为何常见:并行计算给人的直觉是“人越多越快”,忽略了硬件资源是有限的。

误区只要把矩阵运算放到 GPU,性能就一定提升,甚至可以无脑加速所有计算。

正解GPU 的优势在大量独立并行计算,但数据从 CPU 内存搬运到 GPU 显存、再把结果回读,需要时间。小矩阵、轻量任务可能完全被搬运成本吃掉。正确的做法是先估算任务规模和计算密度,再决定要不要上 compute shader。

为何常见:看到“几千个核心”很容易忽略数据搬运和调度开销,而前端环境里这些开销往往很明显。

面试题

Q1. 在 WebGPU 编程模型中,queue.submit() 的角色最像工厂里的哪一步?

单选题 · easy

  • A.把写好的工单交给车间← 正确答案
  • B.重新绘制工厂的图纸
  • C.验收已经生产的零件
  • D.给工人计算月薪奖金

答案:A。queue.submit() 把编码好的命令缓冲区提交给 GPU 队列,相当于厂长把写好的工单交给车间执行。它不是绘图、验收或后勤,而是正式派活。

Q2. WebGPU 的 compute shader 主要用来做什么?

单选题 · easy

  • A.执行通用并行计算任务← 正确答案
  • B.只负责绘制三角形图形
  • C.代替 JavaScript 管理网页
  • D.压缩浏览器网络请求

答案:A。compute shader 的核心定位是通用并行计算,它可以做矩阵乘法、物理模拟和 AI 推理。它不负责图形光栅化,更不管理网页或网络请求。

Q3. 下列哪个 WGSL 属性用于声明一个 workgroup 的线程规模?

单选题 · mid

  • A.@workgroup_size(8,8)← 正确答案
  • B.@binding(0) @compute
  • C.@vertex @fragment
  • D.@group(0) @binding(1)

答案:A。@workgroup_size 用来声明每个班组内的线程数,是 compute shader 的常用属性。B、D 是资源绑定相关,C 是渲染管线入口,都与班组线程规模无关。

Q4. 要将两个 4×4 的 f32 矩阵放入 storage buffer,至少需要多少字节?

单选题 · mid

  • A.32 字节
  • B.64 字节
  • C.128 字节← 正确答案
  • D.256 字节

答案:C。一个 4×4 矩阵有 16 个元素,每个 f32 占 4 字节,所以一个矩阵占 64 字节。两个矩阵共 128 字节。注意别漏掉“两个”或“4 字节”这两个条件。

Q5. global_invocation_id = workgroup_id * workgroup_size + local_invocation_id 中,local_invocation_id 表示什么?

单选题 · mid

  • A.线程在班组内的编号← 正确答案
  • B.班组在整个网格的编号
  • C.需要写入的矩阵行数
  • D.GPU 显卡的代号编号

答案:A。公式里 workgroup_id 表示班组编号,local_invocation_id 是班组内部偏移,组合后得到全局线程编号。它描述的是“工人自己在哪个工位”,不是矩阵维度或显卡型号。

Q6. 在分块矩阵乘法中,如果矩阵维度不是 tile 的整数倍,WGSL 代码必须做什么?

单选题 · hard

  • A.添加边界检查防止越界← 正确答案
  • B.把矩阵大小改成质数
  • C.让 CPU 再算一次结果
  • D.调用 mapAsync 同步修正

答案:A。当矩阵维度不能被 tile 整除时,最后一个 block 会越过矩阵边界,必须在着色器里判断是否仍在有效范围内,越界的值不参与计算。改矩阵、让 CPU 重算或同步映射都不是 GPU 分块算法的常规做法。

Q7. 下列哪种做法最可能让 GPU 矩阵乘法反而变慢?

单选题 · extreme

  • A.每帧都上传大矩阵并回读← 正确答案
  • B.使用共享内存缓存分块数据
  • C.复用已经创建好的管线对象
  • D.用 dispatchWorkgroups 并行调度

答案:A。GPU 并行能力很强,但数据搬运是最大瓶颈。每帧上传大矩阵再回读,会让 CPU 和 GPU 频繁同步,搬运时间淹没计算时间。共享内存、复用管线和正确调度都是优化手段,不会让计算变慢。

Q8. 在创建用于矩阵乘法的 storage buffer 时,下面哪些 usage 组合是合理且常用的?

多选题 · mid

  • A.STORAGE | COPY_DST← 正确答案
  • B.STORAGE | COPY_SRC← 正确答案
  • C.VERTEX | INDEX | COPY_SRC
  • D.COLOR_ATTACHMENT | STORAGE

答案:AB。矩阵乘法的输入缓冲区需要 STORAGE 供着色器读取,配合 COPY_DST 从 CPU 上传;输出缓冲区需要 STORAGE 配合 COPY_SRC 回读。C 用于渲染顶点数据,D 中的 COLOR_ATTACHMENT 是纹理用途,不适合 storage buffer。

Q9. 关于 WebGPU 的调度模型,正确的说法有?

多选题 · mid

  • A.调度命令指定三个方向网格大小← 正确答案
  • B.工作班组内部的线程可共享内存← 正确答案
  • C.计算着色器每次只能运行一个线程
  • D.提交队列会在 CPU 上同步完成计算

答案:AB。dispatchWorkgroups(X,Y,Z) 指定三维网格大小;一个 workgroup 内的线程可以通过 var<workgroup> 共享内存。计算着色器通常同时运行大量线程,queue.submit 只是把命令交给 GPU 队列,不表示 CPU 同步计算。

Q10. 在 WGSL 中读取矩阵 A 的 (row, col) 元素时,下面哪些是必要步骤?

多选题 · hard

  • A.用行距和列号计算一维偏移← 正确答案
  • B.从绑定组声明存储缓冲区读取← 正确答案
  • C.在着色器里调用控制台打印日志
  • D.在 JavaScript 中直接操作 DOM

答案:AB。GPU 缓冲区是一维数组,必须用 row * stride + col 计算下标;同时需要在绑定组中声明 var<storage, read> 缓冲区。着色器运行在 GPU 上,不能使用控制台或 DOM,这些操作只能由 JavaScript 在浏览器主线程完成。