
分布式训练的容错机制设计从checkpoint策略到节点故障自动恢复一、分布式训练的故障不是会不会发生而是多久发生一次在大规模分布式训练中硬件故障不是小概率事件。Google在TPUv3集群上的统计数据表明使用512个TPU训练BERT-Large时由于硬件故障导致的训练中断平均每2-4小时发生一次。随着集群规模的增大故障间隔时间MTBF, Mean Time Between Failures呈反比下降。这个现实对训练流程设计提出了一个硬约束如果训练系统不能在节点故障后自动恢复并继续训练那么在大规模集群上的训练本质上不可行。手动重启训练并手动恢复到最近的checkpoint的策略在单机训练中可行但在512节点的集群上这意味着需要24小时值班的人力和10-30分钟的非计划停机每次累积的时间成本足以使训练周期翻倍。graph TD A[分布式训练运行中] -- B{故障检测} B --|心跳超时| C[节点故障] B --|NCCL超时| D[通信故障] B --|梯度NaN| E[数值不稳定] C -- F[故障节点隔离] D -- F E -- F F -- G[加载最新Checkpoint] G -- H[重建分布式状态] H -- I[从断点恢复训练] I -- A二、Checkpoint策略——频率、格式与一致性Checkpoint是容错机制的核心。策略选择涉及三个关键参数的权衡保存频率更高的频率意味着更少的进度损失但checkpoint的I/O操作本身会阻塞训练。对于LLaMA-65B模型保存一次checkpoint约130GB到分布式文件系统需要3-8分钟。最佳频率通常通过计算期望恢复成本来确定期望恢复成本 故障频率 × (checkpoint开销 平均进度损失)当checkpoint频率从每2小时提高到每30分钟时checkpoint开销增加4倍但进度损失减少4倍最优频率通常在这个交叉点附近保存格式有两种主流格式。Sharded checkpoint——每个rank保存自己的分片恢复时各rank读取各自的分片。优势是保存速度快各rank并行写入缺点是rank数量变化时需要reshard。Consolidated checkpoint——将分片合并后保存为完整模型恢复灵活但保存慢。# 分布式checkpoint管理 # 设计思路支持sharded和consolidated两种模式自动选择最优策略 import torch import torch.distributed as dist import os import glob from typing import Optional from datetime import datetime class DistributedCheckpointManager: 分布式checkpoint管理器 核心设计决策 1. 使用torch.save而非safetensors——前者支持PyTorch分布式对象 但注意torch.save不是线程安全的 2. 先写临时文件再原子重命名——避免写入中断产生的损坏checkpoint 3. 保留最近N个checkpoint——平衡存储空间和恢复灵活性 def __init__( self, checkpoint_dir: str, save_interval_steps: int 1000, keep_last_n: int 3, # 保留最近N个checkpoint use_sharded: bool True, # 是否使用分片保存 ): self.checkpoint_dir checkpoint_dir self.save_interval save_interval_steps self.keep_last_n keep_last_n self.use_sharded use_sharded self.rank dist.get_rank() self.world_size dist.get_world_size() os.makedirs(checkpoint_dir, exist_okTrue) def save( self, model: torch.nn.Module, optimizer: torch.optim.Optimizer, scheduler: Optional[torch.optim.lr_scheduler._LRScheduler], step: int, metrics: dict None, ) - str: 保存checkpoint 返回: checkpoint路径用于日志记录和故障恢复 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) if self.use_sharded: # 分片保存每个rank保存自己的模型分片 # 优势并行I/O保存速度快 # 劣势恢复时要求rank数量不变 ckpt_path os.path.join( self.checkpoint_dir, fstep_{step}_rank_{self.rank}_{timestamp}.pt ) else: # 合并保存仅rank 0保存完整模型 # 需要先gather所有rank的状态 ckpt_path os.path.join( self.checkpoint_dir, fstep_{step}_consolidated_{timestamp}.pt ) # 保存状态 state { step: step, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), scheduler_state_dict: scheduler.state_dict() if scheduler else None, metrics: metrics or {}, rng_state: torch.get_rng_state(), # CPU 随机数状态 cuda_rng_state: torch.cuda.get_rng_state_all(), # GPU 随机数状态 } # 原子写入先写.tmp再重命名 tmp_path ckpt_path .tmp torch.save(state, tmp_path) os.rename(tmp_path, ckpt_path) # 清理旧checkpoint仅rank 0执行 if self.rank 0: self._cleanup_old_checkpoints() return ckpt_path def load(self, step: int None) - dict: 加载checkpoint 参数: step: 指定加载的step。None表示加载最新的checkpoint。 返回: 包含model_state_dict等的状态字典 if step is None: # 查找最新checkpoint if self.use_sharded: pattern fstep_*_rank_{self.rank}_*.pt else: pattern step_*_consolidated_*.pt files glob.glob(os.path.join(self.checkpoint_dir, pattern)) if not files: raise FileNotFoundError(fNo checkpoint found in {self.checkpoint_dir}) # 按修改时间排序取最新的 latest max(files, keyos.path.getmtime) return torch.load(latest, map_locationcpu) else: # 指定step加载 if self.use_sharded: files glob.glob( os.path.join(self.checkpoint_dir, fstep_{step}_rank_{self.rank}_*.pt) ) else: files glob.glob( os.path.join(self.checkpoint_dir, fstep_{step}_consolidated_*.pt) ) if not files: raise FileNotFoundError(fNo checkpoint at step {step}) return torch.load(files[0], map_locationcpu) def _cleanup_old_checkpoints(self): 清理旧checkpoint仅保留最近keep_last_n个 all_ckpts glob.glob(os.path.join(self.checkpoint_dir, step_*)) # 提取step数并按step分组 step_groups {} for ckpt in all_ckpts: # 从文件名中提取step数 filename os.path.basename(ckpt) step_str filename.split(_)[1] step int(step_str) if step not in step_groups: step_groups[step] [] step_groups[step].append(ckpt) # 保留最新的N个step删除其余的 sorted_steps sorted(step_groups.keys(), reverseTrue) for old_step in sorted_steps[self.keep_last_n:]: for f in step_groups[old_step]: os.remove(f)三、自动恢复机制的设计自动恢复需要三个组件协同工作健康监测Health Monitor使用心跳机制检测节点是否存活。PyTorch的torch.distributed提供了内置的超时检测——当NCCL_BLOCKING_WAIT或torch.distributed操作超时时会触发异常。弹性训练Elastic TrainingPyTorch Elastictorchrun支持动态节点变化。当检测到节点退出时剩余节点重新初始化分布式进程组然后从checkpoint恢复。状态恢复不仅仅恢复模型权重和优化器状态还需要恢复data loader的迭代状态确保数据不会重复或缺失、学习率调度器状态和随机数种子。# 使用torchrun的弹性训练启动脚本 # 设计思路利用torchrun的弹性机制实现节点故障自动恢复 #!/bin/bash # elastic_train.sh # torchrun 弹性训练的核心参数 # --nnodes: 最小和最大节点数1:8 1到8个节点 # --nproc-per-node: 每节点GPU数 # --max-restarts: 最大自动重启次数0不限制但建议设置上限 # --rdzv-backend: rendezvous后端c10d使用TCP通信 # --rdzv-endpoint: rendezvous端点master节点地址 # --rdzv-id: rendezvous的唯一标识同一训练任务共享此ID torchrun \ --nnodes1:8 \ --nproc-per-node8 \ --max-restarts10 \ --rdzv-backendc10d \ --rdzv-endpoint${MASTER_ADDR}:${MASTER_PORT} \ --rdzv-id${TRAINING_JOB_ID} \ train.py \ --checkpoint-dir /checkpoints/${TRAINING_JOB_ID} \ --save-interval 500 \ --auto-resume true四、容错机制的边界与残余风险即使有完善的checkpoint和自动恢复机制仍存在以下残余风险静默数据损坏Silent Data CorruptionGPU显存中的bit翻转可能导致梯度计算错误但不会触发任何显式异常。在大规模训练中SDC的概率随GPU-hour线性增长。缓解策略是启用ECC显存和定期校验checkpoint完整性。Checkpoint损坏如果保存checkpoint的存储节点本身故障最近保存的checkpoint可能不可用。这就是需要保留最近N个checkpoint的原因——不是N1而是N≥3且最好分散在不同存储节点。级联故障一个节点的故障可能导致all-reduce操作阻塞进而触发其他节点的timeout形成雪崩效应。在torchrun中--monitor-interval和合适的NCCL timeout设置可以缓解这一问题。五、总结分布式训练的容错机制不是一个可选功能而是大规模训练的必备基础设施。核心由三个组件构成checkpoint策略决定保存什么、多久保存、如何保存、健康监测决定何时触发恢复、弹性训练引擎决定如何在不中断的情况下重组集群。关键参数是checkpoint的保存间隔——它应在保存开销和故障恢复的进度损失之间取得平衡通常的最优区间在每30分钟到2小时之间。保留最近3-5个checkpoint而非仅保留最新的是应对checkpoint损坏的低成本保险策略。