开场

在海光 DCU 上给大模型做 profile,最难的通常不是“能不能生成 trace”,而是 trace 生成以后该怎么看。

一次 Qwen3.5-27B BF16 本地采样共产生:

1
2
3
4
总事件:       235,964
GPU 事件: 61,804
kernel 事件: 60,124
GPU event 总时长: 5773.493 ms

直接按 kernel 总耗时排序,只能看到一长串 Tensile、Triton、Attention 和自定义 kernel 名称。它不会主动告诉你:

  • 哪些属于 Prefill;
  • 哪些属于 Decode;
  • 一个 kernel 每 token 调用了多少次;
  • 调用次数能否对应回模型层数;
  • 哪个时间桶才是真正值得优化的墙。

本文不讨论某个新 kernel,而是用这份海光 DCU 实测 trace,演示怎样从事件上下文和调用计数还原完整推理结构。


1. 先限制 Profile 的覆盖范围

这次采样只覆盖一个有明确边界的请求:

1
2
3
3 个 q=4096 Prefill chunk
1 个 q=1685 Prefill tail
60 个 Decode step

这是一个很重要的前提。

如果 profile 覆盖多个并发请求、模型加载、首次编译和长时间生成,事件数量会迅速膨胀,而且很难把某个 kernel 归属到具体阶段。

对海光 DCU 大模型服务,更可靠的流程是:

1
2
3
4
5
服务先完成加载和必要 warmup
-> 开启 profiler
-> 只发送一个有代表性的请求
-> 等请求完整结束
-> 立即停止并保存 trace

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
2
2586.460 ms / 60
= 43.108 ms/token

这一步比直接看 top kernel 更重要。

同一个 GEMM 名称可能同时出现在 Prefill 和 Decode,但两个阶段的 M 维、并行度和优化方法完全不同。只有先按上下文切开,后面的统计才有物理意义。


3. 第二刀:用调用次数识别模型结构

3.1 LLMM1 kernel

Decode 区间中,LLMM1 总计:

1
2
10,560 launches
1,408.219 ms

除以 60 个 Decode step:

1
2
10,560 / 60 = 176 launches/token
1,408.219 / 60 = 23.470 ms/token

176 不是一个随机数字,它可以完整映射回模型结构:

1
2
3
4
5
6
64 次 MLP gate/up
48 次 GDN qkvz
48 次 GDN BA
16 次 Full-Attention QKV
-------------------------
176 次/token

当 trace 调用数与模型层数完全闭合时,说明 kernel 归因基本可信。

3.2 rocBLAS/Tensile GEMV

同一 Decode 区间中,rocBLAS/Tensile GEMV 为:

1
2
7,740 launches
876.431 ms

换算后:

1
2
7,740 / 60 = 129 launches/token
876.431 / 60 = 14.607 ms/token

129 同样可以映射回模块:

1
2
3
4
5
6
64 次 MLP down projection
48 次 GDN output projection
16 次 Attention output projection
1 次 lm_head
--------------------------------
129 次/token

这类“调用计数闭环”很有价值。它不仅告诉我们 kernel 热不热,还能检查:

  • 是否漏统计了某类 Linear;
  • 某个融合是否真正减少了 launch;
  • runtime marker 与实际调用数是否一致;
  • 模型结构和 trace 是否来自同一版本。

4. 第三刀:把时间换算成每 token 预算

LLMM1 和 rocBLAS 合计:

1
2
23.470 + 14.607
= 38.078 ms/token

相对于 43.108 ms/token 的 Decode:

1
2
38.078 / 43.108
= 88.33%

这说明该次海光 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
2
3
4
5
Prefill:
大 M GEMM + 长序列 Attention

Decode:
M=1 Linear/GEMV + 状态读取

因此,在 trace 分析中,不能把“Linear 占 Decode 88.33%”理解成整次请求的统一占比,也不能用 Decode 的 M=1 kernel 去解释 q=4096 Prefill。

正确做法是分别建立两个预算:

1
2
TTFT 预算 -> Prefill 上下文
TPOT 预算 -> Decode 上下文

6. 为什么只看 kernel 排行会误判

假设 top kernel 列表中第一名是 LLMM1,第二名是某个 Tensile kernel。

如果不做上下文拆分,至少会遇到三个问题。

6.1 相同名称可能对应不同 shape

同一库 kernel 可能被多个 Linear 模块复用。需要结合 M、N、K、调用次数和父事件,才能映射到模型层。

6.2 总时间高可能只是调用次数多

一个 130 us 的 kernel 调用一万次,会比一个 5 ms kernel 更靠前。优化时要同时看:

1
2
3
4
单次耗时
调用次数
覆盖层数
端点时间占比

6.3 Profile 自身不是端点 benchmark

Profiler 会引入记录、同步和缓冲开销。Trace 中的 43.108 ms/token 用于建立时间构成,不能直接替代无 profiler 的服务 TPOT。


7. 一次真实的 Profiler 退出故障

这次目标请求已经正常完成,trace 文件也成功写出;但调用 stop_profile 后,ROCm runtime 触发 VMFault,EngineCore 随后退出。

因此,这次运行的处理方式是:

1
2
3
4
5
6
7
8
已写出的 trace:
作为诊断证据保留

profiled request 的吞吐和 TPOT:
不作为端点性能证据

发生故障后的服务:
不继续复用做 benchmark

这个边界非常重要。

“停止 profiler 时服务崩溃”不代表之前写出的所有 event 都无效;但它说明 profiler 生命周期已经改变了服务状态,不能继续拿同一进程做性能 A/B。

在海光 DCU 上采集长 trace 时,建议把 profiler 服务视为一次性诊断实例。


8. 一套可复用的海光 DCU Trace 分析流程

第一步:固定请求

选择一个能够覆盖目标 Prefill shape 和足够 Decode step 的单请求。

第二步:记录覆盖范围

明确写出:

1
2
3
4
Prefill chunk 数量与 token 数
Decode step 数量
是否包含首次编译
是否包含模型加载

第三步:按上下文分桶

至少分成:

  • 大块 Prefill;
  • 尾部 Prefill;
  • Decode;
  • execute context 之外的采样和拷贝。

第四步:统计 kernel count 和 total time

不要只保留 top-N 时间,还要保留 count 和平均耗时。

第五步:除以 Decode step

把总调用数和总耗时换算成:

1
2
launches/token
ms/token

第六步:映射回模型层

检查调用数能否由 MLP、GDN、Attention 和 lm_head 层数完整相加。

第七步:计算物理上限

对候选优化估算:

1
2
3
maximum endpoint saving
= current bucket time
× removable fraction

第八步:另起干净服务做 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
2
3
4
176 次 LLMM1/token
129 次 rocBLAS GEMV/token
38.078 ms Linear/token
88.33% Decode 占比

当调用数、层数和时间全部闭合时,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 服务的最终吞吐。