想象你是一位连锁餐厅的老板,招牌菜是“红烧肉”,每家分店里都放着一本一模一样的菜谱。一天客人太多,单店厨师忙不过来,于是你开了 N 家分店:各家分店用同一本菜谱、接待不同的客人。打烊后,所有大厨开视频会议对表——“我今天糖色偏重”“我这儿火候小了”——大家把各自发现汇总成一份统一的改进建议,第二天所有分店照同一份新菜谱开工。 这套“多家分店 + 同一菜谱 + 收工对表”的运营法,正是分布式训练中的数据并行(Data Parallel)。它把同一个模型复制到 N 张显卡上,每张卡吃不同的数据批次、算出各自的梯度,再通过 AllReduce 把梯度汇总同步,让所有副本始终步调一致。 不过,分店越多,对表的电话费越贵;菜谱太厚,单家厨房放不下。于是又有了同步与异步的取舍、梯度累积、模型并行和 ZeRO 等补充方案。本文就沿着这家连锁餐厅的扩张故事,把数据并行的原理、机制与 JAX 实践一次讲透。
数据并行是分布式训练的第一课。读懂它,你才能真正看懂大模型训练脚本里“多卡同步、梯度汇聚”的标准流水线,也才能在面试和调优时准确判断瓶颈在算力还是通信。
目录
可视化演示
展示数据并行训练中数据分片、并行梯度计算与AllReduce同步的完整流程
本节配有交互动效(进入视口后自动加载,约 3KB)。
数据分片与分发 全局数据集均匀切分 每个Worker获取独立子集 D_i = D[N/i : N/(i+1)] 总结:数据并行第一步是数据分片
核心思想 — 多店同谱,一菜并行
老板决定开分店,需要同时办三件事:给每家分店发同一本菜谱(模型副本)、给每家分店分配不同的客人(数据批次)、规定每晚对表规则(梯度同步)。这三件事,恰好就是数据并行的全部骨架。
在深度学习里,“菜谱”是模型的参数 w,“客人”是训练数据。数据并行把一份完整模型复制到 N 个设备上(常见的是 GPU),再把每一批次训练数据均匀切成 N 份,每张卡只处理自己那份。由于各卡数据不同,前向计算和反向传播算出的损失梯度也各不相同——这些梯度就是各分店“今天发现的问题”。
数据并行最优雅的地方在于:虽然每张卡只看到 1/N 的数据,但只要把所有本地梯度求平均,再让所有副本都按这个平均梯度更新一次参数,效果就等价于用 N 倍数据训练了一步。这让它在实现上对模型结构零侵入——不需要改动任何网络层,只要把数据切分和梯度同步包在外面。
所以你看,数据并行没有发明任何新的网络结构,它只是把“训练”这件事从单店变成连锁:副本相同、数据分开、意见平均。模型本身完全不用动——这就是工程师们常说的“对模型无侵入”。
- 损失梯度第 i 个设备在自己的数据分片上算出的梯度,相当于这家分店今天的改进意见。
- 跨设备求和把 N 个设备的梯度逐元素相加,等价于把 N 份意见汇总到同一张纸上。
- 求平均除以设备数 N,把汇总梯度折中为一条共同意见,避免某一家分店的一家之言主导全体。
- 参数更新用学习率 η 控制修改幅度,让所有副本朝同一方向迈出完全一致的一步。
AllReduce机制 — 环形传话的对表大会
每晚的对表大会怎么开才高效?最笨的办法是每家分店把意见书寄给总部,总部汇总后再复印发给所有人——总部成了瓶颈,店越多,总部的收发件压力越大。
这个“汇总意见并广播给所有人”的操作,在分布式系统里叫 AllReduce(全归约)。All = 所有节点参与,Reduce = 求和/求平均等归约。数据并行每迭代一次,就要对全部梯度做一次 AllReduce。注意,梯度通常有几亿个浮点数,也就是那张“意见书”厚得惊人。
因此,AllReduce 的工程实现直接决定扩展效率。工业界主流是 Ring-AllReduce(环形全归约):设备们围成一个环,把梯度切成 N 块,先沿环传递并就地累加(reduce-scatter 阶段),再把完整结果沿环传一圈让每个人各拿走一块(all-gather 阶段)。每个节点只和左右邻居通信,没有谁是中心。
回到连锁餐厅:Ring-AllReduce 就像分店们围成一圈传话,每家只需把意见传给左右邻居,传两圈,所有人就同时拿到了全体的平均意见。没有总部,也没有人需要听 N 遍完整发言。
- 梯度总大小 D模型全部梯度参数的字节数,相当于“意见书”的厚度。
- 分块大小 D/N梯度被切成 N 块,每轮每个节点只传一块,工作量被摊薄。
- 绕环轮数 2(N-1)reduce-scatter 与 all-gather 两个阶段各绕环 N-1 轮,共 2(N-1) 轮。
- 每节点通信量化简为 2D(1-1/N),当 N 很大时趋近 2D,只和模型大小有关——这就是 Ring-AllReduce 能支撑千卡训练的原因。
同步与异步 — 齐步走还是各自飞
对表大会有两种开法。第一种:全员必须到齐、确认完所有意见后,才允许任何一家修改菜谱——这是同步训练。第二种:谁先算完谁先改,其他人稍后跟上——这是异步训练。
同步训练(Synchronous Training)中,N 个设备各自前向反向后,必须等最慢的设备完成,AllReduce 结束才统一更新参数。它收敛稳定、行为可复现,但整轮速度被最慢节点拖住,这就是“木桶效应”。异步训练(Asynchronous Training)不需要等待,设备算完就把梯度发到参数服务器并立即拉取最新参数继续下一轮,吞吐更高,但拉取到的参数可能已经又更新了好几轮。
异步的问题在于陈旧梯度(stale gradient):梯度是在旧参数状态下算出的,用它更新新参数就像拿着一个月前的城市地图开车。梯度越旧,方向越不可靠,严重时会导致 loss 震荡甚至不收敛。
所以连锁企业通常的做法是:总部定下的核心配方(同步)稳定为主,允许部分门店小范围试新(异步)。工业界也类似——大规模预训练用同步保证质量,在线场景用异步换吞吐。
- 滞后步数 τ梯度计算完成时参数已落后当前版本多少步,即“意见的保鲜期”。
- 旧状态梯度用旧参数算出的梯度,与当前参数状态存在偏差。
- 异步更新直接用这份旧梯度更新当前参数,每张卡各更各的,互不等待。
- 与同步对照同步训练等价于 τ = 0,梯度永远新鲜,代价是大家都要等最慢者。
梯度累积 — 小本本攒意见
对表大会每天开一次,电话费吃不消。聪明的老板规定:每家分店把连续 K 桌客人的反馈先记在小本本上,攒够一页,再带着这页意见参加对表。
对应到训练,就是梯度累积(Gradient Accumulation):单卡显存不足以直接装下大批次时,把一个大批次拆成 K 个微批次(micro-batch)顺序前向反向,K 个微批次的梯度在本地累加并取平均,攒满 K 步才做一次参数更新和一次 AllReduce。
这样做有两个好处:一是峰值显存只按一个微批次计算,不必为完整大 batch 预留显存;二是 AllReduce 频率降为原来的 1/K,通信开销大减。数学上,在普通 SGD 下,K 个微批次梯度平均再更新,等价于一次性用 K 倍数据更新——只是等效 batch 变大后,学习率通常也要按比例放大以保持收敛速度。
小本本攒意见,攒够了才开会——梯度累积用“少开会、多记录”换来了通信频率的下降和对大 batch 的模拟,是显存与速度之间最常用的折中。
- 设备下标 i第 i 张卡在本地独立执行整个攒梯度流程。
- 微批次梯度第 k 个微批次完整前向反向后得到的梯度。
- 累加求和把 K 个微批次的梯度逐元素相加,即小本本上记录的总意见。
- 平均归一除以 K 使梯度量级与单批次一致,等效扩大训练 batch 但没扩大单步显存。
显存瓶颈 — 菜谱太厚怎么办
有一天老板发现,菜谱已经厚到单家厨房的调料架放不下了。数据并行有个前提:每家分店必须带全套菜谱。所以分店开得再多,单店的“书架空间”也没有变小——显存瓶颈不会因为加卡而缓解。
这是数据并行的硬边界:每个副本都要保存模型参数、梯度以及优化器状态(如 Adam 的动量与方差),这三块加起来通常是参数量的 4~16 倍。当模型大到单卡装不下时,必须请出两位外援。
第一位是模型并行(Model Parallel):把模型按层切开,每家厨房只负责一道工序,数据依次流过所有设备,相当于流水线;第二位是 ZeRO:不切模型计算,而是把参数、梯度、优化器状态分片存到 N 张卡上,谁用到谁“临时调货”,用完就还。ZeRO 与数据并行正交,是目前大模型训练的标准组合。
所以聪明的连锁老板两手抓:数据并行负责“人海战术”,让每张卡忙起来;模型并行负责“流水线分工”,ZeRO 负责“把菜谱拆成 N 本分册放各家书库”。三者互补,缺一不可。
- 参数占用模型参数本身,即“菜谱正文”。
- 梯度和优化器状态反向梯度与 Adam 动量等状态,常是参数量的数倍。
- 分片存储ZeRO-3 把三大块均分到 N 张卡上,单卡负担降为原来的 1/N。
- 激活值不变前向计算的中间缓存只取决于本地微批次大小,不随 N 摊薄,仍占大头。
JAX实践 — 一键分派的总部系统
作为老板,你当然希望“开分店、发名单、统一对表”这些杂事能一键自动化。JAX 的 pmap 就是这样的调度系统:你只需写一份单设备版本的函数,它帮你完成复制、分派、同步。
pmap(parallel map)接受一个函数 f,把它复制到所有设备(jax.devices() 返回的设备列表),并约定:带设备维度的输入按第一维切成 N 份分别送入,不带设备维度的输入则广播给所有副本。返回结果依然是带设备维度的 DeviceArray,代表“每个设备上各有一份结果”。
梯度同步需要显式声明:在 pmap 函数内部用 jax.lax.pmean(g, axis_name='devices') 把各设备梯度求平均并广播——pmean 会被 XLA 编译成真正的 AllReduce 指令。这是 JAX 数据并行的一个鲜明特点:把“同步”明明白白写进代码,而不是藏在框架默认行为里。
pmap 就像总店发出的“今日操作手册”:同一本手册(函数代码)发给所有分店,但附上各自不同的客人名单(数据切片)。打烊后要不要对表、怎么对表,由 pmean 一句指令说了算。
- 总批次大小每个训练迭代的全部样本数,如 1024 张图片。
- 每卡份额平均分到每张卡的样本数。
- 切片区间第 i 张卡取第 i·b 到 (i+1)·b 个样本,各卡互不重叠。
- SPMD 执行同一份函数在所有设备上运行,只是各自处理不同的切片。
经典场景 — 从单店到千店连锁
分店从 4 家开到 1024 家后,老板发现收益不再线性增长:每天光是对表电话费就占了运营成本的大头。这就是分布式训练里著名的“通信与计算比”问题。
设每轮训练中,可并行的计算部分(前向 + 反向)占比为 P,剩下 1-P 是无论如何也无法摊薄的串行部分(同步握手、启动延迟、通信调度)。Amdahl 定律给出了加速比上限:设备数 N 再大,加速比也只能逼近 1/(1-P)。
对现代大模型,P 常在 0.95~0.99,理论加速上限是 20~100 倍。实际训练还会遇到通信带宽瓶颈、负载不均等问题。常用的对冲手段包括:梯度累积加大单卡计算量、用 Ring-AllReduce 替代参数服务器、梯度压缩、以及用 NVLink/IB 等高速互联降低单次通信延迟。
开再多的分店,总部每天要做的“开业训话”与“对表例会”始终省不掉——这就是 1-P 那条串行尾巴。数据并行工程的本质,就是尽量让每家分店多干活、少开会。
- 并行比例 P可被多卡分摊的计算时间比例,如 95%~99%。
- 串行尾巴每轮不可避免的单点开销,不随卡数减少。
- 并行部分摊薄可并行计算被 N 张卡均摊后的耗时。
- 加速比与上限N 趋于无穷时,S 趋于 1/(1-P),卡再多收益也封顶。
AI 应用场景
大语言模型预训练
阶段:分布式训练与算力扩展
解决的问题:单卡训练 70B 参数模型需要数十年;数据并行让 N 张卡并行吃下不同数据,配合 ZeRO 把参数与优化器状态分片存储,将训练时间压缩到周级。
具体案例:某团队用 512 张 A100 训练 70B 大模型:ZeRO-3 负责“把菜谱拆成 512 本分册”,数据并行负责让每张卡处理不同的数据批次,每步等效 batch 达 2048,AllReduce 由 NCCL 在 NVLink 拓扑上完成。
对比学习与超大 batch 训练
阶段:模型训练与超参调优
解决的问题:SimCLR 等对比学习需要 4096 以上的超大 batch,单卡显存远不够;数据并行天然把 batch 切到各卡,再通过 AllReduce 保持梯度一致,就像“各家分店同时接待大量客人再统一对表”。
具体案例:用 128 张 GPU 训练 ResNet-50 对比学习模型,每卡 batch = 32,全局 batch = 4096,学习率按线性缩放规则从 0.06 提高到 1.92(η × 128),最终准确率与单卡小 batch 训练持平。
JAX 科学计算加速
阶段:模型推理/训练加速
解决的问题:蛋白质折叠等科学计算任务输入尺寸大、内存需求高,单机难以完成;JAX 的 pmap 让研究者在 TPU/GPU 集群上以 SPMD 方式并行,只写一份函数即可分散到多核。
具体案例:研究小组用 jax.pmap 包装折叠函数,将蛋白质序列按 batch 分片到 8 个 TPU 核,每个核计算一部分结构,反向时用 pmean 同步每条序列的梯度并统一更新参数。
Python 演示
# -*- coding: utf-8 -*-"""数据并行演示: 用 JAX pmap 在多个设备上训练一个小型线性模型."""from functools import partialimport jaximport jax.numpy as jnpfrom jax import random def build_data(key, n_samples=256, n_features=8): # 请保证 n_samples 能被设备数整除 key_x, key_y = random.split(key) x = random.normal(key_x, (n_samples, n_features)) true_w = jnp.linspace(-1.0, 1.0, n_features) y = x @ true_w + 0.05 * random.normal(key_y, (n_samples,)) return x, y def compute_loss(w, x, y): pred = x @ w return jnp.mean((pred - y) ** 2) @partial(jax.pmap, axis_name='devices')def train_step(w, x, y): # 每个设备副本在自己的数据分片上计算本地梯度 g = jax.grad(compute_loss)(w, x, y) # pmean 触发跨设备 AllReduce: N 个本地梯度求平均并广播回所有副本 g = jax.lax.pmean(g, axis_name='devices') return w - 0.05 * g def main(): n_devices = jax.local_device_count() print('可用设备数:', n_devices) x, y = build_data(random.PRNGKey(0)) # 数据分片: 把批次按设备数均分, 首维变成设备维度 x = x.reshape(n_devices, -1, x.shape[1]) y = y.reshape(n_devices, -1) w = jnp.zeros(x.shape[2]) for epoch in range(100): w = train_step(w, x, y) if epoch % 20 == 0: # 所有副本完全同步, 取任一设备的参数即可 loss_val = compute_loss(w[0], x.reshape(-1, x.shape[2]), y.reshape(-1)) print('epoch {:3d}, loss = {:.4f}'.format(epoch, loss_val)) if __name__ == '__main__': main()
@partial(jax.pmap, axis_name='devices')把 train_step 复制到所有设备并行执行,axis_name 是 pmean 归约时使用的“暗号”。x.reshape(n_devices, -1, x.shape[1])数据分片:把总批次按设备数均分,首维变成设备维度,供 pmap 切分。g = jax.grad(compute_loss)(w, x, y)每张卡在自己的数据分片上算出本地梯度,对应“分店意见”。g = jax.lax.pmean(g, axis_name='devices')跨设备 AllReduce:把所有本地梯度求平均并广播回每个副本。w = train_step(w, x, y)每轮训练后拿到的是所有副本同步一致的新参数。loss_val = compute_loss(w[0], ...)副本完全同步,取任一设备的参数即可评估全局损失。
常见误区
误区数据并行就是把同一份训练代码多开几个进程跑,模型和算法都不用改,卡越多训练一定越快。
正解训练确实不用改模型结构,但“卡越多越快”只在算力远大于通信时成立。每轮迭代结束都要对全部梯度做一次 AllReduce,通信量和梯度大小成正比。当单卡 batch 很小、梯度很大时,通信时间会吃掉并行收益,加速比被 Amdahl 定律封顶。工程上要用梯度累积、梯度压缩、更快的互联来提升计算/通信比。
为何常见:只盯着“前向 + 反向”被 N 张卡均摊,下意识忽略了每轮结束后那条必须在所有卡之间流动的梯度洪流。
误区在 JAX 里只要用 jax.pmap 包一下训练函数,各设备的梯度就会被自动平均同步,不需要写任何额外代码。
正解pmap 只负责把函数复制到设备、把带首维的输入切分、把不带首维的输入广播。要对跨设备的梯度做归约,必须在函数体内显式写 jax.lax.pmean(g, axis_name='devices');否则每张卡都按自己的本地梯度更新广播参数,副本会立刻失步。对 pmap 后的函数整体求导时虽然会自动插入归约,但显式 pmean 才是清晰可控的写法。
为何常见:pmap 的抽象太“自动化”,包装了 SPMD 的复制与分片,让人误以为反向的跨卡同步也一并打包好了。
误区显存不够就用梯度累积,把 8 个微批次梯度加起来,效果和 8 倍大 batch 完全一样,可以放心使用。
正解对以线性层和 LayerNorm 为主的 Transformer 类模型,梯度累积与真实大 batch 在数值上基本等价;但对含 BatchNorm 的模型,BN 的均值/方差仍按每个微批次独立统计,累积 8 步会得到有偏的统计量,必须改用同步 BN。此外,等效 batch 变大后学习率也要相应放大,否则收敛速度变慢。
为何常见:从梯度求和公式看两者确实相等,容易忽略归一化层的统计量差异和超大 batch 下的学习率缩放需求。
误区模型太大单卡放不下?那就多加几张卡做数据并行,反正每张卡只存一份模型,卡多了自然就装下了。
正解数据并行要求每张卡都保存一份完整模型副本,单卡显存占用 = 参数 + 梯度 + 优化器状态 + 激活值,这个值不会随卡数下降。参数超显存时必须用模型并行把计算切到不同设备,或用 ZeRO 把三块状态分片存储。数据并行从来不是显存扩容方案,而是吞吐扩展方案。
为何常见:“多卡 = 更多显存”的印象根深蒂固,忽略了数据并行“每副本完整”这一前提。
误区异步训练不收敛、行为不可复现,早就被淘汰了,工业界现在只用同步训练。
正解同步训练的收敛保证来自“每步都用最新平均梯度”,但它受最慢设备拖累,且掉线一台就要整体阻塞。异步训练收敛可能震荡,但在通信带宽受限、设备异构、在线学习场景中吞吐优势明显。实际系统常以同步为主干,结合梯度累积降低同步频率;异步更新仍是推荐系统在线训练和部分联邦学习的标配。
为何常见:教材普遍论证同步收敛更稳,读者容易把“更稳”演绎成“唯一正确”。
面试题
Q1. 数据并行中“数据分片”指的是什么?
- A.把模型参数按层拆给不同设备
- B.把训练数据按批次拆给多个设备副本← 正确答案
- C.把优化器状态拆分到多张卡上
- D.把前向激活值缓存到本地硬盘
答案:B。数据并行的本质是“模型复制、数据切分”:每个设备持有完整模型,只是各自处理不同的数据批次。A 是模型并行,C 是 ZeRO 的做法,D 是激活值相关优化,都不属于数据分片。
Q2. 关于 AllReduce 操作,下列说法正确的是?
- A.每个设备只得到其他设备的梯度之和
- B.所有设备得到完全相同的归约结果← 正确答案
- C.只有主设备拿到最终的归约结果
- D.每个设备各自返回自己的局部梯度
答案:B。AllReduce = All + Reduce,先归约(求和/平均)再把结果广播给所有节点。若只有主设备拿到结果是 Reduce + Broadcast 而非 All;若各自返回局部梯度则没有归约。
Q3. Ring-AllReduce 相对集中式参数服务器的主要优势是?
- A.完全不需要任何跨设备网络通信
- B.通信量不随设备数膨胀且无单点瓶颈← 正确答案
- C.只适用于单机多卡的小规模场景
- D.能显著提高梯度计算的数值精度
答案:B。Ring 把通信均匀分摊到相邻节点,每个节点通信量约为 2D,不存在集中收发导致的主节点瓶颈,因此容易扩展到千卡。A 显然错误,C 错误(多机同样适用),D 与数值精度无关。
Q4. 在 JAX 中,pmap 训练函数内跨设备求平均梯度应使用?
- A.jax.lax.pmean 求平均← 正确答案
- B.jax.numpy.mean 直接求平均
- C.jax.vmap 自动完成向量化
- D.jax.random.split 拆分数据
答案:A。jax.lax.pmean 配合 pmap 的 axis_name 才会触发跨设备 AllReduce;jnp.mean 只在本设备内求均值;vmap 是向量化映射;random.split 是密钥拆分,均无跨卡通信。
Q5. 梯度累积的主要作用不包括?
- A.减少跨设备 AllReduce 通信频率
- B.在不增加峰值显存时扩大等效批次
- C.让单卡能够容纳更大的模型参数← 正确答案
- D.用多个微批次模拟真正的大批次
答案:C。梯度累积只改变训练批次的节奏,参数本身仍要在每张卡上存一份完整副本,不能减小参数显存。降低激活峰值显存、模拟大 batch、减少 AllReduce 频率都是它的真实收益。
Q6. 数据并行下,单卡显存占用随设备数 N 的变化是?
- A.随设备数 N 增加线性下降为原来的 1/N
- B.与 N 无关,每卡仍需完整的参数/梯度/优化器状态← 正确答案
- C.随 N 线性增加,因为每增加一卡就多一份副本
- D.仅由训练数据集的大小决定,与模型无关
答案:B。数据并行每张卡都要存一份完整模型副本及其梯度与优化器状态,N 只影响“总显存”翻倍,不减少单卡需求。要让单卡负担随 N 下降,需 ZeRO 分片或模型并行。
Q7. 设可并行比例 P = 0.9,根据 Amdahl 定律,加速比上限约为?
- A.加速比上限约为 90 倍
- B.加速比上限约为 10 倍← 正确答案
- C.加速比上限约为 9 倍
- D.加速比上限可无限接近 1000 倍
答案:B。Amdahl 定律 S = 1 / ((1-P) + P/N),N 趋于无穷时上限为 1/(1-P) = 1/0.1 = 10 倍。A 直接把 P 当倍数,C 未取倒数,D 忽略了串行尾巴的存在。
Q8. 模型参数超出单卡显存时,可行方案有?
- A.模型并行:按层把模型切给多卡← 正确答案
- B.ZeRO:分片存储参数与优化器状态← 正确答案
- C.增加数据并行卡数降低单卡显存
- D.梯度累积降低前向激活值显存
答案:AB。参数放不下是“存储”问题,模型并行和 ZeRO 直接改变参数的存放方式。C 是常见误区:数据并行每卡仍存完整副本;D 只降激活值缓存,不降参数显存。
Q9. 关于同步训练与异步训练,正确的有?
- A.同步训练受最慢设备的拖累← 正确答案
- B.异步训练会遇到陈旧梯度问题← 正确答案
- C.同步训练一定收敛得比异步快
- D.异步训练必须依赖参数服务器
答案:AB。A 即木桶效应;B 是异步的核心风险。C 过于绝对:同步每步质量高但整体速率可能被拖累,异步单位时间步数多,收敛快慢取决于问题;D 错误,异步也可用 AllReduce 类通信,不由结构强制。
Q10. 用梯度累积等效模拟大 batch 训练时,合理做法包括?
- A.学习率按累积倍数线性放大← 正确答案
- B.BatchNorm 模型改用同步 BN← 正确答案
- C.与真实大批次完全无条件等价
- D.本地累积 K 步后做一次 AllReduce← 正确答案
答案:ABD。等效大 batch 后学习率通常按线性缩放规则放大(A);BN 统计量需跨设备同步才与大 batch 等价(B);C 过于绝对,归一化与学习率都需调整;D 正是梯度累积的节奏定义。