做前端或 Node.js 开发时,矩阵运算、图像像素处理、物理模拟、大规模数组变换这类计算一旦数据量上来,CPU 侧很容易出现卡顿。GPU.js 的定位是把普通 JavaScript 函数转译成着色器语言,编译后交给 GPU 并行执行;检测不到可用 GPU 时自动回退到普通 JavaScript,代码不需要跟着改。它的浏览器端走 WebGL/WebGL2,Node.js 端走 HeadlessGL,适合数据量大、单元素计算相对独立的场景。官方给出的基准是 512×512 矩阵乘法通常快 1~15 倍,具体取决于硬件;零散小计算则可能被 GPU 调用开销抵消,不一定值得迁移。
安装与运行环境准备
Node.js 项目直接安装 npm 包:
- npm install gpu.js --save
- // 或
- yarn add gpu.js
复制代码
CommonJS 引入方式如下,TypeScript 项目可直接使用自带类型定义,v2 起可这样写:
- const { GPU } = require('gpu.js');
- const gpu = new GPU();
- // TypeScript
- // import { GPU } from 'gpu.js';
复制代码
浏览器环境可以下载发行包后引入 dist/gpu-browser.min.js,也可以走 unpkg 或 jsdelivr 的 CDN。浏览器里 GPU 同时是构造函数和命名空间,后续核函数写法与 Node.js 环境一致,区别只在引入方式。Linux 下 Node 环境编译 HeadlessGL 前需要安装头文件,否则可能安装或运行失败:
- sudo apt install mesa-common-dev libxi-dev
复制代码
不同发行版按实际包名调整,Windows 和 macOS 一般不需要这一步。
createKernel 与输出尺寸
GPU.js 的核心 API 是 gpu.createKernel()。传给它的函数不是执行一次返回一个值的普通函数,而是描述输出中每个位置如何计算的核函数。核函数内部通过 this.thread.x、this.thread.y、this.thread.z 取得当前线程坐标,输出尺寸必须用 output 指定;不指定 output 的核函数无法被加速。
维度对应关系要写清楚:1D 使用 output: [length],核函数内取 value[this.thread.x];2D 使用 [width, height],取 value[this.thread.y][this.thread.x];3D 使用 [width, height, depth],取 value[this.thread.z][this.thread.y][this.thread.x]。
创建时可传配置对象,也可用链式方法设置。常用项包括 output、constants 编译期常量、loopMaxIterations 动态循环最大迭代次数(默认 1000)、graphical 图形化输出、pipeline 纹理管线、precision 取 unsigned 或 single、tactic 取 speed、balanced 或 precision。new GPU() 时可通过 mode 指定模式,默认 gpu 自动选择后端;dev 模式不转译代码并保留 JS 执行环境,便于打断点;cpu 模式用于强制 CPU 运行排查问题。
第一个核函数
下面这个例子让每个线程返回自己的 x 坐标,输出 Float32Array,每一项由对应位置的线程独立计算:
- const { GPU } = require('gpu.js');
- const gpu = new GPU();
- const kernel = gpu.createKernel(function() {
- return this.thread.x;
- }).setOutput([100]);
- const result = kernel();
- // Float32Array [0, 1, 2, 3, ... 99]
复制代码
矩阵乘法与常量复用
矩阵乘法是典型场景,循环上限可以写成固定数字,也可以放到 constants 里复用同一核函数:
- const multiplyMatrix = gpu.createKernel(function(a, b) {
- let sum = 0;
- for (let i = 0; i < this.constants.size; i++) {
- sum += a[this.thread.y][i] * b[i][this.thread.x];
- }
- return sum;
- }, {
- constants: { size: 512 },
- output: [512, 512]
- });
- const c = multiplyMatrix(a, b); // a、b 为 512x512 二维数组
复制代码
输入参数与 input 压平上传
核函数入参支持数字、1~3 维数组(普通 Array 或 Float32Array、Int16Array 等类型化数组)、HTML Image、Image 数组,以及 v2 新增的 Video 元素。大数组建议使用 input 类型预压平后上传,比多层嵌套数组更高效:
- const { GPU, input } = require('gpu.js');
- const gpu = new GPU();
- const kernel = gpu.createKernel(function(a, b) {
- return a[this.thread.y][this.thread.x] + b[this.thread.y][this.thread.x];
- }).setOutput([3, 3]);
- kernel(
- input(new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9]), [3, 3]),
- input(new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9]), [3, 3])
- );
复制代码
input(value, size) 是 new Input(value, size) 的简写,第一个参数是压平后的一维数组,第二个参数声明原始尺寸。
图形化输出与像素处理
把 graphical 设为 true,核函数里用 this.color(r, g, b, a) 输出像素颜色,结果会直接画到 canvas,之后从 kernel.canvas 取画布插入页面:
- const render = gpu.createKernel(function() {
- this.color(0, 0, 0, 1);
- }).setOutput([20, 20]).setGraphical(true);
- render();
- document.body.appendChild(render.canvas);
复制代码
读取图片像素时,可把 image 作为核函数入参,按 this.thread.y、this.thread.x 取像素,再用 this.color 输出:
- const kernel = gpu.createKernel(function(image) {
- const pixel = image[this.thread.y][this.thread.x];
- this.color(pixel[0], pixel[1], pixel[2], pixel[3]);
- }).setGraphical(true).setOutput([100, 100]);
- const image = document.createElement('img');
- image.src = 'my/image/source.png';
- image.onload = () => {
- kernel(image);
- document.body.appendChild(kernel.canvas);
- };
复制代码
做动画渲染时应配合 requestAnimationFrame 调用核函数,官方建议不要用 setTimeout。需要取回像素数据时调用 kernel.getPixels(),返回类似 WebGL readPixels 的压平数组。
组合核函数与保留中间结果
多次 GPU 计算之间来回应付 CPU 与 GPU 数据搬运,开销会明显增加。combineKernels 可以把多个核函数串联起来,让数据尽量留在 GPU 上执行:
- const add = gpu.createKernel(function(a, b) {
- return a[this.thread.x] + b[this.thread.x];
- }).setOutput([20]);
- const multiply = gpu.createKernel(function(a, b) {
- return a[this.thread.x] * b[this.thread.x];
- }).setOutput([20]);
- const superKernel = gpu.combineKernels(add, multiply, function(a, b, c) {
- return multiply(add(a, b), c);
- });
- superKernel(a, b, c);
复制代码
如果一次核计算里还要保留多个中间结果,例如机器学习前向传播结果之后还要用于反向传播,可以用 createKernelMap,返回值中会包含各个命名的中间结果和最终 result:
- const megaKernel = gpu.createKernelMap({
- addResult: function add(a, b) {
- return a + b;
- },
- multiplyResult: function multiply(a, b) {
- return a * b;
- }
- }, function(a, b, c) {
- return multiply(add(a[this.thread.x], b[this.thread.x]), c[this.thread.x]);
- }, { output: [10] });
- megaKernel(a, b, c);
- // 结果:{ addResult: Float32Array, multiplyResult: Float32Array, result: Float32Array }
复制代码
Pipeline 与自定义函数
开启 pipeline 后,核函数输出不再转回 JS 数组,而是以 Texture 形式留在 GPU,可直接作为下一个核函数的入参;需要 JS 数组时再调用 toArray:
- const kernel1 = gpu.createKernel(function(v) {
- return v[this.thread.x];
- }).setPipeline(true).setOutput([100]);
- const kernel2 = gpu.createKernel(function(v) {
- return v[this.thread.x];
- }).setOutput([100]);
- const result1 = kernel1(array); // 返回 Texture
- console.log(result1.toArray());
- const result2 = kernel2(result1);
复制代码
配合 immutable: true 使用时要注意释放显存:不再需要的纹理调用 texture.delete(),想清零保留的调用 texture.clear(),整个实例用完调用 gpu.destroy()。
核函数中还可以调用预先注册的辅助函数,addFunction 必须在 createKernel 之前调用;也可以用 kernel.addFunction() 只注册到单个核:
- gpu.addFunction(function mySuperFunction(a, b) {
- return a - b;
- });
- const kernel = gpu.createKernel(function(a, b) {
- return mySuperFunction(a[this.thread.x], b[this.thread.x]);
- }).setOutput([20]);
复制代码
核函数内部支持 const、let 声明数字、布尔值以及长度 2~4 的小数组,也支持 v2 新增的解构赋值。
常见报错与排查流程
没有 GPU 也能运行。GPU.js 检测不到可用 GPU 会自动回退到普通 JavaScript 执行,功能一致但没有加速;排查时可手动 new GPU({ mode: 'cpu' }) 强制 CPU 模式。
循环执行报错或结果不对,通常是因为 GPU 上的循环必须有确定的最大迭代次数。处理方式有三种:循环条件写成固定数字;用 this.constants.xxx 编译期常量;无法写死时设置 loopMaxIterations,默认 1000。循环次数超过设定值会导致计算错误或异常。
Node.js 下安装或运行失败,优先检查 HeadlessGL 编译依赖,Linux 上安装 mesa-common-dev 和 libxi-dev 后重新 npm install gpu.js;机器确实没有可用显卡环境时,库会回退 CPU 模式,代码仍可运行,只是没有加速效果。
Math.xxx 函数不被支持,是因为核函数会被编译成 GLSL,并非所有 JS Math 方法都能映射。支持列表包括 Math.abs、Math.floor、Math.ceil、Math.sqrt、Math.pow、Math.exp、Math.log、Math.sin/cos/tan 及其反函数和双曲函数、Math.min/Math.max、Math.round、Math.sign、Math.random 等常用方法;明确不支持 Math.clz32 和 Math.hypot,遇到时需要手动改写成支持的组合。
判断当前环境支持哪些能力,可读取 GPU 构造函数上的静态检测属性:GPU.isGPUSupported、GPU.isWebGL2Supported、GPU.isWebGLSupported、GPU.isHeadlessGLSupported、GPU.isOffscreenCanvasSupported、GPU.isSinglePrecisionSupported。
核函数写错时推荐分三步调试:先用 new GPU({ mode: 'dev' }) 进入开发模式,代码不做转译、保留完整 JS 上下文,可以直接打断点;逻辑确认后在 mode: 'cpu' 下编译运行,可在核函数里写 debugger 检查编译产物行为;最后再上 GPU。真正的 GPU 核函数无法打断点,只能用提前 return 中间变量的方式观察数值,或配合 graphical: true 用 this.color() 把数据可视化。
长期运行的应用,尤其是开启 pipeline 和 immutable 后,应显式释放资源:texture.delete() 删除纹理、texture.clear() 清零复用、kernel.destroy() 销毁单个核、gpu.destroy() 销毁整个实例,否则显存会持续增长。
落地建议
接入流程可以归纳为四步:npm install gpu.js 或浏览器 script 引入,new GPU() 创建实例,gpu.createKernel() 编写核函数并指定 output,像普通函数一样调用核函数拿结果。迁移前建议先用官方基准场景实测自己机器上的加速比,再决定哪些模块值得搬到 GPU;小数据量、强依赖上下文的逻辑留在 CPU 更合适。遇到具体报错时,可结合报错信息到 gpujs/gpu.js 的 Issues 中排查。 |