通过 0/1 Adam 优化大规模训练的通信效率
注意! 1) 基于 NCCL 的实现要求 PyTorch >= 1.8(当您有 64 个或更多 GPU 时,NCCL >= 2.8.3)。详情请参阅下文。 2) 尽管 0/1 Adam 兼容 FP16 和 FP32,但目前我们仅验证了混合精度/FP16 训练下的收敛性。 3) 目前基于 MPI 的实现不兼容流水线并行。 4) 频繁加载检查点可能会损害 0/1 Adam 的收敛性。详情请参阅下文。
在本教程中,我们将介绍 DeepSpeed 的 0/1 Adam 优化器,它可以在通信受限的集群上提高模型训练速度,特别是对于通信密集型大型模型。例如,它能够在不影响端到端模型精度的情况下,将 BERT-large 预训练的总通信量减少高达 26 倍。与 1-bit Adam 优化器相比,0/1 Adam 通过自适应方差状态冻结提供了一种更灵活的方式来使用压缩通信。此外,它允许计算节点在训练期间使用一种称为 1-bit 同步的技术跳过通信轮次,而不会影响收敛速度。我们有一篇论文提供了技术细节,包括算法、系统实现和评估。
为了说明 0/1 Adam 优化器的好处和用法,我们以 BERT 预训练任务为例。有关此任务的更多详细信息,请参阅教程。
1. 概述
1.1 安装 DeepSpeed 的先决条件
如果您还没有 DeepSpeed 仓库的副本,请立即克隆它并检出包含 BERT 预训练示例的 DeepSpeedExamples 子模块。
git clone https://github.com/deepspeedai/DeepSpeed
cd DeepSpeed
git submodule update --init --recursive
cd DeepSpeedExamples/
1.2 0/1 Adam 的先决条件
1.2.1 基于 NCCL 的实现
在 DeepSpeed 中,我们引入了使用 PyTorch 分布式的 NCCL 后端进行压缩通信的系统实现。此实现比下面的基于 MPI 的实现提供了更好的性能和可用性。因此,我们强烈建议用户选择此实现。
注意! 此基于 NCCL 的实现需要 PyTorch >= 1.8。当您有 64 个或更多 GPU 时,它还需要 NCCL >= 2.8.3 以避免某些 NCCL 运行时错误。目前(2021/03/16)PyTorch 尚未正式支持 NCCL 2.8.3。我们使用的解决方案是通过 LD_PRELOAD
巧妙地引入 NCCL 2.8.3:1) 安装 NCCL 2.8.3。这适用于我们的 CUDA 11 系统:apt-get install -y libnccl2=2.8.3-1+cuda11.0 libnccl-dev=2.8.3-1+cuda11.0
。2) 将 LD_PRELOAD
设置为库路径。这对我们有效:LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libnccl.so.2.8.3
。要确认 LD_PRELOAD
正在工作,如果您设置了 NCCL_DEBUG=INFO
,可以在 NCCL 日志中看到它使用的版本,它应该显示:NCCL version 2.8.3+cuda11.0。
1.2.2 基于 MPI 的实现
对于此实现,我们依赖消息传递接口 (MPI) 来实现高级通信原语。
我们将必要的依赖项打包在 DeepSpeed docker 镜像中。但是,如果您使用不同的构建系统,请在您的系统上安装 MPI 和 mpi4py。要安装先决条件,请运行
pip install deepspeed[1bit_adam]
我们已经使用 MVAPICH2-GDR 库测试了 CUDA 感知 MPI 通信。但是,任何 CUDA 感知通信库,包括 OpenMPI,都应该与这些示例配合良好。
使用 deepspeed
启动器启动 0/1 Adam 的示例命令如下:
deepspeed --launcher=[mvapich|openmpi] script.py
请注意,对于基于 MPI 的 0/1 Adam 实现,使用 deepspeed
启动器时,--launcher=[mvapich|openmpi]
标志是必需的。
或者,也可以使用标准 mpirun 启动器,如下所示:
mpirun -np [num processes] -ppn [num GPUs on each node] -hostfile [hostfile] [MPI flags] python [training_script.py]
1.2.3 压缩实现
此后端提供了一种抽象 one-bit 优化器通用部分的方法,并使用 DeepSpeed 自定义操作构建器实现了与加速器相关的部分。要使用此 CompressedBackend
,您应确保当前加速器支持 PackbitsBuilder
,以便它可以加载以在高性能地在浮点数和字节数据类型之间进行打包和解包,这在 one-bit 算法中会用到。一个示例可以在 Deepspeed/op_builder/xpu/packbits.py
中找到。此方法不需要基于 NCCL 或 MPI 的通信库。它将自动使用 deepspeed/comm
中加速器选择的默认通信库。
1.3 0/1 Adam 算法
0/1 Adam 算法的详细描述请参见我们的论文。
1.4 0/1 Adam 的配置
通过如下设置优化器配置选项即可使用 0/1 Adam 功能。下面显示了一个示例 json 配置文件。
{
"train_batch_size": 4096,
"train_micro_batch_size_per_gpu": 16,
"optimizer": {
"type": "ZeroOneAdam",
"params": {
"lr": 1e-3,
"weight_decay": 0.01,
"bias_correction": false,
"var_freeze_step": 1000,
"var_update_scaler": 16,
"local_step_scaler": 1000,
"local_step_clipper": 16,
"cuda_aware": false,
"comm_backend_name": "nccl"
}
},
"gradient_clipping": 1.0,
"fp16": {
"enabled": true,
"loss_scale": 0,
"initial_scale_power": 16
}
}
请注意,为了支持 0/1 Adam 功能,已添加了新参数 var_freeze_step
、var_update_scaler
、local_step_scaler
、local_step_clipper
、cuda_aware
和 comm_backend_name
。
var_freeze_step 是更新方差的最新步骤。使用 0/1 Adam 论文中的符号,它表示 $\max{i |
i \in \mathcal{T}_v}$。请注意,这与 1-bit Adam 中的 freeze_step 不同。var_freeze_step 通常是学习率预热的最后一步,因此不需要调优。请注意,此超参数是可选的。在实践中,我们可以通过将其设置为足够大的数字(大于总步数)来避免调优此参数。此后,0/1 Adam 仍然可以在不影响收敛速度的情况下实现显著的通信量减少。 |
var_update_scaler
是更新方差的间隔。请注意,方差的更新策略遵循指数规则。形式上,如果我们将 $k_j$ 表示为第 $j$ 次方差更新发生的步骤,那么 $k_{j+1} - k_j = 2\cdot\exp{\lfloor j/\kappa\rfloor}$(详细解释请参阅 0/1 Adam 论文),而 var_update_scaler
表示该表达式中的 $\kappa$ 因子。在实践中,我们发现其默认值 (16) 在大多数任务中都能很好地工作,包括 BERT-Base/Large 预训练、GPT 预训练和 ImageNet 训练。
local_step_scaler
和 local_step_clipper
是 0/1 Adam 中基于学习率的本地步长策略的两个超参数。形式上,如果我们将 $k_j$ 表示为所有 worker 之间第 $j$ 次同步发生的步骤,那么 $k_{j+1} - k_j = 2\cdot\exp{\min(\lfloor j/\alpha\rfloor, \beta )}$(详细解释请参阅 0/1 Adam 论文)。根据这些符号,local_step_scaler
和 local_step_clipper
分别表示 $\alpha$ 和 $\beta$。非正式地说,local_step_scaler
决定同步频率,而 local_step_clipper
表示 0/1 Adam 可以使用的最大本地步长间隔。学习率策略是 0/1 Adam 中使用的默认策略,local_step_scaler
的值可以预先计算(参见 0/1 Adam 论文第 6 节)。我们也可以通过设置这两个超参数来简单地构建其他策略,例如通过设置 local_step_scaler=1
和 local_step_clipper=constant
来实现常量本地步长间隔策略。
cuda_aware
用于基于 MPI 的实现,指示底层 MPI 库支持 CUDA 感知通信。此功能仅在具有 InfiniBand 互连和 CUDA 感知 MPI 库(如 MVAPICH2-GDR 或支持 CUDA 感知的 OpenMPI)的系统上受支持。将 cuda_aware
设置为 False 将允许在基于以太网的系统上进行训练。但是,通信将在发送方和接收方在 CPU 和 GPU 缓冲区之间进行内存复制之后发生。
comm_backend_name
用于指示要使用哪个后端实现。您可以通过将 comm_backend_name
设置为“nccl”、“mpi”或“compressed”来选择 NCCL、基于 MPI 和压缩实现。使用基于 NCCL 的实现时,无需设置 cuda_aware
。
1.4.1 梯度为常零的参数的动量掩码
由于 1-bit 压缩无法精确表示零,如果参数在训练期间梯度持续为零,则压缩误差会不断累积在动量中。例如,对于 BERT 预训练序列长度 128,bert.embeddings.position_embeddings.weight
在其梯度和动量中,对于行 129 到 512,存在持续的零,因为它只学习到序列长度 128,而模型支持的序列长度高达 512。因此,在 0/1 Adam 中,我们增加了对动量掩码的支持,用户可以指定那些梯度持续为精确零的参数。有关如何配置此动量掩码,请参阅示例脚本。需要注意的是,我们不使用检查点中保存的动量掩码,因为此掩码在训练期间可能会发生变化(例如,BERT 序列长度 128 和 512 需要不同的掩码)。因此,您每次都必须在训练脚本中提供此掩码。
注意! 0/1 Adam 依赖于压缩误差补偿机制来在压缩阶段保持收敛速度。加载检查点时,除了像 1-bit Adam 那样重置压缩误差之外,我们还需要重置本地步长缓冲区。因为如果检查点由不同数量的节点 (GPU) 加载,本地步长缓冲区可能会无法捕捉训练动态。
2. 使用 0/1 Adam 进行 BERT 预训练
有关数据下载和预处理,请参阅 BERT 预训练教程。
2.1 使用 DeepSpeed 和 0/1 Adam 运行预训练
我们在 DeepSpeedExamples/bing_bert/01_adam/ 下提供了示例脚本。共有 3 组脚本,分别对应基于 NCCL 的实现、基于以太网系统的 MPI 实现和基于 InfiniBand 系统的 MPI 实现。对于基于 MPI 的实现,我们提供了使用 deepspeed 或 mpirun 启动时的示例脚本。
2.2 启用 DeepSpeed 和 0/1 Adam 的 BERT 预训练配置
deepspeed_bsz4k_01adam_config_seq128_*.json
和 deepspeed_bsz4k_01adam_config_seq512_*.json
文件允许用户指定 DeepSpeed 选项,包括批大小、微批大小、优化器、学习率及其他参数。在这些文件中,我们包含了用于复现我们论文中实验的调优超参数。
2.3 BERT 预训练的性能结果
性能结果可以在我们的论文中查看。
2.4 GLUE 微调
我们还提供了用于 GLUE 任务的 BERT 预训练检查点的微调脚本。脚本可在 DeepSpeedExamples/BingBertGlue 获得。glue_bert_base.json
和 glue_bert_large.json
文件允许用户指定 DeepSpeed 选项/参数,例如 BERT-base 和 BERT-large 检查点的微批大小。目前我们使用 Adam 作为 GLUE 微调的默认优化器,因为微调任务通常使用小批大小(约 32),不需要大规模系统。run_glue_bert_base_finetune.sh
和 run_glue_bert_large_finetune.sh
提供了启动微调任务的脚本,我们可以在其中修改任务名称、epochs 数量、模型等变量。请注意,要启动微调,我们必须指定检查点路径,例如:
bash run_glue_bert_base_finetune.sh <path to checkpoint>
0/1 Adam 的具体 GLUE 分数和超参数包含在我们的论文表 1 中。