borch

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.

Read in one line. Training: 4.5–10× faster than TF.js, 2.8–7.5× than jax-js, 14–18× than Burn. Inference: the captured forward is ahead of ONNX Runtime Web's f32 file on all three adapters at both batch sizes, for a ResNet and for a ViT; the nearest row is ORT's f16 file on Metal at batch 16, which is a draw. ORT's int8 file is not a GPU path in the browser; borch's int8 forward is a quarter of ORT's best at the same top-1. The price: a first call that compiles (0.05–0.2 s, once per shape per machine, kept by the browser), and an eager path that is slower than the replay.

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 · datelibrarybatch 163264against borch
apple / metal-3 · 2026-09-22, borch-ts 0.6.0 as publishedborch.ts19.331.755.11×
TF.js 4.22.0 (WebGPU)86.5172.8349.14.5× · 5.5× · 6.3× slower
jax-js 0.1.25 + optax 0.1.268.197.1152.23.5× · 3.1× · 2.8×
Burn 0.21 (Rust → wasm, wgpu)264.7520.71025.613.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.ts10.7–11.314.2–14.722.6–23.11×
TF.js 4.22.075.0121.4223.36.6× · 8.3× · 9.9×
jax-js 0.1.25 + optax 0.1.272.286.3114.16.7× · 6.1× · 4.9×
Burn 0.21not 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.ts10.0–10.510.9–12.317.7–17.91×
TF.js 4.22.067.5113.7211.76.4× · 10.4× · 12.0×
jax-js 0.1.25 + optax 0.1.267.374.4103.66.7× · 6.0× · 5.8×
Burn 0.21 (Rust → wasm, wgpu)115.7248.5516.911.6× · 20.2× · 28.9×
nvidia / blackwell through Direct3D 12 (RTX 5050 Laptop, Windows 11) · 2026-09-21borch.ts32.353.798.11×
TF.js 4.22.0174.8334.9659.25.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)).

adapterruntimebatch 1batch 16borch / ORT (under 1× — borch ahead)
apple / metal-3 · 2026-09-22borch.ts f32, fused + captured1.094.120.25× · 0.78×
ONNX Runtime Web 1.29.0, f324.355.30
ONNX Runtime Web, f16 file (I/O kept f32)3.294.260.33× · 0.97× — a draw
nvidia / blackwell (RTX 5080, Vulkan, Chrome 151) · 2026-09-22, 0.6.0 as publishedborch.ts f32, fused + captured0.561.630.15× · 0.44×
borch.ts int8 static + captured0.520.960.14× · 0.26×
ONNX Runtime Web 1.29.0, f323.753.72
ONNX Runtime Web, f16 filethe session refuses — this card's Chrome (Linux, Vulkan) gives ORT's device no f16
ONNX Runtime Web, int8 file (QDQ) · WebGPU / wasm provider38.6 / 4.0109.3 / 55.210–29× slower than its own f32
nvidia / lovelace (RTX 4090, Vulkan, Chrome 153) · 2026-09-22, 0.6.0 as publishedborch.ts f32, fused + captured0.581.650.16× · 0.46×
borch.ts int8 static + captured0.560.830.15× · 0.23×
ONNX Runtime Web 1.29.0, f323.703.61
ONNX Runtime Web, f16 filethe session refuses, as on the 5080 — Linux Chrome gives ORT's device no f16
ONNX Runtime Web, int8 file (QDQ) · WebGPU / wasm provider40.9 / 4.6104.6 / 64.311–29× slower than its own f32
nvidia / blackwell through Direct3D 12 (RTX 5050 Laptop) · 2026-09-21borch.ts f32, fused + captured1.69–1.816.50–7.290.25–0.30× · 0.41–0.46×
ONNX Runtime Web 1.29.0, f325.67–7.3415.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.

adapterborch captured, b1ORT f32, b1borch, b16ORT, b16borch / ORT (under 1× — borch ahead)
apple / metal-3 · 2026-09-22 (the token row padded 197 → 200 for the subgroup matrices)2.225.4610.6419.770.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 throughout1.786.555.0014.110.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 unpadded1.678.124.8811.410.21× · 0.43×
nvidia / blackwell through Direct3D 12 (RTX 5050 Laptop) · 2026-09-213.31–3.3710.0–11.824.0–27.847.5–61.00.29–0.33× · 0.46–0.50×

What this page does not say

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).