用约 1GB 的 Qwen 权重运行一台可用的 Nintendo

Hacker News Top 工具

摘要

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 N120在 N 帧后停止
--buttons SCRIPT无手柄输入(见上文)
--show关闭在终端中绘制帧画面(需要 24 位色彩支持,例如 iTerm2 或 Windows Terminal)
--gif PATHROM 名称保存动图 GIF 的路径
--png DIR关闭将每个保留的帧保存为 DIR 目录下的 PNG。仅使用 --png 时不保存 GIF。加上 --gif 则两者都保存。
--every N1每 N 帧保留一帧
--scale N2将保存的帧放大 N 倍
--enginefastfast(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
1PC 锚点;指令计数器;A、X、Y、SP;标志位 N V Z C I D;指针锁存器;NMI 标志计数器温度计编码;SP ± 1;LO + Y
2—PC = 锚点 + 已执行字节数;PC + 1,PC + 2
3PRG 取指:在 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,0002714.5 秒轨迹和帧画面完全一致
    Thwaite,run1,916,52760010.0 秒全部 300 帧保存帧均匹配
    Full Game,run2,645,8854207.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.pytoken 族和记录
nesformer/cpu.py写出执行轨迹的参考 6502 CPU
nesformer/host.pyCPU 周围的一切: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

Simon Willison's Blog

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

Hacker News Top

Qwenputer 是一个实验性的 Qwen 3.5 4B 模型,其权重中编译了一个功能完整、确定性运行的 RISC-V CPU。它利用模型自身的 Gated DeltaNet 层作为精确的 RAM,完全在前向传播过程中执行算术、C 语言和 MicroPython——无需任何工具或外部运行时。

我在 Qwen 模型权重里运行了 Doom!

Reddit r/LocalLLaMA

作者受到 qwenputer 项目启发,在 Qwen 模型权重内构建的虚拟机上成功运行了基础版 Doom,虽然速度很慢,但实现了「在 LLM 上跑 Doom」的趣味演示,并开源了项目仓库。