multiple-gpus.md 8.3 KB
Newer Older
A
rename  
Aston Zhang 已提交
1
# 多GPU计算
2

3
本教程我们将展示如何使用多个GPU计算,例如使用多个GPU训练模型。正如你期望的那样,运行本节中的程序需要至少两块GPU。事实上,一台机器上安装多块GPU非常常见。这是因为主板上通常会有多个PCIe插槽。如果正确安装了NVIDIA驱动,我们可以通过`nvidia-smi`命令来查看当前机器上的全部GPU。
4 5 6 7 8

```{.python .input  n=1}
!nvidia-smi
```

A
Aston Zhang 已提交
9 10
[“自动并行计算”](./auto-parallelism.md)一节里,我们介绍过,大部分的运算可以使用所有的CPU的全部计算资源,或者单个GPU的全部计算资源。但如果使用多个GPU训练模型,我们仍然需要实现相应的算法。这些算法中最常用的叫做数据并行。

11

M
muli 已提交
12
## 数据并行
13

A
Aston Zhang 已提交
14
数据并行目前是深度学习里使用最广泛的将模型训练任务划分到多个GPU的办法。回忆一下我们在[“梯度下降和随机梯度下降——从零开始”](../chapter_optimization/gd-sgd-scratch.md)一节中介绍的使用优化算法训练模型的过程。下面我们就以小批量随机梯度下降为例来介绍数据并行是如何工作的。
A
Aston Zhang 已提交
15 16 17 18 19

假设一台机器上有$k$个GPU。给定需要训练的模型,每个GPU将分别独立维护一份完整的模型参数。在模型训练的任意一次迭代中,给定一个小批量,我们将该批量中的样本划分成$k$份并分给每个GPU一份。然后,每个GPU将分别根据自己分到的训练数据样本和自己维护的模型参数计算模型参数的梯度。
接下来,我们把$k$个GPU上分别计算得到的梯度相加,从而得到当前的小批量梯度。
之后,每个GPU都使用这个小批量梯度分别更新自己维护的那一份完整的模型参数。

A
Aston Zhang 已提交
20
为了从零开始实现多GPU训练中的数据并行,让我们先导入需要的包或模块。
21

A
Aston Zhang 已提交
22
```{.python .input}
A
Aston Zhang 已提交
23 24 25
import sys
sys.path.append('..')
import gluonbook as gb
A
Aston Zhang 已提交
26
import mxnet as mx
A
Aston Zhang 已提交
27 28
from mxnet import autograd, nd
from mxnet.gluon import loss as gloss
A
Aston Zhang 已提交
29 30
from time import time
```
31

M
muli 已提交
32
## 定义模型
33

A
Aston Zhang 已提交
34
我们使用[“卷积神经网络——从零开始”](../chapter_convolutional-neural-networks/cnn-scratch.md)一节里介绍的LeNet来作为本节的样例模型。
35 36

```{.python .input  n=2}
A
Aston Zhang 已提交
37 38
# 初始化模型参数。
scale = 0.01
A
Aston Zhang 已提交
39
W1 = nd.random.normal(scale=scale, shape=(20, 1, 3, 3))
40
b1 = nd.zeros(shape=20)
A
Aston Zhang 已提交
41
W2 = nd.random.normal(scale=scale, shape=(50, 20, 5, 5))
42
b2 = nd.zeros(shape=50)
A
Aston Zhang 已提交
43
W3 = nd.random.normal(scale=scale, shape=(800, 128))
44
b3 = nd.zeros(shape=128)
A
Aston Zhang 已提交
45
W4 = nd.random.normal(scale=scale, shape=(128, 10))
46 47 48
b4 = nd.zeros(shape=10)
params = [W1, b1, W2, b2, W3, b3, W4, b4]

A
Aston Zhang 已提交
49
# 定义模型。
50
def lenet(X, params):
M
muli 已提交
51
    h1_conv = nd.Convolution(data=X, weight=params[0], bias=params[1],
A
Aston Zhang 已提交
52
                             kernel=(3, 3), num_filter=20)
53
    h1_activation = nd.relu(h1_conv)
A
Aston Zhang 已提交
54 55
    h1 = nd.Pooling(data=h1_activation, pool_type="avg", kernel=(2, 2),
                    stride=(2, 2))
56
    h2_conv = nd.Convolution(data=h1, weight=params[2], bias=params[3],
A
Aston Zhang 已提交
57
                             kernel=(5, 5), num_filter=50)
58
    h2_activation = nd.relu(h2_conv)
A
Aston Zhang 已提交
59 60
    h2 = nd.Pooling(data=h2_activation, pool_type="avg", kernel=(2, 2),
                    stride=(2, 2))
61 62 63
    h2 = nd.flatten(h2)
    h3_linear = nd.dot(h2, params[4]) + params[5]
    h3 = nd.relu(h3_linear)
A
Aston Zhang 已提交
64 65
    y_hat = nd.dot(h3, params[6]) + params[7]
    return y_hat
66

A
Aston Zhang 已提交
67
# 交叉熵损失函数。
A
Aston Zhang 已提交
68
loss = gloss.SoftmaxCrossEntropyLoss()
69 70
```

A
Aston Zhang 已提交
71
## 多GPU之间同步数据
M
muli 已提交
72

A
Aston Zhang 已提交
73
我们需要实现一些多GPU之间同步数据的辅助函数。下面函数将模型参数复制到某个特定GPU并初始化梯度。
74 75 76 77 78 79 80

```{.python .input  n=3}
def get_params(params, ctx):
    new_params = [p.copyto(ctx) for p in params]
    for p in new_params:
        p.attach_grad()
    return new_params
A
Aston Zhang 已提交
81 82 83
```

试一试把`params`复制到`mx.gpu(0)`上。
84

A
Aston Zhang 已提交
85 86 87 88
```{.python .input}
new_params = get_params(params, mx.gpu(0))
print('b1 weight:', new_params[1])
print('b1 grad:', new_params[1].grad)
89 90
```

A
Aston Zhang 已提交
91
给定分布在多个GPU之间的数据。以下函数可以把各个GPU上的数据加起来,然后再广播到所有GPU上。
92 93 94 95 96 97 98

```{.python .input  n=4}
def allreduce(data):
    for i in range(1, len(data)):
        data[0][:] += data[i].copyto(data[0].context)
    for i in range(1, len(data)):
        data[0].copyto(data[i])
A
Aston Zhang 已提交
99
```
100

A
Aston Zhang 已提交
101 102 103 104 105
简单测试一下`allreduce`函数。

```{.python .input}
data = [nd.ones((1,2), ctx=mx.gpu(i)) * (i + 1) for i in range(2)]
print('before allreduce:', data)
106
allreduce(data)
A
Aston Zhang 已提交
107
print('after allreduce:', data)
108 109
```

A
Aston Zhang 已提交
110
给定一个批量的数据样本,以下函数可以划分它们并复制到各个GPU上。
111 112 113 114 115

```{.python .input  n=5}
def split_and_load(data, ctx):
    n, k = data.shape[0], len(ctx)
    m = n // k
A
Aston Zhang 已提交
116 117 118
    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)]
```
119

A
Aston Zhang 已提交
120
让我们试着用`split_and_load`函数将6个数据样本平均分给2个GPU。
121

A
Aston Zhang 已提交
122 123 124 125 126 127 128
```{.python .input}
batch = nd.arange(24).reshape((6, 4))
ctx = [mx.gpu(0), mx.gpu(1)]
splitted = split_and_load(batch, ctx)
print('input: ', batch)
print('load into', ctx)
print('output:', splitted)
129 130
```

A
Aston Zhang 已提交
131
## 单个小批量上的多GPU训练
132

A
Aston Zhang 已提交
133
现在我们可以实现单个小批量上的多GPU训练了。它的实现主要依据本节介绍的数据并行方法。我们将使用刚刚定义的多GPU之间同步数据的辅助函数,例如`split_and_load``allreduce`
134 135

```{.python .input  n=6}
A
Aston Zhang 已提交
136 137 138 139 140
def train_batch(X, y, gpu_params, ctx, lr):
    # 划分小批量数据样本并复制到各个 GPU 上。
    gpu_Xs = split_and_load(X, ctx)
    gpu_ys = split_and_load(y, ctx)
    # 在各个 GPU 上计算损失。
141
    with autograd.record():
A
Aston Zhang 已提交
142 143 144 145 146 147
        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:
        l.backward()
    # 把各个 GPU 上的梯度加起来,然后再广播到所有 GPU 上。
A
Aston Zhang 已提交
148 149
    for i in range(len(gpu_params[0])):
        allreduce([gpu_params[c][i].grad for c in range(len(ctx))])
A
Aston Zhang 已提交
150
    # 在各个 GPU 上更新自己维护的那一份完整的模型参数。
A
Aston Zhang 已提交
151
    for param in gpu_params:
A
Aston Zhang 已提交
152
        gb.sgd(param, lr, X.shape[0])
153 154
```

A
Aston Zhang 已提交
155
## 训练函数
156

A
Aston Zhang 已提交
157
现在我们可以定义训练函数。这里的训练函数和之前章节里的训练函数稍有不同。例如,在这里我们需要依据本节介绍的数据并行,将完整的模型参数复制到多个GPU上,并在每次迭代时对单个小批量上进行多GPU训练。
158 159 160

```{.python .input  n=7}
def train(num_gpus, batch_size, lr):
A
Aston Zhang 已提交
161
    train_iter, test_iter = gb.load_data_fashion_mnist(batch_size)
A
Aston Zhang 已提交
162 163
    ctx = [mx.gpu(i) for i in range(num_gpus)]
    print('running on:', ctx)
A
Aston Zhang 已提交
164
    # 将模型参数复制到 num_gpus 个 GPU 上。
A
Aston Zhang 已提交
165 166
    gpu_params = [get_params(params, c) for c in ctx]
    for epoch in range(1, 6):
167
        start = time()
A
iter  
Aston Zhang 已提交
168
        for X, y in train_iter:
A
Aston Zhang 已提交
169 170
            # 对单个小批量上进行多 GPU 训练。
            train_batch(X, y, gpu_params, ctx, lr)
M
muli 已提交
171
        nd.waitall()
A
Aston Zhang 已提交
172
        print('epoch %d, time: %.1f sec' % (epoch, time() - start))
A
Aston Zhang 已提交
173 174
        # 在 GPU0 上验证模型。
        net = lambda x: lenet(x, gpu_params[0])
A
Aston Zhang 已提交
175
        test_acc = gb.evaluate_accuracy(test_iter, net, ctx[0])
A
Aston Zhang 已提交
176
        print('validation accuracy: %.4f' % test_acc)
177 178
```

A
Aston Zhang 已提交
179
我们先使用一个GPU来训练。
180 181

```{.python .input  n=8}
A
Aston Zhang 已提交
182
train(num_gpus=1, batch_size=256, lr=0.3)
183 184
```

A
Aston Zhang 已提交
185
接下来,我们先使用2个GPU来训练。我们将批量大小也增加一倍,以使得GPU的计算资源能够得到较充分利用。
M
muli 已提交
186

A
Aston Zhang 已提交
187 188
```{.python .input  n=10}
train(num_gpus=2, batch_size=512, lr=0.3)
M
muli 已提交
189 190
```

A
Aston Zhang 已提交
191
由于批量大小增加了一倍,每个迭代周期的迭代次数减小了一半。因此,我们观察到每个迭代周期的耗时比单GPU训练时少了近一半。但由于总体迭代次数的减少,模型在验证数据集上的精度略有下降。这很可能是由于训练不够充分造成的。因此,多GPU训练时,我们可以适当增加迭代周期使训练较充分。
192 193


M
muli 已提交
194

A
Aston Zhang 已提交
195
## 小结
196

A
Aston Zhang 已提交
197
* 我们可以使用数据并行更充分地利用多个GPU的计算资源,实现多GPU训练模型。
198

M
muli 已提交
199
## 练习
200

A
Aston Zhang 已提交
201 202 203
* 在本节实验中,试一试不同的迭代周期、批量大小和学习率。
* 将本节实验的模型预测部分改为用多GPU预测。

S
Sheng Zha 已提交
204

A
Aston Zhang 已提交
205 206
## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/1884)

A
Aston Zhang 已提交
207
![](../img/qr_multiple-gpus.svg)