minimind-o/trainer/trainer_utils.py

202 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""
训练工具函数集合
"""
import os
import random
import math
import numpy as np
import datasets
import torch
import torch.distributed as dist
from torch.utils.data import Sampler
from transformers import AutoTokenizer
from model.model_omni import MiniMindOmni
def is_main_process():
return not dist.is_initialized() or dist.get_rank() == 0
def Logger(content):
if is_main_process():
print(content)
def get_lr(current_step, total_steps, lr):
# 与 mmv 保持一致:初始 lr=1.0*base_lr,最终 lr=0.1*base_lr
return lr * (0.1 + 0.45 * (1 + math.cos(math.pi * current_step / total_steps)))
def init_distributed_mode():
if int(os.environ.get("RANK", -1)) == -1:
return 0 # 非DDP模式
dist.init_process_group(backend="nccl")
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
return local_rank
def setup_seed(seed: int):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
def log_model_params(model, ignore_patterns=['audio_encoder', 'vision_encoder']):
def should_count(n): return not any(p in n for p in ignore_patterns)
total = sum(p.numel() for n, p in model.named_parameters() if should_count(n)) / 1e6
cfg = model.config
n_routed = getattr(cfg, 'n_routed_experts', getattr(cfg, 'num_experts', 0))
n_active = getattr(cfg, 'num_experts_per_tok', 0)
n_shared = getattr(cfg, 'n_shared_experts', 0)
expert = sum(p.numel() for n, p in model.named_parameters() if 'mlp.experts.0.' in n and should_count(n)) / 1e6
shared_expert = sum(p.numel() for n, p in model.named_parameters() if 'mlp.shared_experts.0.' in n and should_count(n)) / 1e6
base = total - (expert * n_routed) - (shared_expert * n_shared)
active = base + (expert * n_active) + (shared_expert * n_shared)
if active < total: Logger(f'Model Params: {total:.2f}M-A{active:.2f}M')
else: Logger(f'Model Params: {total:.2f}M')
def init_omni_model(omni_config, from_weight='full_sft', tokenizer_path='../model', audio_encoder_path='../model/SenseVoiceSmall', vision_model_path='../model/siglip2-base-p32-256-ve', save_dir='../out', device='cuda', freeze_backbone='none', from_resume=0):
tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
model = MiniMindOmni(omni_config, audio_encoder_path=audio_encoder_path, vision_model_path=vision_model_path)
if from_weight != 'none':
moe_suffix = '_moe' if omni_config.use_moe else ''
weight_path = f'{save_dir}/{from_weight}_{omni_config.hidden_size}{moe_suffix}.pth'
if os.path.exists(weight_path):
weights = torch.load(weight_path, map_location=device)
param_shapes = {k: v.shape for k, v in model.named_parameters()}
incompatible = {k for k, v in weights.items() if k in param_shapes and v.shape != param_shapes[k]}
if incompatible:
Logger(f'跳过shape不匹配的权重: {incompatible}')
weights = {k: v for k, v in weights.items() if k not in incompatible}
model.load_state_dict(weights, strict=False)
Logger(f'已加载权重: {weight_path}')
if from_resume == 0 and omni_config.talker_hidden_size == omni_config.hidden_size:
n_talker = omni_config.num_talker_hidden_layers
n_thinker = len(model.thinker.layers)
has_talker = any(k.startswith('talker.layers.') for k in weights)
if not has_talker and n_talker > 0:
for i in range(n_talker):
src = n_thinker - n_talker + i
model.talker.layers[i].load_state_dict(model.thinker.layers[src].state_dict())
Logger(f'Talker层初始化: 复制thinker layers[{n_thinker-n_talker}:{n_thinker}] → talker layers[0:{n_talker}]')
# 冻结策略
if freeze_backbone == 'all':
# 冻结整个主干模型
for param in model.model.parameters():
param.requires_grad = False
elif freeze_backbone == 'last1':
# 冻结除了最后1层之外的所有层
for param in model.model.parameters():
param.requires_grad = False
# 打开最后1层
if hasattr(model.model, 'layers') and len(model.model.layers) > 0:
for param in model.model.layers[-1].parameters():
param.requires_grad = True
return model.to(device), tokenizer
def omni_checkpoint(omni_config, weight='pretrain_omni', model=None, optimizer=None, epoch=0, step=0, wandb=None, save_dir='../checkpoints', **kwargs):
os.makedirs(save_dir, exist_ok=True)
moe_path = '_moe' if omni_config.use_moe else ''
ckp_path = f'{save_dir}/{weight}_{omni_config.hidden_size}{moe_path}.pth'
resume_path = f'{save_dir}/{weight}_{omni_config.hidden_size}{moe_path}_resume.pth'
if model is not None:
from torch.nn.parallel import DistributedDataParallel
raw_model = model.module if isinstance(model, DistributedDataParallel) else model
raw_model = getattr(raw_model, '_orig_mod', raw_model)
# 移除冻结的 audio_encoder / vision_encoder 参数(不需要保存,从预训练路径重新加载)
clean_state_dict = {k: v for k, v in raw_model.state_dict().items() if not k.startswith('audio_encoder.') and not k.startswith('vision_encoder.')}
state_dict = {k: v.half().cpu() for k, v in clean_state_dict.items()}
ckp_tmp = ckp_path + '.tmp'
torch.save(state_dict, ckp_tmp)
os.replace(ckp_tmp, ckp_path)
wandb_id = None
if wandb:
if hasattr(wandb, 'get_run'):
run = wandb.get_run()
wandb_id = getattr(run, 'id', None) if run else None
else:
wandb_id = getattr(wandb, 'id', None)
resume_data = {
'model': state_dict,
'optimizer': optimizer.state_dict(),
'epoch': epoch,
'step': step,
'world_size': dist.get_world_size() if dist.is_initialized() else 1,
'wandb_id': wandb_id
}
for key, value in kwargs.items():
if value is not None:
if hasattr(value, 'state_dict'):
if isinstance(value, DistributedDataParallel):
resume_data[key] = value.module.state_dict()
else:
resume_data[key] = value.state_dict()
else:
resume_data[key] = value
resume_tmp = resume_path + '.tmp'
torch.save(resume_data, resume_tmp)
os.replace(resume_tmp, resume_path)
else: # 加载模式
if os.path.exists(resume_path):
ckp_data = torch.load(resume_path, map_location='cpu')
saved_ws = ckp_data.get('world_size', 1)
current_ws = dist.get_world_size() if dist.is_initialized() else 1
if saved_ws != current_ws:
ckp_data['step'] = ckp_data['step'] * saved_ws // current_ws
Logger(f'GPU数量变化({saved_ws}→{current_ws}),step已自动转换为{ckp_data["step"]}')
return ckp_data
return None
def vlm_collate_fn(batch):
input_ids = torch.stack([b[0] for b in batch])
labels = torch.stack([b[1] for b in batch])
pixel_data = [b[2] for b in batch]
if hasattr(pixel_data[0], 'keys'):
pixel_values = {k: torch.stack([d[k] for d in pixel_data]) for k in pixel_data[0].keys()}
else:
pixel_values = torch.stack(pixel_data)
return input_ids, labels, pixel_values
class SkipBatchSampler(Sampler):
def __init__(self, sampler, batch_size, skip_batches=0):
self.sampler = sampler
self.batch_size = batch_size
self.skip_batches = skip_batches
def __iter__(self):
batch = []
skipped = 0
for idx in self.sampler:
batch.append(idx)
if len(batch) == self.batch_size:
if skipped < self.skip_batches:
skipped += 1
batch = []
continue
yield batch
batch = []
if len(batch) > 0 and skipped >= self.skip_batches:
yield batch
def __len__(self):
total_batches = (len(self.sampler) + self.batch_size - 1) // self.batch_size
return max(0, total_batches - self.skip_batches)