把一台红白机塞进大模型的权重里,是什么体验?开发者 Drew Breunig 的新开源项目 nesformer 给出了答案:一个标准的 Qwen3.5 checkpoint,用 Qwen3_5ForCausalLM.from_pretrained 就能正常加载,不需要任何自定义代码——而仓库首页的 GIF 里,这些权重跑起来的正是《超级马里奥兄弟》1-1 关。红白机的 6502 CPU、2KB 工作内存、32KB 卡带程序 ROM,全部住在模型权重和它的内部状态里。

最反直觉的一点:这些权重没有一个经过训练。每一颗都是编译器算出来的——不是梯度下降的产物,是「编译」的产物。玩游戏时,模型逐 token 预测 CPU 的执行日志,大多数指令一个 token 搞定;宿主程序负责机器以外的一切:渲染画面、读手柄、触发 vblank 中断。

DeltaNet 的状态矩阵就是内存

这个项目站在 Qwenputer 的肩膀上:Divyansh Agrawal 先把 CHIP-8、再把 RISC-V CPU 编译进 Qwen3.5-4B,nesformer 复用了它的编译器,并把核心戏法推到红白机规模——模型的 Gated DeltaNet 状态就是机器的内存。每个 DeltaNet 头维护一个 128×128 的状态矩阵,delta 规则每个 token 更新一次;把门控常开、用 one-hot 键,一次更新就精确覆写矩阵的一行,one-hot 查询再把它读回来。于是每一行都是一个存储单元,卡带、内存、寄存器全住在这些行里。

checkpoint 的规格单很有意思:沿用 Qwen3.5-4B 的头形状(16 个 key 头、32 个 value 头、维度 128)和 9216 的 MLP 宽度,7 层 Gated DeltaNet 加 1 层全零注意力层,隐藏维 768,词表 263,808。总参数 466.6M,bf16 下占 933MB,但非零的只有 9.3M——压缩后整个权重包只有 3.3MB。七层各有分工:第一层的状态存 PC 锚点、A/X/Y 寄存器和标志位,第三层取指令,第五层读写数据,第七层吐出下一个 token。真实游戏代码平均每条指令只要 1.1-1.3 个 token。

慢 150-290 倍,但对得离谱

速度当然是玩具级的:numba 引擎在单核 CPU 上约 1650 token/s,numpy 引擎约 210,stock transformers 只有约 10——而真机需要约 70 万 token/s。宿主还会识别空转的等待循环,把 token 消耗砍掉 2.6-4.2 倍,但算下来整体仍比真机慢 150-290 倍:一帧要 1-6 秒,600 帧(10 秒的游戏时间)得跑约 25 分钟。

正确性倒是拉满。README 报告的自由运行校验里,权重生成的 trace 与参考 CPU 完全一致:测试 ROM Thwaite 跑了 1,916,527 个 token、600 帧,保存下来的 300 帧与常规 6502 模拟器逐帧一致;另一个 ROM Full Game 跑了 2,645,885 个 token、420 帧,保存的 210 帧全部对上。限制也交代得诚实:只支持 NROM 卡带(mapper 0,32KB 程序 ROM),遇到非官方指令直接 HALT,图形由宿主逐扫描线渲染、不进权重,声音则完全没有。

所以呢

这个项目真正值得记的不是「AI 会玩马里奥」——它一次都没「学会」过游戏,这些权重只是被编译成了一台机器。值得记的是它用极端方式演示了一件事:Gated DeltaNet 这类线性注意力的循环状态是一块真正可寻址的内存,delta 规则就是写指令。权重不必只能从数据里学出来,编译器也能「写」模型。对研究模型内部能承载多少结构化状态的人,这是一个漂亮的上限实验;对看热闹的人,项目 MIT 协议、目前 229 星,clone 下来就能在自己电脑上跑。

当权重可以被「编译」出来,训练就不再是造出模型的唯一路径——这大概是 nesformer 留下的又酷又扎心的问题。前身项目 Qwenputer 同样开源,值得一并收藏。