llavaov_1_5
LLaVA-OneVision-1.5 模型与Megatron训练框架详细文档
目录
1. 项目概述
1.1 项目介绍
LLaVA-OneVision-1.5 是一个完全开源的大规模多模态模型(LMM)训练框架,具有以下特点: - 原生分辨率图像处理:支持高分辨率图像输入 - SOTA性能:在多个多模态基准测试上达到业界领先水平 - 高效训练:基于Megatron-LM优化,全流程训练成本约$16,000 - 完全开源:提供完整的训练框架、配置、数据集和训练日志
1.2 核心特性
- 支持MoE(Mixture of Experts)
- FP8精度训练
- 长序列并行
- 原生分辨率图像输入(最大1344x1344)
- 端到端优化的训练流水线
1.3 技术栈
- 基础框架:Megatron-LM (NVIDIA)
- 语言模型:Qwen系列(Qwen2.5/Qwen3)
- 视觉模型:Rice-ViT(基于ViT优化)
- 训练框架:AIAK-Training-LLM(百度AIAK团队)
2. 架构设计
2.1 整体架构
项目采用模块化设计,主要包含以下几个核心模块:
LLaVA-OneVision-1.5/
├── aiak_megatron/ # Megatron-LM核心代码
│ └── megatron/
│ ├── core/ # 核心功能模块
│ │ ├── models/ # 模型定义
│ │ ├── transformer/ # Transformer实现
│ │ ├── optimizer/ # 优化器
│ │ └── pipeline_parallel/ # 流水线并行
│ └── training/ # 训练工具
├── aiak_training_llm/ # AIAK训练框架
│ ├── models/ # 模型实现
│ │ ├── llavaov_1_5/ # LLaVA-OV-1.5模型
│ │ ├── qwen/ # Qwen语言模型
│ │ └── qwen_vl/ # Qwen-VL模型
│ ├── data/ # 数据处理
│ ├── train/ # 训练逻辑
│ │ ├── pretrain/ # 预训练
│ │ └── sft/ # 监督微调
│ └── tokenizer/ # 分词器
└── tools/ # 工具集
└── convert_checkpoint/ # 模型转换工具
2.2 模型工厂模式
项目使用工厂模式管理多个模型,核心文件:aiak_training_llm/models/factory.py
2.2.1 注册机制
# 全局注册表
MODEL_ARCH_CONFIGS = {} # 模型架构配置
MODEL_ARCH_TO_FAMILY = {} # 架构到模型族映射
MODEL_FAMILY_TO_ARCHS = {} # 模型族到架构映射
MODEL_FAMILY_TO_PROVIDER = {} # 模型族到提供者映射
# 注册模型配置
@register_model_config(model_family="llava-ov-1.5", model_arch="llava-ov-1.5-4b")
def llava_one_vision_1_5_4b():
return LlavaOnevision1_5Config(
num_layers=36,
hidden_size=2560,
ffn_hidden_size=9728,
num_attention_heads=32,
group_query_attention=True,
num_query_groups=8,
vocab_size_in_config_file=151936,
# ... 更多配置
)
# 注册模型提供者
@register_model_provider(model_family=[VisionLanguageModelFamilies.LLAVA_OV_1_5])
def rice_vl_model_provider(pre_process=True, post_process=True):
# 模型构建逻辑
pass2.3 训练器工厂模式
训练器也采用工厂模式注册:aiak_training_llm/train/trainer_builder.py
@register_model_trainer(
model_family=[constants.VisionLanguageModelFamilies.LLAVA_OV_1_5],
training_phase=constants.TrainingPhase.PRETRAIN
)
def default_pretrain_trainer(train_args):
trainer = MegatronTrainer(
train_args=train_args,
train_valid_test_dataset_provider=train_valid_test_dataset_provider,
model_provider=model_provider,
model_type=model_type,
forward_step_func=forward_step,
)
return trainer3. 模型实现
3.1 LlavaOnevision1_5 模型
核心文件:aiak_training_llm/models/llavaov_1_5/llavaov_1_5_model.py
3.1.1 模型组成
LlavaOnevision1_5由三个主要组件构成:
class LlavaOnevision1_5(MegatronModule):
def __init__(self, ...):
# 1. 视觉编码器(Vision Encoder)
if self.add_encoder:
self.vision_model = RiceViTModel(
vision_config,
vision_layer_spec,
)
# 2. 适配器(Adapter)- 连接视觉和语言模态
self.adapter = Adapter(
adapter_config,
adapter_layer_spec,
input_size=vision_config.hidden_size,
output_size=language_config.hidden_size,
)
# 3. 语言模型(Language Model)
if self.add_decoder:
self.language_model = QwenModel(
config=language_config,
transformer_layer_spec=language_layer_spec,
vocab_size=language_vocab_size,
max_sequence_length=language_max_sequence_length,
# ... 更多参数
)3.1.2 前向传播流程
def forward(
self,
images: torch.Tensor, # 图像输入
image_grid_thw: torch.Tensor, # 图像网格维度(时间、高、宽)
input_ids: torch.Tensor, # 文本token ID
position_ids: torch.Tensor, # 位置ID
attention_mask: torch.Tensor, # 注意力掩码
labels: torch.Tensor = None, # 标签(训练时使用)
packed_seq_params: PackedSeqParams = None, # 序列打包参数
inference_params: InferenceParams = None, # 推理参数
):
# 步骤1:视觉编码
if self.add_encoder and images is not None:
# 通过视觉模型提取图像特征
image_embeddings, window_index = self.vision_model(
images,
grid_thw=image_grid_thw
)
# 通过适配器映射到语言模型维度
image_embeddings = self.adapter(image_embeddings, window_index)
# 步骤2:文本编码
if self.pre_process:
language_embeddings = self.language_model.embedding(
input_ids=input_ids,
position_ids=None
)
# 步骤3:融合视觉和语言嵌入
if images is not None:
# 找到图像token位置并替换为视觉嵌入
images_mask = (input_ids == self.config.image_token_id)
combined_embeddings = language_embeddings.masked_scatter(
images_mask,
image_embeddings
)
# 步骤4:通过语言模型生成输出
output = self.language_model(
input_ids=None,
decoder_input=combined_embeddings,
attention_mask=attention_mask,
labels=labels,
# ... 其他参数
)
return output3.2 视觉模型:RiceViTModel
核心文件:aiak_training_llm/models/llavaov_1_5/rice_vision_model.py
3.2.1 关键组件
class RiceViTModel(VisionModel):
def __init__(self, config, transformer_layer_spec, spatial_merge_size=2):
# 1. Patch嵌入层
self.patch_embed = PatchEmbed(
patch_size=config.patch_size, # 14
in_channels=config.in_channels, # 3
embed_dim=config.hidden_size, # 1024
)
# 2. 旋转位置编码
self.rotary_pos_emb = VisionRotaryEmbedding(
dim=config.kv_channels // 2
)
# 3. Transformer块
self.decoder = TransformerBlock(
config=config,
spec=transformer_layer_spec,
)
# 4. 类别token(用于全局特征)
self.class_embedding = torch.randn(config.hidden_size)
self.class_pos_emb = torch.randn(1, config.kv_channels // 2)
# 5. 预层归一化
self.pre_layernorm = torch.nn.LayerNorm(
config.hidden_size,
eps=1e-4
)3.2.2 位置编码策略
Rice-ViT使用特殊的2D旋转位置编码:
def rot_pos_emb(self, grid_thw):
"""
为每个图像patch生成2D旋转位置编码
grid_thw: [num_images, 3] - (时间, 高度, 宽度)
"""
pos_ids = []
for t, h, w in grid_thw:
# 生成高度位置ID
hpos_ids = torch.arange(h).unsqueeze(1).expand(-1, w)
hpos_ids = hpos_ids.reshape(
h // self.spatial_merge_size,
self.spatial_merge_size,
w // self.spatial_merge_size,
self.spatial_merge_size,
)
hpos_ids = hpos_ids.permute(0, 2, 1, 3).flatten()
# 生成宽度位置ID
wpos_ids = torch.arange(w).unsqueeze(0).expand(h, -1)
# ... 类似处理 ...
pos_ids.append(torch.stack([hpos_ids, wpos_ids], dim=-1))
# 应用旋转位置编码
rotary_pos_emb_full = self.rotary_pos_emb(max_grid_size)
rotary_pos_emb = rotary_pos_emb_full[pos_ids].flatten(1)
return rotary_pos_emb3.3 语言模型:QwenModel
基于Qwen系列架构,支持: - GQA (Grouped Query Attention):减少KV缓存内存 - RoPE位置编码:更好的长度外推能力 - SwiGLU激活:更好的性能 - RMSNorm:更稳定的训练
3.4 适配器(Adapter)
核心文件:aiak_training_llm/models/qwen_vl/adapter.py
适配器负责将视觉特征映射到语言模型空间:
class Adapter(MegatronModule):
"""
2层MLP适配器
Input: [seq_len, hidden_size_vision]
Output: [seq_len, hidden_size_language]
"""
def __init__(self, config, layer_spec, input_size, output_size):
# 第一层:投影到中间维度
self.linear_fc1 = build_module(
layer_spec.linear_fc1,
input_size,
config.ffn_hidden_size,
)
# 激活函数(通常是GELU)
self.activation_func = config.activation_func
# 第二层:投影到输出维度
self.linear_fc2 = build_module(
layer_spec.linear_fc2,
config.ffn_hidden_size,
output_size,
)3.5 模型配置
3.5.1 支持的模型规格
| 模型 | 层数 | 隐藏层维度 | FFN维度 | 注意力头数 | 参数量 |
|---|---|---|---|---|---|
| llava-ov-1.5-3b | 36 | 2048 | 11008 | 16 | ~3B |
| llava-ov-1.5-4b | 36 | 2560 | 9728 | 32 | ~4B |
| llava-ov-1.5-8b | 36 | 4096 | 12288 | 32 | ~8B |
| llava-ov-1.5-14b | 40 | 5120 | 17408 | 40 | ~14B |
| llava-ov-1.5-32b | 64 | 5120 | 25600 | 64 | ~32B |
| llava-ov-1.5-30b-a3b | 48 | 2048 | 6144 | 32 | ~30B (MoE) |
3.5.2 视觉模型配置
VisionConfig(
num_layers=24, # Transformer层数
hidden_size=1024, # 隐藏层维度
ffn_hidden_size=4096, # FFN维度
num_attention_heads=16, # 注意力头数
patch_size=14, # Patch大小
image_size=(1344, 1344), # 最大图像尺寸
kv_channels=64, # KV通道数
normalization="LayerNorm", # 归一化类型
position_embedding_type="rope" # 位置编码类型
)4. 训练框架
4.1 训练器架构
4.1.1 MegatronTrainer
核心文件:aiak_training_llm/train/megatron_trainer.py
class MegatronTrainer(object):
"""Megatron训练器基类"""
def __init__(
self,
train_args, # 训练参数
train_valid_test_dataset_provider, # 数据集提供者
model_provider, # 模型提供者
model_type, # 模型类型
forward_step_func, # 前向传播函数
process_non_loss_data_func=None, # 非损失数据处理函数
get_embedding_ranks=None, # 获取嵌入层所在rank
get_position_embedding_ranks=None, # 获取位置嵌入所在rank
):
self.train_args = train_args
self.train_valid_test_datasets_provider = train_valid_test_dataset_provider
self.model_provider = model_provider
self.model_type = model_type
self.forward_step_func = forward_step_func
# ...
def train(self):
"""启动训练"""
pretrain(
train_args=self.train_args,
train_valid_test_dataset_provider=self.train_valid_test_datasets_provider,
model_provider=self.model_provider,
model_type=self.model_type,
forward_step_func=self.forward_step_func,
# ...
)4.2 训练主流程
核心文件:aiak_training_llm/train/training_utils.py
4.2.1 预训练主函数
def pretrain(
train_args,
train_valid_test_dataset_provider,
model_provider,
model_type,
forward_step_func,
process_non_loss_data_func=None,
):
"""
主训练流程:
1. 初始化Megatron
2. 构建模型、优化器和学习率调度器
3. 获取数据迭代器
4. 执行训练循环
"""
# 步骤1:初始化
initialize_aiak_megatron(
args=train_args,
get_embedding_ranks=get_embedding_ranks,
get_position_embedding_ranks=get_position_embedding_ranks
)
# 步骤2:构建模型和优化器
model, ema, optimizer, opt_param_scheduler = setup_model_and_optimizer(
model_provider,
model_type,
checkpointing_context=checkpointing_context
)
# 步骤3:构建数据迭代器
train_data_iterator, valid_data_iterator, test_data_iterator = \
build_train_valid_test_data_iterators(
train_valid_test_dataset_provider
)
# 步骤4:训练循环
if args.do_train and args.train_iters > 0:
iteration, num_floating_point_operations_so_far = train(
forward_step_func=forward_step_func,
model=model,
ema=ema,
optimizer=optimizer,
opt_param_scheduler=opt_param_scheduler,
train_data_iterator=train_data_iterator,
valid_data_iterator=valid_data_iterator,
# ...
)4.2.2 训练循环
def train(forward_step_func, model, ema, optimizer, ...):
"""主训练循环"""
while iteration < args.train_iters:
# 1. 执行训练步骤
loss_dict, skipped_iter, should_checkpoint, should_exit, exit_code, \
grad_norm, num_zeros_in_grad = train_step(
forward_step_func,
train_data_iterator,
model,
optimizer,
opt_param_scheduler,
config
)
# 2. 更新EMA模型(如果启用)
if args.enable_ema and ema is not None:
for e, m in zip(ema, model):
update_ema(e, m, rate=args.ema_decay)
# 3. 日志记录
report_memory_flag = training_log(
loss_dict,
total_loss_dict,
learning_rate,
iteration,
loss_scale,
# ...
)
# 4. 验证评估(按间隔)
if args.eval_interval and iteration % args.eval_interval == 0:
evaluate_and_print_results(
prefix,
forward_step_func,
valid_data_iterator,
model,
iteration,
# ...
)
# 5. 保存检查点
should_exit = checkpoint_and_decide_exit(
model,
optimizer,
opt_param_scheduler,
iteration,
num_floating_point_operations_so_far,
# ...
)
iteration += 14.3 训练步骤详解
def train_step(forward_step_func, data_iterator, model, optimizer, ...):
"""单个训练步骤"""
# 1. 清空梯度
for model_chunk in model:
model_chunk.zero_grad_buffer()
optimizer.zero_grad()
# 2. 前向和反向传播
forward_backward_func = get_forward_backward_func()
losses_reduced = forward_backward_func(
forward_step_func=forward_step_func,
data_iterator=data_iterator,
model=model,
num_microbatches=get_num_microbatches(),
seq_length=args.seq_length,
micro_batch_size=args.micro_batch_size,
forward_only=False
)
# 3. 更新参数
update_successful, grad_norm, num_zeros_in_grad = optimizer.step()
# 4. 更新学习率
if update_successful:
increment = get_num_microbatches() * args.micro_batch_size * args.data_parallel_size
opt_param_scheduler.step(increment=increment)
skipped_iter = 0
else:
skipped_iter = 1
# 5. 计算平均损失
loss_reduced = {}
for key in losses_reduced[0].keys():
numerator = sum(x[key][0] for x in losses_reduced)
denominator = sum(x[key][1] for x in losses_reduced)
loss_reduced[key] = numerator / denominator
return loss_reduced, skipped_iter, ...4.4 前向传播函数
4.4.1 预训练阶段
核心文件:aiak_training_llm/train/pretrain/pretrain_llavaov_1_5.py
def forward_step(data_iterator, model):
"""预训练前向传播步骤"""
# 1. 获取批次数据
images, image_grid_thw, pixel_values_videos, video_grid_thw, \
input_ids, position_ids, attention_mask, \
labels, loss_mask, attn_mask_type, packed_seq_params = get_batch(data_iterator)
# 2. 模型前向传播
output_tensor = model(
images, # 图像输入
image_grid_thw, # 图像网格
input_ids, # 文本token
position_ids, # 位置ID
attention_mask, # 注意力掩码
attn_mask_type, # 掩码类型
labels, # 标签
packed_seq_params, # 打包序列参数
pixel_values_videos=pixel_values_videos, # 视频输入
video_grid_thw=video_grid_thw # 视频网格
)
# 3. 返回输出和损失函数
return output_tensor, partial(loss_func, loss_mask)4.4.2 损失函数
def loss_func(loss_mask: torch.Tensor, output_tensor: torch.Tensor):
"""
损失函数计算
Args:
loss_mask: 损失掩码,标记哪些位置需要计算损失
output_tensor: 模型输出的损失张量
Returns:
loss: 标量损失值
local_num_tokens: 本地token数量
loss_reduced_dict: 包含各种统计信息的字典
"""
losses = output_tensor.float()
loss_mask = loss_mask.view(-1).float()
# 计算加权损失
total_tokens = loss_mask.sum()
loss = torch.cat([
torch.sum(losses.view(-1) * loss_mask).view(1),
total_tokens.view(1)
])
# Context Parallel归约
if args.context_parallel_size > 1:
torch.distributed.all_reduce(loss, group=mpu.get_context_parallel_group())
# NaN检查
if args.check_for_nan_in_loss_and_grad:
assert not loss[0].isnan(), f'Found NaN in loss on rank {rank}'
# 跨数据并行组归约
reporting_loss = loss.clone().detach()
torch.distributed.all_reduce(reporting_loss, group=mpu.get_data_parallel_group())
loss_reduced_dict = {
'lm loss': (reporting_loss[0], reporting_loss[1])
}
# 可变序列长度处理
if args.variable_seq_lengths:
num_input_tokens = output_tensor.shape[0] * output_tensor.shape[1]
input_tokens = torch.tensor(num_input_tokens, dtype=torch.int)
torch.distributed.all_reduce(input_tokens, group=mpu.get_data_parallel_group())
loss_reduced_dict["total_inputs"] = input_tokens.item()
return (
loss[0] * args.context_parallel_size,
loss[1].to(torch.int),
loss_reduced_dict
)4.5 模型和优化器初始化
def setup_model_and_optimizer(model_provider_func, model_type):
"""设置模型和优化器"""
# 1. 构建模型
model = get_model(model_provider_func, model_type)
# 2. 构建优化器配置
config = OptimizerConfig(
# 从args中提取配置
lr=args.lr,
weight_decay=args.weight_decay,
adam_beta1=args.adam_beta1,
adam_beta2=args.adam_beta2,
# ...
)
# 3. 创建优化器
optimizer = get_megatron_optimizer(
config,
model,
use_gloo_process_groups=args.enable_gloo_process_groups
)
# 4. 创建学习率调度器
opt_param_scheduler = get_optimizer_param_scheduler(optimizer)
# 5. 加载检查点(如果存在)
if args.load is not None:
args.iteration, args.num_floating_point_operations_so_far = \
load_checkpoint(model, optimizer, opt_param_scheduler)
else:
args.iteration = 0
# 6. 创建EMA模型(如果启用)
if args.enable_ema:
ema = get_model(model_provider_func, model_type)
if args.iteration == 0:
# 初始化EMA为模型参数
for e, m in zip(ema, model):
update_ema(e, m, rate=0)
else:
load_checkpoint(ema, None, None, load_arg="load_ema")
return model, ema, optimizer, opt_param_scheduler5. 数据处理
5.1 数据集架构
项目支持多种数据格式和加载方式:
5.1.1 数据集类型
- 预训练数据集
- WebDataset格式(推荐)
- 支持大规模数据流式加载
- 支持离线样本打包(Sample Packing)
- SFT数据集
- HuggingFace Datasets格式
- 支持多种对话格式(Alpaca, ShareGPT等)
- 支持多模态输入(图像+文本)
5.1.2 数据配置
核心文件:aiak_training_llm/data/blended_hf_dataset_config.py
@dataclass
class SFTDatasetConfig(BlendedHuggingFaceDatasetConfig):
"""SFT数据集配置"""
# 数据路径和混合
dataset_config_file: str # 数据集配置文件路径
blend: Optional[List] # 数据混合比例
blend_per_split: Optional[List] # 各分割的混合比例
# 模板和处理器
chat_template: Optional[ChatTemplate] # 对话模板
processor: Optional[ProcessorMixin] # 数据处理器
# 训练配置
sequence_length: int # 最大序列长度
ignore_index: int = -100 # 忽略索引
train_on_prompt: bool = False # 是否在提示上训练
# 数据增强
packing: bool = False # 是否打包数据
sort_batch: bool = False # 是否排序批次
packing_batch_size: int = 10000 # 打包批次大小
# 流式加载
streaming: bool = False # 是否流式加载
streaming_buffer_size: int = 16384 # 流式缓冲区大小
# 并行配置
context_parallel_size: Optional[int] = None # 上下文并行大小5.2 SFT数据集
核心文件:aiak_training_llm/data/sft_dataset.py
5.2.1 支持的数据格式
class SFTDataFormats:
"""支持的SFT数据格式"""
ALPACA = "alpaca" # Alpaca格式
SHAREGPT = "sharegpt" # ShareGPT格式
OPENAI = "openai" # OpenAI格式5.2.2 数据加载流程
class SFTDataset(HuggingFaceDataset):
"""SFT数据集类"""
def __init__(self, config: SFTDatasetConfig):
super().__init__(config)
# 1. 加载原始数据集
raw_datasets = self._load_datasets()
# 2. 格式转换
processed_datasets = self._convert_format(raw_datasets)
# 3. 分词处理
tokenized_datasets = self._tokenize(processed_datasets)
# 4. 数据打包(可选)
if config.packing:
tokenized_datasets = self._pack_samples(tokenized_datasets)
# 5. 排序(可选)
if config.sort_batch:
tokenized_datasets = self._sort_by_length(tokenized_datasets)
self.dataset = tokenized_datasets5.3 多模态数据处理
核心文件:aiak_training_llm/data/multimodal/qwen2vl_task_encoder.py
5.3.1 任务编码器
class Qwen2VLTaskEncoder:
"""Qwen2-VL任务编码器"""
def __init__(self, args):
self.args = args
self.tokenizer = get_tokenizer()
self.processor = AutoProcessor.from_pretrained(
args.hf_tokenizer_path,
trust_remote_code=True
)
def encode_sample(self, sample):
"""
编码单个样本
Args:
sample: 包含文本和图像的字典
{
"messages": [...], # 对话消息
"images": [...], # 图像路径列表
"videos": [...], # 视频路径列表(可选)
}
Returns:
编码后的样本,包含:
- tokens: token ID
- labels: 标签
- imgs: 图像张量
- image_grid_thw: 图像网格维度
- attn_mask: 注意力掩码
- cu_lengths: 累积长度(用于打包)
"""
# 1. 应用对话模板
text = self.processor.apply_chat_template(
sample["messages"],
tokenize=False,
add_generation_prompt=False
)
# 2. 处理图像
image_inputs = None
if sample.get("images"):
images = [Image.open(img_path) for img_path in sample["images"]]
image_inputs = self.processor.image_processor(
images,
return_tensors="pt"
)
# 3. 分词
text_inputs = self.tokenizer(
text,
return_tensors="pt",
padding=False,
truncation=True,
max_length=self.args.seq_length
)
# 4. 合并输入
encoded = {
"tokens": text_inputs["input_ids"],
"labels": self._build_labels(text_inputs, sample),
"attn_mask": text_inputs["attention_mask"],
}
if image_inputs:
encoded.update({
"imgs": image_inputs["pixel_values"],
"image_grid_thw": image_inputs["image_grid_thw"],
})
return encoded5.4 数据打包(Sample Packing)
数据打包技术将多个短样本组合成一个批次,提高训练效率。
核心文件:aiak_training_llm/data/sft_supervised_utils.py
def pack_samples(samples, max_length, context_parallel_size=1):
"""
将多个样本打包到固定长度
Args:
samples: 样本列表
max_length: 最大序列长度
context_parallel_size: 上下文并行大小
Returns:
打包后的样本
"""
packed_samples = []
current_pack = {
"input_ids": [],
"labels": [],
"attention_mask": [],
"cu_seqlens": [0], # 累积序列长度
"images": [],
"image_grid_thw": [],
}
current_length = 0
for sample in samples:
sample_length = len(sample["input_ids"])
# 检查是否可以添加到当前pack
if current_length + sample_length <= max_length:
# 添加到当前pack
current_pack["input_ids"].extend(sample["input_ids"])
current_pack["labels"].extend(sample["labels"])
current_pack["attention_mask"].extend(sample["attention_mask"])
current_pack["cu_seqlens"].append(current_length + sample_length)
if "images" in sample:
current_pack["images"].append(sample["images"])
current_pack["image_grid_thw"].append(sample["image_grid_thw"])
current_length += sample_length
else:
# 当前pack已满,开始新pack
if current_length > 0:
# 填充到max_length(考虑CP)
pad_length = max_length - current_length
if context_parallel_size > 1:
# 确保长度可被CP整除
pad_length = (max_length // context_parallel_size) * context_parallel_size - current_length
current_pack["input_ids"].extend([0] * pad_length)
current_pack["labels"].extend([-100] * pad_length)
current_pack["attention_mask"].extend([0] * pad_length)
packed_samples.append(current_pack)
# 开始新pack
current_pack = initialize_new_pack()
current_length = 0
return packed_samples5.5 数据加载器
核心文件:aiak_training_llm/data/multimodal/dataloader_provider.py
def get_train_loader(dataset, collator):
"""
创建训练数据加载器
Args:
dataset: 数据集对象
collator: 数据整理函数
Returns:
数据加载器迭代器
"""
args = get_args()
# 1. 创建数据采样器
if args.dataloader_type == 'cyclic':
# 循环数据加载器(用于WebDataset)
dataloader = build_pretraining_data_loader(dataset)
else:
# 标准数据加载器
from torch.utils.data import DataLoader
dataloader = DataLoader(
dataset,
batch_size=args.micro_batch_size,
shuffle=True,
num_workers=args.num_workers,
collate_fn=collator,
pin_memory=True,
drop_last=True,
)
# 2. 创建循环迭代器
def cyclic_iter(dataloader):
while True:
for batch in dataloader:
yield batch
return cyclic_iter(dataloader)5.6 数据整理器(Data Collator)
class MultiModalDataCollatorForSupervisedDataset:
"""多模态监督数据集整理器"""
def __init__(self, processor, plugin):
self.processor = processor
self.plugin = plugin
def __call__(self, samples):
"""
整理一个批次的样本
Args:
samples: 样本列表
Returns:
批次字典
"""
# 1. 提取各个字段
input_ids = [sample["input_ids"] for sample in samples]
labels = [sample["labels"] for sample in samples]
attention_masks = [sample["attention_mask"] for sample in samples]
# 2. 填充到相同长度
max_length = max(len(ids) for ids in input_ids)
padded_input_ids = []
padded_labels = []
padded_attention_masks = []
for ids, labs, mask in zip(input_ids, labels, attention_masks):
pad_length = max_length - len(ids)
padded_input_ids.append(ids + [0] * pad_length)
padded_labels.append(labs + [-100] * pad_length)
padded_attention_masks.append(mask + [0] * pad_length)
# 3. 转换为张量
batch = {
"input_ids": torch.tensor(padded_input_ids, dtype=torch.long),
"labels": torch.tensor(padded_labels, dtype=torch.long),
"attention_mask": torch.tensor(padded_attention_masks, dtype=torch.long),
}
# 4. 处理图像(如果存在)
if "images" in samples[0]:
images = []
image_grid_thw = []
for sample in samples:
if sample["images"] is not None:
images.append(sample["images"])
image_grid_thw.append(sample["image_grid_thw"])
if images:
batch["images"] = torch.cat(images, dim=0)
batch["image_grid_thw"] = torch.tensor(image_grid_thw, dtype=torch.long)
return batch6. 训练流程
6.1 三阶段训练策略
LLaVA-OneVision-1.5采用三阶段训练策略:
6.1.1 Stage 1: Alignment Training(对齐训练)
目标:学习视觉-语言对齐
数据:LLaVA-558K(图像描述数据)
配置: - 只训练Adapter(冻结LLM和ViT) - 学习率:1e-4 - 训练步数:2500 - 序列长度:32768
脚本示例:
# examples/llava_ov_1_5/quick_start/stage_1_alignment_llava_ov_4b.sh
MODEL_ARGS=(
--model-name llava-ov-1.5-4b
)
TRAINING_ARGS=(
--training-phase sft
--trainable-modules adapter # 只训练adapter
--seq-length 32768
--micro-batch-size 1
--global-batch-size 8
--lr 1.0e-4
--train-iters 2500
--bf16
)6.1.2 Stage 1.5: Mid-Training(中期训练)
目标:增强视觉理解能力
数据:LLaVA-OneVision-1.5-Mid-Training-85M(大规模高质量描述数据)
配置: - 训练所有组件(LLM + ViT + Adapter) - 学习率:1e-5 - 训练步数:20000 - 支持样本打包
特点: - 概念平衡的数据分布 - 原生分辨率图像(最大1344x1344) - 严格的质量控制
6.1.3 Stage 2: Instruction Tuning(指令微调)
目标:学习指令跟随和多任务能力
数据:LLaVA-OneVision-1.5-Instruct-Data(综合指令数据)
配置: - 训练所有组件 - 学习率:1e-5 - 训练步数:3500 - 支持多种任务类型
6.2 训练启动流程
6.2.1 入口脚本
核心文件:aiak_training_llm/train.py
def main():
"""训练主入口"""
# 1. 解析参数
args = parse_train_args()
# 2. 构建训练器
trainer = build_model_trainer(args)
# 3. 启动训练
trainer.train()6.2.2 参数解析
核心文件:aiak_training_llm/train/arguments.py
def parse_train_args():
"""解析训练参数"""
args = parse_arguments(
extra_args_provider=aiak_extra_train_args_provider,
validate_extra_args_provider=validate_aiak_extra_args,
)
return args
def aiak_extra_train_args_provider(parser):
"""添加AIAK自定义参数"""
# 1. 模型参数
parser = _add_extra_model_args(parser)
# 2. 分词器参数
parser = _add_extra_tokenizer_args(parser)
# 3. SFT参数
parser = _add_extra_sft_args(parser)
# 4. 多模态参数
parser = _add_extra_multimodal_args(parser)
# 5. 并行参数
parser = _add_extra_parallel_args(parser)
return parser6.2.3 关键参数说明
模型参数:
--model-name llava-ov-1.5-4b # 模型架构
--trainable-modules adapter # 可训练模块(all/adapter/language_model/vision_model)数据参数:
--data-path /path/to/dataset # 数据路径
--dataloader-type external # 数据加载器类型
--split 100,0,0 # 训练/验证/测试分割
--chat-template qwen2-vl # 对话模板
--num-workers 16 # 数据加载工作线程训练参数:
--training-phase sft # 训练阶段(pretrain/sft)
--seq-length 32768 # 序列长度
--micro-batch-size 1 # 微批次大小
--global-batch-size 8 # 全局批次大小
--train-iters 2500 # 训练迭代次数
--lr 1.0e-4 # 学习率
--min-lr 1.0e-6 # 最小学习率
--lr-decay-style cosine # 学习率衰减策略
--lr-warmup-fraction 0.002 # 学习率预热比例
--clip-grad 1.0 # 梯度裁剪
--optimizer adam # 优化器
--adam-beta1 0.9 # Adam beta1
--adam-beta2 0.99 # Adam beta2
--weight-decay 0 # 权重衰减并行参数:
--tensor-model-parallel-size 1 # 张量并行度
--pipeline-model-parallel-size 1 # 流水线并行度
--use-distributed-optimizer # 使用分布式优化器
--attention-backend flash # 注意力后端(flash/native)优化参数:
--bf16 # 使用BF16精度
--recompute-granularity full # 重计算粒度
--recompute-method uniform # 重计算方法
--recompute-num-layers 4 # 重计算层数6.3 分布式训练
6.3.1 并行策略
LLaVA-OneVision-1.5支持多种并行策略:
- 数据并行(Data Parallelism, DP)
- 每个GPU维护完整模型副本
- 梯度在所有GPU间同步
- 张量并行(Tensor Parallelism, TP)
- 模型权重按张量维度切分
- 适合大型模型
- 流水线并行(Pipeline Parallelism, PP)
- 模型按层切分到不同GPU
- 使用微批次流水线减少气泡
- 上下文并行(Context Parallelism, CP)
- 长序列按序列维度切分
- 支持超长上下文训练
- 序列并行(Sequence Parallelism, SP)
- 激活值按序列维度切分
- 减少内存占用
6.3.2 并行配置示例
# 8GPU单节点,使用TP=2, PP=1
GPUS_PER_NODE=8
TP=2
PP=1
# 实际数据并行度 = 8 / (2 * 1) = 4
# 全局批次大小 = 微批次大小 * 数据并行度 * 梯度累积步数6.4 内存优化技术
6.4.1 激活检查点(Activation Checkpointing)
TRAINING_ARGS=(
--recompute-granularity full # 完全重计算
--recompute-method uniform # 均匀选择层
--recompute-num-layers 4 # 重计算4层
)6.4.2 分布式优化器
MODEL_PARALLEL_ARGS=(
--use-distributed-optimizer # 启用分布式优化器
)特点: - 优化器状态跨DP组分片 - 减少优化器内存占用 - 支持Adam/AdamW
6.4.3 Flash Attention
MODEL_PARALLEL_ARGS=(
--attention-backend flash # 使用Flash Attention
)优势: - 更快的注意力计算 - 更低的内存占用 - 支持长序列
6.5 检查点管理
6.5.1 保存检查点
TRAINING_ARGS=(
--save /path/to/checkpoint # 检查点保存路径
--save-interval 2000 # 保存间隔
--ckpt-format torch # 检查点格式(torch/torch_dist/zarr)
)6.5.2 加载检查点
TRAINING_ARGS=(
--load /path/to/checkpoint # 检查点加载路径
--ckpt-fully-parallel-load # 完全并行加载
)6.5.3 异步保存
支持异步检查点保存以减少训练中断:
TRAINING_ARGS=(
--async-save # 启用异步保存
)7. 模型转换
7.1 转换流程
LLaVA-OneVision-1.5提供完整的模型转换工具,支持HuggingFace格式与Megatron格式互转。
7.1.1 HuggingFace → Megatron
转换脚本:examples/llava_ov_1_5/convert/convert_4b_hf_to_mcore.sh
#!/bin/bash
LOAD=$1 # HuggingFace模型路径
SAVE=$2 # Megatron输出路径
TP=$3 # 张量并行度
PP=$4 # 流水线并行度
# 步骤1:转换语言模型
python $CONVERT_CHECKPOINT_PATH/model.py \
--load_platform=huggingface \
--save_platform=mcore \
--common_config_path=$CONVERT_CHECKPOINT_PATH/config/llava-ov-1.5-4b/qwen3.json \
--tensor_model_parallel_size=$TP \
--pipeline_model_parallel_size=$PP \
--load_ckpt_path=$LOAD \
--save_ckpt_path=$SAVE_LANGUAGE_MODEL
# 步骤2:转换视觉模型
python $CONVERT_CHECKPOINT_PATH/model.py \
--load_platform=huggingface \
--save_platform=mcore \
--common_config_path=$CONVERT_CHECKPOINT_PATH/config/llava-ov-1.5-4b/vision-model.json \
--tensor_model_parallel_size=$TP \
--load_ckpt_path=$LOAD \
--save_ckpt_path=$SAVE_VISION_MODEL
# 步骤3:转换适配器
python $CONVERT_CHECKPOINT_PATH/custom/llavaov_1_5/adapter.py \
--load_platform=huggingface \
--save_platform=mcore \
--common_config_path=$CONVERT_CHECKPOINT_PATH/config/llava-ov-1.5-4b/adapter.json \
--tensor_model_parallel_size=$TP \
--load_ckpt_path=$LOAD \
--save_ckpt_path=$SAVE_ADAPTER
# 步骤4:转换视觉补丁嵌入
python $CONVERT_CHECKPOINT_PATH/custom/llavaov_1_5/vision_patch.py \
--load_platform=huggingface \
--save_platform=mcore \
--common_config_path=$CONVERT_CHECKPOINT_PATH/config/llava-ov-1.5-4b/vision-patch.json \
--load_ckpt_path=$LOAD \
--save_ckpt_path=$SAVE_PATCH
# 步骤5:合并所有组件
python $CONVERT_CHECKPOINT_PATH/custom/llavaov_1_5/merge_megatron.py \
--megatron_path $AIAK_MAGATRON_PATH \
--language_model_path $SAVE_LANGUAGE_MODEL/release \
--vision_model_path $SAVE_VISION_MODEL/release \
--vision_patch $SAVE_PATCH/release \
--adapter_path $SAVE_ADAPTER/release \
--save_ckpt_path $SAVE/release \
--tensor_model_parallel_size $TP \
--pipeline_model_parallel_size $PP7.1.2 Megatron → HuggingFace
转换脚本:examples/llava_ov_1_5/convert/convert_4b_mcore_to_hf.sh
#!/bin/bash
LOAD=$1 # Megatron模型路径
SAVE=$2 # HuggingFace输出路径
TP=$3 # 源张量并行度
PP=$4 # 源流水线并行度
# 步骤1:转换语言模型
python $CONVERT_CHECKPOINT_PATH/model.py \
--load_platform=mcore \
--save_platform=huggingface \
--common_config_path=$CONVERT_CHECKPOINT_PATH/config/llava-ov-1.5-4b/qwen3.json \
--tensor_model_parallel_size=$TP \
--pipeline_model_parallel_size=$PP \
--load_ckpt_path=$LOAD \
--save_ckpt_path=$SAVE_LANGUAGE_MODEL
# 步骤2-4:转换其他组件(类似)
# ...
# 步骤5:合并到HuggingFace格式
python $CONVERT_CHECKPOINT_PATH/custom/llavaov_1_5/merge_huggingface.py \
--language_model_path $SAVE_LANGUAGE_MODEL \
--vision_model_path $SAVE_VISION_MODEL \
--adapter_path $SAVE_ADAPTER \
--vision_patch $SAVE_PATCH \
--save_ckpt_path $SAVE
# 复制非模型文件(tokenizer等)
find $ORIGINAL_HF_PATH/ -type f -not -iname '*safetensors*' -exec cp {} $SAVE/ ';'7.2 转换配置
转换配置文件定义了权重映射规则:
7.2.1 语言模型配置示例
文件:tools/convert_checkpoint/config/llava-ov-1.5-4b/qwen3.json
{
"model_type": "GPT",
"num_layers": 36,
"hidden_size": 2560,
"ffn_hidden_size": 9728,
"num_attention_heads": 32,
"num_query_groups": 8,
"weight_mapping": {
"embedding.word_embeddings.weight": "model.embed_tokens.weight",
"decoder.layers.{}.self_attention.linear_qkv.weight": [
"model.layers.{}.self_attn.q_proj.weight",
"model.layers.{}.self_attn.k_proj.weight",
"model.layers.{}.self_attn.v_proj.weight"
],
"decoder.layers.{}.self_attention.linear_proj.weight":
"model.layers.{}.self_attn.o_proj.weight",
"decoder.layers.{}.mlp.linear_fc1.weight": [
"model.layers.{}.mlp.gate_proj.weight",
"model.layers.{}.mlp.up_proj.weight"
],
"decoder.layers.{}.mlp.linear_fc2.weight":
"model.layers.{}.mlp.down_proj.weight",
"output_layer.weight": "lm_head.weight"
}
}7.2.2 视觉模型配置示例
文件:tools/convert_checkpoint/config/llava-ov-1.5-4b/vision-model.json
{
"model_type": "VisionModel",
"num_layers": 24,
"hidden_size": 1024,
"num_attention_heads": 16,
"weight_mapping": {
"decoder.layers.{}.self_attention.linear_qkv.weight": [
"visual.blocks.{}.attn.qkv.weight"
],
"decoder.layers.{}.self_attention.linear_proj.weight":
"visual.blocks.{}.attn.proj.weight",
"decoder.layers.{}.pre_mlp_layernorm.weight":
"visual.blocks.{}.norm2.weight",
"decoder.layers.{}.mlp.linear_fc1.weight":
"visual.blocks.{}.mlp.fc1.weight",
"decoder.layers.{}.mlp.linear_fc2.weight":
"visual.blocks.{}.mlp.fc2.weight"
}
}7.3 权重映射逻辑
核心文件:tools/convert_checkpoint/model.py
def convert_checkpoint(load_platform, save_platform, config, load_path, save_path):
"""执行检查点转换"""
# 1. 加载源检查点
if load_platform == "huggingface":
src_checkpoint = load_huggingface_checkpoint(load_path)
elif load_platform == "mcore":
src_checkpoint = load_mcore_checkpoint(load_path, config)
# 2. 创建目标检查点结构
dst_checkpoint = {}
# 3. 根据映射规则转换权重
weight_mapping = config["weight_mapping"]
for dst_key_template, src_key_template in weight_mapping.items():
# 处理层索引
if "{}" in dst_key_template:
for layer_idx in range(config["num_layers"]):
dst_key = dst_key_template.format(layer_idx)
if isinstance(src_key_template, list):
# 需要拼接多个源权重
src_keys = [k.format(layer_idx) for k in src_key_template]
weights = [src_checkpoint[k] for k in src_keys]
dst_checkpoint[dst_key] = torch.cat(weights, dim=0)
else:
src_key = src_key_template.format(layer_idx)
dst_checkpoint[dst_key] = src_checkpoint[src_key]
else:
# 直接映射
dst_checkpoint[dst_key_template] = src_checkpoint[src_key_template]
# 4. 处理张量并行切分(如果需要)
if save_platform == "mcore" and config.get("tensor_model_parallel_size", 1) > 1:
dst_checkpoint = split_for_tensor_parallel(
dst_checkpoint,
config["tensor_model_parallel_size"]
)
# 5. 保存目标检查点
if save_platform == "huggingface":
save_huggingface_checkpoint(dst_checkpoint, save_path)
elif save_platform == "mcore":
save_mcore_checkpoint(dst_checkpoint, save_path, config)8. 配置与优化
8.1 推荐配置
8.1.1 不同规模模型的配置
4B模型(单节点8卡A100-80GB):
TP=1
PP=1
MBS=1
GBS=8
SEQ_LEN=32768
RECOMPUTE_LAYERS=4
# 内存占用:约60GB/GPU
# 训练速度:约1.2 samples/s8B模型(单节点8卡A100-80GB):
TP=2
PP=1
MBS=1
GBS=8
SEQ_LEN=32768
RECOMPUTE_LAYERS=8
# 内存占用:约70GB/GPU
# 训练速度:约0.8 samples/s32B模型(2节点16卡A100-80GB):
TP=4
PP=2
MBS=1
GBS=16
SEQ_LEN=32768
RECOMPUTE_LAYERS=16
# 内存占用:约75GB/GPU
# 训练速度:约0.3 samples/s8.1.2 性能优化建议
- Flash Attention: 始终启用以获得最佳性能
--attention-backend flash- BF16训练: 推荐使用BF16而非FP16
--bf16- 激活检查点: 根据模型大小调整重计算层数
--recompute-granularity full
--recompute-method uniform
--recompute-num-layers <根据模型调整>- 分布式优化器: 大模型必备
--use-distributed-optimizer- 数据加载: 增加工作线程数
--num-workers 168.2 常见问题与解决
8.2.1 OOM(内存溢出)
症状: CUDA out of memory
解决方案: 1. 减小微批次大小
--micro-batch-size 1 # 最小值- 增加重计算层数
--recompute-num-layers 8 # 或更多- 增加张量并行度
--tensor-model-parallel-size 2 # 或更大- 启用序列并行
--sequence-parallel8.2.2 训练速度慢
症状: 训练吞吐量低
解决方案: 1. 检查Flash Attention是否启用
--attention-backend flash- 优化数据加载
--num-workers 16 # 增加工作线程
--dataloader-type cyclic # 使用循环数据加载器- 启用混合精度
--bf16 # 或 --fp16- 调整并行配置
# 平衡TP和DP
# TP太大会增加通信开销
# DP太大可能导致批次大小过小8.2.3 损失为NaN
症状: Loss突然变成NaN
解决方案: 1. 降低学习率
--lr 5.0e-5 # 从更小的值开始- 启用梯度裁剪
--clip-grad 1.0- 检查数据质量
# 确保数据中没有异常值- 使用更稳定的优化器配置
--optimizer adam
--adam-beta1 0.9
--adam-beta2 0.999
--adam-eps 1e-088.3 监控与日志
8.3.1 TensorBoard监控
LOGGING_ARGS=(
--tensorboard-dir ./tensorboard
--log-timers-to-tensorboard
--log-memory-to-tensorboard
)监控指标: - 训练损失 - 学习率 - 梯度范数 - 内存使用 - 训练吞吐量
8.3.2 WandB集成
export WANDB_API_KEY=<your-api-key>
LOGGING_ARGS=(
--wandb-project llava-onevision-1.5
--wandb-exp-name stage1-alignment
)8.3.3 日志级别
LOGGING_ARGS=(
--log-interval 1 # 日志记录间隔
--detail-log-interval 20 # 详细日志间隔
--timing-log-level 0 # 计时日志级别
)8.4 最佳实践总结
- 渐进式训练
- Stage 1: 只训练Adapter(快速对齐)
- Stage 1.5: 全模型训练(增强能力)
- Stage 2: 指令微调(任务适配)
- 数据质量
- 使用高质量、多样化的数据
- 进行概念平衡
- 严格的质量控制
- 内存管理
- 合理配置并行度
- 使用激活检查点
- 启用分布式优化器
- 训练稳定性
- 适当的学习率和warmup
- 梯度裁剪
- 定期保存检查点
- 效率优化
- Flash Attention
- 混合精度训练
- 样本打包
- 高效的数据加载
总结
LLaVA-OneVision-1.5是一个完整、高效、可扩展的多模态大模型训练框架,具有以下核心优势:
- 完全开源: 提供完整的代码、数据、配置和训练日志
- 高效训练: 基于Megatron-LM深度优化,支持各种并行和优化策略
- 模块化设计: 易于扩展和定制
- 生产级质量: 经过大规模验证,训练成本仅$16K
通过本文档,您应该能够: - 理解项目的整体架构和设计思路 - 掌握模型的实现细节 - 配置和启动训练任务 - 进行模型转换和部署 - 优化训练性能和解决常见问题
建议在实际使用时: 1. 从小规模实验开始,验证配置 2. 逐步增加模型规模和数据量 3. 密切监控训练指标和系统资源 4. 根据具体硬件和任务需求调整配置
如需更多帮助,请参考: - 项目GitHub - 技术报告 - HuggingFace模型