dual-radeon-vllm · 实测长文

两张消费级 Radeon RX 7900 XT,
在 ROCm 与 vLLM 下实测

这里的长篇记录出自一个实测仓库,所有测量均在 ROCm 与 vLLM 下的 2× RX 7900 XT(gfx1100,RDNA3)上完成。 每篇文章所用的图表,均取自仓库负责校验的同一份已入库数据,并在页面上直接注明哪些数字可以重算、哪些不能。

2× RX 7900 XT · gfx1100 · TP=2 VFIO 直通 · 无 P2P · 跨 die PCIe 3.0 仓库

1这台机器能跑多快

图表为每台机器上的每个模型各画一条线。每条线反映的都是该模型实测中最快的配置,而非统一的基线。Radeon 这边六条里 有四条出自同一场 campaign、同一套栈,彼此可以直接比;另外两条来自更晚的栈,因为在这两个模型上,那场 campaign 已经不再是这台机器能达到的最佳表现。A100 包含同样的五个模型,出自它自己单一下午跑完的单场 campaign。每条线都标明了自己运行的软件栈。Radeon 的候选是 ledger 里 2026-07-25 至 2026-08-29 的 TP=2 序列;这对卡九月的阶梯超过 32 000,属于图 3 与图 4。

图 1 · batch 1 的解码速率,每个模型在 ledger 候选中取最快的配置 可由仓库重算
比较

图 1. 批量 1 的解码速率对上下文长度,横轴取对数。默认视图是 2× RX 7900 XT、 张量并行 2 上的所有模型;图上方的开关把问题掉个头,变成一个模型在每台机器上,颜色也跟着问题走: 一种视图里是模型,另一种里是卡。

十一台机器
这一对卡;其中一张;Colab 上的一张 A100 80G、一张 L4 24G、一张 Tesla T4 16G;以及 2026-09-03 租用的六种 —— 一张 H100、H200、B300、RTX PRO 6000,和 TP=2 的两张 H100、两张 PRO 6000。机器之间共用的是 checkpoint —— CUDA 卡全是 vLLM 0.28.0,这一对是 0.23.1。拆分方式和软件栈不共用,每条线的悬浮提示带着它自己的那一套。
那张 T4
它能出现在这里完全是因为 vllm#39018:没有它,引擎在 sm75 上 会在内核加载时要 98 304 字节共享内存,而 Turing 只有 65 536。那个补丁只改预填充的 tile 大小,所以本图读它的方式和读别的线完全一样;图 2 不是,图 2 会说明。
提前结束的线
有四条,每一条的停点都是算术而不是半途而废:卡装不下下一档的 KV 就测不了它,悬浮提示会说它的 KV 装得下多少 token。最短的只有两档 —— gemma-4-31B 在 L4 上开着 CUDA graph 根本起不来, --enforce-eager 给它换回 2.51 GiB 和一个 2 020 token 的池子。
那两种开关
开关上写的都是引擎实际解析出的东西,而不是按钮的名字。投机组显示的是「这条线开了投机 会怎样」—— 同日、同内核 —— 其中一个写 DFlash,因为 Muse-Glimmer 的草稿模型是块扩散而不是 MTP。划不划算没有统一答案:在这两张卡上两组都从最短档就领先,其中一条一路领先到 32 K; 而在 A100 上四组每一档都是净亏。Qwen3.8 还带第二个开关 ROCM_ATTN,加的是运行时自己选的 后端;本仓库用 flag 钉住的那个买到解码、却卖掉更多预填充(见图 2)。
刻意不画的
两条本仓库测过的线。L4 上那条 Qwen3.8-27B 是另一个 checkpoint(对称 compressed-tensors 而非 AWQ,在 gfx1100 上解码相差 1.27–3.24×),而图例按模型名索引,画上去会把两者塞进 同一个条目。单张 7900 XT 上 vLLM 0.27 的 Qwen3-8B 与已画那条逐档吻合到 0.21 %,而换进来会让 图 2 的「第二张卡买到什么」变成一次跨栈比较。
图 2 · 预填充吞吐,所有有阶梯的模型,十一台机器 可从仓库重算
比较

图 2. 同一个问题的另一半:图 1 是每台机器生成得多快,这张是它读提示词 读得多快。六个模型、五十二条线、十一台机器 —— 本仓库在 500 到 32 000 token 之间所有 chart-grade 的预填充阶梯。自七月起每场 campaign 都在 decode 旁边记了 prefill,却从没画过。颜色和线型沿用图 1。

为什么拆两项
预填充不是一个数。把时间拆成固定开销、线性项和 attention 的二次项 —— T(S) = a + b·S + c·S² —— b 是算力,c 是 attention 随长度恶化得多快。悬停可看该条线拟合出的这一对系数。
两条线在一条变窄的链路上
2026-09-02。Radeon 双卡的 gemma-4-31B 和 Qwen3.8-27B 两条线最初测于 2026-08-29,当时两张卡中的一张训练在 PCIe 3.0 x8 而非 x16 —— 宿主机的开机日志记录了这个变化;客户机看不到,因为它的 sysfs 报的是卡内桥的链路。一次重启把宽度恢复了,两组在同一个容器、同样的 serve 参数下重新跑了一遍:31B 的 b 从 868.7 降到 722.6,Qwen3.8 从 846.2 降到 758.5。画出来的就是这两次。图 1 里这两个模型仍是 2026-08-29 的解码 —— 同一套配置, 而链路对解码最多只动 1%。其余五条 Radeon 线是 2026-08-24 的,在 x16 上。
卡对卡
稠密 12B 上,A100 对一张 7900 XT 在 b 上领先 3.3×、c 上 6.7×,即 attention 的差距是算力差距的两倍。L4 在 b 上反而更慢(0.90×)、在 c 上好 3.0×,所以它的曲线起点在 Radeon 之下、终点在其之上。
第二张卡
三个同时有两种拓扑的模型上,它买到 b 的 1.2–1.5× 和 c 的 1.9–2.2×。attention 比 GEMM 更容易并行,因为它不需要通信。
那个 flag 是交易
把 Qwen3.8 钉在 TRITON_ATTN 上 —— 本仓库直到 2026-08-30 都称之为「免费」—— 在 32 K 处 decode 快 1.15×,prefill 却亏掉 1.45×。
有一条线不一样
T4 要跑 gemma-4-12B 非有 vllm#39018 不可,而它把 head_size 512 那些层的 TILE_PREFILL 减半 —— 那正是本图所分解的二次项。所以它的 c 是这张 卡换了内核之后的结果,默认关着、也被排除在上面那组比值之外。它的 b 同样没被自己的 阶梯确定住:32 K 那一档来自第二台 VM,两台在那一档相差 4.61 %,换成由哪一台提供, b 会动 29.9 %。这条曲线二次项主导的程度是这里独一份的 —— 32 K 处 224 s 对 97 s —— 所以线性项把不确定性全吸收了,就像 a 在每条线上那样。
被丢弃的档位
只画两轮一致的档位,而每条线丢掉的那一档并非同一个原因:六条丢的是最浅一档,在 CUDA 卡上那是 冷引擎的第一次请求,runner 直到 2026-08-30 才把它丢弃;三条 Radeon 线丢的是 4 000 档,每次都是 第一轮比第二轮慢,本仓库不知道原因。MoE 在一张 7900 XT 上停在 12 K 则是知道原因的: 16.96 GiB 权重之后只剩 0.93 GiB 的 KV,13 149 个 token。

直到 2026-09-03,这台机器上的每条阶梯都停在 32 000 token。这看似是硬件极限,其实限制来自 campaign:每组都把 max_model_len 设成了 33 000,而它的 KV 池容量足有好几倍。下面两张图 把同样的两个问题延伸到了 128 000。主角是这一对卡,跑着当天专门为它裁出的一条阶梯;同一天租用的机器作为背景, 点一下就能叠上来。

图 3 · 解码速率到 128 000 token,这一对卡与租用的机器 可由仓库重算
比较

图 3. 批量 1 的解码速率对上下文长度,500 到 128 000,横轴取对数。默认视图是 2× RX 7900 XT、张量并行 2,跑 2026-09-03 那条阶梯 —— 这台机器上第一批超过 32 000 的行 —— 其中 Qwen3.8-27B 按图 1 的规则来画:取它实测最快的那个配置,而不是那次 campaign 的栈。 图上方的开关换成一个模型在当天租用的每台机器上。控件和图 1 一样。

整条阶梯
十六档、从 500 起,而不是只画 32 000 以上的五档:一条曲线在深处的形状,只有对着它的起点才读得出来。 凡是本阶梯与图 1 都有的档位,两边是不同日期、不同提示词裁切的两次会话,每个点的悬浮提示带着自己的日期。
Qwen3.8-27B 就是图 1 那条线,又测了一遍
两张图现在画的都是这个 checkpoint 在 TRITON_ATTN 和 vLLM 0.27.1 上的表现, 所以它们是同一个配置相隔一周的两次坐席,一次在收窄的 PCIe 链路上、一次在恢复之后。 在共有的十一档上最差只差 1.78 % —— 当除了坐席之外什么都不变时,跨坐席对照 就该是这个样子。原先画在这里的那条 0.23.1 阶梯不在了。它没有被撤销:它在 campaign-2026-09-03 里,而它和这几条线之间的差,正是 保留率百分比是由什么构成的那篇的主题。 它离开这张图,是因为同一份权重在那里 500 档解码 12.33 tok/s,在这里是 50.52,而首页画前一个数,等于在告诉读者一件关于这台机器、但已经不成立的事。 那条阶梯在 0.27 上越过 32 000 会是什么样,现在测了,一直到 128 000, 两种注意力后端都测:深度成本跨过那个版本从 0.350 降到 0.233 微秒每上下文 token,再加 --attention-backend TRITON_ATTN 降到 0.111——画在这里的 就是后面那条。
刻意不画的
Qwen3-8B,在每台机器上都没有。它自己的配置把上下文封顶在 40 960,所以它在每台跑过它的机器上都只有 图 1 的十一档 —— 这个天花板是模型自带的,不是 campaign 的。
图 4 · 预填充吞吐到 128 000 token,这一对卡与租用的机器 可由仓库重算
比较

图 4. 预填充吞吐对提示词长度到 128 000,画的是与图 3 相同的那些线。 T(S) = a + b·S + c·S² 在这里是对整条阶梯拟合的;图下的注记写明在这一对卡的每条线上, c·S² 从哪一点起超过 b·S,以及它在最深一档里占多少。

同一组,两套拟合
图 2 只对它画出的档位(到 32 000)拟合;本图对十六档拟合。对同一组这是两套不同的系数, 每个悬浮提示都说明自己带的是哪一组。
被丢弃的档位
与图 2 一样,只画两轮一致的档位;每条线的悬浮提示会写出它丢掉的那一档,以及两轮相差多少。

2文章

十三篇,按卡片上的日期从新到旧。这一日期是该文证据最后一次在本机实测、 向上游上报或重新核对的日子。每个标题下方是该文最初的导语。 其下则是它所确立的内容,措辞与综述篇给它分类时的原话一致,两处不会对同一发现有不同表述。

3它们各自记的是哪一天

文章标注的日期是取得证据的日期,而非撰写的日期。十三篇中有三篇落在七月最后一周,八篇落在八月最后一周,两篇落在九月。 机器本身是在 7 月 25、26、28 日和 8 月 1 日跑的,此后三周未再运行。第二批测量大多是在第一批条件有误的情况下重新实测。 横跨两个日期的条形,表示该文证据来自两轮分开的测量,而非期间一直在跑。

图 5 · 每篇文章带的日期 与文章上标注的日期同源
图 5. 按日期排,不按发表顺序。十三篇里有两篇不是对这台机器的测量: 一篇的日期是把缺陷上报到上游的日期,而综述篇是把十六条上游 issue 与 PR 重读了一遍,没有跑任何东西。 这两篇都标出,而不是安一个它们并没有的测量日期。

4怎么读这些文章

每篇文章都是一份有日期的记录,而非长期有效的建议。本仓库中最大的两项结论, 在测完之后便已发生了变化。某个 Ubuntu 内核回归已在 7.0.0-30 中修复; 而最初测出的 3.9× 到 5.6× 加载提速,也在严格控制 page cache 后被撤回。 因此,每篇文章均以「此后发生的变化」一节收尾,该节只往后追加,不回头改写。

每张图表都带有来源标记。可由仓库重算 表示 verify_doc_figures.py 会将该数值对照源文件核对,两者若不一致便以非零状态退出;守着仓库自身文档的也是这同一套检查。原始输出未入库 则表示该数字抄自一次原始输出未入库的运行,无法重算。

这里的绝对速率是下限,而非这套硬件的跑分。测试环境为一台 VFIO 客户机,两张卡之间没有 P2P, 走的是跨 die 的 PCIe 3.0 路径,这是刻意挑选的最不利拓扑。曲线形态可以迁移,绝对数值则是最坏情况。

源码、原始数据,以及产生它们的运行脚本: github.com/cadamcat/dual-radeon-vllm。 各篇文章均给出了上游报告与补丁的链接。