.. _sec_multi_gpu:
Treinamento em Várias GPUs
==========================
Até agora, discutimos como treinar modelos de forma eficiente em CPUs e
GPUs. Nós até mostramos como frameworks de aprendizado profundo como
MXNet (e TensorFlow) permitem paralelizar computação e comunicação
automaticamente entre elas em :numref:`sec_auto_para`. Por último,
mostramos em :numref:`sec_use_gpu` como listar todas as GPUs
disponíveis em um computador usando ``nvidia-smi``. O que *não*
discutimos é como realmente paralelizar o treinamento de aprendizado
profundo (omitimos qualquer discussão de *inferência* em várias GPUs
aqui, pois é um tópico raramente usado e avançado que vai além do escopo
deste livro). Em vez disso, sugerimos que, de alguma forma, seria
possível dividir os dados em vários dispositivos e fazê-los funcionar. A
presente seção preenche os detalhes e mostra como treinar uma rede em
paralelo ao começar do zero. Detalhes sobre como tirar proveito da
funcionalidade do Gluon são relegados a
:numref:`sec_multi_gpu_concise`. Assumimos que o leitor está
familiarizado com algoritmos SGD de minibatch, como os descritos em
:numref:`sec_minibatch_sgd`.
Dividindo o Problema
--------------------
Vamos começar com um problema de visão computacional simples e uma rede
ligeiramente arcaica, por exemplo, com várias camadas de convoluções,
agrupamento e, possivelmente, algumas camadas densas no final. Ou seja,
vamos começar com uma rede que se parece bastante com LeNet
:cite:`LeCun.Bottou.Bengio.ea.1998` ou AlexNet
:cite:`Krizhevsky.Sutskever.Hinton.2012`. Dadas várias GPUs (2 se for
um servidor de desktop, 4 em um g4dn.12xlarge, 8 em um AWS p3.16xlarge,
ou 16 em um p2.16xlarge), queremos particionar o treinamento de maneira
a obter uma boa aceleração enquanto beneficiando simultaneamente de
opções de design simples e reproduzíveis. Afinal, várias GPUs aumentam a
capacidade de *memória* e *computação*. Em suma, temos várias opções,
dado um minibatch de dados de treinamento que desejamos classificar.
- Podemos particionar as camadas de rede em várias GPUs. Ou seja, cada
GPU recebe como entrada os dados que fluem para uma camada
específica, processa os dados em várias camadas subsequentes e, em
seguida, envia os dados para a próxima GPU.
- Isso nos permite processar dados com redes maiores, em comparação
com o que uma única GPU poderia suportar.
- A pegada de memória por GPU pode ser bem controlada (é uma fração
da pegada total da rede)
- A interface entre as camadas (e, portanto, as GPUs) requer uma
sincronização rígida. Isso pode ser complicado, especialmente se
as cargas de trabalho computacionais não forem correspondidas
adequadamente entre as camadas. O problema é agravado por um
grande número de GPUs.
- A interface entre as camadas requer grandes quantidades de
transferência de dados (ativações, gradientes). Isso pode
sobrecarregar a largura de banda dos barramentos da GPU.
- Computação intensiva, mas operações sequenciais não são triviais
para particionar. Veja, por exemplo,
:cite:`Mirhoseini.Pham.Le.ea.2017` para um melhor esforço a este
respeito. Continua sendo um problema difícil e não está claro se é
possível obter uma boa escala (linear) em problemas não triviais.
Não o recomendamos, a menos que haja um excelente suporte de
estrutura / sistema operacional para encadear várias GPUs.
- Podemos dividir o trabalho necessário em camadas individuais. Por
exemplo, em vez de computar 64 canais em uma única GPU, poderíamos
dividir o problema em 4 GPUs, cada uma gerando dados para 16 canais.
Da mesma forma, para uma camada densa, poderíamos dividir o número de
neurônios de saída. :numref:`fig_alexnet_original` ilustra este
design. A figura foi tirada de
:cite:`Krizhevsky.Sutskever.Hinton.2012` onde esta estratégia foi
usada para lidar com GPUs que tinham uma pegada de memória muito
pequena (2 GB na época).
- Isso permite um bom dimensionamento em termos de computação, desde
que o número de canais (ou neurônios) não seja muito pequeno.
- Várias GPUs podem processar redes cada vez maiores, uma vez que a
memória disponível é dimensionada linearmente.
- Precisamos de um número *muito grande* de operações de
sincronização / barreira, pois cada camada depende dos resultados
de todas as outras camadas.
- A quantidade de dados que precisa ser transferida é potencialmente
ainda maior do que ao distribuir camadas entre GPUs. Não
recomendamos esta abordagem devido ao seu custo de largura de
banda e complexidade.
.. _fig_alexnet_original:
.. figure:: ../img/alexnet-original.svg
Paralelismo de modelo no design AlexNet original devido à memória GPU
limitada.
- Por último, podemos particionar os dados em várias GPUs. Desta forma,
todas as GPUs realizam o mesmo tipo de trabalho, embora em
observações diferentes. Os gradientes são agregados entre as GPUs
após cada minibatch.
- Esta é a abordagem mais simples e pode ser aplicada em qualquer
situação.
- Adicionar mais GPUs não nos permite treinar modelos maiores.
- Só precisamos sincronizar após cada minibatch. Dito isso, é
altamente desejável começar a trocar parâmetros de gradientes
enquanto outros ainda estão sendo calculados.
- Um grande número de GPUs leva a tamanhos de minibatch muito
grandes, reduzindo assim a eficiência do treinamento.
Em geral, o paralelismo de dados é a maneira mais conveniente de
proceder, desde que tenhamos acesso a GPUs com memória suficientemente
grande. Veja também: cite: ``Li.Andersen.Park.ea.2014`` para uma
descrição detalhada do particionamento para treinamento distribuído. A
memória da GPU costumava ser um problema nos primeiros dias do *deep
learning*. Até agora, esse problema foi resolvido para todos, exceto os
casos mais incomuns. Nos concentramos no paralelismo de dados a seguir.
Paralelismo de Dados
--------------------
Suponha que haja :math:`k` GPUs em uma máquina. Dado o modelo a ser
treinado, cada GPU manterá um conjunto completo de parâmetros do modelo
de forma independente. O treinamento prossegue da seguinte maneira
(consulte :numref:`fig_data_parallel` para obter detalhes sobre o
treinamento paralelo de dados em duas GPUs).
.. _fig_data_parallel:
.. figure:: ../img/data-parallel.svg
Cálculo do gradiente estocástico de minibatch usando paralelismo de
dados e duas GPUs.
- Em qualquer iteração de treinamento, dado um minibatch aleatório,
dividimos os exemplos no lote em porções de :math:`k` e os
distribuímos uniformemente pelas GPUs.
- Cada GPU calcula a perda e o gradiente dos parâmetros do modelo com
base no subconjunto de minibatch que foi atribuído e nos parâmetros
do modelo que mantém.
- Os gradientes locais de cada uma das :math:`k` GPUs são agregados
para obter o gradiente estocástico do minibatch atual.
- O gradiente agregado é redistribuído para cada GPU.
- Cada GPU usa este gradiente estocástico de minibatch para atualizar o
conjunto completo de parâmetros do modelo que ele mantém.
Uma comparação de diferentes formas de paralelização em várias GPUs é
descrita em :numref:`fig_splitting`. Observe que, na prática,
*aumentamos* o tamanho do minibatch :math:`k`-fold ao treinar em
:math:`k` GPUs, de forma que cada GPU tenha a mesma quantidade de
trabalho a fazer como se estivéssemos treinando em apenas uma única GPU.
Em um servidor de 16 GPUs, isso pode aumentar o tamanho do minibatch
consideravelmente e podemos ter que aumentar a taxa de aprendizado de
acordo. Observe também que :numref:`sec_batch_norm` precisa ser
ajustado (por exemplo, mantendo um coeficiente de norma de lote separado
por GPU). A seguir, usaremos :numref:`sec_lenet` como a rede modelo
para ilustrar o treinamento multi-GPU. Como sempre, começamos importando
os pacotes e módulos relevantes.
.. _fig_splitting:
.. figure:: ../img/splitting.svg
Paralelização em várias GPUs. Da esquerda para a direita - problema
original, particionamento de rede, particionamento de camada,
paralelismo de dados.
.. raw:: html
.. raw:: html
.. code:: python
%matplotlib inline
from mxnet import autograd, gluon, np, npx
from d2l import mxnet as d2l
npx.set_np()
.. raw:: html
.. raw:: html
.. code:: python
%matplotlib inline
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
.. raw:: html
.. raw:: html
Uma Rede Exemplo
----------------
Usamos LeNet conforme apresentado em :numref:`sec_lenet`. Nós o
definimos do zero para ilustrar a troca de parâmetros e a sincronização
em detalhes.
.. raw:: html
.. raw:: html
.. code:: python
# Initialize model parameters
scale = 0.01
W1 = np.random.normal(scale=scale, size=(20, 1, 3, 3))
b1 = np.zeros(20)
W2 = np.random.normal(scale=scale, size=(50, 20, 5, 5))
b2 = np.zeros(50)
W3 = np.random.normal(scale=scale, size=(800, 128))
b3 = np.zeros(128)
W4 = np.random.normal(scale=scale, size=(128, 10))
b4 = np.zeros(10)
params = [W1, b1, W2, b2, W3, b3, W4, b4]
# Definir o modelo
def lenet(X, params):
h1_conv = npx.convolution(data=X, weight=params[0], bias=params[1],
kernel=(3, 3), num_filter=20)
h1_activation = npx.relu(h1_conv)
h1 = npx.pooling(data=h1_activation, pool_type='avg', kernel=(2, 2),
stride=(2, 2))
h2_conv = npx.convolution(data=h1, weight=params[2], bias=params[3],
kernel=(5, 5), num_filter=50)
h2_activation = npx.relu(h2_conv)
h2 = npx.pooling(data=h2_activation, pool_type='avg', kernel=(2, 2),
stride=(2, 2))
h2 = h2.reshape(h2.shape[0], -1)
h3_linear = np.dot(h2, params[4]) + params[5]
h3 = npx.relu(h3_linear)
y_hat = np.dot(h3, params[6]) + params[7]
return y_hat
# Cross-entropy loss function
loss = gluon.loss.SoftmaxCrossEntropyLoss()
.. raw:: html
.. raw:: html
.. code:: python
# Initialize model parameters
scale = 0.01
W1 = torch.randn(size=(20, 1, 3, 3)) * scale
b1 = torch.zeros(20)
W2 = torch.randn(size=(50, 20, 5, 5)) * scale
b2 = torch.zeros(50)
W3 = torch.randn(size=(800, 128)) * scale
b3 = torch.zeros(128)
W4 = torch.randn(size=(128, 10)) * scale
b4 = torch.zeros(10)
params = [W1, b1, W2, b2, W3, b3, W4, b4]
# Define the model
def lenet(X, params):
h1_conv = F.conv2d(input=X, weight=params[0], bias=params[1])
h1_activation = F.relu(h1_conv)
h1 = F.avg_pool2d(input=h1_activation, kernel_size=(2, 2), stride=(2, 2))
h2_conv = F.conv2d(input=h1, weight=params[2], bias=params[3])
h2_activation = F.relu(h2_conv)
h2 = F.avg_pool2d(input=h2_activation, kernel_size=(2, 2), stride=(2, 2))
h2 = h2.reshape(h2.shape[0], -1)
h3_linear = torch.mm(h2, params[4]) + params[5]
h3 = F.relu(h3_linear)
y_hat = torch.mm(h3, params[6]) + params[7]
return y_hat
# Cross-entropy loss function
loss = nn.CrossEntropyLoss(reduction='none')
.. raw:: html
.. raw:: html
Sincronização de Dados
----------------------
Para um treinamento multi-GPU eficiente, precisamos de duas operações
básicas: em primeiro lugar, precisamos ter a capacidade de distribuir
uma lista de parâmetros para vários dispositivos e anexar gradientes
(``get_params``). Sem parâmetros, é impossível avaliar a rede em uma
GPU. Em segundo lugar, precisamos da capacidade de somar parâmetros em
vários dispositivos, ou seja, precisamos de uma função ``allreduce``.
.. raw:: html
.. raw:: html
.. code:: python
def get_params(params, device):
new_params = [p.copyto(device) for p in params]
for p in new_params:
p.attach_grad()
return new_params
.. raw:: html
.. raw:: html
.. code:: python
def get_params(params, device):
new_params = [p.clone().to(device) for p in params]
for p in new_params:
p.requires_grad_()
return new_params
.. raw:: html
.. raw:: html
Vamos tentar copiar os parâmetros do modelo de lenet para gpu (0).
.. raw:: html
.. raw:: html
.. code:: python
new_params = get_params(params, d2l.try_gpu(0))
print('b1 weight:', new_params[1])
print('b1 grad:', new_params[1].grad)
.. parsed-literal::
:class: output
b1 weight: [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] @gpu(0)
b1 grad: [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] @gpu(0)
.. raw:: html
.. raw:: html
.. code:: python
new_params = get_params(params, d2l.try_gpu(0))
print('b1 weight:', new_params[1])
print('b1 grad:', new_params[1].grad)
.. parsed-literal::
:class: output
b1 weight: tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
device='cuda:0', requires_grad=True)
b1 grad: None
.. raw:: html
.. raw:: html
Como ainda não realizamos nenhum cálculo, o gradiente em relação aos
pesos de polarização ainda é :math:`0`. Agora, vamos supor que temos um
vetor distribuído por várias GPUs. A função ``allreduce`` a seguir
adiciona todos os vetores e transmite o resultado de volta para todas as
GPUs. Observe que para que isso funcione precisamos copiar os dados para
o dispositivo acumulando os resultados.
.. raw:: html
.. raw:: html
.. code:: python
def allreduce(data):
for i in range(1, len(data)):
data[0][:] += data[i].copyto(data[0].ctx)
for i in range(1, len(data)):
data[0].copyto(data[i])
.. raw:: html
.. raw:: html
.. code:: python
def allreduce(data):
for i in range(1, len(data)):
data[0][:] += data[i].to(data[0].device)
for i in range(1, len(data)):
data[i] = data[0].to(data[i].device)
.. raw:: html
.. raw:: html
Vamos testar isso criando vetores com diferentes valores em diferentes
dispositivos e agregando-os.
.. raw:: html
.. raw:: html
.. code:: python
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])
allreduce(data)
print('after allreduce:\n', data[0], '\n', data[1])
.. parsed-literal::
:class: output
before allreduce:
[[1. 1.]] @gpu(0)
[[2. 2.]] @gpu(1)
after allreduce:
[[3. 3.]] @gpu(0)
[[3. 3.]] @gpu(1)
.. raw:: html
.. raw:: html
.. code:: python
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])
allreduce(data)
print('after allreduce:\n', data[0], '\n', data[1])
.. parsed-literal::
:class: output
before allreduce:
tensor([[1., 1.]], device='cuda:0')
tensor([[2., 2.]], device='cuda:1')
after allreduce:
tensor([[3., 3.]], device='cuda:0')
tensor([[3., 3.]], device='cuda:1')
.. raw:: html
.. raw:: html
Distribuindo Dados
------------------
Precisamos de uma função de utilitário simples para distribuir um
minibatch uniformemente em várias GPUs. Por exemplo, em 2 GPUs,
gostaríamos de ter metade dos dados a serem copiados para cada uma das
GPUs. Por ser mais conveniente e conciso, usamos a função embutida de
divisão e carga no Gluon (para experimentá-la em uma matriz
:math:`4 \times5`).
.. raw:: html
.. raw:: html
.. code:: python
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)
.. parsed-literal::
:class: output
input : [[ 0. 1. 2. 3. 4.]
[ 5. 6. 7. 8. 9.]
[10. 11. 12. 13. 14.]
[15. 16. 17. 18. 19.]]
load into [gpu(0), gpu(1)]
output: [array([[0., 1., 2., 3., 4.],
[5., 6., 7., 8., 9.]], ctx=gpu(0)), array([[10., 11., 12., 13., 14.],
[15., 16., 17., 18., 19.]], ctx=gpu(1))]
.. raw:: html
.. raw:: html
.. code:: python
data = torch.arange(20).reshape(4, 5)
devices = [torch.device('cuda:0'), torch.device('cuda:1')]
split = nn.parallel.scatter(data, devices)
print('input :', data)
print('load into', devices)
print('output:', split)
.. parsed-literal::
:class: output
input : tensor([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]])
load into [device(type='cuda', index=0), device(type='cuda', index=1)]
output: (tensor([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]], device='cuda:0'), tensor([[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]], device='cuda:1'))
.. raw:: html
.. raw:: html
Para reutilização posterior, definimos uma função ``split_batch`` que
divide os dados e rótulos.
.. raw:: html
.. raw:: html
.. code:: python
#@save
def split_batch(X, y, devices):
"""Split `X` and `y` into multiple devices."""
assert X.shape[0] == y.shape[0]
return (gluon.utils.split_and_load(X, devices),
gluon.utils.split_and_load(y, devices))
.. raw:: html
.. raw:: html
.. code:: python
#@save
def split_batch(X, y, devices):
"""Split `X` and `y` into multiple devices."""
assert X.shape[0] == y.shape[0]
return (nn.parallel.scatter(X, devices),
nn.parallel.scatter(y, devices))
.. raw:: html
.. raw:: html
Treinamento
-----------
Agora podemos implementar o treinamento multi-GPU em um único minibatch.
Sua implementação é baseada principalmente na abordagem de paralelismo
de dados descrita nesta seção. Usaremos as funções auxiliares que
acabamos de discutir, ``allreduce`` e\ ``split_and_load``, para
sincronizar os dados entre várias GPUs. Observe que não precisamos
escrever nenhum código específico para atingir o paralelismo. Uma vez
que o gráfico computacional não tem nenhuma dependência entre
dispositivos dentro de um minibatch, ele é executado em paralelo
*automaticamente*.
.. raw:: html
.. raw:: html
.. code:: python
def train_batch(X, y, device_params, devices, lr):
X_shards, y_shards = split_batch(X, y, devices)
with autograd.record(): # Loss is calculated separately on each GPU
losses = [loss(lenet(X_shard, device_W), y_shard)
for X_shard, y_shard, device_W in zip(
X_shards, y_shards, device_params)]
for l in losses: # Back Propagation is performed separately on each GPU
l.backward()
# Sum all gradients from each GPU and broadcast them to all GPUs
for i in range(len(device_params[0])):
allreduce([device_params[c][i].grad for c in range(len(devices))])
# The model parameters are updated separately on each GPU
for param in device_params:
d2l.sgd(param, lr, X.shape[0]) # Here, we use a full-size batch
.. raw:: html
.. raw:: html
.. code:: python
def train_batch(X, y, device_params, devices, lr):
X_shards, y_shards = split_batch(X, y, devices)
# Loss is calculated separately on each GPU
losses = [loss(lenet(X_shard, device_W), y_shard).sum()
for X_shard, y_shard, device_W in zip(
X_shards, y_shards, device_params)]
for l in losses: # Back Propagation is performed separately on each GPU
l.backward()
# Sum all gradients from each GPU and broadcast them to all GPUs
with torch.no_grad():
for i in range(len(device_params[0])):
allreduce([device_params[c][i].grad for c in range(len(devices))])
# The model parameters are updated separately on each GPU
for param in device_params:
d2l.sgd(param, lr, X.shape[0]) # Here, we use a full-size batch
.. raw:: html
.. raw:: html
Agora, podemos definir a função de treinamento. É um pouco diferente dos
usados nos capítulos anteriores: precisamos alocar as GPUs e copiar
todos os parâmetros do modelo para todos os dispositivos. Obviamente,
cada lote é processado usando ``train_batch`` para lidar com várias
GPUs. Por conveniência (e concisão do código), calculamos a precisão em
uma única GPU (isso é *ineficiente*, pois as outras GPUs estão ociosas).
.. raw:: html
.. raw:: html
.. code:: python
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)]
# Copy model parameters to num_gpus GPUs
device_params = [get_params(params, d) for d in devices]
# num_epochs, times, acces = 10, [], []
num_epochs = 10
animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs])
timer = d2l.Timer()
for epoch in range(num_epochs):
timer.start()
for X, y in train_iter:
# Perform multi-GPU training for a single minibatch
train_batch(X, y, device_params, devices, lr)
npx.waitall()
timer.stop()
# Verify the model on 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)}')
.. raw:: html
.. raw:: html
.. code:: python
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)]
# Copy model parameters to num_gpus GPUs
device_params = [get_params(params, d) for d in devices]
# num_epochs, times, acces = 10, [], []
num_epochs = 10
animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs])
timer = d2l.Timer()
for epoch in range(num_epochs):
timer.start()
for X, y in train_iter:
# Perform multi-GPU training for a single minibatch
train_batch(X, y, device_params, devices, lr)
torch.cuda.synchronize()
timer.stop()
# Verify the model on 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)}')
.. raw:: html
.. raw:: html
Experimento
-----------
Vamos ver como isso funciona bem em uma única GPU. Usamos um tamanho de
lote de 256 e uma taxa de aprendizado de 0,2.
.. raw:: html
.. raw:: html
.. code:: python
train(num_gpus=1, batch_size=256, lr=0.2)
.. parsed-literal::
:class: output
test acc: 0.84, 2.3 sec/epoch on [gpu(0)]
.. figure:: output_multiple-gpus_f17d18_93_1.svg
.. raw:: html
.. raw:: html
.. code:: python
train(num_gpus=1, batch_size=256, lr=0.2)
.. parsed-literal::
:class: output
test acc: 0.82, 2.4 sec/epoch on [device(type='cuda', index=0)]
.. figure:: output_multiple-gpus_f17d18_96_1.svg
.. raw:: html
.. raw:: html
Mantendo o tamanho do lote e a taxa de aprendizado inalterados e
alterando o número de GPUs para 2, podemos ver que a melhoria na
precisão do teste é aproximadamente a mesma que nos resultados do
experimento anterior. Em termos de algoritmos de otimização, eles são
idênticos. Infelizmente, não há aumento significativo a ser obtido aqui:
o modelo é simplesmente muito pequeno; além disso, temos apenas um
pequeno conjunto de dados, onde nossa abordagem um pouco menos
sofisticada para implementar o treinamento multi-GPU sofreu com a
sobrecarga significativa do Python. Encontraremos modelos mais complexos
e formas mais sofisticadas de paralelização daqui para frente. Vamos ver
o que acontece, no entanto, com o Fashion-MNIST.
.. raw:: html
.. raw:: html
.. code:: python
train(num_gpus=2, batch_size=256, lr=0.2)
.. parsed-literal::
:class: output
test acc: 0.85, 4.7 sec/epoch on [gpu(0), gpu(1)]
.. figure:: output_multiple-gpus_f17d18_102_1.svg
.. raw:: html
.. raw:: html
.. code:: python
train(num_gpus=2, batch_size=256, lr=0.2)
.. parsed-literal::
:class: output
test acc: 0.83, 2.4 sec/epoch on [device(type='cuda', index=0), device(type='cuda', index=1)]
.. figure:: output_multiple-gpus_f17d18_105_1.svg
.. raw:: html
.. raw:: html
Resumo
------
- Existem várias maneiras de dividir o treinamento de rede profunda em
várias GPUs. Podemos dividi-los entre camadas, entre camadas ou entre
dados. Os dois primeiros requerem transferências de dados fortemente
coreografadas. O paralelismo de dados é a estratégia mais simples.
- O treinamento paralelo de dados é direto. No entanto, aumenta o
tamanho efetivo do minibatch para ser eficiente.
- Os dados são divididos em várias GPUs, cada GPU executa sua própria
operação de avanço e retrocesso e, posteriormente, os gradientes são
agregados e os resultados transmitidos de volta às GPUs.
- Minibatches grandes podem exigir uma taxa de aprendizado ligeiramente
maior.
Exercícios
----------
1. Ao treinar em várias GPUs, altere o tamanho do minibatch de :math:`b`
para :math:`k \cdot b`, ou seja, aumente pelo número de GPUs.
2. Compare a precisão para diferentes taxas de aprendizagem. Como isso
se dimensiona com o número de GPUs.
3. Implemente um ``allreduce`` mais eficiente que agregue diferentes
parâmetros em diferentes GPUs (por que isso é mais eficiente em
primeiro lugar).
4. Implementar cálculo de precisão de teste multi-GPU.
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
`Discussions `__
.. raw:: html
.. raw:: html
.. raw:: html