最近在搞多卡训练,用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报错梯度不同步怎么办?
全部回复
共 44 条检查下每个进程的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会不小心把参数独立出来。
这种loss不一致的情况我遇到过,大概率是模型里还有未包装的BatchNorm层,DDP默认不处理BN的同步,得手动转成SyncBatchNorm才行。另外建议你跑一下 torch.distributed.all_reduce 验证通信是否正常,先排除nccl后端的问题。还有个小细节,检查下 find_unused_parameters 是不是设成了True,有时候参数没用全会导致梯度同步被跳过。
遇到过类似问题,后来发现是DDP初始化时忘了设置torch.cuda.set_device(local_rank),导致梯度计算时设备没对齐。另外可以检查一下model是否在DDP包装前就移到了GPU上,以及loss.backward()是不是在每个进程里都调用了。如果确认这些都没问题,试试在训练循环里加个torch.distributed.all_reduce手动验证下梯度同步,能快速定位是框架问题还是代码逻辑问题。
看到你这个情况,我第一反应是检查下训练脚本里有没有手动调用loss.backward()之后忘记用optimizer.step()前做all_reduce?DDP虽然会自动处理梯度同步,但如果你用了自定义的梯度累积逻辑,或者手动修改了loss,可能会把DDP内部的hook绕过去。另外,你确认下每个进程的rank和world_size是不是真的传对了,有时候多进程启动时环境变量没export全,比如MASTER_ADDR和MASTER_PORT没设置,虽然你试了init_method,但可能torch.distributed.init_process_group()里backend没指定nccl?PyTorch 1.12里nccl对多卡同步挺敏感的,之前我遇到过类似问题,最后发现是torch.cuda.set_device(rank)没在每个进程里单独设,导致梯度默认跑到了卡0上。还有一个冷门点:如果数据预处理里用了随机种子没按rank设不同值,不同卡的数据顺序不一致也会让梯度看起来不同步,你可以打印下每个rank的梯度范数对比一下。
我最近也踩过这个坑,特别是loss值不一致这个现象,基本能确定是梯度同步没走通。你试过在backward之后手动打印一下每个rank的梯度吗?比如用torch.distributed.all_reduce把梯度拉回来对比一下,这样能直接看到reduce是不是真的没生效。另外init_method那块我建议你检查一下环境变量,比如RANK和WORLD_SIZE有没有在启动脚本里正确传进去,有时候用torchrun或者mp.spawn会省事很多。还有一个容易忽略的点是,你用了DistributedSampler没错,但每张卡上的drop_last设置一致吗?还有batch size别搞混了,要是不同卡数据量不一样,梯度自然对不上。我个人经验是,先降级到2卡跑个简单toy model验证一下整套DDP流程通不通,再上4卡,排查起来效率更高。
这个问题我之前也踩过坑,特别像你描述的这种每卡loss不一样但梯度没报错的情况,大概率不是init_method的锅。你可以先检查一下DDP包装模型时是不是漏了device_ids参数,或者没有把模型先移到对应rank的GPU上再包装,这会导致梯度实际上没在正确的卡间同步。另外,确认一下你的optimizer是不是在每个进程里独立创建的,如果用的是同一个optimizer实例分发给不同进程,梯度更新就会乱掉。还有一招,在backward之后手动加一句torch.distributed.all_reduce(gradient, op=torch.distributed.ReduceOp.SUM)看看能不能强制同步,如果能跑通就说明DDP的hook根本没生效。对了,PyTorch 1.12有个已知的小bug,就是和CUDA 11.6配合时,nccl后端在某些拓扑下会静默降级成gloo,建议显式设置backend='nccl'并且检查一下torch.distributed.is_initialized()。如果还不行,试试在每轮迭代里打印一下model.parameters()的grad norm,看不同卡之间差多少,这样能快速定位是哪个层没同步上。
这个我遇到过类似的坑,卡在相同的地方折腾了两天。你确认一下是不是每个进程的rank和world_size真的传对了,尤其是如果用torchrun启动的话,环境变量里RANK和WORLD_SIZE是自动设的,但有些人会手写launcher脚本导致不一致。另外PyTorch 1.12有个老bug,就是DDP的梯度同步依赖于bucket划分,如果你用了gradient_checkpointing或者模型太大,可能会触发异步梯度计算,导致reduce操作被跳过。建议你显式设置ddp_comm_hook为allreduce,或者试试在backward之前加一句torch.distributed.barrier(),强制同步。还有个小细节,检查一下你的forward里有没有对参数做in-place修改,比如dropout的mask或者某些归一化层的计算,这种操作会破坏梯度图,让DDP的hook检测不到梯度变化。最后,如果你的数据集特别小,不同卡采样到的batch差异太大,也会导致loss幅度不同,可以先用固定随机种子验证一下。
遇到类似情况,我折腾过两天才发现是cuda版本和nccl库不匹配,建议先跑个torch.distributed.all_reduce测试一下是否能正常通信。另外检查下torch.cuda.set_device是不是在每个进程里都设了对应的rank,我当初漏了这个导致梯度一直没归约。
我最近也踩过类似的坑,问题可能出在DDP的初始化时机上,先试试把torch.distributed.init_process_group放在模型包装之前调用,确保backend设成nccl。另外,检查下是不是在模型forward里用了自定义的loss计算,如果有手动做gradient accumulation的话,记得用no_sync上下文管理器避开不必要的梯度同步。你那四个卡的batch size保持一致吗?有时候数据量不均衡也会让loss看起来不一样。
检查下是不是漏了set_epoch,DDP需要每个epoch手动调用sampler.set_epoch(epoch)来打乱数据。
试试把torch.distributed.barrier()加到每个epoch开头,我之前也是同样问题,加了就同步了。
看看DDP的find_unused_parameters有没有设True,另外检查下模型里有没有BN层没同步。
这种loss各卡不一样的情况,我之前也踩过坑,大概率不是init_method的问题。建议检查一下是不是在DDP之前手动调用了model.cuda()或者某个模块没被DDP包裹,导致梯度只在部分参数上同步。另外可以加个torch.distributed.all_reduce手动验一下梯度是否真的没同步,排除是loss打印时机的问题。
这个问题我之前也踩过坑,检查一下是不是DDP包装模型之后,优化器里还保留了原始的model.parameters(),应该换成model.module.parameters()才对。另外,确认一下每个进程的local_rank是不是从launch脚本正确传进去了,有时候init_method没问题但rank设错了也会导致梯度不聚合。你试试在backward之后打印一下梯度看看是不是真的每个卡不一样。
遇到过类似的问题,排查下来多半是模型初始化时没用sync_bn,或者梯度累积时忘了做all_reduce。你可以试试在DDP包装前手动调用一下model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model),另外检查下是否每个进程的batch size设置一致,有时候数据不均匀也会导致loss差异。还有个小细节,确保torch.cuda.set_device(local_rank)在DDP初始化之前调用,不然梯度通信可能走错卡。
这个我之前踩过一样的坑,大概率是模型里某些层没用DDP包裹,或者自己写了梯度更新逻辑把hook绕过去了。你可以试试在训练循环里加个torch.distributed.all_reduce(tensor)手动同步一下梯度,看看能不能对齐。还有,检查下有没有把model.no_sync()上下文管理器用在不该用的地方,那玩意儿会跳过梯度同步。