3、计算瓶颈定位:利用Nsight Systems识别kernel耗时、分析SM利用率、定位低效算子

好,咱们进入第三个实战环节。前面我们聊了数据搬运和显存瓶颈,说白了都是「IO受限」的问题。但很多时候,你的GPU算力根本没跑满,这时候就得盯着计算单元了。

我个人习惯,拿到一个训练任务,先跑一轮Nsight Systems看看全局。如果发现kernel执行时间占比很高,但SM利用率却上不去,那八成是计算瓶颈。嗯,这里要注意,SM利用率低和kernel耗时长,这两个指标有时候是矛盾的——你得会看。

3.1 用Nsight Systems抓kernel耗时

先说说怎么快速抓到耗时的kernel。我一般这么操作:

nsys profile -o my_profile -t cuda,nvtx --stats=true python train.py

跑完之后,用 nsys stats 或者直接打开GUI看。我最关注的是 CUDA Kernel Summary 这张表。它会列出每个kernel的调用次数、平均耗时、总耗时占比。

Kernel名称 调用次数 平均耗时(us) 总耗时占比
gemm_128x128x128 4,521 342.1 38.2%
softmax_f32 2,130 89.5 4.7%
layer_norm_f32 1,892 67.3 3.2%

你看,gemm(矩阵乘法)占了将近40%的时间。这基本就是大头了。但别急着优化——先看看SM利用率再说。

3.2 分析SM利用率:到底有没有跑满?

SM利用率这个指标,很多人会误解。它不是说你的计算单元100%在工作,而是指至少有一个warp在调度的时间占比。

举个例子。我曾经优化过一个Attention模块,kernel耗时很长,SM利用率显示85%。我心想不错啊,挺高的。结果仔细一看,计算吞吐量只有理论峰值的30%。

为什么会这样?因为SM利用率高,不代表计算单元在干正事。可能是大量warp在等待数据、等待同步、或者在做低效的访存。说白了,就是「看起来很忙,实际产出很低」。

核心判断标准:

  • SM利用率 > 80% + 计算吞吐接近理论峰值 → 计算瓶颈,需要优化算法或算子
  • SM利用率 > 80% + 计算吞吐很低 → 访存瓶颈或同步开销大
  • SM利用率 < 50% + kernel耗时很长 → 负载不均衡或warp发散严重

我个人习惯在Nsight Systems里打开 GPU Utilization 和 Compute Workload Analysis 两个视图。前者看整体,后者看细节。

3.3 定位低效算子:从宏观到微观

找到耗时的kernel之后,下一步就是定位具体哪个算子效率低。我一般分三步走:

  1. 看算子类型:是矩阵乘?是softmax?是LayerNorm?不同类型的算子,优化方向完全不同。
  2. 看计算密度:计算量除以访存量。如果这个值很小(比如 < 10),那大概率是访存瓶颈,不是计算瓶颈。
  3. 看warp发散:在Nsight Compute里打开 Warp Stall Reasons,看看warp在等什么。

一个小技巧:

我经常用 nsys profile --trace=cuda,nvtx --gpu-metrics-device=all 来抓更细粒度的GPU指标。这样能看到每个kernel的SM活跃周期、指令吞吐、L1/L2命中率等。

举个例子。有一次我定位一个Transformer的FeedForward层,发现gemm kernel的SM利用率只有55%。打开Nsight Compute一看,warp stall reason 里「long scoreboard」占了40%。这说明warp在等数据从显存加载到寄存器。说白了,就是访存没跟上计算。

解决方案?我用了 tile量化 + 算子融合,把两个连续的gemm合并成一个fused kernel。这样中间结果不用写回显存,直接留在寄存器里。SM利用率直接飙到92%。

3.4 实战:用Nsight Compute做微观分析

Nsight Systems看宏观,Nsight Compute看微观。我一般这么配合使用:

# 先跑Nsight Systems找到热点kernel
nsys profile -o hotspot -t cuda python train.py

# 再针对热点kernel跑Nsight Compute
ncu --kernel-name "gemm_128x128x128" --set full -o kernel_analysis python train.py

打开 kernel_analysis.ncu-rep,我最关注这几个指标:

  • Memory Throughput:实际访存带宽 vs 理论带宽
  • Compute Throughput:实际计算吞吐 vs 理论峰值
  • Occupancy:每个SM上活跃的warp数
  • Warp Stall Reasons:warp停顿的原因分布

注意:

Occupancy不是越高越好。我见过有人把Occupancy调到100%,结果每个warp分到的寄存器太少,反而导致spill到local memory,性能更差。一般来说,Occupancy在60%-80%之间,配合高计算密度,效果最好。

3.5 一张图看懂计算瓶颈定位流程

下面这张图是我自己总结的,每次做性能分析都照着这个流程走一遍,基本不会漏掉关键点。

计算瓶颈定位流程 ① Nsight Systems 宏观分析 抓kernel耗时 & SM利用率 SM利用率高? 是 否 ② 计算吞吐分析 接近理论峰值? ② Warp发散分析 负载均衡? ③ Nsight Compute 微观分析 Occupancy / Stall Reasons / 指令吞吐 ✅ 定位瓶颈 → 制定优化策略 分析步骤 判断节点 输出结果

这张图我每次做性能分析都贴在旁边。从Nsight Systems的宏观数据入手,判断SM利用率高低,然后分两条路走。一条看计算吞吐,一条看warp发散。最后汇聚到Nsight Compute做微观验证。

嗯,这套流程我用了两年多,基本没失手过。你想想看,如果一上来就盯着某个算子调优,很容易陷入局部最优。先看全局,再聚焦,这才是工程思维。

核心总结:

  • Nsight Systems看宏观:kernel耗时、SM利用率、全局热点
  • Nsight Compute看微观:Occupancy、Stall Reasons、指令吞吐
  • SM利用率高 ≠ 计算效率高,要结合计算吞吐一起看
  • 定位低效算子,先看类型,再看计算密度,最后看warp发散

我曾经踩过一个坑:花了两周优化一个gemm kernel,把Occupancy从40%调到85%,结果整体训练速度只提升了3%。后来才发现,真正的瓶颈是后面的softmax算子——它虽然耗时短,但被调用了上万次,累积开销巨大。所以,别只看单个kernel的耗时,要看它在整个训练流程中的调用频率和累积占比。

好了,这一章就到这里。记住,计算瓶颈定位的核心就三句话:宏观抓热点,微观看细节,结合频率算总账。

可可精品资料库