27届大模型面试准备(七十七):多模态大模型预训练工程------数据配比、多阶段训练与模态对齐
引言
本文是「27届大模型面试准备」系列的第 77 篇,承接(七十六)大模型推理服务高可用与容灾工程。前面我们讲了推理侧的部署、调度、容灾,这一篇把视角拉回训练侧最硬核的工程问题之一:多模态大模型(MLLM)怎么训出来。
单模态 LLM 的预训练范式已经相对成熟(数据清洗、tokenizer、next-token 预测、分布式并行),但一旦把图像、视频、音频塞进来,问题立刻复杂一个量级:不同模态的表示空间怎么对齐、训练数据怎么配比、视觉编码器冻不冻、多阶段课程怎么排、跨模态注意力怎么算才不爆显存。这些恰恰是校招/社招面试里区分"用过 API"和"真正训过模型"的分水岭。
本篇用一张总览图 + 多张对比表,逐节拆解多模态预训练的工程链路,并给出可落地的代码片段。结尾附面试速答与高频追问清单。
多模态预训练工程总览
==================================================
训练数据层
┌──────────┬──────────┬──────────┬──────────┐
│ 图文对 │ 交错文档 │ 视频片段 │ 纯文本 │
│(image- │(web │(video │(text │
│ caption) │ page) │ clip) │ corpus) │
└────┬─────┴────┬─────┴────┬─────┴────┬─────┘
│ 清洗/去重 │ 配权重采样 │ 长度过滤 │
└──────────┴────┬─────┴──────────┘
▼
┌──────────────────────┐
│ 模态编码器 (Encoder) │
│ ViT / Whisper / BPE │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ 对齐投影 (Projector) │
│ MLP / Resampler / │
│ Q-Former / Cross-Attn│
└──────────┬───────────┘
▼
┌──────────────────────┐
│ LLM 主干 (Decoder) │
│ LLaMA / Qwen / ... │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ 多阶段训练课程 │
│ 1.对齐 2.预训练 3.SFT │
└──────────────────────┘
一、数据工程:配比与清洗是天花板
多模态训练最容易被低估的就是数据。文本侧有 CommonCrawl、Wikipedia、代码语料;视觉侧最重要的是图文对(LAION、COYO、CC12M、音视频侧的WebVid、AudioSet)。但原始数据噪声极大,工程上要做四件事。
| 环节 | 目标 | 常用手段 | 踩坑点 |
|---|---|---|---|
| 图文对质量过滤 | 去掉水印/NSFW/低信息量 | CLIP 相似度阈值、OCR 文本长度、图像分辨率 | 阈值过严会丢长尾概念 |
| 文本侧去重 | 防记忆、降冗余 | MinHash + LSH、Suffix Array | 跨语言去重常被忽略 |
| 配权重采样 | 平衡模态与来源 | 指数滑动、loss 加权、temperature sampling | 纯文本比例过高会拖慢视觉收敛 |
| 长度与格式归一 | 统一 token 预算 | 图像分辨率分桶、视频抽帧 | 视频帧数过多直接爆显存 |
一个关键工程经验:图文对数据决定了模型"看得见什么",纯文本数据决定了模型"说得好不好"。实践里常见配比是图文对 : 纯文本 ≈ 1 : 1 到 1 : 4,视频/音频作为增量补充。配比不是拍脑袋,要 ablation。
# 多源数据加权采样器(简化版)
import bisect, random
class MultiSourceSampler:
def __init__(self, sources, weights):
# sources: [("image_text", 1.0), ("text", 4.0), ("video", 0.5)]
self.sources = [s for s, _ in sources]
total = sum(weights)
self.cum = []
acc = 0.0
for w in weights:
acc += w / total
self.cum.append(acc)
def next_source(self):
r = random.random()
idx = bisect.bisect_left(self.cum, r)
return self.sources[min(idx, len(self.sources) - 1)]
# 训练时按 source 切换对应 DataLoader,实现软配比
sampler = MultiSourceSampler(
[("image_text", 1.0), ("text", 4.0), ("video", 0.5)]
)
二、模态编码器与对齐投影
视觉侧几乎都用 ViT(CLIP/ViT-L/InternViT)。核心问题是:ViT 输出的 patch token 数量巨大(一张 336×336 图约 576 个 token),直接塞进 LLM 既不经济又容易让文本信号被淹没。于是需要一个投影模块把视觉特征压缩/对齐到 LLM 的语义空间。
常见三种投影结构:
| 结构 | 代表 | 参数量 | 特点 |
|---|---|---|---|
| 线性/MLP 映射 | LLaVA-1.5 | 极少 | 简单,但 token 数不变 |
| Resampler(P-Sampling) | Flamingo / IDFF | 中 | 用固定 N 个 query 重采样,压缩 token 数 |
| Q-Former | BLIP-2 / InstructBLIP | 中 | 学习离散 query 聚合视觉语义 |
| Cross-Attention | Flamingo Perceiver | 中 | 视觉作为 KV 注入 LLM 层 |
工程取舍:想要省 token、跑得快,用 Resampler 固定到 64~256 个视觉 token;想要保留细粒度,用 MLP 但接受长序列。训练顺序上通常先冻结 ViT 和 LLM,只训投影(对齐阶段),让视觉特征先落入语言空间,再解冻逐步联合训练。
import torch
import torch.nn as nn
class MLPProjector(nn.Module):
"""把 ViT 输出 [B, N_img, D_v] 映射到 LLM 维度 D_l"""
def __init__(self, d_vis=1024, d_llm=4096, hidden=4096):
super().__init__()
self.fc1 = nn.Linear(d_vis, hidden)
self.act = nn.GELU()
self.fc2 = nn.Linear(hidden, d_llm)
def forward(self, x):
# x: [B, N_img, D_vis]
return self.fc2(self.act(self.fc1(x))) # [B, N_img, D_llm]
class Resampler(nn.Module):
"""用固定 N_q 个可学习 query 重采样视觉特征,压缩 token 数"""
def __init__(self, d_vis=1024, d_llm=4096, n_query=256, heads=16):
super().__init__()
self.query = nn.Parameter(torch.randn(1, n_query, d_llm))
self.cross = nn.MultiheadAttention(d_llm, heads, batch_first=True)
self.norm = nn.LayerNorm(d_llm)
def forward(self, vis):
# vis: [B, N_img, D_llm](已投影到 llm 维度)
B = vis.size(0)
q = self.query.expand(B, -1, -1)
out, _ = self.cross(q, vis, vis)
return self.norm(out) # [B, N_query, D_llm]
三、多阶段训练课程
工业界几乎都采用分阶段"解冻"策略,而不是一把梭从头训。典型三阶段:
阶段 1 对齐预训练 (alignment / pretrain-projector)
冻结 ViT + LLM,只训 Projector
数据:大规模图文对
目标:让视觉 token 落入 LLM 语义空间
学习率:~1e-3,batch 大
阶段 2 多模态联合预训练 (multimodal pretrain)
解冻 LLM(ViT 可冻可微)
数据:图文 + 交错文档 + 视频 + 纯文本混合
目标:next-token prediction,跨模态指令理解
学习率:LLM 1e-5 ~ 2e-5,Projector 1e-4
阶段 3 指令微调 (SFT)
全参数或半参数(LoRA)微调
数据:多模态指令数据(VQA、 caption、推理链)
目标:对齐人类意图、抑制幻觉
学习率:LoRA 2e-4,全参 1e-6
分阶段的价值在于:先对齐再联合,避免"视觉噪声直接冲击语言模型导致语言能力下降"(catastrophic forgetting of language)。实测中如果直接解冻全训,纯文本 benchmark 会掉 3~5 个点。
四、损失与注意力掩码工程
多模态训练的损失仍是交叉熵,但有一个工程细节:图像/视频 token 不应参与 loss 计算(它们是输入不是预测目标)。需要构造模态掩码,只对文本 token 计算 CE。
def multimodal_ce(logits, labels, loss_mask):
"""
logits: [B, T, V]
labels: [B, T] 图像位置填 -100
loss_mask: [B, T] float,文本=1.0 图像/填充=0.0
"""
loss_fct = torch.nn.CrossEntropyLoss(reduction="none")
loss = loss_fct(logits.view(-1, logits.size(-1)), labels.view(-1))
loss = loss.view(labels.size(0), labels.size(1))
loss = (loss * loss_mask).sum() / (loss_mask.sum() + 1e-8)
return loss
注意力层面,纯文本位置用因果掩码;图像 token 之间可用双向(因为它们是同时输入的一张图),图像对文本用因果。实现上用 4D 注意力掩码(batch, head, q, k)统一处理最干净。
五、分布式训练要点
多模态模型参数量大、序列长,训练并行要组合使用:
| 并行策略 | 适用场景 | 备注 |
|---|---|---|
| 数据并行 DP/FSDP | 通用 | 配合梯度累积 |
| 张量并行 TP | LLM 主干横向切 | 跨节点通信重 |
| 流水线并行 PP | 深网络 | 视觉编码器单独放一张卡 |
| 序列并行 SP | 长视频长文本 | 切序列维度省显存 |
| 冻结参数优化 | ViT/LLM 冻结段 | 不存其梯度/优化器状态 |
一个实用架构:ViT 放在单独的 GPU(甚至不进 FSDP 包装),Projector 和 LLM 走 FSDP。视频训练时序列可能到 8k~32k token,必须用激活重计算(gradient checkpointing)+ 序列并行。
# FSDP 包装 LLM 主干(PyTorch 2.x 风格)
from torch.distributed.fsdp import FSDP, MixedPrecision
from torch.distributed.fsdp.wrap import size_based_auto_wrap_policy
llm_fsdp = FSDP(
llm,
auto_wrap_policy=size_based_auto_wrap_policy,
mixed_precision=MixedPrecision(
param_dtype=torch.bfloat16,
reduce_dtype=torch.bfloat16,
buffer_dtype=torch.bfloat16,
),
use_orig_params=True,
sharding_strategy="FULL_SHARD",
)
# ViT 不进 FSDP,单独 .cuda() 并设 requires_grad=False 在阶段1
六、典型故障与排查
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 语言能力下降 | 解冻太早/纯文本占比低 | 检查 stage2 文本比例、加语言侧辅助 loss |
| 视觉回答泛化差 | 图文对质量低/过拟合 caption | 查 CLIP 相似度分布、增广 |
| loss 不降 | 投影未对齐就联合训 | 退回 stage1 先训投影 |
| OOM | 视频帧/分辨率过高 | 降帧、序列并行、激活重算 |
| 对齐慢 | 学习率不当 | 投影 1e-3,LLM 1e-5 |
面试速答
问:为什么多模态训练要分阶段而不是直接端到端训?
答:直接训会导致视觉噪声冲击语言模型、纯文本能力灾难性遗忘,且投影未对齐时梯度混乱、loss 难降。分阶段先对齐投影、再联合、最后 SFT,稳定且保语言力。
问:Projector 有哪几类,怎么选?
答:线性/MLP(简单但 token 多)、Resampler(固定 query 压缩 token 数)、Q-Former(离散 query 聚合语义)、Cross-Attention(视觉作 KV 注入)。省显存选 Resampler,保细节选 MLP。
问:图像 token 要不要算 loss?
答:不算。图像/视频 token 是输入不是预测目标,用 loss_mask 只对文本位置算 CE,否则会强迫模型"预测像素特征"导致训练崩。
问:ViT 一般冻还是训?
答:阶段1冻结只训投影;阶段2通常微调 ViT 末端或全微(视数据量与算力);阶段3多数保持 ViT 冻结,避免过拟合。
高频追问清单
- 图文对数据噪声怎么量化评估?CLIP 相似度阈值怎么定?
- 纯文本占比多少合适?过小/过大分别有什么副作用?
- 视频多模态怎么处理时序?抽帧策略与位置编码怎么设计?
- 交错文档(web page)和图文对训练目标有何不同?
- 多阶段学习率怎么退火?warmup 比例怎么设?
- 序列并行和激活重算怎么组合用才不互相拖累?
- 训练中出现模态坍缩(只输出文本不看图)怎么救?
- 如何评估多模态预训练质量?除 benchmark 外的过程指标有哪些?
- 视觉 token 数从 576 压到 64,信息损失怎么补偿?
- LoRA 用在多模态 SFT 上,应该只挂 LLM 还是也挂 Projector?