1. PyTorch分布式训练基础概念
当你面对海量数据和复杂模型时,单卡训练就像用吸管喝珍珠奶茶——效率低还容易堵住。PyTorch的分布式数据并行(DDP)就是给你的超级吸管,让多张GPU协同工作。先理解几个核心概念:
- rank:相当于进程身份证号,rank 0是主进程,其他进程依次编号。比如4卡训练时rank就是0到3
- local_rank:每台机器内部的GPU编号,比如两台8卡机器上,第二台机器的local_rank也是0到7
- world_size:全局GPU总数,8卡单机就是8,两台8卡机器就是16
我刚开始用DDP时,经常混淆rank和GPU编号。其实它们没有强制绑定关系,但约定俗成会让一个rank对应一块GPU。通信后端(backend)的选择也有讲究:
# GPU训练选NCCL,CPU训练选Gloo dist.init_process_group("nccl") # NVIDIA的优化通信库2. 环境初始化实战
初始化分布式环境就像组织多人会议,需要确定会议室地址(master_addr)和门牌号(master_port)。推荐两种设置方式:
2.1 TCP初始化方式
import os os.environ['MASTER_ADDR'] = '192.168.1.1' # 主节点IP os.environ['MASTER_PORT'] = '29500' # 任意空闲端口 dist.init_process_group("nccl", rank=rank, world_size=world_size)2.2 ENV初始化方式
更简单的做法是直接读取环境变量:
dist.init_process_group("nccl", init_method="env://")实测发现,在多机训练时TCP方式更稳定。曾经踩过一个坑:防火墙阻塞了默认端口,导致节点间无法通信,换成高端口号(如29500-29599)后解决。
3. 数据分布式处理技巧
DDP的核心思想是"数据并行",就像把披萨切成多块分给不同人。DistributedSampler是关键:
from torch.utils.data.distributed import DistributedSampler sampler = DistributedSampler( dataset, num_replicas=world_size, rank=rank, shuffle=True, seed=42 # 保证各进程初始打乱顺序一致 ) dataloader = DataLoader(dataset, batch_size=64, sampler=sampler)注意:每个epoch前要调用sampler.set_epoch(epoch),否则每次分到的数据顺序相同!我在早期项目中漏掉这行,导致模型收敛异常。
4. 模型包装与梯度同步
用DistributedDataParallel包装模型后,魔法就开始了:
model = MyModel().cuda() ddp_model = DistributedDataParallel( model, device_ids=[local_rank], output_device=local_rank )它的工作原理是:
- 前向传播时各卡独立计算
- 反向传播时自动聚合梯度(All-Reduce)
- 各卡用平均梯度更新参数
实用技巧:通过model.module访问原始模型。有次我需要提取中间层特征,直接调ddp_model.features报错,改用ddp_model.module.features才成功。
5. 训练启动的三种姿势
5.1 手动启动(适合调试)
# 终端1 python train.py --rank 0 --world_size 2 # 终端2 python train.py --rank 1 --world_size 25.2 mp.spawn方式(推荐单机)
import torch.multiprocessing as mp def train(rank, world_size): # 训练代码 if __name__ == "__main__": world_size = torch.cuda.device_count() mp.spawn(train, args=(world_size,), nprocs=world_size)5.3 torchrun方式(适合生产)
# 单机多卡 torchrun --nproc_per_node=4 train.py # 多机多卡(主机) torchrun --nnodes=2 --nproc_per_node=4 --node_rank=0 --master_addr=host1 train.py # 从机 torchrun --nnodes=2 --nproc_per_node=4 --node_rank=1 --master_addr=host1 train.py最近项目中使用torchrun时遇到个典型问题:没设置--master_port导致端口冲突。建议在脚本中添加自动检测:
def find_free_port(): import socket with socket.socket() as s: s.bind(('', 0)) return s.getsockname()[1]6. 模型保存与加载策略
分布式训练下保存模型要注意:
- 只需rank 0保存即可,避免重复写入
- 加载时要映射到当前GPU
if rank == 0: torch.save(ddp_model.module.state_dict(), "model.pth") # 所有进程都需执行的加载代码 map_location = f"cuda:{local_rank}" state_dict = torch.load("model.pth", map_location=map_location) ddp_model.module.load_state_dict(state_dict)7. 分布式评估实现
评估时需要聚合各进程结果,常用all_gather:
def gather_tensors(tensor): output = [torch.zeros_like(tensor) for _ in range(world_size)] dist.all_gather(output, tensor) return torch.cat(output, dim=0) # 使用示例 all_preds = gather_tensors(preds) all_labels = gather_tensors(labels) if rank == 0: accuracy = (all_preds == all_labels).float().mean()8. 性能优化锦囊
梯度累积:小批量数据时累积梯度再更新
with ddp_model.no_sync(): # 前N-1次不同步 loss.backward() # 最后一次同步 loss.backward() optimizer.step()通信优化:调整bucket_cap_mb参数(默认25MB)
ddp_model = DDP(model, bucket_cap_mb=50)混合精度:搭配AMP使用
from torch.cuda.amp import autocast with autocast(): outputs = ddp_model(inputs)
9. 常见报错与解决
CUDA out of memory:
- 尝试减小batch_size
- 使用
gradient_checkpointing
NCCL错误:
NCCL_WARN Failed to open libibverbs.so添加环境变量解决:
os.environ["NCCL_IB_DISABLE"] = "1"死锁问题: 确保所有进程执行相同操作,避免条件分支
10. 完整代码模板
import torch import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel as DDP def setup(rank, world_size): dist.init_process_group("nccl", rank=rank, world_size=world_size) torch.cuda.set_device(rank) def cleanup(): dist.destroy_process_group() class Trainer: def __init__(self, rank, world_size): self.rank = rank self.model = Model().to(rank) self.ddp_model = DDP(self.model, device_ids=[rank]) self.optimizer = torch.optim.Adam(self.ddp_model.parameters()) def train(self, dataloader): for epoch in range(epochs): dataloader.sampler.set_epoch(epoch) for batch in dataloader: inputs, labels = batch outputs = self.ddp_model(inputs.to(self.rank)) loss = F.cross_entropy(outputs, labels.to(self.rank)) loss.backward() self.optimizer.step() self.optimizer.zero_grad() def main(rank, world_size): setup(rank, world_size) trainer = Trainer(rank, world_size) dataloader = get_dataloader(rank, world_size) trainer.train(dataloader) cleanup() if __name__ == "__main__": world_size = torch.cuda.device_count() torch.multiprocessing.spawn(main, args=(world_size,), nprocs=world_size)