same page · same GPU · same weights · the adapter beside every number
Measured against the others
Three libraries train a network in a browser on WebGPU today — TF.js, jax-js and Burn — and one runtime serves one, ONNX Runtime Web. This page runs each of them on the same page as borch.ts, on the same GPU, with the same seeded data or the same torch-exported weights, and prints the adapter under every number. The rows where borch loses are kept, and the commands that reproduce every table are at the bottom. Chrome with WebGPU; the numbers are the day's and the card's, and they move when a run moves them.
Training — ResNet-18 (CIFAR), ms per step
The same architecture, SGD 0.05/0.9, cross-entropy, the same seeded pixels; two warm-up steps then five timed, with a loss readback every step so the clock includes the GPU finishing. Each library runs its own layout (NHWC or NCHW) and its own optimizer — that difference is part of what is compared.
| adapter · date | library | batch 16 | 32 | 64 | against borch |
|---|---|---|---|---|---|
apple / metal-3 · 2026-09-22, borch-ts 0.6.0 as published | borch.ts | 19.3 | 31.7 | 55.1 | 1× |
| TF.js 4.22.0 (WebGPU) | 86.5 | 172.8 | 349.1 | 4.5× · 5.5× · 6.3× slower | |
| jax-js 0.1.25 + optax 0.1.2 | 68.1 | 97.1 | 152.2 | 3.5× · 3.1× · 2.8× | |
| Burn 0.21 (Rust → wasm, wgpu) | 264.7 | 520.7 | 1025.6 | 13.7× · 16.4× · 18.5× | |
nvidia / blackwell (RTX 5080, Linux, Vulkan, Chrome 151) · 2026-09-22, borch-ts 0.6.0 as published (two runs) | borch.ts | 10.7–11.3 | 14.2–14.7 | 22.6–23.1 | 1× |
| TF.js 4.22.0 | 75.0 | 121.4 | 223.3 | 6.6× · 8.3× · 9.9× | |
| jax-js 0.1.25 + optax 0.1.2 | 72.2 | 86.3 | 114.1 | 6.7× · 6.1× · 4.9× | |
| Burn 0.21 | not built on this machine (no Rust toolchain there); the 4090 row below is Burn's NVIDIA number | ||||
nvidia / lovelace (RTX 4090, Linux, Vulkan, Chrome 153) · 2026-09-22, borch-ts 0.6.0 as published (two runs) | borch.ts | 10.0–10.5 | 10.9–12.3 | 17.7–17.9 | 1× |
| TF.js 4.22.0 | 67.5 | 113.7 | 211.7 | 6.4× · 10.4× · 12.0× | |
| jax-js 0.1.25 + optax 0.1.2 | 67.3 | 74.4 | 103.6 | 6.7× · 6.0× · 5.8× | |
| Burn 0.21 (Rust → wasm, wgpu) | 115.7 | 248.5 | 516.9 | 11.6× · 20.2× · 28.9× | |
nvidia / blackwell through Direct3D 12 (RTX 5050 Laptop, Windows 11) · 2026-09-21 | borch.ts | 32.3 | 53.7 | 98.1 | 1× |
| TF.js 4.22.0 | 174.8 | 334.9 | 659.2 | 5.4× · 6.2× · 6.7× | |
On 2026-09-03 the same page read 2.2–2.9× against TF.js. TF.js has not moved (the same bytes, 86.4 → 86.5); borch did — the convolution backward on subgroup matrices, the fused optimizer step, the kernels chosen by measurement on each card. jax-js has no BatchNorm and no cross-entropy module, so its step is assembled from what it has; Burn's wgpu backend on wasm is single-threaded and synchronises on every readback (and is 2.3× faster on the 4090 than on Metal at batch 16 — the one library whose distance to borch moves with the card). Each is the best that library can do in a browser today, not the best implementation of that library.
Inference — ResNet-18 (CIFAR) forward, ms
One network exported from torch once — safetensors for borch.ts, ONNX for ORT — and a
table printed only after both runtimes reproduce torch's logits on a seeded input to
1e-3 (measured ~5e-8). Mean of twenty after three warm-ups, a scope a forward, the
readback included. The borch row is the eval forward recorded and replayed
(compiled(model)).
| adapter | runtime | batch 1 | batch 16 | borch / ORT (under 1× — borch ahead) |
|---|---|---|---|---|
apple / metal-3 · 2026-09-22 | borch.ts f32, fused + captured | 1.09 | 4.12 | 0.25× · 0.78× |
| ONNX Runtime Web 1.29.0, f32 | 4.35 | 5.30 | ||
| ONNX Runtime Web, f16 file (I/O kept f32) | 3.29 | 4.26 | 0.33× · 0.97× — a draw | |
nvidia / blackwell (RTX 5080, Vulkan, Chrome 151) · 2026-09-22, 0.6.0 as published | borch.ts f32, fused + captured | 0.56 | 1.63 | 0.15× · 0.44× |
| borch.ts int8 static + captured | 0.52 | 0.96 | 0.14× · 0.26× | |
| ONNX Runtime Web 1.29.0, f32 | 3.75 | 3.72 | ||
| ONNX Runtime Web, f16 file | the session refuses — this card's Chrome (Linux, Vulkan) gives ORT's device no f16 | |||
| ONNX Runtime Web, int8 file (QDQ) · WebGPU / wasm provider | 38.6 / 4.0 | 109.3 / 55.2 | 10–29× slower than its own f32 | |
nvidia / lovelace (RTX 4090, Vulkan, Chrome 153) · 2026-09-22, 0.6.0 as published | borch.ts f32, fused + captured | 0.58 | 1.65 | 0.16× · 0.46× |
| borch.ts int8 static + captured | 0.56 | 0.83 | 0.15× · 0.23× | |
| ONNX Runtime Web 1.29.0, f32 | 3.70 | 3.61 | ||
| ONNX Runtime Web, f16 file | the session refuses, as on the 5080 — Linux Chrome gives ORT's device no f16 | |||
| ONNX Runtime Web, int8 file (QDQ) · WebGPU / wasm provider | 40.9 / 4.6 | 104.6 / 64.3 | 11–29× slower than its own f32 | |
nvidia / blackwell through Direct3D 12 (RTX 5050 Laptop) · 2026-09-21 | borch.ts f32, fused + captured | 1.69–1.81 | 6.50–7.29 | 0.25–0.30× · 0.41–0.46× |
| ONNX Runtime Web 1.29.0, f32 | 5.67–7.34 | 15.7–16.0 | ||
Top-1 on the same 1,000 held-out CIFAR-10 images, a network trained for the test, on the 5080: borch f32 92.40 %, borch int8 static 92.50 %, ORT f32 92.40 %, ORT int8 92.50 %. The quantised network is the same network; what differs is who runs it on the GPU. ORT's int8 file runs slower on WebGPU than ORT's own f32 on every adapter (6× on Metal, 11–28× on the 5080) and slower than on its wasm provider at batch 1 — its quantised convolutions are not the WebGPU provider's.
Inference — ViT-Tiny/16 (224², 1000 classes) forward, ms
timm's vit_tiny_patch16_224 at seed 0, bimm-ts's model on the borch side,
both gated on torch's logits (measured ~1e-6). Nothing in this library was written for
a transformer: the linear layers run the convolution's GEMM tiles, attention runs the
batched matmul and the subgroup softmax.
| adapter | borch captured, b1 | ORT f32, b1 | borch, b16 | ORT, b16 | borch / ORT (under 1× — borch ahead) |
|---|---|---|---|---|---|
apple / metal-3 · 2026-09-22 (the token row padded 197 → 200 for the subgroup matrices) | 2.22 | 5.46 | 10.64 | 19.77 | 0.41× · 0.54× |
nvidia / blackwell (RTX 5080, Vulkan, Chrome 151) · 2026-09-22, 0.6.0 as published — no f32 subgroup matrices on this card, the scalar GEMM throughout | 1.78 | 6.55 | 5.00 | 14.11 | 0.27× · 0.35× |
nvidia / lovelace (RTX 4090, Vulkan, Chrome 153) · 2026-09-22, 0.6.0 as published — the 5080's kernel set, 197 tokens unpadded | 1.67 | 8.12 | 4.88 | 11.41 | 0.21× · 0.43× |
nvidia / blackwell through Direct3D 12 (RTX 5050 Laptop) · 2026-09-21 | 3.31–3.37 | 10.0–11.8 | 24.0–27.8 | 47.5–61.0 | 0.29–0.33× · 0.46–0.50× |
What this page does not say
- One model each. The ResNet-18 is 32 × 32 CIFAR and the ViT is tiny. Larger inputs and larger models (a 346 MB ViT-Base fine-tuned through a 57 MB window is measured elsewhere, in
docs/SCALE.md) are outside this table. - Chrome only, and a recent one. Safari and Firefox are unmeasured. One machine per adapter. Linux Chrome gives ORT no f16 and borch no f32 subgroup matrices (int8 only) on the 5080 and the 4090. The 4090's Chrome was 143 the morning of its row, and that Chrome's WGSL front end refused the int8 convolution's
subgroupMatrixStore(a uniformity rule 151 and 153 no longer apply tosubgroup_id); the library counted the faults and printed no number, and the row is from Chrome 153. - The laptop's afternoon rows are ranges because the machine was in use; every library read 2–3× slower together and the ratios held. A time without its machine can be quoted but not contested, which is why the adapter is on every row.
- Not in borch: f16 compute kernels (why ORT's f16 draws on Metal), LLM decode, a general graph IR. int8 is the convolution only, and only where the card exposes an int8 subgroup configuration.
- Eager is not the table. A page that calls the model without
compiledsees the eager row: on Metal within 1.1–1.35× of the replay, on the 5080 at batch 1 level with ORT and on the 4090 behind it (5.8 against 3.7) — the card clocks down between short bursts and the eager loop lets it; at batch 16 eager is ahead on both. - The first call compiles. 0.05 s on Metal and the 5080, 0.16 s on the laptop's D3D12 (the browser keeps the compiled pipelines for the next visit; 0.015 s the second time), once per input shape.
Reproduce it
Every table on this page is printed by a runner that refuses a software adapter and names the machine under the numbers.
git clone https://github.com/playidea-lab/borch && cd borch && npm ci && npm run build:ts
npm run compare:ts # training vs TF.js · inference vs ORT Web f32/f16/int8 · ViT-Tiny
npm run compare-peers:ts # jax-js and Burn (the Burn crate builds from tests/browser/burn_resnet18)
npm run capture:ts # the recording, the tuner, the first call's cost
The ledgers behind each number — the predictions written before the runs and which
of them were wrong — are docs/BOOK.md ("How fast it is"),
docs/INFER.md, docs/INT8.md, docs/GEMM.md,
docs/COMPILER.md and docs/FIRST.md. Bytes pinned:
TF.js 4.22.0, jax-js 0.1.25, Burn 0.21, ONNX Runtime Web 1.29.0
(tests/browser/assets.lock).