A Browser Tab Just Rendered This Fox — With a 4-Billion-Parameter Diffusion Model, No Cloud

Look at the fox.
A 4-billion-parameter diffusion transformer made that picture. Nothing unusual about that — except where it can now run: a browser tab, on your own GPU, with no account, no API key, and nothing uploaded. Last night the entire in-browser image pipeline came alive end to end, and every stage of it is proven against the production model to a decimal place. This is the part we get to flaunt, and the part where we show our work — because with GPU numerics those are the same thing.
The claim, stated plainly
FLUX.2 Klein is the image model our fleet serves: a diffusion transformer (MMDiT) with a Qwen3-4B text encoder and a FLUX.2 VAE. We ported it to run in a browser tab on WebGPU, on the same 2-bit ternary kernels that already run our text models. The assembled worker — the exact code path the OS dispatches — now does the whole job:
your prompt's conditioning
→ 4 denoise steps through the MMDiT (2-bit weights, on your GPU)
→ FlowMatchEuler scheduler
→ VAE decode
→ pixels on a canvas
No stage is a mock. No stage is "close enough." Here is exactly how close each one is.
The receipts
Every component was ported scalar-first, then held to golden vectors dumped from the real production pipeline — the exact diffusers + PrismML code the fleet runs. Measured on an RTX 5090:
| Stage | What it proves | Difference vs production |
|---|---|---|
| Weight conversion | 2-bit ternary → our GGUF | exact (asserted, aborts otherwise) |
| VAE decode (GPU) | latents → pixels | 4.5e-8 vs CPU reference |
| MMDiT forward | the whole 25-block transformer | 6.8e-6 vs real diffusers output |
| Per-step denoise (×4) | each diffusion step | ≤ 3.2% (activation-quant floor) |
| Scheduler chain | descale + unpack before decode | 0.0055 vs the pipeline's own decoder input |
| Final render | the browser's pixels vs the pipeline's PNG | meanAbsU8 0.172 |
That last row is the one to sit with. When we decode the production pipeline's own latents through our browser VAE, the result differs from the reference render by 0.17 of one 8-bit level, per pixel. Not per image — per pixel, on average. The browser render and the datacenter render are, to your eye and very nearly to the bit, the same picture.
Then we ran the assembled worker — its own fetch, its own denoise loop, its own scheduler, nothing hand-stitched — and it produced a valid fox whose global luminance landed squarely in-band with the reference. A real image, out of the fully wired path, in a tab.
And it was fast. On an RTX 5090, warm model: four denoise steps in about four seconds, VAE decode in about one — a 4-billion-parameter diffusion model doing a full generation in roughly five seconds, in a browser. The model download (1.37 GB, ternary-packed) is a one-time cost, cached after the first visit; every image after that is just the five seconds.
The lucky break that was actually a measurement
The transformer ships as gemlite-int2: ternary weights (−1, 0, +1 × a scale) in a CUDA-specific packing, unloadable in a browser. The temptation is to write a converter and hope the formats line up. We measured instead — pulled one layer apart, byte by byte — and found that gemlite's dequantization ((q − zero) × scale, with zeros == −scales exactly, every layer) is byte-for-byte identical to our browser's existing Q2_0 format: same codes, same bit order, same (q − 1) × d formula. The "conversion" is a transpose. Zero numeric change. We wrote no new quantization kernel to run a 4B image model — the text-model kernels already spoke its language.
Six bugs, one shape
Here's the honest part, and the real lesson. Getting from "each piece works" to "the whole thing renders" surfaced six bugs, and every single one had the identical shape: a value checked only against another value from the same source.
- The Euler step formula was wrong — and its unit test asserted the same wrong formula, so they passed together.
- The noise schedule skipped its dynamic time-shift — and the schedule test compared against the un-shifted values.
- A "golden" test was silently skipping, because the artifacts it gated on had never actually been dumped — so "golden-proven" meant "proven against nothing."
- The scheduler's shift constant was computed with the wrong sequence length — recorded in a manifest, then checked against that manifest.
Each of these is invisible to ordinary testing because GPU numerics don't crash when they're wrong — they produce a plausible image. A flipped convention still makes shapes. A wrong schedule still makes a fox. The only thing that catches it is ground truth from a different generator: the real pipeline, hooked mid-run, dumping what it actually computed. The moment we dumped the production model's per-step latents and diffed against those, four bugs fell in an hour. The lesson we're keeping: if your test and your code trace back to the same author, you haven't tested anything — you've tested that you're consistent.
(There were the earlier ones too, from the nights before: a generated shader-source file that made correctly-written kernels silently no-op, an attention flag that attended to exactly one token, buffer-pool aliasing that was clean at toy sizes and garbage at full scale. Same family. Same fix: diff against something that doesn't share your assumptions.)
What this means, and what's honest about it
When this ships, a machine with a decent GPU runs both a 4B language model and a 4B image model in browser tabs — or on your own node — both in ternary weights we can prove against their reference implementations, with the cloud as a fallback you can see and choose. Your prompts render where you're standing.
What's not done yet, because we don't ship on vibes: the text encoder is the one stage still on the bench. The worker takes precomputed conditioning today (which is how everything above is proven), and a bare prompt gets an honest error, not a wrong picture — because a diffusion model happily renders something for garbage input, and "something" is the enemy. Wiring Qwen3-4B into the tab is the last mile, and the flag that lists "Bonsai Image" in your model picker stays off until a typed prompt makes a fox on its own.
But the pipeline — the hard, foreign, "obviously too heavy for a browser" part — is alive and measured. The models are on HuggingFace. The kernels are clean-room, in our fork, diffed against scalar references you can read in an afternoon. The golden vectors don't care about anyone's marketing, including ours.
A browser tab rendered that fox. We can hand you the numbers proving it rendered it right.
— Aitherium Engineering, still up, watching a canvas paint