Skip to content

BCG(breakable CUDA Graph) ​

Breakable CUDA Graph(BCG)将模型前向组织为 CUDA Graph 图段与 Eager 函数交替执行的序列:稳定计算通过图重放降低 Kernel 启动开销,动态操作在图段之间正常执行。SGLang 在 Prefill 中利用这一机制,将常规 Attention 留在图外,并捕获周围的投影、归一化和 MLP。

本文以 SGLang 提交 0b3bb0cbe3 的 CUDA 常规 Llama Prefill 路径为例。BCG 是框架层对已有 CUDA Graph 能力的组织方式;每个图段仍是独立的 torch.cuda.CUDAGraph。

1. Prefill 捕获约束 ​

1.1 Kernel 启动开销 ​

一次模型前向包含大量 GPU Kernel。Eager 模式下,CPU 执行框架代码并逐次提交设备操作;当 Kernel 较短时,提交开销可能使 GPU 在相邻计算之间等待。CUDA Graph 预先捕获操作及其依赖,后续通过重放提交这些工作,减少重复经过 Python、框架和驱动的开销。

可重放区域需要保持捕获时的操作结构和设备地址等约束。输入值可以变化,但通常需要先写入固定地址的输入缓冲区;仅替换 Python 变量指向的新张量,不会自动修改图中已经记录的地址。

1.2 请求结构的动态性 ​

Prefill 通常把多个请求本轮新增的 token 拼成一个扁平 Batch。设请求数为 B,请求 r 本轮新增长度为 tr,隐藏维度为 D,则:

T=∑r=1Btr,X∈RT×D

Norm、线性投影和普通 MLP 主要沿 token 行执行计算。固定 T 后,这些操作通常不需要知道各行属于哪个请求。RoPE 需要位置值,但这些值可以通过固定地址的 positions 张量更新。

Attention 还要读取请求边界、序列长度、缓存前缀长度和 KV Cache 索引。相同的 T 可以对应一个长请求,也可以对应多个短请求;不同后端还可能根据这些信息执行运行时规划。因此,固定总 token 数并不足以让整个 Prefill 都满足图捕获条件。

BCG 将需要运行时处理的部分设为显式断点,使其周围的计算仍能使用 CUDA Graph。支持静态元数据准备的 Attention 后端也可以采用 Full Graph;Attention 位于图外是本文分析路径的设计选择,并非 CUDA 的普遍限制。

2. 分段原理 ​

2.1 Attention 边界 ​

Llama 的一层计算可以简化为:

text
Norm → QKV 投影 → RoPE → Attention → O 投影 → 残差/Norm → MLP

这里的 Attention 指消费 Q/K/V 的注意力后端调用。QKV 投影、RoPE 和 O 投影虽然属于模型的 Self-Attention 模块,却位于 BCG 断点之外。

在没有其他断点的情况下,跨层捕获结构为:

text
Graph 0
  Embedding → 第 0 层 Norm → QKV 投影 → RoPE
    ↓
Eager:第 0 层 Attention
    ↓
Graph 1
  第 0 层 O 投影 → 残差/Norm → MLP
  第 1 层 残差/Norm → QKV 投影 → RoPE
    ↓
Eager:第 1 层 Attention
    ↓
  ……
    ↓
Graph L
  最后一层 O 投影 → 残差/Norm → MLP → Final Norm
    ↓
Eager:LM Head 与 Logits Processor

图段由断点之间的连续执行区域决定,可以跨越 Decoder Layer 边界。 若有 L 层、每层只有一个 Attention 断点,主体对应 L+1 个图段和 L 个 Eager 调用。这个数量来自简化路径;MoE、稀疏 Attention、流水线并行等配置可能改变边界。

上述顺序依据 Llama 的 Attention 与 Decoder Layer 实现及 RadixAttention 断点整理。残差与 Norm 可能融合执行,图示表达数据流,不代表每个节点都对应一个独立 Kernel。

2.2 捕获与重放 ​

eager_on_graph 标记需要留在图外的函数。捕获执行到标记处时,框架结束当前图段,执行该函数并记录重放所需的调用状态,然后开启下一张图的捕获。

text
捕获:capture_begin → 稳定计算 → capture_end
                                ↓
                       执行并记录 Eager 函数
                                ↓
                       capture_begin → 后续计算

重放:Graph 0 → Eager 0 → Graph 1 → Eager 1 → ……

分段结构在捕获时确定,在线执行直接使用记录好的序列。这里的 Eager 仍可以提交 GPU Kernel;它表示该区域每次运行普通框架代码。图段之间的顺序也不等于每段结束都进行 CPU–GPU 同步,设备执行仍由 Stream 顺序和必要的依赖约束。

2.3 捕获策略对比 ​

策略图段的形成方式图外区域主要约束
Full CUDA Graph对选定捕获区域整体建图捕获区域之外的准备与尾部处理整个捕获区域需要兼容
TC Piecewise CUDA Graphtorch.compile 跟踪后在注册的算子边界切分注册的切分操作需要满足编译器跟踪要求
BCG捕获过程中遇到显式标记就结束并新建图段标记的普通函数图段本身及跨段数据需要满足重放约束

BCG 与 TC Piecewise 都能形成分段执行结构,区别在于构建方式。BCG 的分段不依赖 torch.compile 理解整个前向,但仍需要开发者选择正确断点;它不会自动把任意动态 Python 流程转成可重放的图。

3. Token 分桶与数据传递 ​

3.1 Token 分桶 ​

算子分段解决捕获兼容性,Token 分桶解决输入形状变化。设预先捕获的 token 数集合为 C,对符合重放条件的 Batch 选择能容纳它的桶:

T^=min{c∈C∣c≥T}

图内隐藏状态按 [\hat{T}, D] 的固定形状执行,真实 token 数 T 另行保留。以下假设配置中存在 256-token 桶,且请求满足其他重放条件:

请求组成新增 token 总数 T图内行数 T^Attention 使用的信息
一个请求,200 tokens200256一个请求的长度与 KV 索引
两个请求,80 和 120 tokens200256两个请求各自的长度与 KV 索引
一个请求,240 tokens240256当前请求的真实长度与 KV 索引

前两种情况可以复用同一桶的图段,但 Attention 必须按各自的请求边界执行,防止跨请求访问。投影会为 padding 行付出额外计算;常规 Attention wrapper 则截取真实 token 行,再调用后端。

分桶也与 Chunked Prefill 分属不同层次:Chunked Prefill 决定本轮处理哪些 token,分桶决定本轮计算适配哪组图,BCG 断点决定这次前向中的哪些操作留在图外。

3.2 固定输出缓冲区 ​

图 A 生成的 Q/K/V 和图 B 消费的 Attention 输出必须通过稳定地址衔接。SGLang 的常规路径在断点之前分配 output,将它作为参数交给图外 Attention:

text
Graph A:向固定地址写入本轮 Q/K/V
    ↓
Eager Attention:读取 Q/K/V 与当前 Batch 元数据
    ↓
向固定 output 地址写入 Attention 结果
    ↓
Graph B:从 output 读取结果,执行 O 投影等计算

后端支持指定输出地址时,直接写入 output[:T];否则返回临时结果,由 wrapper 复制到该区域。以常规 GQA 为例,Q 和输出可表示为 [\hat{T}, H_q, D_h],K/V 为 [\hat{T}, H_{kv}, D_h];Hq、Hkv 分别是本设备的 Query 与 KV 头数,Dh 为头维度。

这条 Attention 包装函数返回 None,通过原地修改 output 传递结果。BCG 的通用 _copy_output() 还负责支持的返回值拷贝场景,但普通 Attention 并不依赖它传递主输出。

3.3 内存与执行顺序 ​

各图段共享图内存池,BCG 对部分已捕获张量保存弱引用视图,避免 Python 引用让每层中间结果都长期占用独立空间。本文版本还在不同捕获尺寸之间复用主体输出缓冲区。

内存复用依赖既定的重放顺序:前一段写入的数据必须在被后续工作覆盖前消费完毕。Eager 函数能够动态执行,并不意味着它可以任意改变下一图段要求的输出形状、布局或地址。相关实现见 BCG 内存与捕获状态和 BCG Backend。

4. SGLang 实现 ​

4.1 Runner 与 Backend ​

PrefillCudaGraphRunner 负责捕获尺寸、静态输入、Batch 元数据和前向入口;BreakableCudaGraphBackend 负责把一次主体前向捕获成图段序列,并保存各尺寸的图与输出。

准备和执行流程如下:

text
初始化
  遍历捕获 token 桶
    → 构造对应形状的 ForwardBatch
    → 准备 Attention 元数据与预热
    → 捕获 Transformer 主体,记录图段与断点函数

在线 Prefill
  检查重放条件 → 选择 token 桶
    → 填充固定输入缓冲区
    → 更新当前 Batch 的 Attention 元数据
    → 交替重放图段和执行 Attention
    → 执行 LM Head 与 Logits Processor

本文版本只对 BCG 的 Transformer 主体建图,输出尾部保持 Eager,以适应请求数量与 logits 处理需求。普通 Attention 在重放前调用 init_forward_metadata();选择专用协议的后端则更新按桶保存的元数据对象,不能将普通路径推广到所有模型。

入口见 capture_one_shape() 与 load_batch()、execute()。

4.2 Attention 断点 ​

RadixAttention.forward() 在 Extend 模式且存在分段执行 context 时分配输出,再根据 BCG 状态选择包装函数。虽然 context 名称仍包含 tc_piecewise,BCG 也复用它来读取当前 Batch 信息,这不表示 BCG 经过了 torch.compile。

📄 radix_attention.py:断点与输出写入
python
# RadixAttention.forward:省略非 BCG 分支及输出维度差异处理
...
output = torch.empty_like(q)
if is_in_breakable_cuda_graph():
    breakable_unified_attention_with_output(
        q, k, v, output, save_kv_cache, self.layer_id, **kwargs
    )
...

# unified_attention_with_output:省略参数整理与平台分支
context = get_tc_piecewise_forward_context()
forward_batch = context.forward_batch
...
real_num_tokens = forward_batch.num_token_non_padded_cpu
query = query[:real_num_tokens]
...
forward_batch._attn_output = output[:real_num_tokens]
ret = get_attn_backend().forward(
    query, key, value, attention_layer, forward_batch, save_kv_cache, **kwargs
)
...
if ret.data_ptr() != output.data_ptr():
    output[:real_num_tokens].view(ret.shape).copy_(ret)
...

breakable_unified_attention_with_output = eager_on_graph(True)(
    unified_attention_with_output
)

4.3 重放序列 ​

BreakableCUDAGraph 保存 _segments 和 _break_fns。重放图段之后立即调用对应断点函数,断点函数再从当前 context 获取服务本轮请求所需的状态。

📄 breakable_cuda_graph.py:顺序重放
python
def replay(self) -> None:
    ...
    try:
        for i, seg in enumerate(self._segments):
            seg.replay()
            if i < len(self._break_fns):
                self._break_fns[i]()
    finally:
        ...

图内普通 Python 不会在重放时重新执行。因此,需要每轮更新的 Python 状态必须由 Runner 或断点函数处理;图内 Kernel 则通过固定地址读取更新后的张量值。

5. 性能与代价 ​

BCG 的收益来自减少图内区域的重复提交开销,并跳过 TC Piecewise 建图所需的编译器跟踪流程。它本身不减少模型 FLOPs,也不自动融合 Kernel。

方面作用与代价
稳态延迟减少图内 Kernel 的逐次提交;图段启动和 Eager 调用仍有开销
启动时间不经过 torch.compile 切图,但仍需预热并捕获各 token 桶
兼容性动态操作可留在断点中,图内区域仍需满足捕获约束
Padding桶覆盖更多输入长度,同时为投影等计算增加无效行
输出衔接固定缓冲区维持地址稳定,部分后端需要额外设备内复制
显存图及其缓冲区常驻,内存池复用可降低重复分配,但不能视为零成本

当大量小 Kernel 的提交间隙占据明显时间时,BCG 更有机会降低 Prefill 延迟;长输入的大型 GEMM 或 Attention 已占据主要时间时,启动开销的相对占比会下降。断点越多、图外区域越大,能够消除的提交开销也越有限。

验证时应固定模型、Attention 后端、并行配置、请求长度分布和并发度,分别比较 Eager、BCG 与支持的其他捕获方式。预热后同时观察 Prefill 延迟、TTFT、GPU Kernel 间隙、padding 比例和图内存,另行记录启动建图时间。本文分析源码与执行机制,未执行 GPU 性能测试。

6. 参考 ​

  1. SGLang:PrefillCudaGraphRunner
  2. SGLang:BreakableCudaGraphBackend
  3. SGLang:BCG 捕获与重放实现
  4. SGLang:RadixAttention
  5. SGLang:Llama 模型实现
  6. SGLang:TC Piecewise Backend