WebGL(Web Graphics Library)是一个 JavaScript API,允许网页在 <canvas> 元素上渲染 2D 和 3D 图形。它基于 OpenGL ES 2.0 规范,通过浏览器直接调用 GPU 进行硬件加速渲染,无需安装任何插件。
JavaScript 代码
↓
WebGL API(浏览器提供)
↓
GPU 驱动
↓
显卡硬件WebGL 的核心能力是把计算任务提交给 GPU 执行。虽然它诞生的目的是图形渲染,但其底层的并行计算能力,恰好可以被用来做神经网络训练。
神经网络的绝大部分计算是矩阵乘法。以一层全连接层为例:
输出 = 输入 × 权重 + 偏置假设输入是 [batch, 1000],权重是 [1000, 500],那么输出是 [batch, 500],计算量是 batch × 1000 × 500 次乘加运算。当 batch = 32 时,这是 1600 万次浮点运算——而一层网络里可能有几十上百个这样的操作。
CPU 通常只有 4-16 个核心,串行处理这些运算会非常慢。GPU 有数千个小型计算核心,可以同时处理大量并行的乘加运算。这正是神经网络训练需要的。
理解 WebGL 如何支持神经网络,需要先理解它的几个核心概念。
WebGL 的计算逻辑写在着色器里。着色器是用 GLSL(OpenGL Shading Language)编写的小程序,运行在 GPU 上。
两种着色器:
类型 | 作用 |
|---|---|
顶点着色器(Vertex Shader) | 处理每个顶点的位置 |
片段着色器(Fragment Shader) | 处理每个像素的颜色 |
TensorFlow.js 的 WebGL 后端主要使用片段着色器来执行数学运算。
纹理是 GPU 上的一块内存,原本用来存储图像。TensorFlow.js 把张量数据存进纹理:
[100, 200] 的张量,可以存为一个 100 × 200 的纹理100 × 200 × 4 的纹理可以存 100 × 200 个四维向量计算的结果不能直接写回纹理(GPU 的限制),需要先渲染到一个帧缓冲,再作为下一次计算的输入纹理。TensorFlow.js 用两个帧缓冲来回切换,实现"计算 → 存结果 → 用结果继续算"的循环。
TensorFlow.js 在 WebGL 上做一次矩阵乘法,大致流程是:
1. 把输入张量编码成纹理
2. 把权重编码成纹理
3. 编译一个执行矩阵乘法的片段着色器
4. 创建一个和输出形状相同的帧缓冲
5. 执行 draw call,GPU 并行计算每个输出元素
6. 从帧缓冲读回结果纹理
7. 把结果纹理作为下一次运算的输入整个过程,每一次运算都是一次"渲染"。GPU 不知道它在算神经网络,它以为自己在画图。
TensorFlow.js 提供了多个后端:
后端 | 计算位置 | 特点 |
|---|---|---|
CPU | CPU(JS 实现) | 最慢,兼容性最好 |
WebGL | GPU | 快,但受显存和设备限制 |
WASM | CPU(WebAssembly) | 稳定,适合 Worker 环境 |
WebGPU | GPU(新 API) | 未来方向,兼容性还不成熟 |
TensorFlow.js 的 WebGL 后端做了几件关键的事:
1. 张量即纹理
每个 tf.Tensor 在 WebGL 后端里对应一个 WebGLTexture。张量的形状映射到纹理的宽高。
2. 算子即着色器
每个算子(add、matMul、conv2d 等)对应一个 GLSL 片段着色器。TensorFlow.js 内置了上百个着色器程序,覆盖了所有常用运算。
3. 内存管理
GPU 显存和 JS 堆是分开的。TensorFlow.js 需要:
tf.dispose() 或 tf.tidy() 时释放纹理4. 数据搬运
texImage2D)readPixels)数据搬运是性能瓶颈。每次训练迭代都要把 batch 数据从 CPU 传到 GPU,训练结束再把结果传回来。
以本站的 LSTM 模型为例,训练流程如下:
原始号码 → one-hot 编码 → 滑动窗口切片 → 训练/验证集划分这些操作在 CPU 上完成,因为数据量不大,且需要灵活的 JS 逻辑。
用 TensorFlow.js 的 Layers API 定义网络结构:
const model = tf.sequential();
model.add(tf.layers.lstm({ units: 128, inputShape: [windowSize, featureCount] }));
model.add(tf.layers.dense({ units: outputSize, activation: 'softmax' }));
model.compile({ optimizer: 'adam', loss: 'categoricalCrossentropy' });模型定义只是声明计算图,真正的计算发生在 fit() 被调用时。
model.fit() 内部做的是:
for epoch in 1..N:
for batch in data:
1. 把 batch 数据上传到 GPU
2. 前向传播(多次着色器运算)
3. 计算损失
4. 反向传播(自动求导 + 多次着色器运算)
5. 更新权重
6. 记录 loss
验证集评估每一步的前向和反向传播,都是几十上百次 GPU 渲染。对于一个小型 LSTM,一个 epoch 可能触发上千次 WebGL 调用。
训练完成后,用最新的窗口数据做一次预测:
输入序列 → 模型 → softmax 输出 → 概率分布 → 采样 → 号码GPU 显存通常只有 1-4 GB,浏览器能用的更少。模型太大、batch 太大都会导致显存溢出。
应对:控制模型规模,减小 batch size,及时 dispose() 中间张量。
浏览器为了省电,会在页面切到后台时暂停 requestAnimationFrame。而 TensorFlow.js 的 WebGL 后端依赖 requestAnimationFrame 来调度 GPU 任务。
结果:用户切到别的标签页,训练就暂停了。
应对:
WebGL 的浮点运算是 FP16(半精度),和 Python 版的 FP32 有差异。在某些模型上会累积误差,导致数值不稳定。
应对:
不同 GPU、不同浏览器对 WebGL 的支持差异很大。部分设备只能用软件渲染器(如 SwiftShader),速度比硬件加速慢几十倍。
应对:
WEBGL_debug_renderer_info 判断是否为软件渲染传统方案是把数据传到服务器,在服务器上训练,再把结果传回浏览器。浏览器端训练的价值在于:
维度 | 服务端训练 | 浏览器端训练 |
|---|---|---|
隐私 | 数据要上传 | 数据不出浏览器 |
成本 | 服务器和 GPU 费用 | 用户设备承担 |
延迟 | 受网络影响 | 无网络往返 |
可扩展性 | 受服务器资源限制 | 天然分布式(每个用户独立) |
可访问性 | 需要后端服务 | 纯前端,可静态部署 |
对于本项目的场景——用户用几十到几百条历史数据训练小型序列模型——浏览器端训练的隐私优势和零成本优势远大于性能劣势。
WebGL 让浏览器获得了访问 GPU 的能力,而 TensorFlow.js 把神经网络的每一次运算都"伪装"成图形渲染,让 GPU 在不知不觉中完成了模型训练。
这套机制的核心是:
理解这些机制,才能在实际开发中做出正确的权衡:什么时候该用 WebGL,什么时候该退回 WASM,什么时候该放弃浏览器端训练。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。