最近在搞多卡训练,用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 条我之前也踩过类似的坑,排查下来发现是DDP的bucket划分导致的,梯度reduce是按bucket来的,如果模型太大默认bucket太小,不同卡上的梯度可能被分到不同bucket里,就会看起来像没同步。你可以试试把bucket_cap_mb调大点,比如设成25或者50,强制把更多梯度塞进同一个桶再reduce。另外init_method那个报错其实不关键,tcp和env都行,关键是确保所有进程的rank和world_size传对了,你可以打印一下每张卡的LOCAL_RANK和RANK确认下是不是有进程重复初始化了。还有个小细节,DistributedSampler要在每个epoch调用set_epoch,不然每个epoch数据顺序一样,也可能让loss表现很怪。
同款问题之前折腾过,我后来发现是忘了在训练循环里调用model.require_backward_grad_sync,不过你这种情况更像是在DDP包装前就把模型放到cuda上了,导致每个rank的模型虽然广播了参数但梯度归并时找不到对应的bucket。可以试试把model.cuda()放到DDP包装之后,另外确认一下loss.backward()是在DDP的forward之后调用的,顺序错了reduce也会失效。还有个坑是PyTorch 1.12的DDP对梯度归并的bucket size默认值比较保守,可以手动设bucket_cap_mb=25看看有没有改善。如果还不行就打印一下每个rank的梯度范数,大概率是数据没真正shuffle导致各卡样本分布不均。
我之前也踩过这个坑,先查一下是不是DDP包裹模型前忘了设置find_unused_parameters=True,BERT里有些层确实可能在某些batch下不参与梯度计算,导致同步时挂起或各卡不一致。另外,你确认一下是不是在创建DDP之后才定义的optimizer,顺序反了的话梯度归约也会出问题。还有个小细节,如果用了gradient accumulation,需要手动把loss除以累积步数再backward,不然多卡下梯度scale会乱。
我上周刚踩过类似的坑,最后发现是torch.cuda.set_device(local_rank)没写对,DDP里每个进程必须手动绑定到对应的卡上,不然梯度虽然reduce了但会算到错误的设备上。你可以先打印一下dist.get_rank()和torch.cuda.current_device()看看是不是对得上。另外PyTorch 1.12的DDP对find_unused_parameters很敏感,如果你的模型里有层没参与loss计算,记得设成True,不然梯度同步会静默失败。还有个土办法,把torch.distributed.barrier()加在每个batch开头,强制对齐一下,能快速排查是不是异步导致的滞后问题。
我之前也踩过类似的坑,DDP的梯度不同步有时候不是init_method的问题,而是world_size和rank没传对,导致进程间没真正建立通信组。你用了DistributedSampler是对的,但检查一下每个进程的rank是不是独立且从0到3,另外PyTorch 1.12用env://的话,环境变量MASTER_ADDR和MASTER_PORT必须所有进程可见,否则reduce根本不会执行。我上次是加了个torch.distributed.barrier()在模型broadcast之后,强制同步一下就好了,你可以试试。还有个小细节,loss不一样也可能是每个卡batch size不同,确认下sampler的shuffle参数是不是True,不然数据分布不均也会让梯度看起来没同步。
八成是没用torch.cuda.set_device,每卡rank要单独设,不然梯度全挤到0号卡上去了。
大概率是没调torch.cuda.set_device(local_rank),每个进程默认抢同一块卡,梯度当然各算各的。
遇到这个情况先别急着怀疑init_method,你确认一下是不是忘了在backward之后手动调all_reduce?DDP默认会在loss.backward()时自动同步梯度,但前提是你在构造DDP之前把模型正确wrap了,而且每个rank的batch size和shuffle seed得完全一致。我之前也踩过类似的坑,最后发现是DataLoader里忘了设sampler的shuffle=True,导致每个卡拿到的数据顺序不一样,梯度自然对不上。另外,你检查一下是不是用了SyncBatchNorm?虽然BERT里没有这个,但如果你改过结构,多卡时必须手动转换,否则BN层的统计量也是各自算的。还有个隐蔽问题:如果你在forward里用了任何非确定性操作(比如dropout没设seed),不同卡上的随机状态不同步,也会让梯度看起来“各自为政”。建议你先在每张卡上打印一下rank对应的loss和梯度范数,对比一下是不是所有卡的梯度都差不多,如果差很多,大概率是数据加载或模型定义的问题,而不是通信层的问题。还有,PyTorch 1.12的DDP有个已知坑,就是如果模型里用了自定义的autograd.Function,且没有正确实现mark_dirty或mark_shared,会导致梯度同步失效,你检查下有没有自定义算子。最后,init_method用tcp://localhost:12355没问题,但注意每张卡要设不同的rank和local_rank,如果用的是torchrun,环境变量应该自动配好,别手动覆盖。实在不行,把world_size打印出来确认一下是不是真的4个进程都在跑,有时候某个进程挂了,剩下的卡也会出现这种诡异现象。
大概率是没调DDP的find_unused_parameters,BERT里有些层没参与loss计算导致梯度同步跳过。
我之前也被这个坑过,你检查下是不是每个进程的rank和local_rank没对应上,尤其是用torchrun启动时,环境变量容易搞混。另外梯度不同步有个很隐蔽的原因,就是模型里有任何一层用了dropout或者batchnorm,在训练模式下每个卡行为会不一致,但你这个是BERT应该还好。如果reduce没生效,试试在backward之后手动打印一下gradient的均值,看是不是真的没合并,有时候是DDP包装的device_ids没设对,导致梯度直接留在各自卡上。还有个笨办法,把batch size调大点,先排除是不是数据量太小导致loss波动大,毕竟4卡每卡batch太小的话,梯度本身就有噪声。
这问题我上周刚踩过坑,大概率不是init_method的锅。你检查下DDP构造前model是否已经.cuda(),以及是否用了torch.cuda.set_device,否则梯度会算到默认卡上。另外,如果用了混合精度,记得grad_scaler要每个rank单独实例化。我那次是忘了在train loop里加model.no_sync()的条件跳过,导致梯度在backward时被DDP的hook重复reduce了。你试试把batch size调小点,逐卡打印一下rank和loss,看看是不是数据顺序问题。
我之前也踩过类似的坑,最后发现是DDP包装的时机出了问题。你确认一下是不是在构造DDP之前就把模型放到cuda上了?正确做法是先model.cuda()再包DDP,而且DDP内部会自动处理broadcast,你手动传参数反而可能干扰它。关于梯度不同步,我猜你八成是没设find_unused_parameters=True,BERT这类带dropout或者有动态计算图的模型,某些层在forward里可能没被用到,DDP默认会卡在等待梯度上。另外,loss不一样还可能是你每个卡上的batch size不一样导致的,DistributedSampler只保证样本不重复,不保证每个卡的数量完全一致,除非你显式设了drop_last=True。init_method那个报错倒不一定是根因,你试试先用torchrun或者torch.distributed.launch启动,让框架自己生成env,别手动指定。还有一个容易忽略的点是,如果你在forward里用了batch norm或者layer norm,它们本身就有跨卡的同步需求,但DDP只同步梯度,不同步BN的统计量,这也会让loss看起来各卡不一致。最后建议你直接在backward之后打印一下每个rank的梯度范数,如果确实不同,再检查是不是有哪个rank的loss没被正确reduce。
我之前也踩过类似的坑,排查了半天发现是world_size和rank传错了,导致DDP根本没进入多卡模式,你确认下初始化时这两个参数是不是从环境变量里正确读的。另外PyTorch 1.12的话,可以试试在构造DDP之前手动跑一个all_reduce做测试,能通就说明后端没问题,不然就是init_method的地址被其他进程占了。还有个小细节,DistributedSampler的shuffle参数要配合set_epoch用,不然每个epoch数据顺序一样,loss可能看起来就是各卡各的。
说实话你这个现象我去年也踩过一模一样的坑,最后排查出来是backend设成gloo导致的,nccl在V100上对梯度聚合的allreduce更靠谱。你检查一下dist.init_process_group里backend参数是不是写死了,PyTorch 1.12默认可能没你想的那么智能。另外一个很隐蔽的点是,如果你在模型forward里手动调用了allreduce或者梯度裁剪,DDP的梯度hook会被覆盖掉,尤其是用了梯度累积的时候,需要把no_sync上下文管理好。还有个小细节,DistributedSampler的shuffle参数在每轮epoch开始时要调用set_epoch,不然数据顺序完全一样,loss虽然不同但梯度方向会趋同,看起来就像没同步。你要是方便的话,可以打印一下每个rank的梯度范数,如果差异超过一个数量级,大概率是数据没切对,而不是reduce没生效。我之前还遇到过因为模型里用了dropout,不同卡随机种子不同导致loss天然有差异,但那个差异不会特别大,如果你发现loss相差好几倍,基本可以排除这个原因。实在不行可以试试把init_method换成file://,用共享文件系统做同步,有时候TCP端口会被防火墙拦。
碰到过类似的情况,最后发现是没在训练循环里调用loss.backward()之前先做一步model.zero_grad()或者optimizer.zero_grad(),导致梯度累积叠加,各卡算出来的梯度对不上。你确认下是不是每个step都清了梯度?另外init_method用tcp和env都行,但端口别跟别的进程冲突了,换个不常用的端口试试。
还有一种可能是你手动调用了all_reduce,但DDP内部已经帮你做了梯度同步,重复操作反而会把梯度搞乱。建议直接去掉自定义的reduce,只用DDP的默认逻辑,然后把batch size调大点看loss变化正不正常。如果还不行,检查下DDP包装的是不是model本身,而不是DataParallel包过的。
我这边之前用1.12也踩过坑,升级到1.13后问题就消失了,说不定是版本bug,你可以降级或者升级试试。另外确认下每张卡的shuffle种子是不是不一样的,DistributedSampler虽然分配了数据,但如果你额外设了全局随机种子,可能让各卡数据分布不一致,影响梯度。
我之前也踩过这个坑,后来发现大概率不是init_method的问题,而是进程组初始化之后忘了调用torch.distributed.barrier()。你试一下在训练循环开始前加一个barrier,确保所有rank都就绪了再同步,有时候rank 0跑太快后面卡还没准备好,梯度归约就会失败。另外你确认过环境变量吗,torchrun或者python -m torch.distributed.launch启动时,RANK和LOCAL_RANK千万别搞混,4张卡的话WORLD_SIZE也得是4,不然DDP内部根本不知道要和谁通信。还有一个隐蔽的点,PyTorch 1.12对NCCL的版本要求挺严格的,CUDA 11.6得配NCCL 2.10以上,你可以跑一下torch.distributed.all_reduce(torch.ones(1).cuda())做个最小验证,如果能同步成功说明框架没问题,那问题就出在你的模型或数据流上。还有,你确认每个rank加载的checkpoint是同一个吗,如果之前有断点续训,每个进程读到的模型参数不一致也会导致梯度对不上。建议你先用纯DDP跑个简单的resnet18 cifar10例子,排除环境问题再回到BERT任务,这样定位更快。
试试把find_unused_parameters=True加上,BERT有些层可能没参与反向传播导致梯度同步卡住。
说实话你这情况我太熟了,之前用1.12踩过一模一样的坑。梯度不同步但loss又各自在降,大概率不是init_method的问题,tcp和env都试过不行那就先排除这个。你确认下是不是忘了在backward之后手动调all_reduce?DDP正常会自动处理梯度同步,但如果你在模型里用了自定义的loss或者对梯度做了clip,可能会干扰到它的hook。另外检查一下是不是把model wrap进DDP之后又用了torch.no_grad()或者设置了find_unused_parameters=True,这两个都会导致梯度在某些层上不参与同步。还有个特别隐蔽的点:如果你在forward里用了类似model.xxx.parameters()的局部变量去更新参数,而不是直接操作DDP包装后的model,那梯度根本不会经过Reducer。我建议你打印一下rank0和rank3的某个中间层梯度范数,看看是不是真的完全没reduce,如果数值差异很大,八成是DDP的bucket划分和你的模型结构不匹配,试着把bucket_cap_mb调小一点,比如10或者5,强制它更频繁地同步。最后,如果实在不行,升级到1.13或2.0,老版本在V100上的通信优化确实有问题,我之前升级后同样的代码直接就好了。
之前跑多卡也踩过这个坑,最容易被忽略的是DDP的bucket_size,默认25M对于BERT这种大模型可能太小,导致梯度归并效率很低,试试调大点或者直接设成模型参数量。另外init_method用tcp方式的话,确保所有进程的rank和world_size传对了,尤其是用torchrun启动时环境变量会自己覆盖掉你手动设的。还有个土办法,在backward之前打印一下每个卡的loss,如果连初始loss都差很多,那基本就是数据没切干净,看看DistributedSampler是不是忘了给每个dataloader单独实例化。
遇到过类似的坑,后来发现是DDP初始化之后忘了调model.train()之前把梯度清零,但你这个loss不一致更像是通信没建立起来。建议先确认一下torch.distributed.is_initialized()是不是True,还有nccl后端有没有正确加载,1.12版本对nccl的兼容性有点迷。另外tcp://那个init_method别用localhost,多卡要写实际IP,env://的话得保证环境变量在每张卡上都传对了。我之前是卡在rank和world_size的传递上,你可以打印出来看看每个进程的rank对不对。