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 (nearly six hundred million) 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 (Japanese for "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 that 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.
Who made this?
An AI wrote most of this code. I directed it, questioned it, and made it prove everything. For this type of project, where my main goal was the understanding of the underlying mechanisms, it didn't make sense to spend multiple weeks writing standardized Rust kernels from scratch when an AI can do it faster and more accurately. 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 would 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: the same prompt run through llama.cpp first, then through 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 kept everything on track. 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 was prepared to have a war story about a long and arduous layer-by-layer divergence hunt but the honest report: the discipline of verifying every kernel against llama.cpp fixtures on the way up meant the assembled pass was right the first time.
- 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.
More than fifty Rust tests and twenty web tests guard all of it; the important ones assert exact reconstruction — every number a teaching surface shows must reproduce the engine's.
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 walks one real prediction, one step per screen, in the causal order it happens: your words become tokens, the model looks back over everything so far, the guess sharpens through the 28 layers, it draws one, and the new word loops back in. Each step docks its deep-dives as drawers, one at a time, and the default path walks through every one. Click any word of the sentence and the steps re-anchor to how that word was made: which earlier tokens attention actually read, what the model predicted, and the exact random draw that picked the winner, seed and all. (The original everything-at-once view survives one click away as "expert mode".)
The centerpiece is the logit lens, inline on the "sharpens" step: 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. The lab captions the exact layer where it happens. This is where "28 layers of the same arithmetic" stops being abstract; you can see the answer form.
The examples are all real demos on the live numbers, never static explanations: a real query·key dot product you can step through, all sixteen attention heads changing jobs as you scrub the layers, 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. Every attention read ends with what it bought each finalist: its contribution to that token's logit, computed from the model's own weights and checked to reconstruct the engine's real output.
Two novel features (I think):
- Fork-diff. Open "what if?" on the draw step and force any real candidate to rewrite history; the model continues for real, and "the two worlds" shows both runs side by side, divergence marked. 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 MB recording of one real run. 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.
It is also not an account of why. Every step is the real computation, but seeing a head reach back to " France" shows you how the answer is assembled, not why the model reaches there at all — that association was written into the weights by training, and it is not recoverable from the numbers on screen. No single value here carries a meaning you can read off; the knowledge is real but distributed in a way this lab does not claim to decode. Call it mechanistic transparency rather than interpretability: the how in full, the why left honestly open. Watching the machine run is not the same as knowing what it learned, and the lab never pretends otherwise.
If you want to see a language model think, the lab is open. Press begin to play the recording, walk the five steps, then open "what if?" and force it to say something else. Or jump right into expert mode and watch the numbers change as you scrub the layers.
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.