最近在魔改一个检测模型,想从单卡改成DDP多卡训练。代码参考了官方tutorial,但一跑就各种幺蛾子:先是dist.barrier()卡死,后来改成nccl后端又报unexpected collective……最迷惑的是我明明只在主进程里save了checkpoint,结果其他卡还是疯狂往同一个目录写日志。我猜是DistributedSampler和DataLoader的num_workers没对齐?但调了半天还是时好时坏。有没有老哥指点下DDP的正确打开姿势?顺便问下,model和optimizer到底该不该用torch.nn.parallel.DistributedDataParallel包多次?总感觉这里有个坑等着我踩。
PyTorch多卡训练DDP老报错,到底哪里没配对啊?
全部回复
共 23 条踩过一样的坑,多半是sampler没跟dataloader绑对,还有save得用rank判断加进程锁。
讲真,你遇到的这几个坑我基本都踩过一遍,dist.barrier()卡死八成是卡在dataloader的worker数量不一致上,试试把num_workers设成0先排除问题。unexpected collective大概率是某个分支里不小心调用了collective操作但没所有进程都执行,检查下代码里有没有提前return或者条件判断。保存checkpoint的话,我习惯用torch.distributed.get_rank()判断然后只让rank0写,日志路径也带上rank就不会互相覆盖了。至于model和optimizer,我建议还是用DistributedDataParallel包一下模型,optimizer保持原样就行,但记得在load_state_dict前先module访问。另外强烈建议把torch.cuda.set_device(local_rank)放在最前面,这个漏了也会出各种玄学问题。
看到你说save只在主进程但日志还是乱写,大概率是logger初始化的时候没做rank判断,跟DDP本身关系不大。dist.barrier()卡死常见原因是你没把init_method里的rank和world_size跟实际启动的进程数对上,或者用了spawn后没传对rank参数。unexpected collective多半是某个分支里有的卡进了collective有的没进,检查下所有return和continue路径是不是都走到了barrier。DistributedSampler和num_workers其实不用严格对齐,但记得每个epoch调sampler.set_epoch(),不然shuffle失效,而且dataloader里pin_memory在多卡下最好开着。model和optimizer用DistributedDataParallel包model就行,optimizer保持原样,但注意torch.nn.parallel.DistributedDataParallel第一个参数要直接传model,别套Sequential啥的。
八成是sampler没给每个rank分好数据,试试把num_workers设成0排除干扰,还有logger记得按rank隔离。
说到DDP的坑我太有感触了,上个月调一个分割模型也是被这些collective通信搞到怀疑人生。你那个dist.barrier卡死的问题,八成是某个进程提前return了或者数据量不一致,导致所有rank没走到同一个同步点,这个得检查下代码里有没有隐形的分支。nccl报unexpected collective的话,大概率是模型里有sparse tensor或者自定义的通信操作,跟nccl的集合通信语义对不上,建议先全换成dense tensor试试。至于save checkpoint那个,虽然你只在主进程save了,但如果其他rank的DataLoader还在跑,日志写入可能是通过print或者logging触发的,跟save没啥关系,得把日志输出也限定在rank0。DistributedSampler和num_workers确实容易出问题,特别是每个rank的worker数量不一样的时候,会导致每个epoch的shuffle顺序错乱,进而引发后续的同步崩溃,建议统一num_workers并且设置seed。model和optimizer的话,DDP会自己处理梯度同步,所以model直接包一下就行,不用手动做别的,但optimizer必须在DDP包裹之后再创建,不然state_dict会乱。最后说个玄学,如果你用了混合精度或者梯度累积,记得把no_sync上下文管理器用在正确的地方,不然很容易出现梯度没同步完就开始下次前向的报错。
说到DDP这个坑我太有共鸣了,刚入坑时也是被dist.barrier()卡到怀疑人生,后来发现多半是进程数跟init_process_group里world_size没对上,或者有进程提前崩了没参与同步。你那个unexpected collective八成是代码里某个分支只在部分进程执行了集合通信操作,比如loss计算里带了个all_reduce但被if包住了,这种不对称最容易炸。关于日志那个问题,我猜你用了logging.FileHandler或者print重定向,但没检查rank,其实最稳的做法是给每个进程单独分配一个日志文件名后缀,或者干脆只在rank==0时初始化logger。DistributedSampler和num_workers其实没直接关系,但有个容易被忽略的点:DataLoader里shuffle必须设False,否则会和Sampler冲突,导致每个epoch数据分布错乱。至于model和optimizer,DistributedDataParallel只包model就行,optimizer保持原样,但save的时候要把model.module.state_dict()和optimizer.state_dict()一起存,加载时也得注意先load_state_dict再包DDP。我现在的习惯是写一个setup函数统一处理rank、device、seed,然后所有跟进程相关的操作都走一个if dist.get_rank() == 0的封装,基本能避免八成问题。另外你提到魔改检测模型,如果里面有自定义的forward里用了torch.where或者mask操作,记得检查这些张量是不是在所有进程都同步了,不然梯度会不一致。最后想问下,你nccl后端是用的gloo做备份了吗?有时候环境变量NCCL_DEBUG=INFO能直接告诉你卡在哪一步。
说实话你贴的这几个报错我基本都踩过,最后发现八成不是num_workers的问题,而是dist.barrier()之前有某个进程提前return了,或者数据集长度在各卡上不一致导致sampler算出来的索引对不上。nccl那个unexpected collective大概率是代码里某个分支只有部分进程执行了通信操作,比如在验证集上忘了加if dist.get_rank() == 0这种判断。
关于checkpoint保存,你只在主进程save是对的,但日志目录那个问题通常是因为你没在子进程里重新设置logging的file handler,所有rank共享了同一个文件描述符,疯狂写同一个文件。我一般会按rank拆目录,或者每个进程单独开一个log文件。
model和optimizer的包装方式,官方推荐是先把model放到gpu上,再包DistributedDataParallel,optimizer不需要额外处理,但要注意torch.load的时候得用map_location指定到对应rank的device,否则加载到cpu再搬回gpu容易出隐性bug。
另外DistributedSampler有个坑,就是每个epoch要手动调用set_epoch(epoch),不然每个epoch的shuffle结果一样,模型会过拟合到固定的batch顺序上。我之前漏了这一步,loss曲线看着正常,但验证集上一直不涨,查了半天。
最后建议你先把dist.barrier()全去掉试试,很多场景下其实不需要,反而容易卡死。真正需要同步的地方用all_reduce或者all_gather更稳妥。如果还不行,把torch.distributed.init_process_group的timeout参数调大点,默认30秒在小数据集上可能不够。
你这几个问题我基本都踩过,最坑的其实是dist.barrier()卡死,多半是某个进程提前退出了,比如DataLoader的worker崩了但主进程还在等。nccl报unexpected collective的话,检查下是不是有if dist.get_rank()==0包裹了不该包的通信操作,比如barrier或者all_reduce必须所有rank都执行。save checkpoint那个事儿,光靠主进程判断不够,得确保其他rank的logger和writer也只在主进程初始化,或者干脆把输出路径按rank分目录。另外DistributedSampler记得在每个epoch开头调set_epoch,不然shuffle会失效但不会报错,容易让你误判。model和optimizer不需要手动包Distributed,直接用DDP包model就行,optimizer保持原样,但注意DDP的gradient同步是自动的,别自己再去all_reduce梯度了。
看到这个我简直梦回上周,一模一样的问题,最后发现是sampler没传进dataloader,导致每个epoch的shuffle根本没生效。你那个barrier卡死八成是rank和world_size没设对,试试看环境变量LOCAL_RANK和全局RANK是不是搞混了。checkpoint那块建议只在rank0上save,但log可以先写到各自的临时目录最后再合并,不然多卡同时写一个文件必炸。model和optimizer就用DistributedDataParallel包一下就行,但注意BN层要转成SyncBN,不然精度会掉。
这种问题八成不是sampler的锅,你先检查下是不是所有进程的batch size没按总卡数翻倍,DDP里每个进程拿到的其实是单卡batch。日志乱写那个简单,直接把logging输出重定向到带rank的文件就行,别纠结是不是主进程的问题。还有model和optimizer都不用包DistributedDataParallel,optimizer只留主进程step就行,其他卡forward/backward完就等着。最后建议你把dist.barrier()全删了,DDP本身就带隐式同步,手动加反而容易卡死。
这问题我熟,之前也被DDP折磨过。dist.barrier()卡死大概率是进程组初始化时机不对,得确保所有rank都跑到了同一行再同步。unexpected collective多半是代码里有条件分支导致不同rank执行了不同的通信操作,比如eval里忘了包torch.no_grad()但train里有。日志乱写那个,建议直接用logging加rank前缀,或者把输出重定向到不同文件,比改num_workers靠谱。DistributedSampler只要保证每个epoch调set_epoch()就行,num_workers不背这锅。最后,model用DistributedDataParallel包一下,但optimizer别包,保持原样就好。
说实话你这个问题我太有共鸣了,DDP刚上手时那个collective通信的报错能把人整到怀疑人生。你提到dist.barrier()卡死,我猜八成是某个进程提前退出了或者卡在数据加载上,比如num_workers设置不一致导致某些rank的dataloader没起来,这时候barrier就永远等不到人。关于save checkpoint,官方推荐的做法是只在rank0上做,但日志目录那个坑我也踩过——你得确保每个进程的logging输出都带上rank后缀,或者干脆只在rank0上初始化logger,不然就是群魔乱舞。
至于DistributedSampler和num_workers,这俩其实没直接关系,但有个容易忽略的点:dataloader的num_workers建议设成0或者保持所有rank一致,不然子进程fork时可能把distributed状态搞乱。你那个unexpected collective大概率是模型里某个操作没包在DDP的forward里,比如自定义的loss函数里用了all_reduce,或者有同步的BN层在DDP外面初始化了。
最后关于model和optimizer,如果你用的是DistributedDataParallel,model肯定要包,但optimizer不用包——你只需要在构造optimizer时传model.module.parameters()而不是model.parameters(),否则梯度同步会出问题。另外,如果模型里有BatchNorm,记得设sync_bn=True,不然多卡效果会明显掉点。建议你先把dist.barrier()换成torch.distributed.barrier()并加上device_ids=[local_rank],然后把所有进程的日志输出加rank前缀,跑通了再逐步加其他东西。
DDP这坑我太懂了,你那个dist.barrier()卡死八成是卡在dataloader的worker数量不一致上,尤其是和sampler的shuffle配合时特别容易出问题。我后来直接把num_workers固定成0调试,通了再往上加,能少掉一半玄学错误。checkpoint那个问题倒是简单,你在主进程save之前加个if dist.get_rank() == 0还不够,得确保日志文件路径里带上rank,或者用torch.distributed.barrier()把其他进程堵在save后面,不然它们会提前跑进下一个epoch然后疯狂写文件。至于model和optimizer,其实不用手动包DistributedDataParallel,直接用torchrun启动,然后在训练循环里把model丢给DDP就行,optimizer保持原样,梯度同步是DDP内部自动做的。你那个unexpected collective我猜是某个地方不小心用了all_reduce但没统一在train模式下,或者有代码分支只在部分进程执行了集合通信,可以检查下val阶段的同步逻辑。
说到DDP这个坑我太有共鸣了,上个月刚被折腾掉半条命。你那个barrier卡死八成是卡在rank间同步时序上,特别是如果数据加载里有非确定性操作,比如shuffle或者某些预处理随机性,没设固定seed的话各卡数据流就对不齐,建议在初始化进程组前把torch.manual_seed和cuda.manual_seed_all都统一设置下。nccl报unexpected collective基本可以锁定是代码某些地方有隐式的allreduce或者gradient hook没按顺序触发,我遇到过一次是因为loss里有个自定义的reduce操作在rank间执行顺序不一致,后来把所有collective调用都显式包在if dist.get_rank()==0条件外才解决。
checkpoint那个问题其实很典型,你光在主进程save不行,得确保其他进程在保存前都同步到同一个训练步,否则它们还在跑旧状态时主进程已经写新文件,就容易出现争抢目录。DistributedSampler和num_workers确实容易踩,但更关键的是每个epoch要调用sampler.set_epoch,不然不同进程拿到的数据划分顺序永远一样,会间接导致梯度同步异常。model和optimizer肯定要用DistributedDataParallel包model,但optimizer千万别包,就保持普通定义,只在每个rank上独立step就行,DDP内部会帮你做梯度同步。
另外你说日志乱写,我建议直接给每个rank配独立的logging文件句柄,文件名带rank后缀,省得一锅粥。最后检查下环境变量里MASTER_ADDR和MASTER_PORT是不是所有节点都一致,之前有个同事就是端口写错,导致部分卡连不上主节点,表现就是时好时坏。
DDP报错十有八九是进程组初始化时机和sampler没配合好,你试试把DistributedSampler的shuffle参数设为False然后手动在每个epoch调set_epoch,能少一堆麻烦。num_workers其实影响不大,真正坑的是dataloader里没给sampler传generator,多卡下每个rank的随机种子不一致会直接导致数据错乱。checkpoint那个问题你八成是忘了判断rank,save前加个if dist.get_rank() == 0包住整个写文件逻辑,包括日志handler也要在子进程里单独创建。至于model和optimizer,官方推荐把model包进DDP,optimizer保持原样,但记得在load_state_dict前先同步一下权重。
看到你说barrier卡死又nccl报unexpected collective,我猜八成是进程组初始化时机或者rank分配的问题。DDP最坑的地方在于,每个进程都得拿到正确的rank和world_size,哪怕你单机多卡,local_rank和全局rank也得捋清楚,不然barrier等的人不对就永远卡着。至于那个collective报错,很可能是不同卡上执行了不同次数的通信操作,比如某个分支只有rank0进去了,其他卡没跟上,这种就得把代码里所有可能影响执行路径的if条件都检查一遍,特别是涉及数据加载和日志的地方。
你提到只在主进程save但其他卡写日志,这个其实很常见,因为日志多半是每个进程独立开的文件句柄,不会自动感知主进程的状态。我一般习惯在初始化日志的时候也判断一下rank,非主进程直接丢到null或者独立文件名里,省得后面清日志头疼。DistributedSampler和num_workers不匹配确实会导致每个epoch数据分布不一致,但我感觉你这个问题更像是dataloader的pin_memory或者drop_last没设对,有时候最后一个batch大小不一样,某些卡就提前退出了collective。
model和optimizer的话,我个人是建议别用DistributedDataParallel包optimizer,只用它包model就行,然后手动同步梯度或者靠all_reduce。你既然在魔改检测模型,大概率有自定义的loss或者梯度处理,如果optimizer也被包进去,反而容易在backward的时候出奇怪的shape不一致。你试试把sampler的shuffle设成False,然后自己写个简单的epoch shuffle逻辑,有时候能绕开不少隐性问题。
我这边之前也踩过类似的坑,最后是把所有进程的随机种子都设成一样,包括cuda的,才稳定下来。你可以先打印一下每个进程的rank和当前设备,确认下launch脚本里的参数传递是不是对的,有时候就是环境变量没传全。另外torch的版本也影响很大,有些老版本对nccl的容错特别差,换个新点的试试说不定就好了。
看到你说dist.barrier卡死,我第一反应就是你是不是在dataloader里也用了barrier或者有进程提前退出了,DDP这玩意最怕进程数不齐。num_workers不齐确实会引发诡异问题,但更常见的是你把模型包装和optimizer创建的顺序搞反了,必须先构造原始model再包DDP,optimizer要用原始model的参数。另外save checkpoint那事,建议你把日志和模型都写到各自的local_rank目录下,最后再统一合并,别只在主进程里操作文件系统,容易踩NFS或共享存储的坑。最后问下你启动命令是用的torchrun还是自己mp.spawn,这两者处理环境变量的方式不一样,也可能导致collective通信错位。
遇到过类似的坑,多半不是sampler的问题,而是进程组初始化时机和rank没传对。DistributedSampler只需要在dataloader里设好,但num_workers如果大于0,每个worker都会复制一份sampler状态,这时候得保证shuffle=False或者手动设置epoch的seed,不然数据顺序会乱。checkpoint那个事建议用rank==0判断后再拼接路径,别只靠主进程save,日志输出也最好用dist.get_rank()做前缀。还有model和optimizer都不用包Distr,DDP只包装model就行,optimizer保持原样,但记得在backward后调用optimizer.step前要等所有梯度同步完,不然会出现你说的unexpected collective。
八成是sampler没传进dataloader,或者rank没设对,先检查下环境变量和sampler的shuffle。
checkpoint只主进程存没问题,但日志得用rank判断下,要不就是进程组初始化顺序有坑。
遇到过类似的,八成不是sampler的问题,是环境变量没配好。unexpected collective多半是rank和world_size没传对,或者某些卡提前跑了,试试在main函数开头加torch.distributed.init_process_group后设个torch.cuda.set_device(local_rank),能解决一大半玄学。
checkpoint那事更常见,你光判断rank==0没用,得把日志和save的路径也按rank隔离,或者干脆所有进程都写但文件名带rank,省得回头清目录都头疼。model和optimizer建议还是包一下,但optimizer的state_dict最好也只在主进程load,不然同步时容易出hidden bug。
另外num_workers设成0先跑通再说,DDP最容易炸的就是data loader里每个worker的子进程又去碰distributed group。最后提醒下,魔改模型时如果forward里有用到torch.Tensor的in-place操作,也可能导致collective卡住,排查时可以用find_unused_parameters=True先试试。