VLM 训练与评测示例#
本文给出一个最小的 VLM 训练与评测流程。以下命令以 realworldqa 作为评测示例;替换评测集非常简单,见后文“替换测试数据集”章节。
⚠️ 注意:目前VLM相关功能位于feat/qwen3vl-qwen3_5分支,由于transformers库版本和main分支不同,main分支原本的功能未经过全面测试,部分功能可能会有影响。
准备环境#
进入仓库根目录:
cd /root/code/fluxvla
如果你在单机多卡环境运行 scripts/train.sh,请先设置分布式环境变量(示例为单机 8 卡):
export MLP_WORKER_GPU=8
export MLP_WORKER_NUM=1
export MLP_ROLE_INDEX=0
export MLP_WORKER_0_HOST=localhost
export MLP_WORKER_0_PORT=29500
备注
scripts/train.sh 内部调用 torchrun,并依赖上述环境变量。若在平台环境(如 MLP)中运行,通常这些变量会由平台自动注入。
1. 数据集要求与自定义构建#
本配置 qwen3_vl_sft_vision2b_llm0_6b_stage2_sft.py 的训练集类型是 LLaVAFormatDataset,示例数据是 Cambrain-7M 的 LLaVA 风格清单(data_sources 里给出的 json 文件)。
1.1 数据格式要求(必须满足)#
每条样本至少应包含:
conversations:对话列表,元素形如{"from": "human"|"gpt", "value": "..."}可选
image/images:单图字符串路径或多图路径列表可选
video/videos:单视频字符串路径或多视频路径列表
其中有两个关键约束:
在
human的文本里使用<image>/<video>占位符时,数量必须和images/videos中的文件数一致若路径是相对路径,需要在配置里设置
train_dataloader.dataset.data_root;如果是绝对路径可不设
1.2 最小可用样例#
[
{
"id": "sample-0001",
"images": ["images/0001.jpg"],
"conversations": [
{"from": "human", "value": "<image>\n请描述图中主要物体并给出抓取建议。"},
{"from": "gpt", "value": "图中主要物体是杯子,建议从杯身中部靠近把手一侧抓取。"}
]
},
{
"id": "sample-0002",
"conversations": [
{"from": "human", "value": "请将下面英文翻译为中文:Pick up the red block."},
{"from": "gpt", "value": "请拿起红色方块。"}
]
}
]
1.3 如果使用你自己的数据集,如何构建#
推荐流程如下:
先整理原始数据(图片/视频 + 指令 + 标注答案)
转换成 LLaVA 格式
json或jsonl(字段见 1.1)在配置中把
train_dataloader.dataset.data_sources指向你的清单文件如使用相对路径,设置
train_dataloader.dataset.data_root为数据根目录先用小样本试跑(例如几百条)验证无格式错误,再全量训练
一个简单的“原始标注 -> LLaVA 格式”转换示例(按你的字段名改):
import json
raw_records = [
# 示例输入,按你的原始标注格式替换
{
"sample_id": "1",
"image_rel_path": "images/0001.jpg",
"instruction": "请描述图中物体。",
"answer": "图中有一个蓝色杯子。"
}
]
out = []
for r in raw_records:
out.append({
"id": f"custom-{r['sample_id']}",
"images": [r["image_rel_path"]], # 相对路径时配合 data_root 使用
"conversations": [
{"from": "human", "value": f"<image>\n{r['instruction']}"},
{"from": "gpt", "value": r["answer"]},
],
})
with open("custom_llava_train.json", "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, indent=2)
配置接入示例(自定义数据):
train_dataloader = dict(
# ...
dataset=dict(
type='LLaVAFormatDataset',
data_sources=['/path/to/custom_llava_train.json'],
processor_type='qwen3_vl',
model_path='/path/to/base_or_stage1_ckpt',
data_root='/path/to/your_dataset_root', # 绝对路径可设为 None
image_max_pixels=32 * 32 * 1280,
video_max_pixels=32 * 32 * 1280 * 2,
truncation_max_length=2000,
statistic_name='my_custom_vlm_sft',
),
)
备注
常见报错是“占位符数量和图片/视频数量不一致”或“路径找不到”。建议先抽样检查 100 条数据:
每条
conversations至少包含一轮 human/gpt;<image>/<video>数量与文件列表一致;路径可在训练机上真实访问。
2. 启动训练#
bash scripts/train.sh \
/root/code/fluxvla/configs/vlm/qwen3_vl_sft_vision2b_llm0_6b_stage2_sft.py \
/path_to_work_dir/
训练命令参数说明:
scripts/train.sh:训练入口脚本(内部调用scripts/train.py)第 1 个位置参数:配置文件路径(这里是
qwen3_vl_sft_vision2b_llm0_6b_stage2_sft.py)第 2 个位置参数:工作目录(日志、checkpoint、配置快照都会写到这里)
第 3 个及之后参数:透传给
scripts/train.py,常用如下:--cfg-options key=value:临时覆盖配置,不改原文件--resume-from /path/to/ckpt.pt:从 checkpoint 继续训练
3. 训练后评测#
python scripts/eval.py \
--config configs/vlm/qwen3_vl_sft_vision2b_llm0_6b_stage2_sft.py \
--ckpt-path /path_to_checkpoint/ \
--cfg-options eval.benchmarks=[realworldqa]
评测命令参数说明:
python scripts/eval.py:评测入口脚本--config:评测配置文件(通常与训练配置一致)--ckpt-path:待评测模型 checkpoint(HF 格式目录或评测 runner 支持的路径)--cfg-options:覆盖配置项;示例中把eval.benchmarks改为仅评测realworldqa
4. 替换测试数据集(评测 benchmark)#
realworldqa 只是示例,换 benchmark 只改 eval.benchmarks:
# 只评测 gqa
python scripts/eval.py \
--config configs/vlm/qwen3_vl_sft_vision2b_llm0_6b_stage2_sft.py \
--ckpt-path /path_to_checkpoint/ \
--cfg-options eval.benchmarks=[gqa]
# 一次评测多个 benchmark
python scripts/eval.py \
--config configs/vlm/qwen3_vl_sft_vision2b_llm0_6b_stage2_sft.py \
--ckpt-path /path_to_checkpoint/ \
--cfg-options eval.benchmarks=[gqa,mmmu,docvqa]
备注
当前 VLM 评测 runner 支持多种 benchmark(例如 gqa、science_qa、textvqa、mmmu、seed、mathvista、ai2d、chartqa、docvqa、mmstar、realworldqa、ocrbench 等)。你只需在 eval.benchmarks=[...] 中填写需要的名称。
5. 训练时如何切换模型#
常见有两种方式:
方式 A:直接换配置文件(最推荐)#
把训练命令中的 config 路径替换为另一份模型配置:
bash scripts/train.sh /path/to/another_vlm_config.py /path_to_work_dir/
方式 B:在同一配置内切换 backbone#
以 configs/vlm/qwen3_vl_sft_vision2b_llm0_6b_stage2_sft.py 为例,核心字段在 model.vlm_backbone:
vlm_backbone_id:选择 VLM 架构/规格(例如不同 Qwen3-VL 变体)vlm_path:初始化权重路径(通常是预训练权重或上游阶段 checkpoint)attn_implementation:注意力实现(如flash_attention_2)
同时请检查与模型相关的处理器字段是否匹配,例如:
train_dataloader.dataset.processor_typetrain_dataloader.dataset.model_pathrunner.tokenizer.model_patheval.processor_type(如果该配置中启用)
6. 如何把某个 Vision Encoder + 某个 LLM 组装成新模型(以 stage1 配置为例)#
configs/vlm/qwen3_vl_sft_vision2b_llm0_6b_stage1_alignment.py 展示了一个典型范式:
用 Qwen3-VL-2B 的 Vision Encoder + Qwen3-0.6B 的 LLM 组合成新的 VLM,然后先做 Stage1 对齐训练(只训练 projection/merger)。
6.1 核心配置怎么写#
关键字段在 model.vlm_backbone:
vlm_backbone=dict(
type='Qwen3VL',
vlm_backbone_id='qwen3_2b_vl_pt',
vlm_path=_qwen3vl_2b,
vision_encoder_path=_qwen3vl_2b,
llm_backbone_path=_qwen3_0_6b,
use_projection=False,
attn_implementation='flash_attention_2',
)
字段含义:
type='Qwen3VL':使用支持“模块化加载”的 Qwen3VL backbone 包装器vlm_backbone_id:指定组装时使用的 VLM 配置模板族vlm_path:主模板路径(通常指向一个完整 VLM;这里用 2B VL)vision_encoder_path:视觉编码器来源(这里是 2B VL)llm_backbone_path:语言模型来源(这里是 0.6B LLM)attn_implementation:底层注意力实现
6.2 代码层面实际发生了什么#
当你同时设置 vision_encoder_path 或 llm_backbone_path 时,会触发 modular loading(模块化加载):
以
vision_encoder_path(或vlm_path)为模板,先构建 Qwen3VL 配置若设置了
llm_backbone_path,会把 LLM 关键结构参数合并到text_config自动把
vision_config.out_hidden_size对齐到 LLM hidden size,保证视觉特征能接到新 LLM先从视觉模板加载可匹配的视觉参数,再从
llm_backbone_path加载 LLM 参数若 merger 最后一层维度不一致,会做“按输出维度裁剪拷贝”来完成初始化
这就是为什么它能把“2B 视觉 + 0.6B 语言”拼成一个可训练的新模型。
6.3 Stage1 为什么只训练 projection/merger#
在该配置中:
freeze_vision_encoder=True
freeze_projection=False
freeze_backbone=True
含义是:
冻结 Vision Encoder
冻结 LLM Backbone
只训练视觉-语言连接处(projection/merger)
这样做的目标是先完成跨模块对齐,再在 Stage2 放开更多参数微调。
6.4 如果你想换成“自己的 Vision + 自己的 LLM”#
直接复用这个模式即可,最小改动是这几项:
把
vision_encoder_path改成你的视觉模型路径把
llm_backbone_path改成你的 LLM 路径vlm_path建议仍指向与你视觉分支同族的 VLM 模板(用于配置与 processor)train_dataloader.dataset.model_path、runner.tokenizer.model_path通常与模板族保持一致先用 Stage1 冻结策略训练验证稳定性,再进入 Stage2
可直接参考:
_my_vision = '/path/to/your_vision_encoder_or_vl_template'
_my_llm = '/path/to/your_llm'
model = dict(
type='VLMForSFT',
vlm_backbone=dict(
type='Qwen3VL',
vlm_backbone_id='qwen3_2b_vl_pt',
vlm_path=_my_vision,
vision_encoder_path=_my_vision,
llm_backbone_path=_my_llm,
use_projection=False,
attn_implementation='flash_attention_2',
),
freeze_vision_encoder=True,
freeze_projection=False,
freeze_backbone=True,
)
备注
为了避免兼容性问题,建议先在同一模型家族内组合(例如 Qwen3-VL 系列视觉分支 + Qwen3 系列 LLM)。跨家族组合可能会在 tokenizer、特殊 token、配置字段或权重映射上出现不匹配,需要额外适配代码。
7. 训练时如何设置冻结某些模块#
本仓库 VLMForSFT 支持 3 个常用冻结开关(在 model 下):
freeze_vision_encoder:是否冻结视觉编码器freeze_projection:是否冻结视觉-语言投影/mergerfreeze_backbone:是否冻结语言骨干(LLM)
示例组合(适合快速实验):
# 仅训练 projection(类似 stage1 alignment)
model = dict(
# ...
freeze_vision_encoder=True,
freeze_projection=False,
freeze_backbone=True,
)
# 训练 projection + LLM,冻结视觉(类似 stage2 SFT)
model = dict(
# ...
freeze_vision_encoder=True,
freeze_projection=False,
freeze_backbone=False,
)
# 全参数训练(全部不冻结)
model = dict(
# ...
freeze_vision_encoder=False,
freeze_projection=False,
freeze_backbone=False,
)
你也可以不改配置文件,直接命令行覆盖:
bash scripts/train.sh \
/root/code/fluxvla/configs/vlm/qwen3_vl_sft_vision2b_llm0_6b_stage2_sft.py \
/path_to_work_dir/ \
--cfg-options \
model.freeze_vision_encoder=True \
model.freeze_projection=False \
model.freeze_backbone=True
8. 配置文件常用参数说明#
下面按“最常改、最影响结果”的优先级说明该配置里的关键参数。
8.1 模型相关(model)#
_stage1_ckpt:Stage2 SFT 的初始化权重路径(通常指向 Stage1 产出的 HF checkpoint 目录)model.vlm_backbone.vlm_backbone_id:选择 backbone 规格(如 2B/其他变体)model.vlm_backbone.vlm_path:backbone 实际加载的权重路径(通常与_stage1_ckpt保持一致)model.vlm_backbone.attn_implementation:注意力实现,常用flash_attention_2model.freeze_vision_encoder:是否冻结视觉编码器model.freeze_projection:是否冻结 visual merger/projectionmodel.freeze_backbone:是否冻结语言骨干
8.2 数据相关(train_dataloader.dataset)#
data_sources:训练数据清单(可配多个 json/jsonl,支持混合训练)data_root:相对路径数据的根目录;若images/videos使用绝对路径,可设为Noneprocessor_type:数据处理器类型,要和模型族匹配(此处是qwen3_vl)model_path:processor/tokenizer 读取路径,通常与当前模型权重路径一致image_max_pixels/video_max_pixels:图像/视频最大像素预算;调大更保细节但更占显存truncation_max_length:文本截断长度;调大可保更多上下文但会增显存和训练时长statistic_name:统计项命名,用于日志/统计文件区分实验
8.3 训练超参(train_dataloader + runner)#
train_dataloader.per_device_batch_size:单卡 batch size(影响显存和吞吐)train_dataloader.per_device_num_workers:DataLoader worker 数(影响数据加载速度)runner.max_epochs:训练轮数runner.learning_rate:学习率(对收敛速度和稳定性影响最大)runner.weight_decay:权重衰减(正则化强度)runner.max_grad_norm:梯度裁剪阈值(抑制梯度爆炸)runner.lr_scheduler_type/runner.warmup_ratio:学习率调度和 warmup 比例runner.enable_gradient_checkpointing:省显存开关(通常会增加一些计算开销)runner.enable_mixed_precision_training+runner.mixed_precision_dtype:混合精度训练配置(常用 bf16)
8.4 评测相关(eval)#
eval.benchmarks:要评测的数据集列表(最常改)eval.max_samples_per_benchmark:每个 benchmark 采样上限(快速验证常设为小整数)eval.batch_size:评测 batch size(受显存限制)eval.max_new_tokens:最大生成长度eval.use_llm_judge:是否启用 LLM API 作为评测 judge(关闭时通常走 exact matching)eval.verbose:是否打印详细推理输出(调试时很有用)eval.output_dir:评测输出目录(不设时用默认目录)
8.5 最常用命令行覆盖示例#
# 不改配置文件,直接覆盖训练关键超参
bash scripts/train.sh \
/root/code/fluxvla/configs/vlm/qwen3_vl_sft_vision2b_llm0_6b_stage2_sft.py \
/path_to_work_dir/ \
--cfg-options \
train_dataloader.per_device_batch_size=8 \
runner.learning_rate=2e-5 \
runner.max_epochs=4 \
model.freeze_vision_encoder=True \
model.freeze_projection=False \
model.freeze_backbone=False
# 快速评测:改 benchmark + 限制样本数
python scripts/eval.py \
--config configs/vlm/qwen3_vl_sft_vision2b_llm0_6b_stage2_sft.py \
--ckpt-path /path_to_checkpoint/ \
--cfg-options eval.benchmarks=[gqa,realworldqa] eval.max_samples_per_benchmark=100
9. 常见输出位置#
训练目录 /path_to_work_dir/ 下通常会包含:
checkpoint_*.pt:训练 checkpointcheckpoints/hf_step-*(如配置启用 HF 导出):HF 格式模型目录config.yaml/config.json:训练时配置快照*.jsonl:训练日志
评测日志会输出到终端,并根据配置写入评测输出目录(例如 outputs/vlm_eval)。