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.
Which browsers have WebGPU
From MDN's browser-compat-data, read 2026-09-10. The rows marked read are read and not seen — every other measurement on this page came from a machine somebody here owns, and these two did not.
| browser | WebGPU from | here |
|---|---|---|
| Chrome, Edge | 113 (partial), 144 complete | measured — every row below, and the golden passes on both graphics APIs underneath: Metal, Vulkan, and Direct3D 12 (a Windows 11 laptop, 2026-09-10) |
| Chrome on Android | 121 | measured — a Galaxy Z Flip 6 |
| Safari on iOS | mirrors Safari | measured — an iPhone 13 mini |
| Safari on macOS | 26 · 18–25 behind the Feature Flag | read — the same engine as the iPhone above, on a machine nobody here opened it on |
| Firefox | 141 (partial) | read — not opened here, and partial is not a promise that this library's surface is all there |
| Firefox on Android | not supported | — |
If you open it on a Safari or a Firefox and the badge names an adapter, that is a row this page would rather have than a citation. The badge text is the whole report.
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 4753 / 4753 [apple / metal-3].
What the first run costs, per adapter
Measured by tests/browser/first_run.py on 2026-09-03 (the first three rows) and 2026-09-09 (the Windows laptop): 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. | ||
nvidia / blackwellRTX 5050 Laptop, driver 581, Windows 11, Chrome (D3D12), Chrome set to High performance in Windows Graphics settings — see the laptop note | first | 3.32 s | 4.2 s | the adapter answers in 200–450 ms and the 17 shaders in 1–4 ms; 2.5 s and more is left unexplained after those, on every visit, warm cache included — a first-run cost of Chrome's D3D12 path this page cannot yet name. Measured by the CQ worker on the laptop, the page served locally, a window open. |
| revisit | 2.57 s | 3.3 s | ||
| revisit, click after warm-up | 3.57 s | 4.3 s | ||
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.
A laptop with two GPUs
Measured 2026-09-09 on a Windows notebook with an RTX 5050: the badge said
intel / gen-12lp. That is the integrated GPU, and every number on the
page was its. The page asks for a high-performance adapter; Chrome on Windows
cannot honour that, because WebGPU runs on whichever GPU Windows assigned to
chrome.exe, and the default for a browser is the power-saving one. Nothing a page
can see says whether a second card exists, so the badge shows a hint rather than a verdict.
- Settings → System → Display → Graphics.
- Add an app → browse to
chrome.exe(usuallyC:\Program Files\Google\Chrome\Application\chrome.exe). - Chrome → Options → High performance → Save.
- Quit Chrome completely — the tray process too — and open the page again. The badge should now name the discrete card.
The NVIDIA Control Panel's Manage 3D settings → Program Settings does the same for an NVIDIA card. Either one is enough.
The golden runs there too. On the same laptop, driven from a CQ worker on it:
borch.ts passed 4057 / failed 0 and the binding agreeing 4753/4753, zero GPU
validation errors — the first time this repository measured its values through Direct3D 12
rather than Metal or Vulkan.
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 — a remote session, a policy that switched acceleration off, an older browser — borch.ts has a second device for one kind of work, and says plainly what it is not.
The cpu device runs a pretrained checkpoint's bytes on
WebAssembly SIMD kernels: the hub's backbones, a linear head trained on their features,
cosine neighbours for the review queue — the workbench's frozen path, which is what a
field trainer with hundreds of images does. It is not a second Tensor
backend: your own model, the small CNN, ONNX export still need the adapter, and each
of those says so by name. Where the page is cross-origin isolated it spreads a forward
over a pool of workers; this site, site/serve.py and the offline bundle on
localhost are, a file:// page never is.
| Your machine | What runs | Measured |
|---|---|---|
| A WebGPU adapter | Everything | EfficientNet-B0 1.5 ms/image, apple / metal-3 |
| No adapter, isolated page | The cpu device on a pool of workers | B0 2.6 ms/image on 8 workers, M4 Max |
| No adapter, plain page | The cpu device on one thread | B0 14 ms/image, M4 Max |
| Software adapter only | All of WebGPU at CPU speed, and the cpu device beside it | SwiftShader 25× slower than the cpu device on B0 |
From Python the same device is import borch_cpu; the
workbench's first cell tries the adapter and falls to it by
itself, saying which it got and on how many threads. The numpy core behind
the Python surface is a third thing again — a change of
language, no adapter needed, no speed to speak of. The numbers above are the book's,
each with its machine, and the checks that produced them run nightly.