Prhub

#48554 [Rust Frontend] Integrate MM audio support

原始 PR 作者 BugenZhao 合并时间 2026-07-15 15:00 文件变更 18 提交数 11 评论 5 代码增减 +1274 / -257

执行摘要

Rust 前端集成音频多模态支持,扩展至 Qwen3-ASR

音频支持已在下游 llm-multimodal 库中实现(参考 PR#1905),此 PR 将其能力引入 vLLM 的 Rust 前端,使用户能够通过 Rust 接口使用音频输入的多模态模型(如 Qwen3-ASR),类似于之前图像/视频的支持(PR#47959)。

值得精读。该 PR 展示了在现有多模态架构中添加新模态的典型模式:从模态数据集到预处理、渲染、模型适配的端到端链路。特别是 ResolvedMultimodalSpec 的 per-modality 重构值得关注,它提高了模块化并减少模态间耦合。

讨论亮点

chatgpt-codex-connector[bot] 指出,当音频请求时长不同时,input_audio_features 被标记为 batched field 可能导致 Python 端处理失败,建议使用 flat_from_sizes。BugenZhao 在 commit b738294 中修复,将 Qwen3-ASR 音频特征改为 flat field。

实现拆解

  1. 新增 audio.rs 模块:实现 prepare_audiospreprocess_audios,通过 llm-multimodalAudioPreProcessor 进行预处理,将结果构建为 PreparedMedia

  2. 重构 MultimodalModelInfo:将原有共享的 ResolvedMultimodalSpec 改为每个模态独立持有(imagevideoaudio 各有一个 ModalitySupport),并添加 audio 字段;ResolvedMultimodalSpec 新增 modality 字段,支持模态特定的 primary_keyfield_layout_for

  3. 提取通用 item 模块:将图像和音频共用的批量 item 构建逻辑提取到 item::build_batched_items,减少重复代码。

  4. 修改 image.rs 和 video.rs:迁移至使用新的 ModalitySupportitem 模块,移除旧的 build_image_itemsbuild_video_item 方法。

  5. 扩展 ChatTemplate 渲染:在 renderer/hf/mod.rs 中增加 audio_token 字段到 MultimodalRenderInfo,并添加 TemplateContentPart::Audio 枚举项,使模板渲染能识别音频内容。

  6. 适配数据模型和路由:在 request.rs 增加 InputAudioAudioUrl 枚举变体,在 server 路由 convert.rs 中处理音频内容部分。

  7. Python 侧模型适配:修改 qwen3_asr.pyqwen2_5_omni_thinker.py,支持接收批处理音频特征(3D tensor flatten)。

文件 模块 状态 重要度
rust/src/chat/src/multimodal/audio.rs 音频模块 added 9.03
rust/src/chat/src/multimodal.rs 多模态引擎 modified 8.84
rust/src/chat/src/renderer/hf/mod.rs 模板渲染器 modified 7.93
rust/src/chat/src/multimodal/expand.rs prompt 扩展 modified 7.48
rust/src/chat/src/multimodal/item.rs 通用 item 构建 added 7.32

关键符号

MultimodalModelInfo::prepare_audios MultimodalModelInfo::preprocess_audios MultimodalModelContext::resolve_audio_processor ResolvedMultimodalSpec::new ResolvedMultimodalSpec::primary_key ResolvedMultimodalSpec::field_layout_for item::build_batched_items expand_prompt_tokens_interleaves_audio_and_image_prepared_media

关键源码片段

rust/src/chat/src/multimodal/audio.rs core-logic

新增文件,实现音频预处理核心逻辑,包括 prepare_audios 和 preprocess_audios 方法

// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project//! Audio-modality preparation through `llm-multimodal`.use std::sync::Arc;
use llm_multimodal::{AudioClip, Modality, PreprocessedEncoderInputs};
use vllm_engine_core_client::protocol::dtype::ModelDtype;
use super::{AudioModalitySupport, MultimodalModelInfo, PreparedMedia, item};
use crate::error::{Error, Result, bail_multimodal, multimodal};/// Forward-kwargs name of the primary audio encoder input.
pub(super) const AUDIO_PRIMARY_KEY: &str = "input_audio_features";impl MultimodalModelInfo {
    /// Preprocess fetched audio clips as one batch and build per-item features.
    pub(super) async fn prepare_audios(
        &self,
        clips: Vec<Arc<AudioClip>>,
        uuids: Vec<Option<String>>,
    ) -> Result<PreparedMedia> {
        let support = self.audio.as_ref().ok_or_else(|| Error::UnsupportedModality {
            modality: Modality::Audio.to_string(),
        })?;
        // 调用 preprocess_audios 在 blocking 线程中进行 CPU 密集预处理
        let preprocessed = self.preprocess_audios(support, &clips).await?;
        // 获取 prompt 替换(占位符展开)
        let replacements = support.spec.prompt_replacements_for(&self.context, &preprocessed)?;
        if replacements.len() != clips.len() {
            bail_multimodal!(
                "number of audio prompt replacements {} does not match number of audio clips {}",
                replacements.len(),
                clips.len()
            );
        }
        let hashes = clips.iter().map(|clip| clip.hash.clone()).collect();
        // 通过通用 item::build_batched_items 构建引擎侧多模态特征
        let items = item::build_batched_items(
            &support.spec,
            preprocessed,
            hashes,
            uuids,
            ModelDtype::Float32,
        )?;
        Ok(PreparedMedia {
            modality: Modality::Audio,
            placeholder: support.placeholder.clone(),
            replacements,
            items,
        })
    }    /// Run CPU-heavy audio preprocessing in a blocking task.
    async fn preprocess_audios(
        &self,
        support: &AudioModalitySupport,
        clips: &[Arc<AudioClip>],
    ) -> Result<PreprocessedEncoderInputs> {
        let processor = Arc::clone(&support.processor);
        let clips = clips.to_vec();
        // 使用 spawn_blocking 避免阻塞异步运行时
        tokio::task::spawn_blocking(move || Ok(processor.preprocess(&clips)?))
            .await
            .map_err(|error| multimodal!("audio preprocessing task failed: {error}"))?
    }
}
rust/src/chat/src/multimodal.rs core-logic

核心重构文件:将 ResolvedMultimodalSpec 改为 per-modality,新增 audio 字段和 resolve_audio_processor 方法

// (省略头部注释和导入)/// Resolved multimodal support for one loaded model.
#[derive(Clone)]
pub struct MultimodalModelInfo {
    context: MultimodalModelContext,
    image: Option<ModalitySupport>,
    video: Option<ModalitySupport>,
    // 新增 audio 字段,单独持有可选音频支持
    audio: Option<AudioModalitySupport>,
    media_connector: Arc<MediaConnector>,
}impl MultimodalModelContext {
    /// Resolve an audio preprocessor for one loaded model.
    fn resolve_audio_processor(
        &self,
        model_spec: &'static dyn ModelProcessorSpec,
        preprocessor_config: &PreProcessorConfig,
    ) -> Option<Arc<dyn AudioPreProcessor>> {
        // 委托给模型 spec 的 audio_processor 方法
        model_spec.audio_processor(&self.config, preprocessor_config).map(Arc::from)
    }
}/// Static model-specific tensor-layout behavior for one modality.
#[derive(Clone)]
struct ResolvedMultimodalSpec {
    raw: &'static dyn ModelProcessorSpec,
    modality: Modality, // 新增:指示该 spec 对应的模态
    field_layouts: EncoderFieldLayouts, // 模态特定的编码器字段布局
    keep_on_cpu_keys: HashSet<String>,
}impl ResolvedMultimodalSpec {
    fn new(raw: &'static dyn ModelProcessorSpec, modality: Modality) -> Self {
        Self {
            raw,
            modality,
            field_layouts: raw.encoder_field_layouts_for(modality),
            keep_on_cpu_keys: raw.keep_on_cpu_keys_for(modality).into_iter().collect(),
        }
    }    fn primary_key(&self) -> &'static str {
        match self.modality {
            Modality::Image => image::IMAGE_PRIMARY_KEY,
            Modality::Video => video::VIDEO_PRIMARY_KEY,
            Modality::Audio => audio::AUDIO_PRIMARY_KEY,
            Modality::ImageEmbeds => unreachable!("image embeds use no encoder preprocessor"),
        }
    }    fn field_layout_for(&self, key: &str) -> Option<&FieldLayout> {
        if key == self.primary_key() {
            Some(&self.field_layouts.encoder_input)
        } else {
            self.field_layouts.metadata.get(key)
        }
    }
}

评论区精华

Qwen3-ASR 音频特征应使用 flat field 而非 batched field 设计

chatgpt-codex-connector[bot] 指出,当音频请求时长不同时,input_audio_features 被标记为 batched field 可能导致 Python 端处理失败,建议使用 flat_from_sizes。

结论:BugenZhao 在 commit b738294 中修复,将 Qwen3-ASR 音频特征改为 flat field。 · 已解决

风险与影响

风险主要包括:

1) Rust 端多模态路径重构(ResolvedMultimodalSpec per-modality 改动)可能影响现有图像/视频处理逻辑;
2) 新增音频处理依赖 llm-multimodal 中的 AudioPreProcessor,其稳定性未在 vLLM 全量测试覆盖;
3) Python 端模型(qwen3_asr.py, thinker.py)改动虽小但涉及 forward 逻辑,可能影响现有用户。建议通过现有的 multimodality 测试和 Qwen3-ASR 端到端测试覆盖。

用户影响:使用 Rust 前端(vllm-rs)且需处理音频输入(如 Qwen3-ASR)的用户将受益;系统影响:新增 CPU 音频预处理路径(通过 tokio::spawn_blocking),可能增加 CPU 负载;团队影响:Rust 前端多模态引擎能力增强,为后续更多模态(如 IMU)提供基础。

核心路径重构 新增 llm-multimodal 依赖 跨模块变更(Rust + Python)

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论