使用 DeepSpeed 进行内存优化

随着深度学习模型和大规模计算的复杂性不断增加,内存优化在训练过程中至关重要。DeepSpeed 提供内存节省技术,例如卸载、梯度检查点和 ZeRO。开发人员可以基于标准硬件中的内存节省技术来训练如此巨大的模型。此外,这些技术还能够训练迄今为止受硬件限制的模型。

DeepSpeed 不仅在研究领域取得了令人瞩目的进步,而且在行业领域也同样取得了进步,因此是深度学习从业者不可替代的工具。这意味着通过这些策略,您可以让您的模型消耗更少的内存,并真正突破硬件的极限。

为什么要进行内存优化?

内存优化是训练深度学习模型最关键的组成部分之一。在 GPT 和 BERT 等数十亿的参数下,在可用硬件上进行训练时必须进行有效的内存管理。 DeepSpeed 是一个用于训练深度学习模型的开源库,具有 ZeRO 优化器、卸载技术和梯度检查点,可避免在训练中大量使用。

深度学习中的内存问题

深度学习模型的大小和复杂性最近有所增加。如此庞大的模型需要大量内存来训练。DeepSpeed 是由微软开发的深度学习优化库,为这些挑战提供了强大的解决方案。

深度学习模型有点艺术性,因为它们代表了一些新的东西,比如随着模型规模的增长而出现的内存相关问题。一些最常见的内存相关问题包括:

  • 模型参数 − 大型模型(例如 GPT-3)具有数千亿个参数,因此需要大量内存来存储。
  • 梯度 −训练期间计算每个参数的梯度也必须计算并保存在内存中,这会消耗更多内存。
  • 激活图 − 前向传递过程中产生的所有中间值都需要存储,直到反向传递仅用于梯度计算,这称为激活图。
  • 批次大小 − 较大的批次大小会增加收敛速度,但会消耗更多内存。
  • 数据并行性 − 在多个 GPU 之间分叉数据是缩短训练时间的好策略,但毫无疑问,除非得到控制,否则它确实会占用大量内存。

除非发现这些陷阱,否则即使在消费级硬件上也不可能训练大型模型。 DeepSpeed 通过使用创新的内存节省技术克服了这些问题。

DeepSpeed 的内存优化技术

DeepSpeed 在训练模型时有多种方法来优化内存使用。一些方法包括 ZeRO(代表零冗余优化器)、梯度检查点和激活重新计算。

1. 零冗余优化器 (ZeRO)

ZeRO 主要关注内存优化,此时优化器的状态、梯度和模型参数的冗余副本将被删除。ZeRO 经历了以下三个阶段:

  • 阶段 1 − 在 GPU 之间分片优化器状态,​​每个 GPU 存储一部分优化器状态。
  • 阶段 2 − 随着梯度在 GPU 之间分片,内存进一步减少。
  • 阶段 3 −模型参数被分片,现在模型可以训练多达一万亿个参数。

示例

import deepspeed

model = MyModel() # 你的 dl 模型
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)

# ZeRO 的 DeepSpeed 配置
ds_config = {
 "train_batch_size": 8,
 "zero_optimization": {
    "stage": 2,   # adjust the stage of ZeRO here
    "allgather_partitions": True,
    "reduce_scatter": True,
    "allgather_bucket_size": 5e8,
    "overlap_comm": True,
    "contiguous_gradients": True
 }
}
# 初始化 DeepSpeed
model_engine, optimizer, _, _ = deepspeed.initialize(
  model=model,
  optimizer=optimizer,
  config_params=ds_config
)

# 训练循环
for batch in train_dataloader:
    loss = model_engine(batch)
model_engine.backward(loss)
model_engine.step()

您会注意到内存使用率要低得多,尤其是对于大型模型。然后,内存分析器可以突出显示 ZeRO 优化开始的位置。

2. 梯度检查点

梯度检查点通过不在缓冲区中存储前向传递期间的激活来减少内存。而是在后向传递中重建这些激活,从而牺牲一些计算来节省一些内存。

示例

import torch
from torch.utils.checkpoint import checkpoint

def custom_forward(*inputs):
    return model(*inputs)

# 梯度检查点
outputs = checkpoint(custom_forward, input_data)
loss = criterion(outputs, labels)
loss.backward()

在这种情况下,节省的内存将取决于中间激活的大小。

3. 卸载技术

DeepSpeed 还提供了另一种形式的卸载。它允许您将模型的各个部分(如优化器状态和梯度)移动到 CPU 甚至 NVMe 存储,从而释放 GPU 内存以供其他用途。

CPU 卸载

DeepSpeed 让我们将优化器状态和梯度卸载到 CPU。这也释放了宝贵的 GPU 内存。如果 GPU 上的内存有限,但 CPU 上的内存相当多,那么这真的很有用。

示例

ds_config = {
    "train_batch_size": 8,
    "zero_optimization": {
        "stage": 2,
        "offload_optimizer": {
            "device": "cpu",
            "pin_memory": True
        },
        "offload_param": {
            "device": "cpu",
            "pin_memory": True
        }
    }
}
model_engine, optimizer, _, _ = deepspeed.initialize(
   model=model,
   optimizer=optimizer,
   config_params=ds_config
)

由于将传输卸载到 CPU 涉及设备间的成本,因此训练相对较慢,但模型大小原本无法适应内存受限的 GPU。

NVMe 卸载

对于大型模型来说,这还不够。DeepSpeed 还将优化器状态和梯度卸载到 NVMe 存储。这将增加可以进行教学的模型的规模。

示例

ds_config = {
    "train_batch_size": 8,
    "zero_optimization": {
        "stage": 2,
        "offload_optimizer": {
            "device": "nvme",
            "nvme_path": "/local_nvme"
        },
        "offload_param": {
            "device": "nvme",
            "nvme_path": "/local_nvme"
        }
    }
}
model_engine, optimizer, _, _ = deepspeed.init(
   model=model,
   optimizer=optimizer,
   config_params=ds_config
)

使用 NVMe 卸载将能够训练大量模型,尽管速度在很大程度上取决于 NVMe 驱动器的 I/O 速度。

内存优化案例研究

让我们讨论一些使用 DeepSpeed − 进行内存优化的真实案例研究

案例研究 1:使用 ZeRO 优化训练 GPT-2

使用 DeepSpeed,一个研究团队将这个 15 亿参数 GPT-2 模型的训练缩小到消费级 GPU。使用 ZeRO 第 3 阶段,可以在 4 个 NVIDIA RTX 3090 GPU 上进行训练,每个 GPU 总共有 24 GB 的内存。如果无法使用 ZeRO 做到这一点,那么训练将无法进行,因为该模型需要每个 GPU 超过 50 GB 的内存。

案例研究 2:使用 NVMe 为 175B 参数模型卸载

微软利用 DeepSpeed 的卸载功能,在内存有限的 GPU 集群上训练了一个 1750 亿参数模型。在模型训练期间,使用几乎没有内存瓶颈来卸载优化器状态和参数,这表明即使在 GPU 资源有限的情况下,卸载也可以为超大型模型让路。