最近在搞多卡训练,用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和world_size是不是对的,我之前漏设LOCAL_RANK就踩过这个坑。
说实话,你这个情况我踩过差不多的坑,单卡正常多卡翻车,十有八九是环境变量或者通信初始化的问题。你确认一下是不是每个进程的rank和world_size真的传进去了,尤其是用torchrun启动的时候,有时候没设置--nproc_per_node会导致实际跑起来的进程数和预期对不上。另外,你提到reduce没生效,我怀疑是模型没有正确包裹在DDP里,或者DDP的device_ids参数没指定当前卡号,比如device_ids=[local_rank]这个很容易漏掉。还有个小细节,如果你用了混合精度训练,amp的GradScaler也要注意,它可能在某些场景下延迟梯度同步。1.12版本我记得有个已知bug,就是nccl后端在某些V100上对多机多卡支持不太好,你可以试试换gloo后端看现象是否消失。init_method这两个写法按理说都没问题,但如果你是用torch.distributed.launch启动的,建议直接用--master_addr和--master_port,省得tcp那串字符解析出错。数据方面,DistributedSampler虽然加了,但你每个epoch有没有手动调用sampler.set_epoch(epoch)?不设的话每个卡拿到的shuffle顺序一样,样本重复也会影响梯度统计。
这个我熟,之前也踩过类似的坑。你确认下是不是把model.to(device)放在了DDP包装之前,但device没正确映射到对应卡的rank?常见问题是忘了在DDP外面套一个torch.cuda.set_device(rank),导致所有卡都默认用了0号卡,梯度虽然是同步的但实际只在一个设备上计算。另外,init_method用env://的话,需要确保环境变量MASTER_ADDR和MASTER_PORT正确设置,而且每个进程的RANK和WORLD_SIZE必须独立传入,不一定是通过torchrun自动配置的。还有一个隐蔽的点:如果你用了梯度累积,记得在每步累积后调用loss.backward()时把retain_graph设为False,并且确保reduce操作只在最后一步触发,否则可能会因为多次反向传播导致梯度累加错乱。建议先用torch.distributed.all_reduce手打一个测试脚本验证通信是否正常,排除框架问题。最后检查下PyTorch版本,1.12在某些CUDA 11.6下有个已知的DDP梯度同步bug,升级到1.13或打补丁试试。
跑过类似的坑,看到这个问题特别有共鸣。你确认一下是不是在训练循环里漏了model.no_sync()的上下文管理,或者在梯度累积时手动调用了all_reduce?我之前也遇到过reduce没生效的情况,后来发现是因为自己在某些分支里用torch.no_grad()包裹了前向传播,导致梯度计算图断开了,DDP的hook就检测不到梯度了。另外,你的find_unused_parameters设成True试试看?有时候模型里有些参数没参与loss计算,DDP会默认跳过同步,虽然BERT通常不会,但可以排除一下。还有个小细节:检查一下torch.distributed.get_world_size()返回的是不是4,有时候多进程启动时rank没对上,实际只跑了一个进程。pytorch 1.12我记得有个已知bug,就是nccl后端在特定卡序下会报梯度同步失败,建议升到1.13或者换gloo后端先验证一下。实在不行,跑个最简单的all_reduce测试脚本,看能不能把不同卡的tensor同步成功,这样能快速定位是框架问题还是你的代码逻辑问题。
这个问题我上周刚踩过类似的坑,DDP梯度不同步很多时候不是init_method的问题,而是模型里某些操作把计算图切断了。你检查一下forward里有没有用到in-place操作或者自定义的loss函数里做了跨卡的mean?我遇到过用torch.nn.DataParallel迁移过来的代码里,有人把loss直接做了.item()然后手动backward,这样梯度根本没法同步。另外你的batch size在4卡上是不是没按总batch去缩放学习率?每卡loss不一样也可能是BN层没同步,虽然BERT里一般不用BN,但如果你用了LayerNorm以外的自定义层,得确认一下DDP的find_unused_parameters参数设了没。还有个小细节,PyTorch 1.12对梯度桶的合并策略有改动,可以试试设置bucket_cap_mb为25或者更大,强制梯度更早同步。最后建议你print一下每个rank的模型参数地址,看是不是真的指向了同一份数据,有时候torch.save和load会不小心把参数独立出来。
遇到过类似问题,当时折腾了半天发现是忘了在backward之前调用model.require_backward_grad_sync=True,DDP默认是同步的但有些操作会把它关掉。你可以试试在loss.backward()前加一句with model.no_sync()看看是不是被上下文管理器影响了。另外检查下是不是用了梯度累积但没处理好同步周期,这个也容易导致卡间梯度不一致。
遇到类似情况确实头大,我之前也踩过这个坑,最后发现是没正确设置torch.distributed.barrier()导致rank不同步。你可以试试在模型初始化后加个torch.distributed.barrier(),或者检查下local_rank和world_size是否传对了。另外PyTorch 1.12的DDP对NCCL后端兼容性有点玄学,换成gloo后端跑一次验证一下梯度是否同步。
之前也踩过类似的坑,最后发现是没在模型forward之后手动调用reduce钩子,DDP默认只同步有梯度的参数,如果某个卡上某些层没参与计算梯度就不会同步。可以试试在loss.backward()前加一句model.require_backward_grad_sync=True强制同步。另外检查下是不是用了梯度累积,累积步数没对应调整同步频率也会出这种问题。
我最近也踩过类似的坑,最后发现是DDP包装模型前没有把model放到正确的device上,导致梯度回传时设备不匹配。建议你检查一下model = DDP(model.to(rank), device_ids=[rank])这一步是不是写对了,另外torch.cuda.set_device(rank)也得在初始化进程组之前调用。如果还不行,可以贴一下完整的训练循环代码,大家帮你看看是不是all_reduce被漏掉了。
试试在初始化DDP前手动设置torch.cuda.set_device(local_rank),我踩过同样的坑,加完就同步了。
遇到同样的问题,我后来发现是忘了在DDP包装模型后把optimizer的state_dict清空重新初始化,你可以试试看。另外检查一下每个进程的rank和world_size是不是对的,我之前就是init写错了导致reduce没生效。如果还不行,把torch.distributed.all_reduce手动加一个测试一下梯度能不能同步。
试试把torch.distributed.barrier()加在模型加载后面,我之前也遇到过,加上就同步了。
检查下DDP初始化时有没有设local_rank参数,或者试试在启动命令里加--master_port手动指定端口。
我最近也踩过类似的坑,排查下来发现是DDP的find_unused_parameters没开,导致某些层参数没参与梯度同步,loss自然对不上。另外建议你检查一下batch_size是不是在每个卡上设成了单卡时的值,这样等效batch会翻倍,学习率也需要对应调整。还有个小细节——试试把torch.distributed.barrier()放在模型broadcast之后,有时候能解决初始化顺序的问题。
这种情况我之前也踩过坑,大概率是DDP的初始化步骤里少了set_device或者没在进程内正确绑定GPU。你可以在每个进程开头加上torch.cuda.set_device(local_rank),然后model.cuda(local_rank)再包DDP,这样reduce才能正确找对设备。另外确认一下你的batch_size是不是设得太小了,梯度方差大也会让各卡loss显得不一致,试试把batch翻倍或者加个梯度累积。
我最近也踩过类似的坑,排查下来发现大概率是model wrapper的问题——DDP的模型必须放在init_process_group之后创建,而且梯度同步默认是按parameter bucket来的,如果模型参数没注册到rank对应的device上,reduce操作就会失效。你可以试试在构造DDP之前手动把model.cuda()到当前rank,同时检查一下nccl后端有没有正确加载。另外PyTorch 1.12有个小bug,sync_bn在某些场景下会导致梯度不对齐,可以先用普通BN排除一下。
这个问题我之前也踩过坑,大概率是DDP初始化时world_rank和local_rank没对应好,或者启动脚本里没用torchrun/torch.distributed.launch来分配进程。你可以先试试在模型forward里加个torch.distributed.get_rank()打印一下每张卡的rank值,确认它们确实在同一个进程组里。另外,PyTorch 1.12有个坑,如果用了torch.utils.checkpoint,梯度同步可能会被跳过,可以临时关掉看看。
遇到这个坑太正常了,我1.12版本也踩过。你说loss下降慢且每个卡不一样,大概率是梯度没真正同步,但DDP默认应该会自动做all-reduce,问题可能出在你用了某些自定义操作绕过了hook。比如模型里如果有in-place操作或者自定义的loss计算没经过DDP包裹,梯度同步就会失效。另外检查一下是不是用了model.no_sync()上下文,或者某个子模块没被DDP管理到。init_method其实影响不大,只要所有进程能正确通信就行,关键是确保每个进程的rank和world_size传对了。建议你先在代码里打印一下torch.distributed.get_world_size()和get_rank()确认进程组初始化没问题,然后看下ddp的bucket大小是不是设得太小导致通信开销异常。还有个小技巧:可以手动在backward前调用model.require_backward_grad_sync=True强制同步,排除一下是不是梯度累积逻辑出了问题。
这种loss不一致的情况我调DDP时也碰到过,大概率不是init_method的锅。建议先确认一下是不是把model wrapper和optimizer的创建顺序搞反了,得先wrap DDP再传给optimizer。另外可以打印下每个rank的梯度范数,看看是不是某些层根本没参与同步,比如batch norm的running stats没处理好。
遇到过类似的问题,重点排查一下模型是否在DDP包装前就已经有了部分参数在cuda上,这样会导致梯度不同步。另外,PyTorch 1.12有个已知的glibc兼容问题,建议升级到1.13或2.0试试,我之前就是升级后解决的。还有个细节,确认下你的batch size是不是太小了,4卡时每卡batch太小会让BN层统计量不一致,影响loss收敛。