A weight-stationary matrix-multiply array compiled into a stock Qwen3_5ForCausalLM. The weight matrix lives in Gated DeltaNet state, the multipliers and requantiser are SwiGLU layers, and each layer's output token is the next layer's input. No training. No external runtime.
Nobody needs a TPU inside Qwen: a real TPU is faster, cheaper and does not lose its arithmetic to a rounding error. So why do it?
A language model is a pile of memory (state), a way to look things up (attention) and a way to compute (FFNs). If that is enough to build an exact matrix-multiply machine, then we learn how much of "intelligence hardware" is hiding in the architecture itself. We are studying the bird, not selling the wing.
Models are sloppy by design. Making every integer survive bf16 rounding, normalisation and a couple of dozen layers of someone else's code is a very unforgiving way to find out what the architecture really does.
Not the first machine in Qwen's weights (Qwenputer and QwenPU got there first). Not faster than a real accelerator, or than QwenPU at the op level. Not useful for production. It is an existence proof, with receipts.
A TPU keeps weights put, streams activations, and accumulates partial sums in place. DeltaNet state S is a matrix with exact row write and row read.
With α=β=1 and a one-hot key, S += k(v − Sᵀk)ᵀ replaces row i exactly. That loads a weight row, or stores a partial sum.
A multi-hot query returns the exact sum of the chosen rows. The contraction over the inner dimension is one read.
Products are bit-ANDs, so accumulators hold small integer counts that bf16 stores exactly. The weighted sum is a free linear form.
This is the array's semantics in JavaScript: the same tokens, state rows, bit-plane counts and clamp chain as qwentpu/circuit.py. It is not a transformer simulation. The result is checked against a plain integer reference.
Qwen's own layers are muted for chip tokens. The appended TPU layers hold the array. The model's sampled token goes back in as the next layer's activation.
ACT tokens, then NC READ tokens, which emit NC OUT tokens. Weights loaded once as CELL(r,c,w) tokens, one per cell. There is no flush: every vector rewrites all KR accumulator rows.Fine print. Qwen l2-normalises the query and scales it by 1/√(nq·128). A multi-hot query is therefore padded to nq = 2odd rows so that scale is an exact power of two. Otherwise bf16 rounds each count differently (a count of 7 comes back as 6.96875).
A language model takes one token per step, and every step is a full trip through every layer. Our first array spent one token per input row and one per output column: KR + NC tokens for KR × NC multiply-adds. That is a delivery truck making one trip per parcel.
Packing loads the truck. An ACT token now carries g activations and a READ token reads h columns and answers with one OUT token carrying h outputs. Tokens per matvec fall from KR + NC to KR/g + NC/h.
The price is real: every column now keeps g copies of its weight head and g accumulator heads, a layer only has 16 key heads, so you need more layers, and the vocabulary balloons because one ACT token can mean any of 8g things. MACs per token go 4 → 8 → 16 on 8×8, 32 on 16×16, 64 on 32×32. Every packed configuration is exact in the float64 evaluator and on real Qwen (0 mismatches). Try the packed presets in the stepper above and watch the token counter.
Qwen's own layers are muted for chip tokens: their attention output is exactly zero and their MLP sees a zero vector. A layer that adds nothing to the residual stream is the identity. So why walk through 24 of them to get nothing?
ChipPath goes straight from the embedding to the appended TPU layers, then the final norm, then only the chip rows of lm_head. It returns the same bits as the full model (a test checks that) and never touches Qwen's cache, so there is nothing to restore afterwards. It is the shortcut through a building where every corridor you skip is empty.
The recurrence S = αS + k(β(v − Sᵀk))ᵀ, o = Sᵀq, read as memory operations.
Exact integer arithmetic on bf16 hardware is a statement about a software stack, not about the maths. Different chips and libraries add numbers in different orders, and bf16 only has 8 bits to spare. So we replay the same traces on many devices and count mismatches against NumPy. Dashed cards have not been run yet, and we say so.
Two exhibits, both of the form "the answer is off by a hair". Neither was our arithmetic. Both were somebody else's rounding.
allow_bf16_reduced_precision_reduction = False keeps the reduction in fp32.Stock Qwen3_5ForCausalLM, bf16, real checkpoints. Every predicted token is compared with NumPy. Hatched bars are designs not yet measured.
v1 needed the host to choose every chip token. In v2 the host builds one prompt (weights, input, entry point) and runs argmax; append. The model emits every READ, ACT and OUTK token itself, applies relu and clamp in its datapath, and halts. v1 is untouched.
Real traces: a 15→15→10 network with 5-bit integer weights, run as a depth-2 program through the 16-wide model (20 chip layers). Every token below was checked against the integer golden model and the circuit IR; the numbers come from social/anim/trace_v2.json.
argmax; append.The same model (Dmax = 4, 4-wide, 3 chip layers) runs a depth-1, depth-2 and depth-4 network. The prompt picks the depth by choosing which block the input is written into.
The crowded part is "a machine inside Qwen". The new part is the machine's shape.
| What it is | Control | Lives in DeltaNet state | A token is | |
|---|---|---|---|---|
| Qwenputer Qwen3.5-4B | RV32IM CPU, plus an in-weights NPU | PC, fetch, decode | registers, code, data | an instruction |
| QwenPU / qwenGPU Qwen3.5-2B | CPU + 64-lane int8×int4 vector unit; SIMT GPU | the CPU's program; host-compiled kernel | vector regs, weight rows, caches | a vector-op step or warp-launch record |
| TinyTPU RTL, inspiration only | systolic array in hardware description | sequencer | (registers in silicon) | (not applicable) |
| qwenTPU this repo | weight-stationary dataflow array | none: the layer stack is the datapath | the weights and the partial sums | a dataflow step |