在 --enforce-eager 下测得 15 tok/s;对照双卡上的 torch.compile,两卡功耗对称,解码速率随上下文变化
早先的一次基准测试中,torch.compile 等了二十分钟后超时。加上
--enforce-eager 后测得约 15 tok/s,本仓库据此得出"MoE 在 ROCm 上不实用"的结论。
实际上,该模型完成引擎初始化需要二十六分钟。而在编译后,它在这台机器实测的五种架构中位居首位。
经编译后,一个 128 专家的 MoE 在 500 token 上下文下解码速率达到 107.82 tok/s,
在 32000 上为 72.80,领先这里测过的每一个稠密模型,包括参数量更大的 checkpoint。但在
--enforce-eager 下,同一模型测得约 15,而此前公开发表的判决正是以此为据。
避开 eager 模式需要付出代价:引擎启动耗时 1569 秒,其中加载权重占 54 秒。
在八核宿主机上,编译全程以单线程运行,因为 vllm/env_override.py 在 import 时
直接赋值 TORCHINDUCTOR_COMPILE_THREADS = "1"。由于这是硬编码赋值而非 setdefault,
在环境变量中 export 该变量完全不起作用。
7.2× 的吞吐损失还不是核心问题。eager 执行还制造了两个定性假象,且均被误判为架构特性: 两张卡之间 131 W / 65 W 的功耗落差,被解读为专家负载不均;而随上下文保持平坦的解码速率, 则被当成类似混合架构的扩展特性。编译之后,实测的 22 行数据在两卡间对称度处于 2.26 % 以内,两卡显存完全一致,且吞吐量在整条上下文阶梯上衰减了三分之一。
这两项错误结论此前均已在本仓库发布,随后被更正。提交给上游的修复仅有两行(见 vllm#53891 与 vllm#53892): 既保留单线程默认值,又允许通过环境变量明确覆盖。
2026-07-25 的这轮 campaign 涵盖七组配置与八次引擎启动,在五个模型及两种并行度设置下累计完成 146 次计时生成。 预热完成后,服务端在数百 token 的生成区间内统计解码吞吐量;矩阵中的每格数据均取两轮测试的均值。 测试同时按卡采样板级功耗,记录该生成窗口内的峰值,并同步记录每张卡的显存占用。
init_engine_s 是引擎初始化,而非编译时间。它涵盖权重加载、profiling、
KV cache 分配、torch.compile 以及计算图捕获。权重加载耗时单独统计,
因此图 2 能够证明两次慢启动的耗时并非花在此处。本测试未能单独剥离出 Inductor
在剩余时间中的具体占比。
benchmarks/results.jsonl 重算得出。eager 相关的测量则缺少已入库的原始记录:
它们出自更早的一次试探性运行,原始数据从未提交入库,仅作为引述文字保存在 docs/benchmarks.md 中。
本文直接从该文档提取这些数据而非重新抄录,以防二者产生分歧;但直接提取并不等同于由仓库重算,
文中的各处图表均已明确标注出处。决定了整个架构最终判决的关键数字,反而是本文中来源依据最薄弱的一个。
最初的探索性探测将该模型记录为"torch.compile 二十分钟没跑完 → 不实用 → 强制
--enforce-eager → 约 15 tok/s",并补充道"功耗严重失衡(131 W / 65 W),
MoE 无法均匀吃满两张卡"。该模型随即被归入"受制于 ROCm 的架构"之列,
仓库架构表中"🟡 一般,约 15 tok/s"这一行挂了好几个月。
这套初始推理本身并无不合理之处。编译耗时二十分钟仍未结束,在工程直觉上确实等同于陷入停滞, 而启用 eager 模式正是官方文档推荐的规避手段。真正站不住脚的是后续的推断:误将 eager 模式报出的运行表现, 当作对该架构固有性能的衡量。
benchmarks/results.jsonl。斜纹那条是同一个模型在
--enforce-eager 下的结果;它画得不一样,因为它是这张图里唯一一个无法从仓库重算的值。
排序之后,这个 MoE 在同一张榜上既在最前,也靠近最后。最下面那条混合 SSM
是另一个有两个成因的故事,
放在这里只是为了标出整条阶梯的位置。
12B 稠密模型也独立复现了同样的误判。早先的一次上下文测试同样借由 --enforce-eager
绕开编译耗时障碍,测出"平坦的 15.8 tok/s,与上下文无关,功耗在两卡间剧烈晃动"。
而经编译运行后,其速率达到 59.91,人为造成的异常平坦现象彻底消失。
编译之所以只占用单核,症结在于每次 import vllm 都会执行的一行代码:
os.environ["TORCHINDUCTOR_COMPILE_THREADS"] = "1"
代码采用了直接赋值而非 setdefault,导致外部环境变量中 export 的设定在 Inductor 读取前即被强行覆写。
无论调用方如何配置,只要 import vLLM,torch._inductor.config.compile_threads 都会被强制设为 1。
而 Inductor 原生的默认行为本应为每个核心分配一个 worker。
setdefault 的写法差异极易被忽略。
7.2× 的吞吐损失属于明显的测量缺陷,读者很容易识别并予以扣除。更深层次的错误在于,eager 模式制造了两个定性层面的伪结果,每一个都被误读为模型本身的架构属性,而非开关参数带来的副作用。
当时测得的功耗读数看似很容易用直觉解释,但该解释从根本上就是错的。在纯张量并行模式下,vLLM 会在每一张 GPU 上完整保留所有专家,并沿中间维度切分各专家的权重,因此无论路由逻辑如何分流,计算始终是对称的。 路由不均是专家并行的性质,而当时跑的不是专家并行。编译运行后功耗不对称现象彻底消失,正是对它的确认: 专家的实际执行方式并未发生任何改变。
改动仅有两行,且完全不影响默认配置:
-os.environ["TORCHINDUCTOR_COMPILE_THREADS"] = "1" +os.environ.setdefault("TORCHINDUCTOR_COMPILE_THREADS", "1")
rocm:export 的 TORCHINDUCTOR_COMPILE_THREADS 被静默覆写。vllm.env_override,得到 compile_threads == 8;不设时仍是 1。
若未打补丁,自定义 export 会被强行重置为 "1"。常有建议称 26 分钟的编译耗时可通过缓存均摊,但这次 campaign 无法证实这一点。 唯一被重复启动的仅有 TP=1 下的 12B 模型,冷启动 59.67 s、热启动 33.36 s,在一个本来就不是问题的启动上省了 1.79×。 那两个超过二十分钟的配置在这里都没有再启动第二次,所以贵的那一档热启动要多少,缺一次实测。 编译缓存在上游是真实存在的;缺的是它在这台宿主机上的一次测量。
指标 init_engine_s 综合了加载、profiling、KV 缓存分配、编译与计算图捕获。加载权重被单独分了出来,耗时极少。
剩下的未做细分,因此"编译要 26 分钟"是"引擎初始化要 26 分钟,而且不是权重"的简写。
eager 的速率和 131 W / 65 W 那次读数都没有写进任何入库文件。它们直接引自早期文档记录;
若要使其与其他实测数据具备同等置信度,需重新跑一次 --enforce-eager。
方向没有疑问;确切的倍数最多只有一位有效数字。
基准测试宿主机为 Zen 1 Threadripper,为容器分配了八个核心。编译线程参数的收益随核心数量扩展,核心更多的系统可获得成比例的更大收益。由于补丁尚待上游采纳,打补丁后的编译耗时在此处未作直接测量。
docs/architecture-notes.md 中按日期记录,而非直接覆写原始条目,
因为此前的记录本身即具对照价值。
main
分支第 105 行依旧维持原有的直接赋值。
results.jsonl 进行了严格核对。
12B 模型的编译耗时在某份文档中被记为 23 分钟,在另一份文档中记作"TP=1 下 24 分钟";
入库的值是 TP=2 下 1537.92 s,而 TP=1 只用了 59.67 s。
此前所谓"1538 s 冷、33 s 热"的数据对,实则是将 TP=2 的冷启动与 TP=1 的热启动放在了一起。
三处误差均已就地更正并标注日期,verify_doc_figures.py
现在把这次 campaign 的每一次引擎启动时间都钉住,让它们不会漂回去。
verify_doc_figures.py 始终在对照 model_meta 校验该计数,
散文是唯一一处把它重新抄了一遍的地方。
verify_doc_figures.py 现在把这三个计数钉住,图注不会再和柱子对不上。
数据来源。测试原始数据记录于
benchmarks/results.jsonl,
每次计时生成记录一行解码速率、每卡功耗与每卡显存,另有每次引擎启动一行
model_meta。本页的图由
verify_doc_figures.py
从它重算;未入库的 eager 数据从
docs/benchmarks.md §1 提取。
上游议题:vllm#53891 与 vllm#53892。 默认值取 1 的理由见 vllm#10480 与 vllm#10619。