4、算子调度日志:识别算子启动、执行、同步三个阶段,定位哪个阶段最耗时

算子调度日志,说白了就是GPU干活时的流水账。

它把每个算子的执行过程拆成了三段:启动、执行、同步。我刚开始调性能时,总盯着执行时间看,觉得那才是算力核心。后来发现,很多时候瓶颈根本不在执行,而在启动和同步上。

4.1 三个阶段到底在干什么?

咱们先捋清楚每个阶段的意义。你想想看,GPU不是一上来就闷头算的,它得先准备一下。

  • 启动(Launch):CPU把kernel指令发出去,告诉GPU“嘿,该干活了”。这个阶段主要花在指令下发和参数传递上。
  • 执行(Execute):GPU真正开始算。矩阵乘、卷积、激活函数,都在这里跑。
  • 同步(Sync):CPU等GPU算完,或者等数据从显存拷回来。说白了就是“你好了没?好了我就继续”。

我在项目中遇到过一种情况:一个算子的执行时间只有0.2ms,但启动花了0.8ms,同步又花了1.5ms。整个算子耗时2.5ms,执行只占8%。你说这算力利用率能高吗?

4.2 怎么从日志里抓出这三个阶段?

大多数深度学习框架都会输出调度日志。以PyTorch为例,开启TORCH_SHOW_CPP_STACKTRACES=1或者用profiler,就能看到类似下面的信息:

# 典型的算子调度日志片段
[2025-03-15 10:23:45.123] [launch] kernel: aten::mm, stream: 7, grid: (128,128,1), block: (256,1,1)
[2025-03-15 10:23:45.124] [execute] kernel: aten::mm, device: cuda:0, duration: 0.45ms
[2025-03-15 10:23:45.126] [sync]     kernel: aten::mm, stream: 7, sync_type: cudaDeviceSynchronize, duration: 1.23ms

嗯,这里要注意:日志里的时间戳是连续的。启动和同步的时间差,就是执行时间。但很多工具已经帮你算好了,直接看三列就行。

我个人习惯用NVIDIA Nsight Systems来抓调度日志。它会把每个算子的三个阶段用不同颜色标出来,一目了然。比如:

算子名称 启动耗时 (μs) 执行耗时 (μs) 同步耗时 (μs) 总耗时 (μs)
aten::mm 820 450 1230 2500
aten::conv2d 150 3200 80 3430
aten::relu 60 120 50 230

看到没?aten::mm的同步时间比执行时间还长。这就是典型的“同步瓶颈”。

4.3 怎么定位哪个阶段最耗时?

方法其实很简单:算占比。

对于每个算子,计算三个阶段各自占总耗时的百分比。然后排序,看哪个阶段占比最高。

我一般这么干:

  1. 先看同步占比。如果超过30%,说明CPU和GPU之间在频繁等待。常见原因是:cudaDeviceSynchronize调用过多,或者数据拷贝没做好异步。
  2. 再看启动占比。如果超过20%,说明kernel太小了。启动开销摊不平执行时间。我遇到过一个小算子,启动花了1ms,执行才0.1ms,这种就该考虑算子融合。
  3. 最后看执行占比。如果执行占比很高(比如80%以上),那说明算力用满了。这时候瓶颈在计算本身,得从算法层面优化。

核心判断逻辑:

  • 同步占比高 → 减少同步点,用异步流
  • 启动占比高 → 算子融合,减少kernel数量
  • 执行占比高 → 优化算法,或者换更快的硬件

4.4 避坑指南:我曾经踩过的坑

我曾经在优化一个BERT模型时,发现同步占比高达60%。我以为是数据拷贝的问题,折腾了半天异步传输,结果一点用没有。

后来仔细看日志才发现,问题出在Python侧的GIL上。框架在每次forward之后都隐式调了torch.cuda.synchronize()。说白了,框架自己加的同步点,不是我能控制的。

注意:有些框架的调度日志不会显式打印同步阶段,而是把同步时间算进了启动阶段。这时候你得看cudaEventSynchronize或者cudaStreamSynchronize的调用次数。次数越多,同步开销越大。

另一个坑是:启动时间会被CPU调度影响。如果你的机器上CPU负载很高,启动时间会显著增加。我建议在抓日志时,先确保CPU负载低于50%,否则数据不准。

4.5 知识体系:算子调度三阶段的核心逻辑

下面这张图,是我自己总结的算子调度流程。你看一眼就明白了。

算子调度三阶段流程图 启动 (Launch) CPU下发kernel指令 执行 (Execute) GPU计算核心运行 同步 (Sync) CPU等待GPU完成 时间 → 启动瓶颈 kernel太小 指令下发频繁 CPU负载高 执行瓶颈 计算量过大 显存带宽不足 算法效率低 同步瓶颈 频繁同步调用 数据拷贝阻塞 流未充分利用

我的小技巧:抓调度日志时,别只看单个算子。把整个模型的算子按阶段汇总,算一下三个阶段的总耗时占比。很多时候,一个模型里90%的同步时间都集中在几个大算子上。抓住它们,优化就成功了一半。

说白了,算子调度日志就是一张“时间账单”。启动、执行、同步,三笔账。哪笔账最大,就从哪下手。别一上来就想着改算法,先看看是不是同步在拖后腿。

想要更多资料,联系可可