图捕获的取舍边界与分档捕获
在 **** 的 ****(8 卡,单卡 64GB HBM,卡间互联 392 GB/s)上用 vLLM 起服务,开启图捕获后,小批 decode 的吞吐上升。把同一服务切到长 prompt 的 prefill 负载,或让输入 shape 在运行中变化,整体吞吐反而低于关闭图捕获时;某些批大小的请求延迟抬升,日志里出现图捕获相关的启动报错并中断。图捕获折叠的是算子逐条下发的开销,代价是对输入 shape 的约束。开启后整体上升、局部变差,是它在覆盖窗口内外两种行为的直接结果:窗口外的输入退回逐条下发,拿不到收益,还要分摊捕获本身的显存与时间开销。
图捕获折叠的是什么
图捕获把一段子图在启动时捕获、之后按图重放,消除每步的 kernel launch 开销。收益集中在「算子碎、单 kernel 短、下发密集」的场景,典型是 decode 的小批;厂商文档对 decode 的表述是,decode 批被捕获进图并按图重放,省掉每步下发开销。prefill 的批大、shape 变化,SGLang 调参文档的判断是,图捕获对「小的、可预测的 batch size」有效,对 prefill 的大而多变的批收益极小,还会与动态 shape 冲突。
代价是约束。被捕获的图绑定一组固定 shape 与 batch size,只有落进覆盖窗口的输入走图,窗口外的输入退回 eager 逐条下发。窗口外那部分既拿不到收益,又要付捕获时的显存与时间开销;捕获区还要占显存,与 KV 池争同一份预算,捕获档位越多,占的激活与工作区越大,留给 KV 的就越少。
参数与取值
| 参数 | 作用 | 代价 / 前提 |
|---|---|---|
--enforce-eager(vLLM) |
关闭图捕获,算子逐条下发 | 单步开销上升、吞吐下降;定位手段,非生产配置 |
--disable-cuda-graph(SGLang) |
关闭图捕获 | SGLang 调参建议 prefill 节点设为 true,decode 节点不设 |
--cuda-graph-max-bs(SGLang) |
限定捕获的最大 batch size | 取值越大覆盖越多 batch,捕获时间与显存开销越大 |
cudagraph_capture_sizes(vLLM) |
显式给出捕获的档位集合 | 档位过多会耗尽 stream 资源,捕获失败 |
--enable-torch-compile / --torch-compile-max-bs(vLLM) |
对草稿模型做算子融合、autotune | 图捕获已开启时收益可能可忽略 |
关闭图捕获的旗标各家不同:vLLM 用 --enforce-eager,SGLang 用 --disable-cuda-graph。前者在 vLLM 上用于定位(让回退落到算子粒度、日志可见),后者按 SGLang 调参文档在 prefill 节点上使用。SGLang 对 --cuda-graph-max-bs 的说明是:它限定捕获的最大 batch size,取值越大覆盖越多 batch,但增加捕获时间与显存开销;当 max-running-requests 高而典型 batch 更小时,取小值降低捕获开销。
分档捕获的做法
图捕获不是对每个可能的 batch size 都捕获一遍,而是选一组档位。档位的取法有两条:一是给 --cuda-graph-max-bs 一个上界,框架在 1..上界 内按约定规则取一组;二是显式给出捕获集合——vLLM 用 cudagraph_capture_sizes,SGLang 用 cuda_graph_bs(2026-06 后拆成 cuda_graph_bs_decode / cuda_graph_bs_prefill)。取档位要对齐服务实际运行的 batch 分布:把 max-running-requests 与观测到的 batch 分布摆在一起,覆盖高频档、放弃长尾档。上界取大、档位取密的代价是捕获时间与显存占用一起涨,极端情况下耗尽 stream,直接捕获失败。
图捕获还有粒度之分:分段捕获把若干节点拼成一块一块的图,全图模式把整段前向包成一张图。分段捕获切得细、对不支持的算子容错好,但图块多、下发次数也多;全图模式下发次数少,代价是对算子覆盖面与 shape 的要求更严,遇到不支持的算子只能整体退回。在 vLLM 上这两档由 cudagraph_mode 选定(PIECEWISE / FULL 等),与档位集合 cudagraph_capture_sizes 是两个彼此独立的旋钮:前者决定怎么切,后者决定切哪些。
分档还决定了混批的待遇。落在档位上的 batch 走上界对齐的图,落在两档之间的 batch 要么向上取整到邻近档、要么退回 eager。档位之间留得越稀,向上取整带来的空算越多;档位取得密,捕获量与显存又增加。这是分档捕获的取舍点。
触发条件与现象
- 动态 shape / 变长输入:只有静态、可预测的 shape 落进覆盖窗口,动态输入掉出窗口退回 eager。
- prefill 长批:批大且 shape 变化,图捕获收益极小,还会与动态 shape 冲突。
- 档位过多:捕获档位集合太大,stream 资源不足,启动时直接报错中断。
- 模型结构耦合:图模式不是统一开关,与模型执行特征深度耦合;部分结构(如 MLA)对查询与 KV 头的比值有取值要求,不是所有结构都能进入图模式。
- 现象:开图后整体吞吐上升,但某个 batch size 或某类请求(长 prompt、多模态前缀)延迟反而变差;捕获失败时启动中断;捕获时间拉长首次可用延迟。
定位动作
- 先关图捕获看真实路径:加
--enforce-eager(vLLM)或--disable-cuda-graph(SGLang),让算子逐条下发,回退不再被折叠在图里,日志里能读到算子粒度的命中结果。关闭后单步开销上升,只在定位时用。 - 单独 profile 图捕获阶段:SGLang 的
--enable-profile-cuda-graph在捕获时按 CUDA 时间与 CPU 时间各打一张前 10 名算子表到日志,并把显存快照写成工作目录下的cuda_graph_runner_memory_usage.pickle,用于看捕获本身的耗时与峰值。 - 核对覆盖窗口:对比
--cuda-graph-max-bs与实际运行的 batch 分布,确认有多少请求落进捕获档位。 - 读捕获失败报错:SGLang 的关键字是
Capture cuda graph failed,紧随其后的一串Possible solutions就是处置清单——调小--mem-fraction-static、调小--cuda-graph-max-bs、去掉--enable-torch-compile,最后才是--disable-cuda-graph。 - 分档对照:对落进窗口与落在窗口外的两类请求分别取指标,确认变差的是窗口外那一类。
- 看显存:确认捕获区占用的预算是否把 KV 池压小,若并发同时下降,先缩档位再谈别的。
# 定位:先关图捕获,让回退可见(字段名示意)
python -m vllm.entrypoints.openai.api_server \
--model <model-path> \
--enforce-eager
# 按 SGLang 调参文档分工:prefill 节点关图,decode 节点按 batch 分布分档
python -m sglang.launch_server --model-path <model-path> --port 30000 --disable-cuda-graph
python -m sglang.launch_server --model-path <model-path> --port 30001 --cuda-graph-max-bs 4
# 捕获失败时的报错(SGLang 口径,字段名示意)
ERROR ... Capture cuda graph failed: <torch 报错原文>
Possible solutions:
1. set --mem-fraction-static to a smaller value (e.g., 0.8 or 0.7)
2. set --cuda-graph-max-bs to a smaller value (e.g., 16)
3. disable torch compile by not using --enable-torch-compile
4. disable CUDA graph by --disable-cuda-graph.
处置
- prefill 关图、decode 开图:SGLang 调参文档给出的分工是 prefill 节点设
--disable-cuda-graph、decode 节点不设。 - 按 batch 分布分档:
--cuda-graph-max-bs取小值,覆盖高频 batch,压低捕获开销。 - 缩小档位集合:
cudagraph_capture_sizes(vLLM)显式给一组档位,避免 stream 耗尽。 - 收敛动态 shape:把动态 shape 收敛到被覆盖的窗口内,让输入落进捕获档。
- 恢复后重测:定位完成后恢复图捕获,重新对照性能,避免把定位状态下逐条下发的表现当成该路径的量级。
记录字段
graph_mode: <enabled / disabled>
captured_or_eager: <命中捕获档 / 退回 eager>
graph_capture_sizes: <档位集合>
cuda_graph_max_bs: <值>
decode_batch_shape: <实际运行 batch 分布>
prefill_node: <是否关图>
capture_time: <捕获耗时>
capture_error: <Capture cuda graph failed / 成功>
perf:
ttft:
itl_p95:
throughput_in_window: # 落进捕获窗口
throughput_out_window: # 落在窗口外
path:
resolved_backend:
小结
图捕获折叠 decode 小批的算子下发开销,代价是绑定固定 shape 与 batch size,窗口外的输入退回 eager,既拿不到收益,又分摊捕获开销与显存。它的开关不能按布尔量处理:prefill 节点按 SGLang 口径关图,decode 节点按 batch 分布分档,档位取密要权衡捕获开销与空算。判定时先用 --enforce-eager 或 --disable-cuda-graph 关图看真实命中路径,再对窗口内与窗口外的请求分别取指标,确认变差的是哪一类。