7. 视觉路径:一张图怎么变成至多 1024 个视觉 token
7.1 bllm 的 token embedding,换成图像 patch
bllm 2.7 节做的事是:把每个 token id 查表得到一个 1024 维向量,序列就变成了一个矩阵,送进 Transformer。视觉分支做的是同一件事,只是"词表"换成了"图像块":把图切成 \(14\times14\) 的小块,每块展平后线性投影成一个向量,然后把这些向量当 token 送进一个 32 层的 Transformer。
整条路径(以 \(1008\times1008\) 的图为例):
图像 1008 x 1008 patch 14 网格 72 x 72 5184 个 patch x 588 维 ViT 32 层 图内交互(双向) 3x3 unshuffle 网格 24 x 24 576 个向量 x 9216 2 层 MLP 576 x 5120 = 576 个视觉位置进主干
最后一步没什么花样:报告只说"把得到的视觉嵌入插到输入序列里对应的 image-token 位置上",config 里也只有 image_token_id,没有行标记、也没有起止标记(有些多模态模型会加,这套没有)。反证很直接:报告说这套编码"支持到约 \(1344\times1344\)",而 \(1344^2\) 经 \(3\times3\) 合并后正好是 1024 个视觉 token,等于 max_image_tokens;若再插 34 个标记,这个上限立刻自相矛盾。
7.2 预算公式:上限是 1024 个视觉 token
设预处理后的像素高宽为 \(H\times W\),patch 边长 \(p=14\),空间合并(pixel-unshuffle)因子 \(r=3\)。合并后的网格是
\[ h'=\left\lceil\frac{H}{p\,r}\right\rceil=\left\lceil\frac{H}{42}\right\rceil, \] \[ w'=\left\lceil\frac{W}{p\,r}\right\rceil=\left\lceil\frac{W}{42}\right\rceil, \]视觉 token 数就是 \(N_{\text{vis}}=h'w'\)。config 里的上限叫 max_image_tokens = 1024,卡的就是这个 \(N_{\text{vis}}\):
配套的还有两端的尺寸约束:min_pixels = 295936 = 544^2,以及报告写的"按比例缩放,把分辨率限制在 \(544\times544\) 到 \(1344\times1344\) 之间"。两条约束合起来才决定了预处理后的画布。
算预算时不要自己发明"span"这个量。报告与 config 只约束视觉 token 数 \(h'w'\);\(1344^2\to 1024\) 正好顶格,这就是"支持到约 \(1344\times1344\)"的由来。
7.3 三个算例:尺寸是怎么被卡住的
例一(顶格的那一档):\(1344\times1344\) 时 patch 网格 \(96\times96\),合并后 \(h'=w'=32\),\(N_{\text{vis}}=1024\)——正好等于上限。这就是报告说"支持到约 \(1344\times1344\)"的意思:再多一个 patch 就超预算。
例二(非方形):\(1920\times1080\) 先按比例缩到最长边 1344,得 \(1344\times756\)(比例不变)。\(756/14=54\)、\(1344/14=96\),合并后 \(32\times18\),\(N_{\text{vis}}=576\),远在预算内——非方形图省下的是短边。
例三(太小):原图面积小于 min_pixels\(=544^2=295{,}936\) 时先等比放大到 544 这一档,否则一张小图切不出足够的 patch,视觉 token 太少不足以表达内容。放大后再走上面的对齐与预算检查。
画布 网格 合并 token 1008x1008 72x72 24x24 576 1344x1344 96x96 32x32 1024 1344x756 96x54 32x18 576
这里的取整是 \(\lceil H/42\rceil\lceil W/42\rceil\);真实预处理还要处理 patch 网格不能被 3 整除时的补边,具体顺序以 deepseek_v41_vision 的 processor 为准。上面三个画布是按报告的两条约束算出来的,不是从报告里抄的具体落点。
7.4 patch embedding:为什么是线性投影
切块之后,一张 \(1008\times1008\) 的 RGB 图变成 \(72^2=5184\) 个块,每块 \(3\times14\times14=588\) 个数。把每块展平成一个 588 维向量,堆成矩阵 \(U\in\mathbb{R}^{5184\times588}\),然后乘一个共享的权重:
\[ X^{(0)}=UW_p+b_p, \qquad W_p\in\mathbb{R}^{588\times1024}, \] \[ b_p\in\mathbb{R}^{1024}. \]所有块共用同一组权重——这正是卷积的"权重共享",而一个核大小 14、步长 14 的卷积在原图上的效果与这个线性投影完全等价。报告的取舍是显式写成线性投影,理由是确保与 Muon 优化器兼容:报告说,卷积核的形状与实现细节让 Muon 不好处理,摊平成矩阵就统一了。
顺带说一句归一化:常见做法是先把像素按 \(x=2I/255-1\) 映射到 \([-1,1]\),与 bllm 2.7 节对 token 做 embedding 之前的那一步是同一类操作,只是这里的"字"是像素。报告没有展开这一步,具体用哪组均值方差以 processor 为准。
7.5 ViT:32 层双向注意力
接下来 32 层视觉 Transformer,输入输出都是 \(5184\times1024\)。每层和 bllm 4.5 节的块同构:RMSNorm → 注意力 → 残差,RMSNorm → 前馈 → 残差,最后整个塔末再归一化一次。差别有三处:
- 双向,不是因果。图像没有"未来",所以不掩码;这对应 bllm 第 1 章讲的 encoder 侧用法。
- 16 个头、每头 64 维。QKV 一个线性层出 \(3\times1024=3072\) 维,再切成 16×64。
- 2D RoPE。位置编码作用在 Q、K 上(不作用在 V),用的是 patch 的 \((i,j)\) 二维坐标,而不是一维的 \(t\)。所以"上面的块"和"左边的块"是不同的相对方向——这是图像里必须区分的信息。
前馈是 SwiGLU:先投到 \(5632=2\times2816\) 维并拆成两支,一支过 SiLU 当门,与另一支逐元素相乘,再投回 1024。\(2\times2816\) 这个"两倍"就是 bllm 4.3 节 SwiGLU 的门控结构,不重复推。
7.6 3×3 pixel-unshuffle:把 9 个 patch 拼成一个 token
32 层之后,每个 patch 都已经"看过"整张图(双向注意力),所以相邻 9 个 patch 的表示是高度冗余的。pixel-unshuffle 就是把 \(3\times3\) 邻域的 9 个 1024 维向量沿通道维拼成一个 9216 维向量(对随后的线性投影来说,等价于首尾相接):
\[ 24\times24\ \text{网格},\quad 576\ \text{个位置}, \] \[ 576\times(9\times1024)=576\times9216 . \]这次拼接没有信息损失(9216 维装下了 9 个 1024 维向量),真正的压缩发生在下一步的两层 MLP:
\[ 576\times9216\ \longrightarrow\ 576\times5120 . \]也就是 \(9216\to5120\),把 9 个 patch 的信息压进一个主干宽度的向量。整个流程的压缩比是:
\[ \frac{5184}{576}=9, \qquad \frac{9\times1024}{5120}=1.8 , \]第一个比值是位置数(9 个 patch 合成 1 个位置),第二个是每个位置的宽度(9216 维压到主干宽度 5120)。位置数降 9 倍才是主要收益:\(1008^2\) 的图从 5184 个 patch 变成 576 个视觉位置。注意 5184 是 ViT 内部的计算规模,576 才是语言主干看到的长度,两者不在同一个阶段,不要混着比较。
为什么敢压 9 倍?因为 ViT 的 32 层已经把图内交互做完了。若先把图切成 9 倍少的 token 再做注意力,全局注意力仍然能连到远处,但细节在压缩这一步就已经丢了:细粒度定位、小物体、文字这类信息,没法在低分辨率网格上补回来。顺序的准确含义是:先在高分辨率网格上做交互,再降采样进语言主干,而不是"先压缩就看不到远处"。
7.7 多模态的 MoE 负载均衡
第 6.2 节说 MoE 是 384 个路由专家选 6 个(外加 1 个共享专家)。多模态带来一个新问题:图像 token 和文本 token 的表示分布不同,会各自偏好不同的专家;如果只维护一份全局的逐专家偏置,两种模态之间的负载差异会被平均掉,各自内部的不均衡反而看不出来。
报告的做法是为文本与图像各维护一组逐专家校正偏置:路由时按 token 所属模态取对应的偏置做选择,但权重仍用原始路由分数(偏置只影响"选谁",不影响"选上之后给多少权重");每个训练步按各自模态的负载独立更新两组偏置。这和 bllm 里无辅助损失的负载均衡是同一个思路,只是把"一份偏置"换成了"每个模态一份"。
7.8 容易算错的地方
- 给视觉预算加"标记开销"。报告与 config 只约束视觉 token 数 \(h'w'\le1024\);没有行标记、起止标记这类额外位置。
- 以为 1344×1344 进不去。它正好是 \(32\times32=1024\),顶格但合法——这正是"支持约 1344²"的来源。
- 把 unshuffle 当成"信息压缩"。9 个向量拼成 9216 维时没有损失,压缩发生在 9216→5120 的投影里。
- 忘了两端约束。上限 1024 个 token,下限
min_pixels\(=544^2\);小图要先放大,大图(或超宽图)要先等比缩小到 1344 以内。 - 把视觉注意力也当因果。ViT 内是双向的,掩码只在语言主干里。