编程 WebGPU 计算管线深度拆解:当浏览器开始「直接调用 GPU 算力」——从 WGSL 计算着色器、Storage Buffer 到浏览器端 GPGPU 与 AI 推理的工程全貌(2026)

2026-07-19 02:43:47 +0800 CST views 10

WebGPU 计算管线深度拆解:当浏览器开始「直接调用 GPU 算力」——从 WGSL 计算着色器、Storage Buffer 到浏览器端 GPGPU 与 AI 推理的工程全貌(2026)

如果说 WebGL 让浏览器学会了「画图」,那么 WebGPU 让浏览器第一次真正学会了「算」。本文不聊 3D 渲染那一套老生常谈,而是聚焦绝大多数教程一笔带过的 Compute Pipeline(计算管线)——也就是用 GPU 做通用并行计算(GPGPU)。我们会从心智模型、WGSL 计算着色器、Storage Buffer,一路打到矩阵乘法、图像卷积、真实性能基准,最后落到 2026 年最火热的话题:浏览器端如何用 WebGPU 直接跑大模型推理。


一、背景介绍:为什么 WebGPU 不是「又一个 WebGL」

1.1 WebGL 的「计算」是一场 hack

2009 年 WebGL 1.0 诞生,它把 OpenGL ES 搬到了浏览器,让网页能画 3D。但 GPU 真正的杀手锏不是画图,而是 海量并行的数值计算。从 2006 年 NVIDIA 提出 CUDA、到 OpenCL 让 CPU/GPU 协同成为常态,原生世界的科学计算、深度学习早就吃满了 GPU 的红利。

而浏览器这一侧,整整落后了十几年。在 WebGPU 出现之前,想在网页里做 GPU 计算,唯一的办法是把数据塞进纹理(texture),用 fragment shader 当计算器,算完再把结果从帧缓冲读回。这套流程有几个致命问题:

  • 语义扭曲:你明明在做矩阵乘法,却要假装自己在「渲染一个全屏四边形」。
  • 精度受限:早期 WebGL 的浮点纹理支持参差不齐,OES_texture_float 扩展不一定有,half-float 精度又不够。
  • 吞吐受缚:fragment shader 一次只算一个像素,你要把 N 维向量「拍扁」成二维图像,索引换算极其反人类。
  • 无法复用:计算结果留在 GPU 显存里,想接着算下一步还得再绕一圈纹理,CPU-GPU 往返成本高得离谱。

一句话:WebGL 是给画家用的 API,不是给工程师用的计算 API。

1.2 WebGPU 的设计哲学:对标 Vulkan / D3D12 / Metal

WebGPU 由 W3C 的「GPU for the Web」社区组推动,参与方包括 Google、Mozilla、Apple、Intel、Microsoft。它的核心目标一句话概括:

给 Web 一个底层、现代、安全的 GPU 接口,能力对齐原生图形 API(Direct3D 12、Metal、Vulkan),并且把通用计算当成一等公民

注意「一等公民」这四个字。在 WebGPU 里,compute shader 不是渲染的附庸,而是和 render pipeline 平级的独立管线。你可以完全不画一个像素,纯粹用 GPU 做计算。

1.3 2026 年的现实位置

  • 浏览器支持:Chrome / Edge 早已稳定支持 WebGPU;Firefox、Safari 也在 2024–2025 陆续默认开启。到 2026 年,主流桌面浏览器基本「开箱即用」。
  • WebGPU 2.0 在路上:Chrome 2026 版的发布说明里已经出现 WebGPU2.0 相关能力(更细粒度的资源控制、更强的计算特性),V8 引擎也开始原生集成本地小模型,浏览器本地 AI 成为主线叙事。
  • 安全成为显性议题:2026 年 Chrome 的 Dawn 组件(WebGPU 的 C++ 实现层)就曾曝出高危漏洞 CVE-2026-5281,根源落在 GPU 资源/着色器处理上。它提醒我们:当浏览器把 GPU 这么底层的硬件能力开放出来,沙箱与输入校验就是生命线——后面架构一节会展开。

对后端、全栈、数据方向的程序员来说,现在学 WebGPU 的理由很实在:模型在本地跑、在浏览器跑、在 GPU 上跑,正在从噱头变成工程常态。 你不一定去写 3D 引擎,但你完全可能用 WebGPU 给前端加一个实时图像降噪、给报表做百万行数据的并行聚合、或者把一个小模型推理直接塞进网页里。


二、核心概念:先把心智模型立住

GPU 编程和 CPU 编程最大的区别是:你不再「一步步执行」,而是「描述一批数据的并行变换,然后交给硬件去铺开」。下面这套概念,是后面所有代码的地基。

2.1 Adapter、Device、Queue:和显卡打交道的三层

// 1. Adapter = 一块物理显卡(或核显)的句柄
const adapter = await navigator.gpu.requestAdapter({
  powerPreference: "high-performance", // 优先要独显
});

// 2. Device = 在这块显卡上「逻辑创建」的一个设备上下文
const device = await adapter.requestDevice();

// 3. Queue = 向 GPU 提交命令的「发送通道」
// 所有 writeBuffer / submit 都走 device.queue

心智模型:Adapter 是硬件,Device 是你和它之间的逻辑会话,Queue 是快递通道。 CPU 这边组织好命令(编码进 CommandEncoder),通过 Queue 一次性 submit 给 GPU;GPU 异步执行,不阻塞你的 JS 主线程。

2.2 Buffer:显存里的数据块

GPU 不直接读你的 JS 数组,数据必须先搬到显存的 Buffer 里。Buffer 的「性格」由 usage 标志位决定:

usage 标志含义
GPUBufferUsage.STORAGE可作为 compute/render 中读写的大块数据(对应 WGSL 的 storage
GPUBufferUsage.UNIFORM小块只读参数(对应 uniform),适合传矩阵维度、标量
GPUBufferUsage.COPY_SRC可作为 copyBufferToBuffer 的源
GPUBufferUsage.COPY_DST可被 writeBuffer / copy 写入
GPUBufferUsage.MAP_READ可被 CPU mapAsync 读回(注意:不能和 STORAGE 共存)

⚠️ 经典坑:MAP_READ 只能和 COPY_DST 组合。所以你永远不会「直接 map 一个 storage buffer」。标准做法是:计算写进 STORAGE | COPY_SRC 的 buffer,再 copyBufferToBuffer 到一个 MAP_READ | COPY_DST 的只读回传 buffer,最后 map 回传 buffer。后面代码实战会看到完整范式。

2.3 Bind Group:把资源「绑」到 shader 上

WGSL 着色器里声明的 @group(0) @binding(1),靠 Bind Group 在 JS 侧真正对应到某个 Buffer。可以把它理解成「GPU 版的参数列表」。

2.4 Pipeline:渲染 vs 计算

  • Render Pipeline:顶点 → 光栅化 → 片元,输出到画布/纹理。
  • Compute Pipeline:只跑 compute shader,读写 storage buffer / storage texture,不输出像素。本文的主角。

2.5 WGSL:为什么不用 GLSL

WebGPU 的着色语言是 WGSL(WebGPU Shading Language),一门为 WebGPU 重新设计、强类型、带清晰内存模型的现代语言。相比 GLSL,它更贴近 Rust/Swift 的语法风格,编译期校验更强,错误信息也友好得多。

一个最精简的 compute 入口长这样:

@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
  // gid 是「全局第几个线程」,从 (0,0,0) 开始
}

2.6 Workgroup 与并行粒度(最关键的概念)

这是 GPU 计算最容易劝退新人的地方,但也是性能的核心。理解它,就理解了 WebGPU 计算的一半。

  • Invocation(调用):一个线程,对应一个 @builtin(global_invocation_id)
  • Workgroup(工作组):一组线程打包调度,是 GPU 调度和共享内存的基本单位。@workgroup_size(x, y, z) 定义每个 workgroup 的形状。
  • GPU 实际按 dispatchWorkgroups(W, H) 派出 W×H 个 workgroup,每个 workgroup 内含 x×y×z 个线程。
总线程数 = dispatchWorkgroups 数量 × workgroup_size

为什么 workgroup 重要? 因为同一个 workgroup 内的线程可以:

  1. 通过 workgroupBarrier() 做同步;
  2. 通过 var<workgroup> 共享一块「工作组内存」(类比 CUDA 的 shared memory / OpenCL 的 local memory)。

这正是后面做高性能矩阵乘法(tiling)的支点。

2.7 异步模型

WebGPU 全程异步:requestAdapterrequestDevicemapAsync 都是 Promise;命令提交(queue.submit)本身不等待 GPU 完成。这意味着你写的是「声明式地铺任务」,而不是「同步地等结果」。


三、架构分析:规范、实现与安全沙箱

3.1 三层结构

┌─────────────────────────────────────────┐
│  你的代码(JS / TS / Rust+wgpu)          │  ← 应用层
├─────────────────────────────────────────┤
│  WebGPU 规范(W3C,WGSL 语法/语义)       │  ← 标准层
├─────────────────────────────────────────┤
│  Dawn (Chrome) / wgpu (Firefox)          │  ← 实现层
├─────────────────────────────────────────┤
│  D3D12 (Win) │ Metal (mac) │ Vulkan (Linux)│  ← 操作系统原生 API
└─────────────────────────────────────────┘

3.2 两个实现:Dawn 与 wgpu

  • Dawn(C++,Chromium 系):把 WebGPU 映射到 D3D12 / Vulkan / Metal。Chrome 和 Edge 都靠它。CVE-2026-5281 就出在 Dawn 组件里——当一个被「沙箱化」的网页请求 GPU 资源时,Dawn 层对缓冲区/着色器参数的边界校验出现疏漏,可能导致越界访问。
  • wgpu(Rust,Firefox 系):Firefox 的实现,同时也是 Rust 原生生态里 wgpu crate 的基础。有意思的是,同一套 wgpu 既能给 Firefox 用,也能给你写原生 Rust 程序用,所以「学 WebGPU」的技能在 Rust 服务端/桌面端也能复用。

3.3 原生 API 映射(为什么它快)

概念VulkanD3D12MetalWebGPU
设备VkDeviceID3D12DeviceMTLDeviceGPUDevice
命令队列VkQueueCommandQueueMTLCommandQueueGPUQueue
资源VkBufferID3D12ResourceMTLBufferGPUBuffer
计算管线VkPipelinePipeline StateMTLComputePipelineGPUComputePipeline
工作组workgroupthreadgroupthreadgroupworkgroup

WebGPU 几乎是一对一翻译这些现代 API,因此能榨干现代 GPU 的并行能力,而不是像 WebGL 那样被老旧的 OpenGL 状态机拖后腿。

3.4 安全沙箱:Web 端开放 GPU 的代价

浏览器里的 WebGPU 必须是沙箱内的。一个恶意网页不能:

  • 读到其他进程/标签页显存里的数据;
  • 通过畸形的着色器让 GPU 跑飞、击穿沙箱;
  • 无限制占用 GPU 导致系统卡死(有「看门狗」超时机制,长时间不返回的 GPU 任务会被杀)。

CVE-2026-5281 的启示很直接:你写 WGSL 时,凡是来自外部输入的维度、索引,必须在 JS 侧 + WGSL 侧双重做边界校验(比如 if (idx >= N) { return; })。不要相信任何外部维度。


四、代码实战:从「Hello GPU」到真刀真枪

4.1 最小可运行:让 GPU 把两个数加起来

先建立一个完整的「init → 跑 → 读回」闭环。

const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();

const src = new Float32Array([3.14, 2.71]);
const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
const inBuf = device.createBuffer({ size: src.byteLength, usage });
const outBuf = device.createBuffer({ size: src.byteLength, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });
const readBuf = device.createBuffer({ size: src.byteLength, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });

device.queue.writeBuffer(inBuf, 0, src);

const shader = device.createShaderModule({
  code: /* wgsl */ `
    @group(0) @binding(0) var<storage, read> input: array<f32>;
    @group(0) @binding(1) var<storage, read_write> output: array<f32>;
    @compute @workgroup_size(2)
    fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
      let i = gid.x;
      output[i] = input[i] * 2.0; // 把每个数翻倍
    }
  `,
});

const pipeline = device.createComputePipeline({ layout: "auto", compute: { module: shader, entryPoint: "main" } });
const bind = device.createBindGroup({
  layout: pipeline.getBindGroupLayout(0),
  entries: [
    { binding: 0, resource: { buffer: inBuf } },
    { binding: 1, resource: { buffer: outBuf } },
  ],
});

const enc = device.createCommandEncoder();
const pass = enc.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bind);
pass.dispatchWorkgroups(1); // 1 个 workgroup,里面有 2 个线程
pass.end();
enc.copyBufferToBuffer(outBuf, 0, readBuf, 0, src.byteLength);
device.queue.submit([enc.finish()]);

await readBuf.mapAsync(GPUMapMode.READ);
const out = new Float32Array(readBuf.getMappedRange().slice(0));
readBuf.unmap();
console.log(out); // [6.28, 5.42]

这就跑通了「CPU 写显存 → GPU 并行算 → 回传 CPU」的完整回路。后面所有实战都是这个骨架的加厚版。

4.2 实战一:矩阵乘法(GEMM)的朴素版本

矩阵乘法 C = A × B 是 GPU 计算的「Hello World」,因为它是教科书级的「高度可并行」任务:结果矩阵有 M×N 个元素,每个元素互相独立,天然适合「一个线程算一个元素」。

WGSL(计算着色器):

struct MatParams {
  M: u32,
  N: u32,
  K: u32,
  _pad: u32,   // uniform 结构体需 16 字节对齐
};

@group(0) @binding(0) var<uniform> params: MatParams;
@group(0) @binding(1) var<storage, read> A: array<f32>;
@group(0) @binding(2) var<storage, read> B: array<f32>;
@group(0) @binding(3) var<storage, read_write> C: array<f32>;

@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
  let row = gid.x;
  let col = gid.y;
  // 边界保护:dispatch 的网格可能比矩阵大(向上取整导致)
  if (row >= params.M || col >= params.N) { return; }

  var sum: f32 = 0.0;
  for (var k: u32 = 0u; k < params.K; k = k + 1u) {
    sum = sum + A[row * params.K + k] * B[k * params.N + col];
  }
  C[row * params.N + col] = sum;
}

JS 驱动(完整可运行骨架):

async function gemm(A, B, M, N, K) {
  const device = (await (await navigator.gpu.requestAdapter()).requestDevice());

  const bufA = makeStorage(device, A);
  const bufB = makeStorage(device, B);
  const bufC = device.createBuffer({
    size: M * N * 4,
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
  });
  const readBuf = device.createBuffer({
    size: M * N * 4,
    usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
  });
  const params = new Uint32Array([M, N, K, 0]);
  const bufP = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
  device.queue.writeBuffer(bufP, 0, params);

  const pipeline = device.createComputePipeline({
    layout: "auto",
    compute: { module: device.createShaderModule({ code: GEMM_NAIVE_WGSL }), entryPoint: "main" },
  });
  const bind = device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [
      { binding: 0, resource: { buffer: bufP } },
      { binding: 1, resource: { buffer: bufA } },
      { binding: 2, resource: { buffer: bufB } },
      { binding: 3, resource: { buffer: bufC } },
    ],
  });

  const enc = device.createCommandEncoder();
  const pass = enc.beginComputePass();
  pass.setPipeline(pipeline);
  pass.setBindGroup(0, bind);
  pass.dispatchWorkgroups(Math.ceil(M / 8), Math.ceil(N / 8));
  pass.end();
  enc.copyBufferToBuffer(bufC, 0, readBuf, 0, M * N * 4);
  device.queue.submit([enc.finish()]);

  await readBuf.mapAsync(GPUMapMode.READ);
  const C = new Float32Array(readBuf.getMappedRange().slice(0));
  readBuf.unmap();
  return C;
}

function makeStorage(device, arr) {
  const buf = device.createBuffer({
    size: arr.byteLength,
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
  });
  device.queue.writeBuffer(buf, 0, arr);
  return buf;
}

朴素版的逻辑清晰,但性能并不好。问题在于内层循环每次都从全局显存(global memory)A[row*K+k]B[k*N+col],而 global memory 带宽虽高但延迟也高,且每个线程各读各的、没有复用。当 K 很大时,这就是瓶颈。

4.3 实战二:用 Workgroup 内存做 Tiling(性能分水岭)

原生 GPU 计算里,矩阵乘法性能飞跃的关键招数是 tiling(分块):把一个大矩阵乘拆成小块,把小块先搬进「快但小」的 workgroup 共享内存,让块内线程复用,再算。这把 global memory 的访问次数砍掉一个数量级。

WebGPU 没有 CUDA 那种显式 shared memory 关键字,但有等价的 var<workgroup>——声明在 workgroup 作用域的数组,同一 workgroup 内所有线程共享。

struct MatParams { M: u32, N: u32, K: u32, _pad: u32, };
@group(0) @binding(0) var<uniform> params: MatParams;
@group(0) @binding(1) var<storage, read> A: array<f32>;
@group(0) @binding(2) var<storage, read> B: array<f32>;
@group(0) @binding(3) var<storage, read_write> C: array<f32>;

const TILE: u32 = 16u;

// 每个 workgroup 256 个线程,协同加载 16×16 的 A/B 分块
var<workgroup> tileA: array<f32, 256>;
var<workgroup> tileB: array<f32, 256>;

@compute @workgroup_size(16, 16)
fn main(
  @builtin(local_invocation_id) lid: vec3<u32>,
  @builtin(global_invocation_id) gid: vec3<u32>,
) {
  let row = gid.x;
  let col = gid.y;
  let tx = lid.x;   // workgroup 内列偏移 0..15
  let ty = lid.y;   // workgroup 内行偏移 0..15
  let K = params.K;

  var acc: f32 = 0.0;
  let numTiles = (K + TILE - 1u) / TILE;

  for (var t: u32 = 0u; t < numTiles; t = t + 1u) {
    // 1) 协同加载分块(带边界保护)
    let aCol = t * TILE + tx;
    let bRow = t * TILE + ty;
    tileA[ty * TILE + tx] = select(0.0, A[row * K + aCol], aCol < K);
    tileB[ty * TILE + tx] = select(0.0, B[bRow * params.N + col], bRow < K);

    // 2) 必须等所有线程都加载完,才能开始算
    workgroupBarrier();

    // 3) 在「快内存」上做点积累加
    for (var k: u32 = 0u; k < TILE; k = k + 1u) {
      acc = acc + tileA[ty * TILE + k] * tileB[k * TILE + tx];
    }

    // 4) 写回前再同步,避免下一轮覆盖 tile
    workgroupBarrier();
  }

  if (row < params.M && col < params.N) {
    C[row * params.N + col] = acc;
  }
}

JS 侧只需要把 dispatchWorkgroups(Math.ceil(M/16), Math.ceil(N/16)) 改成 16 的粒度。其余不变。

为什么这里要两道 workgroupBarrier() 第一道保证「所有线程都把自己那份 tile 加载完了,谁都别提前开算」;第二道保证「所有线程都算完了这轮点积,才允许下一轮覆盖 tile 内存」。漏掉任何一道,线程间就会读到半成品数据——这是 GPU 并行 bug 最典型的来源,且不可复现、极难调试

工程经验:tiling 版本在 1024×1024 以上的矩阵上,通常比朴素版快 5–20 倍,因为它把 global memory 访问量从 O(M·N·K) 降到接近 O(M·N·K / TILE)。但要注意 var<workgroup> 有大小上限(通常 16KB/线程组),TILE 不能无限大。

4.4 实战三:用 Compute 做图像处理(Box Blur 卷积)

计算管线不只能算数字,也能处理图像。把图像存成 array<f32>(灰度,逐像素一个 float),用一个 3×3 卷积核做均值模糊:

@group(0) @binding(0) var<uniform> dims: vec2<u32>; // (width, height)
@group(0) @binding(1) var<storage, read> src: array<f32>;
@group(0) @binding(2) var<storage, read_write> dst: array<f32>;

@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
  let w = dims.x;
  let h = dims.y;
  let x = gid.x;
  let y = gid.y;
  if (x >= w || y >= h) { return; }

  var sum: f32 = 0.0;
  var count: f32 = 0.0;
  for (var dy: i32 = -1; dy <= 1; dy = dy + 1) {
    for (var dx: i32 = -1; dx <= 1; dx = dx + 1) {
      let nx = i32(x) + dx;
      let ny = i32(y) + dy;
      if (nx >= 0 && nx < i32(w) && ny >= 0 && ny < i32(h)) {
        sum = sum + src[u32(ny) * w + u32(nx)];
        count = count + 1.0;
      }
    }
  }
  dst[y * w + x] = sum / count;
}

这套模式的真正价值在于:你可以把整张图(比如 4K 图、视频的每一帧)一次性丢给 GPU,几百万像素并行模糊,CPU 几乎零负担。 同样的思路稍作改动,就是边缘检测(Sobel)、风格迁移的底层算子、甚至实时降噪。

4.5 实战四:真实基准——WebGPU vs WASM(SIMD) vs 纯 JS

光说「GPU 快」没说服力。2026 年社区里一个热门实测就是「WebGPU vs WebAssembly 矩阵乘法对决」。方法论如下(用 WebGPU 的 timestamp query 拿到真实 GPU 耗时,而不是 JS 端的 wall-clock,后者会被调度噪声污染):

const adapter = await navigator.gpu.requestAdapter();
const canTs = adapter.features.has("timestamp-query");
const device = await adapter.requestDevice({
  requiredFeatures: canTs ? ["timestamp-query"] : [],
});

const querySet = device.createQuerySet({ type: "timestamp", count: 2 });
const resolveBuf = device.createBuffer({ size: 16, usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC });
const resultBuf = device.createBuffer({ size: 16, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });

// ... 计算 pass 里加 timestampWrites ...
const pass = enc.beginComputePass({
  timestampWrites: { querySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1 },
});
// setPipeline / dispatch ...
pass.end();
enc.resolveQuerySet(querySet, 0, 2, resolveBuf, 0);
enc.copyBufferToBuffer(resolveBuf, 0, resultBuf, 0, 16);
device.queue.submit([enc.finish()]);

await resultBuf.mapAsync(GPUMapMode.READ);
const ts = new BigInt64Array(resultBuf.getMappedRange());
const gpuMs = Number(ts[1] - ts[0]) / 1e6; // 纳秒→毫秒
resultBuf.unmap();

实测量级(消费级独显,浮点 GEMM,仅供参考,硬件相关):

规模纯 JSWASM + SIMDWebGPU(朴素)WebGPU(tiling)
256×256~120 ms~30 ms~6 ms~2 ms
1024×1024~8000 ms~500 ms~40 ms~8 ms
2048×2048超时~4000 ms~180 ms~30 ms

结论很清楚:规模越大,GPU 的复利越夸张。WASM/SIMD 能比 JS 快几倍,但 WebGPU 在大数据量下是另一个数量级——因为它有成千上万个核心同时跑,而 CPU 再怎么 SIMD 也只有几十条向量通道。

注意:小矩阵(比如 16×16)下,WebGPU 反而可能更慢——因为 mapAsync 回传、命令编码、GPU 调度的固定开销摊不薄。所以经验法则:只有当数据量足够大、计算足够「重」时,才值得上 WebGPU。 这不是银弹,是分工。

4.6 实战五:桥接到 AI 推理(浏览器跑大模型)

这是 2026 年 WebGPU 最性感的应用。你不需要自己写矩阵乘法去凑 Transformer——现成引擎已经把 WebGPU 封装好了:

(a)WebLLM(MLC)——把开源大模型直接跑在浏览器 GPU 上:

import * as webllm from "@mlc-ai/web-llm";

const engine = await webllm.CreateMLCEngine(
  "Llama-3.1-8B-Instruct-q4f32_1-MLC",
  { initProgressCallback: (p) => console.log(`加载进度 ${(p.progress * 100).toFixed(1)}%`) }
);

const reply = await engine.chat.completions.create({
  messages: [
    { role: "system", content: "你是一个严谨的工程师。" },
    { role: "user", content: "用一句话解释 workgroup 在 GPU 计算里的作用。" },
  ],
});
console.log(reply.choices[0].message.content);

WebLLM 底层就是 WebGPU 的 compute pipeline + 量化权重 + 高效的 storage buffer 布局。你浏览器里这个 8B 模型推理,全程走的是本机 GPU,数据不出机器。

(b)ONNX Runtime Web —— 用 WebGPU 后端跑任意 ONNX 模型:

import * as ort from "onnxruntime-web";

// 指定用 webgpu 执行提供方
ort.env.wasm.simd = true;
const session = await ort.InferenceSession.create("/models/resnet18.onnx", {
  executionProviders: ["webgpu"],
  graphOptimizationLevel: "all",
});

const feeds = { input: new ort.Tensor("float32", preprocessed, [1, 3, 224, 224]) };
const results = await session.run(feeds);

只要模型导出成 ONNX,executionProviders: ["webgpu"] 一行就能让推理切到 GPU。图像分类、目标检测、语音降噪,全都能在网页里实时跑。


五、性能优化:把「能跑」变成「跑得快」

  1. workgroup_size 对齐硬件 wave:AMD/NVIDIA 的 wavefront/warp 通常是 32 或 64。把 workgroup_size 设成它们的整数倍(如 64、256),能让 GPU「一艘船坐满人」,避免空转。
  2. 内存访问合并(coalesced access):让相邻线程读相邻地址。GEMM 朴素版里 A[row*K+k] 每个线程读不同行、 stride 大,合并差;tiling 把访问局部化后带宽利用率飙升。
  3. Storage Buffer vs Storage Texture:结构化数值用 buffer;图像类用 texture_storage_2d + textureStore,节省带宽且语义清晰。
  4. 减少 CPU-GPU 往返mapAsync 是异步且昂贵的。能一次性算完再回传,就不要「算一步、读一步」。批量提交命令,让 GPU 流水线一直满。
  5. 用 timestamp query 做真实 profiling:永远用 GPU 时间戳而不是 JS performance.now() 包 submit 来估计算法耗时,后者把调度延迟也算进去了。
  6. dispatch 维度与上限dispatchWorkgroups(x, y, z) 各维有上限(通常 65535),超大问题要分块或在 shader 内用 workgroup_id 做二维展开。
  7. 避免 bank conflictvar<workgroup> 数组的索引模式会影响共享内存的 bank 冲突,tile 矩阵乘里常做「转置写入」来规避。

六、总结与展望

6.1 WebGPU 2.0 之后

WebGPU 1.0 已经能打,但 2.0 的方向更激进:更细粒度的资源生命周期、更丰富的计算特性、对机器学习算子更友好的原语。配合 V8 原生集成本地小模型,浏览器的定位正在从「内容展示器」变成「本地算力入口」。

6.2 浏览器端 AI 的范式转移

过去「AI 在云端」是铁律。现在三股力量合流:模型量化(4-bit/1.58-bit 让模型小到能装进浏览器)+ WebGPU(让浏览器能用上 GPU)+ 隐私合规(数据不出端)。像本文提到的 WebLLM、ONNX Runtime Web,正在把「在网页里跑一个不大但够用的模型」变成默认选项。你做的下一个内部工具、下一个客服面板,很可能推理就发生在用户自己的显卡上。

6.3 生态速览(2026)

  • 渲染:Three.js 的 WebGPURenderer、Babylon.js 已全面支持 WebGPU。
  • 计算/ML:TensorFlow.js 的 WebGPU backend、@webgpu/... 工具链、WebLLM、ONNX Runtime Web。
  • 原生复用:Rust 的 wgpu crate 让你写的 WGSL 既能跑在 Firefox,也能跑在你的服务端/桌面程序里——学一次,两端通吃

6.4 给后端/全栈程序员的落地建议

  • 别为小数据上 WebGPU:千级以下的计算,WASM/SIMD 甚至 JS 更划算。
  • 为「重计算 + 大数据 + 可并行」上 WebGPU:图像/视频处理、科学仿真、推荐/检索的向量相似度、前端实时聚合。
  • 把 AI 推理交给成熟引擎:除非你要做底层算子,否则直接用 WebLLM/ONNX Runtime Web,别从零写 Transformer。
  • 永远做边界校验:任何来自用户输入的维度/索引,JS 和 WGSL 两侧都校验——这是 CVE-2026-5281 给全行业的提醒。

写在最后

WebGPU 不是「又一个前端玩具」。它第一次认真地把 GPU 这种变态级并行算力,以安全、标准、跨平台的方式交到了 Web 工程师手里。当你理解了 adapter/device/queue、storage buffer、compute pipeline 和 workgroup 这几块基石,再回头看「浏览器里跑大模型」「网页端实时 4K 降噪」「百万行数据秒级聚合」,就不再是魔法,而是可以拆解、可以优化、可以落地的工程

而作为后端/全栈工程师,最爽的一点是:WGSL 的并行思维(声明变换、交给硬件铺开、用共享内存做局部性优化),和你在服务端写向量化、写批处理、写 SIMD 的内核是同一套心智。学会 WebGPU,你得到的不是一门「浏览器画图 API」,而是一种在现代硬件上榨干并行度的通用能力


参考与延伸阅读

  • W3C WebGPU 规范:https://www.w3.org/TR/webgpu/
  • WGSL 规范:https://www.w3.org/TR/WGSL/
  • Google 官方 WebGPU 文档(web.dev / Chrome for Developers)
  • wgpu(Rust 原生 + Firefox 实现):https://github.com/gfx-rs/wgpu
  • Dawn(Chrome 实现):https://dawn.googlesource.com/dawn
  • WebLLM(浏览器端大模型推理):https://github.com/mlc-ai/web-llm
  • ONNX Runtime Web(WebGPU 后端):https://onnxruntime.ai/
复制全文 生成海报 WebGPU GPGPU WGSL 计算管线 浏览器AI GPU计算

推荐文章

Vue3的虚拟DOM是如何提高性能的?
2024-11-18 22:12:20 +0800 CST
Vue3中的v-model指令有什么变化?
2024-11-18 20:00:17 +0800 CST
程序员茄子在线接单