Prhub

#38053 [BugFix] Fix TypeError in MiniCPM-O audio feature unpadding

原始 PR 作者 Krishnachaitanyakc 合并时间 2026-06-02 10:57 文件变更 1 提交数 6 评论 11 代码增减 +71 / -4

执行摘要

修复 MiniCPM-O 音频特征长度类型错误和多块对齐

Issue #37981 报告 v0.18.0 运行 MiniCPM-O-4.5 失败,报错 'TypeError: only integer tensors of a single element can be converted to an index'。原因是音频特征长度 (audio_feature_lens) 为张量,用于切片边界时导致类型错误。另外对于长音频(>30s),audio_features 按块组织而 audio_feature_lens 按音频组织,批量大小不匹配导致后续断言错误。

该PR值得阅读,特别是多模态数据处理中字段配置的动态调整技巧。设计决策包括使用 flatten().tolist() 处理张量通用展平,以及用 flat 字段配置替换 batched 来解决多块对齐问题。对于其他可能遇到类似对齐问题的模型有参考价值。

讨论亮点

核心讨论包括:

  • gemini-code-assist 建议使用 flatten().tolist() 增强健壮性,处理后采纳。
  • tc-mb 指出第二层 bug 在 from_hf_inputs 中,即多块音频批量大小不匹配,需在字段配置层处理。
  • wjinxu 发现 flat_from_sizes 与未填充的 list[Tensor] 不兼容,改用 flat() 和普通切片,提交补充修复。
  • DarkLight1337 确认并合入。没有未解决的问题。

实现拆解

实现包括以下步骤:

  1. 在 process_audios 方法中,将 audio_feature_lens(每个音频一个张量)通过 flatten().tolist() 展平为每个块对应的整数长度列表,确保与 audio_features 的第一维(块数)对齐。
  2. 使用展平后的整数列表作为切片边界对 audio_features 进行去填充操作,避免直接使用张量索引。
  3. 在 _minicpmo_field_config 字段配置函数中,检测多块音频场景(num_features > num_audios),计算每个音频的块数,并通过 MultiModalFieldConfig.flat() 替代 batched() 来正确分组 audio_features,使得与 audio_feature_lens 批量大小匹配。
  4. 处理了填充情况下的块数计数回退(使用非零元素计数),确保与实际特征数一致。
    注意:本PR没有新增单元测试,但通过手动编译验证和服务端测试长、短音频进行了验证。
文件 模块 状态 重要度
vllm/model_executor/models/minicpmo.py 模型实现 modified 7.08

关键符号

process_audios _minicpmo_field_config

关键源码片段

vllm/model_executor/models/minicpmo.py core-logic

所有变更集中在该文件,修复了 MiniCPM-O 模型音频处理的核心逻辑和数据契约。

# 核心函数 1:动态配置音频特征字段,处理多块对齐
# (位于文件 vllm/model_executor/models/minicpmo.py)def _minicpmo_field_config(hf_inputs: Mapping[str, torch.Tensor]):
    audio_features = hf_inputs.get("audio_features")
    audio_feature_lens = hf_inputs.get("audio_feature_lens")
    # 先默认使用 batched 模式,适用于单块音频
    audio_features_cfg = MultiModalFieldConfig.batched("audio")
​
    if audio_features is not None and audio_feature_lens is not None:
        num_features = len(audio_features) if isinstance(audio_features, (list, tuple)) else audio_features.shape[0]
        num_audios = len(audio_feature_lens) if isinstance(audio_feature_lens, (list, tuple)) else audio_feature_lens.shape[0]
​
        if num_features > num_audios:
            # 多块音频:计算每个音频对应的块数
            chunks_per_audio: list[int] = []
            for lens in audio_feature_lens:
                if isinstance(lens, torch.Tensor):
                    chunks_per_audio.append(lens.numel())
                else:
                    chunks_per_audio.append(1)
            # 如果计数不匹配(可能是 pad 导致),改用非零元素计数
            if sum(chunks_per_audio) != num_features:
                chunks_per_audio = [
                    int((lens != 0).sum()) if isinstance(lens, torch.Tensor) else 1
                    for lens in audio_feature_lens
                ]
            # 生成 flat slice,按音频分组
            slice_idxs = [0]
            for n in chunks_per_audio:
                slice_idxs.append(slice_idxs[-1] + n)
            audio_features_cfg = MultiModalFieldConfig.flat(
                "audio",
                [slice(slice_idxs[i], slice_idxs[i+1]) for i in range(len(chunks_per_audio))]
            )
​
    return dict(
        **_minicpmv_field_config(hf_inputs),
        audio_features=audio_features_cfg,
        audio_feature_lens=MultiModalFieldConfig.batched("audio"),
        audio_embeds=MultiModalFieldConfig.batched("audio"),
    )
​
​
# 核心函数 2:处理音频特征,展平特征长度避免 TypeError
# process_audios 方法中的关键修改部分def process_audios(self, mm_data, mm_kwargs, tok_kwargs):
    # ... 前处理代码不变 ...
    if isinstance(parsed_audios, MiniCPMOAudioEmbeddingItems):
        audio_inputs = {}
    else:
        audio_inputs = self._base_call_hf_processor(
            prompts=[self.info.audio_pattern] * len(parsed_audios),
            mm_data={"audios": [[audio] for audio in parsed_audios]},
            mm_kwargs={**mm_kwargs, "chunk_input": True},
            tok_kwargs=tok_kwargs,
            out_keys={"audio_features", "audio_feature_lens"},
        )
​
        # 新增:展平 audio_feature_lens 为整数列表,每个块对应一个长度
        flat_feature_lens: list[int] = []
        for lens in audio_inputs["audio_feature_lens"]:
            if isinstance(lens, torch.Tensor):
                # flatten().tolist() 统一处理任意维度张量(0-D, 1-D, 更高维)
                flat_feature_lens.extend(lens.flatten().tolist())
            else:
                flat_feature_lens.append(int(lens))
​
        # 使用展平后的整数列表进行切片,避免张量索引错误
        unpadded_audio_features = [
            feat[:, :feature_len]
            for feat, feature_len in zip(audio_inputs["audio_features"], flat_feature_lens)
        ]
        audio_inputs["audio_features"] = unpadded_audio_features
​
    return audio_inputs

评论区精华

使用 flatten().tolist() 增强张量展平健壮性 正确性

gemini-code-assist 建议用 flatten().tolist() 替代单独的 0-D/1-D 处理,避免更高维张量导致 TypeError。

结论:已采纳,提交 bedb568 中应用。 · 已解决

多块音频在 from_hf_inputs 中的第二层 bug 正确性

tc-mb 指出 process_audios 返回后,audio_features 和 audio_feature_lens 的批量大小不匹配,导致断言错误。

结论:已通过修改 _minicpmo_field_config 使用 flat 字段配置修复。 · 已解决

使用 flat() 与普通切片替代 flat_from_sizes 正确性

wjinxu 发现 flat_from_sizes 与未填充的 list[Tensor] 不兼容,改用 flat() 和普通切片。

结论:已合入,提交 7dd617eb。 · 已解决

风险与影响

风险较低。变更仅涉及单个文件 minicpmo.py,但位于多模态数据处理核心路径。可能的风险包括:

  • 如果 HF 处理器输出格式变化,展平逻辑可能需要调整。
  • 没有单元测试覆盖,依赖手动验证。
  • 极端情况下(如所有音频长度为零)的边界行为未充分测试。

影响范围局限在 MiniCPM-O-4.5 模型。用户:该模型现在可以正常处理音频输入,包括长音频(>30s)。系统:无性能影响。团队:MiniCPM-V 团队参与验证,未来维护者需注意相关配置逻辑。

核心路径变更 缺少测试覆盖

关联 Issue

#37981 [Bug]: v0.18.0 fails to run MiniCPM-o-4.5

完整报告

参与讨论