2. 性能分析工具入门

做GPU优化,最怕什么?

怕你埋头改了一整天代码,结果性能没提升,反而更慢了。

我刚开始接触CUDA时,就犯过这个错。那时候我凭感觉调优,觉得循环展开肯定快,结果一跑,比原来还慢30%。后来老工程师跟我说了一句话,我记到现在:「没有测量,就没有优化」。

所以这一章,咱们就来聊聊性能分析工具。说白了,就是给你的GPU程序做「体检」。

为什么需要性能分析工具?

你想想看,一个CUDA程序跑得慢,原因可能有一百种:

  • 是显存带宽不够?
  • 还是计算单元没喂饱?
  • 或者是核函数启动开销太大?
  • 甚至可能是CPU和GPU之间的数据传输卡住了?

没有工具,你只能靠猜。有了工具,你就能看到每一毫秒GPU在干什么。

核心原则:先测量,后优化。不要相信直觉,要相信数据。

工具全家福:三个工具各司其职

NVIDIA给了我们三把利器。我习惯把它们分成三个层次:

工具 定位 我什么时候用它
Nsight Systems 系统级时间线分析 先看整体,找瓶颈在哪
Nsight Compute Kernel级深度分析 找到瓶颈后,钻进去看细节
nvprof/ncu 命令行快速 profiling 自动化脚本、CI集成、远程服务器

嗯,这里要注意:顺序很重要。我见过有人一上来就用Nsight Compute分析一个Kernel,结果发现那个Kernel只占总时间的5%——这不是白费功夫吗?

Nsight Systems:先看全局

Nsight Systems给我的感觉,就像给程序拍了一张「心电图」。它能展示整个程序运行的时间线,包括:

  • CPU端的函数调用
  • GPU端的Kernel执行
  • 内存拷贝操作(cudaMemcpy)
  • CUDA API调用

我个人习惯,拿到一个程序,第一步就是跑Nsight Systems。为什么?因为我要先回答三个问题:

  1. 瓶颈在CPU还是GPU?——看时间线上谁占大头
  2. 有没有不必要的同步?——看CPU是不是在等GPU
  3. 数据传输是不是太多了?——看cudaMemcpy的占比

小技巧:在Nsight Systems里,你可以按住Ctrl+鼠标滚轮来缩放时间线。我经常放大到纳秒级别,看Kernel之间的间隙——那些间隙往往藏着优化空间。

Nsight Compute:深入Kernel内部

当你用Nsight Systems找到了「嫌疑Kernel」,就该Nsight Compute出场了。

这个工具能告诉你一个Kernel内部发生了什么:

  • 计算吞吐利用率(Compute Utilization)
  • 显存带宽利用率(Memory Bandwidth Utilization)
  • Warp占用率(Occupancy)
  • 指令混合(Instruction Mix)
  • L1/L2缓存命中率

我记得有一次,我优化一个矩阵乘法的Kernel。Nsight Compute告诉我,计算利用率只有30%,但显存带宽利用率已经95%了。这说明什么?说明这个Kernel是带宽受限的,你再怎么优化计算也没用。得从数据复用、访存模式上想办法。

避坑指南:我曾经被Occupancy这个指标骗过。当时我拼命提高Occupancy,结果性能反而下降了。后来才发现,Occupancy高不代表性能好——如果每个线程做的事情太少,高Occupancy只会带来更多的线程调度开销。记住:Occupancy是手段,不是目标。

nvprof/ncu:命令行下的利器

有时候你没有图形界面,比如在远程服务器上。这时候nvprof(老版本)和ncu(新版本)就是你的好帮手。

基本用法很简单:

# 老版本(CUDA 10.x及之前)
nvprof ./my_cuda_program

# 新版本(CUDA 11.x及之后)
ncu ./my_cuda_program

但光这样跑,信息太多了。我一般会加一些参数:

# 只看Kernel的概要
ncu --set full ./my_cuda_program

# 导出到文件,方便对比
ncu -o profile_result ./my_cuda_program

# 只看某个Kernel
ncu --kernel-name "my_kernel" ./my_cuda_program

嗯,这里有个实用场景。我在做CI(持续集成)的时候,会把ncu集成到自动化脚本里。每次提交代码,自动跑一遍profiling,如果某个Kernel的性能下降了超过5%,就报警。这样能及时发现性能回退。

实战:一个完整的分析流程

说了这么多,咱们走一遍完整的流程。假设你有一个程序,感觉跑得慢:

  1. 第一步:Nsight Systems扫全局
    • 发现80%的时间花在一个叫matmul的Kernel上
    • 还有15%的时间在cudaMemcpy上
  2. 第二步:Nsight Compute分析matmul
    • 发现显存带宽利用率只有40%
    • L1缓存命中率很低
    • 说明访存模式有问题
  3. 第三步:优化访存
    • 改成共享内存分块(tiling)
    • 确保合并访问(coalesced access)
  4. 第四步:用ncu验证
    • 跑ncu对比优化前后的指标
    • 确认显存带宽利用率提升到了75%
    • 整体性能提升了1.8倍

你看,这个流程里,每个工具都在它该在的位置上。

知识体系总览

下面这张图,是我自己总结的GPU性能分析知识体系。你可以把它当作一个检查清单:

GPU性能分析知识体系 Nsight Systems Nsight Compute nvprof / ncu 系统级时间线 CPU/GPU活动概览 内存拷贝分析 API调用追踪 Kernel深度分析 计算/带宽利用率 Occupancy分析 指令/缓存分析 命令行快速分析 自动化脚本集成 CI/CD集成 远程服务器分析 分析流程:全局 → 局部 → 优化 → 验证 关键性能指标(KPI) 计算吞吐利用率 显存带宽利用率 Occupancy 缓存命中率 指令级并行度 Warp发散率

一些实用建议

最后,分享几个我踩过的坑:

  • 不要在生产环境直接profiling。Profiling本身有开销,会影响性能数据。我一般会在专门的测试环境跑。
  • 多次运行取平均值。GPU性能有波动,单次运行的数据不可靠。我习惯跑5次,取中位数。
  • 对比优化前后,保持环境一致。包括GPU频率、温度、驱动版本。否则你分不清是优化有效,还是环境变了。
  • 学会看「瓶颈」而不是「指标」。Occupancy 100%不代表性能好,带宽利用率100%才说明你喂饱了显存。

我的工作流:每次优化前,我会用ncu导出一份基线数据。优化后,再导出一份。然后用ncu --import对比两份数据,一眼就能看出哪些指标变了。这个习惯帮我避免了很多「自以为优化了,实际没变化」的尴尬。