AREX Feed Article
SGLang 将 BCG 设为 prefill 默认后端:构图快 3.8–5.2 倍,推出 prefill 全 CUDA Graph
2026 年 8 月 17 日,SGLang 团队在 LMSYS 官网发布技术博客《》,宣布把 Breakable CUDA Graph(BCG)设为 prefill 的默认后端:不再依赖 torch.compile,prefill 构图速度提升 3.8–5.2 倍;同时推出基于 FlashAttention(FA4)与 FlashInfer 的 prefill 全 CUDA Graph 捕获,博客称这是该能力的首次实现。博客自报的 prefill 实测中,BCG 比 eager 执行快 1.70 倍,全捕获达到 1.93 倍。
CUDA Graph 的思路是把 GPU 上一串 kernel 先录制、再以低开销重放,降低 CPU 侧反复发起 kernel 的启动开销。难点在 prefill:一个 batch 的 token 总数和请求数两个维度同时变化,而捕获后的图要求两者固定,加上部分 attention 后端依赖运行时元数据,prefill 因此难以整段入图。这次博客给出两条路径:BCG 允许在捕获时插入 eager 打断,全捕获则用填充把动态批变静态。团队还声称,BCG 机制由 SGLang 首创并开源,prefill 全捕获同样是 SGLang 的首创——这是团队自述口径,博客同时给出了可逐条核对的 PR 记录。
runner 管状态、backend 管捕获:CUDA Graph 拆成两层
重构之前,decode、prefill 和投机解码各有一套自己的 CUDA Graph runner,捕获形状、静态缓冲、重放和配置的逻辑彼此重叠,与 CUDA Graph 相关的服务器参数也变得越来越含糊。重构 PR 把职责切成两层:runner 管理捕获与重放所需的执行状态——捕获形状、静态输入缓冲、attention 元数据、把活 batch 填充到捕获形状;backend 决定这次执行如何被捕获,是整段一张图、一串可打断的段,还是编译器生成的片段。
因为 runner 只依赖一个公共 backend 接口,每条执行路径可以独立选择捕获策略。prefill 和 decode 各有自己的 runner;投机解码则继续加码:EAGLE draft、draft-extend 和 frozen-KV MTP draft 各自基于 decode runner 新建一个 runner,target verify 直接复用 decode runner,每次重放可为一个请求捕获多于一个 token。
三个 backend 因此并存。full 后端对每个选定形状捕获一张完整的 torch.cuda.CUDAGraph,没有 eager 区域,重放启动次数最少——这对 decode 是天然的,每个请求只贡献一个 token,主变量只有 batch size,用一组捕获好的 batch 桶就能覆盖。BCG 在捕获过程中把图安全区域录成段,允许选定的操作在两段之间以 eager 方式执行。TC piecewise 则靠编译器实现同样的分段:torch.compile 以 fullgraph=True 追踪前向,在注册的分割点切开 FX 图,逐段编译捕获。它是 SGLang 对部分捕获的第一个答案,至今仍在未验证 breakable 捕获的平台上随版本提供。
捕获时直接打断:BCG 用 521 行绕开 torch.compile
CUDA Graph 传统上要求被捕获区域完全图兼容,而现代推理负载里总有不能直接捕获的操作,prefill attention 是典型例子:部分后端依赖运行时元数据和 host 侧准备,一个不兼容操作就能让图覆盖不到更大的范围。
BCG 的解法是在捕获进行中直接插入显式打断。开发者用 @eager_on_graph 标记不兼容函数;捕获推进到该函数时先闭合当前图段,函数以 eager 方式运行,之后重新开段继续捕获。重放时图段与 eager 函数按同样顺序执行。跨段传递的张量被保留为持久边界缓冲:捕获时其设备地址固定,后续图段对着这个地址捕获;每次重放,eager 函数返回的新张量被拷贝进保留缓冲,下一段读到更新后的值。BCG 从不检查或追踪 eager 区域内部的操作——它们只要正确执行即可。
这与 TC piecewise 产出的是同一种可重放结构——图段夹着 eager 区域——区别只在构造方式:编译器路径先让编译器理解整个前向再切开,BCG 在捕获当下放分割点。代码量上,博客给出的对比是 521 行对 1,771 行,约为后者的四分之一。构图快 3.8–5.2 倍的原因很简单:根本没有编译环节。
编译恰恰是旧路径最贵的一步:博客称 torch.compile 占 prefill 图准备时间的 78–86%,并随模型复杂度增长。下图是 TP4、4×GB300 上 42 个捕获形状的构图耗时(不含权重加载与 kernel JIT):Qwen3-235B-A22B(94 层 MoE)上 BCG 只要 27.7 秒,TC piecewise 合计 106.6 秒(编译 90.4 秒 + 捕获 16.2 秒),慢 3.8 倍;GLM-5.2(78 层 MoE + DSA)上 BCG 35.2 秒,TC piecewise 183.1 秒(158.2 秒 + 24.9 秒),慢 5.2 倍。
绕开编译器还带来兼容性收益。SGLang 大量依赖自研 CUDA、Triton 和 JIT kernel,为了对 torch.compile 可见,过去常要经 torch.library 包装并提供 fake 实现;编译器还会约束图边界的位置——边界两侧的输入输出必须能被编译器表示,有时为了迁就编译器不得不换切点或扩大 eager 区域。随着 CUDA Graph 要与 DP attention、MoE all-to-all、LoRA、PD 分离、分层缓存、确定性推理等特性共存,团队称这越来越像在做一个 torch.compile 集成项目。BCG 让不兼容区域保持普通 eager 执行,图边界跟着 serving 逻辑走。调试也因此更直接:--debug-cuda-graph()把整个前向包进一个 eager 打断,模型照常逐算子执行,问题是否来自捕获本身一测便知。
token 桶与空 request slot:动态 prefill 如何变静态
prefill 难入图,在于一个 batch 同时变两个维度:token 总数和这些 token 分属的请求数,而捕获的图要求两者固定。博客称,新近的重构()改变了请求槽与 attention 元数据的表示方式,让受支持的 attention 后端不再必须留在图外。
做法是把两个维度分别固定。token 维度用 token 桶:活 batch 被填充到最近的捕获 token 数,类似 decode 把 batch size 填到捕获桶。请求维度用固定数量的 request slot:活请求占据前几个槽,未用的槽改写成零长度哨兵——序列长度、扩展长度为零,偏移停在真实 token 之后;若 batch 的请求数超过图的槽位数,整批回退到 eager 执行。哨兵元数据每次重放都要重写,因为捕获的图仍会读整张请求表;attention 元数据也在图外为填充后的 batch 重建。今天的全捕获因此依赖能按这种风格准备元数据的 attention 后端,即 FlashAttention(fa4)与 FlashInfer。
两种填充的代价不对称。填充的 token 是真实计算:它们成为捕获 batch 里的实际行,随同一批 GEMM 穿过稠密投影;SGLang 单独携带真实 token 数,让 MoE 路由、attention 和线性 attention kernel 跳过大部分填充区,但稠密计算仍要为多出的行付钱。空 request slot 则便宜得多——在 FlashAttention 的变长调度器里,工作量按每个序列的真实长度推导,零长度请求几乎不产生 attention 计算,主要添一点元数据和调度开销。博客的判断是:token 填充是贵的那一维,request slot 填充相对廉价。
实测:全捕获 1.93 倍、BCG 1.70 倍
三条捕获路径加一个 eager 基线,重放时的代价并不相同。博客的 benchmark 只测 prefill:固定输入长度、单个输出 token、一次一个请求,所有对比组都关掉 decode 图,跑在 gpt-oss-120b(TP4,4×GB300)上,四条路径全部可运行。结果是:全捕获比 eager 快 1.93 倍,BCG 1.70 倍,TC piecewise 1.45 倍。四条曲线在 32 倍的 prompt 长度范围内基本平坦——博客指出,这是 launch 开销而非计算量的特征。
BCG 在重放时也比编译器后端快 17%,差距来自每次前向各做什么:BCG 直接重放录好的图段,TC piecewise 每次都要回调编译后的 callable,先付 Torch Dynamo 的 guard 检查与分发,才轮到自己的捕获片段。GLM-5.2 上则只有 BCG 能捕获——TC piecewise 无法追踪它的前向,全捕获对它的稀疏 attention 没有路径——BCG 在那里比 eager 快 1.60 倍。
图内存的账:42 个形状 2.4 GB,峰值在 chunk 上限处消失
分段捕获要同时解决两个内存难题:别让分段把驻留图内存翻倍,以及捕获得足够远、让驻留图内存真正替代最坏的 eager activation 峰值。BCG 用三种复用机制避免翻倍:同一捕获形状的所有图段共用一个 CUDA Graph 内存池;穿过 eager 打断的张量在内存池已持有其存储时只保留弱引用——该技巧来自 vLLM 的 ,本是为让捕获图共享输出缓冲;所有捕获尺寸共享一个最大尺寸的输出缓冲,按各形状需要的行数切片。唯一不能这样处理的是跨打断传递的张量:下一段图对着它的地址捕获,它必须活着并在每次重放时原地更新。结果是较大的捕获表也保持克制:GLM-5.2 上 78 层 MoE 的 42 个形状,图内存合计 2.4 GB。
捕获上限比捕获形状数量更关键。图内存是驻留的,捕获时分配、服务器生命周期内常驻;eager activation 是瞬时的,峰值由最大 prefill 决定。如果捕获阶梯停在最大 prefill 尺寸之下,最大的 prefill 仍回退 eager,保留原来的 activation 峰值,同时服务器还要为之下所有驻留图付内存。由于 chunked_prefill_size 限定了单次 prefill 前向的上界,捕获覆盖到这个尺寸,最坏的峰值就消失:gpt-oss-120b 上从 0.56 GB 降到 0.001 GB,GLM-5.2 上从 1.55 GB 降到 0.35 GB——后者仍剩一截,因为它的稀疏 attention indexer 在打断处仍以 eager 运行。总内存还落到无图基线之下:gpt-oss-120b 低 0.51 GB,GLM-5.2 低 1.10 GB。相对几百 GB 的总体量这不算大,但团队认为它换来的是可预测性:一个随负载变化的瞬时峰值,变成捕获时就确定下来的固定分配。
从 #19102 到默认后端,扩散栈也接上了 BCG
BCG 的公开历史逐条可查。博客给出的时间线:2026 年 2 月 21 日,#19102 的初始提交就包含 BreakableCUDAGraph、eager-break 装饰器和结束/恢复捕获的运行时机制,PR 于 4 月 11 日合并;4 月,基于它的 把 BCG 扩展成 prefill 的免编译器分段后端,4 月 24 日合并,BCG 随后成为 SGLang 的默认 prefill CUDA Graph 策略。团队对“首创”的声明范围也做了限定:指的是面向开源 LLM serving 的这种运行时可打断捕获/重放机制;团队没有声称图分段或 eager 回退这些更宽泛的概念在此前没有先例。
BCG 的适用面已经超出 LLM 的 prefill。扩散栈()在去噪循环里反复执行同一个 DiT 前向,其中大量小 kernel 是 launch-bound:BCG 捕获稳定区域、跨去噪步反复重放,动态区域保持 eager。预热后,Qwen-Image 在单张 B200 上跑 512×512 的端到端延迟从 6.48 秒降到 2.45 秒,Z-Image 从 1.231 秒降到 0.662 秒。博客同时给出边界:BCG 去掉的是 launch 开销,不减少模型 FLOPs,也不会让 compute-bound 的 kernel 变便宜——它只在暴露出的 launch 空隙占执行时间相当比例时最有用。按博客的致谢页,这项工作由 SGLang 团队与 Meta 团队合作完成,并感谢了 NVIDIA、AMD、Thinking Machines Lab 和 Meta PyTorch 团队。
全捕获则仍标着“实验特性”。它需要显式开启,引擎会警告 full 是 experimental,并把生产负载指向 breakable 或 tc_piecewise;目前主要跑在 FlashAttention(fa4)和 FlashInfer 两个后端上。博客把“拓宽后端支持、调优桶位与 slot 的选择”列为尚未完成的工作。进入默认路径的是 BCG;prefill 全 CUDA Graph 跑出的 1.93 倍,目前仍挂在“实验特性”的标签下。