跳到正文
Hacker News· dbreunig·· 5 小时前AI 评分58

nesformer:把 NES 的 6502 CPU 编译进约 1GB 的 Qwen 权重

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

AI 导读

nesformer 将 NES 的 6502 CPU、2KB 工作内存和卡带 ROM 编译进一个标准 Qwen3_5ForCausalLM 检查点,权重由编译器算出而非训练得到,模型逐 token 预测 CPU 执行日志。

正文 · AI 翻译

一个 NES CPU 被编译进了一个现成 Qwen3.5 模型的权重中。

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

nesformer 基于 Divyansh Agrawal 的 Qwenputer 构建,后者曾将一个 CHIP-8 以及随后一个 RISC-V CPU 编译进 Qwen3.5-4B。它复用了 Qwenputer 的编译器及其核心技巧:模型的 Gated DeltaNet 状态就是机器的内存。

Super Mario Bros.(World 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 会在 nesformer/ 包旁边查找 weights/。若要将权重放在别处,请将 --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 mapper 0)的 .nes 文件,例如 Super Mario Bros.、Donkey Kong 或 Excitebike。ROM 由你提供;nesformer 仅附带 roms/ 中的三个自由许可的测试 ROM。

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"

该脚本会启动游戏,然后让 Mario 向右走并大约每秒跳一次。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 秒时。游戏在无法移动的屏幕上会忽略输入,因此稍微提前按住按钮不会造成损害。在 Super Mario Bros. 中,按 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 关闭 将每个保留的帧保存为 DIR 中的 PNG。仅使用 --png 时,不保存 GIF。添加 --gif 可同时获得两者。
--every N 1 保留每第 N 帧
--scale N 2 将保存的帧放大 N 倍
--engine fast fast(numba)、numpy 或 hf(原版 transformers;需要 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 构建,其宿主注入定时器 tick,并根据 trace 绘制 显示。除通过 trace 外,宿主永远看不到 CPU 的寄存器或 RAM。

内存技巧是这样工作的。每个 DeltaNet 头维护一个 128 × 128 状态矩阵, delta 规则在每个 token 处更新它。门保持打开且键为 one-hot 时,一次更新 恰好覆盖该矩阵的一行,而 one-hot 查询会将其读回。因此每一行都是一个 内存单元,卡带、RAM 和寄存器都存放在行中。

检查点

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

架构 Qwen3_5ForCausalLM,采用 Qwen3.5-4B 的 DeltaNet 头形状(16 个键头、32 个值头、维度 128)和 MLP 宽度(9,216)
层 7 个 Gated DeltaNet 层(电路),外加 1 个全零注意力层,Hugging Face 的缓存需要它
隐藏大小 768
词表 263,808 个 token
参数 bf16 下 466.6M(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 — 有效地址,按寻址模式和记录阶段;push 存储的字节
5 在有效地址处读取 RAM 和 PRG 数据 操作数;ADC/SBC 加法器、比较、BIT;地址解码
6 — 结果字节、进位、溢出、标志位、跳转目标
7 — 下一个 token

trace

每条指令成为一条记录,记录的最后一个 token 提交它。与 Qwenputer 的 RISC-V 核心一样,PC 是隐含的而非写出的:每个最终 token 携带其指令的 长度,三个 DeltaNet 计数器统计自上次跳转以来每种长度的指令数, PC 就是跳转目标加上该偏移量。摘自 Full Game:

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)在 3,348 条指令上与 nestest 的黄金日志匹配, 并在 570 万条游戏代码指令上与 py65 模拟器匹配。

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

  • stock transformers 中的检查点(bf16,CPU)预测 nestest 的 2,651 个位置和 Full Game,无任何不匹配。

  • 权重自由运行,生成的轨迹与参考 CPU 完全一致:

    运行 Token 数 帧数 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 tokens/s。 普通 numpy 引擎约为 210,stock transformers 约为 10。真实的 NES 每秒需要约 700,000 个 token。

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

限制

  • 仅支持 NROM 卡带(iNES mapper 0)。 CPU 一次只能寻址 32 KB 的程序 ROM。 更大的游戏带有 bank 切换芯片(mapper,如 MMC1 或 MMC3),用于切换 CPU 看到的 ROM 部分。nesformer 的 ROM 读取发生在模型内部,来自固定的 32 KB 表,因此目前无法跟随 bank 切换。支持 mapper 意味着将整个 ROM 存储在状态中,同时将 bank 寄存器也保存在那里,并添加一个阶段,将每个 CPU 地址转换为 ROM 地址。
  • 仅支持官方操作码。非官方操作码会发出 HALT。
  • 代码必须从卡带 ROM 运行。跳转到 RAM 会发出 HALT。
  • 无 IRQ(APU 帧 IRQ、DMC)。BRK 和 NMI 正常工作。
  • 对 I/O 寄存器的读-改-写指令不会产生 I/O 写入,$4020–$7FFF 读取为 0(无 PRG-RAM)。
  • PPU 和 APU 不在权重中。PPU 一次渲染一条扫描线,因此扫描线中间的效果 不会被复现。Sprite-0 命中与 sprite 溢出是近似的,且没有 声音。

测试

pip install pytest py65
python -m pytest -q tests

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

在 stock transformers 中运行检查点

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 在 stock transformers 中运行权重
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 是顶部展示的 Super Mario Bros. 运行
tests/test_nesformer.py 测试套件

致谢

  • Qwenputer 及其 chipformer 编译器:MIT, © 2026 Divyansh Agrawal(LICENSE.chipformer)。该许可证同样涵盖 nesformer/core.py 和 nesformer/weights.py 中改编自 Qwenputer 的代码。其最后一段提及 Qwenputer 的 CHIP-8 ROM,本仓库不包含这些内容。
  • 测试 ROM:Kevin Horton 的 nestest;Doug Fraker 的 Full Game(MIT);Damian Yerrick 的 Thwaite (GPL-3.0,源码见 pinobatch/thwaite-nes)。

许可证

nesformer 以 MIT 许可证发布(见 LICENSE)。roms/ 中的 ROM 保留其各自的 许可证,列于 roms/SOURCES.md。

来源:Hacker News · github.com