CUDA系统拆解-10-Profiling、调试与瓶颈定位:先找到根因再谈优化

本文是「CUDA系统拆解」系列第 10 篇。
系列导读:CUDA系统拆解-00-导读:从编程模型到 AI 推理系统的学习路线
上一篇:CUDA系统拆解-09-Streams、异步拷贝与Overlap:如何把拷贝和计算叠起来
下一篇:CUDA系统拆解-11-经典CUDA算子模式:elementwise、reduction、reorder 与 blocked compute

1. 这篇解决什么问题

  • Profiling 到底在看什么,为什么不能凭感觉猜性能。
  • 正确性调试和性能分析为什么是两条线。
  • Nsight SystemsNsight ComputeCompute Sanitizer 分别解决什么问题。
  • 分析 CUDA 程序时,正确的顺序应该是什么。
  • 怎样把工具结果映射到算力、带宽、同步、launch、调度这些瓶颈类别。
  • 怎样把这些分析方法直接用于推理延迟、QPS 和 token latency。

2. 先记住的核心结论

  • 先定位再优化,不能看代码猜性能。
  • 正确性调试和性能分析是两条线:前者回答“有没有错”,后者回答“为什么慢”。
  • Nsight Systems 看整条时间线和执行流水,Nsight Compute 看单个 kernel 的微观瓶颈,Compute Sanitizer 抓越界、竞争、未初始化访问和同步错误。
  • CUDA 瓶颈通常可以先分成几类:算力、带宽、同步、launch、调度。
  • 正确的分析顺序通常是:先看全局 timeline,再锁定热点 kernel,再下钻微观指标;有不稳定结果时先排 correctness。
  • 在 AI 推理里,真正重要的不只是单个 kernel 快不快,还包括 prefill / decode 的阶段行为、QPS、首 token 延迟和 steady-state token latency。

3. 正文讲解

3.1 为什么 Profiling 不是可选项

学 CUDA 到这个阶段,最容易出现一种错觉:

  • 理论大概都懂
  • kernel 也能写
  • 访存、同步、occupancy 这些名词都知道

但一到真实工程里就会卡住:

  • 程序慢,不知道慢在哪
  • 某个 batch 变慢,不知道是 kernel 问题还是流水问题
  • 某轮结果异常,不知道是越界、竞争还是同步错了

这就是“会写代码”和“会做工程”的分界线。

真正的工程能力不是背出多少概念,而是能回答:

  • 时间到底花在哪
  • 为什么花在这里
  • 该先改哪一层
  • 改完之后是否真的变好了

所以 profiling 的本质就是:

用证据替代感觉。

3.2 Profiling 到底在看什么

从本质上说,profiling 在做两件事。

第一,看时间分布。
也就是回答:

  • 时间主要花在 kernel、memcpy、同步,还是 CPU 侧调度
  • 是某个大 kernel 特别慢,还是一堆小 kernel 把时间切碎了
  • copy 和 compute 有没有 overlap
  • GPU 是真的忙,还是 timeline 上有很多空洞

第二,看资源为什么没发挥出来。
也就是回答:

  • 是算力没吃满,还是带宽没吃满
  • 是 active warps 不够,还是 stall 太多
  • 是访存模式差,还是同步太重
  • 是 launch 太碎,还是 host 端把 GPU 喂不饱

所以 profiling 不是“看一个总时间”,而是:

先看哪里慢,再看为什么慢。

3.3 正确性调试和性能分析是两条线

这一点必须刻意区分。

正确性调试关心的是:

  • 有没有越界
  • 有没有未初始化访问
  • 有没有 race condition
  • barrier 用法有没有问题
  • 某些输入为什么偶发出错

性能分析关心的是:

  • 是算力瓶颈还是带宽瓶颈
  • 是 launch overhead 还是同步开销
  • 是 CPU 调度问题还是 device 端问题
  • 改 block size、访存、fusion 是否真的有收益

这两条线经常有关联,但不能混着做。
尤其当现象是:

  • 偶发错
  • 不稳定
  • 某些输入才坏

这类问题首先要怀疑 correctness,而不是先做性能优化。

3.4 三类核心工具的分工

这一篇最核心的工具分工只有三类。

Nsight Systems

  • 看系统级 timeline
  • 看 CPU 和 GPU 在干什么
  • 看 stream、memcpy、kernel 是否重叠
  • 看有没有大量 idle 区间
  • 看 launch、调度、流水是否有问题

它最适合回答:

整条执行流水到底怎么跑的。

Nsight Compute

  • 看单个 kernel 的微观行为
  • 看它更像 memory-bound 还是 compute-bound
  • 看 occupancy、warp stall、memory workload、cache 行为
  • 看源码或指令层面的热点位置

它最适合回答:

这个 kernel 为什么慢。

Compute Sanitizer

  • 抓越界和非法访存
  • 抓 shared memory 数据竞争
  • 抓 global memory 未初始化访问
  • 抓同步原语使用错误

它最适合回答:

程序是不是有 correctness 问题。

3.5 正确的分析顺序

很多人一看到程序慢,就立刻去盯单个 kernel 指标。
这经常是顺序错了。

更稳的顺序通常是:

第一步,先用 Nsight Systems 看全局 timeline。

先回答:

  • GPU 有没有长时间空闲
  • memcpy 和 kernel 有没有 overlap
  • 多 stream 是否真的并发
  • CPU 是否喂不饱 GPU
  • timeline 上是不是挤满了短小 kernel

第二步,锁定真正的热点。

也就是:

  • 总时间占比最高的几个 kernel
  • 调用次数极高的短 kernel
  • 某类输入下明显异常的阶段

第三步,再用 Nsight Compute 下钻。

这时才去问:

  • 它是带宽瓶颈还是算力瓶颈
  • occupancy 是否被资源限制
  • stall 主要来自哪里
  • 访存和同步结构是否健康

第四步,如果行为不稳定或结果可疑,先跑 Compute Sanitizer

因为很多看起来像性能问题的现象,最后其实是 correctness 问题。

3.6 如何理解几类常见瓶颈

这篇需要把“瓶颈分类”建立出来。

算力瓶颈

  • SM 侧计算吞吐更接近上限
  • 访存不是主因
  • 更该关注指令吞吐、tensor core 利用、算法结构

带宽瓶颈

  • memory throughput 高或明显受限
  • warp 大量在等内存
  • 更该关注 coalescing、reuse、tiling、fusion

同步瓶颈

  • barrier 多
  • block 内工作不均衡
  • 大量时间花在等待别的线程

launch 瓶颈

  • kernel 很短
  • 但调用次数很多
  • decode 或小 batch 场景特别容易出现

调度瓶颈

  • GPU timeline 有空洞
  • CPU 端提交、拼 batch、准备 metadata 太慢
  • stream 组织不合理

这几类瓶颈不是互斥的,但先把问题粗分到这一层,后面分析会清楚很多。

3.7 Nsight Systems 在看什么

Nsight Systems 最适合看“整条流水”。

你打开 timeline 后,最应该先看的是:

  • GPU 是否有明显 idle 空洞
  • H2D / D2H copy 是否和 kernel overlap
  • 多 stream 任务是不是交错执行
  • API 调用是否很碎
  • host 线程是否经常在等

很多问题在这一层就已经暴露了:

  • GPU 不是慢,而是没活干
  • stream 看起来很多,但其实被同步点拉直了
  • memcpy 很重,真正瓶颈在数据流
  • decode 阶段 kernel 太碎,launch overhead 明显

所以 Nsight Systems 的价值在于:

先判断问题是单个 kernel 的问题,还是整条系统流水的问题。

3.8 Nsight Compute 在看什么

当你已经确定某个 kernel 值得下钻时,才轮到 Nsight Compute

你不需要一开始就看上百个指标,先抓几类:

  • 这个 kernel 整体更像 memory-bound 还是 compute-bound
  • occupancy 是否受寄存器或 shared memory 限制
  • warp stall 的主因是什么
  • memory workload 是否健康
  • 源码哪一段真正是热点

这里最容易犯的错是“盯某个单独指标看”。
例如:

  • occupancy 低,不一定就是主因
  • 某个 stall 高,不一定就最该先改
  • memory throughput 不高,也不代表访存没问题

指标必须放回上下文里解释。

3.9 Compute Sanitizer 在抓什么

当你怀疑程序有 correctness 问题时,Compute Sanitizer 是第一工具。

你至少要知道几类典型问题:

  • 越界、misaligned access、非法访存
  • shared memory race
  • global memory 未初始化访问
  • barrier 或同步原语使用错误

这类问题特别容易出现在:

  • 边界条件没处理好
  • shared memory 协作写错了
  • 只在某些输入规模下才会触发
  • 某些分支没覆盖完整写入

所以遇到“偶发错”“偶发 NaN”“某些 batch 才坏”时,优先跑 sanitizer,比盲改代码稳得多。

3.10 如何把工具结果映射到推理瓶颈

这一篇和 AI 推理的连接点就在这里。

你不能只会看“kernel 快不快”,还要能把工具结果映射到用户真正关心的指标上:

  • 首 token 延迟
  • steady-state token latency
  • QPS
  • 吞吐
  • 尾延迟

几个典型映射方式:

如果 Nsight Systems 显示 decode 阶段 timeline 被大量短 kernel 切碎:

  • 很可能是 launch overhead、调度开销、kernel 粒度问题
  • 可以考虑 fusion、CUDA Graph、persistent kernel、continuous batching

如果 Nsight Compute 显示某个 LayerNorm / softmax kernel 明显 memory-bound

  • 重点就不是继续抠 occupancy
  • 而是看访存模式、fusion、reuse、KV cache 访问结构

如果 timeline 上 GPU 空洞很多:

  • 问题可能不在 kernel
  • 而在 host 端 request packing、metadata 准备、stream 编排、buffer 管理

如果 prefill 很快,但 decode token latency 很差:

  • 很可能不是大算子吞吐问题
  • 而是小 batch、小 kernel、launch、memory latency 和流水线组织问题

所以 profiling 的真正价值不是“读懂工具界面”,而是:

把工具中的时间线和指标,翻译成推理系统里的真实瓶颈。

3.11 prefill 和 decode 为什么要分开看

这是推理 profiling 里必须建立的意识。

prefill

  • 通常并行度更高
  • 大算子更多
  • 更像吞吐问题
  • 大 GEMM / attention kernel 常是主角

decode

  • batch 往往更小
  • 每步工作量更碎
  • 更容易暴露 launch overhead
  • 更容易受 host 调度、stream 组织、memory latency 影响

所以同一套工具,在两个阶段看到的重点经常完全不同。
这也是为什么做推理分析时,必须先把 workload 场景说清楚。

3.12 一个实用的排查模板

以后你拿到一个 CUDA / 推理问题,可以先按这套模板走。

第一问:问题是稳定的,还是偶发的?

  • 偶发或不稳定,优先怀疑 correctness

第二问:时间主要花在哪?

  • 先看 Nsight Systems

第三问:是系统流水问题,还是热点 kernel 问题?

  • 流水问题就查 stream、copy、host 调度
  • kernel 问题再下钻 Nsight Compute

第四问:瓶颈更像哪一类?

  • 算力
  • 带宽
  • 同步
  • launch
  • 调度

第五问:这个问题如何映射回业务指标?

  • QPS 下降
  • 首 token 延迟升高
  • steady-state token latency 变差

这套模板在面试里也很好用,因为它比单纯背指标更像工程思维。

4. 和 AI 推理的关系

这篇和 AI 推理的关系非常强,而且比前面几篇都更直接。

原因很简单:推理系统的瓶颈经常不只存在于单个 kernel,而是存在于整条执行链。

典型问题包括:

  • prefill 是否真的吃满 GPU
  • decode 是否被小 kernel 和 launch overhead 拖慢
  • H2D / D2H copy 是否和计算 overlap
  • stream 编排是否成立
  • host 端 batch 维护和 metadata 准备是否成为瓶颈
  • 某个热点 kernel 是否是 memory-bound
  • KV cache 访问模式是否拖住 token latency

所以做 AI infra / 推理时,这篇的真正意义是:

你要学会把 CUDA 工具的输出,翻译成系统吞吐和时延问题。

5. 常见误区

  • 看代码就能猜出瓶颈。不是,先看证据。
  • 程序慢就先抠单个 kernel。不是,很多问题先要看全局 timeline。
  • correctness 和 performance 可以一起乱改。不是,偶发问题先排 correctness。
  • occupancy 高说明 kernel 没问题。不是,瓶颈可能在带宽、同步、launch 或调度。
  • 多 stream 就一定 overlap。不是,要看 timeline 是否真的成立。
  • GPU 利用率不低就说明推理系统没问题。不是,token latency 和 QPS 还取决于流水线组织。

6. 复习自测

  • Profiling 到底在看什么?
  • 为什么正确性调试和性能分析要分成两条线?
  • Nsight SystemsNsight ComputeCompute Sanitizer 各自解决什么问题?
  • 为什么通常先看全局 timeline,再看单个 kernel?
  • CUDA 瓶颈可以先粗分成哪几类?
  • 如果一个 decode 路径很慢,你会先看哪些系统级现象?
  • 如果一个热点 kernel 很慢,你会怎样判断它更像 memory-bound 还是 compute-bound
  • 如何把工具结果映射到推理的 QPS、首 token 延迟和 token latency?

系列导航