专栏算法工具链【大模型】Qwen2.5-VL 网络结构简介

【大模型】Qwen2.5-VL 网络结构简介

no_name2026-08-20
28
0

引言

Qwen2.5-VL 是一个视觉-语言多模态大模型(VLM)。要理解它,第一件事是搞清楚"它到底长什么样"——有哪些模块、每个模块叫什么名字、数据怎么流过去。

本文 Qwen2.5-VL 的实现代码,描述网络的总体架构、各支路的模块组成与功能、模块名称,以及视觉 token 化的示例。


1. 总体架构

Qwen2.5-VL 整体由三部分组成:

  1. 视觉编码器(Vision Encoder / visual:一个 ViT 变体,把图像/视频编码成视觉 token 序列,经 Patch Merger 下采样后投影到语言模型的隐空间。
  2. 语言模型(Language Model / lm:一个 Qwen2.5 decoder,接收"文本 token embedding + 视觉 token embedding"的混合序列,自回归生成文本。
  3. 多模态融合机制:视觉 token 嵌入到文本序列的图像占位位置;位置编码采用 M-RoPE(多模态旋转位置编码),在时间、高、宽三个维度分别施加 RoPE。
代码顶层类是 Qwen2_5_VLModelForGeneration,它持有 self.visual 和 self.lm 两个子模块,并通过 generate 串联起 prefill(首 token 预填充)与 decode(逐 token 解码)两阶段。

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 关键机制

  1. 窗口注意力 + 全注意力分层:ViT 不是每层都全注意力。config.fullatt_block_indexes=[7,15,23,31] 指定的层做全注意力,其余层在 window_size=112 的局部窗口内做注意力。代码通过 get_window_index 重排 token 顺序、用 cu_seqlens(累积序列长度)标记每个窗口的边界,实现变长窗口注意力。
  2. 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

标准 pre-norm Transformer block:norm1(RMSNorm) → VisionAttention → residual → norm2(RMSNorm) → MLP → residual。其中 Qwen2_5_VLVisionAttention 用 qkv 融合的 Linear(输出 dim*3)一次算出 q/k/v。

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)

三个 Linear:gate_proj、up_proj(hidden → intermediate)、down_proj(intermediate → hidden)。

3.5 M-RoPE 多模态旋转位置编码

普通 RoPE 对 1D 序列位置施加旋转;M-RoPE 把 head_dim 沿 mrope_section=[16,24,24] 切成三段,分别对应 时间 t、高 h、宽 w 三个轴的位置——纯文本只用 t 段,视觉 token 用满三段,图像/视频的空间结构因此被显式编码进位置。
这里要分清两套不同的切分,它们作用在不同的维度上,容易混淆:
  1. 位置 id 的切分(序列维度 dim=1):forward 里把 position_ids 按 split([1, 1, 1], dim=1) 拆成三轴——时间轴位置 id、高轴位置 id、宽轴位置 id。[1,1,1] 是指"沿序列维每次取 1 列",拆出三个长度为 1 的轴位置。
  2. 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)) 翻倍。
两者配合使用:用第 1 步拆出的某个轴位置 id,去查第 2 步切出的对应那一段 cos/sin,三个轴各自查表再 cat 回完整 head_dim。一次切的是"位置"、一次切的是"频率表段",不是同一回事。

4. patch_size 与视觉 token 化

视觉支路的第一步是把图像切成 patch,相关参数全部来自 vision_config:

参数

取值

含义

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 切块与投影

Qwen2_5_VisionPatchEmbed 用一个卷积完成切块 + 线性投影,实现上有个细节值得注意:
  • 结构上始终创建 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)。
关键是:Conv2d 的权重不是独立的参数,而是运行时从 Conv3d 的权重在时间维 sum(2) 派生而来(把时间维的 2 个核相加塌缩成 1 个空间核)。换句话说,存储的始终是 Conv3d 权重,Conv2d 只是把"视频核"沿时间维压扁成"图像核"。所以"图像用 Conv2d、视频用 Conv3d"在前向路径上成立,但在参数来源上是同源的,不是两套独立权重。

4.2 Patch Merger 下采样

ViT 编码后,Patch Merger 把相邻 spatial_merge_size × spatial_merge_size = 2×2 = 4 个 patch 合并成 1 个 token,维度从视觉的 1280 投影到 LM 的 hidden_size。因此送给语言模型的视觉 token 数量只有 patch 数的 1/4。

4.3 图像尺寸对齐

为让 patch 网格和 2×2 合并都能整除,图像宽高要对齐到 patch_size × spatial_merge_size = 14 × 2 = 28 的倍数(即 smart_resize 逻辑)。

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 的关键结构参数对比,两列数值均取自各自权重目录 Qwen2.5-VL-3B-Instruct / Qwen2.5-VL-7B-Instruct 下的 config.json。

参数

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. 总结

Qwen2.5-VL 的结构可以记成"两个支路 + 一座桥":
  • 视觉支路: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)。
算法工具链
社区征文杂谈
评论0
0/600