Only what was measured
The GPU in this tab
borch runs on WebGPU, which means it runs on whatever GPU the browser is willing to hand over. Sometimes the browser is not willing. This page is what to do about it, per platform — and what borch does when there is no GPU to be had.
First, read the badge
Every page that runs code carries the adapter's own name. That badge is the whole diagnosis:
- A name like
apple / metal-3ornvidia / blackwell— a real GPU. Nothing on this page applies to you. google / swiftshader, or anything withllvmpipeorlavapipein it — that is a CPU wearing a GPU's interface. It runs, it agrees with the golden, and every speed it prints is the CPU's.- No adapter at all — the browser refused before the driver was asked. That is the case the rest of this page is about.
The badge says which device; Check your GPU says whether the answers are right on it. It runs the whole golden — every case frozen against real PyTorch — in your tab, and prints the adapter beside the count. Every vendor row on this page came from a machine somebody here owns, which is why one of them was wrong for months.
macOS
Nothing to do. Measured on an Apple M4 Max: the adapter comes back
apple / metal-3 and Run works with no flags, no install, no settings.
Every number on this site that says apple / metal-3 was taken that way,
the whole suite included —
agreeing 4733 / 4733 [apple / metal-3].
What the first run costs, per adapter
Measured by tests/browser/first_run.py on 2026-09-03: the landing page opened
cold, the device probed, Run pressed, until the done line. "In the page" is the page's own
timer; "open→done" is the wall clock from opening the page. The deployed row adds
the transfer from GitHub Pages. Since the first screen became a Python training loop,
the click also pays for Pyodide's first import borch_webgpu (about 0.5 s
on either machine) when it lands before the warm-up is done.
| adapter | visit | in the page | open→done | where the wait is |
|---|---|---|---|---|
apple / metal-3M4 Max, Chrome | first | 1.35 s | 1.6 s | the adapter answers in tens of milliseconds; the first click pays Python's first import (0.5 s) and the run |
| revisit | 0.66 s | 1.3 s | ||
| deployed site, first | 1.71 s | 1.9 s | ||
nvidia / lovelaceRTX 4090, driver 550, Ubuntu (Xorg + GNOME), Chrome 143, monitor on | first | 1.60 s | 1.8 s | the same as Apple: adapter 14 ms, device 15 ms, 22 shaders 1 ms, the first readback 28 ms. Measured in front of a lit monitor — see the note below for what the same machine read with its screen blanked. |
| revisit | 1.51 s | 1.6 s | ||
nvidia / blackwellRTX 5080, driver 580, Ubuntu (Xorg + GNOME), Chrome 151 | to be measured again with its screen on | its earlier row here (2.1 s first, "a 3 s handshake in one-second steps") was taken over ssh in front of a blanked monitor, and that is what it measured — withdrawn. | ||
To the first training — the learner's clock
The first screen trains a model in Python the moment its button is pressed. This
clock starts when the page is opened, on the deployed site (so the download is in
it), and stops at the moments a reader feels: Python ready under the button, the
first loss line after the click, the learned line. Measured by
tests/browser/learner_path.py, nightly.
| adapter | Python ready after opening | click → first loss line | click → learned | revisit, click at once → learned | lesson 0, three blocks |
|---|---|---|---|---|---|
apple / metal-3M4 Max, Chrome | 4.4 s | 0.7 s | 0.8 s | 1.4 s | 2.4 s |
nvidia / lovelaceRTX 4090, driver 550, Ubuntu, Chrome 143, monitor on | 4.7 s | 0.6 s | 0.6 s | 1.6 s | 2.6 s |
A blanked monitor makes every GPU wait a one-second wait. The
first time this table was filled, the 4090's rows read 8.6 s to Python ready and
6.5 s from the click to "learned", and the row above it said the adapter handshake
took 3 s in one-second steps. Both were measured over ssh on a desktop whose HDMI
monitor GNOME had blanked after five idle minutes (xset q: "Monitor is
Off"). In front of a blanked screen each frame swap times out at about a second,
and every GPU wait queues behind the frame: the adapter request took 3.0 s, a
readback after a line was written to the page 1.0 s, a training loop that prints
its loss seven seconds. With the monitor woken, the same probes read 52 ms, 46 ms
and 0.56 s. Reproduced line by line by tests/browser/readback_probe.py
--fresh; the timing scripts now refuse to measure in front of a blanked
screen, and say so.
What this library does on its side: the page asks for the adapter once (the probe's
adapter is held for init()) and makes the device and loads Python while
you are still reading, so the click pays for the run. With the screen on, Linux with
the NVIDIA driver is Apple's equal on every number here.
Linux with an NVIDIA card
Two flags are enough, and the runner now sends those two. What happens without them is not a refusal — it is quieter than that. Chrome hands back its CPU rasteriser and lets the page run, and it keeps doing that until its Vulkan backend is switched on. Measured on both cards the same way, a real X session and the system Chrome:
| Chrome started with | RTX 5080 driver 580 · Chrome 151 |
RTX 4090 driver 550 · Chrome 143 |
|---|---|---|
| nothing | none | none |
--enable-features=Vulkan alone |
nvidia / blackwell |
none |
--enable-unsafe-webgpu |
google / swiftshader |
google / swiftshader |
+ --ignore-gpu-blocklist |
google / swiftshader |
google / swiftshader |
+ --enable-features=Vulkan |
nvidia / blackwell |
nvidia / lovelace |
+ --disable-gpu-driver-bug-workarounds |
nvidia / blackwell |
nvidia / lovelace |
--enable-unsafe-webgpu --enable-features=Vulkanthis row is the runner's flags |
nvidia / blackwell |
nvidia / lovelace |
So this is the line to paste. Two flags, and the second is the one doing the work — on Linux, WebGPU runs on Chrome's Vulkan backend, and until that is switched on the browser hands back its CPU rasteriser instead of refusing. The card was never blocked; the backend was off.
google-chrome --user-data-dir=/tmp/borch-check \
--enable-unsafe-webgpu --enable-features=Vulkan
In chrome://flags they are #enable-unsafe-webgpu and
#enable-vulkan. Relaunch after changing them — a window
already open keeps the old settings, which cost an hour here.
If those two do not do it, add --ignore-gpu-blocklist.
Chrome keeps a list of GPU and driver combinations it refuses before asking the
driver anything, and that flag is how you tell it to go and look. It changed nothing
on any machine measured here — but none of those machines were on the
list, so this page has never seen the situation the flag exists for. Yours
might be it.
The whole suite was run on those two flags, not just an adapter probe:
agreeing 4460 / 4466 [nvidia / blackwell] on the 5080, and the training
agrees as well — thirty steps of a small ResNet with its per-layer gradients,
batch-norm buffers and eval, matched against torch to 64 values, once
through the Python binding and once through borch.ts's own API.
The six that differ are about that machine, not about the GPU. All
six ask what happens with device='cuda', and their frozen answers were
taken where torch has no CUDA: there both sides refuse, so the case reads both
stop. That box has a CUDA build, so torch does not stop and
cuda.device_count answers 1 where the golden expects 0. Nothing there is
a value coming off a shader — the same six pass on Apple Metal, which is the
shape of a case that froze a property of the machine it was frozen on. Two of this
kind were found and fixed before; these are the rest of that family.
The 4090 has an adapter measurement only. Its card left the PCIe bus mid-session and has not come back, so no golden has been run on it.
An earlier version of this table said the blocklist flag opened the 5080.
It does not — three runs today, same machine, all
google / swiftshader. That reading came from a different browser
(Playwright's bundled Chromium, and an older Chrome), which is also why the columns
here name a Chrome version: the two cards differ in card, driver and
browser at once, so a difference between the columns cannot be pinned on
the hardware alone. --enable-features=Vulkan on its own reaches the
5080 and not the 4090, and eight major versions of Chrome sit between them.
The WebGPU working group's
implementation
status gives a different set for Linux — --ozone-platform=x11
--use-angle=vulkan --enable-features=Vulkan,VulkanFromANGLE — and it also
reaches the card. It works because it contains
--enable-features=Vulkan: --use-angle=vulkan on its own
returns no adapter at all, and --ozone-platform=x11 was not needed.
Linux with an AMD or Intel card
Not measured. The blocklist above is an NVIDIA-and-Linux entry, so the same two switches are the first thing to try, but nothing here has run on those cards and a guess would be the kind of claim this repository removes rather than writes.
Windows
Nothing to do. Measured on a Windows desktop with an NVIDIA card:
the playground's badge came back nvidia / blackwell and Run worked
with no flags at all. Chrome uses D3D12 there rather than the
Vulkan path the Linux section is about, so none of that ladder applies — and it
turns out none of it is needed.
This cell said not measured until somebody opened the page. What stood here named its own retirement condition — "it stays marked not measured until someone opens the page on one, because a page that says we read that it works and a page that says we saw it work are different pages". The implementation status page had said Windows on x86 and x64 was enabled by default since Chrome 113, and reading that was never the same as seeing it. It agrees with what came back, which is worth knowing and is not why this paragraph changed.
Windows on ARM64 is still unmeasured, and the same page lists it as
behind #enable-unsafe-webgpu — so "Windows works" is still two answers
depending on which Windows, and only one of them has been seen here.
Phones and tablets
iOS runs it. Measured on an iPhone 13 mini: the badge came back with an Apple adapter and the code actually ran — not merely an adapter obtained, but output on the screen, which on a phone is the part worth checking separately. The iOS version was not written down, so it is not claimed here.
That one measurement says more than a desktop one would: every browser on iOS uses WebKit, so there is no second engine to test. Safari and the browsers that look like other browsers are the same engine underneath.
Android runs it too — measured in Chrome on a Galaxy Z Flip 6: an Adreno adapter, and output on the screen rather than an adapter alone. It is a real GPU and not a software one, which on this page is the distinction that matters.
One device, though, and Android is where that matters most. The iOS line above covers iOS because there is only one engine there; Android has several browsers on several engines, and three GPU families under them — Adreno, Mali, Xclipse. A Snapdragon phone in Chrome is one cell of that grid. The Android version was not written down either, so it is not claimed.
Why the adapter's name is on the badge
An earlier version of this project fell back to WebGL when it could not get WebGPU,
and for a while its own authors read numbers measured on a CPU path as if they were
a GPU's. It happened again in a different form: a golden run passing 845 of 845 was
recorded as "confirmed on a second vendor" when the adapter had been
google / swiftshader the whole time. That sentence lived in three
documents until it was measured properly.
So the rule is not "never run on a CPU" — it is a number measured on a CPU must never be readable as a GPU's. That is why the adapter name is on the badge and why the badge goes dark on a software adapter.
This page said something false here for a day, and it is worth leaving the
correction visible. It claimed init() stops rather than
accepting a software adapter silently. It never did: borch-ts/src had no
such check, and every golden run that ever passed on SwiftShader is the evidence. The
badge is the thing that tells you; the library takes what the browser gives it.
Asking for the CPU on purpose
Since the browser decides which adapter you get, there is a way to ask it for the software one deliberately — useful when you want to see the CPU path, or check that your code does not depend on a particular device:
await init({ forceFallbackAdapter: true });
const { software } = await probe(); // and know which one you got
It is a real run, not a degraded one. Measured today, the whole golden
passes on both: 3079 / 0 on google / swiftshader and
3079 / 0 on apple / metal-3 — same code, same values,
different device. What a software run is not is a source of speeds.
If you cannot get a GPU at all
Two different situations hide under that sentence, and they have different answers. If the browser offers a software adapter, you are covered by the section above — SwiftShader is still WebGPU, the code runs, and only the speeds are not a GPU's. If it offers nothing at all, borch.ts has nothing to give you and says so instead of pretending: there is no path that goes around WebGPU.
The Python surface is a different implementation — the core
is numpy, and it runs with no adapter at all (measured in a browser with
navigator.gpu removed: loss 17.2945 → 0.000001). It is worth knowing
that it exists, but be clear about what it is: that is a change of language,
not a switch from GPU to CPU. The same TypeScript code will not run there.