第1章:CUDA性能分析工具——从Profiling开始
做算子优化,最怕什么?
怕你埋头写了三天三夜的kernel,结果一跑,性能还不如人家随便写的。更怕的是——你根本不知道瓶颈在哪。
我个人习惯是:写代码之前,先想好怎么测。写完之后,第一件事不是优化,而是Profiling。说白了,你得先知道你的程序到底在干什么,才能谈优化。
1.1 为什么需要Profiling?
很多同学一上来就盯着Occupancy调,或者拼命压寄存器。嗯,方向没错,但顺序错了。
我曾经接手过一个项目,同事花了两周优化一个算子,Occupancy从25%调到了75%,结果性能只提升了5%。为什么?因为瓶颈根本不在计算单元,而在显存带宽。他白忙活了。
所以,Profiling的意义在于:先定位瓶颈,再对症下药。
核心原则:不要猜测性能瓶颈。让数据说话。
1.2 NVIDIA Nsight Systems —— 宏观视角
Nsight Systems,我习惯叫它「系统级分析器」。它不关心你的kernel内部怎么跑的,它关心的是:你的程序在GPU上到底干了哪些事?
它能告诉你什么?
- CPU和GPU之间的同步开销
- 内存拷贝(H2D、D2H)的时间占比
- Kernel启动的延迟
- 多个Stream之间的并发情况
举个例子。我优化过一个推理管线,发现GPU利用率只有40%。用Nsight Systems一看,好家伙,CPU在等GPU,GPU又在等CPU,两边互相等。说白了,流水线没搭好。调整了Stream和异步拷贝之后,利用率直接拉到85%。
使用技巧:先跑一次Nsight Systems,看看你的程序是不是「计算密集」还是「拷贝密集」。如果是后者,别急着优化kernel,先优化数据传输。
1.3 NVIDIA Nsight Compute —— 微观视角
Nsight Compute,这才是我们算子优化的主战场。它能看到kernel内部的每一处细节。
关键信息一览
| 指标 | 含义 | 我关注的点 |
|---|---|---|
| Occupancy | SM上活跃的warp数量占比 | 不是越高越好,要看实际瓶颈 |
| Memory Throughput | 显存带宽利用率 | 如果低于60%,说明访存模式有问题 |
| Compute Throughput | 计算单元利用率 | 如果低于50%,说明计算没喂饱 |
| L1/L2 Cache命中率 | 缓存利用情况 | 命中率低,考虑合并访存或调整数据布局 |
我记得第一次用Nsight Compute分析一个矩阵乘算子,发现Memory Throughput只有可怜的20%。当时我以为是显存带宽不够,结果一看L1命中率,只有5%。问题出在数据布局上——我用了行主序访问列主序的数据。改成共享内存做转置之后,吞吐直接翻了三倍。
注意:Nsight Compute的Profiling会引入额外开销,跑出来的时间不准。要看真实性能,还是得用nvcc编译后的release版本直接跑。
1.4 关键指标深度解读
Occupancy —— 别被它骗了
很多教程告诉你Occupancy越高越好。其实不一定。
Occupancy高,意味着SM上同时跑的warp多,可以隐藏访存延迟。但如果你的kernel是计算密集型的,Occupancy太高反而会导致寄存器压力大,降低单个warp的计算效率。
我一般这样看:
- 如果Memory Throughput低,Occupancy高 → 访存模式有问题
- 如果Compute Throughput低,Occupancy高 → 计算密度不够,或者指令级并行度低
- 如果两者都低,Occupancy也低 → 先调Occupancy
Memory Throughput —— 带宽是命根子
对于大多数算子,瓶颈都在显存带宽。你想想看,GPU的计算能力增长比带宽快得多。所以,优化访存往往是性价比最高的。
我曾经优化过一个卷积算子,把Memory Throughput从40%提到了85%,性能提升了2.3倍。怎么做的?其实就是把输入数据做了padding对齐,让全局内存访问合并了。
Compute Throughput —— 计算单元吃饱了吗?
如果Compute Throughput低,说明你的kernel没有充分利用GPU的计算资源。常见原因:
- 指令级并行度不够(比如太多依赖的指令串行执行)
- 使用了太多低吞吐的指令(比如double精度除法)
- 分支发散严重(同一个warp里的线程走了不同分支)
我的经验法则:
- Memory Throughput > 80% → 访存优化到位
- Compute Throughput > 70% → 计算优化到位
- 两者都低 → 先调Occupancy,再看具体瓶颈
1.5 实战流程:一个完整的Profiling步骤
我一般按这个流程走:
- 先跑Nsight Systems,看整体时间分布。如果内存拷贝占大头,先优化数据传输。
- 再跑Nsight Compute,看单个kernel的瓶颈。重点关注Memory Throughput和Compute Throughput。
- 分析Occupancy,看是不是寄存器或共享内存限制了并发。
- 看L1/L2命中率,如果低,考虑调整数据布局或使用共享内存。
- 迭代优化:改一个点,重新Profiling,看指标变化。
嗯,这里要注意:不要一次改太多。我见过有人一次改了五个地方,结果性能提升了,但不知道是哪个改动起了作用。下次遇到类似问题,还是得从头试。
1.6 知识体系总览
下面这张图,是我自己总结的Profiling知识体系。你把它记在心里,以后做优化就不会迷路。
这张图的核心逻辑很简单:先宏观,后微观;先定位,后优化。别一上来就钻进kernel里调寄存器,先看看你的程序到底在等什么。
1.7 避坑指南
最后,分享几个我踩过的坑:
- 别只看Occupancy。我曾经有一个kernel,Occupancy 100%,但性能很差。后来发现是因为每个线程做了太多计算,导致指令级并行度不够。Occupancy高不代表效率高。
- Profiling结果别全信。Nsight Compute会插入额外的指令来收集数据,这会影响时序。我一般用它看比例(比如带宽利用率),不看绝对时间。
- 别忘了检查编译器优化。有时候你写了一个很优雅的kernel,结果编译器给你优化没了。用
--ptxas-options=-v看看生成的指令数,心里有个底。
我的小习惯:每次优化完,我都会把Profiling结果截图保存。这样下次遇到类似问题,可以直接对比,不用从头分析。
好了,这一章就到这里。Profiling是算子优化的基本功,你花一周时间把这两个工具用熟,后面30章的优化技巧才能派上用场。
想要更多资料,联系可可