Skip to content

2.23.0

Latest

Choose a tag to compare

@fuzzie360 fuzzie360 released this 03 Aug 10:12

Multi-pass GPGPU programs stop paying per-pass overhead: gpu.createPipeline traces a plain JavaScript orchestration function — loops included — into a compiled plan, executed as one launch with one readback. No breaking changes.

Pipeline compilation

const sweep = gpu.createKernel(function (u, q) { ... }, { constants: { hi }, output: [1024, 1024] });

const solve = gpu.createPipeline(function (u, q) {
  for (let s = 0; s < this.constants.sweeps; s++) {
    u = sweep(u, q);
  }
  return u;
}, { constants: { sweeps: 512 } });

const result = await solve(u0, q);   // one launch, fences inside, one readback

The orchestration function runs once, at build time, with opaque handles standing in for data; the kernel calls it makes are recorded and JS control flow unrolls into a static plan. A step that would overwrite data a later step still reads gets double-buffering automatically — the loop above compiles to ONE kernel over two alternating buffers, retiring the duplicate-kernel ping-pong idiom (and the upload-kernel idiom with it). Trace-time rules are enforced loudly: reading a handle, using one in arithmetic, or drawing Math.random() during orchestration throws at build, naming the violation.

Every backend runs pipelines, and three backends compile them:

  • webasm fuses the plan over one shared WebAssembly.Memory; with threads, pool workers execute the whole plan on memory-resident Atomics barriers — one dispatch per call, no main-thread round trip per pass. 5.7× on heat (1024 passes), 5.2× on jacobi (512) vs the same kernels called per pass — and 2.8×/3.2× over plain JavaScript on rows the backend previously lost.
  • webgpu records every step into ONE command encoder over persistent storage buffers: one submit, one mapAsync. 1.55–1.62× over per-pass chaining on the same benches, and long chains feel it most — a 12,289-pass wavefront ran 10× faster migrated.
  • GL backends run the generic executor at parity with a hand-rolled two-kernel ping-pong: mutable statically-typed plan clones reproduce exactly the kA/kB + upload-kernel pattern, with zero per-step texture churn. Cross-backend benchmark medians on 13 migrated real workloads: WebGL/WebGL2 within +1–2% of hand-rolled, WebGPU −25%, WebASM −8%.

Introspection is supported API — built so a correctness harness can assert the backend it asked for is the backend that ran: pipeline.executorKind ('fused-threaded' | 'fused-sync' | 'fused-encoder' | 'generic'), pipeline.backend (reports 'cpu' under degradation), pipeline.fallbackReason, and createPipeline(fn, { threads: false }) to pin the webasm lowering single-threaded.

Stated v1 exclusions: no mid-plan readback (this.check is reserved for it), no graphical kernels or kernel maps inside plans (loud errors), toString() deferred.

Also in this release

  • texture.clone() works on mutable kernels' outputs — copy-on-write only ran under immutable: true, so a clone was silently overwritten by the kernel's next render; the GL render path now honors outstanding clone refs unconditionally.
  • Threaded webasm kernels skip a staging copy when no run is in flight: arguments flatten straight into shared memory (heat −27%, jacobi −28 to −48%, residency −24% on the gpu.rocks suite).
  • Adversarial review across the feature's three build phases confirmed and fixed 27 findings — every one reproduced before fixing, every fix pinned by a regression test, including two full days of real-workload benchmarking feedback from the gpu.rocks suite integrated commit by commit.

Installing

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