最近在搞多卡训练,用PyTorch的DistributedDataParallel(DDP)跑一个BERT微调任务。单卡没问题,但换成4卡后,发现loss下降得特别慢,而且每个卡的loss值都不一样。我确认了数据加载用的是DistributedSampler,模型参数也是broadcast过的,但就是梯度不同步?检查了一下,发现reduce操作好像没生效,梯度还是各自为政。有人说是没设对init_method,我试了tcp://localhost:12355和env://都不行。求大佬指点一下,到底是什么环节漏了?我用的PyTorch 1.12,CUDA 11.6,机器是4张V100。
MCP里用PyTorch做分布式训练,DDP报错梯度不同步怎么办?
全部回复
共 135 条我之前也踩过类似的坑,后来发现大概率是每个rank的batch size没按总卡数缩放,导致各自算出的梯度本身就不一样,DDP只是在allreduce时做平均,并不会替你修正数据分布问题。你可以先打印一下每个rank的loss和梯度范数,看看是不是差在初始seed或者shuffle上。另外init_method用tcp的话,记得所有进程的rank和world_size要传对,尤其是用torchrun启动时环境变量容易漏。还有个偏方,试试把find_unused_parameters设成True,有时候是模型里有参数没参与梯度计算导致的同步异常。
我之前也踩过类似的坑,DDP梯度不同步多半不是init_method的问题,而是进程组没正确初始化。你如果确认DistributedSampler和broadcast都做了,那检查一下每个进程的rank和world_size是不是真的传对了,尤其是用torchrun启动时,环境变量可能没被正确读取。另外PyTorch 1.12有个已知问题,就是DDP在混合精度下如果用了grad_scaler,梯度同步可能被延迟,你可以试着把scaler的update频率调低或者暂时关闭amp看看。还有个隐蔽点,就是模型里有BatchNorm的话,DDP默认会同步BN的统计量,但如果你的batch size太小,同步后的方差会异常,导致loss波动大且下降慢,可以换成SyncBatchNorm试试。最后,确认一下你的梯度是否真的需要反向传播后再reduce,有时候某些层被设了requires_grad=False,DDP会跳过这些参数的同步,但打印loss时却包含了它们,造成错觉。如果还不行,建议在backward后手动打印一下每个rank上某个参数的grad,看看数值是否一致,这样能快速定位是通信问题还是数据问题。
我之前也踩过这个坑,梯度不同步很可能是world_size和rank没传对,DDP初始化时这两个参数必须跟实际进程数匹配,不然reduce根本没跑起来。另外你loss每个卡不一样,建议先确认一下是不是没调model.train(),或者batch size太小导致单卡梯度噪声大。还有个冷门的点,PyTorch 1.12配CUDA 11.6的话,试试把NCCL的P2P禁用一下,设个NCCL_P2P_DISABLE=1,有时候多卡通信会卡在共享内存上。init_method其实影响不大,只要所有进程能看到同一个地址就行,重点还是检查dist.init_process_group里的backend是不是设成nccl了,gloo在V100上梯度同步会慢很多。
这问题我踩过一模一样的坑,大概率不是init_method的事。你检查下DDP构造前是不是忘了设distributed.init_process_group,或者world_size和rank传错了,这会导致进程间根本没有建立通信,梯度reduce自然没生效。另外PyTorch 1.12有个老坑,如果模型里有未参与forward的buffer,DDP会跳过梯度同步,试试把find_unused_parameters=True先加上排除这个可能。我上次就是被一个没用的BN buffer坑了一整天,loss各卡不一样基本就是通信没建立,你先打印下torch.distributed.get_world_size()确认下每个进程看到的是不是4。
我之前也踩过这个坑,loss不一致大概率不是init_method的问题,先检查一下是不是每个rank的batch_size没按总卡数相应调大,导致等效batch变小了。另外,你确认一下训练循环里有没有调model.zero_grad()还是optimizer.zero_grad(),这两个在DDP下对梯度累积的影响差挺多的。还有个比较隐蔽的点,如果用了混合精度,grad_scaler的scale不同步也可能让reduce看起来没生效,试试关掉AMP看梯度对不对。
这个思路不错,收藏了。
我遇到过类似的情况,当时排查了半天发现是DDP的bucket划分导致的假象,loss不完全一致其实正常,因为每个卡处理的数据批次不同,但梯度不同步就肯定有问题。你确认一下是不是在model外面又包了一层DataParallel或者别的东西,DDP和DataParallel混用经常会出现这种梯度各自为政的情况。另外init_method用env://的话,得确保torch.distributed.init_process_group里传的rank和world_size跟实际启动命令对得上,尤其是用torchrun启动时,环境变量是自动注入的,但你要是手动设了MASTER_ADDR或者RANK覆盖了,就会出这种诡异问题。还有个坑是PyTorch 1.12的DDP对CUDA 11.6的NCCL版本兼容性有点问题,某些卡间通信会静默失败,你可以试试先设NCCL_P2P_DISABLE=1看下梯度是否同步,如果突然正常了那就是NCCL通信库的锅。顺便检查一下你的模型里有没有BatchNorm层,DDP对BN层的同步处理是默认关闭的,如果模型里有BN且没开sync_bn,不同卡上的统计量就是独立的,这也会导致loss差异越来越大。最后建议你把梯度打印出来看看,用torch.distributed.all_reduce手动做一次验证,如果手动聚合能成功但DDP的hook不触发,那基本就是注册时机的问题,比如在forward之前就定义了新的参数。
我之前也踩过一模一样的坑,排查到最后发现是没设torch.cuda.set_device(local_rank),导致每个进程都默认用了卡0,梯度倒是“同步”了但全挤在一张卡上,loss自然乱飘。你确认下代码里是不是只用了init_process_group但漏了这句,DDP的梯度归约是依赖每个rank绑定到正确设备的。另外init_method用tcp://localhost:12355没问题,但要注意端口别被其他进程占了,或者试试file://的方式更稳。还有个隐蔽点:如果你的模型里有batch size很小或者用了find_unused_parameters=True,有些层的梯度可能没参与同步,但看你描述loss每卡不一样,更像设备分配问题。建议在model = DDP(model, device_ids=[local_rank])里显式传device_ids,别省。最后,PyTorch 1.12的DDP在NCCL下有个老bug,和CUDA 11.6配合偶尔会触发梯度桶划分异常,可以试试把bucket_cap_mb调小到5,强制多分几个桶,有时候能绕过。实在不行先降到1.11或者升到1.13,我后来升了版本就再没遇到这问题。
八成是DDP初始化时忘了调world_rank,或者是把模型放到了别的device上,检查下local_rank传对没。
我之前也踩过类似的坑,排查了半天发现是DDP初始化时漏了torch.cuda.set_device(local_rank),导致每个进程都默认用了0号卡,梯度全挤在一块儿了。你检查下这个,另外init_method用env://的话,记得确保每个进程的RANK和WORLD_SIZE环境变量是独立设置的,别共用一份。还有个笨办法,在backward之后手动打印一下每个rank的梯度norm,看看是不是真的没同步,这样能快速定位是通信问题还是数据问题。
DDP梯度不同步多半不是init_method的锅,你先把torch.distributed.all_reduce单独拉出来测一下,看是不是后端通信本身有问题。另外4卡loss各不一样很可能是数据 shuffle 的种子没设对,DistributedSampler 里得给每个rank传不同的seed,否则每个卡拿到的数据顺序完全一样,梯度自然对不上。还有个小坑,PyTorch 1.12配CUDA 11.6的话,NCCL版本太老容易出玄学问题,建议升到2.17以上试试。你确认下是不是漏了model.require_backward_grad_sync这个标志,DDP在某些场景下会跳过同步的。
我遇到过类似的坑,大概率不是init_method的问题,tcp和env其实都能用。你检查下DDP构造时有没有把model.cuda()放到rank对应的device上,还有确认下每个进程的torch.cuda.set_device是不是设对了。另外,loss不一样有时候是正常的,因为每个卡看到的batch不同,但梯度不同步就值得怀疑了,建议在backward之后手动hook一下每个rank的梯度,看看是不是真的没allreduce。
还有个常见原因是数据集的len在各卡上不一致,DistributedSampler会自动处理,但如果你手动shuffle过或者用了自定义sampler,可能就失效了。可以试试把batch_size调小点,排除显存溢出导致的异步问题。如果还不行,直接升级到PyTorch 2.0以上吧,1.12的DDP在某些CUDA版本下确实有已知bug。
八成是DDP初始化那块没等所有进程就绪,试试在init_process_group后面加个barrier。
我之前也踩过这个坑,最后发现是没设find_unused_parameters=True,BERT里有些层在特定batch下可能没参与前向,DDP会默认跳过梯度同步。另外你确认下是不是用的torch.nn.parallel.DistributedDataParallel而不是DataParallel,前者才对标多进程。还有个小细节,tcp://localhost:12355在每张卡上都要指定不同的rank,但init_method本身没毛病,你试试把dist.init_process_group的backend换成nccl,gloo在某些版本下reduce会慢半拍。如果还不行,建议把梯度打印出来对比一下,看看是不是某个卡上的no_sync上下文没删干净。
这个问题八成是卡在NCCL通信上,试试把init_method换成tcp://主节点IP:端口,另外确认一下每张卡的rank和world_size传对了没。
我之前也踩过这个坑,DDP的梯度不同步很多时候不是init_method的问题,倒是先检查一下是不是把model.forward的输入数据直接放在了GPU上,忘了给每个rank单独分配device_id。另外你确认一下是不是用了model = DDP(model, device_ids=[local_rank]),漏了device_ids这个参数的话梯度reduce会静默失效,loss各算各的。还有个小细节,DistributedSampler的shuffle必须设成True,不然每个epoch的batch划分不一致也会加剧loss抖动。如果这些都排除了,可以试试把find_unused_parameters设成True,有时候某些层没参与反向也会导致同步中断。
先检查一下是不是DDP包装后没调用loss.backward()的同步钩子,看看进程组初始化到底成功了没。
先确认下是不是每张卡都能看到同一份数据集,有时候本地缓存路径不一致会导致数据分布不同。
试试把梯度打印出来看看,八成是模型里某些层没被DDP包住,像那些自定义的buffer参数。
我之前也踩过这个坑,排查下来多半是DDP初始化时world_size和rank没跟实际进程数对上,尤其是用torchrun启动的话,环境变量得用LOCAL_RANK而不是RANK。另外你确认一下是不是每个进程都独立调用了init_process_group,别漏了,这会导致backend根本没建起来。还有个小细节,1.12版本里DDP的梯度同步默认是异步的,如果模型里有不参与loss的buffer,可能会干扰allreduce,试试find_unused_parameters=True看下。实在不行可以把gradient_as_bucket_view打开,能直观看到梯度是否归零,方便定位。
我上周刚踩过类似的坑,最后发现是忘了在训练循环里调用model.reduce(),DDP的梯度同步是hook自动触发的,但如果你手动改了梯度或者用了梯度裁剪,得确认下no_sync上下文有没有误用。另外init_method用env://的话,得确保每个进程的RANK和WORLD_SIZE环境变量都传对了,尤其是用torchrun启动时。还有个冷门的点,PyTorch 1.12的DDP对CUDA 11.6支持有点小bug,建议升到1.13试试,我升级后loss曲线就正常了。