Prhub

#42347 [Perf][4/n] Eliminate various GPU<->CPU syncs

原始 PR 作者 njhill 合并时间 2026-05-19 22:35 文件变更 23 提交数 2 评论 16 代码增减 +129 / -108

执行摘要

消除多处 GPU<->CPU 同步,优化多模态与推理性能

来自 PR #40561 的同步检测分析,该 PR 是清理 '低挂果实' 同步点的最后一波。目标是消除不必要的 GPU<->CPU 同步以提高模型执行效率。

该 PR 值得所有关心推理性能的工程师精读,尤其是 cast_overflow_tensors 的优化决策和 async_tensor_h2d 的封装思路。注意 gpu_model_runner.py_pp_receive_prev_sampled_token_ids_to_input_batch 的增量逻辑,后续可能与其他 PR 冲突。建议在 CI 中增加针对 PP 模式下 spec token 计数的回归测试。

讨论亮点
  • cast_overflow_tensors 无条件 clamp vs 条件检查:审查者 yewentao256 担心无条件 clamp 会引入额外开销。作者 njhill 提供 microbenchmark 表明无条件 clamp 节省了同步和归约开销,比条件检查快 2-8 倍。最终达成一致,保持无条件 clamp。
  • gpu_model_runner.py 中的 num_tokens_no_spec 增量yewentao256 质疑在占位符分支添加计数和 is_token_ids 标记是否会影响正确性。njhill 解释这是与最后 PP 阶段一致的书签管理,用于 spec token 放置和下一步起始索引,没有正确性风险。yewentao256 建议重命名 num_tokens_no_spec,但作者认为与当前 PR 无关,建议后续单独提出。
  • idefics2_vision_model.py 中无意删除 .cpu()gemini-code-assist 指出移除 .cpu() 会导致 CPU 张量索引 GPU 张量的设备错误。njhill 表示该文件是误包含,已经回退该修改。

实现拆解

  1. 引入 async_tensor_h2d 辅助函数:在 vllm/utils/torch_utils.py 中新增 async_tensor_h2d,用于将 Python 列表或张量异步拷贝到指定设备,替代 torch.tensor(..., pin_memory=True).to(device, non_blocking=False) 的同步模式。该函数被后续多处修改引用。

  2. 替换 isin_listThinkingBudgetStateHolder 中的同步构造:在 vllm/model_executor/models/utils.py 中将 isin_list 内的 torch.tensor + .to(non_blocking=True) 替换为 async_tensor_h2d;在 vllm/v1/sample/thinking_budget_state.py 中延迟 GPU 张量分配,将每个迭代的掩码写入改为在 CPU 构建索引列表后一次异步拷⻉到 GPU,消除每步的同步写入。

  3. 为多模态模型添加 non_blocking=True:在 qwen2_5_vl.pyqwen3_vl.pygranite_speech.pyphi4mm_audio.pyinternvl.pyqwen2_5_omni_thinker.pyqwen3_omni_moe_thinker.pybert.py 等模型中,将 .to(device) 调用改为 .to(device, non_blocking=True);在 qwen2_5_vl.pyrotary_pos_emb_thwget_rope_by_thw 中添加 pos_ids.to(cos.device, non_blocking=True);在 granite_speech.py_build_input_features_mask 中将张量构建改为按需异步拷⻉。

  4. 优化 GPU 模型运行器的输入预处理:在 vllm/v1/worker/gpu_model_runner.py_prepare_input_ids 中移除常见路径下的 is_token_ids.gpu 同步标量赋值;在 _preprocess 中用 NumPy 数组替代 GPU 张量索引避免同步;在 _pp_receive_prev_sampled_token_ids_to_input_batch 中为中间 PP 阶段添加正确的 is_token_ids 标记和计数增量。

  5. 消除 cast_overflow_tensors 中的同步检查:在 vllm/model_executor/models/utils.py 中移除 isinf().any() or isnan().any() 条件判断(该操作会触发 GPU<->CPU 同步并返回 Python bool),改为无条件执行 torch.clamp。经 microbenchmark 验证,无条件 clamp 比条件判断更快,且数值无害。

文件 模块 状态 重要度
vllm/model_executor/models/utils.py 工具函数 modified 7.15
vllm/v1/sample/thinking_budget_state.py 采样器 modified 6.94
vllm/model_executor/models/qwen2_5_vl.py 视觉模型 modified 6.49
vllm/v1/worker/gpu_model_runner.py 模型运行器 modified 6.4
vllm/model_executor/models/granite_speech.py 音频模型 modified 6.34

关键符号

cast_overflow_tensors isin_list maybe_create_thinking_budget_state_holder _apply_forcing_to_logits rotary_pos_emb_thw get_rope_by_thw _prepare_input_ids _preprocess _pp_receive_prev_sampled_token_ids_to_input_batch

关键源码片段

vllm/model_executor/models/utils.py data-contract

引入了 async_tensor_h2d、修改 isin_list 和 cast_overflow_tensors,消除多个同步点

# 文件 : vllm/model_executor/models/utils.pydef isin_list(
    elements: torch.Tensor,
    test_elements_list: list[int],
) -> torch.Tensor:
    # 使用异步张量创建避免 GPU<->CPU 同步
    test_elements = async_tensor_h2d(
        test_elements_list, dtype=torch.int64, device=elements.device
    )
    return torch.isin(elements, test_elements)
​
​
def cast_overflow_tensors(tensors: torch.Tensor, offset: float = 1000) -> torch.Tensor:
    # 无条件 clamp,移除之前的 isinf/isnan 同步检查。
    # 经 benchmark 验证,无条件 clamp 比条件检查快 2-8 倍且数值无害。
    clamp_value = torch.finfo(tensors.dtype).max - offset
    return torch.clamp(tensors, min=-clamp_value, max=clamp_value)
vllm/v1/sample/thinking_budget_state.py dependency-wiring

重构 _apply_forcing_to_logits 消除每步的同步标量写入,改为 CPU 构建索引后一次性传输。

# 文件 : vllm/v1/sample/thinking_budget_state.pydef _apply_forcing_to_logits(self, logits: torch.Tensor, ...) -> torch.Tensor:
    # 在 CPU 上构建活跃索引和强制 token 列表,避免每步同步写入 GPU 张量
    active_indices_cpu: list[int] = []
    force_tokens_cpu: list[int] = []
​
    for seq_idx in sorted(self._state.keys()):
        # ( 原有逻辑填充 force_index 省略 )
        for force_idx in force_index:
            mask_idx = self.cu_num_tokens[seq_idx] + force_idx
            if mask_idx < self._mask_capacity and mask_idx < logits.shape[0]:
                active_indices_cpu.append(mask_idx)
                force_tokens_cpu.append(self.think_end_token_ids[end_count])
​
    if active_indices_cpu:
        # 一次性异步传输到 GPU
        active_indices = async_tensor_h2d(active_indices_cpu, dtype=torch.long, device=logits.device)
        force_tokens = async_tensor_h2d(force_tokens_cpu, dtype=torch.long, device=logits.device)
        fill = logits.new_full((len(active_indices_cpu),), 1e9)
        logits.index_put_((active_indices, force_tokens), fill)
    return logits

评论区精华

cast_overflow_tensors 无条件 clamp 与条件检查的性能权衡 性能

审查者 yewentao256 担心无条件 clamp 会增加开销,作者 njhill 提供 microbenchmark 数据表明无条件 clamp 比旧的条件检查快 2-8 倍,且避免了同步。

结论:接受无条件 clamp 方案。 · 已解决

gpu_model_runner.py 中 num_tokens_no_spec 增量是否影响正确性 正确性

yewentao256 质疑在 PP 阶段添加 is_token_ids 标记和 num_tokens_no_spec 增量是否改变了正确行为。njhill 解释这与最后 PP 阶段的书签管理一致,用于后续 spec token 放置和起始索引计算。yewentao256 建议重命名变量但被作者认为与 PR 无关。

结论:认可该修改正确,重命名建议推迟到单独 PR。 · 已解决

idefics2_vision_model.py 中误删 .cpu() 导致设备错误 正确性

gemini-code-assist 指出移除 .cpu() 会导致 CPU 张量索引 GPU 张量,引发 RuntimeError。作者回应是意外包含,已回退。

结论:回退该文件的修改。 · 已解决

风险与影响

  • cast_overflow_tensors 语义变更:无条件 clamp 可能对本来没有溢出的张量施加轻微数值裁剪,但 torch.clamp 使用类型的 max-offset,对正常值影响极小,风险低。
  • gpu_model_runner.py 增量逻辑正确性:新增的 is_token_ids 标记和 num_tokens_no_spec 增量在非最后 PP 阶段的行为与最后阶段一致,但在某些边缘情况(如 prompt embeds 同时存在)可能与其他优化交互,需确保测试覆盖。
  • non_blocking=True 的隐式顺序依赖:在多流或 CUDA graph 场景下,non_blocking 可能使后续操作在传输完成前读取,但目前所有使用处都通过后续 copy_to_gpu 或同步模式保证顺序,风险可控。
  • 性能影响:预期减少推理前向路径中的 GPU<->CPU 同步点,降低延迟,特别在每步迭代中消除连续的 scalar 同步(如 isinf().any() 和标量赋值)效果显著。
  • 兼容性:无 API 变更,对用户完全透明。
  • 维护影响:引入 async_tensor_h2d 工具函数,供后续其他同步消除场景复用;但移除的同步逻辑可能增加调试难度(因为同步点减少)。
核心路径变更 潜在数值语义变化 缺少测试覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论