DCU使用技术报告_中篇_gfx936_DCU_Qwen3.5-27B_Triton-ROCm_Attention与GEMV深度优化实战
DCU使用技术报告(中篇):gfx936 DCU上的Qwen3.5-27B Triton-ROCm Attention与GEMV深度优化实战
系列文章中篇。上篇解决了环境、基线和 Profile 的可信度问题;这一篇进入算子层,记录专用 Prefill Attention、Q10/Q20 Query 复用、Gate/Up Triton GEMV,以及一批看起来合理但最终没有采用的实验。
0. 先给结果,不绕弯子
我们最终保留的主要算子优化有三类:
- Qwen3.5 固定形状的专用 BF16 Prefill Attention,并对长 KV chunk 使用双阶段流水;
- Prefill Query 分块复用:基础 Q10,以及只命中长 KV 完整 chunk 的 Q20;
- gfx936 上固定形状的 Gate/Up Decode Triton GEMV。
实测中,Gate/Up GEMV 相对原 rocBLAS 路径带来了约 14.91% 的三档前五条加权吞吐提升;Q10 Query 复用在它的对照基础上再提升约 6.91%;长 KV Q20 又带来约 1.81% 的增量加权收益,16K-32K 单档提升 3.47%。
同时,我们也放弃了不少方向:全局 ROCm Attention 后端、FP8 KV Cache、Page-aware 地址优化、Gate/Up+SwiGLU 融合、多个 rocBLAS tuned solution、Down GEMV、lm_head GEMV。它们有的局部更快,有的微基准很好看,但到了服务层不是回退,就是收益小到不值得承担数值和维护风险。
1. 先理解 Qwen3.5-27B 在这张卡上的固定形状
这套模型不是纯 Full Attention 堆叠。实际 Decode 中,我们关注到几类高频线性形状:
| 路径 | 形状(M x N x K) |
每个 Decode Token 的调用特征 |
|---|---|---|
| MLP Gate/Up | 34816 x 1 x 5120 |
64 层重复 |
| MLP Down | 5120 x 1 x 17408 |
64 层重复 |
| GDN QKVZ | 16384 x 1 x 5120 |
48 个 Gated DeltaNet 层重复 |
| GDN Output | 5120 x 1 x 6144 |
48 个 Gated DeltaNet 层重复 |
| lm_head | 248320 x 1 x 5120 |
每个 Token 一次 |
Full Attention 的固定参数则是:
query heads = 24
KV heads = 4
head dim = 256
GQA ratio = 6
dtype = BF16
这些形状很重要。通用 vLLM 必须照顾很多模型、dtype、batch 和可选功能,而比赛负载在单并发 Decode 下反复出现同一组 N=1 GEMV。只有把分派条件收紧到设备、dtype、形状、bias 和内存连续性,我们才有资格删掉通用路径中的开销。
2. 专用 Prefill Attention:先把 GQA=6 这件事做好
项目初期,kernel_unified_attention_2d 一度占 GPU kernel 时间的 47.77%。我们试过调 Prefill/Decode tile,也试过切完整 ROCm Attention 后端,效果都不理想。
完整 ROCm Attention 后端确实让 TTFT 有过明显改善,但 Decode TPOT 分别恶化约 84.0%/172.7%/242.1%,三档吞吐下降约 23.0%/30.6%/27.1%。这说明“Prefill 可能更快”和“整个后端适合服务”是两回事。
后来我们把问题缩到 Qwen3.5 的固定 Prefill 形状,单独写了专用 Triton kernel:
- 只处理单序列 BF16 Prefill;
- 固定
24Q/4KV/head_dim=256/GQA=6; - 保留 QK、因果 mask、在线 Softmax、PV 和 FP32 累加;
- Decode 继续走原来的 Triton unified attention;
- 不支持的形状和功能全部回退。
通用 kernel 喜欢按 2 的幂组织 block,但 GQA 比例是 6。专用版本使用 BLOCK_M=32,每个工作组处理 5 token x 6 heads = 30 个有效行,末尾两行屏蔽。这样既覆盖 6:1 的映射,又避免通用映射中重复或无效的输出工作。
这次优化后的 Profile 很直观:同工作负载 GPU kernel 总时间从约 183.3 s 降到 134.7 s,Attention 从 69.3 s 降到 20.3 s,下降约 70.6%。GEMM 绝对时间仍在 81.6 s 左右,所以它的占比反而升到 60.61%。
这就是性能优化经常出现的“热点迁移”:不是 GEMM 变慢了,而是 Attention 被压下去后,GEMM 成了新的主角。
3. 为什么双阶段流水不能全局打开
专用 kernel 之后,我们尝试用 tl.range(..., num_stages=2) 做 KV tile 软件流水,并延迟 V tile 加载,减少 K 和 V 同时存活的时间。
一开始很容易产生一个直觉:既然阶段 2 能重叠访存和计算,那就所有 Prefill 都开。实际不是这样。短 KV、尾 chunk 和长 KV 完整 chunk 的资源条件完全不同:
- 短 KV 没有足够迭代去摊薄流水启动成本;
- 尾 chunk 的 Query 工作量小,增加寄存器和 stage 可能得不偿失;
- 长 KV 完整 chunk 才有稳定的访存重叠空间。
最终采用的分派是:
q_len >= 3072 and max_kv_len > 8192 -> stage 2
其他 Prefill 形状 -> stage 1
Decode -> 原路径
这个条件不根据“数据集是 16K 还是 32K”判断,而是看当前 kernel 的真实 q_len 和 kv_len。一条 32K 请求的前部 chunk 仍可能走 stage 1,进入长 KV 区间后再切换到 stage 2。
4. Q10 Query 复用:真正减少 K/V 重复读取
Page-aware 实验失败后,我们重新看了一遍 kernel 的数据复用关系。
旧配置是:
BLOCK_Q = 5
BLOCK_M = 32
num_warps = 4
一个 program 只服务 5 个 Query token。对于同一个 K/V tile,不同 Query program 会重复加载。于是我们把每个 program 覆盖的 Query 翻倍:
BLOCK_Q = 10
BLOCK_M = 64
num_warps = 4
它没有改变 Attention 数学,只是让同一批 K/V tile 服务更多 Query。真实形状微基准结果如下:
q_len x kv_len |
Q10 相对旧路径 |
|---|---|
4096 x 8K |
1.6220x |
4096 x 16K |
2.0015x |
4096 x 32K |
1.9750x |
3072 x 16K |
1.9693x |
512 x 32K |
1.4811x |
五组输出全部逐位一致。
服务层第一次 8K-16K 前五条只提升 1.11%,第五条 TTFT 还有波动。我们没有立刻宣布成功,而是在同一服务上复跑:稳态吞吐从对照的 11.574609 提升到 12.319985 tok/s,增幅 6.44%。随后三档结果为:
| 桶 | 对照 | Q10 | 提升 |
|---|---|---|---|
| 4K-8K | 12.400507 | 12.606487 | 1.66% |
| 8K-16K | 11.574609 | 12.319985 | 6.44% |
| 16K-32K | 10.174793 | 11.403174 | 12.07% |
按 20%/50%/30% 加权,11.319844 -> 12.102242 tok/s,提升约 6.91%。收益随上下文增长而扩大,和“完整 chunk 越多,K/V 复用越充分”的预期一致。
5. Q20 不是 Q10 的简单放大版
Q10 有效后,我们继续测试:能不能把每个 program 的 Query 数再翻倍?
候选包括:
Q20 / M128 / 4 warps
Q20 / M128 / 8 warps
Q21 / M128 / 4 warps
结果很有代表性。Q20/W8 在长 KV 完整 chunk 上很好:
| 形状 | 相对 Q10 |
|---|---|
4096 x 16K |
1.4446x |
4096 x 32K |
1.4408x |
3072 x 16K |
1.4917x |
但它在 4096 x 8K 和 512 x 32K 上回退。全形状几何平均虽然达到 1.1959x,却没有通过“任一案例不得明显回退”的门禁。
根因也不难理解:BLOCK_M=128 会扩大 Query、Softmax 状态和 Attention 累加器,寄存器压力显著增加。长 KV 时减少 K/V 重复加载的收益足够大;短 KV 或小尾块没有那么多加载可省,资源成本就暴露出来了。
我们没有全局打开 Q20,而是把它限制到:
基础 Q10 已开启
q_len >= 3072
max_kv_len > 8192
pipeline stage == 2
其他情况继续走 Q10。服务稳态结果为:
| 桶 | Q10 | Q20 混合分派 | 增量 |
|---|---|---|---|
| 4K-8K | 12.606487 | 12.629447 | 0.18% |
| 8K-16K | 12.314362 | 12.505853(两轮合并) | 1.56% |
| 16K-32K | 11.403174 | 11.798635 | 3.47% |
增量加权吞吐约提升 1.81%。更关键的是,16K-32K 五条 TTFT 都降低约 0.8 s,而 4K-8K 基本不动,说明分派确实只在预期区域生效。
Profile 也给出了同方向证据:专用 Prefill Attention 从 7.631 s 降到 6.248 s,局部约 1.221x,折算为整段 GPU kernel 时间约 1.56% 的收益。
6. Gate/Up GEMV:这次手写 Triton 确实赢了 rocBLAS
专用 Attention 把瓶颈推向线性层后,最明显的固定形状是:
N=1, M=34816, K=5120, BF16, no bias
这是每个 Decode Token 在 64 层中重复的 MLP Gate/Up 投影。通用 GEMM 库需要覆盖大量矩阵形状,而 N=1 更像 GEMV。我们写了 gfx936 专用 Triton 路径,使用 FP32 累加,并加上严格保护:
device == gfx936
dtype == BF16
N == 1, M == 34816, K == 5120
bias is None
x/weight contiguous
单算子从约 0.501 ms 降到 0.330 ms,约 1.52x。三档前五条服务结果为:
| 桶 | 原路径 | Gate/Up Triton | 提升 |
|---|---|---|---|
| 4K-8K | 10.8661 | 12.4005 | 14.12% |
| 8K-16K | 9.8869 | 11.5746 | 17.07% |
| 16K-32K | 9.1149 | 10.1748 | 11.63% |
加权吞吐 9.8511 -> 11.3198 tok/s,提升约 14.91%。TPOT P99 分别从 69.121/70.674/72.134 ms 降到 57.742/59.200/60.705 ms。
后来我们又扫了六组 Triton 配置。当前 M8/K256/W4 为 0.320097 ms,最快候选 M32/K128/W8 是 0.310118 ms,只快 3.22%,而且不再逐位一致。按当时 17.54% 的热点占比估算,整机上限只有约 0.55%。我们没有为了这半个点继续改。
7. 为什么同一个 Triton GEMV 模板不能到处套
Gate/Up 成功后,最自然的想法是把模板复用到其他 N=1 线性层。结果给了我们一盆很有价值的冷水。
| 形状 | rocBLAS | Triton | 结果 |
|---|---|---|---|
Down 5120x1x17408 |
0.152983 ms | 0.161243 ms | 0.9488x,放弃 |
lm_head 248320x1x5120 |
1.901581 ms | 2.129770 ms | 0.8920x,放弃 |
GDN Output 5120x1x6144 |
0.051207 ms | 0.055234 ms | 0.9271x,放弃 |
形状不同,权重规模、归约长度、输出行数、cache 行为和并行度都不同。N=1 只是共同点,不是性能保证。
GDN QKVZ 的早期 Triton 版本单算子倒是很亮眼:0.241954 -> 0.153135 ms,达到 1.58x。但服务稳态总输出从 887 变为 775,只有 3/5 文本逐字一致,吞吐还低 0.43%。即使 TPOT 改善了 6.26%,我们仍然撤销。比赛看的是完整服务结果,不是单个 kernel 的秒表。
8. Page-aware:微基准赢了,服务却输了
这次失败很值得单独写。
服务的 Attention page size 是 784,KV tile 是 32。一个 tile 最多跨一个 page,于是我们尝试把每个 lane 的整数除法、取模和重复块表读取,改成每个 tile 只算一次页号,再用比较和减法映射偏移。
微基准结果不错:
8K : 1.0356x
16K : 1.1197x
32K : 1.1240x
输出逐位一致,按权重估算约 1.1044x。但进入 8K-16K 前五条后:
吞吐 11.574609 -> 11.339427 tok/s,下降 2.03%
TTFT P99 6591.610 -> 8501.262 ms,上升 28.97%
TPOT P99 59.200 -> 59.144 ms,几乎不变
问题在测试覆盖。首版微基准偏向 q_len=512 尾块,没有充分覆盖主导流量的 q_len≈4096 完整 chunk。省下的整数地址计算,抵不过完整块上的额外指令和寄存器压力。
这次之后,我们给所有 Prefill 微基准加了一条硬规则:必须覆盖 4096/3072/512 三类 Query 长度,并同时覆盖短、中、长 KV。
9. rocBLAS 调优:跑了八分半,solution 还是没赢默认路径
通用 GEMM 已占 Profile 的一半以上,我们当然试过 rocBLAS tuning。
Gate/Up Prefill 形状为:
M=34816, N=4096, K=5120, BF16, compute=FP32
rocblas-gemm-tune 一开始五分钟没有任何输出,我们误以为卡住。后来把权限和输出缓冲问题处理好,给它 30 分钟超时,实际约 8 分 34 秒返回:
solution_index = 20981
随后交替测默认 solution 0 和 20981:
solution 0 4317.90 us
solution 20981 4327.15 us
调优器找到的 solution 并没有赢,差异甚至在噪声范围内。
其他形状也类似:
| 形状 | tuned 相对默认 |
|---|---|
Decode Down 5120x1x17408 |
+0.02% |
GDN QKVZ 16384x1x5120 |
约 0% |
Prefill Down 5120x4096x17408 |
+1.05% |
Prefill GDN Output 5120x4096x6144 |
+0.13% |
我们把 5% 设为局部门槛,这几组全部不进入服务。不是 tuning 工具没用,而是当前库的默认启发式已经选到了几乎相同的实现。把一个 1.05% 的单 GEMM 收益接进 vLLM,最后很可能连测量噪声都盖不住。
10. Gate/Up 与 SwiGLU 融合:理论正确,收益不够
Gate/Up GEMV 后面紧跟 SiluAndMul。融合后可以少写一次 34816 维中间结果、少读一次,并减少一次 kernel launch。这个方向从结构上完全说得通。
实际测试中:
当前 GEMV + 原生 SiluAndMul: 0.339072 ms
最快融合 kernel: 0.320438 ms
局部加速: 1.0581x
最大绝对误差是 0.03125,allclose 和确定性通过,但不逐位一致。按 Gate/Up 路径 25.37% 的占比估算,整机理论收益约 1.39%。
我们最后没有接入。原因不是 1.39% 没价值,而是它不足以覆盖跨层数值变化、共享 MLP 接口改造和完整精度验证的风险。性能优化不能只算省了几次访存,还要算工程成本和出错半径。
11. 中篇小结:形状感知比“全局最优”更重要
这一阶段留下来的经验可以归纳为四条:
- 专用 kernel 必须建立在固定、反复出现的真实形状上。 Gate/Up 赢了,不代表 Down 和 lm_head 也会赢。
- 微基准必须覆盖服务中的完整 shape family。 Page-aware 的失败,本质上是测试只看了容易赢的一部分。
- 局部最优不等于服务最优。 GDN QKVZ Triton 单算子 1.58x,最终却改变输出并拖慢吞吐。
- 允许按张量形状回退,往往比寻找一个全局参数更有效。 Q20 只有在长 KV 完整 chunk 上才值得启用。
下篇会继续讲这些候选如何进入工程:环境变量为什么默认关闭、如何保证异常情况下恢复 BLAS backend、怎样做吞吐和精度门禁,以及我们在部署、hipprof、E-Shell 和容器重建上踩过的坑。
系列导航
- 上篇:环境、基线、分块负载与干净 Profile
- 中篇:专用 Prefill Attention、Query 复用与 GEMM 筛选(本文)
- 下篇:默认关闭、严格回退、精度门禁与可复现部署
建议标签: DCU、ROCm、HIP、Triton、vLLM、GEMV、Attention优化
更多推荐



所有评论(0)