Qwen3-Omni 视频与音频 Token 机制详解
2026-03-05
深度学习
00

目录

Qwen3-Omni 视频与音频 Token 机制详解
一、整体架构概览
特殊 Token ID 对照表
二、temporalpatchsize=2 的完整机制
2.1 核心思想
2.2 模型关键参数
2.3 完整处理流程(以实际样本为例)
Step 1: 计算网格维度
Step 2: 重组为 3D 体素
Step 3: Conv3d 时空融合(核心步骤)
Step 4: ViT 处理
Step 5: 空间合并(PatchMerger)
2.4 完整 Shape 变化汇总
2.5 Token 缩减倍数
三、音频 Token 计算
3.1 处理流程
3.2 精确公式
3.3 计算示例
四、音频与视频的交叉排列
4.1 排列算法
4.2 关键参数
4.3 归并过程详解(前 3 秒)
第 0 秒
第 1 秒
第 2 秒
4.4 最终排列模式
4.5 token 比例
五、视频 Token 完整计算公式
5.1 帧采样
5.2 帧 Resize
5.3 视频 Token 数
5.4 总 Media Token 数
5.5 实际样本验证
六、训练(ms-swift)与推理(vLLM)的差异
6.1 帧采样差异
6.2 交叉排列逻辑
6.3 Resize 逻辑
七、环境变量速查

Qwen3-Omni 视频与音频 Token 机制详解

基于 Qwen3-Omni-30B-A3B-Instruct 模型,ms-swift 4.1.0.dev0 训练框架分析。


一、整体架构概览

Qwen3-Omni 处理视频+音频输入时,token 序列的整体结构为:

展开代码
<|im_start|>user <|vision_start|><|audio_start|> [视频token与音频token交叉排列] <|audio_end|><|vision_end|> 用户的文本 prompt... <|im_end|> <|im_start|>assistant 模型回复... <|im_end|>

use_audio_in_video=true 时,视频和音频 token 被交叉合并在同一个 <|vision_start|><|audio_start|>...<|audio_end|><|vision_end|> 区域内。

特殊 Token ID 对照表

Token IDToken 名称说明
151644<|im_start|>对话轮次开始
151645<|im_end|>对话轮次结束
151652<|vision_start|>视觉区域开始
151653<|vision_end|>视觉区域结束
151655<|image_pad|>图片填充 token
151656<|video_pad|>视频填充 token
151669<|audio_start|>音频区域开始
151670<|audio_end|>音频区域结束
151675<|audio_pad|>音频填充 token

二、temporal_patch_size=2 的完整机制

2.1 核心思想

temporal_patch_size=2 的含义是:将相邻 2 帧视频在时间维度上合并为 1 组,通过一个可学习的 3D 卷积提取时空特征。这不是简单的平均或拼接,而是一个有 170 万+ 参数的 Conv3d 操作。

2.2 模型关键参数

来自 Qwen3-Omni-30B-A3B-Instruct/config.json 中的 thinker_config.vision_config

参数说明
temporal_patch_size2每 2 帧合并为 1 个时间组
patch_size (spatial)16每 16×16 像素为 1 个空间 patch
spatial_merge_size2ViT 输出后,2×2 = 4 个相邻 token 合并为 1 个
hidden_size1152ViT 内部 embedding 维度
out_hidden_size2048输出维度(对齐 LLM hidden_size)
depth27ViT transformer 层数
in_channels3RGB 三通道

2.3 完整处理流程(以实际样本为例)

以一个 1280×720、30fps、81.6 秒的视频为例(FPS=2 采样后 162 帧,resize 后 384×736):

Step 1: 计算网格维度

展开代码
grid_t = 采样帧数 / temporal_patch_size = 162 / 2 = 81(时间格数) grid_h = 高度 / patch_size = 384 / 16 = 24(空间高度格数) grid_w = 宽度 / patch_size = 736 / 16 = 46(空间宽度格数)

Step 2: 重组为 3D 体素

将视频帧重组为 patch 向量,每个向量包含 2 帧同一空间位置的像素:

展开代码
[162帧, 3通道, 384高, 736宽] ↓ 重组 [89424 个体素, 1536 维] 其中:89424 = 81 × 24 × 46(grid_t × grid_h × grid_w) 1536 = 3 × 2 × 16 × 16(通道 × 时间patch × 空间patch × 空间patch)

每个体素物理含义如下图:

展开代码
帧 0(第 1 帧) 帧 1(第 2 帧) ┌──────────────┐ ┌──────────────┐ │ ┌────────┐ │ │ ┌────────┐ │ │ │ 16×16 │ │ │ │ 16×16 │ │ │ │ RGB │ │ │ │ RGB │ │ │ │ 像素块 │ │ │ │ 像素块 │ │ │ └────────┘ │ │ └────────┘ │ │ 384×736 │ │ 384×736 │ └──────────────┘ └──────────────┘ │ │ └───────────┬─────────────────┘ ↓ 1 个 3D 体素: [3通道, 2帧, 16像素, 16像素] = 1536 个数值

Step 3: Conv3d 时空融合(核心步骤)

python
展开代码
Conv3d( in_channels=3, # RGB out_channels=1152, # ViT hidden dim kernel_size=[2, 16, 16],# 覆盖 2帧 × 16px × 16px stride=[2, 16, 16], # 步长=核大小,不重叠 )
展开代码
[89424, 3, 2, 16, 16] → Conv3d → [89424, 1152, 1, 1, 1] → 展平 → [89424, 1152]
  • 1152 个卷积核,每个看 1536 个输入值,输出 1 个标量
  • 步长 = 核大小:不重叠,每对帧只处理一次
  • 数学上等价于对每个 1536 维向量做 nn.Linear(1536, 1152)
  • 这是可学习的,网络可以学到运动检测、时间差分等时空模式

Step 4: ViT 处理

展开代码
[89424, 1152] → 加位置编码 → 27 层 Transformer → [89424, 1152]

注意:ViT 的自注意力是帧内计算的。每帧 1104 个 token(24×46)互相 attend,不跨帧。跨帧信息已在 Conv3d 中融合。

Step 5: 空间合并(PatchMerger)

spatial_merge_size=2:相邻 2×2 = 4 个空间 token 拼接后通过 MLP 降维:

展开代码
[89424, 1152] → 每4个token拼接 → [22356, 4608] → MLP → [22356, 2048]

示意图:

展开代码
ViT 输出(某个 grid_t 的 24×46 = 1104 个 token): ┌────┬────┬────┬────┐ │ t1 │ t2 │ t3 │ t4 │ ├────┼────┼────┼────┤ │ t5 │ t6 │ t7 │ t8 │ └────┴────┴────┴────┘ 空间合并后(12×23 = 276 个 token): ┌──────────┬──────────┐ │concat │concat │ │(t1,t2, │(t3,t4, │ │ t5,t6) │ t7,t8) │ │→MLP→2048 │→MLP→2048 │ └──────────┴──────────┘

2.4 完整 Shape 变化汇总

步骤操作Tensor Shape说明
0原始输入[162, 3, 384, 736]162 帧 RGB
1帧数对齐[162, 3, 384, 736]已整除 2,无需 pad
2重组为体素[89424, 1536]89424 = 81×24×46
3Conv3d[89424, 1152]2 帧融合为 1 个 token
4位置编码[89424, 1152]加绝对位置 + RoPE
527 层 ViT[89424, 1152]帧内自注意力
6空间合并[22356, 2048]4 个 token 合并为 1 个

2.5 Token 缩减倍数

操作缩减维度缩减倍数
temporal_patch_size=2(Conv3d)时间÷ 2
patch_size=16(Conv3d)空间÷ 16 × ÷ 16 = ÷ 256
spatial_merge_size=2(MLP)空间 token 数÷ 4
总缩减162帧×384×736像素 → 22356 个 token

三、音频 Token 计算

3.1 处理流程

展开代码
原始音频(44100Hz 等) ↓ 重采样 16000 Hz(Whisper 采样率) ↓ 特征提取(hop_length=160) feature_frames ≈ 音频秒数 × 100 ↓ 三层下采样编码 audio_tokens ≈ 音频秒数 × 13

3.2 精确公式

python
展开代码
def get_audio_output_lengths(input_lengths): """Qwen3-Omni 音频 token 数计算""" input_lengths_leave = input_lengths % 100 # 余数部分 feat_lengths = (input_lengths_leave - 1) // 2 + 1 output_lengths = ( ((feat_lengths - 1) // 2 + 1 - 1) // 2 + 1 # 余数经三层下采样 + (input_lengths // 100) * 13 # 完整秒部分,每秒 13 token ) return output_lengths

3.3 计算示例

以 81.593 秒音频为例:

展开代码
feature_frames = int(81.593 × 100) = 8159 完整秒部分: 8159 // 100 = 81 → 81 × 13 = 1053 token 余数部分: 8159 % 100 = 59 → 经三层下采样 → 8 token 音频 token 总计: 1053 + 8 = 1061

近似规则:每秒音频 ≈ 13 个 token。


四、音频与视频的交叉排列

4.1 排列算法

use_audio_in_video=true 时,视频和音频 token 通过 merge-sort(归并排序) 方式交叉排列。

核心逻辑(三个代码库 HF Transformers / ms-swift / vLLM 完全一致):

python
展开代码
# 1. 视频 position: 同一 grid_t 的所有 token 共享相同 position video_positions = [] for t in range(grid_t): # t = 0, 1, 2, ..., 80 for _ in range(height * width): # 276 个空间 token video_positions.append(t * video_second_per_grid * position_id_per_seconds) # 2. 音频 position: 简单递增整数 audio_positions = [0, 1, 2, 3, ..., audio_len-1] # 3. 归并:position 小的先排,相等时视频优先(<=) v_idx, a_idx = 0, 0 while v_idx < len(video) and a_idx < len(audio): if video_positions[v_idx] <= audio_positions[a_idx]: 输出 video_pad token; v_idx++ else: 输出 audio_pad token; a_idx++ # 输出剩余 token

4.2 关键参数

在 FPS=2、temporal_patch_size=2 的配置下:

展开代码
video_second_per_grid = temporal_patch_size / FPS = 2 / 2 = 1.0 秒 position_id_per_seconds = 13.0(模型固定值) 视频 position 值: grid_t=0 的 276 个 token → position = 0 × 1.0 × 13.0 = 0.0 grid_t=1 的 276 个 token → position = 1 × 1.0 × 13.0 = 13.0 grid_t=2 的 276 个 token → position = 2 × 1.0 × 13.0 = 26.0 ... 音频 position 值: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, ...]

4.3 归并过程详解(前 3 秒)

第 0 秒

比较video_posaudio_pos结果输出
1~2760.000.0 <= 0 ✓276 个 video_pad
27713.0(grid_t=1)013.0 <= 0 ✗audio_pad
27813.0113.0 <= 1 ✗audio_pad
...13.02~1113.0 <= n ✗audio_pad
28913.01213.0 <= 12 ✗audio_pad
共 13 个 audio_pad

第 1 秒

比较video_posaudio_pos结果输出
29013.01313.0 <= 13 ✓video_pad
291~56513.01313.0 <= 13 ✓276 个 video_pad
56626.0(grid_t=2)1326.0 <= 13 ✗audio_pad
...26.014~2526.0 <= n ✗13 个 audio_pad

第 2 秒

比较video_posaudio_pos结果输出
26.02626.0 <= 26 ✓276 个 video_pad
39.026~3839.0 <= n ✗13 个 audio_pad

4.4 最终排列模式

每秒内:先排完所有视频 token,再排所有音频 token。

展开代码
<|vision_start|><|audio_start|> 第 0 秒: [video_pad × 276] [audio_pad × 13] ← grid_t=0 第 1 秒: [video_pad × 276] [audio_pad × 13] ← grid_t=1 第 2 秒: [video_pad × 276] [audio_pad × 13] ← grid_t=2 ... 第80秒: [video_pad × 276] [audio_pad × 剩余] ← grid_t=80 <|audio_end|><|vision_end|>

为什么是这个顺序:

  • 同一 grid_t 的 276 个视频 token 共享相同的 position 值(如 0.0)
  • 归并比较时 <= 让视频在平局时优先
  • 因此 276 个视频 token 连续输出,然后才轮到音频

4.5 token 比例

展开代码
每秒视频 token: 276 每秒音频 token: ~13 每秒总计: ~289 视频占比: 276 / 289 ≈ 95.5% 音频占比: 13 / 289 ≈ 4.5%

五、视频 Token 完整计算公式

5.1 帧采样

python
展开代码
# qwen_omni_utils 的 smart_nframes 函数 nframes = total_frames / video_fps * sample_fps nframes = floor_by_factor(nframes, temporal_patch_size) # 向下取 2 的倍数

5.2 帧 Resize

python
展开代码
# smart_resize: 保持宽高比,H 和 W 对齐到 factor=32 的倍数 image_factor = patch_size × spatial_merge_size = 16 × 2 = 32 max_pixels = VIDEO_MAX_TOKEN_NUM × image_factor² = 300 × 1024 = 307200 resized_h, resized_w = smart_resize( height, width, factor=32, min_pixels=3136, max_pixels=307200 )

5.3 视频 Token 数

展开代码
video_tokens = (nframes / temporal_patch_size) × (resized_h / patch_size) × (resized_w / patch_size) / spatial_merge_size²

简化为:

展开代码
video_tokens = grid_t × grid_h × grid_w / 4 = grid_t × (resized_h / 32) × (resized_w / 32)

5.4 总 Media Token 数

展开代码
total_media_tokens = video_tokens + audio_tokens

5.5 实际样本验证

视频:1280×720,30fps,2449 帧,81.6 秒。环境变量 FPS=2, VIDEO_MAX_TOKEN_NUM=300

展开代码
帧采样: 2449/30×2 = 163.27 → floor_by_factor(2) → 162 帧 grid_t: 162 / 2 = 81 Resize: 1280×720 → smart_resize(factor=32, max=307200) → 736×384 每帧 token: (736/32) × (384/32) = 23 × 12 = 276 视频 token: 81 × 276 = 22356 音频 token: get_audio_output_lengths(8159) = 1061 总计: 22356 + 1061 = 23417 ← 与训练日志完全匹配 ✓

六、训练(ms-swift)与推理(vLLM)的差异

6.1 帧采样差异

ms-swift(训练)vLLM(推理)
采样公式floor_by_factor(total/fps×2, 2)math.floor(duration×fps)
采样帧数162163
pad 后162(已整除 2)164(pad 到偶数)
grid_t8182
视频 token2235622632
总 media token2341723693
差异+276(多 1 个 grid_t)

6.2 交叉排列逻辑

三个代码库(HF Transformers / ms-swift / vLLM)的归并排序算法完全一致

6.3 Resize 逻辑

两边的 smart_resize 实现一致(相同 factor 和 pixel 限制下结果相同)。


七、环境变量速查

环境变量默认值说明
FPS2.0视频采样帧率
FPS_MAX_FRAMES768最大采样帧数
FPS_MIN_FRAMES4最小采样帧数
VIDEO_MAX_TOKEN_NUM每帧最大 token 数,换算为 max_pixels = N × 32²
VIDEO_TOTAL_PIXELS128000×28×28×0.9所有帧的总像素上限
USE_AUDIO_IN_VIDEOfalse是否提取视频中的音频并交叉排列
ENABLE_AUDIO_OUTPUT控制音频输出
如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:Dong

本文链接:

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