4. Nsight Compute实战:Kernel Profiling详解、Occupancy分析、Memory与Compute分析

好,咱们直接进入正题。Nsight Compute,我习惯叫它NC,是咱们做CUDA性能分析最趁手的工具。没有之一。

说实话,刚接触NC的人,很容易被它那一大堆指标吓到。什么L1命中率、bank conflict、occupancy、stall原因……看得人眼花。但你别慌,这章我就带你把这些指标一个个拆开,讲清楚它们到底在说什么。

4.1 Kernel Profiling:从跑起来到看明白

先说说怎么跑一个profiling。我个人习惯用命令行,因为方便集成到脚本里。命令很简单:

ncu --set full -o my_profile ./my_cuda_app

这个--set full会收集所有能收集的指标。但注意,收集得越多,运行时间就越长。我一般先跑个--set basic快速看一眼,发现问题了再用full深挖。

跑完之后,你会得到一个.ncu-rep文件。用Nsight Compute GUI打开它,就能看到每个kernel的详细信息了。

嗯,这里有个小技巧。如果你只想看某个特定的kernel,可以用--kernel-name参数过滤:

ncu --kernel-name "myKernel" --set full -o my_profile ./my_cuda_app

这样能省不少时间。我在项目里经常要反复调优同一个kernel,每次都跑全量profiling太慢了,用这个过滤就快多了。

4.2 Occupancy分析:别被数字骗了

Occupancy,中文叫“占用率”。很多人一上来就盯着这个数字,恨不得跑到100%。

但我告诉你,高occupancy不等于高性能。

Occupancy衡量的是:当前活跃的warp数量,占硬件最大支持warp数量的比例。比如你的GPU每个SM最多支持64个warp,现在跑了48个,那occupancy就是75%。

为什么说别迷信它?我给你讲个真实案例。

我曾经优化过一个矩阵乘法的kernel。一开始occupancy只有33%,我花了两天时间,把shared memory用量砍了一半,寄存器也压了压,终于把occupancy提到了66%。结果你猜怎么着?性能只提升了5%。

为什么?因为那个kernel是计算密集型的,瓶颈在计算单元,不在访存。提高occupancy只是让更多warp在排队等计算资源,并没有解决根本问题。

所以,occupancy分析的正确用法是:

  • 先看stall原因:如果大部分stall是“long scoreboard”(等待访存),那提高occupancy可以隐藏延迟
  • 如果stall是“execution dependency”(等待计算),提高occupancy基本没用
  • 如果stall是“not selected”(调度器没选上),说明occupancy太高了,反而在争抢资源

NC里有个“Occupancy”页面,会显示理论occupancy和实际occupancy的对比。我一般先看实际occupancy,再看它旁边的“Block Limit”原因——是寄存器不够?shared memory不够?还是warp数量限制?

核心原则:Occupancy是手段,不是目标。你的目标是让GPU的某个资源(计算单元、访存带宽)饱和,而不是让occupancy饱和。

4.3 Memory分析:找到那个偷带宽的贼

Memory分析,说白了就是看你的数据是怎么在各级存储之间流动的。NC的“Memory Workload”页面会告诉你:

  • Global memory的读写量
  • L1/L2 cache的命中率
  • Shared memory的bank conflict情况
  • Memory throughput(实际带宽利用率)

我最常看的是“Memory Throughput”和“L1/L2 Hit Rate”。

举个例子。有一次我优化一个图像处理的kernel,发现global memory读了很多数据,但L1命中率只有30%。这说明什么?说明数据局部性很差,每次读都是新的cache line,L1基本没发挥作用。

怎么解决?我改了一下数据访问模式,让每个thread连续读相邻的地址(coalesced access),L1命中率直接飙到80%,性能翻了一倍。

NC里还有一个特别好用的功能:“Source Counters”。它能告诉你每一行代码消耗了多少指令、发了多少访存请求。我经常用它来定位“哪一行代码最慢”。

我的习惯:先看“Memory Throughput”是不是接近硬件理论带宽。如果差得远,再看L1/L2命中率。如果命中率正常,那就是访存模式有问题(比如非coalesced access)。

4.4 Compute分析:计算单元吃饱了吗?

Compute分析关注的是计算单元(ALU/FPU)的利用率。NC的“Compute Workload”页面会显示:

  • 算术指令的吞吐量
  • 指令混合情况(FMA、ADD、MUL等)
  • Warp stall原因分布

这里有个关键指标:“Arithmetic Intensity”(算术强度)。它等于“总计算量 / 总访存量”。

为什么这个指标重要?因为GPU的瓶颈要么在计算,要么在访存。算术强度高,说明是计算密集型;算术强度低,说明是访存密集型。

NC的“Speed of Light”页面会画一张图,横轴是算术强度,纵轴是性能。图上有一条斜线(roofline),代表理论峰值。你的kernel应该尽量靠近那条线。

我记得有一次,一个kernel的算术强度只有0.5,明显是访存瓶颈。我试着把多个小计算合并成一个大的,算术强度提到了2.0,性能提升了3倍。这就是roofline模型的威力。

注意:不要只看算术指令的数量。有些指令(比如除法、平方根)延迟很高,会拖慢整体速度。NC的“Instruction Statistics”页面会列出每种指令的执行次数和延迟,值得仔细看看。

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

说了这么多,咱们来走一遍完整的流程。假设你有一个kernel,性能不达标:

  1. 第一步:跑NC,看概览

    用ncu --set full跑一次,打开GUI,先看“Summary”页面。重点关注“Duration”(耗时)和“Speed of Light”图。

  2. 第二步:判断瓶颈类型

    看“Speed of Light”图。如果kernel的点在访存边界附近,就是访存瓶颈;如果在计算边界附近,就是计算瓶颈。

  3. 第三步:深挖具体原因
    • 如果是访存瓶颈:进“Memory Workload”页面,看L1/L2命中率、global memory吞吐量、bank conflict
    • 如果是计算瓶颈:进“Compute Workload”页面,看算术强度、指令混合、stall原因
  4. 第四步:定位到代码行

    用“Source Counters”功能,找到最耗时的几行代码。看看能不能改访存模式、减少指令数、或者用更快的指令替代。

  5. 第五步:改代码,重新跑

    改完之后,再跑一次NC,对比前后指标的变化。我一般会保存两份profile,方便对比。

避坑指南:我曾经犯过一个错误——只看一个指标就下结论。比如看到occupancy低,就拼命提高occupancy,结果性能没变。后来我学会了:先看stall原因,再看资源利用率,最后才看occupancy。这个顺序很重要。

4.6 知识体系图

下面这张图总结了本章的核心逻辑。你可以把它当成一个检查清单,每次做性能分析时对照着看:

Nsight Compute 性能分析流程 1. Kernel Profiling ncu --set full 2. 判断瓶颈类型 Speed of Light 图 访存瓶颈 Memory Workload 计算瓶颈 Compute Workload 3. 深挖具体原因 Occupancy分析 stall原因 + 资源限制 Memory分析 L1/L2命中率 + 带宽 Compute分析 算术强度 + 指令混合 4. 定位到代码行 Source Counters 功能 5. 改代码 → 重新跑 → 对比

这张图把整个分析流程串起来了。你每次做性能分析,都可以按这个顺序走一遍。熟练之后,你甚至不用看NC的GUI,光看命令行输出就能猜个八九不离十。

最后说一句:NC是个好工具,但它不会替你思考。指标是死的,人是活的。多跑、多看、多想,慢慢你就会有感觉——哪个kernel该优化哪里,一眼就能看出来。

想要更多资料,联系可可