DEV Community

Lank_M
Lank_M

Posted on

Your WebGPU benchmark may be running on a CPU rasterizer

My first timing table for a browser-side super-resolution run was garbage, and it took me two days to notice. The numbers were internally consistent, the runs all completed, nothing errored. They were just measuring something I did not think I was measuring.

I was comparing five upscaling model tiers against plain interpolation on four samples, all running client-side in Chromium with WebGPU. Here are the three things that quietly wrecked the first pass, in the order I found them.

The WebGPU adapter was a CPU rasterizer

Headless Chromium comes in more than one flavour. The stripped-down shell I reached for first does expose navigator.gpu, it does hand you an adapter, and every run completes. The adapter I got back was a software rasteriser. Same page, same model tier, same image: 104 seconds on that adapter, 11.3 seconds on the full Chromium build talking to Metal. A factor of nine, with no error anywhere in between.

Nothing in the page told me. The UI reported "WebGPU" as the backend in both cases, which is technically true — the API was there and it worked. I now read info.vendor and info.architecture off the adapter before a run, and throw away any timing where it isn't the GPU I expect. That check costs one line and would have saved me the two days.

There is a second, smaller trap next to it: navigator.gpu only exists in a secure context. Probing it on about:blank reports "no WebGPU" and sends you off debugging a problem you don't have. Probe on the real page.

2× does not cost less than 4×

The tool offers 2× and 4×. I assumed 2× would be the cheap option and used it for the quick passes.

All five model tiers are natively 4× — the backend label says x4, x4plus, x4v3 for every one of them. Picking 2× runs the full 4× inference and then downscales the result. The compute is identical. In my matrix 2× frequently came out slower than 4× on the same sample, because the 2× run happened to be first and carried the model download plus pipeline setup with it.

So if you need speed, change tiers, not scale factors. The spread between tiers is real: the fast tier landed at 1.2–2.1 seconds, the heaviest tier at 16–126 seconds, same machine, same images. All of these are from one M4 Mac with one browser build; I would not carry any of them to another machine.

The default export format was lossy

The tool defaults to WebP at quality 92. I ran twenty cases before realising every PSNR number I had included the encoder's loss on top of the model's.

For a quality comparison you want the output bytes to be the model's output and nothing else, so I forced PNG on every single case. This one is obvious in hindsight and easy to miss in practice, because the default is a perfectly sensible default for an actual user — it's only wrong for a measurement harness.

What the clean table actually said

Worth the detour, because the corrected numbers were not what I expected. The four samples were a product shot, a document screenshot and a line-art icon I built myself (brand, order numbers and amounts all fictional), plus a CC0 airport tarmac photo from Wikimedia Commons — a third-party image, not one of mine. Every sample was built the same way: take a high-resolution original, shrink it 4× with LANCZOS, upscale the small version back, compare pixel by pixel against the original. That original is the ground truth, and without one you cannot say anything about accuracy at all — only about sharpness.

PSNR against ground truth, AI best tier vs LANCZOS, four samples at 4×

Four samples, 4×, AI taking the best of the five tiers: a product shot with small print 26.48 dB against LANCZOS at 27.85, the tarmac photo 27.47 against 27.00, a document screenshot 22.88 against 23.62, a line-art icon 27.58 against 22.87. Two losses, one narrow win, one clear win. The two losses are the two samples with small text in them.

And if you score the same outputs with SSIM instead, the model wins all four. SSIM rewards local contrast being restored; it does not ask whether the restored stroke is the stroke that was there. On the product shot the model put 7.10% ink coverage into the small-print region where the ground truth has 5.64%, with sharpness slightly above the ground truth — more strokes than the original, sharper than the original, and further from the original.

The tool I ran this on is ImgIng, which does the whole thing locally in the browser. That mattered for the harness more than for privacy: with no server round trip, every variable that moved was one I could see in the Network panel.

If you are building a comparison like this, the short version is: pin the export format, verify the compute backend before you record a single timing, don't assume a smaller scale factor is cheaper, and never publish an accuracy claim without a ground truth in the loop. I still don't have a good metric for line art specifically — PSNR happens to agree with my eyes there, which feels like luck rather than a method.

Top comments (0)