代码中读作 max_seqlen_q;对照不投机基线实测,在 Radeon 上损失 71 %,在 A100 上损失 61 %
启用 vLLM 文档中记录的 MTP 助手后,gemma-4-31B 在 1K 上下文下解码快 36.9 %,在 32K 下却慢 70.8 %。原因既不在草稿模型,也不在硬件或接受率。投机解码将 max_seqlen_q = 2。某条 or 链中的一句把这一长度误判为预填充而非解码,导致内核从 128 个 workgroup 跌至 8 个。
vLLM 文档标明支持该助手并给出了示例命令行,却对上下文长度只字未提。在四个深度上的实测表明,吞吐变化单调且发生变号:+36.9 %、−14.0 %、−32.4 %、−70.8 %。到 8K 时,投机解码便已成为净亏损。
闸门是 Triton unified-attention 后端 use_3d 条件里的 max_seqlen_q > 1。3D 内核将长 KV 序列切分成段并并行归约。投机的第二行 query 让它失去了走该路径的资格,启动网格从 1 × 8 × 16 塌缩为 1 × 8。代码注释将此子句归因于批次中的预填充请求,但硬性约束其实来自缓冲区几何结构:段缓冲按序列分配,却按 token 索引。这两个维度唯有在一行 query 时才完全重合。
三个看似更合理的解释经实测均被排除。不是 KV 头匮乏:该模型拥有 16 个头,不开启投机时解码一切正常。不是 query 行开销:直接调用内核,在 32K 下从一行增至两行,在一个构造中仅耗费 0.08 %,在第二个构造中耗费 0.16 %。也不是分块常量:将上游针对 Blackwell 调优的分块移植过来,性能反而下降了 8.7 %。
这一论断针对的是内核而非特定硬件。它预测,任何被路由至该内核的 CUDA 系统,随着上下文增长都会复现相同的塌陷,而缺少该闸门的后端则不会。租用的一张 A100 对这两种情况均进行了测试:在被强制的默认路径上,30K 下为 −28.2 %,50K 下为 −61.1 %;而换用 FlashInfer 时塌陷完全消失。两项预测均得到证实。
基准测试评估了 gemma-4-31B-it-qat-w4a16-ct,运行参数为 TP=2、batch 1 及贪婪解码,并搭配官方的 gemma-4-31B-it-assistant,设置 num_speculative_tokens: 1。在同一深度下,用生成 64 个 token 的耗时减去生成 8 个 token 的耗时,以此差分将解码与预填充隔离开来。
benchmarks/speculative-decoding/。四个深度上单调,
而且在 1K 与 8K 之间穿过零 —— 这个交叉点没有测出来。
把鼠标停在任意一个点上可以看到两个速率。MTP 那一组在二十五天之后作为另一次对比的 stock
基线又测了一遍:三个长深度重现到 0.45 %,1K 差 6.0 % ——
也就是说,承载这个结论的那一端,正是重现得最好的那一端。
vLLM 内部计数器在 32K 下记录的数值是 Mean acceptance length: 1.12,即起草的 78 个 token 中仅接受了 9 个。由 1K 处的加速比倒推,可得该深度下的接受率约为 0.46,这表明接受率同样随上下文深度而衰减。被拒的草稿确实带来实打实的开销:每次拒绝都要付出助手的一次前向传递与目标模型的一次验证计算。然而,助手模型仅仅只有四层,单纯的接受率下降根本解释不了 −70.8 % 的跌幅。第 5 节将揭示真正的原因所在。
use_3d = not ( seq_threshold_3D is None or ... or max_seqlen_q > 1 # 投机让它变成 2 or num_seqs > seq_threshold_3D or is_batch_invariant )
这道子句决定的,是内核获准调动这块 GPU 的多大比例:
子句上的代码注释将该闸门归因于批次中的预填充请求。但真正的硬性约束在于缓冲区的几何结构,这直接决定了真正的修复方案该如何设计。段缓冲区按序列分配,形如 [seq_threshold_3D, num_heads_q, segments, headdim];但在寻址时却按 token 索引:segm_output[token, head, segm_idx, :]。当只有一行 query 时,这两个维度刚好重合。到了两行 query,缓冲区就会越界,这道闸门因此将多行 query 全部拒之门外;而 max_seqlen_q > 1 区分不了投机与预填充,顺带把投机解码也拦截了。
最直观的解释是,两行 query 理所当然需要更多计算。为了验证这一点,基准测试直接调用 unified_attention,使用与该模型完全一致的张量形状,既不加载模型权重也不启用投机,对 query 行数进行扫描:
另外两项解释也经过测试并被排除。KV 头匮乏:混合 SSM 那篇中的塌陷源于仅有 4 个 KV 头,而当前模型拥有 16 个头,不开启投机时解码毫无问题。分块机制:上游代码针对 head_size == 256 and max_seqlen_q > 1 包含一个 tuned_large_head 块,该逻辑仅对 Blackwell 开放,其余条件当前硬件均完全契合。将其中 BLOCK_M = 32 移植过来后,32K 下的性能反而变慢了 8.7 %。
kernel_unified_attention 的单次调用耗时,取自保存在测试机上的 torch-profiler trace。由于仓库中仅入库了汇总数据与 trace 文件名,这是本页唯一无法重算的区块:
绝大多数调用耗时几乎不变,少数调用耗时激增。这种分化源于该模型交替出现的注意力结构:滑动窗口层看到的 KV 较短,不受影响;全局层则必须扫描完整的 32K,完全丧失了分段并行能力。若直接援引 9.2× 的均值,两类分布的真实情况都会被掩盖。hipGraphLaunch 计数从 63 增至 324,证实计算图重放机制完好无损。
针对内核提出的论断,能对尚未实测过的硬件给出可检验的预测。如果性能塌陷确实是因为执行路径从 3D 跌落到 2D,那么任何被路由至同一内核的 CUDA 系统,随着上下文加深都应当复现这一现象;而不含该闸门的后端则不会复现。由于 gemma-4 的注意力头维度不规则,只要 FA4 不可用,vLLM 就会强制调用 TRITON_ATTN,而 A100 恰恰不支持 FA4。只需花费一小时的实例租用成本,这项预测便可得到验证。
benchmarks/cuda-a100/gemma4-mtp-backend-matrix.json,
vLLM 0.28.0,每格一次运行 —— 是一次探测,不是一次 campaign。
灰色标记是这台机器 32K 上的 −70.8 %,用作比例尺。在被强制的默认路径上,
MTP 是随深度加深的净亏;在 FlashInfer 上,同一个 MTP 在 30K 上是正的,
而 50K 上残留的 −8.7 % 是草稿模型的经济性问题,不是那种病理性掉落。
30K 的两个 FlashInfer 数字来自软件栈相同的两台不同 Colab VM;
这里其他每一个比值都在同一个会话内计算,而 50K 那四个格子全部来自同一个会话。
通过手动加宽缓冲区并抬高阈值,在 32K 下性能从 8.85 tok/s 恢复至 32.42,投机解码终于跑赢了基线解码。但这并非长久之计:这种改法无法表达「允许投机的两行 query,但排除预填充的四千行」;而且在 30K 提示词下,贪婪解码输出在第 35 个 token 处便与 stock 分叉,当时并未将三种潜在成因逐一拆解。
vllm#45450 自六月便已开启,此前一直因合并冲突而搁置,它恰恰提供了前一段所期望的结构性修复。该 PR 将闸门调整为 max_seqlen_q > decode_query_len,在将预填充继续拒之门外的同时,按 token 为段缓冲区分配尺寸。
输出分叉的疑虑同样得到了澄清。在内核层面对 18 个用例进行固定输入对比,结果表明两条路径在全部 18 个固定输入用例中均呈现逐位确定性(bit-deterministic);两者的计算结果最多相差一个 bf16 ULP。在单行 query 下是这样(此即现行线上运行行为),在两行和四行下亦完全如此。第 35 个 token 处的分叉,其实是既有的 2D/3D 归约顺序结合律重组,在生产环境中仅凭 batch size 的变化本就会触发这一切换,并非准入逻辑本身的缺陷。
上述所有测量结果,仅在模型确实由 Triton unified-attention 内核提供服务时才成立。vLLM 并不总是将执行路由至该处;一旦路由至其他后端,该补丁便处于闲置状态。图 4 中同一组 A/B,做五次的对照实验,正是区分这两种情形的五轮运行,每轮包含十一个档位,每格两轮,k=3:
gemma-4-31B-it,TRITON_ATTN,探针触发一次 ——
平均 +45.6 %,32K 下达 1.95×。gemma-4-26B-A4B,TRITON_ATTN,探针触发一次 ——
+48.4 %,32K 下达 1.99×。Qwen3.8-27B,通过 --attention-backend
强制锁定内核,探针触发两次(每个张量并行 worker 一次)—— +83.9 %,32K 下达 2.87×。Qwen3.8-27B,由引擎自行路由,ROCM_ATTN,
探针无输出 —— −1.9 %:两组实为同一次测量的重复。Qwen3.8-27B,由引擎自行路由,FLASH_ATTN,探针
无输出 —— −0.08 %。在 32K 下为 20.51 对 20.52。在运行阶梯测试前,探针便能指出系统属于哪种情况。当 3D 路径处理步长超过一个 token 的步骤时,inject_45450.py 会在每个 worker 进程中打印一次 PROBE_3D_SPEC_ACTIVE。这要求同时满足三个条件:安装了补丁、该内核处于激活状态、且启用了投机解码。在全部五次运行中,探针输出与最终结果完全吻合:凡是打印了探针信息的运行,打补丁的一组均拉开了差距;凡是保持静默的运行,没有任何一个档位的变动超过 8.8 %,完全在正常的运行间波动范围之内。读者若应用了该补丁而在日志中未见输出,并非操作有误。这纯粹是因为引擎将该模型路由到了其他后端,此时真正需要调整的是后端选择,而非补丁本身。
同样的 3D 跌落至 2D 现象最早见于 vllm#48076(2026-07-09),当时是在单张运行相同架构但不同量化格式的 B200 上测得的。该报告涉及的触发条件是同一条 or 链中的另一子句:num_seqs > seq_threshold_3D。那是一个 batch 大小阈值,其提议的修复方案尝试根据 SM 数量推导该阈值。该修复触及不到这里的触发条件。由于 max_seqlen_q > 1 的判定完全独立于该阈值,无论阈值如何计算,投机解码依然会跌回 2D。本文的新发现,在于指出了第二个触发条件、揭示了其根本成因,并补充了 RDNA3 上的实测数据。
上述发现于 2026-08-01 发布至该 issue 讨论区,随后于 2026-08-26 提交至 vllm#52049,后者是一份在相同模型上记录到相同症状的 CUDA 侧报告。该报告提交者证实,显式指定 FlashInfer 可以避开此问题,这与我们的机理分析完全相符:本文所追踪的性能塌陷特属于该 Triton 内核,显式指定后端便绕过了强制路由。鉴于 ROCm 尚不支持 FlashInfer,在当前硬件上,切实的权宜之计依然是依上下文深度控制投机解码的开关。
不属于该内核所特有的,是在深层上下文中投机解码出现亏损这一现象本身。2026-08-29 的实测显示,Qwen3.8-27B 同样出现性能亏损。无论在 ROCm 的 ROCM_ATTN 上(其后端文件导入了 chunked_prefill_paged_decode,并未导入任何经本补丁修改的文件),还是在 CUDA 的 FLASH_ATTN 上,情况均是如此。这两个后端均未使用该 Triton 内核,本补丁在它们上面也未改变任何行为。它们虽表现出相同的负收益符号,背后却是本文尚未查明的不同机理。若将这两项结果视作前述论证的又一佐证,那将大错特错;它们恰恰证明,前述论断并不能涵盖这些场景。
1K 至 8K 之间未测。A100 侧仅测了 30K 和 50K,只框出 FlashInfer MTP 收益转负的区间,未定位具体交叉点。
ROCm 阶梯每格一进程。MTP 组二十五天后的复测在三处长深度吻合至 0.45 %,但不投机的那一组无复测,且未记录逐格接受率。A100 矩阵跨两台 VM 每格仅跑一次,会话间方差未量化。
全部测试均采用 temperature=0,即两模型最易一致的情形。采样按理会降低接受率并恶化投机,但在两家硬件上均未实测。
仅实测了 gfx1100(TP=2、无点对点通信的 VFIO 客户机、单 checkpoint)与 A100(单卡、单 checkpoint)。内核级论断依赖两台机器的一致结果,而非单次测量。
docs/speculative-decoding-on-rdna.md §1 曾述及其中每张图表均为单次运行且无进程级复测。实际上 MTP 组存在一次复测:vllm#45450 对比实验中的 stock 基线即为相同配置下的再次实测,且在 8K、16K 与 32K 处吻合在 0.45 % 以内。相关文档现已更正,verify_doc_figures.py 亦锁定了这一偏差范围。
数据。ROCm 阶梯、直接调用内核的扫描与正确性用例见 benchmarks/speculative-decoding/;A100 矩阵、日志及在 vLLM 0.28 上显式覆盖 FlashInfer 所需的条件见 benchmarks/cuda-a100/。完整长文见 speculative-decoding-on-rdna.md。每次调用约 2 MB 的 Profiler 原始 trace 留存在测试机上,导出分布与 trace 文件名见 trace-unified-attention.json。
助手模型为 google/gemma-4-31B-it-assistant,0.90 GiB 且无访问限制。其 head_dim 与 global_head_dim 与目标模型完全一致,支持共享 KV cache 分配。