> 上一篇: 18·3 《MRoPE——3D 位置编码给视觉留的席位》| 下一篇: 19·1 《ViT视觉编码:图片如何变成视觉token》
一句话导读:ViT 视觉编码流程:图片像素经预处理、PatchEmbed、24 层自注意力与合并层变成视觉 token;板端 64×64 合成图实测 4 个视觉 token、编码耗时 295.3 ms。
真机实测通过:本文实验已在 RK3588 板端实测完成(2026-09;方法学与原始记录见仓库 docs 与《实验脚本》目录)
Day 18 收尾讲了 MRoPE:视觉 token 在进入 LLM 前会拿到 (pos_t, pos_h, pos_w) 三套坐标。这篇回答“这些视觉 token 从哪来”的第一半:图片像素 → ViT 编码 → 视觉特征。引擎的视觉塔在 src/model/vllm_vision.c,板端(RK3588 + Qwen3-VL-2B 真权重)实测:64×64 合成图经 ViT 24 层得到 4 个视觉 token,编码耗时 295.3 ms。
1. 知识点:视觉塔把图片“裁”成什么
1.1 多模态 LLM 的图片进法
Qwen3-VL 的视觉塔是一条独立的 ViT(Vision Transformer),与 LLM 分开训练权重、分开量化:
RGB 图片 → resize/归一化(preprocess)
→ PatchEmbed(patch=16,把图切成 16×16 的小块嵌入)
→ 可学习位置嵌入 + 视觉 RoPE
→ ViT 24 层自注意力(DeepStack 在指定层额外抽特征)
→ 2×2 空间合并(spatial_merge)
→ Merger 两层 MLP 投影到 LLM 隐空间
→ 每张图得到 n 个“视觉 token”(LLM 维度)
图片分辨率决定 token 数:一张 64×64 的图 → 64/16 × 64/16 = 4×4 = 16 个 patch → 2×2 合并 → 4 个视觉 token。视觉 token 数随图变(不是固定数量),这正是 MRoPE 网格 (g_h, g_w) 的来源。
1.2 实测的 2B 视觉配置
板端加载日志(不是头文件里的“27 层”宣传注释,是 config.json 驱动、manifest 佐证的真实值):
[ST] Vision: depth=24 hidden=1024 heads=16 ffn=4096 patch=16 temporal=2 merge=2 out=2048
[ST] DeepStack layers: 5 11 17
[ST] MRoPE sections: 24 20 20
Config: dim=2048 layers=28 heads=16 kv_heads=8 hd=128 ffn=6144 vocab=151936
注意:2B 的 ViT 是 24 层(vllm_vision.h 头注释写“27-layer ViT”,那是 8B 的口径),视觉隐宽 1024、patch 16、2×2 合并、temporal=2(视频沿时间轴也做 patch)。DeepStack 在层 5/11/17 各抽一组中间特征。生产 model.vqf(2,331,316,232 B,flags=0x63)里能看到对应的 27 个 v_* 张量:4 个 Q8 大矩阵(v_q8_qkv/proj/fc1/fc2,各 24 层)+ patch/pos 嵌入 + 偏置/归一 + merger 6 个 + deepstack 6 个。
2. 对应代码:encode 的八步流水
st_vision_encode_image(vllm_vision.c 1144 行起)按 8 步走完全流程:
/* Step 1: Preprocess (resize + normalize) */ preprocess_image(…) // 1144-1174
/* Step 2: Patch embedding */ patch_embed_image(…) // 1176-1179
/* Step 3: Interpolated position embedding + vision RoPE table */
vision_pos_encode(…) // 1181-1183
/* Step 5: ViT forward with DeepStack */
for (int l = 0; l < cfg->vis_depth; l++) { // 1194-1214
vit_block_forward(vis, l, n_patches);
if (cfg->vis_ds_idx[d] == l) { // DeepStack 层 5/11/17
vision_ds_merge(…); // 抽中间特征 → ds_features
}
}
/* Step 6: Spatial merge of final ViT output */ spatial_merge(…) // 1217-1226
/* Step 7: Main merger */ merger_forward(…) // 1228-1236
/* Step 8: Visual token output = main merged tokens only */ // 1238+
配套的权重结构在 include/model/vllm_vision.h:STVisionWeights(patch_embed、pos_embed、attn_qkv/proj、norm1/2、mlp_fc1/fc2、merger_*、ds_*)与 STVisionState(运行时缓冲:pixel_values、hidden_states、rot_cos/sin、ds_features)。要盯的转换关系有两条:
3. 改动后果:板端跑一次完整图片 → 编码 → 生成
实测口径:RK3588(Orange Pi 5 Plus)/ aarch64 / Release / 2026-09-07。方法:–multimodal 自测(默认 64×64 合成渐变图),模型目录用 bind-mount 把 2B 挂到自测默认寻找的路径下运行。
[ST-Q8] Estimated memory: 4.5 GB (F32 token + Q8_0 + Q4_0 nibble weights)
Weights loading: 20.5 sec
[TOK] Loaded 151669 tokens from vocab.bin (max_len=256)
Tokenizer: 151669 tokens, vision IDs: start=151652 end=151653 img=151655 vid=151656
— Image 1/1: synthetic (64×64) —
[VIS] Image encoded: 64×64 → 16 patches → 4 visual tokens
64×64 -> 4 patches -> 4 visual tokens (grid 2×2)
[VIS] Encode: 295.3 ms
Prompt: 17 tokens (10 text, 6 visual)
Prefill: 513ms (33.16 tok/s)
Decode: 60 tokens in 2974ms (TPOT=50ms, 20.2 tok/s)
Total: TTFT=513ms E2E=3486ms
— Video Analysis —
[VIS] Video encoded: 4 frames 64×64 → 32 patches → 8 visual tokens
Video 4 frames 64×64 -> 8 visual tokens
[VIS] Encode: 628.5 ms
三件事值得拆开读:
4. 学员调试任务
- A 档(板端动手):
- 复刻本文:跑 –multimodal(需要把 2B 或 8B 模型目录放到代码查找的 Modl/千问3_VL_8B_Instruct/ 下,或自己 bind-mount),核对“16 patches → 4 visual tokens / Encode ≈ 295ms / grid 2×2”;
- 开 VLLM_MMDBG=1 重跑一次,观察 [VIS] preprocess/patch_embed/pos_embed/vit_all/merge 各步骤的累计耗时——哪一步是热点?(这是给 Day 28 性能篇留的伏笔)
- 用 manifest_vqf.py(Day 17)核对 model.vqf 里 27 个 v_* 张量的行数与 [ST] Vision: 一行对得上(qkv/proj/fc1/fc2 都是 24 行)。
- B 档(纯读源码):读 vllm_vision.c 1144–1240,回答:① 为什么 encode 的输出只算“main merged tokens”而 DeepStack 特征是另存到 ds_features(提示:vllm_vision.h 124 行注释——它要注入到 LLM 的哪些层,进入 prefill 的哪个环节)?② 若把 vis_merge 从 2 改成 1,同样 64×64 会产出多少 token?改成 4 呢?③ 若 config.json 的 vis_depth 与 model.vqf 里 v_q8_qkv 的实际层数不一致,加载或前向会在哪一步先出错(读 st_vision_load_weights 的分层循环)?
预期输出:一张你自己板子的“图片尺寸 → patch 数 → 合并后 token 数 → encode ms”小表,并能说出 ViT 各步骤里最耗时的一环。
收尾
- 本篇源码点名:vllm_vision.c(encode_image 八步 1144–1240、vit_block_forward/merger)、vllm_vision.h(权重/运行时结构)、vllm_safetensors.c(视觉张量加载与 Q8 量化)
- 开源仓库:Kestrel-LLM (Gitee)(源码可得双许可:学习 / 学术研究免费)
- 下篇预告:板载 ViT 要 295ms,那“把编码挪到客户端”会怎样?19-2 拆 vision_tokens 客户端预编码协议:图片 base64 上行的板端 ViT 路径、缓存命中路径、以及“客户端自己算好特征直接塞进来”的第三条路。
- 关键词:ViT、视觉 token、视觉塔、多模态、PatchEmbed、DeepStack、MRoPE、RK3588
网硕互联帮助中心



评论前必须登录!
注册