Show HN: A working Nintendo in ~1GB of Qwen weights
本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。
An NES CPU compiled into the weights of a stock Qwen3.5 model.
一个被编译进标准 Qwen3.5 模型权重的 NES CPU。
The NES’s 6502 CPU (the core of its 2A03 chip), its 2 KB of work RAM, and a cartridge’s program ROM run inside a standard Qwen3_5ForCausalLM checkpoint. Nobody trained these weights; a compiler computed every one. When you play a game, the model predicts the CPU’s execution log one token at a time. A small host program plays the rest of the console around it: it draws the picture, reads the controller, and fires the vblank interrupt.
NES 的 6502 CPU(其 2A03 芯片的核心)、2 KB 工作内存以及卡带的程序 ROM 都在一个标准的 Qwen3_5ForCausalLM 检查点内运行。这些权重并非通过训练获得,而是由编译器计算出来的。当你玩游戏时,模型会逐个 token 地预测 CPU 的执行日志。一个小型宿主程序负责模拟控制台的其余部分:绘制图像、读取控制器输入并触发垂直空白(vblank)中断。
nesformer builds on Qwenputer by Divyansh Agrawal, which compiled a CHIP-8 and then a RISC-V CPU into Qwen3.5-4B. It reuses Qwenputer’s compiler and its central trick: the model’s Gated DeltaNet state is the machine’s memory.
nesformer 基于 Divyansh Agrawal 开发的 Qwenputer 构建,后者曾将 CHIP-8 和 RISC-V CPU 编译进 Qwen3.5-4B 模型中。它复用了 Qwenputer 的编译器及其核心技巧:利用模型的 Gated DeltaNet 状态作为机器的内存。
Every CPU token in these runs came from the model. For the bundled test games, every frame also matches a conventional 6502 emulator’s output exactly (see How it was checked).
在这些运行过程中,每一个 CPU token 都来自模型。对于捆绑的测试游戏,每一帧的输出也与传统 6502 模拟器的输出完全一致(参见“如何验证”部分)。
Unzip nesformer.zip and nesformer-weights.zip into the same folder. The weights go next to the nesformer/ package, not inside it:
将 nesformer.zip 和 nesformer-weights.zip 解压到同一个文件夹中。权重文件应放置在 nesformer/ 包的旁边,而不是放入其中:
The CLI looks for weights/ beside the nesformer/ package. To keep the weights elsewhere, pass —weights /path/to/weights to run, check or trace, or give the path to info.
命令行界面(CLI)会在 nesformer/ 包旁边查找 weights/ 文件夹。若要将权重存放在其他位置,可以在运行 run、check 或 trace 命令时传入 —weights /path/to/weights 参数,或者在 info 命令中指定路径。
To confirm the weights unzipped correctly, run shasum -a 256 -c weights/SHA256SUMS from the top folder.
要确认权重解压无误,请在顶层文件夹中运行 shasum -a 256 -c weights/SHA256SUMS。
You need Python 3.11 or newer.
你需要 Python 3.11 或更高版本。
—show draws each frame in your terminal as the model generates it. When the run ends, the CLI saves an animated GIF named after the ROM (here thwaite.gif) in the current folder.
—show 参数会在模型生成每一帧时将其绘制在终端中。运行结束后,CLI 会在当前文件夹中保存一个以 ROM 命名的动画 GIF(例如 thwaite.gif)。
Expect it to be slow. The first run spends about 20 seconds compiling the engine with numba. After that, a frame takes 1–6 seconds, so 600 frames (10 seconds of NES time) take around 25 minutes.
运行速度会比较慢。首次运行时,使用 numba 编译引擎大约需要 20 秒。之后,每一帧的生成需要 1 到 6 秒,因此 600 帧(相当于 10 秒的 NES 游戏时间)大约需要 25 分钟。
Point run at any .nes file that uses the NROM board (iNES mapper 0), such as Super Mario Bros., Donkey Kong or Excitebike. You supply the ROM; nesformer only ships the three freely licensed test ROMs in roms/.
将 run 命令指向任何使用 NROM 板(iNES mapper 0)的 .nes 文件,例如《超级马力欧兄弟》、《大金刚》或《越野机车》。你需要自行提供 ROM;nesformer 仅在 roms/ 文件夹中附带了三个免费授权的测试 ROM。
That script starts the game, then walks Mario right and jumps about once a second. The CLI loads the ROM into the model’s state at startup, so the same weights run any NROM game without a rebuild. Games on other boards stop with a message: see Limitations.
该脚本会启动游戏,让马力欧向右走并大约每秒跳跃一次。CLI 在启动时将 ROM 加载到模型状态中,因此同一套权重无需重新编译即可运行任何 NROM 游戏。其他板卡的游戏会停止并显示提示信息:请参阅“限制”部分。
—buttons takes a comma-separated list of entries. Each entry names one or more buttons joined by +, then @, then one or more frame ranges joined by +:
—buttons 参数接受一个逗号分隔的条目列表。每个条目由一个或多个用 + 连接的按钮名称组成,后跟 @,再后跟一个或多个用 + 连接的帧范围:
The buttons are a, b, select, start, up, down, left and right. Frames count from power-on at 60 per second, so frame 600 is 10 seconds in. Games ignore input on screens where you can’t move, so holding a button a little early does no harm. In Super Mario Bros., a longer A press makes a higher jump.
按钮包括 a、b、select、start、up、down、left 和 right。帧数从开机开始计算,每秒 60 帧,因此第 600 帧对应 10 秒处。游戏在无法移动的屏幕上会忽略输入,所以稍微提前按住按钮不会有影响。在《超级马力欧兄弟》中,长按 A 键可以跳得更高。
run, check and trace all accept —weights, —engine, —buttons and —no-skip. check —tokens N sets how far to compare (default 50,000). trace —tokens N sets how many tokens to print (default 40). build —out DIR writes the checkpoint somewhere other than weights/.
run、check 和 trace 命令均接受 —weights、—engine、—buttons 和 —no-skip 参数。check —tokens N 用于设置比较的长度(默认 50,000)。trace —tokens N 用于设置打印的 token 数量(默认 40)。build —out DIR 可将检查点写入 weights/ 以外的目录。
This split follows Qwenputer’s CHIP-8 build, whose host injects timer ticks and draws the display from the trace. The host never sees the CPU’s registers or RAM except through the trace.
这种拆分方式沿用了 Qwenputer 的 CHIP-8 构建方案,其宿主程序负责注入时钟滴答并根据追踪记录绘制显示内容。宿主程序除了通过追踪记录外,无法直接看到 CPU 的寄存器或 RAM。
The memory trick works like this. Each DeltaNet head keeps a 128 × 128 state matrix that the delta rule updates at every token. With the gate held open and a one-hot key, an update overwrites exactly one row of that matrix, and a one-hot query reads it back. So every row is a memory cell, and the cartridge, RAM and registers all live in rows.
内存技巧的原理如下:每个 DeltaNet 头都维护一个 128 × 128 的状态矩阵,该矩阵在每个 token 处通过 delta 规则进行更新。当门控保持打开并使用独热(one-hot)键时,更新操作会精确覆盖矩阵中的一行,而独热查询则将其读回。因此,每一行都是一个内存单元,卡带、RAM 和寄存器都存储在这些行中。
weights/ is an ordinary Hugging Face folder. It loads with Qwen3_5ForCausalLM.from_pretrained and needs no custom code.
weights/ 是一个普通的 Hugging Face 文件夹。它可以通过 Qwen3_5ForCausalLM.from_pretrained 加载,无需任何自定义代码。
nesformer.json records which layer and head holds each part of the machine. A run writes the cartridge, registers and RAM into those DeltaNet states, then decodes greedily.
nesformer.json 记录了机器的各个部分分别存储在哪些层和头中。运行程序时,会将卡带、寄存器和 RAM 写入这些 DeltaNet 状态,然后进行贪婪解码。
Each instruction becomes a record, and the record’s last token commits it. As in Qwenputer’s RISC-V core, the PC is implied rather than written: each final token carries its instruction’s length, three DeltaNet counters tally the instructions of each length since the last jump, and the PC is the jump target plus that offset. Excerpts from Full Game:
每条指令都成为一条记录,记录的最后一个 token 会提交该指令。正如 Qwenputer 的 RISC-V 核心一样,程序计数器(PC)是隐含的而非显式写入的:每个最终 token 都携带其指令的长度,三个 DeltaNet 计数器统计自上次跳转以来每种长度指令的数量,PC 即为跳转目标加上该偏移量。完整游戏摘录:
Most instructions take a single token; real game code averages 1.1–1.3 tokens per instruction. nesformer/spec.py documents every token family.
大多数指令占用一个 token;实际游戏代码平均每条指令占用 1.1–1.3 个 token。nesformer/spec.py 文档记录了每个 token 系列。
The reference CPU (nesformer/cpu.py) matches nestest’s golden log for 3,348 instructions, and the py65 emulator on 5.7 million instructions of game code.
参考 CPU (nesformer/cpu.py) 在 3,348 条指令上与 nestest 的黄金日志匹配,并在 570 万条游戏代码指令上与 py65 模拟器匹配。
The exact circuit, run in Qwenputer’s evaluator with its bf16 exactness checks, predicts every reference token: all of nestest plus 283,610 positions of two games’ boot sequences.
在 Qwenputer 的评估器中运行并进行 bf16 精度检查时,该精确电路预测了每一个参考 token:包括全部 nestest 以及两个游戏启动序列的 283,610 个位置。
The checkpoint in stock transformers (bf16, CPU) predicts 2,651 positions of nestest and Full Game with no mismatches.
在标准 transformers 库(bf16,CPU)中,该检查点预测了 nestest 和完整游戏的 2,651 个位置,且没有任何不匹配。
The weights, free-running, generate traces identical to the reference CPU’s:
自由运行的权重生成的追踪记录与参考 CPU 的完全相同:
The idle-loop skip changes nothing visible: frames and I/O writes, cycle timestamps included, match a run without it.
空闲循环跳过功能不会改变任何可见内容:包括帧、I/O 写入和周期时间戳在内,与不使用该功能时的运行结果一致。
The default engine computes Qwen3.5’s forward pass from model.safetensors, compiled with numba and skipping the all-zero blocks. It ran at about 1,650 tokens/s on one CPU core of the machine it was built on. The plain numpy engine does about 210, and stock transformers about 10. A real NES would need about 700,000 tokens per second.
默认引擎从 model.safetensors 计算 Qwen3.5 的前向传播,使用 numba 编译并跳过全零块。在构建它的机器上,它在单个 CPU 核心上以约 1,650 token/s 的速度运行。普通的 numpy 引擎约为 210 token/s,而标准 transformers 约为 10 token/s。真实的 NES 控制台每秒需要约 700,000 个 token。
Games spend most of each frame in a loop that waits for vblank. When the host sees a loop body repeat exactly, with no RAM or I/O writes, it advances the clock to just before the next PPU event, and the remaining iterations still run. That cuts the token count by 2.6× to 4.2×. Overall, nesformer runs 150–290× slower than the real console.
游戏在每一帧的大部分时间里都在等待垂直空白(vblank)的循环中。当宿主程序检测到循环体完全重复且没有 RAM 或 I/O 写入时,它会将时钟推进到下一个 PPU 事件之前,而剩余的迭代仍然会运行。这减少了 2.6 到 4.2 倍的 token 数量。总体而言,nesformer 的运行速度比真实控制台慢 150–290 倍。
The tests take a few minutes. They check the reference CPU against nestest’s log, the circuit against the reference, the weights free-running against the reference, and the idle-loop skip.
测试过程需要几分钟。它们会检查参考 CPU 与 nestest 日志的匹配情况、电路与参考的匹配情况、权重自由运行与参考的匹配情况,以及空闲循环跳过功能。
nesformer/hfcheck.py shows the whole procedure: load the checkpoint, decode one padding token, write the machine’s state into the cache’s DeltaNet states, then feed tokens one at a time.
nesformer/hfcheck.py 展示了整个过程:加载检查点,解码一个填充 token,将机器状态写入缓存的 DeltaNet 状态,然后逐个输入 token。
nesformer is released under the MIT License (see LI
nesformer 在 MIT 许可证下发布(参见 LI