“一个参数没改,生成速度从 25.5 token/s 干到 107 token/s”——过去一个月,CUDA Graph 的"4 倍加速"故事在中文技术圈被反复转述。 但如果你自己真把 vLLM/SGLang 里的开关拨上去,很可能碰到三种懵:消费卡上吞吐纹丝不动、显存先涨了 1.6GB、或者开了等于没开。哔哩哔哩
最近 30 天里,至少 7 个互相独立的实测(知乎开发日志、Rust 推理引擎作者、SGLang/vLLM 源码向文章)刚好把这三个懵拼成了完整答案:CUDA Graph 的收益账,和大多数人理解的"省 launch 时间"不是一回事。
第一笔账:launch 开销到底是谁的,硬件决定天花板
知乎作者 AUCS 用自研框架 Kavor 在同一套代码上跑了两台机器:H100 + Xeon 6430,和 4080 Super + 9700X,切 CUDA Graph 开关对比 decode 的 TPOT。结果 H100 上接近 2 倍;4080 Super 上只省了 0.4ms。他用 nsys 时间线解释了差别:eager 模式下 CPU 要逐个 launch kernel,kernel 之间有等待空隙;而 CUDA Graph 把整段工作交给一次 cudaGraphLaunch,kernel 之间只剩 ns 级间隔。但关键在前提——launch 开销必须是真瓶颈才有得省。Qwen3-8B 权重约 16GB,4080 Super 带宽 736GB/s,每 decode 一个 token 理论下限就是 16.4GB ÷ 736GB/s ≈ 22.3ms;GPU 执行时间远大于 CPU 发射时间,发射早就被异步藏住了,Graph 自然没戏。反过来,服务器卡带宽高、GPU 快、CPU 主频低,decode 小算子一多,CPU 反而成了拖后腿的那个——GPU 不是自己在等,是在等 CPU 发命令。知乎

顺带一提,那个广为流传的 4 倍故事(PyTorch 官方博客的 gpt-fast:Llama-7B、batch 1,25.5→107 token/s)是 torch.compile + 静态 KV Cache + CUDA Graph 的合计收益,不是 Graph 一个开关的功劳——被当成"单参数魔法"传播,本身就是这轮误解的源头。PyTorch官方博客

第二笔账:Rust 作者泼的冷水——省下的根本不是 launch 时间
写 Rust 推理引擎 pegainfer/Kern 的开发者 SuSun 给了一个更狠的测法:在 GB300(CUDA 13.1)上,Qwen3.8-27B 一次高度 fuse 后的 decode forward 只有 839 次 kernel launch,host 侧发射总共约 2.5ms(单次约 3μs,cuBLAS 因为 split-K 要发两个 kernel,约 4~6μs)。这个发射速率远超 GPU 执行速率——按"省 launch 时间"的口径,Graph 的纸面收益应该是 0。但实测开关 Graph 端到端还是差 283μs。逐个 kernel 看耗时才发现:Graph 下每个 kernel 从"上一个结束"到"自己结束"比 eager 快 0.3~0.5μs(没开 PDL 的算子快约 1μs),839 次正好累计出 300μs。他的结论:Graph 的收益更多在 GPU 侧——省掉了每个 kernel 提交时的预处理,这部分除了开 Graph 别无它法。他还有一个工程圈共鸣很强的观察:Rust/C++ 引擎有时不开 Graph 也看不出差别,Python 引擎开关 Graph 的性能差会大得多。 也就是说,Python 调度层那笔隐性开销,才是很多自建框架吃到 Graph 红利的真正原因。知乎
反过来这也给了一个上限估计:如果你的框架 fuse 不好、kernel 数在 4000 量级、又不吃 PDL,每 kernel 按 0.8μs 算,Graph 纸面收益 3.2ms 起步。kernel 数量,比"开没开 Graph"更值得先数。
第三笔账:开了 ≠ 命中,命中率才是那把暗尺
nano_qwen 作者阿伟的 10 月开发日志最诚实:固定 batch 下 decode full graph 吞吐 1.74 倍,很漂亮;但真实动态负载里,他的捕获桶只有 {1,2,4,8,16},B3 不命中照样走 eager——整个 workload 统计下来 Graph 命中 2609 次、eager 回退 1641 次,回退比例 38.6%。 更扎心的是没命中的 B3 桶吞吐反而从 87.0 掉到 74.7 token/s。prefill 用 piecewise graph,512-token 单请求只测出约 1.3% 提升,图池还要多占约 1.6GB 驻留显存。知乎
vLLM 用户还有个组合坑:开投机解码时,Graph 匹配看的不是请求数而是一次 forward 的 num_tokens——max_num_seqs=8、num_speculative_tokens=3 时捕获上限至少要 8×4=32,上限设 16 直接初始化报错,运行时超限就悄悄回退 eager。 PyTorch 官方博客那条性能阶梯里,最右一格正是投机解码带来的增量(int4+spec 到 244.7 tok/s)——收益阶梯每多叠一层,batch 形状就更容易顶出 Graph 的捕获范围:知乎

而显存这笔账,Minghe 的文章《省下了显存,为什么吞吐没有提高?》把链路拆穿了:省下的显存不会自动变成 KV 池容量——多数引擎启动时静态分配,调参后要盯的是引擎实际报告的 KV token 容量,而不是 nvidia-smi 上的空闲显存。 就算 KV 池真变大了,测试并发 20 打到 120 的容量池也吃不满;就算吃满了,缩小捕获范围还会把 CPU 提交开销放回去。他给的简化例子很说明问题:原来每步 80 请求 ×10ms = 8000 tok/s,改成 100 请求 ×14ms = 7143 tok/s——省了内存,赔了执行,是这类调参最常见的结局。知乎
prefill 侧大家也想清楚了:vLLM 至今不给 prefill 上 CUDA Graph,开发者 MaxwellGeng 的实测解释是 prefill 本身计算量大,host 提交被上一帧执行完全盖住,continuous batching 拉满之后 Graph 那点收益可忽略。知乎
生态在补的,恰好是这三笔账的缝隙
SGLang 9 月底推了 Breakable CUDA Graph(BCG):允许在捕获中插 eager 断点,不兼容区域直接 eager。官方给的理由很现实——torch.compile 占 prefill graph 准备时间的 78%~86%,235B MoE 要 90 秒、GLM-5.2 要 158 秒,Graph 维护已经像在维护一个编译器集成项目。知乎
社区路线在收敛成两半:decode 吃 full graph(固定 batch 收益最大),prefill 吃 piecewise/bucket(收益温和、显存代价真实)。
nano_qwen 的下一步是 PD 分离:让 decode 侧维持稳定连续的大 batch,专门伺候 Graph 复用——调度器和 Graph 已经开始互为前提。
所以,你该不该开、怎么算这笔账
按最近这批实测,给四类人四条结论:
服务器卡(H100/GB 级)跑小 batch decode、CPU 主频不高的:开,这是 Graph 收益最大的人群,接近翻倍是真实存在的(AUCS H100 实测口径)。
消费卡本地部署(4080/5070 Ti 这一档)指望 4 倍故事的:先算带宽下限(权重 GB 数 ÷ 显存带宽),你的瓶颈大概率在搬权重不在 CPU,Graph 帮不上,别为它多掏 1.6GB 显存去挤 KV。
自建 Python 推理框架的:开,但先把 graph_calls / eager_calls 和 batch 分布打在派发位置——命中率低于八成时,你看到的"加速比"是两条路径的混合体;捕获桶要覆盖你的真实 batch,注意投机解码把 num_tokens 上限抬高。
写 Rust/C++、fuse 充分、PDL 开满的:Graph 收益缩水到每 kernel 0.3~0.5μs 量级,839 个 kernel 才 300μs,先把 kernel 数从 4000 降到 800 再谈 Graph。
一个可以照做的验证动作:用 nsys 对比开关前后同一个 decode step 的 CUDA HW 行——kernel 间隙从"山峰"变"平原",Graph 在干活;本来就紧密的,别硬开。
接下来值得盯的信号:vLLM 0.32 之后的 Graph 配置自动化、SGLang BCG 与 piecewise 路线之争、以及各家 PD 分离落地时 Graph 命中率的公开统计。这三个月的争论基本会沉淀成推理引擎的默认配置。