海光 DCU Profile 实战:如何从六万个 GPU kernel 还原一枚 Token
开场
在海光 DCU 上给大模型做 profile,最难的通常不是“能不能生成 trace”,而是 trace 生成以后该怎么看。
一次 Qwen3.5-27B BF16 本地采样共产生:
1 | 总事件: 235,964 |
直接按 kernel 总耗时排序,只能看到一长串 Tensile、Triton、Attention 和自定义 kernel 名称。它不会主动告诉你:
- 哪些属于 Prefill;
- 哪些属于 Decode;
- 一个 kernel 每 token 调用了多少次;
- 调用次数能否对应回模型层数;
- 哪个时间桶才是真正值得优化的墙。
本文不讨论某个新 kernel,而是用这份海光 DCU 实测 trace,演示怎样从事件上下文和调用计数还原完整推理结构。
1. 先限制 Profile 的覆盖范围
这次采样只覆盖一个有明确边界的请求:
1 | 3 个 q=4096 Prefill chunk |
这是一个很重要的前提。
如果 profile 覆盖多个并发请求、模型加载、首次编译和长时间生成,事件数量会迅速膨胀,而且很难把某个 kernel 归属到具体阶段。
对海光 DCU 大模型服务,更可靠的流程是:
1 | 服务先完成加载和必要 warmup |
Profile 的目标是回答“时间花在哪里”,不是复现最高吞吐。
2. 第一刀:按执行上下文切开 Prefill 和 Decode
Trace 中每批 GPU 事件都有上层执行上下文。利用 token 数和 generation 状态,可以先分成:
| 上下文 | GPU event 总时长 |
|---|---|
| 三个 q=4096 Prefill chunk | 2720.145 ms |
| 一个 q=1685 Prefill tail | 464.801 ms |
| 60 个 Decode step | 2586.460 ms |
Decode 平均每步:
1 | 2586.460 ms / 60 |
这一步比直接看 top kernel 更重要。
同一个 GEMM 名称可能同时出现在 Prefill 和 Decode,但两个阶段的 M 维、并行度和优化方法完全不同。只有先按上下文切开,后面的统计才有物理意义。
3. 第二刀:用调用次数识别模型结构
3.1 LLMM1 kernel
Decode 区间中,LLMM1 总计:
1 | 10,560 launches |
除以 60 个 Decode step:
1 | 10,560 / 60 = 176 launches/token |
176 不是一个随机数字,它可以完整映射回模型结构:
1 | 64 次 MLP gate/up |
当 trace 调用数与模型层数完全闭合时,说明 kernel 归因基本可信。
3.2 rocBLAS/Tensile GEMV
同一 Decode 区间中,rocBLAS/Tensile GEMV 为:
1 | 7,740 launches |
换算后:
1 | 7,740 / 60 = 129 launches/token |
129 同样可以映射回模块:
1 | 64 次 MLP down projection |
这类“调用计数闭环”很有价值。它不仅告诉我们 kernel 热不热,还能检查:
- 是否漏统计了某类 Linear;
- 某个融合是否真正减少了 launch;
- runtime marker 与实际调用数是否一致;
- 模型结构和 trace 是否来自同一版本。
4. 第三刀:把时间换算成每 token 预算
LLMM1 和 rocBLAS 合计:
1 | 23.470 + 14.607 |
相对于 43.108 ms/token 的 Decode:
1 | 38.078 / 43.108 |
这说明该次海光 DCU trace 中,Decode 的主要墙是 Linear,而不是某个零散 pointwise kernel。
其他主要 Decode 时间包括:
| 模块 | 每 token 时间 | Decode 占比 |
|---|---|---|
| Page-contiguous Attention | 1.630 ms | 3.78% |
| Packed GDN recurrent | 0.623 ms | 1.44% |
看到这个比例后,优化优先级会变得清晰:
- 一个每层只省 2-3 us 的小融合,很难改变总 TPOT;
- 一个覆盖上百次 Linear 的实现,即使单次只快几个百分点,也可能积累出端点收益;
- 继续优化占比不足 2% 的桶,需要先证明足够大的绝对节省。
Profile 的作用不是自动给出方案,而是约束方案的物理上限。
5. Prefill 不能沿用 Decode 的结论
三个 q=4096 Prefill chunk 的主要时间项为:
| 时间项 | 总耗时 | 占 q=4096 Prefill |
|---|---|---|
| aten::mm | 1840.596 ms | 67.67% |
| Attention | 238.718 ms | 8.78% |
同一个模型在 Prefill 和 Decode 中呈现出不同形态:
1 | Prefill: |
因此,在 trace 分析中,不能把“Linear 占 Decode 88.33%”理解成整次请求的统一占比,也不能用 Decode 的 M=1 kernel 去解释 q=4096 Prefill。
正确做法是分别建立两个预算:
1 | TTFT 预算 -> Prefill 上下文 |
6. 为什么只看 kernel 排行会误判
假设 top kernel 列表中第一名是 LLMM1,第二名是某个 Tensile kernel。
如果不做上下文拆分,至少会遇到三个问题。
6.1 相同名称可能对应不同 shape
同一库 kernel 可能被多个 Linear 模块复用。需要结合 M、N、K、调用次数和父事件,才能映射到模型层。
6.2 总时间高可能只是调用次数多
一个 130 us 的 kernel 调用一万次,会比一个 5 ms kernel 更靠前。优化时要同时看:
1 | 单次耗时 |
6.3 Profile 自身不是端点 benchmark
Profiler 会引入记录、同步和缓冲开销。Trace 中的 43.108 ms/token 用于建立时间构成,不能直接替代无 profiler 的服务 TPOT。
7. 一次真实的 Profiler 退出故障
这次目标请求已经正常完成,trace 文件也成功写出;但调用 stop_profile 后,ROCm runtime 触发 VMFault,EngineCore 随后退出。
因此,这次运行的处理方式是:
1 | 已写出的 trace: |
这个边界非常重要。
“停止 profiler 时服务崩溃”不代表之前写出的所有 event 都无效;但它说明 profiler 生命周期已经改变了服务状态,不能继续拿同一进程做性能 A/B。
在海光 DCU 上采集长 trace 时,建议把 profiler 服务视为一次性诊断实例。
8. 一套可复用的海光 DCU Trace 分析流程
第一步:固定请求
选择一个能够覆盖目标 Prefill shape 和足够 Decode step 的单请求。
第二步:记录覆盖范围
明确写出:
1 | Prefill chunk 数量与 token 数 |
第三步:按上下文分桶
至少分成:
- 大块 Prefill;
- 尾部 Prefill;
- Decode;
- execute context 之外的采样和拷贝。
第四步:统计 kernel count 和 total time
不要只保留 top-N 时间,还要保留 count 和平均耗时。
第五步:除以 Decode step
把总调用数和总耗时换算成:
1 | launches/token |
第六步:映射回模型层
检查调用数能否由 MLP、GDN、Attention 和 lm_head 层数完整相加。
第七步:计算物理上限
对候选优化估算:
1 | maximum endpoint saving |
第八步:另起干净服务做 A/B
Profile 负责定位;无 profiler 的同环境端点负责确认真实吞吐、TTFT、TPOT 和输出一致性。
9. 对海光 DCU 开发者的意义
这份 trace 证明,在 gfx936 本地软件栈中,可以通过 PyTorch/ROCm 兼容的 trace 链路同时观察:
- Tensile GEMM;
- 自定义 HIP kernel;
- Triton kernel;
- CUDAGraph replay;
- Attention 与 GDN 路径;
- Prefill 和 Decode 上下文。
但数据量很大。真正有用的不是生成一个 trace 文件,而是把 60,124 个 kernel event 压缩成可验证的模型结构:
1 | 176 次 LLMM1/token |
当调用数、层数和时间全部闭合时,profile 才从“漂亮的时间线”变成可以指导海光 DCU 优化的工程证据。
10. 结论
从 235,964 条 trace event 中,本地最终还原出:
- 三个 q=4096 Prefill chunk;
- 一个 q=1685 tail;
- 60 个 Decode step;
- 每 token 176 次 LLMM1;
- 每 token 129 次 rocBLAS GEMV;
- Linear 合计 38.078 ms/token;
- Linear 占 Decode 88.33%。
这篇文章的核心不是某个 kernel 排名,而是一种海光 DCU profile 方法:
先按执行上下文切阶段,再用调用次数还原模型结构,最后把总时间换算成每 token 的物理预算。
Profile 用来决定哪里值得做,端点 A/B 用来决定做完是否真的有效。两种证据不能混用。
本文数据来自一次有明确覆盖范围的本地 gfx936 诊断 trace。Profiler 运行具有额外开销,文中 profile 时间只用于算子归因和预算分析,不代表无 profiler 服务的最终吞吐。

