Skip to main content

qwen3-vl

Qwen3-VL 核心结构与计算流程

整体架构

┌─────────────────────────────────────────────────────┐
│ Qwen3VLForConditionalGeneration │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ visual │ │ model │ │ lm_head │ │
│ │ (Vision Enc.) │ │ (Decoder) │ │ (Linear) │ │
│ └──────────────┘ └──────────────┘ └──────────┘ │
└─────────────────────────────────────────────────────┘

模型由三个核心组件组成(qwen3_vl.py:17-38):

  • visual — Vision Encoder,将像素转为 embedding
  • model — Qwen3 语言模型 Decoder(复用纯文本 Qwen3)
  • lm_head — 输出层,映射回词表

完整计算流程(Prefill 阶段)

以一条包含图片的请求为例:

用户输入: "描述这张图片 <image>"
token 化后: [描述, 这张, 图片, <img_pad>, <img_pad>, ..., <img_pad>]
↑————— N 个图像占位 token ————↑

阶段一:Vision Encoder(vision.py:433-478

pixel_values ─→ ① Patch Embed ─→ ② + Pos Embed ─→ ③ Vision Blocks ─→ ④ Merger ─→ vision_embeds
(Conv3D) (插值 2D) (27层 ViT) (降维)

① 3D 卷积 Patch 化 (Qwen3VLVisionPatchEmbed)

原始像素: (N_patches, C=3, T_patch=2, H_patch=14, W_patch=14)
→ Conv3d(3, 1152, kernel=[2,14,14], stride=[2,14,14])
→ (total_patches, 1152)

将原始像素分块为固定大小的 patch,每个 patch 映射到 1152 维向量。

② 绝对位置编码 (_fast_pos_embed_interpolate)

使用可学习的 2D 位置编码表 pos_embed(48x48=2304 个位置),通过双线性插值适配任意分辨率:

x = x + self._fast_pos_embed_interpolate(grid_thw)

③ Vision Transformer Blocks (Qwen3VisionBlock, 27 层)

每层结构:

x ─→ LayerNorm ─→ Attention ─→ + residual
─→ LayerNorm ─→ MLP (SiLU) ─→ + residual

Vision attention 的两个特点:

  • 自有 RoPE:用 2D 空间坐标(H, W)生成旋转嵌入,与 decoder 的 MRoPE 独立(vision.py:343-359
  • 段级 attention:通过 cu_seqlens 确保不同图片/帧的 patch 互不可见(vision.py:153-160

④ Patch Merger(关键降维步骤)

spatial_merge_size²=4 个相邻 patch 合并为 1 个 token:

(total_patches, 1152) → reshape + MLP → (total_patches/4, 2048)
↑ 即 LLM hidden_size

这使得每张 224x224 的图片从 256 个 patch 缩减为 64 个 token。

⑤ Deepstack 特征提取(Qwen3-VL 独创)

在第 8、16、24 层分别通过独立的 deepstack_merger 提取中间特征,与最终特征拼接:

# vision.py:466-471
for layer_num, blk in enumerate(self.blocks):
x = blk(x, ...)
if layer_num in [8, 16, 24]: # deepstack_visual_indexes
ds_feat = deepstack_merger_list[i](x) # 每层独立 merger
deepstack_features.append(ds_feat)

# vision.py:477 — 拼接: (tokens, 2048*4)
hidden_states = torch.cat([x] + deepstack_features, dim=1)

最终输出 shape: (merged_tokens, 2048 * (1+3)) = (merged_tokens, 8192)


阶段二:Vision-Language 融合(qwen3_vl.py:59-92

# 1. 所有 token 先过 text embedding
hidden = embed_tokens(input_ids) # (seq_len, 2048)

# 2. 分离 vision 主特征和 deepstack 特征
vision_embeds = image_embeds[:, :2048] # 主特征
deepstack_embeds = image_embeds[:, 2048:] # 3层 deepstack

# 3. 替换图像占位 token
mask = (input_ids == image_token_id)
hidden[mask] = vision_embeds # 原地替换

替换后的 hidden states:

[描述_emb, 这张_emb, 图片_emb, vision_0, vision_1, ..., vision_N]
↑—— 文本 embedding ——↑ ↑——— vision encoder 输出 ———↑

阶段三:Decoder 前向 + Deepstack 注入(qwen3.py:58-80

for layer_idx, layer in enumerate(self.layers):
x, residual = layer.forward(x, residual) # 标准 Decoder 层

# Deepstack: 在第 0, 1, 2 层注入对应的 vision 中间特征
if layer_idx in deepstack_inject_layers:
x = x + deepstack_embeds[:, layer_idx*2048 : (layer_idx+1)*2048]

Deepstack 的直觉:浅层 decoder 获得 vision encoder 浅层特征(低级视觉信息),深层获得深层特征(高级语义),让 decoder 的不同层都能访问对应粒度的视觉信息。

Decoder 每层结构(与纯文本 Qwen3 完全相同):

x ─→ RMSNorm ─→ QKV投影 ─→ QK Norm ─→ MRoPE ─→ FlashInfer Attention ─→ + residual
─→ RMSNorm ─→ Gate·Up投影 ─→ SiLU ─→ Down投影 ─→ + residual

唯一区别:位置编码使用 MRoPE(3D 位置),而非标准 1D RoPE。


阶段四:输出

output = self.norm(x, residual)      # 最终 RMSNorm
logits = self.lm_head(output) # (seq_len, vocab_size)

总结数据流

像素 ──→ Conv3D ──→ +PosEmbed ──→ 27层ViT ──→ Merger ──→ vision_embeds
↓(8,16,24层) ↓
deepstack_embeds 替换 <img_pad>
↓ ↓
注入 decoder 0,1,2 层 decoder input
↓ ↓
←←←←←←← Decoder (28层, MRoPE) →→→→→→→

RMSNorm → LMHead → logits