Prhub

#36534 [DSV4][BCG] Optimize the heavy memory use of C4 Indexer when BCG is enabled

原始 PR 作者 SYChen123 合并时间 2026-09-16 08:23 文件变更 23 提交数 29 评论 23 代码增减 +344 / -23

执行摘要

新增预填充图固定上下文参数,C4 索引器内存开销降 58%

PR body 明确指出:开启 BCG 后 C4 indexer 内存压力巨大,根因是形状为 [N, context_length] 的索引器 logits 被 pad 到 [N, max_position_embeddings];DeepSeek-V4 的 max_position_embeddings 为 1048576,当 N=8192 时 logits 本身就要占用 8192 * 1048576 // 4 * 4 = 8GB。因此提出新增 --cuda-graph-prefill-max-context,让 logits 只 pad 到用户指定上下文(如 262144)而非模型最大上下文,并规定重放时若任一请求 KV 上下文超过该值则回落 eager。

值得精读。该 PR 展示了"固定上下文元数据 + 超限 eager 回落"的设计模式,以及如何把一个新的性能开关安全地嵌入 prefill CUDA graph 核心路径:能力标记(supports 属性)、启动期校验、守卫双保险(can_run_graph + 后端内 raise)。工程上也有借鉴意义:review 驱动的 getattr 禁令促使数据字段显式化,参数解析收敛到 server args 层而不是散落在 runner 中。关注后续 DSV4 系列演进(如 #39677)对这一契约的扩展。

讨论亮点

核心讨论集中在代码结构与守卫可靠性上:

  • 禁止 getattr:Oasis-Git 在 prefill_cuda_graph_runner.py 和 deepseek_v4_backend.py 多处评论 "Do not use getattr" / "pls be careful about the use of getattr. refer: .claude/rules/no-getattr-defensive.md"。SYChen123 的解法是把 max_seq_len_override 变成 ForwardBatch 的正式字段(默认 None),从而移除所有 getattr 访问。
  • 解析逻辑位置:Oasis-Git 要求 "This part should be in the server args"(针对 runner 内的 _resolve_max_context_size),最终迁移到 cuda_graph_hook.py 的 finalize_cuda_graph_prefill_max_context,并通过 declare_resolution 声明式回写。
  • 命名收敛:参数从最初的 cuda_graph_prefill_context_bucket 改名为 cuda_graph_prefill_max_context(保留 --context-bucket 别名)。
  • 空 context bucket 问题:早期迭代支持多个 context bucket 捕获(token bucket × context bucket 笛卡尔积),Oasis-Git 质疑 "context size may be empty?";最终合入版本简化为单一 max_context_size,图 key 保持 token-only,超限统一走 eager 回落。
  • 总体评价:Oasis-Git 在整体 review 中说 "In general I think the functionality is good. Here are some comments for code structure. Also pls update the pr description.",随后补丁全部落实并 APPROVED。

实现拆解

实现按 5 步展开:

  1. 新增 CLI 参数与配置解析:在 python/sglang/srt/arg_groups/fields/exec_.py 的 ExecGraph 中新增 cuda_graph_prefill_max_context 字段,带 --context-bucket 别名,使用 human_readable_int 类型解析器支持可读数值;在 cuda_graph_hook.py 的 parse_cuda_graph_config 中把它折入 Phase.PREFILL.max_context_size,并新增 finalize_cuda_graph_prefill_max_context 在 page_size 解析之后执行 page 对齐与模型 context_len 校验,通过 declare_resolution 回写配置。

  2. 配置数据契约与注意力后端能力标记:cuda_graph_config.py 为 PhaseConfig 增加 max_context_size 字段并加入 PREFILL 阶段允许的 key;forward_batch_info.py 为 ForwardBatch 增加 max_seq_len_override(默认 None)作为固定上下文元数据的显式契约字段,替代此前 review 中要求移除的 getattr 访问;base_attn_backend.py 提供 supports_prefill_cuda_graph_max_context_size = False 默认值,DeepseekV4AttnBackend 置 True,TBO 与 hybrid 包装器转发 primary 的同名属性。

  3. prefill CUDA graph runner 的捕获调整:prefill_cuda_graph_runner.py 在 __init__ 中读取 max_context_size 并调用 _validate_max_context_capacity 校验不超过 req_to_token 池宽度;TC_PIECEWISE 与 CP breakable 两条路径通过 _ignore_max_context_size 暂时忽略该参数并打 warning(带 TODO(SYChen123));不支持的注意力后端在设置参数时于启动期直接 ValueError;capture_prepare 将捕获 context_length 收敛为 min(max_context_size or 模型 context_len, 模型 context_len);capture/replay 的元数据缓存 attn_metadata_buffers 改以 ShapeKey 为键,并把 max_seq_len_override 透传到 padded view。

  4. eager 回落判定:新增 _batch_max_context_len 计算 batch 最大上下文(优先走 CPU 侧 seq_lens_cpu 避免 GPU 同步);can_replay_locally 新增 batch_max_context_len 参数,超过 max_context_size 即拒绝走图;can_run_graph 统一入口;dp_attn.py 的 _local_prefill_cuda_graph_vote 在 DP 场景把本地 batch 的上下文长度传入投票,保证各 rank 判定一致。

  5. DSV4 后端接入与测试配套:deepseek_v4_backend.py 的 _build_forward_metadata 优先采用 max_seq_len_override,并在 live context 超过上限时抛 ValueError 作为防绕过守卫;capture 与 replay 的 max_seq_len 从固定 MAX_SEQ_LEN_FOR_CAPTURE 改为 max_seq_len_override or MAX_SEQ_LEN_FOR_CAPTURE。测试覆盖:test_server_args.py 的 TestCudaGraphPrefillMaxContextResolution(非正数与超模型上下文拒绝)、test_prefill_cuda_graph_padding.py 的 test_rejects_context_above_fixed_maximum(超限回落)与 test_unsupported_path_ignores_max_context_size(忽略路径)、test_server_args_cli_metadata.py 的可读值解析测试,以及 dsv4/tbo/dp_attn 相关单测。

文件 模块 状态 重要度
python/sglang/srt/model_executor/runner/prefill_cuda_graph_runner.py 预填充图 modified 8.62
python/sglang/srt/layers/attention/deepseek_v4_backend.py 注意力后端 modified 6.92
python/sglang/srt/arg_groups/cuda_graph_hook.py 参数解析 modified 7.45
python/sglang/srt/arg_groups/fields/exec_.py 服务参数 modified 6.04
python/sglang/srt/model_executor/cuda_graph_config.py 图配置 modified 5.88
python/sglang/srt/model_executor/forward_batch_info.py 批量信息 modified 5.74
python/sglang/srt/managers/scheduler_components/dp_attn.py 调度组件 modified 6.01
python/sglang/srt/layers/attention/tbo_backend.py 注意力后端 modified 5.53
test/registered/unit/server_args/test_server_args.py 单元测试 modified 6.28
test/registered/unit/model_executor/runner/test_prefill_cuda_graph_padding.py 单元测试 modified 6.28

关键符号

_validate_max_context_capacity _ignore_max_context_size _batch_max_context_len can_replay_locally can_run_graph capture_prepare _resolve_max_context_size finalize_cuda_graph_prefill_max_context _build_forward_metadata init_forward_metadata_for_breakable_cuda_graph_capture prepare_forward_metadata_for_breakable_cuda_graph_replay _local_prefill_cuda_graph_vote

关键源码片段

python/sglang/srt/model_executor/runner/prefill_cuda_graph_runner.py data-contract

核心变更文件:引入 max_context_size 的读取、容量校验、TC_PIECEWISE/CP 路径忽略、超限 eager 回落判定以及 ShapeKey 化元数据缓存,是固定上下文捕获契约的主载体。

# 固定上下文捕获模式的核心守卫:超限判定与 eager 回落。
# max_context_size 来自 --cuda-graph-prefill-max-context,回填到
# cuda_graph_config[prefill].max_context_size;为 None 时保持旧行为,
# C4 索引器仍按模型最大上下文分配 logits。
​
def _batch_max_context_len(forward_batch: ForwardBatch) -> Optional[int]:
    # 优先读 CPU 侧 seq_lens_cpu,避免在调度热路径触发 GPU 同步;
    # 手工构造的 batch 可能缺 CPU 长度,此时才回退到 GPU 张量并同步。
    seq_lens_cpu = forward_batch.seq_lens_cpu
    if seq_lens_cpu is not None:
        if torch.is_tensor(seq_lens_cpu):
            return int(seq_lens_cpu.max().item()) if seq_lens_cpu.numel() > 0 else 0
        return max((int(value) for value in seq_lens_cpu), default=0)
    seq_lens = forward_batch.seq_lens
    if seq_lens is None or seq_lens.numel() == 0:
        return None
    # 该回退路径会显式同步,宁可慢也不猜测 bucket。
    return int(seq_lens.max().item())
​
# can_run_graph 内:只在固定上限模式才计算 batch 最大上下文,避免无谓开销。
batch_max_context_len = (
    self._batch_max_context_len(forward_batch)
    if self.max_context_size is not None
    else None
)
​
# can_replay_locally 内:任一请求上下文超过固定捕获上限即拒绝走图,
# 由调度器回落 eager —— 捕获期元数据按 max_context_size 固定形状,
# 超限复用会导致越界或错误索引。
if self.max_context_size is not None:
    if batch_max_context_len is None or batch_max_context_len > self.max_context_size:
        return False
python/sglang/srt/layers/attention/deepseek_v4_backend.py core-logic

实际吃到内存收益的落点:C4 索引器 logits 的 max_seq_len 由固定上限决定;新增能力标记与 live context 防绕过守卫。

# DSV4 注意力后端:固定上下文元数据契约的实现点。
# 声明 supports_prefill_cuda_graph_max_context_size = True,prefill runner
# 才会允许 --cuda-graph-prefill-max-context 生效;C4 索引器 logits 的形状
# 由 max_seq_len 决定,捕获期被压到用户指定上限而非模型最大上下文。
​
class DeepseekV4AttnBackend(AttentionBackend, C4IndexerBackendMixin, ...):
    use_captured_forward_metadata_for_breakable_cuda_graph: bool = True
    supports_prefill_cuda_graph_max_context_size: bool = True
​
    def _build_forward_metadata(self, forward_batch, *, max_seq_len_override=None, ...):
        # max_seq_len_override 现在是有默认值的正式字段,不再需要 getattr。
        if max_seq_len_override is None:
            max_seq_len_override = forward_batch.max_seq_len_override
        if max_seq_len_override is not None:
            max_seq_len = max_seq_len_override
            if seq_lens_cpu is not None and len(seq_lens_cpu) > 0:
                actual_max_seq_len = int(seq_lens_cpu.max().item())
                if actual_max_seq_len > max_seq_len:
                    # 与 runner 侧 can_run_graph 守卫互备:任何绕过守卫的路径
                    # 都会在这里失败得响亮,而不是静默写出越界索引。
                    raise ValueError(
                        "Prefill CUDA graph max context size is smaller than the "
                        f"live context: {max_seq_len=} < {actual_max_seq_len=}"
                    )
        elif seq_lens_cpu is not None:
            max_seq_len = int(seq_lens_cpu.max().item())
        else:
            max_seq_len = self.MAX_SEQ_LEN_FOR_CAPTURE
python/sglang/srt/arg_groups/cuda_graph_hook.py dependency-wiring

参数解析主路径:review 要求把解析逻辑从 runner 移到这里,负责 page 对齐、模型上下文校验与声明式回写。

def _resolve_max_context_size(
    *, requested_size: Any, page_size: int, model_context_len: Optional[int]
) -> int:
    # 启动期解析:先拒绝非正数,再按 page_size 向上对齐(与现有 KV 池分页
    # 语义一致),最后与模型 context_len 比较,避免捕获形状超过模型上限。
    if requested_size <= 0:
        raise ValueError("--cuda-graph-prefill-max-context must be a positive integer")
    aligned_size = int((requested_size + page_size - 1) // page_size * page_size)
    if (
        model_context_len is not None
        and model_context_len > 0
        and aligned_size > model_context_len
    ):
        raise ValueError(
            "--cuda-graph-prefill-max-context exceeds the model context length: "
            f"aligned size {aligned_size} > {model_context_len}"
        )
    if requested_size != aligned_size:
        logger.info(
            "Page-aligning prefill CUDA graph max context size %d -> %d "
            "(page_size=%d).",
            requested_size, aligned_size, page_size,
        )
    return aligned_size
​
​
def finalize_cuda_graph_prefill_max_context(server_args: Any) -> None:
    # 严格排在 page_size 解析之后;解析结果写回 Phase.PREFILL.max_context_size,
    # 并保证图 key 仍是 token-only,不引入额外的 shape 维度。
    cfg = resolving_view(server_args)
    requested_size = cfg.cuda_graph_config.prefill.max_context_size
    if requested_size is None:
        return
    page_size = cfg.page_size
    assert page_size is not None and page_size > 0, (
        "page_size must be resolved before prefill CUDA graph max context size"
    )
    max_context_size = _resolve_max_context_size(
        requested_size=requested_size,
        page_size=page_size,
        model_context_len=model_config_of(server_args).context_len,
    )
    logger.info(
        "Prefill CUDA graph max context size: %d; graph keys remain token-only.",
        max_context_size,
    )
    declare_resolution(
        server_args,
        "_finalize_cuda_graph_prefill_max_context",
        cuda_graph_config=with_phase(
            cfg.cuda_graph_config, Phase.PREFILL, max_context_size=max_context_size
        ),
    )

评论区精华

禁止使用 getattr 防御式访问 style

Oasis-Git 在 prefill_cuda_graph_runner.py 的 _shape_key、can_replay_locally 以及 deepseek_v4_backend.py 的 max_seq_len_override 三处评论 "Do not use getattr",并引用仓库 .claude/rules/no-getattr-defensive.md 规则。

结论:SYChen123 把 max_seq_len_override 提升为 ForwardBatch 正式字段(默认 None),移除全部 getattr 访问。 · 已解决

参数解析逻辑应放在 server args 层 设计

Oasis-Git 在 prefill_cuda_graph_runner.py 的 _resolve_max_context_size 位置评论 "This part should be in the server args",认为 runner 不应承担参数解析职责。

结论:解析迁移到 cuda_graph_hook.py 的 finalize_cuda_graph_prefill_max_context,通过 declare_resolution 声明式回写。 · 已解决

context bucket 集合可能为空 正确性

Oasis-Git 在 _capture_one_stream 的问询 "context size may be empty?",针对早期多 context bucket 捕获版本的空序列处理。

结论:最终版简化为单一 max_context_size,去掉多 bucket 笛卡尔积捕获,图 key 保持 token-only;该疑虑随设计收敛而消除。 · 已解决

参数命名统一为 cuda_graph_prefill_max_context 设计

Oasis-Git 在 exec_.py 要求 "use cuda_graph_prefill_max_context",将最初的 cuda_graph_prefill_context_bucket 更名。

结论:字段更名并保留 --context-bucket 别名,PR body 同步更新。 · 已解决

精简注释 style

Oasis-Git 在 cuda_graph_config.py 要求 "trim the comments",认为 PhaseConfig.max_context_size 的注释过长。

结论:SYChen123 回复 "Done!" 并精简注释。 · 已解决

整体功能认可与 PR 描述更新 other

Oasis-Git 总体评价 "In general I think the functionality is good. Here are some comments for code structure. Also pls update the pr description."

结论:所有结构性问题修复后 Oasis-Git 给予 APPROVED 并合入。 · 已解决

风险与影响

  1. 核心图路径数据契约变更:prefill_cuda_graph_runner.py 的 metadata 缓存改以 ShapeKey 为键并新增 max_seq_len_override 透传,任何在捕获/回放顺序上的疏忽都可能引发形状不匹配;TC_PIECEWISE 与 CP breakable 路径目前显式忽略该参数(_ignore_max_context_size),用户设置后可能误以为生效,行为不一致。
  2. 超限回落依赖 seq_lens_cpu:_batch_max_context_len 在缺少 CPU 长度时回退到 GPU 张量并同步,或返回 None 触发 eager 回落;手工构造 batch 或未维护 CPU 长度的路径会出现性能回退,但不会出错。
  3. 防绕过守卫的硬失败风险:deepseek_v4_backend.py 的 _build_forward_metadata 在 live context 超过 max_seq_len_override 时直接 raise ValueError,任何绕过 can_run_graph 的路径都会让请求批次硬失败(fail-loud 优于静默越界,但仍是运行时崩溃点)。
  4. DP 注意力跨 rank 一致性:dp_attn.py 的投票只基于本地 batch 的 seq_lens_cpu,若不同 rank 因负载差异导致部分 rank 走图、部分走 eager,可能破坏 DP 集合通信的同步前提,存在 hang 隐患(提供了 can_replay_locally 的 rank-local 语义说明,但跨 rank 场景未完全消除)。
  5. 启动期强校验:非 DSV4 注意力后端 + 设置参数会在启动时 ValueError,属于有意为之,但对配置组合敏感的部署需要提前知晓。

对使用 DeepSeek-V4-Flash + BCG 的用户:捕获内存 43GB → 18GB(降 58%),解锁 chunked-prefill-size 与 max-captured-size 同时为 16384 的配置(此前 OOM),E2E 延迟提升约 10%,TTFT 从 1075.57ms 降到 830.93ms,输入吞吐从 278k 提升到 306k token/s;收益仅在显式设置该参数时生效,默认行为不变,无兼容性回归。对所有注意力后端:新增 supports_prefill_cuda_graph_max_context_size 契约字段,基础默认 False,未来新后端必须同步实现。对后续特性:ForwardBatch.max_seq_len_override 成为通用数据字段,为其他模型的固定上下文图捕获提供了可复用机制。

核心图路径数据契约变更 TC_PIECEWISE/CP 路径静默忽略参数 超限回落依赖 seq_lens_cpu DP 注意力跨 rank 一致性隐患 启动期强校验失败即报错

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论