千卡月崩五次:loss spike、硬件故障、checkpoint 与断点续训 —— 训练可靠性的生死线

摘要

大模型训练故障不可避免,1000 卡月故障率 5-10%。本文从 loss spike 处置、硬件故障检测、checkpoint 策略、断点续训、故障根因分析、自动化恢复六个切口,给出源码级实现与企业级训练可靠性决策框架。

1. Loss Spike:检测与处置

Loss spike 是大模型训练最常见的故障------损失突然跳升或变 NaN。根因可能是异常数据、梯度爆炸、学习率过高或硬件故障。

graph TD A[Loss Spike 根因] --> B[异常数据: 脏数据/重复] A --> C[梯度爆炸: lr 过大/数值不稳] A --> D[硬件故障: GPU 掉卡/单比特翻转] A --> E[优化器状态损坏: Adam 动量异常] B --> F[跳过 batch + 记录] C --> G[梯度裁剪 + 降 lr] D --> H[自动 checkpoint 恢复] E --> I[重置优化器状态] A --> J[检测: grad_norm > 10] A --> K[恢复: 回滚 + 降 lr + 跳过] classDef default fill:#faf9f5,stroke:#ffffff,color:#000000,stroke-width:0px
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 错误、网络中断、存储故障。需建立多层监控与自动隔离机制。

graph TD A[硬件故障类型] --> B[GPU 故障: 掉卡/ECC/过热] A --> C[网络故障: NCCL 超时/丢包] A --> D[存储故障: 检查点写入失败] A --> E[电源故障: 整机宕机] B --> F[监控: nvidia-smi/GPU 日志] C --> G[监控: NCCL 所有reduce 测试] D --> H[监控: 磁盘空间/IO] A --> I[自动隔离: 标记故障卡] A --> J[自动恢复: 弹性训练] classDef default fill:#faf9f5,stroke:#ffffff,color:#000000,stroke-width:0px
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 是故障恢复的基础,但频率与存储需平衡------过频占存储,过疏恢复损失大。

graph TD A[Checkpoint 策略] --> B[频率: 500 步平衡] A --> C[存储: 异步写避免阻塞训练] A --> D[保留: 最近 3 + 关键节点] A --> E[压缩: BF16 存储省 50%] A --> F[校验: SHA256 防损坏] B --> G[过频: 存储压力大] B --> H[过疏: 恢复损失大] A --> I[分层: 全量+优化器+模型] classDef default fill:#faf9f5,stroke:#ffffff,color:#000000,stroke-width:0px
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. 断点续训:无缝恢复

断点续训需恢复模型权重、优化器状态、学习率调度器、数据迭代器、随机种子,任何一项缺失都导致训练不连续。

graph TD A[断点续训] --> B[恢复模型权重] A --> C[恢复优化器状态] A --> D[恢复学习率调度] A --> E[恢复数据迭代器] A --> F[恢复随机种子] B --> G[模型 state_dict] C --> H[Adam m/v 动量] D --> I[调度器 step 计数] E --> J[DataLoader 偏移] F --> K[torch.manual_seed] A --> L[验证: loss 连续性] classDef default fill:#faf9f5,stroke:#ffffff,color:#000000,stroke-width:0px
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. 故障根因分析

故障恢复是治标,根因分析是治本。需建立故障日志与自动化分析工具。

graph TD A[故障根因分析] --> B[日志收集: spike/硬件/数据] A --> C[时间线重建: 故障前事件] A --> D[关联分析: spike 与数据/硬件] A --> E[统计: 高频故障模式] A --> F[反馈: 优化数据/超参] B --> G[spike_log.csv + gpu_log + data_hash] C --> H[故障前 100 步事件序列] D --> I[异常数据 hash 关联 spike] E --> J[某 GPU 反复故障 -> 硬件问题] classDef default fill:#faf9f5,stroke:#ffffff,color:#000000,stroke-width:0px
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,是最高级形式。

graph TD A[自动化恢复] --> B[检测: 故障识别] B --> C[决策: 回滚/跳过/重启] C --> D[执行: 自动恢复] D --> E[验证: loss 连续性] A --> F[弹性训练: 动态 GPU] F --> G[故障卡剔除] F --> H[新卡加入] A --> I[通知: 告警人工] classDef default fill:#faf9f5,stroke:#ffffff,color:#000000,stroke-width:0px
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. 边界与失败模式

训练故障恢复的失败往往源于监控盲区、恢复不完整、根因分析缺失。

flowchart TD A[故障恢复规划] --> B{训练规模} B -->|1000 卡| E[弹性训练 + 完整监控] C --> F[监控: loss + grad_norm] D --> F E --> F F --> G{恢复后验证} G -->|loss 连续| H[继续训练] G -->|loss 跳升| I[检查状态恢复] I --> J{根因可定位?} J -->|是| K[修复后继续] J -->|否| L[回滚 + 降 lr] classDef default fill:#faf9f5,stroke:#ffffff,color:#000000,stroke-width:0px
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'}]

典型失败模式

  1. 监控盲区------仅监控 loss 未监控硬件,GPU 故障导致 spike 被误判为数据问题。需多层监控。
  2. 恢复不完整------仅恢复模型未恢复优化器状态,恢复后 loss 跳升。需完整状态恢复。
  3. checkpoint 损坏------异步保存中途崩溃,checkpoint 文件不完整。需原子重命名+校验。
  4. 根因分析缺失------频繁 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%),频繁故障需根因分析反馈到数据准备与超参数调整。

相关推荐
TMT星球39 分钟前
虎鲸文娱春苗编剧计划全面升级,已孵化20个剧本进入前期开发
大数据·人工智能
奔跑中的小象7 小时前
统信UOS + 天数AI卡部署SGLang服务手册
人工智能·uos·sglang·天数智芯
DevSecOps选型指南7 小时前
中国版Mythos,为何是悬镜安全灵脉CodeAI?
人工智能·安全
Shockang7 小时前
LangGraph 状态机实战
人工智能
刹那芳华19928 小时前
循环神经网络的从零开始实现(RNN)
人工智能·rnn·深度学习
阿童木写作8 小时前
跨境图片翻译工具多合一,批量图片视频字幕翻译加智能抠图
人工智能·python·音视频·语音识别
科技之门8 小时前
AI3D从建模到贴图、绑骨和动画的完整流程怎么做?V2Fun完整工作流指南
人工智能·3d·贴图
宇宙第一小趴菜8 小时前
二、机器学习的应用领域和发展史
人工智能·机器学习
前端开发江鸟8 小时前
我写过 MCP Server,却一直以为 MCP 只有 Tool
人工智能
llwszx8 小时前
【Java/Go后端手撸原生Agent(第十一篇):RAG检索——给Agent加上“查资料“能力】
llm·向量检索·rag·ai智能体·大模型应用·agent开发·后端转ai