架构

Qwen3-VL 是 Qwen 系列迄今最强的视觉语言模型,引入了三大关键架构升级:增强的 Interleaved-MRoPE、DeepStack 多层特征融合,以及文本时间戳对齐机制。


一、整体结构:三大组件

Qwen3-VL 是一个多模态视觉语言模型,将视觉编码器与大语言模型主干相结合,用于处理视觉和文本双模态输入。架构由三个主要部分组成:视觉编码器(将图像/视频帧转换为视觉 token)、多模态融合层(将视觉特征投影到语言模型的嵌入空间),以及语言模型主干(基于多模态输入生成文本)。


二、视觉编码器(Vision Encoder / ViT)

这是处理图像与视频帧的核心模块:

Patch 嵌入。 与 Qwen2.5-VL 使用 14×14 像素的 patch 不同,Qwen3-VL 将 patch 大小升级为 16×16 像素。这改变了压缩比,影响从每张图像生成的视觉 token 数量。

  1. Qwen3-VL 采用动态分辨率机制,patch 数量随图像大小变化
  2. Qwen3-VL 的 merge_size 默认为 2,即 ViT 输出的 patch 在送入 LLM 前会做 2×2 的空间合并
图像像素数 H × W
  ÷ (16 × 16)   → ViT patch 数
  ÷ (2 × 2)     → 最终 LLM visual token 数
= H × W ÷ 1024

每 1024 个像素对应 1 个最终 visual token

ViT 内部结构。 ViT 架构采用 SwiGLU 激活函数和 RMSNorm 归一化,与 Qwen LLM 主干的结构对齐,并通过 Window Attention(窗口注意力)来加速训练和推理。

DeepStack 多层特征融合。 这是 Qwen3-VL 相对于前代最重要的视觉侧创新。DeepStack 融合来自 ViT 多个层级的特征,用于捕捉细粒度的视觉细节,并强化图像与文本的对齐。它不仅使用最后一层的输出,而是将浅层(低级细节)与深层(高级语义)的特征进行联合融合,从而让模型在细粒度视觉理解上更加精准。


三、多模态融合层(MLP Projector)

视觉编码器的输出维度与 LLM 嵌入空间维度不同,因此需要一个可学习的 MLP 投影层来完成桥接:

  • 将 ViT 输出的视觉特征投影到 LLM 文本嵌入的维度空间
  • 视觉 token 通过特殊标记(<|vision_start|><|image_pad|><|video_pad|><|vision_end|>)插入文本 token 序列的对应位置
  • 之后视觉 token 与文本 token 共同进入 LLM 主干,通过注意力机制实现跨模态交互

四、语言模型主干(LLM Backbone)

2B 参数,Dense 架构。 Qwen3-VL-2B 采用全密集(Dense)架构,所有参数对每个 token 都被激活,适合边缘设备和移动端部署,具有更低的推理延迟。

注意力机制:GQA(分组查询注意力)+ SwiGLU FFN。 这是当前高效大模型的主流设计,在保持性能的同时降低 KV Cache 的显存占用。

超长上下文支持。 所有模型原生支持 256K token 的上下文,可扩展至 100 万 token,可以输入数百页技术文档、整本教科书,甚至长达两小时的视频,模型能精确记忆并检索细节。


五、三大架构创新详解

接下来用图解说明三大创新中最核心的位置编码机制:

① Interleaved-MRoPE(交错式多分辨率旋转位置编码)

Qwen3-VL 使用 Interleaved-MRoPE,这是一种捕获时间、高度、宽度三个维度的 3D 位置编码方案,mrope_section 配置定义了维度分配:时间维度 24 维,高度和宽度各 20 维。

与传统 RoPE 相比,这种编码让模型对每一个 token(无论是图像 patch 还是视频帧)都能精确感知”它在第几帧的哪个位置”,从而大幅提升空间与时序推理能力。

② DeepStack(多层 ViT 特征融合)

DeepStack 有效利用了 ViT 多个层级的特征,从而收紧了视觉与语言的对齐。传统做法只取 ViT 最后一层的输出,而 DeepStack 同时融合浅层(捕捉边缘、纹理等低级细节)和深层(捕捉语义、物体类别等高级信息)的特征,让图文对齐更加精细。

③ Text-Timestamp 对齐

Qwen3-VL 引入 Text-Timestamp 对齐机制,超越了早期版本的 T-RoPE 方案。该机制将生成的文本与视频中的特定时间戳关联,实现精确的事件定位(如”在 1:23,人物进入画面”),支持视频问答中的时序定位。


六、2B 版本的规格总结

组件规格
总参数量2B(Dense,全参激活)
Patch 大小16×16 像素
上下文长度原生 256K,可扩展至 1M(YaRN)
位置编码Interleaved-MRoPE(3D)
视觉特征融合DeepStack(多层 ViT)
FFN 激活函数SwiGLU
归一化RMSNorm
版本Instruct / Thinking 两种

这些组件协同工作,实现了对图像、视频和文本的全面视觉语言理解。模块化架构支持从边缘设备(2B 模型)到云基础设施(235B MoE 模型)的灵活部署。

Visionzip 复现

架构

LLaVA-NeXT 示例:原始 2880 tokens → VisionZip → 160 tokens(5.6%),保留 ~95% 性能 预填充加速 8×,整体推理加速 2-3×,13B 模型可比 7B 跑得更快

一、核心观察:视觉 token 大量冗余

VisionZip 的出发点是一个关键观察:CLIP 和 SigLIP 等视觉编码器产生的 token 中,注意力只集中在少数几个 token 上,而大多数 token 的注意力权重接近于零,说明存在严重冗余。

为什么会这样?原因是 softmax 的”赢者通吃”效应:softmax 函数会使低注意力区域的梯度几乎为零,而高注意力区域愈发突出,最终把信息浓缩到极少数 token 里。所以并不是所有 token 都有价值,VisionZip 就是把有价值的挑出来。


二、算法第一步:主导 Token 选择(Dominant Token Selection)

这一步要回答的问题是:哪些 token 最重要?

VisionZip 使用的判断标准是 ViT 倒数第二层(SELECT_LAYER)的注意力权重来给每个 token 打分。根据模型有无 CLS token,分两种情况:

情况 A:有 CLS token(如 CLIP)

CLS token 会聚合整张图像的信息,所以用 CLS token 对各个 patch token 的注意力分数来衡量每个 token 的重要性——被 CLS 关注越多的 token,包含的信息越有代表性。

# attn shape: [B, H, SeqLen, SeqLen]
# CLS 对其他 token 的注意力,对多头取均值
attn_rec = attn[:, :, cls_idx, cls_idx+1:].mean(dim=1)  # [B, N]
# 取 top-K
_, topk_idx = attn_rec.topk(K, dim=1)
# 主导 token = CLS + top-K
dominant_idx = cat([cls_idx], topk_idx + 1)
dominant_tokens = hidden_states[:, dominant_idx, :]

情况 B:无 CLS token(如 SigLIP / Qwen3-VL 的 ViT)

对于没有 CLS token 的编码器(如 SigLIP),计算每个 token 从序列中所有其他 token 处收到的平均注意力——被其他 token 集体”关注”越多的 token,重要性越高。

# attn_avg: [B, N, N],对多头均值
attn_avg = attn.mean(dim=1)  # [B, N, N]
# 列求和 = 每个 token 被其他 token 关注的总量
token_scores = attn_avg.sum(dim=1)  # [B, N]  (对"被关注"维度求和)
_, topk_idx = token_scores.topk(K, dim=1)
dominant_tokens = hidden_states[:, topk_idx, :]

这两种方式都是与文本无关(text-agnostic) 的打分——不需要知道问题是什么,只看图像自身的注意力结构。


三、算法第二步:上下文 Token 合并(Contextual Token Merging)

主导 token 虽然覆盖了图像的主要信息,但可能遗漏某些小区域的细节(比如角落里的文字)。上下文合并就是为了”打捞”这些被丢弃的 token 里残余的信息。

在自注意力计算中,K(key)向量本身就已经浓缩了每个 token 的内容摘要,因此用 K 向量的点积来度量 token 之间的相似度。具体做法是:先把剩余 token 均匀分成 target 和 merge 两组,然后找到与每个 merge token 最相似的 target token,将它们聚合在一起,最终产生 M 个上下文 token。

# remaining: [B, R, D]  (R = N - K 个剩余 token)
# 均匀分两组
targets, merge_tokens = uniform_split(remaining, M)
 
# 用 key 向量算相似度
sim = bmm(merge_tokens.K, targets.K.transpose(1, 2))  # [B, R/2, M]
 
# 每个 merge token 找最相似的 target,加权聚合
assign_idx = sim.argmax(dim=-1)   # [B, R/2]
for each merge token → 累加到对应 target
 
# 最终 target 做均值池化 → M 个上下文 token

为什么用 key 而不是 value 或 hidden state? key 是在做注意力时”概括自身内容”的表示,语义密度高,用它来比较相似度比 value(输出表示)或原始 hidden state 更准确。


四、最终输出:K + M 个 token

两步完成后,把主导 token 和上下文 token 拼接起来:

最终视觉 token = [dominant_tokens (K个)] + [contextual_tokens (M个)]

K + M 个 token 经过 MLP Projector 后送入 LLM,位置数量都比原来少了几倍到几十倍。


五、可选的投影层微调(Efficient Tuning)

token 数量减少后,视觉空间和 LLM 空间之间会出现轻微的分布偏移。为了弥合这一差距,只需对 MLP Projector 做少量微调,其他所有参数保持冻结,只需 1/10 的 LLaVA-1.5 数据集,在 8 张 A800 上约 30 分钟即可完成。这一步是可选的,免训练模式下已有 ~95% 性能,微调后接近全量。


六、为什么”与文本无关”反而更好?

直觉上你可能觉得”根据问题来选 token”应该更好,但 VisionZip 给出了反驳:

text-agnostic 方法在真实场景下表现更好,尤其是在多轮对话中——因为不同轮次的问题对应不同区域,如果第一轮就根据文本剪掉了某些区域的 token,第二轮再问这些区域的问题时就无从回答了。而 text-agnostic 方法保留的是图像里”信息量大”的区域,不偏向任何特定问题。


七、VisionZip 在 VLM 处理流程中的插入位置

图像
  → ViT 前向(拿到所有层输出 + 注意力权重)
  → 在 ViT 最后输出处,用 VisionZip 压缩:N → K+M
  → MLP Projector(可选微调以重新对齐)
  → LLM(只处理 K+M 个视觉 token + 文本 token)

关键点是压缩发生在 ViT 和 LLM 之间,不修改 LLM 本身,所以对 LLM 的任何优化(量化、KV Cache、FlashAttention 等)都可以继续叠加使用。


八、参数选择直觉

论文实验表明,仅保留原始 token 的 10%,免训练模式就能达到 95% 的性能;经过投影层微调后,同样 10% 的 token 可达到 95.2% 以上的性能,超越此前最好的方法 SparseVLM 约 9 个百分点。

实践中的经验规律:

  • K(主导 token) 通常占原始数量的 8–15%,承担主要性能
  • M(上下文 token) 是 K 的 10–30%,用于兜底细节
  • MMBench 这类综合评测中,64 dominant + 16 contextual 通常是效果/速度的好平衡点

复现逻辑

VisionZip 适配 Qwen3-VL 的难点在哪?

VisionZip 的框架包含两步:先根据 ViT 最后一个全局注意力层的注意力得分选出”主导 token”(dominant tokens),再用余弦相似度将剩余 token 合并为”上下文 token”,两者拼接后送入 LLM。 Wei Meng

Qwen3-VL 相比 LLaVA/Qwen2-VL 有三处关键差异需要专门处理:

  1. Window Attention 问题:Qwen3-VL ViT 大部分层用窗口注意力,attn_weights 只在窗口内有意义,必须在全局注意力层(第 31 层) 上取分数
  2. attn_implementation 问题:用 flash_attention_2 时不返回 attn_weights,必须切换为 sdpaeager
  3. DeepStack 问题:Qwen3-VL 有多个 MLP Merger,token 压缩需在 ViT 的 forward 里最终合并之前完成,压缩后还要同步更新 image_grid_thw 以保持 MRoPE 位置 ID 的一致性

问题一:Window Attention 的注意力权重不可用

为什么普通做法会出问题

VisionZip 的核心是从 ViT 的某一层拿注意力权重来给 token 打重要性分数。在 LLaVA 那类模型里,ViT 全程用的是全局注意力(所有 patch 两两交互),所以任何一层的注意力权重都能反映”哪些 token 被整张图片关注”。

但 Qwen3-VL 的 ViT 是混合注意力结构:第 0–6、8–14、16–22、24–30 层用窗口注意力,只有第 7、15、23、31 层是全局注意力。窗口注意力只在局部小块内计算,一个 patch 只能看到同窗口内的邻居,看不到整张图的其他区域。

如果你在窗口注意力层取 attn_weights,拿到的矩阵形状是 [H, W²_window, W²_window],分数只反映”在这个窗口里谁更重要”,完全感知不到全局语义。比如一张图片右上角的狗和左下角的人,在同一个窗口层里从来不会相互关注,所以窗口层给出的分数无法判断”哪个 patch 对整张图最重要”。

正确的做法

必须在全局注意力层上取分数。VisionZip 的原始论文用的是 ViT 倒数第二层(SELECT_LAYER)的注意力,这一层在全局注意力层里所有 patch 都能互相看到。

对应到 Qwen3-VL:fullatt_block_indexes = [7, 15, 23, 31],应该取最后一个全局注意力层,也就是第 31 层。这一层是整个 ViT 最后一次让所有 patch 全局交互,之后紧跟着 merger 就输出了。在第 31 层拿到的注意力权重,反映的是每个 patch 在”看完整张图之后”觉得哪些地方最重要,这才是可靠的全局重要性信号。

选第 31 层而不是第 7、15、23 层的另一个原因是:越深的层语义越丰富,第 31 层的注意力权重更能代表最终送进 LLM 之前的视觉语义判断。


问题二:Flash Attention 不返回注意力权重

为什么会这样

FlashAttention-2 的核心设计是用 tiling(分块计算)在 SRAM 里流式完成整个 softmax + weighted sum,避免把完整的 N×N 注意力矩阵写回显存。这正是它能节省大量显存、速度快的原因——但代价就是完整的 attn_weights 矩阵从来没有在内存里完整存在过,所以 output_attentions=True 在 FlashAttention-2 下要么报错,要么返回 None

PyTorch 的 SDPA(Scaled Dot-Product Attention)是标准的矩阵乘法实现,会完整算出 [H, N, N] 的注意力矩阵再乘以 V,所以能正常返回 attn_weights

正确的做法

加载模型时把 attn_implementationflash_attention_2 改成 sdpa

但这里有一个权衡要理解:sdpa 比 flash_attention_2 慢,显存占用更高,因为它要实际存储 N×N 的注意力矩阵。对于 Qwen3-VL-2B 这个规模,ViT 的 N 大约在 256–1024 个 patch 之间,N×N 矩阵还可以接受。但如果图片分辨率很高(N 达到几千),sdpa 的显存开销就会明显上升。

还有一个折中方案:只在需要取注意力权重的那一层(第 31 层)用 eager 模式,其他层继续用更快的实现。因为第 31 层是全局注意力层,N×N 矩阵是完整的全图尺寸,必须用 sdpa/eager;而窗口注意力层的 attn_weights 本来就没用,可以继续用 flash_attention_2 加速。这个”混合模式”需要在模型的 forward 里手动控制,在工程实现上会复杂一些,但对高分辨率图像场景有意义。


问题三:DeepStack 让 token 压缩变复杂

先理解 DeepStack 的数据流

Qwen3-VL 的 ViT 不只是跑完 32 层然后输出,它在第 7、15、23 层(不含最后第 31 层)会把中间的隐藏状态”截取”出来,各自经过独立的 MLP Merger 投影到 LLM 的维度,然后在 LLM 的前 3 层做残差注入。整个数据流是:

ViT 层 0–6 → 层 7(全局)→ [提取 F1 → MLP1 → 存起来]
→ 层 8–14 → 层 15(全局)→ [提取 F2 → MLP2 → 存起来]
→ 层 16–22 → 层 23(全局)→ [提取 F3 → MLP3 → 存起来]
→ 层 24–30 → 层 31(全局)→ [最终输出]
→ PatchMerger(最终 MLP)→ visual tokens 送给 LLM 层 0
同时 F1、F2、F3 注入到 LLM 层 1、2、3

问题在哪

VisionZip 的压缩是把 N 个 token 减少到 K+M 个,这个操作必须发生在 ViT 输出之后、送进 LLM 之前。但 DeepStack 有个麻烦:F1、F2、F3 的 token 数量是 N,压缩后的 token 数量是 K+M,两者不匹配

如果你只压缩最终输出(F4 → K+M),但 F1/F2/F3 还是 N 个,那 LLM 里就会出现矛盾:视觉序列占位是 K+M 个,但 DeepStack 注入的特征是 N 个,位置对不上。

更具体地说,LLM 在做注意力时依赖 MRoPE 的位置 ID,每一个 visual token 的位置 ID 是由 image_grid_thw(图像的时间、高度、宽度网格)算出来的。压缩前是 (1, H, W),N = H×W 个 token;压缩后只有 K+M 个,但位置系统还认为有 N 个,就会对不上。

正确的做法

有两条路:

路线 A:只压缩最终输出,放弃 DeepStack 的中间层注入

最简单的做法是在第 31 层之后只压缩 F4,同时不让 F1/F2/F3 注入 LLM(或者也对它们做对应的压缩)。这相当于退化成了”不用 DeepStack”的普通 VisionZip。实现简单,但损失了 Qwen3-VL 的部分视觉能力。对于快速验证 VisionZip 效果来说是合理的起点。

路线 B:对所有层的中间特征同步压缩(正确做法)

在第 31 层拿到注意力权重,确定出哪 K+M 个 token 要保留(选出它们的索引 selected_indices)之后,用同一组索引去筛选 F1、F2、F3 对应位置的 token。这样所有中间层和最终输出的 token 数量是一致的,都是 K+M 个。

这里有一个设计上的考量:VisionZip 的重要性打分是在第 31 层做的,那 F1(第 7 层)的 token 排布是否和第 31 层的 selected_indices 对齐?答案是:对齐的,因为 ViT 的每一层都保持相同的 token 序列长度和顺序,第 7 层的第 i 个 token 对应的就是第 31 层的第 i 个 token,空间位置没有变化。所以用第 31 层的重要性索引去取第 7 层的 token 是合理的——虽然用的是深层的”语义重要性”来代理浅层的”细节重要性”,但这在实践中效果很好,因为语义上重要的区域,细节上也往往值得保留。

image_grid_thw 必须同步更新

压缩完 token 之后,还有一个系统级的问题:Qwen3-VL 的 LLM 在生成 MRoPE 的位置 ID 时,依赖 image_grid_thw 这个参数(形状 [1, T, H, W],记录这张图被切成了多少行多少列的 patch)。模型用它来给每个 visual token 分配 (t, h, w) 三维坐标。

压缩之前:N = T × H × W 个 token,image_grid_thw = [1, 1, H, W] 压缩之后:只剩 K+M 个 token,但 image_grid_thw 还是 [1, 1, H, W],模型会以为有 H×W 个 token,然后给前 K+M 个赋位置、忽略剩下的,或者更糟,整个 position ID 计算出错。

解决方法是:把 image_grid_thw[1, 1, H, W] 改成 [1, 1, h', w'],其中 h' × w' = K + M。通常取 h' = w' = sqrt(K+M) 取整,或者根据原始宽高比调整比例。这样 LLM 就会把这 K+M 个 token 当作一张 h' × w' 的小图来分配位置 ID,位置关系虽然不完全准确(原来的空间布局被打乱了),但至少数量一致,不会报错,而且实践中性能影响很小。

这三个问题本质上是一条链:窗口注意力决定了取哪一层的分数,flash_attention 决定了用什么实现来拿分数,DeepStack + image_grid_thw 决定了压缩完之后整个系统怎么保持一致性。理解这条链,后续写代码的思路就很清晰了。