Skip to content

2.22.0

Choose a tag to compare

@fuzzie360 fuzzie360 released this 03 Aug 01:33

gpu.js grows a fourth compute backend: kernels now compile to WebAssembly — SIMD-vectorized, optionally threaded, running everywhere wasm runs, with no GPU and no headless-gl build. It slots into the automatic fallback chain one step above cpu, so kernels that used to land on plain JavaScript now land on something 2–20× faster without a code change. No breaking changes.

The WebAssembly backend

new GPU({ mode: 'webasm' }), or automatically whenever the GL backends are unavailable. The kernel translates to a hand-assembled wasm module — no Emscripten, no runtime dependency — with the same typing rules as the GL/WebGPU backends and Math.random as the same seeded PCG stream as WebGPU.

Three execution tiers, chosen automatically:

  • Scalar wasm: the baseline, roughly 2× plain JavaScript on arithmetic-dense kernels.
  • SIMD: four cells per step through wasm's 128-bit vectors, wherever the platform validates SIMD (all modern browsers and Node). Divergent control flow vectorizes through mask predication rather than bailing out — both sides of a branch execute blended, which still prices out ahead on real workloads. run and run_simd are bit-identical per cell, pinned by tests.
  • Threaded: under the async contract (asyncMode: true), outputs of 4096+ cells split across a worker pool over one shared memory — each worker running the SIMD path. Pool size follows hardwareConcurrency (cap it with poolSize). No Atomics.wait, no main-thread blocking; without SharedArrayBuffer (no cross-origin isolation) the async contract still holds, minus the threads.

Measured in plain Node (Apple M1 Max):

Workload cpu backend headlessgl webasm scalar webasm SIMD webasm threaded+SIMD
matmul 512×512 331.8 ms 58.6 ms (5.7×) 159.4 ms (2.1×) 84.6 ms (3.9×) 16.2 ms (20.5×)
4M-element map 6.9 ms 15.9 ms (0.4×) 12.1 ms (0.6×) 7.3 ms (0.9×) 3.5 ms (2.0×)
divergent piecewise, 1M cells 6.3 ms 4.0 ms (1.6×) 6.5 ms (1.0×) 4.7 ms (1.3×) 2.9 ms (2.2×)

On transfer-bound work the threaded tier beats not only cpu but headless WebGL — wasm shares memory with JavaScript, and a GL backend pays upload/readback on every call.

pipeline: true is accepted the way the cpu backend accepts it: results are plain typed arrays (fresh per call) that chain straight into downstream kernels. Graphical mode, kernel maps, and texture/image arguments degrade to cpu — and degradations are no longer silent anywhere: the console warning names the reason, and kernel.kernel.fallbackReason carries it queryably. Verified on real devices: iOS 16/17, Android 13, including scalar fallback on Safari 16.1 (no wasm SIMD) and threadless async on non-isolated pages.

Control flow now matches plain JavaScript everywhere

The webasm test suite pins every backend against plain-JS references, and that honesty audit found long-standing divergences in the established backends — all fixed:

  • cpu (#865): an early return inside a loop no longer falls through and lets later statements overwrite the result; do…while keeps its exact JS semantics (continue jumps to the test); assigning to a scalar argument stays per-cell instead of leaking into every later cell.
  • WebGL/WebGL2 (#867): the same do…while continue bug, fixed by rotating the loop emulation so the exit test sits where JavaScript puts it — the fix holds inside switch lowerings and unbraced bodies; assigning to a scalar argument (a GLSL uniform) now works through a per-invocation shadow local, for Number, Integer and Boolean arguments alike.
  • All GL backends (#860): for (i = 0, j = 1; …) — an expression init rather than a declaration — was a TypeError; it now hoists and runs. A loop counter assigned outside its header (an inner loop reusing the counter) is legal JavaScript and now computes JS-exact results on every backend, retiring a 2016-era "not assignable here" throw (#31).
  • WebGL/WebGL2 (#864): return 1e30 emitted 1e+30.0, invalid GLSL. Integer-valued literals of 1e21 and beyond — ray-marcher sentinels — now compile, from kernel bodies and setConstants alike.

Reliability

  • Wasm memory lifetime is bounded (#870): each size signature instantiates over its own WebAssembly.Memory, which is invisible to JS heap accounting; the per-kernel module cache is now LRU-bounded (moduleCacheLimit, default 8), evicted and destroyed entries are scrubbed down to the worker-side instantiations, and a worker that dies is terminated, not just retired — an uncaught worker exception does not kill the thread, and the zombie pinned every memory it held.
  • A GL texture handed to an already-built webasm kernel switches kernels and degrades cleanly instead of crashing; the fallback cpu kernel can itself switch on later argument-type changes.
  • Where the backend runs, it is right: the gpu.rocks 30-workload suite cross-checks every result against plain-JS oracles — zero wrong results. Two workloads that measured 20–32× slower than plain JS turned out to be pricing the gather-for-scatter algorithm rewrite itself, not the compiler: the cpu backend runs within 2% of hand-written JavaScript of the same transposed algorithm (#869), now documented in the README.

Installing

npm install gpu.js@2.22.0
<script src="https://unpkg.com/gpu.js@2.22.0/dist/gpu-browser.min.js"></script>