nano-vllm-omni · 提示 ← P14P16 →
#24 · Track D · 模型胶水(纯函数) · 书 p.102

P18 · MRoPE 三维位置

Qwen-VL/Omni 的 thinker 用 (t,h,w) 三维旋转位置;M01 是“给定位置怎么旋转”,本题是“位置本身怎么算”——算错不会报错,只会悄悄变笨。

⏱ 40 min 难度 中 实现 mine/p18_mrope_positions.py 验收 ./check.sh p18 提示 A-24 · 参考解 C-24 solutions/p18_mrope_positions.py

题面速览

文本 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

易错点

逐步图:例 1(图像在开头 + 矩形网格,spatial_merge=2)

▶ segments = [("image", (2, 4, 6)), ("text", 2)]
1. 图像网格

LLM 网格 = (t=2, h=4//2=2, w=6//2=3) → 12 个 token,t-major,再 h,再 w。

2. 三行位置(start = 0)

t:000000111111

h:000111000111

w:012012012012

3. 下一段的 start

图像段最大位置 = 2(来自 w 轴) → 下一段 start = 3(不是 12!)

4. 文本段

34(三轴相同)

5. delta 与 decode

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。

关键问题

  1. 一个 (t, h, w) 的图像段在 LLM 序列里占几个 token?h 不能被 spatial_merge 整除时会怎样?
  2. 第 k 个图像 token 的 (t_idx, h_idx, w_idx) 怎么由 k 算出来?
  3. 每段结束后 start 更新成什么?为什么不是“目前为止的 token 数”?
  4. 图像段的“最大位置”可能出现在哪个轴上?例 1 是哪个?
  5. delta 什么时候为负、为 0、为正?
  6. decode 时第 i 个 token 的位置为什么三轴相同?decode_position 返回什么形状和 dtype?
  7. ("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详解:答案与逐段追踪详解:答案、逐步追踪、骨架提示(重度剧透)
关键问题答案
  1. t · (h//m) · (w//m) 个;不能整除时向下取整(真实模型里 h、w 总是 merge 的倍数)。
  2. 令 hw = gh·gw:t_idx = k // hw,h_idx = (k % hw) // gw,w_idx = k % gw。
  3. start = 所有已生成位置的最大值 + 1。图像的位置跨度 = max(t, gh, gw) 而非 token 数。
  4. 取决于 t、gh、gw 谁最大;例 1 中 gw=3 最大,最大位置 2 来自 w 轴。
  5. 有图像且 t·gh·gw > max(t,gh,gw) 时为负;只有文本,或图像退化成一条线(例 2)时为 0;不可能为正。
  6. decode 只产生文本 token,文本三轴相同;返回 (3,) long。
  7. 跳过,不更新 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。

面试官追问 & 对照源码

对照源码:model_executor/models/qwen2_5_omni/qwen2_5_omni.py:412-576 · model_executor/models/qwen3_omni/qwen3_omni_moe_thinker.py:1694-1785