/ 什么是系列
什么是CUDA Graph
从 GPU 任务提交开销出发,解释 CUDA Graph 如何把稳定的 CUDA 执行流捕获成可重复 replay 的图,以及它在 PyTorch 和大模型推理中的价值与限制。
在 GPU 编程和大模型推理里,CUDA Graph 是一种减少 CPU 调度开销的 CUDA 任务提交机制。
一句话概括:
CUDA Graph 会把一段稳定的 GPU 操作和它们之间的依赖关系提前捕获成一张图,之后用一次 graph replay 重复提交整段工作流。
这句话里有三个关键词:
- 稳定执行流:被 capture 的 kernel、memcpy、memset 和依赖关系最好保持不变。
- 一次提交:原来 CPU 逐个 launch kernel,现在可以 replay 一整张图。
- 重复执行:越是反复运行同一类 GPU workload,CUDA Graph 越有机会发挥作用。
所以,CUDA Graph 的核心不是让某个 matmul kernel 本身变快,而是减少 CPU 到 GPU 之间反复提交小任务的 overhead。
1. 普通 CUDA 执行的问题在哪里#
普通 CUDA eager 执行可以理解成这样:
CPU launch kernel A
CPU launch kernel B
CPU launch kernel C
CPU launch kernel D
...每启动一个 CUDA kernel,CPU 侧都要经过 runtime、driver、参数准备、提交队列等路径。单次 launch 的开销通常不算夸张,但如果一个 step 里有很多很短的 kernel,这些开销就会累积起来。
在深度学习里,一个模型 forward 往往不是一个大 kernel,而是一串操作:
embedding
normalization
attention
matmul
activation
sampling
KV cache update如果每个操作都拆成多个 CUDA kernel,CPU 就需要不断给 GPU 喂任务。GPU 真正执行 kernel 的时间可能很短,kernel 之间却出现空隙。这时瓶颈不一定在 GPU 算力,而可能在 CPU launch 和调度路径。
CUDA Graph 想解决的就是这个问题:
能不能把一整段固定的 GPU 工作流提前记录下来,
之后不要每次都让 CPU 逐个提交 kernel?答案就是 graph capture 和 graph replay。
2. CUDA Graph 怎么工作#
CUDA Graph 的执行方式可以简化成:
第一次:
capture A -> B -> C -> D
instantiate graph executable
之后:
replay graph
replay graph
replay graph也就是说,第一次运行时把 GPU 操作序列记录下来,形成一张包含节点和依赖关系的图。后续如果还要执行同样结构的 workload,就不再逐个 launch kernel,而是直接 replay 这张图。
可以把普通 eager 和 CUDA Graph 放在一起看:
普通 eager:
CPU -> launch A
CPU -> launch B
CPU -> launch C
CPU -> launch D
CUDA Graph:
CPU -> replay [A -> B -> C -> D]这就是它最重要的工程价值:把多次小提交合并成一次大提交。
3. 它到底优化了什么#
CUDA Graph 主要优化三类问题。
第一,减少 CPU launch overhead。原来每个 kernel 都要走一次 CPU/driver 提交流程,现在一段图可以一次 replay。kernel 数量越多、单个 kernel 越短,这部分收益越明显。
第二,提升 GPU 利用率。当 CPU 提交任务不够快时,GPU 会在 kernel 之间等待。CUDA Graph 通过一次性提交一段 GPU 工作流,减少 GPU 等 CPU 的空隙。
第三,降低执行抖动。普通 eager 模式下,CPU 调度、driver 状态、系统负载都可能让 kernel launch 间隔出现波动。CUDA Graph 避开很多动态 dispatch 路径,重复执行时延迟更稳定。
但它也有一个很重要的边界:
CUDA Graph 优化的是提交开销,
不是单个 kernel 的计算效率。如果你的 profiler 显示大部分时间都在一个巨大的 GEMM 或 attention kernel 里面,kernel 之间几乎没有 gap,那么 CUDA Graph 的收益可能有限。它更适合“小 kernel 多、重复次数多、CPU launch overhead 明显”的场景。
4. 基本生命周期#
使用 CUDA Graph 通常会经历四个阶段:
warmup
capture
instantiate
replay4.1 Warmup:先让系统稳定下来#
warmup 的作用是先正常跑几次模型,让 CUDA、PyTorch、cuBLAS、cuDNN、allocator 和 kernel selection 完成初始化。
这一步很重要,因为 capture 阶段最好不要出现新的动态行为,比如第一次内存分配、第一次选择 kernel、第一次初始化某个库句柄。否则图里捕获到的行为可能不稳定,甚至直接 capture 失败。
4.2 Capture:记录 GPU 操作序列#
在 PyTorch 里,capture 常见写法是:
g = torch.cuda.CUDAGraph()
with torch.cuda.graph(g):
static_output = model(static_input)这里捕获的不是 Python 源码本身,而是这段 Python 代码触发出来的 CUDA 操作,包括 kernel launch、内存操作和它们之间的依赖关系。
4.3 Instantiate:生成可执行图#
capture 之后,CUDA runtime/driver 会把 graph 转成可执行对象。这个阶段可以提前准备 launch 结构,减少后续重复执行时的调度成本。
4.4 Replay:反复重放#
之后每次执行时,只需要把新输入拷贝到固定 buffer,再 replay:
static_input.copy_(real_input)
g.replay()
output = static_output关键点在于:static_input 这个 tensor 的地址不能变。CUDA Graph 记录的是底层 CUDA 操作和参数,其中包括内存地址。replay 时不能换一个新 tensor,只能把新数据 copy 到原来的 tensor 地址里。
错误理解:
static_input = real_input # 地址变了,不适合 graph replay正确方式:
static_input.copy_(real_input) # 地址不变,只更新内容5. PyTorch 里怎么用 CUDA Graph#
PyTorch 里最直接的方式是手动使用 torch.cuda.CUDAGraph:
import torch
model = model.cuda().eval()
g = torch.cuda.CUDAGraph()
static_input = torch.empty((batch_size, hidden_size), device="cuda")
# warmup
s = torch.cuda.Stream()
with torch.cuda.stream(s):
for _ in range(3):
_ = model(static_input)
torch.cuda.current_stream().wait_stream(s)
# capture
with torch.cuda.graph(g):
static_output = model(static_input)
# replay
for real_input in inputs:
static_input.copy_(real_input)
g.replay()
output = static_output这段代码的本质是:
真实输入 real_input
-> copy 到固定地址 static_input
-> replay graph
-> 结果写入固定地址 static_outputPyTorch 还提供了 torch.cuda.make_graphed_callables(),可以把 module 或 function 包装成 graphed callable,并兼容 forward/backward 场景。
另外,torch.compile(mode="reduce-overhead") 也可能在 compiler 层尝试引入 CUDA Graph。自动方式使用更方便,但在极致性能优化里,手动 capture 通常更可控,因为你能明确管理 shape、buffer、warmup 和 replay 时机。
6. 为什么它要求 shape 和地址稳定#
CUDA Graph 最大的限制来自同一个事实:
它 replay 的是一张已经捕获好的执行图。因此,它希望这些东西尽量固定:
- 输入输出 tensor 的内存地址;
- tensor shape;
- kernel 参数;
- 执行路径;
- 控制流;
- capture 期间的内存分配行为。
如果 capture 时的输入是:
batch_size = 8
seq_len = 1
hidden_size = 4096那么 replay 时最好还是同样的 shape。否则,原来捕获的 kernel 参数、grid/block 配置、内存访问模式可能不再适用。
这也是为什么很多 serving 框架会做 bucket:
capture size = [1, 2, 4, 8, 16, 32, 64, 128]真实请求来了以后,先把 batch size 落到某个 bucket,再使用对应的 graph replay。
7. Capture 期间哪些操作有风险#
CUDA Graph capture 期间最怕动态行为,尤其是 CPU 和 GPU 同步。
下面这些操作通常要避免:
x.item()
print(cuda_tensor)
torch.cuda.synchronize()
if x.sum() > 0:
...它们的问题在于:CPU 可能需要等待 GPU 的结果,或者 Python 控制流依赖 GPU 数据。这会破坏 capture 想要的异步执行模型。
数据相关控制流尤其容易踩坑。比如:
if x.sum() > 0:
path_a()
else:
path_b()第一次 capture 记录的是某一条路径。之后 replay 时不会重新执行 Python 判断,而是直接重放当时捕获的 CUDA 图。所以,CUDA Graph 更适合执行路径稳定的代码,不适合强动态分支。
8. 和算子融合有什么区别#
CUDA Graph 很容易和算子融合、编译优化混在一起,但它们解决的问题不同。
算子融合是:
kernel A + kernel B + kernel C
-> fused kernelCUDA Graph 是:
kernel A、kernel B、kernel C 仍然可能是三个 kernel
但 CPU 不再逐个 launch
而是 replay 一整张图所以可以这样区分:
| 技术 | 主要目标 | 改变 kernel 数量吗 |
|---|---|---|
| 算子融合 | 减少 kernel 数量和中间读写 | 通常会 |
| CUDA Graph | 减少重复 launch 提交开销 | 不一定 |
| 编译优化 | 生成更好的执行计划或 kernel | 可能会 |
实际系统里,这几件事可以叠加。例如:
torch.compile / Inductor 先做 fusion
CUDA Graph 再 capture 编译后的稳定执行段但理解时要分清层次:fusion 减少 kernel 本身,CUDA Graph 减少 launch 这些 kernel 的提交成本。
9. 在大模型推理中的作用#
在 LLM serving 里,CUDA Graph 通常更适合 decode 阶段。
原因是 decode 每一步通常只生成一个 token,计算形态更稳定;而 prefill 阶段 prompt 长度变化大、attention metadata 变化大、KV cache 写入量也更动态。
可以粗略对比:
prefill:
prompt 长度差异大
attention shape 变化大
KV cache 写入量大
更动态
decode:
每步生成 1 token
batch size 可以 bucket
shape 更容易固定
更适合 graph replay从 serving engine 的视角看,普通路径像这样:
scheduler 决定 batch
构造 metadata
调用 model forward
逐个 launch CUDA kernels
返回 logitsCUDA Graph 路径像这样:
scheduler 决定 batch
匹配 capture size
把输入和 metadata copy 到固定 buffer
graph.replay()
返回 static output它对性能的帮助通常体现在:
- 降低 decode step latency。
- 提高小 batch GPU 利用率。
- 减少 kernel 间 gap。
- 降低 P50/P99 latency jitter。
- 减少 Python、C++ runtime 和 driver launch 路径开销。
这也是为什么 vLLM 这类推理框架会围绕 CUDA Graph 做 capture size、full graph、piecewise graph、decode-only graph、attention backend compatibility 等工程设计。
10. 为什么会额外占显存#
CUDA Graph 不是零成本优化。它通常会额外占用显存,原因包括:
graph executable 元数据
static input/output buffer
graph memory pool
不同 batch bucket 对应的多份 graph如果你 capture 了很多 size:
[1, 2, 4, 8, 16, 32, 64, 128, 256]框架可能需要为不同 size 准备不同的 static buffer 和 graph executable。这样 replay 命中率更高,padding 浪费更少,但显存占用和初始化时间也会上升。
所以 serving 里的常见权衡是:
| 策略 | 好处 | 代价 |
|---|---|---|
| capture size 多 | 命中率高,padding 少 | 显存占用更高,初始化更慢 |
| capture size 少 | 更省显存 | 可能更多请求走 eager 或 padding 到更大 bucket |
| 禁用 CUDA Graph | 最灵活,显存压力较低 | 失去 replay 带来的 launch overhead 优化 |
11. 和普通 eager 执行的区别#
| 维度 | 普通 eager CUDA 执行 | CUDA Graph |
|---|---|---|
| 提交方式 | 每个 kernel 单独 launch | 一整段图一次 replay |
| CPU 开销 | 每个 kernel 都有 launch overhead | 大幅减少重复提交开销 |
| 灵活性 | 高,shape 和控制流可变 | 低,要求 shape、地址、路径稳定 |
| 首次成本 | 低 | 需要 warmup、capture、instantiate |
| 适合场景 | 动态 workload | 稳定、重复、小 kernel 多的 workload |
一句话说,CUDA Graph 是用灵活性换更低调度开销和更稳定延迟。
12. 常见误区#
误区一:CUDA Graph 会让所有 GPU 计算变快#
不会。
它主要减少 launch overhead。如果瓶颈在大矩阵乘法、attention kernel 或显存带宽本身,CUDA Graph 可能帮不上太多。
误区二:replay 时可以随便换输入 tensor#
不可以。
replay 依赖 capture 时记录的内存地址。正确做法通常是提前准备 static buffer,然后用 copy_() 更新内容。
误区三:CUDA Graph 等于算子融合#
不等于。
算子融合减少 kernel 数量,CUDA Graph 减少提交这些 kernel 的开销。二者可以同时存在,但不是一回事。
误区四:capture size 越多越好#
不一定。
capture size 多可以减少 padding 和 eager fallback,但会增加显存占用、初始化时间和 graph 管理复杂度。serving 系统里要按 workload 分布权衡。
13. 怎么判断它有没有用#
可以先看 profiler。
如果你看到:
kernel 很短
kernel 之间 gap 很多
CPU launch overhead 明显
GPU utilization 不高
decode step 抖动明显CUDA Graph 很可能有收益。
如果你看到:
大部分时间都在大 matmul / attention kernel 里
GPU utilization 已经很高
kernel 之间 gap 很少
shape 和控制流非常动态那么 CUDA Graph 的收益可能有限,甚至会因为 bucket、padding、显存占用而得不偿失。
14. 总结一句话#
CUDA Graph 是一种把稳定 GPU 执行流程提前捕获成图,并在后续反复 replay 的机制。它牺牲一部分动态灵活性,换取更低的 CPU launch overhead、更少的 GPU 空泡和更稳定的执行延迟。
如果放到大模型推理里看,它尤其适合 decode 阶段、固定 batch bucket、多小 kernel、低延迟 serving 这类场景。
但它也要求框架认真处理 shape、tensor 地址、static buffer、warmup、capture size 和内存池。CUDA Graph 不是魔法加速开关,更像是一条 runtime fast path:workload 越稳定、重复越多、launch overhead 越明显,它越值得用。
参考资料#
- NVIDIA, CUDA Programming Guide: CUDA Graphs: https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/cuda-graphs.html
- PyTorch, Accelerating PyTorch with CUDA Graphs: https://pytorch.org/blog/accelerating-pytorch-with-cuda-graphs/
- NVIDIA, CUDA Graph Best Practice for PyTorch: Quantitative Benefits: https://docs.nvidia.com/dl-cuda-graph/cuda-graph-basics/quantitative-benefits.html
- NVIDIA, PyTorch CUDA Graph Integration: https://docs.nvidia.com/dl-cuda-graph/torch-cuda-graph/torch-integration.html
- vLLM, CUDA Graphs: https://docs.vllm.ai/en/stable/design/cuda_graphs/
- vLLM, Conserving Memory: https://docs.vllm.ai/en/v0.11.0/configuration/conserving_memory.html