← TinyGPT · docs · devlog · roadmap · speedup
source: docs/perf_quest.md · view on GitHub ↗

The browser-frontier performance quest

A working document for the in-flight push to give users on the latest Chrome every GPU acceleration the platform exposes — without ever degrading model quality. Started during the HN-launch prep thread, paused mid-flight after the foundation landed.

The product principle

“The best possible performance for users on the latest browser, graceful degradation for everyone else, and we inform them about which path is active.” — user direction

Concretely, every fast path in this push obeys three rules:

  1. Feature-detected at startup. Use it only if the browser exposes it.
  2. Verified against the f32 reference before activating. If the path doesn’t match the slow-but-correct path’s loss curve within 1% over a 500-step Shakespeare run, it’s disabled for the session. Silently. The user gets the slower correct path, never broken output.
  3. Surfaced in the capability pills (+f16, +subgroups, +coop-matrix, +WebNN) so the user can see which acceleration is running, and click each pill for an explainer.

The non-negotiable: no quality regression, ever. Speed only counts if it preserves loss.

The four levers

LeverStatusDetectionRealistic gainRisk
Storage-f16 matmul (existing matmul_tiled_f16.wgsl)scaffolded; not wired into prod path yetalways available (uses pack2x16float)~1.5×, all usersmedium — needs weight pre-packing, train.wgsl bind-layout adaptation
shader-f16 full computeshipped (train_f16_compute.wgsl · gate in verifyShaderF16Compute)device.features.has("shader-f16") — Chrome 121+ stableadditional ~1.2-1.5×medium — precision drift; gated, falls back to f16-storage / vec4 on failure
Cooperative matrix (enable chromium_experimental_subgroup_matrix)probe-compile detection landedprobe-compile a trial shader~3× NVIDIA · ~1.3× Applehigh — experimental WGSL extension, evolving API, sparse docs
WebNN inference (sampling only)scaffolded; probe landsnavigator.ml + createContext({deviceType: "gpu"|"npu"})~3-5× on Apple/Windowshigh — new code path; training stays WebGPU

Each lever is independently shippable — failure of one doesn’t block another.

Most recent landing (commits a9a81504ceeff2) — f16-storage SHIPPED for both inference AND training

The first opportunistic accelerator from the queue is live, gated, and extended to training. Three kernels: matmul_blocked_f16 (forward) + matmul_abt_blocked_f16 (backward dA) + pack_to_f16 (f32 → packed-half upload). All in webgpu/train_f16.wgsl, sharing the train.wgsl bind layout. The same packed buffer powers both forward and backward kernels — just with different row/col interpretation.

webgpu/ops.ts:verifyF16Storage runs at GpuOps.create() time. It checks BOTH forward and backward against the f32 reference; both must pass for the path to activate. Magnitude-aware tolerance: max_abs < 1% of mean|ref| AND mean_rel < 0.5%. See docs/precision.md for the full framework.

GpuModel.prepareForInference() packs every matmul-shaped weight to a packed-f16 buffer on first call. linear() (forward) and linearBackward() (backward dA) opportunistically dispatch the f16 variant when the path is active. repackF16Mirrors() refreshes the packed buffers at the end of every trainStep so they stay in sync with the AdamW-updated f32 weights. Lazy activation: trainStep calls prepareForInference on its first invocation, so training-from-scratch gets the f16 path automatically when the gate passes.

Real measurements on M-series WebGPU:

[ops] f16-storage gate (fwd): mean|ref|=1.22e-1, max_abs=1.33e-4 (11% of budget),
                              mean_rel=0.075% (15% of budget), max_rel=5.40% — PASS
[ops] f16-storage gate (bwd): mean|ref|=1.23e-1, max_abs=1.17e-4 (10% of budget),
                              mean_rel=0.070% (14% of budget), max_rel=5.15% — PASS
[ops] f16-storage gate verdict: PASS — f16 path active

End-to-end inference smoke (browser/smoke_f16.mjs) confirms identical generation between f32 and f16 paths. Training smoke (browser/smoke_f16_train.mjs) verifies training stability with f16 active — runs when GPU is free between gallery retrain runs.

Status of remaining levers — research-grounded numbers (see web search in May 2026 thread) updated the expected gains:

Stacked expectation on Apple: training ~1.2-1.3× (shipped f16-storage already grabs the bandwidth win; the rest of the stack adds marginally). NOT the 1.7-2.2× I earlier claimed. The honest framing is “we made training and sampling meaningfully faster with one solid lever; further levers have diminishing returns.”

Decision: ship the gallery on this stack, treat #91/#92/#93 as a v2 perf drop devlog after the HN launch. Each carries an implementation sketch in this doc + the gate-framework pattern in docs/precision.md.

What landed in commit 28f2533

What’s queued (in execution order)

Shipped in this thread

  1. #90 — Storage-f16 matmul in the production path ✅ SHIPPED in a9a8150 + 9928b71.
  2. #91 (re-scoped) — Extended storage-f16 to training (backward dA + repack + dual-direction gate) ✅ SHIPPED in 4ceeff2. Real-GPU smoke verified: both fwd and bwd gates pass with max_abs ≤ 11% of budget and mean_rel ≤ 15% of budget.
  3. Live GPU activity badge ✅ SHIPPED in 0e22e7c. Pulsing green “GPU active · X tok/s” indicator during training, hidden when idle.

Deferred to v1.1 / future sessions

  1. Recipes gallery model — third-corpus model. Not retrained in this thread (TinyStories + Code shipped; Recipes is ~47 min more wall and the user opted to ship with 3 models). Run caffeinate -i node browser/train_gallery_one.mjs --corpus=/tmp/tinygpt-corpora/recipes.txt --out=recipes --prompt="Ingredients:" then node browser/finalize_gallery.mjs to rebuild the manifest.
  2. Training smoke verificationbrowser/smoke_f16_train.mjs exists; runs a 200-step Medium training and checks loss descent + no NaN. Not yet executed (GPU has been busy retraining). Run when GPU is free post-launch to confirm the training-path plumbing works end-to-end (the inference path is already verified).
  3. Measured A/B benchmark — sampling tokens/sec on f16 vs f32. The theoretical 1.3-1.5× is unverified empirically. Easy to add a small benchmark script.
  4. #91 (original scope) — shader-f16 compute path with enable f16; and f16 accumulators. ~3-5 hr, expected ~1.1× incremental on Apple (the bandwidth win was already grabbed by storage-f16; compute on M-series is largely bandwidth-bound). Real precision risk on K≥256 shapes — gate may reject.
  5. #92 — Cooperative matrix (enable chromium_experimental_subgroup_matrix). ~6-10 hr, HIGH uncertainty on Apple (the WebGPU → simdgroup mapping is unverified in public benchmarks). Wire the +coop-matrix pill via the __tgUpdateGpuAccelPills bridge.
  6. #93 — WebNN inference sampler. Build the model graph in MLGraphBuilder; route inference to CoreML / ANE; training stays WebGPU. ~5-7 hr. Sampling-only path, falls back to WebGPU on context creation failure.
  7. Live activity feed (v1.1 wishlist item). Show recent events from other visitors in real time — “someone in [country] just started training Huge”, “[N] tabs currently sampling from Shakespeare”, Vercel-fluid-style parallel-arrow visualization of in-flight workloads. Needs: realtime backend (PartyKit on CF Durable Objects is the natural choice given we’re already on CF Pages), frontend feed widget, privacy disclosure copy, anti-abuse rate-limit. ~1-2 days. Best framed as a v2 launch — two distinct HN moments better than one delayed.
  8. #91 — shader-f16 full-compute matmul. New WGSL with enable f16; and f16 accumulators. Gated on capabilities.shaderF16. Same numerics gate.
  9. #92 — Cooperative-matrix kernel. WGSL using enable chromium_experimental_subgroup_matrix and subgroup_matrix_multiply. Probe-compile detection already lives in tensor.ts; once it succeeds, wire the path + the worker→main bridge so +coop-matrix appears in the pill cluster. Most uncertain in scope — debugging without docs.
  10. #93 — WebNN inference for sampling. Forward-only path using MLGraphBuilder. Training stays on WebGPU. Routes to CoreML/DirectML/ANE under the hood. Falls back to WebGPU sampling silently if WebNN context creation fails.
  11. #94 — The numerics gate itself. A verifyAcceleratorPath() helper that runs the 500-step Shakespeare check on each enabled path at first use; caches the verdict; falls back automatically. Document measured deltas in docs/precision.md (to be created).
  12. #85 — Retrain the gallery (3 Huge models) sequentially under caffeinate -i on the now-faster path. ~2.5 hr expected at the new throughput.
  13. #87 — E2E + deploy. Add gallery checks to e2e_chrome.mjs, verify on local then live, push.

The gallery dialog UI ships now — manifest-driven, ready to show the other three models the moment they’re trained.

The lesson behind why this took longer than expected

The parallel-on-one-GPU training experiment (3 Playwright Chromium instances training concurrently against the same M-series GPU) was a strategic mistake. The throughput data:

After 2 hours: 40-46% of each run completed, none downloaded, work discarded. The right move was sequential under caffeinate -i. Three sequential Huge runs at ~75 min each = 3.75 hr total; that beats the “parallel + sleep + GPU contention + zero output” actual cost.

The takeaway baked into the queue: #85 explicitly says sequential + caffeinate.

Why this sequencing is right

The user’s launch direction is “lock down everything before HN, no waves.” That gates publishing on every lever working. Sequencing within that:

Cross-references

This doc is the picking-up-where-we-left-off page for whoever resumes the quest. Keep it terse; update it as levers ship.