|
Download README.md from User01110/rv32cpu: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/User01110/rv32cpu/resolve/main/README.md
- Command line
-
hf download hf://User01110/rv32cpu/README.md
-
curl -L -H "Authorization: Bearer $HF_TOKEN" -o README.md https://huggingface.co/User01110/rv32cpu/resolve/main/README.md
10.9 kB
| license: apache-2.0 | |
| library_name: pytorch | |
| tags: | |
| - risc-v | |
| - neural-computer | |
| - compiled-weights | |
| - exact-computation | |
| - micropython | |
| - gated-delta-network | |
| extra_gated_heading: "Request access to rv32cpu" | |
| extra_gated_prompt: >- | |
| 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. | |
| extra_gated_fields: | |
| Name: text | |
| Affiliation: text | |
| Intended use: text | |
| I understand this is experimental research code with no warranty: checkbox | |
| extra_gated_button_content: "Request access" | |
| # 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 | |
| ```python | |
| 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=False` reboots the firmware for every call. The row's **disk** is kept. | |
| - `keep=True` keeps 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](https://huggingface.co/divagr1925/Qwen3.5-4B-Qwenputer), which compiled a RISC-V CPU into Qwen3.5-4B. | |
| - `link_py.elf` contains [MicroPython](https://micropython.org) (MIT license, see `LICENSE-micropython`) and libgcc (GPL with the GCC Runtime Library Exception). | |
| - Everything else in this repo: apache-2.0. | |