Prhub

#38933 [Performance Improvement] Update `batched_count_greater_than` to handle batch size 1 without recompile

原始 PR 作者 Lucaskabela 合并时间 2026-04-09 23:51 文件变更 3 提交数 3 评论 6 代码增减 +100 / -1

执行摘要

消除 batch size 1 导致 torch.compile 重编译

在 RL 推理场景下,采样器常以 batch size=1 调用,触发 PyTorch Dynamo 的 0/1 specialization,一旦 batch size 变为 ≥2 就会重编译,造成 ~205ms 的额外开销(PR 描述)。此项优化消除了该不必要的重编译。

建议优先合入。该 PR 是 PyTorch compile 动态形状处理的典型案例,适合所有需要理解 Dynamo 0/1 specialization 和 mark_unbacked 的开发者精读。设计决策(何时该用 dynamic=True、何时用 mark_unbacked、shape check 如何统一符号)值得记录为团队最佳实践。

讨论亮点
  1. 关于 mark_unbacked 放置位置:gemini-code-assist 建议应移到方法开头以保护 topk 等操作,但作者指出在编译后的采样器内 topk 和 gather 需要 batch dim 可计算(否则引发 GuardOnDataDependentSymNode),因此只能放在紧靠 batched_count_greater_than 调用之前。
  2. 关于 shape 检查的符号统一:gemini-code-assist 建议仅保留 x.shape[0] == values.shape[0] 检查,同时可消除 >=1 冗余;作者采纳了相等检查但保留了 >=1 作为安全断言。
  3. laithsakka 建议对 mark_unbacked 使用 min/max 参数(torch≥2.12)以提供更多信息,作为未来优化方向。

实现拆解

实现分为三部:

  1. vllm/v1/sample/sampler.pygather_logprobs 方法中,在调用 batched_count_greater_than 之前增加 torch._dynamo.decorators.mark_unbacked(logprobs, 0)mark_unbacked(token_logprobs, 0),使 batch 维度成为非特殊化的符号尺寸,避免 Dynamo 对 0/1 特化。
  2. vllm/v1/sample/ops/logprobs.pybatched_count_greater_than 函数中,移除 @torch.compile(dynamic=True) 中的 dynamic=True(因为仅 batch 维变化,不需要所有维动态),并添加 torch._check(x.shape[0] >= 1)torch._check(x.shape[0] == values.shape[0]),通过显式断言让编译器统一两个张量的 batch 维符号,减少编译图和 guard 数量。
  3. 新增测试文件 tests/v1/sample/test_batched_count_greater_than.py,包含 test_batched_count_greater_than_correctness 验证计数正确性,以及 test_gather_logprobs_no_recompile 通过自定义计次 backend 确保 batch size 1→2→8 仅编译一次。
文件 模块 状态 重要度
tests/v1/sample/test_batched_count_greater_than.py 采样器测试 added 6.9
vllm/v1/sample/sampler.py 采样器 modified 5.71
vllm/v1/sample/ops/logprobs.py 采样算子 modified 3.76

关键符号

batched_count_greater_than gather_logprobs

关键源码片段

vllm/v1/sample/sampler.py core-logic

在 gather_logprobs 方法中插入 mark_unbacked 调用,是消除重编译的关键。

    # ... previous code: token_logprobs = logprobs.gather(-1, token_ids)
​
    # Avoid 0/1 specialization recompile on the batch dimension
    # of the compiled batched_count_greater_than. mark_unbacked makes
    # the size fully symbolic so dynamo doesn't specialize when
    # batch_size transitions from 1 to >=2.
    torch._dynamo.decorators.mark_unbacked(logprobs, 0)
    torch._dynamo.decorators.mark_unbacked(token_logprobs, 0)
    token_ranks = batched_count_greater_than(logprobs, token_logprobs)
​
    # Concatenate together with the topk.
    indices = torch.cat((token_ids, topk_indices), dim=1)
    ...
vllm/v1/sample/ops/logprobs.py infrastructure

修改 batched_count_greater_than 装饰器与边界检查,消除不必要的 dynamic=True 并强制 batch 维符号统一。

@torch.compile(backend=current_platform.simple_compile_backend)
def batched_count_greater_than(x: torch.Tensor, values: torch.Tensor) -> torch.Tensor:
    # Explicit shape checks indicate to the compiler that both
    # tensors share the same batch dimension and it is >=1.
    # This allows symbol unification and avoids guards for 0/1 sizes.
    torch._check(x.shape[0] >= 1)
    torch._check(x.shape[0] == values.shape[0])
    return (x >= values).sum(-1)

评论区精华

mark_unbacked 放置位置 设计

gemini-code-assist 认为 mark_unbacked 应放在最前面以避免 topk 等操作也被特化;但 Lucaskabela 回复指出 topk 和 gather 在 Inductor 中需要可计算的 batch dim,否则会引发 GuardOnDataDependentSymNode,因此不能提前。

结论:未采纳移动建议;确认现有位置是合理的权衡。 · 已解决

shape check 统一符号 性能

gemini-code-assist 建议在 batched_count_greater_than 中添加 torch._check(x.shape[0] == values.shape[0]) 以统一两个张量的 batch 符号,并可移除 >=1 检查。作者采纳了相等检查但保留了 >=1。

结论:部分采纳:添加了相等检查和 >=1 检查。 · 已解决

风险与影响

该变更局限在 v1 采样器的编译路径中。主要风险是 mark_unbacked 可能引入数据依赖的符号错误,但代码仅对已存在的 logprobs 和 token_logprobs 调用一次,且位于非导出位置;作者已通过测试验证无 recompile。torch._check 是常规断言,开销极低。如果用户使用低于 2.12 的 torch 版本,不支持 mark_unbacked 的 min/max 参数(当前未使用),不影响功能。兼容性好,不涉及模型配置或文件格式变更。

影响用户:所有使用 v1 采样器的推理场景(默认在 v1 中)都会受益,特别是 batch size 经常变化的 RL 和聊天场景,重编译开销消除有助于降低延迟抖动。影响系统:无运行时配置变化,过程完全透明。影响团队:代码量小,易于维护,测试覆盖关键场景。影响程度:仅修改两个核心文件和一个测试文件,影响范围窄但价值高。

动态形状处理依赖 torch.compile 版本兼容

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论