
摘要
大模型训练故障不可避免,1000 卡月故障率 5-10%。本文从 loss spike 处置、硬件故障检测、checkpoint 策略、断点续训、故障根因分析、自动化恢复六个切口,给出源码级实现与企业级训练可靠性决策框架。
1. Loss Spike:检测与处置
Loss spike 是大模型训练最常见的故障------损失突然跳升或变 NaN。根因可能是异常数据、梯度爆炸、学习率过高或硬件故障。
python
// 来源:大模型训练 loss spike 处置 / 2024
import torch
import math
import os
from dataclasses import dataclass
from typing import Optional
@dataclass
class SpikeEvent:
"""loss spike 事件记录"""
step: int
loss_value: float
grad_norm: float
reason: str
action: str
batch_hash: str # 用于追溯异常数据
class LossSpikeHandler:
"""loss spike 检测与处置"""
def __init__(self, model, optimizer, ckpt_dir, config):
self.model = model
self.optimizer = optimizer
self.ckpt_dir = ckpt_dir
self.config = config
self.last_good_step = 0
self.spike_history = []
self.loss_window = [] # 滑动窗口
self.window_size = 50
def check_spike(self, loss, grad_norm, step, batch_hash):
"""检测 loss spike"""
# 1. NaN/Inf 检测
if torch.isnan(loss) or torch.isinf(loss):
return self._handle_spike(step, loss.item(), grad_norm, 'loss NaN/Inf', 'rollback', batch_hash)
# 2. 梯度爆炸
if grad_norm > self.config['grad_norm_threshold']:
return self._handle_spike(step, loss.item(), grad_norm, f'grad_norm {grad_norm:.1f}', 'rollback', batch_hash)
# 3. 损失突跳 (相对滑动均值)
self.loss_window.append(loss.item())
if len(self.loss_window) > self.window_size:
self.loss_window.pop(0)
if len(self.loss_window) >= self.window_size:
mean_loss = sum(self.loss_window) / len(self.loss_window)
if loss.item() > mean_loss * 3: # 超 3 倍均值
return self._handle_spike(step, loss.item(), grad_norm, f'loss {loss.item():.2f} > 3x mean {mean_loss:.2f}', 'skip_batch', batch_hash)
# 4. 记录健康状态
self.last_good_step = step
return {'healthy': True}
def _handle_spike(self, step, loss, grad_norm, reason, action, batch_hash):
"""处置 spike"""
event = SpikeEvent(step, loss, grad_norm, reason, action, batch_hash)
self.spike_history.append(event)
if action == 'rollback':
# 回滚 + 降 lr
self._load_checkpoint(self.last_good_step)
self._reduce_lr(0.1) # 降 10 倍
self._log_event(event)
return {'recovered': True, 'action': 'rollback', 'reason': reason}
elif action == 'skip_batch':
# 仅跳过当前 batch
self._log_event(event)
return {'recovered': True, 'action': 'skip_batch', 'reason': reason}
def _reduce_lr(self, factor=0.1):
"""降低学习率"""
for pg in self.optimizer.param_groups:
pg['lr'] *= factor
def _load_checkpoint(self, step):
"""加载 checkpoint"""
path = os.path.join(self.ckpt_dir, f'ckpt_{step}.pt')
ckpt = torch.load(path)
self.model.load_state_dict(ckpt['model'])
self.optimizer.load_state_dict(ckpt['optimizer'])
def _log_event(self, event):
"""记录事件 (用于后续根因分析)"""
with open(os.path.join(self.ckpt_dir, 'spike_log.csv'), 'a') as f:
f.write(f'{event.step},{event.loss_value},{event.grad_norm},{event.reason},{event.action},{event.batch_hash}\n')
量化:grad_norm 阈值 1.0(Dense 模型)/2.0(MoE 模型);loss 突跳阈值 3 倍滑动均值;回滚时 lr 降 10 倍。1000 卡训练月均 5-10 次 spike,每次回滚损失约 500-1000 步训练(2-4 小时)。
边界:滑动窗口大小需平衡------过小(<20)误报多,过大(>100)响应慢。grad_norm 阈值需根据模型调整------MoE 模型梯度方差大需调高。回滚后 lr 降 10 倍是经验值,部分场景需降 100 倍。spike 根因需事后分析,不能仅靠回滚掩盖------频繁 spike 说明数据或超参数有系统性问题。
2. 硬件故障检测:GPU 与网络
大规模训练的硬件故障包括 GPU 掉卡、ECC 错误、网络中断、存储故障。需建立多层监控与自动隔离机制。
python
// 来源:硬件故障检测与隔离 / 2024
import subprocess
import torch
import torch.distributed as dist
import time
import json
class HardwareMonitor:
"""硬件故障监控"""
def __init__(self, gpu_ids, check_interval=30):
self.gpu_ids = gpu_ids
self.interval = check_interval
self.failed_gpus = set()
def check_gpu_health(self, gpu_id):
"""检查 GPU 健康"""
# 1. GPU 是否存在
try:
result = subprocess.run(['nvidia-smi', '-i', str(gpu_id), '--query-gpu=name,temperature.gpu,utilization.gpu,ecc.errors.uncorrected.volatile.total', '--format=csv,noheader'], capture_output=True, text=True, timeout=10)
if result.returncode != 0:
return {'healthy': False, 'reason': 'nvidia-smi 失败'}
parts = result.stdout.strip().split(', ')
temp = int(parts[1])
ecc_errors = int(parts[3]) if parts[3] != 'N/A' else 0
# 2. 温度过高
if temp > 90:
return {'healthy': False, 'reason': f'温度 {temp}C 过高'}
# 3. ECC 错误
if ecc_errors > 10:
return {'healthy': False, 'reason': f'ECC 错误 {ecc_errors}'}
return {'healthy': True, 'temp': temp, 'ecc': ecc_errors}
except Exception as e:
return {'healthy': False, 'reason': str(e)}
def check_nccl_health(self, world_size):
"""检查 NCCL 网络健康"""
try:
# 所有 reduce 测试
tensor = torch.ones(1, device='cuda')
dist.all_reduce(tensor, op=dist.ReduceOp.SUM)
expected = float(world_size)
if abs(tensor.item() - expected) > 0.01:
return {'healthy': False, 'reason': f'all-reduce 结果 {tensor.item()} != {expected}'}
return {'healthy': True}
except Exception as e:
return {'healthy': False, 'reason': f'NCCL 超时: {e}'}
def check_disk_health(self, ckpt_dir, min_space_gb=100):
"""检查磁盘空间"""
try:
usage = subprocess.run(['df', '-BG', ckpt_dir], capture_output=True, text=True)
lines = usage.stdout.strip().split('\n')
if len(lines) >= 2:
parts = lines[1].split()
available = int(parts[3].rstrip('G'))
if available < min_space_gb:
return {'healthy': False, 'reason': f'磁盘空间不足 {available}G < {min_space_gb}G'}
return {'healthy': True}
except Exception as e:
return {'healthy': False, 'reason': str(e)}
def isolate_failed_gpu(self, gpu_id, reason):
"""隔离故障 GPU"""
self.failed_gpus.add(gpu_id)
print(f'GPU {gpu_id} 隔离: {reason}')
# 通知训练框架跳过此 GPU (弹性训练)
def monitor_loop(self):
"""监控循环"""
while True:
for gpu_id in self.gpu_ids:
if gpu_id in self.failed_gpus:
continue
health = self.check_gpu_health(gpu_id)
if not health['healthy']:
self.isolate_failed_gpu(gpu_id, health['reason'])
time.sleep(self.interval)
量化:1000 卡月故障率 5-10%,其中 GPU 故障占 60%,网络故障占 25%,存储/电源占 15%。ECC 错误超 10 次的 GPU 99% 会在 48 小时内宕机。NCCL 超时阈值 30 秒,超时即判定网络故障。磁盘需预留 100GB+ 空间给 checkpoint。
边界:硬件监控增加系统复杂度------监控本身可能故障。ECC 错误是 GPU 故障的前兆,但并非所有 ECC 错误都导致宕机。网络故障的根因诊断困难------可能是交换机、线缆、网卡任一环节。弹性训练(自动剔除故障卡)需训练框架支持,并非所有框架兼容。
3. Checkpoint 策略:频率与存储
Checkpoint 是故障恢复的基础,但频率与存储需平衡------过频占存储,过疏恢复损失大。
python
// 来源:Checkpoint 策略实现 / 2024
import torch
import os
import hashlib
import asyncio
from typing import Optional
class CheckpointManager:
"""Checkpoint 管理: 频率/存储/校验"""
def __init__(self, model, optimizer, save_dir, max_keep=3, save_interval=500):
self.model = model
self.optimizer = optimizer
self.save_dir = save_dir
self.max_keep = max_keep
self.interval = save_interval
self.ckpt_list = [] # 已保存 checkpoint 列表
def maybe_save(self, step, loss):
"""按间隔保存 checkpoint"""
if step % self.interval == 0:
self._save_checkpoint(step, loss)
def _save_checkpoint(self, step, loss):
"""保存 checkpoint (异步)"""
ckpt = {
'step': step,
'model': self.model.state_dict(),
'optimizer': self.optimizer.state_dict(),
'loss': loss,
}
path = os.path.join(self.save_dir, f'ckpt_{step}.pt')
# 异步保存 (不阻塞训练)
asyncio.create_task(self._async_save(ckpt, path, step))
self.ckpt_list.append((step, path))
# 清理旧 checkpoint
self._cleanup_old()
async def _async_save(self, ckpt, path, step):
"""异步保存 checkpoint"""
# 1. 先写临时文件
tmp_path = path + '.tmp'
torch.save(ckpt, tmp_path)
# 2. 校验完整性
if self._verify_checkpoint(tmp_path):
# 3. 原子重命名 (避免写一半损坏)
os.rename(tmp_path, path)
print(f'Checkpoint {step} 保存成功')
else:
os.remove(tmp_path)
print(f'Checkpoint {step} 校验失败, 丢弃')
def _verify_checkpoint(self, path):
"""校验 checkpoint 完整性 (SHA256)"""
hasher = hashlib.sha256()
with open(path, 'rb') as f:
while chunk := f.read(8192):
hasher.update(chunk)
# 实际应与预期哈希比对, 这里简化为存在性检查
return os.path.getsize(path) > 0
def _cleanup_old(self):
"""清理旧 checkpoint, 保留最近 max_keep 个"""
while len(self.ckpt_list) > self.max_keep:
old_step, old_path = self.ckpt_list.pop(0)
if os.path.exists(old_path):
os.remove(old_path)
print(f'清理旧 checkpoint {old_step}')
def save_critical(self, step, loss, tag='critical'):
"""保存关键节点 checkpoint (不清理)"""
ckpt = {
'step': step,
'model': self.model.state_dict(),
'optimizer': self.optimizer.state_dict(),
'loss': loss,
'tag': tag,
}
path = os.path.join(self.save_dir, f'ckpt_{tag}_{step}.pt')
torch.save(ckpt, path)
def load_latest(self):
"""加载最新 checkpoint"""
if not self.ckpt_list:
return None
step, path = self.ckpt_list[-1]
ckpt = torch.load(path)
self.model.load_state_dict(ckpt['model'])
self.optimizer.load_state_dict(ckpt['optimizer'])
return ckpt['step']
# 存储估算 (7B 模型):
# 全量 (模型+优化器): 84GB/checkpoint
# 仅模型 (BF16): 14GB/checkpoint
# 保留 3 个全量: 252GB
# 保留 3 个模型: 42GB
# 关键节点 (训练结束): 额外 84GB
量化:7B 模型全量 checkpoint 84GB,仅模型 14GB。500 步间隔对 1000 卡训练是平衡点------存储压力适中,恢复损失 500 步(约 2 小时)。保留 3 个全量+关键节点需 336GB 存储。异步保存使 checkpoint 不阻塞训练(保存耗时 30-60 秒)。
边界:异步保存需足够内存缓存 checkpoint------7B 模型需 84GB 额外内存。原子重命名(先写 tmp 再 rename)避免写一半损坏。SHA256 校验增加 5-10% 保存耗时但防静默损坏。关键节点(训练结束、阶段切换)的 checkpoint 不清理,需额外存储。
4. 断点续训:无缝恢复
断点续训需恢复模型权重、优化器状态、学习率调度器、数据迭代器、随机种子,任何一项缺失都导致训练不连续。
python
// 来源:断点续训实现 / 2024
import torch
import torch.distributed as dist
import os
import json
import random
import numpy as np
class ResumableTrainer:
"""可恢复训练器"""
def __init__(self, model, optimizer, scheduler, dataloader, ckpt_dir):
self.model = model
self.optimizer = optimizer
self.scheduler = scheduler
self.dataloader = dataloader
self.ckpt_dir = ckpt_dir
self.current_step = 0
self.rng_state = None
def save_full_state(self, step, loss):
"""保存完整训练状态"""
ckpt = {
'step': step,
'model': self.model.state_dict(),
'optimizer': self.optimizer.state_dict(),
'scheduler': self.scheduler.state_dict(),
'loss': loss,
# 随机状态
'torch_rng': torch.get_rng_state(),
'cuda_rng': torch.cuda.get_rng_state_all(),
'numpy_rng': np.random.get_state(),
'python_rng': random.getstate(),
# 数据迭代器状态
'dataloader_state': self.dataloader.state_dict() if hasattr(self.dataloader, 'state_dict') else None,
}
path = os.path.join(self.ckpt_dir, f'full_ckpt_{step}.pt')
torch.save(ckpt, path)
# 同时保存元数据 (JSON, 便于检查)
meta = {'step': step, 'loss': loss, 'path': path}
with open(os.path.join(self.ckpt_dir, 'latest.json'), 'w') as f:
json.dump(meta, f)
def resume(self):
"""从最新 checkpoint 恢复"""
meta_path = os.path.join(self.ckpt_dir, 'latest.json')
if not os.path.exists(meta_path):
print('无 checkpoint, 从头训练')
return 0
with open(meta_path) as f:
meta = json.load(f)
ckpt = torch.load(meta['path'])
# 恢复各状态
self.model.load_state_dict(ckpt['model'])
self.optimizer.load_state_dict(ckpt['optimizer'])
self.scheduler.load_state_dict(ckpt['scheduler'])
# 恢复随机状态 (保证数据顺序一致)
torch.set_rng_state(ckpt['torch_rng'])
torch.cuda.set_rng_state_all(ckpt['cuda_rng'])
np.random.set_state(ckpt['numpy_rng'])
random.setstate(ckpt['python_rng'])
# 恢复数据迭代器
if ckpt['dataloader_state']:
self.dataloader.load_state_dict(ckpt['dataloader_state'])
self.current_step = ckpt['step']
print(f'从 step {self.current_step} 恢复, loss={ckpt["loss"]:.4f}')
return self.current_step
def verify_continuity(self, test_batch):
"""验证恢复后训练连续性"""
self.model.eval()
with torch.no_grad():
loss = self.model(test_batch)
# loss 应与 checkpoint 记录的接近 (差异 <5%)
return loss.item()
def train_step(self, batch):
"""训练步"""
self.current_step += 1
loss = self.model(batch)
loss.backward()
torch.nn.utils.clip_grad_norm_(self.model.parameters(), 1.0)
self.optimizer.step()
self.scheduler.step(self.current_step)
self.optimizer.zero_grad()
return loss.item()
量化:完整 checkpoint 含随机状态使恢复后训练完全连续------loss 差异 <1%。仅恢复模型权重不恢复优化器状态,恢复后 loss 跳升 0.1-0.3(Adam 动量丢失)。恢复数据迭代器状态避免重复数据------无恢复则前 500 步用已训练数据。
边界:数据迭代器状态保存需自定义------PyTorch DataLoader 默认不支持 state_dict。分布式训练的恢复需同步所有 rank 的随机种子。恢复后需验证 loss 连续性------差异 >5% 说明状态恢复不完整。修改模型架构后无法恢复(state_dict 键不匹配)。
5. 故障根因分析
故障恢复是治标,根因分析是治本。需建立故障日志与自动化分析工具。
python
// 来源:故障根因分析工具 / 2024
import pandas as pd
import json
from collections import Counter, defaultdict
class RootCauseAnalyzer:
"""故障根因分析"""
def __init__(self, log_dir):
self.log_dir = log_dir
def load_logs(self):
"""加载各类日志"""
logs = {}
# spike 日志
spike_path = f'{self.log_dir}/spike_log.csv'
if os.path.exists(spike_path):
logs['spikes'] = pd.read_csv(spike_path, names=['step', 'loss', 'grad_norm', 'reason', 'action', 'batch_hash'])
# GPU 日志
gpu_path = f'{self.log_dir}/gpu_log.csv'
if os.path.exists(gpu_path):
logs['gpu'] = pd.read_csv(gpu_path)
return logs
def analyze_spike_patterns(self, logs):
"""分析 spike 模式"""
spikes = logs.get('spikes')
if spikes is None or len(spikes) == 0:
return {'total_spikes': 0}
# 1. spike 原因统计
reason_counts = Counter(spikes['reason'].str.split(':').str[0])
# 2. 异常数据关联 (相同 batch_hash 多次 spike)
batch_counts = spikes['batch_hash'].value_counts()
bad_batches = batch_counts[batch_counts > 1].index.tolist()
# 3. 时间分布 (是否集中某时段)
spikes['step_bin'] = (spikes['step'] // 1000) * 1000
step_dist = spikes['step_bin'].value_counts().sort_index()
return {
'total_spikes': len(spikes),
'reason_counts': dict(reason_counts),
'bad_batches': bad_batches,
'step_distribution': step_dist.to_dict(),
}
def correlate_with_hardware(self, logs):
"""关联硬件故障"""
spikes = logs.get('spikes')
gpu = logs.get('gpu')
if spikes is None or gpu is None:
return None
# 检查 spike 时间是否与 GPU 故障重合
correlations = []
for _, spike in spikes.iterrows():
# 查找 step 前后 100 步的 GPU 事件
nearby_gpu = gpu[(gpu['step'] >= spike['step'] - 100) & (gpu['step'] <= spike['step'] + 100)]
if len(nearby_gpu) > 0:
correlations.append({
'spike_step': spike['step'],
'gpu_events': nearby_gpu.to_dict('records'),
})
return correlations
def generate_report(self):
"""生成根因分析报告"""
logs = self.load_logs()
patterns = self.analyze_spike_patterns(logs)
correlations = self.correlate_with_hardware(logs)
report = {
'spike_patterns': patterns,
'hardware_correlations': correlations,
'recommendations': self._recommend(patterns, correlations),
}
return report
def _recommend(self, patterns, correlations):
"""生成优化建议"""
recs = []
if patterns.get('bad_batches'):
recs.append(f'发现 {len(patterns["bad_batches"])} 个异常数据 batch, 建议从数据集移除')
if correlations:
recs.append(f'{len(correlations)} 次 spike 与硬件故障相关, 建议检查故障 GPU')
reason_counts = patterns.get('reason_counts', {})
if reason_counts.get('grad_norm', 0) > 5:
recs.append('梯度爆炸频繁, 建议降低学习率或增强梯度裁剪')
return recs
# 典型分析结果:
# 100 次训练 spike 中:
# - 40%: 异常数据 (某 batch_hash 重复出现)
# - 30%: 梯度爆炸 (grad_norm > 10)
# - 20%: 硬件故障 (GPU ECC 错误)
# - 10%: 原因不明
量化:100 次训练 spike 中 40% 为异常数据、30% 梯度爆炸、20% 硬件故障、10% 原因不明。根因分析使后续训练 spike 频率降 50%------移除异常数据、调整学习率、隔离故障 GPU。
边界:根因分析依赖完整日志------日志缺失无法分析。部分 spike 原因不明(10%),可能是复杂的数值交互。分析需人工介入------自动化工具提供线索但最终判断需专家。分析结果需反馈到数据准备与超参数调整,否则分析无意义。
6. 自动化恢复:弹性训练
自动化恢复目标是故障发生时无需人工干预,训练自动继续。弹性训练(Elastic Training)允许动态增减 GPU,是最高级形式。
python
// 来源:弹性训练恢复框架 / 2024
import torch
import torch.distributed as dist
import time
import os
class ElasticTrainer:
"""弹性训练: 自动故障恢复"""
def __init__(self, model, optimizer, dataloader, ckpt_manager, monitor):
self.model = model
self.optimizer = optimizer
self.dataloader = dataloader
self.ckpt = ckpt_manager
self.monitor = monitor
self.max_retries = 3
self.retry_count = 0
def train_with_recovery(self, total_steps):
"""带自动恢复的训练循环"""
step = self.ckpt.load_latest() or 0
while step < total_steps:
try:
step = self._train_segment(step, total_steps)
except TrainingFailure as e:
print(f'训练故障: {e}')
self.retry_count += 1
if self.retry_count > self.max_retries:
self._alert_human(f'超过最大重试次数: {e}')
break
step = self._recover_from_failure(e, step)
def _train_segment(self, start_step, total_steps):
"""训练一段直到故障或完成"""
for step in range(start_step, total_steps):
# 监控硬件健康
health = self.monitor.check_all()
if not health['healthy']:
raise TrainingFailure(f'硬件故障: {health["reason"]}')
# 获取 batch
try:
batch = next(self.dataloader)
except StopIteration:
self.dataloader.reset()
batch = next(self.dataloader)
# 训练步
loss = self._train_step(batch, step)
# spike 检测
if torch.isnan(loss) or torch.isinf(loss):
raise TrainingFailure(f'loss NaN/Inf at step {step}')
# 定期 checkpoint
self.ckpt.maybe_save(step, loss.item())
return total_steps
def _recover_from_failure(self, failure, current_step):
"""从故障恢复"""
print(f'恢复中... 当前 step {current_step}')
# 1. 等待短暂时间 (硬件可能自恢复)
time.sleep(30)
# 2. 隔离故障 GPU
if 'GPU' in str(failure):
self.monitor.isolate_failed_gpus()
# 3. 重新初始化分布式
self._reinit_distributed()
# 4. 从 checkpoint 恢复
step = self.ckpt.load_latest()
print(f'恢复到 step {step}')
return step
def _reinit_distributed(self):
"""重新初始化分布式环境"""
if dist.is_initialized():
dist.destroy_process_group()
# 用剩余健康 GPU 重新初始化
healthy_gpus = self.monitor.get_healthy_gpus()
dist.init_process_group(backend='nccl', world_size=len(healthy_gpus))
# 重新包装模型为 DDP/FSDP
self.model = self._wrap_distributed(self.model)
def _alert_human(self, message):
"""通知人工介入"""
print(f'[告警] 需人工介入: {message}')
# 实际应发送邮件/短信/钉钉通知
class TrainingFailure(Exception):
"""训练故障异常"""
pass
# 自动化恢复效果:
# 1000 卡训练 30 天:
# - 无自动化: 平均 5 次人工介入, 每次 4 小时, 总停机 20 小时
# - 有自动化: 平均 0.5 次人工介入, 自动恢复 10 次, 总停机 2 小时
# - 弹性训练: 故障卡自动剔除, 零停机 (但训练速度降 10%)
量化:1000 卡训练 30 天无自动化平均停机 20 小时(5 次人工介入×4 小时),有自动化降至 2 小时(自动恢复 10 次),弹性训练零停机但速度降 10%(故障卡剔除)。自动化恢复 ROI 显著------节省 18 小时训练时间价值数万美元。
边界:自动化恢复的最大重试次数需限制------无限重试可能掩盖系统性问题。弹性训练需框架支持(PyTorch Elastic、Ray Train),并非所有框架兼容。故障卡剔除后训练速度下降,需评估是否值得。部分故障(存储损坏)无法自动恢复,必须人工。
7. 边界与失败模式
训练故障恢复的失败往往源于监控盲区、恢复不完整、根因分析缺失。
python
// 来源:故障恢复失败诊断 / 2024
def diagnose_recovery_failure(recovery_log):
"""诊断故障恢复失败"""
issues = []
if recovery_log.get('loss_jump_after_recovery', 0) > 0.1:
issues.append({'issue': '恢复后 loss 跳升', 'action': '检查优化器状态是否完整恢复'})
if recovery_log.get('manual_interventions', 0) > 2:
issues.append({'issue': '频繁人工介入', 'action': '增强自动化恢复'})
if recovery_log.get('ckpt_corruption_count', 0) > 0:
issues.append({'issue': 'checkpoint 损坏', 'action': '启用 SHA256 校验 + 异步保存'})
if not recovery_log.get('root_cause_analyzed', False):
issues.append({'issue': '未做根因分析', 'action': '建立故障日志 + 分析工具'})
return issues if issues else [{'issue': 'healthy'}]
典型失败模式:
- 监控盲区------仅监控 loss 未监控硬件,GPU 故障导致 spike 被误判为数据问题。需多层监控。
- 恢复不完整------仅恢复模型未恢复优化器状态,恢复后 loss 跳升。需完整状态恢复。
- checkpoint 损坏------异步保存中途崩溃,checkpoint 文件不完整。需原子重命名+校验。
- 根因分析缺失------频繁 spike 仅回滚不分析,问题持续。需建立分析工具。
7.1 实战复盘:优化器状态丢失导致 loss 跳升
某团队故障恢复仅加载模型权重,恢复后 loss 从 2.0 跳升至 2.5。
python
// 来源:优化器状态丢失复盘 / 2024
def diagnose_optimizer_state_loss(loss_before, loss_after, restored_components):
"""诊断优化器状态丢失"""
if loss_after - loss_before > 0.3 and 'optimizer' not in restored_components:
return {
'issue': f'恢复后 loss 跳升 {loss_after - loss_before:.2f}',
'action': '恢复优化器 state_dict (Adam m/v 动量)',
'reason': 'Adam 动量丢失使优化器重置, 等效重新开始训练'
}
return {'issue': 'healthy'}
# 修复: checkpoint 增加 optimizer state_dict
# 修复后: 恢复后 loss 差异 <0.02 (连续)
# 结论: 优化器状态与模型权重同等重要
量化:恢复优化器状态后 loss 差异从 0.5 降至 0.02。Adam 的动量(m/v)累积了训练历史,丢失使优化器重置------等效学习率突增,loss 跳升。优化器状态与模型权重必须同时恢复。
7.2 实战复盘:checkpoint 损坏无法恢复
某团队异步保存 checkpoint 时训练崩溃,checkpoint 文件写一半损坏,无法恢复。
python
// 来源:checkpoint 损坏复盘 / 2024
def diagnose_ckpt_corruption(load_success, file_size_expected, file_size_actual):
"""诊断 checkpoint 损坏"""
if not load_success and file_size_actual < file_size_expected * 0.9:
return {
'issue': 'checkpoint 文件不完整',
'action': '启用原子重命名 (tmp + rename) + SHA256 校验',
'reason': '异步保存中途崩溃, 文件写一半'
}
return {'issue': 'healthy'}
# 修复: 先写 .tmp 文件, 完成后原子重命名
# 修复后: 即使保存中途崩溃, 旧 checkpoint 仍可用
# 结论: 原子操作是 checkpoint 可靠性的基础
量化:原子重命名使 checkpoint 损坏率从 5% 降至 0%。先写 .tmp 文件完成后 rename,即使中途崩溃旧 checkpoint 仍可用。SHA256 校验增加 5-10% 保存耗时但防静默损坏。
7.3 实战复盘:弹性训练 GPU 剔除速度下降
某团队 1000 卡训练中 5 卡故障,弹性训练剔除故障卡后用 995 卡继续,训练速度降 15%。
python
// 来源:弹性训练速度复盘 / 2024
def diagnose_elastic_speed_drop(original_speed, current_speed, gpu_ratio):
"""诊断弹性训练速度下降"""
expected_drop = (1 - gpu_ratio) * 100 # 线性预期
actual_drop = (1 - current_speed / original_speed) * 100
if actual_drop > expected_drop * 1.5:
return {
'issue': f'速度降 {actual_drop:.0f}% 超预期 {expected_drop:.0f}%',
'action': '检查负载均衡 + 通信拓扑',
'reason': '故障卡可能影响通信拓扑, 非单纯算力下降'
}
return {'issue': 'healthy', 'drop': f'{actual_drop:.0f}%'}
# 5/1000 卡故障, 线性预期速度降 0.5%, 实际降 15%
# 原因: 故障卡所在节点的通信拓扑受影响
# 修复: 整节点剔除 (8 卡), 速度降 8% (接近线性)
# 结论: 弹性训练应以节点为单位剔除, 非单卡
量化:5 卡故障线性预期速度降 0.5%,实际降 15%(故障卡影响节点通信拓扑)。改为整节点剔除(8 卡)后速度降 8%,接近线性。弹性训练的剔除单位应是节点(8 卡)而非单卡,避免通信拓扑碎片化。
总结
训练故障恢复的工程化落地,核心在于 loss spike 处置、硬件故障检测、checkpoint 策略、断点续训、根因分析、自动化恢复六点。loss spike 需梯度裁剪+回滚+降 lr 三步处置;硬件监控需覆盖 GPU/网络/存储三层,1000 卡月故障率 5-10% 是常态;checkpoint 500 步间隔是平衡点,异步保存+原子重命名+SHA256 校验保可靠性;断点续训需恢复模型/优化器/调度器/数据/随机种子完整状态;根因分析使后续 spike 频率降 50%;自动化恢复使停机时间从 20 小时降至 2 小时。
工程落地的关键在于监控完备与恢复完整。监控盲区导致故障误判------需多层监控覆盖 loss/grad_norm/GPU/网络/存储;恢复不完整导致 loss 跳升------优化器状态与模型权重同等重要;checkpoint 损坏无法恢复------原子重命名是基础;根因分析缺失使问题持续------需建立日志与分析工具。建议根据训练规模选择恢复策略------小规模(<100 卡)基础 checkpoint+手动,中规模(100-1000 卡)自动监控+自动恢复,大规模(>1000 卡)弹性训练+完整监控,弹性训练的剔除单位应是节点(8 卡)而非单卡以避免通信拓扑碎片化,所有恢复需验证 loss 连续性(差异 <5%),频繁故障需根因分析反馈到数据准备与超参数调整。