【训练与微调篇08】多模态大模型训练
🎯 多模态大模型训练:从视觉编码到跨模态对齐的全流程实践
2026年多模态模型已进入「原生训练」时代,从Qwen2.5-VL到InternVL3再到Gemini 3,统一架构正在重塑AI的感知边界
📑 目录
一、多模态模型核心架构
二、视觉编码器进化史
三、桥接模块:连接视觉与语言的桥梁
四、传统三阶段训练范式
五、2026前沿:原生多模态预训练
六、动态高分辨率与Token压缩
七、多模态数据工程
八、后训练:SFT与偏好优化
九、训练基础设施与并行策略
十、实操:完整多模态训练代码
面试加分点
一、多模态模型核心架构
2026年的多模态大模型(MLLM)虽然百花齐放,但其核心架构高度趋同,可以用一个公式概括:
MLLM = Vision Encoder + Connector + LLM
1.1 三大组件
组件 代表 职责 参数量级
视觉编码器(ViT) SigLIP-SO400M, InternViT, DFN 将图像/视频转为视觉特征向量 300M-2B
桥接模块(Connector) MLP, Q-Former, Resampler, Perceiver 将视觉特征对齐到LLM语义空间 20M-500M
语言模型(LLM) Qwen3, InternLM3, Gemma3 理解语义、推理、生成文本 2B-78B
1.2 2026年主流多模态模型一览
模型 视觉编码器 桥接方式 基座LLM 训练范式 亮点
Qwen2.5-VL SigLIP-SO400M MLP Qwen2.5 三阶段 动态分辨率、MRoPE
Qwen3-VL SigLIP-2 DeepStack MLP Qwen3 三阶段+ 交错MRoPE、4阶段训练
InternVL3 InternViT-300M Pixel Shuffle+MLP InternLM3-8B 原生多模态 V2PE、MPO、SOTA 72.2 MMMU
InternVL3.5 InternViT Pixel Shuffle InternLM3 纯原生从头训 从零开始的联合训练
Gemini 3 原生多模态编码器 统一注意力 自研MoE 从头训练 60FPS视频理解、2M上下文
GPT-5.5 原生多模态 统一架构 自研 从头训练 统一多模态理解+生成
LLaVA-NeXT SigLIP MLP Llama-4 两阶段 简洁高效、社区友好
二、视觉编码器进化史
视觉编码器是MLLM的「眼睛」,其进化经历了三代变革。
2.1 第一代:CLIP时代
OpenAI CLIP通过对比学习(InfoNCE Loss)在大规模图文对上训练ViT,成为多模态的基石:
Loss_CLIP = -log(exp(sim(I,T)/τ) / Σexp(sim(I,T_j)/τ))
局限:InfoNCE需要全局对比,Batch Size要求大(32K+),训练不稳定。
2.2 第二代:SigLIP革命
SigLIP使用Sigmoid损失替代InfoNCE,是2024-2026年最广泛使用的视觉编码器:
CLIP vs SigLIP 损失对比
class CLIPLoss(nn.Module):
“”“InfoNCE: 需要batch内归一化,大batch才有效”“”
def forward(self, image_embeds, text_embeds):
logits = image_embeds @ text_embeds.T * self.temp
labels = torch.arange(len(logits), device=logits.device)
loss_i = F.cross_entropy(logits, labels)
loss_t = F.cross_entropy(logits.T, labels)
return (loss_i + loss_t) / 2
class SigLIPLoss(nn.Module):
“”“Sigmoid: 逐对独立计算,batch不敏感,训练更稳定”“”
def forward(self, image_embeds, text_embeds):
logits = image_embeds @ text_embeds.T * self.temp
# 逐对sigmoid二分类损失
labels = 2 * torch.eye(len(logits), device=logits.device) - 1
loss = -F.logsigmoid(labels * logits).sum() / len(logits)
return loss
SigLIP核心优势:
训练速度提升约40%,不需要超大Batch
负样本处理精度提升约23%
支持更大规模ViT(ViT-H/14、SO400M)
2.3 第三代:InternViT与原生视觉编码
2026年视觉编码器的进化方向是与LLM深度耦合。InternVL3使用的InternViT-300M首次与LLM进行原生联合预训练,不再「各学各的」。
关键改进:
Dynamic High Resolution:图像按内容复杂度自适应分辨率
Pixel Shuffle:ViT输出特征通过pixel shuffle压缩token数量
Window Attention:只在相邻patch间做注意力,减少O(n²)计算
class InternViT(nn.Module):
“”“InternVL3 视觉编码器核心结构(简化)”“”
def init(self, img_size=448, patch_size=14, embed_dim=3200):
super().init()
self.patch_embed = PatchEmbed(img_size, patch_size, embed_dim)
self.blocks = nn.ModuleList([
WindowAttentionBlock(embed_dim, window_size=14)
for _ in range(48) # 48层Transformer
])
self.pixel_shuffle = PixelShuffle(upscale_factor=2)
def forward(self, x, dynamic_resolution=True):
if dynamic_resolution:
x = self.dynamic_resize(x) # 自适应分辨率
x = self.patch_embed(x)
for block in self.blocks:
x = block(x)
x = self.pixel_shuffle(x) # 压缩token数
return x # [B, N_reduced, D]
三、桥接模块:连接视觉与语言的桥梁
视觉编码器输出的特征向量与LLM的输入空间不在同一个语义空间,需要桥接模块进行「翻译」。
3.1 三种主流桥接方式
桥接方式 代表模型 参数量 对齐效果 推理速度
MLP LLaVA, Qwen-VL ~20M ⭐⭐⭐ ⭐⭐⭐⭐⭐
Q-Former BLIP-2, InstructBLIP ~200M ⭐⭐⭐⭐ ⭐⭐⭐
Resampler/Perceiver Flamingo, InternVL ~100M ⭐⭐⭐⭐ ⭐⭐⭐⭐
DeepStack MLP Qwen3-VL ~500M ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
3.2 MLP桥接(最简洁高效)
class MLPConnector(nn.Module):
“”“LLaVA式MLP桥接 - 简单但有效”“”
def init(self, vision_dim=3200, llm_dim=4096):
super().init()
self.proj = nn.Sequential(
nn.Linear(vision_dim, llm_dim * 2),
nn.GELU(),
nn.Linear(llm_dim * 2, llm_dim),
)
def forward(self, visual_tokens):
return self.proj(visual_tokens)
3.3 Q-Former(带可学习查询)
class QFormerConnector(nn.Module):
“”“Q-Former: 用可学习queries从视觉特征中提取信息”“”
def init(self, num_queries=32, vision_dim=1408, llm_dim=4096):
super().init()
self.queries = nn.Parameter(torch.randn(1, num_queries, llm_dim))
self.cross_attn = nn.MultiheadAttention(llm_dim, num_heads=8)
self.self_attn = nn.MultiheadAttention(llm_dim, num_heads=8)
self.ffn = nn.Sequential(
nn.Linear(llm_dim, llm_dim * 4),
nn.GELU(),
nn.Linear(llm_dim * 4, llm_dim),
)
def forward(self, visual_features):
# Queries与视觉特征做cross-attention
queries = self.queries.expand(visual_features.size(0), -1, -1)
# 先self-attn -> cross-attn -> FFN
q = self.self_attn(queries, queries, queries)[0]
q = self.cross_attn(q, visual_features, visual_features)[0]
q = self.ffn(q)
return q # [B, 32, 4096]
3.4 Pixel Shuffle + MLP(InternVL方案)
InternVL系列使用的独特方案——先用Pixel Shuffle减少视觉token数,再通过MLP映射:
输入图像 → ViT编码 → [B, N, D_v] → Pixel Shuffle(2x) → [B, N/4, D_v*4]
→ MLP投影 → [B, N/4, D_llm] → LLM
这种方案的关键优势是在保留空间信息的同时压缩token数量。一张448×448的图像标准ViT输出1024个patch token,经过Pixel Shuffle 2x后变为256个,token数减少75%!
四、传统三阶段训练范式
4.1 阶段一:视觉预训练(Visual Pre-training)
目标:训练视觉编码器与桥接模块建立基础对齐
策略:
冻结LLM(语言能力保持)
更新ViT + Connector
使用大量图文对数据(LAION、DataComp)
损失函数:语言建模损失(只计算文本token)
class Stage1Trainer:
“”“阶段一:视觉预训练”“”
def init(self, model, vision_encoder, connector, llm):
self.vision_encoder = vision_encoder
self.connector = connector
self.llm = llm
# 冻结LLM
for param in self.llm.parameters():
param.requires_grad = False
# 解冻视觉编码器和桥接
for param in self.vision_encoder.parameters():
param.requires_grad = True
for param in self.connector.parameters():
param.requires_grad = True
def train_step(self, images, input_ids, labels):
with torch.no_grad(): # 视觉编码器可训练但LLM冻结
visual_feats = self.vision_encoder(images)
visual_tokens = self.connector(visual_feats)
# 拼接视觉token和文本token
embeds = self.llm.get_input_embeddings()(input_ids)
combined = torch.cat([visual_tokens, embeds], dim=1)
outputs = self.llm(inputs_embeds=combined, labels=labels)
return outputs.loss
数据配比:
数据类型 占比 示例
图像描述 40% COCO Captions, LAION
VQA 25% OKVQA, GQA
OCR 20% TextCaps, OCR-CC
视觉知识 15% Wikipedia Image-Text
4.2 阶段二:多模态预训练(Multimodal Pre-training)
目标:全参数对齐,建立深度跨模态理解
策略:
解冻所有参数(ViT + Connector + LLM)
使用更复杂的交错图文数据
处理视频、文档等复杂场景
关键数据:交错图文(Interleaved Image-Text)——网页内容、PDF文档、PPT等图文混合内容。
class Stage2Trainer:
“”“阶段二:多模态预训练(全参数)”“”
def init(self, model, vision_encoder, connector, llm):
# 解冻所有参数
for module in [vision_encoder, connector, llm]:
for param in module.parameters():
param.requires_grad = True
def train_step(self, images, input_ids, labels, num_images):
visual_feats = self.vision_encoder(images)
visual_tokens = self.connector(visual_feats)
# 交错图文拼接:按<image>占位符位置插入视觉token
combined = self.interleave_embeds(
visual_tokens, input_ids, num_images
)
outputs = self.llm(inputs_embeds=combined, labels=labels)
return outputs.loss
4.3 阶段三:长上下文预训练(Long-Context Pre-training)
目标:扩展模型处理长视频和长篇文档的能力
策略:
增加视频数据和长文档数据比例
扩展序列长度(8K → 32K+)
引入动态FPS采样、MRoPE等技术
class DynamicFPSSampler:
“”“动态FPS采样 - 关键帧高采样、平淡帧低采样”“”
def init(self, base_fps=1.0, high_fps=30.0):
self.base_fps = base_fps
self.high_fps = high_fps
def compute_frame_importance(self, frames):
"""计算每帧重要性(基于帧间差异)"""
diffs = []
for i in range(1, len(frames)):
diff = F.mse_loss(frames[i], frames[i-1]).item()
diffs.append(diff)
return diffs
def sample_frames(self, frames, max_frames=256):
"""根据重要性动态采样"""
importances = self.compute_frame_importance(frames)
# 重要片段用高FPS,平淡片段用低FPS
sampled = []
for i, imp in enumerate(importances):
if imp > 0.1: # 高动态区域
if i % int(self.base_fps / self.high_fps * 30) == 0:
sampled.append(frames[i])
else:
if i % 30 == 0: # 1fps
sampled.append(frames[i])
return sampled[:max_frames]
五、2026前沿:原生多模态预训练
5.1 从「后期改造」到「原生训练」
传统范式(Post-hoc):先训练纯文本LLM,再「嫁接」视觉能力
纯文本预训练 → 视觉对齐 → 视觉指令微调
LLM就绪 搭桥+对齐 多模态对话
原生范式(Native):从一开始就让ViT和LLM一起学习
原生多模态预训练 → SFT → MPO
ViT+LLM同时学习 指令跟随 偏好优化
5.2 InternVL3的原生预训练
InternVL3的核心创新在于取消了独立的视觉对齐阶段,在统一的预训练阶段同时训练所有组件。损失只计算文本token:
class NativeMultimodalPretrainLoss:
“”“原生多模态预训练损失:只计算文本token的LM Loss”“”
def compute_loss(self, logits, labels, is_visual_token_mask):
“”"
logits: [B, L, V] 模型输出
labels: [B, L] 目标token
is_visual_token_mask: [B, L] 视觉token位置为True
“”"
loss_fn = nn.CrossEntropyLoss(reduction=‘none’)
per_token_loss = loss_fn(
logits.view(-1, logits.size(-1)),
labels.view(-1)
) # [B*L]
# 只保留文本token的损失
text_mask = ~is_visual_token_mask # 文本token为True
text_loss = per_token_loss[text_mask.view(-1)]
# 平方平均权重
weights = 1.0 / (torch.arange(len(text_loss),
device=text_loss.device) + 1) ** 0.5
return (text_loss * weights).mean()
原生训练的数据配比:文本:多模态 ≈ 1:3
纯文本数据:500B tokens(保证语言能力不退化)
多模态数据:1500B tokens(覆盖图像、视频、文档、GUI等)
5.3 V2PE:可变视觉位置编码
传统的位置编码给每个token分配递增整数位置(0, 1, 2, 3…),但视觉token数量多时容易超限。
InternVL3的V2PE (Variable Visual Position Encoding) 给视觉token分配更小的位置增量:
class V2PE(nn.Module):
“”“可变视觉位置编码”“”
def init(self, max_position=131072, hidden_size=4096):
super().init()
self.rope = RotaryEmbedding(hidden_size)
self.visual_deltas = [1.0, 0.5, 0.25, 0.125, 0.0625,
0.03125, 0.015625, 0.00390625] # 1到1/256
def get_position_ids(self, input_ids, is_visual_mask,
visual_delta=0.25):
"""
为序列生成位置ID
- 文本token: +1
- 视觉token: +delta
"""
positions = torch.zeros_like(input_ids, dtype=torch.float)
pos = 0
for i, is_vis in enumerate(is_visual_mask[0]):
if is_vis:
pos += visual_delta
else:
pos += 1
positions[0, i] = pos
return positions.long()
V2PE的意外发现:在标准短序列任务上,较小的δ(如1/4或1/16)比标准的δ=1表现更好!
5.4 2026年主流模型训练对比
维度 Qwen2.5-VL InternVL3 Gemini 3
训练范式 三阶段(后期改造) 原生多模态预训练 从头原生训练
视觉初始化 SigLIP预训练 随机初始化ViT 随机初始化
LLM初始化 Qwen2.5预训练 InternLM3预训练 随机初始化
总训练数据 4.1T tokens 2T tokens 不可知
训练阶段 3 + SFT + DPO 1 + SFT + MPO 1 + RLHF
最高MMMU 70.2 72.2 74.7+
六、动态高分辨率与Token压缩
6.1 动态高分辨率(Dynamic High Resolution)
不同图像的信息密度差异巨大——一张白底黑字的文档图片不需要高清,而一张满是细节的照片需要高分辨率。动态高分辨率技术让模型根据图像复杂度自动选择分辨率。
InternVL方案:
先设置一个全局缩略图(448×448)
根据图像长宽比从35种预定义配置中选择最佳切分方案
最多支持12个tile(每个tile 448×448)
class DynamicHighResolution:
“”“动态高分辨率 - 自适应图像切分”“”
# 预定义的35种长宽比组合
ASPECT_RATIOS = {
# (rows, cols): max_tiles
(1, 1): 1, (1, 2): 2, (2, 1): 2,
(1, 3): 3, (3, 1): 3, (2, 3): 6,
(3, 2): 6, (1, 4): 4, (4, 1): 4,
(2, 4): 8, (4, 2): 8, (3, 4): 12,
(4, 3): 12, (1, 5): 5, (5, 1): 5,
# … 共35种
}
def best_aspect_ratio(self, img_w, img_h, max_tiles=12):
"""选择最佳切分方案"""
best_ar = (1, 1)
best_mismatch = float('inf')
for (rows, cols), tiles in self.ASPECT_RATIOS.items():
if tiles > max_tiles:
continue
aspect_ratio = cols / rows
img_aspect = img_w / img_h
mismatch = abs(aspect_ratio - img_aspect)
if mismatch < best_mismatch:
best_mismatch = mismatch
best_ar = (rows, cols)
return best_ar
def split_image(self, image, target_size=448, max_tiles=12):
"""将图像按最佳比例切分为多个tile"""
h, w = image.shape[-2:]
rows, cols = self.best_aspect_ratio(w, h, max_tiles)
# 先resize到合适尺寸
tile_h, tile_w = h // rows, w // cols
tiles = []
for i in range(rows):
for j in range(cols):
tile = image[...,
i*tile_h:(i+1)*tile_h,
j*tile_w:(j+1)*tile_w]
tile = F.interpolate(tile, size=(target_size, target_size))
tiles.append(tile)
return torch.stack(tiles) # [n_tiles, C, 448, 448]
6.2 Pixel Shuffle Token压缩
ViT输出大量patch token,直接送入LLM会导致序列过长(448×448图像→1024 tokens)。Pixel Shuffle通过空间到深度变换来压缩token:
def pixel_shuffle_downsample(visual_tokens, scale_factor=2):
“”"
Pixel Shuffle下采样
输入: [B, H, W, D] - ViT输出的空间特征图
输出: [B, H/2, W/2, D*4] - token数减少4倍,维度增加4倍
相当于把2×2的相邻patch合并为1个更高维的token
"""
B, H, W, D = visual_tokens.shape
# 保证H和W能被2整除
assert H % scale_factor == 0 and W % scale_factor == 0
# 重排: [B, H, W, D] -> [B, H/2, 2, W/2, 2, D] -> [B, H/2, W/2, D*4]
visual_tokens = visual_tokens.reshape(
B, H // scale_factor, scale_factor,
W // scale_factor, scale_factor, D
)
visual_tokens = visual_tokens.permute(0, 1, 3, 2, 4, 5)
visual_tokens = visual_tokens.reshape(
B, H // scale_factor, W // scale_factor, D * scale_factor ** 2
)
return visual_tokens # [B, H/2, W/2, D*4]
6.3 Qwen2.5-VL的动态分辨率方案
Qwen2.5-VL的独特之处在于按原生分辨率处理,不做resize,而是通过MRoPE动态编码:
输入图像(1280×720) → 按patch_size=14分割 → 约4679个patch
→ MRoPE编码绝对位置 → 2D-RoPE保持空间关系 → LLM直接处理
这种方案不损失任何图像细节,但需要的序列长度更长。MRoPE(Multi-resolution Rotary Position Encoding)让模型理解不同分辨率下patch的空间位置关系:
class MRoPE(nn.Module):
“”“多分辨率旋转位置编码”“”
def init(self, dim, max_resolution=2048):
super().init()
self.dim = dim
# 预计算不同分辨率的频率
self.freqs = {}
for res in [224, 448, 768, 1024, 2048]:
self.freqs[res] = self.compute_freqs(res)
def compute_freqs(self, resolution):
"""计算给定分辨率下的位置频率"""
patch_size = 14
n_patches = resolution // patch_size
# 2D位置编码
freq_y = 1.0 / (10000 ** (
torch.arange(0, self.dim, 2) / self.dim
))
freq_x = 1.0 / (10000 ** (
torch.arange(0, self.dim, 2) / self.dim
))
return freq_y, freq_x
def forward(self, positions_y, positions_x, resolution):
"""根据绝对位置和分辨率编码"""
freq_y, freq_x = self.freqs[resolution]
sin_y = torch.sin(positions_y.unsqueeze(-1) * freq_y)
cos_y = torch.cos(positions_y.unsqueeze(-1) * freq_y)
sin_x = torch.sin(positions_x.unsqueeze(-1) * freq_x)
cos_x = torch.cos(positions_x.unsqueeze(-1) * freq_x)
return sin_y, cos_y, sin_x, cos_x
七、多模态数据工程
7.1 数据类型与配比
高质量的多模态数据是MLLM成功的关键。2026年顶级模型的数据配比:
数据类型 Qwen2.5-VL占比 InternVL3占比 说明
图像-文本对 35% 25% 基础图文对齐
VQA 20% 20% 视觉问答
OCR/文档 15% 15% 文字识别与理解
交错图文 10% 15% 网页/文档混合内容
视频 8% 10% 动态场景理解
图表/知识 7% 8% 科学图表、知识图谱
GUI/Agent 3% 5% 屏幕操作、工具使用
3D/空间 2% 2% 3D场景理解
7.2 数据质量过滤Pipeline
class MultimodalDataFilter:
“”“多模态数据质量过滤pipeline”“”
def init(self):
self.filters = [
AspectRatioFilter(min_ratio=0.2, max_ratio=5.0),
ResolutionFilter(min_pixels=2828, max_pixels=20482048),
TextLengthFilter(min_chars=3, max_chars=500),
NSFWFilter(threshold=0.1),
LanguageFilter(target_langs=[‘en’, ‘zh’]),
DedupFilter(similarity_threshold=0.85),
]
def filter(self, image, text):
"""逐级过滤,任一条件不满足则丢弃"""
for f in self.filters:
if not f.check(image, text):
return False
return True
class QualityScorer:
“”“多模态数据质量评分”“”
def score_image_text_pair(self, image, text):
scores = {}
# 1. 图文相关性(使用CLIP/SigLIP打分)
scores[‘alignment’] = self.clip_score(image, text)
# 2. 文本质量(信息密度)
scores['informativeness'] = self.compute_info_density(text)
# 3. 视觉质量(清晰度、对比度)
scores['visual_quality'] = self.compute_image_quality(image)
# 4. 去重分数(与已有数据的相似度)
scores['novelty'] = 1.0 - self.max_similarity(text)
# 综合评分
total = (0.4 * scores['alignment'] +
0.3 * scores['informativeness'] +
0.2 * scores['visual_quality'] +
0.1 * scores['novelty'])
return total
7.3 合成数据生成
2026年多模态训练的一个关键趋势是使用模型生成训练数据。利用GPT-5.5、Gemini 3等先进模型生成高质量的图像描述、VQA对、多轮对话数据:
class SyntheticDataGenerator:
“”“合成多模态训练数据生成”“”
def init(self, teacher_model=“gemini-3-pro”):
self.teacher = teacher_model # 使用顶尖模型生成
def generate_caption(self, image):
"""生成高质量的图像描述"""
prompt = """Describe this image in detail, covering:
1. Main subjects and objects
2. Spatial relationships
3. Colors, textures, lighting
4. Any text visible in the image
5. The overall scene and atmosphere"""
return self.teacher.generate(image, prompt)
def generate_qa_pairs(self, image, n_pairs=5):
"""生成多样化的VQA数据"""
prompts = [
"Ask a question about the main subject",
"Ask a question about spatial relationships",
"Ask a counting question",
"Ask a reasoning question",
"Ask a question about text/OCR content",
]
qa_pairs = []
for prompt in prompts:
q = self.teacher.generate(image, prompt)
a = self.teacher.generate(image, f"Answer: {q}")
qa_pairs.append({"question": q, "answer": a})
return qa_pairs
八、后训练:SFT与偏好优化
8.1 多模态监督微调(Visual SFT)
SFT阶段使用高质量的多模态指令数据,让模型学会遵循人类指令进行多模态对话:
class MultimodalSFTTrainer:
“”“多模态SFT训练器”“”
def init(self, model, tokenizer, processor):
self.model = model
self.tokenizer = tokenizer
self.processor = processor # 图像处理器
def prepare_multimodal_sample(self, sample):
"""准备多模态训练样本"""
image = self.processor(sample['image'], return_tensors='pt')
# ChatML格式的消息
messages = [
{"role": "user", "content": [
{"type": "image"},
{"type": "text", "text": sample['question']}
]},
{"role": "assistant", "content": sample['answer']}
]
# 转换为token ids,带损失掩码
input_ids, labels, image_mask = self.chatml_to_tokens(messages)
return {
'pixel_values': image['pixel_values'],
'input_ids': input_ids,
'labels': labels,
'image_mask': image_mask,
}
def chatml_to_tokens(self, messages):
"""将ChatML格式转换为token序列,正确设置损失掩码"""
input_ids = []
labels = []
image_mask = []
for msg in messages:
for content in msg['content']:
if content['type'] == 'image':
# 图像占位符
img_tokens = self.tokenizer.encode(
"<|image_pad|>" * 256, # 用256个视觉token
add_special_tokens=False
)
input_ids.extend(img_tokens)
labels.extend([-100] * len(img_tokens)) # 不计算图像损失
image_mask.extend([True] * len(img_tokens))
else:
# 文本内容
text_ids = self.tokenizer.encode(
content['text'], add_special_tokens=False
)
input_ids.extend(text_ids)
image_mask.extend([False] * len(text_ids))
# 只有assistant的回答计算损失
if msg['role'] == 'assistant':
labels.extend(text_ids)
else:
labels.extend([-100] * len(text_ids))
return input_ids, labels, image_mask
8.2 MPO(混合偏好优化)
InternVL3引入的混合偏好优化(Mixture Preference Optimization)是2026年MLLM后训练的重要创新。它结合了三种损失:
class MPOLoss(nn.Module):
“”"
混合偏好优化损失
L = wp * Lp + wq * Lq + wg * Lg
Lp: DPO偏好损失 - 让模型偏好好的回答
Lq: BCO质量损失 - 识别回答的绝对质量
Lg: 生成损失 - 保持生成能力
"""
def __init__(self, wp=1.0, wq=0.5, wg=0.5, beta=0.1):
super().__init__()
self.wp = wp
self.wq = wq
self.wg = wg
self.beta = beta
self.lm_loss = nn.CrossEntropyLoss()
def forward(self, policy_chosen_logps, policy_rejected_logps,
chosen_logits, chosen_labels,
ref_chosen_logps, ref_rejected_logps):
"""
Args:
policy_chosen_logps: 策略模型对好回答的log概率
policy_rejected_logps: 策略模型对坏回答的log概率
chosen_logits: 好回答的logits
chosen_labels: 好回答的labels
ref_chosen_logps: 参考模型对好回答的log概率
ref_rejected_logps: 参考模型对坏回答的log概率
"""
# Lp: DPO偏好损失
log_ratio = (policy_chosen_logps - ref_chosen_logps) - \
(policy_rejected_logps - ref_rejected_logps)
Lp = -F.logsigmoid(self.beta * log_ratio).mean()
# Lq: BCO质量损失(判断回答绝对质量)
chosen_reward = policy_chosen_logps - ref_chosen_logps
rejected_reward = policy_rejected_logps - ref_rejected_logps
Lq = -F.logsigmoid(chosen_reward).mean() - \
F.logsigmoid(-rejected_reward).mean()
# Lg: 标准语言建模损失(只在好回答上计算)
Lg = self.lm_loss(
chosen_logits.view(-1, chosen_logits.size(-1)),
chosen_labels.view(-1)
)
return self.wp * Lp + self.wq * Lq + self.wg * Lg
8.3 测试时扩展:VisualPRM
2026年MLLM后训练的另一个重要方向是测试时扩展(Test-Time Scaling)——推理时生成多个候选答案,用专门的奖励模型选出最优:
class VisualPRM:
“”"
视觉过程奖励模型
不仅能评估最终答案,还能评估每一步推理的正确性
“”"
def init(self, model_path=“internvl-visualprm-8b”):
self.model = self.load_model(model_path)
def step_reward(self, image, question, steps):
"""评估解题过程中的每一步"""
rewards = []
current_solution = ""
for step in steps:
current_solution += step + "\n"
prompt = f"""Question: {question}
Current solution: {current_solution}
Evaluate if this step is correct.“”"
reward = self.model.generate_score(image, prompt)
rewards.append(reward)
return rewards
def best_of_n(self, image, question, n_candidates=8,
model=None, temperature=0.7):
"""生成N个候选答案,选出最优"""
candidates = []
for _ in range(n_candidates):
answer = model.generate(image, question,
temperature=temperature)
candidates.append(answer)
# 评估每个候选
best_score = -float('inf')
best_answer = None
for answer in candidates:
score = self.score_answer(image, question, answer)
if score > best_score:
best_score = score
best_answer = answer
return best_answer, best_score
九、训练基础设施与并行策略
9.1 InternEVO训练框架
MLLM训练涉及ViT、MLP、LLM三种不同计算特性的模块,传统单一并行策略效率低下。InternVL3团队优化的InternEVO框架通过灵活解耦的分片策略解决了这一难题:
class InternEVOConfig:
“”“InternEVO训练框架配置”“”
def init(self, model_size=“78B”):
self.strategies = {
“vision_encoder”: {
“tensor_parallel”: 2, # ViT用张量并行
“pipeline_parallel”: 1,
“data_parallel”: 16,
“sequence_parallel”: False,
“mixed_precision”: “fp16”,
},
“connector”: {
“tensor_parallel”: 1, # Connector轻量,数据并行
“pipeline_parallel”: 1,
“data_parallel”: 32,
“sequence_parallel”: False,
“mixed_precision”: “bf16”,
},
“llm”: {
“tensor_parallel”: 8, # LLM用大粒度并行
“pipeline_parallel”: 4,
“data_parallel”: 4,
“sequence_parallel”: True, # 长序列优化
“mixed_precision”: “bf16”,
}
}
# 动态负载均衡
self.load_balancing = {
"vision_text_ratio": "auto", # 自动平衡视觉/文本token
"gradient_checkpointing": True,
"activation_offloading": True,
"overlap_comm_computation": True, # 通信-计算重叠
}
InternEVO相对于传统框架的性能提升:
模型大小 传统框架吞吐 InternEVO吞吐 加速比
8B 16K tokens/s 32K tokens/s 2.0x
26B 4K tokens/s 10K tokens/s 2.5x
38B 2.5K tokens/s 7K tokens/s 2.8x
78B 1K tokens/s 3.2K tokens/s 3.2x
9.2 昇腾集群上的多模态训练
2026年中国多模态模型的另一个重要趋势是国产芯片上的大规模训练:
昇腾NPU上的多模态训练配置
ascend_config = {
“device”: “npu”,
“compute_unit”: “ascend_910c”,
# 混合并行策略
"parallelism": {
"data_parallel": 64,
"tensor_parallel": 4,
"pipeline_parallel": 2,
"expert_parallel": 1,
},
# 昇腾特有的优化
"ascend_optimizations": {
"flash_attention": True, # 昇腾FA
"mixed_precision": "O2", # 昇腾O2混合精度
"allreduce_fusion": True, # 通信融合
"overlap_comm": True, # 通信计算重叠
},
# 内存优化
"memory": {
"recompute": True,
"offload": True,
"activation_cpu_offload": True,
},
}
十、实操:完整多模态训练代码
10.1 完整三阶段训练脚本
#!/usr/bin/env python3
“”"
多模态大模型完整训练脚本(基于PyTorch)
支持三阶段训练:视觉预训练 → 多模态预训练 → 长上下文预训练
“”"
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from torch.optim import AdamW
from torch.amp import autocast, GradScaler
import json
import os
from PIL import Image
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
AutoProcessor,
get_cosine_schedule_with_warmup
)
==================== 1. 多模态数据集 ====================
class MultimodalDataset(Dataset):
“”“多模态数据集:支持图像+文本混合”“”
def init(self, data_path, processor, tokenizer,
max_length=8192, stage=1):
self.samples = json.load(open(data_path))
self.processor = processor
self.tokenizer = tokenizer
self.max_length = max_length
self.stage = stage
def __len__(self):
return len(self.samples)
def __getitem__(self, idx):
sample = self.samples[idx]
image = Image.open(sample['image_path']).convert('RGB')
# 处理图像
pixel_values = self.processor(
images=image, return_tensors='pt'
)['pixel_values'].squeeze(0)
# 构建多模态消息
if self.stage == 1:
# 阶段一:简单的图像描述
text = f"<image>\nDescribe this image: {sample['caption']}"
elif self.stage == 2:
# 阶段二:交错图文理解
text = sample['interleaved_text']
else:
# 阶段三:长上下文/视频
text = sample['long_context_text']
# Tokenize
encoding = self.tokenizer(
text, max_length=self.max_length,
padding='max_length', truncation=True,
return_tensors='pt'
)
return {
'pixel_values': pixel_values,
'input_ids': encoding['input_ids'].squeeze(0),
'attention_mask': encoding['attention_mask'].squeeze(0),
'labels': encoding['input_ids'].squeeze(0).clone(),
}
==================== 2. 多模态模型定义 ====================
class MultimodalModel(nn.Module):
“”“完整的MLLM模型”“”
def init(self, vision_encoder, connector, llm):
super().init()
self.vision_encoder = vision_encoder
self.connector = connector
self.llm = llm
self.image_token_id = 151857 # 自定义图像token ID
def get_visual_embeds(self, pixel_values):
"""获取视觉embedding"""
visual_features = self.vision_encoder(pixel_values)
visual_tokens = self.connector(visual_features)
return visual_tokens
def forward(self, pixel_values, input_ids, attention_mask=None,
labels=None, return_loss=True):
# 获取视觉embedding
visual_tokens = self.get_visual_embeds(pixel_values)
# 在input_ids中找到<image>占位符并替换为视觉token
text_embeds = self.llm.get_input_embeddings()(input_ids)
# 插入视觉token
image_positions = (input_ids == self.image_token_id).nonzero(as_tuple=True)
if len(image_positions[0]) > 0:
# 用视觉embedding替换占位符
for b in range(input_ids.size(0)):
img_pos = (input_ids[b] == self.image_token_id).nonzero()
for pos in img_pos[:len(visual_tokens)]:
text_embeds[b, pos] = visual_tokens[b, :text_embeds.size(1)].mean(dim=0)
# LLM前向
outputs = self.llm(
inputs_embeds=text_embeds,
attention_mask=attention_mask,
labels=labels if return_loss else None,
)
return outputs
def generate(self, pixel_values, input_ids, **gen_kwargs):
"""多模态生成"""
visual_tokens = self.get_visual_embeds(pixel_values)
text_embeds = self.llm.get_input_embeddings()(input_ids)
# 插入视觉token
image_positions = (input_ids[0] == self.image_token_id).nonzero()
for i, pos in enumerate(image_positions[:visual_tokens.size(1)]):
text_embeds[0, pos] = visual_tokens[0, i]
return self.llm.generate(
inputs_embeds=text_embeds,
**gen_kwargs
)
==================== 3. 训练器 ====================
class MultimodalTrainer:
“”“三阶段多模态训练器”“”
def init(self, model, config):
self.model = model
self.config = config
self.scaler = GradScaler(‘cuda’)
def configure_optimizer(self, stage):
"""不同阶段配置不同的学习率和参数组"""
if stage == 1:
# 阶段一:只训练ViT+Connector
param_groups = [
{'params': self.model.vision_encoder.parameters(),
'lr': 1e-4},
{'params': self.model.connector.parameters(),
'lr': 2e-4},
# LLM冻结
{'params': self.model.llm.parameters(),
'lr': 0, 'weight_decay': 0},
]
elif stage == 2:
# 阶段二:全参数训练
param_groups = [
{'params': self.model.vision_encoder.parameters(),
'lr': 5e-6},
{'params': self.model.connector.parameters(),
'lr': 1e-5},
{'params': self.model.llm.parameters(),
'lr': 2e-5},
]
else:
# 阶段三:长上下文训练
param_groups = [
{'params': self.model.parameters(), 'lr': 1e-5},
]
# 冻结阶段一的LLM
if stage == 1:
for param in self.model.llm.parameters():
param.requires_grad = False
else:
for param in self.model.llm.parameters():
param.requires_grad = True
self.optimizer = AdamW(
param_groups,
weight_decay=0.1,
betas=(0.9, 0.95),
)
self.scheduler = get_cosine_schedule_with_warmup(
self.optimizer,
num_warmup_steps=self.config['warmup_steps'],
num_training_steps=self.config['total_steps'],
)
def train_step(self, batch):
"""单步训练"""
pixel_values = batch['pixel_values'].cuda()
input_ids = batch['input_ids'].cuda()
attention_mask = batch['attention_mask'].cuda()
labels = batch['labels'].cuda()
with autocast('cuda', enabled=True):
outputs = self.model(
pixel_values, input_ids,
attention_mask=attention_mask,
labels=labels,
)
loss = outputs.loss
self.scaler.scale(loss).backward()
# 梯度裁剪
self.scaler.unscale_(self.optimizer)
torch.nn.utils.clip_grad_norm_(
self.model.parameters(), max_norm=1.0
)
self.scaler.step(self.optimizer)
self.scaler.update()
self.scheduler.step()
self.optimizer.zero_grad()
return loss.item()
def train_epoch(self, dataloader, stage, epoch):
"""完整训练一个epoch"""
self.model.train()
total_loss = 0
for step, batch in enumerate(dataloader):
loss = self.train_step(batch)
total_loss += loss
if step % 100 == 0:
lr = self.scheduler.get_last_lr()[0]
print(f"[Stage {stage}] Epoch {epoch} Step {step}/{len(dataloader)} "
f"Loss: {loss:.4f} LR: {lr:.2e}")
return total_loss / len(dataloader)
==================== 4. 主训练流程 ====================
def main():
# 配置
config = {
‘model_name’: ‘internlm3-8b’,
‘vision_encoder’: ‘siglip-so400m’,
‘data_root’: ‘/data/multimodal’,
‘output_dir’: ‘./checkpoints’,
‘batch_size’: 16, # 每GPU
‘gradient_accumulation’: 4,
‘max_length’: 8192,
‘warmup_steps’: 500,
}
# 初始化组件
processor = AutoProcessor.from_pretrained(config['vision_encoder'])
tokenizer = AutoTokenizer.from_pretrained(config['model_name'])
vision_encoder = AutoModel.from_pretrained(config['vision_encoder'])
llm = AutoModelForCausalLM.from_pretrained(config['model_name'])
connector = MLPConnector(
vision_dim=vision_encoder.config.hidden_size,
llm_dim=llm.config.hidden_size,
)
model = MultimodalModel(vision_encoder, connector, llm)
trainer = MultimodalTrainer(model, config)
# 阶段一:视觉预训练
print("=" * 50)
print("Stage 1: Visual Pre-training")
print("=" * 50)
stage1_dataset = MultimodalDataset(
f"{config['data_root']}/stage1.json",
processor, tokenizer, max_length=8192, stage=1
)
stage1_loader = DataLoader(
stage1_dataset, batch_size=config['batch_size'],
shuffle=True, num_workers=8
)
config['total_steps'] = len(stage1_loader) * 3 # 3 epochs
trainer.configure_optimizer(stage=1)
for epoch in range(3):
loss = trainer.train_epoch(stage1_loader, 1, epoch)
print(f"Stage 1 Epoch {epoch} Avg Loss: {loss:.4f}")
# 保存检查点
torch.save(model.state_dict(),
f"{config['output_dir']}/stage1_epoch{epoch}.pt")
# 阶段二:多模态预训练
print("=" * 50)
print("Stage 2: Multimodal Pre-training")
print("=" * 50)
stage2_dataset = MultimodalDataset(
f"{config['data_root']}/stage2.json",
processor, tokenizer, max_length=16384, stage=2
)
stage2_loader = DataLoader(
stage2_dataset, batch_size=config['batch_size'] // 2,
shuffle=True, num_workers=8
)
config['total_steps'] = len(stage2_loader) * 5 # 5 epochs
trainer.configure_optimizer(stage=2)
for epoch in range(5):
loss = trainer.train_epoch(stage2_loader, 2, epoch)
print(f"Stage 2 Epoch {epoch} Avg Loss: {loss:.4f}")
torch.save(model.state_dict(),
f"{config['output_dir']}/stage2_epoch{epoch}.pt")
# 阶段三:长上下文预训练
print("=" * 50)
print("Stage 3: Long-Context Pre-training")
print("=" * 50)
stage3_dataset = MultimodalDataset(
f"{config['data_root']}/stage3.json",
processor, tokenizer, max_length=32768, stage=3
)
stage3_loader = DataLoader(
stage3_dataset, batch_size=config['batch_size'] // 4,
shuffle=True, num_workers=8
)
config['total_steps'] = len(stage3_loader) * 2
trainer.configure_optimizer(stage=3)
for epoch in range(2):
loss = trainer.train_epoch(stage3_loader, 3, epoch)
print(f"Stage 3 Epoch {epoch} Avg Loss: {loss:.4f}")
torch.save(model.state_dict(),
f"{config['output_dir']}/stage3_epoch{epoch}.pt")
print("Training Complete!")
if name == “main”:
main()
10.2 SFT微调脚本
#!/usr/bin/env python3
“”“多模态SFT微调脚本”“”
from datasets import load_dataset
from transformers import (
Qwen2VLForConditionalGeneration,
Qwen2VLProcessor,
TrainingArguments,
Trainer,
)
def train_multimodal_sft():
# 加载模型
model = Qwen2VLForConditionalGeneration.from_pretrained(
“Qwen/Qwen2.5-VL-7B-Instruct”,
torch_dtype=torch.bfloat16,
attn_implementation=“flash_attention_2”,
device_map=“auto”,
)
processor = Qwen2VLProcessor.from_pretrained(
"Qwen/Qwen2.5-VL-7B-Instruct"
)
# 加载SFT数据
dataset = load_dataset("json", data_files="sft_data.jsonl")
def collate_fn(batch):
"""批处理:动态分辨率+ChatML格式"""
texts = []
images = []
for sample in batch:
messages = [
{"role": "user", "content": [
{"type": "image"},
{"type": "text", "text": sample['question']}
]},
{"role": "assistant", "content": sample['answer']}
]
text = processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=False
)
texts.append(text)
images.append(sample['image'])
batch_dict = processor(
text=texts, images=images,
padding=True, return_tensors="pt",
max_length=8192,
)
# 设置labels = input_ids(自回归)
batch_dict["labels"] = batch_dict["input_ids"].clone()
return batch_dict
# 训练参数
training_args = TrainingArguments(
output_dir="./qwen2.5-vl-sft",
per_device_train_batch_size=4,
gradient_accumulation_steps=8,
learning_rate=1e-5,
warmup_ratio=0.05,
num_train_epochs=3,
logging_steps=10,
save_steps=500,
bf16=True,
gradient_checkpointing=True,
deepspeed="ds_config.json",
report_to="wandb",
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset,
data_collator=collate_fn,
)
trainer.train()
trainer.save_model("./qwen2.5-vl-sft-final")
10.3 DeepSpeed配置
// ds_config.json - 多模态训练的ZeRO-3配置
{
“bf16”: {
“enabled”: true
},
“zero_optimization”: {
“stage”: 3,
“overlap_comm”: true,
“contiguous_gradients”: true,
“sub_group_size”: 1e9,
“reduce_bucket_size”: “auto”,
“stage3_prefetch_bucket_size”: “auto”,
“stage3_param_persistence_threshold”: “auto”,
“stage3_max_live_parameters”: 1e9,
“stage3_max_reuse_distance”: 1e9,
“stage3_gather_16bit_weights_on_model_save”: true
},
“gradient_accumulation_steps”: 8,
“gradient_clipping”: 1.0,
“steps_per_print”: 10,
“train_batch_size”: “auto”,
“train_micro_batch_size_per_gpu”: “auto”,
“wall_clock_breakdown”: false,
“flops_profiler”: {
“enabled”: true,
“profile_step”: 1,
“module_depth”: -1,
“top_modules”: 3
}
}
面试加分点
-
为什么SigLIP的Sigmoid Loss比CLIP的InfoNCE更好?
核心要点:InfoNCE需要计算batch内所有负样本的softmax归一化,因此依赖large batch size(32K+)才能有效区分正负样本。而Sigmoid Loss对每个图像-文本对独立计算二分类损失,batch size不再是关键瓶颈。这使得SigLIP可以用更小的batch(如16K)达到更好的对齐效果,训练效率提升约40%。 -
InternVL3的「原生多模态预训练」相比传统「后期改造」范式有什么优势?
回答思路:
消除模态鸿沟:视觉和语言从一开始就共同学习,而不是后期「强行对齐」
简化流程:将多阶段合并为统一预训练,减少调优复杂度
深度协同:所有参数共同优化,视觉经验可能反哺语言能力(InternVL3的纯文本任务表现意外提升)
减少能力损失:不存在因冻结参数导致的性能瓶颈
- 多模态训练中如何解决高分辨率图像带来的长序列问题?
三种主流方案:
Pixel Shuffle:将2×2相邻patch合并为1个高维token,token数减少75%
V2PE:给视觉token分配更小的位置增量(如1/4),在相同上下文窗口内容纳更多视觉token
动态分辨率:根据图像内容复杂度自适应选择分辨率,信息密度低的区域用低分辨率
- 2026年多模态训练的关键趋势
从后期改造到原生训练:InternVL3和Gemini 3引领的统一预训练范式
视觉反哺语言:多模态训练意外提升了纯文本任务表现
合成数据驱动:GPT-5.5等顶尖模型生成训练数据
过程奖励模型:VisualPRM + Best-of-N推理时扩展
国产芯片适配:昇腾910C+Megatron-npu训练70B级多模态模型
下一篇预告:【训练与微调篇09】模型量化与压缩:从GPTQ到AWQ的推理加速全攻略
当模型训练完成后,如何让它「跑得快、占得少」是关键。下一篇我们将深入训练后量化(PTQ)、量化感知训练(QAT)、结构剪枝、知识蒸馏等模型压缩技术,让你的大模型在生产环境中高效运行。
更多推荐


所有评论(0)