最近在搞多卡训练,用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之前手动设置torch.cuda.set_device,我之前就是漏了这个导致梯度不同步。
试试把torch.distributed.barrier()插在loss.backward()前面,我遇到过类似问题这么搞好的。
我之前也踩过这个坑,排查到最后发现是DDP的find_unused_parameters没设成True,导致某些层因为梯度未参与同步被忽略了。另外建议检查一下batch_size是不是被隐式改小了,DDP里每个卡拿到的只是总batch的一部分,学习率得跟着调。你用的PyTorch 1.12挺稳的,大概率不是版本问题,先试试把init_method换成file:///sharedfile这种基于共享文件的方式,有时会绕过一些通信组初始化bug。
这个问题我也踩过类似的坑,看起来像是DDP的进程组初始化其实没真正跑通。你检查下是不是每个进程的rank和world_size传对了,特别是torchrun或者mp.spawn启动时,环境变量里LOCAL_RANK和RANK容易搞混。另外PyTorch 1.12的DDP有个隐藏雷,如果你的模型里用了BatchNorm,默认开启的同步BN需要额外调用convert_sync_batchnorm,否则每张卡的BN统计量还是独立算的,loss自然对不上。还有个细节是梯度reduce操作依赖backend的NCCL版本,CUDA 11.6配的NCCL可能有老bug,建议试试把环境变量NCCL_IB_DISABLE=1或者NCCL_SOCKET_IFNAME设成你的网卡名,强制走TCP兜底看看。如果这样还不行,你在训练循环里手动print一下每个step的model模块的gradients,如果全是None那就是反向传播根本没连上DDP的hook,得检查下是否不小心把模型参数设成了requires_grad=False。最后建议直接升到PyTorch 2.0以上版本,分布式这块修复了不少问题。
我最近也踩过类似的坑,DDP梯度不同步的问题往往不是init_method的锅,更可能是模型里某些层没有正确包裹进DDP。比如你用了自定义的loss计算或者梯度累积逻辑,这些地方如果没用no_sync或者手动调用了all_reduce,很容易出现你描述的情况——每个卡各自为政。建议先检查一下模型forward里有没有对中间变量做in-place操作,或者hook是不是被意外覆盖了,这会导致DDP的梯度同步钩子失效。另外,PyTorch 1.12有个已知问题,当模型包含某些自定义C++扩展时,DDP的梯度桶分配可能会出错,可以试着手动设置bucket_cap_mb参数小一点,比如2MB。还有个偏方:把torch.distributed.barrier()放在每个epoch开始和结束的地方,强制同步一下进程状态,有时候能暴露是哪个rank卡住了。对了,你确认过torch.distributed.get_world_size()返回的是4吗?有些时候进程没拉起来,实际只有单卡在跑。
试试在启动脚本里加上--master_port=随机端口,有时候端口冲突会导致初始化失败,我遇到过类似问题。
我最近也踩过这个坑,排查了一圈发现是dataloader的num_workers没设对,导致每个进程拿到的数据顺序虽然对但batch内部的padding方式不一致,梯度自然就对不上了。你试过把batch_size调成全局一致的数值再跑一下吗?或者检查下模型里有没有用torch.where这类操作,它们有时候会破坏同步逻辑。另外确认下是不是所有rank都调用了model.parameters()的梯度更新,有时候某个层被冻结了也会出这种问题。
遇到过类似的问题,感觉可能不是init_method的锅,更像是DDP包装后模型内部的BN层或者自定义梯度操作没处理好。你可以检查下模型里有没有手动调loss.backward()之后又额外做了梯度裁剪之类的事,这些操作在DDP下容易破坏同步。另外试试把torch.distributed.barrier()插在关键步骤前后,强制对齐一下进程状态,有时候能发现隐藏的时序问题。
你检查下DDP的rank和world_size是不是对的,之前我也遇到过类似问题,最后发现是torchrun启动时参数没传对。
检查下每个进程的master_addr和rank对不对,环境变量没配好经常导致reduce失效。
这个我去年也被坑过,问题大概率不是init_method,而是你在构造DDP模型之后有没有额外修改过模型的任何参数?比如手动调了某个buffer或者加了自定义的hook,这些操作很容易破坏DDP的梯度同步机制。另外你确认一下每个进程的torch.distributed.get_world_size()返回的是4吗?我猜你可能是分布式初始化成功了但实际只有单进程在跑,因为有的同学会忘了在启动命令里加--nproc_per_node=4。还有一个常见的暗坑是用了torch.no_grad()的上下文去跑backward,或者模型里有某些操作(比如BatchNorm的track_running_stats)在DDP下默认行为会变,导致不同卡的统计量不一致。梯度reduce不生效的话可以试着手动打个断点,看每个rank的梯度norm是不是一样的,如果连这个都不一致那基本就是数据切片或者模型本身写死了某个参数没经过DDP包裹。你用的PyTorch 1.12的话,建议试试把torch.distributed.barrier()插在每个epoch开始前,顺便检查一下nccl后端是不是正确加载了,有时候是torch.distributed.init_process_group里的backend没显式指定成nccl,默认用gloo在V100上反而会出诡异问题。
你这情况我去年也遇到过,检查一下是不是用了torch.nn.DataParallel和DDP混着来,或者模型里某些自定义层没注册到parameter list里。另外可以试试在backward之前手动打印一下梯度,看看是不是某个rank的梯度全是0,有时候batch size设太小也会导致统计不稳定。
我也遇到过类似情况,查了一圈发现是DDP的bucket划分导致梯度同步滞后。你试试设一下bucket_cap_mb参数,比如调小到25,强制更频繁的allreduce,有时能缓解loss不一致的问题。另外确认下是不是有自定义loss函数里做了奇怪的reduction,比如手动sum但没考虑卡数,梯度就容易各自为政。
检查下每个进程的torch.distributed.get_rank是不是对的,之前我也被这个坑过。
遇到过类似的问题,当时排查下来发现是模型forward里用了未包装的自定义op,导致DDP的hook压根没触发梯度同步。你可以检查下模型里有没有自己写的cuda扩展或者特殊操作,必要时得手动调all_reduce。另外init_method用env://其实没问题,但得确保每个进程的RANK和WORLD_SIZE环境变量都设对了,有时候是启动脚本里漏了这些参数。
我最近也踩过类似的坑,最后发现是没在model后面手动调用torch.cuda.sync()导致的,虽然DDP理论上会自动同步,但某些自定义loss计算里操作顺序不对就会让梯度卡住。你试试在每个batch的loss.backward()之前加一句torch.distributed.barrier()强制同步一下,或者检查下是不是模型里有些BN层参数没被正确广播。另外PyTorch 1.12的DDP对梯度累积步数比较敏感,如果用了gradient accumulation要注意把no_sync配合好。
说实话,看到你这个loss不降、梯度各自为政的情况,我第一反应可能是你忘了在训练循环里调用model.reduce()或者根本没有正确启用DDP的梯度同步机制。虽然DDP默认会在backward之后自动做all-reduce,但如果你用了自定义的梯度累积或者手动调了loss.backward()之前的钩子,很容易把同步流程打乱。另外,你提到reduce操作没生效,我建议先确认一下是不是用了torch.no_grad()或者把某些参数设成了requires_grad=False,这会导致那些参数的梯度不被同步。还有一点,init_method虽然常见,但有时候问题出在环境变量没配全,比如RANK、WORLD_SIZE这些没设对,DDP启动时根本不知道自己是哪张卡。我之前踩过类似的坑,最后发现是torchrun启动时MASTER_ADDR漏写了,导致多卡之间通信失败,你可以试一下用torchrun --nproc_per_node=4你的脚本.sh,省去手动配init_method的麻烦。如果还不行,检查一下每个卡的loss是否真的独立,可以打印一下rank和loss值,看看是不是某个卡压根没参与梯度同步。
这问题我蹲了一周才搞定,跟你的情况几乎一模一样。单卡正常,多卡loss各跑各的,最后发现是DDP包装的时机不对——得在模型加载到cuda之后再wrap,否则参数虽然broadcast了,但梯度计算图的device不统一,reduce操作就形同虚设。另外建议你检查一下是否在forward里手动调用了model.module,这会导致DDP的hook被绕过,梯度直接原地踏步。init_method报错的话,可以试试设环境变量MASTER_ADDR和MASTER_PORT,不用显式传参,PyTorch 1.12有些版本对tcp://解析有bug。还有一个容易忽略的点:如果你的数据预处理里用了随机性操作(比如dropout或者数据增强),要确保每个进程的seed是独立且固定的,否则即使sampler对了,样本顺序不同也会让梯度看起来不同步。你loss降得慢很可能就是因为梯度根本没聚合,每个卡都在原地优化自己那点数据,相当于缩小了batch size。建议先跑个最简单的allreduce测试脚本,排除模型和代码逻辑的问题。
试试把find_unused_parameters设成True,BERT里有些层可能没参与反传导致梯度同步失败。
我之前也踩过类似的坑,你检查一下是不是DDP的model被包在torch.nn.parallel里之后,又手动调用了backward两次或者用了accumulate_grad,这样梯度会累加但不同步,最好把loss也做一下all_reduce确认。另外试试把init_method直接换成file://路径,tcp有时在多机多卡环境下会抽风,env://的话要确保每张卡的RANK和WORLD_SIZE都传对了,不然还是各算各的。还有个小细节,PyTorch 1.12的DDP在CUDA 11.6下偶尔会跟NCCL版本打架,你可以试试把NCCL_P2P_DISABLE=1设上,我这边这样设置后梯度就正常了。