最近在搞多卡训练,用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初始化的时候rank和world_size没传对,因为你说的loss不一致和reduce没生效,通常不是init_method的问题,而是进程组压根没建起来。我之前也遇到过类似坑,后来发现是torch.distributed.init_process_group里忘了设backend='nccl',默认glu会走CPU通信,结果就是梯度各算各的。另外,你用torchrun启动的时候,得确保每个进程拿到的local_rank是环境变量里的,而不是硬编码成0,不然四个卡其实都在用同一块GPU的显存,但梯度却分散在各进程里。还有个容易忽略的点:如果你在模型forward里手动调用了all_reduce,那要小心和DDP的钩子重复,可能反而把梯度搞乱。建议你试试在训练循环里加个torch.distributed.barrier(),看loss是否瞬间就同步了,如果同步了那基本就是初始化问题。最后,PyTorch 1.12的DDP对CUDA 11.6挺稳的,建议直接升级到1.13以上,有些底层通信bug修了。要是还不行,就把环境变量NCCL_DEBUG=INFO打开跑一下,看通信日志具体卡在哪一步。
我之前跑YOLOv5也踩过类似的坑,单卡好好的,多卡loss就跟心电图一样各跳各的。你确认DistributedSampler和broadcast都做了,但梯度还是不同步,那大概率不是init_method的问题,tcp和env都能用,关键是看进程组有没有真正初始化成功。我建议你在rank0上打印一下torch.distributed.get_world_size()和get_rank(),确认四个进程都进来了,而不是每个进程都各自起了一套单卡环境。另外,你检查过DDP包装模型之后,是不是不小心把model.module.xxx当成了原模型的属性在用?比如你直接访问model.xxx,这可能导致梯度计算图被断掉。还有个小细节,PyTorch 1.12的DDP默认的bucket_size可能对BERT这种大模型不太友好,梯度归约的粒度太粗,导致每个bucket里的参数相差很大,显式调低bucket_cap_mb试试,比如设成25。最后,你得留意一下是不是所有卡的batch size一样,如果每张卡拿到的样本数不同,即使DistributedSampler也救不回来,loss天然就会不一样。你先用torch.distributed.all_reduce手动跑一下梯度,看看能不能同步,如果能,那问题就出在DDP的构造上,比如梯度是稀疏的没设置find_unused_parameters。
检查一下是不是忘了调world_size,init_method没问题,但rank和world_size对不上就会这样。
说实话你这情况我太熟了,之前我也是被这个卡了好久。你光确认DistributedSampler还不够,得看看每个rank拿到的dataloader是不是真的独立了,有时候shuffle没关或者batch_size没除以卡数会出这种隐性bug。另外init_method没问题的话,重点查一下是不是忘了设world_size和rank,或者环境变量没传对,尤其是用torchrun启动的时候,这几个参数容易漏。还有个小技巧,你可以打印一下每个rank的loss和梯度范数,如果差异特别大,八成是数据采样重叠了,试试把sampler的seed设成不同的值。
说实话你这个现象我踩过一模一样的坑,最后发现是DDP初始化时忘了设置torch.cuda.set_device(local_rank),导致每张卡都默认用0号卡做reduce,梯度全挤到一块儿去了。另外PyTorch 1.12的话,试试把init_method换成file://,用共享文件系统路径比tcp/env都稳,尤其是多机场景。还有个小细节,DistributedSampler记得在每个epoch开头调set_epoch(),不然每个卡拿到的数据顺序固定,loss差异会一直存在。你先检查下这几处,大概率能解决。
先确认下是不是没等所有rank的loss算完就backward了,DDP要求同步的。
你这八成是没设find_unused_parameters=True,BERT里有些层没参与梯度计算就会卡住。
我之前也踩过类似的坑,后来发现是没等所有进程的loss都计算完就直接backward了,DDP虽然会同步梯度,但要是某个卡提前返了或者数据不均匀,确实会出现这种各算各的情况。你试试在backward之前加个torch.distributed.barrier(),强制等齐所有rank,另外检查下是不是有BN层,DDP对BN的同步默认是关闭的,这也会影响收敛。还有一个小细节,PyTorch 1.12的DDP最好用find_unused_parameters=True,尤其是BERT这种有多层dropout的,说不定就是某个参数没参与梯度计算导致同步失败。
我之前也踩过这个坑,多半不是init_method的问题,你检查一下是不是忘了把model.to(device)放在构造DDP之前,而且每个进程的device要对应自己的rank,不然梯度虽然能reduce但会乱套。另外PyTorch 1.12对NCCL的版本比较敏感,V100上最好用NCCL_DEBUG=INFO跑一次看看到底卡在哪个环节。还有一个容易忽略的点:如果用了梯度累积,DDP默认只在step里同步一次,你得多卡下把accumulation_steps调大或者手动调no_sync上下文,不然loss曲线会特别诡异。最后确认下你的batch size是不是太小了,4卡全局batch不够大也会导致每个卡loss波动明显。
看到你这个现象我第一反应是gradient其实同步了,但你没验证对地方。loss不一样是很正常的,因为每张卡的batch样本不同,尤其BERT这种任务,不同卡的loss波动本来就大,真正要看的是所有卡平均后的收敛趋势,而不是单卡数值。你检查reduce没生效,用的是hook还是直接看梯度?建议在backward之后用dist.all_reduce手动测一下某层参数的梯度,对比一下和DDP内部处理后的值,这样能判断是通信没建立还是同步逻辑被覆盖了。另外init_method这两个写法本身没问题,但要注意rank和world_size有没有传对,尤其是用torchrun启动时环境变量可能和你手动设置的不一致。还有个容易忽略的坑,如果你在模型里用了batch norm或者自定义的layer,DDP默认只同步parameters和buffers,有些自定义buffer不同步也会导致梯度异常。再就是PyTorch 1.12的DDP有个已知问题,梯度压缩和混合精度混用时会出怪事,你如果开了amp,先关掉试试。最后建议把日志里每卡的loss和梯度norm都打出来,看是不是某一卡梯度特别小,那可能数据切分有重叠或者shuffle没生效。
看到你说loss每个卡都不一样,这其实是个比较典型的症状,先别急着怀疑init_method,那个只要rank和world_size对得上,tcp和env都能用。我怀疑你可能是忘了给模型输入也做“分片”之外的同步,比如batch norm的统计量,或者是在forward里用了类似dropout这种随机性很强的层,导致每张卡自己算的梯度本身就存在差异。另外,你确认过DDP的梯度归约是在loss.backward()之后自动触发的吗?有个常见坑是你在backward之前手动调用了no_sync()上下文,或者梯度累积逻辑写错了位置,导致reduce没跑。还有一个隐蔽点,如果你的模型里某些参数requires_grad=False,DDP默认不会同步这些buffer,但如果是自定义的buffer没注册好,也会出现这种“各自为政”的错觉。建议你直接打印每张卡的rank和loss,再对比一下torch.distributed.get_world_size()是否真的等于4,有时候多进程没起来,其实还是单卡在跑。最后,试试在backward之后加一句torch.distributed.barrier(),强制等所有卡算完再进下一轮,能排除时序问题。如果还不行,把DDP的find_unused_parameters设成True,虽然会慢点,但能排查是否有参数没参与梯度计算。
我之前跑DDP也踩过类似的坑,特别是PyTorch 1.12这个版本,很多隐性问题都是版本和CUDA组合带来的。你确认了DistributedSampler和broadcast,但梯度不同步很可能不是init_method的问题,而是你在构建模型后没有正确调用model = DDP(model, device_ids=[local_rank]),因为DDP内部会做hook,如果模型没被包进去,reduce操作自然不会生效。另外,检查一下你的训练循环里是不是用了梯度累积或者手动调用了all_reduce,如果多次backward但没有同步,就会出现每卡loss差很多的现象。还有个容易被忽略的点,就是数据加载时shuffle参数在DistributedSampler里要设为False,否则每个epoch的采样顺序会乱,导致数据分布不一致,梯度自然对不上。建议你先把world_size和rank打印出来,确认每个进程拿到正确的rank,然后在backward之后手动加一句torch.distributed.all_reduce(tensor)做一次验证,看看梯度数值是否一致。如果还是不行,试着把torch.backends.cudnn.benchmark设为False,有时候cuDNN的自动调优在分布式下会引入非确定性。最后,1.12对NCCL的默认后端有点老,可以考虑升级到2.0+或者显式指定gloo做对比测试,但V100上NCCL应该没问题,重点还是排查model wrapper和采样器。
我上周刚踩过类似的坑,你确认一下是不是每个进程的batch size没按总卡数缩,DDP默认梯度是all-reduce平均,如果每卡batch没调小,等效学习率会变,loss自然对不上。另外init_method用env://的话,得确保所有进程的MASTER_ADDR和MASTER_PORT环境变量都传对了,尤其是多机场景。还有个土办法,在backward之后手动打印一下每个rank的梯度norm,如果数值差很多,八成是模型里有batch norm或者dropout在作怪,BERT微调时某些层被冻结也会导致梯度分布不一致。
检查下是不是漏了torch.cuda.set_device,每个进程的rank得对应到卡上,不然init_method再对也白搭。
试试把find_unused_parameters设成True,BERT微调经常有层没参与反向,DDP默认会跳过梯度同步。
大概率是没插卡间通信的nccl,试试torch.distributed.init_process_group(backend='nccl'),顺手把torch.cuda.set_device设成local_rank。
这问题我也踩过坑,多半不是init_method的锅,先查一下DDP初始化的world_size和rank是不是从环境变量里正确读到了,有时候多卡脚本里写死rank=0就会导致梯度只在单卡上算。另外PyTorch 1.12有个老坑,如果模型里用了batch size太小或者某些op(比如LayerNorm)的梯度计算在DDP下会默认跳过同步,试试把find_unused_parameters=False显式设上。你确认一下每个卡上的loss是不是从step0就差很多,如果是,大概率是数据shuffle的seed没按rank区分,DistributedSampler得配合set_epoch用。我之前还遇到过CUDA缓存分配导致某个卡OOM假死,梯度一直没写入,最后靠torch.cuda.empty_cache()和调小batch size解决的。实在不行下个PyTorch 2.0,DDP这块稳定不少。
这个现象挺典型的,多半不是init_method的问题,你试试在构造DDP之前用torch.distributed.barrier()同步一下,然后确认一下rank和local_rank的映射有没有搞混,尤其是用torchrun启动时。另外,loss不一样可以先看下是不是学习率没按卡数线性缩放,BERT微调在4卡上梯度累加也容易出这问题。我之前遇到过类似情况,最后发现是DataLoader的num_workers在每张卡上各自为政导致的,但你这用了DistributedSampler应该还好。要不你打印一下每步的grad_norm看看是不是某个卡梯度特别小?
八成是DDP初始化时机有问题,试试把init_process_group放在模型包装前面。
我之前也踩过这个坑,先别急着怀疑init_method,你检查下DDP的构造时机,模型一定要在进程组初始化之后再用DDP包装,顺序反了梯度reduce会静默失效。另外,如果用了混合精度或者自定义梯度hook,得确认allreduce是在优化器step之前触发的。还有个比较隐蔽的点,DistributedSampler的shuffle参数要设True,但每个epoch要调用set_epoch,不然数据顺序错乱会让loss看起来不一致。你试试在backward之后打印一下每个rank的梯度norm,如果差异很大,大概率是模型里有BN或者LayerNorm在同步统计量,V100上多卡容易出这个。
我之前也踩过这个坑,四卡loss不一致大概率不是init_method的问题,而是每个卡的数据顺序没打乱彻底。你确认下DistributedSampler的shuffle参数是不是True,还有每个进程的batch_size是不是没除以卡数,导致有效batch变大梯度平均时数值对不上。
另外你检查过model的device_id和local_rank对应关系吗?DDP要求每张卡显式指定device,不能靠默认。如果用了model.cuda()再包DDP,梯度reduce时会因为上下文不对静默失败。可以贴一下完整的启动命令和DDP初始化代码,这样更直观。
还有个笨办法:在backward前手动打印每个rank的梯度norm,看是全程不一致还是只在某个层不一致。如果只在最后几层,可能是BN的running_mean没同步,但BERT一般没这问题。你试试把torch.distributed.barrier()加在optimizer.step()前,强制卡间同步一下,能临时排除异步问题。
我之前也踩过类似的坑,最后发现是忘了在训练循环里调用model.require_backward_grad_sync,导致梯度累积没触发同步。你确认下是不是这个开关被关掉了,或者试试把find_unused_parameters设成False,有时候参数没全用上也会让reduce卡住。另外,loss不一致的话,检查下每个卡的batch size是不是一样,DistributedSampler要配合drop_last=True用,不然最后一个batch大小不同会直接影响梯度统计。init_method那个报错其实不是关键,只要能连上就行,你可以先单机多卡用tcp试试,把端口换一个不冲突的。