千卡月崩五次: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%),频繁故障需根因分析反馈到数据准备与超参数调整。

相关推荐
有Li1 小时前
通过渐进式域变换实现组织分类的无监督单域泛化文献速递/基于多模态的医学影像分割与理解
人工智能·学习·分类·数据挖掘·文献·医学生
Token炼金师1 小时前
指令的塑形:从野生语言模型到听话助手 —— 监督微调 SFT 全链路
人工智能·深度学习·llm
新知图书1 小时前
多模态智能体开发工具链推荐(IDE/调试工具/日志工具)
人工智能·agent·多模态·ai agent·智能体
双翌视觉1 小时前
当光纤端面遇上AI算力:精密检测的光学成像技术与应用
人工智能·视觉检测
Sirius Wu1 小时前
OpenClaw 并发安全完整详解
网络·人工智能·安全·ai·架构·aigc
武子康1 小时前
🔥 codex-plugin-cc 原理全拆:四层桥接 + JSON-RPC 2.0 + JSONL + 后台任务状态机
人工智能·llm·claude
手写码匠1 小时前
从零实现大模型推理引擎:Continuous Batching 与动态调度系统深度解析
人工智能·深度学习·算法·aigc
科技发布1 小时前
获奖平台传播易 全域广告投放一站式解决方案
人工智能
breeze jiang1 小时前
告别等待焦虑:Vue 3 + DeepSeek 实现 AI 打字机流式对话,用户体验提升立竿见影
vue.js·人工智能·ux