Qwen2.5-VL_RoPE计算流程详解
Qwen2.5-VisionTransformer 中 RoPE cos/sin 的计算流程
整个流程从 forward(x, grid_thw) 开始,分为以下几个阶段:
第一步:初始化时预计算 cos/sin 缓存
1 | self.rotary_pos_emb = get_rope( |
get_rope 最终创建一个 RotaryEmbedding 对象,关键参数:
rotary_dim = head_dim * 0.5→ 只使用一半的 head 维度做旋转- 在
_compute_cos_sin_cache中(base.py:83-92):
1 | # inv_freq 形状: [rotary_dim // 2] |
原理:标准 RoPE 公式 θ_i = base^(-2i/d),每个 head 维度对应一个旋转频率。
第二步:为每张图像/视频计算 2D 空间位置 ID
qwen2_5_vl.py:654-697 rotary_pos_emb_thw(t, h, w)
输入:patch grid 的时空尺寸 (t, h, w),单位是 patch 数量
1 | # 每个 patch 的行号 (H轴位置): shape [h, w] |
然后做 spatial merge 分组重排(为了使相邻的 merge unit 在序列中连续):
1 | hpos_ids = hpos_ids.reshape( |
原理:spatial_merge_size=2 意味着相邻的 2×2=4 个 patch 最终会被合并为一个 LLM token。将它们在序列中排列连续,便于后续 PatchMerger 操作。
接着将 t 帧的位置全部拼接:
1 | pos_ids = torch.stack([hpos_ids, wpos_ids], dim=-1).repeat(t, 1) |
第三步:从 cos/sin 缓存中查表,分别编码 H 和 W
1 | cos, sin = self.rotary_pos_emb.get_cos_sin(max_size) |
这是 2D-RoPE 的核心:
cos_combined的前rotary_dim//2维 = H 位置的旋转角余弦cos_combined的后rotary_dim//2维 = W 位置的旋转角余弦
即用 H 和 W 各自独立的位置频率拼接成完整的旋转嵌入。
第四步:按 spatial merge unit 分组重塑
1 | cos_combined = cos_combined.reshape( |
第五步:按窗口注意力顺序重排(get_rope_by_thw)
1 | window_index_thw, cu_seqlens_window_thw = self.get_window_index_thw(t, h, w) |
原理:Vision Transformer 对大多数 block 使用局部窗口注意力(仅少数 fullatt_block 使用全局注意力),get_window_index_thw 将属于同一个窗口的 patch group 排列到连续的序列位置,使窗口注意力只需要连续的 cu_seqlens 划分即可。
第六步:多图/多视频拼接,搬到 GPU
1 | for t, h, w in grid_thw: |
第七步:在每个 VisionBlock 中将 RoPE 应用到 Q/K
qwen2_5_vl.py:380-399,common.py:166-178
1 | # is_neox_style=True: 把 head_dim 拆成前后两半 |
这就是 2D 旋转矩阵:对特征对 (x[i], x[i + head_dim//2]) 施加角度为 θ_i(pos) 的平面旋转:
- 前
rotary_dim//4个特征对 → 按 H 轴位置旋转 - 后
rotary_dim//4个特征对 → 按 W 轴位置旋转 - 剩余
head_dim - rotary_dim个特征对 → 旋转角为 0(恒等变换)
整体流程图
1 | 输入: pixel_values + grid_thw[(t,h,w), ...] |
设计精髓
Qwen2.5-VL 使用 2D-RoPE(而非 1D 序列位置),将 H 和 W 两个空间轴的位置信息分别编码到 head 维度的不同频率组中,让 attention 的点积相似度自然地感知二维空间相对位置关系,同时通过 partial_rotary_factor=0.5 将 rotary 维度限制在 head_dim 的一半,节省了频率资源,使 H 和 W 各分配到 rotary_dim//4 个独立的旋转频率。