提交 aaf6099b 编写于 作者: M muli

revise the reset perf chapter

上级 cf294c03
......@@ -28,23 +28,23 @@ c
首先实现一个简单计时类:
```{.python .input}
class Benchmark():
def __init__(self, prefix=''):
self.prefix = prefix
class Benchmark(): # Benchmark 类已保存在 gluonbook 里方便以后使用。
def __init__(self, prefix=None):
self.prefix = prefix + ' ' if prefix else ''
def __enter__(self):
self.start = time.time()
def __exit__(self, type, value, traceback):
def __exit__(self, *args):
print('%stime: %.4f sec' % (self.prefix, time.time() - self.start))
```
下面的例子通过计时来展示异步计算的效果。可以看到,当`y = nd.dot(x, x).sum()`返回的时候并没有等待它真正被计算完。
```{.python .input n=4}
with Benchmark('workloads are queued. '):
with Benchmark('workloads are queued.'):
x = nd.random.uniform(shape=(2000, 2000))
y = nd.dot(x, x).sum()
with Benchmark('workloads are finished '):
with Benchmark('workloads are finished.'):
print('sum =', y)
```
......
# 自动并行计算
[“异步计算”](async-computation.md)一节里我们提到MXNet后端会自动构建计算图。通过计算图,系统可以知道所有计算的依赖关系,并可以选择将没有依赖关系的多个任务并行执行来获得性能的提升。[“异步计算”](async-computation.md)一节中的计算图(图8.1)为例。其中`a = nd.ones((1, 2))``b = nd.ones((1, 2))`这两步计算之间并没有依赖关系。因此,系统可以选择并行执行它们。
[“异步计算”](async-computation.md)一节里我们提到MXNet后端会自动构建计算图。通过计算图,系统可以知道所有计算的依赖关系,并可以选择将没有依赖关系的多个任务并行执行来获得性能的提升。例如[“异步计算”](async-computation.md)一节的第一个例子里依次执行了`a = nd.ones((1, 2))``b = nd.ones((1, 2))`。这两步计算之间并没有依赖关系,因此系统可以选择并行执行它们。
通常一个运算符会用到所有CPU或单个GPU上全部的计算资源。例如,`dot`操作符会用到所有CPU(即使是一台机器上有多个CPU处理器)或单个GPU上所有线程。因此单纯在CPU或者GPU上并行运行多个运算符可能效果并不明显。本节中探讨的自动并行计算主要关注同时使用CPU和GPU的并行计算,以及计算和通讯的并行。
通常一个运算符会用到所有CPU或单个GPU上全部的计算资源。例如,`dot`操作符会用到所有CPU(即使是一台机器上有多个CPU处理器)或单个GPU上所有线程。如果每个操作符的计算量足够,单纯在CPU或者GPU上并行运行多个运算符可能效果并不明显。本节中探讨的自动并行计算主要关注同时使用CPU和GPU的并行计算,以及计算和通讯的并行。
首先导入本节中实验所需的包或模块。注意,我们需要至少一个GPU才能运行本节实验。
```{.python .input}
import sys
sys.path.insert(0, '..')
import gluonbook as gb
import mxnet as mx
from mxnet import nd
from time import time
import time
```
## CPU和GPU的并行计算
我们先介绍CPU和GPU的并行计算,例如程序中的计算既发生在CPU,又发生在GPU之上。
先定义一个函数,令它做10次矩阵乘法。
我们先介绍CPU和GPU的并行计算,例如程序中的计算既发生在CPU,又发生在GPU之上。先定义一个函数,令它做10次矩阵乘法。
```{.python .input}
def run(x):
......@@ -37,25 +39,22 @@ run(x_cpu) # 预热开始。
run(x_gpu)
nd.waitall() # 预热结束。
start = time()
run(x_cpu)
nd.waitall()
print('run on CPU: %f sec' % (time() - start))
with gb.Benchmark('run on CPU.'):
run(x_cpu)
nd.waitall()
start = time()
run(x_gpu)
nd.waitall()
print('run on GPU: %f sec' % (time() - start))
with gb.Benchmark('then run on GPU.'):
run(x_gpu)
nd.waitall()
```
我们去掉`run(x_cpu)``run(x_gpu)`两个计算任务之间的`nd.waitall()`,希望系统能自动并行这两个任务。
```{.python .input}
start = time()
run(x_cpu)
run(x_gpu)
nd.waitall()
print('run on both CPU and GPU: %f sec' % (time() - start))
with gb.Benchmark('run on both CPU and GPU in parallel.'):
run(x_cpu)
run(x_gpu)
nd.waitall()
```
可以看到,当两个计算任务一起执行时,执行总时间小于它们分开执行的总和。这表示,MXNet能有效地在CPU和GPU上自动并行计算。
......@@ -69,26 +68,22 @@ print('run on both CPU and GPU: %f sec' % (time() - start))
def copy_to_cpu(x):
return [y.copyto(mx.cpu()) for y in x]
start = time()
y = run(x_gpu)
nd.waitall()
print('run on GPU: %f sec' % (time() - start))
with gb.Benchmark('run on GPU.'):
y = run(x_gpu)
nd.waitall()
start = time()
copy_to_cpu(y)
nd.waitall()
print('copy to CPU: %f sec' % (time() - start))
with gb.Benchmark('then copy to CPU.'):
copy_to_cpu(y)
nd.waitall()
```
我们去掉计算和通讯之间的`waitall`函数,打印这两个任务完成的总时间。
```{.python .input}
start = time()
y = run(x_gpu)
copy_to_cpu(y)
nd.waitall()
t = time() - start
print('run on GPU then copy to CPU: %f sec' % (time() - start))
with gb.Benchmark('run and copy in parallel.'):
y = run(x_gpu)
copy_to_cpu(y)
nd.waitall()
```
可以看到,执行计算和通讯的总时间小于两者分别执行的耗时之和。需要注意的是,这个计算并通讯的任务不同于本节之前介绍的同时使用CPU和GPU并行计算的任务。这里的运行和通讯之间有依赖关系:`y[i]`必须先计算好才能复制到CPU。所幸的是,在计算`y[i]`的时候系统可以复制`y[i-1]`,从而减少计算和通讯的总运行时间。
......@@ -101,8 +96,8 @@ print('run on GPU then copy to CPU: %f sec' % (time() - start))
## 练习
* 本节中定义的`run`函数里做了10次运算。它们之间也没有依赖关系。看看MXNet有没有自动并行执行它们。
* 试试包含更加复杂的数据依赖的计算任务。MXNet能不能得到正确结果并提升计算性能?
* 当运算符足够小时(例如批量大小为1的预测),在CPU或单GPU上并行运行也可能提升效果,实验是否如此。
## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/1883)
......
# 多GPU计算的Gluon实现
在Gluon中,我们可以很方便地使用数据并行进行多GPU计算。比方说,我们并不需要自己实现[“多GPU计算”](multiple-gpus.md)一节里介绍的多GPU之间同步数据的辅助函数。
先导入本节实验需要的包或模块。同上一节,运行本节中的程序需要至少两块GPU。
在Gluon中,我们可以很方便地使用数据并行进行多GPU计算。比方说,我们并不需要自己实现[“多GPU计算”](multiple-gpus.md)一节里介绍的多GPU之间同步数据的辅助函数。先导入本节实验需要的包或模块。同上一节,运行本节中的程序需要至少两块GPU。
```{.python .input n=1}
import sys
......@@ -12,7 +10,7 @@ import gluonbook as gb
import mxnet as mx
from mxnet import autograd, gluon, init, nd
from mxnet.gluon import loss as gloss, nn, utils as gutils
from time import time
import time
```
## 多GPU上初始化模型参数
......@@ -22,21 +20,19 @@ from time import time
```{.python .input n=2}
# 本函数已保存在 gluonbook 包中方便以后使用。
def resnet18(num_classes):
net = nn.Sequential()
# 这里使用了较小的卷积核、步幅和填充,并去掉了最大池化层。
net.add(nn.Conv2D(64, kernel_size=3, strides=1, padding=1),
nn.BatchNorm(), nn.Activation('relu'))
def resnet_block(num_channels, num_residuals, first_block=False):
blk = nn.Sequential()
for i in range(num_residuals):
if i == 0 and not first_block:
blk.add(gb.Residual(num_channels, use_1x1conv=True,
strides=2))
blk.add(gb.Residual(
num_channels, use_1x1conv=True, strides=2))
else:
blk.add(gb.Residual(num_channels))
return blk
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),
resnet_block(128, 2),
resnet_block(256, 2),
......@@ -75,12 +71,6 @@ weight.data(ctx[0])[0], weight.data(ctx[1])[0]
## 多GPU训练模型
我们先定义交叉熵损失函数。
```{.python .input n=6}
loss = gloss.SoftmaxCrossEntropyLoss()
```
当我们使用多个GPU来训练模型时,`gluon.Trainer`会自动做数据并行,例如划分小批量数据样本并复制到各个GPU上,对各个GPU上的梯度求和再广播到所有GPU上。这样,我们就可以很方便地实现训练函数了。
```{.python .input n=7}
......@@ -91,8 +81,9 @@ def train(num_gpus, batch_size, lr):
net.initialize(init=init.Normal(sigma=0.01), ctx=ctx, force_reinit=True)
trainer = gluon.Trainer(
net.collect_params(), 'sgd', {'learning_rate': lr})
for epoch in range(1, 6):
start = time()
loss = gloss.SoftmaxCrossEntropyLoss()
for epoch in range(4):
start = time.time()
for X, y in train_iter:
gpu_Xs = gutils.split_and_load(X, ctx)
gpu_ys = gutils.split_and_load(y, ctx)
......@@ -103,15 +94,22 @@ def train(num_gpus, batch_size, lr):
l.backward()
trainer.step(batch_size)
nd.waitall()
print('epoch %d, training time: %.1f sec' % (epoch, time() - start))
train_time = time.time() - start
test_acc = gb.evaluate_accuracy(test_iter, net, ctx[0])
print('validation accuracy: %.4f' % (test_acc))
print('epoch %d, training time: %.1f sec, test_acc %.2f' % (
epoch, train_time, test_acc))
```
首先在单GPU上训练。
```{.python .input}
train(num_gpus=1, batch_size=256, lr=0.1)
```
我们在2个GPU上训练模型
然后尝试2个GPU。比上一节使用的LeNet,ResNet-18计算更加复杂,其并行效果更佳
```{.python .input n=10}
train(num_gpus=2, batch_size=512, lr=0.3)
train(num_gpus=2, batch_size=512, lr=0.2)
```
## 小结
......
# 多GPU计算
本教程我们将展示如何使用多个GPU计算,例如使用多个GPU训练模型。正如你期望的那样,运行本节中的程序需要至少两块GPU。事实上,一台机器上安装多块GPU非常常见。这是因为主板上通常会有多个PCIe插槽。如果正确安装了NVIDIA驱动,我们可以通过`nvidia-smi`命令来查看当前机器上的全部GPU。
本教程我们将展示如何使用多个GPU计算,例如使用多个GPU训练同一个模型。正如你期望的那样,运行本节中的程序需要至少两块GPU。事实上,一台机器上安装多块GPU非常常见,这是因为主板上通常会有多个PCIe插槽。如果正确安装了NVIDIA驱动,我们可以通过`nvidia-smi`命令来查看当前机器上的全部GPU。
```{.python .input n=1}
!nvidia-smi
......@@ -13,9 +13,9 @@
数据并行目前是深度学习里使用最广泛的将模型训练任务划分到多个GPU的办法。回忆一下我们在[“梯度下降和随机梯度下降”](../chapter_optimization/gd-sgd.md)一节中介绍的使用优化算法训练模型的过程。下面我们就以小批量随机梯度下降为例来介绍数据并行是如何工作的。
假设一台机器上有$k$个GPU。给定需要训练的模型,每个GPU将分别独立维护一份完整的模型参数。在模型训练的任意一次迭代中,给定一个小批量,我们将该批量中的样本划分成$k$份并分给每个GPU一份。然后,每个GPU将分别根据自己分到的训练数据样本和自己维护的模型参数计算模型参数的梯度。
接下来,我们把$k$个GPU上分别计算得到的梯度相加,从而得到当前的小批量梯度。
之后,每个GPU都使用这个小批量梯度分别更新自己维护的那一份完整的模型参数。
假设一台机器上有$k$个GPU。给定需要训练的模型,每个GPU将分别独立维护一份完整的模型参数。在模型训练的任意一次迭代中,给定一个小批量,我们将该批量中的样本划分成$k$份并分给每个GPU一份。然后,每个GPU将分别根据自己分到的训练数据样本和自己维护的模型参数计算模型参数的梯度。接下来,我们把$k$个GPU上分别计算得到的梯度相加,从而得到当前的小批量梯度。之后,每个GPU都使用这个小批量梯度分别更新自己维护的那一份完整的模型参数。图 8.1 演示了使用两个GPU时的情况。
![使用两个GPU的数据平行下的梯度计算。](../img/data-parallel.svg)
为了从零开始实现多GPU训练中的数据并行,让我们先导入需要的包或模块。
......@@ -27,7 +27,7 @@ import gluonbook as gb
import mxnet as mx
from mxnet import autograd, nd
from mxnet.gluon import loss as gloss
from time import time
import time
```
## 定义模型
......@@ -113,7 +113,7 @@ print('after allreduce:', data)
```{.python .input n=5}
def split_and_load(data, ctx):
n, k = data.shape[0], len(ctx)
m = n // k
m = n // k # 为了简单起见假设整除。
assert m * k == n, '# examples is not divided by # devices.'
return [data[i * m: (i + 1) * m].as_in_context(ctx[i]) for i in range(k)]
```
......@@ -136,20 +136,17 @@ print('output:', splitted)
```{.python .input n=6}
def train_batch(X, y, gpu_params, ctx, lr):
# 当 ctx 包含多个GPU时,划分小批量数据样本并复制到各个 GPU 上。
gpu_Xs = split_and_load(X, ctx)
gpu_ys = split_and_load(y, ctx)
# 在各个 GPU 上计算损失。
with autograd.record():
gpu_Xs, gpu_ys = split_and_load(X, ctx), split_and_load(y, ctx)
with autograd.record(): # 在各个 GPU 上分别计算损失。
ls = [loss(lenet(gpu_X, gpu_W), gpu_y)
for gpu_X, gpu_y, gpu_W in zip(gpu_Xs, gpu_ys, gpu_params)]
# 在各个 GPU 上反向传播。
for l in ls:
for gpu_X, gpu_y, gpu_W in zip(gpu_Xs, gpu_ys, gpu_params)]
for l in ls: # 在各个 GPU 上分别反向传播。
l.backward()
# 把各个 GPU 上的梯度加起来,然后再广播到所有 GPU 上。
for i in range(len(gpu_params[0])):
allreduce([gpu_params[c][i].grad for c in range(len(ctx))])
for param in gpu_params:
gb.sgd(param, lr, X.shape[0])
for param in gpu_params: # 在各个 GPU 上分别更新模型参数。
gb.sgd(param, lr, X.shape[0]) # 注意这里使用了完整批量大小。
```
## 训练函数
......@@ -163,34 +160,54 @@ def train(num_gpus, batch_size, lr):
print('running on:', ctx)
# 将模型参数复制到 num_gpus 个 GPU 上。
gpu_params = [get_params(params, c) for c in ctx]
for epoch in range(1, 6):
start = time()
for epoch in range(4):
start = time.time()
for X, y in train_iter:
# 对单个小批量上进行多 GPU 训练。
train_batch(X, y, gpu_params, ctx, lr)
nd.waitall()
print('epoch %d, time: %.1f sec' % (epoch, time() - start))
# 在 GPU 0 上验证模型。
net = lambda x: lenet(x, gpu_params[0])
nd.waitall()
train_time = time.time() - start
net = lambda x: lenet(x, gpu_params[0]) # 在 GPU 0 上验证模型。
test_acc = gb.evaluate_accuracy(test_iter, net, ctx[0])
print('validation accuracy: %.4f' % test_acc)
print('epoch %d, time: %.1f sec, test acc: %.2f' % (
epoch+1, train_time, test_acc))
```
## 多GPU训练训练实验
让我们先从单GPU开始,并使用批量大小为256,学习率为0.2。
```{.python .input}
train(num_gpus=1, batch_size=256, lr=0.2)
```
保持批量大小和学习率不变,将GPU改为2,可以看到测试精度增加同前一致。特别是如果使用同样的初始模型参数,使用多个GPU应该得到跟单个GPU一样的结果。但由于每个GPU得到的批量大小减半了,它的计算效率(每秒能处理的样本数)变低了,而且加入了额外的通讯开销,所以我们并没有看到计算时间有显著下降。
```{.python .input}
train(num_gpus=2, batch_size=256, lr=0.2)
```
我们使用2个GPU和较大的批量大小来训练,以使得GPU的计算资源能够得到较充分利用
接下来我们将批量大小翻倍,这样每个GPU拿到同单GPU情况下一样的批量大小。同时,我们将学习率翻倍,希望能得到和单GPU下一样的测试精度递增速度
```{.python .input n=10}
train(num_gpus=2, batch_size=512, lr=0.3)
```{.python .input}
train(num_gpus=2, batch_size=512, lr=0.4)
```
可以看到计算速度有了明显提升。这是因为我们将每个GPU的计算任务翻倍,这样得到了跟单GPU一样的计算效率。同时每个小批量的计算时间更长,但每次的通讯开销并没有增加,所以并行效率更高。而且在一个迭代周期里,通讯次数也减半。所有这些因素累加一起使得训练性能提升。图 8.2 示意了计算和通讯与总时间的关系。
![计算开销和通讯开销与处理1024个样本的总时间的关系。这里示意了GPU 0上可能看到的执行结果。](../img/comp-comm.svg)
## 小结
* 我们可以使用数据并行更充分地利用多个GPU的计算资源,实现多GPU训练模型。
* 给定超参数下的情况下,改变GPU个数不影响模型训练结果。
* 总计算时间跟计算开销和通讯开销相关,这里批量大小是关键超参数。
## 练习
* 在本节实验中,试一试不同的迭代周期、批量大小和学习率。
* 将本节实验的模型预测部分改为用多GPU预测。
* 测量每个批量纯计算时间和纯通讯时间。
## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/1884)
......
......@@ -34,6 +34,15 @@ def accuracy(y_hat, y):
return (y_hat.argmax(axis=1) == y.astype('float32')).mean().asscalar()
class Benchmark():
"""benchmark a piece of codes"""
def __init__(self, prefix=None):
self.prefix = prefix + ' ' if prefix else ''
def __enter__(self):
self.start = time.time()
def __exit__(self, *args):
print('%stime: %.4f sec' % (self.prefix, time.time() - self.start))
def bbox_to_rect(bbox, color):
"""Convert bounding box to matplotlib format."""
return plt.Rectangle(xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0],
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册