这一节的题目听起来像工程,实际上是两条线:一条数学线、一条算术线。 数学线上数据并行是白送的——八份梯度加权平均跟全量梯度一模一样,差只有 1e-16。 算术线上它一点也不白送。加卡换来的速度有天花板,而天花板由「不干活的那部分」决定。 这一节最后会把这个天花板算成一个具体的倍数。
这是全书唯一一节不讲「怎么算」,只讲「怎么把算连起来」。前面所有章节都默认一台机器、 一份内存;这里把这些前提全部抽掉。 它跟 8.1 有一个很漂亮的呼应: 集成学习是「多个模型投票」,分布式是「一个模型多地算」,两者都栽在同一个地方——汇总。 前者汇总的是票,后者汇总的是梯度,而汇总都要通信。
一、一台机器不够的三道墙
先把「不够」拆开看。它不是一件事,是三件事,解法也完全不同:
| 墙 | 具体表现 | 靠什么绕过去 |
|---|---|---|
| 算力 | 一块卡要算几个月 | 数据并行:把样本切开 |
| 显存 | 一个模型装不进一块卡 | 模型并行:把层切开 |
| 数据量 | 一块盘读不完、读得慢 | 分片:把数据散到多台 |
| 这三件事常被混着说,其实差别很大。算力不够,每一块卡都能装下整个模型, | ||
| 所以可以把样本切分给不同的卡;显存不够时切的是模型,不是样本。 | ||
| 这一节只讲数据并行,因为它最常用,而且它的「不划算」最容易被算清楚。 | ||
| 模型并行要处理的是层与层之间的依赖,那是另一套麻烦。 | ||
| 数据并行的全部逻辑只有三步:每张卡拿一份数据、各自算梯度、把梯度合起来。 | ||
| 第一步和第二步都是纯赚的,问题全在第三步。 |
二、数学上白送:八份梯度等于一份
先说好消息,而且它比想象中更彻底。训练一版逻辑回归,全量梯度是所有样本梯度的平均; 而平均可以拆成「分组算、再加权平均」。
| 算的方式 | 结果 | 跟全量的差 |
|---|---|---|
| 单机一次算完 2000 条 | G_ALL | — |
| 八台各算 250 条再加权平均 | G_SUM | 最大分量 3.053e-16 |
| 梯度本身的量级是 2.117e-01,差是 3.053e-16——中间差着 15 个数量级。 | ||
| 这已经不是「近似」,就是同一个数,剩下的只是浮点数的舍入。 | ||
| 所以数据并行在数学上跟单机完全等价。这条结论很重要,因为它把问题彻底挪走了: | ||
| 既然算出来的东西一样,那么「分布式慢」就只能是别的原因。 | ||
| 但等价有一个前提,而且这个前提极容易被写错:每一份必须按样本数加权。 | ||
| 如果八份梯度直接相加再除 8,那就是给样本少的机器偷偷加了权。Demo 里八份刚好都是 250 条, | ||
| 看不出差别;真实集群里各卡负载不齐,这个 bug 会让收敛变慢,而且极难查。 | ||
| 这是分布式训练最常见的第一个坑:它不报错,只是效果差一点。 |
三、通信:真正的瓶颈
既然算得没错,那慢在哪儿?慢在「把梯度合起来」这一步。先算一笔账, 假设模型有一百万个参数(float32):一份权重 3.8 MB。
| 做法 | 8 卡每轮传 | 64 卡每轮传 | 随卡数怎么变 |
|---|---|---|---|
| 参数服务器 | 61.0 MB | 488.3 MB | 线性增长 |
| 环形全归约 | 6.7 MB | 7.5 MB | 几乎不变 |
| 同一个任务,两种汇总方式差了 65 倍。原因在图的结构里:参数服务器是星形, | |||
| 所有卡都跟中心那台传,中心那台的带宽就是总瓶颈; | |||
| 环形全归约是环,每张卡只跟左右邻居传两次,转一圈就齐了。 | |||
| 环形全归约的总量公式是 2 × (n−1)/n × 权重。 | |||
| n 越大,那个 (n−1)/n 越接近 1,所以总量收敛到「权重的两倍」,跟卡数无关。 | |||
| 这就是为什么 8 卡 6.7 MB、64 卡还是 7.5 MB。 | |||
| 换成大模型,这件事就变得致命。一百二十亿参数,一份权重就是 48 GB,八卡一轮要传 84 GB。 | |||
| 而这个传输每一轮都要做一次,一轮的算只花几百毫秒。 | |||
| 换句话说:算得快,通信没跟上,卡就是在等网线。 | |||
| 所以大模型训练里,通信优化比算子优化更值钱。 | |||
| 算子优化是把 100 毫秒压到 80 毫秒;通信优化是把 500 毫秒压到 50 毫秒。 |
四、同步的代价:每轮都在等最慢的那张
通信之外还有第二笔账,而且这笔账更隐蔽。同步数据并行要求所有卡都算完,才能开始下一轮—— 于是每轮的时间不是平均,是最慢那张卡。 假设一轮的活单卡要算 30 秒,k 张卡平分。 另外每张卡都可能被耽搁(GC 停顿、抢 CPU、网络抖一下),最多 2 秒。跑 4000 次取平均:
| 卡数 | 每卡要算 | 最长耽搁 | 一轮总耗时 | 耽搁占比 |
|---|---|---|---|---|
| 1 | 30.00 秒 | 0.98 秒 | 30.98 秒 | 3% |
| 8 | 3.75 秒 | 1.78 秒 | 5.53 秒 | 32% |
| 64 | 0.47 秒 | 1.97 秒 | 2.44 秒 | 81% |
| 512 | 0.06 秒 | 2.00 秒 | 2.05 秒 | 97% |
| 512 卡那一轮 2.05 秒,其中 2.00 秒在等一张被耽搁的卡——而它自己只算了 0.06 秒。 | ||||
| 这不是网络慢,是「必须一起走」这条规矩的成本。 | ||||
| 卡越多,摊到每张卡上的活越少,可「最长的那次耽搁」只会越来越接近上限。 | ||||
| 算得越少,等得越显得长。 | ||||
| 三条出路,代价各不相同: |
| 做法 | 怎么绕 | 代价 |
|---|---|---|
| 异步训练 | 谁算完谁就更新,不等 | 每张卡用的权重版本不一样,会「陈旧」 |
| 允许落后 s 步 | 只等最慢的那一小撮 | 要调 s,调不好照样慢 |
| 弹性训练 | 慢的直接踢掉,换新的补上 | 要能容错,工程复杂 |
| 异步不是免费的。某张卡拿着三轮前的权重在算梯度, | ||
| 这份梯度是「过期」的,直接拿去更新会把模型往过时的方向推。 | ||
| 所以真实系统里最常见的是中间那条:「允许落后 s 步」的折中。 |
五、分片:加一台机器要搬多少数据
第三笔账不在训练里,在数据摆放上。集群要按 key 把数据分到不同节点, 最直觉的做法是取模:节点号 = 哈希 % N。 它有个致命毛病:N 一变,几乎所有的 key 都要换位置。真算一遍(两万个 key):
| 做法 | 8 台加到 9 台要搬多少 | 为什么 |
|---|---|---|
| 取模 | 88.5% | 分母变了,几乎每个余数都变 |
| 一致性哈希 | 11.4% | 只有新节点接管的那一段环动 |
| 取模搬了 89%,一致性哈希只搬了 11%——而理论上应该接近 1/9 = 11.1%。 | ||
| 实测 11.4%,误差来自哈希本身的随机性。 | ||
| 一致性哈希的想法很朴素:把节点和 key 都哈希到一个环上, | ||
| key 顺时针找到的第一个节点就是它的家。 | ||
| 加一个节点,它只从环上「抢走」一段,别的都不动。 | ||
| 代价是负载不均。8 个节点各放一个点,环上的段长短必然参差。 | ||
| 解法是虚拟节点:一个物理节点在环上撒 120 个点。 | ||
| Demo 实测最忙 2955 个 key、最闲 2052 个,比值 1.44。 | ||
| 虚拟节点就是用来压这个比值的。 | ||
| 这一段的教训是可以搬到别处的:分布式里凡是「按 N 取模」的设计,都要先问一句「N 会变吗」。 | ||
| 会变,就不能取模。 |
六、加速比的天花板
把第 4 段那笔账换算成「加卡到底换多少速度」。加速比 = 单卡一轮时间 ÷ k 卡一轮时间:
| 卡数 | 理论加速比 | 实际加速比 | 并行效率 |
|---|---|---|---|
| 1 | 1.00 倍 | 1.00 倍 | 100.0% |
| 2 | 2.00 倍 | 1.90 倍 | 94.8% |
| 4 | 4.00 倍 | 3.40 倍 | 85.1% |
| 8 | 8.00 倍 | 5.60 倍 | 70.0% |
| 16 | 16.00 倍 | 8.25 倍 | 51.5% |
| 64 | 64.00 倍 | 12.71 倍 | 19.9% |
| 256 | 256.00 倍 | 14.69 倍 | 5.7% |
| 512 | 512.00 倍 | 15.08 倍 | 2.9% |
| 从 1 卡加到 512 卡,理论 512 倍,实际只有 15.1 倍。 | |||
| 注意它最后稳住了,不再往下掉——因为耽搁时间有上限 2 秒,不会再涨。 | |||
| 天花板 = 单卡 30 秒 ÷ 耽搁上限 2 秒 = 15.0 倍。再多卡也超不过。 | |||
| 实测 15.08 倍,跟这个估算几乎重合。 | |||
| 换成阿姆达尔定律的说法就更好记:耽搁 2 秒占单卡 30 秒的 6.7%,而 1 ÷ 0.067 = 15.0。 | |||
| 这就是「堆卡有尽头」的算术原因:那段不随卡数缩小的部分,决定了上限。 | |||
| 它的名字叫串行部分。在真实集群里,它可能是同步等待、可能是通信、 | |||
| 也可能是数据加载——反正一定有那么一段。 | |||
| 所以「多堆卡」从来不是线性变快,真正该优化的是那个不干活的部分。 | |||
| 这一节的落点也在这里:分布式的全部难点,都不在「算」,在「怎么把算连起来」。 |
七、动手 Demo:一台机器不够用的时候
五段:等价性、通信量、同步等待、分片搬迁、加速比天花板。 第 1 段是「数学上白送」,后四段是「工程上要付账」。 这一段的 Demo 里没有神经网络,也没有大数据——全部是算术。 因为分布式训练的瓶颈本来就不在算,把那笔账算清楚,比跑一个真集群有用得多。
def grad(idx): # 只算自己那一份样本的梯度
Xi = Xb[idx]
p = 1.0 / (1.0 + np.exp(-Xi.dot(w)))
return Xi.T.dot(p - Y[idx]) / len(idx)
G_ALL = grad(np.arange(N)) # 单机:全部样本一次算完
G_SUM = np.zeros(D + 1)
for pp in PARTS: # 八台机器:各算一份
G_SUM += grad(pp) * (len(pp) / float(N)) # 关键:按样本数加权
# 通信量:同一份权重,两种汇总方式差一个卡数
ps = 2.0 * BYTES * k # 参数服务器:星形,中心扛全部
ring = 2.0 * (k - 1) / k * BYTES # 环形全归约:只跟左右邻居传
# 同步一轮 = 每卡算 + 那一次最长的耽搁;加速比 = 单卡时间 ÷ k 卡时间
WAIT = dict((k, delay(k)) for k in KS) # 每档卡数都量一遍「最长耽搁」
T1 = base + WAIT[1]
for k in KS:
real = T1 / (base / k + WAIT[k]) # 天花板 = base ÷ 耽搁上限
实跑输出:
--------------------------------------------------------------------
一台机器不够用的时候
--------------------------------------------------------------------
==============================================================
[1] 数据并行:把「算」拆开,数学上一点没变
训练一版逻辑回归。全量梯度是**所有样本的梯度求和再平均**,
而求和可以拆:**八台机器各算一份、再把八份平均起来,结果应该完全一样。**
全量梯度算一遍,跟「八份加权平均」比:
最大分量差 3.053e-16
梯度本身的量级 2.117e-01
**差在 1e-16 量级,就是浮点数的舍入误差。**也就是说数据并行不是「近似」,
**它在数学上跟单机全量完全等价**——前提是每一份都按样本数加权。
如果直接「八份梯度简单平均」,那就不等价了:
**每台机器分到的样本数不一样,简单平均等于给样本少的机器偷偷加权。**
这一点在小数据上无所谓,**在上万张卡的集群里就是会拖慢收敛的实打实的 bug。**
[2] 通信量:分布式真正的瓶颈
假设模型有一百万个参数(float32)。**一份权重 = 3.8 MB。**
做法 每轮传输总量 怎么算的
------------------------------------------------------------------------
参数服务器,8 卡 61.0 MB 2 × 权重 × 8
环形全归约,8 卡 6.7 MB 2 × (n−1)/n × 权重
参数服务器,64 卡 488.3 MB 2 × 权重 × 64
环形全归约,64 卡 7.5 MB 2 × (n−1)/n × 权重
**环形全归约的漂亮之处:总量跟卡数几乎无关。**8 卡 6.7 MB,64 卡 7.5 MB——
**它每张卡只跟左右邻居传两次,转一圈就齐了。**
参数服务器是星形,卡越多,中心那台的带宽压力越大。
一百二十亿参数的大模型呢?**一份权重 48 GB,八卡每轮要传 84 GB。**
**而这个传输每一轮都要做一次,一轮的算只花几百毫秒。**
**这就是为什么大模型训练里,通信优化比算子优化更值钱。**
[3] 同步训练的代价:每轮都在等最慢的那张卡
一轮的活,单卡要算 30 秒;k 张卡平分,每张只算 30/k 秒。
但每张卡都可能被耽搁——GC 停顿、抢 CPU、网络抖一下,最多 2 秒。
**同步训练要求所有卡都算完才开下一轮,所以一轮的时间 = 最慢那张的时间。**
卡数 每卡要算 最长耽搁 一轮总耗时 耽搁占比
----------------------------------------------------------------
1 30.00 秒 0.98 秒 30.98 秒 3%
8 3.75 秒 1.78 秒 5.53 秒 32%
64 0.47 秒 1.97 秒 2.44 秒 81%
512 0.06 秒 2.00 秒 2.05 秒 97%
**512 卡那一轮 2.05 秒,其中 2.00 秒在等一张被耽搁的卡——**
**而它自己只算了 0.06 秒。**算得越少,等得越显得长。
**这就是异步训练、弹性训练存在的理由:宁可各走各的,也不等。**
**但异步也有代价——每张卡用的权重版本不一样,学出来的东西会「陈旧」。**
**所以真实系统里常常是「允许落后 s 步」的折中,而不是完全放开。**
[4] 分片:加一台机器,要搬多少数据
集群里常常要按 key 把数据分到不同节点。
**最直觉的做法是取模:节点号 = 哈希 % N。**
它有个致命毛病:**N 一变,几乎所有的 key 都要换位置。**真算一遍:
取模:8 个节点加到 9 个节点,搬动 88.5% 的 key
一致性哈希:同样的扩容,搬动 11.4% 的 key
**取模搬了 89%,一致性哈希只搬了 11%——理论上应该接近 1/9 = 11.1%。**
**取模的「节点号」几乎每个 key 都被改了。**
**一致性哈希只把「新节点接管的那一段环」上的 key 挪走,别的都不动。**
顺带看一眼负载均衡(一致性哈希,每节点 120 个虚拟节点):
最忙 2955 个 key,最闲 2052 个 key,比值 1.44
**虚拟节点就是用来压这个比值的**:物理节点只在环上放一个点,分得很不均;
**一个物理节点撒 120 个点,负载就摊平了。**
[5] 加速比的天花板
拿第 3 段的账算「加卡到底换多少速度」。
**加速比 = 单卡一轮时间 ÷ k 卡一轮时间。**
卡数 理论加速比 实际加速比 并行效率
----------------------------------------------------------
1 1.00 倍 1.00 倍 100.0%
2 2.00 倍 1.90 倍 94.8%
4 4.00 倍 3.40 倍 85.1%
8 8.00 倍 5.60 倍 70.0%
16 16.00 倍 8.25 倍 51.5%
64 64.00 倍 12.71 倍 19.9%
256 256.00 倍 14.69 倍 5.7%
512 512.00 倍 15.08 倍 2.9%
**从 1 卡加到 512 卡,理论 512 倍,实际只有 15.1 倍。**
**注意它最后稳住了,不再往下掉——因为耽搁时间有上限 2 秒,不会再涨。**
**天花板 = 单卡 30 秒 ÷ 耽搁上限 2 秒 = 15.0 倍**,再多卡也超不过。
**换成「串行占比」的说法:耽搁 2 秒占单卡 30 秒的 6.7%。**
**按阿姆达尔定律,上限就是 15.0 倍**——这就是「堆卡有尽头」的算术原因。
五段连起来:**数据并行在数学上白送**,但要付通信费;
**同步还要等最慢的那张**,分片不当还要搬数据;
**加速比最后被耽搁时间卡住。**
**分布式的全部难点,都不在「算」,在「怎么把算连起来」。**
第 1 段那个 3.053e-16 跟 2.117e-01 差着 15 个数量级,这是本节的地基。 它说明数据并行算出来的东西跟单机是同一个数——所以后面所有的问题都不在「算得对不对」。 第 2 段那张表要横着读:参数服务器那两行从 61.0 涨到 488.3(八倍卡、八倍量), 环形全归约那两行从 6.7 只到 7.5。同一个任务, 两种汇总量差 65 倍——这就是拓扑结构的价值。 第 3 段最扎眼的是 97% 那个数。512 卡的每一轮,97% 的时间在等一张被耽搁的卡。 而这张卡其实只干了 0.06 秒的活。这不是浪费,是同步这一条规矩的必然结果。 第 4 段那个 11.4% 对 11.1%(理论值)很值得看。 它说明一致性哈希不是「大概省一点」, 而是精确地只搬「该搬的那一段」——被抢走的那一段正好是新节点的份额 1/9。 第 5 段那个 15.08 倍是整节最该记住的数。 算它只用了两个数:单卡 30 秒、耽搁上限 2 秒,30 ÷ 2 = 15。 下次看到「我们用了 512 张卡」,就该反问一句:单卡一轮刷多久、最慢那张拖多久?
30 秒实验
- 把第 3 段的
jit从 2.0 改成 0.5,重跑第 5 段。 天花板会从 15 倍抬到 60 倍——这就是「把最长耽搁压下去」为什么比「多买卡」值钱。 - 把第 4 段的
vnode从 120 改成 1,重跑负载均衡那两行。 最忙比最闲的比值会明显变大——这一改就说明了虚拟节点不是装饰,是必需品。 - 把第 2 段那个
P从一百万改成十亿,重跑通信量那四行。 参数服务器会到几百 GB 量级, 而环形全归约只翻一千倍——这个差距就是「为什么大模型一定要用环」的答案。
常见误区
- 以为「卡多就一定快」。Demo 第 5 段:512 卡的效率只有 2.9%, 从 1 卡加到 512 卡,真正到手的只有 15.1 倍。
- 以为分布式的主要成本是「算得慢」。算的东西跟单机一模一样(差 1e-16), 成本全在通信和等待上。
- 以为「梯度直接取平均」最简单。八份样本数不一样时,简单平均等于给样本少的卡加权, 必须按样本数加权。这个 bug 不报错,只让效果差一点。
- 以为异步训练是免费的加速。它省掉的是等待, 付出的是「陈旧梯度」——每张卡拿着不同版本的权重在算,学出来的东西会偏。
自测三问(点开对答案)
1. 为什么说「数据并行在数学上是白送的」?这个结论有什么用? —— 因为**求和可以拆**:八份样本各算一份梯度、再按样本数加权平均, **结果跟全量梯度在数学上完全相等**(Demo 实测最大分量差 3.053e-16, 而梯度量级是 2.117e-01)。 **它的用处是把问题挪走**:既然算出来是同一个数, **那么「分布式慢」就只能是通信和等待造成的**,**不用再怀疑算法本身。** 2. 为什么参数服务器和环形全归约的通信量差了 65 倍? —— **差在图的结构。**参数服务器是星形,**每张卡都要跟中心传,中心那台的带宽就是总瓶颈**, 总量是 2 × 权重 × 卡数,**随卡数线性涨**; 环形全归约每张卡只跟左右邻居传两次,总量是 2 × (n−1)/n × 权重, **n 越大越接近「权重的两倍」,跟卡数几乎无关。**Demo 里 8 卡是 61.0 MB 对 6.7 MB, 64 卡是 488.3 MB 对 7.5 MB。 3. 跑 512 张卡只能快 15 倍,这个 15 是怎么来的? —— **它是「不随卡数缩小的那部分」决定的。**Demo 里单卡一轮算 30 秒, **而每张卡都可能被耽搁最多 2 秒,卡越多「最长那次耽搁」越接近 2 秒。** **天花板 = 30 ÷ 2 = 15。**换成阿姆达尔定律:耽搁占单卡的 6.7%,1 ÷ 0.067 = 15。 **所以想突破这个天花板,唯一的办法是把那 2 秒压小,不是继续买卡。**第 8 章到这儿结束。这一章三节其实是一条线:8.1 说「一个模型不够就养一群」, 8.3 说「一个任务数据不够就借别的任务」,8.4 说「一台机器不够就分到很多台」。 三个「不够」,三种借法。 下一章进入深度学习——那里的「不够」变成了另一件事:手工设计的特征不够用了。
- ☐ (本章完,下一节 9.1 多层神经网络)