mixed precision + Adam 训练时,每个参数要占 16 B 的 model states:fp16 参数与梯度各 2 B,fp32 master weight、momentum、variance 各 4 B。 70B 模型仅这部分就是 1,120 GB,单卡 HBM 装不下。offload 把其中一部分搬到 CPU 或 NVMe,用 PCIe / NVLink 带宽换容量。 代价是每步多一次往返;能不能被计算盖住,决定了它是否划算。推理侧还有一条不同的路:weight offload 与 KV cache offload 搬的不是同一种东西。
显存墙的主因不是权重本身,而是训练状态。按 ZeRO-Offload §2 的记账,一个参数要 16 B;70B 模型的 model states 是 1,120 GB,其中只有 140 GB 是 GPU 前向/反向真正需要的 fp16 参数。 剩下 980 GB 是梯度与 fp32 optimizer states,正是 offload 的对象。
训练侧的 offload 是分层的:ZeRO-Offload 把 fp16 梯度与 fp32 optimizer states 放 CPU,参数更新也在 CPU 做,GPU 只保留 fp16 参数(另有 activations);ZeRO-Infinity 再把 partitioned params / gradients / optimizer states 放到 NVMe;FSDP 的 CPU offload 则把 sharded params、grads 与 optimizer step 一起下沉到 CPU。 每层都需要预取或流水线,否则 PCIe 延迟会直接暴露在关键路径上。
推理侧的 offload 要分成两件事:weight offload 降低静态权重的常驻显存,但每个 token 的 forward 都要读一次被卸载的权重;KV cache offload 降低动态 KV 池的常驻显存,只在 block 换出或 prefix cache 命中时搬 KV。 前者按 token 重复付费,后者按 block 付费,不能互相外推。
收益边界由传输/计算比决定。ZeRO-Offload 把每步 CPU↔GPU 流量压到 4M 字节,但小模型、小 batch、PCIe 3.0 或延迟敏感的 decode 仍可能让 offload 变成负收益;模型能装下时,offload 通常不值得。
先算清一个参数在混合精度 Adam 下占多少字节,才能判断该卸载什么、不该卸载什么。
ZeRO-Offload §2 给出的账本是:fp16 参数 2 B、fp16 梯度 2 B、fp32 master weight 4 B、Adam 的 momentum 与 variance 各 4 B,合计 16 B/参数。 ZeRO-Infinity §3 把 fp32 梯度也计入 optimizer states,得到 20 B/参数。两篇的记账口径不同,本页的计算统一采用 ZeRO-Offload 的 16 B 口径,并在手算里注明。
按十进制 GB(1 GB = 109 B)计;未计 activation、临时 buffer 与碎片。
这不是“把 70B 权重塞进一张卡”的问题:fp16 权重本身就要 140 GB,单张 80GB GPU 仍放不下;offload 解决的是把 980 GB 的梯度与 optimizer states 移出 GPU,从而让每张卡保留完整的 fp16 参数,只把梯度与 optimizer states 的分片移到 CPU。 真正的前向/反向仍必须在 GPU 上完成,ZeRO-Offload §3.2 的原则是只把计算复杂度低于 O(MB) 的更新类计算放到 CPU。
推理的账本不同:bf16 权重是 2 B/参数,70B 约 140 GB;此外还有随上下文增长的 KV cache。vLLM 把 KV 作为独立的 block 池管理,并在 vllm/config/cache.py:399-408 提供单独的 kv_offloading_size,与权重显存分开核算。
| 组成 | 训练:混合精度 Adam | 推理:BF16 | 是否可 offload |
|---|---|---|---|
| 参数 | fp16 副本 2 B + fp32 master 4 B,每参数 | bf16 2 B,每参数 | 训练与推理都可 |
| 梯度 | fp16 2 B;ZeRO-Infinity 另计 fp32 梯度 4 B | 无 | 仅训练 |
| Optimizer states | momentum + variance,8 B(fp32) | 无 | 仅训练 |
| Model states 合计 | 16 B/参数(ZeRO-Offload 口径);20 B/参数(ZeRO-Infinity 口径) | 2 B/参数,另有 KV | 部分可 |
| KV cache | 无 | 随 token 增长,按 block 分配 | 推理可,KV offload |
| Activations | 随 batch × seq 增长 | 随 batch × seq 增长,本页不展开 | 训练通常靠重计算;推理不在本页范围 |
ZeRO-Offload 的脚注明确把 activation offload 排除在范围外;ZeRO-Infinity §3 则用 activation checkpointing 把 residual states 压到可接受范围。也就是说,offload 主要处理 model states,不负责解决所有显存项。
重计算用算力换显存,并行用通信与多卡换显存,offload 用带宽换容量;先分清代价,才能判断何时该用哪条。
前向不保存全部激活,反向时重算。ZeRO-Infinity §3 给出的代价是约 0.33× 额外重计算;它压的是 activation,不减少参数、梯度与 optimizer states。
ZeRO-Infinity §3;ZeRO-Offload §2
把模型状态切到多张 GPU 的聚合显存里。容量随卡数增长,但引入 all-gather / reduce-scatter / pipeline bubble;3D 并行还需要改模型切分。ZeRO-Infinity 摘要指出,仅为了放下一万亿参数就要约 800 张 V100。
ZeRO-Infinity §1/§2;ZeRO-Offload §2
把暂时不参与计算的 state 放到 CPU 或 NVMe,需要时再取回。容量来自主机内存与 SSD,代价是 PCIe / NVLink / NVMe 带宽,以及必须做预取与重叠。
ZeRO-Offload §3/§4;ZeRO-Infinity §5
FSDP 落在“并行”和“offload”之间:它先把参数、梯度、optimizer states 分片到各 rank,减少数据并行的冗余;再打开 CPUOffload(offload_params=True) 时,才把 sharded 参数与梯度进一步下沉到 CPU。
PyTorch 源码把这点写得很直白:参数不在计算中时 offload 到 CPU,梯度也一起下沉,因此 optimizer step 在 CPU 上跑(torch/distributed/fsdp/api.py:231-242)。FSDP2 的 CPUOffloadPolicy 则明确写到“parameters, gradients, and optimizer states to CPU”,并在 all-gather 前把 shard 拷回设备(torch/distributed/fsdp/_fully_shard/_fsdp_api.py:239-255)。
三条路不是互斥的。ZeRO-Offload 本身就是 ZeRO-2 的分片 + CPU offload;ZeRO-Infinity 又在 ZeRO-3 的分片之上加 CPU/NVMe;FSDP 也可以和 activation checkpointing 叠加。真正要问的是:被搬到慢速层级的那部分,是否能在下一次使用前被取回,且取回时间小于它遮住的 compute。
训练 offload 的对象是 optimizer states / gradients / params;推理 offload 的对象是静态权重或动态 KV。对象不同,数据流和付费频率也不同。
ZeRO-Offload §3.5 的策略是:fp16 参数留在 GPU,fp16 梯度、fp32 master weight、momentum、variance 全部放 CPU,参数更新在 CPU 执行。 前向/反向仍在 GPU;反向产生的梯度可以先在 GPU 上暂存,再按参数或小批量拷到 CPU,并与剩余反向计算重叠(§4.1)。每步 CPU↔GPU 的最小通信量是 4M:2M 梯度下行 + 2M 更新后的参数上行。
ZeRO-Offload §3.5/§4.1;M 为参数个数,按每参数字节数直接换算。
ZeRO-Infinity 在这之上再加一层 NVMe。它不再要求参数常驻 GPU:partitioned params、gradients、optimizer states 都可以在 GPU / CPU / NVMe 之间移动,由 infinity offload engine 负责调度;memory-centric tiling 把大算子切成 tile,避免单个 layer 必须靠 tensor parallel 才放得下(§5.1.3/§8.5)。
DeepSpeed 的实现把这些抽象落成配置:offload_optimizer 支持 stage 1/2/3,device 可选 cpu 或 nvme,且 optimizer computation 无论如何都在 CPU(deepspeed/runtime/zero/offload_config.py:55-62);offload_param 只允许 stage 3(同文件 21-28)。
NVMe 路径不是简单 write():它走 swap_tensor 的异步 IO 与 buffer pool(deepspeed/runtime/swap_tensor/partitioned_optimizer_swapper.py:26-37),并用 pipeline_read/pipeline_write 把 tile 读写与 CPU optimizer 计算重叠(offload_config.py:84-94)。
| 层级 | 训练侧放什么 | 推理侧放什么 | 谁执行计算 | 传输介质 |
|---|---|---|---|---|
| GPU | fp16 参数、activations;FSDP 的 all-gathered shard | 权重、当前 token 的 KV block | forward / backward;attention / decode | HBM |
| CPU | fp16 梯度 + fp32 optimizer states(ZeRO-Offload);FSDP sharded params / grads + optimizer(CPU step) | 被 offload 的权重;被换出的 KV block | CPU Adam / optimizer step | PCIe 或 NVLink |
| NVMe | partitioned params + optimizer states(ZeRO-Infinity) | 本页不展开;vLLM connector 另有 disk backend | CPU 分 tile 读入,再送 GPU | NVMe → CPU → GPU |
FSDP 的 CPU offload 与 ZeRO-Offload 不是同一个切法。ZeRO-Offload 保留完整 fp16 参数副本在每张 GPU 上,只切梯度与 optimizer states;FSDP 先把参数也按 rank 切开,再把 shard 放到 CPU。
FSDP1 在初始化时要求开启 offload 的模块留在 CPU(torch/distributed/fsdp/_init_utils.py:672-673),local shard 与 _cpu_grad 预分配在 pinned CPU(_flat_param.py:1258-1265);前向 all-gather 前用 flat_param_to(device, non_blocking=True) 把 shard 拷回设备(同文件 1335-1338)。梯度在 post-backward 阶段以 non_blocking 拷到 CPU(_runtime_utils.py:969-991),并在 CPU optimizer step 前显式同步(同文件 1107-1111)。
FSDP2 的 CPUOffloadPolicy(pin_memory=True) 默认用 pinned memory,让 H2D/D2H 拷贝可以与 compute 重叠(_fsdp_api.py:248-252);shard 在 all-gather 前以 non_blocking=True 拷回设备(_fsdp_param.py:1203-1206)。
vLLM 的 cpu_offload_gb 是“每张 GPU 卸载多少 GiB 权重”。源码把它描述成一种虚拟扩大显存的方式:24GB 卡设 10,就相当于 34GB 卡;代价是每次 forward 都要从 CPU 取一部分权重(vllm/config/offload.py:23-32)。
UVA 后端把参数放在 pinned CPU 内存,再用 CUDA view 让 GPU 直接读,省掉显式 copy,但仍然消耗 PCIe 带宽(vllm/model_executor/offloader/uva.py:23-28);prefetch 后端则按层分组,用独立的 copy stream 异步 H2D,把传输藏到前面的层后面(vllm/config/offload.py:54-68、offloader/prefetch.py:171-172)。
KV cache offload 搬的是另一块内存。vLLM 用 kv_offloading_size 指定 CPU 侧的 KV 缓冲大小,默认不开启;kv_offloading_backend 支持 native 与 lmcache(vllm/config/cache.py:399-408)。
SimpleCPUOffloadConnector 的 CPU 容量默认是 8 GiB(.../simple_cpu_offload_connector.py:55),按 block 做 LRU;scheduler 只在 prefix cache 命中时请求把连续的 CPU block 取回(vllm/v1/simple_kv_offload/manager.py:371-374)。
传输走低优先级 stream,让 KV I/O 给 compute 让路(worker.py:170-173);CPU block 是 pinned 的,并且只在 GPU→CPU store 完成后才允许被 load(同文件 241-251)。
| 维度 | Weight offload | KV cache offload |
|---|---|---|
| 对象 | 模型参数,静态,大小由参数量决定 | KV block,动态,随请求与上下文长度增长 |
| 触发频率 | 每个 token 的 forward 都可能读取被卸载的层 | block 换出时写,prefix cache 命中时读;当前 decode token 的 KV 留在 GPU |
| 传输量 | 正比于被 offload 的权重,按 token 重复 | 正比于 block 大小与命中长度,按 block 计 |
| 目标 | 降低权重常驻显存,装下更大的模型 | 扩大 KV 池、提高并发或复用前缀,不降低权重占用 |
| vLLM 配置 | cpu_offload_gb;prefetch 用 offload_group_size | kv_offloading_size;kv_offloading_backend |
| 源码 | vllm/config/offload.py:23-32;offloader/uva.py:23-28 | vllm/config/cache.py:399-408;simple_kv_offload/worker.py:170-173 |
两者可以同时开,但代价相加:weight offload 让每个 token 多读权重,KV offload 让 block 换入换出多走一次 PCIe;如果模型已经能装下,或者 decode 对每 token 延迟敏感,先不要动 weight offload。
ZeRO-Offload 把每步 CPU↔GPU 流量压到 4M;是否值得,取决于传输时间与计算时间的比值,以及这段传输能否与反向/下一轮前向重叠。
按 §3.5/§4.1,ZeRO-Offload 每步只搬 4M 字节:2M 梯度从 GPU 到 CPU,2M 更新后的参数从 CPU 回 GPU。这个量比 L2L 的 28M 小 7×(28M / 4M,ZeRO-Offload §6.2.2)。通信量小是它能用 CPU optimizer 还不掉太多吞吐的前提。
全双工对称链路下,下行梯度与上行参数可同时进行,墙钟时间取 2M/BW;串行则取 4M/BW。计算量按 ZeRO-Infinity §4 Eq.7 的 8ND 口径,T 为每步 token 数,P 为有效算力。
ZeRO-Infinity §4 进一步给出了效率公式:efficiency = AIT × BW / (AIT × BW + peak),其中 AIT 是 arithmetic intensity(总计算 / 总数据移动)。带宽越低,AIT × BW 越小,通信占比越高。 §4.2 还指出一个关键差别:optimizer states 在 fwd/bwd 结束后才更新,不能像梯度那样与反向计算重叠,所以它对带宽的要求更高;NVMe 路径必须把 tile 读、CPU compute、tile 写三者流水起来(§5.2.2 补充:optimizer states 可以跨设备并行更新,但更新时点仍在 fwd/bwd 之后)。
| 指标 | 1× V100 | 10× V100 | 100× V100 |
|---|---|---|---|
| 每设备 slow memory 带宽 (GB/s) | 3.0 | 30.0 | 300.0 |
| 512 设备聚合带宽 (TB/s) | 1.5 | 15.0 | 150.0 |
| GPU-to-GPU 带宽 (GB/s) | 70.0 | 700.0 | 7000.0 |
这张表的含义是:在 V100 世代,每设备 3 GB/s 的慢速内存带宽就够;算力涨 10× 时,需要 30 GB/s。PCIe 4.0 ×16 的理论单向带宽是 31.5 GB/s,刚好压线;按 80% 有效系数算是 25.2 GB/s,低于 30 GB/s。 ZeRO-Infinity §9 给出的对照是 Summit 用 NVLink 把 V100 连到 CPU 内存,达到 40 GB/s/GPU;这解释了为什么论文强调“慢速内存”不等于“慢速链路”,NVLink 级别的 CPU 链路可以满足 10× V100 的要求。
NVLink 本身主要是 GPU↔GPU 链路;CPU↔GPU 是否走 NVLink,取决于平台(Summit 是论文给出的例子)。在普通 PCIe 服务器上,offload 的重叠能力就取决于 pinned memory、copy stream 和 kernel 是否能同时跑。ZeRO-Offload 用 DPU(delayed parameter update)把 optimizer update 与下一轮 forward 重叠,代价是 1-step staleness(§6.2.4);ZeRO-Infinity 用 tile pipeline 把 NVMe 读写、CPU Adam 和 GPU compute 串起来;FSDP 用 non_blocking 拷贝;vLLM 用独立 copy stream 与低优先级 KV stream。
结论:在这个 batch 与算力下,4M 的传输只占计算时间的 2.42%,PCIe 4.0 不是瓶颈。注意这还没有把 CPU Adam 的 optimizer 时间算进去;Table 4 显示 10B 时 CPU-Adam 单次 2.57 s,GPU Adam 为 1.00 s。
口径说明:140/980 GB 是整个模型的量,单卡放不下 140 GB 参数;实际 70B 需要多卡或 MP 分片,ZeRO-2/FSDP 还会把 optimizer states 按 DP rank 再切。交互器显示的是整模型总量,不是单卡实际值。
结论:模型变小、batch 变小、链路降到 PCIe 3.0 后,重叠传输已经吃掉 77.50% 的计算时间;如果串行,传输 4.13 s 大于计算 2.66 s,offload 直接变成瓶颈。这就是“小模型不划算”的定量版本。
简化与未覆盖:只算 model states,且为整模型总量,未按 DP/TP/PP 再分片;用十进制 GB;假设带宽稳定、无竞争;计算按 8ND,不含 attention 的 seq² 项与通信;未计 CPU Adam、activation、内存碎片与 NUMA 跨 socket。两组手算都按同一公式,JS 输出保留两位小数,与手算逐位一致。
把两条数据流画在同一张分层图上,才能看出谁在重复付费:训练搬梯度与参数,weight offload 搬权重,KV offload 搬 block。
这些数字都带硬件与 batch 条件;换链路、换模型规模或换并行策略,结论会变。
| 指标 | 条件 | 数值 | 出处 |
|---|---|---|---|
| 最大可训练模型 | 单张 V100 32GB | 13B;PyTorch DDP 基线 1.4B | §6.2.1 |
| 最大可训练模型 | 单台 DGX-2,16×V100 | 70B;Megatron 基线 15B,原文称 4.5× | §6.2.1 |
| 吞吐 | 单卡 V100,10B | 40 TFLOPS/GPU;PyTorch 1.4B 为 30 | Abstract、§6.2.2 |
| 吞吐 | 1–15B,单 DGX-2 | 最高 1.33× / 1.11× / 1.64× 于 PyTorch / ZeRO-2 / Megatron | §6.2.2 |
| 对比 L2L | 单卡,billion-scale | 平均高 14%(相对 L2L),最高 22% | §6.2.2 |
| DPU | GPT-2,小 micro-batch | 1.12–1.59× | §6.2.4 |
| CPU-Adam 延迟 | 1B / 10B | 0.22 s / 2.57 s;PT-CPU 1.39 s / 14.76 s;PT-GPU 0.10 s / 1.00 s | Table 4 |
| 指标 | 条件 | 数值 | 出处 |
|---|---|---|---|
| 最大模型 | 512×V100(32 台 DGX-2) | §1 贡献列表称 32T;§8.2 实测图描述到 20T,原文称 40× 于 3D 并行 | §1/§8.2/Figure 5a |
| 吞吐 | 512×V100 | 25+ PFLOPS;原文标注 40% of peak | Abstract/§1 |
| 单节点 | 单台 DGX-2,16×V100 | 1T 参数,无模型并行 | Abstract/Figure 5c |
| 模型规模对比 | 单节点,vs 数据并行 | 1T vs 1.4B,原文称 700× | §8.4 |
| 吞吐 | 4 节点 64×V100,1T | 2.8+ PFLOPS;44 TFLOPS/GPU | §8.3 |
| 吞吐 | 512×V100,20T | 34 TFLOPS/GPU(10T 为 43) | §8.2 |
ZeRO-Infinity §8.2 还给出一个边界信号:20T 时吞吐从 10T 的 43 TFLOPS/GPU 降到 34 TFLOPS/GPU,原因不是 NVMe 带宽,而是每 GPU 的 batch 太小、CPU 内存放不下 activation checkpoint。offload 把容量问题转移到 CPU/NVMe 后,新的瓶颈会出现在主机内存与 batch 上。
offload 是把容量问题换成带宽问题;带宽不够、无法重叠或 CPU 侧成为瓶颈时,收益为负。
ZeRO-Offload 的 4M/步是理论最小值;如果每步 token 少、链路只有 PCIe 3.0,传输时间可能超过计算时间。ZeRO-Infinity §2 对 ZeRO-Offload 的评价是:它需要较大的 batch 才能保持效率,原因之一就是 PCIe 带宽有限。
ZeRO-Infinity §2;本页 §04 手算 2
模型能放进 GPU 时,offload 只增加拷贝与 CPU optimizer 开销。ZeRO-Offload Table 4 的实测是:1B 时 CPU-Adam 0.22 s,GPU Adam 0.10 s;10B 时 2.57 s 对 1.00 s。offload 的收益来自“否则装不下”,不是来自“CPU 更快”。
ZeRO-Offload Table 4
Weight offload 每 token 都要读被卸载的权重;UVA 零拷贝只是省掉显式 copy,带宽仍然要付。Prefetch 只有在“传输能被当前层 compute 盖住”时才有用;decode 每 token 的 compute 很短,几十 GB 的权重搬运很难藏住。KV offload 改善的是 KV 容量与 prefix 复用,不是 decode 的单 token 延迟。
vLLM 源码 config/offload.py:23-32;offloader/uva.py:23-28
CPU offload 依赖 pinned memory;DeepSpeed 的 pin_memory 默认开启,并注明它需要足够的 memlock 限额与常驻内存(offload_config.py:45-52/75-82)。多 socket 机器上,GPU 连到哪个 CPU socket、CPU-Adam 线程落在哪个 NUMA 节点,都会改变有效带宽与 optimizer 时间。offload_config.py 本身不绑定 NUMA;DeepSpeed launcher 提供可选的 --bind_cores_to_rank / --bind_core_list(deepspeed/launcher/launch.py:83-88),deepspeed/utils/numa.py:104 用 numactl 做绑定。
DeepSpeed offload_config.py;NUMA 影响为 推断(依据:pinned host memory 与 CPU Adam 多线程实现;offload_config.py 未绑定,launcher 的绑定是可选项)
| 场景 | 建议 | 依据 |
|---|---|---|
| 模型 + batch 已能放进 GPU | 不要 offload;先调 batch 或换并行 | CPU Adam 慢于 GPU Adam(Table 4) |
| 单卡/单节点装不下,但 PCIe 4.0 以上且 batch 足够大 | 训练侧优先 ZeRO-Offload;需要更大规模再加 ZeRO-Infinity 的 NVMe 层 | §6.2.1/§8.2 的容量结果 |
| 多卡已经很多(64/128) | 先比较 ZeRO-2;offload 的 CPU↔GPU 开销可能不再划算 | ZeRO-Offload §6.2.3 |
| 推理只是权重装不下、并发不高 | 可试 weight offload;优先 prefetch,避免 UVA 每 token 直读 | vLLM cpu_offload_gb / prefetch 源码 |
| 推理要长上下文、多请求复用前缀 | 看 KV offload;它扩大 KV 池,不解决权重占用 | vLLM kv_offloading_size 与 connector 源码 |
| decode 延迟是硬 SLO | 不要把权重放到 PCIe 后面;KV offload 也只适合容量换吞吐 | weight offload 的 per-token 传输模型 |
不要把训练 offload 的数字外推到推理。训练每步搬一次梯度与参数,推理 weight offload 每个 token 搬一次被卸载的权重,KV offload 每个 block 搬一次 KV;三者的频率、体积、可重叠性都不同。ZeRO-Offload 的 4M/步和 vLLM 的 cpu_offload_gb 不是同一个量。
也不要把 CPU offload 的数字外推到 NVMe。ZeRO-Infinity 的 NVMe 层需要 tile 读写、CPU 内存中转与 fragmentation 控制;§5.2.2 明确提醒,NVMe↔CPU↔GPU 的往返会造成 CPU/GPU 内存碎片,容量够也可能 OOM。NVMe 适合极端规模,不适合作为默认的第一层。