DEV Community

AsyncMonk
AsyncMonk

Posted on

WebGPU vs WASM cold start for a matting model: only 80–140 ms apart

Picture a small tool that strips the background off short clips, like a five-second product demo or a talking-head intro for a landing page. The first thing a user waits for is one preview frame, and that wait is session creation plus a single inference. So the cold-start numbers are the ones to read before picking a backend, and they were much closer than the steady-state numbers suggest. On an Apple M4 Mac in desktop Chromium with ONNX Runtime Web 1.27.0, the RVM MobileNetV3 matting model (FP16, 7,503,483 bytes) took 406–427 ms from InferenceSession.create to the first alpha read back on WebGPU, and 492–544 ms on 4-thread WASM, across 512×288, 1280×720 and 1920×1080 inputs. The same run showed the catch: on this machine the WebGPU mask for this model was almost empty, and nothing threw.

The model file is the one ImgIng https://imging.ai/ loads for the compatible tier of its video matting tool, a Beta that runs in a desktop browser. Each configuration ran in a fresh browser process with the model already in memory, three times for WebGPU and 4-thread WASM (medians below) and once for a single thread. One still frame was fed repeatedly with the recurrent state passed back in, on one machine and one engine, so treat this as a narrow sample.

Session creation cost the same on both backends

Creating the session took 284–286 ms on WebGPU and 276–278 ms on 4-thread WASM. For a 7.5 MB model the backend changed nothing there. The whole gap sits in the first frame, 119–144 ms against 214–269 ms, which leaves the two backends 80–140 ms apart before the user sees anything, keeping in mind that the WebGPU side of that gap produced an unusable mask on this machine (next section). With about 280 ms spent before any inference, the cheaper move is to create the session when the page opens rather than on the preview click. That holds whichever backend you pick, and it hides more time than switching would. Thread count barely mattered at this stage either: single-thread WASM started in 565–619 ms, roughly 20–100 ms behind 4 threads. Threads pay off later, at 2.6–3.0× faster in steady state, and only on a cross-origin isolated page, so the COOP and COEP headers are one more line in the static host config that quietly costs you if forgotten.

WebGPU's steady-state lead came with empty masks

The median per-frame time for frames 2 to 13 was 29.4, 35.0 and 43.6 ms on WebGPU, 89.4, 87.2 and 91.8 ms on 4-thread WASM, and 228.3, 251.2 and 279.3 ms on one thread. On paper that is 2.1–3.0× for WebGPU. But at 1280×720 its first frame came back as scattered patches, and by frame 13 the foreground share was 0.00% against 5.03% on WASM, same frame, same file, no error anywhere. Those WebGPU milliseconds measure how fast a wrong answer gets computed. The cause wasn't traced in this run. My guess is how the WebGPU path handles some ops or precision in this FP16 recurrent network, and other hardware or browsers may behave differently. ImgIng's page states the resulting choice plainly; in my translation, its RVM tiers use stable WASM inference to avoid some WebGPU devices silently producing empty masks. Its heavier MatAnyone2 tier tries WebGPU first and shows a message when it switches to WASM, which is a split worth copying.

What a short clip actually pays

A 3-second clip at 30 fps is 90 frames. At 1280×720, 4-thread WASM would spend about 0.49 s reaching the first frame and about 7.8 s on the other 89 at 87.2 ms each, which is my arithmetic from the medians, not a timed run. A correct WebGPU path at 35.0 ms would land near 3.5 s, though here that figure is hypothetical because the masks were empty. So the claim gets narrower: the first preview costs 80–140 ms more on WASM, and over a whole clip the gap grows only for a model whose WebGPU output you have checked. Post-processing deserves a look first anyway. Converting the full alpha from FP16 to 32-bit floats in JavaScript added roughly 77–88 ms per frame at 1920×1080, longer than WebGPU's 43.6 ms of inference there, with the same empty-output caveat. The very first WebGPU run on a machine can also be noticeably slower than the numbers above. It showed up once, later fresh processes were back in range, and a shader cache is only a guess.

If I were shipping this, WASM would be the default for this model, with the session created on page load and crossOriginIsolated confirmed true. WebGPU would come later behind a first-frame check that compares its foreground share against a WASM run, since "no error" proved nothing here. navigator.gpu alone isn't enough either: requestAdapter() can return null, and ['webgpu', 'wasm'] falls back silently without telling you which backend you got. What this run can't answer is Windows, a discrete GPU or Safari, where WebGPU might return a correct mask and the per-frame gap would start deciding things for longer clips.

Top comments (0)