DCU使用技术报告_下篇_gfx936_DCU_Qwen3.5-27B_rocBLAS-hipBLASLt调优、vLLM工程化与踩坑实战
DCU使用技术报告(下篇):gfx936 DCU上的Qwen3.5-27B rocBLAS/hipBLASLt调优、vLLM工程化与踩坑实战
系列文章下篇。前两篇分别讲了基线/Profile 和算子优化,这一篇讨论最后一道门槛:怎样把候选接进 vLLM,同时守住正确性、服务稳定性、部署复现和比赛边界。
0. 先说结论
在大模型推理项目里,kernel benchmark PASS 和 可以合入服务 中间隔着很长一段路。
我们最终采用的优化都有几个共同特征:
- 分派条件只依赖设备、dtype 和张量形状;
- 不支持的情况完整回退原路径;
- 先以默认关闭的环境变量接入;
- 微基准、前五条吞吐、SLA、输出一致性和精度逐级验证;
- 运行日志能证明候选实际命中;
- 部署包和
site-packages可以逐文件核对。
相反,局部再快,只要出现请求失败、长桶回退、输出异常、backend 没恢复,或者收益不到测量噪声,我们就不会保留。
这套方法看起来比“改完直接跑总分”慢,实际节省了很多时间。因为真正贵的不是写几十行 Triton,而是启动 27B 模型、做三档吞吐、跑精度,再发现候选从一开始就没有正确命中。
1. 我们给项目划的边界
性能优化最容易滑向另一个方向:为了分数改调度、改输出长度、减少请求,甚至根据数据集选择路径。我们在项目里明确不碰下面这些东西:
- 模型权重和模型结构;
- 有效计算和 Attention 数学;
- 采样参数与最大输出 Token;
max_num_seqs、max_num_batched_tokens等官方调度设置;- 数据集、请求顺序和请求覆盖;
- 根据 prompt 内容或数据集名称做分派。
允许使用的专用信息只有:
GPU 架构
dtype
张量 shape/stride/contiguous
q_len 与当前 kv_len
是否存在 bias
既有 pipeline 状态
例如长 KV Q20 的分派只看 q_len>=3072、max_kv_len>8192 和 stage 2,不知道请求来自 8K-16K 还是 16K-32K 数据集。
2. 默认关闭、严格保护、原路回退
一个候选第一次接入服务时,我们会给它单独的环境变量,默认值设为关闭。例如当前仍在验证的 GDN QKVZ hipBLASLt 候选:
VLLM_CSCC_QWEN35_GFX936_GDN_QKVZ_HIPBLASLT=0
它的 shape guard 等价于:
def is_target_shape(n, m, k, dtype, bias):
return (
n == 1
and m == 16384
and k == 5120
and dtype == torch.bfloat16
and bias is None
)
实际分派还要同时满足:
flag enabled
device == gfx936
input contiguous
weight contiguous
任何条件不满足都进入原来的 ROCm GEMM 分派。这样做有两个好处:一是候选不会污染其他模型和 Prefill 形状;二是出现问题时只需要关闭一个环境变量,不必重新换整套代码。
候选通过完整验证后,是否改成默认开启要单独决策。Gate/Up Triton、Q10 和 Q20 是在吞吐与精度门禁通过后才进入默认路径;正在验证的 hipBLASLt 候选仍然默认关闭。
3. 为什么我们没有全局切换 hipBLASLt
Profile 显示通用 GEMM 占比超过一半,很容易得出“把 backend 全切到 hipBLASLt”的结论。我们先用三类 Decode 形状做了离线对比:
| 形状 | 当前路径 | hipBLASLt | 相对速度 |
|---|---|---|---|
Decode Down 5120x1x17408 |
150.944 us | 401.968 us | 0.3755x |
Shared Output 5120x1x6144 |
53.030 us | 141.597 us | 0.3745x |
GDN QKVZ 16384x1x5120 |
239.817 us | 209.085 us | 1.1470x |
结论非常清楚:hipBLASLt 不是“整体更快”,只是当前库在 GDN QKVZ 这个固定形状上更合适。若全局切换,两个高频路径会慢到原来的约 2.7 倍。
于是我们只为 16384x1x5120 做 scoped dispatch。更接近服务调用方式的测试中:
GPU kernel speedup 1.1347x
含 backend 切换 wall time 1.1339x
数值 allclose PASS
重复执行确定性 PASS
backend 恢复 PASS
按当时 Profile 占比估算,整段 GPU kernel 时间理论提升约 1.0167x。这不算巨大,但大于噪声门槛,值得进入服务验证。
需要强调当前状态:这个候选已经通过离线 scoped gate 和服务启动/图捕获,但尚未完成稳定的前五条 A/B,因此还不能写成最终采用。
4. 全局 backend 是共享状态,必须保证恢复
PyTorch 提供:
torch.backends.cuda.preferred_blas_library(...)
在 HIP 构建里 API 仍位于 torch.backends.cuda 命名空间。我们只在原 backend 为 cublas 时临时切到 cublaslt,完成一次 F.linear 后恢复:
original_backend = torch.backends.cuda.preferred_blas_library()
torch.backends.cuda.preferred_blas_library(backend="cublaslt")
try:
output = torch.nn.functional.linear(x, weight)
finally:
torch.backends.cuda.preferred_blas_library(backend="cublas")
finally 不是装饰。如果 F.linear 抛异常而 backend 没恢复,后续所有线性层都可能被悄悄切到 hipBLASLt,前面两个 0.37x 的慢路径也会跟着生效。
我们为此专门加了测试:
- 精确目标 shape 应命中;
- N/M/K、dtype、bias 任一变化都应拒绝;
- 原 backend 不是 cublas 时不进入候选;
- 正常返回后调用顺序必须是
cublaslt -> cublas; - 异常路径也必须由
finally恢复。
服务启动时出现过下面的警告:
torch.backends.cuda.preferred_blas_library is an experimental feature
这条警告本身不是失败。真正的门禁是图捕获能否完成、API 能否启动、正式请求是否报错,以及 backend 是否影响其他形状。
5. 从离线 PASS 到服务 PASS,要过五道门
我们的候选验证顺序是固定的。
5.1 第一关:单算子数值和计时
离线脚本至少检查:
- 与当前服务路径的误差;
- 多次执行是否确定;
- 交替运行候选和对照,避免测试顺序偏差;
- 多轮中位数,而不是只取一次最好结果;
- 覆盖服务会出现的 shape family。
Attention 分块候选要求逐位一致。GEMM 若累加顺序不同,可以先用 allclose,但进入服务后必须继续核对输出与精度。
5.2 第二关:服务启动和运行时命中
能 import 不代表能跑,能启动也不代表候选命中。日志必须出现类似:
CSCC Qwen3.5 dedicated prefill attention kernel selected.
CSCC Qwen3.5 gfx936 gate/up decode Triton GEMV selected.
CSCC Qwen3.5 gfx936 GDN QKVZ decode hipBLASLt selected.
Application startup complete.
同时不能有 Traceback、图捕获失败或服务进程退出。
5.3 第三关:单请求冒烟
先跑 8K-16K 第一条,要求:
failed=0
API 仍然存活
输出长度与对照一致
服务日志无新错误
单请求只用于排除崩溃,不用于判断性能。
5.4 第四关:固定前五条吞吐和 SLA
正式筛选始终是前五条,不用三条,也不随机抽样。每个候选写入独立 RESULT_ROOT,比较:
output_throughput
p99_ttft_ms
p99_tpot_ms
failed
output_lens
逐请求生成文本
三档加权公式为:
weighted = throughput_4_8 * 0.20
+ throughput_8_16 * 0.50
+ throughput_16_32 * 0.30
低于 1% 默认按噪声处理;出现失败请求、长桶明显回退或 SLA 恶化则直接拒绝。小幅正收益先复跑,达到稳定门槛才进入完整测试。
5.5 第五关:精度
吞吐通过后才运行精度脚本。Q10 和 Q20 采用前的本地前五条精度门禁结果为:
| 数据集 | 结果 |
|---|---|
| HotpotQA | 80.00 |
| GovReport | 37.01 |
| Retrieval Multi Point(重算) | 100.00(5/5) |
| Keyword Aggregation(重算) | 100.00(5/5) |
Q20 与 Q10 的四项结果完全一致。这里必须把话说严谨:这是本地 all 5 门禁,不应该冒充完整官方评测成绩。它证明候选没有在当前固定样本上破坏精度,完整吞吐和完整精度仍应在最终提交前执行。
6. 输出吞吐不是单纯的“每 Token 更快”
这次项目里有一个很容易误判的案例。
早期 GDN QKVZ Triton 候选让 TPOT P99 改善约 6.26%,单算子更是达到 1.58x。但候选前五条总输出为 775 Token,对照是 887 Token;最终输出吞吐反而低 0.43%,只有三条文本逐字一致。
输出 Token 数会受生成内容和 EOS 位置影响。一个数值路径稍有变化,可能让模型更早停止,也可能让输出变长。单看 tok/s 有时会被输出长度变化误导,所以我们同时核对:
总生成 Token
每条 output_lens
生成文本
TPOT
TTFT
总 benchmark duration
Gate/Up Triton 的部分文本也并非全部逐位一致,但精度门禁没有下降,且吞吐与 TPOT 收益足够大,因此进入后续验证。不同候选需要根据数值变化和收益大小做风险判断,不能只用一个指标盖章。
7. 部署时最容易出错的不是代码,而是文件组合
我们有一次部署 GDN 候选,只复制了:
envs.py
utils.py
cscc_qwen35_gemv.py
服务加载后才发现安装目录里的 platforms/rocm.py 还是旧版本,没有 on_gfx936。候选甚至还没执行,就在导入分派函数时失败。
另一轮更隐蔽:GDN 文件部署成功,但专用 Prefill Attention 文件没有复制。结果 TPOT 明显改善,TTFT 却退回原始慢路径。如果只看加权吞吐,很容易把“部署不完整”误判成“GDN 候选拖慢 Prefill”。
后来每个候选包都列出完整依赖,并用下面三步验证:
python3 -m py_compile <全部修改文件>
cmp -s "$SRC/$F" "$SITE/$F" || exit 1
cd /tmp && python3 -c "import vllm; print(vllm.__file__)"
服务日志中的命中标记则是第四步。文件一致、import 正确、运行时命中,这三件事缺一不可。
8. TunableOp 和 rocBLAS 工具为什么会“长时间没反应”
PyTorch TunableOp 的:
max_tuning_duration=5
max_tuning_iterations=10
不等于整个 Python 脚本五秒结束。它通常限制单个算子或单轮候选搜索,而一个脚本可能包含多种 backend、多个 shape、初始化、正确性检查和正式计时。我们曾经等了十多分钟,终端只显示开头几行,看起来像卡死。
rocblas-gemm-tune 也会长时间沉默。五分钟 timeout 只能说明“搜索还没完成”,不能证明方向无效。后来给 Gate/Up Prefill 形状 30 分钟窗口,工具实际约八分半返回 solution 20981。随后复测发现它没有赢默认 solution,这才是放弃的依据。
工具长时间无输出时,我们现在会做三件事:
- 看 GPU 是否持续有负载;
- 看进程 CPU 时间和状态是否变化;
- 给明确 timeout,但把 timeout 解释为“本轮没完成”,不是“候选失败”。
9. hipprof、后台进程和 Web 终端的生存问题
性能报告最后往往会漏掉这些细节,但它们确实占了我们不少时间。
9.1 旧数据库必须处理
同名 profile 输出会报:
File retained_xxx.db already exists
要么换唯一名称,要么确认旧结果已经备份后再删除。不要反复执行同一条命令,错误不会自己消失。
9.2 Profile 导出要优雅停止
session-client --stop 后,hipprof 可能还要整理数据库和导出 CSV。我们曾看到命令停在 hipprof_pid=... 几分钟,最后正常得到:
144M .db
19K hipkernel.csv
1.8K hiptrace.csv
只要进程仍在做导出,就不要马上 kill -KILL。
9.3 WebShell 断线不一定是容器崩了
SCNet E-Shell 偶尔提示实例停止并自动 logout。重新连接后,vLLM、EngineCore 和 96% GPU 显存占用仍然存在,API 也能响应。这类情况更像 WebSocket/PTY 中断。
长服务和 benchmark 后来都改为 nohup,日志和 PID 写到文件。浏览器断开不会让整轮实验报废,也不会因为看不到终端1而贸然杀进程。
9.4 清卡时只杀目标进程
服务、EngineCore、hipprof 和 standalone GEMM 测试不能同时抢一张卡。清理时按明确 PID 或进程组处理,先 TERM 等待释放显存,再用 KILL 处理残留。不要用会扫到无关 Python 进程的宽泛命令。
10. 当前保留栈与候选状态
截至本文整理时,已经通过吞吐和本地精度门禁、保留在项目中的优化包括:
| 优化 | 状态 |
|---|---|
| Qwen3.5 专用 Prefill Attention | 已保留 |
| 长 KV Chunk-aware 双阶段流水 | 已保留 |
| Q10 Query 复用 | 已保留 |
| 长 KV 完整 chunk Q20 复用 | 已保留 |
| gfx936 Gate/Up Triton GEMV | 已保留 |
| GDN QKVZ scoped hipBLASLt | 离线与启动门禁通过,服务 A/B 待完成 |
保留栈的干净 8K-16K Profile 中:
通用 GEMM 56.57%
Gate/Up GEMV 25.78%
Prefill Attention 7.13%
Gated DeltaNet 3.89%
Decode Attention 2.76%
下一步如果继续优化,优先级应该放在线性层的固定形状选择、合法的算子融合和减少 backend/launch 开销,而不是继续微调已经降到 7% 的 Prefill Attention。
11. 一套可以复用到其他 DCU 项目的清单
如果换成另一张 DCU、另一个 vLLM 版本或另一个模型,我会按下面顺序重新来一遍,而不是复制现成阈值:
- 记录设备 gfx、显存、DTK、PyTorch/HIP 和 vLLM 版本;
- 确认仓库、安装包和运行 import 路径;
- 固化官方调度和 benchmark 参数,建立独立结果目录;
- 丢弃编译预热,获取可重复基线;
- 只采正式推理窗口的 HIP Profile;
- 按 kernel 名、调用次数和累计 GPU 时间映射到源码;
- 为固定 shape 写离线微基准,覆盖完整 shape family;
- 候选默认关闭,严格 guard,保留原 fallback;
- 依次过启动、冒烟、前五条、三档权重、SLA 和精度门禁;
- 记录失败方向,避免下一轮又从头试一遍。
12. 最后一点体会
这次做 DCU 推理优化,最容易上瘾的是看一个 kernel 从 0.50 ms 变成 0.33 ms。真正难的却是后面的判断:它一层有多少次调用,是否命中 CUDA Graph,是否改变生成路径,是否只在某个 chunk 上有效,部署到另一个容器后会不会悄悄回退。
我们留下来的并不是一组“神奇参数”,而是一条比较笨但可靠的路线:Profile 找热点,真实形状做微基准,局部候选用 guard 接入,服务结果决定去留,精度最后否决。
DCU、ROCm、Triton 和 vLLM 的版本都会变化,今天的最佳 BLOCK_M 明天可能不再成立。但这条验证路线可以复用。对性能工程来说,这比某一个 solution index 更值钱。
系列导航
- 上篇:环境、基线、分块负载与干净 Profile
- 中篇:专用 Prefill Attention、Query 复用与 GEMM 筛选
- 下篇:默认关闭、严格回退、精度门禁与可复现部署(本文)
参考阅读: 海光破壁:从海光专属栈到通用 AI 模型栈的开荒
建议标签: DCU、ROCm、HIP、vLLM、Qwen3.5、性能工程、大模型部署
更多推荐



所有评论(0)