feat: add device-agnostic training utilities

mengxin 2026-09-06 02:41:29 +08:00
parent 6fc918beb6
commit 3d4d313617
1 changed files with 108 additions and 9 deletions

View File

@ -7,6 +7,7 @@ __package__ = "trainer"
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import random import random
import math import math
from contextlib import nullcontext
import numpy as np import numpy as np
import torch import torch
import torch.distributed as dist import torch.distributed as dist
@ -15,6 +16,91 @@ from torch.utils.data import Sampler
from transformers import AutoTokenizer, AutoModel, AutoModelForSequenceClassification from transformers import AutoTokenizer, AutoModel, AutoModelForSequenceClassification
from model.model_minimind import MiniMindForCausalLM from model.model_minimind import MiniMindForCausalLM
SUPPORTED_DTYPES = {
'float32': torch.float32,
'float16': torch.float16,
'bfloat16': torch.bfloat16,
}
def get_device_type(device):
"""Return the accelerator type for strings such as cuda:0, mps, and cpu."""
return torch.device(device).type
def get_default_device():
"""Prefer CUDA, then Apple Silicon MPS, and finally CPU."""
if torch.cuda.is_available():
return 'cuda:0'
if torch.backends.mps.is_available():
return 'mps'
return 'cpu'
def validate_device(device):
device_type = get_device_type(device)
if device_type == 'cuda' and not torch.cuda.is_available():
raise RuntimeError('CUDA was requested but is not available.')
if device_type == 'mps' and not torch.backends.mps.is_available():
reason = ('this PyTorch build has no MPS support'
if not torch.backends.mps.is_built()
else 'the current macOS device does not expose MPS')
raise RuntimeError(f'MPS was requested but is not available: {reason}.')
if device_type not in {'cuda', 'mps', 'cpu'}:
raise ValueError(f'Unsupported training device: {device}')
return device_type
def get_torch_dtype(dtype):
try:
return SUPPORTED_DTYPES[dtype]
except KeyError as exc:
choices = ', '.join(SUPPORTED_DTYPES)
raise ValueError(f'Unsupported dtype {dtype!r}; choose one of: {choices}') from exc
def get_autocast_context(device, dtype):
"""Create the appropriate AMP context for CUDA or Apple MPS."""
device_type = validate_device(device)
torch_dtype = get_torch_dtype(dtype)
if device_type == 'cpu' or torch_dtype == torch.float32:
return nullcontext()
return torch.autocast(device_type=device_type, dtype=torch_dtype)
def get_grad_scaler(device, dtype):
"""Enable gradient scaling for fp16 CUDA/MPS and use a no-op otherwise."""
device_type = validate_device(device)
enabled = dtype == 'float16' and device_type in {'cuda', 'mps'}
try:
return torch.amp.GradScaler(device_type, enabled=enabled)
except (TypeError, RuntimeError):
# Older PyTorch releases only expose a CUDA GradScaler. Keep CUDA
# compatibility and fail clearly for MPS instead of silently training
# fp16 without scaling.
if device_type == 'cuda':
return torch.cuda.amp.GradScaler(enabled=enabled)
if enabled:
raise RuntimeError(
'This PyTorch version cannot scale MPS float16 gradients. '
'Upgrade PyTorch or use --dtype bfloat16/float32.'
)
return torch.cuda.amp.GradScaler(enabled=False)
def should_pin_memory(device):
"""Pinned host memory accelerates CUDA copies but does not benefit MPS."""
return get_device_type(device) == 'cuda'
def empty_device_cache(device):
device_type = get_device_type(device)
if device_type == 'cuda' and torch.cuda.is_available():
torch.cuda.empty_cache()
elif device_type == 'mps' and torch.backends.mps.is_available():
torch.mps.empty_cache()
def get_model_params(model, config): def get_model_params(model, config):
total = sum(p.numel() for p in model.parameters()) / 1e6 total = sum(p.numel() for p in model.parameters()) / 1e6
n_routed = getattr(config, 'n_routed_experts', getattr(config, 'num_experts', 0)) n_routed = getattr(config, 'n_routed_experts', getattr(config, 'num_experts', 0))
@ -41,10 +127,17 @@ def get_lr(current_step, total_steps, lr):
return lr*(0.1 + 0.45*(1 + math.cos(math.pi * current_step / total_steps))) return lr*(0.1 + 0.45*(1 + math.cos(math.pi * current_step / total_steps)))
def init_distributed_mode(): def init_distributed_mode(device=None):
if int(os.environ.get("RANK", -1)) == -1: if int(os.environ.get("RANK", -1)) == -1:
return 0 # 非DDP模式 return 0 # 非DDP模式
device = device or get_default_device()
if get_device_type(device) != 'cuda':
raise RuntimeError(
'Distributed training in MiniMind currently requires CUDA/NCCL. '
'Run MPS training as a single process with `python train_xxx.py --device mps`.'
)
dist.init_process_group(backend="nccl") dist.init_process_group(backend="nccl")
local_rank = int(os.environ["LOCAL_RANK"]) local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank) torch.cuda.set_device(local_rank)
@ -55,10 +148,13 @@ def setup_seed(seed: int):
random.seed(seed) random.seed(seed)
np.random.seed(seed) np.random.seed(seed)
torch.manual_seed(seed) torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed(seed) torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed) torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False torch.backends.cudnn.benchmark = False
if torch.backends.mps.is_available():
torch.mps.manual_seed(seed)
def lm_checkpoint(lm_config, weight='full_sft', model=None, optimizer=None, epoch=0, step=0, wandb=None, save_dir='../checkpoints', **kwargs): def lm_checkpoint(lm_config, weight='full_sft', model=None, optimizer=None, epoch=0, step=0, wandb=None, save_dir='../checkpoints', **kwargs):
os.makedirs(save_dir, exist_ok=True) os.makedirs(save_dir, exist_ok=True)
@ -69,6 +165,7 @@ def lm_checkpoint(lm_config, weight='full_sft', model=None, optimizer=None, epoc
if model is not None: if model is not None:
raw_model = model.module if isinstance(model, DistributedDataParallel) else model raw_model = model.module if isinstance(model, DistributedDataParallel) else model
raw_model = getattr(raw_model, '_orig_mod', raw_model) raw_model = getattr(raw_model, '_orig_mod', raw_model)
model_device = next(raw_model.parameters()).device
state_dict = raw_model.state_dict() state_dict = raw_model.state_dict()
state_dict = {k: v.half().cpu() for k, v in state_dict.items()} state_dict = {k: v.half().cpu() for k, v in state_dict.items()}
ckp_tmp = ckp_path + '.tmp' ckp_tmp = ckp_path + '.tmp'
@ -103,7 +200,7 @@ def lm_checkpoint(lm_config, weight='full_sft', model=None, optimizer=None, epoc
torch.save(resume_data, resume_tmp) torch.save(resume_data, resume_tmp)
os.replace(resume_tmp, resume_path) os.replace(resume_tmp, resume_path)
del state_dict, resume_data del state_dict, resume_data
torch.cuda.empty_cache() empty_device_cache(model_device)
else: # 加载模式 else: # 加载模式
if os.path.exists(resume_path): if os.path.exists(resume_path):
ckp_data = torch.load(resume_path, map_location='cpu') ckp_data = torch.load(resume_path, map_location='cpu')
@ -116,7 +213,8 @@ def lm_checkpoint(lm_config, weight='full_sft', model=None, optimizer=None, epoc
return None return None
def init_model(lm_config, from_weight='pretrain', tokenizer_path='../model', save_dir='../out', device='cuda'): def init_model(lm_config, from_weight='pretrain', tokenizer_path='../model', save_dir='../out', device=None):
device = device or get_default_device()
tokenizer = AutoTokenizer.from_pretrained(tokenizer_path) tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
model = MiniMindForCausalLM(lm_config) model = MiniMindForCausalLM(lm_config)
@ -158,7 +256,8 @@ class SkipBatchSampler(Sampler):
class LMForRewardModel: class LMForRewardModel:
def __init__(self, model_path, device="cuda", dtype=torch.float16): def __init__(self, model_path, device=None, dtype=torch.float16):
device = device or get_default_device()
self.tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) self.tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
self.model = AutoModel.from_pretrained(model_path, torch_dtype=dtype, trust_remote_code=True) self.model = AutoModel.from_pretrained(model_path, torch_dtype=dtype, trust_remote_code=True)
self.model = self.model.to(device).eval() self.model = self.model.to(device).eval()