Prhub

#56912 [XPU][Bugfix] Fix Qwen2-Audio ValueError on audio clips longer than 30s

原始 PR 作者 jbyczkow 合并时间 2026-09-16 16:54 文件变更 1 提交数 4 评论 5 代码增减 +1 / -0

执行摘要

Qwen2-Audio 长音频崩溃修复,强制音频截断

PR body 把根因写得很清楚:InputProcessingContext.call_hf_processor() 为了不截断 vLLM 自己构造的占位文本,设置了 merged_kwargs.setdefault("truncation", False);而 Qwen2AudioProcessor 会把同一个 truncation kwarg 原样转发给 Whisper 风格 feature extractor——后者自身默认是 truncation=True,负责把每条音频裁到模型固定的 30s 窗口(3000 mel 帧)。truncation=False 泄漏进来后,>30s 音频的 input_features 保留原生帧数,随后在 Qwen2AudioFeatureInputs 的定长校验中抛出 ValueError: input_features dim[2] expected 3000, got 3495,直接导致 EngineDeadError:引擎核心崩溃、后续所有请求 500、必须重启服务。PR 明确指出这是 PR#53275(commit 0b19ebcacd0e9578460206035c4b3575361c19c4,"[MM] Simplify _apply_hf_processor_main")引入的回归,且与 --enforce-eager 无关——bug 发生在模型执行之前的多模态预处理阶段,eager 与 torch.compile 两条路径都会复现。

值得花 5 分钟精读,但要连 PR body 一起读——合入后的代码只有一行 truncation=True,,真正有价值的是它背后的根因链路:call_hf_processor() 的 setdefault("truncation", False) 如何经 Qwen2AudioProcessor 泄漏进 Whisper 特征提取器,最终以 input_features dim[2] expected 3000 的方式拖垮 EngineCore。

关注三个设计决策:一是把修复放在最贴近模型语义的 _preprocess_hf_mm_data(),用"显式键优先于 setdefault"做局部覆盖,而不是修改共享的全局默认值,避免波及文本占位序列对齐;二是"静默截断"而非放宽校验,保持了 Qwen2-Audio 固定 30s 窗口的模型契约;三是接受 reviewer 建议删掉解释性注释,使根因知识只存在于 PR 描述中,后续维护者容易丢失上下文。

给后续动作两条建议:(1)把这个 case 固化成回归测试,例如在 tests/models/multimodal/ 下用一段 >30s 的合成音频断言 input_features 形状为 3000 帧;(2)顺着同一透传路径排查其它音频类 MM 处理器(Whisper 系、Omni 系)是否同样依赖后端 truncation=True 默认值,把一次性修复升级为一次性排查。

讨论亮点
  • 注释去留(唯一实质性 review 交锋):jikunshang 在 vllm/model_executor/models/qwen2_audio.py 上提交 suggestion,正文是 truncation=True, 加一句 "remove comments added by AI",即要求删掉初版那段解释 truncation=False 泄漏链路的 5 行注释。作者接受了该建议(commit 54a40ea "Apply suggestion from @jikunshang"),最终合入的 diff 只有一行 truncation=True,——根因分析因此只留在 PR body 里,代码里不留解释。
  • CI 与分支新鲜度:jikunshang 首次 /ci run 被 github-actions bot 拒绝——"This PR is 10 commits behind upstream main... No new CI build was started",随后作者/maintainer 两次 Merge branch 'main' 同步上游,第二次 /ci run 才触发 Buildkite CI #89238。这是 vLLM 仓库对 fork PR 的常规流程摩擦,非技术分歧。
  • 自动化评审缺席:claude[bot] 标注 "This pull request is from a fork — automated review is disabled",因此本 PR 没有走自动代码审查,只有维护者的人工 APPROVED。

实现拆解

  1. 变更入口:唯一改动落在 vllm/model_executor/models/qwen2_audio.py 的 Qwen2AudioMultiModalProcessor._preprocess_hf_mm_data()。这个方法是 Qwen2-Audio 多模态预处理的适配钩子:它把 vLLM 侧的 audios 载荷改写成 HF processor 期望的单数键 audio,并把 sampling_rate=feature_extractor.sampling_rate 合并进返回值 hf_processor_mm_kwargs。

  2. 根因链路:vllm/multimodal/processing.py 的 InputProcessingContext.call_hf_processor() 用 merged_kwargs.setdefault("truncation", False) 保护占位文本(<audio_bos><audio_token><audio_eos>)不被截断;该 kwarg 顺着 Qwen2AudioProcessor 一路传到音频 feature extractor,覆盖了它原本的 truncation=True 默认值。结果是 >30s 音频不再被裁/补到 3000 mel 帧,触发 Qwen2AudioFeatureInputs 的固定形状校验失败,异常向上冒泡成引擎级故障。

  3. 修复方式:在最靠近模型语义的这一层显式给出 truncation=True。因为下游是 dict.setdefault,已存在的显式键不会被覆盖,于是"音频侧必须截断"这个模型约束重新生效;同时不必去动 call_hf_processor() 的全局默认值,避免影响文本占位序列与多模态 token 位置的对齐。作者在 PR body 中补充论证:占位文本本身极小,无论 truncation 取值都不会被真正截断,所以这处覆盖不会伤到它原本保护的目标。

  4. 评审收敛:初版实现带有 5 行解释性注释(AI 生成)。Reviewer jikunshang 在该文件上给出 suggestion,要求只保留 truncation=True, 一行,作者以 commit 54a40ea("Apply suggestion from @jikunshang")接受;随后 maintainer 合并两次上游 main(905b784、8a5223a)并重新触发 CI,最终 APPROVED。

  5. 测试与配套:无新增测试、无文档、无配置或 schema 变更。验证依赖 PR body 中给出的可复现脚本——用 soundfile 合成 35s/16kHz 正弦音频后走 /v1/chat/completions,修复前复现 ValueError: input_features dim[2] expected 3000, got 3500 与 EngineDeadError,修复后返回 200。

关注点 文件/符号 说明
唯一改动 vllm/model_executor/models/qwen2_audio.py → _preprocess_hf_mm_data() 新增 truncation=True,+1/-0
交互方(未改动) vllm/multimodal/processing.py → call_hf_processor() setdefault("truncation", False) 的来源
形状契约 Qwen2AudioFeatureInputs 定长校验 / _get_prompt_updates 依赖 3000 帧与 feature_attention_mask 推算的 audio token 数
平台验证 Intel BMG B70(XPU)、NVIDIA A100-SXM4-80GB(CUDA) 说明 bug 与平台无关,XPU 只是复现环境
文件 模块 状态 重要度
vllm/model_executor/models/qwen2_audio.py 音频预处理 modified 5.36

关键符号

Qwen2AudioMultiModalProcessor._preprocess_hf_mm_data InputProcessingContext.call_hf_processor

关键源码片段

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

唯一改动文件。`Qwen2AudioMultiModalProcessor._preprocess_hf_mm_data()` 是 Qwen2-Audio 多模态预处理的适配钩子,负责把 vLLM 的 `audios` 载荷归一化为 HF 期望的 `audio` 键并补齐 `sampling_rate`;本次在其中新增 `truncation=True`,压过 `call_hf_processor()` 注入的 `truncation=False`,让 Whisper 风格特征提取器恢复把音频裁 / 补到 3000 mel 帧的行为,从而避免 `Qwen2AudioFeatureInputs` 定长校验抛 `ValueError` 并拖垮 EngineCore。

# vllm/model_executor/models/qwen2_audio.py
class Qwen2AudioMultiModalProcessor(BaseMultiModalProcessor[Qwen2AudioProcessingInfo]):
    def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
        # 占位文本由 vLLM 自行构造且极短(<audio_bos><audio_token><audio_eos>),
        # 无论 truncation 取何值都不会被真正截断
        return self.dummy_inputs.get_dummy_text(mm_counts)
​
    def _preprocess_hf_mm_data(
        self,
        mm_data: Mapping[str, object],
        hf_processor_mm_kwargs: Mapping[str, object],
    ) -> tuple[Mapping[str, object], Mapping[str, object]]:
        feature_extractor = self.info.get_feature_extractor(**hf_processor_mm_kwargs)
​
        # HF processor 期望的是单数键 "audio",这里把 vLLM 侧的 "audios" 载荷做一次归一化
        mm_data = dict(mm_data)
        audios = mm_data.pop("audios", [])
        if audios:
            mm_data["audio"] = audios
​
        hf_processor_mm_kwargs = dict(
            **hf_processor_mm_kwargs,
            sampling_rate=feature_extractor.sampling_rate,
            # 本次修复所在的一行:InputProcessingContext.call_hf_processor() 为了保护
            # 占位文本不被截断,会 setdefault("truncation", False);而 Qwen2AudioProcessor
            # 会把同一个 kwarg 原样透传给 Whisper 风格 feature extractor——后者自身默认
            # truncation=True,负责把每条音频裁 / 补到模型固定的 30s、3000 个 mel 帧。
            # 由于下游合并 kwargs 走的是 dict.setdefault,这里的显式键不会被覆盖,
            # 于是 >30s 音频不再保留原生帧数(如 3495),也就不会在
            # Qwen2AudioFeatureInputs 的定长校验处抛 ValueError(dim[2] expected 3000)
            # 进而触发 EngineDeadError。
            truncation=True,
        )
​
        return mm_data, hf_processor_mm_kwargs

评论区精华

删掉 AI 生成的解释性注释,只保留 `truncation=True,` style

Reviewer jikunshang 在 `vllm/model_executor/models/qwen2_audio.py` 上以 suggestion 形式给出 `truncation=True,`,并附言 "remove comments added by AI",要求移除初版中 5 行解释 `truncation=False` 泄漏链路的注释。

结论:作者接受建议,commit `54a40ea`("Apply suggestion from @jikunshang")落地,最终 diff 仅保留一行 `truncation=True,`;维护者随后 APPROVED。副作用是根因知识只留在 PR body 中,代码内不留上下文。 · 已解决

CI 触发前必须与 upstream main 同步 other

首次 `/ci run` 被 github-actions bot 拒绝:"This PR is 10 commits behind upstream `main`... No new CI build was started",要求先 merge/rebase 到最新 main。

结论:先后两次 `Merge branch 'main'`(`905b784`、`8a5223a`)同步上游,第二次 `/ci run` 成功触发 Buildkite CI #89238;属流程性摩擦,非技术分歧。 · 已解决

风险与影响

  1. 缺少回归测试(最实际的风险):全仓库没有任何测试文件变更,验证仅靠 PR body 中的手工脚本。这意味着 "call_hf_processor() 的 truncation 默认值 × 音频 feature extractor 的 truncation 默认值" 这层隐式耦合仍然没有护栏;如果 vllm/multimodal/processing.py 或 kwarg 透传链路再被重构(正如 PR#53275 当初那样),同样的形状崩溃会静默回归,且只在收到 >30s 音频时才暴露。
  2. 同类模型可能存在同源问题:修复只覆盖 Qwen2AudioMultiModalProcessor。凡是把 hf_processor_mm_kwargs(含 truncation)透传给音频 feature extractor、并依赖后端 truncation=True 默认值的多模态处理器,理论上都会遭受同样的默认值外溢。材料未给出这些处理器的排查结果,属于待确认风险,建议做一次全局 kick-the-tires 排查。
  3. 跨模块默认值外溢的结构性脆弱:本次是"文本侧为保护占位序列设定的全局默认值,污染了音频侧的特征提取语义"。这类问题不会在改动 PR#53275 时暴露,而是延迟到运行时、以引擎崩溃的形式出现,说明 MM kwargs 的合并顺序(显式键 > setdefault)已经成为一个隐式契约。
  4. 行为语义需向用户交代:恢复 truncation=True 意味着 >30s 音频的超出部分会被静默丢弃而非报错。这是 Qwen2-Audio 既定模型窗口(Whisper 编码器固定 30s 输入)的固有行为、也是 #53275 之前的原有语义,不构成本 PR 新引入的回归,但用户可能误以为模型"听完了"整段音频。
  5. 文本侧覆盖的残余不确定性:显式 truncation=True 会进入同一次 processor 调用的合并 kwargs,理论上也可能作用于文本分词。作者以"占位文本只有 <audio_bos><audio_token><audio_eos>,两种取值下都不会被截断"作为论证,逻辑成立,但这一层假设没有测试固定下来。
  6. 平台影响面小:bug 在 CUDA A100 上同样复现,属于平台无关的多模态预处理缺陷;XPU 标签只反映复现与验证环境,不构成 XPU 独有风险。
  • 用户侧:此前只要请求里带一段 >30s 的音频,Qwen2-Audio 服务就会在首次请求时整机崩溃(EngineDeadError,后续全部 500,必须重启);修复后同一请求返回 200 与转写结果,长音频场景从"不可用"变为"可用(超出 30s 部分按模型窗口截断)"。
  • 系统侧:变更位于模型执行之前的多模态预处理路径,eager 与 torch.compile 两条路径同时受益,不涉及算子、调度器与 KV 缓存等核心链路,回归面被严格限制在 Qwen2-Audio 的音频特征提取参数上。
  • 兼容性:属于恢复 PR#53275 之前的既有语义,对依赖固定 3000 帧形状的 Qwen2AudioFeatureInputs 校验与 _get_prompt_updates 中基于 feature_attention_mask 的 audio token 数推算是正向对齐,无对外接口或数据格式变更。
  • 团队侧:+1/-0 的极小改动却修复了服务级故障,维护成本几乎为零;同时也再次提示 MM 处理器适配层存在"全局默认值被模型侧透传放大"的系统性隐患,值得在团队内沉淀为排查清单。
缺少测试覆盖 同类音频模型可能同源 跨模块 kwargs 默认值外溢 长音频静默截断语义

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论