拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DreamDDP:按层解耦的局部同步如何优化反向传播通信

DreamDDP:按层解耦的局部同步如何优化反向传播通信

把Local SGD的整模型同步拆进反向传播:DreamDDP到底解决什么问题

MLSys的论文里,分布式训练方向的题目总是字短事大。看到“DreamDDP:按层解耦的部分同步,把Local SGD的整模型同步拆进反向传播”这个题目,你大概能嗅到两个关键词的分量:Local SGD 和 反向传播。前者代表了分布式训练里“降低通信频率”这条经典路线,后者则是每个搞深度学习的人每天都要打交道的核心算法。把这两者结合起来,等于在说:我们不再让整模型像一块铁板那样同步了,而是让每一层按照自己的节奏,在反向传播过程中逐步完成通信。这篇博客,我围绕这个方案从原理到落地拆开聊,谈谈它解决了什么痛点、核心环节怎么设计,以及你自己动手复现时最容易踩的坑。

文章主要面向两类读者:一是想理解分布式训练前沿做法的研究者,二是需要在生产集群上压低通信开销、又怕Local SGD收敛不稳的工程团队。前者可以重点关注思路设计与实验对照,后者可以直接跳到参数配置和排查技巧部分。为了保证表述不过度超出公开资料范围,涉及具体实现细节的地方,我会基于分布式训练的通用工程实践做合理补充,并明确标注哪些是推断内容,方便你对照自己的场景做取舍。

1. 内容整体设计与思路拆解

1.1 背景痛点:AllReduce通信才是分布式训练的真正瓶颈

先别急着聊DreamDDP,我们需要对齐一个共识:在数据并行训练里,计算不是瓶颈,通信才是。以经典的AllReduce同步为例,每个GPU卡各自前向反向算出一份梯度后,需要把全卡的梯度做全局求和再广播回去。假设你有64张卡,模型是10亿参数,每个参数4字节,那么单次AllReduce的通信量至少是 64 × 10^9 × 4 × 2 ≈ 512GB级别(这里×2是因为求和与广播各有一次数据移动)。这个数字意味着什么?即使跑在25GB/s的NVLink或200Gb/s的InfiniBand上,一次同步也得好几秒。而一个step的前反向计算,对小模型可能只要几十毫秒。算力在等通信,GPU的空闲率肉眼可见地飙升。

传统DDP(Distributed Data Parallel)解决这个问题的方法是“通信计算重叠”:把梯度按bucket切分,反向传播算完一个bucket就立刻对这个bucket发起AllReduce,不等整层全部算完。重叠之后,通信时间被“藏”在计算背后,理论上能做到近零额外开销。听起来很完美对吧?但有个前置条件——你的集群必须有能力把通信带宽吃满。跨机场景下,网卡带宽往往只有卡间互联的几分之一,一旦梯度量超过带宽上限,重叠也救不了你,通信还是会周期性拖慢训练。

于是大家开始想另一条路:能不能少通信几次?

1.2 Local SGD为什么省通信、却带来一致性悬崖

Low-frequency SGD,也叫Local SGD,核心思想朴素得让人怀疑是不是太简单:每个worker先在本地连续迭代K步(K是同步间隔,也叫通信周期),攒够K步梯度后,才做一次全局同步。K=1时退化成普通AllReduce DDP;K越大,通信次数越少,通信开销近似降到1/K。听起来是个白捡的加速方案,但实际问题很快浮出水面。

本地迭代意味着每个worker的模型参数会沿着自己的优化轨迹漂移,K步之后,不同worker之间的模型差异已经不是同一层级的小扰动,而是可能相差很远的解空间位置。这时候做一次全局平均,相当于把一群已经走散的人强行拉到几何中心。物理上可以计算,但优化效果很讽刺:loss会被瞬间拉高,产生“一致性悬崖”(consistency cliff)。梯度统计上,这等价于用一个偏差很大的平均梯度去替代本地的真实梯度,momentum和二阶统计量全部失准。尤其当K偏大(比如32或更大)、batch size偏大时,模型发散的风险迅速上升。

研究者做过大量实验,Local SGD在凸问题和部分非凸问题上被证明可以收敛,但收敛速率里的误差项会随着K和模型异质性增加而放大。落到业务上,你会发现训是训得动,但精度往往比不过普通DDP,尤其在图像分类、大规模语言模型这类高维非凸问题上。这就是Local SGD的尴尬:通信确实省了,但优化质量赔进去了。

我自己的理解是:Local SGD把“通信频率”这个单一变量当成了全部,忽略了模型本身是分层的、不同层对同步延迟的耐受度完全不同。底层卷积层或embedding层梯度变化剧烈,差一步就偏离很远;高层分类头或输出层梯度相对平缓,晚几步同步问题不大。把所有层绑定在同一个同步节奏里,是在用整模型的下限换取通信收益。DreamDDP想做的,就是把这种绑定关系拆掉。

1.3 按层解耦的设计哲学:把“全同步”拆成“随反向传播逐层同步”

DreamDDP的思路,我概括成一句话:让每一层独立决定自己何时同步,并在反向传播的天然时序中完成这些同步。这样做有三个直接好处。

首先,不同层可以根据自己的梯度变化率选择不同步调,从而把通信频次从“整模型一个值”细化为“每层一个值”。梯度变化剧烈的层可以保持K=1,几乎每次反向传播都同步;梯度平缓的深层可以K=16甚至更大,几乎不怎么同步。整体通信量自然降下来,却不牺牲关键层的优化稳定性。

其次,通信被嵌入反传播的计算流中,天然实现了更细粒度的通信计算重叠。传统DDP是“按bucket重叠”,但bucket层级的划分仍是粗粒度的、基于简单排序的启发式;DreamDDP则按层切分,每一层梯度就绪后直接进入异步通信通道,通信时机与计算时机之间的耦合更精确,Gradient等待时间可以进一步缩短。

第三个好处相对隐蔽:按层解耦之后,同步不再是全模型统一时刻的“硬同步”,而是分散在若干个step里完成的“软同步”。本地迭代过程中,模型的每个部分始终有一部分在同步、一部分在自由演进,从全局看不存在Local SGD那样显著的“突跳点”,梯度流更平滑。这种介于“完全同步”和“完全异步”之间的状态,恰好介于SGD和Local SGD之间,理论上可以兼得两者的优点。

说实话,“按层解耦”这个方向并不新颖,分布式优化里的“分层同步”“异构频率”已经有不少论文做过类似尝试。但DreamDDP的差异点在于:它没有把层同步当作一个独立的、与训练流程正交的模块,而是把同步动作直接写进了反向传播的算子流里。相当于你不是在做“每K步同步一次”,而是在反向传播图里天然地分布了一组“同步节点”,每个节点只负责一个层组的梯度通信。这个设计在系统实现层面的复杂度比Local SGD高一个量级,但优化空间也大得多。

2. 核心细节解析与实操要点

2.1 层分组策略:怎么做“按层拆”才合理

要拆,第一步当然是决定按什么粒度拆。最粗糙的做法是逐层拆开,每一层一个同步组。对ResNet-50这种50层的网络,意味着50个独立的同步调度单元;对Transformer-175B这种动辄上千层的模型,逐层拆产生的管理开销和通信调度复杂度会失控。所以实际工程里几乎一定做“层分组”。

我基于常见实践整理出三种可复用的分组策略:

分组依据做法适用场景
梯度范数变化率统计每层梯度二范数在连续step间的波动幅度,波动大的层单独成组,波动小的合组训练前期梯度分布变化剧烈时效果好
计算图距离按照反向传播的依赖关系,把同一层组内的参数放在同一bucket,减少跨节点的通信依赖通用场景,实现最简单
通信拓扑亲和性同一台机器内的层合组,通过NVLink通信;跨机器的层单独成组,走RDMA大规模多机场景,通信瓶颈分化时效果明显

我补充一个经验性结论:不要把分层同步做成静态的,训练过程中梯度分布会漂移。前期embedding层变化剧烈,后期分类头变化剧烈,如果分组是训练前固定死的,效果大概率打折扣。更稳妥的做法是设定一个“重分组温度”,每隔一定步数或当loss平台期出现时,重新统计各层梯度变化率并动态调整分组。当然,动态重分组对工程实现的要求很高,因为涉及通信组的重建和梯度的重新编排,非必要不建议在第一版本就上。

2.2 每层同步频率怎么定:从“一刀切K”到“层级自适应K”

Local SGD里只有一个超参K,DreamDDP里这个超参变成了“每一组一个K”。如果全靠手工调,那基本是噩梦——20个层组就是20个K,网格搜索的复杂度指数级爆炸。所以方案里必须包含一个自适应的频率调节机制。

我的建议是:以梯度二阶矩为信号。对每个层组g,实时维护一个EMA统计量:

V_g(t) = β * V_g(t-1) + (1-β) * ||∇W_g(t)||²

然后用相邻两个step的V_g比值 r_g(t) = V_g(t) / V_g(t-1) 来调节该层组的同步频率K_g:

if r_g(t) > θ_high: # 梯度变化剧烈,说明该层在快速漂移 K_g = max(K_min, K_g - 1) elif r_g(t) < θ_low: # 梯度进入平缓区 K_g = min(K_max, K_g + 1)

这个机制背后的直觉是:梯度变化剧烈的层,同步越频繁越安全;梯度平缓的层,即使延迟几个step再同步,梯度信息也不会过期太多。这样K不再是人为拍脑袋的固定值,而是跟随优化过程自动演化。实际操作时把θ_high设为1.2~1.5、θ_low设为0.7~0.9,K_min=1、K_max=16是比较合理的起始区间。注意,不同层的K不要同步跳变,建议用EMA平滑一下K本身,避免通信行为抖得厉害。

2.3 通信调度与数据一致性:这一条最容易被忽略

当你把同步从“整模型每K步一次”变成“每层按需同步”之后,一个微妙的问题就浮出来了:反向传播计算第l层的梯度时,依赖的是前向传播时第l+1层传来的激活值。如果第l+1层的参数在反向传播过程中已经被“同步”更新过,而第l层参数还是本地版本,那么整个梯度计算路径上的参数就不一致了。这种跨层不一致在数学上不会导致梯度爆炸——因为反传路径本身是逐层计算的,每一层的梯度都基于“当时那层的输入”——但从优化视角看,它等同于在梯度中引入了额外噪声。

处理这个问题的标准做法,是把“同步”留到梯度计算完成之后再触发。也就是说,反向传播经过第l层时,我们先用本地参数算出该层的梯度,然后将梯度推入该层组的通信队列,但参数的本地更新仍按本地step执行。等到下一次反传播到同一层组时,再判断该层组的同步条件是否满足,决定是否用收到的全局平均梯度覆盖本地梯度,或者做一次动量修正。这样既享受了通信与计算的重叠,又把跨层不一致的风险限制在了梯度更新环节,而不是前向计算环节。

数据一致性方面还要注意一个细节:batch normalization的统计量。Local SGD场景下,每个worker各自维护BN的running mean/variance,一旦同步拉长,BN统计量会漂移得很厉害。DreamDDP如果用在含BN的CNN上,建议把BN的统计量同步频率设为与所在层组的K一致,且至少不要低于该层组的同步频率;否则同步回来的参数和过期统计量之间会产生额外偏差。Transformer场景通常用LayerNorm,没这个困扰,这也是为什么这个方案在LLM类任务上比在CNN上更容易出效果的原因之一。

3. 实操过程与核心环节实现

3.1 基于PyTorch DDP engine的实现思路

如果要在PyTorch生态里落地DreamDDP,第一原则是:不要把框架推翻重来,而是在DDP的引擎基础上做改造。DDP本身已经具备梯度bucket管理、AllReduce通信编排、计算通信重叠这三大能力,我们要改的只是“同步时机”这一件事。

我推荐的改造路径是:把DDP默认的“所有bucket梯度就绪后立即AllReduce”,改为“每个bucket对应一个层组,bucket的通信触发条件由该层组的同步节奏决定”。具体操作上,核心是重写DDP的autograd hook逻辑。标准DDP里的hook是在每个bucket梯度就绪时触发的,触发后做本地reduce和跨卡AllReduce;改造后的hook应该增加一个判断:当前bucket所属层组的K_g条件是否满足,不满足则仅做本地梯度累积,不发起跨卡通信;满足时才把累积的梯度通过AllReduce同步并清空累积器。

代码层面,可以用PyTorch的register_comm_hook接口挂上自定义通信钩子。默认的allreduce_hook是所有梯度就绪即通信,你需要替换成能读取层组同步状态的钩子。注意,这个hook拿到的bucket对象带parameters()方法,你可以据此判断bucket对应哪些参数、属于哪个层组,实现起来并不复杂。

3.2 一个可运行的伪代码框架

我给出一个偏工程侧的伪代码框架,分为三个模块:层组管理器、梯度累积器、通信调度器。如果你要自己实现,建议严格按这三个模块的边界去写,不要揉成一团——后面调试会轻松很多。

# 伪代码:DreamDDP 核心模块划分 class LayerGroupManager: def __init__(self, model, group_config): self.groups = [] for g_name, params in group_config.items(): self.groups.append(ParameterGroup(g_name, params)) def assign_bucket_to_group(self, bucket): # 根据bucket内的参数找到对应层组 param_ids = [id(p) for p in bucket.parameters()] for g in self.groups: if any(id(p) in param_ids for p in g.params): return g return None class GradientAccumulator: def __init__(self): self.buffer = {} # key: group_id, value: 累积梯度 def accumulate(self, group_id, grad): if group_id not in self.buffer: self.buffer[group_id] = grad.clone() else: self.buffer[group_id] += grad def pop_and_clear(self, group_id): grad_sum = self.buffer[group_id] / self.accum_counter[group_id] del self.buffer[group_id] return grad_sum class CommScheduler: def __init__(self, accelerator, manager, k_min=1, k_max=16): self.acc = accelerator self.manager = manager self.k = {g_id: 1 for g_id in manager.groups.keys()} self.v_ema = {} def need_sync(self, group_id, grad_norm_sq): # 更新EMA beta = 0.9 v = self.v_ema.get(group_id, grad_norm_sq) v = beta * v + (1 - beta) * grad_norm_sq self.v_ema[group_id] = v # 根据EMA与历史值之比调整K if 'v_prev' in self.v_ema: r = v / self.v_ema['v_prev'] if r > 1.3: self.k[group_id] = max(1, self.k[group_id] - 1) elif r < 0.8: self.k[group_id] = min(16, self.k[group_id] + 1) # 判断是否达到同步周期 self.counter[group_id] = self.counter.get(group_id, 0) + 1 if self.counter[group_id] >= self.k[group_id]: self.counter[group_id] = 0 return True return False class DreamDDPStep: def __init__(self, model, scheduler): self.model = model self.scheduler = scheduler self.acc = GradientAccumulator() # 注册反向传播hook for name, param in model.named_parameters(): if param.requires_grad: param.register_hook(self._make_hook(name)) def _make_hook(self, name): def hook(grad): group = self.scheduler.manager.get_group_for_param(name) self.acc.accumulate(group.id, grad) grad_norm_sq = grad.norm() ** 2 if self.scheduler.need_sync(group.id, grad_norm_sq): g_avg = self.acc.pop_and_clear(group.id) self.scheduler.allreduce(g_avg, group) return grad return hook

这套框架里有一点需要说明:我故意没有在hook里直接更新参数。参数的更新交给优化器在step()时统一完成,同步只影响“用什么梯度更新”。这样设计的好处是,你仍然可以使用PyTorch原生的Adam、SGD加momentum,不需要为DreamDDP重写优化器。代价是,momentum的统计量可能因为梯度的“跨步累积”产生偏差,后面4.1节我会专门讲这个问题怎么处理。

3.3 多机多卡环境下的部署注意点

如果你是在单机8卡上做实验,DreamDDP和普通DDP的部署差异不大,无非是换了一个hook。但到了多机场景,有几个细节必须提前处理:AllReduce的通信后端选择、通信流的隔离、以及对NCCL超时的预期。

多机环境下,层组之间的通信如果共用一条NCCL通信流,低频率层组的通信可能会阻塞高频层组的通信,因为NCCL通信是串行入队执行的。建议给不同频率档位的层组分配不同的NCCL通信组(process group)或至少不同的stream,这样低频组的通信不会拖慢高频组。代价是进程内通信资源占用翻倍,需要平衡。实际经验值是“高频组一个stream、中低频组合并共享一个stream”,实测减少20%左右的端到端阻塞,相比给每个组都开stream的配置,省下的GPU内存也很可观。

另外,NCCL的ncclCommWatchdogTimeout在混合同步频率下更容易触发超时,因为低频组可能超过一定时间没有通信活动,而NCCL默认超时是按通信间隔估算的。如果你观察到奇怪的超时中断,先尝试把这个超时值调大,比如从默认的30秒调到5分钟。这不是心理安慰,而是混合同步模式下NCCL的完整性问题,属于工程实证经验。

4. 常见问题与排查技巧实录

4.1 精度波动比预期大:先检查优化器状态

我最早跑这类方案时踩的坑是:模型收敛曲线抖动明显,loss忽高忽低,怎么看都不对劲。第一反应是层组划分有问题,或K值调节参数不对。但调了一圈,发现问题根本不在通信,而在优化器的momentum状态。

标准Adam或带momentum的SGD,其状态变量(一阶矩、二阶矩)是在每个step更新参数时同步更新的。当你采用“梯度累积K步再同步”的时候,优化器看到的是一个“K步平均梯度”,它的分布和单步梯度差异巨大——尤其梯度的scale被整体缩小(因为是平均)后,Adam的二阶矩会下意识地认为梯度很小,导致实际更新步长被放大。这就是为什么Local SGD和DreamDDP这类方案在配合Adam时,LR需要比普通DDP更低,或需要额外做梯度scale矫正。

我的建议是:当你把同步间隔K调大时,把优化器LR等比缩小到原来的1/√K或1/K。具体用哪个比例,取决于你的梯度累积是“累加后平均”还是“累加不平均”。如果是累加不平均,等效于一个大batch,LR应该调大而不是调小。如果是累加后平均,等效于用一个小梯度频繁更新,LR应该调小。注意,这两种情况的方向是相反的,很多人在这里踩坑。

如果你不想动LR,另一个办法是给优化器喂入“同步后的梯度”而不是“累积的梯度”来更新momentum状态。这个方案在DeepSpeed的局部状态下有实现思路,你可以借鉴,但改动量会大不少。

4.2 通信和计算重叠效果不明显:查看bucket划分的细粒度

有朋友反馈说,改完hook之后训练速度没有明显提升,通信时间仍然暴露在前向反向的路径上。这个问题多数不是算法的问题,而是DDP的bucket划分太粗,或者你的层组边界和bucket边界交叉造成的。

标准DDP用一个bucket size参数(默认是25MB)把连续的参数空间切成bucket,bucket内收集满才触发通信。当你的层组和bucket不对齐时,一个bucket可能横跨两个层组,导致hook判断层组逻辑时出现错乱:某层组的K条件满足了,但bucket还没收集满;另一个层组的K没满足,bucket却已经满了。结果是通信行为完全失控。

解决方案很直接:把bucket size设小,或者直接按层组的边界重新划分bucket,保证一个bucket完整属于一个层组。我建议把bucket size调到“与层组内最大层参数总量匹配”的程度,然后用torch.distributed._set_bucket_size之类的接口去设置。这个细节如果不处理,后面的K_g自动调节机制做得再好也白搭。

4.3 反向传播时序错乱:检查hook的执行顺序

DreamDDP把通信hook挂在参数梯度上,一个容易被忽视的问题是hook的执行顺序和梯度累积的顺序可能不一致。具体来说,PyTorch在反向传播时按计算图的逆序计算梯度。正常情况下,越靠近输入层的参数,其hook越晚执行。如果你在多个param上注册了hook,而hook内部又直接发起了通信(AllReduce),则通信顺序会被hook触发顺序决定,一旦你的层组结构和这个计算顺序不匹配,可能出现高频率层组迟迟不发通信、低频率层组频繁发通信的时序倒挂。

解决这个问题有两种思路。第一种是hook内部不直接作通信,而是把状态放入队列,由后台的通信线程统一调度。这也是我推荐的方式——把“调度决策”和“通信执行”解耦,调度逻辑在收到梯度后立即判断,但真正的AllReduce丢给后台线程,这样即使hook顺序有偏差,通信队列也是按层组优先级有序执行的。第二种是调整层组的编号顺序,让它和反传播的访问顺序一致,但这相当于为每个模型手写排序逻辑,通用性差,维护成本高,不建议作为第一选择。

这里再补一个运营层面的注意点:多机多卡环境下,不同卡上同一层组的参数哈希可能不一致。如果你的层组是通过参数名字字符串前缀匹配来划分的,务必在所有rank上加载相同的预训练模型、使用相同的组配置,否则不同rank的分组边界不同,AllReduce会拿到形状不一致的张量,直接报错。这类bug在单机环境下不容易暴露,一上多机就会炸。

5. 参数配置与调优路线参考

5.1 推荐一组可落地的初始配置

我整理了基于常见实践推导的初始配置表,可以直接作为实验起点。这些数值不是论文里的最优值,但大概率能让你的模型在“墙钟时间显著下降”和“精度轻微波动”之间取得平衡。

配置项推荐值说明
层组数量8~12组过多通信调度开销大,过少退化为Local SGD
基础同步间隔K4作为所有层组的初始K
K自适应范围[1, 16]超过16精度风险陡增
EMA因子β0.9用于梯度二阶矩EMA统计
K变化阈值θ_high / θ_low1.3 / 0.8对应梯度急剧/平缓的判断
bucket size对齐层组是必须保证bucket边界不跨层组
优化器LR缩放1/√K仅针对“累加后平均”的同步模式

这套配置的应用场景是:模型在1B~10B参数规模,卡数在16~64卡,网络是常见的InfiniBand或高速以太网。如果你的网络带宽更低(如万兆以太网),建议把K_max上调到32甚至更大,因为通信开销的绝对值决定了优化的优先级,精度折损可以靠后面的LR warmup和重分组技巧弥补。

5.2 三个值得优先尝试的改进方向

第一个,把同步频率和GPU利用率显式挂钩。我在多机实验中发现,当某层组的通信发生频率过高时,整体的GPU利用率反而下降——因为通信等待时间变长了。一个更聪明的做法是用一个轻量的profiler采样GPU idle率,当idle率超过某个阈值(比如30%)时,自动降低该层组的同步频率,直到idle率回落到正常区间。这相当于用系统指标闭环修正算法超参,效果比单纯用梯度EMA调节更符合工程直觉。

第二个,在层同步中加入“软更新”而不是硬覆盖。标准做法是同步后用全局平均梯度直接覆盖本地累积梯度;软更新则是对同步梯度和本地梯度做加权平均,比如:

g_final = α * g_global + (1 - α) * g_local

α的值可以随训练进行逐渐增大,前期允许更多本地信息,后期强化全局一致性。这个技巧在Local SGD的文献中也有类似讨论,实验下来对稳定精度有帮助。

第三个方向我认为最有潜力:把DreamDDP和梯度裁剪、学习率调度器做更深度的绑定。同步频率影响梯度的尺度和统计分布,而梯度裁剪阈值和warmup步数都是围绕“单步梯度”设计的。如果你能根据“当前step的同步比例”动态调整裁剪阈值,比如同步比例高的step允许更激进的lr,同步比例低的step自动降低lr,训练曲线会比固定lr平滑不少。这一点的实现很轻量——只需要在优化器step前读取各层组的同步状态加权计算一个有效同步率。强烈推荐一试。

6. 站在工程视角的最终体会

我对DreamDDP这类方案的整体评价是:方向上比Local SGD更接近工业级落地的需求,但真正上生产之前还有一段距离。它没有回避Local SGD最大的痛点(整模型同步的一致性悬崖),而是用按层解耦的方式把问题拆小、拆细、拆到系统调度能处理的粒度。这个思路的价值不只在分布式训练领域,任何一个“全局强一致拖累局部效率”的系统,都可以借鉴这种按部件粒度解耦调度的方法。

如果你打算在论文工作的基础上做系统实现,我的实操建议是,先不要一上来就冲“全自适应K调节”这种高复杂度目标,先把固定K的按层解耦跑通,观察不同层的梯度变化趋势,积累数据后再上自适应机制。跑通一个“简单但正确”的版本,比追求“复杂但精美”的设计更容易从中发现真正影响性能的关键变量。

最后分享一个小技巧:调试这类系统时,不要只盯着最终的val accuracy曲线,记得把“各层组实际同步次数”“同步时刻与反传播时隙的对应关系”“通信空闲时间占比”这几项指标存到日志里。分布式训练的问题大多数时候躲在曲线背后,曲线看不出异常,但这些中间指标一眼就能暴露调度逻辑的错误。我就是靠这个习惯,在一次实验中发现了某个层组因为bucket边界错位而从未触发同步的bug——那一次,val accuracy比预期低了3个点,整整排查了两天。

DreamDDP还远不是终点,但这种“把全局同步拆成局部节奏”的思考方式,值得每一位做分布式训练的人认真对待。

返回列表