欢迎来到飞桨分布式技术文档主页¶
欢迎您关注飞桨分布式训练,我们希望能帮助每一位用户走上大规模工业化生产之路!
飞桨分布式训练技术源自百度的业务实践,在自然语言处理、计算机视觉、搜索和推荐等领域经过超大规模业务的检验。飞桨分布式支持参数服务器和基于规约(Reduce)模式的两种主流分布式训练构架,具备包括数据并行、模型并行和流水线并行等在内的完备的并行能力,提供简单易用地分布式训练接口和丰富的底层通信原语。本文档旨在帮助用户快速了解如何使用飞桨分布式,详解各种飞桨分布式能力和使用方法,赋能用户业务发展。
整体介绍与内容概览¶
近十年来,深度学习技术不断刷新视觉、自然语言处理、语音、搜索和推荐等领域任务的记录。这其中的原因,用一个关键词描述就是“大规模”。大规模的数据使得模型有足够的知识可以记忆,大规模参数量的模型使得模型本身有能力记忆更多的数据,大规模高性能的算力(以GPU为典型代表)使得模型的训练速度有百倍甚至千倍的提升。大规模的数据、模型和算力作为深度学习技术的基石,在推动深度学习技术发展的同时,也给深度学习训练带来了新的挑战:大规模数据和大规模模型的发展使得深度学习模型的能力不断增强,如何更加合理地利用大规模集群算力高效地训练,这是分布式训练需要解决的问题。
飞桨分布式从产业实践出发,提供参数服务器(Parameter Server)和集合通信(Collective)两种主流分布式训练构架,具备包括数据并行、模型并行和流水线并行等在内的完备的并行能力,提供简单易用地分布式训练接口和丰富的底层通信原语,赋能用户业务发展。
下面,我们总体介绍飞桨分布式能力和整体文档组织结构。
飞桨分布式训练提供的核心价值¶
源自产业实践的经验¶
飞桨分布式训练技术源自百度的业务实践,在自然语言处理、计算机视觉、搜索和推荐等领域经过超大规模业务检验。基于产业实践,飞桨分布式支持参数服务器和集合通信两种主流分布式训练架构:
参数服务器架构:该架构对于存储超大规模模型参数的训练场景十分友好,常被用于训练拥有海量稀疏参数的搜索推荐领域模型。
集合通信架构:该架构往往是由高算力计算芯片通过高速网络互联而成,如高性能计算的 GPU之间的高速网络互联 NVLINK 和 InfiniBand等,因此非常适合 CV 和 NLP 领域的计算密集型训练任务。
下面将进一步详细介绍这两种架构。
参数服务器架构¶
参数服务器是一种编程范式,方便用户分布式编程。参数服务器架构的重点是对模型参数的分布式存储和协同支持。参数服务器架构如下图所示,集群中的节点分为两种角色:计算节点(Worker)和参数服务器节点(Server)。
Worker负责从参数服务节点拉取参数,根据分配给自己的训练数据计算得到参数梯度,并将梯度推送给对应的Server。
Server负责存储参数,并采用分布式存储的方式各自存储全局参数的一部分,同时接受Worker的请求查询和更新参数。

具体地讲,参数服务器架构下,模型参数分配到所有的参数服务器节点,即每个服务器节点上只保存部分的模型参数。在高可靠性要求场景下,也可以将每个参数在多个参数服务器节点中进行备份。每个计算节点上的计算算子都是相同的(即数据并行),完整的数据集被切分到每个计算节点,每个计算节点使用本地分配的数据进行计算:在每次迭代中,计算节点从参数服务器节点拉取参数用于训练本地模型,计算完成后得到对应参数的梯度,并把梯度上传给参数服务器节点更新参数。参数服务器节点获取计算节点传输的梯度后,将汇总并更新参数。
集合通信架构¶
与参数服务器架构具有两种角色不同,集合通信架构中所有的训练节点通常是对等的,可以说都是Worker。节点间通过Collective集合通信原语通信,因此也称为Collective训练,如下图所示。一种典型的集合通信原语是基于NVIDIA NCCL通信库的集合通信原语。集合通信架构的典型应用方式是使用多张GPU卡协同训练,典型应用场景包括计算机视觉和自然语言处理等。

典型应用场景下,如数据并行模式下,数据集也是切分到各个计算节点,每个计算节点中包含完整的模型参数,并根据本地训练数据训练模型,并得到本地梯度,随后所有计算节点使用集合通信原语获取全局梯度,并更新参数。
完备的并行模式¶
数据、算法和算力是深度学习从理论走向实践的关键因素。单纯从算力的角度看,大规模算力增长主要体现在两个方面:一方面,单个计算设备(如GPU)的算力逐年递增;另一方面,大规模计算集群使得集群整体算力急剧增长。单个设备算力的增长降低了同等规模模型的训练时间。然而,随着互联网和大数据技术的发展,可供模型训练的数据集极速扩增。例如,自然语言处理任务的数据集可达数TB。单个设备完成模型训练的时间需要数月或更多。因此,大规模计算集群的使用进一步加速了训练进程。例如,使用2048张Tesla P40 GPU可以在4分钟内完成ImageNet训练[1]。从算法的角度讲,规模更大的模型可以取得更好的效果。例如,更大规模的语言模型在文章补全、问答系统和对话系统等自然语言处理任务中起着重要作用。通常来讲,有两种方式来扩展模型规模:一种是增加模型的层数,即模型的深度;另一种是增加模型隐层的大小,即模型的宽度。然而,训练这类大规模模型的显存需求远远超过主流GPU的单卡显存容量。例如,OpenAI发布的GPT-3模型具有175B参数量[2];当采用FP32格式存储时,仅存储模型参数就需要700GB显存。因此,为了训练超大规模模型,需要使用流水线并行、张量模型并行和Sharding并行等并行技术。
飞桨分布式提供以下并行技术,实现训练的加速和高效的大规模模型训练。
数据并行:每个计算设备上包含完整的模型副本,因此要求模型训练时的显存需求不超过计算设备的显存容量。在深度学习模型训练过程中,前向计算和反向传播阶段会生成大量的中间状态(Activation),这些中间状态的显存占用和batch size成正比。数据并行可以看作从batch size维度进行切分,通过将较大的batch size切分到
N
个计算设备上,使得每个计算设备上中间状态的显存开销降到原来的1/N
,从而可以训练更大的模型。然而,数据并行存在以下极限:当每个计算设备上的batch size为1时,如果模型训练的显存消耗仍然超过单个计算设备的显存容量,则数据并行无能无力,需要使用流Sharding、流水线并行、张模型并行或者是混合并行技术。更多信息请参考数据并行。张量模型并行:张量模型并行将同一张量切分到不同计算设备,解决大规模模型训练显存需求超过单个计算设备显存容量的问题,并实现高效的大规模模型训练。然而,张量模型并行模式下,通信无法和计算重叠,因此通常将张量模型并行限制在单机内,以利用机内的高通信带宽。更多信息请参考张量模型并行。
流水线并行:增加模型层数是扩展模型规模一种方式;流水线并行是将模型按层拆分到不同计算设备,以流水线的方式逐层完成训练计算任务,从而解决大规模模型训练显存需求超过单个计算设备显存容量的问题,并实现高效的大规模模型训练。因为,不同切分间通信的数据量仅为切分间的中间状态,通信量较小,因此通常将流水线并行应用到机间。更多信息请参考流水线并行。
Sharding并行:Sharding并行本质上是一种数据并行。与数据并行存在多份模型参数副本不同,Sharding并行通过参数切分,确保模型参数在多个设备间只存在一个副本,降低数据并行的显存消耗,实现大规模模型训练。然而,Sharding并行的通信量为三倍的参数量,因此通常适用于机器数较少的训练场景。通常来讲,当参数规模为百亿或以下时,可以使用Sharding并行。当参数规模达到千亿或者更大时,则建议使用基于张量模型并行、流水线并行的混合并行方式。更多信息请参考使用Sharding训练超大模型。
混合并行:通常来讲,不太建议单独使用张量模型并行和流水线并行,而应该在参数规模较大时(如千亿规模以上)采用张量模型并行、流水线并行和数据并行等组合的混合并行,以充分利用机内和机间存储和带宽,实现高效的模型训练。更多信息请参考飞桨4D混合并行训练使用指南。
综上所述,可以参考如下的流程图选择您需要的并行模式。

更多关于每种并行模式特性和如何根据模型特性选择对应的并行模式,请参考飞桨4D混合并行训练使用指南。
开始你的分布式训练之旅¶
整体内容:我们推荐您直接根据主页,按照章节顺序逐个浏览学习,如果有任何疑问都可以在Paddle、PaddleFleetX提交issue提问。
快速上手:如果想最低成本的了解飞桨的分布式训练,我们推荐阅读GPU多机多卡(Collective)训练快速开始和参数服务器训练快速开始。
GPU多机训练:如果您已经开始使用GPU进行多机多卡训练,Collective训练包含了诸多飞桨多机多卡的训练能力和优化方法,建议阅读。
参数服务器:信息检索、推荐系统领域常用的并行训练方式,参数服务器训练包含了飞桨参数服务器的训练能力,建议阅读。
性能基准:可以参考性能基准章节获取飞桨分布式性能数据。
FAQ:对于高频出现的问题,我们会定期整理相关内容到FAQ。
安装PaddlePaddle¶
docker镜像安装¶
使用飞桨进行分布式训练的最小安装集合就是安装PaddlePaddle。我们强烈建议您通过飞桨官方docker镜像使用PaddlePaddle。飞桨官方镜像中包含已经安装最新发行版PaddlePaddle和相关的环境,如CUDA,CUDNN和NCCL等。关于如何获取官方docker镜像,请参考安装信息。更多关于docker的使用信息,请参考docker文档和nvidia-docker。在使用飞桨分布式时,请确保您在所有机器上部署了docker镜像。
物理机安装¶
当您选择在物理机安装PaddlePaddle,请确保您的物理机上安装了符合安装指南中环境准备和开始安装部分要求的操作系统、Python版本和CUDA工具包等,并在安装完后验证安装。需要注意的是,当您选择使用GPU进行分布式训练时,您还需要额外安装NVIDIA NCCL通信库。关于如何安装NVIDIA NCCL通信库,请参考NCCL安装指南。在使用飞桨分布式时,请确保您在所有机器上安装了PaddlePaddle和上述软件环境。
K8S平台安装¶
当您选择在K8S平台安装PaddlePaddle,请参考Kubernetes 部署。
获取更多安装信息,请参考安装指南。
备注:目前飞桨分布式仅支持Linux系统(CentOS/Ubuntu等), 暂不支持Windows和Mac系统
更多whl包下载¶
您也可以自行选择下载PaddlePaddle whl安装版安装需要的PaddlePaddle版本。在安装前,请参考上面的章节安装docker镜像或者是需要的软件环境。更多whl包下载地址如下:
官方正式版, 可以从多版本whl包列表-Release下载。
官方开发版, 可以从多版本whl包列表-develop下载。
Collective训练¶
快速开始¶
Collective训练快速开始¶
本节将以CV领域经典模型ResNet50为例,介绍如何使用Fleet API(paddle.distributed.fleet)完成Collective分布式训练。我们采用Paddle内置的flowers数据集和Momentum优化器方法,循环迭代10个epoch,并在每个step打印当前模型的损失值和精度值。具体代码请参考PaddleFleetX/examples/resnet,其中包含动态图和静态图两种执行方式。resnet_dygraph.py为动态图模型相关代码,train_fleet_dygraph.py为动态图训练脚本。resnet_static.py为静态图模型相关代码,train_fleet_static.py为静态图训练脚本。
版本要求¶
在编写分布式训练程序之前,用户需要确保已经安装paddlepaddle-2.0.0-cpu或paddlepaddle-2.0.0-gpu及以上版本的飞桨开源框架。关于如何安装paddlepaddle框架,请参考安装指南。
操作方法¶
与单机单卡的普通模型训练相比,无论静态图还是动态图,Collective训练的代码都只需要补充三个部分代码:
导入分布式训练需要的依赖包。
初始化Fleet环境。
设置分布式训练需要的优化器。
下面将逐一进行讲解。
初始化fleet环境¶
包括定义缺省的分布式策略,然后通过将参数is_collective设置为True,使训练架构设定为Collective架构。
strategy = fleet.DistributedStrategy()
fleet.init(is_collective=True, strategy=strategy)
设置分布式训练使用的优化器¶
使用distributed_optimizer设置分布式训练优化器。
optimizer = fleet.distributed_optimizer(optimizer)
动态图完整代码¶
train_fleet_dygraph.py的完整训练代码如下所示。
# -*- coding: UTF-8 -*-
import numpy as np
import paddle
# 导入必要分布式训练的依赖包
from paddle.distributed import fleet
# 导入模型文件
from paddle.vision.models import ResNet
from paddle.vision.models.resnet import BottleneckBlock
from paddle.io import Dataset, BatchSampler, DataLoader
base_lr = 0.1 # 学习率
momentum_rate = 0.9 # 冲量
l2_decay = 1e-4 # 权重衰减
epoch = 10 #训练迭代次数
batch_num = 100 #每次迭代的batch数
batch_size = 32 #训练批次大小
class_dim = 102
# 设置数据读取器
class RandomDataset(Dataset):
def __init__(self, num_samples):
self.num_samples = num_samples
def __getitem__(self, idx):
image = np.random.random([3, 224, 224]).astype('float32')
label = np.random.randint(0, class_dim - 1, (1, )).astype('int64')
return image, label
def __len__(self):
return self.num_samples
# 设置优化器
def optimizer_setting(parameter_list=None):
optimizer = paddle.optimizer.Momentum(
learning_rate=base_lr,
momentum=momentum_rate,
weight_decay=paddle.regularizer.L2Decay(l2_decay),
parameters=parameter_list)
return optimizer
# 设置训练函数
def train_resnet():
# 初始化Fleet环境
fleet.init(is_collective=True)
resnet = ResNet(BottleneckBlock, 50, num_classes=class_dim)
optimizer = optimizer_setting(parameter_list=resnet.parameters())
optimizer = fleet.distributed_optimizer(optimizer)
# 通过Fleet API获取分布式model,用于支持分布式训练
resnet = fleet.distributed_model(resnet)
dataset = RandomDataset(batch_num * batch_size)
train_loader = DataLoader(dataset,
batch_size=batch_size,
shuffle=True,
drop_last=True,
num_workers=2)
for eop in range(epoch):
resnet.train()
for batch_id, data in enumerate(train_loader()):
img, label = data
label.stop_gradient = True
out = resnet(img)
loss = paddle.nn.functional.cross_entropy(input=out, label=label)
avg_loss = paddle.mean(x=loss)
acc_top1 = paddle.metric.accuracy(input=out, label=label, k=1)
acc_top5 = paddle.metric.accuracy(input=out, label=label, k=5)
avg_loss.backward()
optimizer.step()
resnet.clear_gradients()
if batch_id % 5 == 0:
print("[Epoch %d, batch %d] loss: %.5f, acc1: %.5f, acc5: %.5f" % (eop, batch_id, avg_loss, acc_top1, acc_top5))
# 启动训练
if __name__ == '__main__':
train_resnet()
静态图完整代码¶
train_fleet_static.py的完整训练代码如下所示。
# -*- coding: UTF-8 -*-
import numpy as np
import paddle
# 导入必要分布式训练的依赖包
import paddle.distributed.fleet as fleet
# 导入模型文件
from paddle.vision.models import ResNet
from paddle.vision.models.resnet import BottleneckBlock
from paddle.io import Dataset, BatchSampler, DataLoader
import os
base_lr = 0.1 # 学习率
momentum_rate = 0.9 # 冲量
l2_decay = 1e-4 # 权重衰减
epoch = 10 #训练迭代次数
batch_num = 100 #每次迭代的batch数
batch_size = 32 #训练批次大小
class_dim = 10
# 设置优化器
def optimizer_setting(parameter_list=None):
optimizer = paddle.optimizer.Momentum(
learning_rate=base_lr,
momentum=momentum_rate,
weight_decay=paddle.regularizer.L2Decay(l2_decay),
parameters=parameter_list)
return optimizer
# 设置数据读取器
class RandomDataset(Dataset):
def __init__(self, num_samples):
self.num_samples = num_samples
def __getitem__(self, idx):
image = np.random.random([3, 224, 224]).astype('float32')
label = np.random.randint(0, class_dim - 1, (1, )).astype('int64')
return image, label
def __len__(self):
return self.num_samples
def get_train_loader(place):
dataset = RandomDataset(batch_num * batch_size)
train_loader = DataLoader(dataset,
places=place,
batch_size=batch_size,
shuffle=True,
drop_last=True,
num_workers=2)
return train_loader
# 设置训练函数
def train_resnet():
paddle.enable_static() # 使能静态图功能
paddle.vision.set_image_backend('cv2')
image = paddle.static.data(name="x", shape=[None, 3, 224, 224], dtype='float32')
label= paddle.static.data(name="y", shape=[None, 1], dtype='int64')
# 调用ResNet50模型
model = ResNet(BottleneckBlock, 50, num_classes=class_dim)
out = model(image)
avg_cost = paddle.nn.functional.cross_entropy(input=out, label=label)
acc_top1 = paddle.metric.accuracy(input=out, label=label, k=1)
acc_top5 = paddle.metric.accuracy(input=out, label=label, k=5)
# 设置训练资源,本例使用GPU资源
place = paddle.CUDAPlace(int(os.environ.get('FLAGS_selected_gpus', 0)))
train_loader = get_train_loader(place)
#初始化Fleet环境
strategy = fleet.DistributedStrategy()
fleet.init(is_collective=True, strategy=strategy)
optimizer = optimizer_setting()
# 通过Fleet API获取分布式优化器,将参数传入飞桨的基础优化器
optimizer = fleet.distributed_optimizer(optimizer)
optimizer.minimize(avg_cost)
exe = paddle.static.Executor(place)
exe.run(paddle.static.default_startup_program())
epoch = 10
step = 0
for eop in range(epoch):
for batch_id, (image, label) in enumerate(train_loader()):
loss, acc1, acc5 = exe.run(paddle.static.default_main_program(), feed={'x': image, 'y': label}, fetch_list=[avg_cost.name, acc_top1.name, acc_top5.name])
if batch_id % 5 == 0:
print("[Epoch %d, batch %d] loss: %.5f, acc1: %.5f, acc5: %.5f" % (eop, batch_id, loss, acc1, acc5))
# 启动训练
if __name__ == '__main__':
train_resnet()
当使用paddle.distributed.launch
组件启动飞桨分布式任务时,在静态图模式下,可以通过FLAGS_selected_gpus
环境变量获取当前进程绑定的GPU卡,如上面的例子所示。
运行示例¶
通过paddle.distributed.launch
组件启动飞桨分布式任务,假设要运行2卡的任务,那么只需在命令行中执行:
动态图:
python -m paddle.distributed.launch --gpus=0,1 train_fleet_dygraph.py
您将看到显示如下日志信息:
----------- Configuration Arguments -----------
gpus: 0,1
heter_worker_num: None
heter_workers:
http_port: None
ips: 127.0.0.1
log_dir: log
...
------------------------------------------------
launch train in GPU mode
INFO 2021-03-23 14:11:38,107 launch_utils.py:481] Local start 2 processes. First process distributed environment info (Only For Debug):
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:59648 |
| PADDLE_TRAINERS_NUM 2 |
| PADDLE_TRAINER_ENDPOINTS 127.0.0.1:59648,127.0.0.1:50871 |
| FLAGS_selected_gpus 0 |
| PADDLE_TRAINER_ID 0 |
+=======================================================================================+
I0323 14:11:39.383992 3788 nccl_context.cc:66] init nccl context nranks: 2 local rank: 0 gpu id: 0 ring id: 0
W0323 14:11:39.872674 3788 device_context.cc:368] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.2, Runtime API Version: 9.2
W0323 14:11:39.877283 3788 device_context.cc:386] device: 0, cuDNN Version: 7.4.
[Epoch 0, batch 0] loss: 4.77086, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 5] loss: 15.69098, acc1: 0.03125, acc5: 0.18750
[Epoch 0, batch 10] loss: 23.41379, acc1: 0.00000, acc5: 0.09375
...
静态图:
python -m paddle.distributed.launch --gpus=0,1 train_fleet_static.py
您将看到显示如下日志信息:
----------- Configuration Arguments -----------
gpus: 0,1
heter_worker_num: None
heter_workers:
http_port: None
ips: 127.0.0.1
log_dir: log
...
------------------------------------------------
WARNING 2021-01-04 17:59:08,725 launch.py:314] Not found distinct arguments and compiled with cuda. Default use collective mode
launch train in GPU mode
INFO 2021-01-04 17:59:08,727 launch_utils.py:472] Local start 2 processes. First process distributed environment info (Only For Debug):
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:17901 |
| PADDLE_TRAINERS_NUM 2 |
| PADDLE_TRAINER_ENDPOINTS 127.0.0.1:17901,127.0.0.1:18846 |
| FLAGS_selected_gpus 0 |
| PADDLE_TRAINER_ID 0 |
+=======================================================================================+
...
W0104 17:59:19.018365 43338 device_context.cc:342] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.2, Runtime API Version: 9.2
W0104 17:59:19.022523 43338 device_context.cc:352] device: 0, cuDNN Version: 7.4.
W0104 17:59:23.193490 43338 fuse_all_reduce_op_pass.cc:78] Find all_reduce operators: 161. To make the speed faster, some all_reduce ops are fused during training, after fusion, the number of all_reduce ops is 5.
[Epoch 0, batch 0] loss: 0.12432, acc1: 0.00000, acc5: 0.06250
[Epoch 0, batch 5] loss: 1.01921, acc1: 0.00000, acc5: 0.00000
...
请注意,不同飞桨版本上述显示信息可能会略有不同。
单机八卡训练启动命令类似,只需正确指定gpus
参数即可,如下所示:
# 动态图
python -m paddle.distributed.launch --gpus 0,1,2,3,4,5,6,7 train_fleet_dygraph.py
# 静态图
python -m paddle.distributed.launch --gpus 0,1,2,3,4,5,6,7 train_fleet_static.py
从单机多卡到多机多卡训练,在代码上不需要做任何改动,只需再额外指定ips
参数即可。其内容为多机的IP列表,命令如下所示(假设两台机器的ip地址分别为192.168.0.1和192.168.0.2):
# 动态图
python -m paddle.distributed.launch --ips="192.168.0.1,192.168.0.2" --gpus 0,1,2,3,4,5,6,7 train_fleet_dygraph.py
# 静态图
python -m paddle.distributed.launch --ips="192.168.0.1,192.168.0.2" --gpus 0,1,2,3,4,5,6,7 train_fleet_static.py
了解更多启动分布式训练任务信息,请参考分布式任务启动方法。
数据并行¶
简介¶
近年来,以GPU为代表的计算设备的算力大幅增长,这主要体现在以下两个方面:一方面,单个计算设备的算力逐年递增;另一方面,大规模计算集群使得集群整体算力急剧增长。单个设备算力的增长降低了同等复杂度问题的计算时间。然而,随着互联网和大数据技术的发展,可供模型训练的数据集极速扩增。例如,自然语言处理任务的数据集可达数TB。并且,模型规模也在不断增长。因此,模型训练的复杂度在持续增长,并且增长速度显著快于单个计算设备算力增长的速度。因此,单个设备完成模型训练的时间往往也变得更长。这时,就需要使用大规模计算集群和并行计算进一步加速训练。例如,使用2048张Tesla P40 GPU可以在4分钟内完成ImageNet训练[1]。
数据并行是深度学习领域最常用的并行方法。以工厂产品生产为例,我们可以将训练数据比作生产产品的原材料,把训练一个mini-batch比作生产一件商品,把计算设备比作生产设备和工人。那么,单卡训练相当于只有一套生产设备和一个工人,工人每次从原材料中取出一份,经由生产设备的加工产出产品。然后,继续取出原材料进行加工生产,循环往复,直到完成生产任务。多卡分布式训练任务则相当于有多套生产设备和多个工人。这里,我们把单机多卡训练和多机多卡训练统一看作分布式训练,而不做特殊区分。那么,我们可以把原材料也分为多份。每个工人从分给自己的原材料中取出一份,经由生产设备的加工产出产品。然后,继续取出原材料进行加工生产,循环往复,直到完成生产任务。显然地,这里面存在两种情形:一种情形是,各个工人间独立生产,当其生产完一个产品时,即刻开始下一个产品的生产,而不需要考虑其它工人的生产状况,这相当于并行训练中的异步训练;另一种情形是,工人间需要相互协同,即当某个工人生产完一件产品后,其需要等待其他工人也完成产品的生产,然后才开始下一件产品的生产,这相当于并行训练中的同步训练。由于每个工人的熟练程度和生产设备的生产效率不同,因此各个工人生产产品的速度也必然存在差异。所以,在协同生产方式下,生产效率会降低。
同样地,同步训练的速度通常也低于异步训练。然而,同步训练在收敛性等方面往往优于异步训练,Collective架构分布式任务普遍采用同步训练的方式。因此,下文我们仅针对同步训练方式展开介绍。
原理介绍¶
数据并行方式下,每个卡上保存完整的模型副本,并行处理多个数据。训练过程中,通过下文介绍的同步机制,确保各个卡上的模型参数始终保持一致。如下图(a)所示。通常,训练数据集被平均为多份,各个卡独立处理一份数据集;通过这种并行方式,加速模型训练过程。

深度学习模型训练过程计算通常分为前向计算、反向计算和梯度更新。由于各个计算设备的初始随机状态不同,各个计算设备上的初始模型参数也因此存在差异。数据并行方式下,为了保持各个计算设备上参数的一致性,在初始阶段需要通过广播的方式将第一张计算设备上的模型参数广播到其它所有计算设备。这样,各个计算设备上的模型参数在广播完成后是一致的。前向计算阶段,各个计算设备使用自己的数据计算模型损失值。由于各个计算设备读取的数据不同,因此各个计算设备上得到的模型损失值也往往是不同的。反向计算阶段,各个计算设备根据其前向计算得到的损失值计算梯度,使用AllReduce操作逐个累加每个参数在所有计算设备上的梯度值,并计算累积梯度的平均值,从而确保各个计算设备上用于更新参数的梯度值是相同的。参数更新阶段,使用梯度平均值更新参数。整个计算过程如上图(b)所示。
由于在训练起始阶段,通过广播操作确保了各个计算设备上的参数一致性;反向阶段,各个计算设备上使用相同的梯度均值更新参数;因此,可以保证训练过程中各个计算设备上的参数值始终是一致的。
数据并行训练主要包括以下两种方式。
各个卡的批处理大小(batch size)和单卡训练保持一致。假设单卡的批处理大小为B,数据并行训练使用的卡数为K。那么,数据并行方式下,单次迭代处理的数据量为KB。在理想情形下,数据并行训练的吞吐量是单卡训练的K倍。但实际情形下,分布式训练引入了额外的通信和计算开销,如累积各个卡上梯度值并计算平均值。这种额外的通信开销和计算开销通常较小。因此,数据并行训练相比于单卡训练的吞吐量更高,加速比通常略小于K。
各个卡的批处理大小总和与单卡训练的批处理大小一致。那么,分布式训练方式下,各个卡的批处理大小为B/K。因此,分布式训练方式下,每次迭代的时间均明显小于单卡训练,从而在整体上提高训练吞吐量。
操作实践¶
与单机单卡的普通模型训练相比,Collective训练的代码只需要补充三个部分代码:
导入分布式训练需要的依赖包。
初始化Fleet环境。
设置分布式训练需要的优化器。
下面将逐一进行讲解。
初始化fleet环境¶
包括定义缺省的分布式策略,然后通过将参数is_collective设置为True,使训练架构设定为Collective架构。
strategy = fleet.DistributedStrategy()
fleet.init(is_collective=True, strategy=strategy)
设置分布式训练使用的优化器¶
使用distributed_optimizer设置分布式训练优化器。
optimizer = fleet.distributed_optimizer(optimizer)
下面,我们分别介绍在动态图和静态图模式下如何使用飞桨分布式。
动态图¶
动态图完整训练代码如下所示(train.py):
# -*- coding: UTF-8 -*-
import numpy as np
import paddle
# 导入必要分布式训练的依赖包
from paddle.distributed import fleet
# 导入模型文件
from paddle.vision.models import ResNet
from paddle.vision.models.resnet import BottleneckBlock
from paddle.io import Dataset, BatchSampler, DataLoader
base_lr = 0.1 # 学习率
momentum_rate = 0.9 # 冲量
l2_decay = 1e-4 # 权重衰减
epoch = 10 #训练迭代次数
batch_num = 100 #每次迭代的batch数
batch_size = 32 #训练批次大小
class_dim = 102
# 设置数据读取器
class RandomDataset(Dataset):
def __init__(self, num_samples):
self.num_samples = num_samples
def __getitem__(self, idx):
image = np.random.random([3, 224, 224]).astype('float32')
label = np.random.randint(0, class_dim - 1, (1, )).astype('int64')
return image, label
def __len__(self):
return self.num_samples
# 设置优化器
def optimizer_setting(parameter_list=None):
optimizer = paddle.optimizer.Momentum(
learning_rate=base_lr,
momentum=momentum_rate,
weight_decay=paddle.regularizer.L2Decay(l2_decay),
parameters=parameter_list)
return optimizer
# 设置训练函数
def train_resnet():
# 初始化Fleet环境
fleet.init(is_collective=True)
resnet = ResNet(BottleneckBlock, 50, num_classes=class_dim)
optimizer = optimizer_setting(parameter_list=resnet.parameters())
optimizer = fleet.distributed_optimizer(optimizer)
# 通过Fleet API获取分布式model,用于支持分布式训练
resnet = fleet.distributed_model(resnet)
dataset = RandomDataset(batch_num * batch_size)
train_loader = DataLoader(dataset,
batch_size=batch_size,
shuffle=True,
drop_last=True,
num_workers=2)
for eop in range(epoch):
resnet.train()
for batch_id, data in enumerate(train_loader()):
img, label = data
label.stop_gradient = True
out = resnet(img)
loss = paddle.nn.functional.cross_entropy(input=out, label=label)
avg_loss = paddle.mean(x=loss)
acc_top1 = paddle.metric.accuracy(input=out, label=label, k=1)
acc_top5 = paddle.metric.accuracy(input=out, label=label, k=5)
avg_loss.backward()
optimizer.step()
resnet.clear_gradients()
if batch_id % 5 == 0:
print("[Epoch %d, batch %d] loss: %.5f, acc1: %.5f, acc5: %.5f" % (eop, batch_id, avg_loss, acc_top1, acc_top5))
# 启动训练
if __name__ == '__main__':
train_resnet()
静态图¶
静态图完整训练代码如下所示(train.py):
# -*- coding: UTF-8 -*-
import numpy as np
import paddle
# 导入必要分布式训练的依赖包
import paddle.distributed.fleet as fleet
# 导入模型文件
from paddle.vision.models import ResNet
from paddle.vision.models.resnet import BottleneckBlock
from paddle.io import Dataset, BatchSampler, DataLoader
import os
base_lr = 0.1 # 学习率
momentum_rate = 0.9 # 冲量
l2_decay = 1e-4 # 权重衰减
epoch = 10 #训练迭代次数
batch_num = 100 #每次迭代的batch数
batch_size = 32 #训练批次大小
class_dim = 10
# 设置优化器
def optimizer_setting(parameter_list=None):
optimizer = paddle.optimizer.Momentum(
learning_rate=base_lr,
momentum=momentum_rate,
weight_decay=paddle.regularizer.L2Decay(l2_decay),
parameters=parameter_list)
return optimizer
# 设置数据读取器
class RandomDataset(Dataset):
def __init__(self, num_samples):
self.num_samples = num_samples
def __getitem__(self, idx):
image = np.random.random([3, 224, 224]).astype('float32')
label = np.random.randint(0, class_dim - 1, (1, )).astype('int64')
return image, label
def __len__(self):
return self.num_samples
def get_train_loader(place):
dataset = RandomDataset(batch_num * batch_size)
train_loader = DataLoader(dataset,
places=place,
batch_size=batch_size,
shuffle=True,
drop_last=True,
num_workers=2)
return train_loader
# 设置训练函数
def train_resnet():
paddle.enable_static() # 使能静态图功能
paddle.vision.set_image_backend('cv2')
image = paddle.static.data(name="x", shape=[None, 3, 224, 224], dtype='float32')
label= paddle.static.data(name="y", shape=[None, 1], dtype='int64')
# 调用ResNet50模型
model = ResNet(BottleneckBlock, 50, num_classes=class_dim)
out = model(image)
avg_cost = paddle.nn.functional.cross_entropy(input=out, label=label)
acc_top1 = paddle.metric.accuracy(input=out, label=label, k=1)
acc_top5 = paddle.metric.accuracy(input=out, label=label, k=5)
# 设置训练资源,本例使用GPU资源
place = paddle.CUDAPlace(int(os.environ.get('FLAGS_selected_gpus', 0)))
train_loader = get_train_loader(place)
#初始化Fleet环境
strategy = fleet.DistributedStrategy()
fleet.init(is_collective=True, strategy=strategy)
optimizer = optimizer_setting()
# 通过Fleet API获取分布式优化器,将参数传入飞桨的基础优化器
optimizer = fleet.distributed_optimizer(optimizer)
optimizer.minimize(avg_cost)
exe = paddle.static.Executor(place)
exe.run(paddle.static.default_startup_program())
epoch = 10
step = 0
for eop in range(epoch):
for batch_id, (image, label) in enumerate(train_loader()):
loss, acc1, acc5 = exe.run(paddle.static.default_main_program(), feed={'x': image, 'y': label}, fetch_list=[avg_cost.name, acc_top1.name, acc_top5.name])
if batch_id % 5 == 0:
print("[Epoch %d, batch %d] loss: %.5f, acc1: %.5f, acc5: %.5f" % (eop, batch_id, loss, acc1, acc5))
# 启动训练
if __name__ == '__main__':
train_resnet()
运行示例¶
可以通过paddle.distributed.launch
组件启动飞桨分布式任务,假设要运行2卡的任务,那么只需在命令行中执行:
python -m paddle.distributed.launch --gpus=0,1 train.py
您将看到显示如下日志信息:
----------- Configuration Arguments -----------
gpus: 0,1
heter_worker_num: None
heter_workers:
http_port: None
ips: 127.0.0.1
log_dir: log
...
------------------------------------------------
launch train in GPU mode
INFO 2021-03-23 14:11:38,107 launch_utils.py:481] Local start 2 processes. First process distributed environment info (Only For Debug):
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:59648 |
| PADDLE_TRAINERS_NUM 2 |
| PADDLE_TRAINER_ENDPOINTS 127.0.0.1:59648,127.0.0.1:50871 |
| FLAGS_selected_gpus 0 |
| PADDLE_TRAINER_ID 0 |
+=======================================================================================+
I0323 14:11:39.383992 3788 nccl_context.cc:66] init nccl context nranks: 2 local rank: 0 gpu id: 0 ring id: 0
W0323 14:11:39.872674 3788 device_context.cc:368] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.2, Runtime API Version: 9.2
W0323 14:11:39.877283 3788 device_context.cc:386] device: 0, cuDNN Version: 7.4.
[Epoch 0, batch 0] loss: 4.77086, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 5] loss: 15.69098, acc1: 0.03125, acc5: 0.18750
[Epoch 0, batch 10] loss: 23.41379, acc1: 0.00000, acc5: 0.09375
...
请注意,不同飞桨版本上述显示信息可能会略有不同。更多信息请参考Collective训练快速开始。了解更多启动分布式训练任务信息,请参考分布式任务启动方法。
数据并行使用技巧¶
首先,我们阐述数据并行模式下学习率的设置技巧,其基本原则是学习率正比于global batch size
。
与单卡训练相比,数据并行训练通常有两种配置:
1. 一种是保持保持所有计算设备的batch size的总和(我们称为global batch size
)与单卡训练的batch size保持一致。这中情形下,由于数据并行训练和单卡训练的global batch size
是一致的,通常保持数据并行模式下各个计算设备上的学习率与单卡训练一致。
2. 另一种情形是,保持数据并行模式下每个计算设备的batch size和单卡训练的batch size一致。这种情形下,数据并行模式的global batch size
是单卡训练的N
倍。这里,N
指的是数据并行计算的设备数。因此,通常需要将数据并行模式下每个计算设备的学习率相应的设置为单卡训练的N
倍。这样,数据并行模式下的初始学习率通常较大,不利于模型的收敛。因此,通常需要使用warm-up机制。即,在初始训练时使用较小的学习率,并逐步缓慢增加学习率,经过一定迭代次数后,学习率增长到期望的学习率。
接着,我们介绍数据集切分问题。数据并行中,我们通常将数据集切分为N
份,每个训练卡负责训练其中的一份数据。这里,N
是数据并行的并行度。如我们前面介绍的,每一个迭代中,各个训练卡均需要做一次梯度同步。因此,我们需要确保对于每个epoch
,各个训练卡经历相同的迭代数,否则,运行迭代数多的训练卡会一直等待通信完成。实践中,我们通常通过数据补齐或者丢弃的方式保证各个训练卡经历相同的迭代数。数据补齐的方式指的是,为某些迭代数少训练数据补充部分数据,从而保证切分后的各份数据集的迭代次数相同;丢弃的方式则是丢弃部分迭代次数较多的数据,从而保证各份数据集的迭代次数相同。
通常,在每个epoch
需要对数据做shuffle处理。因此,根据shuffle时机的不同,有两种数据切分的方法。一种是在数据切分前做shuffle;即,首先对完整的数据做shuffle处理,做相应的数据补充或丢弃,然后做数据的切分。另一种是在数据切分后做shuffle;即,首先做数据的补充或丢弃和数据切分,然后对切分后的每一份数据分别做shuffle处理。
需要注意的是,上述只是给出一些常见的数据并行技巧。在实际使用中,用户需要根据实际业务需要,灵活处理。
性能优化¶
设计综述¶
背景¶
纵观深度学习的发展史,不难发现,很多奠基性的工作,其实早在上世纪四五十年代就被提出了。但是囿于算力,当时的成果主要集中在理论上。直到最近十年间,随着计算性能的不断提升,我们能够在有限的时间内训练出更大更深的神经网络,才让深度学习得以腾飞。不夸张的说,深度学习如今在各个领域取得的成就,和大规模是分不开的。
不管是学术界还是工业界,都一直致力于更快速的训练更大更深的神经网络。大体来分,我们在深度学习领域主要有两个追求,一是训练性能更快,二是模型规模更大。分布式深度学习领域中的很多概念和技巧,都是为了解决这两个问题产生的。
接下来我们着眼于分布式训练从性能优化和大模型训练两方面入手,介绍几个常用的方法。
性能优化¶
在通用GPU发布之后,使用显卡训练神经网络的热度开始爆炸性地增长。NVIDIA的CUDA编程语言可以让用户以一种像C一样的语言实现任意代码。那么如何在通用GPU上设计出高效的代码,对性能提升就变得至关重要。本小节大部分的内容,正是关注于此。值得一提的是,除了GPU,市场上还涌现了很多其他硬件厂商开发的AI专用芯片,例如百度的昆仑、华为的昇腾910等。当然,在这些不同芯片上的优化思路都是类似的。
在分布式机器学习中,最常用的并行模式是数据并行,即每个工作节点拥有全部模型参数,并训练全量数据的一部分,之后对计算出来的梯度(或参数)进行通信(通常为all-reduce操作)以实现全局信息共享。可以看出,计算和通信是分布式深度学习任务中最主要的两部分。所以常用的性能优化策略也正是从这两部分入手进行的。在计算方面优化手段主要包括计算算子融合;在通信方面优化手段主要包括通信算子融合、通信拓扑优化等;当然,还有一部分优化同时涉及两部分,例如混合精度训练。下面将会逐一介绍。
计算OP融合¶
在深度学习框架中,最基本的计算单元是算子(Operator)。例如常见的矩阵乘法操作,就是以MatMul算子的形式存在。一个完整的计算网络,通常就是由多个算子组合起来的。这样的设计十分灵活,用户可以通过组合不同的算子来验证不同的想法。
但是,鱼和熊掌不可兼得。拥有巨大灵活性所要付出的代价就是性能。举例来讲,假设我们要计算三个输入a、b、c相加的结果,调用过程可能是tmp=add(a, b); out=add(tmp, c)
。在这样的网络中,我们会启动两次计算,并开辟了一个中间变量用于存放中间计算结果。在CUDA开发中,这样的一次计算通常是由一个或多个Kernel进行的,而Kernel的启动通常需要一定时间开销,因此在这个例子中加法运算所用到的Kernel就要启动两次,即将产生双倍的时间开销。
针对这个操作的一种优化方法是,我们开发一个支持三个输入的OP(假设名为add3)。那么我们只需要启动一次Kernel计算,即out=add3(a, b, c)
,便可以得到最终的结果。该方法的一个附加好处是还节省了一个临时空间的申请。
这种思路就是所谓的计算OP融合(Fusion),详细内容请参考计算融合。需要说明的是,OP融合在单卡下就有效果,并不是分布式特有的策略。对分布式训练来讲,如何在计算和通信并重的情况下获得更优秀的性能,是我们关注的重点。
接下来的几个小节会结合一个生动的例子来阐述各种优化策略的思想。我们的主人公是Alice和Bob两位小朋友,他们要在各自的房间里做一沓试卷,每张试卷上有若干题目,覆盖不同的知识点。他们的目标是做完所有的试卷,并学到相应的知识。特别的,他们可以通过交换各自学到的内容来修正或巩固自己的知识。Alice和Bob一开始选定的做法是:每当他们之间有人做完一道题,就拨电话给对方,等对方也做完这道题并接起电话后,同步各自的答案,然后同时开始做下一道题。
通信OP融合¶
Alice和Bob所在的国家电话号码很长,所以他们发现每做完一道题就互相通话,拨电话号码的耗时有些难以接受。他们想,如果商定好做完多道题目,再通话一次进行交流,能省去很多拨电话号码造成的时间开销。
这就是通信OP融合的思想。我们知道每次触发通信都会有一些额外的操作(如先建立连接等),减少这些额外的操作将对性能有很大帮助[1]。顺着这个思路,如果我们能够将多次通信的内容先拼接成连续的数据,然后在一次通信内全部发送/接收,那么将会更充分的利用硬件资源,获得更大的性能提升。
通信OP融合的使用方法请参考通信融合。
计算和通信重叠¶
按照之前的约定,做题快的人(比如Alice)拨通电话后,要等待Bob完成对应的题目之后接起电话才能开始这次通信。在等待Bob接听电话的时候,Alice只是闲坐在那里听着听筒里的彩铃音乐。她突然想到,为什么要听这种无聊的声音,而不开始提前做下面的题目呢?
这就是通信和计算重叠的思想。CUDA中有流(stream[2])的概念,一个流表示一个GPU操作队列,该队列中的操作将以添加到流中的先后顺序而依次执行。那么通过令计算和通信操作加入不同的流中,可以做到二者的执行在时间上重叠。详细内容请参考通信重叠。
通信拓扑优化¶
现在做题的团队壮大了,除了Alice和Bob,又加入了几位新同学。他们的目标变成要让每个人算出来的答案,都被所有其他人知道。最简单的做法,自然是所有人之间通一次电话。但是这样做时间开销太大了。聪明的他们选择了另一种做法,把所有人分成几组,每个组选出一名组长,组员把答案汇总给组长。组长间先互相交换所有的信息,然后再分发给所有组员。
不同的信息交换策略,对应到分布式训练中,就是不同的通信拓扑。上述采用的通信策略借鉴了分层(hierarchical)通信的思想。在业界,有ring-allreduce[3],Double binary trees[4]等多种拓扑结构。
通信拓扑优化的更多使用方法,请参考通信拓扑优化。
深度梯度压缩¶
再次回到仅有Alice和Bob两人做题学习的场景来。他们在做题过程中发现,随着学习的进行,对于不同知识点的掌握程度有好有坏。有的知识点已经掌握的很好了,再做题也提供不了太多新的知识。但另外一些,却仍然感到模棱两可。于是两人约定,每做完T张试卷,选出最拿不准的几个知识点来交流答案,而掌握充分的那些知识点,就不在电话中交流了。
上述思路就是深度梯度压缩(Deep Gradient Compression, DGC)的主要思想。DGC通过将梯度稀疏化,在每轮训练时只选择出一部分比较“重要”的梯度进行同步,以达到降低通信量的目的。当然,减少通信量势必会造成精度损失。为了减少损失程度,作者还提出了动量修正(momentum correction)、本地梯度裁剪(local gradient cliping)、动量因子遮蔽(Momentum factor masking) 等几项技巧。详细内容可以参考DGC优化低配网络的分布式GPU训练。
Local SGD¶
Alice和Bob觉得没必要每道题都打电话交流答案,就算使用了前述通信OP融合的技术,也只是减少了打电话的频率,但还是每一道题都要对答案。
于是两人又想到了一个能够减少打电话次数的策略:他们决定各自先做T张试卷,自行学习梳理各个知识点的知识,然后再通电话交流各个知识点的心得。当按照这个方法执行的时候两人发现,尽管花费在打电话上的时间确实减少了,但副作用是他们各自学到的知识可能不一定准确,交流次数的减少让他们没法及时纠正自己某些错误的理解。因此他们又想到了另一个更好的沟通策略:刚开始学习的时候交流频繁一点,当对各个知识点有了大致的了解后,再慢慢降低通话的频率。毕竟具备了基础知识后,只有在题海中遇到新题才能带来新的认识,刷再多重复的题目是没什么意义的。
Local SGD就是基于这个思路,最基本的Local SGD属于上例的第一个策略,直接增大参数同步的间隔来减少通信耗时,但是弊端是可能造成训练精度的损失。而对于第二种策略,Local SGD又衍生出了post Local SGD和Adaptive Local SGD两款“加强版”:
post Local SGD训练的第一个阶段保持每算出一个参数的梯度,就完成一次同步通信,以保证训练精度;之后到了第二阶段,则增大同步间隔(该间隔是固定的),以提升训练效率。
Adaptive Local SGD相对于post Local SGD而言,更加灵活,它会动态调整梯度同步通信的间隔,从而达到训练精度和训练速度之间的平衡。
详细内容可以参考使用Local SGD优化低带宽下分布式训练。
自动混合精度¶
Alice和Bob有一个特殊记忆能力,就是可以把想表述的内容,提炼成少量文字(原先的字数的一半),这样可以减少记忆的内容,但是同时也会导致准确性稍稍出现偏差。
随着知识点和题目越来越多,Alice和Bob觉得脑子发沉,可能脑容量已经快用完了,而打电话交流的时间也越来越长。于是他们决定用上那种记忆能力,这样就释放了大脑中更多的空间,而且打电话交流的内容也随之减半。
在实际应用中,对应这种记忆能力的就是半精度(FP16)类型,使用半精度类型进行训练,称之为混合精度训练(AMP)。混合精度训练有若干好处,例如减小显存使用量,增大通信吞吐等。当然精度的降低会导致数字表示范围的缩小,进而导致比FP32更容易溢出,为了应对这些问题,我们引入了Dynamic loss scaling和op黑白名单等策略来避免。
Dynamic loss scaling:在AMP训练过程中,为了避免精度下溢,每训练一定数量批次的数据,就将Loss放大指定倍数。如果Loss在放大过程中发生上溢,则可以再缩小一定倍数,确保整个训练过程中,梯度可以正常收敛。
op黑白名单:通过使用大量模型在不同应用场景中反复验证后,飞桨团队根据半精度数据类型计算的稳定性和加速效果,梳理出一系列适合转换为半精度计算的算子,并将这些算子定义到了一份白名单文件中。同时对于一些经过验证发现不适合转换的算子,也就是使用半精度计算会导致数值不精确的算子将被记录到黑名单文件中。此外一些对半精度计算没有多少影响的算子归类于灰名单。在使用自动混合精度训练过程中,系统会自动读取黑白名单,从而感知到哪些算子需要被转换为半精度计算。
详细内容请参考自动混合精度训练。
OP融合(计算,通信)¶
说明:本章内容仅适用于飞桨静态图分布式。
计算融合¶
计算融合指的是由单个”大”算子替换多个”小”算子,完成同样的功能,以优化训练速度和显存消耗。我们以计算a、b、c三个张量的和为例说明计算融合的原理,如下图所示。

假设,我们当前有一个add2
算子,该算子接受两个输入,计算这两个张量的和,并输出结果。那么,为了完成上述计算,需要以下步骤:
启动算子,访问显存读取两个输入值
a
和b
,计算e=a+b
并将结果e
写入显存;再次启动算子,访问显存读取两个输入值
c
和e
,计算d=e+c
并将结果d
写入显存。
可见,通过这种常规方式计算d=a+b+c
,需要启动两次算子和4次访存操作:两次读取算子的输入和两次写入结果。我们知道,每次启动算子都是有时间开销的,且每次访存会带来额外的开销。尤其对于相对简单的计算,访存开销占比更高。使用算子融合时,我们可以开发一个接受三个输入的算子add3
。使用该算子,可以一次读取全部的三个输入,计算输入张量的和,并将结果写会显存。使用算子融合,仅需要启动一次算子和两次访存操作,因此可以加速训练速度。同时,我们注意到,使用算子融合,我们还可以节省掉中间结果e
,因此算子融合还可以一定程度上降低显存消耗。
目前Fleet中支持如下3种的OP融合:
fuse_all_optimizer_ops:表明是否融合(fuse) 优化器算子,目前仅对部分优化器有效:SGD、Adam和Momentum。
fuse_elewise_add_act_ops:表明是否融合(fuse) elementwise_add算子和activation算子。
fuse_bn_act_ops:表明是否融合(fuse) batch_norm算子和activation算子。
通常使用这些策略会加速整体执行速度。
通信融合¶
如我们在数据并行一节所介绍,深度学习模型训练过程分为前向计算、反向传播和参数更新三个阶段。数据并行模式下,需要使用AllReduce操作同步参数梯度。默认情形下,对每个参数梯度均需要调用一次AllReduce同步操作。通常来讲,当通信数据量较小时,无法充分利用网络带宽,且每次同步操作需要额外的通信开销。因此,一种直观的想法是将多个梯度的AllReduce操作合并为一次AllReduce通信操作,如下图所示。这即是我们要介绍的通信融合:将多次梯度AllReduce操作合并为单次AllReduce同步操作。

如图所示,一方面我们减少了AllReduce同步操作的次数;另一方面,我们增加了每次AllReduce同步操作的数据量。这有助于提升通信效率,从而提升训练速度。
默认情况下,AllReduce通信融合会将同一layer中多个参数梯度的多个AllReduce操作合并成一个。例如,对于全连接层FC中的Weight和Bias两个参数,通常需要两次AllReduce同步操作;但使用AllReduce通信融合后,只需要使用一次AllReduce同步操作,从而可以减少梯度同步的通信耗时。
此外,为支持更大粒度的参数梯度融合,飞桨提供了以下两个选项供用户选择:用户可以在DistributedStrategy中设置:
fuse_grad_size_in_MB: 指定每次AllReduce同步操作的梯度字节数。假如该参数值等于16,则飞桨底层会将多个参数梯度的AllReduce同步操作聚合为单次AllReduce同步操作,尽量保证每次AllReduce同步操作聚合的梯度大小达到16MB。该参数值通常设置为每次迭代总通信量的十分之一,即模型参数量的十分之一。
_fuse_grad_size_in_TFLOPS: 指定每次AllReduce操作的最大层数,即当聚合的梯度层数达到该层数就进行一次AllReduce同步操作。假如该参数值等于50, 那么最多聚合50层参数梯度即做一次 AllReduce同步操作。
注意: 目前,AllReduce通信融合不支持稀疏参数的梯度。
操作实践¶
# 计算融合
build_strategy = paddle.static.BuildStrategy()
build_strategy.fuse_elewise_add_act_ops = True
build_strategy.fuse_bn_act_ops = True
build_strategy.fuse_relu_depthwise_conv = True
build_strategy.fuse_broadcast_ops = True
build_strategy.fuse_all_optimizer_ops = True
strategy = paddle.distributed.fleet.DistributedStrategy()
strategy.build_strategy = build_strategy
# 通信融合
strategy.fuse_grad_size_in_MB = 16
strategy._fuse_grad_size_in_TFLOPS = 50
strategy.fuse_all_reduce_ops=True
完整示例请参考:example/resnet/train_fleet_static_op_fusion.py。
假设要运行2卡训练任务,那么只需在命令行中执行:
python -m paddle.distributed.launch --gpus=0,1 train_fleet_static_op_fusion.py
您将看到显示如下日志信息:
----------- Configuration Arguments -----------
gpus: None
heter_worker_num: None
heter_workers:
http_port: None
ips: 127.0.0.1
log_dir: log
...
------------------------------------------------
WARNING 2021-01-19 14:53:04,943 launch.py:316] Not found distinct arguments and compiled with cuda. Default use collective mode
launch train in GPU mode
INFO 2021-01-19 14:53:04,945 launch_utils.py:472] Local start 8 processes. First process distributed environment info (Only For Debug):
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:28355 |
| PADDLE_TRAINERS_NUM 8 |
| PADDLE_TRAINER_ENDPOINTS ... 0.1:33653,127.0.0.1:27766,127.0.0.1:16631|
| FLAGS_selected_gpus 0 |
| PADDLE_TRAINER_ID 0 |
+=======================================================================================+
...
W0119 14:53:16.871562 68031 device_context.cc:362] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.2, Runtime API Version: 9.2
W0119 14:53:16.875859 68031 device_context.cc:372] device: 0, cuDNN Version: 7.4.
W0119 14:53:25.973377 68031 build_strategy.cc:116] Currently, fuse_broadcast_ops only works under Reduce mode.
I0119 14:53:27.382609 68031 graph_pattern_detector.cc:101] --- detected 16 subgraphs
I0119 14:53:27.390769 68031 graph_pattern_detector.cc:101] --- detected 16 subgraphs
W0119 14:53:27.407582 68031 fuse_optimizer_op_pass.cc:207] Find momentum operators : 161, and 161 for dense gradients. To make the speed faster, those optimization are fused during training.
W0119 14:53:27.436177 68031 fuse_all_reduce_op_pass.cc:79] Find all_reduce operators: 161. To make the speed faster, some all_reduce ops are fused during training, after fusion, the number of all_reduce ops is 6.
[Epoch 0, batch 0] loss: 0.15131, acc1: 0.00000, acc5: 0.03125
[Epoch 0, batch 5] loss: 1.15416, acc1: 0.00000, acc5: 0.03125
需要注意的是,不同飞桨版本,上述信息可能会有所差异。
通信重叠¶
简介¶
说明:本章内容仅适用于飞桨静态图分布式。
如我们在数据并行一节所介绍,深度学习模型训练过程分为前向计算、反向传播和参数更新三个阶段。数据并行模式下,需要使用AllReduce操作同步参数梯度。这里存在多种选择:一种是串行执行机制,即在完成反向传播生成所有参数的梯度后,使用AllReduce同步参数梯度;另一种是在生成某个参数的梯度后,即使用AllReduce同步参数梯度,且反向传播计算过程和通信通常是可以并行执行的,即计算和通信重叠(Overlap)。当然,这里AllReduce同步操作也有多种可选方式,比如对每个参数梯度调用一次AllReduce同步操作,或者利用通信融合技术将多个参数梯度的AllReduce操作聚合为一个。无论采用哪种方式,这种计算和通信重叠(overlap)的技术,可以有效提升训练速度。
原理介绍¶
目前,飞桨框架只支持单个计算流,但可以有多个通信流。每个流可以看作一个独立的执行序列,多个流之间可以并行执行。在通信为瓶颈的网络中,通过融合计算和通信流以及融合多个通信流,可以有效利用通信带宽,从而获得更优的通信和训练性能。多流相关的概念请参考:cuda-streams-best-practices。
下图给出通信重叠的示意图,每个计算操作产生一份梯度,随后接着通信该梯度。图(a)中,所有的计算和通信操作共用一个计算流,所以计算和通信操作串行执行。图(b)中,有一条计算流和一条通信流,计算和通信操作分别在两条流上执行。当产生完一个梯度后,即可开始通过通信流通信该梯度。然而,由于通信时间大于计算时间,因此整体通信时间仍然较长。然而,相比于单条计算流的串行模式,这种计算和通信重叠的方式可以一定程度的降低执行时间。图(c)中,采用单个计算流和三条通信流,当产生完一个梯度后,即可开始通过通信流通信该梯度。可见,通过通信流重叠的方式,可以进一步优化执行时间。

使用方法¶
飞桨分布式默认实现计算和通信的重叠,并提供多通信流重叠(overlap)功能。为了实现多通信流重叠,只需设置通信器数量nccl_comm_num,即可以加快GPU之间的通信效率。按照经验,建议在单机环境下将nccl_comm_num的值设置为1,在多机环境下将nccl_comm_num的值设置为2。设置方法如下所示:
strategy = fleet.DistributedStrategy()
strategy.nccl_comm_num = 2
strategy.sync_nccl_allreduce=False
完整的示例代码请参考:example/resnet/train_fleet_static_overlap.py。
假设要运行用2卡执行上述任务,那么只需在命令行中执行:
python -m paddle.distributed.launch --gpus=0,1 train_fleet_static_overlap.py
您将看到显示如下日志信息:
----------- Configuration Arguments -----------
gpus: 0,1
heter_worker_num: None
heter_workers:
http_port: None
ips: 127.0.0.1
log_dir: log
...
------------------------------------------------
...
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:10097 |
| PADDLE_TRAINERS_NUM 2 |
| PADDLE_TRAINER_ENDPOINTS 127.0.0.1:10097,127.0.0.1:59371 |
| FLAGS_selected_gpus 0 |
| PADDLE_TRAINER_ID 0 |
+=======================================================================================+
...
W0118 21:44:34.542804 70071 device_context.cc:362] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.2, Runtime API Version: 9.2
W0118 21:44:34.547377 70071 device_context.cc:372] device: 0, cuDNN Version: 7.4.
W0118 21:44:40.178053 70071 fuse_all_reduce_op_pass.cc:79] Find all_reduce operators: 161. To make the speed faster, some all_reduce ops are fused during training, after fusion, the number of all_reduce ops is 5.
[Epoch 0, batch 0] loss: 0.14466, acc1: 0.00000, acc5: 0.03125
[Epoch 0, batch 5] loss: 4.00225, acc1: 0.00000, acc5: 0.03125
...
需要注意的是,不同飞桨版本,上述信息可能会有所差异。
通信拓扑优化¶
说明:本章内容仅适用于飞桨静态图分布式。
原理¶
如我们在数据并行一节所介绍,深度学习模型训练过程分为前向计算、反向传播和参数更新三个阶段。数据并行模式下,需要使用AllReduce操作同步参数梯度。本节,我们以AllReduce操作同步参数梯度为例说明通信拓扑优化技术。
AllReduce操作将不同机器上的数据整合(Reduce)后将结果发送到各个机器,常见的整合操作包括sum
求和、max
求最大值和min
求最小值等。以数据并行下的梯度同步为例,则是采用sum
操作求各个机器上的梯度和。下图给出一种AllReduce求和操作实现方法示例。这种实现中,所有训练进程将数据发送到worker1,worker1进程计算数据的和值,并将结果发送到其它所有训练进程。

为了实现更高效的通信,AllReduce操作还存在其它多种实现方式,如Ring AllReduce。然而,随着训练设备的增加,通信依然成为影响训练效率的因素。一种解决方案是优化通信拓扑结果,使用层次化通信方式。
我们以下图为例说明层次化拓扑的原理。将所有计算设备分为多个组,并在每个组中选择一个计算设备作为leader
。图中,16个计算设备被划分为4个组,每个组内包含4个计算设备。具体地将,worker0 ~ worker3为一个组,worker4 ~ worker7为一个组,worker8 ~ worker11为一个组,worker12 ~ worker15为最后一个组。各个组的leader
分别为worker3、worker5、woker10和worker13。通信时,首先在组内做AllReduce,各个节点得到组内汇聚的结果。接着,在各个组的leader
间做组间AllReduce操作;那么,leader
设备上等价于获取了所有设备的汇聚结果。最后,各个组间leader
设备将其结果广播到组内所有其它设备。

操作实践¶
飞桨实现层次化通信拓扑,支持分层AllReduce操作。为了使用该功能,用户只需要设置相应的DistributedStrategy策略,如下面的例子所示:
dist_strategy = fleet.DistributedStrategy()
dist_strategy.use_hierarchical_allreduce = True
dist_strategy.hierarchical_allreduce_inter_nranks = 8
其中,hierarchical_allreduce_inter_nranks
表示leader
设备的数量。每个组的大小可以根据该值自动推断。
需要说明的是,层次化通信拓扑目前只适用于多GPU训练。
上述例子存放在:example/resnet/train_fleet_static_communication_topology.py。
假设要运行8卡的任务,那么只需在命令行中执行:
python -m paddle.distributed.launch --gpus=0,1,2,3,4,5,6,7 train_fleet_static_communication_topology.py
您将看到显示如下日志信息:
----------- Configuration Arguments -----------
gpus: None
heter_worker_num: None
heter_workers:
http_port: None
ips: 127.0.0.1
log_dir: log
...
------------------------------------------------
...
INFO 2021-01-19 14:58:43,720 launch_utils.py:472] Local start 8 processes. First process distributed environment info (Only For Debug):
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:53762 |
| PADDLE_TRAINERS_NUM 8 |
| PADDLE_TRAINER_ENDPOINTS ... 0.1:58938,127.0.0.1:54203,127.0.0.1:44221|
| FLAGS_selected_gpus 0 |
| PADDLE_TRAINER_ID 0 |
+=======================================================================================+
...
W0119 14:58:52.487838 95116 device_context.cc:362] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.2, Runtime API Version: 9.2
W0119 14:58:52.493592 95116 device_context.cc:372] device: 0, cuDNN Version: 7.4.
W0119 14:59:01.665702 95116 fuse_all_reduce_op_pass.cc:79] Find all_reduce operators: 161. To make the speed faster, some all_reduce ops are fused during training, after fusion, the number of all_reduce ops is 5.
[Epoch 0, batch 0] loss: 0.13468, acc1: 0.00000, acc5: 0.06250
[Epoch 0, batch 5] loss: 0.18902, acc1: 0.03125, acc5: 0.03125
需要注意的是,不同飞桨版本,上述信息可能会有所差异。
通信频率优化¶
说明:本章内容仅适用于飞桨静态图分布式。
在网络带宽较低的训练场景(如:公有云上训练,联邦训练)中,梯度同步在低带宽网络下的延迟成为训练速度的主要瓶颈。Fleet作为Paddle通用的分布式训练API实现了:Deep Gradient Compression
和Local SGD
两种训练策略来针对性解决这一问题。
DGC 优化低配网络的分布式GPU训练¶
DGC 简介¶
大规模分布式训练需要较高的网络带宽以便进行梯度的聚合更新,这限制了多节点训练的扩展性,同时也需要昂贵的高带宽设备。在低带宽的网络环境下进行分布式训练时,梯度同步成为训练加速的瓶颈。Deep Gradient Compression发现:分布式SGD中有99.9%的梯度交换都是冗余的,可以使用深度梯度压缩选择重要梯度进行通信来减少通信量,降低对通信带宽的依赖。Fleet实现了DGC的稀疏通信方式,可有效在低配网络下进行GPU分布式训练。Fleet实现了DGC论文中的预热训练 (warming up training)
、动量修正 (Momentum Correction)
、局部梯度修剪 (local gradient clipping)
和动量因子掩藏 (Momentum factor masking)
等策略,以及正则化项修正 (Weight Decay Correction)
避免稀疏梯度通信训练带来的最终模型精度损失。
下面将介绍 DGC 稀疏通信方式的适用场景、试验效果、基本原理和使用方法。
DGC稀疏通信在低带宽通信瓶颈时会有较大的性能提升,但在单机多卡及RDMA网络通信并非瓶颈情况下,并不会带来性能上的提升。同时由于AllGather的通信量会随卡数的增多而增大,所以DGC的多机训练规模也不宜过大。故DGC适用于低配网络,同时节点规模不宜过大,如大于128张卡。在云网络或高带宽网络设备昂贵时,DGC可有效降低训练成本。
试验效果¶
模型:FasterRCNN
硬件: P40两机分布式,每台机器一卡,TCP网络测试。
取300-700步耗时/400step。
精度无损。
实验结果如下表所示:
带宽 |
训练耗时-Momentum(step /s) |
训练耗时-DGCMomentum(step /s) |
加速比 |
---|---|---|---|
100G |
0.3725 |
0.375 |
0.993 |
10G |
0.55 |
0.375 |
1.467 |
1G |
2.45 |
0.375 |
6.533 |
DGC 原理简介¶
这里将简单介绍介绍Fleet DGC中的一些原理和对应参数应该如何设置。
DGC的基本思路是通过只传送重要梯度,即只发送大于给定阈值的梯度来减少通信带宽的使用。为避免信息的丢失,DGC会将剩余梯度在局部累加起来,最终这些梯度会累加大到足以传输。换个角度,从理论依据上来看,局部梯度累加等同于随时间推移增加batch size,(DGC相当于每一个梯度有自己的batch size)。
假设 N是训练节点个数, b为单卡batch size,局部梯度累加可以被认为batch size从\(Nb\)增大为\(NbT\),其中T是两次更新的稀疏通信间隔。详细的公式推导请参阅[1]。
对于正常的训练,使用DGC一般需进行预热训练,否则可能会有精度损失。由于paddle稀疏梯度聚合通信使用了AllGather,通信量会随卡数增加而增长,所以在卡数较多时不推荐较低稀疏度的预热训练。参数设置如下:
# 1. 以1252个step为一个epoch,前2个epochs使用正常dense通信,后3个epochs逐步提升稀疏度为99.9%
strategy.dgc_configs = {
"rampup_begin_step": 1252*2,
"rampup_step": 1252*3,
"sparsity": [0.984375, 0.996, 0.999]
}
# 2. 前面4个epochs都使用dense通信,之后默认0.999稀疏度运行
strategy.dgc_configs = {
"rampup_begin_step": 1252*4,
"rampup_step": 1,
"sparsity": [0.999]
}
对于Fine-tuning训练,可无需预热训练,从第0个epoch直接使用DGC即可。
# 从第0步开始DGC稀疏通信
strategy.dgc_configs = {
"rampup_begin_step": 0,
"rampup_step": 1,
"sparsity": [0.999]
}
正常情况,稀疏更新会严重影响收敛性。DGC中采用动量修正(Momentum Correction)、局部梯度裁减(Local Gradient Clipping)、动量因子掩藏和正则化项修正4个策略来解决这个问题。
上文”局部梯度累加等同于随时间推移增加batch size“的推导没有考虑Momentum存在的情况。当稀疏度很高时,使用原始Momentum公式会显著降低模型性能,所以需要在原始公式的基础上对梯度进行修正。
动量修正使用部累加速度项\(u_t\)而非累加真实的梯度\(\nabla_{k, t}\)来修改Momentum方程,修正后的动量更新公式如下:
梯度修剪是防止梯度爆炸的常用方法。这方法由Pascanu等人在2013年提出,当梯度的l2-norms和大于给定阈值时,就对梯度rescale。正常梯度修剪在梯度聚合后使用,而DGC因为每个节点独立的进行局部梯度累加,所以DGC在使用\(G_t\)累加前对其进行局部梯度修剪。阈值缩放为原来的\(N^{-1/2}\)。
因为推迟了较小梯度更新权重的时间,所以会有权重陈旧性问题。稀疏度为99.9%时大部分参数需600到1000步更新一次。迟滞效应会减缓收敛并降低模型精度。DGC中使用下面方程来掩藏动量因子减缓陈旧性问题。
此掩码可以停止延迟梯度产生的动量,防止陈旧梯度把权重引入错误的方向。
类似动量修正,DGC中我们同样需要对正则化项进行修正来让参数的延迟更新方向更加准确。
和动量修思路相同,修正需要在局部梯度上添加局部Weight Decay。
上述策略已经在Fleet 框架中实现,用户无须设置。
DGC 快速开始¶
下文以单机八卡上训练ResNet50为例子简单介绍Fleet中DGC的使用。因为8张GPU的通信都在同一节点内, 一般情况下梯度通信并不会成为训练的瓶颈,这里只是以其为例子,介绍Fleet中DGC参数的设置。
注意:
硬件环境要求: DGC目前只支持GPU多卡及分布式collective训练,需要有相应的cuda、cuDNN、nccl环境。
Paddle环境要求:DGC只支持GPU,所以需GPU版本的Paddle。
这里假设:1252个step为一个epoch,前2个epochs使用正常dense通信,后3个epochs逐步提升稀疏度为99.9%,则
rampup_begin_step (int)
:DGC(含预热训练)开始的step。rampup_step (int)
:DGC中预热训练持续的step。如果sparsity是[0.75, 0.9375, 0.984375, 0.996, 0.999],rampup_step设成100时,在0~19 steps时sparsity=0.75,在20~39 steps时sparsity=0.9375,依次类推。sparsity (list[float])
:稀疏度threshold, (1 - current sparsity)% 的gradient将会被allreduce。
strategy = fleet.DistributedStrategy()
strategy.dgc = True
strategy.dgc_configs = {
"rampup_begin_step": 1252*2,
"rampup_step": 1252*3,
"sparsity": [0.984375, 0.996, 0.999]
}
基于ResNet50网络的DGC代码:example/resnet/train_fleet_static_dgc.py。
假设要运行2卡的任务,那么只需在命令行中执行:
python -m paddle.distributed.launch --gpus=0,1 train_fleet_static_dgc.py
使用Local SGD 优化低带宽下分布式训练¶
简介¶
在使用distributed SGD进行数据并行的分布式训练时,常会遇到以下两个问题:
分布式训练的吞吐会受到集群中随机慢节点(straggling node)和通信延迟的影响。
数据并行分布式增大了训练实际的batch size,过大的batch size会影响最终的训练精度。
Local SGD通过延长节点间同步的间隔(局部异步训练)来减轻慢节点的影响和减少通信频率,以此提升训练的吞吐。
原理介绍¶
Local SGD减轻慢节点的影响和减少通信频率,提升训练的吞吐。为了减小相对于本地训练(小batch
size)的精度损失,[1]和[2]分别提出了:post-Local SGD
和自适应步长 (Adaptive Communication) Local SGD
策略,来减少参数同步频率降低带来的精度损失。
在Local SGD训练中,集群中的每个训练进程各自会独立的进行H个连续的SGD更新,然后集群中的所有训练进程会进行通信,同步(averaging)所有训练进程上的参数。
Local SGD中的一个关键问题是如何确定参数同步的间隔(频率),以达到训练吞吐和训练精度间更好的平衡[1]:
增大参数同步的间隔可以减少训练进程间通信延迟的影响提高训练吞吐,
但增大同步间隔可能会造成最终训练精度的损失。
以下两个策略从不同角度试图达到更好的平衡:
post Local SGD将训练过程分成两个阶段:第一阶段训练进程间同步的间隔为1个步长,即同步SGD,来保证最终训练精度;在第二阶段增大同步间隔到固定常数H,来提升训练吞吐。
Adaptive Communication Local SGD通过动态的调整参数同步的间隔来尝试达到训练吞吐和精度间的更好的平衡。
Fleet中实现了post Local SGD
和Adaptive Communication Local SGD
两种策略。
功能效果¶
实验设置
model |
dataset |
local batch size |
cluster |
dtype |
warming up |
learning rate decay |
---|---|---|---|---|---|---|
resnet50 |
Imagenet |
128 |
4 x 8 x V100 |
FP32 |
30 |
polynomial |
实验结果
local step |
qps |
acc1 |
acc5 |
---|---|---|---|
1 |
8270.91 |
0.7579 |
0.9266 |
2 |
8715.67 |
0.7533 |
0.9265 |
4 |
8762.66 |
0.7551 |
0.9260 |
8 |
9184.62 |
0.7511 |
0.9239 |
16 |
9431.46 |
0.7429 |
0.9206 |
ADACOMM |
8945.74 |
0.7555 |
0.9270 |
可以看到在post Local SGD(固定同步间隔)情况下,更新间隔越长训练的吞吐越高,但是模型的最终精度也会损失越大。当使用 ADAPTIVE COMMUNICATION策略后,训练在吞吐和精度间达到了一个更好的平衡。
使用方法¶
下文将以单机8卡训练ResNet50为例子,简单介绍Local SGD的用法。需要注意的是单机八卡的通信都在同一机器节点内,一般情况下参数同步不会成为训练的瓶颈,这里只是以其为例子,介绍Fleet中Local SGD参数的设置。
用户首先需要定义paddle SGD 对象,并在SGD对象中设置学习率参数。目前local SGD和自适应步长local SGD都仅支持SGD和Momentum两种优化器。
在post Local SGD中,有两个参数
begin_step
和k_steps
,局部更新和参数同步都由框架自动完成。begin_step指定从第几个step之后进行local SGD算法,取值为大于0的整数;k_step指定训练过程中的全局参数更新间隔,取值为大于0的整数。
strategy = fleet.DistributedStrategy()
strategy.localsgd = True
strategy.localsgd_configs = {
"k_steps": 1,
"begin_step": 1,
}
在自适应步长 local SGD中,有两个参数
begin_step
和init_k_steps
。begin_step 指定从第几个step之后进行自适应local SGD算法,取值为大于0的整数;用户需要设置init_k_steps作为第一次参数同步的间隔,之后的同步间隔将由动态确定:在学习率较大时,参数变化大,减小step,多进行通信从而保证快速收敛;在学习率较小时,参数变化小,增大step,减少通信次数,从而提升训练速度。需要注意的是在自适应步长策略中,系统会默认限制最大的同步间隔为16 step,当计算出的间隔大于16时,按16 steps进行参数同步。
strategy = fleet.DistributedStrategy()
strategy.adaptive_localsgd = True
strategy.adaptive_localsgd_configs = {
"init_k_steps": 1,
"begin_step": 1,
}
上述例子存放在:example/resnet/train_fleet_static_localsgd.py下面。
假设要运行2卡的任务,那么只需在命令行中执行:
python -m paddle.distributed.launch --gpus=0,1 train_fleet_static_localsgd.py
您将看到显示如下日志信息:
----------- Configuration Arguments -----------
gpus: 0,1
heter_worker_num: None
heter_workers:
http_port: None
ips: 127.0.0.1
log_dir: log
...
------------------------------------------------
...
INFO 2021-01-18 22:01:11,969 launch_utils.py:472] Local start 2 processes. First process distributed environment info (Only For Debug):
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:10913 |
| PADDLE_TRAINERS_NUM 2 |
| PADDLE_TRAINER_ENDPOINTS 127.0.0.1:10913,127.0.0.1:14758 |
| FLAGS_selected_gpus 0 |
| PADDLE_TRAINER_ID 0 |
+=======================================================================================+
...
W0118 22:01:20.860090 45921 device_context.cc:362] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.2, Runtime API Version: 9.2
W0118 22:01:20.864220 45921 device_context.cc:372] device: 0, cuDNN Version: 7.4.
W0118 22:01:25.578325 45921 gen_nccl_id_op_helper.cc:115] connect addr=127.0.0.1:14758 failed 1 times with reason: Connection refused retry after 0.5 seconds
[Epoch 0, batch 0] loss: 0.14602, acc1: 0.00000, acc5: 0.03125
[Epoch 0, batch 5] loss: 0.16445, acc1: 0.00000, acc5: 0.06250
需要注意的是,不同飞桨版本,上述信息可能会有所差异。
自动混合精度训练¶
简介¶
传统上,深度学习训练通常使用32比特双精度浮点数FP32
作为参数、梯度和中间Activation等的数据存储格式。使用FP32
作为数据存储格式,每个数据需要4个字节的存储空间。为了节约显存消耗,业界提出使用16比特单精度浮点数FP16
作为数据存储格式。使用FP16
作为数据存储格式,每个数据仅需要2个字节的存储空间,相比于FP32
可以节省一半的存储空间。除了降低显存消耗,FP16
格式下,计算速度通常也更快,因此可以加速训练。
单精度浮点训练可以带来以下好处:
自动混合精度原理¶
我们首先介绍半精度(FP16)浮点数的表示,如下图所示。半精度浮点数是一种相对较新的浮点类型,在计算机中使用2字节(16比特)存储。在IEEE 754-2008标准中,它亦被称作binary16。与计算中常用的单精度(FP32)和双精度(FP64)浮点类型相比,因为FP16表示范围和表示精度更低,因此FP16更适于在精度要求不高的场景中使用。

在使用相同的超参数下,混合精度训练使用半精度浮点(FP16)和单精度(FP32)浮点即可达到与使用纯单精度训练相同的准确率,并可加速模型的训练速度。这主要得益于英伟达推出的Volta及Turing架构GPU在使用FP16计算时具有如下特点:
FP16可降低一半的内存带宽和存储需求,这使得在相同的硬件条件下研究人员可使用更大更复杂的模型以及更大的batch size大小。
FP16可以充分利用英伟达Volta及Turing架构GPU提供的Tensor Cores技术。在相同的GPU硬件上,Tensor Cores的FP16计算吞吐量是FP32的8倍。
使用自动混合精度训练时,主要训练过程如下:模型参数使用单精度浮点格式存储,在实际计算时,模型参数从单精度浮点数转换为半精度浮点数参与前向计算,并得到半精度浮点数表示中间状态和模型的loss值,然后使用半精度浮点数计算梯度,并将参数对应的梯度转换为单精度浮点数格式后,更新模型参数。计算过程如下图所示。

如前所述,通常半精度浮点数的表示范围远小于单精度浮点数的表示范围,在深度学习领域,参数、中间状态和梯度的值通常很小,因此以半精度浮点数参与计算时容易出现数值下溢,即接近零的值下溢为零值。为了避免这个问题,通常采用loss scaling
机制。具体地讲,对loss乘以一个称为loss_scaling
的值,根据链式法则,在反向传播过程中,梯度也等价于相应的乘以了loss_scaling
的值,因此在参数更新时需要将梯度值相应地除以loss_scaling
的值。
然而,在模型训练过程中,选择合适的loss_scaling
的值是个较大的挑战。因此,需要采用一种称为动态loss scaling
的机制。用户只需要为loss_scaling
设置一个初始值:init_loss_scaling
。在训练过程中,会检查梯度值是否出现nan或inf值,当连续incr_every_n_steps
次迭代均未出现nan和inf值时,将init_loss_scaling
的值乘以一个因子:incr_ratio
;当连续decr_every_n_steps
次迭代均出现nan和inf值时,将init_loss_scaling
的值除以一个因子:decr_ratio
。
同时,我们知道某些算子不适合采用半精度浮点数参与计算,因为这类算子采用半精度浮点数进行计算容易出现nan或者inf值。为了解决这个问题,通常采用黑名单和白名单机制。其中,黑名单中放置不宜采用半精度浮点数进行计算的算子,白名单中放置适合采用半精度浮点数进行计算的算子。
飞桨中,我们引入自动混合精度(Auto Mixed Precision, AMP),混合使用FP32
和FP16
,在保持训练精度的同时,进一步提升训练的速度。实现了 自动维护FP32 、FP16参数副本
,动态loss scaling
, op黑白名单
等策略来避免因FP16
动态范围较小而带来的模型最终精度损失。Fleet作为飞桨通用的分布式训练API提供了简单易用的接口, 用户只需要添加几行代码就可将自动混合精度应用到原有的分布式训练中进一步提升训练速度。
静态图操作实践¶
为了使用AMP,只需要打开相应的配置选项:
strategy = fleet.DistributedStrategy()
strategy.amp = True
strategy.amp_configs = {
"init_loss_scaling": 32768,
"decr_every_n_nan_or_inf": 2,
"incr_every_n_steps": 1000,
"incr_ratio": 2.0,
"use_dynamic_loss_scaling": True,
"decr_ratio": 0.5,
"custom_white_list": [],
"custom_black_list": [],
}
其中,use_dynamic_loss_scaling
表示是否采用,动态loss scaling
机制。飞桨中维护了算子黑白名单,用户也可以通过custom_white_list
和custom_black_list
参数改变某些算子的默认位置。
上述例子存放在:example/resnet/train_fleet_static_amp.py。假设要运行8卡的任务,那么只需在命令行中执行:
python -m paddle.distributed.launch --gpus=0,1,2,3,4,5,6,7 train_fleet_static_amp.py
您将看到显示如下日志信息:
----------- Configuration Arguments -----------
gpus: None
heter_worker_num: None
heter_workers:
http_port: None
ips: 127.0.0.1
log_dir: log
...
------------------------------------------------
...
INFO 2021-01-19 14:46:03,186 launch_utils.py:472] Local start 8 processes. First process distributed environment info (Only For Debug):
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:54114 |
| PADDLE_TRAINERS_NUM 2 |
| PADDLE_TRAINER_ENDPOINTS ... 0.1:24697,127.0.0.1:53564,127.0.0.1:37181|
| FLAGS_selected_gpus 0 |
| PADDLE_TRAINER_ID 0 |
+=======================================================================================+
W0119 14:46:16.315114 84038 device_context.cc:362] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.2, Runtime API Version: 9.2
W0119 14:46:16.320163 84038 device_context.cc:372] device: 0, cuDNN Version: 7.4.
W0119 14:46:25.249166 84038 fuse_all_reduce_op_pass.cc:79] Find all_reduce operators: 161. To make the speed faster, some all_reduce ops are fused during training, after fusion, the number of all_reduce ops is 8.
[Epoch 0, batch 0] loss: 0.19354, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 5] loss: 0.20044, acc1: 0.00000, acc5: 0.00000
需要注意的是,不同飞桨版本,上述信息可能会有所差异。
动态图操作实践¶
使用飞桨框架提供的API:paddle.amp.auto_cast
和paddle.amp.GradScaler
能够实现动态图的自动混合精度训练,即在相关OP的计算中,自动选择FP16或FP32格式计算。开启AMP模式后,使用FP16与FP32进行计算的OP列表可以参见AMP概览。
下面来看一个具体的例子,来了解如果使用飞桨框架实现动态图自动混合精度训练。
首先定义辅助函数,用来计算训练时间。
import time
# 开始时间
start_time = None
def start_timer():
# 获取开始时间
global start_time
start_time = time.time()
def end_timer_and_print(msg):
# 打印信息并输出训练时间
end_time = time.time()
print("\n" + msg)
print("共计耗时 = {:.3f} sec".format(end_time - start_time))
接着构建一个简单的网络,用于对比使用单精度浮点数进行训练与使用自动混合精度训练的速度。该网络由三层Linear组成,其中前两层Linear后接ReLU激活函数。
import paddle
import paddle.nn as nn
class SimpleNet(nn.Layer):
def __init__(self, input_size, output_size):
super(SimpleNet, self).__init__()
self.linear1 = nn.Linear(input_size, output_size)
self.relu1 = nn.ReLU()
self.linear2 = nn.Linear(input_size, output_size)
self.relu2 = nn.ReLU()
self.linear3 = nn.Linear(input_size, output_size)
def forward(self, x):
x = self.linear1(x)
x = self.relu1(x)
x = self.linear2(x)
x = self.relu2(x)
x = self.linear3(x)
return x
这里为了能有效的对比自动混合精度训练在速度方面的提升,我们将input_size与output_size的值设为较大的值,为了充分利用NVIDIA GPU提供的Tensor Core能力,我们将batch_size设置为8的倍数。
epochs = 5
input_size = 4096 # 设为较大的值
output_size = 4096 # 设为较大的值
batch_size = 512 # batch_size 为8的倍数
nums_batch = 50
train_data = [paddle.randn((batch_size, input_size)) for _ in range(nums_batch)]
labels = [paddle.randn((batch_size, output_size)) for _ in range(nums_batch)]
mse = paddle.nn.MSELoss()
下面给出单精度浮点数训练的代码:
model = SimpleNet(input_size, output_size) # 定义模型
optimizer = paddle.optimizer.SGD(learning_rate=0.0001, parameters=model.parameters()) # 定义优化器
start_timer() # 获取训练开始时间
for epoch in range(epochs):
datas = zip(train_data, labels)
for i, (data, label) in enumerate(datas):
output = model(data)
loss = mse(output, label)
# 反向传播
loss.backward()
# 训练模型
optimizer.step()
optimizer.clear_grad()
print(loss)
end_timer_and_print("默认耗时:") # 获取结束时间并打印相关信息
下面给出程序运行的输出:
Tensor(shape=[1], dtype=float32, place=CUDAPlace(0), stop_gradient=False,
[1.25010288])
默认耗时:
共计耗时 = 2.943 sec
下面,我们介绍在动态图中如何使用AMP训练模型。在飞桨框架中,使用自动混合精度训练,需要以下三个步骤:
定义 GradScaler,用于缩放loss比例,避免浮点数下溢,即进行
loss scaling
。使用auto_cast创建AMP上下文环境,该上下文中自动会确定每个OP的输入数据类型(FP16或FP32)。
使用步骤1中定义的GradScaler完成loss的缩放,并用缩放后的loss进行反向传播,完成训练。
实现代码如下所示:
model = SimpleNet(input_size, output_size) # 定义模型
optimizer = paddle.optimizer.SGD(learning_rate=0.0001, parameters=model.parameters()) # 定义优化器
# Step1:定义 GradScaler,用于缩放loss比例,避免浮点数溢出
scaler = paddle.amp.GradScaler(init_loss_scaling=1024)
start_timer() # 获取训练开始时间
for epoch in range(epochs):
datas = zip(train_data, labels)
for i, (data, label) in enumerate(datas):
# Step2:创建AMP上下文环境,开启自动混合精度训练
with paddle.amp.auto_cast():
output = model(data)
loss = mse(output, label)
# Step3:使用 Step1中定义的 GradScaler 完成 loss 的缩放,用缩放后的 loss 进行反向传播
scaled = scaler.scale(loss)
scaled.backward()
# 训练模型
scaler.minimize(optimizer, scaled)
optimizer.clear_grad()
print(loss)
end_timer_and_print("使用AMP模式耗时:")
程序的输出如下:
Tensor(shape=[1], dtype=float32, place=CUDAPlace(0), stop_gradient=False,
[1.23644269])
使用AMP模式耗时:
共计耗时 = 1.222 sec
上述例子存放在:example/amp/amp_dygraph.py。
其他(调节资源的配比、增大bs等)¶
说明:本章内容仅适用于飞桨静态图分布式。
原理¶
飞桨使用“线程池”模型调度并执行Op算子。在启动GPU计算之前,通常需要CPU的协助,如调度算子执行,然而如果Op算子本身计算时间很小,“线程池”模型下会带来额外的调度开销。根据实践经验,对于CPU任务设置使用的线程数num_threads=2*dev_count时性能较好,对于GPU任务,设置线程数num_threads=4*dev_count时性能较好。注意:线程池不是越大越好。
操作实践¶
用户只需要指定相应的DistributedStrategy()的开关,就可以设置线程数量。
strategy = fleet.DistributedStrategy()
exe_strategy = paddle.static.ExecutionStrategy()
exe_strategy.num_threads = 3
strategy.execution_strategy = exe_strategy
上述例子存放在:example/resnet/train_fleet_static_others.py。
假设要运行8卡的任务,那么只需在命令行中执行:
python -m paddle.distributed.launch --gpus=0,1,2,3,4,5,6,7 train_fleet_static_others.py
您将看到显示如下日志信息:
----------- Configuration Arguments -----------
gpus: None
heter_worker_num: None
heter_workers:
http_port: None
ips: 127.0.0.1
log_dir: log
...
------------------------------------------------
...
INFO 2021-01-19 14:50:52,903 launch_utils.py:472] Local start 8 processes. First process distributed environment info (Only For Debug):
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:20485 |
| PADDLE_TRAINERS_NUM 8 |
| PADDLE_TRAINER_ENDPOINTS ... 0.1:23281,127.0.0.1:41983,127.0.0.1:17503|
| FLAGS_selected_gpus 0 |
| PADDLE_TRAINER_ID 0 |
+=======================================================================================+
...
W0119 14:51:04.500844 77798 device_context.cc:362] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.2, Runtime API Version: 9.2
W0119 14:51:04.506238 77798 device_context.cc:372] device: 0, cuDNN Version: 7.4.
W0119 14:51:12.378418 77798 fuse_all_reduce_op_pass.cc:79] Find all_reduce operators: 161. To make the speed faster, some all_reduce ops are fused during training, after fusion, the number of all_reduce ops is 5.
[Epoch 0, batch 0] loss: 0.11252, acc1: 0.03125, acc5: 0.06250
[Epoch 0, batch 5] loss: 0.11252, acc1: 0.03125, acc5: 0.06250
[Epoch 0, batch 10] loss: 0.11252, acc1: 0.03125, acc5: 0.06250
[Epoch 0, batch 15] loss: 0.11252, acc1: 0.03125, acc5: 0.06250
需要注意的是,不同飞桨版本,上述信息可能会有所差异。
大模型训练优化¶
前向重计算¶
简介¶
使用更大的模型和更大的batch size 可以带来更好的效果,但用户需要考虑是随之而来的显存瓶颈问题。Paddle Fleet 提供以下两种策略来解决大模型(大batch size)训练中可能遇到的显存瓶颈问题:
前向重计算(FRB, Forward Recomputation Backpropagation)策略通过清除正向计算过程中的中间计算结果,来降低训练过程中使用的存储空间,从而确保硬件有足够的内存做更大batch Size 的训练。Recompute-Offload策略则基于前向重计算策略,将显存中checkpoint 卸载到Host 内存中,进一步节省显存空间支持更大batch Size的训练。
原理¶
我们知道,深度学习网络的一次训练迭代包含三个步骤:
前向计算: 运行前向算子(Operator) 来计算中间隐层(张量) 的值 。
反向计算: 运行反向算子来计算参数(Parameter)的梯度。
优化: 应用优化算法以更新参数值 。
在前向计算过程中,前向算子会计算出大量的中间结果,由于这些中间结果是训练数据和算子计算得到的,所以训练数据的batch bize 越大,中间结果占用的内存也就越大。飞桨核心框架会使用张量来存储这些隐层的中间结果。当模型层数加深时,其中间结果的数量可达数千甚至数万,占据大量的内存。飞桨核心框架的显存回收机制会及时清除无用的中间结果以节省显存,但是有些中间结果是反向计算过程中算子的输入,这些中间结果必须存储在内存中,直到相应的反向算子计算完毕。
对于大小固定的内存来说,如果用户希望使用大batch bize 的数据进行训练,则将导致单个中间结果占用内存增大,那么就需要减少中间结果的存储数量,FRB就是基于这种思想设计的。FRB是将深度学习网络切分为k个部分(segments)。对每个segment 而言:前向计算时,除了小部分必须存储在内存中的张量外,其他中间结果都将被删除;在反向计算中,首先重新计算一遍前向算子,以获得中间结果,再运行反向算子。简而言之,FRB 和普通的网络迭代相比,多计算了一遍前向算子。
具体过程如下图所示:

我们把切分网络的变量叫做checkpoints。那么问题来了,如何选择checkpoints 呢?自从FRB方法提出以来,大量学者在研究这一关键问题。我们知道深度学习网络通常是由一个个模块串联得到的,比如ResNet-50由16个block串联而成,Bert-Large由24个Encoder layers 串联而成,以两个子模块中间的变量作为切分点就是一个很好的选择。对于非串联的网络(比如含有大量shortcut结构的网络),FRB也支持对其做切分,只是可能多耗费一点内存(用于存储shortcut的输出张量)。
在上面的Recomputation步骤中,同样作为Forward中间结果的checkpoints会驻留显存,方便在Backward中重计算。 然而在checkpoint的生命周期中,仍有一段较长的未被使用的,从极致节省显存的角度去看, 这也是对显存的一种浪费。Recompute-Offload原理大致可以分为两步:
Forward:当checkpoint在前向中被生成后,将其卸载(Offload)到Host内存中,让其所占据的显存可以被释放。
Backward:当checkpoint在反向中被重新调用之前,将其预取(Pre-fetch) 回显存中,完成之后的重计算。
注意:
因为checkpoint 在内存和显存间的拷贝较慢,该策略是通过进一步牺牲速度换取更大的batch size, 需要用户权衡训练吞吐和batch size。
Recompute-Offload 支持多卡并行训练, 当多卡并行时开启Offload,训练中同一节点上所有GPU 上的checkpoints 都将卸载到Host 内存中,会存在以下风险:
PCIe 带宽瓶颈: 同一节点上的所有GPU 和Host 内存间共享一根PCIe 带宽,如同一节点上GPU 数量较多(单机八卡)容易因为PCIe 带宽限制让训练速度进一步减慢。
Host 内存溢出: 当同一节点上GPU 数量较多,且每张GPU checkpoints size 较大时,需要注意卸载量是否超出Host 内存大小。
效果¶
我们在BERT-Large模型上对Recompute 的效果进行了测试,Recompute 可以让batch size 扩大 10倍, Offload 可以在Recompute 的基础上再扩大1.43 倍。 batch size = #seq * seq_max_len 硬件: 单卡 V100 32GB
策略 |
amp |
amp + Recompute |
amp + Recompute + offload |
---|---|---|---|
batch size |
18 * 512 |
180 * 512 |
258 * 512 |
speed |
23.94 sents/s |
17.82 sents/s |
15.47 sents/s |
静态图使用方法¶
为了使用Recompute策略,我们将dist_strategy.recompute
设置为True
并设置我们事先定义好的checkpoints。 checkpoint 的选取可以参考论文 《Training Deep Nets with Sublinear Memory Cost》 。
示例中使用的ResNet50 模型的 checkpoint 不是固定的,不符合 Offload 的要求,固该功能暂无法开启。 当使用 Transformer 时,可以选取每一layer 的FC output 作为checkpoint, 这时各个layer 的checkpoints shapes 一致,可以使用Offload。
res2a.add.output.5.tmp_0 等是用户组网时定义的张量名称。
checkpoint_idx = ["2a", "2b", "2c", "3a", "3b", "3c", "3d", "4a", "4b", "4c", "4d", "4e", "4f", "5a", "5b", "5c"]
checkpoints = ['res{}.add.output.5.tmp_0'.format(idx) for idx in checkpoint_idx]
strategy = fleet.DistributedStrategy()
strategy.recompute = True
strategy.amp = True
strategy.recompute_configs = {
"checkpoints": checkpoints,
"enable_offload": False,
"checkpoint_shape": []
}
上述例子的完整代码存放在:train_fleet_recompute.py下面。假设要运行2卡的任务,那么只需在命令行中执行:
python -m paddle.distributed.launch --gpus=0,1 train_fleet_recompute.py
您将看到显示如下日志信息:
----------- Configuration Arguments -----------
gpus: 0,1
heter_worker_num: None
heter_workers:
http_port: None
ips: 127.0.0.1
log_dir: log
...
------------------------------------------------
...
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:17901 |
| PADDLE_TRAINERS_NUM 2 |
| PADDLE_TRAINER_ENDPOINTS 127.0.0.1:17901,127.0.0.1:18846 |
| FLAGS_selected_gpus 0 |
| PADDLE_TRAINER_ID 0 |
+=======================================================================================+
...
+==============================================================================+
| |
| DistributedStrategy Overview |
| |
+==============================================================================+
| amp=True <-> amp_configs |
+------------------------------------------------------------------------------+
| init_loss_scaling 32768.0 |
| incr_every_n_steps 1000 |
| decr_every_n_nan_or_inf 2 |
| incr_ratio 2.0 |
| decr_ratio 0.800000011920929 |
| use_dynamic_loss_scaling True |
+==============================================================================+
| recompute=True <-> recompute_configs |
+------------------------------------------------------------------------------+
| checkpoints res2a.add.output.5.tmp_0 |
| res2b.add.output.5.tmp_0 |
| res2c.add.output.5.tmp_0 |
| res3a.add.output.5.tmp_0 |
| res3b.add.output.5.tmp_0 |
| res3c.add.output.5.tmp_0 |
| res3d.add.output.5.tmp_0 |
| res4a.add.output.5.tmp_0 |
| res4b.add.output.5.tmp_0 |
| res4c.add.output.5.tmp_0 |
| res4d.add.output.5.tmp_0 |
| res4e.add.output.5.tmp_0 |
| res4f.add.output.5.tmp_0 |
| res5a.add.output.5.tmp_0 |
| res5b.add.output.5.tmp_0 |
| res5c.add.output.5.tmp_0 |
| enable_offload False |
+==============================================================================+
...
W0104 17:59:19.018365 43338 device_context.cc:342] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.2, Runtime API Version: 9.2
W0104 17:59:19.022523 43338 device_context.cc:352] device: 0, cuDNN Version: 7.4.
W0104 17:59:23.193490 43338 fuse_all_reduce_op_pass.cc:78] Find all_reduce operators: 161. To make the speed faster, some all_reduce ops are fused during training, after fusion, the number of all_reduce ops is 5.
[Epoch 0, batch 0] loss: 0.12432, acc1: 0.00000, acc5: 0.06250
[Epoch 0, batch 5] loss: 1.01921, acc1: 0.00000, acc5: 0.00000
...
完整2卡的日志信息也可在./log/
目录下查看。
动态图使用方法¶
动态图recompute功能在Paddle2.1以上加入,建议将Paddle版本升级到最新版。注:当recompute中存在随机性算子比如dropout时,需要在最开始指定paddle.seed,保证反向的重计算随机性。动态图使用recompute功能步骤如下:
一、首先导入需要的包。
import numpy as np
import paddle
from paddle.distributed.fleet.utils import recompute
import random
二、定义组网,在需要使用recompute的地方直接调用函数:recompute(function, checkpoint),paddle就会自动进行recompute相关操作。recompute函数的第一个参数function
是前向计算函数,第二参数checkpoint
是选择的checkpoint点。
def get_fc_block(block_idx, input_size, is_last=False):
block_name = "block_" + str(block_idx)
block = paddle.nn.Sequential(
(block_name + "_fc_0", paddle.nn.Linear(input_size, input_size, bias_attr=False)),
(block_name + "_dropout", paddle.nn.Dropout(p=0.5)),
(block_name + "_relu_1", paddle.nn.ReLU()),
(block_name + "_fc_1", paddle.nn.Linear(input_size, input_size, bias_attr=False)),
(block_name + "_relu_2", paddle.nn.ReLU()),
)
if is_last:
block.add_sublayer(
block_name + "_fc_2",
paddle.nn.Linear(
input_size, 1, bias_attr=False
)
)
else:
block.add_sublayer(
block_name + "_fc_2",
paddle.nn.Linear(input_size, input_size, bias_attr=False)
)
return block
class Naive_fc_net(paddle.nn.Layer):
def __init__(self, input_size=10,
recompute_blocks=[1, 3],
recompute_kwargs={}):
super(Naive_fc_net, self).__init__()
self.recompute_blocks = recompute_blocks
self.recompute_kwargs = recompute_kwargs
self.runfunc0 = get_fc_block(0, input_size, is_last=False)
self.runfunc1 = get_fc_block(1, input_size, is_last=False)
self.runfunc2 = get_fc_block(2, input_size, is_last=False)
self.runfunc3 = get_fc_block(3, input_size, is_last=False)
self.runfunc4 = get_fc_block(4, input_size, is_last=True)
self.total_func = [self.runfunc0, self.runfunc1, self.runfunc2, self.runfunc3, self.runfunc4]
def forward(self, inputs):
nums = len(self.total_func)
for i in range(nums):
if i in self.recompute_blocks:
inputs = recompute(self.total_func[i], inputs)
else:
inputs = self.total_func[i](inputs)
return inputs
三、定义运行程序。
def run_model(cuda_state, recompute_block=[], recompute_kwargs={}):
gen = paddle.seed(10)
gen.manual_seed(10)
np.random.seed(10)
random.seed(10)
if cuda_state:
paddle.set_cuda_rng_state(cuda_state)
batch_size, input_size = 1, 10
model = Naive_fc_net(
input_size,
recompute_blocks=recompute_block,
recompute_kwargs=recompute_kwargs)
optimizer = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters())
loss_ = []
param_ = []
grad_ = []
for _ in range(5):
x_data = np.random.randn(batch_size, input_size).astype(np.float32)
x = paddle.to_tensor(x_data)
y_pred = model(x)
loss = y_pred.mean()
loss_.append(np.asarray(loss).tolist())
loss.backward()
optimizer.step()
param_.append(np.asarray(model.parameters()[9]).tolist())
grad_.append(np.asarray(model.parameters()[3]._grad_ivar()).tolist())
optimizer.clear_grad()
return loss_, param_, grad_
然后执行运行程序,并打印结果,将正常的没有recompute的loss与recompute的loss进行比较,结果应该是相等的。
cuda_state = paddle.get_cuda_rng_state()
# without recompute
loss_ref, param_ref, grad_ref = run_model(
cuda_state, recompute_block=[]
)
loss, param, grad = run_model(cuda_state, recompute_block=[1, 2])
print("normal_loss: {},\n recompute_loss: {}".format(loss_ref, loss))
运行方式:
python recompute_dygraph.py
recompute动态图代码:example/recompute。
输出:
normal_loss: [[0.0], [-0.12574796378612518], [0.6378830075263977], [0.00968710333108902], [0.0]],
recompute_loss: [[0.0], [-0.12574796378612518], [0.6378830075263977], [0.00968710333108902], [0.0]]
数据并行下的重计算¶
当结合使用数据并行和重计算时,建议采用如下方式:
from paddle.distributed.fleet.utils.hybrid_parallel_util import fused_allreduce_gradients
def run_model(cuda_state, recompute_block=[], recompute_kwargs={}):
gen = paddle.seed(10)
gen.manual_seed(10)
np.random.seed(10)
random.seed(10)
if cuda_state:
paddle.set_cuda_rng_state(cuda_state)
batch_size, input_size = 1, 10
model = Naive_fc_net(
input_size,
recompute_blocks=recompute_block,
recompute_kwargs=recompute_kwargs)
optimizer = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters())
loss_ = []
param_ = []
grad_ = []
for _ in range(5):
x_data = np.random.randn(batch_size, input_size).astype(np.float32)
x = paddle.to_tensor(x_data)
# 结合使用重计算和数据并行时,需使用no_sync并手动实现梯度allreduce
with model.no_sync():
y_pred = model(x)
loss = y_pred.mean()
loss_.append(np.asarray(loss).tolist())
loss.backward()
fused_allreduce_gradients(list(model.parameters()), None)
optimizer.step()
param_.append(np.asarray(model.parameters()[9]).tolist())
grad_.append(np.asarray(model.parameters()[3]._grad_ivar()).tolist())
optimizer.clear_grad()
return loss_, param_, grad_
Gradient Merge¶
简介¶
为了提升模型的性能,人们开始追求:更大规模的数据集、更深的网络层、更庞大的参数规模。但是随之而来的就是给模型训练带来了巨大的压力,因此分布式技术及定制化AI 芯片应运而生。但在分布式训练中,经常会遇到显存或者内存不足的情况,通常是以下几点原因导致的:
输入的数据过大,例如视频类训练数据。
深度模型的参数过多或过大,所需的存储空间超出了内存/显存的大小。
AI芯片的内存有限。
为了能正常完成训练,我们通常只能使用较小的batch size 以降低模型训练中的所需要的存储空间,这将导致很多模型无法通过提高训练时的batch size 来提高模型的精度。
Gradient Merge (GM) 策略的主要思想是将连续多个batch 数据训练得到的参数梯度合并做一次更新。 在该训练策略下,虽然从形式上看依然是小batch 规模的数据在训练,但是效果上可以达到多个小batch 数据合并成大batch 后训练的效果。
原理¶
Gradient Merge 只是在训练流程上做了一些微调,达到模拟出大batch size 训练效果的目的。具体来说,就是使用若干原有大小的batch 数据进行训练,即通过“前向+反向” 网络计算得到梯度。其间会有一部分显存/内存用于存放梯度,然后对每个batch计算出的梯度进行叠加,当累加的次数达到某个预设值后,使用累加的梯度对模型进行参数更新,从而达到使用大batch 数据训练的效果。
在较大的粒度上看, GM 是将训练一个step 的过程由原来的 “前向 + 反向 + 更新” 改变成 “(前向 + 反向 + 梯度累加)x k + 更新”, 通过在最终更新前进行 k 次梯度的累加模拟出 batch size 扩大 k 倍的效果。 更具体细节可以参考 《MG-WFBP: Efficient Data Communication for Distributed Synchronous SGD Algorithms》 。
静态图使用方法¶
Gradient Merge 策略在使用方面也很简单,用户只需要定义将多少batch 的数据计算出的梯度叠加更新模型参数,便可以实现大batch 训练的目的。
训练代码的框架和其他fleet 训练代码基本一样,用户只需要在 fleet.DistributedStrategy 中配置Gradient Merge 相关参数即可。
假设我们定义了batch
size 为 N;通过设置k_steps
,使用4个batch
size来模拟一个大batch的训练,从而达到了batch size 为 4*N 的训练效果。
在gradient_merge_configs
中,avg 选项用于控制梯度累计的形式:当被设置为
True
时,会对每次的梯度求和并做平均;反之将直接对梯度求和,并对参数进行更新。
strategy = fleet.DistributedStrategy()
# 使用Gradient merge策略并设置相关参数
strategy.gradient_merge = True
strategy.gradient_merge_configs = {"k_steps": 4, "avg": True}
上述例子的完整代码存放在:train_fleet_gradient_merge.py下面。假设要运行2卡的任务,那么只需在命令行中执行:
python -m paddle.distributed.launch --gpus=0,1 train_fleet_gradient_merge.py
您将看到显示如下日志信息:
----------- Configuration Arguments -----------
gpus: 0,1
heter_worker_num: None
heter_workers:
http_port: None
ips: 127.0.0.1
log_dir: log
...
------------------------------------------------
...
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:17901 |
| PADDLE_TRAINERS_NUM 2 |
| PADDLE_TRAINER_ENDPOINTS 127.0.0.1:17901,127.0.0.1:18846 |
| FLAGS_selected_gpus 0 |
| PADDLE_TRAINER_ID 0 |
+=======================================================================================+
...
+==============================================================================+
| |
| DistributedStrategy Overview |
| |
+==============================================================================+
| gradient_merge=True <-> gradient_merge_configs |
+------------------------------------------------------------------------------+
| k_steps 4 |
| avg True |
+==============================================================================+
...
W0104 17:59:19.018365 43338 device_context.cc:342] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.2, Runtime API Version: 9.2
W0104 17:59:19.022523 43338 device_context.cc:352] device: 0, cuDNN Version: 7.4.
W0104 17:59:23.193490 43338 fuse_all_reduce_op_pass.cc:78] Find all_reduce operators: 161. To make the speed faster, some all_reduce ops are fused during training, after fusion, the number of all_reduce ops is 5.
[Epoch 0, batch 0] loss: 0.12432, acc1: 0.00000, acc5: 0.06250
[Epoch 0, batch 5] loss: 1.01921, acc1: 0.00000, acc5: 0.00000
...
完整2卡的日志信息也可在./log/
目录下查看。
动态图使用方法¶
需要说明的是,动态图是天然支持Gradient Merge。即,只要不调用 clear_gradient
方法,动态图的梯度会一直累积。
动态图下使用Gradient Merge的代码片段如下:
for batch_id, data in enumerate(train_loader()):
... ...
avg_loss.backward()
if batch_id % k == 0:
optimizer.minimize(avg_loss)
model.clear_gradients()
使用LARS / LAMB 优化分布式超大batch 训练¶
简介¶
在数据并行分布式训练场景中, 常使用增加GPU数量的方式来加速训练。 为了保证GPU的算力得到充分利用, 每张GPU卡上的batch size都需要足够大。 因此在增加GPU 数量同时, 训练的全局batch size 也会变大。
但越大的全局batch size 会带来训练的收敛问题[1] [2]:
模型最终精度损失
收敛速度变慢, 需要更多的epoch 才能收敛
LARS[3] 和 LAMB[4] 两个优化策略常用来解决上述超大batch 训练中的收敛问题。
Paddle 实现了这两种优化策略,paddle.distributed.fleet 作为Paddle通用的分布式训练API提供了简单易用的接口, 用户只需要添加几行代码就可将策略加入到原有的训练中。 通过这两个优化策略, 我们在超大batch 场景中实现了更快的收敛速度和无损的精度, 结合Fleet 中其他的策略(e.g. AMP) 可以缩短整体训练收敛时间。
原理¶
LARS¶
LARS 公式如下:
可以看到LARS 其实是在 带weight decay
的momentum
优化器的基础上加入了local learning rate
的逻辑,
对每一层的learning rate
进行了放缩。
LAMB¶
LAMB 公式如下:
和LARS 类似, LAMB 也是在内层优化器的基础上,
套了一个local learning rate
的逻辑, 对每一层的learning rate
进行了放缩。
效果¶
使用 LARS 在超大batch size 下训练 resnet50:
resnet50 imagenet |
Global batch size |
epoch |
top1 |
---|---|---|---|
[Goyal et al] |
8k |
90 |
76.3% |
LARS Paper |
32k |
90 |
72.3% |
[fleet: lars + amp] |
16k |
60 |
76.2% |
[fleet: lars + amp] |
32k |
62 |
75.9% |
使用方法¶
LARS¶
fleet 将 LARS实现为一个 fleet meta optimizer, 在使用时需要设置以下几点:
LARS meta optimizer 的 inner optimizer 必须为
momentum
, 并在 momentum 中定义mu
和lr
参数。在DistributedStrategy 中设置LARS 特有的
lars_coeff
参数和lars_weight_decay
参数。LARS 已经将
weight decay
包含进公式中, 用户不需要再在 optimizer中设置regularization
。fleet 中还提供 lars_weight_decay 过滤策略, 可以通过在
exclude_from_weight_decay
参数加入对应layer 的name string
, 让这一 layer 的参数不进行 lars weight decay。 (通常我们将BN
参数 和FC_bias
从lars weight decay 中过滤)
strategy = fleet.DistributedStrategy()
strategy.lars = True
strategy.lars_configs = {
"lars_coeff": 0.001,
"lars_weight_decay": 0.0005,
"exclude_from_weight_decay": ['batch_norm', '.b_0']
}
上述例子的完整代码存放在:train_fleet_lars.py下面。假设要运行2卡的任务,那么只需在命令行中执行:
python -m paddle.distributed.launch --gpus=0,1 train_fleet_lars.py
您将看到显示如下日志信息:
----------- Configuration Arguments -----------
gpus: 0,1
heter_worker_num: None
heter_workers:
http_port: None
ips: 127.0.0.1
log_dir: log
...
------------------------------------------------
...
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_TRAINER_ID 0 |
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:12464 |
| PADDLE_TRAINERS_NUM 2 |
| PADDLE_TRAINER_ENDPOINTS 127.0.0.1:12464,127.0.0.1:43227 |
| FLAGS_selected_gpus 0 |
+=======================================================================================+
...
+==============================================================================+
| |
| DistributedStrategy Overview |
| |
+==============================================================================+
| lars=True <-> lars_configs |
+------------------------------------------------------------------------------+
| lars_coeff 0.0010000000474974513 |
| lars_weight_decay 0.0005000000237487257 |
| epsilon 0.0 |
| exclude_from_weight_decay batch_norm |
| .b_0 |
+==============================================================================+
...
W0114 18:07:51.588716 16234 device_context.cc:346] Please NOTE: device: 4, GPU Compute Capability: 7.0, Driver API Version: 11.0, Runtime API Version: 10.0
W0114 18:07:51.593963 16234 device_context.cc:356] device: 4, cuDNN Version: 7.6.
[Epoch 0, batch 0] loss: 0.14651, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 5] loss: 1.82926, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 10] loss: 0.00000, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 15] loss: 0.13787, acc1: 0.03125, acc5: 0.03125
[Epoch 0, batch 20] loss: 0.12400, acc1: 0.03125, acc5: 0.06250
[Epoch 0, batch 25] loss: 0.17749, acc1: 0.00000, acc5: 0.00000
...
完整 2卡的日志信息也可在./log/
目录下查看。
LAMB¶
fleet 将 LAMB实现为一个 fleet meta optimizer, 在使用时需要设置以下几点:
LAMB meta optimizer 的 inner optimizer 必须为
adam
, 并在 adam 中定义 学习率lr
, 一阶 moment 的指数衰减率beta1
和二阶moment 的指数衰减率beta2
参数。在 DistributedStrategy 里定设置AMB 特有的
lamb_weight_decay
参数.LAMB 已经将
weight decay
包含进公式中, 用户不需要再在 optimizer中设置regularization
。fleet 中还提供 lamb_weight_decay 过滤策略, 可以通过在
exclude_from_weight_decay
参数加入对应layer 的name string
, 让这一 layer 的参数不进行 lars weight decay。 (通常我们将LN
从lamb weight decay 中过滤)
strategy = fleet.DistributedStrategy()
strategy.lamb = True
strategy.lamb_configs = {
'lamb_weight_decay': 0.01,
'exclude_from_weight_decay': ['layer_norm'],
}
上述例子的完整代码存放在:train_fleet_lamb.py下面。假设要运行2卡的任务,那么只需在命令行中执行:
python -m paddle.distributed.launch --gpus=0,1 train_fleet_lamb.py
您将看到显示如下日志信息:
----------- Configuration Arguments -----------
gpus: 0,1
heter_worker_num: None
heter_workers:
http_port: None
ips: 127.0.0.1
log_dir: log
...
------------------------------------------------
...
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_TRAINER_ID 0 |
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:12464 |
| PADDLE_TRAINERS_NUM 2 |
| PADDLE_TRAINER_ENDPOINTS 127.0.0.1:12464,127.0.0.1:43227 |
| FLAGS_selected_gpus 0 |
+=======================================================================================+
...
+==============================================================================+
| |
| DistributedStrategy Overview |
| |
+==============================================================================+
| lamb=True <-> lamb_configs |
+------------------------------------------------------------------------------+
| lamb_weight_decay 0.009999999776482582 |
| exclude_from_weight_decay layer_norm |
+==============================================================================+
...
W0114 18:07:51.588716 16234 device_context.cc:346] Please NOTE: device: 4, GPU Compute Capability: 7.0, Driver API Version: 11.0, Runtime API Version: 10.0
W0114 18:07:51.593963 16234 device_context.cc:356] device: 4, cuDNN Version: 7.6.
[Epoch 0, batch 0] loss: 0.14651, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 5] loss: 1.82926, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 10] loss: 0.00000, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 15] loss: 0.13787, acc1: 0.03125, acc5: 0.03125
[Epoch 0, batch 20] loss: 0.12400, acc1: 0.03125, acc5: 0.06250
[Epoch 0, batch 25] loss: 0.17749, acc1: 0.00000, acc5: 0.00000
...
完整2 卡的日志信息也可在./log/
目录下查看。
飞桨大规模分类库使用介绍¶
简介¶
图像分类技术日趋成熟,ResNet网络在ImageNet数据集上的top5准确率已超过96%。然而,如何高效地完成百万类别甚至是更大规模的分类任务,则是一个极具挑战性的课题。
从多分类神经网络的实现角度分析,其最后一层通常是由全连接层和Softmax构成的组合层,全连接层输出结点数挂钩分类任务的类别数,所以对应的参数量随分类类别数的增长而线性增长。因此,当类别数非常大时,神经网络训练过程占用的显存空间也会很大,甚至是超出单张GPU卡的显存容量,导致神经网络模型无法训练。
以新闻推荐系统为例,假设要对百万类细分类别的新闻条目进行分类,那么仅存储全连接层参数就需要约2GB的显存空间(这里假设神经网络最后一层隐层的输出结点的维度为512,并假设以32比特浮点数表示数据,见下式)。再考虑神经网络训练过程中生成的数量庞多的中间变量,那么训练过程中需要的存储总量往往会超出单张GPU卡的显存容量。
$$全连接层参数显存消耗=\frac{512*10^6*4B}{1024^3}\approx2GB$$
原理介绍¶
该如何解决这个问题呢?常用的做法是“拆分”。考虑到全连接层的线性可分性,可以将全连接层参数切分到多张GPU卡,采用模型并行方案,减少每张GPU卡的参数存储量。
以下图为例,全连接层参数按行切分到不同的GPU卡上。每次训练迭代过程中,各张GPU卡分别以各自的训练数据计算隐层的输出特征(feature),并通过集合通信操作AllGather得到汇聚后的特征。接着,各张GPU卡以汇聚后的特征和部分全连接层参数计算部分logit值(partial logit),并基于此计算神经网络的损失值。详细推导过程请参阅附录。

这个方案可以有效解决全连接层参数量随分类类别数线性增长导致的显存空间不足的问题。然而,为了实现这一方案,开发者需要基于现有的深度学习平台设计和实现上例描述的所有操作,包括全连接层参数的切分和集合通信等,动辄需要数百行实现代码,大大增加了开发者的负担。飞桨大规模分类库(PLSC: PaddlePaddle Large Scale Classification),为用户提供了大规模分类任务从训练到部署的全流程解决方案。只需数行代码,即可实现千万类别分类的神经网络。并且,通过PLSC库提供的serving功能用户可以快速部署模型,提供一站式服务。
更多PLSC使用文档,请参阅: PLSC Repo。
使用Sharding 训练超大模型¶
简介¶
当模型参数达到百亿或者千亿时, 传统的数据并行训练可能会遇到显存瓶颈。 在数据并行训练中,每个gpu worker 都有一份完整模型参数和优化器状态副本。 《ZeRO: Memory Optimizations Toward Training Trillion Parameter Models》 指出在每个GPU 上都保存一份模型参数和优化器状态副本是冗余的。 我们可以通过将上述参数和副本划分到不同GPU 中, 在每个GPU 只保存部分副本,来减少每张GPU上显存的占用,从而可以支持更大模型的训练。
原理¶
Sharding¶
Sharding 实现了类似ZeRO-DP 的训练策略,将模型参数(parameter)、参数梯度(gradient)、参数对应的优化器状态(moment)切分到每一张GPU 上。让模型参数部分所占的显存随并行卡数的增加而减少。 通过 paddle.distributed.fleet 提供的简单易用接口, 用户只需要添加几行代码就可将策略加入到原有的训练中。
模型训练过程中的显存消耗主要由两大部分组成:模型参数及优化器状态、训练产生的中间变量(activations)。 sharding 策略只切分了模型参数和优化器状态,因此模型参数和优化器状态所消耗的显存可以随着并行GPU数量增加而线性减少; 但是每张GPU上仍然维护着模型完整的前向和反向,所以每张GPU依然需要存放模型的训练过程中的产生的全部的中间变量,这部分显存消耗 不会随着GPU 数量的增加而减少。 用户可以通过结合 recompute 策略来减少 activation这部分的显存消耗。
通过sharding 和增加并行GPU 数量,用户可以训练百亿甚至千亿参量的超大模型 (需要结合 recompute, amp 策略)。
Sharding-hybrid-dp¶
Sharding hybrid数据并行策略,在sharding 并行的基础上再增加一层数据并行逻辑。
该策略的目的是通过 限制sharding 通信的节点数
和 增加多路数据并行
来提高训练吞吐。 如果一个模型在普通Sharding 训练时需要M 张GPU,则则开启hybrid-dp 至少需要 N*M GPU (N>= 2)。
Sharding-hybrid-dp 适用的场景如下:
当前有 4个 8 卡v100 节点
目标模型A 在Sharding 训练时至少需要 8卡 v100 (一个完整的8 卡v100节点)
希望利用全部的 4 个节点来加速训练
上述情况如果直接使用全部的 4 个节点 进行普通的sharding 训练, 那么全部的 32 gpus 之间组成一个完整 Sharding parallelism。这样会因为通信瓶颈造成训练速度非常慢:
Sharding 中的broadcast 通信 会涉及全部的32 张卡,且为跨节点通信。
Sharding 中的 allreduce 通信 会涉及全部的32 张卡,且为跨节点通信。
开启 hybrid-dp 并设置 sharding_group_size = 8
后, 每个节点内的 8 张卡组成一个完整的 Sharding parallelism,4 个节点构成4路hybrid data parallelism:
Sharding 中的broadcast 通信被限制在每个节点内的 8 张GPU 之间, 没有跨节点通信。
Sharding 中的 allreduce 为跨节点通信,但每个allreduce 通信只涉及 对应 sharding_group 上 rank 相同的 4 张GPUs, 且每张GPU仅需要 allreduce通信 1/8 的模型参数。
Sharding-hybrid-dp 通过上述措施,可以较大程度 减少 Sharding 训练 从1节点扩展到4 节点时的(跨节点)通信量。提高节点增加时的加速比,提高训练吞吐。
P.S. hybrid dp 是因为 Sharding parallelism 本身内含一层 data parallelism 逻辑, hybrid dp 是在 Sharding parallelism之上再增加新的一层 data parallelism 逻辑。
效果¶
下面表格将对比 Sharding 策略对显存的影响。
模型为 Bert-Large,试验环境为 v100 (32GB), recompute = ON, amp = ON, hybrid-dp = OFF。 模型不变,单卡batch size 不变,当并行GPU数量增加时,显存的消耗将减小。 省下的显存可以用来增大模型。
setting |
GPU Mem |
---|---|
sharding—off |
8667 MB |
sharding—on N1C2 |
5615 MB |
sharding—on N1C4 |
4841 MB |
sharding—on N1C8 |
4127 MB |
sharding—on N2C16 |
3700 MB |
Sharding 结合 amp + recompute,可以在 128 张 32GB V100 并行的情况下支持千亿参数(115B)ERNIE 训练。
使用方法¶
静态图¶
sharding 可以设置以下参数:
sharding_segment_strategy(float, optional): 选择sharding 中用来将前向反向program 切segments 的策略。目前可选策略有:”segment_broadcast_MB” 和 “segment_anchors”。 segment 是sharding中引入的一个内部概念,目的是用来让通信和计算相互重叠掩盖(overlap)。默认值是 segment_broadcast_MB.
segment_broadcast_MB(float, optional): 根据sharding 广播通信中的参数量来切segments,仅当 sharding_segment_strategy = segment_broadcast_MB时生效。sharding 会在前向和反向中引入参数广播,在该segment 策略下,每当参数广播量达到 “segment_broadcast_MB”时,在program 中切出一个segment。该参数是一个经验值,最优值会受模型大小和网咯拓扑的影响。 默认值是 32.
segment_anchors(list): 根据用户选定的锚点切割 segments,仅当 sharding_segment_strategy = segment_anchors 生效。该策略可以让用户更精确的控制program 的切分,目前还在实验阶段。
sharding_degree(int, optional): sharding并行数。 sharding_degree=1 时,sharding 策略会被关闭。 默认值是 8。
gradient_merge_acc_step(int, optional): 梯度累积中的累积步数。 gradient_merge_acc_step=1 梯度累积会被关闭。 默认值是 1。
optimize_offload(bool, optional): 优化器状态卸载开关。 开启后会将优化器中的状态(moment) 卸载到Host 的内存中,以到达节省GPU 显存、支持更大模型的目的。开启后,优化器状态会在训练的更新阶段经历:预取-计算-卸载(offload)三个阶段,更新阶段耗时会增加。 这个策略需要权衡显存节省量和训练速度,仅推荐在开启梯度累积并且累积步数较大时开启。 因为累积步数较大时,训练中更新阶段的比例将远小于前向&反向阶段, 卸载引入的耗时将不明显。
dp_degree(int, optional): 数据并行的路数。 当dp_degree>=2 时,会在内层并行的基础上,再引入dp_degree路 数据并行。用户需要保证 global_world_size = mp_degree * sharding_degree * pp_degree * dp_degree。 默认值是 1。
mp_degree(int, optional): [仅在混合并行中使用] megatron 并行数。 mp_degree=1 时,mp 策略会被关闭。 默认值是 1。
pp_degree(int, optional): [仅在混合并行中使用] pipeline 并行数。 pp_degree=1 时,pipeline 策略会被关闭。 默认值是 1。
pp_allreduce_in_optimize(bool, optional): [仅在混合并行中使用] 在开启pipeline 并行后,将allreduce 操作从反向阶段移动到更新阶段。根据不同的网络拓扑,该选项会影响训练速度,该策略目前还在实验阶段。 默认值是 False。
为了示例代码的简练,下面例子中使用较小 resnet50 模型作为演示。实际训练中,sharding 的目标是通过牺牲训练速度以换取对更大模型的支持,故不适用于 resnet50 等单卡就能训练的模型。
因为resnet50 较小,我们可以令sharding_degree = 2
让模型参数被切分为2 个shards,然后在 一个单机4卡 v100 的节点上组成 2 路 dp 并行进行演示。
strategy = fleet.DistributedStrategy()
strategy.sharding = True
strategy.sharding_configs = {
"sharding_segment_strategy": "segment_broadcast_MB",
"segment_broadcast_MB": 32,
"sharding_degree": 2,
"dp_degree": 2,
}
上述例子的完整代码存放在:train_fleet_sharding.py下面。假设要运行4卡的任务,那么只需在命令行中执行:
python -m paddle.distributed.launch --gpus=4,5,6,7 train_fleet_sharding.py
您将看到显示如下日志信息:
----------- Configuration Arguments -----------
gpus: 4,5,6,7
heter_worker_num: None
heter_workers:
http_port: None
ips: 127.0.0.1
log_dir: log
...
------------------------------------------------
...
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_TRAINER_ID 0 |
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:18362 |
| PADDLE_TRAINERS_NUM 4 |
| PADDLE_TRAINER_ENDPOINTS ... 0.1:23911,127.0.0.1:35135,127.0.0.1:38263|
| FLAGS_selected_gpus 4 |
+=======================================================================================+
...
2021-05-12 12:02:20 INFO Hybrid DP mode turn on !
2021-05-12 12:02:20 INFO global word size: 4
2021-05-12 12:02:20 INFO global rank: 0
2021-05-12 12:02:20 INFO global endpoints: ['127.0.0.1:10033', '127.0.0.1:21161', '127.0.0.1:13997', '127.0.0.1:27877']
2021-05-12 12:02:20 INFO global ring id: 3
2021-05-12 12:02:20 INFO ##############################
2021-05-12 12:02:20 INFO mp group size: 1
2021-05-12 12:02:20 INFO mp rank: -1
2021-05-12 12:02:20 INFO mp group id: -1
2021-05-12 12:02:20 INFO mp group endpoints: []
2021-05-12 12:02:20 INFO mp ring id: -1
2021-05-12 12:02:20 INFO ##############################
2021-05-12 12:02:20 INFO sharding group size: 2
2021-05-12 12:02:20 INFO sharding rank: 0
2021-05-12 12:02:20 INFO sharding group id: 0
2021-05-12 12:02:20 INFO sharding group endpoints: ['127.0.0.1:10033', '127.0.0.1:21161']
2021-05-12 12:02:20 INFO sharding ring id: 1
2021-05-12 12:02:20 INFO ##############################
2021-05-12 12:02:20 INFO pp group size: 1
2021-05-12 12:02:20 INFO pp rank: -1
2021-05-12 12:02:20 INFO pp group id: -1
2021-05-12 12:02:20 INFO pp group endpoints: []
2021-05-12 12:02:20 INFO pp ring id: -1
2021-05-12 12:02:20 INFO ##############################
2021-05-12 12:02:20 INFO pure dp group size: 2
2021-05-12 12:02:20 INFO pure dp rank: 0
2021-05-12 12:02:20 INFO pure dp group endpoints: ['127.0.0.1:10033', '127.0.0.1:13997']
2021-05-12 12:02:20 INFO pure dp ring id: 2
2021-05-12 12:02:20 INFO ##############################
...
+==============================================================================+
| sharding=True <-> sharding_configs |
+------------------------------------------------------------------------------+
| sharding_segment_strategy segment_broadcast_MB |
| segment_broadcast_MB 32.0 |
| sharding_degree 2 |
| mp_degree 1 |
| dp_degree 2 |
| hybrid_dp False |
| gradient_merge_acc_step 1 |
| optimize_offload False |
| pp_allreduce_in_optimize False |
| pp_degree 1 |
+==============================================================================+
...
W0114 18:07:51.588716 16234 device_context.cc:346] Please NOTE: device: 4, GPU Compute Capability: 7.0, Driver API Version: 11.0, Runtime API Version: 10.0
W0114 18:07:51.593963 16234 device_context.cc:356] device: 4, cuDNN Version: 7.6.
[Epoch 0, batch 0] loss: 4.58475, acc1: 0.03125, acc5: 0.18750
[Epoch 0, batch 5] loss: 23.57863, acc1: 0.06250, acc5: 0.06250
[Epoch 0, batch 10] loss: 13.08259, acc1: 0.00000, acc5: 0.06250
[Epoch 0, batch 15] loss: 9.19330, acc1: 0.00000, acc5: 0.06250
[Epoch 0, batch 20] loss: 7.46575, acc1: 0.03125, acc5: 0.06250
[Epoch 0, batch 25] loss: 4.44061, acc1: 0.15625, acc5: 0.18750
[Epoch 0, batch 30] loss: 5.20638, acc1: 0.06250, acc5: 0.12500
[Epoch 0, batch 35] loss: 4.75518, acc1: 0.03125, acc5: 0.09375
[Epoch 0, batch 40] loss: 5.02654, acc1: 0.06250, acc5: 0.09375
...
完整4卡的日志信息也可在./log/
目录下查看。
动态图¶
首先简单总结sharding stage1、stage2、stage3分别实现减小参数规模的原理。stage1、stage2、stage3分别在训练过程中对模型优化器状态、梯度+优化器状态、参数+梯度+优化器状态进行切分,通过减小训练的相关Tensor(参数、梯度、优化器状态)达到同样计算资源下能够训练更大模型的效果。
以下是分别从sharding的三种实现阶段分别介绍下使用方式,stage1的动态图实现方式:
import paddle
# init fleet and setting sharding degree
import paddle.distributed.fleet as fleet
form paddle.distributed.fleet.meta_optimizers.dygraph_optimizer.dygraph_sharding_optimizer import DygraphShardingOptimizer
strategy = fleet.DistributedStrategy()
strategy.hybrid_configs = {
"dp_degree": args.dp_degree,
"mp_degree": 1,
"pp_degree": 1,
"sharding_degree": args.sharding_degree,
}
fleet.init(is_collective=True, strategy=strategy)
# wrap model & optimizer
model = model_class(...)
model = fleet.distributed_model(model)
optimizer = DygraphShardingOptimizer(
hcg = fleet.get_hybrid_communicate_group(),
user_defined_strategy = strategy,
params = model.parameters(),
inner_optimizer_class = paddle.optimizer.AdamW,
learning_rate = lr_scheduler,
epsilon = args.adam_epsilon,
weight_decay = args.weight_decay,
apply_decay_param_fun = lambda x: x in decay_params,
)
optimizer = fleet.distributed_optimizer(optimizer)
# use optimizer as normal
out = model(input=data)
loss = criterion(out)
loss.backward()
optimizer.step()
optimizer.clear_grad()
- 目前stage2、3还不支持混合并行方式,其动态图实现方式:
使用group_sharded_parallel和save_group_sharded_model两个API可以进行训练和保存。使用group_sharded_parallel提供stage1的选项,内部使用stage2完成优化实现。参考`group_sharded_parallel <https://www.paddlepaddle.org.cn/documentation/docs/en/develop/api/paddle/distributed/sharding/group_sharded_parallel_en.html#group-sharded-parallel>`__,save_group_sharded_model。
此处需要注意,使用save_group_sharded_model保存模型,再次load时需要在调用group_sharded_parallel前对model和optimizer进行set_state_dict。
目前stage2、3已经适配GPT模型,可以参考请参考 示例代码。
其次解决组网中共享参数训练问题,stage3 需要额外在组网中加入外置参数注册逻辑,在组网中需要注册self.extra_parameters = [self.gpt.embeddings.word_embeddings.weight],这部分可以参考PaddleNLP中gpt-3的组网。
import paddle
from paddle.fluid.dygraph.nn import Linear
from paddle.distributed import fleet
from paddle.distributed.sharding import group_sharded_parallel, save_group_sharded_model
fleet.init(is_collective=True)
group = paddle.distributed.new_group([0, 1])
model = Linear(1000, 1000)
clip = paddle.nn.ClipGradByGlobalNorm(clip_norm=1.0)
optimizer = paddle.optimizer.AdamW(learning_rate=0.001, parameters=model.parameters(), weight_decay=0.00001, grad_clip=clip)
# wrap sharding model, optimizer and scaler
model, optimizer, scaler = group_sharded_parallel(model, optimizer, "p_g", scaler=scaler)
img, label = data
label.stop_gradient = True
img.stop_gradient = True
out = model(img)
loss = paddle.nn.functional.cross_entropy(input=out, label=label)
loss.backward()
optimizer.step()
optimizer.clear_grad()
# save model and optimizer state_dict
save_group_sharded_model(model, optimizer, output=output_dir)
进阶用法¶
上面例子介绍了静态图 sharding 的基本用法,能直接应用于 resnet、 transformer 等常见组网组网。 如果用户的组网比较特殊或希望修改sharding 的逻辑可以阅读下面内容。
Sharding 通信组¶
Sharding 会自动每一个Rank(GPU)创建其通信所需的资源 ———— 通信组(groups), 在Paddle 静态图中每一个通信组都有一个唯一 ring_id 标识。 Sharding 会为每一个 Rank 创建两个通信组:
Sharding 通信组(必须):ring_id=1, group_size = sharding_degree
DP 通信组(仅当开启sharidng-dp 时):ring_id=2, group_size = dp_degree
例如在上文 sharding_degree = 2, dp_degree = 2 的例子中, rank0 上的两个通信组为:
Sharding 通信组:ring_id=1, group_size = 2,组成员为[rank0, rank1]
DP 通信组:ring_id=2, group_size = 2, 组成员为[rank0, rank3]
用户也可以从训练开始前打印的日志信息中看到对应的信息。 如果用户希望在模型中引入新的通信组, 需要避免sharding已经占用的 ring_id (1 和 2)。
Sharding 通信Ops¶
通信组建立好后,Sharding 会向模型的前向、反向组网中插入同步通信ops (broadcast)。 用户可以通过打印 Sharidng 生效后生成的 Program 查看 Sharidng 通信ops 具体插入的位置。
同步通信操作的乱序(各rank 间同步通信op插入/执行的顺序的不匹配)非常容易造成训练 hang死或计算错误,所以用户组网中如果希望引入自定义通信op,需要主动避免和原有Sharding 通信ops 产生乱序。
Sharidng 通信op 的插入逻辑建立在每个rank 相同的组网之上(因为Sharding 本质也是数据并行),并在每一rank上执行相同的插入规则(因为同步通信), 不会和组网中已存在的用户自定义通信ops 产生组网的“插入乱序”。
“执行乱序”的情况比较特殊,会涉及到模型具体执行逻辑和调度方式。Sharding 中的调度方式是将Sharding 通信ops 和模型计算ops 分别调度到不同的stream上,让通信和计算能最大程度重叠。 一个简单但不太高效的方法是在模型组网里的自定义通信ops 的前后,插入强制的同步, 避免执行时的通信乱序。Paddle 静态图中提供了两个强制同步 op:
c_sync_comm_stream: 同步通信流
c_sync_calc_stream: 同步计算流
用户可以也尝试使用 wait op 做更进阶的同步和重叠。
模型并行¶
简介¶
通常来讲,训练更大规模的网络模型可以在多种任务上取得更好的效果,如自然语言处理类 任务的准确率。然而,训练更大规模的网络模型会消耗更多的显存资源,甚至是超过单个设 备的显存容量,从而导致模型无法训练。模型并行通过将网络中的张量(Tensor)切分到不 同的设备,从而降低单个设备的显存消耗,使得超大规模模型训练成为可能。本文主要介绍 飞桨模型并行的基本原理和使用方法。
原理介绍¶
自2017年提出以来, Transformer 及其 变种模型成为自然语言类任务的常用模型,并于近年来被应用到图像视觉领域。 Transformer模型的基础结构是由Attention和MLP组成的Encoder和Decoder,以及 Embedding,如下图所示[1]。其中Attention和MLP的底层实现均为矩阵乘法运算,而Embedding是一种 查找表实现。

对于Embedding操作,可以将其理解为一种查找表操作。即,将输入看做索引,将Embedding参数 看做查找表,根据该索引查表得到相应的输出,如下图(a)所示。当采用模型并行时, Embedding的参数被均匀切分到多个卡上。假设Embedding参数的维度为N*D,并采用K张卡执行模型 并行,那么模型并行模式下每张卡上的Embedding参数的维度为N//K*D。当参数的维度N不能被卡 数K整除时,最后一张卡的参数维度值为(N//K+N%K)*D。以下图(b)为例,Embedding参数的维度 为8*D,采用2张卡执行模型并行,那么每张卡上Embedding参数的维度为4*D。
为了便于说明,以下我们均假设Embedding的参数维度值D可以被模型并行的卡数D整除。此时,每张 卡上Embeeding参数的索引值为[0, N/K),逻辑索引值为[k*N/K, (k+1)*N/K),其中k表示卡序号, 0<=k<K。对于输入索引I,如果该索引在该卡表示的逻辑索引范围内,则返回该索引所表示的表项(索引 值为I-k*N/K;否则,返回值为全0的虚拟表项。随后,通过AllReduce操作获取所有输出表项的和,即 对应该Embeding操作的输出;整个查表过程如下图(b)所示。

对于矩阵乘操作,是按行或者列将矩阵切分K份。假设原始矩阵的维度为M*N,则按行切分后,各个 卡上的矩阵维度为M/K*N;若按列切分,则各个卡上矩阵的维度值为M*N/K。
下图给出按列切分矩阵乘法的示例图。其中,图(a)给出单卡上的矩阵乘法。图(b)给出模型并行 模式下的矩阵乘法,其中第二个矩阵按列切分到2张卡上;两张卡分别得到结果矩阵的一部分。最后,通过 AllGather通信操作汇聚最终的结果。

下图给出按行切分矩阵乘法的示例图。其中,图(a)给出单卡上的矩阵乘法。图(b)给出模型并行 模式下的矩阵乘法,其中第二个矩阵按行切分到2张卡上;第一个矩阵需要按列切分,以满足矩阵乘法 的维度要求;两张卡分别得到结果矩阵的一部分。最后,通过 AllReduce通信操作按元素累加结果矩阵得到最终的结果。

我们观察到,可以把上述按列切分矩阵乘法和按行切分矩阵乘法串联起来,从而省略掉一次AllGather通信 操作,如下图所示。同时,我们注意到Transformer的Attention和MLP组件中各种两次矩阵乘法操作。因此,我们 可以按照这种串联方式分别把Attention和MLP组件中的两次矩阵乘法串联起来,从而进一步优化性能。

我们观察到,在模型并行模式下,Transformer的Attention组件中存在两种类型的Dropout操作,如下图 所示[1]。第一类是softmax算子后的Dropout算子;其输入是按列切分矩阵乘法的部分结果,我们称为局部 Dropout。直观理解,模型并行下,所有卡上的Dropout算子构成一个完整的Dropout算子,因此我们需要 确保不同卡上该类Dropout算子的丢弃位置是不同。第二类是图中g操作之后的Dropout操作,对于此类Dropout,其 输入均为完整且相同的输出,我们需要确保Dropout算子的输出也相同,即各个卡上该类Dropout算子选择 的丢弃位置是相同的。我们称此类Dropout为全局Dropout。我们通常通过设置种子来控制两类Dropout的输出。 具体地讲,对于局部Dropout,我们在不同的卡上为他们设置不同的种子,从而确保它们选择的丢弃位置是 不同的。而对于全局Dropout算子,我们在不同的卡上为它们设置相同的种子,从而确它们在不同卡上选择的 丢弃位置是相同的。

我们需要注意一下几点:
模型并行下,需要确保模型并行组中各个卡读取相同的数据;
模型并行下,除了被切分的算子对应的输出外,其它所有算子的输出在各个卡上是一致的。
使用方法¶
下面我们将分别介绍如何在静态图和动态图模式下使用飞桨模型并行。
静态图使用方法¶
静态图中,我们提供了 paddle.distributed.split 实现
Embedding和矩阵乘法算子的切分。我们需要对该API的 gather_out
参数做一些特殊说明:对于Embedding切分操作,该参数始终设置
为True。对于矩阵切分操作,如果该参数设置为True,则会在算子操作后使用通信操作获取最终结果。参照上文,对于连续的两个切分的矩阵
乘法操作,我们通常对第一个矩阵乘法采用按列切分方法,对第二个矩阵乘法采用按行切分方法;并且,对于按列切分的矩阵乘法,我们
将 gather_out
参数设置为False,从而省略掉一次通信操作。
下面的例子给出在两张卡上实现Embedding算子模型并行的示例。
emb_out = padle.distributed.split(
in,
(8, 8),
operation="embedding",
num_partitions=2)
此外,我们还需要配置Fleet的选项,以使用模型并行功能。
fleet.init(is_collective=True)
dist_strategy = paddle.distributed.fleet.DistributedStrategy()
dist_strategy.tensor_parallel = True
strategy.tensor_parallel_configs = {"tensor_parallel_degree": 4}
其中, tensor_parallel_degree
指定模型并行的并行度。
如上文所述,对于Transformer模型,存在两种类型的Dropout:全局Dropout和局部Dropout;对于 全局Dropout,需要在模型并行的所有卡上设置相同的种子,对于局部Dropout,则需要设置不同的 种子。我们通过如下代码分别设置全局和局部种子:
mp_local_seed = basic_seed + mp_rank * 11
mp_global_seed = basic_seed
paddle.framework.random.set_random_seed_generator('mp_local_seed', mp_local_seed)
paddle.framework.random.set_random_seed_generator('mp_global_seed', mp_global_seed)
上例只是一种示例实现,用户可以根据自己的需要实现不同的种子设置方式,但需要确保同一模型并行 组内,全局Dropout的种子是一致的,而局部Dropout的种子是不同的。
在使用 dropout
接口时,我们还需要根据其类型设置其种子参数,如下例所示:
# For local dropout
weights = dropout(
weights,
p=dropout_rate,
rng_name='mp_local_seed',
training=True,
mode='upscale_in_train')
# For global dropout
weights = dropout(
weights,
p=dropout_rate,
rng_name='mp_global_seed',
training=True,
mode='upscale_in_train')
当结合使用模型并行和数据并行时,无需指定额外的参数。但需要确保,训练卡总数是模型并行并行度的整数倍。
在Paddle里面,模型并行主要体现为三种方式,切分Embedding层,列切分Linear层和行切分Linear层。
下面代码在Paddle2.0以上可以运行,建议将Paddle版本升级到最新版
首先导入需要的包
import paddle
import numpy as np
import random
import paddle.distributed as dist
import paddle.fluid as fluid
import paddle.distributed.fleet as fleet
import os
import paddle.nn as nn
因为是使用静态图方式,所以需要在程序一开始的地方就声明使用静态图方式
paddle.enable_static()
# 声明一些需要使用的全局变量
vocab_size = 20
hidden_size = 10
inner_size = 8
output_size = 10
seq_length = 2
batch_size = 4
为了验证模型并行的正确性,需要定义一个单卡模型作比较
class SimpleNet(nn.Layer):
def __init__(self, vocab_size, hidden_size, inner_size, output_size, np_fc1, np_fc2):
super(SimpleNet, self).__init__()
self.linear1 = paddle.nn.Linear(
hidden_size,
inner_size,
weight_attr=paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Assign(np_fc1)
),
bias_attr=None
)
self.linear2 = paddle.nn.Linear(
inner_size,
hidden_size,
weight_attr=paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Assign(np_fc2)
),
bias_attr=None
)
self.linear3 = paddle.nn.Linear(
hidden_size,
output_size,
weight_attr=paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Constant(0.1)
)
)
self.embedding = paddle.nn.Embedding(
vocab_size,
hidden_size,
weight_attr=paddle.nn.initializer.Constant(value=0.5)
)
def forward(self, x):
x = self.embedding(x)
x = self.linear1(x)
x = self.linear2(x)
x = self.linear3(x)
return x.mean()
然后使用paddle.distributed.split函数进行三种切分方式的演示,
首先
paddle.distributed.split(…., operation=”embedding”, axis=0, gather_out=True, ….): 切分embedding层,需要指定operation参数为”embedding”, 然后切分的维度axis只支持0,无论gather_out指定为什么,都会在切分embedding做完相应的计算后进行all reduce通信
paddle.distributed.split(…., operation=”linear”, axis=1, gather_out=False, …): 列切分linear层,需要指定operation参数为”linear”, axis的值为1,gather_out为True时会在linear后添加all gather通信
paddle.distributed.split(…., operation=”linear”, axis=0, gather_out=True, ….): 行切分linear层,需要指定operation参数为”linear”, axis的值为0,gather_out为True时会在linear后添加all reduce通信
class SimpleMPNet(nn.Layer):
def __init__(self, vocab_size, hidden_size, inner_size, output_size, np_fc1,
np_fc2, mp_id):
super(SimpleMPNet, self).__init__()
if mp_id == 0:
init_fc1_data = np_fc1[:, :(inner_size // 2)]
init_fc2_data = np_fc2[:(inner_size // 2), :]
else:
init_fc1_data = np_fc1[:, (inner_size // 2):]
init_fc2_data = np_fc2[(inner_size // 2):, :]
self.weight_attr1 = paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Assign(init_fc1_data)
)
self.weight_attr2 = paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Assign(init_fc2_data)
)
self.linear3 = paddle.nn.Linear(
hidden_size,
output_size,
weight_attr=paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Constant(0.1)
)
)
self.embedding_weight = paddle.nn.initializer.Constant(value=0.5)
def forward(self, x):
x = paddle.distributed.split(
x, size=(vocab_size, hidden_size), operation="embedding", axis=0, num_partitions=2,
gather_out=True, weight_attr=self.embedding_weight
)
x = paddle.distributed.split(
x, size=(hidden_size, inner_size), operation="linear", axis=1, num_partitions=2,
gather_out=False, weight_attr=self.weight_attr1
)
x = paddle.distributed.split(
x, size=(inner_size, hidden_size), operation="linear", axis=0, num_partitions=2,
gather_out=True, weight_attr=self.weight_attr2
)
x = self.linear3(x)
return x.mean()
定义生成数据的方式
def gen_data():
np.random.seed(2021)
while True:
data = [np.random.randint(0, vocab_size, [seq_length])]
yield data
分布式环境初始化,生成对应program
train_mp_col_program = fluid.Program()
mp_startup_program = fluid.Program()
strategy = fleet.DistributedStrategy()
strategy.tensor_parallel = True
strategy.tensor_parallel_configs = {'tensor_parallel_degree': 2}
fleet.init(is_collective=True)
因为要和单卡比对,所以要固定住seed,同时因为模型并行切分了linear层,导致对于切分linear的参数即使固定seed也不会和单卡对应,需要手动创建numpy矩阵作为linear的weight参数
def set_random_seed(seed, rank_id):
"""Set random seed for reproducability."""
random.seed(seed)
np.random.seed(seed)
paddle.seed(seed + rank_id)
device_id = int(os.getenv("FLAGS_selected_gpus", "0"))
set_random_seed(1024, device_id)
np_fc1 = np.random.random_sample((hidden_size, inner_size))
np_fc2 = np.random.random_sample((inner_size, hidden_size))
然后构建program,创建出单卡模型和数据并行模型的实体,dataloader
with fluid.program_guard(main_program=train_mp_col_program, startup_program=mp_startup_program):
data_in = fluid.data(
name="data_in", shape=[batch_size, seq_length], dtype="int32"
)
train_reader = paddle.batch(gen_data, batch_size=batch_size)
data_loader = fluid.io.DataLoader.from_generator(
feed_list=[data_in],
capacity=64,
use_double_buffer=False,
iterable=False
)
rank = fleet.worker_index()
model_mp = SimpleMPNet(vocab_size, hidden_size, inner_size, output_size,
np_fc1, np_fc2, mp_id=rank)
model_single = SimpleNet(vocab_size, hidden_size, inner_size, output_size, np_fc1, np_fc2)
avg_cost_mp = model_mp(data_in)
avg_cost_single = model_single(data_in)
mp_opt = fluid.optimizer.SGD(0.1)
dist_opt = fleet.distributed_optimizer(mp_opt, strategy=strategy)
dist_opt.minimize(avg_cost_mp)
single_opt = fluid.optimizer.SGD(0.1)
single_opt.minimize(avg_cost_single)
然后运行startup program和mp program
place = paddle.CUDAPlace(device_id)
exe = paddle.static.Executor(place)
exe.run(mp_startup_program)
data_loader.set_sample_list_generator(train_reader, place)
data_loader.start()
fetch_lists = []
fetch_lists.extend([avg_cost_mp, avg_cost_single])
for i in range(5):
vars = exe.run(train_mp_col_program, fetch_list=fetch_lists)
print("mp_loss: ", vars[0], "single_loss: ", vars[1])
data_loader.reset()
运行方式(需要保证当前机器有两张gpu):
export CUDA_VISIBLE_DEVICES=0,1
python -m paddle.distributed.launch mp_static.py
模型并行的静态图代码:example/model_parallelism/mp_static.py。
控制台输出信息如下:
WARNING 2021-10-27 08:51:49,126 launch.py:381] Not found distinct arguments and compiled with cuda or xpu. Default use collective mode
launch train in GPU mode!
INFO 2021-10-27 08:51:49,128 launch_utils.py:525] Local start 2 processes. First process distributed environment info (Only For Debug):
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_TRAINER_ID 0 |
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:10129 |
| PADDLE_TRAINERS_NUM 2 |
| PADDLE_TRAINER_ENDPOINTS 127.0.0.1:10129,127.0.0.1:34811 |
| PADDLE_RANK_IN_NODE 0 |
| PADDLE_LOCAL_DEVICE_IDS 0 |
| PADDLE_WORLD_DEVICE_IDS 0,1 |
| FLAGS_selected_gpus 0 |
| FLAGS_selected_accelerators 0 |
+=======================================================================================+
日志信息位于log目录下, 需要注意的是模型并行的loss与单卡模型的loss在小数点后三位是能够精确对齐的,然后两张卡上对应的loss应该是一样的:
mp_loss: [11.943981] single_loss: [11.943981]
mp_loss: [-2.2283082] single_loss: [-2.2283082]
mp_loss: [-13.341571] single_loss: [-13.341571]
mp_loss: [-29.284101] single_loss: [-29.284101]
mp_loss: [-63.219418] single_loss: [-63.21941]
动态图使用方法¶
动态图中,我们提供了以下接口实现Embeeding和矩阵切分:
paddle.distributed.fleet.meta_parallel.VocabParallelEmbedding
paddle.distributed.fleet.meta_parallel.ColumnParallelLinear
paddle.distributed.fleet.meta_parallel.RowParallelLinear
定义如下:
class VocabParallelEmbedding(Layer):
def __init__(self,
num_embeddings, # Embedding参数的行数
embedding_dim, # Embedding参数的列数
weight_attr=None,
name=None):
super(VocabParallelEmbedding, self).__init__()
class RowParallelLinear(Layer):
def __init__(self,
in_features,
out_features,
weight_attr=None,
has_bias=True,
input_is_parallel=False, #输入是否是并行输入,为否的话需要按列切分输入参数
name=None):
super(RowParallelLinear, self).__init__()
class ColumnParallelLinear(Layer):
def __init__(self,
in_features,
out_features,
weight_attr=None,
has_bias=None,
gather_output=True, # 是否在该算子后汇聚所有卡的输出
name=None):
下面的例子给出在两张卡上实现Embedding算子模型并行的示例。
import paddle.distributed.fleet as fleet
word_embeddings = fleet.meta_parallel.VocabParallelEmbedding(
vocab_size,
hidden_size,
weight_attr=paddle.ParamAttr(initializer=nn.initializer.Normal(
mean=0.0, std=initializer_range)))
此外,我们还需要配置Fleet的选项,以使用模型并行功能。
dist_strategy = paddle.distributed.fleet.DistributedStrategy()
strategy.hybrid_configs = {
"mp_degree": 2,
"dp_degree": 1,
}
fleet.init(is_collective=True, strategy=strategy)
hcg = fleet.get_hybrid_communicate_group()
global_rank = hcg.get_global_rank() # 全局rank
mp_rank = hcg.get_model_parallel_rank() # 模型并行组rank
当结合使用模型并行和数据并行时,我们需要指定 dp_dgree
参数,设置数据并行的并行度。
如上文所述,对于Transformer模型,存在两种类型的Dropout:全局Dropout和局部Dropout;对于 全局Dropout,需要在模型并行的所有卡上设置相同的种子,对于局部Dropout,则需要设置不同的 种子。我们通过如下代码分别设置全局和局部种子:
from paddle.distributed.fleet.meta_parallel import get_rng_state_tracker
local_seed = basic_seed + mp_rank * 11
global_seed = basic_seed
tracker.add('global_seed', global_seed)
tracker.add('local_seed', local_seed)
上例只是一种示例实现,用户可以根据自己的需要实现不同的种子设置方式,但需要确保同一模型并行 组内,全局Dropout的种子是一致的,而局部Dropout的种子是不同的。
在使用 Dropout
接口时,我们还需要根据其类型设置其种子,如下例所示:
# For local dropout
import paddle.nn.functional as F
from paddle.distributed.fleet.meta_parallel import get_rng_state_tracker
with get_rng_state_tracker().rng_state('local_seed'):
weights = F.dropout(
weights,
dropout_rate,
training=True,
mode='upscale_in_train')
# For global dropout
with get_rng_state_tracker().rng_state('global_seed'):
weights = F.dropout(
weights,
dropout_rate,
training=True,
mode='upscale_in_train')
动态图的例子代码主要使用上面提到的三种类
下面代码在Paddle2.0以上可以运行,建议将Paddle版本升级到最新版
首先导入需要的包
import paddle
import numpy as np
import random
import paddle.distributed as dist
import paddle.fluid as fluid
import paddle.distributed.fleet as fleet
声明一些需要使用的全局变量
vocab_size = 20
hidden_size = 10
inner_size = 8
output_size = 10
seq_length = 2
batch_size = 4
定义单卡模型
class SimpleNet(fluid.dygraph.Layer):
def __init__(self, vocab_size, hidden_size, inner_size, output_size, np_fc1, np_fc2):
super(SimpleNet, self).__init__()
self.linear1 = paddle.nn.Linear(
hidden_size,
inner_size,
weight_attr=paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Assign(np_fc1)
),
bias_attr=paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Constant(0.0)
)
)
self.linear2 = paddle.nn.Linear(
inner_size,
hidden_size,
weight_attr=paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Assign(np_fc2)
),
bias_attr=paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Constant(0.0)
)
)
self.linear3 = paddle.nn.Linear(
hidden_size,
output_size,
weight_attr=paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Constant(0.0)
),
bias_attr=paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Constant(0.0)
)
)
self.embedding = paddle.nn.Embedding(
vocab_size,
hidden_size,
weight_attr=paddle.nn.initializer.Constant(value=0.5)
)
def forward(self, x):
x = self.embedding(x)
x = self.linear1(x)
x = self.linear2(x)
x = self.linear3(x)
return x
定义模型并行的模型
class SimpleMPNet(fluid.dygraph.Layer):
def __init__(self, vocab_size, hidden_size, inner_size, output_size, np_fc1,
np_fc2, mp_id):
super(SimpleMPNet, self).__init__()
if mp_id == 0:
init_fc1_data = np_fc1[:, :(inner_size // 2)]
init_fc2_data = np_fc2[:(inner_size // 2), :]
else:
init_fc1_data = np_fc1[:, (inner_size // 2):]
init_fc2_data = np_fc2[(inner_size // 2):, :]
self.linear1 = fleet.meta_parallel.ColumnParallelLinear(
hidden_size,
inner_size,
weight_attr=paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Assign(init_fc1_data)
),
gather_output=False,
has_bias=True
)
self.linear2 = fleet.meta_parallel.RowParallelLinear(
inner_size,
hidden_size,
weight_attr=paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Assign(init_fc2_data)
),
input_is_parallel=True,
has_bias=True
)
self.linear3 = paddle.nn.Linear(
hidden_size,
output_size,
weight_attr=paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Constant(0.0)
),
bias_attr=paddle.framework.ParamAttr(
initializer=paddle.nn.initializer.Constant(0.0)
)
)
self.embedding = fleet.meta_parallel.VocabParallelEmbedding(
vocab_size,
hidden_size,
weight_attr=paddle.nn.initializer.Constant(value=0.5)
)
def forward(self, x):
x = self.embedding(x)
x = self.linear1(x)
x = self.linear2(x)
x = self.linear3(x)
return x
定义训练过程
def train_batch(batch, model, optimizer):
output = model(batch)
loss = output.mean()
loss.backward()
optimizer.step()
optimizer.clear_grad()
return loss
定义固定种子的函数
def set_random_seed(seed, rank_id):
"""Set random seed for reproducability."""
random.seed(seed)
np.random.seed(seed)
paddle.seed(seed + rank_id)
初始化分布式环境,创建模型,训练
paddle.distributed.init_parallel_env()
strategy = fleet.DistributedStrategy()
model_parallel_size = 2
data_parallel_size = 1
strategy.hybrid_configs = {
"dp_degree": data_parallel_size,
"mp_degree": model_parallel_size,
"pp_degree": 1
}
# 注意strategy是这里传递的,动态图只能这里,静态图还可以在distributed_optimizer里传
fleet.init(is_collective=True, strategy=strategy)
hcg = fleet.get_hybrid_communicate_group()
mp_id = hcg.get_model_parallel_rank()
rank_id = dist.get_rank()
set_random_seed(1024, rank_id)
np_fc1 = np.random.random_sample((hidden_size, inner_size))
np_fc2 = np.random.random_sample((inner_size, hidden_size))
model_b = SimpleNet(vocab_size, hidden_size, inner_size, output_size, np_fc1, np_fc2)
optimizer_b = paddle.optimizer.SGD(learning_rate=0.001, parameters=model_b.parameters())
model_a = SimpleMPNet(vocab_size, hidden_size, inner_size, output_size,
np_fc1, np_fc2, mp_id)
optimizer_a = paddle.optimizer.SGD(learning_rate=0.001, parameters=model_a.parameters())
model_a = fleet.distributed_model(model_a)
optimizer_a = fleet.distributed_optimizer(optimizer_a)
for _ in range(5):
np_data = np.random.randint(0, vocab_size, (batch_size, seq_length, ))
batch = paddle.to_tensor(np_data)
loss_a = train_batch(batch, model_a, optimizer_a)
loss_b = train_batch(batch, model_b, optimizer_b)
print("mp_loss: ", loss_a.numpy()[0], " single_loss: ", loss_b.numpy()[0])
模型并行的动态图代码:example/model_parallelism/mp_dygraph.py。
运行方式(需要保证当前机器有两张gpu):
export CUDA_VISIBLE_DEVICES=0,1
python -m paddle.distributed.launch mp_dygraph.py
控制台输出信息如下:
WARNING 2021-10-27 09:19:24,072 launch.py:381] Not found distinct arguments and compiled with cuda or xpu. Default use collective mode
launch train in GPU mode!
INFO 2021-10-27 09:19:24,074 launch_utils.py:525] Local start 2 processes. First process distributed environment info (Only For Debug):
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_TRAINER_ID 0 |
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:10129 |
| PADDLE_TRAINERS_NUM 2 |
| PADDLE_TRAINER_ENDPOINTS 127.0.0.1:10129,127.0.0.1:13182 |
| PADDLE_RANK_IN_NODE 0 |
| PADDLE_LOCAL_DEVICE_IDS 0 |
| PADDLE_WORLD_DEVICE_IDS 0,1 |
| FLAGS_selected_gpus 0 |
| FLAGS_selected_accelerators 0 |
+=======================================================================================+
日志信息位于log目录下, 需要注意的是模型并行的loss与单卡模型的loss在小数点后三位是能够精确对齐的,然后两张卡上对应的loss应该是一样的:
mp_loss: 0.0 single_loss: 0.0
mp_loss: -0.14513375 single_loss: -0.14513376
mp_loss: -0.2902736 single_loss: -0.2902736
mp_loss: -0.43542737 single_loss: -0.43542737
mp_loss: -0.5806184 single_loss: -0.5806184
流水线并行¶
简介¶
通常来讲,训练更大规模的网络模型可以在多种任务上取得更好的效果,如提升图像分类任务的准确率。然而,训练更大规模的网络模型会消耗更多的显存资源,甚至是超过单个设备的显存容量,从而导致模型无法训练。流水线并行通过将网络模型不同的层放置到不同的设备,从而降低单个设备的显存消耗,使得超大规模模型训练成为可能。本文主要介绍飞桨流水线并行的基本原理和使用方法。
原理介绍¶

与数据并行不同,流水线并行将模型的不同层放置到不同的计算设备,降低单个计算设备的显存消耗,从而实现超大规模模型训练。以上图为例,示例模型包含四个模型层。该模型被切分为三个部分,并分别放置到三个不同的计算设备。即,第1层放置到设备0,第2层和第三3层放置到设备1,第4层放置到设备2。相邻设备间通过通信链路传输数据。具体地讲,前向计算过程中,输入数据首先在设备0上通过第1层的计算得到中间结果,并将中间结果传输到设备1,然后在设备1上计算得到第2层和第3层的输出,并将模型第3层的输出结果传输到设备2,在设备2上经由最后一层的计算得到前向计算结果。反向传播过程类似。最后,各个设备上的网络层会使用反向传播过程计算得到的梯度更新参数。由于各个设备间传输的仅是相邻设备间的输出张量,而不是梯度信息,因此通信量较小。
下图给出流水线并行的时序图。最简配置流水线并行模型下,任意时刻只有单个计算设备处于计算状态,其它计算设备则处于空闲状态,因此设备利用率和计算效率较差。

为了优化流水线并行中设备的计算效率,可以进一步将mini-batch切分成若干更小粒度的micro-batch,以提升流水线并行的并发度,进而达到提升设备利用率和计算效率的目的。如下图所示,一个mini-batch被切分为4个micro-batch;前向阶段,每个设备依次计算单个micro-batch的结果;从而增加了设备间的并发度,降低了流水线并行bubble空间比例,提高了计算效率。

功能效果¶
使用流水线并行,可以实现超大规模模型训练。例如,使用多个计算设备,可以实现单个计算设备显存无法容纳的模型训练。
静态图使用方法¶
在使用流水线并行的训练策略时,我们通过device_guard
接口将不同的计算层放置在不同的设备上,如device_guard("gpu:0")
。需要注意的是,当前流水线并行仅支持GPU设备。并且,模型中每个层都需要指定放置设备。
# device_guard 使用示例
def build_network():
with paddle.fluid.device_guard("gpu:0"):
data = paddle.static.data(name='sequence', shape=[1], dtype='int64')
data_loader = paddle.io.DataLoader.from_generator(
feed_list=[data],
capacity=64,
use_double_buffer=True,
iterable=False)
emb = nn.embedding(input=data, size=[128, 64])
with paddle.fluid.device_guard("gpu:1"):
fc = nn.fc(emb, size=10)
loss = paddle.mean(fc)
return data_loader, loss
通过设定dist_strategy.pipeline
为True,将流水线并行的策略激活。
fleet.init(is_collective=True)
dist_strategy = paddle.distributed.fleet.DistributedStrategy()
dist_strategy.pipeline = True
进一步地,可以通过dist_strategy.pipeline_configs
配置流水线并行中mini-batch的切分粒度。假设mini-batch的大小为128,可以通过下述代码将mini-batch切为4份更小粒度的micro-batch,每个micro-batch的大小为32。需要注意地是,用户需要保证mini-batch大小是micro-batch大小的整数倍。
fleet.init(is_collective=True)
dist_strategy = paddle.distributed.fleet.DistributedStrategy()
strategy.pipeline_configs = {"accumulate_steps": 4,
"micro_batch_size": 32}
基于ResNet50网络的流水线并行代码:example/resnet。
使用下述命令行运行示例代码:
python -m paddle.distributed.launch \
--gpus="0,1,2,3" \
train_fleet_pipeline.py
控制台输出信息如下:
WARNING 2021-01-08 15:53:27,677 launch.py:314] Not found distinct arguments and compiled with cuda. Default use collective mode
launch train in GPU mode
INFO 2021-01-08 15:53:27,679 launch_utils.py:471] Local start 5 processes. First process distributed environment info (Only For Debug):
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_TRAINER_ID 0 |
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:52033 |
| PADDLE_TRAINERS_NUM 5 |
| PADDLE_TRAINER_ENDPOINTS ... 0.1:12178,127.0.0.1:28915,127.0.0.1:32114|
| FLAGS_selected_gpus 0 |
+=======================================================================================+
INFO 2021-01-08 15:53:27,679 launch_utils.py:475] details abouts PADDLE_TRAINER_ENDPOINTS can be found in log/endpoints.log.
grep: warning: GREP_OPTIONS is deprecated; please use an alias or script
server not ready, wait 3 sec to retry...
not ready endpoints:['127.0.0.1:40388', '127.0.0.1:12178', '127.0.0.1:28915', '127.0.0.1:32114']
server not ready, wait 3 sec to retry...
not ready endpoints:['127.0.0.1:12178']
W0108 15:53:37.673019 103703 device_context.cc:342] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 11.0, Runtime API Version: 10.1
W0108 15:53:37.678391 103703 device_context.cc:352] device: 0, cuDNN Version: 7.6.
日志信息位于log目录下,log/workerlog.4日志文件的内容如下:
grep: warning: GREP_OPTIONS is deprecated; please use an alias or script
W0108 15:52:27.723405 103188 device_context.cc:342] Please NOTE: device: 4, GPU Compute Capability: 7.0, Driver API Version: 11.0, Runtime API Version: 10.1
W0108 15:52:27.728278 103188 device_context.cc:352] device: 4, cuDNN Version: 7.6.
I0108 15:52:32.665313 103188 gen_nccl_id_op_helper.cc:176] Server listening on: 127.0.0.1:32347 successful.
W0108 15:52:36.874132 103188 operator.cc:1194] Device index is only supported under pipeline parallelism, so it will be ignored.
grep: warning: GREP_OPTIONS is deprecated; please use an alias or script
W0108 15:53:31.393914 103723 device_context.cc:342] Please NOTE: device: 4, GPU Compute Capability: 7.0, Driver API Version: 11.0, Runtime API Version: 10.1
W0108 15:53:31.398906 103723 device_context.cc:352] device: 4, cuDNN Version: 7.6.
I0108 15:53:34.465754 103723 gen_nccl_id_op_helper.cc:176] Server listening on: 127.0.0.1:32114 successful.
W0108 15:53:40.784844 103723 operator.cc:1194] Device index is only supported under pipeline parallelism, so it will be ignored.
[Epoch 0, batch 5] loss: 0.37770, acc1: 0.03125, acc5: 0.03125
[Epoch 0, batch 10] loss: 0.06200, acc1: 0.00000, acc5: 0.03125
[Epoch 0, batch 15] loss: 0.26105, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 20] loss: 0.00000, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 25] loss: 0.37330, acc1: 0.00000, acc5: 0.06250
[Epoch 0, batch 30] loss: 0.00000, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 35] loss: 0.07487, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 40] loss: 0.12932, acc1: 0.03125, acc5: 0.06250
[Epoch 0, batch 45] loss: 0.19604, acc1: 0.00000, acc5: 0.03125
[Epoch 0, batch 50] loss: 0.07977, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 55] loss: 0.00000, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 60] loss: 0.13464, acc1: 0.00000, acc5: 0.06250
[Epoch 0, batch 65] loss: 0.13940, acc1: 0.00000, acc5: 0.03125
[Epoch 0, batch 70] loss: 0.00000, acc1: 0.00000, acc5: 0.00000
[Epoch 0, batch 75] loss: 0.00000, acc1: 0.00000, acc5: 0.00000
注意事项¶
由于流水线并行将模型的层放置到不同的计算设备,因此在fetch信息时,只有所fetch的数据所在设备进程对应的日志信息中输出数据信息,其它设备进程对应的日志输出None。以上面的示例说明,由于获取的损失值和精度值只在最后一个设备上,因此只有log/workerlog.4日志文件中会输出对应的数据,其它日志文件不会输出对应的数据。
动态图使用方法¶
流水线并行根据执行的策略,可以分为F then B 和 1F1B 两种模式,目前Paddle动态图流水线只支持1F1B模式。
下面代码在Paddle2.0以上可以运行,建议将Paddle版本升级到最新版
首先导入需要的包
import numpy as np
import os
import paddle
from paddle.distributed import fleet
from paddle.fluid.dygraph.container import Sequential
import paddle.nn as nn
from paddle.fluid.dygraph.layers import Layer
from paddle.distributed.fleet.meta_parallel import LayerDesc, PipelineLayer
import paddle.nn.functional as F
import paddle.distributed as dist
import random
然后构造一个普通的AlexNet模型, 作为对比
class ReshapeHelp(Layer):
def __init__(self, shape):
super(ReshapeHelp, self).__init__()
self.shape = shape
def forward(self, x):
return x.reshape(shape=self.shape)
class AlexNet(Layer):
def __init__(self, num_classes=10):
super(AlexNet, self).__init__()
self.features = Sequential(
nn.Conv2D(
1, 64, kernel_size=11, stride=4, padding=5),
nn.ReLU(),
nn.MaxPool2D(
kernel_size=2, stride=2),
nn.Conv2D(
64, 192, kernel_size=5, padding=2),
nn.ReLU(),
nn.MaxPool2D(
kernel_size=2, stride=2),
nn.Conv2D(
192, 384, kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2D(
384, 256, kernel_size=3, padding=1),
nn.ReLU(),
nn.Conv2D(
256, 256, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2D(
kernel_size=2, stride=2), )
self.reshape_layer = ReshapeHelp(shape=[-1, 256])
self.classifier = nn.Linear(256, num_classes)
self.loss_fn = nn.loss.CrossEntropyLoss()
def forward(self, x, y):
x = self.features(x)
x = self.reshape_layer(x)
x = self.classifier(x)
return self.loss_fn(x, y)
然后构建一个可以运行流水线的模型,模型的layer需要被LayerDesc或者继承了LayerDesc的SharedLayerDesc包裹,这里因为不需要共享参数,所以就使用LayerDesc
class AlexNetPipeDesc(PipelineLayer):
def __init__(self, num_classes=10, **kwargs):
self.num_classes = num_classes
decs = [
LayerDesc(
nn.Conv2D, 1, 64, kernel_size=11, stride=4, padding=5),
LayerDesc(nn.ReLU),
LayerDesc(
nn.MaxPool2D, kernel_size=2, stride=2),
LayerDesc(
nn.Conv2D, 64, 192, kernel_size=5, padding=2),
F.relu,
LayerDesc(
nn.MaxPool2D, kernel_size=2, stride=2),
LayerDesc(
nn.Conv2D, 192, 384, kernel_size=3, padding=1),
F.relu,
LayerDesc(
nn.Conv2D, 384, 256, kernel_size=3, padding=1),
F.relu,
LayerDesc(
nn.Conv2D, 256, 256, kernel_size=3, padding=1),
F.relu,
LayerDesc(
nn.MaxPool2D, kernel_size=2, stride=2),
LayerDesc(
ReshapeHelp, shape=[-1, 256]),
LayerDesc(nn.Linear, 256, self.num_classes), # classifier
]
super(AlexNetPipeDesc, self).__init__(
layers=decs, loss_fn=nn.CrossEntropyLoss(), **kwargs)
然后初始化分布式环境,这一步主要是构建流水线通信组的拓扑
batch_size = 4
micro_batch_size = 2
strategy = fleet.DistributedStrategy()
model_parallel_size = 1
data_parallel_size = 1
pipeline_parallel_size = 2
strategy.hybrid_configs = {
"dp_degree": data_parallel_size,
"mp_degree": model_parallel_size,
"pp_degree": pipeline_parallel_size
}
strategy.pipeline_configs = {
"accumulate_steps": batch_size // micro_batch_size,
"micro_batch_size": micro_batch_size
}
fleet.init(is_collective=True, strategy=strategy)
为了能够和普通模型的loss进行逐位比较,需要将控制随机变量的种子设置一致
def set_random_seed(seed, dp_id, rank_id):
"""Set random seed for reproducability."""
random.seed(seed)
np.random.seed(seed + dp_id)
paddle.seed(seed + dp_id)
print("seed: ", seed)
print("rank_id: ", rank_id)
print("dp_id: ", dp_id)
hcg = fleet.get_hybrid_communicate_group()
world_size = hcg.get_model_parallel_world_size()
dp_id = hcg.get_data_parallel_rank()
pp_id = hcg.get_stage_id()
rank_id = dist.get_rank()
set_random_seed(1024, dp_id, rank_id)
然后创建出普通模型以及对应的优化器
model_a = AlexNet(10)
scheduler_a = paddle.optimizer.lr.PiecewiseDecay(
boundaries=[2], values=[0.001, 0.002], verbose=False
)
optimizer_a = paddle.optimizer.SGD(learning_rate=scheduler_a, parameters=model_a.parameters())
然后创建出流水线并行的模型,
AlexNetPipeDesc(….):这一步主要是在切分普通模型的layer,将属于当前卡的layer添加到模型里面
fleet.distributed_model(….):这一步则是真正进行流水线模型并行的初始化,会得到之前构建拓扑组已经组建好的流水线通信组,并且如果流水线并行混合了数据并行,模型并行,会对数据并行和模型并行相关参数进行broadcast
fleet.distributed_optimizer(…):这一步则是为优化器添加分布式属性,如果流水线并行混合了数据并行,sharding,就会对相应梯度进行all reduce
model_b = AlexNetPipeDesc(num_stages=pipeline_parallel_size, topology=hcg._topo)
scheduler_b = paddle.optimizer.lr.PiecewiseDecay(
boundaries=[2], values=[0.001, 0.002], verbose=False
)
optimizer_b = paddle.optimizer.SGD(learning_rate=scheduler_b,
parameters=model_b.parameters())
model_b = fleet.distributed_model(model_b)
optimizer_b = fleet.distributed_optimizer(optimizer_b)
流水线并行将模型按layers切分,为了能够和普通模型loss对齐,需要采用热启模式,先保存普通模型的参数,然后流水线并行模型加载相关参数
# 保存普通模型参数
param_len = len(model_a.parameters())
parameters = []
for param in model_a.parameters():
parameters.append(param.numpy())
# 流水线并行模型加载参数
for idx, param in enumerate(model_b.parameters()):
param.set_value(parameters[idx + pp_id * (param_len // 2)])
创建mnist数据集
train_reader = paddle.batch(
paddle.dataset.mnist.train(), batch_size=batch_size, drop_last=True
)
开始训练
model_b.train_batch(…):这一步主要就是执行1F1B的流水线并行方式
for step_id, data in enumerate(train_reader()):
x_data = np.array([x[0] for x in data]).astype("float32").reshape(
batch_size, 1, 28, 28
)
y_data = np.array([x[1] for x in data]).astype("int64").reshape(
batch_size, 1
)
img = paddle.to_tensor(x_data)
label = paddle.to_tensor(y_data)
img.stop_gradient = True
label.stop_gradient = True
if step_id >= 5:
break
loss_a = model_a(img, label)
loss_a.backward()
optimizer_a.step()
optimizer_a.clear_grad()
scheduler_a.step()
loss_b = model_b.train_batch([img, label], optimizer_b, scheduler_b)
print("single_loss: ", loss_a.numpy(), "pp_loss: ", loss_b.numpy())
运行方式(需要保证当前机器有两张gpu):
export CUDA_VISIBLE_DEVICES=0,1
python -m paddle.distributed.launch alexnet_dygraph_pipeline.py # alexnet_dygraph_pipeline.py是用户运行动态图流水线的python文件
基于AlexNet的流水线并行动态图代码:example/alex。
控制台输出信息如下:
WARNING 2021-10-21 14:47:54,245 launch.py:381] Not found distinct arguments and compiled with cuda or xpu. Default use collective mode
launch train in GPU mode!
INFO 2021-10-21 14:47:54,246 launch_utils.py:525] Local start 2 processes. First process distributed environment info (Only For Debug):
+=======================================================================================+
| Distributed Envs Value |
+---------------------------------------------------------------------------------------+
| PADDLE_TRAINER_ID 0 |
| PADDLE_CURRENT_ENDPOINT 127.0.0.1:10101 |
| PADDLE_TRAINERS_NUM 2 |
| PADDLE_TRAINER_ENDPOINTS 127.0.0.1:10101,127.0.0.1:13727 |
| PADDLE_RANK_IN_NODE 0 |
| PADDLE_LOCAL_DEVICE_IDS 0 |
| PADDLE_WORLD_DEVICE_IDS 0,1 |
| FLAGS_selected_gpus 0 |
| FLAGS_selected_accelerators 0 |
+=======================================================================================+
日志信息位于log目录下:
single_loss: [2.299683] pp_loss: [2.2996738]
single_loss: [2.287039] pp_loss: [2.2870412]
single_loss: [2.3449194] pp_loss: [2.3449283]
single_loss: [2.3162396] pp_loss: [2.3162327]
single_loss: [2.3100634] pp_loss: [2.310072]
注意事项¶
与静态图的流水线不一样的是每张卡都会输出loss,并且流水线loss的值是相等的,与普通模型的loss在小数点后三位应该是相等的。
飞桨4D混合并行训练使用指南¶
当前飞桨集合通信模式已经可以支持文心ERNIE千亿语言模型的训练,其Sharding-DP策略更是在近期助力文心ERNIE的多项任务分数刷新GLUE榜单。而Sharding-DP策略正是飞桨集合通信模式为了支持训练ERNIE这样的大规模复杂模型所开发的多种并行策略中的一种。那么飞桨是使用哪些策略成功支持文心ERNIE千亿语言模型训练的呢?这些策略是如何工作的呢?接下来将为大家详细介绍。
原理介绍¶
ERNIE千亿级模型采用100多层transformer网络结构,计算复杂,训练需要占用T级显存资源,如果想用更少的机器高效训练,必须采取一系列性能优化和显存优化措施。
首先看如何性能优化。我们通过一个公式来看哪些因素可以影响训练速度,在固定的硬件环境下:
其中单卡速度由数据读取和计算速度决定;多卡加速比由计算/通信效率决定。显而易见,这三个是关键因素。除了单卡可以使用的算子融合、混合精度之类的基础性能优化策略之外,分布式训练还引入一系列并行策略。并行策略的核心思想是将数据和计算有关的图/算子切分到不同设备上,同时尽可能降低设备间通信所需的代价,合理使用多台设备资源,实现高效的并发调度训练,最大化提升训练速度。常见并行策略有数据并行DP(Data Parallel)、Layer间并行(流水线并行PP,Pipeline Parallel)、Layer内并行(模型并行MP,Model Parallel)。我们从设备资源和计算/通信效率来分析三种策略的优缺点:
数据并行训练加速比最高,但要求每个设备上都备份一份模型,显存占用比较高。为此我们的改进方案是分组参数切片数据并行策略(具体原理后文介绍),兼容了MP+DP的优势,但缺点是通信量大。
模型并行,通信量比较高,适合在机器内做模型并行。
流水线并行,训练设备容易出现空闲状态,加速效率没有DP高;但能减少通信边界支持更多的层数,适合在机器间使用。
其次看显存问题,通过下表分析的显存占用来源可以看出,上述的并行策略同样可以很好的应对不同来源的显存占用,更多的层数可以通过流水线并行和分组参数切分策略来解决;某层参数很大可以通过模型并行来解决;其次飞桨还提供一些其它灵活的优化方式,例如每层输出占用的显存,可以通过重计算和offload来解决。

综上所述,针对性能优化和显存优化,几种并行策略都有用武之地,但是同时也有各自的局限性,所以如果想高效训练千亿模型,需要这几种策略相互组合,取长补短,发挥各自的优势。
那么如何组合呢?具体可以参考下面的示例代码进行不同的策略设置和选择。另外,对于如何选择策略组合,本文也提供了一些组合的理论分析供参考。
对于混合并行,假设每个mini-batch切分为micro_step个micro batches,每个micro-batch的batch size为micro_bsz,并假设流水线的级数为pp_num。那么,对于sharding,每个micro step需要对参数进行2次broadcast和1次reduce操作,因此每个micro step中总的通信量为2*M+M=3M,其中M为参数量大小。对于数据并行,每个micro step需要使用allreduce sum操作同步当前机器上所有参数的梯度信息。假设模型参数在所有流水线间均匀切分,那么每个流水线级中包含的参数量为M/pp_num,因此每个micro step总的通信量为2M/pp_num,其中2表示allreduce sum的通信因子。对于流水线并行,每个micro step传输的通信量为流水线相邻层间activation的大小;对于Transformer类模型,相邻层间的activation大小为hid_size * micro_bsz * seq_len;其中,hid_size表示模型参数隐层大小,seq_len表示模型参数序列长度,micro_bsz表示每个micro的batch size。对于模型并行,每个Transformer Encoder层包含两次allreduce sum通信,每次通信量大小为hid_size*micro_bsz*seq_len;由于结合流水线并行,每个流水线级中包含的Transformer Encoder的层数为L/pp_num,其中,其中L表示模型总的Transformer Encoder的层数,起于各参数的意义同上;因此,模型并行配置下每台机器内部每个micro step的通信总量为4L*(hid_size*micro_bsz*seq_len)/pp_num,其中因子4表示allreduce_sum通信因子2与每个Transformer Encoder层包含两次allreduce sum通信次数的乘积。
下表给出集中典型策略组合下的通信量。

我们在实现Ernie训练时,采用了机内模型并行、机间流水并行,并在外层添加数据并行的策略。
静态图使用方法¶
可以通过DistributedStrategy配置使用混合并行训练。
fleet.init(is_collective=True)
dist_strategy = paddle.distributed.fleet.DistributedStrategy()
dist_strategy.sharding = args.use_sharding
dist_strategy.pipeline = args.num_pp > 1
dist_strategy.sharding_configs = {"segment_broadcast_MB": 32,
"sharding_degree": 1,
"mp_degree": args.num_mp,
"pp_degree": args.num_pp,
"dp_degree":args.num_dp,
"gradient_merge_acc_step": acc_steps,
"optimize_offload": False,
}
dist_strategy.pipeline_configs = {"schedule_mode": "1F1B",
"micro_batch_size": micro_bsz,
"accumulate_steps": acc_steps,
}
其中,sharding_degree、mp_degree、pp_degree和dp_degree分别表示sharding、模型并行、流水线并行和数据并行的并行度。参数optimize_offload表示是否开启offload功能,以节省显存。schedule_mode参数用于配置流水线并行的调度方式,为了节省显存,建议设置为”1F1B”。参数micro_batch_size和accumulate_steps分别表示流水线并行中每个micro batch的batch size和梯度累积的次数,即每个mini-batch切分为多少个micro-batch。
示例代码可参见:examples/hybrid_parallelism。
动态图使用方法¶
strategy = fleet.DistributedStrategy()
strategy.hybrid_configs = {
"dp_degree": args.dp_degree,
"mp_degree": args.mp_degree,
"pp_degree": args.pp_degree,
"sharding_degree": args.sharding_degree
}
accumulate_steps = args.local_batch_size // args.micro_batch_size
strategy.pipeline_configs = {
"accumulate_steps": accumulate_steps,
"micro_batch_size": args.micro_batch_size
}
strategy.tensor_parallel_configs = {"tensor_init_seed": args.seed}
fleet.init(is_collective=True, strategy=strategy)
完整示例代码可参见:GPT-3。
ParameterServer训练¶
快速开始¶
在大数据浪潮的推动下,有标签训练数据的规模取得了飞速的增长。现在人们通常用数百万甚至上千万的有标签图像来训练图像分类器(如,ImageNet包含1400万幅图像,涵盖两万多个种类),用成千上万小时的语音数据来训练语音模型(如,Deep Speech 2系统使用了11940小时的语音数据以及超过200万句表述来训练语音识别模型)。在真实的业务场景中,训练数据的规模可以达到上述数据集的数十倍甚至数百倍,如此庞大的数据需要消耗大量的计算资源和训练时间使模型达到收敛状态(数天时间)。
为了提高模型的训练效率,分布式训练应运而生,其中基于参数服务器的分布式训练为一种常见的中心化共享参数的同步方式。与单机训练不同的是在参数服务器分布式训练中,各个节点充当着不同的角色:
训练节点:该节点负责完成数据读取、前向计算、反向梯度计算等过程,并将计算出的梯度上传至服务节点。
服务节点:在收到所有训练节点传来的梯度后,该节点会将梯度聚合并更新参数。最后将参数发送给训练节点,开始新一轮的训练。
根据参数更新的方式不同,可以分为同步/异步/Geo异步三种:
同步训练:所有Worker的进度保持一致,即每训练完一个Batch后,所有Worker会上传梯度给Server,然后开始等待Server返回更新后的参数。Server在拿到所有Worker上传的梯度后,才会开始计算更新后的参数。因此在任何一个时间点,所有Worker都处于相同的训练阶段。同步训练的优势在于Loss可以比较稳定的下降,缺点是整个训练速度较慢,这是典型的木桶原理,速度的快慢取决于最慢的那个Worker的训练计算时间,因此在训练较为复杂的模型时,即模型训练过程中神经网络训练耗时远大于节点间通信耗时的场景下,推荐使用同步训练模式。
异步训练:与同步训练不同,在异步训练中任何两个Worker之间的参数更新都互不影响。每一个Worker完成训练、上传梯度后,Server都会立即更新参数并将结果返回至相应的训练节点。拿到最新的参数后,该训练节点会立即开始新一轮的训练。异步训练去除了训练过程中的等待机制,训练速度得到了极大的提升,但是缺点也很明显,那就是Loss下降不稳定,容易发生抖动。建议在个性化推荐(召回、排序)、语义匹配等数据量大的场景使用。
GEO异步训练:GEO异步训练是飞桨独有的一种异步训练模式,训练过程中任何两个训练节点之间的参数更新同样都互不影响,但是每个训练节点本地都会拥有完整的训练流程,即前向计算、反向计算和参数优化,而且每训练到一定的批次(Batch) 训练节点都会将本地的参数计算一次差值(Step间隔带来的参数差值),将差值发送给服务端累计更新,并拿到最新的参数后,该训练节点会立即开始新一轮的训练。所以显而易见,在GEO异步训练模式下,Worker不用再等待Server发来新的参数即可执行训练,在训练效果和训练速度上有了极大的提升。但是此模式比较适合可以在单机内能完整保存的模型,在搜索、NLP等类型的业务上应用广泛,比较推荐在词向量、语义匹配等场景中使用。
本节将采用推荐领域非常经典的模型wide_and_deep为例,介绍如何使用Fleet API(paddle.distributed.fleet)完成参数服务器训练任务,本次快速开始的示例代码位于https://github.com/PaddlePaddle/PaddleFleetX/tree/old_develop/examples/wide_and_deep。
版本要求¶
在编写分布式训练程序之前,用户需要确保已经安装paddlepaddle-2.0.0-cpu或paddlepaddle-2.0.0-gpu及以上版本的飞桨开源框架。
操作方法¶
参数服务器训练的基本代码主要包括如下几个部分: 1. 导入分布式训练需要的依赖包。 2. 定义分布式模式并初始化分布式训练环境。 3. 加载模型及数据。 4. 定义参数更新策略及优化器。 5. 开始训练。
下面将逐一进行讲解。
导入依赖¶
import paddle
import os
import paddle.distributed.fleet as fleet
import paddle.distributed.fleet.base.role_maker as role_maker
定义分布式模式并初始化分布式训练环境¶
通过fleet.init()
接口,用户可以定义训练相关的环境,注意此环境是用户预先在环境变量中配置好的,包括:训练节点个数,服务节点个数,当前节点的序号,服务节点完整的IP:PORT列表等。
# 当前参数服务器模式只支持静态图模式, 因此训练前必须指定`paddle.enable_static()`
paddle.enable_static()
role = role_maker.PaddleCloudRoleMaker()
fleet.init(role)
加载模型及数据¶
# 模型定义参考examples/wide_and_deep中model.py
from model import net
from reader import data_reader
feeds, predict, avg_cost = net()
train_reader = paddle.batch(data_reader(), batch_size=4)
reader.decorate_sample_list_generator(train_reader)
定义同步训练 Strategy 及 Optimizer¶
在Fleet
API中,用户可以使用fleet.DistributedStrategy()
接口定义自己想要使用的分布式策略。
其中a_sync
选项用于定义参数服务器相关的策略,当其被设定为False
时,分布式训练将在同步的模式下进行。反之,当其被设定成True
时,分布式训练将在异步的模式下进行。
# 定义异步训练
dist_strategy = fleet.DistributedStrategy()
dist_strategy.a_sync = True
# 定义同步训练
dist_strategy = fleet.DistributedStrategy()
dist_strategy.a_sync = False
# 定义Geo异步训练, Geo异步目前只支持SGD优化算法
dist_strategy = fleet.DistributedStrategy()
dist_strategy.a_sync = True
dist_strategy.a_sync_configs = {"k_steps": 100}
optimizer = paddle.optimizer.SGD(learning_rate=0.0001)
optimizer = fleet.distributed_optimizer(optimizer, dist_strategy)
optimizer.minimize(model.loss)
开始训练¶
完成模型及训练策略以后,我们就可以开始训练模型了。因为在参数服务器模式下会有不同的角色,所以根据不同节点分配不同的任务。
对于服务器节点,首先用init_server()
接口对其进行初始化,然后启动服务并开始监听由训练节点传来的梯度。
同样对于训练节点,用init_worker()
接口进行初始化后,
开始执行训练任务。运行exe.run()
接口开始训练,并得到训练中每一步的损失值。
if fleet.is_server():
fleet.init_server()
fleet.run_server()
else:
exe = paddle.static.Executor(paddle.CPUPlace())
exe.run(paddle.static.default_startup_program())
fleet.init_worker()
for epoch_id in range(1):
reader.start()
try:
while True:
loss_val = exe.run(program=paddle.static.default_main_program(),
fetch_list=[avg_cost.name])
loss_val = np.mean(loss_val)
print("TRAIN ---> pass: {} loss: {}\n".format(epoch_id,
loss_val))
except paddle.core.EOFException:
reader.reset()
fleet.stop_worker()
运行训练脚本¶
定义完训练脚本后,我们就可以用paddle.distributed.launch
模块运行分布式任务了。其中server_num
,
worker_num
分别为服务节点和训练节点的数量。在本例中,服务节点有1个,训练节点有两个。
python -m paddle.distributed.launch --server_num=1 --worker_num=2 train.py
设计思想¶
综述¶
参数服务器概述¶
参数服务器是个编程框架,用于方便分布式并行程序的编写,其中重点是对大规模参数的分布式存储和协同的支持。
工业界需要训练大型的机器学习模型,这些模型参数往往超大,达到了百GB甚至TB级别,超过了单台服务器的容纳能力,同时训练这些模型的数据量巨大,一次参与训练的数据可能达到上百TB,需要多台服务器共同分担,加速整个训练任务。在这种情况下,采用参数服务器架构可以很好的利用分布式计算的能力来提升训练任务中模型规模的上限和训练效率。
一般参数服务器架构如图(原图论文地址):

即将整个训练节点划分为计算节点(Worker)和参数更新节点(PServer)两种,其中计算节点负责将分配到此节点的数据进行计算,将算得的梯度发送给对应的参数更新节点,后从参数更新节点获取最新的参数更新到本地。参数更新节点采用一定的参数划分方式,将模型参数均匀的划分在多个节点上,每个节点在收到梯度后根据优化策略进行参数更新,同时参数更新节点作为服务方,会对计算节点提供参数查询功能。
飞桨参数服务器概述¶
飞桨参数服务器的基本架构源自 Parameter Server for Distributed Machine Learning 和 Large Scale Distributed Deep Networks,并在其基础上做了大量创新来满足百度和其他公司对于参数服务器性能和功能的需求。
飞桨参数服务器拥有以下特性:
支持同步训练、异步训练和GEO异步训练三种模式,能够多方面满足用户需求,保障模型的性能和稳定性。
支持千亿级别大规模稀疏模式,支持准入、遗忘策略,完备支持流式训练。
采用BRPC作为多节点之间通信的主要方法,极大提升了网络可用性(BRPC在百度内部久经考验,值得信赖)。
超高的吞吐及多机加速比,能够有效利用计算资源,提升训练速度。
飞桨参数服务器架构¶
架构图如下所示:

基本组件描述:
FleetAPI: 贯穿整个分布式训练的API, 分布式所有对外暴露的API均由Fleet暴露,不允许其他任何组件暴露API。
DistributedOptimizer: 编译期,结合配置将单机训练网络转换为分布式训练网络,生成适配于各个节点的Program及配置文件。
Reader: 包含Dataset/DataLoader/Feeder, Reader与训练解耦,训练可以与任意Reader适配。
Executor: 每个训练节点(Worker)的主方法,适配各种Reader, 分布式中只通过send/recv和外部进行交互。
Communicator: Worker端负责梯度/参数聚合、拆分、收发的核心模块,独立运行,通过初始化配置及编译期间生成的Worker参数收发配置文件的内容进行工作。
RPC/GLOO: 负责参数传递、节点控制等,通信核心模块。 RPC逻辑会从收发Tensor更新为收发二进制, GLOO负责控制训练过程中对于训练流程的控制,包括Barrier,以及通过GLOO API实现分布式Auc/分布式Acc等逻辑。
ParameterServer: 参数服务器模块,独立运行于PServer端,包含Dense/Sparse参数存储及更新、Decay/Clip/Show、Click等处理逻辑。
参数服务器整体架构分编译期和运行期两个阶段。
编译阶段,框架需在FleetAPI的配合下,将用户定义的单机组网计算图拆分为两部分内容:
计算节点(Worker)端计算图, Worker端的计算图主要由基础训练网络构成,包含数据读取,前向,反向及与通信组件(Communicator)通信的算子。
配置文件,PServer端需据此启动RPC Server服务,以及生成参数的存储格式。Worker端需据此完成通信组件Communicator的配置。
运行阶段,PServer端需启动RPC服务,监听并处理Worker的参数拉取、更新等请求。运行阶段,Worker端的训练线程需基于自己划分的训练数据,进行学习,将梯度(参数)发送给Communicator后,根据配置(同步、异步、GEO异步)来确定是等待通信完成,还是直接进行下一轮训练,以此来完成整个参数服务器的分布式训练。Worker端的Communicator通信组件也需在运行阶段初就完成启动,并不断将当前Worker各个训练线程产出的梯度聚合后发送给PServer,然后从PServer上拉取最新参数以供训练线程使用。
分布式训练模式¶
当前飞桨共支持三种分布式训练模式,同步、异步、GEO异步,每种模式的特点及适用场景为:
同步训练:训练一个minibatch后,每个节点会合并所有线程的梯度发给PServer, PServer端收到所有节点的梯度后,进行梯度聚合及参数更新。因同步训练的过程中有诸多的等待或同步机制,导致整个训练速度较慢,推荐在复杂模型(神经网络训练耗时远大于节点间通信耗时)使用。
异步训练:训练一个minibatch后,每个节点的每个线程会发送梯度给PServer,PServer端不再等待收到所有节点的梯度,而是直接基于已收到的梯度进行参数更新。异步训练去除了训练过程中的等待机制,训练速度得到了极大的提升, 但是因为引入了异步更新的机制会导致训练效果有所波动,建议在召回、排序、语义匹配等数据量大的场景使用。
GEO(Geometric Stochastic Gradient Descent)异步训练:GEO是飞桨自研的异步训练框架,在训练效果和训练速度上有了极大的提升,目前只支持SGD优化算法。 每个节点在本地训练若干个minibatch后(具体训练多少个minibatch由配置决定),发送参数更新给PServer端,PServer端接收后通过加和方式更新参数。GEO速度极快,并在搜索、NLP等业务上广泛应用, 推荐在词向量、语义匹配等领域进行使用。
存储设计¶
本节主要介绍大规模稀疏参数服务器的存储设计。 神经网络训练中,参数共分为稠密参数和稀疏参数两种, 其中稠密参数指每次训练都需要全部更新的参数,例如全连接层(fc, fully-connected)的权重(weight)和偏置(bias)等。 稀疏参数指每次训练仅需部分更新的参数,例如Embedding表,每次训练仅需更新输入对应的Embedding行即可。
原理¶
参数服务器中,参数的存储设计应该分为两部分,分配和存储。前者介绍每个参数应该存储在哪个PServer上,后者介绍每个PServer上的参数应该如何存储。
PServer参数分配¶
稠密参数会全部展平成一维数组,拼接在一起变成一个大的一维数组,然后均匀分配在所有PServer上。
稀疏参数的分配方式为取余,每个id应该被分配到哪个PServer上,可通过公式 id % PServer_num 计算得到。
PServer参数存储¶
每个PServer上的参数存储格式如下图所示:

可以发现,无论是稠密参数(DenseTable)还是稀疏参数(SparseTable),最终存储的内容(ValueData)格式都一样,除参数本身外,还需额外存储参数优化计算公式中除梯度外所有的其他中间状态值,下面以sgd,adagrad,adam三种常见优化算法为例进行说明。
若优化算法为Sgd,随机梯度下降,参数更新公式为:
需存储参数(param)和学习率(lr, 维度为1)。
若优化器为Adagrad,参数更新公式为:
需存储参数(param)、梯度的二阶矩估计(moment,维度和参数一致)以及学习率(lr,维度为1)。
若优化器为Adam,参数更新公式为:
需存储参数(param),梯度的一阶、二阶矩估计(moment_1, moment_2,维度和参数一致),一阶、二阶矩估计的指数衰减率的累积值(beta1pow, beta2pow, 维度均为1)以及学习率(lr,维度为1)。
稠密参数的存储格式为一个二维Vector数组,第一维大小为分配到当前PServer上的所有稠密参数元素个数和,第二维大小为ValueData的存储长度,如上文所讲,和优化算法相关,其中param大小为1。例如分配到当前PServer上的稠密参数元素个数为dense_numels_i,优化算法为sgd,则Vector的维度为[dense_numels_i,2]。
为了能提高并发处理能力,每个PServer上稀疏参数一般会进行分shard存储,每个id被分到哪个shard可直接通过 id % shard_num 计算得到。每个shard的存储格式为字典(Map),字典关键字(key)为id,值(value)为ValueData,如上文所讲,和优化算法相关,其中param的大小为emb_size。
通信设计¶
简介¶
神经网络的训练过程一般由三部分组成,前向计算、反向传播、参数更新。前向计算获取损失值 Loss,然后根据链式法则,反向计算得到每个参数的梯度,最后根据指定的优化算法,完成参数更新。
在多机参数服务器分布式训练中,存在两种不同角色的节点,Worker 和 PServer :
Worker:负责完成数据读取、前向计算、反向梯度计算等过程,并将计算出的梯度上传至 PServer 。
PServer:在收到 Worker 传来的梯度后,根据指定的优化方法,更新参数并将参数发送给 Worker ,开始新一轮的训练。
为了减少通信请求、提高整体训练速度,Worker在将梯度发送给PServer前,往往会将本地的参数梯度进行批量融合(Merge),然后才发送给PServer。一般来说,梯度融合的数量为训练线程数。
本节主要对分布式训练任务中,Worker和PServer间的通信流程进行介绍。
原理¶

上图展示了分布式训练过程中,Worker 任务的整个流程。可以发现,分布式训练中 Worker 任务相较于单机训练任务而言,主要有两点区别:
分布式训练网络相较于单机组网而言,删除了参数更新算子,增加了通信算子(Send OP)。
增加了 Communicator 通信组件,用以完成梯度融合、发送、接收等操作。
Communicator是分布式参数服务器框架中的通信组件,由多个参数的梯度队列和一个运行主线程组成。Worker通过前向、反向算子得到参数的梯度后,通过通信算子(Send OP)将梯度发送给Communicator中每个参数对应的梯度队列里。
Communictor在框架中以单例形式存在,在 fleet.init_worker() 中完成初始化和启动。启动后,Communicator的主线程会从每个参数的梯度队列中不停取出梯度直至满足融合条件后进行融合,发送至PServer,从PServer端获取最新的参数用于新的训练,并且重复上述一系列操作直至任务结束。梯度融合的条件有以下两个,满足任意一个即可:
数量等于最多可允许的梯度融合数阈值。该阈值通过环境变量 FLAGS_communicator_max_merge_var_num 配置,默认为 CPU_NUM 环境变量指定的线程数,若 CPU_NUM 环境变量未定义,则默认为1。
连续多次均未从参数对应的梯度队列里取出值,且尝试次数达到等待阈值。该阈值通过环境变量 FLAGS_communicator_send_wait_times 配置,默认为5。
在满足梯度融合条件后,只要待融合的梯度数量大于0,无论其是否等于 FLAGS_communicator_max_merge_var_num ,都会进行融合、发送及接收。需要注意的是,Communicator只会从PServer端接收稠密参数,稀疏参数的获取是通过前向算子 distributed_lookup_table 完成的。
性能优化¶
计算图拆分与优化¶
简介¶
计算图拆分目前仅支持Paddle静态图的参数服务器模式
参数服务器的分布式训练为一种常见的中心化共享参数的同步方式。与单机训练不同的是在参数服务器分布式训练中,各个节点充当着不同的角色:
训练节点:该节点负责完成数据读取、前向计算、反向梯度计算等过程,并将计算出的梯度上传至服务节点。训练节点又可进一步分为同构的计算节点 Worker ,与异构的计算节点 Heter-Worker
服务节点:在收到所有训练节点传来的梯度后,该节点会将梯度聚合并更新参数。最后将参数发送给训练节点,开始新一轮的训练。
根据参数更新的方式不同,可以分为同步/异步/Geo异步三种:
同步训练:在同步参数服务器分布式训练中,所有训练节点的进度保持一致。每训练完一个Batch后,训练节点会上传梯度,然后开始等待服务节点返回更新后的参数。服务节点拿到所有训练节点上传的梯度后,才会对参数进行更新。因此,在任何一个时间点,所有训练节点都处于相同的训练阶段。
异步训练:与同步训练不同,在异步训练中任何两个训练节点之间的参数更新都互不影响。每一个训练节点完成训练、上传梯度后,服务节点都会立即更新参数并将结果返回至相应的训练节点。拿到最新的参数后,该训练节点会立即开始新一轮的训练。
GEO异步训练:与前两种训练不同,GEO异步训练也是一种异步训练模式,每个训练节点本地都会拥有完整的训练流程(前向-反向-参数优化),训练过程中任何两个训练节点之间的参数更新都互不影响。每训练到一定的轮次(Step) 训练节点会将本地的参数计算一次差值(Step间隔带来的参数差值),将差值发送给服务端累计更新,并拿到最新的参数后,该训练节点会立即开始新一轮的训练。
根据训练节点所使用的设备,与整体架构的不同,可以分为 PS-CPU/PS-GPU/PS-Heter三种:
PS-CPU:PServer 使用CPU集群机器,Worker 使用同构的CPU集群机器,组成训练拓扑
PS-GPU:PServer 使用CPU集群机器,Worker 使用同构的GPU集群机器,组成训练拓扑
PS-Heter:PServer 使用CPU集群机器,Worker 使用同构的CPU集群机器,Heter-Worker 使用异构的AI算力集群机器(GPU/Kunlun等),三者组成训练拓扑
本文将具体展开, 详细介绍各个角色的计算图拆分原理,及如何在此基础上进行性能/效果优化。
原理¶
参数服务器的计算图拆分,按照角色不同,计算图也有所不同。我们首先从单机的深度学习计算图开始上手:
深度学习网络中有两个基本元素:
Operator:op,组成计算的最小操作,比如加减/FC/Embedding查表
Variable:var,网络学习的参数,又会分为稠密参数和稀疏参数。
稠密参数(Dense_Var)是指每个step都会更新的参数,比如FC的Weight参数。
稀疏参数(Sparse_Var)是指每个step不是必须更新的参数,如Embedding参数,只会更新当前step中涉及到的key的相应value
单机的计算图如下所示:
计算图拿到参数的值(Var)之后,会首先执行前向OP(FF OP),OP可能会执行在不同的设备上,如CPU/GPU/Kunlun 或其他AI芯片,我们用XPU代指。
前向OP计算完成后,得到loss,会继续计算反向OP(BP OP),得到各个参数的梯度(Var_Grad)
指定SGD/Adam等优化器,利用参数的梯度(Var_Grad)更新原始参数(Var)
重复以上流程,迭代参数的更新,实现深度学习网络的训练

那么单机的计算图如何转换为参数服务器不同角色的计算图呢?代码又具体怎样实现呢?下面具体介绍。
计算图拆分——PServer¶
参数服务器模式下,所有参数的全局真值都被分片保存在各个Pserver上,PServer执行Optimizer OP,执行参数的更新。
不失一般性,以PS-CPU异步模式的计算图介绍PServer的计算图,如下图所示

Worker(Trainer)在计算得到参数的梯度(Var_Grad)后,会通过RPC发送给PServer
PServer监听通信端口,将收到的不同参数分别通过不同的Oprimizer OP完成更新
Worker在下一个迭代时,请求PServer上最新的参数
重复以上流程,迭代参数的更新,实现分布式参数服务器的训练
通过上述流程,Pserver的计算图实现的功能主要分为三类:
执行Optimizer,进行参数更新的功能
接收Worker发送的梯度,触发Optimzer的功能
接收Worker发送的请求,发送指定参数的功能
功能2、3通过RPC Server即可实现,本节不再赘述
功能1有两种实现途径:a、使用Paddle组网,构成以optimizer OP; b、使用定制的数据结构及配套的优化算法实现,存储并更新参数,通常该方法用于大规模稀疏的场景。
在同步/异步模式的情况下:
PServer将计算图按照上述规则进行生成,并根据训练需要,添加LearningRate Decay等操作组件。
在GeoSGD的情况下:
参数的更新OP被放置在Worker上,PServer负责统筹全局参数:没有优化器OP,仅使用Sum等OP,利用各节点发送的参数diff,更新全局参数。更多详细介绍,可以参考文档 低频通信参数服务器训练算法
代码实现
PServer的计算图生成源代码位于 build_pserver_program
使用Fleet API时,参考以下python代码:server_demo.py
# server_demo.py
import random
import paddle
import paddle.distributed.fleet as fleet
import paddle.distributed.fleet.base.role_maker as role_maker
paddle.enable_static()
input_data = paddle.static.data(name="sparse_input", shape=[
None, 1], dtype="int64")
input_label = paddle.static.data(
name="label", shape=[None, 1], dtype="int64")
label = paddle.cast(input_label, dtype="int64")
embedding = paddle.static.nn.embedding(
input_data, is_sparse=True, size=[1000, 128])
fc1 = paddle.static.nn.fc(embedding, size=1024, activation="relu")
fc2 = paddle.static.nn.fc(fc1, size=512, activation="relu")
fc3 = paddle.static.nn.fc(fc2, size=256, activation="relu")
predict = paddle.static.nn.fc(fc3, size=2, activation="softmax")
cost = paddle.nn.functional.cross_entropy(input=predict, label=label)
role = role_maker.PaddleCloudRoleMaker()
fleet.init(role)
strategy = fleet.DistributedStrategy()
strategy.a_sync = True
strategy.a_sync_configs = {"launch_barrier": False}
optimizer = paddle.optimizer.Adam(1e-4)
optimizer = fleet.distributed_optimizer(optimizer, strategy)
optimizer.minimize(cost)
if fleet.is_server():
fleet.init_server()
export PSERVER_DEBUG=1
fleetrun --worker_num=1 --server_num=1 server_demo.py
cat log/serverlog.0
通过以上命令运行 server_demo.py 后,日志应包含以下的输出
server:
server_param {
downpour_server_param {
service_param {server_class: "BrpcPsServer" client_class: "BrpcPsClient" service_class: "BrpcPsService" start_server_port: 0 server_thread_num: 12
}
downpour_table_param {table_id: 1 table_class: "CommonSparseTable" shard_num: 256 type: PS_SPARSE_TABLE
accessor {accessor_class: "CommMergeAccessor" fea_dim: 1000 embedx_dim: 128
}
common {name: "adam" table_name: "embedding_0.w_0" trainer_num: 1 sync: false params: "Param" params: "Moment1" params: "Moment2" params: "Beta1Pow" params: "Beta2Pow" params: "LearningRate" dims: 128 dims: 128 dims: 128 dims: 1 dims: 1 dims: 1 initializers: "uniform_random&0&-0.0729324966669&0.0729324966669" initializers: "fill_constant&0.0" initializers: "fill_constant&0.0" initializers: "fill_constant&0.899999976158" initializers: "fill_constant&0.999000012875" initializers: "fill_constant&9.99999974738e-05"
}
}
downpour_table_param {table_id: 0 table_class: "CommonDenseTable" shard_num: 256 type: PS_DENSE_TABLE
accessor {accessor_class: "CommMergeAccessor" fea_dim: 788738 embedx_dim: 1
}
common {name: "adam" table_name: "MergedDense" trainer_num: 1 sync: false params: "Param" params: "Moment1" params: "Moment2" params: "Beta1Pow" params: "Beta2Pow" params: "LearningRate" dims: 788738 dims: 788738 dims: 788738 dims: 1 dims: 1 dims: 1 initializers: "fill_constant&0.0" initializers: "fill_constant&0.0" initializers: "fill_constant&0.0" initializers: "fill_constant&0.899999976158" initializers: "fill_constant&0.999000012875" initializers: "fill_constant&9.99999974738e-05"
}
}
downpour_table_param {table_id: 2 table_class: "BarrierTable" shard_num: 256 type: PS_OTHER_TABLE
accessor {accessor_class: "CommMergeAccessor" fea_dim: 0 embedx_dim: 0
}
common {name: "" table_name: "barrier_table" trainer_num: 1 sync: false
}
}
}
}
以上是server计算图的配置信息,可以看到一共有3个数据表:
0号表存储了Dense参数,维度是组网中所有FC层weight和b参数的累和,更新方式是adam
1号表存储了Sparse参数,保存的参数是embedding_0.w_0, 维度是[1000, 128],更新方式是adam
2号表是控制各个节点间初始化的同步
计算图拆分——Worker¶
参数服务器模式下,训练过程的数据读取,前向计算,反向计算在Worker上执行。
不失一般性,以PS-CPU异步模式的计算图介绍Worker的计算图,如下图所示

Worker读取当前批次的训练数据
进行前向OP的计算,得到Loss
基于Loss,进行反向OP的计算,得到各个参数的梯度
发送(Send)参数的梯度给PServer
接收(Recv)更新后的参数
重复以上流程,迭代训练数据,实现分布式参数服务器的训练
通过上述流程,Wokre的计算图与单机的计算图区别为:
去除了Optimzier OP
在Optimizer OP原来的位置前, 添加了Send OP
在Optimizer OP原来的位置后, 添加了Recv OP
在同步/异步模式的情况下:
Worker的计算图按照上述规则进行生成,并根据训练需要,添加Clip等操作组件。
目前Paddle的实现中,通信流程使用单例 Communicator 实现,全异步进行训练与通信,因此计算图中仅在最后有Send OP,作用是触发Communicator
在GeoSGD的情况下:
Worker实现参数更新的全流程,通过Send OP 触发 GeoCommunicator,计算并发送本地与全局参数的diff,更多详细介绍,可以参考文档 低频通信参数服务器训练算法
代码实现
Worker的计算图生成源代码位于 build_trainer_program
使用Fleet API时,参考以下python代码:worker_demo.py
# worker_demo.py
import random
import paddle
import paddle.distributed.fleet as fleet
import paddle.distributed.fleet.base.role_maker as role_maker
paddle.enable_static()
input_data = paddle.static.data(name="sparse_input", shape=[
None, 1], dtype="int64")
input_label = paddle.static.data(
name="label", shape=[None, 1], dtype="int64")
label = paddle.cast(input_label, dtype="int64")
embedding = paddle.static.nn.embedding(
input_data, is_sparse=True, size=[1000, 128])
fc1 = paddle.static.nn.fc(embedding, size=1024, activation="relu")
fc2 = paddle.static.nn.fc(fc1, size=512, activation="relu")
fc3 = paddle.static.nn.fc(fc2, size=256, activation="relu")
predict = paddle.static.nn.fc(fc3, size=2, activation="softmax")
cost = paddle.nn.functional.cross_entropy(input=predict, label=label)
role = role_maker.PaddleCloudRoleMaker()
fleet.init(role)
strategy = fleet.DistributedStrategy()
strategy.a_sync = True
strategy.a_sync_configs = {"launch_barrier": False}
optimizer = paddle.optimizer.Adam(1e-4)
optimizer = fleet.distributed_optimizer(optimizer, strategy)
optimizer.minimize(cost)
if fleet.is_worker():
print("worker_main_program: {}".format(
paddle.static.default_main_program()))
fleetrun --worker_num=1 --server_num=1 worker_demo.py
cat log/workerlog.0
通过以上命令运行 worker_demo.py 后,可以打印worker的全部计算图,发现其中并没有Adam相关OP,并且计算图最后是Send OP
{Out=[]} = send(inputs={X=[u'embedding_0.w_0@GRAD']}, is_sparse = 1, op_device = , op_namescope = /, op_role = 4, op_role_var = [], send_varnames = [u'embedding_0.w_0@GRAD'], table_id = 1)
{Out=[]} = send(inputs={X=[u'fc_0.b_0@GRAD', u'fc_0.w_0@GRAD', u'fc_1.b_0@GRAD', u'fc_1.w_0@GRAD', u'fc_2.b_0@GRAD', u'fc_2.w_0@GRAD', u'fc_3.b_0@GRAD', u'fc_3.w_0@GRAD']}, is_sparse = 0, op_device = , op_namescope = /, op_role = 4, op_role_var = [], send_varnames = [u'Dense@Grad'], table_id = 0)
计算图拆分——Heter-Worker¶
异构参数服务器模式下,训练过程的前向计算,反向计算中的一部分在Heter-Worker上执行。

Worker(Trainer)读取当前批次的训练数据
Worker计算前置的在CPU上的前向OP
Woker将前向OP计算结果发给Heter-Woker
Heter-Worker计算在xPU上的前向OP,得到loss
Heter-Worker计算在xPU上的反向OP,得到相关参数梯度
Heter-Worker将部分梯度发送回Worker
Woker计算在CPU上的反向OP,得到相关参数梯度
Woker与Heter-Worker发送(Send)持有的参数的梯度给PServer
Worker与Heter-Woker接收(Recv)更新后的参数
重复以上流程,迭代训练数据,实现分布式参数服务器的训练
通过上述流程,Heter-Worker实现的主要功能是:
与Worker通信,接收并发送指定参数
与PServer通信,发送梯度,接收更新
执行前向/反向 OP的运行
Heter-Woker的计算图由一个或多个异构Block构成,每个Block为一段连续的OP的组合,对应着全局计算图中的一部分。
一个异构block的运行,必然需要依赖前置Variable的产出,同时向后传递本Block生成的,在后续计算所需要的Variable

在Heter-PS模式中,Worker使用send_and_recv OP来触发Hetet-Worker上的异构block的运行,Worker向Heter-Worker发送Entrance Variable,同时等待接收Exit Varibale,实现整个计算图流程的通路。
Heter-PS目前仅支持Async模式
代码实现
Heter-Worker的计算图生成源代码位于 build_trainer_program
使用Fleet API时,参考以下python代码:heter_demo.py(需要安装GPU版本的PaddlePaddle):
# heter_demo.py
import random
import paddle
import paddle.distributed.fleet as fleet
import paddle.distributed.fleet.base.role_maker as role_maker
paddle.enable_static()
with paddle.static.device_guard("cpu"):
input_data = paddle.static.data(name="sparse_input", shape=[
None, 1], dtype="int64")
input_label = paddle.static.data(
name="label", shape=[None, 1], dtype="int64")
label = paddle.cast(input_label, dtype="int64")
embedding = paddle.static.nn.embedding(
input_data, is_sparse=True, size=[1000, 128])
with paddle.static.device_guard("gpu"):
fc1 = paddle.static.nn.fc(embedding, size=1024, activation="relu")
fc2 = paddle.static.nn.fc(fc1, size=512, activation="relu")
fc3 = paddle.static.nn.fc(fc2, size=256, activation="relu")
predict = paddle.static.nn.fc(fc3, size=2, activation="softmax")
cost = paddle.nn.functional.cross_entropy(input=predict, label=label)
role = role_maker.PaddleCloudRoleMaker()
fleet.init(role)
strategy = fleet.DistributedStrategy()
strategy.a_sync = True
strategy.a_sync_configs = {"heter_worker_device_guard": "gpu", "launch_barrier": False}
optimizer = paddle.optimizer.Adam(1e-4)
optimizer = fleet.distributed_optimizer(optimizer, strategy)
optimizer.minimize(cost)
if fleet.is_server():
if role._is_heter_worker():
print("heter_main_program: {}".format(
paddle.static.default_main_program()))
fleetrun --worker_num=1 --server_num=1 --heter_worker_num=1 heter_demo.py
cat log/heterlog.0
通过以上命令运行 heter_demo.py 后,可以打印heter-worker的全部计算图,可以发现计算图中包含了一个异构block,该异构Block的起始OP是从FC的mul操作开始的,在算出embedding的梯度后,以Send OP结束。若同时打印worker的计算图,会观察到原来的FC层,被send_and_recv OP替代。
优化¶
触发稀疏参数更新¶
稠密参数会默认打包为一个大参数后,分片放到各个PServer上
稀疏参数会被均分到各个PServer上
稀疏参数的保存和更新目前可以通过以下OP触发,它们都实现了远程查寻embedding稀疏表的功能,区别在于输入与输出的维度,功能是一致的,在PS模式时,经过图编译,以下OP都会被等价替换成 distributed_lookup_table OP:
paddle.nn.Embedding
import paddle paddle.enable_static() # sparse=True, 触发参数的稀疏化,加快训练和通信速度 embedding = paddle.nn.Embedding( input=x, size=[id_num, id_value_shape], sparse=True)
paddle.static.nn.embedding
import paddle paddle.enable_static() # is_sparse=True, 触发参数的稀疏化,加快训练和通信速度 embedding = paddle.static.nn.embedding( input=x, size=[id_num, id_value_shape], is_sparse=True)
paddle.static.nn.embedding
import paddle paddle.enable_static() # is_sparse=True, 触发参数的稀疏化,加快训练和通信速度 embedding = paddle.static.nn.embedding( input=x, size=[id_num, id_value_shape], is_sparse=True)
paddle.static.nn.sparse_embedding
import paddle paddle.enable_static() # sparse_embedding 触发emb的大规模稀疏 embedding = paddle.static.nn.sparse_embedding( input=x, size=[id_num, id_value_shape])
使用InMemoryDataset/QueueDataset进行训练¶
简介¶
为了能高速运行模型的训练,我们使用InMemoryDataset/QueueDataset
API进行高性能的IO,具体介绍可以参考文档InMemoryDataset
和 QueueDataset
,
以下简称Dataset。Dataset是为多线程及全异步方式量身打造的数据读取方式,每个数据读取线程会与一个训练线程耦合,形成了多生产者-多消费者的模式,会极大的加速我们的模型训练。
本文以训练wide&deep模型为例,在训练中引入基于Dataset 以下是使用Dataset接口一个比较完整的流程:
引入dataset¶
通过
dataset = paddle.distributed.InMemoryDataset()
或者dataset = paddle.distributed.QueueDataset()
创建一个Dataset对象指定dataset读取的训练文件的列表, 通过
set_filelist
配置。通过
dataset.init()
api 进行Dataset的初始化配置,init()
接口接收**kwargs参数, 详见api文档,列举几个配置的初始化将我们定义好的数据输入格式传给Dataset, 通过
use_var
配置。指定我们的数据读取方式,由
my_data_generator.py
实现数据读取的规则,后面将会介绍读取规则的实现, 通过pipe_command
配置。pipe_command
是Dataset特有的通过管道来读取训练样本的方式,通过set_filelist
设置的训练样本文件将被作为管道的输入cat
到管道中经过用户自定义的pipe_command
最终输出。指定数据读取的batch_size,通过batch_size配置。
指定数据读取的线程数,一般该线程数和训练线程应保持一致,两者为耦合的关系,通过
thread_num
配置。
dataset = paddle.distributed.InMemoryDataset()
batch_size = config.config["batch_size"]
thread_num = config.config["thread_num"]
dataset.init(use_var=model.inputs, pipe_command="python reader.py", batch_size=batch_size, thread_num=thread_num)
dataset.set_filelist([config.config["train_files_path"]])
如何指定数据读取规则¶
在上文我们提到了由my_data_generator.py
实现具体的数据管道读取规则,那么,怎样为dataset创建数据读取的规则呢?
以下是reader.py
的全部代码,具体流程如下: 1.
首先我们需要引入data_generator的类,位于paddle.distributed.fleet.data_generator
。
2.
声明一些在数据读取中会用到的类和库。
3.
创建一个子类WideDeepDatasetReader
,继承fleet.data_generator
的基类,基类有多种选择,如果是多种数据类型混合,并且需要转化为数值进行预处理的,建议使用MultiSlotDataGenerator
;若已经完成了预处理并保存为数据文件,可以直接以string
的方式进行读取,使用MultiSlotStringDataGenerator
,能够进一步加速。在示例代码,我们继承并实现了名为Word2VecReader
的data_generator子类,使用MultiSlotDataGenerator
方法。
4.
继承并实现基类中的generate_sample
函数,逐行读取数据。该函数应返回一个可以迭代的reader方法(带有yield的函数不再是一个普通的函数,而是一个生成器generator,成为了可以迭代的对象,等价于一个数组、链表、文件、字符串etc.)
5.
在这个可以迭代的函数中,如示例代码中的def wd_reader()
,我们定义数据读取的逻辑。例如对以行为单位的数据进行截取,转换及预处理。
最后,我们需要将数据整理为特定的batch的格式,才能够被dataset正确读取,并灌入的训练的网络中。使用基类中的
generate_batch
函数, 我们无需再做声明 根据设定的’batch_size’, 该函数会在generator_sample
函数产生样本数达到batch_size
时,调用该函数内队逐条样本的处理逻辑,如示例代码中的def local_iter()
。简单来说,数据的输出顺序与我们在网络中创建的
inputs
必须是严格一一对应的,并转换为类似字典的形式。在示例代码中,我们将参数名与数值构成的元组组成了一个list,并将其yield输出。如果展开来看,我们输出的数据形如[('dense_input',[value]),('C1',[value]),......('label',[value])]
import paddle
import paddle.distributed.fleet as fleet
import os
import sys
cont_min_ = [0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
cont_max_ = [20, 600, 100, 50, 64000, 500, 100, 50, 500, 10, 10, 10, 50]
cont_diff_ = [20, 603, 100, 50, 64000, 500, 100, 50, 500, 10, 10, 10, 50]
hash_dim_ = 1000001
continuous_range_ = range(1, 14)
categorical_range_ = range(14, 40)
class WideDeepDatasetReader(fleet.MultiSlotDataGenerator):
def line_process(self, line):
features = line.rstrip('\n').split('\t')
dense_feature = []
sparse_feature = []
for idx in continuous_range_:
if features[idx] == "":
dense_feature.append(0.0)
else:
dense_feature.append(
(float(features[idx]) - cont_min_[idx - 1]) / cont_diff_[idx - 1])
for idx in categorical_range_:
sparse_feature.append(
[hash(str(idx) + features[idx]) % hash_dim_])
label = [int(features[0])]
return [dense_feature]+sparse_feature+[label]
def generate_sample(self, line):
def wd_reader():
input_data = self.line_process(line)
feature_name = ["dense_input"]
for idx in categorical_range_:
feature_name.append("C" + str(idx - 13))
feature_name.append("label")
yield zip(feature_name, input_data)
return wd_reader
if __name__ == "__main__":
my_data_generator = WideDeepDatasetReader()
my_data_generator.set_batch(16)
my_data_generator.run_from_stdin()
快速调试Dataset¶
我们可以脱离组网架构,单独验证Dataset的输出是否符合我们预期。使用命令
cat 数据文件 | python dataset读取python文件
进行dataset代码的调试:
cat data/part-0 | python reader.py
输出的数据格式如下:
13 0.0 0.00663349917081 0.01 0.0 0.0423125 0.054 0.12 0.0 0.074 0.0 0.4 0.0 0.0 1 371155 1 846239 1 204942 1 600511 1 515218 1 906818 1 369888 1 507110 1 27346 1 698085 1 348211 1 170408 1 597913 1 255651 1 415979 1 186815 1 342789 1 994402 1 880474 1 984402 1 208306 1 26235 1 410878 1 701750 1 934391 1 552857 1 1
理想的输出为(截取了一个片段):
...
13 0.0 0.00663349917081 0.01 0.0 0.0423125 0.054 0.12 0.0 0.074 0.0 0.4 0.0 0.0 1 371155 1 846239 1 204942 1 600511 1 515218 1 906818 1 369888 1 507110 1 27346 1 698085 1 348211 1 170408 1 597913 1 255651 1 415979 1 186815 1 342789 1 994402 1 880474 1 984402 1 208306 1 26235 1 410878 1 701750 1 934391 1 552857 1 1
...
使用Dataset的一些注意事项 - Dataset的基本原理:将数据print到缓存,再由C++端的代码实现读取,因此,我们不能在dataset的读取代码中,加入与数据读取无关的print信息,会导致C++端拿到错误的数据信息。 - dataset目前只支持在
unbuntu
及CentOS
等标准Linux环境下使用,在Windows
及Mac
下使用时,会产生预料之外的错误,请知悉。
数据准备¶
完整数据下载以及预处理之后可以选取一个part的文件作为demo数据保存在data目录下
训练¶
import paddle
import paddle.distributed.fleet as fleet
import config
# 开启paddle静态图模式
paddle.enable_static()
fleet.init()
model = X.applications.Word2vec()
"""
need config loader correctly.
"""
loader = model.load_dataset_from_file(train_files_path=[config.config["train_files_path"]], dict_path=config.config["dict_path"])
strategy = fleet.DistributedStrategy()
strategy.a_sync = True
optimizer = fleet.distributed_optimizer(optimizer, strategy)
optimizer.minimize(model.cost)
if fleet.is_server():
fleet.init_server()
fleet.run_server()
if fleet.is_worker():
place = paddle.CPUPlace()
exe = paddle.static.Executor(place)
exe.run(paddle.static.default_startup_program())
fleet.init_worker()
distributed_training(exe, model)
clear_metric_state(model, place)
fleet.stop_worker()
完整示例代码可以参考 PaddleFleetX/tree/old_develop/examples/wide_and_deep_datase 目录
通过以上简洁的代码,即可以实现wide&deep模型的多线程并发训练
低频通信参数服务器训练算法¶
简介¶
众所周知,在同步/异步参数服务器分布式训练中Worker每训练完一个周期,都会将梯度上传至PServer,等待PServer分发最新的参数后才开始新一轮的训练。在这种训练方式中,节点间的通信会消耗大量的时间成本,进而影响训练的效率。
为了降低节点见通信对训练速度的影响,Fleet提供了一种更高效的参数更新策略:GeoSGD
原理¶

在GeoSGD更新策略中,Worker的参数更新也是在全异步的条件下进行的。但与异步参数服务器有以下不同:
与普通的参数服务器不同,在GEO策略中,每个Worker负责在本地维护自己的参数更新,在训练一定数量的步数后将本轮训练出的参数与上一轮结束后的参数做差。并除以Worker的个数,将结果上传至PServer。PServer则负责为每个Worker计算其参数与全局参数的diff。
GEO更新策略会在训练过程中启动多个进程,负责参数更新及节点通信。在Worker与PServer的整个交互过程中,主进程会保持模型的训练,由子进程负责与PServer进行交互,在拿到与全局参数的diff后将其更新至主进程。
GEO策略通过模型训练与节点通信同步进行的方式,在保证模型效果的前提下,大大提升了训练的速度。在Word2Vec模型上测试发现,GEO策略相比异步参数服务器,训练速度提高了3倍多。
使用方法¶
添加依赖¶
首先我们需要添加训练中所用到的python模块paddle
和paddle.distributed.fleet
,后者主要提供分布式相关的接口和策略配置。
目前Paddle默认为动态图运行模式,分布式参数服务器训练当前仅支持在静态图模式下运行,所以需要自行打开静态图开关。
import paddle
import paddle.distributed.fleet as fleet
paddle.enable_static()
初始化分布式训练环境¶
多机参数服务器均是通过fleet.init()
接口初始化分布式训练环境,用户可通过传入 role_maker 进行相关配置,若为None,则框架会自动根据用户在环境变量中的配置进行分布式训练环境的初始化。
fleet.init(role_maker=None)
配置GEO策略及优化算法¶
在Fleet API中,用户可以使用fleet.DistributedStrategy()
接口定义自己想要使用的分布式策略。
想要使用GEO策略,用户首先需要打开异步参数服务器开关,即设置a_sync
为True。
然后用户需要通过dist_strategy.a_sync_configs
设置Worker上传参数的频率,下面的代码中我们设置Worker每训练400个Batch后与PServer进行交互。
dist_strategy = fleet.DistributedStrategy()
dist_strategy.a_sync = True
dist_strategy.a_sync_configs = {"k_steps": 400}
optimizer = paddle.optimizer.SGD(learning_rate=0.0001)
optimizer = fleet.distributed_optimizer(optimizer, dist_strategy)
optimizer.minimize(model.cost)
开始训练¶
GEO策略的训练代码沿用了参数服务器分布式训练的形式。
对于PServer节点,首先用init_server()
接口对其进行初始化,然后启动服务并开始监听由训练节点传来的参数变化值。
同样对于训练节点,用init_worker()
接口进行初始化后,开始执行训练任务。
if fleet.is_server():
fleet.init_server()
fleet.run_server()
else:
exe.run(paddle.static.default_startup_program())
fleet.init_worker()
# do training
distributed_training(exe, model)
运行方法¶
完整运行示例见 examples/wide_and_deep, 需注意,该示例指定的分布式训练模式为异步,可参考GEO模式策略配置方法,将任务运行模式变为GEO模式。
配置完成后,通过fleetrun
指令运行分布式任务。命令示例如下,其中server_num
, worker_num
分别为服务节点和训练节点的数量。
fleetrun --server_num=2 --worker_num=2 train.py
使用Fleet进行异构参数服务器训练¶
异构参数服务器目前仅支持在静态图下运行
什么是异构参数服务器?¶
在开始使用异构参数服务器
前,您需要先了解参数服务器的基本知识。我们先进行简单回顾:
参数服务器的应用领域以及解决的问题¶
参数服务器集中应用在NLP
、推荐
以及
搜索
领域,其主要针对以下两个问题:
大数据:
原始数据集庞大,动辄几百G的数据量,单机训练速度难以承受,需要依赖数据并行加速训练,以此解决大数据的问题。
大参数:
在上述场景中,数据量大的同时,伴随着特征的稀疏性,开发者们通常使用Embedding技术来将业务场景中的高维稀疏特征向量转化为低维的稠密特征向量。
在工业场景中,该Embedding参数的维度往往是亿级,占用的存储空间在上百GB,单机内存以及单卡显存无法承受完整的参数保存。但我们可以观察到,使用每个batch的数据进行训练时,并不会用到全部的稀疏参数,仅需与样本有关的部分参数,内存或显存可以承受。因此在这种场景下,开发者们通常使用参数服务器模式,将大参数分片放到各个
Server
上。Worker
训练模型时,仅请求当前batch数据需要用到的参数,以此解决大参数的问题。
传统参数服务器的局限¶
传统参数服务器采用纯CPU机器进行训练。在实际工业应用中,存在着以下问题:
CPU机器算力有瓶颈
利用多台CPU机器多核的优势,在简单模型上极大的提升数据吞吐,整体训练达到较好的性能。但是,随着深度学习模型的日渐复杂,在一些计算能力要求高的模型中,比如
BERT
,计算能力严重不足,模型计算耗时极高。分布式CPU机器成本大
由于工业场景下大规模和大参数的背景,通常会并发使用几十甚至几百台CPU机器进行离线训练,然而随着GPU的迅猛发展,GPU机器价格逐渐下降,上百台CPU带来的成本消耗比少量GPU机器带来的成本消耗少很多。
异构参数服务器基本原理¶
PaddlePaddle基于工业实践,创新性的提出了异构参数服务器,支持纯GPU-ps训练,也支持CPU+GPU机器混合训练,将任务进行硬件感知切分,做到物尽其用。
一个深度学习模型的训练过程可以拆分为三步:1、前向计算Loss;2、反向计算梯度;3、利用梯度更新参数。
参数服务器模式下,前向及反向步骤在Worker
端(也称为Trainer
)完成,参数更新在Server
端完成。
异构参数服务器模式中,我们可以将Embedding查表,输入数据的Reshape等IO密集型的OP放置于CPU-Trainer
,将RNN,Attention等计算密集型的OP放置于Heter-Trainer
。
CPU-Trainer
执行数据的读取,Embedding查表,数据预处理等步骤后,将运算结果通过RPC请求发往Heter-Trainer
;Heter-Trainer
收到前向结果后,执行这些参数的后续的前向与反向步骤,运算结束后,将后续流程需要的上下文参数发回 CPU-Trainer
。同时两个Trainer都独立与Server通信,发送当前设备上的产生的梯度,统一由Server执行Optimizer更新参数

异构参数服务器使用方法¶
本节将采用推荐领域非常经典的DNN模型为例,介绍异构参数服务器中纯GPU-ps训练的使用方法,详细示例代码可参考:https://github.com/PaddlePaddle/PaddleRec/tree/master/tools/static_gpubox_trainer.py
环境构建¶
机器准备:带有GPU卡的Linux机器
- 版本要求:paddlepaddle-2.1-gpu及以上版本的飞桨开源框架。推荐使用以下链接下载最新whl:
导入依赖¶
import paddle
import paddle.distributed.fleet as fleet
定义分布式模式并初始化分布式训练环境¶
通过fleet.init()
接口,进行分布式模式初始化。
# 当前参数服务器模式只支持静态图模式, 因此训练前必须指定`paddle.enable_static()`
paddle.enable_static()
fleet.init()
加载模型及数据¶
通过get_model
加载模型,get_reader
加载数据dataset,模型和dataset具体的配置可参考:models/rank/dnn/config_gpubox.yaml
# 模型定义参考models/rank/dnn/net.py
self.model = get_model(self.config)
self.metrics = self.model.net(self.input_data)
self.reader, self.file_list = get_reader(self.input_data, self.config)
定义Optimizer¶
选择 Optimizer
优化器,并调用minimize方法构建反向。
# 优化器调用参考models/rank/dnn/static_model.py
optimizer = paddle.fluid.optimizer.Adam(learning_rate=5e-06)
optimizer = fleet.distributed_optimizer(optimizer, strategy)
optimizer.minimize(model.cost)
开始训练¶
完成模型定义和优化器选择后,我们开始训练模型。和快速开始中介绍的训练方式一样,因为在参数服务器模式下会有不同的角色。
对于服务器节点,首先用init_server()
接口对其进行初始化,然后启动服务并开始监听由训练节点传来的梯度。
同样对于训练节点,调用init_worker()
接口进行基本初始化后,还需要调用PSGPU进行GPU相关的初始化,set_slot_vector
接口传入模型中稀疏参数的名字列表,init_gpu_ps
接口传入worker端所需GPU卡的地址,接着就可以执行训练任务。
为了提高模型运行速度,我们使用 InMemoryDataset
进行训练,详细可参考:使用InMemoryDataset/QueueDataset进行训练
if fleet.is_server():
fleet.run_server()
if fleet.is_worker():
place = paddle.CUDAPlace(0)
exe = paddle.static.Executor(place)
exe.run(paddle.static.default_startup_program())
fleet.init_worker()
gpus_env = os.getenv("FLAGS_selected_gpus")
psgpu = paddle.fluid.core.PSGPU()
psgpu.set_slot_vector(model.slots_name)
psgpu.init_gpu_ps([int(s) for s in gpus_env.split(",")])
for epoch in range(epochs):
self.reader._set_use_ps_gpu(1)
psgpu.begin_pass()
exe.train_from_dataset(
program=paddle.static.default_main_program(),
dataset=self.reader)
self.reader.release_memory()
psgpu.end_pass()
psgpu.finalize()
fleet.stop_worker()
运行训练脚本¶
在PaddleRec根目录下,使用已提供的运行脚本进行训练即可。
sh run_gpubox.sh
脚本中通过 fleetrun
命令启动分布式任务,其中需要关注并设置的参数有:
# set free port if 29011 is occupied
export PADDLE_PSERVERS_IP_PORT_LIST="127.0.0.1:29011"
export PADDLE_PSERVER_PORT_ARRAY=(29011)
# set gpu numbers according to your device
export FLAGS_selected_gpus="0,1,2,3,4,5,6,7"
# set your model yaml
SC="tools/static_gpubox_trainer.py -m models/rank/dnn/config_gpubox.yaml"
增量训练¶
简介¶
增量训练是一种常见的机器学习方法,在深度学习领域内有广泛的应用,它代表的是一种动态学习的训练方式,即训练数据随着时间的推移源源不断的加入到当前训练流程中,扩展当前模型的知识和能力。
飞桨的参数服务器训练支持增量训练,支持训练在某一时间点进行训练模型参数(含部分优化器的状态)的全量保存,在重启训练时将之前保存的全量参数进行加载,结合新的训练数据继续训练,从而学习到新的有用信息。
原理介绍¶
飞桨参数服务器增量训练包含两部分内容,即模型保存和模型加载。训练节点分为PServer和Worker两种,每个Worker上都有完整的稠密参数,没有稀疏参数。稀疏参数和稠密参数分布于全部的PServer节点上。
飞桨模型参数在参数服务器下分为稠密参数和稀疏参数两种, 在调用模型保存的接口后,会分别在PServer端和0号Worker端进行参数的保存,其中0号Worker端将保存全部的稠密参数及相关的状态,每个PServer将以分片的形式保存位于该PServer端的稀疏参数。
飞桨模型参数在参数服务器下分为稠密参数和稀疏参数两种, 需要分别在PServer端和0号Worker端进行加载才能完成对参数的加载。
训练启动时每个PServer的基本初始流程如下:
每个节点执行 fleet.init_server(dirname=None, var_names=None, **kwargs) 进行PServer端初始化,分配到此节点的稠密参数会按照定义的形状和初始化方法进行初始化, 稀疏参数则只预定义出初始化方法,稀疏参数会在训练过程中根据前向通信算子发送过来的ID进行实时初始化。 init_server用有两个选配参数,分别是 dirname`和`var_names,`dirname`表示需要增量加载的模型路径,两个选配参数相互配合实现稀疏参数的加载,注意, 如果只指定 dirname, 则表示会从指定的目录中加载全部的稀疏参数, 如果还指定了`var_names`,则表示加载指定参数名的稀疏参数。 注意,init_server 只会加载稀疏参数,稠密参数的加载在Worker端进行。
每个节点执行 fleet.run_server() 表明当前节点已经初始化成功,可以支持Worker端的连接和通信。
训练启动时每个Worker的基本初始流程如下:
每个节点执行 exe.run(paddle.static.default_startup_program()) 进行参数初始化。
0号节点执行 paddle.static.load_vars() 指定要加载的稠密参数的名字列表和模型目录,将稠密参数通过此方式进行加载。
每个节点执行 fleet.init_worker() , 其中0号节点的稠密参数将同步给相应的PServer,其他节点(非0号)会从PServer端将稠密参数取回本地赋值给本地的稠密参数。
至此,完成了整个训练开始前,PServer和Worker中稠密参数和稀疏参数的加载和同步。
功能效果¶
训练开始时,使用上述方法,可实现模型参数的全量加载。
训练结束是,使用上述方法,可实现模型参数的全量保存。
使用方法¶
模型保存:
# 在需要保存模型的地方,执行下面的命令,即可完成模型中全量参数的保存
# 其中, 稠密参数会被保存在0号Worker上, 稀疏参数会被保存在每个PServer上的同名路径下
dirname = "/you/path/to/model"
if fleet.is_first_worker():
fleet.save_persistables(dirname)
模型加载(文章末尾附录了获取稀疏/稠密参数的代码,参考或复制使用):
对于流式训练,模型加载需要使用全量保存的模型(fleet.save_persistable, 配置mode=0),如果使用增量保存(Base+Detla)的方式,在拥有准入配置的情况下,可能会丢一部分未被准入的特征。
# 模型加载需要区分是PServer还是Worker
dirname = "/you/path/to/model"
if fleet.is_server():
sparse_varnames = [var.name for var in get_sparse_vars()]
fleet.init_server(dirname, sparse_varnames)
fleet.run_server()
if fleet.is_worker():
place = paddle.CPUPlace()
exe = paddle.static.Executor(place)
exe.run(paddle.static.default_startup_program())
dense_vars = get_dense_vars()
paddle.static.load_vars(executor=exe, dirname=path, vars=dense_vars)
fleet.init_worker()
运行成功提示¶
模型加载当前并没有提示
模型保存成功,会在相应的目录保存下模型文件, 稀疏参数会被保存在每个PServer上的同名路径下。
常见问题与注意事项¶
节点动态调整
训练节点在发生变化的情况下, 稀疏参数需要做一次重新分布分布以满足新的加载需求。
当前框架并没有提供此稀疏参数重分布脚本,目前需要用户自行编写。
加载指定稠密参数
用户可以选择性的加载所需的稠密参数,具体是在 0号 Worker 执行 `paddle.static.load_vars`时 ,指定的 vars的列表来控制。
加载指定稀疏参数
用户可以选择性的加载指定的稀疏参数,具体是在PServer执行`init_server`时,指定`var_names`的列表,通过此列表来控制加载的参数名单。
论文/引用¶
[略]
附录¶
获取稀疏/稠密参数的代码¶
def get_sparse_vars():
import paddle
program = paddle.static.default_main_program()
SPARSE_OP_TYPE_DICT = {"lookup_table", "lookup_table_v2"}
def is_sparse_op(op):
if op.type in SPARSE_OP_TYPE_DICT and op.attr('is_sparse') is True or \
op.type == "distributed_lookup_table":
return True
return False
def get_sparse_varnames():
tablenames = set()
for op in program.global_block().ops:
if is_sparse_op(op):
tablenames.add(op.input("W")[0])
return list(tablenames)
varnames = get_sparse_varnames()
sparse_vars = set()
for varname in varnames:
sparse_vars.add(program.global_block().vars[varname])
return list(sparse_vars)
def get_dense_vars():
import paddle
program = paddle.static.default_main_program()
def is_persistable(var):
if var.desc.type() == paddle.fluid.core.VarDesc.VarType.FEED_MINIBATCH or \
var.desc.type() == paddle.fluid.core.VarDesc.VarType.FETCH_LIST or \
var.desc.type() == paddle.fluid.core.VarDesc.VarType.READER:
return False
return var.persistable
exe = paddle.static.Executor(paddle.CPUPlace())
sparse_varnames = [var.name for var in get_sparse_vars()]
dense_vars = set()
for name, var in program.global_block().vars.items():
if is_persistable(var) and var.name not in sparse_varnames:
dense_vars.add(var)
return list(dense_vars)
流式训练¶
简介¶
飞桨参数服务器训练支持流式训练模式,支持配置千亿级大规模稀疏及[0, INT64]范围内的ID映射,支持模型自增长及配置特征准入(不存在的特征可以以适当的条件创建)、淘汰(够以一定的策略进行过期的特征的清理)等策略,支持模型增量保存,通过多种优化来保证流式训练的流程及效果。
原理介绍¶
流式训练(OnlineLearning), 即训练数据不是一次性放入训练系统,而是随着时间流式的加入到训练过程中去。 整个训练服务不停止,数据经过预处理后进入训练系统参与训练并产出线上所需的预测模型参数。通过流式数据的生产、实时训练及快速部署上线来提升推荐系统的性能和效果。流式训练是按照一定顺序进行数据的接收和处理,每接收一个数据,模型会对它进行预测并对当前模型进行更新,然后处理下一个数据。 像信息流、小视频、电商等场景,每天都会新增大量的数据, 让每天(每一刻)新增的数据基于上一天(上一刻)的模型进行新的预测和模型更新。
使用方法¶
大规模稀疏及准入配置¶
-
paddle.static.nn.
sparse_embedding
(input, size, padding_idx=None, is_test=False, entry=None, param_attr=None, dtype='float32')¶
飞桨参数服务器模式使用大规模稀疏需要使用`paddle.static.nn.sparse_embedding`作为embedding lookup层的算子, 而不是使用 paddle.nn.functional.embedding。 paddle.static.nn.sparse_embedding 采用稀疏模式进行梯度的计算和更新,支持配置训练模式(训练/预测),支持配置准入策略,且输入接受[0, INT64]范围内的特征ID, 更加符合在线学习的功能需求。
参数:
input, (Tensor): 存储特征ID的Tensor,数据类型必须为:int32/int64,特征的范围在[0, INT64]之间,超过范围在运行时会提示错误。
size, (list|tuple): 形状为(num_embeddings, embedding_dim), 大规模稀疏场景下, 参数规模初始为0,会随着训练的进行逐步扩展,因此num_embeddings 暂时无用,可以随意填一个整数,embedding_dim 则为词嵌入权重参数的维度配置。
padding_idx, (int),如果配置了padding_idx,那么在训练过程中遇>到此id时会被用0填充。
is_test, (bool),训练/预测模式,在预测模式(is_test=False)下,遇到不存在的特征,不会初始化及创建,直接以0填充后返回。
entry, (ProbabilityEntry|CountFilterEntry, optinal),准入策略配置,目前支持概率准入和频次准入。
param_attr, (ParamAttr, optinal),embedding层参数属性,类型是ParamAttr或者None, 默认为None。
dtype, (float32|float64, optinal),输出Tensor的数据类型,支持float32、float64。当该参数值为None时, 输出Tensor的数据类型为float32。默认值为None。
用法示例:
... import paddle sparse_feature_dim = 1024 embedding_size = 64 # 训练过程中,出现超过10次及以上的特征才会参与训练 entry = paddle.distributed.CountFilterEntry(10) input = paddle.static.data(name='ins', shape=[1], dtype='int64') emb = paddle.static.nn.sparse_embedding(( input=input, size=[sparse_feature_dim, embedding_size], is_test=False, entry=entry, param_attr=paddle.ParamAttr(name="SparseFeatFactors", initializer=paddle.nn.initializer.Uniform()))
淘汰配置¶
-
paddle.distributed.fleet.
shrink
(threshold)¶
使用此接口,可以按照一定的频率进行过期ID特征的清理,稀疏参数在初始化的时候,会在内部设定一个最近出现时间戳的记录(相对值计数,非timestamp),当特征ID在训练中出现时,此值被置位0,当用户显示调用`paddle.distributed.fleet.shrink(threshold)`是,此值会主动递增1,当值超过`threshold`时,则会被清理掉。
参数:
threshold, (int): 对于超过一定时间未出现的特征进行清理。
... import paddle ... dataset, hour, day = get_ready_training_dataset() do_training ... # 天级别的淘汰,每天的数据训练结束后,对所有特征的过期时间+1,对超过30天未出现的特征进行清理 unseen_days = 30 if fleet.is_first_worker() and hour == 23: paddle.distributed.fleet.shrink(unseen_days)
保存及增量保存配置¶
-
paddle.distributed.fleet.
save_persistables
(exe, dirname, mode)¶
模型保存接口,使用该接口会将当前训练中涉及到的模型权重,优化器的中间值全量保存下来,供增量训练、恢复训练、在线预测使用。 针对大规模稀疏,会提供对应的save_base、save_delta等增量保存方案,降低模型保存的磁盘占用及耗时。
参数:
executor, (Executor): 用于保存持久性变量的
executor
。dirname, (str): 用于储存持久性变量的文件目录。`
mode, (0|1|2, optinal),仅支持 0、1、2 三个数值的配置,0 表示全量保存,1 表示保存base模型, `2`表示保存增量模型。
... import paddle ... dataset, hour, day = get_ready_training_dataset() do_training ... # 天级别的淘汰,每天的数据训练结束后,对所有特征的过期时间+1,对超过30天未出现的特征进行清理 unseen_days = 30 if fleet.is_first_worker() and hour == 0: # 每天的0点,保存一次全量模型 if hour == 0: fleet.save_persistables(exe, "output/epoch_{}".format(day), 1) # 其他时间点,每个小时保存一次增量模型 else: fleet.save_persistables(exe, "output/epoch_{}".format(day), 2)
常规训练流程¶
流式训练是个上下游牵涉众多的训练方法,本文只贴出训练相关的配置给用户做一个讲解,具体使用需要结合实际情况进行代码的伪代码:
# 初始化分布式环境
fleet.init()
# your real net function
model = net()
# 使用参数服务器异步训练模式
strategy = paddle.distributed.fleet.DistributedStrategy()
strategy.a_sync = True
# 分布式训练图优化
adam = paddle.optimizer.Adam(learning_rate=5e-06)
adam = fleet.distributed_optimizer(adam, strategy=strategy)
adam.minimize(model.avg_cost)
# 启动PServer
if fleet.is_server():
fleet.init_server()
fleet.run_server()
if fleet.is_worker():
# 初始化Worker
exe.run(paddle.static.default_startup_program())
fleet.init_worker()
while True:
# 持续不断的从`get_ready_training_set`获取可训练的书记集和相关的配置
# 下面是一个按小时训练的例子
dataset, hour, day = get_ready_training_dataset()
if dataset is None:
break
# 使用`dataset`中的数据进行训练和模型保存
exe.train_from_dataset(program=paddle.static.default_main_program(),
dataset=dataset,
fetch_list=[model.auc],
fetch_info=["avg_auc"],
print_period=10)
# 0号保存模型即可,每天第0个小时进行全量保存, 剩余时间进行增量保存
if fleet.is_first_worker():
unseen_days = 30
if hour == 23:
paddle.distributed.fleet.shrink(unseen_days)
if hour == 0:
fleet.save_persistables(exe, "output/epoch_{}".format(day), 1)
else:
fleet.save_persistables(exe, "output/epoch_{}".format(day), 2)
fleet.stop_worker()
运行成功提示¶
[略]
常见问题与注意事项¶
训练过程中,如需使用分布式指标,请参考<分布式指标章节>。
如果训练中途中断,需要加载模型后继续训练,请参考<增量训练章节>
论文/引用¶
[略]
分布式指标¶
简介¶
分布式指标是指在分布式训练任务中用以评测模型效果的指标。它和单机模型评测指标的区别在于,单机指标仅评测当前节点的测试数据,而分布式指标需评测所有节点的全量测试数据。
原理¶
分布式指标的计算一般包含三步,下面我们以分布式准确率为例介绍整个过程。
初始化分布式训练环境
import paddle.distributed.fleet as fleet fleet.init()
定义指标计算需要的所有中间状态统计值,每个训练节点统计各自的状态值。准确率计算需要样本总数和正确分类的样本数两个统计值。
... pred, label = model() # 1. 定义中间状态统计值,样本总数和正确分类的样本数 correct_cnt = paddle.static.create_global_var(name="right_cnt", persistable=True, dtype='float32', shape=[1], value=0) total_cnt = paddle.static.create_global_var(name="total_cnt", persistable=True, dtype='float32', shape=[1], value=0) # 2. 训练节点自己的状态统计 batch_cnt = paddle.sum( paddle.full(shape=[paddle.shape(label)[0], 1], fill_value=1.0)) batch_accuracy = paddle.static.accuracy(input=pred, label=label) batch_correct = batch_cnt * batch_accuracy paddle.assign(correct_cnt + batch_correct, correct_cnt) paddle.assign(total_cnt + batch_cnt, total_cnt) accuracy = correct_cnt / total_cnt
所有训练节点间进行 all_reduce 操作,获取全局统计值,然后根据指标计算公式,计算全局指标。
global_cnt = fleet.metrics.sum(total_cnt) global_correct = fleet.metrics.sum(corrent_cnt) global_accuracy = float(global_correct) / float(global_cnt)
分布式指标¶
为方便使用,Paddle在 paddle.distributed.metrics 下将常见的一些指标计算进行了封装,下面对这些API的功能及参数进行说明,并提供用法示例。
分布式AUC¶
-
paddle.distributed.fleet.metrics.
auc
(stat_pos, stat_neg, scope=None, util=None)¶
分布式AUC(Area Under the Curve)。AUC 是一个二分类任务中常用的效果评价指标,指ROC曲线和横坐标轴之间的面积,该值介于0~1之间,越大代表分类器效果越好。
参数:
stat_pos, (numpy.array|Tensor|string, required): 单机正样例中间统计结果,即单机 paddle.static.auc 的 stat_pos 输出。
stat_neg, (numpy.array|Tensor|string, required): 单机负样例中间统计结果,即单机 paddle.static.auc 的 stat_neg 输出。
scope, (Scope, optional),作用域,若为None,则使用全局/默认作用域,默认为None。
util, (UtilBase, optinal),分布式训练工具类,若为None,则使用默认工具类 fleet.util, 默认为None。
用法示例:
... pred, label = model() # 1. 单机组网阶段,计算正负样例中间统计结果。 auc, batch_auc, [batch_stat_pos, batch_stat_neg, stat_pos, stat_neg] = \ paddle.static.auc(input=pred, label=label) # 2. 分布式训练阶段,计算全局AUC。 global_auc = fleet.metrics.auc(stat_pos, stat_neg)
分布式Accuracy¶
-
paddle.distributed.fleet.metrics.
acc
(correct, total, scope=None, util=None)¶
分布式准确率。准确率(Accuracy)是分类任务中常用的一个效果评价指标。通过比对预测标签和实际标签是否一致,从而计算模型的分类效果,公式如下:
\[accuracy = \frac{correct}{total}\]
其中,correct 是预测标签等于真实标签的样本总数,total 是全部样本总数。
参数:
correct, (numpy.array|Tensor|string, required): 单机预测标签等于真实标签的样本总数。
total, (numpy.array|Tensor|string, required): 单机样本总数。
scope, (Scope, optional),作用域,若为None,则使用全局/默认作用域,默认为None。
util, (UtilBase, optinal),分布式训练工具类,若为None,则使用默认工具类 fleet.util, 默认为None。
用法示例:
... pred, label = model() # 1. 单机组网阶段,计算样本总数和预测正确的样本数 correct_cnt = paddle.static.create_global_var(name="right_cnt", persistable=True, dtype='float32', shape=[1], value=0) total_cnt = paddle.static.create_global_var(name="total_cnt", persistable=True, dtype='float32', shape=[1], value=0) batch_cnt = paddle.sum( paddle.full(shape=[paddle.shape(label)[0], 1], fill_value=1.0)) batch_accuracy = paddle.static.accuracy(input=pred, label=label) batch_correct = batch_cnt * batch_accuracy paddle.assign(correct_cnt + batch_correct, correct_cnt) paddle.assign(total_cnt + batch_cnt, total_cnt) accuracy = correct_cnt / total_cnt # 2. 分布式训练阶段,计算全局准确率。 global_accuracy = fleet.metrics.acc(correct_cnt, total_cnt)
分布式MAE¶
-
paddle.distributed.fleet.metrics.
mae
(abserr, total_ins_num, scope=None, util=None)¶
分布式平均绝对误差(Mean Absolute Error)。平均绝对误差是绝对误差的平均值,一般用于计算 loss 损失值。
\[ \begin{align}\begin{aligned}abserr &= \sum |input - label|\\mae &= \frac{abserr}{total\_ins\_num}\end{aligned}\end{align} \]
其中,input 是样本预测结果, label 是样本真实标签,abserr 为绝对误差和,total_ins_num 是样本总数。
参数:
abserr, (numpy.array|Tensor|string, required): 单机绝对误差和统计值。
total_ins_num, (numpy.array|Tensor|string, required): 单机样本总数。
scope, (Scope, optional),作用域,若为None,则使用全局/默认作用域,默认为None。
util, (UtilBase, optinal),分布式训练工具类,若为None,则使用默认工具类 fleet.util, 默认为None。
用法示例:
... pred, label = model() # 1. 单机组网阶段,计算绝对误差和样本总数 abserr = paddle.static.create_global_var(name="abserr", persistable=True, dtype='float32', shape=[1], value=0) total_cnt = paddle.static.create_global_var(name="total_cnt", persistable=True, dtype='float32', shape=[1], value=0) batch_cnt = paddle.sum( paddle.full(shape=[paddle.shape(label)[0], 1], fill_value=1.0)) batch_abserr = paddle.nn.functional.l1_loss(pred, label, reduction='sum') paddle.assign(abserr + batch_abserr, abserr) paddle.assign(total_cnt + batch_cnt, total_cnt) mae = abserr / total_cnt # 2. 分布式训练阶段,计算全局准确率。 global_mae = fleet.metrics.mae(abserr, total_cnt)
分布式MSE¶
-
paddle.distributed.fleet.metrics.
mse
(sqrerr, ins_num, scope=None, util=None)¶
分布式均方误差(Mean Squared Error)。均方误差是误差平方和的平均值,一般用于计算 loss 损失值。
\[ \begin{align}\begin{aligned}sqrerr &= \sum (input - label)^2\\mse &= \frac{sqrerr}{total\_ins\_num}\end{aligned}\end{align} \]
其中,input 是样本预测结果, label 是样本真实标签,sqrerr 为平方误差和,total_ins_num 是样本总数。
参数:
sqrerr, (numpy.array|Tensor|string, required): 单机平方误差和统计值。
total_ins_num, (numpy.array|Tensor|string, required): 单机样本总数。
scope, (Scope, optional),作用域,若为None,则使用全局/默认作用域,默认为None。
util, (UtilBase, optinal),分布式训练工具类,若为None,则使用默认工具类 fleet.util, 默认为None。
用法示例:
... pred, label = model() # 1. 单机组网阶段,计算平方误差和样本总数 sqrerr = paddle.static.create_global_var(name="sqrerr", persistable=True, dtype='float32', shape=[1], value=0) total_cnt = paddle.static.create_global_var(name="total_cnt", persistable=True, dtype='float32', shape=[1], value=0) batch_cnt = paddle.sum( paddle.full(shape=[paddle.shape(label)[0], 1], fill_value=1.0)) batch_sqrerr = paddle.nn.functional.mse_loss(pred, label, reduction='sum') paddle.assign(sqrerr + batch_sqrerr, sqrerr) paddle.assign(total_cnt + batch_cnt, total_cnt) mse = sqrerr / total_cnt # 2. 分布式训练阶段,计算全局准确率。 global_mse = fleet.metrics.mse(sqrerr, total_cnt)
分布式RMSE¶
-
paddle.distributed.fleet.metrics.
rmse
(sqrerr, total_ins_num, scope=None, util=None)¶
分布式均方根误差(Root Mean Squared Error)。均方根误差是均方误差的算术平方根,亦称标准误差,一般用于计算 loss 损失值。
\[ \begin{align}\begin{aligned}sqrerr &= \sum (input - label)^2\\rmse &= \sqrt{\frac{sqrerr}{total\_ins\_num}}\end{aligned}\end{align} \]
其中,input 是样本预测结果, label 是样本真实标签,sqrerr 为平方误差和,total_ins_num 是样本总数。
参数:
sqrerr, (numpy.array|Tensor|string, required): 单机平方误差和统计值。
total_ins_num, (numpy.array|Tensor|string, required): 单机样本总数。
scope, (Scope, optional),作用域,若为None,则使用全局/默认作用域,默认为None。
util, (UtilBase, optinal),分布式训练工具类,若为None,则使用默认工具类 fleet.util, 默认为None。
用法示例:
... pred, label = model() # 1. 单机组网阶段,计算平方误差和样本总数 sqrerr = paddle.static.create_global_var(name="sqrerr", persistable=True, dtype='float32', shape=[1], value=0) total_cnt = paddle.static.create_global_var(name="total_cnt", persistable=True, dtype='float32', shape=[1], value=0) batch_cnt = paddle.sum( paddle.full(shape=[paddle.shape(label)[0], 1], fill_value=1.0)) batch_sqrerr = paddle.nn.functional.mse_loss(pred, label, reduction='sum') paddle.assign(sqrerr + batch_sqrerr, sqrerr) paddle.assign(total_cnt + batch_cnt, total_cnt) mse = sqrerr / total_cnt rmse = paddle.sqrt(mse) # 2. 分布式训练阶段,计算全局准确率。 global_rmse = fleet.metrics.rmse(sqrerr, total_cnt)
分布式Sum¶
-
paddle.distributed.fleet.metrics.
sum
(input, scope=None, util=None)¶
分布式求和。一般用于自定义指标计算。
参数:
input, (numpy.array|Tensor|string, required),需要分布式求和的输入参数。
scope, (Scope, optional),作用域,若为None,则使用全局/默认作用域,默认为None。
util, (UtilBase, optinal),分布式训练工具类,若为None,则使用默认工具类 fleet.util, 默认为None。
用法示例:
... # 1. 单机组网阶段,计算Loss loss = model() # 2. 分布式训练阶段,计算全局Loss和 loss_val, = exe.run(paddle.static.default_main_program(), fetch_list=[loss.name]) loss_sum = fleet.metrics.sum(loss_val)
分布式Max¶
-
paddle.distributed.fleet.metrics.
max
(input, scope=None, util=None)¶
分布式求最大值。一般用于自定义指标计算。
参数:
input, (numpy.array|Tensor|string, required),需要分布式求最大值的输入参数。
scope, (Scope, optional),作用域,若为None,则使用全局/默认作用域,默认为None。
util, (UtilBase, optinal),分布式训练工具类,若为None,则使用默认工具类 fleet.util, 默认为None。
用法示例:
... # 1. 单机组网阶段,计算Loss loss = model() # 2. 分布式训练阶段,计算全局最大Loss loss_val, = exe.run(paddle.static.default_main_program(), fetch_list=[loss.name]) max_loss = paddle.metrics.max(loss_val)
分布式Min¶
-
paddle.distributed.fleet.metrics.
min
(input, scope=None, util=None)¶
分布式求最小值。一般用于自定义指标计算。
参数:
input, (numpy.array|Tensor|string, required),需要分布式求最大值的输入参数。
scope, (Scope, optional),作用域,若为None,则使用全局/默认作用域,默认为None。
util, (UtilBase, optinal),分布式训练工具类,若为None,则使用默认工具类 fleet.util, 默认为None。
用法示例:
... # 1. 单机组网阶段 loss = model() # 2. 分布式训练阶段,计算全局最小Loss loss_val, = exe.run(paddle.static.default_main_program(), fetch_list=[loss.name]) min_loss = fleet.metrics.min(loss_val)
使用方法¶
完整运行示例见 examples/wide_and_deep。
通过fleetrun
指令运行分布式任务。命令示例如下,其中server_num
, worker_num
分别为服务节点和训练节点的数量。
fleetrun --server_num=2 --worker_num=2 train.py
分布式预测¶
简介¶
分布式预测任务将预测数据均匀分布式在多台机器上,每台机器仅预测整个数据集的一部分,节点之间通过 all_reduce 等集合通信操作完成各自预测结果的同步,从而获取整个数据集的预测结果。
为什么要做分布式预测,除了通过数据并行的方式节省预测时间外,另一个很重要的原因是,在某些场景,例如推荐系统或者搜索引擎中, 稀疏参数(embedding)的 feature id 可能会非常多,当 feature id 达到一定数量时,稀疏参数会变得很大以至于单机内存无法存放,从而导致无法预测。
原理¶
分布式预测的原理基本和分布式训练一致,都将节点分为 Worker 和 PServer 两类,这两类节点在训练任务和预测任务中的分工如下:
Worker:在训练时,Worker负责完成训练数据读取、从PServer上拉取稀疏参数然后进行前向网络计算、反向梯度计算等过程,并将计算出的梯度上传至PServer。在预测时,Worker负责完成预测数据读取、从PServer上拉取稀疏参数然后进行前向计算。所有Worker间可进行集合通信,从而获取全局的预测结果。
PServer:在训练时,PServer在收到训练Worker传来的梯度后,会根据指定的优化器完成更新参数,并将参数发送给训练Worker。在预测时,PServer仅作为稀疏参数存储器,响应预测Worker拉取稀疏参数的请求。
分布式预测任务的流程主要有以下三步:
自定义预测组网
初始化分布式集群环境,加载模型参数。
生成分布式预测组网,自定义reader,开始预测。
分布式预测功能主要通过 DistributedInfer 工具类完成,下面对相关API的功能和参数进行介绍。
-
class
paddle.distributed.fleet.utils.ps_util.
DistributedInfer
(main_program=None, startup_program=None)¶ PaddlePaddle的分布式预测工具类。
- 参数:
main_program(paddle.static.Program, optional),单机预测组网,若为None,则认为 paddle.static.default_main_program() 为单机预测组网。默认为None。
startup_program(paddle.static.Program, optional),单机预测初始化组网,若为None,则认为 paddle.static.default_startup_program() 为单机预测初始化组网。默认为None。
方法:
-
init_distributed_infer_env
(exe, loss, role_maker=None, dirname=None)¶ 初始化分布式集群环境,加载模型参数。需要注意,该接口仅在纯分布式预测的任务中才需要被调用,在先训练后预测的分布式一体任务里,此接口无需调用,且不会生效。
- 参数:
exe, (paddle.static.Executor, required),初始化分布式集群环境时需要用到的网络执行器。
loss, (Tensor, required), 预测网络 loss 变量。
role_maker, (RoleMakerBase, optional), 分布式训练(预测)任务环境配置,若为None,则框架会自动根据用户在环境变量中的配置进行分布式训练(预测)环境的初始化。默认为None。
dirname, (String, optional), 参数路径。若为None,则不加载参数。默认为None。
-
get_dist_infer_program():
生成分布式预测组网。相较于单机预测组网,两者区别仅在于:将稀疏参数查询操作替换为分布式稀疏参数查询操作,即将 lookup_table 算子替换为 distributed_lookup_table 。
- 返回:
Program,分布式预测组网。
使用方法¶
分布式预测常见的应用场景有以下两种,分布式训练+预测一体任务,及独立的分布式预测任务,两种任务的特点分别为:
分布式训练 + 预测一体任务:指分布式训练结束后,Worker节点不向PServer发送任务结束通知,而是继续开始预测。这类任务在进行预测时,分布式集群环境已经初始化好,且不需要进行参数加载。
分布式预测任务:指纯预测的分布式任务。这类任务在进行预测时,分布式集群环境还未初始化好,且往往需要进行参数加载。
下面分别介绍对这两种分布式预测任务的使用方法:
分布式训练 + 预测一体任务¶
...
model = WideDeepModel()
model.net(is_train=True)
if fleet.is_server():
fleet.init_server()
fleet.run_server()
else:
exe.run(paddle.default_startup_program())
fleet.init_worker()
# 分布式训练
distributed_training(exe, model)
# 1. 生成单机预测组网
test_main_program = paddle.static.Program()
test_startup_program = paddle.static.Program()
with paddle.static.program_guard(main_program=test_main_program, startup_program=test_startup_program):
with paddle.utils.unique_name.guard():
model.net(is_train=False)
# 2. 生成分布式预测组网,定义reader,进行预测
dist_infer = DistributedInfer(main_program=test_main_program, startup_program=test_startup_program)
dist_infer_program = dist_infer.get_dist_infer_program()
test_data = WideDeepDataset(data_path="./data")
reader = model.loader.set_sample_generator(test_data, batch_size=batch_size, drop_last=True, places=place)
reader.start()
batch_idx = 0
try:
while True:
loss_val = exe.run(program=dist_infer_program,
fetch_list=[model.cost.name])
if batch_idx % 10 == 0:
loss_val = np.mean(loss_val)
message = "TEST ---> batch_idx: {} loss: {}\n".format(batch_idx, loss_val)
except fluid.core.EOFException:
reader.reset()
fleet.stop_worker()
分布式预测任务¶
...
# 1. 定义单机预测组网
model = WideDeepModel()
model.net(is_train=False)
# 2. 初始化分布式预测环境,加载模型参数
dist_infer = DistributedInfer(main_program=test_main_program, startup_program=test_startup_program)
exe = paddle.static.Executor()
dirname = "./init_params/"
dist_infer.init_distributed_infer_env(exe, model.cost, dirname=dirname)
# 3.生成分布式预测组网,定义reader,进行预测
if fleet.is_worker():
dist_infer_program = dist_infer.get_dist_infer_program()
test_data = WideDeepDataset(data_path="./data")
reader = model.loader.set_sample_generator(test_data, batch_size=batch_size, drop_last=True, places=place)
reader.start()
batch_idx = 0
try:
while True:
loss_val = exe.run(program=dist_infer_program,
fetch_list=[model.cost.name])
if batch_idx % 10 == 0:
loss_val = np.mean(loss_val)
message = "TEST ---> batch_idx: {} loss: {}\n".format(batch_idx, loss_val)
print(message)
except fluid.core.EOFException:
reader.reset()
fleet.stop_worker()
运行方法¶
完整运行示例见 examples/wide_and_deep。该示例为分布式训练 + 预测一体任务。
配置完成后,通过fleetrun
指令运行分布式任务。命令示例如下,其中server_num
, worker_num
分别为服务节点和训练节点的数量。
fleetrun --server_num=2 --worker_num=2 train.py
启动分布式任务¶
飞桨通过paddle.distributed.launch
组件启动分布式任务。该组件可用于启动单机多卡分布式任务,也可以用于启动多机多卡分布式任务。该组件为每张参与分布式任务的训练卡启动一个训练进程。默认情形下,该组件将在每个节点上启动N
个进程,这里N
等于训练节点的卡数,即使用所有的训练卡。用户也可以通过gpus
参数指定训练节点上使用的训练卡列表,该列表以逗号分隔。需要注意的是,所有节点需要使用相同数量的训练卡数。
为了启动多机分布式任务,需要通过ips
参数指定所有节点的IP地址列表,该列表以逗号分隔。需要注意的是,该列表在所有节点上需要保持一致,即各节点IP地址出现的顺序需要保持一致。
例如,可以通过下面的命令启动单机多卡分布式任务,假设节点包含8张GPU卡:
python -m paddle.distributed.launch --gpus 0,1,2,3,4,5,6,7 train.py --batch_size=64
其中,train.py
为用户训练脚本,后面可进一步增加脚本参数,如batch size等。用户也可以只使用部分卡进行训练。例如,下面的例子中,仅使用2、3两张卡进行训练:
python -m paddle.distributed.launch --gpus 2,3 train.py --batch_size=64
从单机到多机分布式任务,只需额外指定ips
参数即可,其内容为多机的IP列表。假设两台机器的IP地址分别为192.168.0.1和192.168.0.2,那么在这两个节点上启动多机分布式任务的命令如下所示:
# 第一个节点
python -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --ips="192.168.0.1,192.168.0.2" train.py --batch_size=64
# 第二个节点
python -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" --ips="192.168.0.1,192.168.0.2" train.py --batch_size=64
需要注意的是,两个节点上ips
列表的顺序需要保持一致。用户也可使用gpus
参数指定每个节点上只使用部分训练卡,命令如下所示:
# 第一个节点
python -m paddle.distributed.launch --gpus=0,1,2,3 --ips="192.168.0.1,192.168.0.2" train.py --batch_size=64
# 第二个节点
python -m paddle.distributed.launch --gpus=0,1,2,3 --ips="192.168.0.1,192.168.0.2" train.py --batch_size=64
两个节点上可以使用不同的训练卡进行训练,但需要使用相同数量的训练卡。例如,第一个节点使用0、1两张卡,第二个节点使用2、3两张卡,启动命令如下所示:
# 第一个节点
python -m paddle.distributed.launch --gpus=0,1 --ips="192.168.0.1,192.168.0.2" train.py --batch_size=64
# 第二个节点
python -m paddle.distributed.launch --gpus=2,3 --ips="192.168.0.1,192.168.0.2" train.py --batch_size=64
下面将介绍paddle.distributed.launch
组件在不同场景下的详细使用方法。
Collective分布式任务¶
Collective分布式任务场景下,paddle.distributed.launch
组件支持以下参数:
usage: launch.py [-h] [--log_dir LOG_DIR]
[--run_mode RUN_MODE] [--gpus GPUS] [--ips IPS]
training_script ...
启动分布式任务
optional arguments:
-h, --help 给出该帮助信息并退出
Base Parameters:
--log_dir LOG_DIR 训练日志的保存目录,默认:--log_dir=log/
--run_mode RUN_MODE 任务运行模式, 可以为以下值: collective/ps/ps-heter;
当为collective模式时可省略。
--gpus GPUS 训练使用的卡列表,以逗号分隔。例如: --gpus="4,5,6,7"
将使用节点上的4,5,6,7四张卡执行任务,并分别为每张卡
启动一个任务进程。
training_script 用户的任务脚本,其后为该任务脚本的参数。
training_script_args 用户任务脚本的参数
Collective Parameters:
--ips IPS 参与分布式任务的节点IP地址列表,以逗号分隔,例如:
192.168.0.16,192.168.0.17
各个参数的含义如下:
log_dir:训练日志储存目录,默认为
./log
目录。该目录下包含endpoints.log
文件和各个卡的训练日志workerlog.x
(如workerlog.0,wokerlog.1等),其中endpoints.log
文件记录各个训练进程的IP地址和端口号。run_mode:运行模式,如collecitve,ps(parameter-server)或者ps-heter。
gpus:每个节点上使用的gpu卡的列表,以逗号间隔。例如
--gpus="0,1,2,3"
。需要注意:这里的指定的卡号为物理卡号,而不是逻辑卡号。training_script:训练脚本,如
train.py
。training_script_args:训练脚本的参数,如batch size和学习率等。
ips:所有训练节点的IP地址列表,以逗号间隔。例如,
--ips="192.168.0.1,192.168.0.2
。需要注意的是,该列表在所有节点上需要保持一致,即各节点IP地址出现的顺序在所有节点的任务脚本中需要保持一致。
通过paddle.distributed.launch
组件启动分布式任务,将在控制台显示第一张训练卡对应的日志信息,并将所有的日志信息保存到log_dir
参数指定的目录中;每张训练卡的日志对应一个日志文件,形式如workerlog.x
。
PaddleCloud平台¶
当在百度内部PaddleCloud平台使用飞桨分布式时,可以省略ips
参数。假设使用两台机器执行分布式任务,则命令行如下所示:
# 第一台机器:
python -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" train.py
# 第二台机器:
python -m paddle.distributed.launch --gpus="0,1,2,3,4,5,6,7" train.py
更多关于如何通过在PaddleCloud上启动分布式任务,请参考PaddleCloud官方文档。
物理机或docker环境启动分布式任务¶
我们以下面的场景为例说明如何在物理机环境或者docker环境中启动飞桨分布式任务。假设我们有两台机器,每台机器包含4张GPU卡。两台机器的IP地址分别为192.168.0.1和192.168.0.2。该IP地址可以为两台物理机的IP地址,也可以为两台机器内部Docker容器的IP地址。

为了在两台机器上启动分布式任务,首先需要确保两台机器间的网络是互通的。可以通过ping
命令验证两台机器间的网络互通性,如下所示:
# 第一个节点
ping 192.168.0.2
# 第二个节点
ping 192.168.0.1
如果两台机器间的网络无法连通,请联系您的网络管理员获取帮助。
假设用户的训练脚本为train.py
,则可以通过如下命令在两台机器上启动分布式训练任务:
# 第一台机器:192.168.0.1
python -m paddle.distributed.launch --gpus="0,1,2,3" --ips="192.168.0.1,192.168.0.2" train.py
# 第二台机器:192.168.0.2
python -m paddle.distributed.launch --gpus="0,1,2,3" --ips="192.168.0.1,192.168.0.2" train.py
当每台机器均使用所有4张训练卡时,也可以省略gpus
参数,如下所示:
# 第一台机器:192.168.0.1
python -m paddle.distributed.launch --ips="192.168.0.1,192.168.0.2" train.py
# 第二台机器:192.168.0.2
python -m paddle.distributed.launch --ips="192.168.0.1,192.168.0.2" train.py
用户也可以通过gpus
参数指定只使用部分训练卡,例如只使用0、1两张卡:
# 第一台机器:192.168.0.1
python -m paddle.distributed.launch --gpus="0,1" --ips="192.168.0.1,192.168.0.2" train.py
# 第二台机器:192.168.0.2
python -m paddle.distributed.launch --gpus="0,1" --ips="192.168.0.1,192.168.0.2" train.py
通过paddle.distributed.launch
组件启动分布式任务时,该组件将为gpus
参数指定的每张训练卡启动一个训练进程。为了实现进程间通信,该组件同时为每个进程绑定一个端口号,进程的IP地址和端口号成为该进程的网络地址。paddle.distributed.launch
组件随机查找机器上的可用端口,作为训练进程的端口号。假设,Node 0上4个训练进程的端口号分别为3128、5762、6213和6170,则该机器上4个训练进程的网络地址分别为: 192.168.0.1:3128
、192.168.0.1:5762
、192.168.0.1:6213
和192.168.0.1:6170
。当paddle.distributed.launch
组件无法获取足够的可用端口时,任务启动失败。
日志信息说明¶
首先,我们介绍一些基本概念。我们使用world_size
或nranks
(number of ranks)表示分布式任务使用的卡的总数,使用N
表示每台机器上使用的卡数,使用M
表示分布式任务使用的总机器数;那么,\(world\_size=N*M\)。按照机器在ips
参数中出现的顺序,每台机器被赋予一个节点id:M_id
,这里\(0<=M\_id<M\)。例如,假设,ips
参数为”192.168.0.1,192.168.0.2”,那么以192.168.0.1为IP地址的机器在ips
参数列表的索引为0,故其M_id
为0。同理,以192.168.0.2为IP地址的机器在ips
参数列表的索引为1,故其M_id
为1。同样的,我们根据每台机器上训练卡在gpus
参数列表出现的顺序为其赋予一个卡id:N_id
,这里\(0<=N\_id<N\)。例如,假设gpus
参数为”2,3”,那么卡2的N_id
为0,卡3的N_id
为1。我们也可以将N_id
称为local_rank
。我们为每张训练卡赋予唯一的标识:rank
,这里\(0<=rank<world\_size\)。一般来讲,我们可以通过如下的公式计算每张卡的rank
值。
这里,需要注意local_rank
和rank
的区别:local_rank
是局部的,在同一机器内部是唯一的,但是不同机器上的卡可以具有相同的local_rank
;而rank
是全局唯一的,同一任务中所有的卡具有不同的rank
值。
通过paddle.distributed.launch
组件启动分布式任务时,将在终端打印rank
值为0的卡对应的训练日志信息,而其余所有卡对应的训练日志信息保存在log_dir
指定的目录中。该目录下存在两类文件:endpoints.log和workerlog.id,这里id表示卡的rank
值,如workerlog.0
、workerlog.1
等。需要注意的是,日志目录中只会保存该机器上所有卡的训练日志,而不会保存其它机器上卡的训练日志。因此,需要登录到对应机器上,以查看相应卡的训练日志。
其中,endpoints.log中记录所有训练进程的网络地址,示例如下:
PADDLE_TRAINER_ENDPOINTS:
192.168.0.1:3128
192.168.0.1:5762
192.168.0.1:6213
192.168.0.1:6170
192.168.0.2:4215
192.168.0.2:2213
192.168.0.2:3211
192.168.0.2:5231
需要说明的是,当多次启动分布式任务时,训练是以追加的方式追加到日志文件中的。因此,在查看日志信息时,请注意查看相应任务对应的日志信息。一般情况下,可以直接跳转到文件末尾,以查看最近任务的日志信息。在调试时,为了避免信息干扰,一种方法是在每次启动分布式任务前清空日志目录。
报错信息说明¶
这里,我们对分布式任务中常见的一类报错信息进行说明,方便用户快速定位错误信息。
一般在用户分布式任务出错时,控制台会输出如下信息:
"ABORT!!! Out of all 8 trainers, the trainer process with rank=[2,3] was aborted. Please check its log.".
上述信息给出分布式任务的world_size
为8,其中rank
值为2和3的进程终止。因此,通过上述信息,用户可以快速判断出错的进程,并查看相应的训练日志获取更多错误信息。例如,可以直接查看workerlog.2
和workerlog.3
两个进程的错误日志,获取更多错误信息。
当训练日志包含如下信息时,通常表明其它训练进程出错,导致当前训练进程被中断。也就是说,用户需要查看其它训练进程的日志信息获取更多任务失败原因。
[SignalInfo: *** SIGTERM (@0x3fe) received by PID 1164 (TID 0x7f6cf1fc6700) from PID 1022 ***]
例如,某用户在排查报错信息时,发现workerlog.0
日志中存在上述信息。进一步查看其它进程的日志信息,最终在workerlog.4
中发现如下报错信息,进而定位出错原因是数据读取出错。
2021-11-03 05:08:55,091 - ERROR - DataLoader reader thread raised an exception!
Error: [Errno 5] Input/output error
Traceback (most recent call last):
File "/root/paddlejob/workspace/env_run/reader.py", line 218, in __next__
data = next(self, loader)
File "/usr/local/lib/python3.7/site-packages/paddle/fluid/dataloader/dataloader_iter.py", line 779, in __next__
data = self._reader.read_next_var_list()
SystemError: (Fatal) Blocking queue is killed because the data reader raises an exception.
[Hint: Expected killed_ != true, but received killed_:1 == true:1.] (at /paddle/paddle/fluid/operators/reader/blocking_queue.h:158)
ParameterServer分布式任务¶
ParameterServer相关参数如下:
--servers: 多机分布式任务中,指定参数服务器服务节点的IP和端口,例如 --servers="192.168.0.16:6170,192.168.0.17:6170"。
--workers: 多机分布式任务中,指定参数服务器训练节点的IP和端口,也可只指定IP,例如 --workers="192.168.0.16:6171,192.168.0.16:6172,192.168.0.17:6171,192.168.0.17:6172"。
--heter_workers: 在异构集群中启动分布式任务,指定参数服务器异构训练节点的IP和端口,例如 --heter_workers="192.168.0.16:6172,192.168.0.17:6172"。
--worker_num: 单机模拟分布式任务中,指定参数服务器训练节点的个数。
--server_num: 单机模拟分布式任务中,指定参数服务器服务节点的个数。
--heter_worker_num: 在异构集群中启动单机模拟分布式任务, 指定参数服务器异构训练节点的个数。
--http_port: 参数服务器模式中,用 Gloo 启动时设置的连接端口。
Elastic 参数¶
--elastic_server: etcd 服务地址 host:port,例如 --elastic_server=127.0.0.1:2379。
--job_id: 任务唯一 ID,例如 --job_id=job1。
--np: 任务 pod/node 编号,例如 --np=2。
--host: 绑定的主机,默认等于 POD_IP 环境变量。
服务型弹性蒸馏训练¶
简介¶
蒸馏训练¶
在很多场景下,模型越大,层数越多,模型效果就越好。但受限于推理速度,显存资源等要求,大模型通常无法直接部署,需要对模型进行压缩。知识蒸馏是一种经典的模型压缩技术,由《Distilling the Knowledge in a Neural Network》 在2015年第一次提出,是将知识从一个复杂模型(Teacher)迁移到另一个轻量级模型(Student)上的方式来实现模型压缩。
训练步骤可以分为两步:
训练好一个Teacher模型。
使用Teacher模型的知识来训练Student模型。 所谓Teacher模型的知识是指Teacher模型的推理结果,我们称之为soft label,这个soft label将作为Student网络的训练目标,Student的推理结果需要尽可能接近Teacher的推理结果。
服务型蒸馏训练¶
服务型蒸和其他常见蒸馏方式的对比:
离线蒸馏训练: 先使用Teacher模型做推理并将结果保存在磁盘中,然后Student模型使用磁盘中保存的样本和Teacher模型的推理结果作为数据集进行训练。这种训练方式一般需要数据增强,而且需要占用巨大的磁盘空间,因此应用环境受到了一定的限制。
常规蒸馏训练: 常规蒸馏训练是指将 Teacher 模型和 Student 模型放入同一网络中,固定 Teacher 模型参数只做前向,Student 模型则正常做反向传播训练。这也是目前主流的蒸馏训练方式, 单这种方式下 Student 模型的训练完全依赖 Teacher 模型,Student 模型要等 Teacher 模型输出一个 batch 的推理结果才可以训练,而 teacher 模型也要等 Student 训练完一个 batch,才能开始下一个 batch 的推理,对整体的训练速度有一定的影响。
服务型蒸馏训练: 是基于 Elastic Deep Learning 提出的一种训练方案。它将Teacher模型和Student模型解耦,Teacher模型被部署为线上推理服务,Student模型则以客户端的身份通过互联网实时发送样本到Teacher模型获取推理结果进行训练
服务型蒸馏训练收益¶
节约显存资源: 由于Student模型和Teacher模型的解耦,所以服务型蒸馏训练可以使用异构的资源,也就是把Student模型和Teacher模型的部署到不同的设备上。原先受限于显存大小而难以部署到单个GPU卡上的蒸馏网络可以通过该方式部署到不同卡上。
提升训练速度:由于节约了显存资源,这样就可以使Student模型能够训练更大的batch size;同时由于Student模型和Teacher模型是异构流水线,Student模型不用等Teacher模型推理结束后再训练。
提高训练资源利用率:我们可以将Teacher模型部署到线上的弹性预估卡集群,利用线上预估卡闲时的算力资源提升蒸馏任务中Teacher模型侧的吞吐量。同时由于Teacher模型可以弹性调度,不用担心高峰时线上实例被抢占造成的任务失败。还可以将Teacher模型部署到集群碎片资源,或者如k40等使用率较低的资源上,充分利用集群的空闲、碎片资源。
提升训练效率:用户可以根据Teacher和Student的吞吐性能灵活设置Teacher和Student的比例,也就是说多个老师可以教多个学生,而不是只能保持1比1的家教模式,最大限度地提高训练的产出。
EDL 服务型弹性蒸馏效果¶
ResNet50_vd模型, ImageNet 数据集
服务型弹性蒸馏¶
DistillReader¶
服务型弹性蒸馏的核心是将Teacher模型部署成了服务端,而Student模型成了客户端。
将Teacher模型被部署为在线可容错弹性服务, 在Student模型一侧则通过
DistillReader
来封装Student模型与Teacher模型之间的通信,访问Teacher服务。

DistillReader 产生可供Student模型训练的数据reader。如上图所示,Student模型将训练样本和标签传入训练reader,DistillReader从训练reader中读取训练样本发送给Teacher模型,然后获取推理结果。 DistillReader 的结构如下图。
推理结果和原训练reader中的数据封装在一起,返回一个包含推理结果的新reader给Student模型,这样Teacher 模型的推理和Student 模型的训练就可以流水行并行起来了。

可容错弹性服务¶
可容错弹性服务的实现架构如下图所示,首先我们通过Paddle Serving将多个Teacher模型部署成服务,并注册服务到Redis数据库中;Student模型则作为客户端从服务发现中查询所需的Teacher服务;服务发现从Redis数据库查询并按某种负载均衡策略返回客户端所需的Teacher列表;每当Teacher变化时,客户端就可以实时拿到最新Teacher列表,连接Teacher进行蒸馏训练,不用担心发生由于连接到被收回的Teacher资源而导致任务失败的请况。
Student 模型给Teacher 模型发送样本并获取推理结果,而Teacher 模型服务端则可以随意增删,弹性调整。

快速开始¶
下文通过训练图像分类模型来简单介绍服务型蒸馏训练的使用。
为简单起见,使用的是单机环境,服务端和客户端部署在了同一个服务器上,服务端的IP地址是127.0.0.1。如果部署在不同设备上,修改下代码中的IP地址即可。
环境准备¶
下命令拉取镜像,镜像为CUDA9.0的环境,在里面我们预装了EDL、飞桨核心框架和Padde Serving等相关依赖。
docker pull hub.baidubce.com/paddle-edl/paddle_edl:latest-cuda9.0-cudnn7
nvidia-docker run -name paddle_edl hub.baidubce.com/paddle-edl/paddle_edl:latest-cuda9.0-cudnn7 /bin/bash
启动Teacher模型¶
如下命令在1号GPU卡启动Teacher服务,其中Teacher模型为图像分类模型ResNeXt101_32x16d_wsl,服务的端口号为9898,并启动了内存优化功能。
cd example/distill/resnet
wget --no-check-certificate https://paddle-edl.bj.bcebos.com/distill_teacher_model/ResNeXt101_32x16d_wsl_model.tar.gz
tar -zxf ResNeXt101_32x16d_wsl_model.tar.gz
python -m paddle_serving_server_gpu.serve \
--model ResNeXt101_32x16d_wsl_model \
--mem_optim True \
--port 9898 \
--gpu_ids 1
启动Student模型训练¶
如下命令在0号GPU卡启动Student模型,启动的student模型为ResNet50_vd。 其中train_with_fleet.py是用于启动训练的脚本,用户需要在其中添加蒸馏训练相关的代码,如果用户想了解脚本的修改方法或可以参考如github。
python -m paddle.distributed.launch --gpus 0 \
./train_with_fleet.py \
--model=ResNet50_vd \
--data_dir=./ImageNet \
--use_distill_service=True \
--distill_teachers=127.0.0.1:9898
推荐阅读:¶
弹性训练¶
概述¶
在分布式训练中,单节点故障导致这个任务失败的情况时有发生,尤其是节点较多的场景,不仅出错概率增加,重新运行任务的代价也相对更高。 针对这样的场景,弹性训练的需求应运而生。
paddle 目前已支持 Collective 训练模式基于热重启的容错方案。
热重启即用户的任务进程会被重启,所以需要用户代码中做好 checkpoint 逻辑。
容错¶
方案概述¶
当前方案的容错是 pod 级别,即组成分布式网络节点为单位,该节点可能管理多卡,当该节点中出现故障时,该节点所有资源释放,需要重启。
基本设计思路包括以下几点:
使用中心化的外部服务 etcd 进行节点数据同步和异常节点的感知,同时无需调度平台收集所有节点信息进行配置;
当故障节点故障退出后,需要调度平台将新节点以同样配置(可能异地)重启后进入恢复训练阶段;
故障节点退出后,非故障节点感知到即进入无限等待模式,退出和超时由外部调度系统负责;
训练的恢复依赖在用户代码中设置的 checkpoint 完成;
使用方法¶
推荐通过 paddle-operator 使用该功能, 在提交任务时指定需要开启弹性功能,
apiVersion: batch.paddlepaddle.org/v1
kind: PaddleJob
metadata:
name: job-elastic
spec:
elastic: 1
...
详见 kubernetes 部署 .
以下通过 resnet 示例介绍使用方法:
需要先安装 etcd server,以获得服务地址和端口如 127.0.0.1:2379
用户程序运行环境中需要安装 etcd 客户端 etcd3
pip install --no-cache-dir -U etcd3 -i https://mirror.baidu.com/pypi/simple
在用户程序中添加 checkpoint 相关逻辑,主要部分如下
load 环节
resnet = ResNet(class_dim=class_dim, layers=50)
if int(fleet.local_rank()) == 0 and checkpoint_path and os.path.isfile(checkpoint_path):
try:
chkpt = paddle.load(checkpoint_path)
resnet.set_state_dict(chkpt)
start_epoch = chkpt.get('epoch',0)
print("load checkpoint succuss")
except Exception as e:
print("load checkpoint failed", e)
save 环节
if int(fleet.local_rank()) == 0:
state_dict = resnet.state_dict()
state_dict['epoch'] = eop
paddle.save(state_dict, checkpoint_path)
完整示例见 resnet .
注意:如示例中所示,save 和 load 均在 rank 为 0 的节点上进行,checkpoint 所在目录要确保能够被访问。
用户程序入口需要使用 python -m paddle.distributed.launch 启动,相关参数可通过环境变量或者启动参数提供
在环境中添加以下变量
PADDLE_ELASTIC_SERVER = 127.0.0.1:2379 # etcd 服务地址
PADDLE_ELASTIC_FAULT_TOLERANC_LEVEL = 1 # 启用容错等级
PADDLE_ELASTIC_JOB_ID = 'XXXXXX' # 任务唯一id
PADDLE_ELASTIC_NP = 2 # 本次任务节点数
POD_IP = 10.10.10.1 # 本节点地址,一般由调度系统指定,或不配置由程序获取
或者使用以下命令运行 (注意需要在 np 个节点上都运行该命令),
python -m paddle.distributed.launch --elastic_server=127.0.0.1:2379 --np=2 --job_id=XXXXXX train_fleet_dygraph_ckpt.py
弹性¶
概述¶
在分布式训练中,除了容错外,集群的资源剩余情况可能随时间而不同、任务的优先级也可能有不同, 基于这样的场景,实现弹性训练即任务可以在运行时动态调整训练资源而不影响或尽可能小地影响训练进程,能够最大限度地实现资源利用率提升同时提升训练任务质量。
paddle 目前已支持 Collective 训练模式基于热重启的弹性训练方案。
热重启即用户的任务进程会被重启,所以需要用户代码中做好 checkpoint 逻辑,同时如 batchsize 和 learning rate 这样需要随节点数变化的参数也需要用户进程自动调整。
方案概述¶
本方案以上述容错为基础,分为扩容和缩容两种情况,流程如下:
正常启动训练;
为训练任务配置可变节点数,例如np=2:4,表示任务最小需要2个节点,最大需要4个节点,平台调度系统可以在这个区间内为任务分配计算节点;
训练过程中,若为任务扩容,当节点加入满足所需训练节点要求时,各节点感知判断满足训练条件,继续训练; 若为任务缩容,当节点剩余节点满足所需训练节点要求时,各节点感知判断满足训练条件,继续训练;
> 这里的节点加入和退出需要外部调度系统负责实现。
使用方法¶
推荐通过 paddle-operator 使用该功能,首先在提交任务中开启弹性功能,然后任务正常运行中通过 kubectl 或 api 的其他方式修改 paddlejob 中的 replicas 字段即可实现改功能。 详见 kubernetes 部署 .
以下通过 resnet 示例介绍使用弹性的方法:
运行任务 (注意需要在 np 个节点上都运行该命令),
python -m paddle.distributed.launch --elastic_server=127.0.0.1:2379 --np=2:4 --job_id=XXXXXX train_fleet_dygraph_ckpt.py
执行扩容或缩容
通过k8s进行扩缩容操作,下面的命令是执行扩容操作,由2个节点扩容到3个节点(缩容也类似),等待超时时间可由PADDLE_ELASTIC_TIMEOUT(默认值是120秒)环境变量控制
kubectl scale --current-replicas=2 --replicas=3 paddlejob/paddle-controller-manager-698dd7b855-n65jr
公有云配置¶
公有云上有两类产品可以方便的运行 PaddlePaddle,一是基于 kubernetes 的云原生容器引擎,例如百度云CCE产品、阿里云ACK产品、华为云CCE产品等;二是各个云厂商的AI开发平台,例如百度云BML平台、华为云ModelArts平台、阿里云PAI平台。
1、在基于 kubernetes 的云原生容器引擎产品上使用 PaddlePaddle¶
在公有云上运行 PaddlePaddle 分布式可以通过选购容器引擎服务的方式,各大云厂商都推出了基于标准 kubernetes 的云产品,
云厂商 |
容器引擎 |
链接 |
百度云 |
CCE |
|
阿里云 |
ACK |
|
华为云 |
CCE |
使用流程:
购买服务,包括节点及 cpu 或 gpu 计算资源;
2、在云厂商AI开发平台产品上使用 PaddlePaddle¶
2.1、百度公有云BML平台¶
百度BML全功能AI开发平台,为企业及个人开发者提供机器学习和深度学习一站式AI开发服务,BML平台上预制了对PaddlePaddle的支持,发起PaddlePaddle分布式训练任务比较简单,可开箱即用。
1、模型训练-自定义作业¶

2.2、华为云ModelArts平台¶
ModelArts是面向开发者的一站式AI开发平台,我们采用 ModelArts使用自定义镜像创建训练作业 方式在ModelArts上使用PaddlePaddle
制作PaddlePaddle Docker镜像¶
准备dockerfile
run_train.sh
#!/bin/bash
# Initialize environment
DLS_USER_HOME_DIR="$( cd "$(dirname "$0")" ; pwd -P )"
cd "$DLS_USER_HOME_DIR"
DLS_USER_JOB_DIR="$DLS_USER_HOME_DIR/user-job-dir"
export PYTHONPATH="$DLS_USER_JOB_DIR:$PYTHONPATH"
export PYTHONUNBUFFERED=1
# Get job/task-related information from environmental variables
source utils.sh
dls_fix_dns
unset_job_env_except_self "$DLS_JOB_ID"
decrypt_dls_aes_env
app_url="$DLS_APP_URL"
log_url="/tmp/dls-task-$DLS_TASK_INDEX.log"
echo "user: `id`"
echo "pwd: $PWD"
echo "app_url: $app_url"
echo "log_url: $log_url"
echo "command:" "$@"
# Launch process (task)
mkdir -p "$DLS_USER_JOB_DIR" && cd "$DLS_USER_JOB_DIR"
dls_create_log "$log_url"
tail -F "$log_url" &
TAIL_PID=$!
DLS_DOWNLOADER_LOG_FILE=/tmp/dls-downloader.log
dls_get_app "$app_url" 2>&1 | tee "$DLS_DOWNLOADER_LOG_FILE"
if [ "${PIPESTATUS[0]}" = "0" ]
then
stdbuf -oL -eL "$@" 2>&1 | dls_logger "$log_url"
RET_CODE=${PIPESTATUS[0]}
else
(echo "App download error: "; cat "$DLS_DOWNLOADER_LOG_FILE") | dls_logger "$log_url"
RET_CODE=127
fi
if [ ! -z "$DLS_USE_UPLOADER" ] && [ "$DLS_USE_UPLOADER" != "0" ]
then
dls_upload_log "$log_url" "$DLS_UPLOAD_LOG_OBS_DIR" 2>&1 | tee -a "$DLS_DOWNLOADER_LOG_FILE"
if [ "${PIPESTATUS[0]}" != "0" ]
then
(echo "Log upload error: "; cat "$DLS_DOWNLOADER_LOG_FILE") | dls_logger "$log_url" "append"
fi
fi
sleep 3
kill $TAIL_PID
exit $RET_CODE
来源:https://github.com/huaweicloud/ModelArts-Lab/blob/master/docs/custom_image/mnist/run_train.sh
Dockerfile
# modelarts提供了各种类型的基础镜像,详细:https://support.huaweicloud.com/engineers-modelarts/modelarts_23_0217.html#modelarts_23_0217__section1126616610513,请根据需要按需选择基础镜像,该示例中选择的是GPU镜像
FROM swr.cn-north-1.myhuaweicloud.com/modelarts-job-dev-image/custom-base-cuda10.0-cp36-ubuntu18.04-x86:1.1
COPY --chown=work:work run_train.sh /home/work/
# 安装PaddlePaddle,详细:https://www.paddlepaddle.org.cn/,该示例选择的是PaddlePaddle 2.0.1\Linux\pip\GPU版本
RUN python -m pip install paddlepaddle-gpu==2.0.1.post100 -f https://paddlepaddle.org.cn/whl/mkl/stable.html
构建docker镜像
docker build -f Dockerfile . -t swr.cn-north-4.myhuaweicloud.com/deep-learning-diy/paddle-gpu-cuda10-2.0.1:latest
将docker镜像推入镜像仓库
docker push swr.cn-north-4.myhuaweicloud.com/deep-learning-diy/paddle-gpu-cuda10-2.0.1:latest
准备运行脚本(Collective模式)¶
运行脚本
run.sh
if [[ ${DLS_TASK_NUMBER} == 1 ]]; then
# 单机
config="--selected_gpus=0,1,2,3,4,5,6,7 --log_dir mylog"
python -m paddle.distributed.launch ${config} train.py
else
# 分布式
node_host_str=""
for i in $(seq 0 $[DLS_TASK_NUMBER-1])
do
env_key=BATCH_CUSTOM${i}_HOSTS
if [[ $i == $[DLS_TASK_NUMBER-1] ]]; then
node_host_str="${node_host_str}$(eval echo '$'$env_key)"
else
node_host_str="${node_host_str}$(eval echo '$'$env_key),"
fi
done
node_hosts=${node_host_str}
node_ip=${BATCH_CURRENT_HOST}
python -m paddle.distributed.launch \
--cluster_node_ips=${node_hosts} \
--node_ip=${node_ip} \
--started_port=${BATCH_CURRENT_PORT} \
--selected_gpus=0,1,2,3,4,5,6,7 \
train_with_fleet.py
fi
组网代码
train_with_fleet.py
# -*- coding: utf-8 -*-
import os
import numpy as np
import paddle.fluid as fluid
# 区别1: 导入分布式训练库
from paddle.fluid.incubate.fleet.collective import fleet, DistributedStrategy
from paddle.fluid.incubate.fleet.base import role_maker
# 定义网络
def mlp(input_x, input_y, hid_dim=1280, label_dim=2):
fc_1 = fluid.layers.fc(input=input_x, size=hid_dim, act='tanh')
fc_2 = fluid.layers.fc(input=fc_1, size=hid_dim, act='tanh')
prediction = fluid.layers.fc(input=[fc_2], size=label_dim, act='softmax')
cost = fluid.layers.cross_entropy(input=prediction, label=input_y)
avg_cost = fluid.layers.mean(x=cost)
return avg_cost
# 生成数据集
def gen_data():
return {"x": np.random.random(size=(128, 32)).astype('float32'),
"y": np.random.randint(2, size=(128, 1)).astype('int64')}
input_x = fluid.layers.data(name="x", shape=[32], dtype='float32')
input_y = fluid.layers.data(name="y", shape=[1], dtype='int64')
# 定义损失
cost = mlp(input_x, input_y)
optimizer = fluid.optimizer.SGD(learning_rate=0.01)
# 区别2: 定义训练策略和集群环境定义
dist_strategy = DistributedStrategy()
role = role_maker.PaddleCloudRoleMaker(is_collective=True)
fleet.init(role)
# 区别3: 对optimizer封装,并调用封装后的minimize方法
optimizer = fleet.distributed_optimizer(optimizer, strategy=DistributedStrategy())
optimizer.minimize(cost, fluid.default_startup_program())
train_prog = fleet.main_program
# 获得当前gpu的id号
gpu_id = int(os.getenv("FLAGS_selected_gpus", "0"))
print(gpu_id)
place = fluid.CUDAPlace(gpu_id)
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())
step = 100
for i in range(step):
cost_val = exe.run(program=train_prog, feed=gen_data(), fetch_list=[cost.name])
print("step%d cost=%f" % (i, cost_val[0]))
# 区别4: 模型保存
model_path = "./"
if os.path.exists(model_path):
fleet.save_persistables(exe, model_path)
提交分布式训练任务¶
在ModelArts上提交PaddlePaddle任务:

如上图所示,需要填入下列信息:
镜像地址:swr.cn-north-4.myhuaweicloud.com/deep-learning-diy/paddle-gpu-cuda10-2.0.1:latest
代码目录:从obs选择运行脚本和组网代码,提交PaddlePaddle任务前需要将运行脚本和组网代码上传到obs,然后才能从obs选择代码目录。
启动命令:bash /home/work/run_train.sh python /home/work/user-job-dir/run.sh, 注意:需要完整的复制该命令
选择数据集:数据集需要事先导入到ModelArts数据集中或者上传到Obs存储里,当前示例中使用的是自行构造的数据,无需上传数据集,选择一个obs空目录即可。
2.3、阿里云PAI平台¶
由于阿里云PAI平台不支持自定义的方式来提交训练任务,目前 PaddlePaddle 还无法在阿里云PAI平台上运行。
Kubernetes 部署¶
概述¶
在 kubernetes 上部署分布式任务需要安装 paddle-operator 。 paddle-operator 通过添加自定义资源类型 (paddlejob) 以及部署 controller 和一系列 kubernetes 原生组件的方式实现简单定义即可运行 paddle 任务的需求。
目前支持运行 ParameterServer (PS) 和 Collective 两种分布式任务,当然也支持运行单节点任务。
paddle-operator 安装¶
准备¶
安装 paddle-operator 需要有已经安装的 kubernetes (v1.8+) 集群和 kubectl (v1.8+) 工具。
本节所需配置文件和示例可以在 这里 找到, 可以通过 git clone 或者复制文件内容保存。
deploy
|-- examples
| |-- resnet.yaml
| |-- wide_and_deep.yaml
| |-- wide_and_deep_podip.yaml
| |-- wide_and_deep_service.yaml
| `-- wide_and_deep_volcano.yaml
|-- v1
| |-- crd.yaml
| `-- operator.yaml
`-- v1beta1
|-- crd.yaml
`-- operator.yaml
部署 CRD¶
注意:kubernetes 1.15 及以下使用 v1beta1 目录,1.16 及以上使用目录 v1.
执行以下命令,
$ kubectl create -f https://raw.githubusercontent.com/PaddleFlow/paddle-operator/dev/deploy/v1/crd.yaml
或者
$ kubectl create -f deploy/v1/crd.yaml
注意:v1beta1 请根据报错信息添加 –validate=false 选项
通过以下命令查看是否成功,
$ kubectl get crd
NAME CREATED AT
paddlejobs.batch.paddlepaddle.org 2021-02-08T07:43:24Z
部署 controller 及相关组件¶
注意:默认部署的 namespace 为 paddle-system,如果希望在自定义的 namespace 中运行或者提交任务, 需要先在 operator.yaml 文件中对应更改 namespace 配置,其中
namespace: paddle-system 表示该资源部署的 namespace,可理解为系统 controller namespace;
Deployment 资源中 containers.args 中 –namespace=paddle-system 表示 controller 监控资源所在 namespace,即任务提交 namespace。
执行以下部署命令,
$ kubectl create -f https://raw.githubusercontent.com/PaddleFlow/paddle-operator/dev/deploy/v1/operator.yaml
或者
$ kubectl create -f deploy/v1/operator.yaml
通过以下命令查看部署结果和运行状态,
$ kubectl -n paddle-system get pods
NAME READY STATUS RESTARTS AGE
paddle-controller-manager-698dd7b855-n65jr 1/1 Running 0 1m
通过查看 controller 日志以确保运行正常,
$ kubectl -n paddle-system logs paddle-controller-manager-698dd7b855-n65jr
提交 demo 任务查看效果,
$ kubectl -n paddle-system create -f deploy/examples/wide_and_deep.yaml
查看 paddlejob 任务状态, pdj 为 paddlejob 的缩写,
$ kubectl -n paddle-system get pdj
NAME STATUS MODE PS WORKER AGE
wide-ande-deep-service Completed PS 2/2 0/2 4m4s
以上信息可以看出:训练任务已经正确完成,该任务为 ps 模式,配置需求 2 个 pserver, 2 个在运行,需求 2 个 woker,0 个在运行(已完成退出)。 可通过 cleanPodPolicy 配置任务完成/失败后的 pod 删除策略,详见任务配置。
查看 pod 状态,
$ kubectl -n paddle-system get pods
卸载¶
通过以下命令卸载部署的组件,
$ kubectl delete -f deploy/v1/crd.yaml -f deploy/v1/operator.yaml
注意:重新安装时,建议先卸载再安装
paddlejob 任务提交¶
在上述安装过程中,我们使用了 wide-and-deep 的例子作为提交任务演示,本节详细描述任务配置和提交流程供用户参考提交自己的任务, 镜像的制作过程可在 docker 镜像 章节找到。
示例 wide and deep¶
本示例采用 PS 模式,使用 cpu 进行训练,所以需要配置 ps 和 worker。
准备配置文件,
$ cat demo-wide-and-deep.yaml
apiVersion: batch.paddlepaddle.org/v1
kind: PaddleJob
metadata:
name: wide-ande-deep
spec:
withGloo: 1
intranet: PodIP
cleanPodPolicy: OnCompletion
worker:
replicas: 2
template:
spec:
containers:
- name: paddle
image: registry.baidubce.com/paddle-operator/demo-wide-and-deep:v1
ps:
replicas: 2
template:
spec:
containers:
- name: paddle
image: registry.baidubce.com/paddle-operator/demo-wide-and-deep:v1
说明:
提交命名需要唯一,如果存在冲突请先删除原 paddlejob 确保已经删除再提交;
ps 模式时需要同时配置 ps 和 worker,collective 模式时只需要配置 worker 即可;
withGloo 可选配置为 0 不启用, 1 只启动 worker 端, 2 启动全部(worker端和Server端), 建议设置 1;
cleanPodPolicy 可选配置为 Always/Never/OnFailure/OnCompletion,表示任务终止(失败或成功)时,是否删除 pod,调试时建议 Never,生产时建议 OnCompletion;
intranet 可选配置为 Service/PodIP,表示 pod 间的通信方式,用户可以不配置, 默认使用 PodIP;
ps 和 worker 的内容为 podTemplateSpec,用户可根据需要遵从 kubernetes 规范添加更多内容, 如 GPU 的配置.
提交任务: 使用 kubectl 提交 yaml 配置文件以创建任务,
$ kubectl -n paddle-system create -f demo-wide-and-deep.yaml
示例 resnet¶
本示例采用 Collective 模式,使用 gpu 进行训练,所以只需要配置 worker,且需要配置 gpu。
准备配置文件,
$ cat resnet.yaml
apiVersion: batch.paddlepaddle.org/v1
kind: PaddleJob
metadata:
name: resnet
spec:
cleanPodPolicy: Never
worker:
replicas: 2
template:
spec:
containers:
- name: paddle
image: registry.baidubce.com/paddle-operator/demo-resnet:v1
command:
- python
args:
- "-m"
- "paddle.distributed.launch"
- "train_fleet.py"
volumeMounts:
- mountPath: /dev/shm
name: dshm
resources:
limits:
nvidia.com/gpu: 1
volumes:
- name: dshm
emptyDir:
medium: Memory
注意:
这里需要添加 shared memory 挂载以防止缓存出错;
本示例采用内置 flower 数据集,程序启动后会进行下载,根据网络环境可能等待较长时间。
提交任务: 使用 kubectl 提交 yaml 配置文件以创建任务,
$ kubectl -n paddle-system create -f resnet.yaml
更多配置¶
Volcano 支持¶
paddle-operator 支持使用 volcano 进行复杂任务调度,使用前请先 安装 。
本节使用 volcano 实现 paddlejob 运行的 gan-scheduling。
使用此功能需要进行如下配置:
创建 paddlejob 同名 podgroup,具体配置信息参考 volcano 规范;
在 paddlejob 任务配置中添加声明:schedulerName: volcano , 注意:需要且只需要在 worker 中配置。
配置示例,
---
apiVersion: batch.paddlepaddle.org/v1
kind: PaddleJob
metadata:
name: wide-ande-deep
spec:
cleanPodPolicy: Never
withGloo: 1
worker:
replicas: 2
template:
spec:
restartPolicy: "Never"
schedulerName: volcano
containers:
- name: paddle
image: registry.baidubce.com/paddle-operator/demo-wide-and-deep:v1
ps:
replicas: 2
template:
spec:
restartPolicy: "Never"
containers:
- name: paddle
image: registry.baidubce.com/paddle-operator/demo-wide-and-deep:v1
---
apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
metadata:
name: wide-ande-deep
spec:
minMember: 4
在以上配置中,我们通过创建最小调度单元为 4 的 podgroup,并将 paddlejob 任务标记使用 volcano 调度,实现了任务的 gan-scheduling。
可以通过以下命运提交上述任务查看结果,
$ kubectl -n paddle-system create -f deploy/examples/wide_and_deep.yaml
GPU 和节点选择¶
更多配置示例,
apiVersion: batch.paddlepaddle.org/v1
kind: PaddleJob
metadata:
name: wide-ande-deep
spec:
intranet: Service
cleanPodPolicy: OnCompletion
worker:
replicas: 2
template:
spec:
containers:
- name: paddle
image: registry.baidubce.com/paddle-operator/demo-wide-and-deep:v1
resources:
limits:
nvidia.com/gpu: 1
nodeSelector:
accelerator: nvidia-tesla-p100
ps:
replicas: 2
template:
spec:
containers:
- name: paddle
image: registry.baidubce.com/paddle-operator/demo-wide-and-deep:v1
resources:
limits:
nvidia.com/gpu: 1
nodeSelector:
accelerator: nvidia-tesla-p100
数据存储¶
在 kubernentes 中使用挂载存储建议使用 pv/pvc 配置,详见 persistent-volumes 。
这里使用 nfs 云盘作为存储作为示例,配置文件如下,
$ cat pv-pvc.yaml
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv
spec:
capacity:
storage: 10Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
storageClassName: slow
mountOptions:
- hard
- nfsvers=4.1
nfs:
path: /nas
server: 10.12.201.xx
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc
spec:
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
storage: 10Gi
storageClassName: slow
volumeName: nfs-pv
使用以下命令在 namespace paddle-system 中 创建 pvc 名为 nfs-pvc 的存储声明,实际引用为 10.12.201.xx 上的 nfs 存储。
$ kubectl -n paddle-system apply -f pv-pvc.yaml
注意 pvc 需要绑定 namespace 且只能在该 namespace 下使用。
提交 paddlejob 任务时,配置 volumes 引用以使用对应存储,
apiVersion: batch.paddlepaddle.org/v1
kind: PaddleJob
metadata:
name: paddlejob-demo-1
spec:
cleanPolicy: OnCompletion
worker:
replicas: 2
template:
spec:
restartPolicy: "Never"
containers:
- name: paddle
image: registry.baidubce.com/paddle-operator/paddle-ubuntu:2.0.0-18.04
command: ["bash","-c"]
args: ["cd /nas/wide_and_deep; python3 train.py"]
volumeMounts:
- mountPath: /nas
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: nfs-pvc
ps:
replicas: 2
template:
spec:
restartPolicy: "Never"
containers:
- name: paddle
image: registry.baidubce.com/paddle-operator/paddle-ubuntu:2.0.0-18.04
command: ["bash","-c"]
args: ["cd /nas/wide_and_deep; python3 train.py"]
volumeMounts:
- mountPath: /nas
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: nfs-pvc
该示例中,镜像仅提供运行环境,训练代码和数据均通过存储挂载的方式添加。
FAQ¶
问:当程序报错时,如何排查错误?
答:首先查看日志,是否可以可以定位错误的信息,如显存不够OOM等。
问:如果程序hang,如何排查出错原因?
答:一般引起程序hang的问题,都是通信问题。比如,两个进程同步不一致:一个进程等待同步A数据,而另一个进程却在等待同步B数据,从而导致程序hang。一般排查步骤是定位进程hang的位置,然后具体分析导致hang的原因。可以通过设置如下环境变量查看程序hang时执行的算子:
export GLOG_v=3; export FLAGS_benchmark=1
。问:程序中报错,显示NCCL相关错误,怎么排查原因?
答:可以通过设置如下环境变量查看程序错误信息:
export NCCL_DEBUG=INFO
。并重点关注NCCL WARN相关信息。
FleetX使用Apache License 2.0开源协议