任意分辨率视觉:Patch-n'-Pack 与 NaFlex
真实图像不是 224x224 的方块。一张收据是 9:16,一张图表是 16:9,一张医学扫描可能是 4096x4096,一张手机截图是 9:19.5。2024 年之前的 VLM 答案——把一切都缩成固定方块——丢掉了让 OCR、文档理解和高分辨率场景解析得以运作的信号。NaViT(Google,2023)证明了你可以用块对角掩码把可变分辨率的 patch 打包进单个 transformer batch。Qwen2-VL 的 M-RoPE(2024)彻底扔掉了绝对位置表。LLaVA-NeXT 的 AnyRes 把高分辨率图切成基础图 + 子图。SigLIP 2 的 NaFlex 变体(2025)如今是那些想用单个 checkpoint 服务所有长宽比的开放 VLM 的默认编码器。本节课从头到尾实现 patch-n'-pack。
类型: Build 语言: Python(标准库,patch 打包器 + 块对角掩码) 前置要求: Phase 12 · 01(ViT patch)、Phase 12 · 05(LLaVA) 预计时间: ~120 分钟
学习目标
- 把一批可变分辨率图像的 patch 打包进一条序列,并构建块对角注意力掩码。
- 为给定任务在 AnyRes 切块(LLaVA-NeXT)、NaFlex(SigLIP 2)、M-RoPE(Qwen2-VL)之间做选择。
- 在不缩放的前提下计算 OCR、图表和摄影图的 token 预算。
- 说出方块缩放的三种失败模式:被挤扁的文字、被裁掉的内容、浪费在填充上的 token。
问题背景
Transformer 期望一条序列。一个 batch 是一摞相同长度的序列。如果你的图都是 224x224,那每次都得到 196 个 patch token,不用填充,搞定。在 224 上训,在 224 上推,从此再不用想分辨率的事。
可世界不配合。文档是竖版(8.5x11 英寸,约 2:3)。图表截图是横版(16:9)。收据又高又窄(1:3)。医学影像出货时是 2048x2048 或更大。手机设备截图是 1170x2532(0.46:1)。
2024 年之前的三个选项,以及它们各自为什么失败:
- 缩成固定方块(224x224 或 336x336)。挤压会扭曲文字和人脸。降采样会毁掉图表标签和 OCR 内容。这是 LLaVA-1.5 之前的标准做法。
- 裁成固定长宽比。你扔掉了图像大部分,而选裁剪位置本身又是个视觉问题。
- 填充到最长边。修好了扭曲,但竖版图有 50%+ 的 token 浪费在填充上。所有这些填充 token 还要付平方级的注意力成本。
2024-2025 的答案是:让 transformer 在图像原生分辨率下吃 patch,并想清楚怎么把一个异质 batch 打包进一条序列而不浪费算力。
核心概念
NaViT 与 patch-n'-pack
NaViT(Dehghani 等人,2023)是证明这套能大规模运作的论文。想法很机械:
- 对 batch 里每张图,在选定的 patch 大小(比如 14)下算出它的原生 patch 网格。
- 把每张图的 patch 摊平成它自己的变长序列。
- 把所有图的 patch 拼成 batch 的一条长序列。
- 构建块对角注意力掩码,让图 A 的 patch 只在图 A 内部相互关注。
- 携带每个 patch 的位置信息(2D RoPE 或分数位置嵌入)。
三张图——336x336(576 token)、224x224(256 token)、448x336(768 token)——的一个 batch 变成一条 1600-token 序列,配一个 1600x1600 的块对角掩码。没有填充。没有浪费算力。transformer 处理任意长宽比。
NaViT 还引入了训练时的分数 patch 丢弃——在整个 batch 上随机丢 50% 的 patch——既正则化又加速训练。SigLIP 2 继承了这个。
AnyRes(LLaVA-NeXT)
LLaVA-NeXT 的 AnyRes 是务实的替代方案。给定一张高分辨率图和一个固定编码器(336 下的 CLIP 或 SigLIP),把图切块:
- 从一组预定义布局——(1x1)、(1x2)、(2x1)、(1x3)、(3x1)、(2x2) 等——里挑一个最契合图像长宽比的网格布局。
- 把整张图切进网格;每个 tile 成为一个 336x336 裁块。
- 还产出一张缩略图:整张图缩到 336x336 作为全局上下文 token。
- 让每个 tile 过那个冻结的 336 编码器。拼接 tile token + 缩略图 token。
一张 672x672 的图用 2x2 网格加缩略图:4 * 576 + 576 = 2880 个视觉 token。贵但有效——LLM 既看到局部细节又看到全局上下文。
当你的编码器被冻结且只支持一种分辨率时,AnyRes 是首选路线。它会让大图的 token 数爆炸(一张 1344x1344 的图用 4x4 网格是 9216 + 576 ≈ 9800 个 token,能塞满一个 8k LLM 上下文的大部分)。
M-RoPE(Qwen2-VL)
Qwen2-VL 引入了多模态旋转位置嵌入。不用 NaViT 的分数位置或 AnyRes 的切块加缩略图,每个 patch 携带一个 3D 位置(时间、高、宽)。query/key 旋转处理任意 H、W 和时间长度。
M-RoPE 不用重训就自带原生动态分辨率。推理时你喂任意 HxW 图,patch 嵌入器产出 H/14 x W/14 个 token,每个 token 拿到它的 (t=0, r=行, c=列) 位置,RoPE 用正确的频率旋转注意力,搞定。Qwen2.5-VL 和 Qwen3-VL 延续这个。InternVL3 的 V2PE 是同样的想法,每种模态用可变编码。
与 AnyRes 不同,M-RoPE 在原生分辨率下是 O(H x W / P^2) 个 token——没有切块的乘性开销。与 NaViT 不同,它仍然期望每次前向一张图。跨分辨率批处理仍需在它之上叠 patch-n'-pack。
NaFlex(SigLIP 2)
NaFlex 是 SigLIP 2 checkpoint 的原生灵活模式。单个模型在推理时服务多种序列长度(256、729、1024 token)。内部它在训练时用 NaViT 式的 patch-n'-pack,每个 patch 用绝对分数位置。卖点是:一个 checkpoint,推理时按任务挑你的 token 预算。
语义任务(分类、检索)用 256 token。OCR 或图表理解用 1024 token。不用重训。
打包掩码
块对角掩码是大多数实现栽跟头的地方。对一条长度为 N_total、覆盖图像 i=0..B-1(各自长度 n_i)的打包序列,形状为 (N_total, N_total) 的掩码 M:当两个索引都落在同一张图的块里时为 1,否则为 0。你可以从一个累计长度列表构建它:
offsets = [0, n_0, n_0+n_1, ..., N_total]
M[i, j] = 1 iff there exists b where offsets[b] <= i < offsets[b+1] and offsets[b] <= j < offsets[b+1]在 PyTorch 里用 torch.block_diag 或一次显式 gather 就是一行。FlashAttention 的变长路径(cu_seqlens)完全跳过掩码,直接用累计长度张量在各序列内部做注意力——对典型 batch 比稠密掩码快约 10 倍。
token 预算
按任务挑你的策略:
- OCR / 文档:1024-4096 token。1024 下的 SigLIP 2 NaFlex,或 AnyRes 3x3 + 缩略图。
- 图表与 UI:384-448 原生下 729-1024 token。带最大像素上限的 Qwen2.5-VL 动态分辨率。
- 自然照片:256-576 token 就够。下游 LLM 看得够多。把 token 花在内容密度高的地方。
- 视频:空间池化后每帧 64-128 token,2-8 FPS。第 12.17 课讲这个。
2026 年的生产法则:为每个任务挑一个最大像素上限,在该上限内以原生长宽比编码,打包 batch,跳过填充。Qwen2.5-VL 暴露了 min_pixels 和 max_pixels,正是这根旋钮。
mm-patch-n-pack实际使用
code/main.py 为一个异质图像 batch(整数像素坐标)实现了 patch-n'-pack。它:
- 接收一组 (H, W) 图像尺寸。
- 在 patch 大小 14 下算出每张图的 patch 序列长度。
- 把它们打包进一条总长
sum(n_i)的序列。 - 构建块对角注意力掩码(为清晰起见用稠密版)。
- 把打包成本与方块缩放、AnyRes 切块作对比。
- 为一个混合 batch(收据、图表、截图、照片)打印 token 预算表。
跑一下。掉出来的那些数字,就是每个 2026 年开放 VLM 都用 patch-n'-pack 的原因。
拿去用
本节课产出 outputs/skill-resolution-budget-planner.md。给定一个混合长宽比的工作负载(OCR、图表、照片、视频帧)和一个总 token 预算,它挑出正确的策略(NaFlex、AnyRes、M-RoPE 或固定方块),并产出每请求的配置。当你为某个产品给 VLM 定规格时就用这个 skill——它能避免那种悄无声息、压垮延迟预算的 10 倍 token 暴涨。
练习
一张收据是 600x1500(1:2.5)。在 patch 大小 14 下有多少原生分辨率 token?方块缩放到 336 后有多少?实践中哪个损失的 OCR 准确率更多?
为一个长度分别为 256、576、729、1024 的四图 batch 构建块对角掩码。验证注意力矩阵是 2585x2585,且恰好有
256^2 + 576^2 + 729^2 + 1024^2个非零项。对一张 1792x896、patch 14 的图,对比:(a) 缩成方块 336 再编码,(b) AnyRes 2x1 + 缩略图,(c) 原生下的 M-RoPE。哪个用的 token 最少?哪个保留的细节最多?
实现分数 patch 丢弃:给定一条打包序列,均匀随机丢 50% 的 token,并相应更新块对角掩码。测一下掩码稀疏度的变化。
读 Qwen2-VL 论文(arXiv:2409.12191)第 3.2 节。用两句话描述
min_pixels和max_pixels控制什么,以及为什么两个边界都重要。
关键术语
| 术语 | 大家怎么说 | 它实际指什么 |
|---|---|---|
| Patch-n'-pack | "NaViT 式打包" | 把来自不同图像的变长 patch 序列拼进一个 batch 维度 |
| 块对角掩码 | "打包掩码" | 把每张图的 patch 限制为只关注自己、不关注打包里的邻居的注意力掩码 |
| AnyRes | "LLaVA-NeXT 切块" | 把高分辨率图切成固定大小 tile 的网格外加一张全局缩略图;用固定编码器编码每个 tile |
| NaFlex | "SigLIP 2 原生灵活" | 单个 SigLIP 2 checkpoint,推理时无需重训即可服务 256/729/1024-token 预算 |
| M-RoPE | "多模态 RoPE" | 3D 旋转位置编码(时间、行、列),无需位置表即可处理任意 H、W、T |
| cu_seqlens | "FlashAttention 打包" | FlashAttention 变长路径用的累计长度张量,替代稠密块对角掩码 |
| min_pixels / max_pixels | "分辨率边界" | Qwen2.5-VL 的每请求旋钮,对极小或极大输入封顶 token 数 |
| 视觉 token 预算 | "每张图多少 token" | 每张图产出的 patch token 粗略数;决定 LLM 的 prompt 预算和注意力成本 |
延伸阅读
- Dehghani et al. — Patch n' Pack: NaViT (arXiv:2307.06304)
- Wang et al. — Qwen2-VL (arXiv:2409.12191)
- Laurençon et al. — What matters when building vision-language models? (Idefics2, arXiv:2405.02246)
- Tschannen et al. — SigLIP 2 (arXiv:2502.14786)
- Qwen Team — Qwen2.5-VL Technical Report (arXiv:2502.13923)