Show HN:我把 DOOM 变成了一台 Transformer
Show HN: I Turned DOOM into a Transformer
Doomformer 将整数神经网络编译进 ZDoom 地图,用 voodoo dolls 执行算术、以扇区亮度和地板高度存储结果,在 ViZDoom 中运行。
Doomformer 将整数神经网络编译进 ZDoom 地图,并在 ViZDoom 中运行。巫毒娃娃(DOOM 为额外的玩家 1 出生点生成的玩家副本)负责算术运算,扇区光照等级和平面高度保存求和结果。Python 从运行中的游戏里读回每一个暴露的值,并与 NumPy 整数参考结果比较,容差为 0。只要有一个值差一,运行就失败。它很慢:在小 MLP 上比 NumPy 慢约 23,000 倍(引擎时间;若在整个调用外层加计时器则为 65,000 倍),在默认调度下,一个手写数字在一个地图中需要 163 秒的实时 DOOM,而在 --opt 3 下需要 8 秒。
安装并运行
pip install git+https://github.com/mottopanikeiku/doomformer # Python 3.11+, Linux or macOS doomformer hello # 2 s: a small MLP, every value checked doomformer draw --digit 7 # 15 s: the marine walks a 7 and the level reads it
doomformer doctor 会检查 ViZDoom、Freedoom 和显示环境,若缺少 ACC 则构建它,并在报告成功前编译一个 ACS 探针。首次构建需要 git、cmake、一个 C 编译器以及对 github.com 的网络访问。Linux 使用动态运行时:不需要 glibc-static。在 Fedora 上:sudo dnf install git cmake gcc gcc-c++ make。Colab notebook 无需设置,但你要在浏览器画布上绘制,然后 doomformer mnist --image 将其送入 DOOM,没有陆战队员。
证明:走一个数字,DOOM 读取它
按住 FIRE 时,陆战队员在他脚下的地板单元格上涂出一个 3×3 的笔刷。READ 让关卡将 784 个像素写入网络,同一地图中的 1,355 个巫毒娃娃计算它。使用 --digit 时,Python 代替你操作:它按下陆战队员的按键并执行一次 USE,然后只推进时钟并读取。
▄▄▄▄▄▄▄▄▄▄▄▄▄▄
▄████████████████
████▀▀ ▀████▀
▀▀▀ ▄███▀
████
███
███
███
████
███▀
████
▀███
DOOM reads: 7 (right) -- the winner's doll lit it on the 7-segment display
EXACT: all 248 values DOOM computed [...] equal the NumPy integer reference (tolerance 0)
READ pass: 273 game tics = 8 s of real-time DOOM at 35 tics/s, 3 s wall [...]
在 [...] 处节选;docs/commands.md 有完整输出。draw 用 --opt 3 构建其地图,因此一次 READ 需要 273 个游戏 tic;在 --opt 0(原始调度)下,需要 5,705 个(163 秒的实时 DOOM)。没有 --digit 时,该命令会打开一个窗口,你自己行走和绘制;DOOM 随后实时运行。
娃娃们计算 784 个像素 → 64 个隐藏单元 → 45 次成对分数比较 → 10 个胜出计数。只有赢得全部九次比较的数字才能让一个娃娃通过,而那个娃娃穿过的线会启动关卡脚本,在显示器上点亮该数字。Python 读回 784 个像素、隐藏层、每次比较和答案,并检查全部 248 个值(119 个值,每个都与其取反的孪生值一起读取,外加 10 个输出)。
EXACT 意味着 DOOM 等于整数网络。它并不说明该数字是否正确。在前 100 个 MNIST 测试数字上,DOOM 正确读取 99 个,从全部 10,000 个中采样的 1,000 个中正确读取 966 个;每个结果都是精确的。整数网络在全部 10,000 个上得分为 97.16%(浮点模型得分为 97.58%)。30 个走出的数字(0 到 9,每个有三个脚本化笔画变体)在 --opt 3 下全部读取正确且精确,而且网络也在该笔画分布上训练过:此演示的第一个版本是一个仅在 MNIST 上训练的 784-32-10 网络,对此类笔画的读取率仅为 41.08%。通过真实的 X 按键事件,一个绘制的 7 在默认 --opt 3 下读取精确;在 --opt 0 下用更早的网络尝试了一个 7 和一个 1。详情 · 影片
博物馆地图:点亮一个数字,重新连接一条连接
doomformer museum build 写道 museum.wad,这是一张无需 Python 即可游玩的地图。你踩上七个地板垫来点亮一个数字,然后按下 READ。136 个巫毒娃娃计算一个小型手写网络:每个数字的得分统计与其一致的线段,获胜者的娃娃点亮显示器。MUTATE 在运行中的关卡内重新连接该网络的一条连接:线段 a 到数字 7 从 +1 变为 -1,通过改变其两条权重线的 specials 实现。同一个 7 随后读作 1,RESTORE 将其恢复。HUD 和记分板在每次 READ 后显示十个得分,一个摄像机站点显示数字 7 的第六个得分娃娃:它在 MUTATE 之前通过其门,之后停在门前。
$ doomformer museum verify
7 pads abc w_7a +1 DOOM reads 7 scores [4, 6, 3, 5, 4, 3, 2, 7, 3, 4] EXACT (148 values; 744 tics; [...])
7 after MUTATE pads abc w_7a -1 DOOM reads 1 scores [4, 6, 3, 5, 4, 3, 2, 5, 3, 4] EXACT (148 values; 744 tics; [...])
7 after RESTORE pads abc w_7a +1 DOOM reads 7 scores [4, 6, 3, 5, 4, 3, 2, 7, 3, 4] EXACT (148 values; 744 tics; [...])
这里 Python 发挥作用:它按下 marine 的按键和 USE,并在每次 READ 期间只读取,使用受保护的读数。它将全部 148 个值与当时接线状态下网络的 NumPy 参考进行比较。在不运行 DOOM 的情况下,静态提取器还显示重新接线后的地图的网表正是恰好改变了一个权重的网络。我只在 ViZDoom 中运行过整张地图。其自测地图读取数字 0 到 9,然后读取 MUTATE 之后和 RESTORE 之后的 7,无需玩家。在 GitHub runner 上,它记录 GZDoom 4.14.2、LZDoom 3.88b、UZDoom 5.0.3 和 ZDoom 2.8.1 中的精确值,而在 Zandronum 3.2.1 中仅当每个扇区标签低于 32,768 时才记录。详情
检查内容
Python 从运行中的游戏读取扇区光照水平以及地板和天花板高度,并与同一网络的独立 NumPy 整数实现进行比较。检查是在已进行的运行上的整数相等性,并不说明未运行的输入。测试对其发起攻击:在一个小型编译地图中将一条权重线从 5 提高到 6,必须在恰好那些娃娃越过该线的输入上恰好失败 +1,而回显 Python 写入值的读数必须失败(pytest tests/test_tamper.py,三个测试,约 15 秒)。
回读默认受保护:在每个页面之前,Python 毒化并检查其 USER 槽位,然后要求页面替换它们。丢弃页面不能静默重用重复输入的答案或上一页的值。guard=False 是一个显式的单地图历史退出选项。受保护构建具有新哈希(ACS 和元数据,而非几何或算术);哈希记录解释了这一变化。
doomformer fuzz --nets 40 比较了在 DOOM 中运行的 40 个随机网络的每个值(306 次通过,0 次不匹配,使用 --workers 2 约 10 秒;发现了一个植入的编译器 bug)。fuzz --edge 将权重、偏置和输入置于范围极限:15,000 个网络在十个新种子上,在 --opt 0 处、在 --opt 3 处以及作为瓦片地图,进行了 113,605 次通过并比较了 1,987,632 个值,无不匹配(范围检查拒绝了它抽取的 47,686 个输入,约 30%),另外 3,000 个使用生成器扩展到下方边界附近的平面起点(22,682 次通过),还有 2,000 个也从 15,000 到 16,000 抽取起点(14,782 次通过),全部无不匹配。在调度修复之后,早期的活动运行了 2,000 个随机 MLP(15,374 次通过,276,594 个值)、300 个脉冲网络和 100 个随机 transformer,覆盖所有 8 个注意力、归一化和 MLP 变体(运行了 99 个;一个全零矩阵不受支持);200 个脉冲 --edge 网络进行了 12,794 次通过,无不匹配。fuzz --edge 不抽取 transformer。CI 在每次推送时用新的、打印的种子对 60 个网络进行模糊测试,并且每周一进行更长时间的活动。模糊测试器和独立运行在此仓库自己的地图中发现了四个故障:两个调度 bug 和两个平面限制。从零开始超过 14,018 个单位的平面移动会使其移动器忙碌三个 tic,并丢弃该平面上的下一个请求,而从 16,000 以上开始的平面在第一次通过时精确,在后续通过时错误,因此 DOOM 在 net.check 已接受的网络上与参考不一致。check 和 compile 现在拒绝它们:平面在 [-15,000, 16,000] 内开始,并在 ±14,018 内移动(doomformer verify --plane-sweep,docs/verification.md)。
doomformer verify 运行小型 MLP 的每个输入类(在同一页面中陈述的假设下,这些类在 20 个网络中的 20 个上与每个输入一致)。df.coverage 计算比较的内容:在 MNIST 上所有 248 个暴露值,即参考定义的 129 个值和 119 个孪生值,当丢弃一个读出名称时计数下降。该地图不暴露同一网络的 903 个阶梯计数和 1,351 个门比较;它们通过它们馈入的和来检查。doomformer verify --extract net.wad --net net.npz 从编译的地图中读回网络而不启动 DOOM,并将每个权重、偏置和阈值与源网络比较:发布的 MNIST 网络(784-64-45-10-10)仅从其地图在 --opt 0 到 3 处返回相等,490 个随机网络中的 484 个在 --opt 0 处也是如此(其他 6 个相等但违反平面契约),并且在 --opt 1 到 3 的每个处 199 个中的 199 个也是如此。EQUAL 覆盖算术网表、平面契约、触发和重复标志、移动器速度、重置脚本和读出页面;时序和几何打印为 NOT CHECKED。更改的权重行显示为恰好该权重。在 --opt 0 处对小地图进行 23,622 次单次编辑中,提取器检测到 17,906 次,而在 DOOM 中对 12 个输入运行的 98 次未检测到的编辑都是精确的。它显示地图所说的内容;DOOM 按读取运行地图是精确性检查所测试的。一个变异测试工具更改编译地图的一行:在运行的 1,004 个站点(共 5,578 个)中,297 个被杀死,其中包括所有重量、符号和目标类。在 707 个幸存者中,590 个匹配一个规则(一个冗余滚动器,一个前向传递永远不需要的冻结行),聚合控制支持该规则,117 个是输入缺口(只有某些输入暴露的行走地板);没有一个被证明等价。doomformer lint --fuzz 在 5,397 个地图上将静态调度检查器与 DOOM 比较:0 个假阴性。该方法是翻译验证(Pnueli、Siegel 和 Singerman,1998):对每次编译进行测试,而不是证明编译器是正确的。
谁做什么:
- 玩偶执行每一次乘加、激活和比较。ACS 层脚本寻址单元格、移动输入门、倒回玩偶、感知世界并复制结果。网络中没有任何算术在 ACS 中运行。工具包的
df.Agent是唯一的例外:它在 ACS 中取 argmax(docs/agents.md)。 mnist:Python 写入 784 个像素端口并判断玩偶何时停止;玩偶挑选数字。demo和ask:Python 写入 token id 并判断玩偶何时停止,从 DOOM 计算出的 logits 中采样每个字母,并且当一个模型被切分到多个地图时,将整数从一个游戏复制到下一个游戏。demo --one-map是一个地图,层与层之间没有 Python;Python 写入 token 并在每一轮之后采样字母。draw,他的大脑就是关卡:Python 只推进时钟并运行读出脚本(在draw中,从 READ 开始)。关卡提供输入(784 个像素、感知到的 token),玩偶挑选数字或动作。demo --one-map --loop:Python 只推进时钟并运行读出脚本。玩偶挑选每个字母并将其写入下一个位置。- 苍蝇循环:Python 只推进时钟并运行读出脚本,在此之前会在一个单独的、由它写入的游戏上校准步进周期。关卡绘制输入脉冲,输出脉冲触发推力线。
--opt 3(draw的默认值;在其他地方是--opt 0,即带有受保护回读的原始调度):ACS 脚本将每一层的光照水平复制到下一层的门高度。它不做任何加法或比较,玩偶仍然执行求和、阈值、比较和选择,但“层与层之间没有 ACS”仅在--opt 0到 2 时成立。doomformer machine:玩偶运行程序计数器和寄存器。默认情况下,循环、倒回和停止检查是一个 ACS 脚本,存储的程序在第一个 tic 之前由宿主加载。使用--clock dolls时,一个循环玩偶和行特殊功能会重启、倒回并停止关卡,ACS 只读取值并检查玩偶是否已停止(docs/machine.md)。learn digits:Python 写入每个示例的像素和标签,推进时钟并读取;玩偶计算分数、argmax 和权重更新(docs/learning.md)。python -m demos.brainlevel --lesion-at N或--lesion-when near|ahead:关卡自身的时钟在决策 N 之前关闭受损单元的门,或者在它感知到的 token 首次满足条件时关闭;Python 只读取。
在 draw 中,他的大脑、循环、苍蝇和机器运行一个包装器(ReadoutOnly),使任何其他调用都会引发异常,并且命令会被计数:无头 draw 运行中为 58,他的大脑的电影运行中为 710,损伤运行中为 154,其他为 0。
这些地图使用 ZDoom 的 UDMF 格式和 ACS,因此它们需要 ZDoom(这里是 ViZDoom 1.3.2 配合 Freedoom 资源),而不是 1993 年的可执行文件。net.quantization_report(x) 衡量整数模型与其来源浮点模型的差距。EXACT 不衡量。
工作原理
docs/HOW_IT_WORKS.md 将网络的每一部分映射到地图的每一部分:一个值映射到一组玩偶,一个权重映射到一条线,一个激活映射到一段楼梯,注意力映射到一条天花板门走廊,消融映射到一扇门。
引擎存在尖锐的边界,编译器则绕开它们。平面移动器在同一平面上拒绝第二个请求,只要第一个请求仍在移动,后一个请求就会丢失。传送线会取消玩偶在该 tic 中先前穿过的线。距离 BSP 节点超过 32,767 单位的物体可能被归入错误的扇区,因此每张地图都适配 32,000 单位。这些是在 ViZDoom 1.3.2 中测得的 ZDoom 2.8.1 行为。32,000 单位规则在 Linux 上是必需的;在 macOS arm64 上,探测地图中未发生回绕(原因未调查),但该规则在所有平台上都保留。导出的探测地图和 hello 大脑也在原版 ZDoom 2.8.1(53 行中 53 行匹配)、Zandronum 3.2.1(53 行中 50 行:它无法找到高于 32,767 的扇区标签)以及 GZDoom 4.14.2、LZDoom 和 UZDoom(53 行中 44 行:那里的平面是双精度浮点数,且 UDMF 解析器拒绝超出 ±32,768 的坐标;坐标回绕仍然存在)中运行。hello 大脑记录的值在 ZDoom、GZDoom、LZDoom 和 UZDoom 中与 NumPy 参考值相等,在 Zandronum 中一旦其标签移到 32,768 以下也相等。玩偶时钟计数器机器(multiply 7 9,464 步,235,567 tics)和一个 SUBLEQ 加法也在 GZDoom、LZDoom 和 UZDoom 中运行,每一步的 pc 和寄存器都与解释器的日志相等;ZDoom 2.8.1 和 Zandronum(标签低于 32,768)精确运行了一个 50 步乘法和 SUBLEQ 加法,而 Zandronum 在默认标签下于第 1 步停滞(其他端口)。doomformer facts 重新运行 28 个探测(55 个用例),每行显示 PASS 或 FAIL(约 10 秒),引擎事实 列出了每项发现及其背后的探测或测试。
DOOM 地图内的计算机已经存在。counter.wad(2006)是一个由巫毒玩偶和传送带构成的 4 位计数器,conway.wad 在 Boom 地图中运行生命游戏,而 Danny Spencer 的 DOOM 计算器 是一台加法机。Doomformer 增加了一个从整数网络到 ZDoom 地图的编译器、一份测得的会破坏精确性的引擎行为列表,以及一个针对 NumPy 参考的容差为 0 的检查。docs/related_work.md 列出了打开了哪些工作以及每个条目共享和不共享的内容。
命令
| 命令 | DOOM 做什么 | 时间 | 技能等级 |
|---|---|---|---|
doomformer hello |
一个小型 MLP,5 个随机输入 | 2 秒 | 我是年轻得死不了 |
doomformer draw --digit 7 |
海军陆战队员走一个 7,关卡读取它 | 15 秒 | |
doomformer mnist |
读取 4 个测试数字,或你自己的图像 | 1.5 到 3 分钟 | 嘿,没那么粗糙 |
doomformer demo |
一个 transformer 写出关卡名称 | 15 到 60 秒 | 狠狠揍我 |
doomformer demo --nightmare |
2 层模型,106 张瓦片地图 | 3.5 到 7 分钟 | 噩梦! |
doomformer ask "what does iddqd do" |
从 189 条 DOOM 事实中回答 | 约 11 分钟 | 极度暴力 |
doomformer machine subleq all |
一台 SUBLEQ 机器在一张地图上运行三个程序,其中一个会自我重写 | 约 5 分钟 | |
doomformer learn digits --classes 4 --steps 300 |
一个空白感知机在一张地图内学习 8×8 数字 | 约 5 分钟 |
hello 运行一个 4-6-5-3 整数 MLP,demo 是一个在 136 个官方关卡名称上训练的 1 层 transformer,这些名称被切成 38 张瓦片地图,--nightmare 是 2 层模型(56,390 个玩偶),ask 是一个在 189 条 DOOM 事实上训练的 2 层 transformer。docs/commands.md 包含每个命令和变体及示例输出:mnist --image 和 --tiles、demo --softmax、--model doomnames_modern、--one-map、--loop、--opt、fuzz、verify、lint、tune、facts、machine、learn。
时间来自一台空闲的 16 核 arm64 Linux 机器,同时运行最多 8 个游戏;hello 和 draw 使用一个游戏。在 2 个游戏的情况下,demo 在一台繁忙机器上耗时约 1 分钟,demo --softmax 约 3.5 分钟,而 ask 在 3 个游戏下耗时 20 分钟。
- “不在其训练名称中”是个很低的标准:2 层模型在温度 200 下 30 个未选中样本中有 29 个是新的,而默认的
demo在 10 个种子中有 6 个写出了其 136 个训练名称之一(输出会说明是哪个)。默认种子(20)被选中。 - DEMON CANYON,即
--nightmare写出的名称,是手工挑选的样本(种子 2,温度 150;此前尝试了多少个种子未记录)。2 层模型在 12,934 个清理后的关卡名称中的 11,341 个上训练。 ask对其训练释义的正确率为 94.7%,对相同事实的新表述的正确率为 35.4%。“what does iddqd do” 是一个训练问题,回答 GOD MODE。一个超出 189 个事实的问题会得到一个精确但无意义的答案。
更大的实验
他的大脑就是关卡。 一个关卡脚本感知最近的恶魔,1,345 个海军陆战队员的巫毒娃娃运行他的策略 transformer 并选择动作,关卡转动他并扣动扳机。Python 推进时钟并读取。50 个决策全部精确(种子 4)。一个决策需要 4,338 个游戏 tick(124 秒实时 DOOM);使用 --opt 3 时,同样的 50 个决策选择同样的 50 个动作,全部精确,每个耗时 774 tick(22 秒)。在那次运行中,他的娃娃在第一个恶魔很远时转动了 21 个决策,在决策 22 首次选择 FIRE,总共选择了五次;五个恶魔死亡。该策略模仿一个能看到精确恶魔位置的脚本化自瞄。恶魔在他思考时被冻结,而当它们清醒时(--live,见下文),他在 5.5 秒后死亡,0 击杀,这几乎是竞技场强制的;影片对思考过程进行了延时。
影片 · 代码
从克隆运行实验(pip install -e .,然后 python -m demos.<name>)。demos/README.md 列出了每条命令、其耗时以及它打印的内容:
python -m demos.brainlevel:约 4 分钟内 5 个决策;--decisions 50 --seed 4是 50 个全中的运行,约 31 分钟;使用--opt 3时,50 个决策在负载机器上约需 5 分钟。python -m demos.brainlevel --seed 2 --decisions 12 --opt 3 --lesion 'x0:0;L0.ao:7;L0.x1:7;L0.m:0;L0.x2:0' --lesion-at 3:从决策 3 开始,关卡的时钟关闭 5 个单位的门,海军陆战队员转向健康大脑开火的位置;12 个决策全部精确,约 2 分钟。python -m demos.brainlevel --live --lives 3:恶魔在他的娃娃计算时保持清醒;关卡每个决策采样一次传感器,动作在 773 tick 后落地。在 90 条生命(种子 4、5、6,每个决策都精确)中,他 0 击杀,并在中位数 191 tick(5.5 秒)后死亡,在他的第一个动作落地之前;同样的竞技场冻结时每条命 22 击杀。在这个竞技场中他无法移动,第一个恶魔伤害在中位数 128 tick 后到达,因此任何等待 773 tick 的行动者都会先死:同一策略的 Python 副本在相同延迟下也是 0 击杀,而在 3 tick 延迟下为 3.3。有信息量的部分是扫描(python -m demos.brainlive sweep):每 4 tick 一个新动作,他在 3 tick 旧感知下保持 79% 的冻结击杀,6 tick 时为 36%,12 tick 时为 19%。python -m demos.aimlearn:约 3 分钟内 2 条命;--lives 5 --seed 4约需 11 分钟。python -m demos.flyloop:约 3 分钟内 40 步;--steps 900运行到第 169 步结束,约 13 分钟。python -m demos.lobotomy:DOOM 中的损伤扫描和 3 个决策,约 1.5 分钟。
展示片涵盖了所有这些。docs/DEMOS.md 包含数字、每项检查的范围以及所选种子列表,docs/FLY.md 涵盖果蝇。
构建你自己的
将网络编译进地图,让其输出打开门、闪烁灯光或推动陆战队员。按原样粘贴即可运行:
import numpy as np import doomformer as df # 2 inputs -> [a AND b, a OR b] -> 2 outputs, all integers net = df.MLP( weights=[np.array([[1, 1], [1, 1]]), np.array([[1, 0], [0, 1]])], biases=[np.array([-1, 0]), np.array([0, 0])], thresholds=[np.array([1]), np.array([1])], signed_input=False, ) with df.compile(net) as brain: for x in ([0, 0], [1, 0], [1, 1]): # computed by voodoo dolls; raises df.MismatchError on any difference from net.reference print(x, "->", brain(np.array(x)).tolist()) brain.record([np.array([0, 1]), np.array([1, 1])], "dolls.gif") # film the dolls doing it
df.MLP.from_torch 量化一个 PyTorch 模型(示例 02),而 world 和
actions 钩子将房间放入地图,并将门、灯光和推力附加到输出(示例
03)。同一 API 接受脉冲网络、transformer、Conv1d 和 Conv2d,以及在地图内更新权重的学习器;mode="tiles" 将模型分散到一个游戏池上(doomformer plan 告诉你属于哪种情况)。指南是 docs/BUILD_YOUR_OWN.md,还有
十二个单文件示例。构建了奇怪的东西?展示和
讲述。
同时运行多个游戏
一个地图就是一个游戏,一个游戏使用一个核心。df.run_many 将每个输入交给下一个空闲游戏,检查每一轮
并按顺序返回输出,可在你的机器上或你启动的 Ray 集群上运行。不托管任何内容。在单台 16 核机器上,14 个游戏中的 1,000 个 MNIST 测试
数字耗时 2 小时:966 个读取正确,全部 1,000 个精确,5,696,000 个游戏 tic 或 45
小时实时 DOOM(models/mnist/doom_1000.json)。
Tiles 是多地图设计:四张 tile 地图,每层一张,驻留在游戏中,由 Python 将层值从一个游戏复制到下一个游戏。doomformer mnist --tiles --all --workers 6 在 1,390 秒内读取全部 10,000 个测试数字:10,000 个中 10,000 个精确(每个 248 个值),9,716 个正确(97.16%),DOOM 的 one-hot 在每个数字上都与 NumPy 相等(models/mnist/doom_tiles_10000.json)。一个数字需要 216 个游戏 tic 和 0.83 秒,而单地图中需要 5,696 个 tic 和约 104 秒。doomformer plan 预测秒数(在 11 个留出的 K3 和 K8 网络上,单地图的 rms 误差为 13%,tiles 为 22%;在其他 K 的网络上,实测秒数比预测高 0 到 40%,MNIST 在 --opt 0 处高 21%),打印带有误差带的游戏 tic(MNIST 在 --opt 3 处为 276 ± 7.2%,实测 272)并推荐一个。doomformer bench tiles 在 1 个 worker 下以每分钟 81 个数字读取 MNIST,16 个 worker 下为 880 个(稳态;游戏启动时为 79 和 552),每一遍都精确,每个数字的引擎 CPU 时间在 1 个 worker 下为 0.72 秒,16 个 worker 下为 0.92 秒(100 个交付数字,两次;16 核)。
doomformer mnist --tiles --all --workers 6 # all 10,000 digits as tile maps, about 23 min pip install "doomformer[ray]" ray start --head # other machines: ray start --address=HEAD:6379 doomformer mnist --all --ray --record mnist.json # all 10,000 digits, one map each, on a cluster
每个游戏预算一个核心和约 1.3 GB 内存(实测峰值 1.24 GB)。docs/scaling.md 中有测量数据。
它还做什么
- 调度。
--opt 1、2和3(df.compile(net, opt=N))缩短了单地图调度:doomnames_onemap从 3,488 个游戏 tic 降至 1,512、1,128 和 544,MNIST 在--opt 3处从 5,696 降至 272(使用最终常量时 100 个中 100 个精确、99 个正确,最后一次重新调优前为 300 个中 300 个),DrawPad READ 从 5,705 降至 273(30 个中 30 个行走数字正确且精确)。--opt 2在 MNIST 上运行了 76 个数字(3,560 个 tic)。--opt 3处的 ACS 副本位于“谁做什么”下。doomformer lint WAD标记可以共享一个 tic 的平面移动(在编译器交付时的 5,397 个模糊测试地图上 0 个假阴性;恢复一个旧的屏障高度 bug 后,它捕获了 67 个失败地图中的 65 个,在其模型学会了它之前遗漏的首 tic 停止后,在相同网络上捕获了 66 个中的 66 个),doomformer lint --net MODEL还检查平面边界,doomformer tune测量时序边缘(docs/scheduling.md)。 - 一台计数器机和两台存储程序机。
doomformer machine multiply 7 9是一台 ROM 计数器机(程序就是地图的几何结构):它在 464 步后打印 63(39 个娃娃),程序计数器和每个寄存器在每一步都被检查;fib 20、gcd、primes和pow2 40也运行。--clock dolls取出 ACS 循环脚本(一个循环娃娃和行特殊项重新启动并倒回关卡),--blind只在最后读取一次。doomformer machine subleq all运行一台 SUBLEQ 机器,具有 18 个内存单元(-64 到 63),操作数作为地址,程序可以重写自己的指令:加法、7 × 9 = 63 和一个自修改求和在一张地图上运行(一个 sha256),每步 1,624 个游戏 tic,每一步都与 Python 解释器在容差 0 下比较。更改 ROM 或解码的一行会使运行在预测的步骤处发散。一个程序如果写入它正在执行的指令的三个单元,就会在地图中设置一个违规锁存器,在该步骤停止关卡(--latch;400 个随机程序中有此行为的全部 28 个都在解释器的步骤处被标记);没有它,地图会在那里计算出一个错误的值。一个 103 步的循环在两个时钟上都精确运行,而冒泡排序无法放入 18 个单元。内存和寄存器是有界的平面高度,因此这些是有限状态机,并不声称 DOOM 是图灵完备的;普通 SUBLEQ 在无界内存下是通用的,对于这种受限形式没有给出通用性论证(docs/machine.md)。demo --one-map --loop --until-eos让文本结束时的获胜者停止循环。 - 从空白开始的学习器。
doomformer learn digits --steps 1500:一个10类感知机,370个权重全为0,在一张地图内学习8×8数字。权重是天花板高度;玩偶计算分数、argmax和更新,Python写入每个样本的像素和标签。在1,500步的每一步之后,全部455个暴露值(包括每个权重)都与NumPy孪生体在容差0下相等(在负载机器上约80分钟)。在500个数字上,留出准确率从10%升至71.2%;不带权重钳位的教科书感知机在同一数据流上达到81.0%。在20个NumPy数据流上(种子0到19,其中一个是DOOM流),带钳位的孪生体平均为74.8% ± 5.3。前向通道有6个门,因此一个分数只能看到[-3, 3]范围内的权重,更大的权重会继续移动而不被看到;这在该数据流上损失约10个点,平均损失3.6个点。更新规则和标签由Python负责(docs/learning.md)。 - 量化。
df.MLP.from_torch(model, calib_x, levels=4, calibrate="mse")还折叠BatchNorm,降低AvgPool和MaxPool(MaxPool通过比较实现,在阶梯计数上精确),并将GELU和SiLU视为阶梯函数;df.qat.fit加入量化感知微调。在8×8数字、4个级别下,深度6的MLP从最大校准的88.9%提升到MSE校准的93.7%(浮点95.9%),CNN从92.7%提升到95.1%(3个种子的均值,models/quant/EVAL.md);在ReLU Linear和Conv网络上,QAT并不比MSE校准更好,而在有多个max pool或GELU的网络上则领先(BatchNorm + 2 MaxPool CNN:2个级别时+4.6个点,4个级别时+0.5个点)。降低后的网络在DOOM中对每个5个数字精确运行。df.Transformer.from_torch降低一个用阶梯配方训练的torch_models.GPT:一个24k参数的级别名称模型在困惑度上比其浮点阶段差5.6%(7.75对7.34),在DOOM中对3个留出名称精确运行,并落后于5-gram基线(5.84)。在第二个语料库上(Python参考散文,训练中不存在的窗口,一个种子),它比其浮点阶段差6%,比相同步数的普通浮点GPT差17%,比其5-gram差34%。它不能转换在其他地方训练的transformer。 - 一个学会了算法的模型。
models/induction,一个2层整数transformer,在随机token上的归纳任务上训练,这是任何计数表都无法解决的。留出准确率为0.9995,而相同大小的浮点GPT为0.9991,bigram表为0.069,4-gram表为0.233;在训练中从未见过的对上,它为0.9995,而表为0.000。python -m demos.induction --heldout 25在DOOM中运行25个序列:25个全部精确,520,749个值。每行一个种子,一个d=16 ReLU K=4的运行未能学习(日志保留)。 - 干预。
intref.forward(interventions=...)和 DOOM 运行会在某个站点、位置和头上修补一个值(置零、设置、取均值、重采样、缩放、添加或移除一个方向),并将每个值与同样修补的参考进行比较:doomplayer的 24 行激活修补中有 24 行、归纳模型的 40 次修补运行中有 40 次(每种 10 对)在 DOOM 中精确运行。在瓦片模式下,Python 读取玩偶计算出的每个预激活值,并将修补后的值写入下一个瓦片的端口,玩偶计算所有下游内容;精确性并不检查修补值是否正确。修补恢复了手工构建的 1 层模型的植入头,(b + c) mod 4,在 384 对上 0 分歧;在有一个备份头的情况下,单次修补以 0.5 和 0.5 分裂,并且仍有两个电路;而在某些上下文中降低 logit 差异的头,均值比会掩盖它,因此电路搜索也报告逐上下文一致性(OR 和 AND 冗余对单细胞修补来说看起来相同)。修补整个残差或在读取位置修补整个站点的行会被标记并从主要数字中排除。沿探针方向引导doomplayer会改变 86 个上下文中的 39 个;它优于随机、数据跨度和随机上下文方向,但不优于打乱标签的探针(其中打乱有足够多的不同方向)或“触发 vs 不触发”探针。归纳模型中的“无紧凑电路”对每个位置的均值消融都成立;以损坏运行为基线,6 个组件中有 4 个在答案对上恢复 0.991。病变统计纠正了上述演示:DEMON DEATH 不是选择性病变,因为 416 个单神经元中有 355 个改变了该样本(docs/interventions.md)。
限制
- DOOM 很慢。一次传递相对于 NumPy 参考(14 到 48 µs)的引擎时间对于 12→16→6 MLP 约为 23,000 倍,对于单张地图中的 MNIST 为 150 万倍(引擎 CPU 73.2 s),在
--opt 3处为 60,000 倍(2.84 s),对于瓦片中的 MNIST 约为 15,000 倍(docs/scaling.md)。单地图传递的成本与空闲地图的成本相同:传送带滚动器就是--opt 3处 MNIST 的全部 2.84 s(doomformer bench idle)。更少的滚动器没有帮助:使用一半时,游戏 tick 翻倍,一次传递的成本增加 1.8 倍,而每个扇区一个时,传递是错误的(doomformer bench stack),因此每次传递 0.5 s 需要更改引擎。在整个调用周围使用计时器(写入输入、运行、读回),12→16→6 MLP 为 726 ms 对 11.1 µs,约 65,000 倍(30 次运行,Speed),而 fly-circuit 步骤约为 73,000 倍。 - 注意力是整数注意力:大多数模型中是 ReLU 风格,三个模型中由玩偶计算 softmax。没有模型有 LayerNorm(
doomnames_modern有整数 RMSNorm),也没有一个是可直接替换的浮点 transformer。Transformer 在 PyTorch 中离线训练,使用量化感知训练(training/);MLP.from_torch转换 MLP 和 CNN,Transformer.from_torch仅转换用该配方训练的torch_models.GPT模块。 - 编译器在 ViZDoom 1.3.2 中验证。探针地图、
hello大脑和玩偶时钟计数器机器在其他引擎(原版 ZDoom、Zandronum、GZDoom、LZDoom、UZDoom)中运行;没有比hello更大的网络运行过,并且在 GZDoom 系列中平面算术不同,因此此仓库的平面限制是 ZDoom 的。编译器发出高于 32,767 的扇区标签,Zandronum 无法找到。 - 检查覆盖了已运行的网络和输入。模糊测试器不覆盖学习地图、超过 4,096 个对象或瓦片模式下的 transformer(Not covered)。
--one-map运行的模型比demo更小(1 层,d=8,6 个位置),因为 136 个名称的模型需要 116,968 个 dolls,无法放进一张地图。- 该 marine 的策略模仿一个能看到精确位置的脚本化 aimbot。Python 将其分箱为 86 种可能的观测,并提供标签。没有奖励驱动的强化学习,也没有从像素中学习。
- 展示的种子、病灶和名称是挑选出来的。种子与选择 列出了每一项选择。
盒子里有什么
| 路径 | 内容 |
|---|---|
doomformer/ |
工具包:地图编译器、DOOM 运行时、精确检查、CLI |
demos/ |
研究代码:他的大脑就是关卡、行走器和关卡生成器、飞行回路、学习竞技场 |
training/ |
对每个模型进行量化感知训练,附带语料库及其来源 |
models/ |
每个训练好的模型及其训练日志和评估 |
examples/ |
十二个单文件示例,大多数在 15 秒以内 |
tests/ |
安装 torch 后运行 479 个测试(不安装则更少),其中 175 个在真实无头 DOOM 上运行(4 个仅在 DOOMFORMER_HEAVY=1 下运行或需要数分钟);CI 在 Linux 和 macOS 上运行测试套件 |
docs/ |
工作原理、每个演示及其数字、引擎事实、工具包指南、每个命令及示例输出 |
CHANGELOG.md |
每个版本新增了什么 |
许可
代码和文档:MIT。数据保留其自身许可(THIRD_PARTY.md):关卡名称列表来自 Doom Wiki(CC BY-SA 4.0)和 id Software 的源代码(GPL-2.0),100 个 MNIST 样本数字为 CC BY-SA 3.0,models/fly 中的果蝇回路源自 FlyWire 连接组(CC BY-NC 4.0,非商业)。Freedoom 图形为 BSD 许可。DOOM 是 id Software / ZeniMax 的商标;本项目与其无关联。
来源:Hacker News · github.com













