RDNA3 · vLLM · 实测于 2× RX 7900 XT

Triton paged-attention 回退实现解释了
12.1 到 4.2 tok/s 的全部解码下滑

vLLM 下双卡 Radeon 上的混合 SSM 解码速率;以稠密模型、llama.cpp 与上游 split-KV 内核作对照检验

线性注意力本应让长上下文的处理开销变低。在这台机器上,Qwen3.6-27B 在 500 到 32 000 token 之间从 12.1 跌落至 4.2 tok/s;而同个下午测得的稠密 8B 模型仍保持了 77 % 的短上下文速率。问题并不出在 48 层线性注意力上,而是剩下的 16 层。

2× RX 7900 XT · gfx1100 · TP=2 vLLM 0.23.1.dev1 · ROCm 7.14 VFIO 直通 · 无 P2P · 跨 die PCIe 3.0 测量批次 2026-07-25

摘要

在 vLLM 下,一个混合 Mamba / gated-delta-net 架构的 checkpoint 在 500 到 32 000 token 上下文之间失去了 65 % 的解码速率。这条下滑曲线比同一台机器、同一场 campaign 中测得的任何稠密模型都要陡十四到四十倍。

GPU 内核级 profile 显示,增加的耗时几乎全部集中在一处:kernel_paged_attention_2d。该 Triton 回退实现用于运行模型的 16 层全注意力,其单次调用耗时增长了 28.3 倍;在此期间,该解码步中的其余内核耗时均保持持平或微幅下降。线性注意力层不是耗时增加的地方。

该模型有三个独立条件不满足 ROCm 自定义 paged-attention 内核的准入要求,因此走了 Triton 回退实现。回退内核专门针对此模型表现迟缓,是因为在 batch 为 1 时仅有 4 个 KV head,内核几乎找不到可供并行的维度。两项独立的观察印证了这一判断:单卡功耗随上下文深度下降,且在按照读取的 KV 字节数归一化后,性能差距不仅没有收窄,反而进一步扩大。

同一台机器在同一天内,相同的 checkpoint 在 llama.cpp 下测得的衰减斜率要平缓 28 倍,从而排除了驱动和硬件这两种解释。上游已有可用的修复补丁,并在本文中进行了两次实测验证,最近一次是在 vLLM 0.27 上执行的受控 A/B 测试:32 K 上下文下达到 9.46 倍。该 A/B 测试中存在一个表现出双峰分布的数据格,后文对此做了说明。

1测了什么,怎么测的

第 2 节中的所有数据均出自同一场 campaign:同一套软件栈、同一个下午,所有模型均严格锁定在相同环境上。该测试轮次在五种模型架构和十一个上下文长度上共采集了 292 次解码与预填充测量。每个数据格均由 vllm serve 提供服务,配置为 TP=2、启用 CUDA graph、--gpu-memory-utilization 0.85 以及 512 token 输出,最终数据取两轮测试的平均值。

本文汇报速率数据时给出的是各测试轮次自身的散布极差,而非标准差。在每格仅进行二到四轮测试的情况下,标准差给出的表面精度远超其实际样本量能支撑的水平。以极差占均值的百分比来衡量方差,不仅清晰直观,而且无需引入某种特定的分布假设。图 1 中的每一个数据点均附带了其测试轮数及对应的极差区间,将光标悬停即可查看。

关于绝对数值的解读 测试环境运行在 VFIO 直通客户机内,这一对卡之间没有 P2P 互连,且走的是跨 die 的 PCIe 3.0 路径——这是有意选择的最不利硬件拓扑。本文展示的曲线形态可以迁移到别处;而给出的绝对速率仅供参考下界,不可视作该硬件的真实性能标定。

2观察到的现象

五种架构、十一个上下文深度、同一场 campaign。其中四个架构随上下文长度增加呈现出平缓下滑,这是 KV cache 的固有开销所注定的;然而第五个架构的表现却截然不同。

图 1 · 解码速率对上下文长度 可从仓库重算
点击模型名增减曲线 · 悬停数据点查看轮数与极差
图 1. 解码 tok/s 对上下文长度,2026-07-25 campaign,TP=2,未打补丁的 vLLM 0.23.1.dev1。 默认亮起两条:Qwen3.6-27B,本文讨论的混合 SSM 检查点;Qwen3-8B,稠密对照。 另外三条点一下就出来。从 500 到 32 000 token 的保留率:8B 77.2 %、12B 69.9 %、 26B MoE 67.5 %、31B 68.4 %、混合 SSM 35.1 %。 最后两条线是后继 checkpoint 在两个内核上的阶梯 —— 引擎可以用它们中的任一个来服务全注意力层, 2026-08-29 测的,两组都不带投机;它们默认不亮,因为那是和上面这次 campaign 不同的一套栈。 数据来源:benchmarks/ledger.jsonl。

在各个深度下,混合模型的绝对速率均为全场最慢。仅从参数量看,它不该是最慢的。不仅如此,它也是唯一一条曲线持续向下弯曲而未能趋平的模型。保留率指标让这一对比更为直观:到 32 000 token 时,其余每套配置均能保住短上下文速率的 67 % 至 78 %;而该模型只保住了 35.1 %。

若折算为斜率(即每增加一个 token 上下文所带来的解码微秒增量),本场 campaign 中混合模型的值为 4.84 µs,而稠密 8B 对照模型仅为 0.118 µs。二者相差了 41 倍,本文接下来就拆这个数字。

3归因:是内核问题,而非架构问题

在两个上下文深度下进行的 torch profile 均保持开启 CUDA graph。我们将内核调用严格归属到其所对应的引擎步时间窗口内,从而避免将分块预填充(chunked prefill)的调用混入解码数据中。

图 2 · 单次调用 GPU 时间,1 K 对 32 K 原始 trace 未入库
图 2. rank 0 上的单次调用 GPU 时间,对数刻度。两个深度的调用次数完全相同—— 线性注意力 kernel 528 次(48 层 × 11 步),paged attention 176 次(16 × 11)—— 所以这是单次调用变贵,不是发出了更多工作。与本文其他图不同,这份 profile 的原始输出 不在仓库里;这四行是从写作稿转录的,无法从已提交数据重算。

模型中的 48 个 gated-delta-net 层在 32 K 处的耗时与 1 K 处基本一致(仅为 0.95 倍),这正是线性注意力应有的表现,该实现也做到了。量化 GEMM 的耗时同样保持平稳。全程只有一个内核的时间发生了大幅漂移,而且其增幅高达 28.3 倍。

两笔账对得上。按每步调用 16 次计算,paged attention 的总耗时从每步 5.71 ms 增加到 161.5 ms,净增 155.8 ms。而通过 CUDA graph 回放实测的解码步自身总耗时,则从 85.85 ms 增加到 236.9 ms,净增 151 ms。新增的耗时基本都来自 paged attention;在更长的上下文深度下,其他所有内核的耗时均保持持平或略微加快。

4为什么会走回退路径

模型中的 16 层全注意力之所以落到 Triton 上,是因为 ROCm 的自定义 paged-attention 内核拒绝了该模型。在 vllm/platforms/rocm.py 中,use_rocm_custom_paged_attention 的 RDNA 分支要求(在其他前置条件之外)必须满足 head_size == 128 且 block_size == 16。

use_rocm_custom_paged_attention · 反事实调用
# 在运行中的引擎上测出来的,不是从 config 里读的
head_dim                = 256       # gate 要 128
cache_config.block_size = 784       # = 16 x 49,gate 要 16
is_pow2(784)            = False     # 在它自己那条路径上就否掉 use_custom
gqa_ratio               = 6         # 这一条是过的

# 单独放宽任何一个不满足的条件,仍然不够
use_rocm_custom_paged_attention(head_size=128)   -> False
use_rocm_custom_paged_attention(block_size=16)   -> False

没有任何单一条件是唯一的阻塞点。单独放宽任何一个不满足的条件,闸门依然紧闭;同时 chunked_prefill_paged_decode.py 还会独立地由于 is_pow2 检查不通过而拒绝 use_custom。自定义内核在此处面临三重不可达的困境。因此,对该类模型而言,提升回退实现的执行速度是唯一切实有效的路径。

显而易见的修复思路行不通 仅放宽 head_size 检查只是将报错位置转移,并未真正解决问题。csrc/rocm/attention.cu 中的 CALL_CUSTOM_LAUNCHER_BLK_HEAD 仅分发 case 64 与 case 128,遇到其他取值均直接触发 TORCH_CHECK。该条件属于内核的物理能力硬限制,而非人为设置的保守门槛。

性能下滑中的一部分源于内核,而非架构。后继 checkpoint Qwen3.8-27B 在 vLLM 0.27 上于 2026-08-29 针对引擎可用于服务那 16 层的两个内核分别完成了测试。测试中的两组均未启用投机解码,运行于相同的 checkpoint 和会话,复现误差均优于 0.12 %。在默认配置下,ROCm 选择器会采用 ROCM_ATTN,因为 TURBOQUANT 与解码器不兼容,前者是其首先选用的回退内核。若改为锁定使用 TRITON_ATTN(仅需设置一个参数,无需任何补丁),相同的阶梯在 32 K 处提速 15.0 %,全区间的保留率也从 73.4 % 提升至 84.3 %。本文所讨论的性能下滑中,有十一个百分点取决于由哪个内核来服务那 16 层全注意力。其余的性能损失则与此无关:即使锁定了内核,该模型到 32 K 处仍损失了 15.7 % 的短上下文速率,这正是下一节探讨的主题。

5为什么回退路径在这个模型上特别慢

同一台机器上的稠密模型同样走这条 Triton 路径,却并未发生类似的性能劣化。二者的根本区别在于:在 batch 为 1 的解码阶段,内核究竟具备多少可供调度的并行度。

模型KV headsquery headsGQA每 token 上下文的微秒数
Qwen3-8B,BF16 稠密8324:10.118
gemma-4-31B,w4a16 稠密16322:10.339
Qwen3.6-27B,混合4246:14.840

在 4 个 KV head 且 batch 为 1 的条件下,内核几乎没有足够的计算任务来分摊到显卡的 84 个 CU 上。两项相互独立的信号均表明,瓶颈在于计算单元的占用率不足,而非实际计算负载过大:

功耗随上下文增长而下降

在 518 token 时,双卡总功耗为 265 W + 265 W;而到 24 K 时,功耗反而下降至 232 W + 227 W。在整场测试扫描中,没有任何其他模型会出现功耗随深度下降的反常现象。其余所有模型均逐步攀升至 265 W 并维持高位,唯独该模型在中后段出现回落。各模型的基准功耗水平虽有不同(在 518 token 时,8B 为 245 W,12B 为 206 W,26B MoE 为 193 W),但关键的对比在于变化方向,而非绝对功率数值。计算单元处于空转闲置状态,而非高负荷饱和。

按 KV 流量归一化后差距更大

在全网 64 层中,仅有 16 层需要维护 KV cache,且其 GQA 压缩比例颇高。其结果是,该模型在三者中读取的 KV 数据量实际上是最少的:每 token 仅需读取 64 KiB,相比之下 Qwen3-8B 需要 144 KiB,gemma-4-31B 则为 160 KiB。若按每 KiB 读取数据折算耗时,它达到了 Qwen3-8B 的 92 倍以及 gemma-4-31B 的 36 倍。这一事实直接推翻了「该模型本身计算开销更为繁重」的猜想。

6对照:同一个模型换一个引擎

同一台机器、同一天、同一个 checkpoint 家族,仅仅更换了推理引擎。如果性能崩溃是由驱动程序、GPU 硬件本身或互连拓扑所致,那么在换用引擎后理应同样复现。

图 3 · 解码斜率,每 token 上下文的微秒数 可从仓库重算
图 3. 越低越平。阴影区间是这台机器上稠密模型在 vLLM 下产生的范围,0.118 到 0.339 µs。 llama.cpp 跑的是 Q4_K_M 而 vLLM 跑的是 AWQ-INT4,所以跨引擎的绝对吞吐不可比,只有斜率可比。 ROCm 后端用的是与 vLLM 相同的 userspace。数据来源: benchmarks/llamacpp-depth-sweep-{rocm,vulkan}.json、benchmarks/results.jsonl。

llama.cpp 在 ROCm 后端下的衰减斜率恰好落在稠密模型于 vLLM 中呈现的基线区间之内;而在 Vulkan 后端下测得的斜率甚至低于该区间,比所有测试模型都更为平缓。由此可见,两套彼此独立的软件栈对「该硬件上正常的衰减斜率形态」给出了高度一致的答案;唯独本篇所聚焦的这套特定配置,其斜率高出了整整一个数量级。

一个受到扰动的数据点 ROCm 后端在 16 384 token 处测得的速率为 21.35 tok/s,其标准差达到 2.635,而其余所有测试点仅在 0.06–0.09 之间波动;并且更深处的 24 576 token 速率反而更快。该单次测量显然受到了外界扰动,不应被单独拿来做定量引申。它保留在图表中。

与此同时,该配置下的预填充表现却走向了另一个极端:其吞吐量随上下文长度增加反而上升,从 500 到 32 K 录得 802 → 881 tok/s,增幅达 9.9 %;而同场测得的每一组稠密对照在相同区间内都掉了 36–44 %。发生反转的仅仅是解码阶段。(2026-08-31 更正:此处此前误记为 8–44 %。其中 8 来自 8B 模型,该模型的 500 token 档位未达到图表级精度,其两轮测量误差达 22 %,且原先引用了两者中偏高的一轮,从而掩盖了实际跌幅。在符合验收门槛的档位上,它从 1 K 起的跌幅达到 37 %,此处实测的任何稠密对照组跌幅均不少于 36 %。)

预填充吞吐量的上升属于当前硬件配置,而非该架构的固有属性。在 2026-08-31 之前,本文曾基于单一机器上单一模型手工记录的两个数据点,误将其归因为架构特征。Qwen3.8-27B 采用完全相同的 hybrid-SSM 设计,但在本测试机上其预填充速率并未上升:在 vLLM 0.23 下它下跌了 11.8 %,锁定在 Triton 时下跌 32.6 %;而在全宽链路上重新测试的对照组中,其表现为完全持平的 +0.3 %;在 A100 上它则下跌 8.1 %。(在 2026-09-02 之前,本句曾为该测试组引用了 7.5 % 的跌幅数据。那次运行中有一张显卡协商至 PCIe 3.0 x8 速率;随后的 campaign-2026-09-02c 重新进行了测量,将这一组的走势由「下跌」修正为「持平」,但仍非「上升」。)(2026-09-03 更正:这句话原来写的是这个 checkpoint 的预填充「从来没有上升过」。然而在 H100 上,同一模型在相同的测试档位下上升了 19.4 %,增幅甚至超过了本节讨论的 Qwen3.6 测试组。原先的论断将特定机器上的实测结果误推演为模型架构的一般结论。)与此同时,完全不含任何线性注意力层的 MoE 模型 gemma-4-26B-A4B,其预填充吞吐上升幅度同样超过了 Qwen3.6,在 7 月为 23.8 %,8 月为 21.9 %。目前收录于 benchmarks/prefill.jsonl 中的二十四条 stock hybrid-SSM 阶梯(涵盖同一 checkpoint 家族跨十种机器配置的测量)显示:十条阶梯上升超过 1 %,一条持平,十三条下降。相同的 Qwen3.8-27B checkpoint,其预填充变化幅度从双卡 Radeon 上 2026-09-07 那条到 128 000、锁定 TRITON_ATTN 的阶梯的 −68.3 %,一直延伸到双卡 H100 上的 +103.0 %。同一种架构和同一个 checkpoint,产生了横跨 171 个百分点且正负符号截然相反的测试结果。这就是核心发现:预填充吞吐曲线的上升,仅仅说明在该测试组上短 prompt 的固定开销压过了计算复杂度的二次增长项,并不意味着模型内部维持着某种循环状态。无论预填充走势如何,本文所记录的解码阶段性能崩溃均不受影响:那是直接实测得出的客观事实,并非依赖此类推论得出。

7修复,测了两次

上游给出的修复补丁是 vllm#45916。该 PR 为 Triton 回退实现引入了一个 split-KV 解码内核:执行方式不再局限于每个(序列,KV head)由单个 workgroup 遍历全部上下文,而是将整个 KV 缓存区间切分给多个 workgroup 并行处理,最后再执行规约操作。这恰好补齐了该模型所匮乏的并行度。

我们最初于 7 月在 vLLM 0.23.1 上对此进行了初步验证。当时在轻微不同的上下文深度下进行单次差分测试,在 32 K 处录得了 2.52 倍的提速。随后,我们在 0.27 镜像上做了一次受控 A/B 测试:测试的两组运行于完全相同的软件栈与测试 harness 下,每个测试格均启动全新容器,并且仅替换对应的内核文件;整套测试重复运行两轮,并在第二轮中颠倒两组的执行顺序。

图 4 · vLLM 0.27 上的受控 A/B 可从仓库重算
点击柱组在速率与每 token 毫秒之间切换
图 4. Qwen3.8-27B-AWQ-INT4,TP=2,vLLM 0.27.1.dev5,离线 64-vs-8 差分。 与图 1 是不同的模型、不同的栈——见下方的限定。路由是从 TP worker 内部记录的,不是推断的: 两个 rank、每个深度、两轮都读到 (head_size 256, sliding_window 0, use_splitkv True); 两个文件的 md5 在运行时断言,所以某一组如果没换成功会中止而不是把另一组测两遍。 数据来源:benchmarks/hybrid-splitkv-027/。

两轮测试数据汇总后显示:1 K 处提速 1.38 倍,8 K 处为 3.52 倍,32 K 处达到 9.46 倍。在 32 K 上下文下,单 token 解码耗时从 261.9 ms 大幅缩减至 27.7 ms,衰减斜率从 7.408 锐降至每 1 000 token 上下文 0.262 ms,而 1 K 到 32 K 的性能保留率也从 10.2 % 跃升至 70.0 %。性能崩溃的现象彻底消失了。

8没有被确立的部分

列举四项局限。

A/B 有一格是双峰的

在 8 192 token 处,打了补丁的那一组在四轮测试中分别跑出 47.02、40.14、47.58 与 42.41 tok/s。经排序后,中间数值的落差占据了总极差的一半以上:这代表相距约 15 % 的两个独立模态,而非随机噪声。加跑第三、四轮后极差从 15.8 % 扩大到 16.8 %。确定性的切分启发式算法不可能导致这种现象,其底层机理尚未确立。正文引用的 3.52 倍均值对任一聚类均无法准确描述。

图 4 是另一个模型、另一套栈

图 1 测试的是 0.23.1 上的 Qwen3.6-27B;图 4 则是 0.27 上的 Qwen3.8-27B。更新的软件栈启用了不同的 W4A16 线性内核,在不计入注意力机制的情况下便能单独带来约 3 倍提速。两张图中的绝对性能数值不具可比性,但比值关系与曲线形态依然成立。

因果 A/B 只有一台机器

因果 A/B 测试全程在单一机型、单一芯片厂商且 batch 为 1 的受控条件下完成。在四张租用的 NVIDIA 显卡上延伸至 128 000 token 的解码阶梯显示,Qwen3.8-27B 在 H100 上降幅为 21.8 %(与稠密 31B 模型相当)。尽管各厂商硬件均由注意力机制决定上下文深度开销,但由于各卡选用的后端各异,无法构成纯粹的同内核对照。占用率假说预测增大 batch size 能缓解性能崩溃,但本文对此未做实测。

这个修复没修掉的东西

Paged attention 占整个解码步耗时的比例由约 68 % 降至 11 %。其余耗时主要被量化 GEMM 占据,其在短上下文时约占总时间的 77 %。本文所修复的是斜率问题;而基准吞吐量的优化则属于另一个独立范畴。

9此后发生的变化

本文是一份带日期的记录,而非长期适用的既成结论。在上述测量完成之后,已有三项事实发生了变动。

2026-08-10 · gate 不再需要谁去放宽 7 月的验证在本地将 on_gfx12x() 放宽为 on_gfx1x(),以便 gfx11 能调用新内核。该修改已于 2026-08-10 合并至 vllm#45916 自身的分支,因此该 PR 目前已原生支持 RDNA3,不再需要任何本地修改。
2026-08-28 · 基线那个问题有答案了 7 月的文稿最终停留在「斜率问题已解决,基线表现则是另一个问题」。图 4 中的 0.27 A/B 测试给出了确切答案:在 32 K 深度下,该模型的解码速率现已提升至 36.1 tok/s(最初 campaign 测得的数据为 4.2);且其残余斜率完全落入了本机稠密模型产生的基准区间内,不再高出 27 %。
2026-08-29 · 本文有两个数字和它自己的图对不上 摘要初稿曾称混合模型的衰减曲线比稠密模型「陡四到四十倍」。对照图 3 所绘制的基线区间(每 token 0.118 到 0.339 µs),混合模型的 4.84 对应的是 14.3× 至 41.0× 的陡度,其下界应为十四而非四。此外,「llama.cpp 的斜率落在区间之内」的论断仅对 ROCm 后端成立,Vulkan 后端实测为 0.107,完全低于该区间。以上两处均已完成就地更正,verify_doc_figures.py 现已对这一比值及两个后端相对基准区间的位置进行了刚性校验。

数据与复现。所有标注了 可从仓库重算 的图表,均由 verify_doc_figures.py 基于源数据直接验证;若正文记述与实测数据存在任何不一致,该脚本将以非零状态退出。背景文档详见 docs/hybrid-decode-on-rdna.md。原始数据行记录:ledger.jsonl · hybrid-splitkv-027 · 仓库。

该问题已作为 vllm#50264 提交上报。对应的修复代码为 @feiyehua 提交的 vllm#45916,针对 gfx11 架构的适配放宽则由 @thegoldenflow 贡献;本项工作为其提供了 gfx1100 上的端到端实测验证以及 0.27 版本上的 A/B 测试数据。