P18 · MRoPE 三维位置
Qwen-VL/Omni 的 thinker 用 (t,h,w) 三维旋转位置;M01 是“给定位置怎么旋转”,本题是“位置本身怎么算”——算错不会报错,只会悄悄变笨。
mine/p18_mrope_positions.py
验收 ./check.sh p18
提示 A-24 · 参考解 C-24 solutions/p18_mrope_positions.py
题面速览
- 在系统中的位置:model runner 在 prefill 时为每个请求算出
[3, T]的位置,以及一个 delta 供 decode 使用。
文本 n 个 token: 三轴都是 start + arange(n)
图像 (t, h, w) patch: LLM 网格 (t, h/m, w/m),m = spatial_merge(2)
token 顺序 t-major,然后 h,然后 w;位置 = (t_idx, h_idx, w_idx) + start
每段结束:start = 当前最大位置 + 1 # 注意:不是“已有 token 数”!
delta = (max_pos + 1) - T # decode 时位置 = 绝对下标 + delta(三轴相同)
- 例子:
text(2) | image(1,4,4) → 2×2 网格 | text(3)
t: 0 1 | 2 2 2 2 | 4 5 6
h: 0 1 | 2 2 3 3 | 4 5 6
w: 0 1 | 2 3 2 3 | 4 5 6
T = 9, max = 6, delta = 7 - 9 = -2,第 9 个 token decode 位置 = 7
图像占了 4 个 token,却只推进了 2 个位置——这就是 delta 为负的原因,也是“用 len(tokens) 当 decode 位置”的 bug 来源。
验收要点:纯文本退化为普通 RoPE · text+image+text 的精确位置与 delta · 多帧矩形网格 · 形状与 dtype。
直觉与类比
文本 token 一人一张票:坐“第 k 排第 k 座”(三轴相同),排号一个个往后推。一张图片像一个旅行团:6 个人包了一个 3 排 × 2 座的方块,票写“第 2+行 排、第 2+列 座”——整个团只用掉 3 排,而不是 6 排。后面的散客从团里最大排号 + 1 开始;如果按“已经进场 8 个人”发第 8 排的票,中间就空出好几排——模型会以为两段文字隔得很远。
t 是楼层、h 和 w 是房间行列号。一段视频是 3 层楼、每层 1 间房:坐电梯上 3 层(t 推进 3),房间号不变。出楼之后下一段文字从“三个坐标里最大的那个 + 1”继续——不管最大值是楼层(视频)还是列号(宽图)。
框架视角 · shape trace
以 [("text",2), ("image",(1,6,4)), ("text",…)…] 为例(共 5 段、T = 14):
| # | 量 | 形状 / dtype | 发生了什么 |
|---|---|---|---|
| 1 | segments | 5 段 | 按序列顺序,与 prompt 里的占位 token 一一对应 |
| 2 | 图像 (1, 6, 4) → (t, gh, gw) | (1, 3, 2) | gh = h // m,gw = w // m;t 方向不合并 |
| 3 | (t_idx, h_idx, w_idx) | 3 个 [6] |
t 最外、h 其次、w 最内:h = 0,0,1,1,2,2;w = 0,1,0,1,0,1 |
| 4 | 块 = stack + start | [3, 6] long |
start = 2,三行都加 2 |
| 5 | 新 start | int | 块的最大值(三轴一起取)+ 1 = 4 + 1 = 5 |
| 6 | 文本块 | [3, n] |
start + arange(n) 复制成三行 |
| 7 | positions | [3, 14] long |
所有块沿 dim=1 拼起来 |
| 8 | delta | int | (10 + 1) − 14 = −3,存进请求状态 |
| 9 | decode_position(i, delta) | (3,) long | i = 14 → [11, 11, 11],紧接 prefill 的最大位置 10 |
易错点
start += token 数:纯文本时正确,有图像就错。- h、w 互换 / 展开顺序错:方形网格看不出来,一定要用矩形网格(如 3×2)和 t > 1 自测;meshgrid 要
indexing="ij"。 - 只看某一个轴的 max:最大值可能来自 t(视频)、h(高图)或 w(宽图)。
- delta 少了 +1,或 decode 时直接用 index 当位置:第一个 decode 位置应该是 max+1。
- 空段(n = 0 或网格为 0)没有 max,不能用它更新 start;没有任何段时返回
[3, 0]的 long tensor。 - dtype:位置是整数下标,用
torch.long。
逐步图:例 1(图像在开头 + 矩形网格,spatial_merge=2)
LLM 网格 = (t=2, h=4//2=2, w=6//2=3) → 12 个 token,t-major,再 h,再 w。
t:000000111111
h:000111000111
w:012012012012
图像段最大位置 = 2(来自 w 轴) → 下一段 start = 3(不是 12!)
34(三轴相同)
T = 14,max = 4,delta = (4+1) − 14 = −9
decode_position(14, -9) = [5, 5, 5],decode_position(15, -9) = [6, 6, 6]
三级提示
L1I/O 示例与关键问题I/O 示例 + 关键问题 + 细节(轻度剧透)
例 1 见逐步图。
例 2:时间维很长的图像(视频帧):[("text",1), ("image",(4,2,2)), ("text",1)] → 网格 (4,1,1):
t: 0 | 1 2 3 4 | 5
h: 0 | 1 1 1 1 | 5
w: 0 | 1 1 1 1 | 5
T = 6, max = 5, delta = 0 # t 轴推进了 4,正好等于 token 数
delta 为 0 不代表“没有图像”,只代表位置跨度恰好等于 token 数。
例 3:两张图紧挨着:[("text",1), ("image",(1,4,4)), ("image",(1,2,4)), ("text",1)],merge=2:
t: 0 | 1 1 1 1 | 3 3 | 5
h: 0 | 1 1 2 2 | 3 3 | 5
w: 0 | 1 2 1 2 | 3 4 | 5 T=8, max=5, delta=-2
第二张图的 start = 3:第一张图最大位置是 2,不是“已有 token 数 5”。[("image",(1,2,4))],merge=1 → 网格 (1,2,4),8 个 token:t 全 0;h = 0 0 0 0 1 1 1 1;w = 0 1 2 3 0 1 2 3;T=8, max=3, delta=−4。
关键问题
- 一个 (t, h, w) 的图像段在 LLM 序列里占几个 token?h 不能被 spatial_merge 整除时会怎样?
- 第 k 个图像 token 的 (t_idx, h_idx, w_idx) 怎么由 k 算出来?
- 每段结束后 start 更新成什么?为什么不是“目前为止的 token 数”?
- 图像段的“最大位置”可能出现在哪个轴上?例 1 是哪个?
- delta 什么时候为负、为 0、为正?
- decode 时第 i 个 token 的位置为什么三轴相同?decode_position 返回什么形状和 dtype?
("text", 0)这种空段该如何处理?
L2思路与常见坑思路拆分与常见坑(中度剧透)
- 整体结构:维护
start = 0和 parts 列表;逐段生成[3, n_seg]块并 append;每段之后用这一块的最大值更新 start;最后torch.cat(parts, dim=1),再算 delta。 - 文本段:
idx = start + arange(n),再变成 3 行(expand(3, n)/repeat/stack([idx]*3))。expand 得到 view,后面 cat 会拷贝。 - 图像段:
gh, gw = h // m, w // m;torch.meshgrid(arange(t), arange(gh), arange(gw), indexing="ij")后各自 flatten——ij 索引 + 行主序 flatten 天然就是 t-major、再 h、再 w。或用 view/expand 广播。 - start 更新:
start = block.max() + 1(转 int);空块跳过。 - 空输入:segments 为空时返回
torch.zeros(3, 0, dtype=torch.long)与 delta=0。
L3详解:答案与逐段追踪详解:答案、逐步追踪、骨架提示(重度剧透)
关键问题答案
t · (h//m) · (w//m)个;不能整除时向下取整(真实模型里 h、w 总是 merge 的倍数)。- 令 hw = gh·gw:
t_idx = k // hw,h_idx = (k % hw) // gw,w_idx = k % gw。 - start = 所有已生成位置的最大值 + 1。图像的位置跨度 = max(t, gh, gw) 而非 token 数。
- 取决于 t、gh、gw 谁最大;例 1 中 gw=3 最大,最大位置 2 来自 w 轴。
- 有图像且 t·gh·gw > max(t,gh,gw) 时为负;只有文本,或图像退化成一条线(例 2)时为 0;不可能为正。
- decode 只产生文本 token,文本三轴相同;返回 (3,) long。
- 跳过,不更新 start(也不必加入 parts)。
逐段追踪(例 3 第一组):
| 段 | start(进入) | 网格/长度 | 块 (t / h / w) | 块 max | start(离开) |
|---|---|---|---|---|---|
| text 1 | 0 | 1 | 0/0/0 | 0 | 1 |
| image (1,4,4) | 1 | 1×2×2 | 1111 / 1122 / 1212 | 2 | 3 |
| image (1,2,4) | 3 | 1×1×2 | 33 / 33 / 34 | 4 | 5 |
| text 1 | 5 | 1 | 5/5/5 | 5 | 6 |
T = 8,max = 5,delta = 6 − 8 = −2;下一个 decode token(index 8)位置 = 6。
自检:例 1(矩形 + 多帧)检查 h/w 没有互换;例 3 检查第二张图从 3 开始;pos.dtype == torch.long、pos.shape == (3, T);任意输入下 decode_position(T, delta) 的值应等于 pos.max() + 1。
面试官追问 & 对照源码
- Qwen2.5-Omni 的 TMRoPE 如何按秒交错音频和视频?
对照源码:model_executor/models/qwen2_5_omni/qwen2_5_omni.py:412-576 · model_executor/models/qwen3_omni/qwen3_omni_moe_thinker.py:1694-1785