
文章目录前言一、原理三块积木拼出来的多模态1. 结构LLM Align Layer ViT2. 损失计算二、论文里的实验三、复现8×L20 上的双阶段训练1. stage1Align 层对齐训练2. stage2Visual 指令微调3. 实验结果POPE 和 MME总结前言LLaVA 是开源多模态大模型里最好上手的项目之一。它把视觉编码器和语言模型拼在一起分两阶段训练。论文和代码都公开可真在 8 卡机器上跑一遍坑一点不少。本文先讲结构再看论文实验最后给出 8×L20 上的复现脚本和踩坑记录。适合想自己跑一遍 LLaVA 的同学。论文: https://arxiv.org/abs/2310.03744 代码: https://github.com/haotian-liu/LLaVA/tree/main一、原理三块积木拼出来的多模态1. 结构LLM Align Layer ViTLLaVA 的结构只有三块LLM、Align Layer、ViT。ViT 负责看图。它把图片切成 patch编码成一串视觉特征。Align Layer 是两层 MLP把视觉特征投影到语言模型的词向量空间。LLM 负责理解。它拿到视觉 token和文字 token 一起做自回归生成。真正要学的只有中间那层 MLP。stage-1 只训它收敛很快。结构图如下。。2. 损失计算训练时不是所有 token 都算损失。输入里有图片 token、用户提问、模型回答。交叉熵损失只作用在绿色的回答部分图片 token 和提问都被 mask 掉。我们只想让模型学会怎么答不想让它预测用户会问什么。下图绿色字体才算损失。。二、论文里的实验论文在 11 个基准上做了评测覆盖问答、OCR、推理等方向。数据只用 1.2M比同期方案小一个量级效果更好。其中 POPE 专门测幻觉MME 测感知和认知是复现时最常看的两项。结论很清楚结构简单加数据干净比堆模块管用。对比结果如下。三、复现8×L20 上的双阶段训练我用的机器是 8 张 L20单卡 46GB 显存。整个流程分两步stage-1 对齐stage-2 指令微调。1. stage1Align 层对齐训练这一步只训 Align LayerViT 和 LLM 全部冻结。数据用 558K 图文对让视觉特征学会说人话。有几个参数和官方 A100 脚本不同都是显存逼出来的模型换成 7B。ZeRO-2 不切分参数每卡一份完整副本。13B 的 bf16 要 26GB46GB 余量太少。全局 batch 对齐论文。单卡 16累积 2 步8 卡合计 256。每 1000 步存一次。一个 epoch 有 2180 步崩了不至于全白跑。关掉 wandb 上报。它装了但没登录初始化会卡在要 key 的提示上。脚本开头加了预检确认 558K 图片解压完整。缺失时 dataloader 会在中途才报错。#!/bin/bash# Stage 1 (feature alignment / pretrain) for LLaVA-v1.5 on 8x NVIDIA L20 (46GB).## Differences from scripts/v1_5/pretrain.sh (the 8xA100 reference):# - 7B Vicuna instead of 13B: ZeRO-2 does not shard parameters, so every GPU# holds a full replica. 13B bf16 26GB/card leaves too little headroom on 46GB.# - batch 16 x accum 2 x 8 GPUs 256 global, matching the papers global batch.# - checkpointing every 1000 steps so a crash doesnt cost the whole epoch# (2180 steps/epoch at global batch 256).# - --report_to none: wandb is installed but unauthenticated, and wandb.init()# blocks on an API-key prompt. Set to wandb once you have run wandb login.## Run: conda activate org_llava bash scripts/v1_5/pretrain_8xL20.sh#set-euopipefail#exportCUDA_VISIBLE_DEVICES0,1,2,3,4,5,6,7#DATA_JSON./playground/data/LLaVA-Pretrain/blip_laion_cc_sbu_558k.jsonIMAGE_DIR./playground/data/LLaVA-Pretrain/images## Local copies of both hub checkpoints, so nothing is fetched mid-run and a container# restart cant pull the weights out from under a training job.MODEL_PATH./pretrain_weight/vicuna-7b-v1.5VISION_TOWER./checkpoints/hf_models/clip-vit-large-patch14-336## --- preflight: the 558k images must be fully extracted before training starts ---if[!-f$DATA_JSON];thenechoERROR: missing$DATA_JSON2exit1fiforpin$MODEL_PATH$VISION_TOWER;doif[!-d$p];thenechoERROR: missing$p(run from the repo root)2exit1fidoneN_IMAGES$(find$IMAGE_DIR-name*.jpg2/dev/null|wc-l)echoimages found:$N_IMAGES/ 558000if[$N_IMAGES-lt550000];thenechoWARNING: extraction looks incomplete; the dataloader will crash on missing images.2fi#deepspeed llava/train/train_mem.py\--deepspeed./scripts/zero2.json\--model_name_or_path$MODEL_PATH\--versionplain\--data_path$DATA_JSON\--image_folder$IMAGE_DIR\--vision_tower$VISION_TOWER\--mm_projector_typemlp2x_gelu\--tune_mm_mlp_adapterTrue\--mm_vision_select_layer-2\--mm_use_im_start_endFalse\--mm_use_im_patch_tokenFalse\--bf16True\--output_dir./checkpoints/llava-v1.5-7b-pretrain\--num_train_epochs1\--per_device_train_batch_size16\--per_device_eval_batch_size4\--gradient_accumulation_steps2\--evaluation_strategyno\--save_strategysteps\--save_steps1000\--save_total_limit2\--learning_rate1e-3\--weight_decay0.\--warmup_ratio0.03\--lr_scheduler_typecosine\--logging_steps1\--tf32True\--model_max_length2048\--gradient_checkpointingTrue\--dataloader_num_workers4\--lazy_preprocessTrue\--report_tonone2. stage2Visual 指令微调这一步要更新全部 7B 参数训练策略必须换。最要紧的是换 DeepSpeed 配置stage-1 用 zero2.json这里换成 zero3.json。ZeRO-2 不切分参数每卡放不下 7B只能让 ZeRO-3 把参数也切了。其余关键差异有这几处版本改成 v1启用对话模板。stage-1 是 plain。数据换成 665K 混合指令集。图片根目录指到 data 这一层json 里是相对路径。注入 stage-1 训好的投影层。去掉只训 adapter 的开关变成全量微调。学习率 2e-5比 stage-1 低 50 倍。加上按比例填充、按长度分组1.5 版标配。全量微调有个副作用checkpoint 会很大。单个 checkpoint 约 88GB。bf16 权重 12.6GBfp32 master 25.1GBAdam 动量 50.2GB。优化器状态占了六分之七。只按 7B 的 14GB 去估会少算 6 倍2026 年 9 月 22 号我踩过这个坑。保存间隔也是血泪教训。上游默认 1000 步存一次。结果第一次保存就失败白跑 4 小时 08 分。所以我把间隔改成 500 步。全程存 10 次多花约 1 小时写盘换来崩一次最多丢 2 小时。写盘慢是硬件决定的。NFS 实测 249MB/s一次保存要 6 分钟。只要中间产物做评测、不需要续训就打开只存权重的开关。checkpoint 会瘦身88GB 降到 12.6GB。#!/bin/bash# Stage 2 (visual instruction tuning) for LLaVA-v1.5-7B on 8x NVIDIA L20 (46GB).## 接 stage-1 的产物:--pretrain_mm_mlp_adapter 指向 checkpoints/llava-v1.5-7b-pretrain/mm_projector.bin## 和 pretrain_8xL20.sh 的 8 处关键差异:# - deepspeed 换成 zero3.json: stage-2 要更新全部 7B 参数, ZeRO-2 不切分参数,# 每卡一份 7B 放不下, 必须让 ZeRO-3 把参数也切了。# - --version v1 (stage-1 是 plain): 启用 vicuna 对话模板。# - --data_path 换成 llava_v1_5_mix665k.json。# - --image_folder 指到 playground/data 这一层: json 里的路径是# coco/train2017/xxx.jpg 这种相对路径, 不是相对 coco/。# - --pretrain_mm_mlp_adapter 注入 stage-1 训好的 projector。# - 去掉 --tune_mm_mlp_adapter: 不写即 False, 变成全量微调。# 副作用: LLaVATrainer._save_checkpoint 不再走只存 adapter的分支, 而是走 HF 的# save_pretrained, 于是 DeepSpeed 的优化器状态也一起落盘。单个 checkpoint ≈ 88GB# (6.738B 参数实测算得): bf16 权重 12.6GB fp32 master 25.1GB Adam exp_avg/sq 50.2GB。# 优化器状态占了 6/7 —— 只按整个 7B ~14GB估会少算 6 倍(2026-09-22 踩过)。# save_total_limit 2 峰值 ~176GB, 盘上还有 5TB, 不是瓶颈; 瓶颈是写盘时间 ——# NFS 实测 249MB/s, 一次保存约 6 分钟, 训到一半会因为保存而停顿。# - --image_aspect_ratio pad / --group_by_modality_length True: LLaVA-1.5 的规定。# - --learning_rate 2e-5 (stage-1 是 1e-3, 差 50 倍)。# - --save_steps 500 (上游默认 1000): 5198 步全程保存 10 次, 多花约 1 小时写盘。# 换来的是一条硬底线 —— 崩一次最多丢 2 小时。2026-09-22 因为 save_steps1000 且首次# 保存在第 1000 步保存失败, 直接白跑 4 小时 08 分(权重零字节落盘)。# 注意: 只要 --save_only_model 没开, 每个 checkpoint 就必须带优化器状态, 否则无法 resume# (train.py 见到 checkpoint-* 会走 resume_from_checkpointTrue)。若只要中间产物做评测、# 不需要断点续训, 可加 --save_only_model True: checkpoint 从 88GB 降到 12.6GB, 保存也快得多。## Run: conda activate org_llava bash scripts/v1_5/finetune_8xL20.sh#set-euopipefail#exportCUDA_VISIBLE_DEVICES0,1,2,3,4,5,6,7#DATA_JSON./playground/data/llava_v1_5_mix665k.jsonIMAGE_DIR./playground/dataMODEL_PATH./pretrain_weight/vicuna-7b-v1.5VISION_TOWER./checkpoints/hf_models/clip-vit-large-patch14-336MM_PROJECTOR./checkpoints/llava-v1.5-7b-pretrain/mm_projector.binOUTPUT_DIR./checkpoints/llava-v1.5-7b## --- preflight ---forfin$DATA_JSON$MM_PROJECTOR;doif[!-f$f];thenechoERROR: missing$f(run from the repo root)2exit1fidonefordin$IMAGE_DIR$MODEL_PATH$VISION_TOWER;doif[!-d$d];thenechoERROR: missing$d(run from the repo root)2exit1fidone# 665k 的图片散在 5 个子目录里, 少任何一个都会在训练中途才炸。forsubincoco gqa ocr_vqa textvqa vg;doif[!-d$IMAGE_DIR/$sub];thenechoERROR: missing$IMAGE_DIR/$sub2exit1fidone## 已经有人跑过就提醒一下, 免得手滑覆盖。if[-e$OUTPUT_DIR][-n$(ls-A$OUTPUT_DIR2/dev/null)];thenechoWARNING:$OUTPUT_DIRalready exists and is not empty.2fi#echopreflight OK#deepspeed llava/train/train_mem.py\--deepspeed./scripts/zero3.json\--model_name_or_path$MODEL_PATH\--versionv1\--data_path$DATA_JSON\--image_folder$IMAGE_DIR\--vision_tower$VISION_TOWER\--pretrain_mm_mlp_adapter$MM_PROJECTOR\--mm_projector_typemlp2x_gelu\--mm_vision_select_layer-2\--mm_use_im_start_endFalse\--mm_use_im_patch_tokenFalse\--image_aspect_ratiopad\--group_by_modality_lengthTrue\--bf16True\--output_dir$OUTPUT_DIR\--num_train_epochs1\--per_device_train_batch_size16\--per_device_eval_batch_size4\--gradient_accumulation_steps1\--evaluation_strategyno\--save_strategysteps\--save_steps500\--save_total_limit2\--learning_rate2e-5\--weight_decay0.\--warmup_ratio0.03\--lr_scheduler_typecosine\--logging_steps1\--tf32True\--model_max_length2048\--gradient_checkpointingTrue\--dataloader_num_workers4\--lazy_preprocessTrue\--report_tonone3. 实验结果POPE 和 MME跑完后我用 POPE 和 MME 两个基准做了评测。POPE 测幻觉看模型会不会瞎说有、瞎说没有。MME 分感知和认知两大类十几个子任务。结果和论文基本对齐脚本没把模型改坏。成绩如下。总结LLaVA-1.5 的结构很朴素ViT 看图MLP 对齐LLM 说话。复现的难点不在模型在工程细节。先按小间隔保存再谈跑多久。崩一次丢 4 小时比多花 1 小时写盘亏得多。基本复现已完成后续会出源码解读。想要训练日志的同学可以关注加私信。