用约 1GB 的 Qwen 权重运行一台可用的 Nintendo
摘要
nesformer 是一个开源项目,通过编译器将 NES 的 6502 CPU、2KB RAM 与卡带 ROM 直接编译进一个未经训练的 Qwen3.5 模型权重中,让模型逐 token 预测 CPU 执行日志来运行《超级马力欧》等游戏,且帧输出与传统模拟器完全一致。
查看缓存全文
缓存时间: 2026/10/30 18:13
dbreunig/nesformer
来源:https://github.com/dbreunig/nesformer
nesformer
一个 NES CPU 被编译进了原版 Qwen3.5 模型的权重之中。
NES 的 6502 CPU(2A03 芯片的核心)、2 KB 工作内存以及卡带的程序 ROM,全部运行在一个标准的 Qwen3_5ForCausalLM 检查点内。没有任何人训练过这些权重——每一个权重都是由编译器计算得出的。当你玩游戏时,模型会逐个 token 预测 CPU 的执行日志。一个小的宿主程序则负责模拟主机的其余部分:绘制画面、读取手柄输入、触发 vblank 中断。
nesformer 建立在 Divyansh Agrawal (https://divagr.com/blog/qwenputer) 的 Qwenputer (https://github.com/divagr18/qwenputer) 之上,后者将 CHIP-8 和 RISC-V CPU 编译进了 Qwen3.5-4B。nesformer 复用了 Qwenputer 的编译器及其核心技巧:模型的 Gated DeltaNet 状态就是机器的内存。
| Super Mario Bros.(World 1-1) | Full Game,Doug Fraker 作品(标题画面和第一关) |
|---|---|
| Super Mario Bros.,由权重驱动 | Full Game,由权重驱动 |
这些运行中的每一个 CPU token 都来自模型。对于随附的测试游戏,每一帧都与传统 6502 模拟器的输出完全一致(参见验证方式)。
快速开始
1. 文件夹结构
将 nesformer.zip 和 nesformer-weights.zip 解压到同一文件夹中。权重文件放在 nesformer/ 包的旁边,而不是其内部:
nesformer/ <- 在这里运行命令 ├── nesformer/ <- Python 包 ├── chipformer/ <- Qwenputer 的编译器 ├── roms/ <- 测试 ROM(也可以把你的 ROM 放在这里) ├── examples/ <- 示例 GIF 和帧图片 ├── tests/ ├── LICENSE ├── LICENSE.chipformer ├── README.md ├── pyproject.toml └── weights/ <- 来自 nesformer-weights.zip ├── config.json ├── generation_config.json ├── model.safetensors ├── nesformer.json └── SHA256SUMS
CLI 会在 nesformer/ 包旁边查找 weights/ 目录。如果想把权重放在其他位置,请在 run、check 或 trace 命令中传入 --weights /path/to/weights,或向 info 命令提供路径。
要确认权重解压正确,请在顶层目录运行 shasum -a 256 -c weights/SHA256SUMS。
2. 安装
需要 Python 3.11 或更高版本。
bash python3 -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install numpy scipy numba pillow
3. 玩游戏
bash 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 命名的动图文件(此处为 thwaite.gif)。
请做好慢速运行的准备。首次运行时,numba 编译引擎约需 20 秒。之后每帧需要 1–6 秒,因此 600 帧(相当于 10 秒的 NES 游戏时间)大约需要 25 分钟。
运行你自己的 ROM
将 run 指向任何使用 NROM 卡板(iNES mapper 0)的 .nes 文件即可,例如 Super Mario Bros.、Donkey Kong 或 Excitebike。ROM 由你自行准备;nesformer 仅附带 roms/ 中三个自由许可的测试 ROM。
bash 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 在第 60 至 65 帧期间按下 Start right@200-600 从第 200 帧按住方向右直到第 600 帧 a@250-265+300-315 按两次 A right+b@200-600 同时按住方向右和 B(在 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 token/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 工作内存 | 手柄控制器;APU 寄存器(写入,但不做其他处理) |
| 32 KB PRG-ROM,存储两份:一份用于取指令,一份用于数据读取 | vblank 时的 NMI 时序,以 NMI token 的形式注入 |
OAM DMA 读取(256 字节以 DM token 的形式出现) | I/O 读取的应答,以 IN token 的形式注入 |
这种拆分沿袭了 Qwenputer 的 CHIP-8 版本,其宿主程序注入定时器滴答并根据执行轨迹绘制显示内容。宿主程序除了通过执行轨迹之外,从不接触 CPU 的寄存器或内存。
内存技巧的原理如下。每个 DeltaNet 头维护一个 128 × 128 的状态矩阵,delta 规则在每个 token 时更新它。当门保持打开且 key 为独热向量时,一次更新恰好覆盖该矩阵的一行,而独热 query 则可以将其读回。因此每一行就是一个存储单元,卡带、RAM 和寄存器都存在于这些行中。
检查点
weights/ 是一个普通的 Hugging Face 文件夹。它可以用 Qwen3_5ForCausalLM.from_pretrained 加载,无需任何自定义代码。
| 架构 | Qwen3_5ForCausalLM,采用 Qwen3.5-4B 的 DeltaNet 头形状(16 个 key 头、32 个 value 头、维度 128)和 MLP 宽度(9,216) |
| 层数 | 7 层 Gated DeltaNet(电路),加上 1 层全零 attention 层(Hugging Face 的缓存需要) |
| 隐藏维度 | 768 |
| 词表大小 | 263,808 个 token |
| 参数量 | 466.6M bf16 参数(933 MB),其中 9.3M 非零,因此权重压缩后仅 3.3 MB |
nesformer.json 记录了机器的各部分分别存放在哪一层和哪个头中。运行时将卡带、寄存器和 RAM 写入相应的 DeltaNet 状态,然后进行贪心解码。
层规划
| 层 | Attention(状态) | 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 处的 opcode 和两个操作数的字节 | opcode 解码;操作数位;索引求和;分支测试和目标 |
| 4 | — | 按寻址模式和记录阶段计算有效地址;push 存储的字节 |
| 5 | 在有效地址处的 RAM 和 PRG 数据读取 | 操作数;ADC/SBC 加法器、比较、BIT;地址解码 |
| 6 | — | 结果字节、进位、溢出、标志位、跳转目标 |
| 7 | — | 下一个 token |
执行轨迹
每条指令变成一条记录,记录的最后一个 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) <- 宿主程序注入 IN BPL $8017 J($8017) JSR $8279 PSH($FE) PSH($FD) J($8279) <- 返回地址在电路内被压栈 RTS PUL($FC) LO($7B) PUL($FD) J($827C) STA ($72),Y LO($08) HI($03) ST($00 a7=0 L2) <- 先锁存指针字节,然后存储 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 个位置。
-
原版 transformers 中的检查点(bf16,CPU)在 nestest 和 Full Game 的 2,651 个位置上预测无一失配。
-
自由运行的权重生成的轨迹与参考 CPU 完全一致:
运行 Token 数 帧数 NES 时间 结果 nestest(官方指令) 4,586 — — 完全一致,以预期的 HALT 结束 Thwaite, check700,000 271 4.5 秒 轨迹和帧画面完全一致 Thwaite, run1,916,527 600 10.0 秒 全部 300 帧保存帧均匹配 Full Game, run2,645,885 420 7.0 秒 全部 210 帧保存帧均匹配 -
空闲循环跳过不改变任何可见内容:帧画面和 I/O 写入(包括周期时间戳)与未启用跳过的运行完全一致。
速度
默认引擎从 model.safetensors 计算 Qwen3.5 的前向传播,用 numba 编译并跳过全零块。在构建机器的单个 CPU 核上,速度约为 1,650 token/s。纯 numpy 引擎约 210 token/s,原版 transformers 约 10 token/s。真实的 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 逐扫描线渲染,因此无法重现扫描线中途的效果。精灵 0 命中和精灵溢出是近似模拟的,且没有声音。
测试
bash pip install pytest py65 python -m pytest -q tests
测试需要几分钟。它们将参考 CPU 与 nestest 日志对比、电路与参考对比、自由运行的权重与参考对比,以及空闲循环跳过功能进行验证。
在原版 transformers 中运行检查点
bash pip install "torch>=2.6" "transformers>=5.22" python -m nesformer run roms/thwaite.nes --frames 3 --engine hf
nesformer/hfcheck.py 展示了完整流程:加载检查点、解码一个 padding token、将机器状态写入缓存的 DeltaNet 状态,然后逐个喂入 token。
文件
| 路径 | 内容 |
|---|---|
nesformer/spec.py | token 族和记录 |
nesformer/cpu.py | 写出执行轨迹的参考 6502 CPU |
nesformer/host.py | CPU 周围的一切:PPU、手柄控制器、NMI 时序、周期计数、空闲跳过 |
nesformer/core.py | 电路和状态快照 |
nesformer/weights.py | 将电路转换为 Qwen3_5ForCausalLM 检查点的发射器 |
nesformer/engine.py、fastengine.py | 运行权重(numpy / numba) |
nesformer/hfcheck.py | 在原版 transformers 中运行权重 |
nesformer/run.py、check.py、cli.py、__main__.py | 驱动程序、teacher-forced 检查、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 (https://github.com/divagr18/qwenputer) 及其
chipformer编译器:MIT 许可,© 2026 Divyansh Agrawal(LICENSE.chipformer)。该许可证同样适用于nesformer/core.py和nesformer/weights.py中从 Qwenputer 改编的代码。其最后一段提到的是 Qwenputer 的 CHIP-8 ROM,本仓库并未包含这些 ROM。 - 测试 ROM:nestest 由 Kevin Horton 开发;Full Game 由 Doug Fraker 开发(MIT);Thwaite 由 Damian Yerrick 开发(GPL-3.0,源码位于 pinobatch/thwaite-nes (https://github.com/pinobatch/thwaite-nes))。
许可证
nesformer 以 MIT 许可证发布(参见 LICENSE)。roms/ 中的 ROM 保留各自的许可证,列于 roms/SOURCES.md。
相似文章
dbreunig/nesformer
Drew Breunig 的 nesformer 项目将一个可运行的任天堂娱乐系统(NES)6502 CPU 模拟器嵌入 Qwen3.5-4B 的权重中,使《超级马里奥兄弟》等 NES ROM 能够在模型内部(缓慢地)运行。该项目受 Divyansh Agrawal 的 qwenputer 启发,借助 Opus 5.5 提示词构建,并使用 Christopher Pow 十年前的 nestest 一致性测试套件进行验证。
Qwenputer:在 Qwen3.5 内部运行的完整、确定性 RISC-V CPU
Qwenputer 是一个实验性的 Qwen 3.5 4B 模型,其权重中编译了一个功能完整、确定性运行的 RISC-V CPU。它利用模型自身的 Gated DeltaNet 层作为精确的 RAM,完全在前向传播过程中执行算术、C 语言和 MicroPython——无需任何工具或外部运行时。
有人在Qwen模型权重中直接构建了一台32位RISC-V CPU,使其能够在模型内部执行指令!
有人在Qwen模型权重中直接构建了一台32位RISC-V CPU,使其能够在模型内部执行指令,这可能对工具调用和计算机操作能力产生影响。
我在 Qwen 模型权重里运行了 Doom!
作者受到 qwenputer 项目启发,在 Qwen 模型权重内构建的虚拟机上成功运行了基础版 Doom,虽然速度很慢,但实现了「在 LLM 上跑 Doom」的趣味演示,并开源了项目仓库。
如果半年前你告诉我,一个运行在我办公室的本地模型能够一键生成超级马里奥的克隆游戏,我会觉得你发了疯。但Qwen3.8-27B就是这么与众不同。
作者对Qwen3.8-27B模型能在本地一键生成超级马里奥克隆游戏的能力表示震惊,并讨论了其在Framework Desktop上的性能表现,同时寻求速度优化的建议。