未验证 提交 6f23af8e 编写于 作者: G goldmermaid 提交者: GitHub

[polish] chapter 12 peer review (#1018)

* chapter12 0-3 polish

* soft polish chapter 12 done
上级 62914ade
......@@ -45,7 +45,7 @@ with d2l.Benchmark('mxnet.np'):
```{.python .input}
#@tab pytorch
# GPU 计算热身
# GPU计算热身
device = d2l.try_gpu()
a = torch.randn(size=(1000, 1000), device=device)
b = torch.mm(a, a)
......@@ -94,7 +94,7 @@ with d2l.Benchmark():
广义上说,PyTorch有一个用于与用户直接交互的前端(例如通过Python),还有一个由系统用来执行计算的后端。如 :numref:`fig_frontends`所示,用户可以用各种前端语言编写Python程序,如Python和C++。不管使用的前端编程语言是什么,PyTorch程序的执行主要发生在C++实现的后端。由前端语言发出的操作被传递到后端执行。后端管理自己的线程,这些线程不断收集和执行排队的任务。请注意,要使其工作,后端必须能够跟踪计算图中各个步骤之间的依赖关系。因此,不可能并行化相互依赖的操作。
:end_tab:
![编程语言前端和深度学习框架后端](../img/frontends.png)
![编程语言前端和深度学习框架后端](../img/frontends.png)
:width:`300px`
:label:`fig_frontends`
......@@ -115,12 +115,12 @@ z = x * y + 2
z
```
![后端跟踪计算图中各个步骤之间的依赖关系](../img/asyncgraph.svg)
![后端跟踪计算图中各个步骤之间的依赖关系](../img/asyncgraph.svg)
:label:`fig_asyncgraph`
上面的代码片段在 :numref:`fig_asyncgraph`中进行了说明。每当Python前端线程执行前三条语句中的一条语句时,它只是将任务返回到后端队列。当最后一个语句的结果需要被打印出来时,Python前端线程将等待C++后端线程完成变量`z`的结果计算。这种设计的一个好处是Python前端线程不需要执行实际的计算。因此,不管Python的性能如何,对程序的整体性能几乎没有影响。 :numref:`fig_threading`演示了前端和后端如何交互。
![前端和后端的交互](../img/threading.svg)
![前端和后端的交互](../img/threading.svg)
:label:`fig_threading`
## 障碍器与阻塞器
......
......@@ -141,7 +141,7 @@ with d2l.Benchmark('复制到CPU'):
:end_tab:
:begin_tab:`pytorch`
这种方式效率不高。注意到当列表中的其余部分还在计算时,我们可能就已经开始将`y`的部分复制到CPU了。例如,当我们计算一个小批量的(反传)梯度时。某些参数的梯度将比其他参数的梯度更早可用。因此,在GPU仍在运行时就开始使用PCI-Express总线带宽来移动数据对我们是有利的。在PyTorch中,`to() ``copy_()`等函数都允许显式的`non_blocking`参数,这允许在不需要同步时调用方可以绕过同步。设置`non_blocking=True`让我们模拟这个场景。
这种方式效率不高。注意到当列表中的其余部分还在计算时,我们可能就已经开始将`y`的部分复制到CPU了。例如,当我们计算一个小批量的(反传)梯度时。某些参数的梯度将比其他参数的梯度更早可用。因此,在GPU仍在运行时就开始使用PCI-Express总线带宽来移动数据对我们是有利的。在PyTorch中,`to()``copy_()`等函数都允许显式的`non_blocking`参数,这允许在不需要同步时调用方可以绕过同步。设置`non_blocking=True`让我们模拟这个场景。
:end_tab:
```{.python .input}
......@@ -159,11 +159,11 @@ with d2l.Benchmark('在GPU1上运行并复制到CPU'):
torch.cuda.synchronize()
```
两个操作所需的总时间少于它们各部分操作所需时间的总和。请注意,与并行计算的区别是通信操作使用的资源:CPU和GPU之间的总线。事实上,我们可以在两个设备上同时进行计算和通信。如上所述,计算和通信之间存在的依赖关系是必须先计算`y[i]`,然后才能将其复制到CPU。幸运的是,系统可以在计算`y[i]` 的同时复制`y[i-1]`,以减少总的运行时间。
两个操作所需的总时间少于它们各部分操作所需时间的总和。请注意,与并行计算的区别是通信操作使用的资源:CPU和GPU之间的总线。事实上,我们可以在两个设备上同时进行计算和通信。如上所述,计算和通信之间存在的依赖关系是必须先计算`y[i]`,然后才能将其复制到CPU。幸运的是,系统可以在计算`y[i]`的同时复制`y[i-1]`,以减少总的运行时间。
最后,我们给出了一个简单的两层多层感知机在CPU和两个GPU上训练时的计算图及其依赖关系的例子,如 :numref:`fig_twogpu`所示。手动调度由此产生的并行程序将是相当痛苦的。这就是基于图的计算后端进行优化的优势所在。
![在一个 CPU 和两个 GPU 上的两层的多层感知机的计算图及其依赖关系。](../img/twogpu.svg)
![在一个CPU和两个GPU上的两层的多层感知机的计算图及其依赖关系](../img/twogpu.svg)
:label:`fig_twogpu`
## 小结
......@@ -174,7 +174,7 @@ with d2l.Benchmark('在GPU1上运行并复制到CPU'):
## 练习
1. 在本节定义的`run` 函数中执行了八个操作,并且操作之间没有依赖关系。设计一个实验,看看深度学习框架是否会自动地并行地执行它们。
1. 在本节定义的`run`函数中执行了八个操作,并且操作之间没有依赖关系。设计一个实验,看看深度学习框架是否会自动地并行地执行它们。
1. 当单个操作符的工作量足够小,即使在单个CPU或GPU上,并行化也会有所帮助。设计一个实验来验证这一点。
1. 设计一个实验,在CPU和GPU这两种设备上使用并行计算和通信。
1. 使用诸如NVIDIA的[Nsight](https://developer.nvidia.com/nsight-compute-2019_5) 之类的调试器来验证你的代码是否有效。
......
# 编译器和解释器
:label:`sec_hybridize`
目前为止,本书主要关注的是*命令式编程*(imperative programming)。命令式编程使用诸如`print``+``if`之类的语句来更改程序的状态。考虑下面这段简单的命令式程序。
目前为止,本书主要关注的是*命令式编程*(imperative programming)。
命令式编程使用诸如`print`、“`+`”和`if`之类的语句来更改程序的状态。
考虑下面这段简单的命令式程序:
```{.python .input}
#@tab all
......@@ -19,7 +21,7 @@ print(fancy_func(1, 2, 3, 4))
Python是一种*解释型语言*(interpreted language)。因此,当对上面的`fancy_func`函数求值时,它按顺序执行函数体的操作。也就是说,它将通过对`e = add(a, b)`求值,并将结果存储为变量`e`,从而更改程序的状态。接下来的两个语句`f = add(c, d)``g = add(e, f)`也将执行类似地操作,即执行加法计算并将结果存储为变量。 :numref:`fig_compute_graph`说明了数据流。
![命令式编程中的数据流](../img/computegraph.svg)
![命令式编程中的数据流](../img/computegraph.svg)
:label:`fig_compute_graph`
尽管命令式编程很方便,但可能效率不高。一方面原因,Python会单独执行这三个函数的调用,而没有考虑`add`函数在`fancy_func`中被重复调用。如果在一个GPU(甚至多个GPU)上执行这些命令,那么Python解释器产生的开销可能会非常大。此外,它需要保存`e``f`的变量值,直到`fancy_func`中的所有语句都执行完毕。这是因为程序不知道在执行语句`e = add(a, b)``f = add(c, d)`之后,其他部分是否会使用变量`e``f`
......@@ -195,7 +197,7 @@ net(x)
#@tab all
#@save
class Benchmark:
"""用于测量运行时间"""
"""用于测量运行时间"""
def __init__(self, description='Done'):
self.description = description
......@@ -377,7 +379,7 @@ net(x)
:end_tab:
:begin_tab:`pytorch,tensorflow`
1. 回顾前几章中你感兴趣的模型,你能通过重新实现它们来提高它们的计算性能吗?
1. 回顾前几章中你感兴趣的模型,你能提高它们的计算性能吗?
:end_tab:
:begin_tab:`mxnet`
......
# 计算性能
:label:`chap_performance`
在深度学习中,数据集和模型通常都很大,导致计算量也会很大。因此,计算的性能非常重要。本章将集中讨论影响计算性能的主要因素:命令式编程、符号编程、异步计算、自动并行和多GPU计算。通过学习本章,你可以进一步提高前几章中实现的那些模型的计算性能。例如,我们可以在不影响准确性的前提下,减少训练时间。
在深度学习中,数据集和模型通常都很大,导致计算量也会很大。
因此,计算的性能非常重要。
本章将集中讨论影响计算性能的主要因素:命令式编程、符号编程、
异步计算、自动并行和多GPU计算。
通过学习本章,对于前几章中实现的那些模型,你可以进一步提高它们的计算性能。
例如,我们可以在不影响准确性的前提下,大大减少训练时间。
```toc
:maxdepth: 2
......
......@@ -24,7 +24,7 @@ from torch import nn
```{.python .input}
#@save
def resnet18(num_classes):
"""稍加修改的 ResNet-18 模型。"""
"""稍加修改的ResNet-18模型"""
def resnet_block(num_channels, num_residuals, first_block=False):
blk = nn.Sequential()
for i in range(num_residuals):
......@@ -36,7 +36,7 @@ def resnet18(num_classes):
return blk
net = nn.Sequential()
# 该模型使用了更小的卷积核、步长和填充,而且删除了最大汇聚层
# 该模型使用了更小的卷积核、步长和填充,而且删除了最大汇聚层
net.add(nn.Conv2D(64, kernel_size=3, strides=1, padding=1),
nn.BatchNorm(), nn.Activation('relu'))
net.add(resnet_block(64, 2, first_block=True),
......@@ -51,7 +51,7 @@ def resnet18(num_classes):
#@tab pytorch
#@save
def resnet18(num_classes, in_channels=1):
"""稍加修改的 ResNet-18 模型。"""
"""稍加修改的ResNet-18模型"""
def resnet_block(in_channels, out_channels, num_residuals,
first_block=False):
blk = []
......@@ -63,12 +63,13 @@ def resnet18(num_classes, in_channels=1):
blk.append(d2l.Residual(out_channels, out_channels))
return nn.Sequential(*blk)
# 该模型使用了更小的卷积核、步长和填充,而且删除了最大汇聚层
# 该模型使用了更小的卷积核、步长和填充,而且删除了最大汇聚层
net = nn.Sequential(
nn.Conv2d(in_channels, 64, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(64),
nn.ReLU())
net.add_module("resnet_block1", resnet_block(64, 64, 2, first_block=True))
net.add_module("resnet_block1", resnet_block(
64, 64, 2, first_block=True))
net.add_module("resnet_block2", resnet_block(64, 128, 2))
net.add_module("resnet_block3", resnet_block(128, 256, 2))
net.add_module("resnet_block4", resnet_block(256, 512, 2))
......@@ -185,8 +186,8 @@ def train(num_gpus, batch_size, lr):
npx.waitall()
timer.stop()
animator.add(epoch + 1, (evaluate_accuracy_gpus(net, test_iter),))
print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch '
f'on {str(ctx)}')
print(f'测试精度:{animator.Y[0][-1]:.2f},{timer.avg():.1f}秒/轮,'
f'{str(ctx)}')
```
```{.python .input}
......@@ -198,7 +199,7 @@ def train(net, num_gpus, batch_size, lr):
if type(m) in [nn.Linear, nn.Conv2d]:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights)
# 在多个 GPU 上设置模型
# 在多个GPU上设置模型
net = nn.DataParallel(net, device_ids=devices)
trainer = torch.optim.SGD(net.parameters(), lr)
loss = nn.CrossEntropyLoss()
......@@ -215,8 +216,8 @@ def train(net, num_gpus, batch_size, lr):
trainer.step()
timer.stop()
animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu(net, test_iter),))
print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch '
f'on {str(devices)}')
print(f'测试精度:{animator.Y[0][-1]:.2f},{timer.avg():.1f}秒/轮,'
f'{str(devices)}')
```
让我们看看这在实践中是如何运作的。我们先[**在单个GPU上训练网络**]进行预热。
......@@ -245,27 +246,27 @@ train(net, num_gpus=2, batch_size=512, lr=0.2)
:begin_tab:`mxnet`
* Gluon通过提供一个上下文列表,为跨多个设备的模型初始化提供原语。
* *神经网络可以在(可找到数据的)单GPU上进行自动评估。
* 注意每台设备上的网络需要先初始化,然后再尝试访问该设备上的参数,否则会遇到错误。
* 神经网络可以在(可找到数据的)单GPU上进行自动评估。
* 每台设备上的网络需要先初始化,然后再尝试访问该设备上的参数,否则会遇到错误。
* 优化算法在多个GPU上自动聚合。
:end_tab:
:begin_tab:`pytorch`
* 神经网络可以在(可找到数据的)单GPU上进行自动评估。
* 注意每台设备上的网络需要先初始化,然后再尝试访问该设备上的参数,否则会遇到错误。
* 每台设备上的网络需要先初始化,然后再尝试访问该设备上的参数,否则会遇到错误。
* 优化算法在多个GPU上自动聚合。
:end_tab:
## 练习
:begin_tab:`mxnet`
1. 本节使用ResNet-18尝试不同的迭代周期数、批量大小和学习率,以及使用更多的GPU进行计算。如果使用$16$个GPU(例如,在AWS p2.16xlarge实例上)尝试此操作,会发生什么?
1. 本节使用ResNet-18,请尝试不同的迭代周期数、批量大小和学习率,以及使用更多的GPU进行计算。如果使用$16$个GPU(例如,在AWS p2.16xlarge实例上)尝试此操作,会发生什么?
1. 有时候不同的设备提供了不同的计算能力,我们可以同时使用GPU和CPU,那应该如何分配工作?为什么?
1. 如果去掉`npx.waitall()`会怎样?你将如何修改训练,以使并行操作最多有两个步骤重叠?
:end_tab:
:begin_tab:`pytorch`
1. 本节使用ResNet-18尝试不同的迭代周期数、批量大小和学习率,以及使用更多的GPU进行计算。如果使用$16$个GPU(例如,在AWS p2.16xlarge实例上)尝试此操作,会发生什么?
1. 本节使用ResNet-18,请尝试不同的迭代周期数、批量大小和学习率,以及使用更多的GPU进行计算。如果使用$16$个GPU(例如,在AWS p2.16xlarge实例上)尝试此操作,会发生什么?
1. 有时候不同的设备提供了不同的计算能力,我们可以同时使用GPU和CPU,那应该如何分配工作?为什么?
:end_tab:
......
......@@ -34,10 +34,10 @@
当通道或单元的数量不太小时,使计算性能有良好的提升。
此外,由于可用的显存呈线性扩展,多个GPU能够处理不断变大的网络。
![由于GPU显存有限,原有AlexNet设计中的模型并行](../img/alexnet-original.svg)
![由于GPU显存有限,原有AlexNet设计中的模型并行](../img/alexnet-original.svg)
:label:`fig_alexnet_original`
然而,我们需要大量的同步或*屏障操作*(barrier operations),因为每一层都依赖于所有其他层的结果。
然而,我们需要大量的同步或*屏障操作*(barrier operation),因为每一层都依赖于所有其他层的结果。
此外,需要传输的数据量也可能比跨GPU拆分层时还要大。
因此,基于带宽的成本和复杂性,我们同样不推荐这种方法。
......@@ -49,7 +49,7 @@
而且,GPU的数量越多,小批量包含的数据量就越大,从而就能提高训练效率。
但是,添加更多的GPU并不能让我们训练更大的模型。
![在多个GPU上并行化。从左到右:原始问题、网络并行、分层并行、数据并行](../img/splitting.svg)
![在多个GPU上并行化。从左到右:原始问题、网络并行、分层并行、数据并行](../img/splitting.svg)
:label:`fig_splitting`
:numref:`fig_splitting`中比较了多个GPU上不同的并行方式。
......@@ -64,7 +64,7 @@
给定需要训练的模型,虽然每个GPU上的参数值都是相同且同步的,但是每个GPU都将独立地维护一组完整的模型参数。
例如, :numref:`fig_data_parallel`演示了在$k=2$时基于数据并行方法训练模型。
![利用两个GPU上的数据,并行计算小批量随机梯度下降](../img/data-parallel.svg)
![利用两个GPU上的数据,并行计算小批量随机梯度下降](../img/data-parallel.svg)
:label:`fig_data_parallel`
一般来说,$k$个GPU并行训练过程如下:
......@@ -199,11 +199,11 @@ def get_params(params, device):
```{.python .input}
#@tab all
new_params = get_params(params, d2l.try_gpu(0))
print('b1 weight:', new_params[1])
print('b1 grad:', new_params[1].grad)
print('b1 权重:', new_params[1])
print('b1 梯度:', new_params[1].grad)
```
由于还没有进行任何计算,因此偏置参数的梯度仍然为零。
由于还没有进行任何计算,因此权重参数的梯度仍然为零。
假设现在有一个向量分布在多个GPU上,下面的[**`allreduce`函数将所有向量相加,并将结果广播给所有GPU**]。
请注意,我们需要将数据复制到累积结果的设备,才能使函数正常工作。
......@@ -228,17 +228,17 @@ def allreduce(data):
```{.python .input}
data = [np.ones((1, 2), ctx=d2l.try_gpu(i)) * (i + 1) for i in range(2)]
print('before allreduce:\n', data[0], '\n', data[1])
print('allreduce之前:\n', data[0], '\n', data[1])
allreduce(data)
print('after allreduce:\n', data[0], '\n', data[1])
print('allreduce之后:\n', data[0], '\n', data[1])
```
```{.python .input}
#@tab pytorch
data = [torch.ones((1, 2), device=d2l.try_gpu(i)) * (i + 1) for i in range(2)]
print('before allreduce:\n', data[0], '\n', data[1])
print('allreduce之前:\n', data[0], '\n', data[1])
allreduce(data)
print('after allreduce:\n', data[0], '\n', data[1])
print('allreduce之后:\n', data[0], '\n', data[1])
```
## 数据分发
......@@ -251,9 +251,9 @@ print('after allreduce:\n', data[0], '\n', data[1])
data = np.arange(20).reshape(4, 5)
devices = [npx.gpu(0), npx.gpu(1)]
split = gluon.utils.split_and_load(data, devices)
print('input :', data)
print('load into', devices)
print('output:', split)
print('输入:', data)
print('设备:', devices)
print('输出:', split)
```
```{.python .input}
......@@ -324,7 +324,8 @@ def train_batch(X, y, device_params, devices, lr):
# 将每个GPU的所有梯度相加,并将其广播到所有GPU
with torch.no_grad():
for i in range(len(device_params[0])):
allreduce([device_params[c][i].grad for c in range(len(devices))])
allreduce(
[device_params[c][i].grad for c in range(len(devices))])
# 在每个GPU上分别更新模型参数
for param in device_params:
d2l.sgd(param, lr, X.shape[0]) # 在这里,我们使用全尺寸的小批量
......@@ -339,7 +340,7 @@ def train_batch(X, y, device_params, devices, lr):
def train(num_gpus, batch_size, lr):
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
devices = [d2l.try_gpu(i) for i in range(num_gpus)]
# 将模型参数复制到 `num_gpus` 个GPU
# 将模型参数复制到num_gpus个GPU
device_params = [get_params(params, d) for d in devices]
num_epochs = 10
animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs])
......@@ -354,8 +355,8 @@ def train(num_gpus, batch_size, lr):
# 在GPU 0 上评估模型
animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu(
lambda x: lenet(x, device_params[0]), test_iter, devices[0]),))
print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch '
f'on {str(devices)}')
print(f'测试精度:{animator.Y[0][-1]:.2f},{timer.avg():.1f}秒/轮,'
f'{str(devices)}')
```
```{.python .input}
......@@ -363,7 +364,7 @@ def train(num_gpus, batch_size, lr):
def train(num_gpus, batch_size, lr):
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
devices = [d2l.try_gpu(i) for i in range(num_gpus)]
# 将模型参数复制到`num_gpus`个GPU
# 将模型参数复制到num_gpus个GPU
device_params = [get_params(params, d) for d in devices]
num_epochs = 10
animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs])
......@@ -378,8 +379,8 @@ def train(num_gpus, batch_size, lr):
# 在GPU 0上评估模型
animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu(
lambda x: lenet(x, device_params[0]), test_iter, devices[0]),))
print(f'test acc: {animator.Y[0][-1]:.2f}, {timer.avg():.1f} sec/epoch '
f'on {str(devices)}')
print(f'测试精度:{animator.Y[0][-1]:.2f},{timer.avg():.1f}秒/轮,'
f'{str(devices)}')
```
让我们看看[**在单个GPU上运行**]效果得有多好。
......@@ -411,7 +412,7 @@ train(num_gpus=2, batch_size=256, lr=0.2)
## 练习
1. 在$k$个GPU上进行训练时,将批量大小从$b$更改为$k \cdot b$,即按GPU的数量进行扩展。
1. 比较不同学习率时模型的精确度随着GPU数量的增加学习率应该如何扩展?
1. 比较不同学习率时模型的精确度随着GPU数量的增加学习率应该如何扩展?
1. 实现一个更高效的`allreduce`函数用于在不同的GPU上聚合不同的参数?为什么这样的效率更高?
1. 实现模型在多GPU下测试精度的计算。
......
......@@ -7,12 +7,12 @@
## 数据并行训练
让我们回顾一下在分布式架构中数据并行的训练方法,因为在实践中它的实现相对简单,因此本节将排除其他内容只对其进行介绍。由于当今的GPU拥有大量的显存,因此在实际场景中(不包括图深度学习)只有数据并行这种并行训练策略值得推荐。图 :numref:`fig_parameterserver`描述了在 :numref:`sec_multi_gpu`中实现的数据并行的变体。其中的关键是梯度的聚合需要在GPU0上完成,然后再将更新后的参数广播给所有GPU。
让我们回顾一下在分布式架构中数据并行的训练方法,因为在实践中它的实现相对简单,因此本节将排除其他内容只对其进行介绍。由于当今的GPU拥有大量的显存,因此在实际场景中(不包括图深度学习)只有数据并行这种并行训练策略值得推荐。图 :numref:`fig_parameterserver`描述了在 :numref:`sec_multi_gpu`中实现的数据并行的变体。其中的关键是梯度的聚合需要在GPU 0上完成,然后再将更新后的参数广播给所有GPU。
![左图:单GPU训练。右图:多GPU训练的一个变体:(1)计算损失和梯度,(2)所有梯度聚合在一个GPU上,(3)发生参数更新,并将参数重新广播给所有GPU。](../img/ps.svg)
![左图:单GPU训练;右图:多GPU训练的一个变体:(1)计算损失和梯度,(2)所有梯度聚合在一个GPU上,(3)发生参数更新,并将参数重新广播给所有GPU](../img/ps.svg)
:label:`fig_parameterserver`
回顾来看,选择GPU0进行聚合似乎是个很随便的决定,当然也可以选择CPU上聚合,事实上只要优化算法支持,在实际操作中甚至可以在某个GPU上聚合其中一些参数,而在另一个GPU上聚合另一些参数。例如,如果有四个与参数向量相关的梯度$\mathbf{g}_1, \ldots, \mathbf{g}_4$,还可以一个GPU对一个$\mathbf{g}_i (i = 1, \ldots, 4$)地进行梯度聚合。
回顾来看,选择GPU 0进行聚合似乎是个很随便的决定,当然也可以选择CPU上聚合,事实上只要优化算法支持,在实际操作中甚至可以在某个GPU上聚合其中一些参数,而在另一个GPU上聚合另一些参数。例如,如果有四个与参数向量相关的梯度$\mathbf{g}_1, \ldots, \mathbf{g}_4$,还可以一个GPU对一个$\mathbf{g}_i (i = 1, \ldots, 4$)地进行梯度聚合。
这样的推断似乎是轻率和武断的,毕竟数学应该是逻辑自洽的。但是,我们处理的是如 :numref:`sec_hardware`中所述的真实的物理硬件,其中不同的总线具有不同的带宽。考虑一个如 :numref:`sec_hardware`中所述的真实的$4$路GPU服务器。如果它的连接是特别完整的,那么可能拥有一个100GbE的网卡。更有代表性的数字是1-10GbE范围内,其有效带宽为100MB/s到1GB/s。因为CPU的PCIe通道太少(例如,消费级的Intel CPU有$24$个通道),所以无法直接与所有的GPU相连接,因此需要[multiplexer](https://www.broadcom.com/products/pcie-switches-bridges/pcie-switches)。CPU在16x Gen3链路上的带宽为16GB/s,这也是每个GPU连接到交换机的速度,这意味着GPU设备之间的通信更有效。
......@@ -30,18 +30,18 @@
当谈及现代深度学习硬件的同步问题时,我们经常会遇到大量的定制的网络连接。例如,AWS p3.16xlarge和NVIDIA DGX-2实例中的连接都使用了 :numref:`fig_nvlink`中的结构。每个GPU通过PCIe链路连接到主机CPU,该链路最多只能以16GB/s的速度运行。此外,每个GPU还具有$6$个NVLink连接,每个NVLink连接都能够以300Gbit/s进行双向传输。这相当于每个链路每个方向约$300\div 8\div 2\approx 18 \mathrm{GB/s}$。简言之,聚合的NVLink带宽明显高于PCIe带宽,问题是如何有效地使用它。
![ 8 台 V100 GPU 服务器上连接 NVLink(图片由英伟达提供)](../img/nvlink.svg)
![8台V100 GPU服务器上连接NVLink(图片由英伟达提供)](../img/nvlink.svg)
:label:`fig_nvlink`
:cite:`Wang.Li.Liberty.ea.2018`的研究结果表明最优的同步策略是将网络分解成两个环,并基于两个环直接同步数据。
:numref:`fig_nvlink_twoloop`描述了网络可以分解为一个具有双NVLink带宽的环(1-2-3-4-5-6-7-8-1)和一个具有常规带宽的环(1-4-6-3-5-8-2-7-1)。在这种情况下,设计一个高效的同步协议是非常重要的。
![ NVLink 网络分解为两个环。](../img/nvlink-twoloop.svg)
![NVLink网络分解为两个环。](../img/nvlink-twoloop.svg)
:label:`fig_nvlink_twoloop`
考虑下面的思维试验:给定由$n$个计算节点(或GPU)组成的一个环,梯度可以从第一个节点发送到第二个节点,在第二个结点将本地的梯度与传送的梯度相加并发送到第三个节点,依此类推。在$n-1$步之后,可以在最后访问的节点中找到聚合梯度。也就是说,聚合梯度的时间随节点数线性增长。但如果照此操作,算法是相当低效的。归根结底,在任何时候都只有一个节点在通信。如果我们将梯度分为$n$个块,并从节点$i$开始同步块$i$,会怎么样?因为每个块的大小是$1/n$,所以总时间现在是$(n-1)/n \approx 1$。换句话说,当我们增大环的大小时,聚合梯度所花费的时间不会增加。这是一个相当惊人的结果。 :numref:`fig_ringsync`说明了$n=4$个节点上的步骤顺序。
![跨4个节点的环同步。每个节点开始向其左邻居发送部分梯度,直到在其右邻居中找到聚合的梯度](../img/ringsync.svg)
![跨4个节点的环同步。每个节点开始向其左邻居发送部分梯度,直到在其右邻居中找到聚合的梯度](../img/ringsync.svg)
:label:`fig_ringsync`
如果我们使用相同的例子,跨$8$个V100 GPU同步160MB,我们得到的结果大约是$2 \times 160 \mathrm{MB} \div (3 \times18 \mathrm{GB/s}) \approx 6 \mathrm{ms}$。这比使用PCIe总线要好,即使我们现在使用的是$8$个GPU。请注意,这些数字在实践中通常会差一些,因为深度学习框架无法将通信组合成大的突发传输。
......@@ -61,12 +61,12 @@
6. 更新后的参数信息发送到本地一个(或多个)GPU中。
7. 所有GPU上的参数更新完成。
![多机多 GPU 分布式并行训练。](../img/ps-multimachine.svg)
![多机多GPU分布式并行训练](../img/ps-multimachine.svg)
:label:`fig_ps_multimachine`
以上这些操作似乎都相当简单,而且事实上它们可以在一台机器内高效地执行,但是当我们考虑多台机器时,就会发现中央的参数服务器成为了瓶颈。毕竟,每个服务器的带宽是有限的,因此对于$m$个工作节点来说,将所有梯度发送到服务器所需的时间是$\mathcal{O}(m)$。我们也可以通过将参数服务器数量增加到$n$来突破这一障碍。此时,每个服务器只需要存储$\mathcal{O}(1/n)$个参数,因此更新和优化的总时间变为$\mathcal{O}(m/n)$。这两个数字的匹配会产生稳定的伸缩性,而不用在乎我们需要处理多少工作节点。在实际应用中,我们使用同一台机器既作为工作节点还作为服务器。设计说明请参考 :numref:`fig_ps_multips`(技术细节请参考 :cite:`Li.Andersen.Park.ea.2014`)。特别是,确保多台机器只在没有不合理延迟的情况下工作是相当困难的。我们在下面忽略了关于阻塞的细节,只简单介绍一下同步和异步(unsynchronized)更新。
![上图:单参数服务器是一个瓶颈,因为它的带宽是有限的。下图:多参数服务器使用聚合带宽存储部分参数。](../img/ps-multips.svg)
![上图:单参数服务器是一个瓶颈,因为它的带宽是有限的;下图:多参数服务器使用聚合带宽存储部分参数](../img/ps-multips.svg)
:label:`fig_ps_multips`
## 键值存储
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册