当前位置: 首页 > news >正文

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}Mspatial_merge_size(默认 2)
    • 单图/单视频合并后 token 数:tokens=t×(h⋅w)/M2\text{tokens} = t \times (h \cdot w)/M^2tokens=t×(hw)/M2
  • 张量形状记法:

    • 文本嵌入:[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 标记)。

逐步讲解(含关键张量形状)

1) 输入与初始嵌入

  • 未预融合(常见路径):

    • inputs_embeds = embed_tokens(input_ids) → 形状 [B, S, H]
  • 预融合input_ids is Noneinputs_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}tM2hw

(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

有三种来源/阶段:

  1. 外部已提供 position_ids:直接使用。

    • 若是 [4, B, S]:第一维为 text 1D,后三维为 vision t/h/w。
    • 若是 [3, B, S]:表示视觉 3D 坐标,text 1D 位置由模型内部另行补齐。
  2. 预填充阶段(prefill)或首次前向cache_position==0past_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 以供解码阶段使用
  3. 解码阶段(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 的配置信息

内部关键步骤:

  1. 构建 causal mask 映射

    • full_attention:标准自回归因果掩码
    • sliding_attention:若配置 use_sliding_window 且相应层为 sliding_attention,则构建滑动窗口因果掩码
    • 二者以 dict 形式存放,层内按 layer_types[layer_idx] 取用
  2. 共享 RoPE cos/sin(文本+多模态 3D)

    • Qwen2_5_VLRotaryEmbedding.forward(x, position_ids) 计算
    • 返回 (cos, sin),随后各层 attention 共用
  3. 逐层 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_idscu_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]
    • 残差连接

    • 后归一化(RMSNorm)+ MLP(SwiGLU)

    • 残差连接

    • (可选)收集 attentions/hidden_states

  4. 最终归一化RMSNorm 得到 last_hidden_state: [B, S, H]

  5. 返回
    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 内部结构要点

  1. 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}TpHinpWin,通道 =Hv=H_v=Hv
  1. RoPE(视觉)
  • Qwen2_5_VisionRotaryEmbedding 按网格坐标(t/h/w 映射到频率)生成 cos⁡,sin⁡\cos,\sincos,sin
  1. 多层 Qwen2_5_VLVisionBlock
  • 每层:RMSNorm → 自注意力(应用视觉 RoPE)→ 残差 → RMSNorm → MLP → 残差
  1. PatchMerger
  • M×MM \times MM×M 空间窗口合并,序列长度缩短为 1/M21/M^21/M2,维度投到 out_hidden_size(与 HHH 对齐)
  1. 排列/窗口索引
  • 通过 get_window_index / cu_seqlens 支持变长、窗口化注意力(尤其 FA2),最后合并与还原顺序

get_rope_index 的关键逻辑(预填充阶段)

  • 视觉部分

    • 基于 (t,h,w)(t,h,w)(t,h,w) 生成三条位置序列:

      • temporal:依据 tokens_per_secondsecond_per_grid_ts 计算步长,形成稀疏时间索引(例如每个 temporal patch 时间间隔固定)
      • height / width:网格坐标 0…(h/M-1)、0…(w/M-1)
  • 文本部分

    • 紧接在视觉最大位置之后 +1 起始,连续递增(保证 text 与 vision 的 ID 空间不冲突)
  • rope_deltas

    • 记录“视觉位置的整体偏移量 − 文本长度”,使得解码时能把当前步文本位置 + 偏移,快速得到对齐的视觉 3D 位置

语言侧注意力(单层)数据流(补充)

  1. 投影:
  • 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=nkvgroups
  1. 多模态 RoPE:
  • HHH(头维 × 头内维)按 mrope_section 分成(t/h/w)三段 + 文本段规则,分别乘以相应 cos/sin,执行旋转 $ (x\cos + \text{rot}(x)\sin)$
  1. 注意力实现(eager/SDPA/FA2),应用掩码(全因果/滑窗)
  2. 聚合输出 [B,S,H][B,S,H][B,S,H],残差、MLP、残差

输出(Qwen2_5_VLModelOutputWithPast

  • last_hidden_state: [B,S,H]
  • past_key_values: 用于加速解码
  • hidden_statesattentions(按需)
  • rope_deltas: [B,1]:为后续解码阶段增量构造 3D 视觉位置所用

没问题~我把关键细节进一步“打散 + 做实”,给你更多可渲染 Mermaid 小图形状表、以及一条带具体数字的完整算例,覆盖:占位符映射、FA2 的 cu_seqlens、mask 构造、mRoPE 通道切分与旋转、以及视觉 token 计数与还原。
(依旧采用双引号节点 + <br/> 换行 + 圆括号,确保可渲染。)


A) 视觉占位符对齐(从占位符到特征写回)

inputs_embeds (B,S,H) 初始为 text embedding 或已部分融合
从 input_ids 标出占位符
image_token_id / video_token_id
get_placeholder_mask →
image_mask, video_mask (B,S,H)
并校验元素总数与特征元素数一致
image_embeds 拼接 (Σ_img_tokens,H)
masked_scatter: inputs_embeds[image_mask] ← image_embeds
video_embeds 拼接 (Σ_vid_tokens,H)
masked_scatter: inputs_embeds[video_mask] ← video_embeds
融合后的 inputs_embeds (B,S,H)

形状与计数细则

  • 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=1mtiM2hiwi

  • masked_scatter扁平顺序写入:image_embeds 必须与 mask True 的元素一一对应


B) FA2 变长注意力的 cu_seqlens(视觉侧)

grid_thw (N,3) 列出每张图(或视频段)的 (t,h,w)
计算合并后的每段长度
len_i = t_i * (h_i*w_i) / M^2
累加得到 cumsum: L1, L2, ..., Lk
cu_seqlens = [0, L1, L2, ..., Lk] (int32)
长度为 k+1
flash_attention_2 接口:
cu_seq_lens_q=cu_seqlens, cu_seq_lens_k=cu_seqlens,
max_length_q = max(Li)
  • FA2 的 cu_seqlens 保证每张图/视频段单独做注意力,再整体拼接;避免跨样本/跨段互相注意。
  • 在视觉编码器中还会生成窗口级 cu_window_seqlens(用于局部注意力窗口拼接与剪裁)。

C) 文本侧 mask 构造(全因果 / 滑窗)

输入: attention_mask (B,S) 可选
text_position_ids (1,B,S) 构造或外给
create_causal_mask → full_attention (B,1,S,S)
上三角为 -inf, 下三角为 0
config.use_sliding_window?
causal_mask_mapping = {'full_attention': M2}
create_sliding_window_causal_mask(窗口 W) → sliding_attention (B,1,S,S)
causal_mask_mapping = {'full_attention': M2, 'sliding_attention': M5}
  • 每层解码器在 forward 时,会按 layer_types[layer_idx] 选择使用 full_attentionsliding_attention
  • attention_mask==0 的位置会在 mask 中强制设为 -inf 屏蔽。

D) 多模态 RoPE 的通道切分与旋转(文本侧 Q/K)

Q (B,n_heads,S,d), K (B,n_kv,S,d)
position_ids (4,B,S):
text(1) + vision(3: t,h,w)
Qwen2_5_VLRotaryEmbedding → (cos,sin)
(同形状可广播到 Q/K)
按照 mrope_section 在通道维切分
例如: [d_t, d_h, d_w, d_txt] 且 d_t+d_h+d_w+d_txt = d*2 (因 cos/sin 拼接)
对 (t 段) 用 cos_t/sin_t 旋转;(h 段) 用 cos_h/sin_h;(w 段) 用 cos_w/sin_w;(txt 段) 等价 1D RoPE
拼回 Q', K' (B,*,S,d) 供注意力计算
  • 旋转公式(对每一段通道):

    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~=Qcos+rotate_half(Q)sin,K~=Kcos+rotate_half(K)sin

  • mrope_section 控制 t/h/w/text 的通道占比(代码中按段循环重组 cos/sin)。


E) 视觉编码器内部形状(更精细)

输入像素(经预处理后的序列化张量)
Qwen2_5_VisionPatchEmbed (3D Conv)
输出 (seq_len, H_v)
rot_pos_emb(grid_thw) → rotary_pos_emb (seq_len, H_v/2)×2 拼为 (seq_len, H_v) 的角频
get_window_index(grid_thw) → window_index, cu_window_seqlens
按 window_index 重排 hidden_states/rotary_emb
for i in 1..depth: VisionBlock:
RMSNorm → VisionAttention(带 RoPE, 可能用 FA2, cu_seqlens) → 残差
RMSNorm → VL-MLP → 残差
Qwen2_5_VLPatchMerger (合并 MxM)
维度: (seq_len/M^2, out_hidden_size≈H)
反排回原顺序 → 输出 (总视觉 tokens, H)

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_maskTrue 的位置数 = 294 * H
    • 样本 2:video_maskTrue 的位置数 = 147 * H
  • masked_scatter 写回后:

    • 两个样本的 inputs_embeds 均为 (B,S,H) ;其中样本 1 的两段图像 token 区间、样本 2 的一段视频 token 区间已经被视觉特征替换。
4. get_rope_index(样本 2 的时间步举例)
  • 视频 t=3,second_per_grid_t=0.5tokens_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=24head_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)
  • 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)(广播到头维)逐段旋转
  • 注意力输出重排回 (S,H_v)proj

I) 常见“坑”与对策清单

  1. 占位符数量 vs 特征数量不对齐

    • 典型原因:grid_thw 与实际 Processor 的切块策略不一致,或 M 值算错。
    • 对策:打印每段 (t,h,w)Σ tokens,核对 image_mask.sum()/H 是否等于 Σ tokens
  2. FA2 报错 cu_seqlens 维度/类型

    • 必须 int32,长度 = 段数 + 1,首元素为 0,递增且末元素为总 tokens。
    • max_length_q/k 要设为段长度最大值。
http://www.xdnf.cn/news/1413649.html

相关文章:

  • 恒香全新旗舰店开幕 新店传承百年文化
  • 容器seccomp配置文件在云服务器安全策略中的实施规范
  • 常用定位技术对比解析
  • MySQL数据库——0.MySQL大纲
  • 【全功能图片处理工具详解】基于Streamlit的现代化图像处理解决方案
  • OpenCV 图像轮廓检测
  • 【系统分析师】高分论文:论面向服务方法在信息系统开发中的应用
  • 基于CotSegNet网络和机器学习的棉花点云器官分割和表型信息提取
  • 获取某天的零点日期
  • 解锁GPU计算潜能:深入浅出CUDA架构与编程模型
  • Day18 (前端:JavaScript基础阶段)
  • Langflow 评估与迭代技术深度分析
  • Cookie、Session 和 JWT
  • git中使用SSH的配置
  • 堆排序:高效稳定的大数据排序法
  • 【图论】 Graph.jl 概览
  • 面试问题详解十三:Qt 多线程同步【QReadWriteLock】讲解
  • 24数学建模国赛C
  • 【数据分享】上市公司-国际化程度-营业收入指标(2005-2023)
  • Linux软件升级方法总结
  • (树)Leetcode94二叉树的中序遍历
  • RK3568平台开发系列讲解:瑞芯微平台4G模块篇移植
  • Java 类加载器解析
  • macos自动安装emsdk4.0.13脚本
  • 【开题答辩全过程】以 家庭理财管理系统的设计与实现为例,包含答辩的问题和答案
  • Playwright 中Codegen的优点与局限性分析
  • a3002盘式制动器刹车cad➕三维图➕设计说明书
  • flutter工程
  • kkfileview自建cdn引入
  • 血缘元数据采集开放标准:OpenLineage Integrations Compatibility Tests Structure