关键技术一:Memory-centric tiling for working memory(用于工作内存的内存中心分块)
即使把模型状态(参数、梯度、优化器状态)都卸载到 CPU/NVMe,在 GPU 上执行前向/后向传播时,仍然需要把当前算子的参数和梯度加载到 GPU 内存中。如果单个算子太大,它的参数和梯度本身就可能超过 GPU 内存,导致无法训练。
传统做法是用模型并行(张量切片)把一个大算子拆到多个 GPU 上。但 ZeRO-Infinity 想避免模型并行,于是提出了“内存中心分块”。
核心思想
把一个大线性算子 Y=XWY=XW 拆成多个更小的线性算子,按顺序执行。具体来说,把权重矩阵 WW 沿输出维度切成 NN 块:
W=[W1,W2,…,WN]W=[W1,W2,…,WN]
然后依次计算:
Y1=XW1,Y2=XW2,…,YN=XWNY1=XW1,Y2=XW2,…,YN=XWN
最后把 Y1,Y2,…,YNY1,Y2,…,YN 拼接起来得到完整输出 YY。
每次只把一块 WiWi 加载到 GPU 内存,计算完就释放,再加载下一块。这样 GPU 上同时只需要容纳一块参数及其梯度,而不是整个大矩阵。
结合 ZeRO-3 的参数分区机制,每个数据并行进程只拥有参数的一部分。内存中心分块可以配合 ZeRO-3 的“按需 allgather、用完即释放”模式:需要哪一块参数,就通过 allgather 收集哪一块,算完立即释放。
具体例子
假设一个 Transformer 的 FFN 层,隐藏维度 hd=64Khd=64K。第一个线性层把 64K64K 映射到 4hd=256K4hd=256K。
权重矩阵大小:64K×256K=16G64K×256K=16G 个参数。
FP16 存储:16G×2=3216G×2=32 GB。
梯度也是 FP16:再加 32 GB。
总共需要64 GB的 GPU 内存来容纳这个线性层的参数和梯度。
而单块 NVIDIA V100 只有 32 GB 内存,根本放不下,更别说还要放激活和其他东西。
使用内存中心分块:
把输出维度 256K256K 切成 N=16N=16 块,每块大小 16K16K。
每块权重矩阵:64K×16K=1G64K×16K=1G 参数。
FP16 下每块权重占 22 GB,梯度占 22 GB,合计4 GB。
依次计算 16 块:加载 W1W1 → 计算 Y1Y1 → 释放 W1W1 和梯度 → 加载 W2W2 → 计算 Y2Y2 → 释放 W2W2 …… 直到 Y16Y16。
最终得到完整的输出 YY。
这样,单个 GPU 就能处理这个巨大的线性层,不需要模型并行,也不需要把层拆到多个 GPU 上。
好处
避免模型并行:数据科学家不用修改模型代码,不用把算子手动切分到多个 GPU。
降低工作内存:GPU 上只需要同时驻留一块参数和梯度,内存需求降为原来的 1/N1/N。
支持任意大的算子:只要分块数足够多,理论上可以支持任意大的隐藏维度。
缓解内存碎片:分块降低了对连续大块内存的需求,减少因碎片导致的 OOM。
关键技术二:Bandwidth-Centric Partitioning(带宽中心分区)
当模型状态被卸载到 CPU 或 NVMe 后,如何高效地把它们加载回 GPU的问题。核心思想是:不要让一个 GPU 独自承担全部数据的加载,而是让所有数据并行进程一起分担,利用所有 PCIe 链路的聚合带宽。
下面用对比的方式解释。
1. 传统方法:广播(Broadcast)—— 只有一条 PCIe 在工作
在 ZeRO 和 ZeRO-Offload 中,每个参数(或每层参数)完整地属于某一个数据并行进程。比如有 4 个 GPU(dp=4),某个参数完整地存放在 GPU 0 的 CPU 内存中。
当需要这个参数时:
GPU 0 先从自己的 CPU 内存把参数读到自己的 GPU 内存(通过 GPU 0 的 PCIe)。
然后 GPU 0 通过 GPU-GPU 通信(NVLink 或 PCIe)把参数广播给 GPU 1、2、3。
问题在于:从 CPU 到 GPU 的这一步,只有 GPU 0 的 PCIe 链路在传输数据。GPU 1、2、3 的 PCIe 链路完全空闲。单条 PCIe Gen3 的带宽大约 12 GB/s,这远低于训练所需的 70 GB/s(参数和梯度)或 1.5 TB/s(优化器状态)。
所以,传统广播方式下,CPU/NVMe 到 GPU 的有效带宽被限制在单条 PCIe 的带宽,无法随 GPU 数量增加而提升。
2. ZeRO-Infinity 的方法:带宽中心分区 + Allgather
ZeRO-Infinity 换了一种思路:
不再让一个进程完整拥有一个参数,而是把每个参数切成 dp 份,每个数据并行进程只拥有其中的 1/dp1/dp。
这些分片分别存储在各个进程自己的 CPU 或 NVMe 中。
当需要完整参数时,所有进程同时从各自的 CPU/NVMe 读取自己那一份到自己的 GPU(所有 PCIe 链路并行工作)。
然后执行allgather集合通信,每个 GPU 都从其他 GPU 那里收到别人拥有的分片,最终每个 GPU 都得到完整的参数。
关键点:
从慢速内存到 GPU 的传输:所有 GPU 的 PCIe 同时工作,聚合带宽 = 单 PCIe 带宽 × dp。
GPU 之间的 allgather:数据量是完整的参数,通信成本与广播相同(因为每个 GPU 最终都要得到完整参数)。但这一步走的是 GPU-GPU 高速链路,不是瓶颈。
这样,从 CPU/NVMe 到 GPU 的有效带宽就随数据并行度 dp 线性增长。
3. 具体数字例子
假设有 16 个 GPU(dp=16),每个 GPU 的 PCIe Gen3 到 CPU 的带宽是 12 GB/s。
传统广播:从 CPU 到 GPU 的带宽始终是12 GB/s(只有拥有者的 PCIe 在工作)。
带宽中心分区:每个 GPU 从自己的 CPU 读取 1/161/16 的参数,16 条 PCIe 同时工作。理论聚合带宽 = 16×12=19216×12=192 GB/s。实际中受限于 CPU 内存带宽或 NVMe 带宽,论文中测得:
CPU 内存到 GPU 的聚合带宽约48 GB/s(每 GPU 3 GB/s)。
NVMe 到 GPU 的聚合带宽约25 GB/s(每 GPU 1.6 GB/s)。
虽然单条 PCIe 仍然只有 12 GB/s,但聚合后已经能接近甚至超过训练所需的 70 GB/s(参数和梯度)。随着节点增加,聚合带宽继续线性增长。例如 64 个 DGX-2 节点,CPU 内存聚合带宽超过 3 TB/s,NVMe 聚合带宽超过 1.5 TB/s,完全满足优化器状态的带宽需求。
4. 为什么这很重要?
打破了单 PCIe 带宽瓶颈:卸载到 CPU/NVMe 后,参数和梯度的加载不再受限于单条 PCIe。
随规模线性扩展:GPU 越多,聚合带宽越高,训练效率不会因为卸载而下降。
使得 ZeRO-Infinity 能在有限资源上训练超大模型:即使只有少量 GPU,也能通过聚合带宽高效利用慢速内存。
5. 一句话总结
带宽中心分区就是把参数切成很多份,让每个 GPU 只负责从自己的慢速内存中读取一小部分,然后所有 GPU 同时读取,最后通过 allgather 拼成完整参数。这样,所有 PCIe 链路都在并行传输,聚合带宽随 GPU 数量线性增长,从而避免了单条 PCIe 带宽不足的问题。
这就像搬家:传统方式是所有东西都放在一个人家里,搬家时只有他一个人搬,其他人看着;带宽中心分区是把东西分散到所有人家里,搬家时所有人同时搬自己的那份,最后在目的地拼起来,速度就快多了。
关键技术三:Overlap-Centric Design(重叠中心设计)
重叠中心设计”(Overlap-Centric Design)是 ZeRO-Infinity 用来把慢速内存的数据搬运时间隐藏到 GPU 计算背后的核心机制。它的目标很简单:别让 GPU 闲着等数据。
下面我按“问题 → 思路 → 具体做法 → 效果”的顺序讲清楚。
1. 问题:数据搬运太慢,GPU 会空等
当模型状态卸载到 CPU 或 NVMe 后,GPU 需要参数时,数据要走三段路(以 NVMe 为例):
NVMe → CPU 内存(nc-transfer)
CPU 内存 → GPU 内存(cg-transfer,走 PCIe)
GPU 之间 allgather(gg-transfer,走 NVLink 或 PCIe)
这三段路的速度依次是:NVMe 最慢,PCIe 次之,GPU 间通信最快。
如果串行执行:
text
先花时间把数据从 NVMe 读到 CPU, 再花时间从 CPU 拷到 GPU, 再花时间 allgather 到所有 GPU, 最后才开始计算。
那么总通信时间 = nc + cg + gg,而这段时间 GPU 完全空闲。训练效率会非常低。
所以需要“重叠”:让数据搬运和 GPU 计算同时进行。
2. 核心思想:一边算,一边提前搬后面的数据
ZeRO-Infinity 的重叠引擎有两个主要部分:
(1)动态预取器(Dynamic Prefetcher)
它会跟踪前向/后向传播中算子的执行顺序,提前知道接下来要用哪些参数。然后,在计算当前算子的同时,为未来几个算子发起数据搬运。
论文举了一个非常具体的例子:
假设现在正在执行第 ii 个算子。预取器会同时做三件事:
为第 i+3i+3 个算子做nc-transfer(从 NVMe 读到 CPU)
为第 i+2i+2 个算子做cg-transfer(从 CPU 拷到 GPU)
为第 i+1i+1 个算子做gg-transfer(GPU 间 allgather)
这三个传输全部与第 ii 个算子的 GPU 计算并行发生。
等到计算推进到第 i+1i+1 个算子时,它的参数已经通过 allgather 准备好了;到第 i+2i+2 个时,CPU→GPU 拷贝也完成了;到第 i+3i+3 个时,NVMe→CPU 也完成了。
GPU 一直在算,数据搬运被“藏”在计算后面。
而且,预取器还能处理动态工作流(比如不同迭代的算子顺序变了),它会更新内部的算子序列映射,保证预取正确。
(2)后向传播中的重叠
后向传播需要计算梯度,并把梯度写回慢速内存。
ZeRO-Infinity 可以:
在计算第 ii 个算子的梯度时,同时为第 i+1i+1 个算子的梯度做reduce-scatter(GPU 间通信)。
同时把第 i+2i+2 个算子的梯度从 GPU 写回 CPU 或 NVMe。
这样,梯度的通信和卸载也完全与计算重叠。
3. 为什么需要“三步”分开预取?
因为三段传输的速度不同,如果全部同时开始,慢的(NVMe→CPU)会拖后腿。所以预取器错开启动时间:
最慢的 nc-transfer 提前最多(为 i+3i+3 准备)
中速的 cg-transfer 提前中等(为 i+2i+2 准备)
最快的 gg-transfer 提前最少(为 i+1i+1 准备)
这样,当计算到达某个算子时,它的参数刚好已经“流水线”式地准备好了,不需要等待。
4.流水线的“节拍”由谁决定?
流水线的吞吐量由最慢的阶段决定。在这个例子里,nc 最慢,所以:
NVMe 一直在忙(处理不同算子的 nc)
PCIe 会空闲一段时间(因为 cg 比 nc 快)
GPU 间链路也会空闲(gg 最快)
GPU 计算也可能空闲
这不是断流,而是流水线被最慢阶段限制。快阶段有空闲是正常的。
论文通过聚合多个节点/GPU 的 nc 带宽来解决这个问题:节点越多,nc 聚合带宽越高,最终 nc 不再是瓶颈。
5. 效果
论文第 8.6 节图 6d 显示:在 64 块 GPU 上训练 8B 模型,开启预取和重叠后,小批量下吞吐量显著提升;关闭后性能急剧下降。
一句话总结:重叠中心设计就是让 GPU 一边算当前算子,一边提前把后面要用的参数从 NVMe/CPU 搬过来,同时把算完的梯度搬回去。数据搬运和计算并行,GPU 不再空等慢速内存,从而在卸载到慢速内存的同时仍保持高训练效率。
你可以把它想象成做菜:如果等水烧开再切菜,就慢;如果烧水的同时切菜、备料,就快。重叠中心设计就是让“烧水”(数据搬运)和“切菜”(GPU 计算)同时进行。