Request access to rv32cpu
This repository is publicly accessible, but you have to accept the conditions to access its files and content.
rv32cpu is a research artifact: a RISC-V computer whose logic, registers and memory are network weights, plus firmware (an exact calculator, a file store and MicroPython) that runs on it. Access is reviewed by hand. Please say briefly what you want to use it for.
Log in or Sign Up to review the conditions and access this model content.
rv32cpu: a RISC-V computer made of network weights
A 32-bit RISC-V CPU (RV32I + Zmmul) compiled into the weights of a sparse ReLU network with GDN-style memory. Nothing in it is trained. Every weight is set by a compiler, and every instruction is checked against a plain Python RISC-V emulator. One network step executes one instruction, exactly.
The repo also has firmware for it: an exact calculator (fractions, decimals, roots, logs, trig), a file store, MicroPython, and dl, a small stack language in which every byte string is a valid program. Firmware is ordinary C compiled with gcc into the CPU's memory. dl and the batched training profile are built for models that call the CPU from inside their own forward pass and learn what to run by sampling programs.
| Revision | v5 (code format rv32cpu/2) |
| Residual width | 72,989 |
| Levels per instruction | 16 |
| Nonzero weights | 533,344 (dense equivalent: 12,488,636,867) |
| Memory | 1024 KB in one GDN state, rows of 8 words; registers: 32 GDN rows |
| Firmware | link_py.elf (374 KB), link_small.elf (92 KB); MicroPython 1.24.1 |
Quick start
import os, sys
os.environ["HF_HUB_DISABLE_XET"] = "1" # see the note below
import huggingface_hub
huggingface_hub.constants.HF_HUB_DISABLE_XET = True
from huggingface_hub import snapshot_download
d = snapshot_download("User01110/rv32cpu", revision="v5") # gated: log in with an approved account first
sys.modules.pop("rv32cpu", None) # use this revision's code, not another copy
sys.path.insert(0, d)
import rv32cpu as R
cpu = R.open_dir(d, device="cuda", batch=16) # loads the weights + prebuilt firmware, no gcc
print(cpu.call(["calc 12345678901234567890 * 98765432109876543210"])[0].text)
print(cpu.call(["py x = 2**100"], keep=True)[0].text) # keep=True: this row's memory carries over
print(cpu.call(["py x + 1"], keep=True)[0].text) # -> 1267650600228229401496703205377
op = {n: i for i, n in enumerate(R.DL_OPS)}
print(cpu.call([R.dl_request([op["NUM1"], op["NUM0"], op["MUL"]], text=b"6 boxes of 7")])[0].text) # -> 42
open_dir compiles the step with torch.compile and captures a CUDA graph, which takes tens of
seconds per session. Use the .py files from the same revision as the weights: the weight file
records its format and refuses to load into a different version.
Why Xet is switched off: this repo is gated, and downloading its large files (rv32cpu.pt,
*.elf) through the Xet backend can fail with 401 Unauthorized at xet-read-token, even with
a token that has access. Plain HTTPS downloads work with the same token. Once you have the code,
R.from_hub("User01110/rv32cpu", revision="v5") does all of this in one call.
How it works
| Part | How it is built from weights |
|---|---|
| Logic | ReLU neurons as exact gates: AND = relu(Ξ£x β (nβ1)), thresholds, a 4-level carry-lookahead adder, a one-hot shifter |
| Multiplier | 32Γ32 partial products β column counts β carry-save rounds β 64-bit adder, in one step (MUL, MULH, MULHSU, MULHU) |
| Registers | A GDN state of 32 rows Γ 32 bits. The register number becomes a one-hot key; writes are delta-rule overwrites |
| Memory | One GDN state for code, data, heap, disk, stack and I/O, in rows of several words. The key comes from address-decoder neurons; a word mux follows the read |
| Exactness | Every value is an integer of fp32-exact size. TF32 is disabled. Writes have Ξ² β {0, 1}, so they overwrite exactly |
| Executor | Each batch row is an independent computer. Two engines: the weights path (sparse gather / index_add per level, torch.compile + CUDA graphs) and the fast path: the same circuit compiled with one-hot memory keys evaluated as the row numbers they encode (no address-decoder neurons), run by one generated Triton kernel per row that loops instructions on the GPU. Both are checked bit for bit against the emulator. |
Where the weights go
| part | neurons | memory ops | nonzero weights |
|---|---|---|---|
| fetch: row select | 32864 | 1 | 197717 |
| fetch: word mux | 264 | 0 | 1061 |
| pc adders | 296 | 0 | 5792 |
| decode | 43 | 0 | 4358 |
| registers: GDN read | 64 | 2 | 2742 |
| agu: rs1 + imm | 133 | 0 | 3362 |
| writeback | 519 | 0 | 7547 |
| alu | 331 | 0 | 3962 |
| next state | 133 | 0 | 1952 |
| multiplier | 3035 | 0 | 56523 |
| load/store lanes | 232 | 0 | 3423 |
| shifter | 816 | 0 | 3515 |
| data: word mux | 264 | 0 | 1397 |
| branch | 6 | 0 | 39 |
| data: row select | 32865 | 1 | 203290 |
| store: row merge | 513 | 0 | 2824 |
| write: mem | 0 | 1 | 33281 |
| write: regs | 0 | 1 | 559 |
| total | 72378 | 6 | 533344 |
Memory map
| region | from | to |
|---|---|---|
| firmware + heap | 0x100000 |
0x1a8000 |
| disk (file store) | 0x1a8000 |
0x1e8000 |
| stack (grows down) | 0x1e8000 |
0x1f8000 |
| I/O in: [length][bytes] | 0x1f8000 |
0x1fc000 |
| I/O out: [length][bytes] | 0x1fc000 |
0x200000 |
Verification
On the run that produced this revision, the weights matched the Python emulator on:
| check | passed |
|---|---|
| demo programs (8 checks each: steps, pc, halt, registers, memory, write log, pc trace, exact 0/1) | 13 / 13 |
| random programs: ALU, shifts, multiplies, loads/stores, branches + random words | 144 / 144 (30,864 instructions) |
| fast path (indexed net, fused engine): demo programs + random programs vs the emulator | 157 / 157 |
| link firmware: calc vs its Python reference, files, rollback, Python, sessions | 25 / 25 |
| dl: link firmware: NUM0 NUM1 MUL over '6 by 7', and an empty program | 2 / 2 |
| dl: link firmware: 64 random programs + toolsets + texts | 64 / 64 |
| dl: link firmware: PYRUN (a ```python block, and one that raises) | 42 Β· NameError: name 'y' isn't defined |
| dl: training profile (128 KB rows): 1,024 random programs at once (CPU.many) | 1024 / 1024 |
| dl: training profile: 256 calc calls (decimals, %, commas, round, sqrt) at once, vs the reference | 256 / 256 |
The link firmware
link_py.elf (with MicroPython) or link.elf (C only) answers one text request per call:
| Request | Reply |
|---|---|
calc <expr> |
An exact result: fractions, decimals, 1,200, 15%, 20% of 80, + - * / // % ** ^ !, sqrt root round floor ceil abs min max sum mean gcd lcm comb perm exp ln log sin cos tan, pi e deg. Non-terminating decimals show 12 places |
dl <bytes> |
A dl program's result over a text (below) |
py <code> |
What the code printed. A single line is shown like the REPL (its value is printed) |
run <file> |
Runs a Python file from the disk |
put <name>\n<data> / append <name>\n<data> |
ok |
get <name> / ls / rm <name> / df |
File contents, a listing, ok, or usage |
reset |
Forgets the Python state |
help |
The command list and the MicroPython version |
The status comes back in a0: 0 ok, 1β4 and 11 calc errors (syntax, division by zero, overflow, too complex, math domain), 5 Python exception, 6 file not found,
7 disk full, 8 unknown command, 9 no Python in this build, 10 fatal.
Sessions. Each batch row is a session:
keep=Falsereboots the firmware for every call. The row's disk is kept.keep=Truekeeps the whole memory: Python variables, imports and files.
A call that does not end cleanly (a fault, an illegal instruction, or running out of steps) is
rolled back. CPU.save_state / load_state save and restore sessions to a file.
dl: a language with no wrong programs
A request is dl + [n][16 program bytes][16 toolset slots Γ (len + 15 ops)][text]
(R.dl_request builds it). Every byte is an op (byte mod 53), and every op is defined on every
stack: an empty stack reads as 0, x // 0 is 0, and overflow wraps at 64 bits. So any byte string
runs and ends, which is what lets a model learn the CPU by sampling programs.
| Ops | Meaning |
|---|---|
NUM0βNUM7, COUNT, SUM, MAXN, MINN |
The k-th most recent number in the text (digit runs); count, sum, max, min of all of them |
C0 C1 C2 C10, DUP SWAP DROP OVER |
Constants and stack moves |
ADD SUB MUL DIV MOD NEG ABS MIN MAX EQ LT SEL |
64-bit integer arithmetic (DIV/MOD with Python floor semantics), comparisons, select |
STORE LOAD, SAVE RECALL |
16 number variables; 4 text slots (the text's last line). Kept across calls in a keep=True session |
CALL0βCALL15 |
Run toolset slot k (up to 4 deep). The model grows its own toolset during training |
PYRUN |
Run the last ```python block of the text (or its whole lines) in MicroPython; show the last printed line |
The reply is the top of the stack (or the text that RECALL or PYRUN showed). The status is
0 ok, 1 empty, 2 out of budget (256 ops), 3 PYRUN without Python, 4 PYRUN raised.
R.dl_ref is the reference interpreter. For training there is a smaller CPU profile
(CPUConfig.small(): 128 KB rows, C firmware, rv32cpu_small.pt + link_small.elf), and
CPU.many runs thousands of one-shot calls at once.
Limits
- Speed: measured on the run that produced this revision: weights net Β· compile + cudagraph (batch 64, traced): 2,684; indexed net Β· fused (batch 64): 2,290; one calc call on the link machine (fused): 9,032 steps in 4028 ms; dl calls, training profile (fused, 1,024 rows at once): 17 calls/s (8,563 instructions per call). A MicroPython call is roughly 10β΅ instructions or more.
- Arithmetic: division and remainder run in software (libgcc). Floats are software single precision.
- Size: 1 MB of memory per row by default (
CPUConfig), 128 KB in the training profile. Bigger memories cost about one neuron per memory row. - Status: a research artifact. Interfaces may change between revisions.
Credits and licenses
- Inspired by Qwenputer, which compiled a RISC-V CPU into Qwen3.5-4B.
link_py.elfcontains MicroPython (MIT license, seeLICENSE-micropython) and libgcc (GPL with the GCC Runtime Library Exception).- Everything else in this repo: apache-2.0.