dual-radeon-vllm · 实测长文

两张消费级 Radeon,
逐项实测

这些长文出自一个在 2× RX 7900 XT(gfx1100,RDNA3)上做 ROCm 与 vLLM 实测的仓库。 每篇文章的图都取自仓库自己会去校验的那份已入库数据,并在页面上标明哪些数字可以重算、哪些不能。

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

1这台机器今天能跑多快

每个模型、每台机器一条线,画的都是它自己实测到的最快配置,而不是一个统一口径。Radeon 这边六条里 有四条出自同一场 campaign、同一套栈,彼此可以直接比;另外两条来自更晚的栈,因为在这两个模型上, 那场 campaign 已经不是这台机器的上限了。A100 那边有同样五个模型,是它自己一个下午跑完的一整场。 每条线都写明自己是哪一种。

图 1 · batch 1 的解码速率,每个模型取实测最快的配置 可由仓库重算
比较

图 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-31BQwen3.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 的行; 图上方的开关换成一个模型在当天租用的每台机器上。控件和图 1 一样。

整条阶梯
十六档、从 500 起,而不是只画 32 000 以上的五档:一条曲线在深处的形状,只有对着它的起点才读得出来。 凡是本阶梯与图 1 都有的档位,两边是不同日期、不同提示词裁切的两次会话,每个点的悬浮提示带着自己的日期。
刻意不画的
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。 上游的报告与补丁在每篇文章里给出链接。