最近在搞多卡训练,用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跑起来loss各卡不一致,大概率不是init_method的锅,先检查一下是不是代码里手动调了loss.backward()但忘了用model.no_sync()包住,或者optimizer.step()前没做梯度裁剪。另外PyTorch 1.12有个老问题,如果模型里有未参与forward的buffer,DDP的梯度同步会静默失效,你试试把reduce和bucket_size调小一点,或者升级到1.13+看看。我之前就是加了句torch.distributed.barrier()在step之后,莫名其妙就好了,你可以先试下这个土办法。
看到这个我太有共鸣了,之前调DDP的时候也被这种“看似都做了但就是不对”的状态折磨过。你确认了DistributedSampler和broadcast,但梯度不同步,我怀疑问题不一定在init_method,反而可能在进程组的world_size和rank设置上,尤其是如果用了torchrun或者手动spawn,环境变量不一致很容易出现这种“假同步”现象。还有一个特别容易被忽略的点,就是模型里如果有batch size相关的操作或者自定义的loss函数里用了全局统计量,比如BN或者跨卡归约,这会导致每张卡的梯度本身就存在数值差异,即使同步了也会表现为loss不收敛。另外PyTorch 1.12的DDP有个已知坑,就是如果没用find_unused_parameters=True,而模型里有参数没参与梯度计算,reduce会静默失败,但不会报错,建议你检查一下BERT微调时是不是有冻结层或者某些参数被设了requires_grad=False。还有个更直接的排查方法,你可以在每个step后手动打印一下rank0和rank1的梯度范数,如果完全一样,那说明同步没生效,问题在初始化;如果不一样但接近,那可能是loss本身的计算方式有隐患。我上次就是因为dataloader的num_workers设太大,导致不同卡读到的数据顺序错位,虽然用了sampler但shuffle逻辑被覆盖了,最后加了set_epoch才解决,你也可以看看这个。
看到你这个情况我第一反应是DDP的梯度同步其实是在backward的时候自动触发的,理论上只要模型是用DDP包装的就不该出现“各自为政”的现象。你检查一下是不是所有进程都执行了同一个optimizer.step(),如果某个rank的loss没反传或者被detach了,梯度allreduce就会卡在那一步,而且loss差异会一直累积。另外init_method用env://的话得确保每个进程的RANK和WORLD_SIZE都从环境变量里读到了,你可以在代码里print一下这些值确认,有时候是启动命令里漏传了参数导致进程间根本没建立通信组。还有个细节,PyTorch 1.12的DDP对CUDA缓存分配器有点敏感,如果每张卡的显存占用差太多,可能会触发奇怪的延迟同步,你可以试试给每个进程设不同的CUDA_VISIBLE_DEVICES顺序。最后我建议你开启torch.distributed.barrier()在每个epoch开始后强制同步一下,再观察loss曲线,如果还是差得远,就检查一下数据集的shuffle种子是不是没按rank设置好,DistributedSampler的seed参数在1.12里有个已知的坑,它默认用当前时间戳,多卡下会生成不同划分。我之前也踩过类似的坑,最后是把sampler的seed固定成全局的,问题就解决了。
我之前也踩过这个坑,重点不是init_method,而是看你DDP的初始化到底在做什么。你确认一下是不是每个进程的rank和world_size都设对了,还有torch.cuda.set_device(local_rank)有没有在construct DDP之前调用,这个顺序错了梯度reduce会直接失效。另外PyTorch 1.12配CUDA 11.6的话,建议直接升到1.13或2.0,老版本DDP在多卡通信上有一些已知bug,尤其是NCCL版本不匹配时。还有个笨办法,你可以把torch.distributed.barrier()加在每次step之后,强制同步一下,看看loss是否变一致,能帮你定位是通信问题还是数据采样问题。
检查下是不是漏了torch.distributed.barrier(),还有梯度累积的话得手动同步。
看到你这个情况,我第一反应是检查一下是不是忘了在optimizer.step()之前调用loss.backward()之后做梯度规约,但你说reduce没生效,那就要看DDP构造时有没有把find_unused_parameters设成True,BERT里有些层在特定输入下可能真的会闲置,这会导致梯度同步被跳过。另外,你确认一下每个进程的world_rank是不是真的不一样,我之前遇到过用env://但环境变量没传到子进程的情况,所有卡都成了rank 0,那梯度自然是各算各的。还有个小细节,DistributedSampler的shuffle参数要配合每个epoch调用set_epoch,不然数据顺序会重复,但这不影响梯度同步,只是loss曲线会怪。你试试在DDP初始化后打印一下每个rank的device和model.device,看看是不是有卡被分到了CPU上。如果实在排查不出来,可以先降级到torch 1.13或者2.0,1.12的DDP在NCCL版本上有些已知的坑,尤其是和CUDA 11.6组合的时候。另外,你用tcp://localhost:12355的话,要确保四个进程都用了同一个端口,而且init_method里别带空格,我上次就是被一个看不见的缩进坑了。
看到你说loss每个卡都不一样,我第一反应是怀疑你check梯度的时候是不是在backward之前打印的,那会儿梯度本来就没同步完。DDP的梯度同步是挂在backward的hook里的,得等loss.backward()执行完再观察,不然看到的就是各卡自己的梯度。另外init_method那块,tcp和env都试过不行的话,问题可能不在通信初始化,你确认一下world_size和rank传对没,尤其是用torchrun启动时,环境变量会自动设好,但你手动传参的话容易漏。
我上个月也踩过类似的坑,最后发现是模型里的batch size没除以卡数,导致每张卡算出的梯度量级不一样,虽然DDP会做all-reduce平均,但如果你的loss本身就没按总batch归一化,那同步完的梯度依然不对劲。你微调BERT的话,可以试试把梯度打印出来看下数值分布,如果各卡梯度的norm差很多,那大概率是数据分布不均,DistributedSampler虽然能切分,但如果你用了shuffle=False或者没设drop_last,最后一批数据可能不齐。
还有个冷门的点,PyTorch 1.12有个已知问题,如果你在模型里用了某些自定义的autograd.Function,且没有正确标记为同步,DDP的梯度钩子会漏掉它。你可以先用torch.distributed.all_reduce手动对某个参数的梯度做一次操作,如果手动能同步,那说明DDP的自动reduce没覆盖到所有参数,这时候检查下是否有buffer或者未包装的Parameter混在模型里。最后实在不行,开一下NCCL的debug日志,看下是不是通信超时或者拓扑识别问题,V100老卡有时会有PCIe switch的坑。
看到你这个现象我第一反应是检查一下你是不是在forward之后手动调用了backward,但DDP的梯度同步其实是在loss.backward()的allreduce钩子里自动触发的,如果你用了自定义的训练循环,比如自己调用了reduce或者梯度裁剪之类的操作,很容易把DDP内部的梯度同步状态打乱。另外PyTorch 1.12的DDP对CUDA的版本和NCCL后端特别敏感,你CUDA 11.6配NCCL的话,可以试试把环境变量NCCL_DEBUG=INFO跑一次,看看每个rank是不是真的建立了通信,有时候init_method看着没问题但实际多进程没走对,比如torch.multiprocessing.spawn的启动方式不对也会导致各卡各自为战。
还有个细节你可能忽略了,就是DDP要求模型参数在构造后、broadcast之前不能有任何未同步的buffer,特别是BERT里那些running_mean之类的BN统计量,虽然你说broadcast过,但如果你用的是预训练模型加载权重,有些buffer是标记为persistent=False的,这些不会参与broadcast,在DDP里会造成梯度计算图不一致。我建议你直接在backward之前打印一下每个rank的loss值,如果差异很大而且随步数波动不收敛,大概率是数据sampler没生效——比如你在每个进程里都用了同一个dataset实例但没按rank切分,或者shuffle=True但没设seed,导致每张卡看到的是完全不同的数据分布。
试着把DistributedSampler的num_replicas和rank参数显式传进去,别依赖默认值,然后确认一下你的dataloader是不是每个epoch都调用了sampler.set_epoch(),不然多卡训练时每个rank拿到的样本顺序完全错位,梯度自然对不上。如果这些都排查了还不行,我建议你降级到PyTorch 1.10试试,1.12那个版本DDP在NCCL的allreduce上确实有已知的bug,特别是和CUDA 11.6混用的时候,很多人在GitHub上提过类似的现象,换版本比调参快多了。
看到你说loss不一致而且降得慢,第一反应是怀疑你确认“reduce生效”的方式可能不太对——DDP的梯度同步是在backward()里自动触发的,如果你手动打印每个rank的梯度,在allreduce完成前抓取,确实会看到各自为政的假象。建议在backward后面加个torch.distributed.barrier()再对比参数梯度,或者直接看每步更新后的权重是否一致。
另外PyTorch 1.12有个已知坑,就是DDP和CUDA 11.6的组合下,如果没设torch.cuda.set_device(local_rank),容易导致每个进程都默认跑到卡0上,虽然你用了DistributedSampler但数据还是会撞车,loss自然乱。你可以打印一下每个rank的device和当前样本索引验证一下。
init_method用env://的话,记得要显式设置MASTER_ADDR和MASTER_PORT环境变量,如果是在普通脚本里手动launch而不是torchrun,很容易漏这一步。tcp://写法本身没问题,但端口冲突或防火墙也可能导致通信初始化不完整,但不报错只表现为同步异常。
还有一个高频坑是模型里如果有BatchNorm,DDP默认会同步BN的统计量,但你如果用了自定义的forward或者冻结了某些层,可能导致同步被跳过。建议先换一个不带BN的简单模型跑通多卡,排除模型结构影响。
如果上述都排除了,试试把梯度检查放在step之后,看optimizer更新后的参数是否四卡一致,不一致就说明reduce确实没挂上。实在不行可以降级到DDP的“find_unused_parameters=True”试试,虽然慢但能帮助定位是不是有参数没参与loss计算导致同步跳过。
我之前也踩过类似的坑,你那loss不一致其实挺典型的,先别急着怀疑init_method。建议你直接在DDP初始化后打印一下rank和world_size,确认四张卡是不是真的都进到同一个进程组里了,有时候是mpirun或者torchrun的启动方式不对导致通信没建立起来。另外你检查一下每个卡上的batch size是不是一样,如果数据没打乱或者采样器没设shuffle=True,各卡拿到的子集分布差异大也会让loss看起来像“没同步”,梯度其实同步了但数值本身就飘。还有个小细节,PyTorch 1.12记得用torch.cuda.set_device(rank)把当前进程绑到对应卡,不然默认全挤在0号卡上,通信拓扑就是乱的。如果这些都排除了,试试把reduce操作显式写出来,比如在backward之后手动打印一下每个参数的grad_norm,看是不是完全一样,这样能定位是梯度没归约还是模型更新逻辑出了问题。
我之前也踩过这个坑,loss各跑各的基本就是DDP没真正生效。你检查下是不是在model = DDP(model)之前就跑了forward,或者optimizer在DDP包装前就创建了?还有种可能是find_unused_parameters没设对,BERT这种有pooler层容易漏参数,加上这个参数试试。另外确认下每张卡的batch size是不是被均分了,不然loss对不上也正常。
会不会是忘了用DistributedSampler的同时把shuffle设成False?另外你确认下是不是每张卡都跑了独立的optimizer.step(),如果只在rank 0更新参数那肯定不同步。还有个坑是PyTorch 1.12里如果用了find_unused_parameters=True但有些层确实没参与loss,梯度reduce也会出问题。建议先加个torch.distributed.barrier()在backward前看看是不是所有卡都正常进入训练循环了。
查下是不是忘了在模型外面包DDP,或者forward里把loss算重了。
DDP梯度不同步,先看看你是不是把模型包在DDP里之前就做了forward?如果先forward再包DDP,那第一次迭代的梯度确实不会同步,而且容易忽视。还有个坑是4卡时batch size没跟着调,loss下降慢可能是学习率相对变小了,各卡loss不同倒不一定是梯度问题,可能是BN层或者dropout的随机性。另外PyTorch 1.12里如果用了find_unused_parameters=True,某些参数不参与loss计算时梯度确实不会reduce,这个也得确认下。
梯度不同步八成是没包model再DDP或者find_unused_parameters没开,先print下各卡梯度范数确认下。