https://github.com/vllm-project/vllm/blob/main/vllm/model_executor/models/qwen2_audio.py

下面给你一份逐段、逐类、逐流程的详细解释,把 /mnt/data/qwen2_audio.py(qwen2+audio 推理整合)从高层目的到低层实现、数据形状、调用流程、扩展点和工程注意项都讲清楚。目标是让你能读懂代码、知道如何喂数据、如何在 vLLM 中注册/使用该多模态模型、以及哪些地方可能要改或优化。


一、文件总体功能 / 背景

这个文件实现了一个推理用的 Qwen2-Audio 多模态封装,主要作用是将音频输入(原始特征或提前计算好的 audio embeddings)通过一个音频编码器 + 投影层,转换为与文本后端(language model)兼容的 embedding,最后交由已注册的 vLLM 文本模型(Qwen2ForCausalLM 风格)进行后续生成。它包含:

  • 音频输入的 TensorSchema(类型与形状约定)

  • Audio encoder wrapper(Qwen2AudioEncoder 来自 transformers)

  • 投影层(Qwen2AudioMultiModalProjector)将 audio hidden -> text hidden size

  • 多模态处理器 / 数据解析 / prompt 替换逻辑(注册到 MULTIMODAL_REGISTRY)

  • 顶层模型类 Qwen2AudioForConditionalGeneration:组合音频 tower + projector + language model,并提供 get_multimodal_embeddingsforwardcompute_logitsload_weights 等方法,方便在 vLLM pipeline 中作为已注册模型使用

核心目标:把音频变成能被语言模型消费的“pseudo tokens”(embedding 序列),并在 prompt 中用特殊音频占位符(audio token)替换成对应数量的 audio token ids(由 PromptReplacement 完成),以实现端到端“语音上下文 + LLM”推理流。


二、关键数据结构与形状(TensorSchema)

文件定义了两类输入 schema(TensorSchema):

  1. Qwen2AudioFeatureInputs(type="audio_features")

    • input_features: Tensor 或 list[Tensor]

      • 形状注释:("na", "nmb", 3000) -> na=number of audios,nmb=number of mel bins (或 freq bins),最后维度固定3000(表示时间维 length)

    • feature_attention_mask: Tensor

      • 形状注释:("na", 3000),用于表示每个 audio 的有效帧数(1/0)

  2. Qwen2AudioEmbeddingInputs(type="audio_embeds")

    • audio_embeds: list[Tensor]

      • 形状注释:("bn", "naf", "hs") -> bn=batch size, naf=number of audio features (tokens) per audio, hs=hidden size (must match LM hidden size)

Qwen2AudioInputs 是二者之一。调用者可以直接传入 audio_embeds(已经计算好的 embeddings)或 input_features + feature_attention_mask(原始 Mel/feature),由模型负责处理。


三、音频到文本 embedding 的核心模块

1) Qwen2AudioMultiModalProjector

class Qwen2AudioMultiModalProjector(nn.Module):
    def __init__(self, audio_hidden_size: int, text_hidden_size: int):
        self.linear = nn.Linear(audio_hidden_size, text_hidden_size, bias=True)
    def forward(self, audio_features):
        hidden_states = self.linear(audio_features)
        return hidden_states
  • 作用:将音频 encoder 的 hidden size 映射到语言模型的隐藏维度(text_hidden_size),是最简单可靠的投影层(可以替换为更复杂的 adapter)。

  • 输入:audio_features 形状通常 (num_audio_tokens, audio_hidden_size)(batch, seq, audio_hidden_size);输出维度变为 text_hidden_size

2) _get_feat_extract_output_lengths

  • 帮助函数,复制自 Qwen2AudioEncoder:根据原始帧长度计算中间层 / 输出长度(因为 encoder 内部有下采样 / conv 抽样)。输入为 input_lengths(tensor),计算两个下采样阶段后的 feat_lengthsoutput_lengths,用于后续 mask/切分。


四、ProcessingInfo / DummyInputs 构件(用于 vLLM multimodal 框架)

  • Qwen2AudioProcessingInfo:提供获取 HF 配置(Qwen2AudioConfig)、处理器(Qwen2AudioProcessor)、WhisperFeatureExtractor 的方法,并声明支持的 multimodal limits(audio 无上限)。

  • Qwen2AudioDummyInputsBuilder:为测试/占位生成dummy文本与dummy音频数据(用于模型 warmup / tracing / profiling)。其中 get_dummy_text 用 hf_processor 的 audio_token 占位;get_dummy_mm_data 构造基于 feature_extractor 的假音频长度。

用途:vLLM 的 multimodal pipeline 需要这些来生成占位提示和虚拟输入以便在没有真实音频时也能跑一遍(比如做 shape inference、jit trace、alloc tensor 等)。


五、MultiModalDataParser / Processor(把用户输入变成模型实际需要的 tensors)

Qwen2AudioMultiModalDataParser

  • 重写 _parse_audio_data:如果用户直接传入字典(比如 {"audio_embeds": [...]}),则用 DictEmbeddingItems 把这些字段解析为 audio modality,并定义哪些字段是必需的(audio_embeds),以及字段工厂(如何 batch 这些字段)_qwen2audio_field_config

_qwen2audio_field_config 返回:

{
 audio_embeds: MultiModalFieldConfig.batched("audio"),
 input_features: MultiModalFieldConfig.batched("audio"),
 feature_attention_mask: MultiModalFieldConfig.batched("audio"),
}

这告诉 vLLM 框架如何把这些字段打包成 batch。

Qwen2AudioMultiModalProcessor(核心)

  • 继承自 BaseMultiModalProcessor,负责与 transformers 的 Qwen2AudioProcessor / WhisperFeatureExtractor 对接。

  • _call_hf_processor:这个方法负责调用 HF 复合 processor,把 prompt 和音频数据一并送进 HF 的处理器(注意:transformers 的 processor 期望字段名 audio 而不是 audios,所以这里做了兼容性处理)。

  • _get_mm_fields_config:返回 multimodal 字段配置(调用上面的 helper)。

  • _get_prompt_updates关键 —— 负责把 prompt 中的 audio 占位符替换成具体数量的 audio tokens(<|AUDIO|>)与包围的 BOS/EOS(<|audio_bos|><|audio_eos|>),以及 embed token id 的指定。

详细说明 _get_prompt_updates

  1. 获取 processor、tokenizer、vocab(token -> id)。

  2. 兼容旧版 transformers,取 audio_token, audio_bos_token, audio_eos_token(默认 <|AUDIO|>/...)。

  3. 计算每个 audio 的输出 token 数(audio_output_lengths):

    • 如果 feature_attention_mask 可用:通过 _get_feat_extract_output_lengths(feature_attention_mask.sum(-1)) 得到 audio_output_lens

    • 否则,如果用户传入 audio_embeds:直接用 embedding 的第一维长度作为 num_features。

  4. 为每个 audio 构建 num_featuresaudio_token_id 的序列,并在两端加 audio_bos_id / audio_eos_id。返回 PromptReplacement,指示替换哪一个 prompt token(audio_token)为这些 token id 的序列,同时传递 embed_token_id(用于 LLM input embedding 的选择)。

用途:当最终构造 prompt ids 给 language model 时,原先 prompt 里的一处 <|AUDIO|> 占位会被替换为对应数量的 audio token ids,从而让语言模型在其输入序列中看到“audio tokens”的位置(后续用 get_multimodal_embeddings 提供对应的 embeddings)。


六、核心模型类:Qwen2AudioForConditionalGeneration

这是把 audio tower + projector + language model 组合在一起的顶层类,并注册为 multimodal processor(@MULTIMODAL_REGISTRY.register_processor 前面注解那块)。

关键点按方法来解释:

初始化 __init__

  • vllm_config 中拿到 HF 的模型配置 config = vllm_config.model_config.hf_config,quant 与 multimodal 配置也被读取。

  • 构造:

    • self.audio_tower = Qwen2AudioEncoder(config.audio_config):transformers 提供的 audio encoder,负责从 input features(mel specs)计算 audio hidden states。

    • self.multi_modal_projector = Qwen2AudioMultiModalProjector(config.audio_config.d_model, config.text_config.hidden_size):线性映射 audio d_model -> text hidden size。

    • self.language_model = init_vllm_registered_model(...):通过 vLLM 的注册器初始化已注册的 language model(通常是 Qwen2ForCausalLM 风格的模型)。注意 prefix=maybe_prefix(prefix, "language_model") 用于参数命名空间。

  • self.make_empty_intermediate_tensors 直接引用 language_model 的方法(用于 vLLM 的中间缓存结构)。

_parse_and_validate_audio_input

  • 从 kwargs 中提取 input_features, audio_embeds, feature_attention_mask,并返回相应的 Qwen2AudioInputs(FeatureInputs 或 EmbeddingInputs),或 None(如果既没有原始 features 也没有 embeddings)。

这是对入参的一次规范化:调用者可以传两种类型数据。

_process_audio_input

负责把 Qwen2AudioInputs 转成一组 1D embedding 列表(tuple),每个元素对应一个 audio 的 embedding(shape=(num_tokens, hs))。

详细流程:

  1. 如果 type == "audio_embeds",直接把 audio_embeds 列表转为 tuple 返回(跳过 encoder)。

  2. 否则 input_features + feature_attention_mask

    • 计算 audio_feat_lengths, audio_output_lengths:调用 audio tower 的 _get_feat_extract_output_lengths,这会考虑内部 conv 下采样。

    • 构造 padding_mask:基于 max_mel_seq_len 和 audio_feat_lengths,形成 boolean mask。

    • 构造 audio_attention_maskfloat("-inf") 填充(用于 transformer attention),并与 encoder 的权重 dtype / device 对齐。

    • 调用 self.audio_tower(input_features, attention_mask=audio_attention_mask) 得到 audio_outputs.last_hidden_stateselected_audio_feature)。

    • selected_audio_feature 通过 self.multi_modal_projector 映射到语言模型 hidden size -> audio_features

    • 通过 audio_output_lengthsaudio_features 按序列长度拆分:先用 boolean mask选取有效 tokens,reshape 后按每 audio 的长度切分,最终 torch.split 返回一组 tensors,每个 tensor 是单个 audio 的 embedding 序列。

  3. 返回值类型:tuple[tensor1, tensor2, ...],每个 tensor 形状 (num_features_i, embed_dim)。这正是 get_multimodal_embeddings 期望的形式。

注意:该方法的实现要点:

  • 处理 padding 和 attention mask 非常关键;对 attention mask 使用 -inf 来屏蔽 pad 在 attention logit 上的影响。

  • 投影后的 masked_audio_features 被拼成一维再 split(因此 intermediate memory peak 可能较高,注意显存)。

get_multimodal_embeddings

  • 调用 _parse_and_validate_audio_input,然后 _process_audio_input;如果没有 audio 输入返回空列表;否则返回 tuple/list 的 masked audio features。

  • 这是给外部(vLLM multimodal pipeline)调用的接口:vLLM 会在构造 prompt 以及替换 audio placeholder 后调用此函数,为语言模型提供对应位置的 embedding。

forward

  • 这里只是将输入(已经是 input_ids/positions 或 inputs_embeds)传递给 language model 的 model(),得到 hidden states 或 intermediate tensors(如果传入 intermediate_tensors 以支持 kv cache)。

  • 注意:forward 并不处理音频 —— 音频已经在 get_multimodal_embeddings 阶段被转换成 embeddings 并与 prompt 里的 audio token ids 对齐注入到 language model。

compute_logits

  • 代理调用 self.language_model.compute_logits(hidden_states),保持接口一致(vLLM 需要 compute_logits 用于 softmax / sampling)。

load_weights

  • 使用 AutoWeightsLoader(self) 将外部权重加载到当前模块命名空间。这是实现从 HF checkpoint(权重 names)到当前 model 对象参数映射的实用工具(vLLM 的自动加载器会处理 prefix、嵌套模块名等)。


七、Prompt 替换与运行时流程(完整调用链)

把上面片段串联起来,实际运行时(高层)会发生哪些事:

  1. 用户/应用提供 prompt + audio

    • 例如 prompt: "Transcribe: <|AUDIO|>",同时提供 audiosaudio_embeds 数据(mm_data)。

  2. MultiModalProcessor 被触发Qwen2AudioMultiModalProcessor._call_hf_processor):

    • 把 audios 传给 HF 的 feature_extractor(WhisperFeatureExtractor),得到 input_features + feature_attention_mask(或接收直接的 audio_embeds 字段)。

    • 通过 _get_prompt_updates,把 prompt 中的 <|AUDIO|> 替换成 [<|audio_bos|>, <|AUDIO|>, ..., <|AUDIO|>, <|audio_eos|>](token ids),其中 <|AUDIO|> 被重复 num_features 次对应每个时间步。

    • 这样构建出final token id 序列,但对应这些 audio token 的 embedding 尚未填充(只保留 token id 占位与 embed_token_id 信息)。

  3. vLLM 在准备 language model 输入时

    • 将 prompt 的 token ids 转为输入 embeddings(通常用 LM 的 token embedding lookup)。

    • 对于 embed_token_id(audio token),vLLM 的 multimodal machinery 会调用模型的 get_multimodal_embeddings,得到与这些 token 数量对应的一组 audio embeddings(每 audio 一个序列),并用这些 embeddings 替换 prompt 中这些 special token 的 embedding slots。

    • 结果是:入到语言模型的 inputs_embeds 是一个 mix:文本 token 的 embedding 来源于 LM 的 embedding table,audio token 的 embedding 来源于 get_multimodal_embeddings 的输出(投影后的 audio features)。

  4. 语言模型推理

    • 语言模型把混合 embedding 序列做 forward(forward),生成 hidden states,再 compute_logits 输出 logits,进行采样/beam search 等。

  5. 输出

    • 语言模型生成文本(例如转写结果、语义理解结果等)。

总结:替换机制把“原始占位符 token id”映射到一段由 audio encoder 生成的真实向量序列,让语言模型“感知”到音频内容并用于生成。


八、如何在实践中使用(示例调用方式)

假设你用 vLLM multimodal 框架,给出两种常见输入方式:

A. 传入原始 features(HF processor will be used)

  1. 使用 transformers 的 WhisperFeatureExtractor / Qwen2AudioProcessor 将 wav -> mel features,并获得 feature_attention_mask

  2. 把这些放入 multimodal pipeline,例如:

mm_data = {"audios": [raw_audio_array1, raw_audio_array2]}
response = vllm_client.generate(prompt="Transcribe this: <|AUDIO|>", multimodal=mm_data)

内部会走 HF 处理器把 audios -> input_features/feature_attention_mask

B. 传入预计算的 audio_embeds

如果你已经在离线用 encoder 处理过音频并缓存了 embeddings(节省计算):

embeds_list = [tensor_of_shape(num_tokens1, text_hidden), tensor_of_shape(num_tokens2, text_hidden)]
# pass as dict so parser picks up audio_embeds path
mm_data = {"audio": {"audio_embeds": embeds_list}}
response = vllm_client.generate(prompt="Transcribe: <|AUDIO|>", multimodal=mm_data)

注意 audio_embeds 每个 tensor 的第二维必须等于 LM 的 hidden size(text_hidden_size),否则会错位。若 embeds 是 audio_hidden_size,必须先通过 projector 做映射(或直接传原始 features 由模型投影)。


九、实现细节 & 风险点(工程注意)

  1. shape 与 dtype 一致性

    • multi_modal_projector 把 audio d_model -> text hidden size,最后传入 LM 时会直接替换 inputs_embeds。确保 dtype/device一致,避免不必要的复制。

  2. 显存峰值

    • _process_audio_input 中,masked_audio_features = audio_features[audio_features_mask].view(-1, embed_dim) 会把 batch concat,随后 torch.split 再切分。对长音频/大 batch 会产生大内存峰值。可优化为按-sample 遍历处理以节省内存,但代价是额外的 Python 级循环。

  3. streaming & latency

    • 目前实现偏向“utterance级”处理(需要知道 audio 输出长度以替换 prompt token)。若做低延迟流式识别,需要改造:逐 chunk 提交、动态 prompt 更新、保留 kv cache 等。

  4. token 对齐与 timestamps

    • CTC-like timestamp 不是此处职责(Qwen2Audio 是 encoder -> LLM 集成),若需要 word-level alignment,需在 audio_tower 上下游增加 alignment 模块或额外输出。

  5. quantization / inference speed

    • vllm_config.quant_config 被保存于类中,但实际量化细节由 language_model / loader 管理。注意 audio_tower 未做量化(若想在低端设备加速需要自己处理)。

  6. transformers 版本兼容

    • 文件里做了 transformers API 的向后兼容(如 getattr(processor, "audio_token", "<|AUDIO|>")),注意不同 transformers 版本可能改名(4.54 等)。

  7. 错误处理

    • _get_replacement_qwen2_audio 会在 num_features == 0 抛错(音频太短无法表示),在真实产品需捕获并返回合理的提示或 fallback。

  8. 并发

    • get_multimodal_embeddings 支持 batch(返回多 audio 的 tuple),vLLM 会把它合并到 inputs_embeds。但如果你的后端 LM 有特殊 batching 规则,要确保 multimodal 输出和 LM batch 顺序一致。

  9. 权重加载

    • load_weights 使用 AutoWeightsLoader(self):vLLM 提供映射 HF 权重到当前模块的能力。若你要加载 HF checkpoint,确保 weight 名与模块命名一致或提供映射。


十、扩展与改造建议(若你想自定义)

  1. 替换 projector:把线性 projector 替为小型 transformer / attention 层,使 audio→text 映射更强。

  2. 离线缓存 audio embeddings:对于高吞吐场景,提前把 audio encoder 的输出 cache 到磁盘(或 redis),生成时直接使用 audio_embeds

  3. 支持 streaming:将 _process_audio_input 改为 chunked 处理,并允许 PromptReplacement incremental 替换、与 LM KV cache 一起工作。

  4. 热词/LLM 引导:在 _get_prompt_updates 中加入上下文动态提示或外部实体 bias,以提升专有名词识别。

  5. 更细粒度时间戳:加入 forced-alignment 模块或 CTCLoss-based alignment for downstream timestamp extraction。

  6. 量化 audio_tower:尝试 ONNX/torchscript 导出并量化 audio_tower 减少推理成本(需要保证精度)。


十一、如何加载 HF 权重 & 启动(快捷指导)

  • init_vllm_registered_model(...) 会在 vLLM 框架内创建并注册 language_model。确保在 vllm config 中指定正确的 HF text/audio config。

  • weights 加载路径:使用 AutoWeightsLoader(self).load_weights(weights)weights 通常来自 AutoWeightsLoader 的上层调度(从 HF ckpt 或 vllm checkpoint)。

  • 需要安装 transformers 且版本兼容(含 Qwen2AudioEncoderQwen2AudioProcessorWhisperFeatureExtractor)。


十二、总结(要点回顾)

  • 这个模块的职责是把音频(原始 features 或 embeddings)转成文本 LM 可消费的 embeddings 并在 prompt 中替换 audio placeholder,从而实现 audio→LLM 的联合推理。

  • 关键组成:Qwen2AudioEncoder(HF)、Qwen2AudioMultiModalProjector(线性投影)、Qwen2AudioMultiModalProcessor(prompt 替换与 HF processor 对接)、Qwen2AudioForConditionalGeneration(整体组合并暴露 get_multimodal_embeddings / forward / compute_logits)。

  • 使用上最重要的是确保 audio_embeds 的隐藏维与 LM hidden size 对齐,并理解 pipeline:prompt 替换 → embeddings 替换 → LM forward。

  • 工程上要注意内存峰值、streaming 需求与 transformers 版本兼容。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐