CUDA Graph 是什么?
launch overhead 怎么来的,怎么去掉?
先弄清楚:每个概念是什么
- Launch
- CPU 让 GPU 跑一个 kernel 的那次调用,不等 kernel 跑完就返回。
- Launch overhead
- 每次 launch 花在 CPU 上的时间,微秒级,和 kernel 处理多少数据无关。
- Launch-bound
- kernel 又小又多,GPU 跑完一个要等 CPU 发下一个。
- CUDA Graph
- 把一整串 kernel 和它们的参数录下来(capture),之后一次调用重放整串(replay)。
看图:逐个 launch 和一次 replay
30 秒回答
Every kernel launch costs the CPU a few microseconds to set up arguments and submit the work. With many tiny kernels, that overhead dominates and the GPU sits idle waiting for the CPU. A CUDA Graph captures the whole sequence of kernels and their arguments once, then replays it with a single call. It removes launch overhead but doesn't make any kernel faster. It needs fixed shapes and memory addresses, with no CPU sync or data-dependent branches. For dynamic shapes, I capture one graph per shape bucket, pad inputs up to the bucket, and fall back to eager when nothing matches.
什么时候有用:手算一遍
按图里的数字放大到 100 个小 kernel。逐个 launch 约 500 µs,其中 GPU 只忙 200 µs。CUDA Graph 是一次 replay 加 100 × 2 µs,约 205 µs,快约 2.4 倍。
反过来,每个 kernel 要跑 5 ms 时,5 µs 的 launch 只占 0.1%,用了也看不出差别(benchmark 调大单次运行是同一个道理,见 kernel benchmark)。
NVIDIA 博客在 V100 上实测 2.9 µs 的 kernel,连续 launch 时平均每个 3.8 µs,用 CUDA Graph 是 3.4 µs。
流程和限制
- Warmup:先用 eager 跑几次
- Capture:再跑一次,kernel 不真的执行,只把 kernel 和参数记进 graph
- Replay:之后每次调用一次,整串 kernel 按录下的参数重跑
- Fallback:输入的 shape 对不上任何 graph,就退回 eager
| 限制 | 原因 |
|---|---|
| shape 固定 | 每次 replay 里每个 tensor 的 size 和 layout 都不能变 |
| 内存地址固定 | 每次 replay 读写同一批地址,新输入要先 copy 进 capture 时用的那块 tensor |
| 不能有 CPU 同步 | .item() 这类要等 GPU 结果的 op 不能在 capture 里 |
| 不能有依赖数据的分支 | 录下的是固定的 kernel 序列,分支在 capture 时就定死了 |
动态 shape 按 batch size 分桶,每个桶 capture 一个 graph,输入 padding 到桶的大小。分 8、16、32 三个桶时(示意),batch 11 padding 到 16,有 5/16 ≈ 31% 的计算是白做的,batch 40 对不上任何桶就 fallback。
必答点
- launch overhead:每次 launch 在 CPU 上花几微秒,小 kernel 多时 GPU 在等 CPU
- CUDA Graph:capture 一次整串 kernel,之后每次一次 replay,只去掉 launch overhead,不改单个 kernel 的速度
- 限制:shape 和内存地址固定,不能有 CPU 同步和依赖数据的分支
- 动态 shape:分桶,每桶一个 graph,输入 padding,对不上就 fallback 到 eager
- shape 经常变反而更慢:反复 capture,多占显存
追问
launch 的时候 CPU 做了什么?
准备 kernel 的参数,把命令提交给 GPU,立刻返回。PyTorch eager 里每个 op 在这之前还要过 Python 和 C++ 的 dispatch,这几层 replay 都跳过。
怎么知道自己是 launch-bound?
Nsight Systems 的 timeline 上 kernel 之间有密集的小空隙,GPU 在等 CPU(见 Nsight profiling 和 训练慢怎么查)。
它和 kernel fusion 有什么区别?
CUDA Graph 不改 kernel,kernel 数和读写 HBM 的量都不变。Kernel fusion 把 launch 次数和 memory traffic 一起降。两个可以叠加,torch.compile 的 reduce-overhead 模式就是这样做的。
为什么用了 CUDA Graph 显存占得更多?
PyTorch 默认给每次 capture 一个 private memory pool,graph 和 capture 时创建的 tensor 还活着它就不释放,桶越多 pool 越多。小马智行实习面问过,出处是 AIInfraGuide 的二手整理。
来源PyTorch CUDA semantics:CUDA Graphs(capture 不执行、Constraints、private pool、warmup)· NVIDIA blog:Getting Started with CUDA Graphs(V100、CUDA 10.1 的实测)· ml-八股 08 第 13 题 · 面经出处见 面试题与 JD 3.5 节和 NVIDIA 题库笔记 1.7、1.9 节