Qwen2.5-VL代码初步解读
Qwen2.5-VL代码初步解读
约定与符号
-
批量与长度:
- BBB:batch size
- SSS:文本 token 序列长度
-
维度与配置:
- HHH:语言侧隐藏维度(
config.text_config.hidden_size
) - HvH_vHv:视觉侧隐藏维度(
vision_config.hidden_size
,与 HHH 通常对齐) - NimgN_\text{img}Nimg:图像数量;NvidN_\text{vid}Nvid:视频数量
- 图像/视频网格:每张图/每段视频有 $ (t, h, w) \equiv \text{grid_thw}$
- 视觉块合并:空间合并因子 M≡spatial_merge_sizeM \equiv \text{spatial\_merge\_size}M≡spatial_merge_size(默认 2)
- 单图/单视频合并后 token 数:tokens=t×(h⋅w)/M2\text{tokens} = t \times (h \cdot w)/M^2tokens=t×(h⋅w)/M2
- HHH:语言侧隐藏维度(
-
张量形状记法:
- 文本嵌入:[B,S,H][B, S, H][B,S,H]
- 图像像素:[Nimg,C,Hin,Win][N_\text{img}, C, H_{in}, W_{in}][Nimg,C,Hin,Win]
- 视频像素:[Nvid,T,C,Hin,Win][N_\text{vid}, T, C, H_{in}, W_{in}][Nvid,T,C,Hin,Win]
- 视觉编码输出:拼接后 [∑tokens,Hv][\sum \text{tokens}, H_v][∑tokens,Hv]
-
位置编码(多模态 3D RoPE):
position_ids
形状为 [3,B,S][3, B, S][3,B,S](仅视觉维度的 cos/sin),或 拼好 text+vision 的 [4,B,S][4, B, S][4,B,S](第一维是 text 1D,后三维是 vision 的 t/h/w)。
-
其它:
- “占位符”是指输入里用于插入图像/视频特征的特殊 token 位点(由
image_token_id
/video_token_id
标记)。
- “占位符”是指输入里用于插入图像/视频特征的特殊 token 位点(由
逐步讲解(含关键张量形状)
1) 输入与初始嵌入
-
若 未预融合(常见路径):
inputs_embeds = embed_tokens(input_ids)
→ 形状 [B, S, H]
-
若 预融合(
input_ids is None
且inputs_embeds
已给出):- 要求必须同时提供
position_ids
(否则报错),因为此时模型无法稳妥地重建多模态 3D RoPE。
- 要求必须同时提供
2) 视觉侧编码(仅未预融合时发生)
对 pixel_values
(图)与 pixel_values_videos
(视频)分别走一条相同结构的编码链:
(a) PatchEmbed / 3DConv
- 将输入(图像视作 t=1t{=}1t=1 的视频)分时空切块,3D Conv 的 kernel/stride 为
[tpatch,p,p][t_\text{patch}, p, p][tpatch,p,p](时间块、空间块)。 - 输出序列化的块特征,投到维度 HvH_vHv。
(b) 视觉 RoPE & 注意力块
- 计算视觉 RoPE(
Qwen2_5_VisionRotaryEmbedding
),按需要生成 cos/sin。 - 若后端为 flash-attn-2,使用
cu_seqlens
做变长注意力。 - 多层
Qwen2_5_VLVisionBlock
:每层 = RMSNorm → Self-Attn(带 RoPE)→ 残差 → RMSNorm → MLP → 残差。
© PatchMerger(降序列长)
- 合并 M×MM \times MM×M 空间相邻块,输出维度为
vision_config.out_hidden_size
(通常等于 HHH)。 - 对一个 (t,h,w)(t,h,w)(t,h,w) 的网格,合并后 token 数变为:t⋅h⋅wM2\displaystyle t \cdot \frac{h\cdot w}{M^2}t⋅M2h⋅w。
(d) 按样本切分 & 拼接
-
所有图像(视频)编码后得到列表,再
cat
成:image_embeds
:[∑itokensi,Hv][\sum_i \text{tokens}_i, H_v][∑itokensi,Hv]video_embeds
:[∑jtokensj,Hv][\sum_j \text{tokens}_j, H_v][∑jtokensj,Hv]
-
一般 HvH_vHv 会与语言侧 HHH 对齐,随后拷贝到
inputs_embeds
的 dtype/device。
3) 占位符对齐与特征写回
-
get_placeholder_mask(input_ids, inputs_embeds, image_features=?/video_features=?)
:- 产生
image_mask
/video_mask
,形状 [B, S, H],并校验占位符总元素数与特征元素数一致。
- 产生
-
回写(逐元素散点替换):
inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds)
inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds)
-
结果仍为 [B, S, H],只是视觉占位符位置被真实视觉特征替换。
4) 3D RoPE 的 position_ids
& rope_deltas
有三种来源/阶段:
-
外部已提供
position_ids
:直接使用。- 若是 [4, B, S]:第一维为 text 1D,后三维为 vision t/h/w。
- 若是 [3, B, S]:表示视觉 3D 坐标,text 1D 位置由模型内部另行补齐。
-
预填充阶段(prefill)或首次前向(
cache_position==0
或past_key_values
为空):-
调用
get_rope_index(input_ids, image_grid_thw, video_grid_thw, second_per_grid_ts, attention_mask)
计算:position_ids
([3, B, S]):视觉 t/h/w 的位置序列(text 部分与视觉不重叠)rope_deltas
([B, 1]):“视觉位置相对文本位置的偏移”,缓存到self.rope_deltas
以供解码阶段使用
-
-
解码阶段(decoding):
- 依据当前步的
cache_position
(或者从past_key_values.get_seq_length()
推断)得到text_positions
([1, B, 1] 或 [1,B,S_step]) - 用缓存的
rope_deltas
生成vision_positions
([3, B, 1] 或 [3,B,S_step]) - 拼成 [4, B, S(step)]。
- 依据当前步的
小结:预填充计算一次完整视觉 3D 位置与
rope_deltas
;解码只增量用rope_deltas
平移当前步的视觉位置,避免重复重建。
5) 调用语言模型(Qwen2_5_VLTextModel.forward
)
输入(关键参数):
inputs_embeds
:[B, S, H](已含视觉特征)position_ids
:[4, B, S](text 1D + vision 3D)attention_mask
:[B, S](可选)past_key_values
/cache_position
:缓存与步位置- 以及
_attn_implementation
、是否使用 sliding-window 的配置信息
内部关键步骤:
-
构建 causal mask 映射
full_attention
:标准自回归因果掩码sliding_attention
:若配置use_sliding_window
且相应层为sliding_attention
,则构建滑动窗口因果掩码- 二者以 dict 形式存放,层内按
layer_types[layer_idx]
取用
-
共享 RoPE cos/sin(文本+多模态 3D)
Qwen2_5_VLRotaryEmbedding.forward(x, position_ids)
计算- 返回
(cos, sin)
,随后各层 attention 共用
-
逐层 Decoder(共
num_hidden_layers
层)
对每层:-
输入层归一化(RMSNorm)
-
Self-Attention:
-
q_proj/k_proj/v_proj
:- Q: [B,S,H]→[B,S,nheads,d][B, S, H] → [B, S, n_\text{heads}, d][B,S,H]→[B,S,nheads,d]
- K/V: [B,S,nkv,d][B, S, n_\text{kv}, d][B,S,nkv,d]
-
多模态 RoPE 应用(关键):
- 将 [4,B,S][4,B,S][4,B,S] 的 text+vision 位置分量拆解为 text 1D 与 vision t/h/w 三路,
- 通过
apply_multimodal_rotary_pos_emb(q, k, cos, sin, mrope_section)
分段对 channel 维做旋转。
-
注意力实现:
- eager / SDPA / FlashAttention2(FA2 会用到
position_ids
及cu_seqlens
) - 输出 attn:[B,S,nheads,d][B, S, n_\text{heads}, d][B,S,nheads,d] → 合并 →
o_proj
回到 [B,S,H][B, S, H][B,S,H]
- eager / SDPA / FlashAttention2(FA2 会用到
-
-
残差连接
-
后归一化(RMSNorm)+ MLP(SwiGLU)
-
残差连接
-
(可选)收集
attentions
/hidden_states
-
-
最终归一化:
RMSNorm
得到last_hidden_state: [B, S, H]
-
返回:
BaseModelOutputWithPast
(在Qwen2_5_VLModel
外面包装成Qwen2_5_VLModelOutputWithPast
),包含:last_hidden_state: [B, S, H]
past_key_values
: 缓存hidden_states
/attentions
(按需)rope_deltas: [B,1]
(从Qwen2_5_VLModel
透传缓存结果)
常见形状一览(快速对照)
名称 | 形状 |
---|---|
input_ids | [B,S][B,S][B,S] |
inputs_embeds (文本嵌入后) | [B,S,H][B,S,H][B,S,H] |
pixel_values | [Nimg,C,Hin,Win][N_\text{img}, C, H_{in}, W_{in}][Nimg,C,Hin,Win] |
pixel_values_videos | [Nvid,T,C,Hin,Win][N_\text{vid}, T, C, H_{in}, W_{in}][Nvid,T,C,Hin,Win] |
image_grid_thw / video_grid_thw | [N,3][N, 3][N,3] |
视觉编码拼接 image_embeds | [∑img tokens,Hv][\sum \text{img tokens}, H_v][∑img tokens,Hv] |
视觉编码拼接 video_embeds | [∑vid tokens,Hv][\sum \text{vid tokens}, H_v][∑vid tokens,Hv] |
占位符 mask(图/视) | [B,S,H][B,S,H][B,S,H](bool,广播到最后一维) |
position_ids (视觉 3D) | [3,B,S][3,B,S][3,B,S] |
position_ids (text+vision 完整) | [4,B,S][4,B,S][4,B,S] |
rope_deltas | [B,1][B,1][B,1] |
last_hidden_state | [B,S,H][B,S,H][B,S,H] |
视觉侧 visual
内部结构要点
- PatchEmbed(3DConv)
kernel=stride=[t_patch, p, p]
,把 (T,Hin,Win)(T,H_{in},W_{in})(T,Hin,Win) 切成小块- 输出序列长度 ≈T⋅Hinp⋅Winp\approx T \cdot \frac{H_{in}}{p} \cdot \frac{W_{in}}{p}≈T⋅pHin⋅pWin,通道 =Hv=H_v=Hv
- RoPE(视觉)
- 用
Qwen2_5_VisionRotaryEmbedding
按网格坐标(t/h/w 映射到频率)生成 cos,sin\cos,\sincos,sin
- 多层
Qwen2_5_VLVisionBlock
- 每层:RMSNorm → 自注意力(应用视觉 RoPE)→ 残差 → RMSNorm → MLP → 残差
- PatchMerger
- 按 M×MM \times MM×M 空间窗口合并,序列长度缩短为 1/M21/M^21/M2,维度投到
out_hidden_size
(与 HHH 对齐)
- 排列/窗口索引
- 通过
get_window_index
/cu_seqlens
支持变长、窗口化注意力(尤其 FA2),最后合并与还原顺序
get_rope_index
的关键逻辑(预填充阶段)
-
视觉部分:
-
基于 (t,h,w)(t,h,w)(t,h,w) 生成三条位置序列:
- temporal:依据
tokens_per_second
与second_per_grid_ts
计算步长,形成稀疏时间索引(例如每个 temporal patch 时间间隔固定) - height / width:网格坐标 0…(h/M-1)、0…(w/M-1)
- temporal:依据
-
-
文本部分:
- 紧接在视觉最大位置之后 +1 起始,连续递增(保证 text 与 vision 的 ID 空间不冲突)
-
rope_deltas
:- 记录“视觉位置的整体偏移量 − 文本长度”,使得解码时能把当前步文本位置 + 偏移,快速得到对齐的视觉 3D 位置。
语言侧注意力(单层)数据流(补充)
- 投影:
- Q:[B,S,H]→[B,nh,S,d]Q: [B,S,H] \to [B,n_h,S,d]Q:[B,S,H]→[B,nh,S,d]
- K/V:[B,nkv,S,d]K/V: [B,n_{kv},S,d]K/V:[B,nkv,S,d](GQA 时 nh=nkv⋅groupsn_h = n_{kv} \cdot \text{groups}nh=nkv⋅groups)
- 多模态 RoPE:
- 将 HHH(头维 × 头内维)按
mrope_section
分成(t/h/w)三段 + 文本段规则,分别乘以相应 cos/sin,执行旋转 $ (x\cos + \text{rot}(x)\sin)$
- 注意力实现(eager/SDPA/FA2),应用掩码(全因果/滑窗)
- 聚合输出 [B,S,H][B,S,H][B,S,H],残差、MLP、残差
输出(Qwen2_5_VLModelOutputWithPast
)
last_hidden_state: [B,S,H]
past_key_values
: 用于加速解码hidden_states
、attentions
(按需)rope_deltas: [B,1]
:为后续解码阶段增量构造 3D 视觉位置所用
没问题~我把关键细节进一步“打散 + 做实”,给你更多可渲染 Mermaid 小图、形状表、以及一条带具体数字的完整算例,覆盖:占位符映射、FA2 的 cu_seqlens
、mask 构造、mRoPE 通道切分与旋转、以及视觉 token 计数与还原。
(依旧采用双引号节点 + <br/>
换行 + 圆括号,确保可渲染。)
A) 视觉占位符对齐(从占位符到特征写回)
形状与计数细则
-
image_mask
是(B,S,H)
的布尔张量,True
的元素数必须等于image_embeds.numel()
;同理视频。 -
若某样本有
m
张图,每张(t,h,w)
,合并因子M
,则该样本图像特征 token 数:img_tokens=∑i=1mti⋅hi⋅wiM2 \text{img\_tokens} = \sum_{i=1}^{m} t_i \cdot \frac{h_i\cdot w_i}{M^2} img_tokens=i=1∑mti⋅M2hi⋅wi
-
masked_scatter
按扁平顺序写入:image_embeds
必须与 maskTrue
的元素一一对应。
B) FA2 变长注意力的 cu_seqlens
(视觉侧)
- FA2 的
cu_seqlens
保证每张图/视频段单独做注意力,再整体拼接;避免跨样本/跨段互相注意。 - 在视觉编码器中还会生成窗口级
cu_window_seqlens
(用于局部注意力窗口拼接与剪裁)。
C) 文本侧 mask 构造(全因果 / 滑窗)
- 每层解码器在 forward 时,会按
layer_types[layer_idx]
选择使用full_attention
或sliding_attention
。 attention_mask==0
的位置会在 mask 中强制设为 -inf 屏蔽。
D) 多模态 RoPE 的通道切分与旋转(文本侧 Q/K)
-
旋转公式(对每一段通道):
Q~=Q⋅cos+rotate_half(Q)⋅sin,K~=K⋅cos+rotate_half(K)⋅sin \tilde{Q} = Q\cdot \cos + \text{rotate\_half}(Q)\cdot \sin,\quad \tilde{K} = K\cdot \cos + \text{rotate\_half}(K)\cdot \sin Q~=Q⋅cos+rotate_half(Q)⋅sin,K~=K⋅cos+rotate_half(K)⋅sin
-
mrope_section
控制 t/h/w/text 的通道占比(代码中按段循环重组 cos/sin)。
E) 视觉编码器内部形状(更精细)
F) 端到端数字化算例(一步步数清 token 与形状)
假设:
批量 B=2,文本长度 S=128,隐藏维度 H=3072(示例)。
spatial_merge_size M=2
,视觉编码输出维度与 H 对齐。图像 patch 内部细节由 Processor 决定,
grid_thw
给出 LLM 所见的网格:
样本 1:2 张图
- 图1:grid_thw=(t=1, h=28, w=28)
- 图2:grid_thw=(1, h=14, w=28)
样本 2:1 段视频
- 视1:grid_thw=(t=3, h=14, w=14),
second_per_grid_t=0.5
(每个时间网格 0.5 秒)
tokens_per_second=25
(来自视觉 config)
1. 视觉 token 数
-
样本 1(图像):
- 图1 tokens = 1 * (28*28) / 2^2 = 196
- 图2 tokens = 1 * (14*28) / 4 = 98
- 合计 = 294
-
样本 2(视频):
- 视1 tokens = 3 * (14*14) / 4 = 147
- 合计 = 147
2. 视觉编码输出拼接形状
-
合批后按样本拼:
image_embeds
(只样本 1 有):(294, H)video_embeds
(只样本 2 有):(147, H)
3. 占位符布局与写回
-
样本 1 的
input_ids
中有vision_start_token_id
后紧跟image_token_id
两处;样本 2 中紧跟video_token_id
一处。 -
get_placeholder_mask
生成:- 样本 1:
image_mask
为True
的位置数 = 294 * H - 样本 2:
video_mask
为True
的位置数 = 147 * H
- 样本 1:
-
masked_scatter
写回后:- 两个样本的
inputs_embeds
均为 (B,S,H) ;其中样本 1 的两段图像 token 区间、样本 2 的一段视频 token 区间已经被视觉特征替换。
- 两个样本的
4. get_rope_index
(样本 2 的时间步举例)
-
视频 t=3,
second_per_grid_t=0.5
,tokens_per_second=25
,temporal 步长Δt=tps×sec_per_grid=25×0.5=12.5 \Delta_t = \text{tps} \times \text{sec\_per\_grid} = 25 \times 0.5 = 12.5 Δt=tps×sec_per_grid=25×0.5=12.5
取整数(代码里有
long()
),得到 [0, 12, 25] 或按实现细节可能为 [0, 12, 12+12=24] 等(以整除/取整为准)。 -
h/w 位置是 0…(14/2-1)=0…6 的网格坐标展开。
-
position_ids (3,B,S)
中视觉段使用上述 (t,h,w) 三条序列拼接;文本段位置接在视觉最大值之后,从max_vision_pos+1
开始累加。 -
rope_deltas (B,1) = max_position + 1 - S
,用于解码阶段平移视觉 3D 位置。
5. 文本模型前向
position_ids
最终用于Qwen2_5_VLRotaryEmbedding
,产生共享(cos,sin)
。- 每层:RMSNorm → Self-Attn(应用多模态 RoPE) → 残差 → RMSNorm → MLP → 残差。
- 输出
last_hidden_state (B,S,H)
。
G) 文本注意力单层的维度核对清单
-
设
n_heads=24
,head_dim=H/n_heads=128
(示例),n_kv=8
(GQA,每 3 个 query 头共享 1 个 kv 头)。 -
线性投影:
- Q:
(B,S,H) → (B,S,n_heads*head_dim) → 视图 (B,n_heads,S,head_dim)
- K/V:
(B,S,H) → (B,S,n_kv*head_dim) → 视图 (B,n_kv,S,head_dim)
- Q:
-
GQA 展开:实现内部会把 K/V 通过
repeat_kv
展开到(B,n_heads,S,head_dim)
以与 Q 对齐(eager 路径中)。 -
注意力:
- 分数
(B,n_heads,S,S)
;加 mask;softmax
;乘 V →(B,n_heads,S,head_dim)
→ 合并头 →(B,S,H)
。
- 分数
H) 视觉注意力单层的维度核对清单
-
输入
hidden_states (S,H_v)
(视觉侧是“序列在前、批在后”的内部布局,随后加一个伪 batch 维度 1)- Q/K/V:
(1,n_heads,S,head_dim_v)
- RoPE:按
rot_pos_emb
产生的(cos,sin)
(广播到头维)逐段旋转
- Q/K/V:
-
注意力输出重排回
(S,H_v)
再proj
。
I) 常见“坑”与对策清单
-
占位符数量 vs 特征数量不对齐
- 典型原因:
grid_thw
与实际 Processor 的切块策略不一致,或M
值算错。 - 对策:打印每段
(t,h,w)
与Σ tokens
,核对image_mask.sum()/H
是否等于Σ tokens
。
- 典型原因:
-
FA2 报错
cu_seqlens
维度/类型- 必须
int32
,长度= 段数 + 1
,首元素为 0,递增且末元素为总 tokens。 max_length_q/k
要设为段长度最大值。
- 必须