最近在搞多卡训练,用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报错梯度不同步怎么办?
全部回复
共 45 条检查下每个进程的torch.distributed.get_rank是不是独立设好的,我踩过同样的坑,init_method反倒没问题。
这个问题我之前也踩过类似的坑,看起来更像是DDP的backend通信没真正跑通。你确认一下是不是把model放到GPU上之后再wrap的DDP?顺序搞反了会导致梯度不聚合。另外,PyTorch 1.12的nccl后端在特定驱动版本下会有玄学bug,建议先切到gloo试试能不能同步——虽然慢点但能验证问题。还有就是,每个进程的local_rank和cuda:0绑定了吗?我猜你可能是没用torchrun或者launch脚本直接手写多进程,手动分配rank时容易漏调set_device。还有个小细节:DistributedSampler的drop_last参数,如果是False且数据量不能被batch_size整除,最后几个batch的size不一致也会让梯度看起来像没同步——虽然实际梯度是同步的,但loss值会飘。最后再查一下环境变量里MASTER_ADDR和WORLD_SIZE是不是在每个进程里都设对了,尤其是多机场景下。
这个问题我之前也踩过坑,大概率是DDP初始化时world_size和rank没对齐,或者torch.distributed.barrier()没卡在正确位置。你检查下是不是每个进程的local_rank和cuda device绑对了,很多时候reduce没生效是因为模型没被完整wrap进DDP,或者optimizer在DDP外面创建的。另外PyTorch 1.12有个已知的nccl后端小bug,可以试试换成gloo看梯度同步是否正常。
遇到类似问题排查过一轮,除了init_method,建议也确认下torch.distributed.barrier()有没有在训练循环前调用,这个很容易漏掉。另外你用的PyTorch 1.12其实有个小坑,DDP默认的梯度同步是异步的,可以手动设置find_unused_parameters=False试试,有时候模型里有些子模块没用到就会导致梯度不同步。还有loss每个卡不一样的问题,可以打印一下每个rank的loss是否真的在reduce后同步了,可能你打印的是本地的未同步值。
遇到过类似问题,当时排查半天发现是DDP包装模型后忘了调用model.module来访问原始模块,导致参数更新逻辑没走通。另外你的loss差异大可以看看是不是batch size太小,每个卡数据少梯度噪声大。还有init_method用env://的话要确保环境变量RANK、WORLD_SIZE这些手动设对了,不然确实可能各卡各算各的。
遇到过类似的问题,后来发现是每个进程的batch size没设对,导致梯度累积步数不一致。你检查一下是不是每个卡实际拿到的数据量不一样?另外DDP默认会用all_reduce,如果自己写了loss计算逻辑,记得确保只有主进程做backward,其他卡只做前向。还有个小坑,PyTorch 1.12在某些CUDA版本下,nccl后端可能不稳定,试试换成gloo看梯度能不能同步。
遇到过类似的问题,排查下来大概率是batch size太小导致不同卡上的loss差异被放大了,试试把global batch size调大一点,比如每卡batch size 16起步。另外init_method用env://的话,记得检查环境变量RANK、WORLD_SIZE这些是不是正确传给了每个进程,有时候torchrun会自动设但手动启动容易漏。还有个小坑就是DDP默认会在第一个backward时同步梯度,如果你中间有梯度裁剪或者累加操作,得留意一下是否影响了reduce的时机。
遇到过同样的问题,后来发现是DDP初始化时没有正确设置rank和world_size,尤其是用torchrun启动的话,环境变量会自动配好,手动写脚本容易漏。你试试在启动命令里加--nproc_per_node=4,然后用torch.distributed.init_process_group(backend='nccl'),别自己传参数。另外检查下是不是模型里有BatchNorm没处理好,DDP对BN的同步也有要求,可以换成SyncBatchNorm试试。
我上周也踩过这个坑,检查下来发现是漏调了set_epoch,每个epoch没重置DistributedSampler的种子,导致数据划分错位,梯度自然对不上。另外init_method用env://的话,记得要在启动脚本里手动export MASTER_ADDR和MASTER_PORT,不然DDP通信根本建立不起来。你可以先单进程跑一下torch.distributed.init_process_group看看有没有报错,定位会更准。
我也踩过这个坑,特别是刚切到DDP的时候,最容易忽略的就是loss本身没做同步。你提到每个卡loss不一样,大概率是每个rank自己在算loss然后只打印了自己的那部分,DDP默认只同步梯度,不会帮你同步loss值。我一般会在所有进程里算完loss之后,用all_reduce平均一下再打印,这样每张卡看到的loss就一致了,也能看出来梯度到底有没有用上。
另外你提到reduce没生效,建议检查一下模型里有没有用到BatchNorm之类的层,DDP对BN的处理比较特殊,每张卡还是各自算均值和方差,这也会导致loss不同步。可以把BN换成SyncBatchNorm试一下,对收敛速度有帮助。
还有个小细节,init_method用env://的话,确保每张卡的RANK、WORLD_SIZE、MASTER_ADDR、MASTER_PORT都配对了,特别是MASTER_PORT要没被占用。我之前有一次就是端口冲突导致通信初始化失败,但没报错,就是梯度不同步。可以用torch.distributed.get_world_size()和get_rank()打印出来确认一下。
这种情况我去年也碰到过,排查到最后发现是DDP的bucket设置问题——默认的bucket大小可能会导致小模型(像BERT这种)的梯度更新不同步,你试试把bucket_cap_mb设小一点,比如25,或者直接调成0。另外确认一下你的torch.distributed.all_reduce是不是被某个自定义hook覆盖了,有时候自己写了loss聚合但忘了调这个。实在不行换个思路,先降级到torch 1.10看看,我之前1.12的版本也有过奇怪的通信bug。
遇到过类似的情况,检查一下是不是每个进程的batch_size没除以卡数,或者梯度累积步数没调整,会导致实际batch对不上。另外可以看看DDP初始化时有没有设local_rank,有时候没传对的话,reduce操作会跳过。我上次就是torchrun启动时参数漏了,搞得梯度不同步。
这个问题我也踩过坑,大概率是DDP的rank和world_size没传对,尤其是用torchrun启动时环境变量要跟代码里一致。另外检查一下是不是模型里有batch size相关的操作,比如LayerNorm的affine参数没冻结,或者用了SyncBN但忘记设process_group。要不要试试把梯度打印出来看下norm,如果每个卡差异很大,八成是某个自定义forward里用了torch.Tensor而不是通过nn.Module传参。
老哥这个问题我也踩过坑,大概率是DDP初始化时world_rank和world_size没配对,或者你用了torch.distributed.init_process_group但backend没设成nccl导致reduce不走GPU。之前我用tcp://也遇到过类似情况,后来发现是每个进程的local_rank没正确赋值,建议检查下launch参数和torch.cuda.set_device的调用顺序。还有如果用了混合精度,amp的GradScaler得在DDP外面初始化,不然梯度缩放也会各算各的。
检查下每个进程的rank和world_size是不是对的,我上次也这样,结果是torchrun没加--nproc_per_node参数。
遇到过类似问题,后来发现是DDP的model wrapper里忘了设find_unused_parameters=True,因为BERT某些层在微调时可能没参与梯度计算,导致reduce跳过。另外可以试试在训练循环开头加个torch.distributed.all_reduce同步下loss,看看各卡梯度是不是真的一致。
你这情况我好像也踩过类似的坑,当时我是因为手动调了model.parameters()里的requires_grad才出的问题,DDP要求所有参数在构造时就得保持一致。你确认一下有没有对某层单独设过requires_grad=False,或者用了自定义的梯度掩码?另外init_method报错的话可以试试把world_size和rank打印出来,有时候是mp.spawn传参漏了。还有个小细节,如果你用了梯度累积,得确保每张卡累积的步数一致,不然reduce会在不同step触发。PyTorch 1.12那个版本我记得有个小bug,和nccl后端通信锁有关,可以试下显式设backend='nccl'加上set_start_method。方便的话贴一下DDP的初始化代码段,光看描述感觉像rank映射没对上。
试试把torch.distributed.barrier()加在loss backward前面,我之前也这样搞定的。
这问题我也踩过坑,大概率不是init_method的锅,你检查下是不是每个进程的rank和world_size传对了,特别是用torchrun启动时环境变量容易漏。另外可以打印下模型参数的grad_finite看看,如果某个卡梯度全为None那可能是BN层没同步,DDP默认只同步有梯度的参数。
遇到过类似的问题,当时折腾了好久才发现是DDP的loss计算方式没对齐——每个卡自己算loss然后backward,但模型里如果有batch normalization或者自定义的reduce操作没处理好就容易出这种问题。建议你检查一下模型结构里有没有用SyncBN,或者手动打印一下每个rank的梯度看看是不是真的没同步。另外init_method用env://的话,环境变量MASTER_ADDR和MASTER_PORT是不是都正确设了?有时候torchrun启动方式和手动设置这些变量会有冲突。