CPU Offloading
System · 训练与推理 · 2021–2026

CPU Offloading:把显存墙拆成 GPU / CPU / NVMe 三层

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 · ZeRO-Infinity · PyTorch FSDP · vLLM ZeRO-Offload ↗ ZeRO-Infinity ↗ FSDP 文档 ↗ vLLM 源码 @ e740bad ↗
TL;DR · 30 秒抓住核心

显存墙的主因不是权重本身,而是训练状态。按 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 通常不值得。

13B
单张 32GB V100 可训练
PyTorch DDP 基线为 1.4B
40 TFLOPS/GPU
单卡 V100、10B 模型的实测吞吐
PyTorch 1.4B 为 30 TFLOPS
20T
512×V100 上 §8.2 实测描述到 20T 参数
§1 贡献列表称 32T
01

显存墙不是参数放不下,而是训练状态放不下

先算清一个参数在混合精度 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 口径,并在手算里注明。

Mstates = 16 × Mparams
70B: 16 × 70 = 1,120 GB
参数本身仍需: 2 × 70 = 140 GB

按十进制 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,与权重显存分开核算。

综合表 1 · 训练与推理的显存构成(依据 ZeRO-Offload §2、ZeRO-Infinity §3、vLLM 源码)
组成训练:混合精度 Adam推理:BF16是否可 offload
参数fp16 副本 2 B + fp32 master 4 B,每参数bf16 2 B,每参数训练与推理都可
梯度fp16 2 B;ZeRO-Infinity 另计 fp32 梯度 4 B无仅训练
Optimizer statesmomentum + 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,不负责解决所有显存项。

02

重计算、并行、offload:三条路把成本放在不同地方

重计算用算力换显存,并行用通信与多卡换显存,offload 用带宽换容量;先分清代价,才能判断何时该用哪条。

重计算

Activation checkpointing

前向不保存全部激活,反向时重算。ZeRO-Infinity §3 给出的代价是约 0.33× 额外重计算;它压的是 activation,不减少参数、梯度与 optimizer states。

ZeRO-Infinity §3;ZeRO-Offload §2

并行

DP / TP / PP / 3D

把模型状态切到多张 GPU 的聚合显存里。容量随卡数增长,但引入 all-gather / reduce-scatter / pipeline bubble;3D 并行还需要改模型切分。ZeRO-Infinity 摘要指出,仅为了放下一万亿参数就要约 800 张 V100。

ZeRO-Infinity §1/§2;ZeRO-Offload §2

Offload

CPU / NVMe 分层

把暂时不参与计算的 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。

03

训练把状态分层,推理把权重和 KV 分开卸载

训练 offload 的对象是 optimizer states / gradients / params;推理 offload 的对象是静态权重或动态 KV。对象不同,数据流和付费频率也不同。

3.1 训练侧:ZeRO-Offload 的 CPU 层,ZeRO-Infinity 的 NVMe 层

ZeRO-Offload §3.5 的策略是:fp16 参数留在 GPU,fp16 梯度、fp32 master weight、momentum、variance 全部放 CPU,参数更新在 CPU 执行。 前向/反向仍在 GPU;反向产生的梯度可以先在 GPU 上暂存,再按参数或小批量拷到 CPU,并与剩余反向计算重叠(§4.1)。每步 CPU↔GPU 的最小通信量是 4M:2M 梯度下行 + 2M 更新后的参数上行。

Vstep = 2Mgrads + 2Mparams = 4M bytes
CPU 侧持有: 2M (fp16 grads) + 12M (fp32 master/m/v) = 14M bytes
GPU 侧保留: 2M bytes

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)。

综合表 2 · 三层 offload 的对象与执行位置(DeepSpeed f47dc3e / PyTorch 2ee7a63)
层级训练侧放什么推理侧放什么谁执行计算传输介质
GPUfp16 参数、activations;FSDP 的 all-gathered shard权重、当前 token 的 KV blockforward / backward;attention / decodeHBM
CPUfp16 梯度 + fp32 optimizer states(ZeRO-Offload);FSDP sharded params / grads + optimizer(CPU step)被 offload 的权重;被换出的 KV blockCPU Adam / optimizer stepPCIe 或 NVLink
NVMepartitioned params + optimizer states(ZeRO-Infinity)本页不展开;vLLM connector 另有 disk backendCPU 分 tile 读入,再送 GPUNVMe → 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)。

3.2 推理侧:weight offload 与 KV cache offload 是两笔账

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)。

综合表 3 · 推理侧两种 offload 的差别(vLLM e740bad 源码)
维度Weight offloadKV 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_sizekv_offloading_size;kv_offloading_backend
源码vllm/config/offload.py:23-32;offloader/uva.py:23-28vllm/config/cache.py:399-408;simple_kv_offload/worker.py:170-173

两者可以同时开,但代价相加:weight offload 让每个 token 多读权重,KV offload 让 block 换入换出多走一次 PCIe;如果模型已经能装下,或者 decode 对每 token 延迟敏感,先不要动 weight offload。

04

4M 字节不是瓶颈,前提是它能被算力盖住

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 还不掉太多吞吐的前提。

txfer,serial = 4M / BW
txfer,overlap = 2M / BW
tcompute = 8 × M × T / P

全双工对称链路下,下行梯度与上行参数可同时进行,墙钟时间取 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 之后)。

Table 3(ZeRO-Infinity 原文)· 512 设备集群保持效率所需的慢速内存带宽
指标1× V10010× V100100× V100
每设备 slow memory 带宽 (GB/s)3.030.0300.0
512 设备聚合带宽 (TB/s)1.515.0150.0
GPU-to-GPU 带宽 (GB/s)70.0700.07000.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。

GPU 保留
140 GB
CPU 接管
980 GB
GPU 省下
980 GB
每步搬运
280 GB
重叠传输
5.56 s
计算时间
229.38 s
传输 / 计算
重叠传输占计算时间的 2.42%;串行传输需 11.11 s。 传输可被计算时间盖住(2.42%)。
口径:整模型总量,未按 DP/TP/PP 再分片;16 B/参数;PCIe 带宽为理论单向值 × 0.8;计算按 8ND;默认算力 40 TFLOPS/GPU 取自 ZeRO-Offload 在 10B 单卡 V100 的实测(Abstract)。未计 CPU Adam、activation、通信与 NUMA。
手算 1:70B、16,384 tokens、PCIe 4.0 ×16 对应滑块默认值
M = 70B;每参数 16 B;GPU 只留 fp16 参数(2 B/参数)
GPU 保留 = 2 × 70 = 140 GB
CPU 接管 = (16 − 2) × 70 = 14 × 70 = 980 GB
GPU 省下 = 980 GB(占 1,120 GB 的 (16−2)/16 = 87.5%)
每步搬运 = 4 × 70 = 280 GB(2M 梯度下行 + 2M 参数上行)
PCIe 4.0 有效单向带宽 = 31.5 × 0.8 = 25.2 GB/s
重叠传输 = 2 × 70 / 25.2 = 5.56 s
串行传输 = 4 × 70 / 25.2 = 11.11 s
计算时间 = 8 × 70 × 16.384 / 40 = 229.376 ≈ 229.38 s
传输/计算 = 5.5556 / 229.376 = 2.42%

结论:在这个 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 再切。交互器显示的是整模型总量,不是单卡实际值。

手算 2:13B、1,024 tokens、PCIe 3.0 ×16 小模型 + 小 batch 的边界
M = 13B;每参数 16 B;GPU 只留 fp16 参数(2 B/参数)
GPU 保留 = 2 × 13 = 26 GB
CPU 接管 = 14 × 13 = 182 GB
GPU 省下 = 182 GB(占 208 GB 的 87.5%)
每步搬运 = 4 × 13 = 52 GB
PCIe 3.0 有效单向带宽 = 15.75 × 0.8 = 12.6 GB/s
重叠传输 = 2 × 13 / 12.6 = 2.06 s
串行传输 = 4 × 13 / 12.6 = 4.13 s
计算时间 = 8 × 13 × 1.024 / 40 = 2.6624 ≈ 2.66 s
传输/计算 = 2.0635 / 2.6624 = 77.50%

结论:模型变小、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 输出保留两位小数,与手算逐位一致。

05

ZeRO-Offload 每步一次往返,推理每 token 或每 KV 块一次搬运

把两条数据流画在同一张分层图上,才能看出谁在重复付费:训练搬梯度与参数,weight offload 搬权重,KV offload 搬 block。

训练:ZeRO-Offload / ZeRO-Infinity 推理:vLLM weight / KV offload GPU 训练:fp16 参数 2M + activations forward / backward 都在 GPU 推理:权重 + 当前 KV blocks attention / decode 在 GPU CPU 训练:fp16 梯度 2M + fp32 optimizer states 12M CPU Adam 更新(ZeRO-Offload) FSDP:sharded params / grads + optimizer 推理:offloaded weights / KV blocks UVA 零拷贝或异步 prefetch KV 按 block 换入 / 换出 NVMe 训练:partitioned params + optimizer states(ZeRO-Infinity) 按 tile 读入 CPU,再送 GPU 推理:KV offload 以 CPU 为主;disk backend 另算 本页不展开 NVMe 推理路径 grad ↓ param ↑ tile ↓ tile ↑ W/KV ↑ KV ↓
图 1 · 训练每步一次往返(梯度下行、参数上行);推理 weight offload 每个 token 重读被卸载的权重,KV offload 只在 block 换出或 prefix 命中时搬运。箭头方向表示数据移动,粗细不代表带宽。

训练一步的时序(ZeRO-Offload)

  1. GPU 用常驻的 fp16 参数做 forward,不需要访问 CPU。
  2. backward 产生梯度;梯度可以在计算过程中按参数或小批量拷到 CPU,与剩余反向图重叠(§4.1)。
  3. 多卡时先做 reduce-scatter,每张卡只把自己负责的梯度分片写到 CPU(§4.2)。
  4. CPU 上用 CPU-Adam 更新 fp32 master weight、momentum、variance,并得到更新后的参数分片。
  5. 参数分片从 CPU 回 GPU,再 all-gather 成完整的 fp16 参数,供下一轮 forward 使用。
  6. DPU 打开时,optimizer update 可以与下一轮 forward 重叠,代价是参数晚一步生效(1-step staleness,§6.2.4)。

推理的两种时序

06

论文实测:单卡 13B、单节点 70B、512 卡 20T

这些数字都带硬件与 batch 条件;换链路、换模型规模或换并行策略,结论会变。

综合表 4 · ZeRO-Offload 实测(原文 Table 2 / Table 4 / §6.2)
指标条件数值出处
最大可训练模型单张 V100 32GB13B;PyTorch DDP 基线 1.4B§6.2.1
最大可训练模型单台 DGX-2,16×V10070B;Megatron 基线 15B,原文称 4.5×§6.2.1
吞吐单卡 V100,10B40 TFLOPS/GPU;PyTorch 1.4B 为 30Abstract、§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
DPUGPT-2,小 micro-batch1.12–1.59×§6.2.4
CPU-Adam 延迟1B / 10B0.22 s / 2.57 s;PT-CPU 1.39 s / 14.76 s;PT-GPU 0.10 s / 1.00 sTable 4
综合表 5 · ZeRO-Infinity 实测(原文 Table 1 / §8)
指标条件数值出处
最大模型512×V100(32 台 DGX-2)§1 贡献列表称 32T;§8.2 实测图描述到 20T,原文称 40× 于 3D 并行§1/§8.2/Figure 5a
吞吐512×V10025+ PFLOPS;原文标注 40% of peakAbstract/§1
单节点单台 DGX-2,16×V1001T 参数,无模型并行Abstract/Figure 5c
模型规模对比单节点,vs 数据并行1T vs 1.4B,原文称 700×§8.4
吞吐4 节点 64×V100,1T2.8+ PFLOPS;44 TFLOPS/GPU§8.3
吞吐512×V100,20T34 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 上。

07

什么时候 offload 不划算:小模型、小 batch、延迟敏感、NUMA

offload 是把容量问题换成带宽问题;带宽不够、无法重叠或 CPU 侧成为瓶颈时,收益为负。

边界 1

PCIe 带宽压不住

ZeRO-Offload 的 4M/步是理论最小值;如果每步 token 少、链路只有 PCIe 3.0,传输时间可能超过计算时间。ZeRO-Infinity §2 对 ZeRO-Offload 的评价是:它需要较大的 batch 才能保持效率,原因之一就是 PCIe 带宽有限。

ZeRO-Infinity §2;本页 §04 手算 2

边界 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

边界 3

延迟敏感的推理

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

边界 4

NUMA 与主机内存

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 的绑定是可选项)

一张决策表

综合表 6 · 什么时候优先考虑 offload(依据本页引用的论文与源码)
场景建议依据
模型 + 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 适合极端规模,不适合作为默认的第一层。