You Can Now Just Ask for an Image — and Your Browser Draws It
Two days ago we showed a browser tab rendering a fox with a 4-billion-parameter diffusion model. There was one asterisk: you couldn't type a prompt and get the fox. The text encoder — the thing that turns words into the model's idea of meaning — was proven, but it wasn't in the browser yet, and the model weights lived on a test server.
As of today, both of those are gone. Type a prompt into the OS. Get an image, drawn on your GPU, in a browser tab, from publicly shipped weights. No account, no fleet, no API key. This is the whole pipeline, in a tab, end to end:
"a red fox sitting in snow at dawn"
→ Qwen3-4B text encoder (4-bit, in-browser, 36 layers)
→ 4 denoise steps through the MMDiT (2-bit, on your GPU)
→ FlowMatchEuler scheduler
→ VAE decode
→ pixels on a canvas
The last time we wrote about this, the encoder was the unbuilt stage. Today it runs in the browser, and the entire chain is verified against the production model to a decimal place. Then we wired it into the OS, so a visitor can ask for it. This is the story of the final push, the bug that cost three full runs, and the numbers.
Closing the last gap: the text encoder
The encoder is Qwen3-4B — 36 layers, 32 attention heads, a 151,936-token vocabulary. It converts a prompt into the 7,680-dimensional conditioning vectors that steer the diffusion model. On the fleet it runs at full precision; in the browser we ship it 4-bit (HQQ4), which brings it from ~7 GB down to 2.8 GB — and still lands every number within the activation-quantization noise floor of the production model.
The verification is a differential harness: run our GPU forward on an RTX 5090, diff it against golden vectors dumped from the real text encoder. Across 36 layers, at three checkpoint layers, the GPU output tracks the production model to within 0.005 relative error — the Q8 noise envelope, 25× under our pass gate. Then we proved the tokenizer: both test prompts encode to byte-for-byte the exact token IDs the production pipeline produces, chat template and thinking markers included. The words, the encoder, the conditioning — all real, all in the browser.
The bug that hid in one flipped bit
Here is the honest part, because it's the best lesson in the whole project. Our GPU encoder was diverging from golden — not by a little, by whole numbers. Every token's output was completely wrong in value, in a way that looked like a model that had never been trained. We checked the obvious things: the RoPE, the attention mask, the QK norm. All correct. Then we isolated the matmul kernel with a tiny hand-built test and stared at one line.
The kernel reads 4-bit weights — two codes packed per byte, low nibble and high nibble. WGSL's select(a, b, cond) returns b when the condition is true. Our line was:
select(low_nibble, high_nibble, is_even)
Which assigns even positions the high nibble and odd positions the low nibble — the exact opposite of how we packed the bytes. An earlier "fix" had swapped both the arguments and the condition, which is a net no-op — the code looked different and did the same wrong thing. Every adjacent pair of weight codes was swapped. A 4-bit model whose every other weight is inverted is not a 4-bit model; it's noise that happens to be shaped like weights.
The fix was one line, made unambiguous on purpose:
let code = (cbyte >> ((t & 1u) * 4u)) & 0xfu;
And the tiny test that caught it — a single 2-column matmul, GPU vs. CPU, ratio 0.00024 — is now a permanent part of the harness suite, so a nibble order can never silently invert again.
The hosting, and the ~1.8 GB mystery
Shipping the weights meant putting ~4.4 GB of artifacts somewhere the browser could fetch them with range requests and CORS. We used the same lane as our text models: a GitHub release (immutable, no egress bill) fronted by a Cloudflare Worker that adds CORS and stitches files larger than GitHub's 2 GB cap. The MMDiT (1.37 GB), VAE (168 MB), and tokenizer all fit as single assets; the encoder (2.8 GB) got split into two parts, exactly like our 27B text model.
Then the first end-to-end run failed at a suspiciously exact point. Every time, around 75% through the encoder download. The error was a JS byteLength on undefined — which in our code means a buffer that ended before it should have. The smoking gun: the mirror's whole-body stream of the stitched 2.8 GB file truncated at ~1.8 GB. Range requests across the part boundary worked perfectly (206, correct bytes); a single giant stream silently ended early. The worker could stitch, but not stream 2.8 GB in one go.
The fix was architectural, not a hack: don't ask the mirror to stream 2.8 GB in one request. The browser fetches the encoder as its two parts (each well under 2 GB) and reassembles them locally. No giant single stream, no truncation, no mystery. The lesson is the same one this whole project keeps teaching: with GPU numerics and big artifacts, the failure mode is never a crash — it's a plausible-looking thing that's secretly wrong, and the only defense is measuring against something that doesn't share your assumptions.
The receipts
The full pipeline, driven through the actual worker the OS dispatches — real fetches, real encoder, real denoise loop — against the deployed mirror. Measured on an RTX 5090:
| Stage | What it proves | Difference vs production |
|---|---|---|
| Qwen3 tokenizer | prompt → token IDs | byte-for-byte exact (both test prompts) |
| Encoder forward (GPU) | 36-layer, 4-bit | ≤ 0.005 vs golden, 25× under gate |
| Weight conversion | ternary → our GGUF | exact (asserted) |
| Final render | browser pixels vs pipeline PNG | mean in-band (4/4 structural checks) |
| End to end | bare prompt → image, hosted weights | PASS, ~100 s first load |
That last row is the one that matters. Not "the pieces work if you assemble them by hand" — the assembled worker, fetching its own weights from a public URL, turning a typed sentence into a picture. And after the first load, the weights are cached: every image after that is a few seconds on your GPU.
What this means
The OS you're looking at has, as a first-class capability, image generation that runs on your own hardware, from public artifacts, with no account. The local-first deal text generation has always had now extends to pictures. You don't need us to draw for you — the model is in the tab.
There are real limits, and we're not hiding them: the model is 4B, not 80B; a 256×256 image in a few seconds needs a discrete GPU (the software fallback exists but is slow); and a 4.4 GB first download is not a phone-with-2GB-data thing yet. But the architecture is done. The full diffusion pipeline — text encoding, denoising, decoding — runs on visitor hardware, verified against the datacenter to a decimal place, and it's live in the OS.
Next time someone asks the OS to draw something, the answer might just be a canvas filling with a picture that never left their machine.
The models are on HuggingFace and GitHub. Every number in this post was measured on the 5090, not estimated. If a number looks suspiciously clean, it is — because we diffed against ground truth from a different generator, which is the only test that counts.