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 如果使用你自己的数据集,如何构建#

推荐流程如下:

  1. 先整理原始数据(图片/视频 + 指令 + 标注答案)

  2. 转换成 LLaVA 格式 jsonjsonl(字段见 1.1)

  3. 在配置中把 train_dataloader.dataset.data_sources 指向你的清单文件

  4. 如使用相对路径,设置 train_dataloader.dataset.data_root 为数据根目录

  5. 先用小样本试跑(例如几百条)验证无格式错误,再全量训练

一个简单的“原始标注 -> 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 条数据:

  1. 每条 conversations 至少包含一轮 human/gpt;

  2. <image>/<video> 数量与文件列表一致;

  3. 路径可在训练机上真实访问。

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(例如 gqascience_qatextvqammmuseedmathvistaai2dchartqadocvqammstarrealworldqaocrbench 等)。你只需在 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_type

  • train_dataloader.dataset.model_path

  • runner.tokenizer.model_path

  • eval.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_pathllm_backbone_path 时,会触发 modular loading(模块化加载):

  1. vision_encoder_path(或 vlm_path)为模板,先构建 Qwen3VL 配置

  2. 若设置了 llm_backbone_path,会把 LLM 关键结构参数合并到 text_config

  3. 自动把 vision_config.out_hidden_size 对齐到 LLM hidden size,保证视觉特征能接到新 LLM

  4. 先从视觉模板加载可匹配的视觉参数,再从 llm_backbone_path 加载 LLM 参数

  5. 若 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”#

直接复用这个模式即可,最小改动是这几项:

  1. vision_encoder_path 改成你的视觉模型路径

  2. llm_backbone_path 改成你的 LLM 路径

  3. vlm_path 建议仍指向与你视觉分支同族的 VLM 模板(用于配置与 processor)

  4. train_dataloader.dataset.model_pathrunner.tokenizer.model_path 通常与模板族保持一致

  5. 先用 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:是否冻结视觉-语言投影/merger

  • freeze_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_2

  • model.freeze_vision_encoder:是否冻结视觉编码器

  • model.freeze_projection:是否冻结 visual merger/projection

  • model.freeze_backbone:是否冻结语言骨干

8.2 数据相关(train_dataloader.dataset#

  • data_sources:训练数据清单(可配多个 json/jsonl,支持混合训练)

  • data_root:相对路径数据的根目录;若 images/videos 使用绝对路径,可设为 None

  • processor_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:训练 checkpoint

  • checkpoints/hf_step-*(如配置启用 HF 导出):HF 格式模型目录

  • config.yaml / config.json:训练时配置快照

  • *.jsonl:训练日志

评测日志会输出到终端,并根据配置写入评测输出目录(例如 outputs/vlm_eval)。