GPU系统拆解-09-Profiling 与性能定位:先找到瓶颈,再谈优化
GPU系统拆解-09-Profiling 与性能定位:先找到瓶颈,再谈优化
本文是「GPU系统拆解」系列第 09 篇。
系列导读:GPU系统拆解-00-导读:从架构认知到推理系统的学习路线
上一篇:GPU系统拆解-08-Tensor Core、GEMM 与 FlashAttention:AI 计算热点为什么这样组织
下一篇:GPU系统拆解-10-面试表达与系统思维:怎么把 GPU 理解讲成工程判断
这一篇要解决的是一个非常实际的问题:当一个 CUDA 程序或推理系统变慢时,你应该先看什么、怎么判断、哪些指标真正有用,以及这些指标最后怎样转化成优化动作。学完这一篇之后,你的目标不是“会点开 Nsight”,而是建立一套从现象走到结论的性能诊断框架。
1. 先给结论
- Profiling 的核心价值不是“看很多指标”,而是把“感觉慢”变成“知道慢在哪里、为什么慢”。
- 第一层先分系统问题和 kernel 问题,所以通常先看
Nsight Systems,再看Nsight Compute。 - 第二层先分
compute-bound和memory-bound,不要一上来就盯着occupancy。 occupancy很重要,但它只是手段,不是目标;高 occupancy 也可能很慢。warp stall的意义,不是告诉你“线程没开起来”,而是告诉你“调度器此刻没有足够 ready 的 warp 可发射”。memory workload和roofline能帮助你把“算慢了”与“喂不动了”区分开。- 对推理系统来说,
prefill和decode往往是两类完全不同的性能问题。 - 真正成熟的性能分析,不是背指标定义,而是能把指标翻译成下一步动作。
2. 为什么 Profiling 是 GPU 学习的分水岭
很多人学 CUDA 会停留在这一步:
- 会写 kernel
- 会 launch kernel
- 知道
shared memory、coalescing、occupancy这些词
但一到真实工程就开始失效,原因很简单:
性能问题不是靠猜出来的,而是靠 profile 定位出来的。
没有 profiling 时,优化很容易沦为这些动作:
- 盲目调 block size
- 盲目上 fusion
- 盲目加 shared memory
- 盲目追高 occupancy
这些动作偶尔会碰巧有效,但你无法稳定复用。
所以 profiling 是一个明显分水岭:
- 会写 CUDA,不代表会做 GPU 工程
- 会看 profile,才开始真正具备定位能力
3. 先建立一张工具分工图
在 NVIDIA 的工具链里,你最常用的通常是两个 profiler:
Nsight Systems:看系统级时间线Nsight Compute:看单个 kernel 的微观表现
一定要把它们的职责分开。
3.1 Nsight Systems:先看“时间到底花在哪”
Nsight Systems 更像一张全局时间线,它擅长回答:
- CPU 在干什么
- GPU 有没有持续在工作
- kernel 有没有被切得很碎
- 有没有频繁的
H2D / D2H - stream 是否真的并发
- 同步是不是太多
它解决的第一个关键问题是:
慢,到底是慢在系统层,还是慢在某个 kernel 本身?
3.2 Nsight Compute:再看“这个 kernel 为什么慢”
Nsight Compute 是 kernel 级 profiler,它更擅长回答:
- 这个 kernel 更像
compute-bound还是memory-bound warp stall主要来自什么global memory、L2、shared memory的行为如何- 寄存器是不是太多
occupancy被什么限制- 有没有真的用到预期的 Tensor Core 路径
所以更合理的顺序通常是:
先用
Nsight Systems找热点,再用Nsight Compute解剖热点。
4. 一个标准的性能定位流程
以后遇到“程序慢”,建议先固定按这个顺序走。
4.1 第一步:先判断是系统层问题还是 kernel 层问题
先看 Nsight Systems。
如果你发现:
- CPU 线程经常空转或阻塞
- kernel 很碎
- memcpy 很频繁
- graph 没 capture 住
- 每一步都在同步
那这更像系统层问题。
如果你发现:
- GPU 大部分时间都花在少数几个 kernel 上
- 某个 kernel 明显占了主要时间
那才值得往 kernel 层继续深挖。
4.2 第二步:在热点 kernel 上先分大类
到了热点 kernel,不要第一眼就看 occupancy。
先回答一个更基础的问题:
这个 kernel 主要是算力受限,还是带宽受限?
这一步会决定你后面看哪些指标、做哪些优化。
4.3 第三步:再看 launch、occupancy、stall、memory workload
这时再进入 Nsight Compute 去看:
- launch statistics
- occupancy
- warp stall breakdown
- memory workload
- source correlation
注意顺序很重要。正确顺序是“先有方向,再看细节”,不是“先淹死在一堆细节里”。
5. compute-bound 和 memory-bound 应该怎么分
这是 GPU 性能分析里最关键的第一道分流。
5.1 什么更像 compute-bound
如果一个 kernel 有这些特征:
- 算术强度高
- ALU 或 Tensor Core 很忙
- 内存吞吐没有逼近瓶颈
- stall 更像执行依赖或 pipe 压力
那它更接近 compute-bound。
典型例子:
- 大 GEMM
- 大 batch dense matmul
- Tensor Core 友好的矩阵 kernel
这类 kernel 的常见优化方向是:
- 改 tile
- 改 pipeline
- 提高 Tensor Core 利用率
- 减少多余指令
- 提高 ILP
5.2 什么更像 memory-bound
如果一个 kernel 有这些特征:
- DRAM 或 L2 很忙
- 运算量不算大,但耗时不低
- warp 大量时间在等内存
- 算力单元并没有特别忙
那它更接近 memory-bound。
典型例子:
- gather / scatter
- embedding lookup
- 某些
layernorm/softmax - decode 阶段和
KV cache相关的操作 - 低算术强度的 elementwise / reduction
这类 kernel 更常见的优化方向是:
- 改访问模式
- 提高 coalescing
- 做数据复用
- 减少 global round-trip
- 融合中间步骤
6. occupancy 为什么重要,但不能迷信
很多人学 profiling 时最容易犯的错误,就是把 occupancy 当成性能目标。
更准确的理解应该是:
occupancy 的价值,在于判断一个 SM 上有没有足够多的活跃 warp 来隐藏延迟。
如果 occupancy 太低,常见风险是:
- 某些 warp 一等内存
- 调度器没有足够多的其他 warp 可以切换
- SM 更容易空转
但这不等于 occupancy 越高越快。
6.1 为什么 occupancy 高也可能慢
因为高 occupancy 只是说明“挂上去的 warp 多”,不代表:
- 访存模式合理
- 数据复用足够
- stall 小
- 指令选择合理
- Tensor Core 用起来了
而且很多时候,为了追高 occupancy,反而会付出代价:
- 每线程寄存器预算更紧
- spill 风险更高
- shared memory tile 变小
- 算法结构变差
所以更合理的态度是:
- 把 occupancy 当成约束信息
- 不把 occupancy 当成最终目标
7. Warp Stall 到底在告诉你什么
从硬件视角看,调度器每个周期都想找到一个 ready warp 发射下一条指令。
如果找不到,就会出现 stall。
所以 warp stall 的真正含义不是“线程少”,而是:
此刻缺少能立刻继续执行的 warp,或者即使有 warp,也被某种资源或依赖卡住了。
7.1 等内存
这类 stall 通常暗示:
- global memory latency 高
- 访问不连续
- cache 命中不好
- 数据复用不足
优化方向更偏:
- 改访问模式
- 做 tiling
- 提高 coalescing
- 增强局部复用
7.2 等执行依赖
这类 stall 更像:
- 指令链太直
- 独立工作不够多
- ILP 不足
优化方向更偏:
- 重排计算顺序
- 合理 unroll
- 让独立数据块并行推进
7.3 等 barrier / 同步
这通常提示:
- block 内工作不均衡
- 同步太频繁
- 共享数据结构设计不够顺
优化方向更偏:
- 减少不必要同步
- 重构 block 内工作划分
- 调整 shared memory 使用方式
7.4 等调度资源或 pipe
这更像是:
- 某类指令过于集中
- 某条执行管线过热
- 指令混合不够均衡
优化方向更偏:
- 看指令构成
- 看是否能走更合适的矩阵或向量路径
- 调整 tile 和 pipeline
7.5 不要孤立看 stall 细项
这点非常重要。
单看某个 stall 细项高,不一定说明它就是主矛盾。更合理的方式是把它和整体 issue 效率一起看。
只有当发射效率本身就不高时,stall breakdown 才真正有很强的诊断价值。
8. Memory Workload Analysis 应该怎么看
这一页往往是判断 memory-bound 问题的核心证据层。
它在帮你回答这些问题:
- global load / store 多不多
- L1 / L2 / DRAM 吞吐高不高
- cache 利用率怎么样
- 访问模式规整不规整
这里最典型的两种坏情况是:
8.1 带宽接近极限
这说明 kernel 真的可能被带宽卡住了。
常见动作是:
- 减少数据搬运量
- 提高复用
- 融合算子
- 降精度
8.2 带宽没打满,但依然很慢
这通常更麻烦,因为它往往意味着:
- 访问模式差
- 请求不规整
- 事务合并不好
- cache 利用率低
这时重点通常不是“带宽不够”,而是“带宽没被高效用起来”。
对推理来说,这类问题很常见于:
- embedding / gather
- 不规则 KV 访问
- 小 kernel 串联造成的重复读写
9. Roofline 思维为什么重要
你不一定需要熟练画 roofline 图,但一定要有这个脑子。
它的核心思想是:
一个 kernel 的性能上限,要么更接近算力天花板,要么更接近带宽天花板。
而决定它偏向哪边的关键,是 arithmetic intensity。
简单说就是:
- 每搬一个单位数据,做了多少计算
这会把很多现象统一起来:
- GEMM 算术强度高,更容易逼近 compute roof
- gather、elementwise 算术强度低,更容易逼近 bandwidth roof
- prefill 更可能偏 compute-bound
- decode 更可能偏 memory-bound
这也是为什么:
- 更高的 TFLOPS 不一定线性改善 decode
- 有些优化对 prefill 很有效,对 decode 却一般
10. 一个更贴近工程的诊断顺序
这里给你一套更实用的工程化流程。
10.1 场景 A:端到端推理吞吐差
先看 Nsight Systems:
- GPU 是否持续有活
- stream 是否真的并发
- CPU 是否成了瓶颈
- kernel 是否过碎
- memcpy 是否频繁
- 是否存在过多同步
如果这里已经发现问题,比如:
- launch 太碎
- CPU pipeline 不顺
- graph 没 capture
- H2D 太频繁
那先改系统问题,不要急着抠单个 kernel。
10.2 场景 B:某个 kernel 特别慢
这时再看 Nsight Compute:
- 先看它在总时间里占比多大
- 再看 launch config 和 occupancy 是否明显异常
- 再看 memory workload,先判断大方向
- 再看 scheduler / warp stall
- 必要时做 source correlation,映射回代码
10.3 场景 C:某个优化后反而更慢
这时最常见的原因不是“优化思路完全错了”,而是 trade-off 没算对。
高频原因包括:
- 寄存器上升太多
- shared memory 占用过大
- fusion 虽然减少了访存,但引入了更多同步或更差的指令调度
- block size 换了之后,不再适合当前架构
- 编译器没按你预期生成更好的代码
所以 profile 的作用不是证明“某个技巧很高级”,而是帮你看清:
你到底是在用什么换来了什么。
11. 结合推理场景看,最常见的几类 profile 现象
11.1 prefill 快,decode 慢
这往往不是 bug,而是 workload 形态不同。
prefill更接近 dense computedecode更容易暴露缓存、带宽和KV cache问题
所以两者瓶颈不同很正常。
11.2 GPU 利用率不低,但吞吐还是一般
这通常说明“GPU 有活”不等于“活的质量高”。
可能的问题包括:
- memory-bound 很重
- 活跃 warp 多,但 stall 也多
- 有很多小 kernel 串联
- 系统层流水并不顺
11.3 做了 fusion,延迟反而升高
fusion 不是天然正确。
常见反效果包括:
- 寄存器更高
- shared memory 压力更大
- block 内同步更多
- 指令调度变差
11.4 block size 更大,反而没更快
更大的 block 有时会带来:
- 更高资源占用
- 更低驻留数量
- 更差 occupancy
- 更差的工作均衡
所以 block size 不是越大越好,而是要和 kernel 结构匹配。
12. 4090 / Ada 上做 profile 时,应该注意什么
你现在主要是在 4090 上学习,这很合适,但结论不能机械外推。
12.1 4090 上的结论,不等于 H100 / B200 上的结论
不同 GPU 在这些方面都会影响 profile 结论:
- 显存体系
- 带宽规模
- cache 行为
- Tensor Core 路径
- 系统互连和部署方式
所以 4090 非常适合学习方法论,但不要把某个具体数值结论直接外推到数据中心 GPU。
12.2 4090 上更容易看出某些 memory-bound 问题
很多推理实验在 4090 上会更明显地暴露:
- KV cache 访问
- 小 kernel 反复读写
- decode 里的带宽和缓存问题
这对学习其实是好事,因为现象更直观。
13. 怎样把指标翻成动作
Profiling 最终不是为了“会解释图”,而是为了知道下一步改什么。
可以把思路压成三层:
13.1 先分层
- 系统层
- kernel 层
13.2 再分流
compute-boundmemory-bound
13.3 最后翻成动作
例如:
- 如果系统层碎,就减少 launch、减少同步、改善流水
- 如果 memory-bound,就优先改访问模式和复用
- 如果 compute-bound,就看 tile、pipeline、指令和 Tensor Core 路径
- 如果 trade-off 变差,就看寄存器、shared memory、occupancy 的代价
只有走到这一步,profile 才真正转化成工程价值。
14. 这一篇必须记住的几句话
- 先分系统问题和 kernel 问题,通常先看
Nsight Systems,再看Nsight Compute。 - 先分
compute-bound和memory-bound,不要一上来就盯 occupancy。 occupancy重要,但不是性能目标。warp stall的意义是“为什么调度器现在发不出去”,不是“线程没开起来”。memory workload和roofline能帮助你把“算慢了”和“喂不动了”分开。- 推理里的
prefill和decode往往是两类不同问题。 - 真正成熟的 profiling,不是看指标本身,而是能把指标翻成下一步动作。
15. 精简版面试表达
如果面试官问你平时怎么做 GPU profiling,可以这样答:
我一般先用 Nsight Systems 看系统级时间线,先区分问题是出在 CPU 调度、同步、memcpy、kernel 过碎,还是集中在少数热点 kernel 上。找到热点后,再用 Nsight Compute 判断这个 kernel 更像 compute-bound 还是 memory-bound,再结合 occupancy、warp stall、memory workload 和 source correlation 去看瓶颈究竟是算力、访存、寄存器压力还是控制流问题。对推理系统来说,我会特别注意 prefill 和 decode 的差异,因为前者更接近 dense compute,后者更容易暴露 KV cache、带宽和缓存系统问题。


