Prhub

#40654 [Core] Avoid seq_lens_cpu GPU->CPU sync

原始 PR 作者 njhill 合并时间 2026-04-24 08:35 文件变更 19 提交数 5 评论 7 代码增减 +142 / -26

执行摘要

避免 GPU→CPU 同步,引入 seq_lens_cpu_upper_bound

主要动机是消除 prefill 阶段的 CPU 同步(特别是 DS3.2 场景)。作者在 issue 评论中说明:'eliminating the current prefill cpu sync for DS3.2',并希望将 V1 特有的 optimistic_seq_lens_cpu 泛化到 CommonAttentionMetadata 中,同时废弃 seq_lens_cpu 属性以避免隐式同步。

此 PR 值得精读,尤其是从事 speculative decoding 或 attention 后端开发的工程师。设计决策:用 CPU 计算的上界替代 GPU 张量访问,是一种典型的异步优化模式。建议关注 eagle.py 中减法操作的实现,确认其无同步。

讨论亮点

主要讨论集中在 gemini-code-assist 对 eagle.py 中减法操作的潜在同步风险提出质疑,认为 num_rejected_tokens 可能是 GPU 张量,赋值给 seq_lens_cpu_upper_bound 会导致后续 .item()/.numpy() 触发同步。作者直接回复 'Incorrect',表明该问题已被充分考虑(num_rejected_tokens 已在 CPU 上或操作不会引入同步)。WoosukKwon 批准了该 PR。

实现拆解

实现主要分为以下步骤:

  1. InputBatch 中新增 seq_lens_cpu_upper_bound 字段vllm/v1/worker/gpu/model_runner.py
    - 在 prepare_inputs 方法中,通过 np.add(num_computed_tokens_np, num_scheduled_tokens) 计算上界,并转换为 Tensor 存入 InputBatch
    - 该上界是纯 CPU 计算的,不依赖 GPU 数据,避免了同步。

  2. CommonAttentionMetadata 中新增 seq_lens_cpu_upper_bound 字段(涉及 vllm/v1/attention/backends/utils.pyvllm/v1/worker/gpu/attn_utils.py 等)
    - 在构建元数据时传入该字段,替代原先使用 seq_lens_cpu 计算 max_seq_len 等操作。

  3. 在各 attention 后端中改用上界cross_attention.py, mla_attention.py, flex_attention.py, mla/indexer.py
    - 在交叉注意力中,利用 seq_lens_cpu_upper_bound - query_lens_cpu 计算 num_computed_tokens_cpu,避免从 GPU 读取。
    - 在 MLA 注意力中,用上界计算 context_lens_cpu

  4. ubatch_utils.py 中调整切片逻辑
    - 读取 _seq_lens_cpu 时先检查是否为 None,避免触发属性同步。
    - 改用 seq_lens_cpu_upper_bound 计算 max_seq_len

  5. postprocesspostprocess_pool 中乐观推进 CPU 镜像model_runner.py
    - 每次后处理后,将 num_computed_tokens_np 加上 num_scheduled_tokens,确保上界在下一轮仍然有效。

  6. 在 speculative decoding 组件中适配eagle.py, dflash.py, llm_base_proposer.py
    - 在 eagle.py 中,对 seq_lens_cpu_upper_bound 进行减去 rejected tokens 的修正,保持上界正确。

文件 模块 状态 重要度
vllm/v1/worker/ubatch_utils.py UBatch modified 6.85
vllm/v1/worker/gpu/model_runner.py ModelRunner modified 6.43
vllm/v1/spec_decode/eagle.py SpecDecode modified 6.0
vllm/model_executor/layers/attention/cross_attention.py 交叉注意力 modified 6.1
vllm/model_executor/layers/attention/mla_attention.py MLA 注意力 modified 6.08

关键符号

prepare_inputs postprocess postprocess_pool prepare_attn _make_metadata_with_slice build (cross attention) build (MLA attention)

关键源码片段

vllm/v1/worker/ubatch_utils.py core-logic

核心切片逻辑:改用 _seq_lens_cpu 和 seq_lens_cpu_upper_bound,避免同步并计算 max_seq_len。

# vllm/v1/worker/ubatch_utils.py
# 改动核心:直接读取内部字段 _seq_lens_cpu 和新增的 seq_lens_cpu_upper_bound,
# 避免触发 .seq_lens_cpu 属性引起的 GPU→CPU 同步。
seq_lens = attn_metadata.seq_lens[request_slice]
# 直接访问私有字段以绕过同步;若为 None 则退化为 None
seq_lens_cpu = (
    attn_metadata._seq_lens_cpu[request_slice]
    if attn_metadata._seq_lens_cpu is not None
    else None
)
seq_lens_cpu_upper_bound = (
    attn_metadata.seq_lens_cpu_upper_bound[request_slice]
    if attn_metadata.seq_lens_cpu_upper_bound is not None
    else None
)
num_computed_tokens_cpu = (
    attn_metadata._num_computed_tokens_cpu[request_slice]
    if attn_metadata._num_computed_tokens_cpu is not None
    else None
)
# 对 split 的边界处理:克隆并修改上界(如果存在)
if splits_last_request:
    # ...
    if seq_lens_cpu is not None:
        seq_lens_cpu = seq_lens_cpu.clone()
        seq_lens_cpu[-1] -= tokens_skipped
    if seq_lens_cpu_upper_bound is not None:
        seq_lens_cpu_upper_bound = seq_lens_cpu_upper_bound.clone()
        seq_lens_cpu_upper_bound[-1] -= tokens_skippedassert seq_lens_cpu_upper_bound is not None
max_seq_len = int(seq_lens_cpu_upper_bound.max()) # 使用上界而非 seq_lens_cpu
vllm/v1/worker/gpu/model_runner.py data-contract

主入口:计算并传递 seq_lens_cpu_upper_bound,并在 postprocess 中推进 CPU 镜像。

# vllm/v1/worker/gpu/model_runner.py
# prepare_inputs 中计算 CPU 上界
# 利用 numpy 纯 CPU 计算,避免 GPU 张量访问
seq_lens_cpu_upper_bound_np = np.zeros(num_reqs_padded, dtype=np.int32)
np.add(
    self.req_states.num_computed_tokens_np[idx_mapping_np],
    num_scheduled_tokens,
    out=seq_lens_cpu_upper_bound_np[:num_reqs],
)
seq_lens_cpu_upper_bound = torch.from_numpy(seq_lens_cpu_upper_bound_np)
# 传入 InputBatch
return InputBatch(
    # ...
    seq_lens_cpu_upper_bound=seq_lens_cpu_upper_bound,
    # ...
)# postprocess 中乐观推进 CPU 镜像,保持上界正确
self.req_states.num_computed_tokens_np[idx_mapping_np] += (
    input_batch.num_scheduled_tokens
)
vllm/model_executor/layers/attention/cross_attention.py data-contract

交叉注意力:用上界计算 num_computed_tokens,避免读取 GPU 属性。

# vllm/model_executor/layers/attention/cross_attention.py
# 利用上界计算已计算的 token 数,避免调用 .num_computed_tokens_cpu 属性(触发同步)
query_lens_cpu = (
    common_attn_metadata.query_start_loc_cpu[1:]
    - common_attn_metadata.query_start_loc_cpu[:-1]
)
assert common_attn_metadata.seq_lens_cpu_upper_bound is not None
num_computed_tokens_cpu = (
    common_attn_metadata.seq_lens_cpu_upper_bound - query_lens_cpu
)
num_cache_decodes = (num_computed_tokens_cpu > 0).sum().item()

评论区精华

eagle.py 中上界减去 num_rejected_tokens 可能引入同步 正确性

gemini-code-assist 指出:计算 `new_seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound - num_rejected_tokens` 若 `num_rejected_tokens` 是 GPU 张量,会导致同步,并且赋值给 `seq_lens_cpu_upper_bound` 后后续 `.item()` 调用仍会同步。

结论:njhill 回复 'Incorrect',表明该问题已考虑,`num_rejected_tokens` 在 CPU 上(来自 verification 逻辑),无同步风险。 · 已解决

风险与影响

  1. 核心路径变更(所有 attention 后端和 model runner):引入 seq_lens_cpu_upper_bound 后,若在极端情况下上界过紧(如 speculative decoding 中 rejection 较多),可能导致 workspace 分配不足或 kernel dispatch 错误。但逻辑上上界始终 >= 真实长度,不会低估。
  2. 兼容性风险seq_lens_cpu 属性仍被部分后端使用(如 flash attention),但 PR 已确保主要路径(cross, MLA, flex)都已迁移。遗留路径若触发同步不会导致错误,只会性能回退。
  3. 测试覆盖:测试文件有相应调整,但未发现对边界条件(如空请求、dummy run)的专项测试。

用户影响:降低推理延迟,特别是在 long context 或 speculative decoding 场景下,消除 GPU→CPU 同步等待。系统影响:减少跨设备数据移动,提升吞吐量。团队影响:统一了上界计算模式,后续开发者应优先使用 seq_lens_cpu_upper_bound 而非 seq_lens_cpu

核心路径变更 跨模块适配

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论