LoRA 微调 VLM 2B~8B:参数、显存、踩坑记录
覆盖 Qwen2.5-VL / Qwen3-VL / InternVL 2B~8B 模型,聚焦可直接使用的参数与代码。
一、VLM 和 LLM 微调的区别
LLM 是单一模型,所有参数属于同一个 Transformer,微调时整个模型共享一套学习率和策略。VLM 则由三个功能完全不同的模块拼接而成,每个模块对学习率、冻结策略、量化方式的要求各不相同:
图像 → [Vision Encoder (ViT)] → [Projector / Merger] → [LLM 主干] → 文本输出
预训练视觉特征提取 跨模态对齐 语言生成
通常冻结 小数据冻结/大数据训练 LoRA 微调这带来几个 LLM 微调中不存在的问题:
1. 模块间学习率必须分层
LLM 微调只需设一个学习率。VLM 如果三个模块用同一个 LR,ViT 的预训练特征会被破坏(视觉能力退化),而 LLM 部分又学不够快。实践中 Vision LR 必须是 LLM LR 的 1/10 以内。
2. 图像 token 占上下文但不算 loss
LLM 的每个 input token 要么参与 loss 计算,要么是被 mask 的 prompt(至少语义上有意义)。VLM 中一张图片经 ViT 编码后产生数百到数千个视觉 token(Qwen2.5-VL 一张 1080p 图 ≈ 2584 token),这些 token 全部被 mask(label=-100),不参与 loss 但占满了 context window。一条 VLM 样本真正算 loss 的 token 可能只有 12%。
3. 分辨率直接影响显存和训练速度
LLM 的序列长度由文本决定,变化范围有限。VLM 的序列长度很大程度取决于图像分辨率 —— 同一张图片,不限制分辨率 vs 压缩到 512px,视觉 token 数可以差 10 倍以上,显存需求完全不同。
4. 量化有额外限制
LLM 做 QLoRA 时整个模型统一量化到 4bit。VLM 的 ViT 不能量化 —— 量化 vision tower 会导致训练崩溃。此外,训练 embed_token 时必须同步给 lm_head 挂 LoRA,因为两者权重共享,缺一侧会训练发散。
各模块的默认策略:
| 模块 | 典型组件 | 默认策略 | 学习率 |
|---|---|---|---|
| Vision Encoder(ViT) | Qwen2.5-VL 675M ViT、InternViT-300M | 冻结 | — |
| Projector / Merger | MLP、Resampler | 训练(>5k 样本)/ 冻结(<5k 样本) | 1e-5 |
| LLM 主干 | Qwen2、InternLM2 等 | LoRA all-linear | 1e-4 |
from peft import LoraConfig, get_peft_model
def build_vlm_lora(model, r=16, alpha=32, dropout=0.05):
skip_keywords = ("visual", "vision", "patch_embed", "img_projection")
linear_names = set()
for name, module in model.named_modules():
if any(k in name for k in skip_keywords):
continue
if isinstance(module, torch.nn.Linear):
linear_names.add(name.split(".")[-1])
config = LoraConfig(
r=r,
lora_alpha=alpha,
lora_dropout=dropout,
target_modules=list(linear_names),
bias="none",
task_type="CAUSAL_LM",
)
return get_peft_model(model, config)二、模型选型与参数规格
2.1 Qwen 系列
Qwen2.5-VL:全系列共用 675M ViT,patch size 14×14。
| 模型 | 总参数 | ViT | LLM | Merger 输出维度 |
|---|---|---|---|---|
| Qwen2.5-VL-3B | ~3.0B | 675M | 2.3B Qwen2.5 | 2048 |
| Qwen2.5-VL-7B | ~7.0B | 675M | 6.3B Qwen2.5 | 3584 |
Qwen3-VL:改用分级 ViT(SigLIP2),patch size 16×16,token 数比 Qwen2.5-VL 少 21%。
| 模型 | 总参数 | ViT | LLM |
|---|---|---|---|
| Qwen3-VL-2B | ~2.0B | SigLIP2-Large 300M | 1.7B Qwen3 |
| Qwen3-VL-4B | ~4.0B | SigLIP2-Large 300M | 3.7B Qwen3 |
| Qwen3-VL-8B | ~8.6B | SigLIP2-SO 400M | 8.2B Qwen3 |
2.2 InternVL 系列
统一 ViT-MLP-LLM 三段式架构,1B~14B 使用 InternViT-300M。
| 模型 | ViT | LLM | 总参数 |
|---|---|---|---|
| InternVL2/2.5-2B | InternViT-300M | InternLM2-1.8B | ~2.2B |
| InternVL2.5-4B | InternViT-300M | Qwen2.5-3B | ~3.5B |
| InternVL2/2.5-8B | InternViT-300M | InternLM2.5-7B | ~8.0B |
| InternVL3-2B | InternViT-300M | Qwen2.5-1.5B | ~2.0B |
| InternVL3-8B | InternViT-300M | Qwen2.5-7B | ~8.0B |
2.3 LoRA 可训练参数量(all-linear,LLM 部分)
| 模型 | hidden_dim | layers | r=16 参数量 | r=64 参数量 |
|---|---|---|---|---|
| Qwen2.5-VL-3B | 2048 | 36 | ~16.5M | ~66M |
| Qwen2.5-VL-7B | 3584 | 28 | ~22.4M | ~90M |
| Qwen3-VL-4B | 2560 | 36 | ~20.6M | ~83M |
| Qwen3-VL-8B | 4096 | 32 | ~29.4M | ~118M |
三、LoRA 超参数
3.1 Rank 与 Alpha
| 参数 | 推荐值 | 说明 |
|---|---|---|
| r | 16(起点) | 简单分类用 r=8;结构化输出/多任务用 r=32~64 |
| alpha | r 或 2r | 高 rank(≥32)时 alpha/r=0.5 也常见 |
| scaling | alpha/r | 本质是权重更新的缩放系数,与 LR 联动 |
小数据场景的过拟合规律(实验数据):
| 配置 | 总步数 | 最优步数 | 浪费比例 |
|---|---|---|---|
| r=16, epochs=10 | 2200 | 1320(ep6) | 40% |
| r=64, epochs=10 | 4400 | 1320(ep3) | 70% |
| r=16, epochs=10,数据清洗后 | 4400 | 2200(ep5) | 50% |
| r=64, 复杂任务 | 2800 | 930(ep5) | 67% |
r=64 过拟合比 r=16 严重得多。数据质量带来的收益远大于提高 rank:相同数据量清洗 5% 错误标签,任务准确率可提升 4~5 个百分点。
两条进阶选项:
- rsLoRA(
use_rslora=True):缩放改为 alpha/√r,rank ≥ 32 时建议开启,防止梯度不稳 - DoRA:权重分解为方向+幅度,通常有 1~2pp 稳定提升,训练慢约 20%
3.2 Target Modules
只挂 q_proj, v_proj 是 LoRA 原论文写法,不是最优。推荐挂所有 linear:
target_modules = [
"q_proj", "k_proj", "v_proj", "o_proj", # Attention
"gate_proj", "up_proj", "down_proj", # FFN
]
# LLaMA-Factory 等价写法:lora_target: all代价:可训练参数增加约 3.5 倍,显存 +15%。收益:下游指标通常提升 2~5pp。2B~4B 模型全挂 LoRA 压力不大,7B+ 建议开 QLoRA 配合。
3.3 Dropout 与正则
| 参数 | 推荐 | 说明 |
|---|---|---|
| lora_dropout | 0.05~0.1 | epochs≤2 用 0.05;epochs=5 用 0.1 |
| bias | "none" | 训练 bias 收益极小 |
| weight_decay | 0.01 | HF Trainer 默认值 |
Dropout 和训练长度必须配套。高 LR + 长训练必须用更强正则。
四、训练超参数
4.1 学习率
LoRA 只训练低秩增量矩阵(A·B),参数量 <1%,梯度不影响预训练权重。因此 LoRA LR 可以比全参微调高 10~100 倍。
| 微调方式 | 典型 LR |
|---|---|
| Full fine-tuning | 2e-6 |
| LoRA(通用) | 1e-4 |
| QLoRA | 2e-4 |
| VeOmni (ByteDance) | 3e-4(warmup=0.007) |
按模型规格细分:
| 模型规格 | LoRA LR | 全参 FT LR |
|---|---|---|
| 2B-3B | 2e-4~5e-4 | 2e-5 |
| 4B | 1e-4~3e-4 | 1e-5~2e-5 |
| 7B-8B | 1e-4~2e-4 | 1e-5 |
更大的模型对 LR 更敏感,建议偏低选择。
4.2 Epochs 与 Warmup
- Epochs:通用场景 2~3 epoch 足够,小数据(<5000 样本)给 5 epoch + early_stop
- Warmup:LoRA 参数少,梯度方差天然小,warmup_ratio ≤ 0.03 足够,超过 0.05 是浪费
4.3 其他关键参数
| 参数 | 推荐值 | 说明 |
|---|---|---|
| lr_scheduler | cosine | warmup 后余弦衰减 |
| per_device_batch_size | 1~4 | VLM 瓶颈在图像 token 数量 |
| gradient_accumulation_steps | eff_bs = 16~128 | 视任务复杂度 |
| optimizer | adamw_torch | AdamW 即可 |
| precision | bf16 | A100/H100/4090 均支持 |
| gradient_checkpointing | True | 显存减少 50%+,速度损失约 20% |
| max_grad_norm | 1.0 | 高 LR 下防梯度爆炸 |
| cutoff_len | 2048~8192 | 图像 token 已占数百个 |
4.4 只对 Response 计算 Loss
多轮对话中,user 部分不参与 loss 计算可提升约 1% 精度并减少一半无效计算:
def mask_user_tokens(labels, input_ids, assistant_token_id):
in_response = False
for i, tid in enumerate(input_ids):
if tid == assistant_token_id:
in_response = True
if not in_response:
labels[i] = -100 # HF Trainer 忽略 -100
return labels五、显存规划
5.1 GPU 需求
| 模型规格 | LoRA BF16(单卡) | QLoRA 4bit(单卡) | 推荐 batch_size | 推荐 grad_accum |
|---|---|---|---|---|
| 2B-3B | 1× 24GB | 1× 12GB | 2-4 | 4-8 |
| 4B | 1× 24GB | 1× 16GB | 1-2 | 8-16 |
| 7B-8B | 2× 24GB 或 1× 80GB | 1× 24GB | 1 | 8-16 |
含 1 张 1920×1080 图像(约 2000~2600 token)的估算:
| 模型 | LoRA BF16 训练显存 | QLoRA 4bit 训练显存 |
|---|---|---|
| 2B-3B | ~10-14 GB | ~6-8 GB |
| 4B | ~16-20 GB | ~8-12 GB |
| 7B-8B | ~28-40 GB | ~10-16 GB |
2B 模型单卡参考(RTX 4090 24GB):
| 配置 | 显存 |
|---|---|
| batch=1, cutoff=1024, 单图 | ~14 GB |
| batch=2, grad_accum=8, cutoff=1024 | ~20 GB |
| 全分辨率(1920×1080),batch=1, cutoff=8192 | ~18 GB |
5.2 QLoRA 4bit
| 指标 | 全参微调 | LoRA BF16 | QLoRA 4bit |
|---|---|---|---|
| 7B 训练显存 | ~100-120 GB | ~28 GB | ~8-10 GB |
| 性能保留 | 100% | 90-95% | 80-90% |
| 适用 GPU | 8× H100 | 1× A100 80GB | 1× 4090 24GB |
from transformers import BitsAndBytesConfig
import torch
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
)
model = AutoModelForCausalLM.from_pretrained(
model_path, quantization_config=bnb_config, device_map="auto"
)
model = prepare_model_for_kbit_training(model)5.3 Gradient Checkpointing
7B+ 模型几乎必需。7B BF16 + AdamW 显存分布:参数 14GB + 梯度 14GB + 优化器 56GB + 激活值 20~40GB,其中激活值可通过 gradient checkpointing 减少 50%+,训练速度损失约 20%。
args = TrainingArguments(
gradient_checkpointing=True,
gradient_checkpointing_kwargs={"use_reentrant": False}, # 与 PEFT 兼容
)六、图像分辨率与 VLM 特有问题
6.1 Qwen2.5-VL 动态分辨率机制
图像以原始宽高比处理,不强制裁剪:
- 宽高取整为 28 的整数倍(14×14 patch 经 2×2 merge,每 token 覆盖 28×28 px)
visual_tokens = (H/28) × (W/28)
preprocessor_config.json 中默认 max_pixels 是架构上限(12,845,056),不是推荐训练值。1920×1080 不限制时产生 2584 个 token,4K 图像可达 10,000+,直接 OOM。
6.2 Qwen3-VL 的变化
patch size 从 14→16,取整倍数从 28→32,每 token 覆盖 1024px(vs 784px):
| 系列 | 取整分辨率 | 1920×1080 视觉 Token |
|---|---|---|
| Qwen2.5-VL | 1904×1064 | 68×38 = 2,584 |
| Qwen3-VL | 1920×1088 | 60×34 = 2,040 |
Qwen3-VL 相同图像少 21% token,显存和速度直接受益。
此外,Qwen3-VL 修复了 Qwen2.5-VL 的 MRoPE 频率不均衡问题(Interleaved MRoPE),长视频位置编码更稳定。
6.3 Image 参数对照
不能将 Qwen2.5-VL 的配置直接复制到 Qwen3-VL,patch size 28→32 会导致静默错误缩放:
| 参数 | Qwen2.5-VL | Qwen3-VL |
|---|---|---|
image_min_pixels |
256 × 28² = 200,704 | 256 × 32² = 262,144 |
image_max_pixels |
1280 × 28² = 1,003,520 | 1280 × 32² = 1,310,720 |
# Qwen2.5-VL
processor.image_processor.min_pixels = 256 * 28 * 28
processor.image_processor.max_pixels = 1280 * 28 * 28
# Qwen3-VL(必须用 32)
processor.image_processor.min_pixels = 256 * 32 * 32
processor.image_processor.max_pixels = 1280 * 32 * 326.4 常见图像陷阱
截断图像:训练报 IOError: image file is truncated。
from PIL import ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True
# 或在数据集加载时预过滤
from PIL import Image
def filter_corrupt(data_list):
valid = []
for item in data_list:
try:
Image.open(item["image"]).verify()
valid.append(item)
except Exception:
pass
return valid全分辨率 vs 压缩:需要坐标精度(GUI Agent、OCR 定位)必须保持全分辨率,不设 image_max_pixels 上限。纯分类任务可以压缩以节省显存。
七、基线配置
7.1 通用 VLM 微调(数据量 >3000,无坐标精度要求)
# LLaMA-Factory 配置
model_name_or_path: Qwen/Qwen2-VL-2B-Instruct
finetuning_type: lora
lora_target: all
lora_rank: 16
lora_alpha: 32
lora_dropout: 0.05
freeze_vision_tower: true
freeze_merger: false
learning_rate: 1.0e-4
vision_lr: 2.0e-6
merger_lr: 1.0e-5
lr_scheduler_type: cosine
warmup_ratio: 0.03
weight_decay: 0.01
optim: adamw_torch
max_grad_norm: 1.0
per_device_train_batch_size: 2
gradient_accumulation_steps: 8
num_train_epochs: 3
cutoff_len: 2048
bf16: true
gradient_checkpointing: true
image_max_pixels: 1003520
image_min_pixels: 200704
val_size: 0.05
eval_strategy: steps
eval_steps: 200
save_steps: 200
load_best_model_at_end: true
metric_for_best_model: eval_loss7.2 小数据 + 全分辨率(数据量 <5000,需要坐标精度)
# 与通用配置的差异
lora_dropout: 0.1 # 训练更长,需要更强正则
freeze_merger: true # 数据不足以安全训练 merger
num_train_epochs: 5
early_stopping: true
early_stopping_patience: 2
cutoff_len: 8192
# 不设 image_max_pixels # 保持全分辨率
per_device_train_batch_size: 1
gradient_accumulation_steps: 87.3 两套配置对比
| 维度 | 通用 | 小数据全分辨率 |
|---|---|---|
| 数据量 | >3000 | <5000 |
| 图像分辨率 | 压缩(max_pixels 限制) | 全分辨率 |
| Epochs | 3 | 5 + early_stop |
| Dropout | 0.05 | 0.1 |
| Merger | 训练 | 冻结 |
| 坐标精度 | 无要求 | 需要(<30px) |
八、过拟合:分析与防治
8.1 规律
小数据 VLM 微调的过拟合模式高度一致:
- 最优点集中在 epoch 3~6,之后 eval_loss 持续上升
- train_loss 后期降到极低(~0.001),eval_loss 反弹 30~50%
- r=64 过拟合比 r=16 严重,且数据越少越明显
- 最优 epoch 由数据量和任务复杂度决定,与超参关系不大
8.2 防过拟合策略组合
from transformers import TrainingArguments, EarlyStoppingCallback
args = TrainingArguments(
num_train_epochs=5,
eval_strategy="steps",
eval_steps=200,
save_steps=200,
load_best_model_at_end=True,
metric_for_best_model="eval_loss",
greater_is_better=False,
save_total_limit=3,
)
early_stopping = EarlyStoppingCallback(early_stopping_patience=2)8.3 数据质量是核心
- 500~5000 高质量样本通常已足够(2B~3B 模型);超过 1w 收益边际递减
- 相同数据量,清洗 5% 的错误标签比调高 rank 的收益大
- Response 格式、长度、JSON 结构必须统一,模型无法同时学习多种输出风格
8.4 训练稳定性信号
| 信号 | 含义 | 处理 |
|---|---|---|
| train_loss < 0.1,eval_loss 反弹 | 过拟合 | 检查 early_stopping 是否生效 |
| loss 不降 | 数据或格式问题 | 检查 <image> token 位置和 chat template |
| 梯度爆炸 | LR 过高 | 设 max_grad_norm=1.0,降低 LR |
| 灾难性遗忘 | 垂域数据比例过高 | 混入 5~20% 通用 VQA 数据 |
九、多卡训练(FSDP)
9.1 策略选择
| 场景 | 推荐策略 |
|---|---|
| LoRA,单卡放得下 | DDP |
| LoRA,7B+ 单卡放不下 | SHARD_GRAD_OP(ZeRO-2) |
| 全参微调 | FULL_SHARD(ZeRO-3) |
LoRA 已大幅减少可训练参数,FULL_SHARD 的主要收益对 LoRA 场景不关键。VLM 的序列并行比参数分片更有价值。
| 策略 | 等价 ZeRO | 分片内容 |
|---|---|---|
| NO_SHARD | DDP | 无 |
| SHARD_GRAD_OP | ZeRO-2 | 梯度 + 优化器状态 |
| FULL_SHARD | ZeRO-3 | 参数 + 梯度 + 优化器 |
| HYBRID_SHARD | — | 多节点优化 |
9.2 LoRA Adapter 保存(已知 Bug)
FSDP 保存的 state_dict 中 LoRA key 不含 adapter name,PEFT 加载时期望 lora_A.default.weight 格式,导致 key 不匹配。
from transformers import TrainerCallback
import os
class PeftSavingCallback(TrainerCallback):
def on_save(self, args, state, control, **kwargs):
ckpt = os.path.join(args.output_dir, f"checkpoint-{state.global_step}")
kwargs["model"].save_pretrained(ckpt)
return controlaccelerate config 必须设置:
fsdp_config:
fsdp_use_orig_params: true # 关键:支持 LoRA 冻结+可训练参数混合
fsdp_sharding_strategy: SHARD_GRAD_OP
fsdp_state_dict_type: FULL_STATE_DICT9.3 NCCL 超时
FSDP 每步触发 all-gather + reduce-scatter,默认 30 分钟超时容易触发:
export NCCL_ASYNC_ERROR_HANDLING=1
export NCCL_TIMEOUT=1800
export TORCH_NCCL_HEARTBEAT_TIMEOUT_SEC=1800
export NCCL_P2P_LEVEL=NVLimport torch.distributed as dist
from datetime import timedelta
dist.init_process_group(backend="nccl", timeout=timedelta(seconds=3600))Docker 启动必须加 --shm-size=10g --ulimit memlock=-1,默认 64MB shared memory 不够。
十、训练监控
10.1 解析 trainer_state.json(不需要 WandB)
HF Trainer 每个 checkpoint 自动保存 trainer_state.json,包含完整训练日志:
import json
from pathlib import Path
def show_progress(output_dir: str):
checkpoints = sorted(
Path(output_dir).glob("checkpoint-*"),
key=lambda p: int(p.name.split("-")[1])
)
if not checkpoints:
return
state = json.load(open(checkpoints[-1] / "trainer_state.json"))
logs = state["log_history"]
trains = [l for l in logs if "loss" in l and "eval_loss" not in l]
evals = [l for l in logs if "eval_loss" in l]
print(f"Progress : {state['global_step']} / {state['max_steps']}")
print(f"Best ckpt: {state.get('best_model_checkpoint', 'N/A')}")
print(f"Best val : {state.get('best_metric', 'N/A')}\n")
print("Eval history:")
for e in evals[-5:]:
t = next((x["loss"] for x in reversed(trains) if x["step"] <= e["step"]), None)
gap = e["eval_loss"] - t if t else float("nan")
print(f" step {e['step']:5d} | eval={e['eval_loss']:.4f} | gap={gap:+.4f}")
print("Train (recent):")
for t in trains[-3:]:
print(f" step {t['step']:5d} | loss={t['loss']:.4f} | lr={t.get('learning_rate', 0):.2e}")
show_progress("output/my_model")后台训练时实时查看:
watch -n 60 python3 monitor.py output/my_model10.2 TensorBoard(WandB 替代)
args = TrainingArguments(
report_to="tensorboard",
logging_dir="runs/exp1",
logging_steps=10,
)# 启动
tensorboard --logdir runs/ --port 6006
# 远程服务器
ssh -L 6006:localhost:6006 user@server10.3 WandB:自定义 train/eval gap
import wandb
from transformers import TrainerCallback
class GapMonitorCallback(TrainerCallback):
def on_log(self, args, state, control, logs=None, **kwargs):
if not (logs and state.is_world_process_zero):
return
metrics = {k: v for k, v in logs.items()
if k in ("loss", "eval_loss", "learning_rate", "grad_norm")}
trains = [l for l in state.log_history if "loss" in l and "eval_loss" not in l]
evals = [l for l in state.log_history if "eval_loss" in l]
if trains and evals:
metrics["train_eval_gap"] = evals[-1]["eval_loss"] - trains[-1]["loss"]
if metrics:
wandb.log(metrics, step=state.global_step)十一、调参顺序与速查
11.1 实验顺序(节省 GPU 时间)
每次只动一组变量:
- 跑通基线:确认 loss 正常下降
- 数据质量 A/B:换不同数据子集,通常这步收益最大
- Rank 扫描:r ∈ {8, 16, 32, 64}
- LR 扫描:lr ∈ {5e-5, 1e-4, 2e-4, 3e-4}
- Target modules:Q/V only vs all-linear
- Merger / ViT 解冻:图像分布与自然图像差异较大时再试
- rsLoRA / DoRA:基线稳定后的进阶选项
11.2 综合参数速查
| 参数 | 2B-3B | 4B | 7B-8B |
|---|---|---|---|
| LoRA rank | 16 | 16-32 | 16-32 |
| LoRA alpha | 16-32 | 32 | 32-64 |
| LoRA target | all-linear | all-linear | all-linear |
| LR | 2e-4~5e-4 | 1e-4~3e-4 | 1e-4~2e-4 |
| Warmup ratio | 0.03 | 0.03 | 0.03 |
| Batch size | 2-4 | 1-2 | 1 |
| Grad accum | 4-8 | 8-16 | 8-16 |
| Epochs | 3-5 + early_stop | 1-3 | 1-3 |
| Gradient checkpointing | 可选 | 推荐 | 必需 |
| 分布式策略 | DDP | DDP / SHARD_GRAD_OP | SHARD_GRAD_OP |
| QLoRA(单卡) | 12GB | 16-24GB | 24GB |
| Flash Attention 2 | 推荐 | 推荐 | 必需 |
11.3 经验法则
- LR:LoRA 用 1e-4,QLoRA 用 2e-4,Full FT 用 1e-5~1e-6
- 数据 > 参数:清洗 5% 错误标签 > 任何超参调整
- Rank:小数据 r=16,大数据/多任务再考虑 r=64
- Warmup:≤ 0.03
- Dropout:epochs≤2 用 0.05,epochs=5 用 0.1
- 冻结 ViT:分布贴近自然图像时冻结,省 40% 显存
- 全分辨率:坐标精度任务必须保持,纯分类可压缩
- Merger:数据量 >5000 训练,<5000 冻结
- Early stopping:必须开,patience=2 对小数据集足够
11.4 Qwen2.5-VL → Qwen3-VL 迁移检查清单
- 所有
28*28改为32*32(image_min/max_pixels) - 确认
mrope_interleaved: true - Qwen3-VL ViT 更小(300M vs 675M),可考虑解冻 ViT
- 相同图像 token 数少 21%,可适当增大 batch_size
- LLaMA-Factory 已支持 Qwen3-VL,检查最新版本配置格式
References
- Unsloth · LoRA Hyperparameters Guide
- Sebastian Raschka · Practical Tips for Finetuning LLMs Using LoRA
- InternVL · LoRA Finetune 文档
- Qwen-VL-Series-Finetune
- Dettmers et al. · QLoRA
- LIMA · Less Is More for Alignment
- VeOmni · ByteDance Training Framework
- Qwen3-VL Technical Report
- HuggingFace PEFT · FSDP
- Learning Rate Matters for LoRA
补充:多阶段 LoRA 流水线中的 Rank 选择(2026-07 消融实验)
本节基于 Qwen3-VL-8B 三阶段 LoRA 串联训练的消融实验,补充上文 LoRA rank 取 4~32 就够结论的适用边界。
背景
上文建议 8B 模型用 r=4~32,这个建议适用于单阶段任务微调。但在多阶段串联流水线中,首阶段的 rank 选择需要额外考虑。
我们的三阶段流水线:
Qwen3-VL-8B-Instruct
-> S1 LoRA (r=64, 170M params) + merge # 游戏 UI 理解预训练
-> S2P1 LoRA (r=8, 21M params) + merge # 动作类型学习
-> S2P2 LoRA (r=8, 21M params) + merge # 完整 Agent 能力
= v5.1 (50px=81.2%)消融实验结果
| 版本 | S1 | S2P1 | S2P2 | 50px% | vs v5.1 |
|---|---|---|---|---|---|
| v5.1 (baseline) | r=64 | r=8 | r=8 | 81.2 | - |
| v5.1d (只 S2P2) | 无 | 无 | r=8 | 76.2 | -5.0pp |
| v5.1e (全 r=8) | r=8 | r=8 | r=8 | 75.7 | -5.5pp |
| v5.1c (跳 S1) | 无 | r=8 | r=8 | 75.1 | -6.1pp |
核心发现:S1 是领域适配预训练
S1 的表面任务是 13 类 screen_type 分类(3512 条数据),但它的真正效果是领域适配:用 170M 参数把 LLM 从通用模型改造成理解游戏 UI 的专用模型。这和 S2P1/S2P2 学习具体输出格式的任务微调本质不同。
S1 r=8 甚至不如完全跳过 S1(v5.1e 75.7% < v5.1d 76.2%),说明 r=8 的参数量不足以完成领域适配,反而引入有害的不完整知识。
串联 merge 的生存性问题
多阶段 LoRA 的独特约束:每个阶段 merge 后,后续阶段的 LoRA 会覆盖前一阶段的修改。S1 r=64 修改了 64 个独立的权重方向,后续 S2P1(r=8) 最多只能覆盖其中 8 个,剩余 56 个方向的领域知识保留下来。如果 S1 也用 r=8,那 8 个方向会被 S2P1 完全覆盖,领域知识全部丢失。
Rank 选择建议(修订)
| 场景 | 推荐 rank | 原因 |
|---|---|---|
| 单阶段任务微调 | r=4~32 | 本文上半部分的结论,依然成立 |
| 多阶段流水线 - 任务微调阶段 | r=4~32 | 学习具体的输出格式 |
| 多阶段流水线 - 领域适配阶段 | r=64+ | 需要大参数量做 domain shift,且修改要能在后续 merge 中存活 |
总结:r=4~32 的建议适用于任务微调。当首阶段承担领域适配角色时,需要 r=64+ 以确保领域知识在串联 merge 后存活。判断依据是该阶段的目的:学新格式(低 rank)还是改造模型的领域理解(高 rank)。