这个问题问到了 ZeRO 之所以存在的根本原因。答案很简单:优化器状态里存的不是一份参数,而是好几份和参数同样大的副本。
假设模型有 1 个参数 w,用 Adam 优化器训练。训练时需要存这些东西:
| 存储项 | 含义 | 大小 |
|---|---|---|
| 参数 | 模型权重本身 | 1 份 |
| 梯度 | 反向传播算出的梯度 | 1 份 |
| 一阶动量 | 梯度的指数移动平均 | 1 份 |
| 二阶动量 | 梯度平方的指数移动平均 | 1 份 |
所以每个参数实际需要 4 份存储(参数 + 梯度 + 两个动量)。
如果模型有 N 个参数,用 FP32(4 字节)存储:
其中优化器状态(m 和 v)占了 8N 字节,也就是一半。
以一个 10B(100 亿)参数的模型为例:
| 存储项 | 大小 |
|---|---|
| 参数(FP32) | 40 GB |
| 梯度(FP32) | 40 GB |
| 一阶动量(FP32) | 40 GB |
| 二阶动量(FP32) | 40 GB |
| 总计 | 160 GB |
一张 A100 只有 80 GB 显存,光模型状态就装不下,更别说还要留空间给激活值了。
而其中优化器状态就占了 80 GB,正好是一整张 A100 的容量。
根本原因在于:Adam 这类自适应优化器,需要为每个参数单独维护历史信息。
这两个量都是和参数一一对应的,参数量有多大,它们就有多大。而且它们必须用 FP32 存,因为需要高精度累积,否则训练会不稳定。
ZeRO 的核心洞察就是:参数和梯度在每个 GPU 上都必须有完整副本(计算需要),但优化器状态其实不需要每个 GPU 都存一份。
因为在标准数据并行中,每个 GPU 算完梯度后,会通过 All-Reduce 把所有 GPU 的梯度求和平均,得到完全相同的梯度。既然梯度都一样,那么用这个梯度去更新优化器状态,每个 GPU 算出来的新优化器状态也是一模一样的。
也就是说,每个 GPU 都在重复存储和计算完全相同的优化器状态——这是巨大的浪费。
ZeRO-1 做的就是:把优化器状态切成 N 份,每个 GPU 只存自己那份。更新参数时,每个 GPU 只更新自己负责的那部分参数,然后再把更新后的参数广播给其他 GPU。
这样:
优化器状态显存占用:从 8N 降到 $8N/N_{GPU}$
参数和梯度暂时还是完整的(ZeRO-2 再切梯度,ZeRO-3 再切参数)
优化器状态之所以占显存,是因为 Adam 为每个参数都存了两个 FP32 的历史动量,加上参数和梯度,每个参数实际需要 4 份存储。 而在数据并行中,这些优化器状态在每个 GPU 上都是完全重复的,ZeRO 就是把这部分重复彻底消除掉。
评论专区
评论加载中...登录后即可发表评论