本章内容包含:
了解数据并行、模型并行和管道并行
运用在Kubernetes中支撑数据并行练习的示例练习服务
运用多个GPU进行练习大型模型
在深度学习研究领域中,一个显着的趋势是经过更大的数据集和更杂乱的模型架构来提升模型功用。可是更多的数据和更巨大的模型也带来了一些问题:模型练习进程和模型开发进程变得缓慢。在核算中,功用往往与速度产生矛盾。例如,运用单个GPU练习BERT(双向编码器表明来自Transformer的表明)自然语言处理模型或许需求几个月的时刻。
为了处理不断增加的数据集和模型参数规模的问题,研究人员提出了各种散布式练习战略。首要的练习结构,如TensorFlow和PyTorch,供给了完结这些练习战略的SDK。凭借这些练习SDK,数据科学家能够编写在多个设备(CPU或GPU)上并行运转的练习代码。
在本章中,咱们将从软件工程师的视点评论怎么支撑散布式练习。具体而言,咱们将了解怎么编写一个练习服务,以在一组机器上履行由数据科学家开发的不同散布式练习代码。
阅读完本章后,您将全面了解散布式练习的作业原理,从数据科学家和开发者的视点来看。您将了解几种散布式练习战略和散布式练习代码形式,以及练习服务怎么支撑不同的散布式练习代码。
散布式练习办法的类型
有三种首要的散布式练习办法:模型并行、数据并行和管道并行。模型并行是将神经网络分割为多个接连的子网络,并在不同的设备(GPU或CPU)上运转每个子网络的战略。经过这种办法,咱们能够运用一组GPU对大型模型进行练习。
管道并行是模型并行的高级版别。模型并行的一个首要问题是在练习进程中只需一个GPU处于活动状况,其他GPU处于闲暇状况。经过将每个练习样本批次分红小的微批次,管道并行能够在层之间堆叠核算,以最大化GPU的功用。这样不同的GPU能够一起处理不同的微批次。GPU的练习吞吐量和设备运用率得到改善,模型练习速度比模型并行快得多。
数据并即将数据集分红较小的子集,并让每个设备独自练习这些子集。因为每个设备现在练习的数据集较小,练习速度进步了。
将单设备练习代码转换为模型并行或管道并行练习需求进行很多的代码更改,包含将神经网络分割为多个子网络,在不同的GPU上运转子网络,并在不同的GPU上复制子网络的核算输出。这些更改的数量和杂乱性使其变得扎手且难以调试。每个模型算法或许具有截然不同的模型架构,因而没有规范化的办法能够将模型分割为模型并行或管道并行。数据科学家有必要依据实践状况逐一构建代码。
相反,数据并行仅需求对单设备练习代码进行最小的代码更改。而且有规范化的形式能够将非散布式练习代码转换为数据并行,而无需更改模型算法或架构。此外,数据并行代码相对简略了解和调试。这些长处使数据并行成为散布式练习的首选办法。
尽管数据并行具有许多长处,但模型并行和管道并行也有各自的优势和用处。例如,当你有无法放入一个GPU中的大型模型时,它们是最佳的散布式处理方案。咱们将在第4.4节中更详细地评论它们。
数据并行
在本节中,咱们将评论数据并行理论及其并行履行的应战,以及运用PyTorch、TensorFlow和Horovod的示例练习代码。
了解数据并行化
数据并行化触及一组练习设备一起处理一个大型数据集。经过让每个设备处理数据集的一个子集,咱们能够大大削减练习时刻。 同步数据并行是最常用的数据并行办法。它将模型网络复制到练习组中的每个设备上,不管是 GPU 仍是 CPU。数据集被分红小批次,并将这些批次分发到一切设备上(CPU 或 GPU)。练习进程一起进行,在每个设备上运用不同的小批次;因而,设备自身充任其自己的数据分区。在核算梯度以更新神经网络时,算法经过从每个设备聚合梯度来核算最终梯度。然后,它将聚合的梯度分发回每个设备,以更新其本地神经网络。尽管每个设备上的练习数据集不同,但这些设备本地的神经网络是相同的,因为它们在每个练习迭代中运用相同的梯度进行更新。因而,这个进程被称为同步数据并行。
您能够在图 4.1 中可视化此进程。图 (a) 在左边显现了在单个 GPU 上进行深度学习练习的进程,图 (b) 在右侧显现了运用三个 GPU 进行同步数据并行练习的设置。 经过比较图 (a) 和 (b),您能够看到同步数据并行相关于单设备练习引入了两个额定的进程。榜首个额定的进程是将一个练习批次分红三个小批次,以便每个设备能够处理自己的小批次。第二个进程是同步从一切设备聚合的梯度,以便它们在更新本地模型时运用相同的梯度。 留意:要聚合不同作业器核算的梯度,能够运用 all-reduce 算法。这是一种盛行的算法,能够独登时将一切进程的数据数组组合成一个数组。在“运用 PyTorch 编写散布式应用程序”(pytorch.org/tutorials/i…)中,您能够找到PyTorch 怎么支撑 all-reduce 算法的示例。 从完结的视点来看,数据并行化关于单设备模型练习进程的改动很小。它的首要开支在于增加梯度聚合的同步进程。

模型参数更新:同步 vs. 异步
在数据并行化中,有两种关于在作业器之间聚合梯度的思路:同步更新和异步更新。让咱们详细了解每种办法的作业原理、长处和缺陷,以便您能够依据自己的需求进行挑选:
- 同步模型更新:如图4.1所示,同步模型更新在梯度同步进程中暂停练习迭代,直到一切设备接收到聚合的梯度。然后持续下一步,更新模型参数。这样,一切设备在每次练习迭代中一起得到相同的梯度更新,保证每个作业器的模型在同一页面上。同步模型更新的问题很显着:在梯度在作业器之间同步时,练习迭代被堵塞,因而没有一个作业器能够开端处理下一个小批量的数据。假如存在一些慢速机器或网络问题,整个散布式作业组都会被堵塞,而速度较快的作业器则处于闲暇状况。
- 异步模型更新:比较之下,异步模型更新办法不强制每个练习设备或作业器等候接收来自其他设备的梯度。相反,只需一个设备完结核算梯度,它就当即更新本地模型,而无需查看其他设备。每个设备都独立作业,尽管它的梯度依然需求复制到每个设备,但不需求同步这些更新。异步办法或许看起来十分吸引人;它简略,而且比同步办法每分钟能够运转更多的练习进程。异步办法的缺陷是练习时刻较长,生成的模型比同步模型更新办法的准确性较低。
在运用异步办法时,不同设备上的梯度是独立核算的。某些机器运转得更快,而其他机器运转得更慢;因而,这些梯度能够来自每个设备的不同练习迭代。因而,不能保证聚合的梯度将指向最优方向。例如,假定梯度来自慢速机器核算的第5个练习迭代,而其他更快的机器现已进入第20个练习迭代。当咱们聚合一切作业器的梯度时,较低迭代的梯度会应用于较高迭代的梯度;这会下降梯度的质量。
此外,异步办法一般收敛较慢,准确性丢失较高。因而,如今大多数数据并行化库都运用同步模型更新。在本章中,当提及数据并行化及其代码完结时,咱们指的是同步数据并行化。
数据集和模型的内存约束
在深度学习中,数据集和模型在练习期间占用了核算实例的大部分内存。假如练习数据或神经网络(模型)超出了本地设备的内存约束,练习进程将因内存不足(OOM)过错而中止。数据并行化旨在进步练习速度,而不是处理内存约束问题。
关于由加载数据集引起的内存不足(OOM),咱们能够减小练习数据的批量巨细,这样练习进程在每个练习循环中加载的数据量就较小。在数据并行化的上下文中,咱们需求保证小批量练习数据能够习惯每个作业设备的内存。
关于由模型巨细引起的内存不足(OOM),咱们需求选用模型并行化或管道并行化(拜见第4.4节)。数据并行化触及多个练习设备在一个大型数据集上一起作业。经过使每个设备处理数据集的子集,咱们能够大大削减练习时刻。
同步模型更新是最常用的数据并行化办法。它将模型网络复制到练习组中的每个设备上,不管是 GPU 仍是 CPU。数据集被分红小批次,并将这些批次分发到一切设备上(不管是 CPU 仍是 GPU)。练习进程一起进行,在每个设备上运用不同的小批次;因而,设备自身充任其自己的数据分区。在核算梯度以更新神经网络时,算法经过从每个设备聚合梯度来核算最终梯度。然后,它将聚合的梯度分发回每个设备,以更新其本地神经网络。尽管每个设备上的练习数据集不同,但这些设备本地的神经网络是相同的,因为它们在每次练习迭代中运用相同的梯度进行更新。因而,这个进程被称为同步模型更新。
您能够在图 4.1 中可视化这个进程。图 (a) 在左边显现了在单个 GPU 上进行深度学习练习的进程,图 (b) 在右侧显现了运用三个 GPU 进行同步数据并行练习的设置。
经过比较图 (a) 和 (b),您能够看到同步数据并行化相关于单设备练习引入了两个额定的进程。榜首个额定的进程是将一个练习批次分红三个小批次,以便每个设备能够处理自己的小批次。第二个进程是同步从一切机器聚合的梯度,以便它们在更新本地模型时运用相同的梯度。
留意:要聚合不同作业器核算的梯度,能够运用 all-reduce 算法。这是一种盛行的算法,能够独登时将一切进程的数据数组组合成一个数组。在“运用 PyTorch 编写散布式应用程序”(pytorch.org/tutorials/i… PyTorch 怎么支撑 all-reduce 算法的示例。
从完结的视点来看,数据并行化关于单设备模型练习进程的改动很小。它的首要开支在于增加梯度聚合的同步进程。
在深度学习中,数据集和模型占据了核算实例的大部分内存。假如练习数据或神经网络(模型)超出了本地设备的内存约束,练习进程将因内存不足(OOM)过错而中止。数据并行化旨在进步练习速度,而不是处理内存约束问题。
关于由加载数据集引起的内存不足(OOM),咱们能够减小练习数据的批量巨细,这样练习进程每个练习循环中加载的数据量就较小。在数据并行化的上下文中,咱们需求保证小批量练习数据能够习惯每个作业设备的内存。
关于由模型巨细引起的内存不足(OOM),咱们需求选用模型并行化或管道并行化(拜见第4.4节)。数据并行化在单设备上的模型巨细超越内存约束时无法正常作业。
多作业器练习的应战
容错性和带宽饱和度是咱们作为软件开发人员在履行数据并行代码时需求处理的两个应战。处理这两个应战关于下降数据并行散布式练习的运营本钱和进步练习功用至关重要。
容错性
咱们不期望整个散布式练习组在一个作业器意外失利时也随之失利。这不仅会影响服务的可用性,还会增加练习本钱,因为假如一个作业器失利,其他一切作业器的努力都将白费。
为了进步容错性,咱们能够在每个作业器的长途文件体系中保留每个练习进程的练习状况(即模型参数)。然后,假如一个作业器失利或花费太长时刻来完结一个练习迭代,咱们能够重新发动该作业器并加载其最近的从前状况。
TensorFlow和PyTorch结构都具备备份和康复功用。作为练习服务的开发人员,咱们能够设置长途磁盘或备份存储体系,并将访问装备传递给练习容器。然后,在练习进程中,练习代码能够运用外部文件体系来备份或康复状况。
带宽饱和度
向散布式练习组中增加更多的GPU和机器并不总是会进步功用。不管咱们运用同步仍是异步模型更新,算法都有必要在每个练习迭代的末尾在练习作业器之间传递梯度或模型参数。将数据移入和移出GPU内存以及在网络中传输数据所花费的时刻最终将超越经过拆分练习作业负载取得的加速作用。
因而,存在一个并行实例数量的上限,超越这个上限,数据并行性将到达其功用峰值。这个上限由模型参数的数量和模型的密度(模型权重中的非零值数量)决议。假如是一个参数和梯度传输较多的大型密布模型,它的饱和度将高于较小的模型或大型稀少模型。
有一些引荐的并行实例数量,例如关于神经机器翻译的8个GPU能够完结6倍加速,关于ImageNet模型的50个GPU能够完结32倍加速。可是,咱们需求经过自己的试验来确定最佳功用点,因为GPU和模型架构都在快速演进,规范引荐很快就会过时。作为渠道开发人员,除了挑选合适数量的并行作业器之外,咱们还有三种下降带宽饱和度的办法。
首要,咱们能够将并行作业器(即容器或Pod)分组到较少的机器上,以削减网络跳数。例如,在Kubernetes中,您能够运用节点挑选器和亲和性规矩(mng.bz/qo76)来在几个选定的服务器上分配练习实例(Kubernetes Pod),这些服务器具有更好的网络和更强壮的核算才能。
第二个挑选是一直晋级练习镜像以运用最新版别的练习结构。盛行的结构如PyTorch、TensorFlow等不断演进,以削减在散布式练习中在网络中传输的数据量。密切重视发布阐明并运用这些改善。
最终,不要低估在初始化散布式组时进行小的调整所带来的收益。以PyTorch为例,PyTorch的数据并行库将神经网络参数梯度分割成桶(bucket),然后在梯度同步进程中将这些桶发送给作业器。桶的巨细决议了在不同设备之间传输的数据量。因而,经过挑选恰当的桶巨细,咱们能够在设备饱和度和网络饱和度之间找到一个最佳点,然后到达最佳的练习速度。能够在PyTorch散布式数据并行(DDP)组件的构造函数中装备桶的巨细(mng.bz/7ZB7)。
为不同的练习结构编写散布式练习(数据并行)代码
在本节中,您将看到三种练习结构(TensorFlow、PyTorch和Horovod)中用于数据并行散布式练习的一些练习代码片段。假如这儿的代码样本很难了解,不要忧虑。目的是体会数据科学家在处理散布式练习时的办法,以便让您对练习服务怎么支撑散布式练习有所了解。
PYTORCH
PyTorch结构有一个DDP(DistributedDataParallel)库,它在模块级别完结了数据并行。DDP包装了模型目标,使其能够在多台机器上无缝运转。它的练习进程能够在同一台机器上或跨多台机器上进行。
要将单设备/进程的练习代码转换为数据并行的散布式练习代码,咱们需求进行以下两个修改。首要,咱们有必要经过答应每个练习进程向主进程注册自己来初始化练习组。其间一个进程将自称为主进程,其他进程将自称为作业进程。每个练习进程在此注册阶段将处于等候状况,直到一切作业进程参加散布式组。
要注册一个进程,咱们需求知道练习进程的总数(world_size),该进程的仅有ID(rank),以及主进程的地址(在环境变量中界说MASTER_ADDR和MASTER_PORT)。代码示例如下:
def setup(rank, world_size):
os.environ['MASTER_ADDR'] = 'xxxx'
os.environ['MASTER_PORT'] = 'xxx'
# initialize the process group, "gloo" is one of the communication
# backends Pytorch supports, it also supports MPI and NCCL.
# rank is the process’s rank, it's a globally unique id
# for this process. rank=0 means master process.
# world_size is the total number of processes in this training group.
dist.init_process_group("gloo", rank=rank, world_size=world_size)
def cleanup():
dist.destroy_process_group()
其次,咱们运用DDP类来包装模型目标。PyTorch的DDP类将处理散布式数据通讯、梯度聚合和本地模型参数更新:
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
# create model and move it to GPU
model = DpModel().to(device)
# wrap the model with DDP
ddp_model = DDP(model, device_ids=[rank])
outputs = ddp_model(data)
# compute the loss and sync gradient with other workers.
# when 'backward' function returns, the param.grad already
# contains synchronized gradient tensor
loss_fn(outputs, labels).backward()
关于高级用例,PyTorch库供给了API,让您能够在较低层次上完结自己的梯度同步函数。您能够在官方教程“运用PyTorch编写散布式应用程序”(mng.bz/m27W)中了解详细信息。
TENSORFLOW/KERAS
TensorFlow以十分类似的办法支撑散布式练习;它首要界说一个散布式练习战略(如MultiWorkerMirroredStrategy),然后运用该战略初始化模型。为了让战略识别散布式组中的作业进程,咱们需求在每个练习进程中界说一个TF_CONFIG环境变量。TF_CONFIG包含作业进程的仅有ID和组中一切其他作业进程的地址。以下是代码示例:
# Step 1: define 'TF_CONFIG' environment variable to describe
# the training group and the role for the process.
# The worker array defines the IP addresses and ports of
# all the TensorFlow servers used in this training.
tf_config = {
'cluster': {
'worker': ['192.168.4.53:12345', '192.168.4.55:23456']
},
# A 'task' provides information of the current task and is
# different for each worker. It specifies the 'type' and
# 'index' of that worker.
'task': {'type': 'worker', 'index': 0}
}
os.environ['TF_CONFIG'] = json.dumps(tf_config)
# Step 2: define distributed training strategy,
# the MultiWorkerMirroredStrategy takes
# care of the synchronous data parallel distributed training.
strategy = tf.distribute.MultiWorkerMirroredStrategy()
global_batch_size = per_worker_batch_size * num_workers multi_worker_dataset = mnist.mnist_dataset(global_batch_size)
# Step 3: start the distributed training.
with strategy.scope():
# Model building/compiling need to be within 'strategy.scope()'.
multi_worker_model = mnist.build_and_compile_cnn_model()
multi_worker_model.fit(multi_worker_dataset,epochs=3, steps_per_epoch=70)
**HOROVOD **
Horovod是一个单一用处的散布式结构。与TensorFlow和PyTorch能够用于数据处理、模型练习和模型服务等多种使命不同,Horovod只专注于一个使命:使散布式深度学习练习快速且易于运用。 Horovod最大的优势是它能够与不同的练习结构配合运用,如TensorFlow、Keras、PyTorch和Apache MXNet。因而,咱们能够以一种办法(即Horovod办法)装备练习集群,以运转PyTorch、TensorFlow和其他结构的散布式练习。在这儿,咱们只列出了运用Horovod与TensorFlow和PyTorch的两个代码示例,但您能够在Horovod的网站上查看其他结构的示例。
让咱们来看一下TensorFlow的示例。要设置数据并行的散布式练习,首要咱们初始化Horovod练习组,它会主动查找集群中的其他Horovod节点。接下来,咱们将0号(主作业节点)的初始变量状况广播给一切其他进程。这将保证一切作业节点的初始状况共同。然后,咱们运用散布式梯度记录器来包装梯度记录器,它会在一切作业节点上平均梯度。其他的代码仅仅正常的TensorFlow练习代码。请参阅以下代码(github.com/horovod/hor…):
hvd.init()
.. .. ..
@tf.function
def training_step(images, labels, first_batch):
with tf.GradientTape() as tape:
probs = mnist_model(images, training=True)
loss_value = loss(labels, probs)
# Wrap tape with Horovod Distributed GradientTape.
# This gradient tape averages gradients from all
# workers by using allreduce or allgather, and then
# applies those averaged gradients back to the local model.
tape = hvd.DistributedGradientTape(tape)
grads = tape.gradient(loss_value, mnist_model.trainable_variables) opt.apply_gradients(zip(grads, mnist_model.trainable_variables))
# Broadcast initial variable states
# from rank 0 to all other processes.
if first_batch:
hvd.broadcast_variables(mnist_model.variables, root_rank=0)
hvd.broadcast_variables(opt.variables(), root_rank=0)
return loss_value
for batch, (images, labels) in \
enumerate(dataset.take(10000 / hvd.size())):
loss_value = training_step(images, labels, batch == 0)
.. .. ..
# save checkpoints only on worker 0 to
# prevent other workers from corrupting it.
if hvd.rank() == 0:
checkpoint.save(checkpoint_dir)
以下代码是运用Horovod与PyTorch的示例。PyTorch Horovod的一些API与TensorFlow不同,例如hvd.DistributedOptimizer与hvd.DistributedGradientTape。可是这些API都来自同一个Horovod SDK,在底层同享相同的作业节点间机制。让咱们看一下PyTorch的代码片段:
# Horovod: initialize Horovod.
import torch
import horovod.torch as hvd
# Initialize Horovod
hvd.init()
.. .. ..
# Build model...
model = ...
optimizer = optim.SGD(model.parameters())
# Add Horovod Distributed Optimizer, this is equal
# to hvd.DistributedGradientTape(tape)
# for Tensorflow2
optimizer = hvd.DistributedOptimizer(optimizer,named_parameters=model.named_parameters())
# Broadcast parameters from rank 0 to
# all other processes.
hvd.broadcast_parameters(model.state_dict(),root_rank=0)
for epoch in range(100):
for batch_idx, (data, target) in enumerate(train_loader):
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
.. .. ..
尽管这两个代码片段中的模型界说在两个不同的结构(TensorFlow 2和PyTorch)中,但咱们能够看到它们都运用相同的Horovod SDK来运转散布式练习。这儿的好处是咱们能够运用一个规范办法(Horovod办法)来设置练习集群中的散布式作业组,而且它依然适用于运用不同练习结构编写的练习代码。
关于练习代码,有两个要害需求着重:
- 尽管本节中的代码示例运用不同的结构和不同的API完结了散布式练习,但代码都遵循了第4.2.1节中描绘的相同的数据并行主义范式。也便是说,代码总是(1)为每个并行练习进程设置通讯组,并(2)装备模型目标以在一切作业节点上聚合梯度。因而,作为开发人员,咱们能够运用一致的办法来设置和办理不同练习结构的散布式练习进程。
- 将模型练习代码从单设备练习扩展到数据并行散布式练习的作业相对较小。如今,散布式练习结构/SDK十分强壮,咱们不需求完结数据并行的每个细节,例如在网络上同步梯度的梯度同步。练习结构和SDK会处理这些进程,使其无缝运转。散布式数据并行练习代码与单设备练习代码简直相同,除了在装备练习组时有所不同。
在数据并行散布式练习中的工程投入
因而,在出产环境中启用数据并行散布式练习的作业是什么样的呢?首要,它需求数据科学家和服务开发人员之间的一起工程努力。关于数据科学家来说,他们需求将单设备练习代码晋级为能够散布式运转的代码,运用前面部分中的代码片段等。一起,服务开发人员有必要增强练习服务,以主动设置散布式作业组,以完结散布式练习。
为了使练习服务对用户友爱,服务应该整合不同的散布式练习结构的设置细节。因而,数据科学家只需界说他们所需的练习的并行实例数量。
让咱们以TensorFlow散布式练习为例。依据咱们在4.2.3节中的评论,每个设备上的TensorFlow练习代码有必要将tf_config(如下面的示例所示)作为环境变量。因而,练习进程中的TensorFlow散布式库知道怎么与其他练习进程进行通讯:
tf_config = { 'cluster': {
'worker': ['192.168.4.53:12345', '192.168.4.55:23456'] },
# A 'task' provides information of the current task
# and is different for each worker. It specifies
# the 'type' and 'index' of that worker.
'task': {'type': 'worker', 'index': 0}
}
从可用性的视点来看,咱们不能指望数据科学家为每个散布式练习进程找到设置值 – 服务器IP地址和使命索引,尤其是假如整个练习组是动态装备的。一个练习服务应该主动创立一组核算资源用于散布式练习恳求,运用正确的IP地址初始化散布式练习库,并发动练习进程。
图4.2是支撑散布式练习的练习服务的概念图。从图中能够看到,数据科学家Alex发送一个练习恳求以发动散布式练习。然后,由服务开发人员Tang构建的服务生成两台作业机器,并散布履行练习代码。除了准备练习代码外,Alex还能够指定练习运转的装备,例如并行作业人数和散布式练习结构的类型(TensorFlow、PyTorch或Horovod)。

让咱们慢慢地阅读一下这个图表,以更好地了解体系是怎么设置的,每个人的责任是什么。咱们能够看到,作为工程师的Tang需求进行三项改善-在图4.2中标为1、2和3-将练习服务从单设备练习器(正如咱们在第3章中看到的)转变为数据并行的散布式练习器。
榜首步是更新练习服务,以在运转时依据需求构建散布式练习组。当服务接收到散布式练习的恳求时,它会从练习集群中分配多个作业节点给练习使命,并将练习代码分发给每个作业节点。 第二步是以编程办法为每个练习进程初始化正确的服务器IP、端口号和练习进程ID。这保证了散布式库(如TensorFlow等结构)有满足的信息来为练习组设置节点间通讯。正如咱们在前面的章节中所看到的,每个散布式练习结构的设置装备都有所不同。练习服务应该知道怎么为不同的结构设置节点间通讯,这样数据科学家就只需求重视算法的开发,而不用忧虑底层基础设施。 第三步是供给长途存储来备份和康复每个作业节点的练习状况。在散布式练习中,假如一个作业节点失利,整个练习组都会失利,很多核算资源将被糟蹋。因而,让散布式练习组具有从硬件毛病或网络问题中康复的才能十分重要。经过供给长途存储和备份API,散布式练习进程能够在每个练习迭代后保存其练习状况(神经网络)。当练习进程在练习中间失利时,它能够康复到之前的状况并重新开端,整个练习组能够持续进行。 留意:假如你想了解更多关于数据并行的内容,你能够参阅以下两篇文章:O’Reilly的博客文章《散布式TensorFlow:经过运用多个GPU服务器,削减神经网络的试验时刻和练习时刻》(作者:Jim Dowling,链接:www.oreilly.com/content/dis… uted-tensorflow/)和Google Brain的论文《重访散布式同步SGD》(作者:Chen等,链接:arxiv.org/pdf/1604.00…)。
一个支撑数据并行散布式练习的示例服务
在本节中,咱们将扩展前一章节(第3.3节)介绍的示例服务,以支撑数据并行散布式练习。
服务概述
与第3.3节中评论的单设备练习比较,用户作业流程坚持不变。数据科学家Alex首要构建模型练习代码,并向练习服务发送练习恳求。然后,服务运转实践的练习进程,并在最终生成模型。
可是,有一些要害的差异。首要,Alex将目的分类练习代码晋级,使其能够在单个设备和多个设备上运转。其次,服务开发人员Tang修改了练习服务的API,供给了一个新的参数PARALLEL_INSTANCES。这个参数答应Alex界说他的散布式练习使命的作业组巨细。
为了正确办理服务器集群,咱们需求Kubernetes的协助。Kubernetes能够协助咱们削减在作业资源分配和作业节点间通讯方面的作业量。因而,咱们引入了一个新的组件——Kubernetes作业盯梢器,用于在Kubernetes中办理练习作业。你能够在图4.3中看到更新后的服务设计图和用户作业流程。

图4.3(a)重复了咱们在第3.3节中评论的练习服务的体系图,该体系运用Docker作业盯梢器在Docker引擎中运转练习作业。图4.3(b)展现了更新后的练习服务,现在支撑散布式练习,包含Kubernetes和Docker引擎后端。增加了Kubernetes作业盯梢器,用于在Kubernetes集群中运转散布式练习作业。该组件经过发动Kubernetes Pod来履行练习作业,并在内存存储中监督和更新作业履行状况。
咱们还对目的分类的PyTorch练习代码进行了一些更改,使其能够进行散布式练习。咱们将在4.3.5节中扼要介绍这些更改。
一个很大的时刻节省是,咱们不需求改动现已创立的服务API接口(第3.3.3节)。咱们的用户能够在Docker引擎和Kubernetes集群中运用相同的API来练习模型。这符合练习服务的榜首准则,即运用一致的API,并使其与后端完结无关。
玩转服务
首要,让咱们运用Kubernetes后端运转练习服务,请参阅以下指令(scripts/ts-001-start-server-kube.sh):
$ docker build -t orca3/services:latest -f services.dockerfile .
$ docker run --name training-service -v \
$HOME/.kube/config:/.kube/config --env \
APP_CONFIG=config-kube.properties \
--network orca3 --rm -d -p
"${TS_PORT}":51001
orca3/services:latest training-service.jar
留意:本节仅包含运转示例服务所需的首要进程和要害指令。因而,能够明晰地演示概念,而无需冗长的代码和履行输出。假如您想在本节中运转试验,请按照orca3/MiniAutoML git存储库中的“Distributed trainer training demo”(github.com/orca3/MiniAutoML/blob/main/training-service/distributed_trainer_demo.md)文档中的阐明进行操作。
一旦练习服务容器正在运转,咱们就能够提交一个练习的gRPC恳求。尽管服务现在运转在Kubernetes后端,但练习API依然坚持不变。与咱们在Docker后端演示中发送的练习恳求(拜见3.3.1节)比较,在恳求有效负载中只增加了一个额定的参数PARALLEL_INSTANCES=3。这告诉练习服务创立一个由三个作业节点组成的散布式练习组来练习模型。假如将此参数设置为1,则为单设备练习恳求。请参阅以下代码片段,以运用三个并行实例提交散布式练习恳求(scripts/ts-004-start-parallel-run.sh 1):
# submit a distributed training request
$ grpcurl -plaintext -d "{ "metadata":
{ "algorithm":"intent-classification",
"dataset_id":"1",
"Name":"test1",
"train_data_version_hash":"hashBA==",
"Parameters":{
"LR":"4","EPOCHS":"15",
"PARALLEL_INSTANCES": "3" ,
"BATCH_SIZE":"64","FC_SIZE":"128"}}
}"
${TS_SERVER}:${TS_PORT}
training.TrainingService/Train
查看练习履行进度的办法是运用GetTrainingStatus API:
grpcurl -plaintext -d "{"job_id": "$1"}"
${TS_SERVER}:"${TS_PORT}"
training.TrainingService/GetTrainingStatus
除了查询练习服务API以获取作业履行状况外,咱们还能够在Kubernetes中查看练习进度。经过运用Kubernetes指令kubectl get all,咱们能够看到在本地Kubernetes环境中创立了三个作业器(worker)Pod。其间一个是主作业器(master worker),别的两个是普通的作业器(normal workers)。还创立了一个名为intent-classification-1-master-service的Kubernetes服务目标,用于主作业器(master pod)与作业器(worker pod)之间的网络连接。以下是代码片段:
# check Kubernetes resources status.
# We could see a distributed training group contains
# with three pods and one service are created in Kubernetes
$ kubectl get all -n orca3
NAME READY STATUS
pod/intent-classification-1-1-worker 0/1 Completed
pod/intent-classification-1-2-worker 0/1 Completed
pod/intent-classification-1-master 0/1 Completed
NAME TYPE .. ..
service/intent-classification-1-master-service ClusterIP
发动练习作业
现在,让咱们看看运用Kubernetes后端发动练习作业的作业流程。当接收到一个练习恳求时,恳求将被增加到作业队列中。一起,Kubernetes作业盯梢器会监督作业队列。当盯梢器发现等候的作业而且体系有可用容量时,它将开端处理这些作业。
为了发动一个PyTorch散布式练习作业,盯梢器首要创立所需数量的Kubernetes pod。每个pod保管一个练习进程。盯梢器还向每个pod传递独自的参数,然后将作业从作业队列移动到发动列表中(图4.4)。
在图4.4中,Kubernetes作业盯梢器能够处理单设备练习和散布式练习。它为单设备练习创立一个Kubernetes pod,并为散布式练习创立多个pod。 Kubernetes作业盯梢器类似于Docker作业盯梢器,它运转一个练习pod。它将一切用户界说的参数封装在环境变量中,并将它们传递给Kubernetes pod。
为了设置具有多个pod的PyTorch散布式练习,服务处理了别的两个功用。首要,它创立了一个Kubernetes服务目标,用于与主pod进行通讯。

依据PyTorch散布式练习算法部分(4.2.3)的内容,咱们知道每个PyTorch练习进程都需求主进程(pod)的IP地址来初始化散布式练习组。例如,每个PyTorch代码在练习逻辑开端之前需求增加以下代码片段:
def setup(rank, world_size):
os.environ['MASTER_ADDR'] = 'xxx xxx xxx xxx'
os.environ['MASTER_PORT'] = '12356'
dist.init_process_group("gloo",rank=rank, world_size=world_size)
可是在Kubernetes中,Pod是一种时刻短的资源,因而咱们不能依靠Pod的IP地址来定位Pod。相反,咱们运用Kubernetes的域名服务(DNS)作为永久地址来定位Pod。即使Pod在不同的节点被毁掉和重建,IP也发生了改变,咱们依然能够运用相同的DNS来访问它。因而,为了启用练习组的初始化,咱们首要为主Pod创立一个Kubernetes服务,然后将该DNS地址传递给一切的作业Pod作为主Pod的地址。
其次,它向每个Pod传递了四个环境变量。每个练习Pod所需的四个变量是WORLD_SIZE、RANK、MASTER_ADDR和MASTER_PORT:
- WORLD_SIZE表明练习组中Pod的总数,包含主Pod和作业Pod。
- RANK是一个练习进程的仅有ID;主进程的rank有必要为0。
- MASTER_ADDR和MASTER_PORT界说了主进程的主机地址和端口号,以便每个作业进程能够运用它们来访问主Pod。
例如,在运用三个实例进行散布式练习时,咱们为每个Pod创立了三个环境变量(一个主Pod,两个作业Pod):
Master Pod:
WORLD_SIZE:3; RANK:0,
MASTER_ADDR: intent-classification-1-master-service,
MASTER_PORT: 12356
Worker Pod 1:
WORLD_SIZE:3; RANK:1,
MASTER_ADDR: intent-classification-1-master-service,
MASTER_PORT: 12356
Worker Pod 2:
WORLD_SIZE:3; RANK:2,
MASTER_ADDR: intent-classification-1-master-service,
MASTER_PORT: 12356
鉴于一切的解说,让咱们来看一下实践代码是怎么完结的。以下清单突出了在Kubernetes中发动散布式练习的完结办法。
protected List<String> launchTrainingPods(int jobId, int worldSize, TrainingJobMetadata metadata, .. ..) {
.. .. ..
// It's a distributed training if the worldSize is greater than 1.
if (worldSize > 1) {
// .. .. ..
api.createNamespacedService(
config.kubeNamespace, serviceBody,null, null, null);
serviceTracker.add(masterServiceName);
logger.info(String.format("Launched master service %s",
masterServiceName));
.. .. ..
}
// create training pods definition
for (int rank = 0; rank < worldSize; rank++) {
envs.put("WORLD_SIZE", Integer.toString(worldSize)); // RANK 0 is master
envs.put("RANK", Integer.toString(rank));
envs.put("MASTER_ADDR", masterPodDnsName);
envs.put("MASTER_PORT", Integer.toString(masterPort));
V1PodSpec podSpec = new V1PodSpec()
.restartPolicy("Never")
.addContainersItem(new V1Container()
.image(algorithmToImage(metadata.getAlgorithm()))
.env(envVarsToList(envs)) .. .. ..
String workerPodName = rank == 0 ? masterPodName :String.format("job-%d-%d-%s-worker-%d", jobId,now, metadata.getName(), rank);
V1Pod workerPodBody = new V1Pod();
workerPodBody.apiVersion("v1");
.. .. ..
// (3)
api.createNamespacedPod(config.kubeNamespace,workerPodBody, null, null, null);
.. .. ..
}
return podNames;
}
您或许留意到,在这个示例中创立的Kubernetes的Pod和服务是为PyTorch散布式练习库定制的。实践上,这个示例服务并不限于PyTorch。为了支撑其他结构编写的练习代码,比方TensorFlow 2,咱们能够扩展Kubernetes作业盯梢器,以支撑TensorFlow散布式练习的设置。
例如,咱们能够收集一切worker pod的IP地址或DNS,并将它们组合起来,然后将它们广播回每个worker pod。在广播进程中,咱们将worker组信息设置到每个pod的TF_CONFIG环境变量中,以发动散布式练习组。TF_CONFIG环境变量是TensorFlow散布式库的特殊要求。
更新和获取作业状况
创立练习 Pod 后,Kubernetes 作业盯梢器将持续查询 Pod 的履行状况,并在状况发生改变时将作业移动到其他作业列表中。例如,假如 Pod 成功创立并开端运转,盯梢器将作业从发动列表移动到运转列表中。假如 Pod 履行完结,盯梢器将作业从运转列表移动到已完结作业列表中。图 4.5 描绘了这个进程。

当用户提交作业状况查询时,练习服务将在内存存储中的四个作业队列中查找作业 ID,并返回作业目标。有趣的是,尽管存在多个练习 Pod,咱们只需求查看主 Pod 的状况来盯梢散布式练习的进度。这是因为在同步数据并行练习中,一切的作业节点在每个练习周期都需求进行同步,所以主 Pod 能够代表其他作业 Pod。
查询和更新作业履行状况的代码与咱们在第3.3.5节中看到的 Docker 作业盯梢器十分相似。仅有的差异是咱们查询 Kubernetes 集群而不是 Docker 引擎来获取练习状况。咱们留下代码供您探索;您能够在 KubectlTracker 类的 updateContainerStatus 办法中找到它。
将练习代码转换为散布式运转
咱们对目的分类练习代码(在前一章节中介绍的第3.3.6节)进行了两处更改,以支撑散布式形式和单设备形式。
榜首个更改:初始化练习组
咱们运用WORLD_SIZE环境变量来查看练习代码是否应该在散布式练习中运转。假如world size等于1,则运用咱们在第3.3.6节中看到的相同的单设备练习代码。
可是假如值大于1,则初始化练习进程以参加散布式组。请留意,从练习服务(Kubernetes作业盯梢器)传递了每个pod的仅有RANK值,这关于散布式组的初始化是必需的。在自我注册到散布式组后,咱们还声明晰模型和数据采样器的散布式办法。以下是更改的代码示例:
def should_distribute():
return dist.is_available() and config.WORLD_SIZE > 1
def is_distributed():
return dist.is_available() and dist.is_initialized()
if should_distribute():
# initialize the distributed process group,
# wait until all works are ready.
dist.init_process_group("gloo",rank=config.RANK, world_size=config.WORLD_SIZE)
if is_distributed():
# wrap the model with DistributedDataParallel (DDP)
# package to enable data parallel training.
model = DDP(model)
if is_distributed():
# restricts data loading to a subset of the dataset
# exclusive to the current process
train_sampler = DistributedSampler(dataset=split_train_, num_replicas=config.WORLD_SIZE, rank=config.RANK)
第二个更改:只答应主节点(rank = 0)上传最终模型
第二个更改:仅答应主节点(rank = 0)上传最终模型。这是为了防止每个作业节点屡次上传相同的模型:
if config.RANK == 0:
accu_test = evaluate(test_dataloader)
.. .. ..
# upload model to metadata store.
artifact = orca3_utils.create_artifact(Rank 0 is the master pod.
config.MODEL_BUCKET, config.MODEL_OBJECT_NAME)
.. .. ..
改善
假如咱们持续将这个示例服务推向出产环境,咱们能够按照第4.2.2节中的思路来改善容错性并削减网络带宽饱和度。咱们还能够扩展Kubernetes作业盯梢器以支撑TensorFlow和Horovod的散布式练习。从练习服务的视点来看,它们并没有太大的差异,因为练习服务传递给练习代码的装备是十分通用的;这些信息关于一切结构都是必需的,仅仅称号不同罢了。只需练习服务和练习代码之间的协议明晰安稳,咱们依然能够将练习代码视为黑盒,即使在散布式环境中也是如此。
练习无法在单个GPU上加载的大型模型
神经网络的巨细(由参数数量界说)在研究领域中迅速增加,咱们不能忽视这一趋势。以ImageNet应战为例,2014年的冠军(GoogleNet)有400万个参数;2017年的冠军(Squeeze-and-Excitation Networks)有1.458亿个参数;当时的抢先办法拥有超越10亿个参数。
尽管咱们的神经网络巨细增加了近300倍,但GPU内存仅增加了4倍。在未来,咱们会经常遇到无法练习模型的状况,因为它无法加载到单个GPU上。
在本节中,咱们将评论练习大型模型的常见战略。与第4.2节中介绍的数据并行战略不同,这儿介绍的办法需求在练习代码上付出努力的作业。
留意:尽管本节介绍的办法一般由数据科学家施行,但咱们期望您依然能够了解它们。了解这些练习技能背后的战略关于设计练习服务和练习代码之间的通讯协议十分有协助。它还为练习服务中的毛病排除或功用微调供给了洞察力。为了简化起见,咱们只会以概念层面描绘算法,并侧重从工程视点看所需的作业。
传统办法:内存节省
让咱们假定您的数据科学团队期望练习一个能够加载到练习集群中最大的GPU上的模型,例如,他们想在10GB内存的GPU上练习一个24GB的BERT模型。在这种状况下,团队能够运用几种节省内存的技能来练习模型,包含梯度累积和内存交流。这些作业一般由数据科学家来完结。作为渠道开发人员,您只需求了解这些选项即可。咱们将扼要介绍它们,以便您知道何时主张运用每种办法。
留意:还有其他几种节省内存的办法,例如OpenAI的梯度查看点技能(github.com/cybertronai…)和NVIDIA的vDNN(arxiv.org/abs/1602.08…),但因为本书不触及深度学习算法,咱们将留给独立研究。
梯度累积
在深度学习练习中,数据集被分红批次。在每个练习进程中,为了核算丢失、核算梯度和更新模型参数,咱们一次性将整个批次的示例(练习数据)加载到内存中,并进行核算。
经过减小批次巨细,咱们能够减轻内存压力,例如,将批次巨细从32减小到16。可是减小批次巨细或许会导致模型收敛速度变慢。这时,梯度累积就能够发挥作用。
梯度累积将批次示例分红可装备数量的小批次,然后在每个小批次之后核算丢失和梯度。可是,它不会当即更新模型参数,而是等候并累积一切小批次的梯度。最终,它依据累积的梯度更新模型参数。
让咱们经过一个示例来看看这怎么加速进程。假定因为GPU内存约束,咱们无法运用批次巨细为32进行练习。经过梯度累积,咱们能够将每个批次分红四个小批次,每个小批次巨细为8。因为咱们累积了一切四个小批次的梯度,而且只在悉数完结后更新模型,所以这个进程简直等同于运用批次巨细为32进行练习。不同之处在于,咱们在GPU中一次只核算8个示例,而不是32个,因而与32个批次比较,速度慢了4倍。
内存交流(GPU和CPU)
内存交流办法十分简略:它在CPU和GPU之间来回复制激活(activation)。假如您对深度学习术语不熟悉,能够将激活视为神经网络每个节点的核算输出。其思维是仅在当时核算进程中保留所需的数据在GPU上,并将核算结果交流到CPU内存中供将来的进程运用。
根据这个思维,一种名为L2L(从层到层)的新中继式履行技能仅将履行层和中间缓冲区保留在GPU上。整个模型和保存状况的优化器存储在CPU空间中。L2L能够大大进步GPU的吞吐量,并答应咱们在可承受的设备上开发大型模型。假如您对这种办法感兴趣,能够查看Pudipeddi等人的论文《Training Large Neural Networks with Constant Memory Using a New Execution Algorithm》(arxiv.org/abs/2002.05…),该论文还在GitHub上供给了PyTorch的完结。
梯度累积和内存交流都是在较小的GPU上练习大型模型的有效办法。可是,像大多数事物一样,它们也有一个代价:它们往往会减慢练习速度。因为这个缺陷,咱们一般只在原型验证阶段。
为了取得可行的练习速度,咱们的确需求在多个GPU上进行散布式练习。因而,鄙人一节中,咱们将介绍一种更挨近出产环境的办法:管道并行。它能够以惊人的练习速度对大型模型进行散布式练习。
管道模型并行
在第4.2节中,咱们评论了最常用的散布式练习办法:数据并行。该办法在每个设备上保存整个模型的副本,并将数据分割到多个设备中。然后,它聚合梯度并在每个练习进程中更新模型。数据并行的整个办法在能够将整个模型加载到一个GPU中时体现杰出。可是,正如咱们在本节中所看到的,咱们并不总是能够做到这一点。而这便是管道并行的用武之地。在本节中,咱们将学习管道并行,一种在多个GPU上散布式练习大型模型的办法。 为了了解管道并行,让咱们首要扼要了解模型并行。这个小插曲将使咱们更简略了解管道并行。
模型并行
模型并行的思维是将神经网络分割成较小的子网络,并在不同的GPU上运转每个子网络。图4.6阐明晰模型并行的办法。

图4.6展现了模型并行的进程。它首要将一个神经网络(四层)转换为四个子神经网络(单层),然后为每个单层网络分配一个独立的GPU。经过这样做,咱们在四个GPU上散布式地运转一个模型。 模型并行的概念很简略,但实践完结或许会比较扎手,这取决于网络的架构。为了让您有一个主意,以下是一个虚拟的PyTorch代码片段,使一个网络在两个GPU上运转。
gpu1 = 1
gpu2 = 2
class a_large_model(nn.Module):
def __init__(self):
super().__init__()
# initialize the network as two subnetworks.
self.subnet1 = ...
self.subnet2 = ...
# put subnetwork 1 and 2 to two different GPUs
self.subnet1.cuda(gpu1)
self.subnet2.cuda(gpu2)
def forward(x):
# load data to GPU 1 and calculate output for
# subnet 1, GPU 2 is idle at the moment.
x = x.cuda(gpu1)
x = self.subnet1(x)
# move the output of subnet 1 to GPU 2 and calculate
# output for subnet 2. GPU 1 is idle
x = x.cuda(gpu2)
x = self.sub_network2(x)
return x
正如您在4.2节的代码清单4.2中所看到的,两个子网络在__init__函数中初始化并分配给两个GPU,然后在forward函数中连接起来。因为深度学习网络的结构多样性,没有通用的办法(范例)来拆分网络。咱们有必要依据具体状况完结模型并行。
模型并行的另一个问题是其对GPU资源的严峻低效运用。因为练习组中的一切设备都有次序依靠性,一次只能有一个设备作业,这糟蹋了很多的GPU周期。图4.7可视化了运用三个GPU进行模型并行练习时的GPU运用状况。
让咱们经过这个图来看看为什么GPU运用率如此低。在左边的图4.7(a)中,咱们看到了模型并行的设计。咱们将一个模型网络拆分为三个子网络,并让每个子网络在不同的GPU上运转。在每个练习迭代中,在进行前向传达时,咱们首要核算子网络1,然后是子网络2和子网络3;在进行反向传达时,梯度更新是反向进行的。 在右侧的图4.7(b)中,您能够看到练习期间三个GPU的资源运用状况。时刻轴分为两个部分:前向传达和反向传达。前向传达表明模型推理的核算,从GPU 1到GPU 2和GPU 3,而反向传达表明模型权重更新的反向传达,从GPU 3到GPU 2和GPU 1。

不管是前向传达仍是反向传达,假如您笔直查看时刻条,您会发现一次只需一个GPU处于活动状况。这是因为每个子网络之间存在次序依靠联系。例如,在前向传达中,子网络2需求等候子网络1的输出以完结自身的前向核算,因而在前向传达进程中,GPU 2将在GPU 1的核算完结之前处于闲暇状况。
不管您增加多少个GPU,一次只需一个GPU能够作业,这是一种巨大的糟蹋。这便是流水线并行的用武之地。流水线并行经过消除这种糟蹋并充分运用GPU来使模型练习愈加高效。让咱们来看看它是怎么作业的。
流水线并行
流水线并行本质上是模型并行的改善版。除了将网络分割到不同的GPU上,它还将每个练习示例批次划分为小的小批次,并在层之间堆叠这些小批次的核算。经过这样做,它能够让一切的GPU大部分时刻都坚持繁忙状况,然后进步了GPU的运用率。
这种办法有两个首要的完结:PipeDream(微软)和GPipe(谷歌)。咱们在这儿运用GPipe作为演示示例,因为它在每个练习进程中优化了梯度的更新,并具有更好的练习吞吐量。您能够从Huang等人的论文“GPipe: Easy scaling with micro-batch pipeline parallelism”中了解更多关于GPipe的细节(arxiv.org/abs/1811.06…)。让咱们在图4.8中以高层次的办法看一下GPipe的作业原理。

图4.8(a)描绘了由四个子网络组成的神经网络;每个子网络加载在一个GPU上。F表明前向传达,B表明反向传达,Fk和Bk在GPUk上运转。练习的次序是首要进行前向传达:F0 -> F1 -> F2 -> F3,然后进行反向传达:F3 -> (B3, F2) -> (B2, F2) -> (B1, F1) -> B0。 图4.8(b)展现了传统的模型并行办法的练习流程。咱们能够看到GPU的运用率十分低;在前向传达和反向传达中,只需一个GPU处于活动状况;因而,每个GPU有75%的时刻处于闲暇状况。
图4.8(c)展现了GPipe在练习操作次序上的改善。GPipe首要将每个练习示例批次分为四个持平的微批次,并经过四个GPU进行流水线处理。图中的F(0,2)表明在GPU 0上运用微批次2进行的前向传达核算。在反向传达进程中,依据用于前向传达的相同模型参数核算每个微批次的梯度。要害是它不会当即更新模型参数;相反,它会累积每个微批次的一切梯度。在每个练习批次结束时,咱们运用来自一切四个微批次的累积梯度来更新跨一切四个GPU的模型参数。
经过比较图4.8(b)和(c),咱们能够看到GPU运用率大大进步;现在每个GPU在47%的时刻内处于闲暇状况。让咱们看一个运用PyTorch GPipe完结的代码示例,运用两个GPU练习一个Transformer模型(请拜见下面的示例代码)。为了明晰地演示这个主意,咱们只保留了与流水线相关的代码,并将其分为四个部分。您能够查看Pritam Damania的教程“PyTorch: Training transformer models using pipeline parallelism”以获取完整的代码(mng.bz/5mD8)。
## Part One: initialize remote communication
# for multiple machines
rpc.init_rpc(
name="worker",
# set rank number to this node, rank is the global
# unique id of a node, 0 is the master,
# other ranks are observers
rank=0,
# set the number of workers in the group
world_size=1,.. .. .. )
.. .. ..
## Part Two: split model to 2 subnetworks, load
# to different GPUs and initialize the pipeline.
num_gpus = 2
partition_len = ((nlayers - 1) // num_gpus) + 1
# Add all the necessary transformer blocks.
for i in range(nlayers):
transformer_block = TransformerEncoderLayer(emsize, nhead, nhid, dropout)
.. .. ..
# Load first half encoder layers to GPU 0 and second hard encoder layers to GPU 1.
device = i // (partition_len) tmp_list.append(**transformer_block.to(device)** )
# Load decoder to GPU 1.
tmp_list.append(Decoder(ntokens, emsize).cuda(num_gpus - 1)) module_list.append(nn.Sequential(*tmp_list))
## Part Three**: Build up the pipeline.
chunks = 8 # Set micro-batches number to 8.
model = Pipe(torch.nn.Sequential(*module_list), chunks = chunks)
.. .. ..
## Part 4: Train with pipeline
def train():
model.train() # Turn on the train mode
.. .. ..
for batch, i in enumerate(range(0, nbatches, bptt)):
data, targets = get_batch(train_data, i)
optimizer.zero_grad()
# Compute pipeline output,by following the pipeline setup,
# the Pytorch framework will coordinate the network computation
# between GPU 0 and GPU 1.
# Since the Pipe is only within a single host and process the "RRef"
# returned by forward method is local to this node and can simply
# retrieved via "RRef.local_value()".
output = model(data).local_value()
# Compute the loss on GPU 1.
# Need to move targets to the device where the output of the # pipeline resides.
loss = criterion(output.view(-1, ntokens), targets.cuda(1))
# Backprop and model parameters update are the same as single GPU training.
# The Pytorch framework hides all the details of micro-batches
# computation and model parameters update.
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 0.5)
optimizer.step()
.. .. ..
正如咱们从代码清单4.3中能够看到的那样,流水线并行代码比散布式数据并行代码要杂乱得多。除了设置通讯组之外,咱们还需求考虑怎么划分模型网络以及在作业节点之间进行梯度和激活值(子网络的前向输出)的传输。
软件工程师怎样支撑流水线并行
你或许现已留意到,在本节中咱们评论的一切办法都是用于编写练习代码的技能。因为数据科学家一般编写练习代码,你或许想知道作为软件开发人员,咱们能够做些什么来支撑流水线并行练习。
首要,咱们能够致力于构建练习服务,以主动化流水线练习的履行并改善资源运用率(例如,一直坚持 GPU 繁忙)。这种主动化包含分配作业资源、启用作业进程间通讯,并将相应的初始化参数与流水线练习代码分发给每个作业进程(例如作业进程的 IP 地址、进程 ID、GPU ID 和作业组巨细)。
其次,咱们能够向数据科学家团队介绍新的散布式练习选项。有时数据科学家团队或许不知道能够改善模型练习体会的新工程办法,因而沟通至关重要。咱们能够与团队成员合作,并引导关于尝试流水线并行办法的评论。
第三,咱们能够致力于进步模型练习的可用性。在4.2.4节中,咱们评论了散布式练习的脆弱性;它要求每个作业进程坚持共同的运转。假如一个作业进程出现毛病,整个练习组都会失利,这将是时刻和预算的巨大糟蹋。数据科学家会十分感激咱们在练习进程监控、毛病转移和毛病康复方面所做的努力。
总结
- 散布式练习有两种思路:数据并行和模型并行。管道并行是模型并行的改善版别。
- 假如模型能够加载到一个GPU中,数据并行是完结散布式练习的首要办法;它简略易用且能显著进步练习速度。
- 运用Kubernetes来办理核算集群能够大大下降核算资源办理的杂乱性。
- 尽管每个练习结构(TensorFlow、PyTorch)供给了不同的装备和API来编写散布式练习代码,但它们的代码形式和履行流程十分相似。因而,练习服务能够运用一致的办法支撑各种散布式练习代码。
- 在封装各种练习结构的设置装备之后,练习服务依然能够将练习代码视为一个黑盒,在散布式练习环境中运用。
- 要获取数据并行练习的发展/状况,只需查看主作业进程,因为一切作业进程一直坚持同步。此外,为了防止在练习作业完结时从一切作业进程保存重复的模型,能够将练习代码设置为仅在主作业进程履行时耐久化模型和查看点文件。
- Horovod是一个很好的散布式练习结构。它供给了一个一致的办法来运转运用不同结构编写的散布式练习代码:PyTorch、TensorFlow、MXNet和PySpark。假如练习代码运用Horovod来完结散布式练习,练习服务能够运用单个办法(Horovod办法)来履行它,不管它运用哪个练习结构编写。
- 可用性、弹性和毛病康复是散布式练习的重要工程问题。
- 关于无法放入一个GPU中的模型,有两种战略:节省内存的办法和模型并行的办法。
- 节省内存的办法每次只加载模型的一部分或小批量数据到GPU中,例如梯度累积和内存交流。这些办法易于完结,但会下降模型练习速度。
- 模型并行办法将大模型划分为一组子神经网络,并将它们散布在多个GPU上。这种办法的缺陷是GPU运用率低。为了克服这一问题,发明晰管道模型并行。