Prhub

#45673 [BugFix] Support async scheduling with prompt embeds for multimodal models

原始 PR 作者 mrn3088 合并时间 2026-06-16 12:12 文件变更 2 提交数 2 评论 1 代码增减 +5 / -20

执行摘要

修复多模态模型 prompt_embeds 与异步调度的兼容性

继PR #45383为多模态模型支持prompt_embeds后,仍存在一个底层问题:异步调度(默认开启)下,多批量中的后序请求输出完全退化。PR#45383的临时方案是检测到多模态+prompt_embeds组合时禁用异步调度。此PR旨在根本解决该问题,并恢复异步调度带来的性能收益。

此PR值得精读。它提供了一个清晰的异步调度中GPU数据同步Bug的调试和修复案例,展示了如何通过分析不同调度路径中的GPU copy时机来定位问题。对于理解vLLM的输入批次管理和多模态嵌入路径有重要参考价值。

讨论亮点

Reviewer @qthequartermasterman 对修复方案表示认可:"This fix makes sense to me. I wish there was a way to avoid the copy to GPU on every step, but I don't think it's avoidable in this case." 表明每步进行一次小规模H2D拷贝(布尔数组,仅几百字节)是必要代价,无法避免。没有其他争议。

实现拆解

  1. 根因分析:在_prepare_input_ids中,异步调度的纯decode快速路径只会上传input_ids.gpu,但多模态prompt_embeds路径需要读取is_token_ids.gpu来判断每个位置是否需要重新嵌入。当num_common_tokens == total_without_spec时,该快速路径跳过了is_token_ids的GPU上传,导致is_token_ids.gpu失效,多模态路径读到过时标志,将生成token误判为输入嵌入,从而产生乱码输出。

  2. 核心修复vllm/v1/worker/gpu_model_runner.py):在_prepare_input_ids方法中,num_common_tokens计算之后,enable_prompt_embeds条件下,无条件执行self.is_token_ids.copy_to_gpu(total_num_scheduled_tokens)。此举将原本仅在“非纯decode”分支中执行的GPU上传,提前到所有路径——包括纯decode快速路径。同时将原来在num_common_tokens < total_without_spec块内的is_token_ids.copy_to_gpu移到外部,避免重复。需要注意的是,inputs_embeds.gpu不需要类似处理,因为decode位置是token位置,会被嵌入覆盖,其GPU副本在prefill/mixed步骤已刷新。

  3. 移除临时禁用vllm/config/vllm.py):删除之前为多模态+prompt_embeds组合关闭异步调用的两段代码(显式启用时的raise ValueError,以及自动启用时的logger.warning_once+async_scheduling=False)。现在异步调度默认可以正确工作。

  4. 测试验证:PR提供了重现脚本repro_prompt_embeds_mm_async.py,使用google/gemma-3-12b-it模型,对比prompt_embedsprompt_token_ids两种方式的生成结果。修复前,后续请求输出退化;修复后所有请求完全匹配。

文件 模块 状态 重要度
vllm/config/vllm.py 配置 modified 5.89
vllm/v1/worker/gpu_model_runner.py 模型运行器 modified 6.08

关键符号

_prepare_input_ids

关键源码片段

vllm/config/vllm.py core-logic

移除了之前为多模态 +prompt_embeds 组合禁用异步调度的临时回退,使异步调度能够正确启用。

# vllm/config/vllm.py (partial, __post_init__ 方法 )
from vllm.v1.executor.abstract import Executorexecutor_backend = self.parallel_config.distributed_executor_backend
executor_class = Executor.get_class(self)
executor_supports_async_sched = executor_class.supports_async_scheduling()if self.scheduler_config.async_scheduling:
    # 显式启用异步调度时的检查
    if self.speculative_config is not None:
        # ... 各种不兼容检查 ...
        pass
    # +++ 删除块 ( 之前的多模态 +prompt_embeds 硬错误 ) +++
    # if (
    # self.model_config is not None
    # and self.model_config.enable_prompt_embeds
    # and self.model_config.is_multimodal_model
    # ):
    # raise ValueError("Async scheduling is not yet supported...")
​
    if not executor_supports_async_sched:
        raise ValueError(...)
elif self.scheduler_config.async_scheduling is None:
    # 自动启用逻辑:依次检查不兼容条件
    if ...: # pooling
        self.scheduler_config.async_scheduling = False
    elif ...: # speculative 不兼容
        self.scheduler_config.async_scheduling = False
    elif ...: # disable_padded_drafter_batch
        self.scheduler_config.async_scheduling = False
    elif not executor_supports_async_sched:
        self.scheduler_config.async_scheduling = False
    # +++ 删除 elif 块 ( 之前的多模态 +prompt_embeds 静默关闭 ) +++
    # elif (
    # self.model_config is not None
    # and self.model_config.enable_prompt_embeds
    # and self.model_config.is_multimodal_model
    # ):
    # logger.warning_once(...)
    # self.scheduler_config.async_scheduling = False
    else:
        self.scheduler_config.async_scheduling = True
vllm/v1/worker/gpu_model_runner.py core-logic

核心修复:在 `_prepare_input_ids` 中,于所有快速路径之前无条件刷新 `is_token_ids.gpu`,确保多模态嵌入路径读取到最新标志。

# vllm/v1/worker/gpu_model_runner.py (partial, _prepare_input_ids 方法 )num_common_tokens = len(sample_flattened_indices)
total_without_spec = total_num_scheduled_tokens - total_num_spec_tokens# 核心修复:只要启用了 prompt_embeds,就每步刷新 is_token_ids.gpu
# 保证多模态嵌入路径(唯一读取 is_token_ids.gpu 的路径)不会读到过时标志
if self.enable_prompt_embeds:
    self.is_token_ids.copy_to_gpu(total_num_scheduled_tokens)if num_common_tokens < total_without_spec:
    # 非纯 deocde 快速路径:上传 input_ids 和 inputs_embeds 到 GPU
    self.input_ids.copy_to_gpu(total_num_scheduled_tokens)
    if self.enable_prompt_embeds:
        self.inputs_embeds.copy_to_gpu(total_num_scheduled_tokens)
        # +++ 原来这里的 self.is_token_ids.copy_to_gpu(...) 已移到上方 +++

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  • 回归风险:低。修改仅限于enable_prompt_embeds条件分支,对非prompt_embeds场景无行为改变。is_token_ids.gpu的每步刷新只增加一次小数据量的GPU拷贝(约几百字节),性能影响可忽略。
  • 兼容性:高。已移除临时禁用异步调度的配置回退,确保多模态prompt_embeds用户能够自动获得异步调度加速。
  • 遗漏场景:PR说明中指出,刷新逻辑基于enable_prompt_embeds而非更窄的supports_mm_inputs,因为现有的非异步刷新也是无条件基于enable_prompt_embeds。这意味着纯文本模型如果启用了enable_prompt_embeds,也会执行该拷贝,但PR认为冗余拷贝可忽略且保持对称性。
  • 用户影响:多模态模型用户在使用prompt_embeds功能时,生成结果将正确,并且无需手动启用异步调度(原本在PR#45383中会默认禁用)。性能可与纯token-id路径一致。
  • 系统影响:无;仅在enable_prompt_embeds=True时每decode step增加一次小量H2D拷贝。
  • 团队影响:小型修复,易于理解。
核心路径变更 缺少测试覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论