Prhub

#47729 [Model] Support MOSS-Transcribe-Diarize

原始 PR 作者 gcanlin 合并时间 2026-07-08 19:05 文件变更 7 提交数 7 评论 23 代码增减 +925 / -0

执行摘要

支持 MOSS-Transcribe-Diarize 语音转写模型

MOSS-Transcribe-Diarize 是一个端到端语音转文本模型,支持长格式转写并生成带时间戳的说话人标签。PR body 指出:"MOSS-Transcribe-Diarize is an end-to-end speech-to-text model for long-form transcription with timestamped speaker labels." 该模型在 vLLM 中注册后,用户可使用 OpenAI 兼容的 ASR 端点。

值得精读。该 PR 展示了 vLLM 多模态模型中音频路线(转录)的完整集成模式,包括 TensorSchema 定义、MultiModalProcessor 实现、WeightsMapper 使用。review 中关于多音频并发正确性的发现具有普遍参考价值,建议关注类似模型中的 batch 处理模式。

讨论亮点
  1. 多音频并发正确性(linyueqian,高优先级):_process_audio_input 始终返回单元素列表,但 v1 engine 可能将多个音频请求分在一批,触发断言失败。作者添加循环处理并修复。
  2. 硬编码合并因子(linyueqian,中优先级):编码器直接乘 4,而配置中有 audio_merge_size,若不一致则形状错误。作者改为从配置读取。
  3. 重复/冗余代码(Isotr0py):提示构建和 Qwen3Config 创建重复。作者简化并移除。
  4. 权重映射(Isotr0py):建议使用 WeightsMapper 兼容旧格式。作者采用。
  5. Prompt 注入风险(bot,低优先级):用户输入未转义拼接至 ChatML,可能注入控制标记;未修复。

实现拆解

  1. 配置类:在 vllm/transformers_utils/configs/moss_transcribe_diarize.py 中新增 MossTranscribeDiarizeConfig,继承 PretrainedConfig,整合 Qwen3ConfigWhisperConfig,默认参数匹配 Whisper-medium + Qwen3-0.6B,并暴露 audio_merge_sizeadaptor_input_dim 等超参。
  2. 模型实现:在 vllm/model_executor/models/moss_transcribe_diarize.py 中定义 MossTranscribeDiarizeForConditionalGeneration,组合 WhisperEncoder、时域自适应器(time-merge)和 Qwen3 语言模型。实现 SupportsTranscription 接口,定义输入输出 TensorSchema(MossTranscribeDiarizeAudioInputs/MossTranscribeDiarizeEmbeddingInputs),并构建基于 ChatML 的提示模板。
  3. 模型注册:在 vllm/model_executor/models/registry.py 中添加 MossTranscribeDiarizeForConditionalGeneration 的映射,使模型名称可被 vLLM 识别。
  4. 配置映射:依次修改 vllm/transformers_utils/configs/__init__.pyvllm/transformers_utils/config.py,将 MossTranscribeDiarizeConfig 加入配置查找表,支持从 HuggingFace ID 自动加载。
  5. 测试与文档:在 tests/models/registry.py 中添加模型条目(标记为不可在线获取),并在 docs/models/supported_models.md 中列出新模型。
  6. 审查修复:根据 review 反馈删除了重复代码、使用 WeightsMapper 加载权重、简化提示构造、将硬编码合并因子改为从配置读取、修正 _process_audio_input 使其支持多音频批处理。
文件 模块 状态 重要度
vllm/model_executor/models/moss_transcribe_diarize.py 核心模型 added 9.36
vllm/transformers_utils/configs/moss_transcribe_diarize.py 模型配置 added 8.34
vllm/model_executor/models/registry.py 模型注册 modified 5.68
vllm/transformers_utils/configs/__init__.py 配置映射 modified 5.07
vllm/transformers_utils/config.py 配置工具 modified 4.75
tests/models/registry.py 测试覆盖 modified 4.27
docs/models/supported_models.md 文档 modified 1.93

关键符号

MossTranscribeDiarizeConfig MossTranscribeDiarizeForConditionalGeneration MossTranscribeDiarizeAudioInputs MossTranscribeDiarizeEmbeddingInputs _compute_total_audio_tokens _get_max_audio_samples _as_audio_embedding_list _get_required_token_id _get_audios_from_mm_data _add_vllm_audio_metadata load_weights forward _process_audio_input

关键源码片段

vllm/transformers_utils/configs/moss_transcribe_diarize.py core-logic

新增配置类,继承 PretrainedConfig,管理 Qwen3 和 Whisper 的子配置,暴露关键超参。

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from typing import Any
from transformers import PretrainedConfig, Qwen3Config
from transformers.models.whisper.configuration_whisper import WhisperConfigclass MossTranscribeDiarizeConfig(PretrainedConfig):
    """Configuration for MOSS-Transcribe-Diarize."""
    model_type = "moss_transcribe_diarize"
    # 子配置声明,使 transformers 能自动加载
    sub_configs = {"text_config": Qwen3Config, "audio_config": WhisperConfig}
    keys_to_ignore_at_inference = ["past_key_values"]
​
    def __init__(
        self,
        text_config: dict[str, Any] | Qwen3Config | None = None,
        audio_config: dict[str, Any] | WhisperConfig | None = None,
        audio_token_id: int = 151671, # <|audio_start|> token id
        audio_merge_size: int = 4, # 时域合并倍数
        adaptor_input_dim: int | None = None, # 自适应器输入维度,默认由 audio_config.d_model * merge_size 计算
        tie_word_embeddings: bool = True,
        **kwargs: Any,
    ) -> None:
        # text_config:默认使用 Qwen3-0.6B 配置
        text_config_obj: Qwen3Config
        if text_config is None:
            text_config_obj = Qwen3Config(
                vocab_size=151936, hidden_size=1024, intermediate_size=3072,
                num_hidden_layers=28, num_attention_heads=16, num_key_value_heads=8,
                head_dim=128, max_position_embeddings=40960,
                tie_word_embeddings=tie_word_embeddings, rope_theta=1_000_000.0,
                layer_types=["full_attention"] * 28,
            )
        elif isinstance(text_config, dict):
            text_config_obj = Qwen3Config(**text_config)
        else:
            text_config_obj = text_config
​
        # audio_config:默认使用 Whisper-medium 配置
        audio_config_obj: WhisperConfig
        if audio_config is None:
            audio_config_obj = WhisperConfig(
                num_mel_bins=80, d_model=1024, encoder_layers=24,
                encoder_attention_heads=16, encoder_ffn_dim=4096,
                max_source_positions=1500, dropout=0.0, attention_dropout=0.0,
                activation_dropout=0.0, activation_function="gelu",
                encoder_layerdrop=0.0, scale_embedding=False,
            )
        elif isinstance(audio_config, dict):
            audio_config_obj = WhisperConfig(**audio_config)
        else:
            audio_config_obj = audio_config
​
        text_config_obj.tie_word_embeddings = tie_word_embeddings
        if not getattr(text_config_obj, "layer_types", None):
            text_config_obj.layer_types = ["full_attention"] * text_config_obj.num_hidden_layers
​
        super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs)
​
        self.text_config = text_config_obj
        self.audio_config = audio_config_obj
        self.audio_token_id = int(audio_token_id)
        self.audio_merge_size = int(audio_merge_size)
        self.adaptor_input_dim = (
            int(adaptor_input_dim) if adaptor_input_dim is not None
            else int(audio_config_obj.d_model) * int(audio_merge_size)
        )
        # 暴露常用属性,方便模型代码直接引用
        self.vocab_size = int(text_config_obj.vocab_size)
        self.hidden_size = int(text_config_obj.hidden_size)
        self.num_hidden_layers = int(text_config_obj.num_hidden_layers)
        self.num_attention_heads = int(text_config_obj.num_attention_heads)
        self.num_key_value_heads = int(text_config_obj.num_key_value_heads)
        self.head_dim = int(text_config_obj.head_dim)
        self.hidden_act = text_config_obj.hidden_act
        self.max_position_embeddings = int(text_config_obj.max_position_embeddings)
        self.rms_norm_eps = float(text_config_obj.rms_norm_eps)
        self.is_causal = True

评论区精华

多音频并发正确性 正确性

linyueqian 指出 _process_audio_input 始终返回单元素列表,但在 v1 engine 中多个音频请求可能被分组,导致断言失败。

结论:作者修复为返回与输入数量匹配的嵌入列表。 · 已解决

编码器合并因子硬编码 设计

linyueqian 指出编码器直接乘 4,而配置中有 audio_merge_size 参数,若值不同将产生形状不匹配。

结论:作者改为从配置读取 merge factor。 · 已解决

重复代码与冗余配置 style

Isotr0py 指出两处重复(_build_audio_prompt 和重复的 Qwen3Config 创建)。

结论:作者移除重复,简化提示构造。 · 已解决

权重映射使用 WeightsMapper 设计

Isotr0py 建议使用 AutoWeightsLoader 的 mapper 参数兼容旧格式权重。

结论:作者采纳建议,增加 WeightsMapper。 · 已解决

Prompt 注入安全风险 安全

depthfirst-app[bot] 指出用户输入未转义直接拼接到 ChatML 模板中,可被注入控制标记。

结论:未回复,风险未修复。建议后期参考 qwen3_asr.py 的 sanitize_chatml_input 函数处理。 · unresolved

风险与影响

  1. 多音频并发正确性(高):原实现假设单音频请求,在并发场景会导致崩溃。虽已修复,但类似批量处理模式在其他模型可能潜伏,需审查相似代码。
  2. 硬编码值(中):合并因子硬编码曾导致配置不一致,已修复。但编码器内部还有其他硬编码值(如 WHISPER_ENCODER_STRIDE)未配置化。
  3. Prompt 注入(低):用户输入直接嵌入模板,未转义 ChatML 标记,存在理论安全风险。参考 qwen3_asr.py 的 sanitize_chatml_input 可缓解。
  4. 新路径覆盖:语音转写流程与现有视觉多模态共享架构,但测试只覆盖注册,缺乏端到端推理测试,增加回归风险。

用户:新增 MOSS-Transcribe-Diarize 模型支持,可通过 OpenAI 兼容 API 调用 ASR 带说话人分离功能。
系统:无性能或资源影响,仅新模型文件,不影响现有模型。
团队:维护负担轻微,新增约 900 行代码,配置映射已标准化。

多音频并发正确性 硬编码合并因子 Prompt 注入

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论