~/satyajit

Qwenputer: a RISC-V computer compiled into Qwen3.5-4B, and what its memory actually is

mdjsonmcp

2026-10-30 · 21 min · qwen · linear-attention · hybrid-attention · compilers · agents

Why read this

Notabletop 60%

Checkpoint headers, 12 bit-identical weight comparisons, and the delta-rule RAM rebuilt on transformers' own recurrence. The model itself was not run.

  • Original analysis
  • Widely used
  • A new technique

LLM architectureRuns on a consumer GPUApache-2.0Research teardown

How this was scored
Is it new?
2 of 3: A real new idea, method or capability
Can I trust it?
2 of 3: Measures key facts from files, code or configs
Can I run it?
1 of 3: API-only, gated or restrictive licence
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
1 of 3: Relevant for months
Does it affect many?
2 of 3: A widely used model, tool or lab release
Only here?
2 of 3: A teardown or measurement few others did

Score 62 of 100, ranked 329 of 666 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

@divagr1925 posted that Qwen3.5-4B now has "a full RISC-V CPU inside its weights, using the model's own Gated DeltaNet layers as RAM", running C and MicroPython inside its forward passes with no external runtime. On the DeepMind arithmetic sets the post reports a base score of 55 to 90%, 95 to 100% for base Qwen with a Python tool, and 97.5 to 100% for Qwenputer. The write-up calls it "not a plugin, a tool or a sandbox".

That is a strong claim, and most of it can be checked without a GPU. The checkpoint's tensor headers are public, any tensor can be sampled by range request, the compiler is open, and the memory trick is a few lines of linear algebra that transformers already implements. This article does that. It checks the layout of the checkpoint, tests whether Qwen's weights are really untouched, rebuilds the memory on the reference recurrence from transformers, and runs a toy computer on top of it. I did not run Qwenputer itself: it wants a 24 GB GPU, and I had a CPU box. A later pass ran the repo's emulator, firmware and tests on that box; see "What running it showed".

divagr1925/Qwen3.5-4B-Qwenputer@2d90685 · snapshot 2026-10-30
parameters
9.36B
repo size
18.82 GB
architecture
Qwen3_5ForCausalLM
task
text-generation
library
transformers
license
apache-2.0
safetensors
8 shards
largest file
3.80 GB
files
21
downloads
572
likes
97
parameters by dtype
BF169.36BF323.8K
qwen3.5risc-vrv32imarithmeticcompiled-weightsgated-deltanetqwenputer

repo last modified 2026-10-23

divagr18/qwenputer@f07a0b2 · snapshot 2026-10-30
tracked files
682
license
MIT
branch
main
tests
151 files
source
1.8 MB
commit date
2026-10-30
source by language
Python1.7 MB(174)C35.7 kB(20)Shell17.3 kB(24)Makefile2.7 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-10-30 at f07a0b2 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount

shallow clone: counts describe the pinned tree, not the history

What is claimed

The repository's own summary, read at commit f07a0b2 (30 October), is more careful than the post (all reported):

The repo's gate table is the cleanest account of what the machine buys. On 40 problems per set, with Qwen's token budget at 4,096 (reported, repo docs/rv32.md; calc v1 in every row except the last, which is the v2 re-run for Qwenputer and the host-answered tool, and base Qwen's score is from the v1 run):

SetQwenputerBase Qwen + tool answered by the hostBase Qwen, no tool
arith (seeded set)39/4039/4030/40
mul, big39/4040/4022/40
add or sub, big39/4039/4035/40
div, big38/4039/4029/40
mul and div, multiple, longer (calc v2)40/4039/4036/40

So the machine matches a Python tool, it does not beat one, and the repo says why the misses happen: "rv1's misses are Qwen's, not the machine's". Every value the machine returned was exact, and the failures were a dropped sign, a re-subtraction after a correct result, and loops of 44 calls on an answer that was already right. The headline claim is therefore about where the arithmetic runs. The gain over base Qwen comes from having a calculator at all.

What the checkpoint is

Every number in this section is measured: I read the tensor headers of both checkpoints through HTTP range requests, and sampled rows of the tensors I wanted to look inside.

Is Qwen's own weight block untouched? I compared sampled rows of the Qwenputer tensors with the matching base tensors (Qwenputer layer i + 11 against base layer i). I compared 24 random rows each of the MLP up_proj and down_proj of four layers, the in_proj_qkv of a linear layer, the q_proj and k_proj of a full-attention layer, and 48 rows of the embedding table, always over the 2,560 shared dimensions. All twelve comparisons were bit-identical. That supports "Qwen's original weights stay exactly as they are". It is a sample, not a proof: a few dozen rows per tensor out of billions of weights, and I did not touch the layers between those I sampled.

Is the padding really zero? In the sampled Qwen layers, the nonzero entries were exactly the first 2,560 columns: 64.5% of each matrix (2,560/3,968). The rest is zero, which is the "mostly zero padding" the post describes. Counting the Qwen layers as their base values plus zeros, the unique content of the file is roughly the 4.2B-parameter base text model, the embedding rows, and about a million chip weights, in a file that is 9.36B parameters wide.

What does a compiled layer look like? Very different. In rows sampled from chip layers 0, 5 and 10, at most 0.09% of the entries were nonzero, and in one tensor (in_proj_qkv of layer 0) none of the 48 rows I sampled held a nonzero value. The values that are there are large: the biggest magnitudes were 230 (1.07 × 109) and 247 (1.41 × 1014). The design note explains why. The compiler needs every number the circuit touches to be exact in bf16, so it keeps values to at most eight significant bits and uses huge power-of-two scales to push other effects away (reported). The sampled rows are consistent with a layer built that way; they do not verify the author's count of 1,040,595 nonzero weights, which at a density of 0.054% is too sparse to estimate from 48 rows.

The embedding table shows a second thing. The design note says every token carries a "ballast" of large constants so that normalisation layers divide by an exactly known power of two. In the sampled rows, Qwen's own token rows had about 93 nonzero entries beyond the 2,560 they need (2,653 on average, with a largest magnitude of 240), and the appended chip-token rows were 2.4% nonzero, about 94 entries each. So the ballast is in the table, on both kinds of token (measured, 64 rows per region).

The memory trick, on the real recurrence

Qwen3.5 uses Gated DeltaNet in three of every four layers. Each head keeps a state matrix SS with one row per key dimension, and each token updates it with the delta rule. The reference code in transformers (torch_recurrent_gated_delta_rule, read from the installed package) does, for each token:

S←eg S,S←S+β k (v−k⊤S)⊤,o=q⊤S.S \leftarrow e^{g}\, S, \qquad S \leftarrow S + \beta\, k\,(v - k^{\top} S)^{\top}, \qquad o = q^{\top} S .

The first step is the gate: ege^{g} is a number between 0 and 1 that fades the whole memory. The second nudges the state along the key direction toward the new value vv, and β\beta says how far. The third reads the state with a query. Now take a one-hot key k=eak = e_a, set the gate to 0 in the log domain (so nothing fades), and set β=1\beta = 1. The second step becomes Sa←vS_a \leftarrow v: it overwrites row aa and nothing else. A one-hot query q=eaq = e_a then reads back exactly row aa. That is a RAM with addresses, and its size does not depend on how long the conversation is. The idea that the delta rule is an associative memory is older: Schlag, Irie and Schmidhuber showed it in 2021 (Linear Transformers Are Secretly Fast Weight Programmers). What Qwenputer adds is making it exact and addressable inside a production model, and the next sections check how exact.

How exact, on the reference code

I ran the reference function itself, in fp32 on CPU, with one head of 128 key rows and four byte-valued columns (a 32-bit word). Each token either writes (a one-hot key, β=1\beta = 1) or reads (β=0\beta = 0, so nothing changes).

TestResult (measured)
20,000 random writes and reads of byte values, key magnitude 1no read wrong after rounding; largest error 2.4 × 10-4
the same, key magnitude 210no read wrong after rounding; largest error 1.5 × 10-5
the same, key magnitude 220no read wrong after rounding; largest error 1.5 × 10-5
16, 64 and 128 one-hot items written, then all read back, key magnitude 220largest error 1.5 × 10-5 each time

A unit one-hot vector is not exactly one inside this code. The function L2-normalises keys and queries as x * rsqrt(sum(x²) + 1e-6), so a unit key becomes 0.9999995, writes only approximately overwrite, and a byte read comes back up to about 2.4 × 10-4 off. For byte values that is invisible after rounding to the nearest integer, and it is the reason the design keeps its values to eight bits. Scaling the key up before the norm makes the 1e-6 vanish, and the remaining 1.5 × 10-5 is the rounding of dividing by 128\sqrt{128} in the query scale. This is the same mechanism the design note describes when it gives every token a large ballast so the normalisations see exactly known magnitudes (reasoned: I did not trace this through the compiled model, I showed it on the reference function). The practical reading is that exact RAM is not a property of the delta rule alone. It is the delta rule plus a lot of care about magnitudes.

What breaks it

Everything the compiler has to hold fixed. All rows below are measured on the same reference function, one head, 128 key rows.

ChangeWhat a read returns
gate 0.99999 per token, 1,000 idle tokens, then read a stored 10099.003 (expected 99.004)
gate 0.999990.47
gate 0.99936.73
gate 0.990.0043
one write of 100 with β\beta = 0.9 / 0.5 / 0.190.0 / 50.0 / 10.0
old value 40, new value 200 written with β\beta = 0.5120.0, a blend of the two
random unit keys instead of one-hot, 16 / 64 / 128 / 256 / 512 items storedrelative read error 23% / 45% / 70% / 93% / 112%

Stock Qwen3.5 sits in the middle of that table. Its gate and write strength are learned, and the model has no reason to hold either at 1, so I expect (reasoned) a smooth, fading, blended memory: good for language, wrong for addresses. Qwenputer's circuit overrides both on the positions it owns, which is the point of its "mute" mechanism, where columns on a marker dimension drive Qwen's own gate and write-strength inputs to −1024-1024 on chip tokens so that Qwen's state passes through a trace unchanged (reported, design note). The random-key row is the usual associative memory: with random keys the read error grows with the number of items and passes 100% once items exceed the 128 rows. One-hot keys have no crosstalk at all.

Why bother with the state at all

Full attention keeps a KV cache that grows with the conversation. A Gated DeltaNet head keeps a fixed matrix. From Qwen3.5-4B's config (measured: 24 linear layers, 8 full layers, 32 value heads of 128 by 128, 4 KV heads of width 256):

Size
the Gated DeltaNet state, all 24 layers, fp3248 MiB, fixed
the KV cache of the 8 full-attention layers, bf1632 KiB per token
the length where the cache passes the state1,536 tokens
the KV cache at one million tokens30.5 GiB

The state is a constant 48 MiB, and the cache overtakes it at 1,536 tokens. That is the post's "fixed-size as the chat grows" (reasoned from the config; the compiled core's 32 KB of RAM is a small corner of the same states).

From RAM to a computer

A memory is not a CPU. The compiler's job is to turn a circuit description into weights, and the repo's design note (reported) says how: a long list of small steps (read this register, add these two numbers, jump if zero) becomes pieces of transformer layers. Attention heads do the lookups, SwiGLU units do the arithmetic, and the Gated DeltaNet heads hold the state. The arithmetic uses thresholds: silu(K x) with K=128K = 128 is an exact ReLU at every value the circuit produces, and a 32-bit add is two MLP sublayers. Eleven layers are enough because depth is set by the longest dependency chain in the design, not by the number of instructions.

The memory map is a good picture of how it fits. The code ROM is 256 KB of pre-decoded 4-byte words (one word per instruction, with the destination and source register slots and the immediate already unpacked), held in the key heads of two sublayers. The 32 KB of RAM is 16 key heads by 2 value heads by 128 rows of 32-bit words, again in two sublayers. The register file is two key heads (the two read ports) with 32 rows each. A write is gated: a key is the address from the previous token and a store flag, and β\beta is 1 only on a store.

The program counter is the thing that makes a computer. Each instruction is a few generated tokens: one naming the memory row to write (ADDR), one carrying the value (VAL), and a STEP token to end the instruction. Those tokens are the CPU's execution trace, so while Qwen decodes them the CPU is running. At 1.2 to 1.4 tokens per instruction, the repo reports about 3,200 trace tokens a second on a single L4 with its fused kernel (reported).

I could not run their model in the first version, and the repo's own core only later (see "What running it showed"). What I could show then is the memory half of the idea, on the same recurrence. I built a toy register machine: eight 32-bit registers and a small data memory live in rows of one head's state, with each word stored as four byte columns. Every register read or write goes through the reference recurrence as a token. The ALU and the program counter are plain Python, so this is the RAM alone, not the compiled arithmetic. I checked every run against a pure-Python interpreter.

Program (measured, toy)InstructionsTokens through the recurrenceResult
sum 1 to 1003037145,050, same as Python
Fibonacci(40)204456102,334,155 and 165,580,141 in the two registers, same as Python
12,345 × 6,789 by repeated addition20,37147,53983,810,205, same as Python

My toy spends 2.3 tokens an instruction because it reads each operand with its own token. The compiled core presumably folds the reads into the attention that produces the next token, which would explain the repo's 1.2 to 1.4; I did not check that. The two numbers are not comparable, and I do not claim anything about the core's speed from mine. The point of the exercise is narrower. Twenty thousand instructions of read-modify-write traffic through the real Qwen3.5 recurrence never lost a bit, once the keys carry enough magnitude.

How Qwen uses it

The circuit and the language model share positions. After a <tool_call> tag, a bridge captures the call's tokens into a text buffer that the core reads as memory at address 0x8000 (16 bytes per token, up to 80 tokens per call). </tool_call> starts a run. Each byte the firmware prints is echoed as the matching Qwen token, so Qwen reads the answer as an ordinary tool response, and a halt is echoed as a newline. The circuit owns exactly those positions, and Qwen predicts everywhere else (reported, design note). The calc firmware is C compiled with -DBRIDGE; it parks between calls, evaluates the first parameter, and prints a <tool_response> with values of up to 62 digits.

Two details of the repo are worth keeping in view:

Limits

All reported by the author or the repo, and consistent with what the headers show:

What running it showed

The first version of this article read the repo and did not run its code. That was a limit of the sandbox I had then, not a choice, and I have since run the code (commit bec1a0c, 31 October, with the owner's permission; the scripts and logs are in public/articles/qwenputer/verify/). bec1a0c differs from the f07a0b2 I first read in the README only, so everything quoted from the design notes still stands. Everything below is measured by me.

What it shows

The useful part of Qwenputer is not that a language model can emulate a CPU; people have shown language models executing programs for a while. It is that Qwen3.5's hybrid architecture contains a memory that, with its gate and write strength pinned and its keys one-hot, is exact, addressable and fixed-size, and that the model's own layers can be rewired to use it without touching Qwen's weights (the headers and my 12 sampled comparisons agree). The price is also visible in the file: a checkpoint twice as wide, most of it zeros, and a driver that has to keep the two halves apart. Whether this is a route to useful in-model tools, or a very good argument about what linear attention can store, is the open question. What would settle it is a model that learns to use such a memory, not one that has it compiled in. The author's follow-up on hybrid models hiding reasoning in their RNN state asks a related question from the other side.

Reproducing it

The scripts and raw outputs are committed next to the article, in public/articles/qwenputer/: toy.py (the recurrence experiments and the register machine, which imports torch_recurrent_gated_delta_rule from transformers 5.20) with toy_results.json, trace_export.py with trace.json (the trace the widget steps through), density.py, exact.py and emb.py with their outputs (the row sampling and the comparison with base Qwen), and headers.json (every tensor's shape in both checkpoints). The 31 October re-runs against the repo (its tests, calc, and repo_crosscheck.py) are in public/articles/qwenputer/verify/, with RUNLOG.md listing every command. They need torch, transformers and requests, and about 15 minutes on a CPU, most of it waiting on range requests.

Sources

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Qwenputer: a RISC-V computer compiled into Qwen3.5-4B, and what its memory actually is", ai.thesatyajit.com, October 2026.

bibtex
@misc{ghana2026qwenputer,
  author = {Satyajit Ghana},
  title  = {Qwenputer: a RISC-V computer compiled into Qwen3.5-4B, and what its memory actually is},
  url    = {https://ai.thesatyajit.com/articles/qwenputer},
  year   = {2026}
}
share