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

SUPPORTED_QUANT_TYPES 中的一行代码每步增加固定 60 ms,
压在随上下文增长的注意力开销下面

vLLM 下双 Radeon 卡的非对称与对称解码速率;跨上下文长度实测,并对照原生 kernel 与上游修复

一个 27B 模型在这台机器上解码只有 12 tok/s。此前这里的每一份文档都将其视为单一原因导致的单一故障。其实这是叠在一起的两笔开销:一笔固定不变,另一笔随上下文增长。只需一次 A/B 就能将二者分开。拆开后会发现,两笔开销各由别人写出了上游修复。

2× RX 7900 XT · gfx1100 · TP=2 vLLM 0.23.1.dev 与 0.27.1.dev5 Qwen3.8-27B · compressed-tensors int4 实测 2026-08-27

摘要

同一个模型家族,分别采用非对称与对称打包,在 1K 上下文相差 3.241×,8K 相差 1.786×,32K 相差 1.266×。按比值看,这像是一个正在消退的效应。按每步解码耗时看,事实恰好相反:两组差距始终落在 58.03 到 62.79 ms 之间,散布 8.2 %,而比值下降了 60.9 %。一笔固定开销压在一笔增长开销下面。

这笔固定开销来自 W4A16 线性路径。非对称 checkpoint 的权重类型为 uint4。由于 SUPPORTED_QUANT_TYPES 这一行代码只允许 uint4b8,该层便静默退回 Triton。全程没有任何警告,模型名称也不会提示里面是哪种格式。

随上下文增长的开销来自 paged attention,另有专文讨论。两部分开销在上游均有对应修复,且两个修复都不是我们做的:vllm#40977 已合并,补充了接受 uint4 的第二个原生 kernel;vllm#45916 放宽了 split-KV 注意力闸门,目前尚未合并。

本文的贡献在于将二者剥离,并在过程中发现了一个事实:在 gfx1100 上遍历非对称配置空间,十二个配置中有三个可由两个 kernel 服务,一个由 Triton 服务,其余八个则没有任何 kernel 支持。所有包含 act-order 的非对称 checkpoint 都属于最后一组。

1测的是什么

本文的解码速率采用差分测量,而非服务端自报数值:先生成 8 个 token,再生成 64 个,用多出的 56 个除以多消耗的时间。这种做法消除了预填充与首 token 延迟,使指标真正反映稳态解码速率。每个测试格均在基于原版镜像的全新容器中运行,且在正式测试前均执行一次弃用的预热生成。

预热是必要步骤,绝非形式。每个测试格都会 JIT 编译恰好四个 Triton kernel。在两组中,全部四次编译均发生在预热阶段,因此计时的两次调用均不包含编译耗时。这一点直接从容器日志中确认过,而非凭空假定;并且两组均做了核对,避免编译开销单方面抬高某一组的耗时。

线性层究竟由哪个 kernel 实际服务,是在 worker 内部记录的。在 --tensor-parallel-size 2 下,线性层是在派生进程中构建的,在父进程打上的 monkeypatch 在子进程中完全不可见。因此,我们在引擎启动前直接在磁盘上修改了 choose_mp_linear_kernel,由各个 rank 独立写出判定。本文中的每个格子都对应两个不同的 pid;如果插桩未能覆盖到 worker,该格将写下空记录并以零退出。

这些绝对数值值多少 这是一台配置了 VFIO 直通的客户机,两张卡之间没有对等直连,走的是跨 die 的 PCIe 3.0 线路;这种选型是刻意构建的最不利拓扑,而非最佳展示环境。文中的曲线形状可以迁移,而绝对水平仅代表下界。在不同容器镜像上测得的格子在绝对数值上不可直接比对,图 3 已标明具体适用范围。

2被它替换掉的那个结论

2026-08-24,这台机器在同一天实测了两个 27B 模型:作为基线的稠密模型为 44.8 tok/s,而混合 SSM 模型仅为 12.3,在参数量保持一致的前提下相差 3.64×。当时这一差异被解读为架构差异所致,本仓库早期的文档也沿用了这一说法。

但这项对比存在变量混杂。在那次 campaign 中,除两款 Qwen3.x AWQ checkpoint 外,其余所有量化 checkpoint 均为对称量化,而那两款恰好是文档中单独挑出指责其缓慢的对象。架构上的比较与打包方式上的差异,恰好重叠在得出结论的那一点上。下文的 A/B 测试正是为了将二者剥离,而仅打包差异本身就高达 3.24×。

3一个家族,两种打包

测试选用两个 checkpoint 评估同一种模型架构:Qwen3_5ForConditionalGeneration。两边均采用 compressed-tensors 进行 pack-quantized,运行在相同的两张卡和同一套镜像上,每个测试格使用独立的全新容器。其中一个是 symmetric: false,另一个是 symmetric: true。

每 token 的解码时间具有可加性。如果根因是每步执行的 GEMM,它必然表现为一个与上下文无关的固定毫秒数。在这种情况下,吞吐比值之所以下降,仅仅是因为两组底层的注意力开销随上下文不断增长。这是单纯看比值无法预测的现象,也是本图默认以毫秒而非看似更悬殊的倍数进行展示的原因。

图 1 · 一笔固定开销压在一笔增长开销下面 可由仓库重算
图 1. 六个格子,每组每深度一个,来自 benchmarks/w4a16-symmetry/w4a16-ab.jsonl。毫秒视图里橙色块就是两组之差, 三行按同一比例尺绘制,这正是论点所在:它不随上下文增长。比值视图是同样六个数字, 按标题的读法读一遍。每格只起了一次容器,所以这里不报 run 间区间; 这次实验唯一的一次重复把对称组移动了至多 1.31 %, 所以 8.2 % 的散布大于噪声底,但与它同量级。

4为什么其中一个拿得到原生 kernel

自 vllm#41394 合并后,vLLM 便拥有了针对 gfx1100 的原生 W4A16 kernel,注册在 compressed-tensors 所用的同一条混合精度路径上。其文档注释将其优先级排在 ROCm-RDNA3 路径上的 Triton kernel 之前。但能否命中该 kernel,由一行代码决定:

vllm/model_executor/kernels/linear/mixed_precision/rdna3_w4a16.py
SUPPORTED_QUANT_TYPES = [scalar_types.uint4b8]

uint4b8 代表对称 int4。compressed_tensors_wNa16.py 会根据 checkpoint 自身的 symmetric 标志选择权重类型,并设置 zero_points = not symmetric。因此,非对称 checkpoint 会作为 uint4 传入,进而无法通过类型检查。直接向注册表查询,其返回的信息正是如此:

choose_mp_linear_kernel,从 TP worker 内部记录
no        RDNA3W4A16LinearKernel   Quant type (uint4) not supported by
                                   RDNA3 W4A16 kernel; supported: [uint4b8]
SELECTED  TritonW4A16LinearKernel

没有任何警告。模型正常加载,正常运行,回答正确,只是速度缓慢。

图 2 · 选择跟随 symmetric 标志,而不是 group size 可由仓库重算
图 2. 上:把对称性 × group size 的四个角都问了一遍注册表 (w4a16-selection-2x2.json)。它跟随 symmetric 标志、忽略 group size, 这正是用来排掉那个显而易见的混杂的 —— 两组的 group size 也不同,32 对 128。 下:这台机器上每个量化 checkpoint,以及注册表按它真实量化参数挑出的 kernel。 这里测过的最快模型与最慢模型 group size 相同,而 gemma-4-26B-A4B-AWQ 名字里带 AWQ 却是对称的,所以名字不是判据。真正决定的字段是 quantization_config.config_groups.*.weights.symmetric。

两组之间的另外两处差异必须通过实测排除,而不能仅凭理论推演。其一是 group size(32 对 128):目前不存在能将 group size 与对称性解耦的 checkpoint,因此本次 A/B 测试无法在单一变量下隔离它。然而,can_implement 的源码中并未包含针对 group-size-128 的条件限制,前述四个角落测试也忽略了它,且这台机器上跑得最快的模型采用的正是 group 32。其二是哪些层被量化:两者的 ignore 列表存在差异,因此我们直接统计了各自 model.safetensors.index.json 中的量化线性层,而非凭空推测。统计结果为 399 对 400,仅相差一层,且该层只在对称 checkpoint 中进行了量化。这反而使对称组多承担了一次 W4A16 GEMM,因而绝非对称组速度更快的原因。

5另一项

这笔固定开销虽然构成了两组之间的全部差异,但绝非整个解码步骤的全部耗时。在 32K 上下文下,两组的大部分单步时间都耗费在 kernel_paged_attention_2d 之中。从 1K 到 32K,它的单次调用开销增长了 28 倍,而性能剖面中的其余所有 kernel 耗时均保持平稳。这就是第二个问题:它与量化本身毫无关系,并且有其专属的分析文章与修复方案。

这两项开销解释了为何单纯引用头条数字从来不是合格的总结。在 1K 上下文时,打包罚金几乎占据了全部性能差距;但在 32K 时,相比注意力开销它已沦为微不足道的舍入误差。脱离上下文长度单独给出的一个比值,掩盖了它所衡量的主要究竟是哪一部分开销。

6两个修复,都不是我们做的

vllm#40977 于 2026-07-14 合并,引入了 RDNAHybridW4A16LinearKernel,该 kernel 支持 uint4、group size 32 以及全部 gfx11 架构。因此在当前的 main 分支上,非对称 checkpoint 确实能获得原生 kernel 支持。不过那是另一个不同的 kernel,其生效路径是放宽注册表匹配规则,而非修改类型门。此前在 Triton 上解码速率仅为 11.41 tok/s 的同一 checkpoint,在那里达到了 37.32。

vllm#45916 解决了注意力相关的另一半问题。本仓库对其所做的贡献在于 gfx1100 上的端到端验证,而非核心代码本身。

图 3 · 两个修复的上下文剖面正好相反 可由仓库重算
图 3. 同一个非对称 checkpoint,三套栈。两组纵向对比的性质并不相同。 0.27 那一对是受控 A/B:同一镜像、同一容器配置、只差一个 patch、每格两次 run, 取自 benchmarks/ledger.jsonl。从 0.23.1 到 0.27 这一步同时跨过了 ROCm、vLLM 和 max_num_seqs,所以那是关于两套栈的陈述,不是关于某一个变量的; 打包罚金的受控测量是图 1,不是这支箭头。数值后面的空心圈标记 ledger 拒绝给出制图评级的格子。 把鼠标停在任意一个点上可以看到它的 run 数与区间。

一项跨 campaign 的对照实验使 0.27 这一组的数据本身具备了可信度。1K 的原版配置格子在 split-KV campaign 中被测试过两次;数周后,又由另一个独立脚本作为本目录下 kernel 对比的 hybrid 组再次测试。两次 campaign 的测试结果仅相差 0.21 %,且彼此从未针对对方做过人为修正。

7剩下的那部分,以及它为什么还不是一个 PR

类型闸门是可以放开的。但放开后,第一个问题背后立刻浮现出第二个问题,而这才是真正值得记录的部分:checkpoint 交付的零点张量形状是 (N/8, groups),而 kernel 所需的形状是 (groups, N/8)。结果就是该层通过了选择与加载,却在进入 kernel 时因 b_scales must have same group count as qzeros 被拒绝执行。HIP kernel 从未携带错误数据运行过,它在入口处就直接以张量形状不匹配为由将其拦截。

只需三行 Python 代码即可同时修好这两处:允许 uint4;执行 process_weights_after_loading 已经对另外两个张量做过的转置;以及传入该 kernel 一直支持的零点表示约定,而非硬编码为 GPTQv1 的格式。在 0.23.1 上实测达到 3.11×,输出结果与 Triton 对照组逐 token 完全一致。

让人相信第三行的是那组对照配置 仅包含前两处修改的测试组能跑出 35.45 tok/s(对比 35.50),说明它确实执行了原生 kernel。但其平均 logprob 差了 24 倍,输出也变成了 terasterasterasteras…。若一个补丁在修正布局后便止步,除了正确性以外,它在所有评估维度上都会看起来像是成功了。

但在 main 分支上,这依然是个错误的补丁。在 Hybrid 适用的场景下,Hybrid 拥有 5.9 % 的性能优势(37.32 对比 35.22),而在 ROCm 优先级列表中 RDNA3W4A16LinearKernel 恰好排在 Hybrid 之前。因此,若在那里放行 uint4,就会把该层交由两个可用 kernel 中较慢的那一个去执行。这个补丁无法将这种情况与"它是唯一可选方案"的情况区分开来。

图 4 · 十二个非对称配置,以及今天谁能服务它们 可用 probe_coverage_gap.py 自行核对
图 4. group size 对 act-order,全程非对称,在 0.27.1 镜像上问真实注册表 (coverage-gap.json)。这里只有 can_implement,没有 kernel 真的跑, 所以它说的是哪些配置能加载,不是哪些快。今天在 gfx1100 上, 所有带 act-order 的非对称配置都没有任何 kernel:Hybrid 拒 g_idx,Triton 也拒, choose_mp_linear_kernel 直接抛异常。desc_act=True 是 GPTQ 的常规质量选项,不是什么冷门设置。

这八种配置经过了数值验证,而不仅凭注册表的结论:我们针对通过 g_idx 索引 scale 和 zero point 的 fp32 参考实现,构造真实的 desc_act 置换,覆盖 fp16 与 bf16,以及 decode 与 prefill 的各种张量形状。打补丁前有四个测试用例失败,打补丁后四个全部通过,整套测试套件达到 54 项全部通过且无任何回归。

现状便是如此。我们没有提交 PR,因为要在保留那九种配置的同时避开那三种配置,需要由负责优先级决策的维护者来定夺;而且围绕 gfx1151 的类似诉求已在 vllm#46186 中提出过。上游更希望的做法应当是放宽 Hybrid,而那不是我们有权修改的代码。

8没有被确立的部分

那笔固定开销是否只有 GEMM

可加分解符合每步开销与上下文无关的假设,kernel 记录也明确了实际运行的 GEMM。但这本身无法排除伴随存在的其他固定开销。实测确立的事实是:在 32 倍上下文跨度内,两组差距的波动幅度仅为 8.2 %,且两组确实运行了不同的线性 kernel。

精度

两个 checkpoint 来自不同机构在不同 group size 下的独立量化,本文仅测试了推理速度。"在 RDNA3 上优先选用对称 checkpoint"只是吞吐层面的结论,两者在生成质量上是否相当并未在此验证。

结论能走多远

测试限定于单台机器、单一模型家族、仅限 gfx1100、TP=2 且仅限 bf16 激活。partition_scales 与逐通道路径均未涉及,张量重排亦假定为分组量化布局。覆盖扫描只是针对合成参数的注册表检查,那九种配置在实际发布中有多少被采用并未测试。

A/B 每格只有一次 run

A/B 矩阵的六个格子各自仅启动了一次容器。现有的重复测试仅覆盖了对称组,且变动幅度至多 1.31 %。图 3 中 0.27 的两组每格包含两次 run,并附带各自的波动区间。

9此后发生的变化

2026-07-14 · 兜底不再是 Triton vllm#40977 合并,引入了 RDNAHybridW4A16LinearKernel。在 main 分支上,非对称 checkpoint 已能匹配到原生 kernel,"退回 Triton"这一表述仅适用于 0.23.x 及未合入该 PR 的构建版本。至于 RDNA3W4A16LinearKernel 本身的类型闸门、转置的零点布局以及 use_v2_format,在 main 分支上均未改变。
2026-08-27 · 在 0.27 上实测,而第一次读法太宽 在 0.27.1.dev5 镜像上对比这对模型时,Hybrid 取得了 5.9 % 的领先,当时最初记录为"此补丁带来性能倒退"。实际上,它仅在该特定配置下表现为倒退。次日开展的扫描确认了三种配置下它会替代更快的 kernel,以及九种配置下它是唯一可用选项,相关论断随之收窄以符合实情。
2026-08-28 · campaign 里的架构结论已就地更正 两个 27B 模型之间 3.64× 的差距此前被归因于模型架构,但这一现象实际上与量化打包混杂在一起。README.md、docs/benchmarks.md 及 docs/architecture-notes.md 中的相关论述已就地更正并标明日期,docs/open-questions.md §9 亦已标记为已回答。完全保持不变的是:关于曲线斜率的全部结论,以及四款处于最佳 kernel 路径上的模型之间的相对优劣次序。
写测试改变了这个 patch 最初版本将零点表示约定与 c.zero_points 绑定。该版本虽然通过了端到端测试,却破坏了十个已有用例,因为 GPTQ checkpoint 会显式存储零点且仍沿用 v1 的 "+1" 偏移量。正确的判断依据应当是 not c.weight_type.has_bias()。若按 zero_points 判断,会导致 RDNA3 上所有非对称 GPTQ checkpoint 被静默错误反量化,同时还能顺利通过原本用于验证它的测试。
2026-08-29 · 两个修复里一个已合并、一个没有 摘要此前表述为"两半如今都已在上游修好",将已合并的改动与仍处于开放状态的 PR 混为一谈。vllm#40977 已合并;vllm#45916 则尚未合并,正如 docs/benchmarks.md 所述:"unmerged, so on a stock install this stands"。第 9 节记录属实,而摘要表述有误,现已就地更正。

每张图表背后的数据。A/B 测试数据、强制指定 kernel 的运行记录、三行修复及其对照组、覆盖度扫描结果以及提交给上游的测试用例,均可在 benchmarks/w4a16-symmetry/ 中查阅,并附有每个格子在 worker 内部记录的 kernel 决策。本页所有图表均由 verify_doc_figures.py 基于这些文件重新计算生成,若有任何不一致则直接非零退出。

上游进展:vllm#40977 针对固定开销提供修复,vllm#45916 针对随上下文增长的开销提供修复;后者的问题报告见 vllm#50264。关于放宽 RDNA3 kernel 与挤掉 Hybrid 之间的权衡取舍,@JartX 已在 vllm#46186 中从 gfx1151 的角度进行了详细阐述。