feat: add device-agnostic training utilities
parent
6fc918beb6
commit
3d4d313617
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue