最近在魔改一个检测模型,想从单卡改成DDP多卡训练。代码参考了官方tutorial,但一跑就各种幺蛾子:先是dist.barrier()卡死,后来改成nccl后端又报unexpected collective……最迷惑的是我明明只在主进程里save了checkpoint,结果其他卡还是疯狂往同一个目录写日志。我猜是DistributedSampler和DataLoader的num_workers没对齐?但调了半天还是时好时坏。有没有老哥指点下DDP的正确打开姿势?顺便问下,model和optimizer到底该不该用torch.nn.parallel.DistributedDataParallel包多次?总感觉这里有个坑等着我踩。
PyTorch多卡训练DDP老报错,到底哪里没配对啊?
全部回复
共 28 条你提到的几个症状基本都踩在DDP的经典坑上了。dist.barrier()卡死通常是因为只有部分进程执行到了这里,比如你在主进程里加了判断,其他卡就永远等不到集合通信,得保证所有rank都调用同一个collective。unexpected collective多半是模型里有些分支只在rank0跑,或者forward里有依赖数据的条件逻辑,导致各卡执行顺序不一致。至于日志乱写,那是你只判断了rank==0来save,但logging或tensorboard的writer可能每个进程都建了,得把summary writer和文件句柄也包进if rank==0里。DistributedSampler记得每epoch调set_epoch,不然shuffle会失效,num_workers本身不需要和卡数对齐。model必须用DistributedDataParallel包,而且要在model.to(device)之后包,optimizer用普通的就行,不用DDP包。如果还时好时坏,建议加TORCH_DISTRIBUTED_DEBUG=DETAIL环境变量,能打出具体哪个collective对不上,比盲调快多了。
日志乱写是因为没设local_rank做device判断,save时加个rank==0就行。DDP那块model必须包,optimizer不用。
日志重复写大概率是没在主进程用torch.distributed.get_rank()==0包住logging或者writer,DDP不会帮你自动拦这个。barrier卡死常见原因是各卡进入次数不一致,比如有的卡在dataloader里挂太久或者异常提前退了。unexpected collective基本就是某张卡多跑或少跑了一次allreduce,检查下有没有if分支只在rank0执行却包含DDP通信。DistributedSampler记得每epoch调set_epoch,num_workers和persistent_workers配好一般问题不大。
日志乱写八成是没设local_rank做判断,DDP里每个进程都会跑一遍你的代码,得用rank区分。
看到save checkpoint那段我差点笑出声,这个坑我去年也踩过,其他rank疯狂写日志基本就是没在dist.get_rank()==0外面套判断,或者日志库自己开了file handler每个进程都初始化了一遍。barrier卡死十有八九是某个rank提前退出了或者根本没进到barrier,比如你某个分支里只有rank0执行了save然后直接return,其他卡就永远等在那。nccl那个unexpected collective一般是不同rank走的collective顺序不一致,常见于你在forward里写了条件分支,只有部分卡走了all_reduce。DistributedSampler记得每个epoch调set_epoch,不然shuffle种子一样,还有DataLoader的shuffle必须关掉。model用DDP包没问题,optimizer不用包,但记得先建optimizer再DDP(model),顺序反了参数引用会对不上。num_workers对齐这个说法有点玄学,真正要对齐的是sampler和batch size的per-gpu设置,建议先把num_workers设0跑通再加。
卡barrier大概率是某张卡提前退出了,或者根本没进到barrier那行,建议先确认所有rank都执行了同一段逻辑。日志乱写是因为没加local_rank判断,每个进程都以为自己该写,用if rank == 0包一下就行。DDP里model必须用DistributedDataParallel包,optimizer不用特殊处理,但记得在model = DDP(model)之后再建optimizer。num_workers其实跟DDP没直接关系,倒是DistributedSampler要记得每个epoch调set_epoch。
日志乱写大概率是没在初始化时设好rank和local_rank,每个进程都以为自己该输出,建议用torchrun启动并直接读环境变量里的RANK和LOCAL_RANK,别自己手写argparse传。barrier卡死经常是某个进程提前挂了或者条件分支只在部分rank执行,把所有print换成print(f"[rank{rank}] ...")再跑一遍会清楚很多。DistributedSampler记得每epoch调set_epoch,不然shuffle是假的,num_workers只要不炸显存影响没那么大。model用DDP包,optimizer在DDP之后建就行,顺序反了参数引用会对不上。
先检查下有没有在初始化进程组前就调用了barrier,还有sampler的set_epoch每轮都得调,不然shuffle会失效。