Follows #866 (WebAssembly backend). Not a defect claim — that PR states plainly that anything the backend cannot take degrades to cpu. This is a measurement of what that costs on a real workload suite, because the fraction turned out much larger than I expected.
Measurement
I added a webasm column to the gpu.rocks benchmark suite — 30 GPGPU workloads, each cross-checked against a plain-JS reference before timing. Apple M1 Max, Chrome 150, branch build of #866 (11b4c0e).
13 of 30 ran on WebAssembly. 17 of 30 silently ran on cpu instead.
Every one of the 17 was rejected by the same guard, and none for any other reason:
// src/backend/web-assembly/kernel.js, build()
if (this.graphical) return this.requestFallback(arguments);
if (this.subKernels && this.subKernels.length) return this.requestFallback(arguments);
if (this.pipeline) return this.requestFallback(arguments);
Minimal reproduction — the kernel body is irrelevant, only the setting matters:
new GPU({ mode: 'webasm' }).createKernel(function (a) { return a[this.thread.x] * 2; })
.setOutput([8])(input); // mode 'webasm'
new GPU({ mode: 'webasm' }).createKernel(function (a) { return a[this.thread.x] * 2; })
.setPipeline(true).setOutput([8])(input); // mode 'cpu'
setImmutable(true), setPrecision('single'), 3D output, setDynamicOutput, setLoopMaxIterations, nested loops, while, Math.min/max, added functions, integer division and modulo all compile to WebAssembly fine. It is specifically pipeline (and graphical, and sub-kernels).
Why this bites harder than it looks
Pipelining is not an optimisation people sprinkle on — it is how any multi-pass GPGPU algorithm keeps its working set on the device between passes. In this suite the rejected set is essentially "everything iterative":
bitonic-sort, canny, erosion, fft, gray-scott, heat, ising, jacobi, launch-overhead, life, path-trace, reduction, residency, sobel, spectral-filter, topk, wavefront
An FFT is 14 dependent passes; Conway's Life is 96; a Jacobi solve is a stencil stepped hundreds of times. Those are exactly the shapes someone reaches for a compute backend to accelerate, and all of them get plain cpu.
There is an argument that pipelining is meaningless without device memory to pipeline into — wasm has no textures, and a Texture handle is not a thing the emitter could consume. But the observable behaviour is that a kernel written for GL, run under mode: 'webasm', produces correct results at cpu speed while reporting nothing. Two things would help even without supporting it:
- Make it visible.
kernel.kernel.constructor.mode is the only way to discover the degradation. I only caught it because the benchmark asserts the backend it asked for is the backend that ran. A one-time console.warn naming the reason, or a queryable degradedReason, would stop this being silent.
- Consider accepting
pipeline as a no-op. On this backend the output is already a plain typed array in linear memory; treating a pipelined kernel as an unpipelined one returning its array would let the 17 run, at the cost of the caller's .delete()/.toArray() expectations. That may well be the wrong trade — but silently halving the backend's reach is also a trade.
What the 13 that did run look like
Not the point of this issue, but for calibration — speed-up against plain JS, and against the cpu backend on the same kernel:
| workload |
webasm |
vs plain JS |
vs cpu backend |
| monte-carlo |
315 ms |
6.95× |
7.06× |
| ode-rk4 |
661 ms |
4.06× |
4.06× |
| gradient-descent |
460 ms |
3.98× |
3.97× |
| sdf-march |
385 ms |
2.30× |
2.60× |
| nbody |
255 ms |
2.25× |
2.27× |
| matmul |
857 ms |
1.63× |
3.62× |
| ncc-template |
776 ms |
1.33× |
6.69× |
Zero WRONG and zero errors across all 30 rows — where it runs, it is correct. Checksums matched the plain-JS reference within 1e-4 relative on every row.
Follows #866 (WebAssembly backend). Not a defect claim — that PR states plainly that anything the backend cannot take degrades to cpu. This is a measurement of what that costs on a real workload suite, because the fraction turned out much larger than I expected.
Measurement
I added a
webasmcolumn to the gpu.rocks benchmark suite — 30 GPGPU workloads, each cross-checked against a plain-JS reference before timing. Apple M1 Max, Chrome 150, branch build of #866 (11b4c0e).13 of 30 ran on WebAssembly. 17 of 30 silently ran on cpu instead.
Every one of the 17 was rejected by the same guard, and none for any other reason:
Minimal reproduction — the kernel body is irrelevant, only the setting matters:
setImmutable(true),setPrecision('single'), 3D output,setDynamicOutput,setLoopMaxIterations, nested loops,while,Math.min/max, added functions, integer division and modulo all compile to WebAssembly fine. It is specificallypipeline(andgraphical, and sub-kernels).Why this bites harder than it looks
Pipelining is not an optimisation people sprinkle on — it is how any multi-pass GPGPU algorithm keeps its working set on the device between passes. In this suite the rejected set is essentially "everything iterative":
bitonic-sort, canny, erosion, fft, gray-scott, heat, ising, jacobi, launch-overhead, life, path-trace, reduction, residency, sobel, spectral-filter, topk, wavefrontAn FFT is 14 dependent passes; Conway's Life is 96; a Jacobi solve is a stencil stepped hundreds of times. Those are exactly the shapes someone reaches for a compute backend to accelerate, and all of them get plain cpu.
There is an argument that pipelining is meaningless without device memory to pipeline into — wasm has no textures, and a
Texturehandle is not a thing the emitter could consume. But the observable behaviour is that a kernel written for GL, run undermode: 'webasm', produces correct results at cpu speed while reporting nothing. Two things would help even without supporting it:kernel.kernel.constructor.modeis the only way to discover the degradation. I only caught it because the benchmark asserts the backend it asked for is the backend that ran. A one-timeconsole.warnnaming the reason, or a queryabledegradedReason, would stop this being silent.pipelineas a no-op. On this backend the output is already a plain typed array in linear memory; treating a pipelined kernel as an unpipelined one returning its array would let the 17 run, at the cost of the caller's.delete()/.toArray()expectations. That may well be the wrong trade — but silently halving the backend's reach is also a trade.What the 13 that did run look like
Not the point of this issue, but for calibration — speed-up against plain JS, and against the cpu backend on the same kernel:
Zero WRONG and zero errors across all 30 rows — where it runs, it is correct. Checksums matched the plain-JS reference within 1e-4 relative on every row.