想象一个班级要合作完成一幅巨型拼图:每个学生面前都摊着一份完整拼图的参考小样,但只负责拼自己手里的那一块区域。大家独立拼完自己负责的区域后,必须停下来一起核对整幅图的接缝,确认所有区域都对得上,才允许所有人同时把新拼好的一整圈钉到画板上。这幅拼图,就是我们要训练的大模型;每个学生,就是一块 GPU;而“停下来对齐接缝再集体前进”的过程,就是同步 SGD。 同步 SGD(Stochastic Gradient Descent)是分布式训练最基础、最常用的一种并行方式:所有 GPU 各自用本地的数据切片算出一份梯度,大家通过通信把梯度汇总成同一个平均梯度,再同时更新同一个模型参数。它听起来简单,但背后藏着“梯度聚合”"All-Reduce"这些关键机制。这篇文章不用公式吓唬你,就用拼图班的比喻,把同步 SGD 的原理、通信方式和工程注意点讲清楚。

岗位关联

对 AI 从业者而言,同步 SGD 是理解 PyTorch DDP、Horovod 等分布式训练框架的基石。掌握了它,你就知道多卡训练时模型参数为什么始终一致、通信为什么会成为瓶颈,以及如何调优。

目录
  1. 可视化演示
  2. 并行训练背景
  3. 同步SGD原理
  4. 梯度聚合流程
  5. AllReduce通信
  6. 同步异步对比
  7. 工程调优要点
  8. 实践工具生态
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示同步SGD分布式训练原理的核心机制,通过源头/管道/节点/终端四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

同步SGD分布式训练原理:同步SG 同步SGD的基本原理与执行流程 All-Reduce通信机制与 核心机制可视化 同步SGD分布式训练原理整体架构与关键组件

并行训练背景

拼图班里,一个人拼完整幅巨型拼图可能要一整天,还容易中途犯困。于是老师把拼图分成几十个区域,每人负责一块,大家同时动手。可问题来了:每块区域拼完后,最终还是要拼到一起,如果每个人各拼各的、标准不统一,最后根本对不上。训练大模型也是同理,单个 GPU 的显存和算力都非常有限,于是我们用数据并行:每张 GPU 都保存一份完整的模型副本,只分配不同的数据切片。这样所有卡同时算,训练速度就上去了。

但“同时算”只是个起点。大家拼好的小块必须用同一个标准拼接到整图上,分布式训练里这个“标准”就是梯度。无论同步还是异步,所有 GPU 都要围绕模型参数做文章。同步策略是:大家算完这一批后,非要等所有人都把答案交上来,统一改成正确答案,再一起做下一批;异步策略则是谁先算完谁先改。本文的主角是前者。

并行能加速多少,有一个经典视角叫扩展定律。它提醒我们:不是卡数翻倍,速度就翻倍;总有一些串行环节无法并行。公式如下:

拼图班的人再多,最后对齐接缝的环节总是要花的;同步 SGD 的核心,正是把“对齐接缝”这件事做得既标准又高效。

S(N)=1(1−P)+PN\displaystyle S(N)=\frac{1}{(1-P)+\frac{P}{N}}
这个公式叫阿姆达尔定律。它告诉我们:如果 80% 的工作能并行,那加再多卡,加速比也不会超过 5 倍;同步 SGD 就是要尽量提高那个 P。
  1. 串行比例表示无法并行化的工作量占比。
    1−P\displaystyle 1-P
  2. 并行比例表示可以被 N 张卡分摊的工作量占比。
    P\displaystyle P
  3. 并行分摊并行部分被 N 张卡同时处理后,耗时变成原来的 N 分之一。
    PN\displaystyle \frac{P}{N}
  4. 总加速比串行时间加上并行时间作为分母,除以原来的总时间 1,得到加速比。
    S(N)=1(1−P)+PN\displaystyle S(N)=\frac{1}{(1-P)+\frac{P}{N}}

同步SGD原理

现在拼图班达成共识:每拼完一小轮,大家必须停下,等所有人都报告“我这边完成了”,然后全班一起对答案。训练模型时,这个“对答案”就是对梯度做平均。所谓同步 SGD,就是所有 worker 用各自的数据切片算好梯度后,谁也不准提前更新参数;必须等全部梯度到齐,聚合成一个全局梯度,再同时更新。

每张 GPU 上的模型副本始终是完全一样的,因为初始参数相同、每次更新用的梯度也相同。唯一的区别是它们喂进去的数据不同,所以算出来的局部梯度会有差异。把这些差异平均掉,就得到了更能代表全局数据分布的梯度方向。假设有 K 张卡,第 k 张卡在参数 θ_t 下的局部损失是 L_k,那么同步 SGD 的更新规则是:

拼图班里没有人会单独乱动整图,所有人必须等老师说“答案统一了”再一起钉下一圈。同步 SGD 的纪律性,换来了稳定可靠的收敛过程。

θt+1=θt−η1K∑k=1K∇Lk(θt)\displaystyle \theta_{t+1} = \theta_t - \eta \frac{1}{K}\sum_{k=1}^{K} \nabla L_k(\theta_t)
这个公式说明:同步 SGD 不是把 K 个模型合并成一个,而是把 K 份梯度先平均,再用同一个梯度去更新同一个模型。
  1. 当前参数θ_t 表示第 t 步时大家共同持有的模型参数。
    θt\displaystyle \theta_t
  2. 本地梯度每个 GPU 用自己那份数据计算损失对参数的偏导。
    ∇Lk(θt)\displaystyle \nabla L_k(\theta_t)
  3. 平均梯度把所有 K 个局部梯度相加再除以 K,得到全局平均梯度。
    1K∑k=1K∇Lk(θt)\displaystyle \frac{1}{K}\sum_{k=1}^{K}\nabla L_k(\theta_t)
  4. 参数更新用学习率 η 控制步长,沿平均梯度的反方向走一步。
    θt+1=θt−η1K∑k=1K∇Lk(θt)\displaystyle \theta_{t+1} = \theta_t - \eta \frac{1}{K}\sum_{k=1}^{K} \nabla L_k(\theta_t)

梯度聚合流程

一次完整的同步 SGD 迭代就像拼图班的一次“集体对齐”。第一步,每张卡把自己的数据切片跑一遍前向传播和反向传播,得到一份本地梯度。第二步,所有本地梯度通过通信聚合成一个平均梯度。第三步,每张卡用这个平均梯度更新自己手里的模型副本。第四步,回到第一步,开始下一轮。这个流程周而复始,直到模型收敛。

其中最关键的是第二步“梯度聚合”。它要求最终的平均梯度必须原封不动地出现在每一张卡上,而不是只出现在某一台服务器上。这样才能保证所有模型副本同步更新。用集合符号表示,假设每张卡交出的梯度是 G_1, G_2, ……, G_K,那么聚合操作可以写成:

拼图班对完答案后,每个人都把正确答案抄到自己的小样上,所以下一轮拼图时大家手里的参考图依然完全一致。

AllReduce({Gi}i=1K)=1K∑i=1KGi\displaystyle \mathrm{AllReduce}\left(\{G_i\}_{i=1}^{K}\right) = \frac{1}{K}\sum_{i=1}^{K} G_i
AllReduce 是一个通信原语:先对所有梯度求和,再广播给全体 worker。每个人拿到的结果一模一样。
  1. 输入集合花括号里的 \{G_i\} 表示 K 张卡各自算出的梯度向量。
    {Gi}i=1K\displaystyle \{G_i\}_{i=1}^{K}
  2. 向量求和把 K 个梯度按对应位置相加,得到未归一化的总和。
    ∑i=1KGi\displaystyle \sum_{i=1}^{K} G_i
  3. 取平均值总和除以 K,得到平均梯度。
    1K∑i=1KGi\displaystyle \frac{1}{K}\sum_{i=1}^{K} G_i
  4. 同步广播AllReduce 保证每个 worker 都拿到这个平均梯度,而不是只给某个中心节点。
    AllReduce({Gi}i=1K)=1K∑i=1KGi\displaystyle \mathrm{AllReduce}\left(\{G_i\}_{i=1}^{K}\right) = \frac{1}{K}\sum_{i=1}^{K} G_i

AllReduce通信

AllReduce 听起来像“把所有东西交给一个中央大脑”,但实际上,工程中常用的是环形结构。我们可以把 K 张 GPU 想象成围成一圈传纸条的学生:每张卡先把梯度切成 K 份,然后每个学生把自己手里的第一份传给右边的同学,同时接收左边同学传来的数据并累加。传完一圈,每个学生手里都有了一部分的“总和”。这叫 reduce-scatter 阶段。接下来再做一轮 all-gather,把每个学生手里的那部分总和广播给所有人,最后每张卡都能拼出完整的平均梯度。

这种环形方式最大的好处是:不需要中心服务器,每张卡只和自己的左右邻居通信,带宽被充分利用。通信时间可以用一个简化公式估算:

传纸条比把所有答案都塞给老师再让老师念给大家听更高效:没有单点瓶颈,还能让每个人都参与汇总。

Tring=2(K−1)MKB\displaystyle T_{\text{ring}} = \frac{2(K-1)M}{K B}
其中 M 是全部梯度数据量,B 是单条链路带宽,K 是 worker 数量。这个公式说明:梯度总量越大,通信越慢;所以“梯度压缩”和“通信隐藏”才那么重要。
  1. 单次传输量M/K 表示每轮每个节点实际发送的数据量。
    MK\displaystyle \frac{M}{K}
  2. 传输轮数reduce-scatter 和 all-gather 两个阶段合起来要传 2(K-1) 轮。
    2(K−1)\displaystyle 2(K-1)
  3. 带宽折算总数据量除以带宽 B,得到通信耗时。
    2(K−1)MKB\displaystyle \frac{2(K-1)M}{K B}
  4. 总时间整理后就是环型 AllReduce 的粗略通信时间。
    Tring=2(K−1)MKB\displaystyle T_{\text{ring}} = \frac{2(K-1)M}{K B}

同步异步对比

同步 SGD 最大的优点是好收敛:因为每步更新用的都是“全班核对过”的平均梯度,相当于把 K 个 mini-batch 拼成一个大 mini-batch 再更新。从数学上看,K 个局部梯度的平均,就是 K 份局部损失之和的平均的梯度:

异步 SGD 则像拼图班里有人不等别人,拼完一块就立刻改参考图。它的吞吐量更高,但可能拿着“过时的答案”更新参数,导致收敛波动甚至发散。实际工程中,模型大、集群异构、网络慢时,人们会更慎重地选择同步策略。

1K∑i=1K∇li(θt)=∇(1K∑i=1Kli(θt))\displaystyle \frac{1}{K}\sum_{i=1}^{K}\nabla l_i(\theta_t) = \nabla \left( \frac{1}{K}\sum_{i=1}^{K} l_i(\theta_t) \right)
这条公式意味着同步 SGD 并没有改变优化目标,只是在用更大的全局批次做更稳定的梯度估计。
  1. 左侧局部梯度平均每个 worker 先算梯度,再求平均。
    1K∑i=1K∇li(θt)\displaystyle \frac{1}{K}\sum_{i=1}^{K}\nabla l_i(\theta_t)
  2. 右侧损失先平均把 K 份局部损失相加后再求梯度。
    ∇(1K∑i=1Kli(θt))\displaystyle \nabla \left( \frac{1}{K}\sum_{i=1}^{K} l_i(\theta_t) \right)
  3. 算符交换梯度和求和可以交换顺序,所以两者完全等价。
    ∑∇=∇∑\displaystyle \sum \nabla = \nabla \sum
  4. 结论同步 SGD 等价于一个批次更大的标准 SGD,收敛行为更可预测。

工程调优要点

同步 SGD 在实践中最直观的问题是:卡变多了,全局批次变大了,直接沿用原来的学习率会让模型“步子迈得太大”。拼图班原来的拼图规则是每 10 分钟对一次答案,现在变成了每 100 分钟对一次,如果每次改动幅度还一样,整幅图很容易偏。于是人们提出线性缩放规则:当全局批次变为原来的 B_new/B_base 倍时,学习率也按相同比例放大:

除了学习率,工程上还会用梯度压缩来减少通信量,比如只传量化后的梯度;用通信隐藏让梯度传输和下一轮计算重叠;再加上容错机制处理掉线节点。同步 SGD 的课堂纪律虽然严格,但也要有应急预案。

ηnew=ηbase×BnewBbase\displaystyle \eta_{\text{new}} = \eta_{\text{base}} \times \frac{B_{\text{new}}}{B_{\text{base}}}
线性缩放常配合“学习率热身”使用:训练初期先用小学习率稳定几步,再逐步放大,避免大步长导致发散。
  1. 基准学习率在原始 batch size 下已经调好的学习率。
    ηbase\displaystyle \eta_{\text{base}}
  2. 批次比新全局批次除以旧全局批次,得到缩放倍数。
    BnewBbase\displaystyle \frac{B_{\text{new}}}{B_{\text{base}}}
  3. 新学习率基准学习率乘以批次比,得到调整后的学习率。
    ηnew=ηbase×BnewBbase\displaystyle \eta_{\text{new}} = \eta_{\text{base}} \times \frac{B_{\text{new}}}{B_{\text{base}}}

实践工具生态

如果你真的去训练一个模型,不会手写 AllReduce,而是直接使用成熟框架。PyTorch 的 DistributedDataParallel(DDP)是最常见的同步 SGD 实现:它会自动把模型复制到每张卡上,在每个反向传播结束时触发梯度 AllReduce,然后再让优化器更新参数。底层通信通常交给 NCCL,这是 NVIDIA 专门为 GPU 设计的通信库。同步 SGD 的更新依然可以用一个简洁的式子表示:

拼图班有了 DDP 这个“班长”,就不需要每个学生手动传纸条了。班长负责统一收发答案,大家只要专心拼好自己的区域就行。

θ(t+1)=θ(t)−ηGˉ(t)\displaystyle \theta^{(t+1)} = \theta^{(t)} - \eta \bar{G}^{(t)}
这里 \bar{G}^{(t)} 是第 t 步 AllReduce 之后每张卡都拿到的平均梯度。DDP、Horovod 这些框架只是在替你高效地完成这件事。
  1. 当前参数第 t 步训练结束时所有 worker 共同持有的模型参数。
    θ(t)\displaystyle \theta^{(t)}
  2. 平均梯度经过 AllReduce 后,每张卡得到的全局平均梯度。
    Gˉ(t)\displaystyle \bar{G}^{(t)}
  3. 按步更新用学习率 η 乘以平均梯度,更新到下一步参数。
    θ(t+1)=θ(t)−ηGˉ(t)\displaystyle \theta^{(t+1)} = \theta^{(t)} - \eta \bar{G}^{(t)}

AI 应用场景

大模型多卡预训练

阶段:分布式训练 / 数据并行

解决的问题:单个 GPU 显存放不下大模型,训练时间动辄数周。传统单卡方案既慢又受限;同步 SGD 配合 DDP 让多卡像一个人一样稳定训练。

具体案例:某团队用 8 张 A100 训练一个 7B 参数的 BERT 类模型。他们用 PyTorch DDP 包装模型,每个 batch 被切成 8 份分给 8 张卡。每张卡算完梯度后,NCCL 自动执行 AllReduce,所有卡拿到平均梯度再统一更新。团队只调整了全局 batch 大小和学习率,就复现了单卡训练的收敛曲线。

推荐系统 CTR 模型多机训练

阶段:特征工程 / 模型训练 / 上线迭代

解决的问题:推荐模型数据量大、特征稀疏,单机训练迭代太慢;异步 SGD 又容易出现参数不一致导致的模型抖动。同步 SGD 提供更稳定的在线效果。

具体案例:某广告推荐团队用 Horovod 在多台 CPU/GPU 混合集群上训练 LR 与 DeepFM 模型。每个 worker 读取不同分片数据,每个 step 结束做梯度 AllReduce,保证线上模型参数始终一致。配合梯度压缩,通信带宽占用下降了 60%。

Python 演示

python43 行
import numpy as np def generate_data():    rng = np.random.default_rng(42)    X = rng.normal(size=(128, 4))    true_w = np.array([2.0, -1.0, 0.5, 3.0])    y = X @ true_w + 0.1 * rng.normal(size=128)    return X, y def compute_grad(w, X, y, start, end):    grad = np.zeros_like(w)    for i in range(start, end):        pred = np.dot(X[i], w)        grad += (pred - y[i]) * X[i]    return grad / (end - start) def all_reduce_mean(local_grads):    # 简化模拟 AllReduce:先求和,再除以 worker 数量    total = np.zeros_like(local_grads[0])    for g in local_grads:        total += g    return total / len(local_grads) def sync_sgd(X, y, workers=4, epochs=6, lr=0.1):    n = len(X)    chunk = n // workers    w = np.zeros(X.shape[1])    for epoch in range(epochs):        local_grads = []        for k in range(workers):            start = k * chunk            end = n if k == workers - 1 else start + chunk            local_grads.append(compute_grad(w, X, y, start, end))        avg_grad = all_reduce_mean(local_grads)        w -= lr * avg_grad        loss = np.mean((X @ w - y) ** 2) / 2        print("epoch", epoch + 1, "loss", round(float(loss), 6))    return w if __name__ == "__main__":    X, y = generate_data()    w = sync_sgd(X, y)    print("learned w:", np.round(w, 4))
目的:用最直观的方式演示“每个 worker 算局部梯度 → 聚合平均 → 同步更新”的完整流程,方便你把同步 SGD 的抽象概念落到可运行的代码上。
  1. local_grads.append(compute_grad(...))每个 worker 用自己那一段数据算出局部梯度,放入列表等待聚合。
  2. all_reduce_mean(local_grads)模拟 AllReduce 的语义:把所有人的梯度加起来再除以 worker 数量。
  3. total += g对应 AllReduce 中的逐向量累加,核心是让每张卡都贡献一份信息。
  4. w -= lr * avg_grad所有 worker 共享同一个平均梯度,所以模型参数始终保持一致。
  5. loss = np.mean((X @ w - y) ** 2) / 2每个 epoch 结束后计算全局损失,用来观察同步 SGD 是否在稳定收敛。

常见误区

误区我觉得数据并行就是把数据集拆成几份,每张卡各自训练一个独立的模型,最后再把 K 个模型合并起来取平均。

正解这是把“模型并行”和“数据并行”搞混了。同步 SGD 里,每张卡保存的是同一个模型的完整副本,只是喂的数据切片不同。每步结束后,大家用同一个平均梯度更新参数,所以 K 个副本始终完全一样,不存在“最后合并模型”这一步。

为何常见:因为“数据并行”听起来像是“数据分开了,模型也分开了”,实际上分开的只是数据,模型始终是共享的一份。

误区我以为所有 GPU 算完梯度后,都要把梯度发给一台中心服务器,等它算完平均再广播回来。

正解AllReduce 是一类去中心化的通信原语。环形 AllReduce 让每张卡只和相邻节点通信,通过多轮传输让所有节点都拿到最终平均结果。参数服务器是另一种架构,不是 AllReduce 的必要条件。

为何常见:因为传统分布式系统里“主从模式”太深入人心,大家习惯性地认为必须有一个中央节点负责汇总。

误区同步要等最慢的卡,异步不用等,所以同步肯定又慢又不好用,异步全面优于同步。

正解同步 SGD 虽然要等最慢的 worker,但它的梯度更新稳定,收敛曲线更像传统单卡大 batch 训练;异步 SGD 虽然吞吐高,却会引入梯度陈旧问题,可能导致收敛不稳定。实际中很多大模型训练反而优先选同步 SGD。

为何常见:大家容易把“等待”直接等同于“低效”,忽略了稳定性和收敛质量带来的长期收益。

误区每张卡只处理一小份数据,梯度还要取平均,所以有效 batch size 比单卡时更小。

正解如果每张卡用 b 条样本,K 张卡组成全局 batch size 为 K×b。平均梯度并不代表数据量变小,而是等价于在一个更大的全局 batch 上求梯度。正因如此,才需要用线性学习率缩放规则来匹配变大的 batch。

为何常见:看到“平均”两个字,直觉上以为是缩小,忘了平均是对 K 份梯度做的,而数据总量是 K 倍。

面试题

Q1. 同步SGD中,多个GPU上的模型参数在什么时候保持一致?

单选题 · easy

  • A.初始化后即刻一致
  • B.每步更新之后一致← 正确答案
  • C.只在训练结束时一致
  • D.同步时永远不一致

答案:B。同步 SGD 中所有 GPU 从相同初始化出发,每步都用同一个平均梯度更新,所以“每步更新之后”参数保持一致。C 错误,因为中间过程也一致;D 显然错误。

Q2. 数据并行训练中,每个GPU处理的数据是什么?

单选题 · easy

  • A.完整数据集的独立副本
  • B.数据集的不同切片← 正确答案
  • C.同一份数据的多个副本
  • D.完全不相关的其他数据

答案:B。数据并行会把数据集切成多份,每张卡只处理其中一份,而不是每张卡都存完整数据集(A)或喂同一份数据(C)。D 则完全偏离了训练目标。

Q3. AllReduce操作的最终结果是?

单选题 · easy

  • A.每张卡各自得到一个结果
  • B.只有一台机器拿到结果
  • C.所有卡获得相同结果← 正确答案
  • D.结果随机分配给一张卡

答案:C。AllReduce 包含 Reduce 和 Broadcast 两层含义:先归约求和,再把结果广播给所有节点。所以最终每张卡拿到的是同一个结果,而不是只给一台机器。

Q4. 同步SGD中一个worker计算很慢,会发生什么?

单选题 · mid

  • A.其他worker继续前进
  • B.所有worker等待它完成← 正确答案
  • C.慢worker自动被淘汰
  • D.它从服务器拉取快照

答案:B。同步 SGD 要求所有 worker 的梯度都到齐后才更新参数,所以慢 worker 会拖慢整个训练过程。这也解释了为什么需要“通信隐藏”和“负载均衡”等工程手段。

Q5. 梯度聚合时,同步SGD使用的是哪种梯度?

单选题 · mid

  • A.各worker梯度的最大值
  • B.各worker梯度的随机值
  • C.各worker梯度的平均值← 正确答案
  • D.各worker梯度的中位数

答案:C。同步 SGD 把所有局部梯度相加后除以 worker 数量,得到平均梯度。最大值、随机值或中位数都不是标准 AllReduce 的语义,也不能保证与全局 batch 的梯度等价。

Q6. Ring AllReduce的通信拓扑结构是?

单选题 · mid

  • A.所有节点连到一个中心
  • B.节点组成一个环状结构← 正确答案
  • C.节点按树形层级连接
  • D.节点两两全连接通信

答案:B。Ring AllReduce 让节点围成一个环,每张卡只和左右邻居交换数据,通过多轮传输完成归约和广播。它不依赖中心节点(A),也不是树形(C)或全连接(D)。

Q7. 关于Ring AllReduce,下列说法正确的是?

多选题 · hard

  • A.每张卡先按维度分块传输并累加← 正确答案
  • B.最终每张卡都拿到相同的平均梯度← 正确答案
  • C.需要一个中心节点汇总所有数据
  • D.与树形AllReduce相比更关注带宽利用← 正确答案

答案:ABD。Ring AllReduce 把梯度切成若干块,每个节点在环上逐跳传递并累加,最后所有节点都获得相同结果,因此 A、B 正确。它不需要中心节点,C 错误。它通过让每张卡都参与传输,充分利用链路带宽,D 正确。

Q8. 同步SGD相比异步SGD的特点有哪些?

多选题 · mid

  • A.梯度更新更加稳定一致← 正确答案
  • B.不会出现梯度陈旧问题← 正确答案
  • C.所有worker必须互相等待← 正确答案
  • D.每个worker单独维护参数

答案:ABC。同步 SGD 每次都用最新全局平均梯度更新,所以更稳定且没有陈旧梯度问题(A、B);同时要等所有 worker 到齐(C)。D 错误,因为所有 worker 维护的是同一个模型的副本,不是各自独立的参数。

Q9. 以下哪些是同步SGD工程调优的常见手段?

多选题 · hard

  • A.线性学习率缩放策略← 正确答案
  • B.梯度压缩以降低带宽← 正确答案
  • C.通信与计算相互重叠← 正确答案
  • D.完全关闭梯度同步

答案:ABC。全局 batch 变大后常用线性学习率缩放(A);梯度压缩能显著减少通信量(B);通信和计算重叠可以掩盖通信耗时(C)。D 完全违背同步 SGD 的前提。

Q10. PyTorch DDP中,哪个组件负责完成梯度的AllReduce?

单选题 · extreme

  • A.数据加载器采样模块
  • B.自动求导引擎组件
  • C.DDP通信后端组件← 正确答案
  • D.优化器状态字典

答案:C。PyTorch DDP 在反向传播过程中挂钩梯度同步逻辑,底层通过分布式数据并行后端(如 NCCL)执行 AllReduce。DataLoader 负责取数据,自动求导引擎负责算梯度,优化器负责更新,它们都不直接做通信。