vllm / vllm-omni 推理关键点
2026-03-05
深度学习
00

目录

1. 视频是否抽音频(useaudioin_video)
2. 视频帧:抽多少帧、是否按“每秒”抽
3. 音视频 token 是否交叠(useaudioin_video)
4. 单帧图像的 resize
ms-swift bug
修改说明
1. 在 replacetag 开头为 omniv3 准备 kwargs(约 634–637 行)
2. 把 omnikwargs 传给 fetchimage 和 fetch_video
修改后的代码片段

根据 vllm / vllm-omni 源码,视频请求在 Omni 里的处理方式可以总结如下。


1. 视频是否抽音频(use_audio_in_video)

会。 只有在请求里带上 mm_processor_kwargs: { "use_audio_in_video": true } 时才会从视频里抽音频。

  • 抽音频在 vllm_omni/entrypoints/chat_utils.pyextract_audio_from_video_async 里完成。
  • vllm_omni/entrypoints/openai/serving_chat.py_preprocess_chat 里,若 use_audio_in_video=Truemulti_modal_datavideo 没有 audio,会遍历 messages 里所有 video_url,对每个 URL 调用 extract_audio_from_video_async,然后把结果写回 engine_prompt["multi_modal_data"]["audio"]
  • 抽音频用 librosalibrosa.load(file_path, sr=16000),即固定 16kHz 采样率。
56:58:vllm-omni-0.16.0/vllm_omni/entrypoints/chat_utils.py
展开代码
audio_array, sample_rate = await asyncio.to_thread(_load_audio_sync, temp_video_file_path) return audio_array, sample_rate

2. 视频帧:抽多少帧、是否按“每秒”抽

num_frames 表示一共抽多少帧,是均匀抽帧。

fps 表示每秒抽多少帧。

最终取的是其中的最小值。

bash
展开代码
vllm serve "/mnt/cpfs/cache/xiedong/20260203-q3-omni-s2v6/20260203-q3-omni-s2v6-30k-30epochs" \ --omni --port 8091 \ --stage-configs-path "/mnt/cpfs/xiedong/video-understanding-omni-vllm/stage_config_thinker_4gpu.yaml" \ --trust-remote-code \ --media-io-kwargs '{"video":{"num_frames":64,"fps":2}}'

3. 音视频 token 是否交叠(use_audio_in_video)

会交叠。use_audio_in_video=True 时,音视频在 token 序列里按时间对齐、交错排列。

  • 交叠逻辑在 vllm_omni/model_executor/models/qwen3_omni/qwen3_omni_moe_thinker.pyget_updates_use_audio_in_video_compute_interleaved_positions
  • video_second_per_grid_t(默认 2.0 秒)和 position_id_per_seconds 把视频格子的时间与音频 token 的时间对齐,按时间戳比较决定下一个放 video token 还是 audio token,得到交错序列。
  • 模型里用 check_interleaved_audio_videomerge_interleaved_embeddings(见 vllm_omni/model_executor/models/qwen2_5_omni/qwen2_5_omni_thinker.py)在 embedding 阶段按这个交错顺序合并。
419:432:vllm-omni-0.16.0/vllm_omni/model_executor/models/qwen3_omni/qwen3_omni_moe_thinker.py
展开代码
while video_data_index < len(video_token_indices) and audio_data_index < len(audio_token_indices): if video_token_indices[video_data_index] <= audio_token_indices[audio_data_index]: updates += [video_token_id] video_data_index += 1 else: updates += [audio_token_id] audio_data_index += 1 ... updates += [audio_end_token_id] return updates

所以:是“按时间交叠”的 video/audio token 序列,不是先整段视频再整段音频。


4. 单帧图像的 resize

qwen_omni_utils 里 resize 用的是谁? ms-swift 框架使用qwen_omni_utils,根据qwen_omni_utils的逻辑有:

fetch_video 里只用 token 数算像素上限,没有用任何 PIXELS 变量:

406:451:usr/local/lib/python3.12/dist-packages/qwen_omni_utils/v2_5/vision_process.py
展开代码
VIDEO_FRAME_MIN_PIXELS = VIDEO_MIN_TOKEN_NUM * image_factor * image_factor VIDEO_FRAME_MAX_PIXELS = VIDEO_MAX_TOKEN_NUM * image_factor * image_factor ... min_pixels = ele.get("min_pixels", VIDEO_FRAME_MIN_PIXELS) ... max_pixels = max(min(VIDEO_FRAME_MAX_PIXELS, total_pixels / nframes * FRAME_FACTOR), int(min_pixels * 1.05)) max_pixels_supposed = ele.get("max_pixels", max_pixels) ... resized_height, resized_width = smart_resize( height, width, factor=image_factor, min_pixels=min_pixels, max_pixels=max_pixels, )

也就是说:

  • 每帧的 resize 上限来自 VIDEO_FRAME_MAX_PIXELS
  • VIDEO_FRAME_MAX_PIXELS 只由 VIDEO_MAX_TOKEN_NUMimage_factor 决定。
  • 整个 qwen_omni_utils 里没有任何地方使用 VIDEO_MAX_PIXELS

所以:在训练框架里,真正控制每帧 resize 的是 VIDEO_MAX_TOKEN_NUM,VIDEO_MAX_PIXELS 在 qwen_omni_utils 里没有起作用

VIDEO_MAX_PIXELS 只在qwen_vl_utils里有效,在qwen_omni_utils里没有使用VIDEO_MAX_PIXELS。

Qwen3-Omni 的 image_factor 是 32

从当前 Qwen3-Omni 官方模型/配置里读到的结果是:

bash
展开代码
vision_config.patch_size = 16 vision_config.spatial_merge_size = 2(processor 里是 image_processor.merge_size = 2) 所以: image_factor = patch_size × spatial_merge_size = 16 × 2 = 32

Qwen3-Omni 官方模型在 HuggingFace Transformers 里的配置。

processor:Qwen3OmniMoeProcessor.from_pretrained('Qwen/Qwen3-Omni-30B-A3B-Instruct') 的 image_processor.patch_size = 16,image_processor.merge_size = 2。 Config:同一模型的 thinker_config.vision_config 里 patch_size = 16,spatial_merge_size = 2。

因此:image_factor = patch_size × spatial_merge_size = 16 × 2 = 32,这是“Qwen3-Omni 本来要用 32”的依据(来自官方模型/processor 配置)。

ms-swift bug

已按「Qwen3-Omni 用 32」修好,修改说明如下。


修改说明

文件ms-swift-main/swift/template/templates/qwen.py
Qwen2_5OmniTemplate(同时负责 omni_v2_5 和 omni_v3)

1. 在 replace_tag 开头为 omni_v3 准备 kwargs(约 634–637 行)

  • 增加 omni_kwargs = {}
  • 仅当 self.version == 'omni_v3' 时设置
    omni_kwargs['image_patch_size'] = self.processor.image_processor.patch_size
    (Qwen3-Omni 的 processor 里为 16,即 image_factor=32)。
  • omni_v2_5 不设置,仍用 qwen_omni_utils 默认 14(image_factor=28)。

2. 把 omni_kwargs 传给 fetch_imagefetch_video

  • fetch_image({'image': ...}) 改为
    fetch_image({'image': inputs.images[index]}, **omni_kwargs)
  • fetch_video({'video': video}) 改为
    fetch_video({'video': video}, **omni_kwargs)

这样:

  • Qwen3-Omni(omni_v3):用 processor.image_processor.patch_size(16)→ image_factor = 32
  • Qwen2.5-Omni(omni_v2_5)omni_kwargs 为空,仍用默认 14 → image_factor = 28,行为不变。

修改后的代码片段

634:653:ms-swift-main/swift/template/templates/qwen.py
展开代码
# Qwen3-Omni uses patch_size=16 (image_factor=32); Qwen2.5-Omni uses 14 (28) omni_kwargs = {} if self.version == 'omni_v3': omni_kwargs['image_patch_size'] = self.processor.image_processor.patch_size if media_type == 'image': inputs.images[index] = fetch_image({'image': inputs.images[index]}, **omni_kwargs) ... elif media_type == 'video': video = inputs.videos[index] _video = fetch_video({'video': video}, **omni_kwargs)

无需改环境变量或配置,训练 Qwen3-Omni 时会自动用 32;VIDEO_MAX_TOKEN_NUM 若按像素换算,应用 1024(32×32)作为每帧像素对应的 token 数。

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:Dong

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC。本作品采用《知识共享署名-非商业性使用 4.0 国际许可协议》进行许可。您可以在非商业用途下自由转载和修改,但必须注明出处并提供原作者链接。 许可协议。转载请注明出处!