Skip to content

FE-1340: Add an experimental WebGPU compute backend for experiments - #9179

Draft
kube wants to merge 1 commit into
cf/fe-1341-swappable-experiment-backendsfrom
cf/fe-1340-webgpu-experiment-backend
Draft

FE-1340: Add an experimental WebGPU compute backend for experiments#9179
kube wants to merge 1 commit into
cf/fe-1341-swappable-experiment-backendsfrom
cf/fe-1340-webgpu-experiment-backend

Conversation

@kube

@kube kube commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

An experiment's runs are independent, which makes them a natural fit for a GPU: one invocation per run, stepping the whole net on the device.

This adds an experimental WebGPU backend that generates WGSL from the net's lowered HIR — the same HIR the CPU engine compiles to buffer programs — so dynamics, firing rates and transition kernels execute on the device rather than being interpreted per frame. Metrics reduce on-GPU into per-frame histograms, and only a compact per-run summary is read back.

It is opt-in per experiment and falls back to the CPU, with the reason recorded, for any net it cannot take.

🔗 Related links

🚫 Blocked by

🔍 What does this change?

A subset engine, asked rather than told. It reports whether it can run a net before an experiment starts, through the contract added in #9178, and a net it declines runs on the CPU instead. Requirements it enforces:

  • every place holding typed tokens declares a token capacity, so buffer sizes are known up front (FE-1237: Add optional per-place token capacity #9177);
  • arcs consume at most two typed tokens per place — a weight-2 pairwise condition is scanned over every pair by combinatorial unranking, which preserves the CPU's lexicographic firing order;
  • no string or uuid token attributes, which need more than the 32 bits WGSL offers;
  • metrics that measure place token counts, without a time aggregation.
flowchart LR
  H[HIR from artifacts] --> G[generate WGSL]
  G --> S[shader: 1 invocation per run]
  S -->|per frame| A[workgroup atomics -> histogram]
  S -->|final| U[compact per-run summary]
  A & U --> RB[single readback]
  RB --> M[metric frames]
Loading

Run state never leaves the device. The host decodes only each run's place counts and status, so the shader writes those into a compact summary buffer. Reading the full state back needed host-visible memory equal to the state itself, which capped a 3112-byte-per-run net at ~689k runs; the summary is ~16 bytes per run.

Device limits are requested, not defaulted. requestDevice() without requiredLimits returns the WebGPU floor — 128 MiB per storage binding — regardless of hardware. An Apple metal-3 adapter reports 4096 MiB for both limits that bind here.

Supporting surfaces: a per-experiment backend toggle in the create-experiment drawer, and a Compilation panel (behind a user setting) reporting what the compiler made of each condition, kernel and equation, and what stops a net running on the GPU.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • modifies an npm-publishable library and I have added a changeset file(s)

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

  • Results are not seed-identical to the CPU. WebGPU cannot reproduce the CPU generator, so trajectories differ while distributions agree — on the SIR example the two backends' mean token counts agree to within half a percent. Continuous dynamics integrate with RK4, which is more accurate than the CPU's method rather than merely different. Which backend ran an experiment is recorded so results stay attributable.
  • The GPU steps every run to the configured max time, while the CPU stops a run as soon as it cannot fire; a net that finishes early therefore reports a higher frame count on the GPU for the same results.
  • Nothing validates the generated WGSL in CI — the tests assert on the emitted text, which is why a same-scope redeclaration reached a user. A real validator (web-naga as a wasm devDependency, or Dawn via webgpu with a null backend) can run headlessly in vitest; not wired up here.
  • Run state is a single storage buffer, so run count is still bounded by maxStorageBufferBindingSize. Removing that bound means chunking the dispatch.
  • Some GPU-vs-CPU divergences are refused rather than supported: integer token attributes are stored unsigned, and expression metrics cannot run on the device.

🐾 Next steps

WGSL validation in the test suite; dispatch chunking; a token-reducing metric kind so expression-style metrics have a GPU-serviceable equivalent.

🛡 What tests cover this?

compile-net-shader.test.ts (shader generation, the summary ABI, and a naga-calibrated same-scope redeclaration check across all three ODE methods), eligibility.test.ts, compilation-report.test.ts, pair-selection.test.ts (pair ordering against the engine's own enumerator), emit-wgsl.test.ts, runner.test.ts (buffer-limit arithmetic, chunked seeding, device limits). Verified end to end on a real adapter: 4096 SIR runs dispatch with no validation or uncaptured errors and decode correctly.

❓ How to test this?

  1. Check out the branch in a browser with WebGPU (Chrome, Edge, Safari 26+).
  2. Enable WebGPU (experimental) in user settings, then create an experiment and switch the Backend toggle to GPU.
  3. Run it and confirm the summary shows a GPU badge, and that the distributions match a CPU run of the same configuration.
  4. Try the satellites example without capacities: the toggle should be disabled, explaining on hover which place needs one.

One GPU invocation per run, stepping the whole net on the device, with
WGSL generated from the net's lowered HIR so user code executes rather
than being interpreted per frame. Metrics reduce on-GPU into per-frame
histograms and only a compact per-run summary is read back.

A subset engine, asked rather than told: it reports whether it can run
a net, and one it cannot take falls back to the CPU with the reason
recorded. Typed places need capacities, arcs consume at most two typed
tokens, and a weight-2 pairwise condition is scanned over every pair by
unranking, preserving the CPU's firing order.

Results are not seed-identical — WebGPU cannot reproduce the CPU
generator — so the backend that ran each experiment is recorded.
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview Aug 8, 2026 1:11am
petrinaut Ready Ready Preview Aug 8, 2026 1:11am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Aug 8, 2026 1:11am

@github-actions github-actions Bot added area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team area/apps > hash.design Affects the `hash.design` design site (app) labels Aug 8, 2026
Comment on lines +647 to +650
const value = this.emit(expr, env);
const literal = /^-?\d+(?:\.0)?$/u.exec(
value.kind === "f32" ? value.code : "",
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps > hash.design Affects the `hash.design` design site (app) area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

2 participants