LLaVA1.5-7B复现

文章目录

  • 前言
  • 一、原理:三块积木拼出来的多模态
    • [1. 结构:LLM + Align Layer + ViT](#1. 结构:LLM + Align Layer + ViT)
    • [2. 损失计算](#2. 损失计算)
  • 二、论文里的实验
  • [三、复现:8×L20 上的双阶段训练](#三、复现:8×L20 上的双阶段训练)
    • [1. stage1:Align 层对齐训练](#1. stage1:Align 层对齐训练)
    • [2. stage2:Visual 指令微调](#2. stage2:Visual 指令微调)
    • [3. 实验结果:POPE 和 MME](#3. 实验结果:POPE 和 MME)
  • 总结

前言

  LLaVA 是开源多模态大模型里最好上手的项目之一。

  它把视觉编码器和语言模型拼在一起,分两阶段训练。

  论文和代码都公开,可真在 8 卡机器上跑一遍,坑一点不少。

  本文先讲结构,再看论文实验,最后给出 8×L20 上的复现脚本和踩坑记录。

  适合想自己跑一遍 LLaVA 的同学。

text 复制代码
论文: https://arxiv.org/abs/2310.03744
代码: https://github.com/haotian-liu/LLaVA/tree/main

一、原理:三块积木拼出来的多模态

1. 结构:LLM + Align Layer + ViT

  LLaVA 的结构只有三块: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. stage1:Align 层对齐训练

  这一步只训 Align Layer,ViT 和 LLM 全部冻结。

  数据用 558K 图文对,让视觉特征学会说人话。

  有几个参数和官方 A100 脚本不同,都是显存逼出来的:

  • 模型换成 7B。ZeRO-2 不切分参数,每卡一份完整副本。13B 的 bf16 要 26GB,46GB 余量太少。
  • 全局 batch 对齐论文。单卡 16,累积 2 步,8 卡合计 256。
  • 每 1000 步存一次。一个 epoch 有 2180 步,崩了不至于全白跑。
  • 关掉 wandb 上报。它装了但没登录,初始化会卡在要 key 的提示上。

  脚本开头加了预检,确认 558K 图片解压完整。缺失时 dataloader 会在中途才报错。

bash 复制代码
#!/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 paper's global batch.
#   - checkpointing every 1000 steps so a crash doesn't 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 -euo pipefail
#
export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
#
DATA_JSON=./playground/data/LLaVA-Pretrain/blip_laion_cc_sbu_558k.json
IMAGE_DIR=./playground/data/LLaVA-Pretrain/images
#
# Local copies of both hub checkpoints, so nothing is fetched mid-run and a container
# restart can't pull the weights out from under a training job.
MODEL_PATH=./pretrain_weight/vicuna-7b-v1.5
VISION_TOWER=./checkpoints/hf_models/clip-vit-large-patch14-336
#
# --- preflight: the 558k images must be fully extracted before training starts ---
if [ ! -f "$DATA_JSON" ]; then
    echo "ERROR: missing $DATA_JSON" >&2
    exit 1
fi
for p in "$MODEL_PATH" "$VISION_TOWER"; do
    if [ ! -d "$p" ]; then
        echo "ERROR: missing $p (run from the repo root)" >&2
        exit 1
    fi
done
N_IMAGES=$(find "$IMAGE_DIR" -name '*.jpg' 2>/dev/null | wc -l)
echo "images found: $N_IMAGES / 558000"
if [ "$N_IMAGES" -lt 550000 ]; then
    echo "WARNING: extraction looks incomplete; the dataloader will crash on missing images." >&2
fi
#
deepspeed llava/train/train_mem.py \
    --deepspeed ./scripts/zero2.json \
    --model_name_or_path "$MODEL_PATH" \
    --version plain \
    --data_path "$DATA_JSON" \
    --image_folder "$IMAGE_DIR" \
    --vision_tower "$VISION_TOWER" \
    --mm_projector_type mlp2x_gelu \
    --tune_mm_mlp_adapter True \
    --mm_vision_select_layer -2 \
    --mm_use_im_start_end False \
    --mm_use_im_patch_token False \
    --bf16 True \
    --output_dir ./checkpoints/llava-v1.5-7b-pretrain \
    --num_train_epochs 1 \
    --per_device_train_batch_size 16 \
    --per_device_eval_batch_size 4 \
    --gradient_accumulation_steps 2 \
    --evaluation_strategy "no" \
    --save_strategy "steps" \
    --save_steps 1000 \
    --save_total_limit 2 \
    --learning_rate 1e-3 \
    --weight_decay 0. \
    --warmup_ratio 0.03 \
    --lr_scheduler_type "cosine" \
    --logging_steps 1 \
    --tf32 True \
    --model_max_length 2048 \
    --gradient_checkpointing True \
    --dataloader_num_workers 4 \
    --lazy_preprocess True \
    --report_to none

2. stage2:Visual 指令微调

  这一步要更新全部 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.6GB,fp32 master 25.1GB,Adam 动量 50.2GB。

  优化器状态占了六分之七。

  只按 7B 的 14GB 去估,会少算 6 倍,2026 年 9 月 22 号我踩过这个坑。

  保存间隔也是血泪教训。上游默认 1000 步存一次。

  结果第一次保存就失败,白跑 4 小时 08 分。

  所以我把间隔改成 500 步。全程存 10 次,多花约 1 小时写盘,换来崩一次最多丢 2 小时。

  写盘慢是硬件决定的。NFS 实测 249MB/s,一次保存要 6 分钟。

  只要中间产物做评测、不需要续训,就打开只存权重的开关。checkpoint 会瘦身:88GB 降到 12.6GB。

bash 复制代码
#!/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_steps=1000 且首次
#     保存在第 1000 步保存失败, 直接白跑 4 小时 08 分(权重零字节落盘)。
#     注意: 只要 --save_only_model 没开, 每个 checkpoint 就必须带优化器状态, 否则无法 resume
#     (train.py 见到 checkpoint-* 会走 resume_from_checkpoint=True)。若只要中间产物做评测、
#     不需要断点续训, 可加 --save_only_model True: checkpoint 从 88GB 降到 12.6GB, 保存也快得多。
#
# Run:  conda activate org_llava && bash scripts/v1_5/finetune_8xL20.sh
#
set -euo pipefail
#
export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
#
DATA_JSON=./playground/data/llava_v1_5_mix665k.json
IMAGE_DIR=./playground/data
MODEL_PATH=./pretrain_weight/vicuna-7b-v1.5
VISION_TOWER=./checkpoints/hf_models/clip-vit-large-patch14-336
MM_PROJECTOR=./checkpoints/llava-v1.5-7b-pretrain/mm_projector.bin
OUTPUT_DIR=./checkpoints/llava-v1.5-7b
#
# --- preflight ---
for f in "$DATA_JSON" "$MM_PROJECTOR"; do
    if [ ! -f "$f" ]; then
        echo "ERROR: missing $f (run from the repo root)" >&2
        exit 1
    fi
done
for d in "$IMAGE_DIR" "$MODEL_PATH" "$VISION_TOWER"; do
    if [ ! -d "$d" ]; then
        echo "ERROR: missing $d (run from the repo root)" >&2
        exit 1
    fi
done
# 665k 的图片散在 5 个子目录里, 少任何一个都会在训练中途才炸。
for sub in coco gqa ocr_vqa textvqa vg; do
    if [ ! -d "$IMAGE_DIR/$sub" ]; then
        echo "ERROR: missing $IMAGE_DIR/$sub" >&2
        exit 1
    fi
done
#
# 已经有人跑过就提醒一下, 免得手滑覆盖。
if [ -e "$OUTPUT_DIR" ] && [ -n "$(ls -A "$OUTPUT_DIR" 2>/dev/null)" ]; then
    echo "WARNING: $OUTPUT_DIR already exists and is not empty." >&2
fi
#
echo "preflight OK"
#
deepspeed llava/train/train_mem.py \
    --deepspeed ./scripts/zero3.json \
    --model_name_or_path "$MODEL_PATH" \
    --version v1 \
    --data_path "$DATA_JSON" \
    --image_folder "$IMAGE_DIR" \
    --vision_tower "$VISION_TOWER" \
    --pretrain_mm_mlp_adapter "$MM_PROJECTOR" \
    --mm_projector_type mlp2x_gelu \
    --mm_vision_select_layer -2 \
    --mm_use_im_start_end False \
    --mm_use_im_patch_token False \
    --image_aspect_ratio pad \
    --group_by_modality_length True \
    --bf16 True \
    --output_dir "$OUTPUT_DIR" \
    --num_train_epochs 1 \
    --per_device_train_batch_size 16 \
    --per_device_eval_batch_size 4 \
    --gradient_accumulation_steps 1 \
    --evaluation_strategy "no" \
    --save_strategy "steps" \
    --save_steps 500 \
    --save_total_limit 2 \
    --learning_rate 2e-5 \
    --weight_decay 0. \
    --warmup_ratio 0.03 \
    --lr_scheduler_type "cosine" \
    --logging_steps 1 \
    --tf32 True \
    --model_max_length 2048 \
    --gradient_checkpointing True \
    --dataloader_num_workers 4 \
    --lazy_preprocess True \
    --report_to none

3. 实验结果:POPE 和 MME

  跑完后,我用 POPE 和 MME 两个基准做了评测。

  POPE 测幻觉,看模型会不会瞎说有、瞎说没有。

  MME 分感知和认知两大类,十几个子任务。

  结果和论文基本对齐,脚本没把模型改坏。

  成绩如下。

  


总结

  LLaVA-1.5 的结构很朴素:ViT 看图,MLP 对齐,LLM 说话。

  复现的难点不在模型,在工程细节。

  先按小间隔保存,再谈跑多久。崩一次丢 4 小时,比多花 1 小时写盘亏得多。

  基本复现已完成,后续会出源码解读。想要训练日志的同学可以关注加私信。

相关推荐
小淮AI1 小时前
AI生成PPT工具的功能观察:百度文库、Gamma、WPS AI
人工智能·powerpoint·wps
小爷毛毛(卓寿杰)1 小时前
【Agent 意图识别】输出协议、评估与置信度
人工智能·算法·大模型·prompt·大语言模型·agent
仙人掌_lz2 小时前
3090 上的部署两种基于Qwen3.5-4B 判别模型open jev:llama.cpp和 vLLM ,谁更快、谁更准
人工智能·llm·llama·vllm·判别模型·jev
温暖小土2 小时前
Spring AI 接入通义千问向量模型
java·人工智能·spring
龙腾AI白云2 小时前
AI微调技术:让通用大模型精准适配垂直行业
数据库·人工智能·机器学习·flask·scikit-learn
武雄(小星Ai)2 小时前
Opus 5.5 降价40%、GPT-6 API腰斩:2026年9月AI编程模型选购指南(附成本计算器)
人工智能·ai·编程语言
二川bro2 小时前
当AI加速错误:美军Maven误击事件深度拆解
人工智能
麦豆GEO2 小时前
GEO信源布局策略:看懂大模型信源偏好,搭建动态可迭代的全域信源矩阵
大数据·人工智能·矩阵
旋生万物2 小时前
Agent System Prompt 注入螺旋公理:让 AI 推理不跑偏的可复制模板(附完整 Prompt)
人工智能·算法