跳到正文
原文
Hacker News· dbreunig·· 9 天前精选AI 评分70

NES CPU编译进Qwen3.5模型权重

Show HN: A working Nintendo in ~1GB of Qwen weights

AI 摘要

一个NES CPU被编译进Qwen3.5模型的权重中,其6502 CPU、2 KB工作RAM和游戏卡ROM在标准Qwen3_5ForCausalLM检查点内运行,模型预测CPU执行日志,主机程序处理图像绘制、控制器读取和中断

正文 · AI 翻译

一台 NES CPU 编译成了 Qwen3.5 模型的权重。

NES 的 6502 CPU(其 2A03 芯片的核心)、它的 2 KB 工作 RAM 以及一个卡带的程序 ROM 运行在一个标准的 Qwen3_5ForCausalLM 检查点内。没有人训练过这些权重;编译器计算了每一个。当你玩一个游戏时,模型逐个 token 地预测 CPU 的执行日志。一个小型主机程序在它周围播放控制台剩余的部分:它绘制图片,读取控制器,并发送 vblank 中断

nesformer 基于 Qwenputer 由 Divyansh Agrawal 构建,它将一个 CHIP-8 然后是一个 RISC-V CPU 编译到 Qwen3.5-4B 中。它重用了 Qwenputer 的编译器和它的核心技巧:模型的 Gated DeltaNet 状态是机器的内存。

超级马里奥兄弟 (世界 1-1) 完整的游戏由 Doug Fraker (标题屏幕和第一关) 制作
Super Mario Bros., played by the weights Full Game, played by the weights

这些运行中的每个 CPU token 都来自模型。对于捆绑的测试游戏,每一帧也精确匹配一个传统的 6502 模拟器的输出(参见 如何进行检查)

快速入门

1. 文件夹布局

解压 nesformer.zip 和 nesformer-weights.zip 到同一个文件夹。权重放在 nesformer/ 包的旁边,而不是里面:

nesformer/              <- run commands from here
├── nesformer/          <- the Python package
├── chipformer/         <- Qwenputer's compiler
├── roms/               <- test ROMs (put your own here too)
├── examples/           <- sample GIFs and frame sheets
├── tests/
├── LICENSE
├── LICENSE.chipformer
├── README.md
├── pyproject.toml
└── weights/            <- from nesformer-weights.zip
    ├── config.json
    ├── generation_config.json
    ├── model.safetensors
    ├── nesformer.json
    └── SHA256SUMS

CLI 在 weights/ 旁边寻找 nesformer/ 包。为了将权重放在其他地方,将 --weights /path/to/weights 传递给 run,check 或 trace,或者给 info 路径

为了确认权重已正确解压,从顶层文件夹运行 shasum -a 256 -c weights/SHA256SUMS

2. 安装

你需要 Python 3.11 或更高版本

python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install numpy scipy numba pillow

3. 玩一个游戏

python -m nesformer run roms/thwaite.nes --frames 600 --show \
  --buttons "start@90-95+200-205,a@230-233+250-253+270-273+290-293+310-313+330-333+350-353+370-373+390-393+410-413+430-433+450-453+470-473+490-493+510-513+530-533+550-553+570-573+590-593"

--show 在模型生成它的同时绘制每一帧到你的终端。当运行结束时,CLI 会保存一个以 ROM 命名的动画 GIF(这里 thwaite.gif)在当前文件夹中

预期会很慢。第一次运行大约花费 20 秒用 numba 编译引擎。之后,每一帧需要 1-6 秒,所以 600 帧(10 秒的 NES 时间)大约需要 25 分钟

玩你自己的 ROM

将 run 指向任何使用 NROM 板(iNES 映射器 0)的 .nes 文件,例如超级马里奥兄弟、唐基克或兴奋自行车。你提供 ROM;nesformer 仅随附三个自由许可的测试 ROM 在 roms/ 中

python -m nesformer run roms/smb.nes --frames 600 --show \
  --buttons "start@60-65,right@200-600,a@250-265+300-315+360-375+420-435+480-495+540-555"

那个脚本启动游戏,然后让马里奥向右走并大约每秒跳跃一次。CLI 在启动时将 ROM 加载到模型的状态中,所以相同的权重可以运行任何 NROM 游戏,而无需重建。其他板上的游戏会停止并显示消息:参见 限制

控制器脚本

--buttons 接受一个逗号分隔的列表。每个条目命名一个或多个由 + 连接的按钮,然后是 @,然后是一个或多个由 + 连接的帧范围:

start@60-65                 press Start during frames 60 to 65
right@200-600               hold Right from frame 200 to frame 600
a@250-265+300-315           press A twice
right+b@200-600             hold Right and B together (run, in Super Mario Bros.)

按钮是 a、b、select、start、up、down、left 和 right。帧数从开机开始计算,每秒 60 帧,所以第 600 帧是 10 秒。游戏会忽略你无法移动的屏幕上的输入,所以稍微提前按住按钮没有坏处。在超级马里奥兄弟中,较长的 A 按键会跳得更高

命令

命令 它做什么
python -m nesformer run ROM 在一个 ROM 上玩权重并保存 GIF
python -m nesformer check ROM 并排运行权重和参考 CPU;在第一个差异处停止
python -m nesformer trace ROM 打印模型的 token 流(默认 40 个 token)
python -m nesformer build 编译电路并写入 weights/ (大约 10 秒)
python -m nesformer info 描述检查点

run 的选项:

选项 默认值 含义
--frames N 120 在 N 帧后停止
--buttons SCRIPT 无 控制器输入(见上文)
--show 关闭 在终端中绘制帧(需要 24 位颜色,例如 iTerm2 或 Windows Terminal)
--gif PATH ROM 名称 在哪里保存动画 GIF
--png DIR 关闭 将每个保留的帧作为 PNG 保存到 DIR。仅使用 --png 时,不保存 GIF。添加 --gif 获取两者
--every N 1 保留每 N 帧的帧
--scale N 2 将保存的帧放大 N 倍
--engine fast fast (numba)、numpy 或 hf (标准 transformer;需要 torch,~10 tokens/s)
--no-skip 关闭 运行空闲等待循环的每次迭代(较慢;相同的帧)
--max-tokens N 无 N 个 token 后停止
--trace FILE 无 保存 token id

run、check 和 trace 都接受 --weights、--engine、--buttons 和 --no-skip。 check --tokens N 设置比较范围(默认 50,000)。trace --tokens N 设置打印多少 token(默认 40)。build --out DIR 在 weights/ 之外写入检查点.

工作原理

权重中包含什么,不包含什么

在模型(权重 + DeltaNet 状态)中 在主机(nesformer/host.py)中
6502 核心:所有 151 个官方指令码,每种寻址模式,标志位,堆栈,BRK 和 NMI 入口 PPU:扫描线渲染器和调色板
2 KB 的工作 RAM 控制器;APU 寄存器(写入,否则忽略)
32 KB 的 PRG-ROM,存储两次:一份用于指令获取,一份用于数据读取 vblank 时的 NMI 定时,作为 NMI token 注入
OAM DMA 读取(256 字节作为 DM token 出现) I/O 读取的响应,作为 IN token 注入

这种拆分遵循 Qwenputer 的 CHIP-8 构建,其主机注入定时器滴答并从跟踪中绘制显示。主机除了通过跟踪外,永远不会看到 CPU 的寄存器或 RAM.

内存技巧的工作原理如下。每个 DeltaNet 头保持一个 128 × 128 的状态矩阵,该矩阵由 delta 规则在每个 token 时更新。保持门打开并使用一个热键,更新会覆盖该矩阵的恰好一行,而一个热键查询会读取它。因此,每一行都是一个内存单元,而卡带、RAM 和寄存器都存在于行中.

检查点

weights/ 是一个普通的 Hugging Face 文件夹。它加载 Qwen3_5ForCausalLM.from_pretrained 并不需要自定义代码.

架构 Qwen3_5ForCausalLM,具有 Qwen3.5-4B 的 DeltaNet 头形状(16 个键头,32 个值头,维度 128)和 MLP 宽度(9,216)
层 7 个门控 DeltaNet 层(电路),加上 1 个全零注意力层,Hugging Face 的缓存需要它
隐藏大小 768
词汇表 263,808 个 token
参数 466.6M 在 bf16(933 MB),其中 9.3M 是非零,因此权重压缩到 3.3 MB

nesformer.json 记录哪个层和头持有机器的每个部分。运行将卡带、寄存器和 RAM 写入那些 DeltaNet 状态,然后贪婪解码.

层计划

层 注意力(状态) MLP
1 PC 锚;指令计数器;A、X、Y、SP;标志位 N V Z C I D;指针锁存器;NMI 标志 计数器温度计;SP ± 1;LO + Y
2 — PC = 锚 + 自从执行以来的字节数;PC + 1, PC + 2
3 PRG 获取:PC 处的操作码和两个操作数字节 操作码解码;操作位;索引和;分支测试和目标
4 — 有效地址,通过寻址模式和记录阶段;推送存储的字节
5 RAM 和 PRG 数据读取在有效地址处 操作数;ADC/SBC 加法器,比较,BIT;地址解码
6 — 结果字节,进位,溢出,标志位,跳转目标
7 — 下一个 token

跟踪

每条指令都变成一条记录,记录的最后一个 token 确认它。与 Qwenputer 的 RISC-V 核心一样,PC 是隐含的而不是写入的:每个最终 token 带有它指令的长度,三个 DeltaNet 计数器统计自上次跳转以来每种长度的指令数量,PC 是跳转目标加上该偏移量。全游戏的摘录:

SEI            P(000100 px0 L1)
LDX #$40       X($40 c0 v0 px0 L2)
STX $4017      IOW($4017=$40 L3)
TXS            S($FF L1)
BIT $2002      IOR($2002) IN($00) P(000110 px0 L3)       <- the host injects IN
BPL $8017      J($8017)
JSR $8279      PSH(\(FE) PSH(\)FD) J($8279)                 <- the return address is pushed in-circuit
RTS            PUL($FC) LO($7B) PUL($FD) J($827C)
STA ($72),Y    LO($08) HI($03) ST($00 a7=0 L2)            <- pointer bytes latched, then the store
STA $4014      IOW($4014=$02 L0) DM(0:$FF) … DM(255:$00) NT(L3)   <- OAM DMA

大多数指令使用单个 token;真实游戏代码平均每条指令 1.1–1.3 个 token。 nesformer/spec.py 记录了每个 token 家族.

如何进行检查

  • 参考 CPU (nesformer/cpu.py) 与 nestest 的黄金日志对于 3,348 条指令匹配,并且 py65 模拟器在 5.7 百万条游戏代码指令上匹配。

  • 精确电路,在 Qwenputer 的评估器中运行,并使用其 bf16 精确性检查,预测了每个参考 token:所有 nestest 加上两个游戏启动序列的 283,610 个位置。

  • 库存转换器中的检查点 (bf16, CPU) 预测了 nestest 的 2,651 个位置,并且 Full Game 没有错误匹配。

  • 自由运行的重心 产生的跟踪与参考 CPU 的完全相同:

    Run Tokens Frames NES 时间 结果
    nestest (官方指令集) 4,586 — — 相同,以预期的 HALT 结束
    Thwaite, check 700,000 271 4.5 s 相同的跟踪和帧
    Thwaite, run 1,916,527 600 10.0 s 所有 300 个保存的帧匹配
    Full Game, run 2,645,885 420 7.0 s 所有 210 个保存的帧匹配
  • 空闲循环跳过 什么可见的都没有改变:帧和 I/O 写入,周期时间戳 包括在内,与没有它的运行匹配。

速度

默认引擎从 model.safetensors 计算 Qwen3.5 的前向传递,编译时使用 numba 并跳过全零块。它在构建机器的一个 CPU 核心上运行速度约为 1,650 个 token/s。普通的 numpy 引擎大约是 210,库存转换器大约是 10。一个真实的 NES 需要大约 700,000 个 token 每秒。

游戏在每帧的大部分时间中都在一个等待 vblank 的循环中。当主机看到循环体完全重复,没有任何 RAM 或 I/O 写入时,它会将时钟推进到下一个 PPU 事件之前,剩余的迭代仍然运行。这使 token 数量减少了 2.6× 到 4.2×。 总体而言,nesformer 比真实主机慢 150–290×。

限制

  • 仅支持 NROM 卡带 (iNES 映射器 0)。 CPU 一次只能寻址 32 KB 的程序 ROM。较大的游戏带有银行切换芯片(一个 映射器,例如 MMC1 或 MMC3),它切换 CPU 看到的 ROM 部分。nesformer 的 ROM 读取在模型内部进行,来自一个固定的 32 KB 表,所以它还不能跟随银行切换。支持映射器意味着将整个 ROM 存储在状态中,同时保留银行寄存器在那里,并添加一个阶段将每个 CPU 地址转换为 ROM 地址。
  • 仅支持官方指令集。 非官方指令集会发出 HALT。
  • 代码必须从卡带 ROM 运行。 跳转到 RAM 会发出 HALT。
  • 没有 IRQ (APU 帧 IRQ, DMC)。BRK 和 NMI 有效。
  • I/O 寄存器上的读-修改-写指令不会产生 I/O 写入,并且 $4020–$7FFF 的读取为 0 (没有 PRG-RAM)。
  • PPU 和 APU 没有在权重中。PPU 一次渲染一行扫描线,所以中扫描线效果无法重现。精灵-0 击中和精灵溢出是近似的,并且没有声音。

测试

pip install pytest py65
python -m pytest -q tests

测试需要几分钟。它们检查参考 CPU 与 nestest 的日志,电路与参考,权重自由运行与参考,以及空闲循环跳过。

在库存转换器中运行检查点

pip install "torch>=2.6" "transformers>=5.22"
python -m nesformer run roms/thwaite.nes --frames 3 --engine hf

nesformer/hfcheck.py 显示了整个过程:加载检查点,解码一个填充 token,将机器状态写入缓存 DeltaNet 状态,然后逐个输入 token。

文件

路径 内容
nesformer/spec.py token 家族和记录
nesformer/cpu.py 写入跟踪的参考 6502
nesformer/host.py CPU 周围的一切:PPU、控制器、NMI 定时、周期计数、空闲跳过
nesformer/core.py 电路和状态快照
nesformer/weights.py 将电路转换为 Qwen3_5ForCausalLM 检查点的发射器
nesformer/engine.py, fastengine.py 运行权重 (numpy / numba)
nesformer/hfcheck.py 在库存转换器中运行权重
nesformer/run.py, check.py, cli.py, __main__.py 驱动程序、教师强制检查、CLI
chipformer/ Qwenputer 的 q35 编译器 (q35/ 发射器和原语,dsl/ 图)
weights/ 编译的检查点及其 SHA-256 校验和
roms/ 测试 ROM; roms/SOURCES.md 列出了它们的作者和许可证
examples/ 运行时的 GIF 和帧表; demo.gif 是顶部显示的超级马里奥兄弟运行
tests/test_nesformer.py 测试套件

鸣谢

  • Qwenputer 和它的 chipformer 编译器:MIT, © 2026 Divyansh Agrawal (LICENSE.chipformer)。该许可也涵盖了从 Qwenputer 中改编的 nesformer/core.py 和 nesformer/weights.py 中的代码。它的最后一段提到了 Qwenputer 的 CHIP-8 ROMs, 而这个存储库不包含这些 ROMs.
  • 测试 ROMs:nestest 由 Kevin Horton 提供;Full Game 由 Doug Fraker (MIT) 提供;Thwaite 由 Damian Yerrick (GPL-3.0,源代码位于 pinobatch/thwaite-nes)。

许可

nesformer 是根据 MIT 许可证发布的(参见 LICENSE)。roms/ 中的 ROMs 保留它们自己的 许可证,列在 roms/SOURCES.md 中.

来源:Hacker News · github.com

00