Chapter 09

多模态:LLaVA / Qwen2-VL / Qwen3-VL 微调

📌 本章基于 src/llamafactory/data/mm_plugin.py 与 examples/train_lora/qwen3vl_*.yaml · commit 01398eb18dd4 。

9.1 多模态模型在 LLaMA-Factory 里的统一处理

无论 LLaVA / Qwen-VL / Qwen2-VL / Qwen3-VL / InternVL / MiniCPM-V / GLM-4V,LLaMA-Factory 都用一个统一的"多模态插件"机制:

flowchart LR Raw["原始数据
(text + images + videos)"] --> MMP["mm_plugin
(每个模型一个 plugin)"] MMP --> Embed["把 image/video token 拼进文本序列"] Embed --> Tpl[template] Tpl --> Tok["tokenizer + processor"] Tok --> Trn["训练 loop(和文本完全相同)"]

简单说:多模态被转成"含特殊 token 的文本序列",剩下的流程和文本 SFT 一模一样。这就是为什么第 4 章那一份 6-args 配置体系完全能扩展到多模态。

9.2 一份多模态 YAML 长什么样

仓库自带 examples/train_lora/qwen3vl_lora_sft.yaml

### model
model_name_or_path: Qwen/Qwen3-VL-4B-Instruct
image_max_pixels: 262144           # ★ 多模态特有:单图最大像素
video_max_pixels: 16384            # ★ 多模态特有:单帧最大像素
trust_remote_code: true

### method
stage: sft
do_train: true
finetuning_type: lora
lora_rank: 8
lora_target: all

### dataset
dataset: mllm_demo,identity,alpaca_en_demo    # ★ mllm_demo 是图文数据
                                              # video: mllm_video_demo
template: qwen3_vl_nothink                     # ★ template 必须是 _vl_ 版
cutoff_len: 2048
max_samples: 1000
preprocessing_num_workers: 16
dataloader_num_workers: 4
...

和文本 SFT YAML 比对,多模态要改的就 4 个字段:

字段含义
model_name_or_path 换成多模态基座(Qwen3-VL / LLaVA-1.5 / InternVL2 等)
image_max_pixels 限制每张图最大像素(避免 OOM)
video_max_pixels 限制每帧最大像素
template 必须是该模型的 vl 版(如 qwen3_vl_nothinkllavainternvl2
dataset 多模态数据集

9.3 多模态数据格式

LLaMA-Factory 多模态走 sharegpt 格式 + images / videos。仓库 data/mllm_demo.json 是参考。一条样本长这样:

{
  "messages": [
    {
      "role": "user",
      "content": "<image>描述这张图片"
    },
    {
      "role": "assistant",
      "content": "这是一只猫,坐在窗台上看着窗外。"
    }
  ],
  "images": ["data/images/cat.jpg"]
}

data/dataset_info.json 中要这样注册:

{
  "mllm_demo": {
    "file_name": "mllm_demo.json",
    "formatting": "sharegpt",
    "columns": {
      "messages": "messages",
      "images":   "images"
    }
  }
}
字段占位符对应列
图片 <image> images: [path, ...]
视频 <video> videos: [path, ...]
音频 <audio> audios: [path, ...]

占位符必须出现在 content 里,个数和列表元素数对齐。一条样本里有 2 张图,那 content 必须出现 2 个 <image>

9.4 mm_plugin:每个模型一个适配器

src/llamafactory/data/mm_plugin.py 维护一组 Plugin,每个对应一种多模态架构。它们负责:

  1. 读取图像 / 视频文件(PIL / decord);
  2. 调模型自己的 image_processor / video_processor
  3. 把处理后的 visual feature 用正确数量的 placeholder token 占位到序列里;
  4. 训练时把 visual feature 替换回去。
Plugin(命名风格)支持模型
LlavaPlugin LLaVA-1.5 / 1.6
Qwen2VLPlugin Qwen2-VL
Qwen3VLPlugin Qwen3-VL(支持图 + 视频)
InternVLPlugin InternVL2 / InternVL2.5
MiniCPMVPlugin MiniCPM-V / MiniCPM-O
VideoLlavaPlugin Video-LLaVA
PaliGemmaPlugin PaliGemma / PaliGemma2

遇到没列出的多模态新模型,通常是因为 LLaMA-Factory 还没适配。给项目提 PR 加一个 Plugin 是常见贡献方式

9.5 多模态训练的特别注意

显存分布

多模态训练显存 ≈ 文本部分 + visual encoder + 处理后的 visual feature。image_max_pixels 越大,第二项越大:

image_max_pixels边长(正方)每张图的 visual token 数(Qwen2-VL 默认 patch=14)
4096 (默认低) ≈ 64×64 ~16
65536 ≈ 256×256 ~256
262144(demo 默认)≈ 512×512 ~1024
1048576 ≈ 1024×1024 ~4096

训练时如果一张样本含一张 1024×1024 大图 + 2048 文本 token,序列实际长度约 6000 tokencutoff_len 必须够大。

vision tower 是否参与训练

默认情况下 LLaMA-Factory冻结 visual encoder,只训语言侧 LoRA + projector。如果要训 vision tower:

finetuning_type: lora
lora_target: all                  # 默认只覆盖 language 部分
freeze_vision_tower: false        # 把 vision tower 放开
freeze_multi_modal_projector: false  # connector 也放开

这种全开训练通常需要 ≥ 80 GB 显存。

视频训练

视频 = 多帧图像。LLaMA-Factory 在 mm_plugin 里做帧采样(默认稀疏均匀采样),video_max_pixels 限制每帧分辨率。常见 YAML 加一行:

video_fps: 1.0                    # 每秒采 1 帧
video_max_pixels: 16384           # 每帧 ≈ 128×128
video_min_pixels: 256

9.6 多模态 + DPO / RM

LLaMA-Factory 多模态也可以走偏好对齐:

stage: dpo
finetuning_type: lora
template: qwen3_vl_nothink
dataset: dpo_mllm_demo           # 同样格式 + chosen/rejected + images
pref_loss: sigmoid

data/dpo_mllm_demo.json 演示了 schema:

{
  "conversations": [
    {"from": "human", "value": "<image>评价这张图的构图"},
    {"from": "gpt", "value": "..."}
  ],
  "chosen": {"from": "gpt", "value": "好回复"},
  "rejected": {"from": "gpt", "value": "差回复"},
  "images": ["..."]
}

9.7 多模态推理

llamafactory-cli chat \
  model_name_or_path=Qwen/Qwen3-VL-4B-Instruct \
  adapter_name_or_path=saves/qwen3-vl-4b/lora/sft \
  template=qwen3_vl_nothink

在 chat CLI 中用 <image>path/to/img.png 这种语法直接附图:

User: <image>/tmp/cat.jpg 请描述这张图
Assistant: 一只橘色的猫坐在窗台上...

9.8 常见踩坑

现象原因怎么修
图未参与训练(loss 异常正常) template 写成非 vl 版 改用 qwen3_vl_nothink 等正确 vl template
RuntimeError: shape mismatch占位符数 ≠ images 列长度 逐条数据集校验,必要时跑预处理脚本剔脏数据
显存爆 + 单条样本贼大 样本图过大 调小 image_max_pixels
训完模型看到图全输出"我看不见图" train_on_prompt=true 把 visual tag 当训练目标恢复默认 train_on_prompt: false
视频读不进 未装 decord pip install decord;或导出 mp4 → 帧图片

9.9 CompositeModel:每个 VLM 在源码里"长什么样"

多模态模型形态千差万别,LLaMA-Factory 在 src/llamafactory/model/model_utils/visual.py:39CompositeModel dataclass 用统一 schema 描述:

# visual.py:39
@dataclass
class CompositeModel:
    projector_keys: list[str]        # 多模态 projector 路径,如 ["multi_modal_projector"]
    vision_model_keys: list[str]     # 视觉塔路径,默认会被 freeze
    language_model_keys: list[str]   # 文本侧路径,用于选择性 freeze
    lora_conflict_keys: list[str]    # 与 LoRA 不兼容的模块名

仓库里已经注册了若干 VLM 族(同文件后续):

VLM 模型族projector_keysvision_model_keys特别处理
LLaVA(line 302) ["multi_modal_projector"] 默认
Qwen2-VL / Qwen3-VL(line 2073–2085)["visual.merger"] ["visual"] video 支持
InternVL3(line 274) 默认 默认
Gemma-3N(line 231) 默认 + audio_tower 默认 lora_conflict_keys 含 timm_model
LLaVA-Next / LLaVA-OneVision 默认 默认
Qwen2.5-Omni / Qwen3-Omni ["model.embed_vision", "model.embed_audio"]同上 load_model 会取 .thinker 子模型(loader.py:173)

"我想训 Qwen3-VL 时只动 LM、冻 vision tower 和 projector"——这是默认行为freeze_vision_tower: true + freeze_multi_modal_projector: true 默认 True)。要联训 projector,加一行 freeze_multi_modal_projector: false;要训 vision tower,再加 freeze_vision_tower: false。三者解耦。

9.10 mm_plugin:多模态数据预处理插件

每个 VLM 族的 image / video / audio 怎么 resize、怎么变 token、怎么拼到 prompt 里——逻辑都在 src/llamafactory/data/mm_plugin.py 的 plugin 类里,按模型族分实现。每个 plugin 实现这几个方法:

方法做什么
process_messages(messages, images, videos, audios, processor)<image> 占位符换成模型期望的 token 序列(数量由图大小决定)
process_token_ids(input_ids, labels, ...)token 化后,把 image token 区段对应的 label 设为 -100(不计 loss)
get_mm_inputs(images, videos, audios, ...) 用 processor 真正预处理 PIL.Image / video / audio → tensor

典型 plugin 命名:Qwen2vlPlugin / Qwen2omniPlugin / InternvlPlugin / LlavaPlugin / LlavaNextPlugin / Gemma3Plugin / PaliGemmaPlugin 等。template 注册时通过 mm_plugin=... 参数关联。"加一个新 VLM 支持"流程:

  1. 写一个 MyVlmPlugin(BasePlugin) 子类;
  2. visual.py 注册一个 CompositeModel,标 projector / vision keys;
  3. data/template.py 注册 template,传入新 plugin;
  4. data/dataset_info.json 允许 columns 映射 images / videos / audios 字段。

9.11 关键 processor 字段(YAML 入口)

多模态 processor 的运行时行为,通过 patcher.py:290–305ModelArguments 抽出来塞给 HF processor:

YAML 字段含义典型值
image_max_pixels 单图最大像素 768*768
image_min_pixels 单图最小像素(防止图过小) 56*56
crop_to_patches 是否切成 patch(InternVL 系)视模型
video_max_pixels 每帧最大像素 128*128
video_fps 视频抽帧率 2.0
video_maxlen 视频最长帧数 16 / 32 / 64
audio_sampling_rate 音频采样率 16000

调显存最直接的两个旋钮:image_max_pixelsvideo_maxlen。前者控单图 token 数(Qwen2-VL 用 28×28 patch,768*768 约 729 个 visual token);后者控视频总帧数。

9.12 projector 的 dtype 转换

QLoRA 跑 VLM 时偶尔 NaN,多半是 projector 输出 dtype 不一致。visual.py:139–159autocast_projector_dtype() 给所有 projector 注册了一个 post-hook,把输出强转到 compute_dtype。这是仓库自动加的,但你需要知道它存在——自定义 VLM plugin 时要把新的 projector 路径加进 projector_keys,否则 dtype 链就断了。