最近在搞多卡训练,用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初始化后没正确调用model = DDP(model, device_ids=[local_rank]),而且每个进程的rank和device要严格对应,用env://的话得手动设好RANK和LOCAL_RANK。另外你确认下是不是忘了在训练循环外面套torch.cuda.synchronize()?有时候梯度不同步是因为backward和step之间没做同步,导致reduce操作被异步执行了。还有个排查办法:打印一下每个rank上loss的数值,如果差异越来越大,八成是batch的shuffle没按sampler来,或者模型里有batch norm没处理。
看到你说loss每个卡不一样,我第一反应是你可能没在训练循环里手动调model.no_sync()或者梯度累积相关的逻辑,但既然你说了reduce没生效,那问题多半出在进程组初始化上。PyTorch 1.12那个版本有个坑,env://需要确保每张卡的RANK、LOCAL_RANK和WORLD_SIZE环境变量都正确传递,尤其是用torchrun启动时,如果脚本里手动设了MASTER_ADDR和MASTER_PORT,但没同步到所有子进程,就会出现这种“看起来初始化成功但实际通信没建立”的诡异现象。另外你试tcp://localhost:12355时,如果4个进程都绑定同一个端口,后启动的进程会连接失败,但DDP有时候不会直接报错,而是静默地退化成单卡模式,这正好能解释你观察到的loss不降和梯度独立。建议你直接打印一下dist.get_rank()和dist.get_world_size(),确认每个进程都拿到了正确的rank,然后看看torch.distributed.barrier()能不能正常同步——如果卡住,那就是网络通信层的问题,检查一下V100的NCCL设置,比如NCCL_DEBUG=INFO跑一轮,看有没有报链路错误。我之前遇到过类似情况,最后发现是init_method里的hostname写成了localhost,但多卡机器上应该用实际IP,否则跨卡通信会走到环回接口上,数据包根本出不去。还有个偏方,你试试把dist.init_process_group里的backend='nccl'换成backend='gloo'先跑一次,如果正常,至少能确定是NCCL和驱动/网络栈的兼容性问题,再回头升级驱动或关掉NCCL_P2P_DISABLE=1试试。
我之前也踩过这个坑,排查下来大概率是DDP的bucket通信没生效,而不是init_method的问题。你试试在构造DDP之前加一句torch.distributed.barrier(),然后确认一下每个进程的rank对应GPU序号是不是对的,V100上经常是环境变量没设好。另外,loss不一致的话,建议把batch size调小点先验证梯度是否真的同步了,有时候是数据增强的随机性导致每个卡看到的样本差异太大。如果还不行,检查下是不是在模型forward里用了dropout但没设seed,这也会让每卡loss漂移。
碰到过类似的情况,当时折腾了我一整个周末。你确认DistributedSampler和broadcast都没问题的话,我建议先看一眼DDP初始化之后,模型是不是真的被包进去了,有时候在DDP外面又套了别的模块,梯度归约会被跳过。另外,PyTorch 1.12这个版本我记得对NCCL的allreduce有个已知的坑,尤其多机多卡时容易静默失败,你试试把torch.distributed.all_reduce单独拉出来跑一下,看能不能正常同步一个张量,能快速定位是框架问题还是你代码里梯度挂钩的问题。还有个隐蔽点,如果loss是每个卡各自算的,但你没把loss也做allreduce,那就算梯度同步了,打印出来的loss也是各算各的,看起来像没同步,实际不影响训练,但你会误判。我当时最后发现是dataloader的num_workers设成0导致每个卡拿到的batch顺序其实不一样,虽然用了sampler但shuffle逻辑被重复打乱了,改成4之后就好了。你可以先试试把NCCL的debug日志打开,设NCCL_DEBUG=INFO跑几步,看reduce有没有真正触发,日志里会显示每个rank的梯度哈希值,一眼就能看出来是不是真的同步了。
这问题我之前也踩过,大概率不是init_method的锅。你试下在构造DDP前手动调用torch.distributed.barrier(),然后确认一下backend是不是设的nccl,glibc或者gloo在多卡上容易出这种诡异问题。还有个小细节,DistributedSampler的shuffle参数最好显式设成True,不然每轮epoch数据顺序不一致,梯度算出来本来就是歪的。如果这些都不行,检查下是不是有哪个卡上的数据没被加载完,比如dataset长度没对齐,这也会导致梯度reduce时形状不匹配但又不报错。
遇到这个情况先别急着怀疑init_method,tcp和env都试过了基本可以排除。你确认一下是不是在构造DDP之前就把模型放到cuda上了,这个顺序错的话会导致每个rank的初始权重其实根本没同步,虽然broadcast了但可能broadcast的是另一个没包装的model实例。另外你说reduce没生效,有没有想过是梯度没被计算到正确device上?比如某些参数还留在CPU上,DDP只会同步那些参与autograd的cuda参数。还有个比较隐蔽的坑是如果你用了混合精度或者自定义的gradient hook,容易把DDP内部的allreduce给覆盖掉。我建议你在每个rank上打印一下当前进程的rank和loss,看看是不是数据本身有重复或缺失,DistributedSampler虽然设了但shuffle和seed没对齐也会导致每个卡看到的样本分布差异很大。另外PyTorch 1.12的DDP有个已知问题就是如果模型里有未参与forward的buffer,可能会导致梯度同步异常,你可以在构造DDP时设置find_unused_parameters=True试试,虽然会慢一点但能排查是不是这个原因。最后检查一下你的dataloader是不是在每次迭代时都调用了set_epoch,不然每个epoch的shuffle顺序在四个rank上会完全一样,导致loss曲线看起来“各走各的”。
我之前跑DDP也踩过类似的坑,尤其是你提到“reduce没生效”这个点,其实大概率不是init_method的问题,而是卡在梯度同步的时机上。DDP默认是每个step结束自动做allreduce,但如果你在backward之前手动改了梯度或者用了梯度累积,就很容易把同步打乱,导致每个卡各算各的。建议你先确认一下是不是在optimizer.step()之前调用了model.zero_grad(),或者有没有对loss做了奇怪的操作,比如手动除以卡数但又忘了在allreduce里做补偿。另外,PyTorch 1.12的DDP有个坑,如果模型里用了BatchNorm,并且开了sync_bn,但实际没正确初始化进程组,也会出现梯度像没同步一样。你可以试一下在构造DDP前单独打印一下每个rank的梯度范数,看看是不是完全不一致,如果一致但loss还是不同,那可能是数据采样顺序差异太大,虽然用了DistributedSampler但shuffle的seed没固定,导致每个卡看到的batch分布差异明显。还有个小细节,tcp://localhost:12355这种写法在4卡上其实没问题,但你得确保所有进程都用同一个地址和端口,并且没有别的进程占用了这个端口,建议用env://配合torchrun会更省心。最后如果实在排查不出来,可以试试把梯度同步的调试开关打开,设环境变量TORCH_DISTRIBUTED_DEBUG=DETAIL,它会直接告诉你哪个参数没同步,比瞎猜快多了。
试下把nccl换成gloo跑一版看看,如果梯度能同步那就是通信后端的问题,之前我遇到过类似情况是nccl版本和cuda版本不匹配导致的。另外你检查过每张卡的实际batch size吗,如果数据没均匀分配,loss差异大很正常,DistributedSampler只能保证样本不重叠,不能保证每个step都平衡。还有个小坑,记得在backward之后确认一下梯度的gradient是否真的在所有rank上都非空,有时候模型里某些层因为padding或者mask导致梯度为0,DDP会跳过同步。
试试把find_unused_parameters设成True,BERT有些层可能没参与反向传播导致梯度同步被跳过。
八成是DDP初始化时world_size和rank没跟实际进程数对上,试试torchrun启动并打印rank确认下。
试试把find_unused_parameters=True加上,BERT有些层可能没参与反向传播导致同步跳过。
我之前也踩过这个坑,检查一下是不是DDP包装之后又把模型参数手动改回去了,或者有哪个forward里有dropout但没同步seed。另外gradient accumulation如果没配合no_sync的话,梯度累积次数不同步也会导致你看到的现象。还有个小细节,确认一下是不是用了find_unused_parameters=True但某些层确实没参与计算,这会影响梯度归约的。建议先单卡多step打印梯度对比下,再逐卡排查。
我之前也踩过这个坑,你检查下是不是DDP包装前就把模型放到cuda上了,或者optimizer是在包装前创建的,这两个都会导致梯度不同步。另外PyTorch 1.12的DDP对梯度归约是异步的,你可以试试在backward后手动调一下torch.distributed.all_reduce看看能不能对上数,先定位是通信没建立还是归约逻辑错了。还有个小细节,DistributedSampler得在每次epoch开头调用set_epoch,不然每个卡拿到的数据顺序一样,loss也会不一致。
试试在train_step里加一句torch.distributed.barrier(),等所有卡都跑到这再算梯度,我之前就是这么解决的。
大概率是初始化那步没走对,试试在代码最前面加torch.distributed.init_process_group(backend='nccl')。
你这个情况我之前也踩过,init_method其实不太影响reduce,重点检查一下是不是每个进程的rank和world_size对上了,尤其是用torchrun启动时环境变量有没有正确传递。另外,loss不一样很可能是数据没打乱彻底,DistributedSampler要配合shuffle=True,还得确认每个卡拿到的batch确实不同。如果方便的话,可以在backward后手动打印一下每个rank的梯度norm,看看是不是真的没同步,有时候是梯度累积步数不一致导致的。
我之前也踩过这个坑,排查到最后发现是DDP的bucket大小没调好,默认25MB对BERT这种大模型可能太小,把bucket_cap_mb设成200试试,梯度同步频率会明显改善。
另外你用的init_method没问题,但记得要在每个进程里正确设置rank和world_size,特别是用torchrun启动时容易漏掉环境变量。还有个小细节,检查一下是不是模型里的BN层在作怪,DDP对BN的同步支持有限,换成SyncBatchNorm可能会好。
如果还不行,可以在backward之后手动调一下torch.distributed.all_reduce,至少能确认通信是否真的通了。我后来就是靠这个方法定位到是代码里某个模块没包进DDP。
八成是DDP初始化时world_rank没对应上,试试打印下每卡rank和loss对齐看看。
我之前也踩过类似的坑,你检查一下是不是DDP的初始化放在了模型构建之后?init_method其实只要保证所有进程拿到的是同一个地址就行,但更关键的是看每个进程的rank是不是真的从0到3,可以用torch.distributed.get_rank()打出来验证下。另外你用的是torchrun还是手动spawn?如果是手动spawn的话,world_size很容易漏传,导致每个进程都以为自己是独立的世界。还有个细节,确认下你的模型是不是在DDP包装之后才往cuda上放的,顺序反了梯度归并也会出问题。
试试把find_unused_parameters设成True,BERT里有些层可能没参与反向传播导致梯度同步跳过。