CUDA系统拆解-10-Profiling、调试与瓶颈定位:先找到根因再谈优化
CUDA系统拆解-10-Profiling、调试与瓶颈定位:先找到根因再谈优化
本文是「CUDA系统拆解」系列第 10 篇。
系列导读:CUDA系统拆解-00-导读:从编程模型到 AI 推理系统的学习路线
上一篇:CUDA系统拆解-09-Streams、异步拷贝与Overlap:如何把拷贝和计算叠起来
下一篇:CUDA系统拆解-11-经典CUDA算子模式:elementwise、reduction、reorder 与 blocked compute
1. 这篇解决什么问题
- Profiling 到底在看什么,为什么不能凭感觉猜性能。
- 正确性调试和性能分析为什么是两条线。
Nsight Systems、Nsight Compute、Compute 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 Systems、Nsight Compute、Compute Sanitizer各自解决什么问题?- 为什么通常先看全局 timeline,再看单个 kernel?
- CUDA 瓶颈可以先粗分成哪几类?
- 如果一个 decode 路径很慢,你会先看哪些系统级现象?
- 如果一个热点 kernel 很慢,你会怎样判断它更像
memory-bound还是compute-bound? - 如何把工具结果映射到推理的 QPS、首 token 延迟和 token latency?


