BERT 预训练
注意:在 2022 年 8 月 15 日,我们在 github.com/microsoft/Megatron-DeepSpeed/tree/main/examples_deepspeed/bert_with_pile 添加了另一个 BERT 预训练/微调示例,其中包含一个描述如何使用它的 README.md。与下面描述的示例相比,Megatron-DeepSpeed 中的新示例增加了对 ZeRO 和张量切片模型并行性的支持(从而支持更大的模型规模),使用了公共且更丰富的 Pile 数据集(用户也可以使用自己的数据),以及对模型架构和训练超参数的一些更改,如 这篇论文 中所述。因此,新示例训练的 BERT 模型能够提供比原始 BERT 更好的 MNLI 结果,但具有略微不同的模型架构和更大的计算需求。如果您想训练更大规模或质量更好的 BERT 风格模型,我们建议您遵循 Megatron-DeepSpeed 中的新示例。如果您的目标是严格复制原始 BERT 模型,我们建议您遵循 DeepSpeedExamples/bing_bert 下面描述的示例。另一方面,下面的教程有助于解释如何将 DeepSpeed 集成到预训练代码库中,无论您使用哪个 BERT 示例。
在本教程中,我们将 DeepSpeed 应用于预训练 BERT(来自 Transformer 的**双向编码器表示**),它广泛用于许多自然语言处理 (NLP) 任务。BERT 的详细信息可以在此处找到:BERT:用于语言理解的深度双向 Transformer 的预训练。
我们将逐步介绍如何设置数据管道以及如何运行原始 BERT 模型。然后,我们将逐步展示如何修改模型以利用 DeepSpeed。最后,我们将演示使用 DeepSpeed 的性能评估和内存使用量减少。
在没有 DeepSpeed 的情况下预训练 Bing BERT
我们根据 huggingface/transformers 和 NVIDIA/DeepLearningExamples 的改编版本进行工作。我们已在 DeepSpeedExamples/bing_bert 下分叉此存储库,并在其脚本中进行了一些修改
- 我们从 NVIDIA 的 BERT 中采用了建模代码,位于
bing_bert/nvidia/
下。 - 我们从 图灵项目 扩展了数据管道,位于
bing_bert/turing/
下。
训练数据设置
注意:下载和预处理说明即将推出。
下载 Wikipedia 和 BookCorpus 数据集,并在模型配置文件 DeepSpeedExamples/bing_bert/bert_large_adam_seq128.json
中指定其路径
{
...
"datasets": {
"wiki_pretrain_dataset": "/data/bert/bnorick_format/128/wiki_pretrain",
"bc_pretrain_dataset": "/data/bert/bnorick_format/128/bookcorpus_pretrain"
},
...
}
运行 Bing BERT 模型
从 DeepSpeedExamples/bing_bert
运行
python train.py \
--cf bert_large_adam_seq128.json \
--train_batch_size 64 \
--max_seq_length 128 \
--gradient_accumulation_steps 1 \
--max_grad_norm 1.0 \
--fp16 \
--loss_scale 0 \
--delay_allreduce \
--max_steps 10 \
--output_dir <path-to-model-output>
启用 DeepSpeed
要使用 DeepSpeed,我们需要编辑两个文件
train.py
:训练的主要入口点utils.py
:训练参数和检查点保存/加载实用程序
参数解析
我们首先需要使用 deepspeed.add_config_arguments()
将 DeepSpeed 的参数解析添加到 train.py
中。此步骤允许应用程序识别 DeepSpeed 特定的配置。
def get_arguments():
parser = get_argument_parser()
# Include DeepSpeed configuration arguments
parser = deepspeed.add_config_arguments(parser)
args = parser.parse_args()
return args
初始化和训练
我们修改 train.py
以启用使用 DeepSpeed 进行训练。
初始化
我们使用 deepspeed.initialize()
来创建模型、优化器和学习率调度器。对于 Bing BERT 模型,我们在其 prepare_model_optimizer()
函数中初始化 DeepSpeed,如下所示,以传递原始模型和优化器(从命令选项指定)。
def prepare_model_optimizer(args):
# Loading Model
model = BertMultiTask(args)
# Optimizer parameters
optimizer_parameters = prepare_optimizer_parameters(args, model)
model.network, optimizer, _, _ = deepspeed.initialize(args=args,
model=model.network,
model_parameters=optimizer_parameters,
dist_init_required=False)
return model, optimizer
请注意,对于 Bing BERT,原始模型保存在 model.network
中,因此我们将 model.network
作为参数传递,而不是只传递 model。
训练
由 deepspeed.initialize
返回的 model
是 DeepSpeed 的模型引擎,我们将使用它来使用前向、后向和步骤 API 训练模型。由于模型引擎公开了与 nn.Module
对象相同的前向传递 API,因此前向传递没有变化。因此,我们只修改后向传递和优化器/调度器步骤。
通过使用模型引擎直接调用 backward(loss)
来执行反向传播。
# Compute loss
if args.deepspeed:
model.network.backward(loss)
else:
if args.fp16:
optimizer.backward(loss)
else:
loss.backward()
DeepSpeed 引擎中的 step()
函数更新模型参数以及学习率。在每个步骤更新权重后,DeepSpeed 会自动处理梯度清零。
if args.deepspeed:
model.network.step()
else:
optimizer.step()
optimizer.zero_grad()
检查点保存和加载
DeepSpeed 的模型引擎具有灵活的 API 用于检查点保存和加载,以便处理客户端模型状态及其自身内部状态。
def save_checkpoint(self, save_dir, tag, client_state={})
def load_checkpoint(self, load_dir, tag)
在 train.py
中,我们在 checkpoint_model()
函数中使用 DeepSpeed 的检查点 API,如下所示,我们收集客户端模型状态并通过调用 save_checkpoint()
将它们传递给模型引擎
def checkpoint_model(PATH, ckpt_id, model, epoch, last_global_step, last_global_data_samples, **kwargs):
"""Utility function for checkpointing model + optimizer dictionaries
The main purpose for this is to be able to resume training from that instant again
"""
checkpoint_state_dict = {'epoch': epoch,
'last_global_step': last_global_step,
'last_global_data_samples': last_global_data_samples}
# Add extra kwargs too
checkpoint_state_dict.update(kwargs)
success = model.network.save_checkpoint(PATH, ckpt_id, checkpoint_state_dict)
return
在 load_training_checkpoint()
函数中,我们使用 DeepSpeed 的加载检查点 API 并返回客户端模型的状态
def load_training_checkpoint(args, model, PATH, ckpt_id):
"""Utility function for checkpointing model + optimizer dictionaries
The main purpose for this is to be able to resume training from that instant again
"""
_, checkpoint_state_dict = model.network.load_checkpoint(PATH, ckpt_id)
epoch = checkpoint_state_dict['epoch']
last_global_step = checkpoint_state_dict['last_global_step']
last_global_data_samples = checkpoint_state_dict['last_global_data_samples']
del checkpoint_state_dict
return (epoch, last_global_step, last_global_data_samples)
DeepSpeed JSON 配置文件
使用 DeepSpeed 的最后一步是创建配置文件 JSON 文件(例如,deepspeed_bsz4096_adam_config.json
)。此文件提供用户定义的 DeepSpeed 特定参数,例如每个 GPU 的批大小、优化器及其参数,以及是否启用 FP16 训练。
{
"train_batch_size": 4096,
"train_micro_batch_size_per_gpu": 64,
"steps_per_print": 1000,
"optimizer": {
"type": "Adam",
"params": {
"lr": 2e-4,
"max_grad_norm": 1.0,
"weight_decay": 0.01,
"bias_correction": false
}
},
"fp16": {
"enabled": true,
"loss_scale": 0,
"initial_scale_power": 16
}
}
特别是,此示例 json 向 DeepSpeed 指定了以下配置参数
train_batch_size
:使用 4096 的有效批大小train_micro_batch_size_per_gpu
:每个 GPU 都有足够的内存来立即容纳 64 的批大小optimizer
:使用 Adam 训练优化器fp16
:使用初始损失缩放因子 2^16 启用 FP16 混合精度训练。
就是这样!为了使用 DeepSpeed,您只需要进行这些修改。我们已包含一个名为 DeepSpeedExamples/bing_bert/deepspeed_train.py
的修改后的 train.py
文件,其中应用了所有更改。
启用 DeepSpeed 的 Transformer 内核
要启用 Transformer 内核以获得更高的性能,首先在 utils.py
中添加一个参数 --deepspeed_transformer_kernel
,我们可以将其默认设置为 False
,以便轻松地打开/关闭。
parser.add_argument('--deepspeed_transformer_kernel',
default=False,
action='store_true',
help='Use DeepSpeed transformer kernel to accelerate.')
然后在建模源文件的 BertEncoder
类中,使用 DeepSpeed Transformer 内核实例化 Transformer 层,如下所示。
if args.deepspeed_transformer_kernel:
from deepspeed import DeepSpeedTransformerLayer, DeepSpeedTransformerConfig, DeepSpeedConfig
if hasattr(args, 'deepspeed_config') and args.deepspeed_config:
ds_config = DeepSpeedConfig(args.deepspeed_config)
else:
raise RuntimeError('deepspeed_config is not found in args.')
cuda_config = DeepSpeedTransformerConfig(
batch_size = ds_config.train_micro_batch_size_per_gpu,
max_seq_length = args.max_seq_length,
hidden_size = config.hidden_size,
heads = config.num_attention_heads,
attn_dropout_ratio = config.attention_probs_dropout_prob,
hidden_dropout_ratio = config.hidden_dropout_prob,
num_hidden_layers = config.num_hidden_layers,
initializer_range = config.initializer_range,
local_rank = args.local_rank if hasattr(args, 'local_rank') else -1,
seed = args.seed,
fp16 = ds_config.fp16_enabled,
pre_layer_norm=True,
attn_dropout_checkpoint=args.attention_dropout_checkpoint,
normalize_invertible=args.normalize_invertible,
gelu_checkpoint=args.gelu_checkpoint,
stochastic_mode=True)
layer = DeepSpeedTransformerLayer(cuda_config)
else:
layer = BertLayer(config)
self.layer = nn.ModuleList([copy.deepcopy(layer) for _ in range(config.num_hidden_layers)])
所有配置设置都来自 DeepSpeed 配置文件和命令参数,因此我们必须将 args
变量传递到此模型。
注意
batch_size
是输入数据的最大批大小,所有微调训练数据或预测数据都不应超过此阈值,否则将引发异常。在 DeepSpeed 配置文件中,微批大小定义为train_micro_batch_size_per_gpu
,例如,如果将其设置为 8 并且预测使用 12 的批大小,我们可以将 12 用作 Transformer 内核批大小,或者使用“–predict_batch_size”参数将预测批大小设置为 8 或更小的数字。local_rank
在 DeepSpeedTransformerConfig 中用于将 Transformer 内核分配到正确的设备。由于模型在此之前已运行 set_device(),因此不需要在此处设置。stochastic_mode
在启用时具有更高的性能,我们在预训练中启用它,在微调中禁用它。- Transformer 内核有其自己的参数,因此使用 Transformer 内核生成的检查点文件必须由启用了 Transformer 内核的模型加载(例如在微调中)。
有关 Transformer 内核的更多详细信息,请参阅 DeepSpeed Transformer 内核 和 DeepSpeed 快速 BERT 训练。
开始训练
在四个节点(每个节点有四个 GPU)上启动 deepspeed_train.py
的示例如下
deepspeed --num_nodes 4 \
deepspeed_train.py \
--deepspeed \
--deepspeed_config deepspeed_bsz4096_adam_config.json \
--cf /path-to-deepspeed/examples/tests/bing_bert/bert_large_adam_seq128.json \
--train_batch_size 4096 \
--max_seq_length 128 \
--gradient_accumulation_steps 4 \
--max_grad_norm 1.0 \
--fp16 \
--loss_scale 0 \
--delay_allreduce \
--max_steps 32 \
--print_steps 1 \
--deepspeed_transformer_kernel \
--output_dir <output_directory>
有关启动 DeepSpeed 的更多信息,请参阅 入门 指南。
使用 DeepSpeed 重现最快的 BERT 训练结果
我们在实现 SQUAD 1.1 开发集上 90.5 或更高的 F1 分数方面保持行业竞争力,同时实现了最快的 BERT 训练时间。请遵循 BERT 微调 教程来微调由 Transformer 内核预训练的模型并重现 SQUAD F1 分数。
- 我们使用 1024 个 V100 GPU(64 个 NVIDIA DGX-2 节点)在 44 分钟内完成了 BERT 预训练。相比之下,NVIDIA 此前最先进的结果使用 1472 个 V100 GPU 耗时 47 分钟。DeepSpeed 不仅速度更快,而且使用的资源减少了 30%。使用相同的 1024 个 GPU,NVIDIA BERT 比 DeepSpeed 慢 52%,训练时间为 67 分钟。
- 与 Google 原始 BERT 训练时间相比,后者在 64 个 TPU2 芯片上达到相同水平大约需要 96 小时,我们在 4 个 DGX-2 节点(64 个 V100 GPU)上训练时间不到 9 小时。
- 在 256 个 GPU 上,我们花费了 2.4 小时,比 NVIDIA 使用其超级集群在相同数量的 GPU 上获得的最先进结果(3.9 小时)快(链接)。
节点数 | V100 GPU 数量 | 时间 |
---|---|---|
1 个 DGX-2 | 16 | 33 小时 13 分钟 |
4 个 DGX-2 | 64 | 8 小时 41 分钟 |
16 个 DGX-2 | 256 | 144 分钟 |
64 个 DGX-2 | 1024 | 44 分钟 |
我们上面展示的 BERT 训练结果的配置可以通过 DeepSpeedExamples 仓库中的脚本/JSON 配置文件进行复现。下表总结了这些配置。具体来说,请查看 ds_train_bert_bsz64k_seq128.sh
和 ds_train_bert_bsz32k_seq512.sh
脚本以获取更多详细信息,这些脚本位于 DeepSpeedExamples 中。
参数 | 128 序列长度 | 512 序列长度 |
---|---|---|
总批次大小 | 64K | 32K |
每个 GPU 的训练微批次大小 | 64 | 8 |
优化器 | 优化器 | 优化器 |
学习率 | 11e-3 | 2e-3 |
初始学习率 (lr_offset ) |
10e-4 | 0.0 |
Lamb 系数最小值 | 0.01 | 0.01 |
Lamb 系数最大值 | 0.3 | 0.3 |
学习率调度器 | warmup_exp_decay_exp |
warmup_exp_decay_exp |
预热比例 | 0.02 | 0.02 |
衰减率 | 0.90 | 0.90 |
衰减步长 | 250 | 150 |
最大训练步数 | 7500 | 7500 |
重新预热学习率 | N/A | True |
输出检查点编号 | 150 | 160-162 |
样本数量 | 403M | 18-22M |
迭代次数 | 150 | 160-162 |
DeepSpeed 单 GPU 吞吐量结果
与 SOTA 相比,DeepSpeed 显着提高了基于 Transformer 模型(如 BERT)的单 GPU 性能。上图显示了使用 DeepSpeed 优化的 BERT-Large 模型的单 GPU 吞吐量,并与两个知名的 PyTorch 实现(NVIDIA BERT 和 HuggingFace BERT)进行了比较。DeepSpeed 的吞吐量分别达到了 64 和 53 teraflops(对应于 272 和 52 个样本/秒),序列长度分别为 128 和 512,比 NVIDIA BERT 提高了高达 28% 的吞吐量,比 HuggingFace BERT 提高了高达 62%。我们还支持将批次大小提高 1.8 倍,而不会出现内存不足的情况。
有关我们如何实现创纪录的 BERT 训练时间的更多详细信息,请查看 DeepSpeed BERT 的深入分析 最快 BERT 训练