Watching a language model think
I used AI to build an LLM inference engine from scratch, so I could understand the thing that built it. Every number it computes is verified against llama.cpp, and you can watch it work in your browser.
Type "The capital of France is" into a language model and it says " Paris". Between those two events, 596,049,920 numbers are read off disk and pushed through 28 rounds of the same arithmetic. Most explanations of that arithmetic use cartoon diagrams or toy weights. I wanted to see the real thing: the actual numbers, from an actual model, at every step.
So I built suiron (推論, "inference"): an inference engine for Apple Silicon written from scratch in Rust, with zero runtime dependencies. No PyTorch, no candle, no tokenizers crate, no ggml, not even serde. The rule was: if a byte gets parsed or a float gets multiplied, code in this repo did it. On top of the engine sits the part I actually set out to make, a browser-based microscope that shows every stage of next-token prediction running live, on real weights, with nothing simulated.
The elephant, first
An AI wrote most of this code. I directed it, questioned it, and made it prove everything; Claude typed. I read that as the only sensible way to do this in 2026: hand-writing code an AI can write faster is a bad use of my hours, but shipping code neither of us understands is worse. So the project ran under two hard rules that made the AI's speed safe to use:
- From scratch, std only. A framework call hides exactly the parts worth learning. If the tokenizer is a library import, you never learn that "The" is assembled by two byte-pair merges. Zero dependencies was never about purity; it was about leaving no place for understanding to hide.
- Faithful or nothing. Every compute path is gated on matching a reference, llama.cpp first, then our own f32 path once faster paths existed. Nothing ships on "looks right."
Using AI to help me understand AI turned out to be the project's most honest description. The engine explains the model; building the engine explained the AI that helped build it; and the verification regime is what kept both honest. More on that below, because the regime earned its keep in a way I did not expect.
What got built
The stack, bottom to top, each piece gated before the next began:
- A GGUF parser. GGUF is the binary format llama.cpp models ship in. A v3 reader in plain Rust: metadata, tensor index, and the quantized blocks themselves. Qwen3-0.6B's file is 640 MB: a Q8_0 block is 34 bytes, an f16 scale followed by 32 signed bytes, and value = scale × byte. That one sentence is most of what "quantization" means, and the lab shows you a real block from the file.
- The tokenizer. Byte-level BPE from the merge table in the model file. Verified token-exact against llama.cpp's tokenizer on 13/13 fixtures, including Japanese and code.
- The f32 forward pass. RMSNorm, GQA attention with RoPE (plus Qwen3's per-head q/k norm), SwiGLU feed-forward, 28 layers, tied unembedding. The first run of the full pass produced " Paris" at logit 17.40. I would love to tell a war story about the layer-by-layer divergence hunt I had braced for; the honest report is that the discipline of verifying every kernel against fixtures on the way up meant the assembled pass was right the first time. The war stories came later, and they were more interesting.
- Generation. KV cache, seeded temperature/top-k/top-p sampling, UTF-8-safe streaming, chat template. The gate: greedy output identical to llama.cpp, token for token, 5/5 prompts × 32 tokens.
- A Metal backend. The no-dependency rule made this the fun one: ~100
lines of hand-rolled
objc_msgSendFFI, MSL kernels compiled at runtime, 4.1× over CPU, bit-checked against the CPU path. - Quantized inference. A Q8 compute path that computes directly on the quantized weights, runtime-switchable against f32, which stays forever as the reference (the lab shows the live tok/s of each).
- A WebAssembly build. The same crates compiled to a 294 KB wasm module, so the whole engine runs in a browser tab.
56 Rust tests and 19 web tests guard all of it (CI runs them on every push; the model-dependent ones self-skip without the 640 MB file and run locally); the important ones assert exact reconstruction, not vibes, and several reconstruct engine values exactly.
The war stories that actually happened
The demo that lied. The lab has a "worked unembedding" demo: the final hidden vector, the real embedding rows for the top candidates, and the dot products that become their logits, so you can check the model's last step by hand. The first version of it, AI-written, ranked candidates by re-running the scoring helper on a vector that had already been normalized: the final RMSNorm applied twice. The rankings looked completely plausible. It got caught because every teaching surface in the lab carries an invariant test against the engine, and this one asserts that the demo's top candidate equals the engine's actual argmax. That is the whole thesis in one bug: an AI will occasionally produce something subtly, convincingly wrong, and "every number on screen must reproduce the engine's number" is what turns that from a quiet lie into a red test.
The memory wall. The browser build's naive plan was to load the model the way the native engine does, dequantized to f32: 2.4 GB, which kills a phone tab instantly. The fix was to keep the Q8 blocks themselves resident (the same 640 MB as the file) and dequantize rows on demand, with the browser running the same Q8 compute path as native. The gate before shipping: a Node harness ran the wasm engine and the native engine side by side. Identical tokens, identical top-k probabilities, all 28 lens layers agreeing.
The payoff: the microscope
The lab is one page, organized as a token's life: prompt → tokens → 28 layers → predictions → geometry → the draw. Inspecting any token shows how it was produced: which earlier tokens attention actually read (real weights, drawn as arcs), what the model predicted, and the exact random draw that picked the winner, seed and all.
The centerpiece is the logit lens: stop the model at any layer, apply the final unembedding early, and see what it would predict so far. You can watch " Paris" surface partway up the stack and take the lead, layer by layer. This is where "28 layers of the same arithmetic" stops being abstract; you can see the answer form.
Every stage opens into a worked demo on the live numbers: a real query·key dot product you can step through, RoPE visibly rotating a query as position changes, softmax → weights → weighted values reconstructing attention's output with zero measured difference from the engine's recorded value, and that once-lying unembedding demo, now test-pinned.
Two things I have not seen elsewhere in a teaching tool:
- Fork-diff. Click any predicted-but-not-chosen candidate to rewrite history and let the model continue for real; the lab keeps the discarded future and shows both runs side by side. Force " the" instead of " Paris" and the model recovers with "…is the city of Paris." At the fork point the two distributions are identical and only the choice differs; a few tokens later the two histories genuinely predict different things, and you can compare them.
- Curated experiments with claims checked against the engine before the copy was written. My favorite: give the model nonsense ("glorp zim vex glorp zim vex glorp zim") and it still continues the pattern, because an induction head, layer 6, head 11 in this model, puts 81% of its attention on what followed the last repeat. The lab finds and labels that head from the recorded attention, and stays silent on prompts where no head clears the bar.
The browser version boots in seconds on a 7.5 MB recording of one real run, honestly labeled as a recording; one click downloads the model and replays the same seeded run live, and every moment in the lab is a shareable URL.
What it is not
It is not fast, and it is not trying to be; llama.cpp exists and suiron's f32 path is deliberately the simplest correct implementation of each operation. The claim I will defend is narrower: real numbers, from a real model, at every visible step, verified, in your browser. Static explainers with toy weights teach the shapes of things; this shows one particular model actually doing it.
If you want to see a language model think, the lab is open. Start with the tour, then click " Paris" and force it to say something else.
suiron is Qwen3-0.6B under the hood: small enough for a browser, big enough to be a real transformer. The engine, lab, and this writeup are on GitHub.