提交 16e0ac3d 编写于 作者: A Aston Zhang

asynchronous

上级 4abc381d
# 自动并行计算
[惰性计算”](./lazy-evaluation.md)一节里我们提到MXNet后端会自动构建计算图。通过计算图,系统可以知道所有计算的依赖关系,并可以选择将没有依赖关系的多个任务并行执行来获得性能的提升。以[“惰性计算”](./lazy-evaluation.md)一节中的计算图(图8.1)为例。其中`a = nd.ones((1, 2))``b = nd.ones((1, 2))`这两步计算之间并没有依赖关系。因此,系统可以选择并行执行它们。
[异步计算”](./lazy-evaluation.md)一节里我们提到MXNet后端会自动构建计算图。通过计算图,系统可以知道所有计算的依赖关系,并可以选择将没有依赖关系的多个任务并行执行来获得性能的提升。以[“异步计算”](./lazy-evaluation.md)一节中的计算图(图8.1)为例。其中`a = nd.ones((1, 2))``b = nd.ones((1, 2))`这两步计算之间并没有依赖关系。因此,系统可以选择并行执行它们。
通常一个运算符会用掉一个CPU/GPU上所有计算资源。例如,`dot`操作符会用到所有CPU(即使是有多个CPU)或单个GPU上所有线程。因此在单CPU/GPU上并行运行多个运算符可能效果并不明显。本节中探讨的自动并行计算主要关注CPU和GPU的并行计算,以及计算和通讯的并行。
......
# 计算性能
无论是当数据集很大还是计算资源或应用有约束条件时,深度学习十分关注计算性能。本章将重点介绍影响计算性能的重要因子:命令式编程、符号式编程、惰性计算、自动并行计算和多GPU计算。通过本章的学习,你将很可能进一步提升已有模型的计算性能,例如在不影响模型精度的前提下减少模型的训练时间。
无论是当数据集很大还是计算资源或应用有约束条件时,深度学习十分关注计算性能。本章将重点介绍影响计算性能的重要因子:命令式编程、符号式编程、异步计算、自动并行计算和多GPU计算。通过本章的学习,你将很可能进一步提升已有模型的计算性能,例如在不影响模型精度的前提下减少模型的训练时间。
```eval_rst
......
# 惰性计算
# 异步计算
MXNet使用惰性计算(lazy evaluation)来提升计算性能。理解它的工作原理既有助于开发更高效的程序,又有助于在内存资源有限的情况下主动降低计算性能从而减小内存开销。
MXNet使用异步计算来提升计算性能。理解它的工作原理既有助于开发更高效的程序,又有助于在内存资源有限的情况下主动降低计算性能从而减小内存开销。
我们先导入本节中实验需要的包或模块。
......@@ -12,23 +12,11 @@ import subprocess
from time import time
```
惰性计算的含义是,程序中定义的计算仅在结果真正被取用的时候才执行。我们先看下面这个例子。
## MXNet中的异步计算
```{.python .input n=2}
a = 1 + 1
a = 2 + 2
a = 3 + 3
a
```
在这个例子中,前三句都在对变量`a`赋值,最后一句打印变量`a`的计算结果。事实上,我们可以把三条赋值语句的计算延迟到即将执行打印语句之前。这样的主要好处是系统在即将计算变量`a`时已经看到了全部有关计算`a`的语句,从而有更多空间优化计算。例如,这里我们并不需要对前两条赋值语句做计算。
## MXNet中的惰性计算
广义上,MXNet包括用户直接用来交互的前端和系统用来执行计算的后端。例如,用户可以使用不同的前端语言编写MXNet程序,像Python、R、Scala和C++。无论使用何种前端编程语言,MXNet程序的执行主要都发生在C++实现的后端。换句话说,用户写好的前端MXNet程序会传给后端执行计算。后端有自己的线程在队列中不断收集任务并执行它们。
广义上,MXNet包括用户直接用来交互的前端和系统用来执行计算的后端。例如,用户可以使用不同的前端语言编写MXNet程序,像Python、R、Scala和C++。无论使用何种前端编程语言,MXNet程序的执行主要都发生在C++实现的后端。换句话说,用户写好的前端MXNet程序会传给后端执行计算。后端有自己的线程来不断收集任务,构造、优化并执行计算图。后端优化的方式有很多种,其中包括本章将介绍的惰性计算。
假设我们在前端调用以下四条语句。MXNet后端的线程会分析它们的依赖关系并构建出如图8.1所示的计算图。
MXNet通过前端线程和后端线程的交互实现异步计算。异步计算指,前端线程无需等待当前指令从后端线程返回结果就继续执行后面的指令。为了便于解释,假设Python前端线程调用以下四条指令。
```{.python .input n=3}
a = nd.ones((1, 2))
......@@ -37,11 +25,9 @@ c = a * b + 2
c
```
![MXNet后端的计算图](../img/frontend-backend.svg)
在惰性计算中,前端执行前三条语句的时候,仅仅是把任务放进后端的队列里就返回了。当最后一条语句需要打印计算结果时,前端会等待后端线程把`c`的结果计算完。此设计的一个好处是,这里的Python前端线程不需要做实际计算。因此,无论Python的性能如何,它对整个程序性能的影响会很小。只要C++后端足够高效,那么不管前端语言性能如何,MXNet都可以提供一致的高性能。
在异步计算中,Python前端线程执行前三条语句的时候,仅仅是把任务放进后端的队列里就返回了。当最后一条语句需要打印计算结果时,Python前端线程会等待C++后端线程把`c`的结果计算完。此设计的一个好处是,这里的Python前端线程不需要做实际计算。因此,无论Python的性能如何,它对整个程序性能的影响很小。只要C++后端足够高效,那么不管前端语言性能如何,MXNet都可以提供一致的高性能。
下面的例子通过计时来展示惰性计算的效果。可以看到,当`y = nd.dot(x, x)`返回的时候并没有等待它真正被计算完。
下面的例子通过计时来展示异步计算的效果。可以看到,当`y = nd.dot(x, x)`返回的时候并没有等待它真正被计算完。
```{.python .input n=4}
start = time()
......@@ -52,7 +38,7 @@ print(y)
print('workloads are completed: %f sec' % (time() - start))
```
的确,除非我们需要打印或者保存计算结果,我们基本无需关心目前结果在内存中是否已经计算好了。只要数据是保存在NDArray里并使用MXNet提供的运算符,MXNet后端将默认使用惰性计算来获取最高的计算性能。
的确,除非我们需要打印或者保存计算结果,我们基本无需关心目前结果在内存中是否已经计算好了。只要数据是保存在NDArray里并使用MXNet提供的运算符,MXNet将默认使用异步计算来获取高计算性能。
## 用同步函数让前端等待计算结果
......@@ -78,7 +64,7 @@ nd.waitall()
time() - start
```
此外,任何将NDArray转换成其他不支持惰性计算的数据结构的操作都会让前端等待计算结果。例如当我们调用`asnumpy``asscalar`函数时。
此外,任何将NDArray转换成其他不支持异步计算的数据结构的操作都会让前端等待计算结果。例如当我们调用`asnumpy``asscalar`函数时:
```{.python .input n=7}
start = time()
......@@ -94,45 +80,44 @@ y.norm().asscalar()
time() - start
```
由于`asnumpy``asscalar``print`函数会触发让前端等待后端计算结果的行为,我们通常把这类函数称作同步函数。
上面介绍的`wait_to_read``waitall``asnumpy``asscalar``print`函数会触发让前端等待后端计算结果的行为,我们通常把这类函数称作同步函数。
## 使用惰性计算提升计算性能
在下面例子中,我们不断对`y`进行赋值。如果不使用惰性计算,我们可以在for循环内使用`wait_to_read`做1000次赋值计算。在惰性计算中,MXNet会省略掉一些不必要执行。
## 使用异步计算提升计算性能
在下面例子中,我们用for循环不断对`y`赋值。当for循环内使用同步函数`wait_to_read`时,每次赋值不使用异步计算;当for循环外使用同步函数`waitall`时,则使用异步计算。
```{.python .input n=9}
start = time()
for i in range(1000):
for _ in range(1000):
y = x + 1
y.wait_to_read()
print('no lazy evaluation: %f sec' % (time() - start))
print('synchronous: %f sec' % (time() - start))
start = time()
for i in range(1000):
for _ in range(1000):
y = x + 1
nd.waitall()
print('with lazy evaluation: %f sec' % (time() - start))
print('asynchronous: %f sec' % (time() - start))
```
## 惰性计算对内存使用的影响
我们观察到,使用异步计算能提升一定的计算性能。为了解释这个现象,让我们对Python前端线程和C++后端线程的交互稍作简化。在每一次循环中,前端和后端的交互大约可以分为三个阶段:
在惰性计算中,只要不影响最终计算结果,MXNet后端不一定会按前端代码中定义的执行顺序来执行。
1. 前端令后端将计算任务`y = x + 1`放进队列;
1. 后端从队列中获取计算任务并执行真正的计算;
1. 后端将计算结果返回给前端。
考虑下面的例子
我们将这三个阶段的耗时分别设为$t_1, t_2, t_3$。如果不使用异步计算,执行1000次计算的总耗时大约为$1000 (t_1+ t_2 + t_3)$;如果使用异步计算,由于每次循环前端都无需等待后端返回计算结果,执行1000次计算的总耗时可以降为$t_1 + 1000 t_2 + t_3$(假设$1000t_2 > 999t_1$)
```{.python .input n=10}
a = 1
b = 2
a + b
```
## 异步计算对内存使用的影响
上例中,第一句和第二句之间没有依赖。所以,把`b = 2`提前到`a = 1`前执行也是可以的。但这样可能会导致内存使用的变化
为了解释异步计算对内存使用的影响,让我们先回忆一下前面章节的内容
为了解释惰性计算对内存使用的影响,让我们先回忆一下前面章节的内容。在前面章节中实现的模型训练过程中,我们通常会在每个小批量上评测一下模型,例如模型的损失或者精度。细心的你也许发现了,这类评测常用到同步函数,例如`asscalar`或者`asnumpy`。如果去掉这些同步函数,前端会将大量的小批量计算任务同时放进后端,从而可能导致较大的内存开销。当我们在每个小批量上都使用同步函数时,前端在每次迭代时仅会将一个小批量的任务放进后端执行计算。换言之,我们通过适当减少惰性计算,从而减小内存开销。这也是一种“时间换空间”的策略
在前面章节中实现的模型训练过程中,我们通常会在每个小批量上评测一下模型,例如模型的损失或者精度。细心的你也许发现了,这类评测常用到同步函数,例如`asscalar`或者`asnumpy`。如果去掉这些同步函数,前端会将大量的小批量计算任务在极短的时间内丢给后端,从而可能导致较大的内存开销。当我们在每个小批量上都使用同步函数时,前端在每次迭代时仅会将一个小批量的任务丢给后端执行计算,并通常会减小内存开销
由于深度学习模型通常比较大,而内存资源通常有限,我们建议大家在训练模型时对每个小批量都使用同步函数。类似地,在使用模型预测时,为了减小内存开销,我们也建议大家对每个小批量预测时都使用同步函数,例如直接打印出当前批量的预测结果。
由于深度学习模型通常比较大,而内存资源通常有限,我们建议大家在训练模型时对每个小批量都使用同步函数,例如用`asscalar`或者`asnumpy`评价模型的表现。类似地,在使用模型预测时,为了减小内存开销,我们也建议大家对每个小批量预测时都使用同步函数,例如直接打印出当前小批量的预测结果。
下面我们来演示惰性计算对内存使用的影响。我们先定义一个数据获取函数,它会从被调用时开始计时,并定期打印到目前为止获取数据批量总共耗时。
下面我们来演示异步计算对内存使用的影响。我们先定义一个数据获取函数,它会从被调用时开始计时,并定期打印到目前为止获取数据批量总共耗时。
```{.python .input n=11}
num_batches = 41
......@@ -162,7 +147,7 @@ trainer = gluon.Trainer(net.collect_params(), 'sgd',
loss = gloss.L2Loss()
```
这里定义辅助函数来监测内存的使用。需要注意的是,这个函数只能在Linux运行。
这里定义辅助函数来监测内存的使用。需要注意的是,这个函数只能在Linux或MacOS运行。
```{.python .input n=13}
def get_mem():
......@@ -195,7 +180,7 @@ nd.waitall()
print('increased memory: %f MB' % (get_mem() - mem))
```
如果去掉同步函数,虽然每个小批量的生成间隔较短,训练过程中可能会导致内存开销过大。这是因为默认惰性计算下,前端会将所有小批量计算一次性添加进后端。
如果去掉同步函数,虽然每个小批量的生成间隔较短,训练过程中可能会导致内存开销过大。这是因为默认异步计算下,前端会将所有小批量计算在短时间内全部丢给后端。
```{.python .input n=18}
mem = get_mem()
......@@ -213,14 +198,14 @@ print('increased memory: %f MB' % (get_mem() - mem))
* MXNet包括用户直接用来交互的前端和系统用来执行计算的后端。
* MXNet能够通过惰性计算提升计算性能。
* MXNet能够通过异步计算提升计算性能。
* 我们建议使用每个小批量训练或预测时至少使用一个同步函数,从而避免将过多计算任务同时添加进后端。
* 我们建议使用每个小批量训练或预测时至少使用一个同步函数,从而避免在短时间内将过多计算任务丢给后端。
## 练习
* 本节中提到了“时间换空间”的策略。本节中哪些部分与“空间换时间”的策略有关
* 在“使用异步计算提升计算性能”一节中,我们提到使用异步计算可以使执行1000次计算的总耗时可以降为$t_1 + 1000 t_2 + t_3$。这里为什么要假设$1000t_2 > 999t_1$
## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/1881)
......
......@@ -4,7 +4,7 @@
$$\mathbb{P}(w_t \mid w_{t-(n-1)}, \ldots, w_{t-1}).$$
需要注意的是,以上概率并没有考虑到比$t-(n-1)$更早时刻的词对$w_t$可能的影响。然而,考虑这些影响需要增大$n$的值,那么$n$元语法的存储开销将随之呈指数级增长(可参考上一节的练习)。为了解决$n$元语法的局限性,我们可以在神经网络中引入隐藏状态来记录时间序列的历史信息
需要注意的是,以上概率并没有考虑到比$t-(n-1)$更早时刻的词对$w_t$可能的影响。然而,考虑这些影响需要增大$n$的值,那么$n$元语法的模型参数的数量将随之呈指数级增长(可参考上一节的练习)。为了解决$n$元语法的局限性,我们可以在神经网络中引入隐藏状态。隐藏状态既可以捕捉时间序列的历史信息,且模型参数的数量不随着历史而增长
......@@ -12,19 +12,17 @@ $$\mathbb{P}(w_t \mid w_{t-(n-1)}, \ldots, w_{t-1}).$$
## 不含隐藏状态的神经网络
让我们回顾一下不含隐藏状态的神经网络,例如只有一个隐藏层的多层感知机。
让我们回顾一下不含隐藏状态的神经网络,例如只有一个隐藏层的多层感知机。
假设隐藏层的激活函数是$\phi$,对于一个样本数为$n$特征向量维度为$x$的批量数据$\boldsymbol{X} \in \mathbb{R}^{n \times x}$($\boldsymbol{X}$是一个$n$行$x$列的实数矩阵)来说,那么这个隐含层的输出就是
给定样本数为$n$、输入个数(特征数或特征向量维度)为$x$的小批量数据样本$\boldsymbol{X} \in \mathbb{R}^{n \times x}$。设隐藏层的激活函数为$\phi$,那么隐藏层的输出$\boldsymbol{H} \in \mathbb{R}^{n \times h}$计算为
$$\boldsymbol{H} = \phi(\boldsymbol{X} \boldsymbol{W}_{xh} + \boldsymbol{b}_h)$$
$$\boldsymbol{H} = \phi(\boldsymbol{X} \boldsymbol{W}_{xh} + \boldsymbol{b}_h),$$
假定隐含层长度为$h$,其中的$\boldsymbol{W}_{xh} \in \mathbb{R}^{x \times h}$是权重参数。偏移参数 $\boldsymbol{b}_h \in \mathbb{R}^{1 \times h}$在与前一项$\boldsymbol{X} \boldsymbol{W}_{xh} \in \mathbb{R}^{n \times h}$ 相加时使用了[广播](../chapter_crashcourse/ndarray.md)。这个隐含层的输出的形状为$\boldsymbol{H} \in \mathbb{R}^{n \times h}$。
其中权重参数$\boldsymbol{W}_{xh} \in \mathbb{R}^{x \times h}$,偏差参数 $\boldsymbol{b}_h \in \mathbb{R}^{1 \times h}$,$h$为隐藏单元个数。我们之前也提到,上式的两项相加使用了广播机制。把隐藏变量$\boldsymbol{H}$作为输出层的输入,且设输出个数为$y$(例如分类问题中的类别数),输出层的输出
把隐含层的输出$\boldsymbol{H}$作为输出层的输入,最终的输出
$$\boldsymbol{O} = \boldsymbol{H} \boldsymbol{W}_{hy} + \boldsymbol{b}_y,$$
$$\hat{\boldsymbol{Y}} = \text{softmax}(\boldsymbol{H} \boldsymbol{W}_{hy} + \boldsymbol{b}_y)$$
假定每个样本对应的输出向量维度为$y$,其中 $\hat{\boldsymbol{Y}} \in \mathbb{R}^{n \times y}, \boldsymbol{W}_{hy} \in \mathbb{R}^{h \times y}, \boldsymbol{b}_y \in \mathbb{R}^{1 \times y}$且两项相加使用了[广播](../chapter_crashcourse/ndarray.md)
其中输出变量$\boldsymbol{O} \in \mathbb{R}^{n \times y}$, 输出层权重参数$\boldsymbol{W}_{hy} \in \mathbb{R}^{h \times y}$, 输出层偏差参数$\boldsymbol{b}_y \in \mathbb{R}^{1 \times y}$。如果是[分类问题](../chapter_supervised-learning/classification.md),我们可以使用$\text{softmax}(\boldsymbol{O})$来计算输出类别的概率分布。
......
......@@ -11,11 +11,11 @@ $$\mathbb{P}(w_1, w_2, \ldots, w_T).$$
## 语言模型的计算
既然语言模型这么有用,那该如何计算它呢?根据全概率公式,我们有
既然语言模型有用,那该如何计算它呢?根据全概率公式,我们有
$$\mathbb{P}(w_1, w_2, \ldots, w_T) = \prod_{t=1}^T \mathbb{P}(w_t \mid w_1, \ldots, w_{t-1}) .$$
例如
例如一段含有四个词的文本序列的概率
$$\mathbb{P}(w_1, w_2, w_3, w_4) = \mathbb{P}(w_1) \mathbb{P}(w_2 \mid w_1) \mathbb{P}(w_3 \mid w_1, w_2) \mathbb{P}(w_4 \mid w_1, w_2, w_3) .$$
......
......@@ -6,7 +6,7 @@
## 线性回归
让我们先回忆一下上节中的内容。设数据样本数为$n$,特征数为$d$。给定批量数据样本的特征$\boldsymbol{X} \in \mathbb{R}^{n \times d}$和标签$\boldsymbol{y} \in \mathbb{R}^{n \times 1}$,线性回归的批量输出$\boldsymbol{\hat{y}} \in \mathbb{R}^{n \times 1}$的计算表达式为
让我们先回忆一下上节中的内容。设数据样本数为$n$,输入个数为$d$。给定批量数据样本的特征$\boldsymbol{X} \in \mathbb{R}^{n \times d}$和标签$\boldsymbol{y} \in \mathbb{R}^{n \times 1}$,线性回归的批量输出$\boldsymbol{\hat{y}} \in \mathbb{R}^{n \times 1}$的计算表达式为
$$\boldsymbol{\hat{y}} = \boldsymbol{X} \boldsymbol{w} + b,$$
......@@ -24,7 +24,7 @@ import random
我们在这里描述用来生成人工训练数据集的真实模型。
设训练数据集样本数为1000,输入个数(特征数)为2。给定随机生成的批量样本特征$\boldsymbol{X} \in \mathbb{R}^{1000 \times 2}$,我们使用线性回归模型真实权重$\boldsymbol{w} = [2, -3.4]^\top$和偏差$b = 4.2$,以及一个随机噪音项$\epsilon$来生成标签
设训练数据集样本数为1000,输入个数为2。给定随机生成的批量样本特征$\boldsymbol{X} \in \mathbb{R}^{1000 \times 2}$,我们使用线性回归模型真实权重$\boldsymbol{w} = [2, -3.4]^\top$和偏差$b = 4.2$,以及一个随机噪音项$\epsilon$来生成标签
$$\boldsymbol{y} = \boldsymbol{X}\boldsymbol{w} + b + \epsilon,$$
......
......@@ -67,7 +67,9 @@ $$b \leftarrow b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \frac{
![线性回归是一个单层神经网络](../img/linreg.svg)
在图3.1所表示的神经网络中,输入分别为$x_1$和$x_2$,因此输入层的输入个数为2。由于该网络的输出为$o$,输出层的输出个数为1。需要注意的是,我们直接将图3.1中神经网络的输出$o$作为线性回归的输出,即$\hat{y} = o$。由于输入层并不涉及计算,按照惯例,图3.1所示的神经网络的层数为1。所以,线性回归是一个单层神经网络。输出层中负责计算$o$的单元又叫神经元。在线性回归中,$o$的计算依赖于$x_1$和$x_2$。也就是说,输出层中的神经元和输入层中各个输入完全连接。因此,这里的输出层又叫全连接层(dense layer或fully-connected layer)。
在图3.1所表示的神经网络中,输入分别为$x_1$和$x_2$,因此输入层的输入个数为2。输入个数也叫特征数或特征向量维度。由于图3.1中网络的输出为$o$,输出层的输出个数为1。需要注意的是,我们直接将图3.1中神经网络的输出$o$作为线性回归的输出,即$\hat{y} = o$。由于输入层并不涉及计算,按照惯例,图3.1所示的神经网络的层数为1。所以,线性回归是一个单层神经网络。输出层中负责计算$o$的单元又叫神经元。在线性回归中,$o$的计算依赖于$x_1$和$x_2$。也就是说,输出层中的神经元和输入层中各个输入完全连接。因此,这里的输出层又叫全连接层或稠密层(fully-connected layer或dense layer)。
### 矢量计算表达式
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册