引言
本文 Qwen2.5-VL 的实现代码,描述网络的总体架构、各支路的模块组成与功能、模块名称,以及视觉 token 化的示例。
1. 总体架构
Qwen2.5-VL 整体由三部分组成:
- 视觉编码器(Vision Encoder / visual):一个 ViT 变体,把图像/视频编码成视觉 token 序列,经 Patch Merger 下采样后投影到语言模型的隐空间。
- 语言模型(Language Model / lm):一个 Qwen2.5 decoder,接收"文本 token embedding + 视觉 token embedding"的混合序列,自回归生成文本。
- 多模态融合机制:视觉 token 嵌入到文本序列的图像占位位置;位置编码采用 M-RoPE(多模态旋转位置编码),在时间、高、宽三个维度分别施加 RoPE。
1.1 网络结构流程图

2. 视觉编码器 Qwen2_5_VLVisionModel
2.1 模块组成与功能
模块名 | 类 | 功能 |
|---|---|---|
patch_embed | Qwen2_5_VisionPatchEmbed | 把图像切成 patch_size×patch_size 的 patch,用 Conv2d 投影到 ViT 的 hidden_size 维(3B/7B 均为 1280) |
rotary_pos_emb | Qwen2_5_VisionRotaryEmbedding | 视觉专用的 2D RoPE,预计算 cos/sin,dim = head_dim//2 |
blocks | ModuleList[Qwen2_5_VLVisionBlock] | ViT 主体,层数 = config.depth(3B/7B 均为 32) |
merger | Qwen2_5_VLPatchMerger | 把相邻 spatial_merge_size²=4 个 patch 合并、投影到 LM 维度 out_hidden_size |
2.2 关键机制
- 窗口注意力 + 全注意力分层:ViT 不是每层都全注意力。config.fullatt_block_indexes=[7,15,23,31] 指定的层做全注意力,其余层在 window_size=112 的局部窗口内做注意力。代码通过 get_window_index 重排 token 顺序、用 cu_seqlens(累积序列长度)标记每个窗口的边界,实现变长窗口注意力。
- Patch Merger 降采样:spatial_merge_size=2,将 2×2 邻接 patch 合并成 1 个 token,视觉 token 数量降为 1/4,再把维度从 ViT 的 1280 投影到 LM 的 hidden_size(3B=2048 / 7B=3584)。这是视觉与语言两个支路对接的"桥",桥宽由 LM 的 hidden_size 决定。
2.3 Qwen2_5_VLVisionBlock
3. 语言模型 Qwen2_5_VLTextModel
3.1 模块组成与功能
模块名 | 类 | 功能 |
|---|---|---|
embed_tokens | Embedding | 文本 token id → embedding,形状 vocab_size × hidden_size |
layers | ModuleList[Qwen2_5_VLDecoderLayer] | decoder 主体,层数 = num_hidden_layers(3B=36 / 7B=28) |
norm | RMSNorm | 最后一层归一化 |
lm_head | Linear | 隐状态 → 词表 logits,形状 hidden_size × vocab_size,bias=False |
rotary_emb | Qwen2_5_VLRotaryEmbedding | 文本 M-RoPE,预计算 cache_cos/cache_sin |
cache_cos, cache_sin | buffer | 预计算好的 RoPE 查找表(按 max_kvcache_len 长度),避免 decode 时重算 |
3.2 Qwen2_5_VLDecoderLayer
pre-norm 结构,前向流程:
3.3 Qwen2_5_VLAttention
- GQA(分组查询注意力):num_heads 个 query 头共享 num_key_value_heads 个 KV 头(3B: 16Q/2KV,7B: 28Q/4KV)。
- 投影层:q_proj(hidden → num_heads*head_dim)、k_proj/v_proj(hidden → num_kv_heads*head_dim)、o_proj(num_heads*head_dim → hidden)。其中 q/k/v_proj 带 bias=True,o_proj bias=False。
- M-RoPE:apply_multimodal_rotary_pos_emb 对 q、k 同时施加多模态旋转位置编码。
3.4 Qwen2_5_VLMLP(SwiGLU)
3.5 M-RoPE 多模态旋转位置编码
- 位置 id 的切分(序列维度 dim=1):forward 里把 position_ids 按 split([1, 1, 1], dim=1) 拆成三轴——时间轴位置 id、高轴位置 id、宽轴位置 id。[1,1,1] 是指"沿序列维每次取 1 列",拆出三个长度为 1 的轴位置。
- cos/sin 查找表的切分(隐层维度 dim=-1):预计算好的 cache_cos/cache_sin 在隐层维度上按 mrope_section * 2 = [32, 48, 48] 切成三段,分别对应 t/h/w 三段旋转。乘 2 是因为 cos/sin 在 forward 里做了 torch.cat((freqs, freqs)) 翻倍。
4. patch_size 与视觉 token 化
参数 | 取值 | 含义 |
|---|---|---|
patch_size | 14 | 空间上每个 patch 覆盖 14×14 像素 |
spatial_patch_size | 14 | 与 patch_size 一致,空间 patch 边长 |
temporal_patch_size | 2 | 视频时序上每 2 帧合成 1 个时间步 |
spatial_merge_size | 2 | ViT 之后把相邻 2×2 个 patch 合并成 1 个 token |
in_chans | 3 | 输入通道数(RGB) |
4.1 切块与投影
- 结构上始终创建 Conv3d(self.proj,卷积核 [2, 14, 14]、步长 [2, 14, 14],时间维每 2 帧压成 1 个时间步),这是视频路径用的。
- 图像路径(use_conv2d=True)会额外创建一个 Conv2d(self.proj_2d,卷积核 [14, 14]、步长 [14, 14],不重叠地把图像切成网格)。实例化时传的就是 use_conv2d=True,所以跑图像时走 Conv2d 分支,每个 14×14×3 的 patch 被投影成 ViT 的一个 embedding 向量(维度 = 视觉 hidden_size = 1280)。
4.2 Patch Merger 下采样
4.3 图像尺寸对齐
4.4 一个算例:448×448 图像
阶段 | 计算 | 数量 |
|---|---|---|
原图 | 448×448 | 200704 像素 |
Patch 网格 | 448/14 = 32,32×32 | 1024 个 patch |
Merger 合并 | 2×2 合并,32/2 × 32/2 = 16×16 | 256 个视觉 token |

5. 3B 与 7B 的结构差异
参数 | 3B | 7B |
|---|---|---|
text hidden_size | 2048 | 3584 |
text num_hidden_layers | 36 | 28 |
text num_attention_heads | 16 | 28 |
text num_key_value_heads | 2 | 4 |
text intermediate_size | 11008 | 18944 |
text max_window_layers | 70 | 28 |
text tie_word_embeddings | true | false |
vocab_size | 151936 | 152064 |
text head_dim(推导 hidden/heads) | 128 | 128 |
vision depth | 32 | 32 |
vision hidden_size | 1280 | 1280 |
vision num_heads | 16 | 16 |
vision out_hidden_size | 2048 | 3584 |
vision 其余(patch/window/merge 等) | 相同 | 相同 |
rope_scaling.mrope_section | [16,24,24] | [16,24,24] |
rope_theta | 1e6 | 1e6 |
要点:
- 视觉部分几乎相同,只有 out_hidden_size 跟着 LM 的 hidden_size 变(对接桥的宽度)。
- head_dim 在 3B/7B 都是 128(2048/16 = 3584/28 = 128),意味着 attention 内部 head 维度一致。
M-RoPE 切分、RoPE 基频两者相同。
6. 总结
- 视觉支路:Patch Embed 切块 → ViT(窗口/全注意力分层)→ Patch Merger 降采样到 1/4。
- 语言支路:标准 Qwen2.5 decoder(GQA + SwiGLU + M-RoPE + KV cache)。
- 桥:Patch Merger 把视觉维度投影到 LM 的 hidden_size,桥宽随 LM 规模变化(3B=2048 / 7B=3584)。
