使用英特尔技术加速PyTorch分布式微调

Use Intel technology to accelerate PyTorch distributed fine-tuning.

尽管它们表现出色,但现代深度学习模型通常需要很长时间来训练。为了加快训练作业的速度,工程团队依赖于分布式训练,这是一种分而治之的技术,其中集群服务器每个都保留模型的副本,在训练集的子集上训练它,并交换结果以收敛到最终模型。

图形处理单元(GPU)长期以来一直是训练深度学习模型的事实选择。然而,迁移学习的兴起正在改变游戏规则。现在,模型很少从头开始在庞大的数据集上训练。相反,它们经常在特定(较小)的数据集上进行微调,以构建更适用于特定任务的专门模型,这些模型比基础模型更准确。由于这些训练作业时间较短,使用基于CPU的集群可能是一个既能控制训练时间又能控制成本的有趣选择。

这篇文章是关于什么的

在本文中,您将学习如何通过在由冰湖架构驱动的Intel Xeon可扩展CPU服务器集群上进行分布式训练来加速PyTorch训练作业,这些服务器运行性能优化的软件库。我们将使用虚拟机从头开始构建集群,您应该能够轻松地在自己的基础设施上复制演示,无论是在云端还是在本地。

我们将在MRPC数据集(GLUE基准测试中包含的任务之一)上微调BERT模型来运行文本分类作业。MRPC数据集包含从新闻来源中提取的5800个句子对,带有一个标签,告诉我们每对句子是否在语义上等价。我们选择这个数据集是因为它的训练时间合理,并且尝试其他GLUE任务只需要调整一个参数。

一旦集群运行起来,我们将在单个服务器上运行一个基准作业。然后,我们将扩展到2台服务器和4台服务器,并测量加速比。

在此过程中,我们将涵盖以下主题:

  • 列出所需的基础设施和软件构建模块
  • 设置我们的集群
  • 安装依赖项
  • 运行单节点作业
  • 运行分布式作业

让我们开始工作吧!

使用Intel服务器

为了获得最佳性能,我们将使用基于冰湖架构的Intel服务器,该架构支持硬件功能,如Intel AVX-512和Intel向量神经网络指令(VNNI)。这些功能加速了深度学习训练和推理中通常出现的操作。您可以在此演示文稿(PDF)中了解更多信息。

所有三个主要的云提供商都提供基于Intel冰湖CPU的虚拟机:

  • 亚马逊网络服务:Amazon EC2 M6i和C6i实例
  • Azure:Dv5/Dsv5系列,Ddv5/Ddsv5系列和Edv5/Edsv5系列虚拟机
  • 谷歌云平台:N2计算引擎虚拟机

当然,您也可以使用自己的服务器。如果它们基于冰湖架构(冰湖的前身),那么它们也可以使用,因为冰湖还包括AVX-512和VNNI。

使用Intel性能库

为了利用PyTorch中的AVX-512和VNNI,Intel设计了适用于PyTorch的Intel扩展。该软件库提供了训练和推理的即插即用加速,因此我们应该安装它。

在分布式训练方面,主要的性能瓶颈通常是网络。实际上,集群中的不同节点需要定期交换模型状态信息以保持同步。由于transformer是具有数十亿参数的大型模型(有时甚至更多),信息量是巨大的,随着节点数量的增加,情况只会变得更糟。因此,使用为深度学习优化的通信库非常重要。

事实上,PyTorch包括torch.distributed包,支持不同的通信后端。在这里,我们将使用Intel oneAPI集体通信库(oneCCL),它是一种高效的深度学习通信模式(全约简等)的实现。您可以在这篇PyTorch博文中了解oneCCL与其他后端的性能比较。

既然我们对构建模块有了清晰的认识,让我们谈谈训练集群的整体设置。

设置我们的集群

在此演示中,我使用运行Amazon Linux 2的Amazon EC2实例(c6i.16xlarge,64个虚拟CPU,128GB内存,25Gbit/s网络)。在其他环境中设置将有所不同,但步骤应该非常相似。

请记住,您将需要4个相同的实例,因此您可能希望计划某种自动化措施,以避免重复运行相同的设置4次。在这里,我将手动设置一个实例,从该实例创建一个新的Amazon Machine Image(AMI),并使用此AMI启动三个相同的实例。

从网络的角度来看,我们需要进行以下设置:

  • 在所有实例上打开端口22以进行ssh访问进行设置和调试。
  • 在主实例(您将从中启动训练的实例)和所有其他实例之间配置无密码ssh
  • 在所有实例上打开所有TCP端口以在集群内进行oneCCL通信。请确保不要向外部世界开放这些端口。AWS提供了一种方便的方法,只允许来自运行特定安全组的实例的连接。这是我的设置方式。

现在,让我们手动准备第一个实例。我首先创建实例本身,附加上述安全组,并添加128GB的存储。为了优化成本,我将其作为spot实例启动。

实例启动后,我使用ssh连接到它以安装依赖项。

安装依赖项

以下是我们将遵循的步骤:

  • 安装Intel工具包,
  • 安装Anaconda发行版,
  • 创建一个新的conda环境,
  • 安装PyTorch和用于PyTorch的Intel扩展,
  • 编译和安装oneCCL,
  • 安装transformers库。

看起来很多,但没有什么复杂的。我们开始吧!

安装Intel工具包

首先,我们下载并安装Intel OneAPI基础工具包以及AI工具包。您可以在Intel网站上了解它们。

wget https://registrationcenter-download.intel.com/akdlm/irc_nas/18236/l_BaseKit_p_2021.4.0.3422_offline.sh
sudo bash l_BaseKit_p_2021.4.0.3422_offline.sh

wget https://registrationcenter-download.intel.com/akdlm/irc_nas/18235/l_AIKit_p_2021.4.0.1460_offline.sh
sudo bash l_AIKit_p_2021.4.0.1460_offline.sh 

安装Anaconda

然后,我们下载并安装Anaconda发行版。

wget https://repo.anaconda.com/archive/Anaconda3-2021.05-Linux-x86_64.sh
sh Anaconda3-2021.05-Linux-x86_64.sh

创建一个新的conda环境

我们退出并重新登录以刷新路径。然后,我们创建一个新的conda环境以保持整洁。

yes | conda create -n transformer python=3.7.9 -c anaconda
eval "$(conda shell.bash hook)"
conda activate transformer
yes | conda install pip cmake

安装PyTorch和用于PyTorch的Intel扩展

接下来,我们安装PyTorch 1.9和Intel扩展工具包。版本必须匹配

yes | conda install pytorch==1.9.0 cpuonly -c pytorch
pip install torch_ipex==1.9.0 -f https://software.intel.com/ipex-whl-stable

编译和安装oneCCL

然后,我们安装一些编译oneCCL所需的本机依赖项。

sudo yum -y update
sudo yum install -y git cmake3 gcc gcc-c++

首先,我们克隆oneCCL存储库,构建库并安装它。 再次强调,版本必须匹配

source /opt/intel/oneapi/mkl/latest/env/vars.sh
git clone https://github.com/intel/torch-ccl.git
cd torch-ccl
git checkout ccl_torch1.9
git submodule sync
git submodule update --init --recursive
python setup.py install
cd ..

安装transformers库

接下来,我们安装transformers库和运行GLUE任务所需的依赖项。

pip install transformers datasets
yes | conda install scipy scikit-learn

最后,我们克隆transformers存储库的一个分支,其中包含我们将要运行的示例。

git clone https://github.com/kding1/transformers.git
cd transformers
git checkout dist-sigopt

我们完成了!让我们运行一个单节点作业。

启动单节点作业

为了获得基准线,让我们在transformers/examples/pytorch/text-classification中运行run_glue.py脚本,启动一个单节点作业。这应该适用于任何实例,并且在进行分布式训练之前,它是一个很好的健全性检查。

python run_glue.py \
--model_name_or_path bert-base-cased --task_name mrpc \
--do_train --do_eval --max_seq_length 128 \
--per_device_train_batch_size 32 --learning_rate 2e-5 --num_train_epochs 3 \
--output_dir /tmp/mrpc/ --overwrite_output_dir True

这个作业需要7分钟46秒。现在,让我们使用oneCCL设置分布式作业并加快速度!

使用oneCCL设置分布式作业

运行分布式训练作业需要三个步骤:

  • 列出训练集群的节点,
  • 定义环境变量,
  • 修改训练脚本。

列出训练集群的节点

在主节点的transformers/examples/pytorch/text-classification中,我们创建一个名为hostfile的文本文件。这个文件存储了集群中各个节点的名称(IP地址也可以)。第一行应该指向主节点。

这是我的文件内容:

ip-172-31-28-17.ec2.internal
ip-172-31-30-87.ec2.internal
ip-172-31-29-11.ec2.internal
ip-172-31-20-77.ec2.internal

定义环境变量

接下来,我们需要在主节点上设置一些环境变量,特别是它的IP地址。您可以在文档中找到有关oneCCL变量的更多信息。

for nic in eth0 eib0 hib0 enp94s0f0; do
  master_addr=$(ifconfig $nic 2>/dev/null | grep netmask | awk '{print $2}'| cut -f2 -d:)
  if [ "$master_addr" ]; then
    break
  fi
done
export MASTER_ADDR=$master_addr

source /home/ec2-user/anaconda3/envs/transformer/lib/python3.7/site-packages/torch_ccl-1.3.0+43f48a1-py3.7-linux-x86_64.egg/torch_ccl/env/setvars.sh

export LD_LIBRARY_PATH=/home/ec2-user/anaconda3/envs/transformer/lib/python3.7/site-packages/torch_ccl-1.3.0+43f48a1-py3.7-linux-x86_64.egg/:$LD_LIBRARY_PATH
export LD_PRELOAD="${CONDA_PREFIX}/lib/libtcmalloc.so:${CONDA_PREFIX}/lib/libiomp5.so"

export CCL_WORKER_COUNT=4
export CCL_WORKER_AFFINITY="0,1,2,3,32,33,34,35"
export CCL_ATL_TRANSPORT=ofi
export ATL_PROGRESS_MODE=0

修改训练脚本

我们已经对训练脚本(run_glue.py)进行了以下更改,以启用分布式训练。当您使用自己的训练代码时,您需要进行类似的更改。

  • 导入torch_ccl包。
  • 接收主节点的地址和集群中节点的本地等级。
+import torch_ccl
+
 import datasets
 import numpy as np
 from datasets import load_dataset, load_metric
@@ -47,7 +49,7 @@ from transformers.utils.versions import require_version


 # 如果没有安装 Transformers 的最低版本,将会出错。自行决定是否删除。
-check_min_version("4.13.0.dev0")
+# check_min_version("4.13.0.dev0")

 require_version("datasets>=1.8.0", "修复方法:pip install -r examples/pytorch/text-classification/requirements.txt")

@@ -191,6 +193,17 @@ def main():
     # 或通过在此脚本中传递 --help 标志。
     # 现在我们保留不同的参数集,以更清晰地分离功能。

+    # 为 CPU 分布式添加本地等级
+    sys.argv.append("--local_rank")
+    sys.argv.append(str(os.environ.get("PMI_RANK", -1)))
+
+    # CCL 特定的环境变量
+    if "ccl" in sys.argv:
+        os.environ["MASTER_ADDR"] = os.environ.get("MASTER_ADDR", "127.0.0.1")
+        os.environ["MASTER_PORT"] = "29500"
+        os.environ["RANK"] = str(os.environ.get("PMI_RANK", -1))
+        os.environ["WORLD_SIZE"] = str(os.environ.get("PMI_SIZE", -1))
+
     parser = HfArgumentParser((ModelArguments, DataTrainingArguments, TrainingArguments))
     if len(sys.argv) == 2 and sys.argv[1].endswith(".json"):

设置已经完成。现在让我们将训练作业扩展到2个节点和4个节点。

使用 oneCCL 运行分布式作业

主节点上,我使用mpirun启动一个2节点作业:-np(进程数)设置为2,-ppn(每个节点的进程数)设置为1。因此,将选择hostfile中的前两个节点。

mpirun -f hostfile -np 2 -ppn 1 -genv I_MPI_PIN_DOMAIN=[0xfffffff0] \
-genv OMP_NUM_THREADS=28 python run_glue.py \
--model_name_or_path distilbert-base-uncased --task_name mrpc \
--do_train --do_eval --max_seq_length 128 --per_device_train_batch_size 32 \
--learning_rate 2e-5 --num_train_epochs 3 --output_dir /tmp/mrpc/ \
--overwrite_output_dir True --xpu_backend ccl --no_cuda True

几秒钟后,作业在前两个节点上开始运行。作业完成所需时间为4分钟39秒,加速比为1.7x

-np设置为4并启动一个新作业,现在我可以看到集群中每个节点上都有一个进程运行。

训练完成所需时间为2分钟36秒,加速比为3x

最后一件事。将--task_name更改为qqp,我还运行了基于更大数据集(超过400,000个训练样本)的 Quora Question Pairs GLUE 任务。微调的时间如下:

  • 单节点:11小时22分钟,
  • 2个节点:6小时38分钟(1.71倍),
  • 4个节点:3小时51分钟(2.95倍)。

看起来加速比相当稳定。随意尝试不同的学习率、批量大小和 oneCCL 设置。相信您可以做得更快!

结论

在本篇文章中,您学习了如何基于英特尔CPU和性能库构建分布式训练集群,以及如何使用该集群加速微调任务。确实,迁移学习将CPU训练重新引入游戏中,当您设计和构建下一个深度学习工作流程时,应该考虑使用它。

感谢阅读这篇长篇文章。希望您觉得有益。欢迎在[email protected]提供反馈和问题。下次再见,继续学习吧!

Julien