多模态:LLaVA / Qwen2-VL / Qwen3-VL 微调
9.1 多模态模型在 LLaMA-Factory 里的统一处理
无论 LLaVA / Qwen-VL / Qwen2-VL / Qwen3-VL / InternVL / MiniCPM-V / GLM-4V,LLaMA-Factory 都用一个统一的"多模态插件"机制:
(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_nothink、llava、internvl2) |
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,每个对应一种多模态架构。它们负责:
- 读取图像 / 视频文件(PIL / decord);
- 调模型自己的
image_processor/video_processor; - 把处理后的 visual feature 用正确数量的 placeholder token 占位到序列里;
- 训练时把 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 token。cutoff_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:39 的 CompositeModel 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_keys | vision_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 支持"流程:
- 写一个
MyVlmPlugin(BasePlugin)子类; - 在
visual.py注册一个CompositeModel,标 projector / vision keys; - 在
data/template.py注册 template,传入新 plugin; - 在
data/dataset_info.json允许 columns 映射images / videos / audios字段。
9.11 关键 processor 字段(YAML 入口)
多模态 processor 的运行时行为,通过 patcher.py:290–305 从 ModelArguments 抽出来塞给 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_pixels 和 video_maxlen。前者控单图 token 数(Qwen2-VL 用 28×28 patch,768*768 约 729 个 visual token);后者控视频总帧数。
9.12 projector 的 dtype 转换
QLoRA 跑 VLM 时偶尔 NaN,多半是 projector 输出 dtype 不一致。visual.py:139–159 的 autocast_projector_dtype() 给所有 projector 注册了一个 post-hook,把输出强转到 compute_dtype。这是仓库自动加的,但你需要知道它存在——自定义 VLM plugin 时要把新的 projector 路径加进 projector_keys,否则 dtype 链就断了。