Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
CoolBran
d2l-zh
提交
6f23af8e
D
d2l-zh
项目概览
CoolBran
/
d2l-zh
与 Fork 源项目一致
从无法访问的项目Fork
通知
3
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
D
d2l-zh
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
前往新版Gitcode,体验更适合开发者的 AI 搜索 >>
未验证
提交
6f23af8e
编写于
11月 29, 2021
作者:
G
goldmermaid
提交者:
GitHub
11月 29, 2021
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
[polish] chapter 12 peer review (#1018)
* chapter12 0-3 polish * soft polish chapter 12 done
上级
62914ade
变更
8
展开全部
隐藏空白更改
内联
并排
Showing
8 changed file
with
83 addition
and
74 deletion
+83
-74
chapter_computational-performance/async-computation.md
chapter_computational-performance/async-computation.md
+4
-4
chapter_computational-performance/auto-parallelism.md
chapter_computational-performance/auto-parallelism.md
+4
-4
chapter_computational-performance/hardware.md
chapter_computational-performance/hardware.md
+16
-16
chapter_computational-performance/hybridize.md
chapter_computational-performance/hybridize.md
+6
-4
chapter_computational-performance/index.md
chapter_computational-performance/index.md
+6
-1
chapter_computational-performance/multiple-gpus-concise.md
chapter_computational-performance/multiple-gpus-concise.md
+16
-15
chapter_computational-performance/multiple-gpus.md
chapter_computational-performance/multiple-gpus.md
+23
-22
chapter_computational-performance/parameterserver.md
chapter_computational-performance/parameterserver.md
+8
-8
未找到文件。
chapter_computational-performance/async-computation.md
浏览文件 @
6f23af8e
...
...
@@ -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`
## 障碍器与阻塞器
...
...
chapter_computational-performance/auto-parallelism.md
浏览文件 @
6f23af8e
...
...
@@ -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
)
之类的调试器来验证你的代码是否有效。
...
...
chapter_computational-performance/hardware.md
浏览文件 @
6f23af8e
此差异已折叠。
点击以展开。
chapter_computational-performance/hybridize.md
浏览文件 @
6f23af8e
# 编译器和解释器
: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`
...
...
chapter_computational-performance/index.md
浏览文件 @
6f23af8e
# 计算性能
:label:
`chap_performance`
在深度学习中,数据集和模型通常都很大,导致计算量也会很大。因此,计算的性能非常重要。本章将集中讨论影响计算性能的主要因素:命令式编程、符号编程、异步计算、自动并行和多GPU计算。通过学习本章,你可以进一步提高前几章中实现的那些模型的计算性能。例如,我们可以在不影响准确性的前提下,减少训练时间。
在深度学习中,数据集和模型通常都很大,导致计算量也会很大。
因此,计算的性能非常重要。
本章将集中讨论影响计算性能的主要因素:命令式编程、符号编程、
异步计算、自动并行和多GPU计算。
通过学习本章,对于前几章中实现的那些模型,你可以进一步提高它们的计算性能。
例如,我们可以在不影响准确性的前提下,大大减少训练时间。
```
toc
:maxdepth: 2
...
...
chapter_computational-performance/multiple-gpus-concise.md
浏览文件 @
6f23af8e
...
...
@@ -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:
...
...
chapter_computational-performance/multiple-gpus.md
浏览文件 @
6f23af8e
...
...
@@ -34,10 +34,10 @@
当通道或单元的数量不太小时,使计算性能有良好的提升。
此外,由于可用的显存呈线性扩展,多个GPU能够处理不断变大的网络。
![
由于GPU显存有限,原有AlexNet设计中的模型并行
。
](
../img/alexnet-original.svg
)
![
由于GPU显存有限,原有AlexNet设计中的模型并行
](
../img/alexnet-original.svg
)
:label:
`fig_alexnet_original`
然而,我们需要大量的同步或
*屏障操作*
(barrier operation
s
),因为每一层都依赖于所有其他层的结果。
然而,我们需要大量的同步或
*屏障操作*
(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('a
fter allreduce:
\n', data[0], '\n', data[1])
print('a
llreduce之后:
\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('a
fter allreduce:
\n', data[0], '\n', data[1])
print('a
llreduce之后:
\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
\c
dot b$,即按GPU的数量进行扩展。
1.
比较不同学习率时模型的精确度
。
随着GPU数量的增加学习率应该如何扩展?
1.
比较不同学习率时模型的精确度
,
随着GPU数量的增加学习率应该如何扩展?
1.
实现一个更高效的
`allreduce`
函数用于在不同的GPU上聚合不同的参数?为什么这样的效率更高?
1.
实现模型在多GPU下测试精度的计算。
...
...
chapter_computational-performance/parameterserver.md
浏览文件 @
6f23af8e
...
...
@@ -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上聚合另一些参数。例如,如果有四个与参数向量相关的梯度$
\m
athbf{g}_1,
\l
dots,
\m
athbf{g}_4$,还可以一个GPU对一个$
\m
athbf{g}_i (i = 1,
\l
dots, 4$)地进行梯度聚合。
回顾来看,选择GPU
0进行聚合似乎是个很随便的决定,当然也可以选择CPU上聚合,事实上只要优化算法支持,在实际操作中甚至可以在某个GPU上聚合其中一些参数,而在另一个GPU上聚合另一些参数。例如,如果有四个与参数向量相关的梯度$
\m
athbf{g}_1,
\l
dots,
\m
athbf{g}_4$,还可以一个GPU对一个$
\m
athbf{g}_i (i = 1,
\l
dots, 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
\d
iv 8
\d
iv 2
\a
pprox 18
\m
athrm{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
\a
pprox 1$。换句话说,当我们增大环的大小时,聚合梯度所花费的时间不会增加。这是一个相当惊人的结果。 :numref:
`fig_ringsync`
说明了$n=4$个节点上的步骤顺序。
![
跨4个节点的环同步。每个节点开始向其左邻居发送部分梯度,直到在其右邻居中找到聚合的梯度
。
](
../img/ringsync.svg
)
![
跨4个节点的环同步。每个节点开始向其左邻居发送部分梯度,直到在其右邻居中找到聚合的梯度
](
../img/ringsync.svg
)
:label:
`fig_ringsync`
如果我们使用相同的例子,跨$8$个V100 GPU同步160MB,我们得到的结果大约是$2
\t
imes 160
\m
athrm{MB}
\d
iv (3
\t
imes18
\m
athrm{GB/s})
\a
pprox 6
\m
athrm{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$个工作节点来说,将所有梯度发送到服务器所需的时间是$
\m
athcal{O}(m)$。我们也可以通过将参数服务器数量增加到$n$来突破这一障碍。此时,每个服务器只需要存储$
\m
athcal{O}(1/n)$个参数,因此更新和优化的总时间变为$
\m
athcal{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.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录