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.

Open the live lab ↗ GitHub ↗
The lab's 'sharpens' step: the machine map (5 vectors → read·think × 28 layers → 151,936 scores) above the logit-lens drawn as one vector in a space of word-directions, rotated and locked onto ' Paris' at 65%, with the computed caption that it took the lead at layer 22 — the engine's real prediction.

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:

  1. 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.
  2. 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:

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 'looks back' step with the sixteen-readers drawer open: the 16 attention heads at layer 0 drawn as gaze-dials, each needle pointing at the token that head reads hardest, head 7 (red) reading ' is' at 100%, with the layer scrubber and the drawer dock below.

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):

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.