5.6. GPUs
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in SageMaker Studio Lab

Em tab_intro_decade, discutimos o rápido crescimento de computação nas últimas duas décadas. Em suma, o desempenho da GPU aumentou por um fator de 1000 a cada década desde 2000. Isso oferece ótimas oportunidades, mas também sugere uma necessidade significativa de fornecer tal desempenho.

Nesta seção, começamos a discutir como aproveitar este desempenho computacional para sua pesquisa. Primeiro usando GPUs únicas e, posteriormente, como usar várias GPUs e vários servidores (com várias GPUs).

Especificamente, discutiremos como para usar uma única GPU NVIDIA para cálculos. Primeiro, certifique-se de ter pelo menos uma GPU NVIDIA instalada. Em seguida, baixe o NVIDIA driver e CUDA. e siga as instruções para definir o caminho apropriado. Assim que esses preparativos forem concluídos, o comando nvidia-smi pode ser usado para ver as informações da placa gráfica.

!nvidia-smi
Sat Dec 11 05:38:38 2021
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 418.67       Driver Version: 418.67       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla V100-SXM2...  Off  | 00000000:00:1B.0 Off |                    0 |
| N/A   37C    P0    36W / 300W |     11MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   1  Tesla V100-SXM2...  Off  | 00000000:00:1C.0 Off |                    0 |
| N/A   37C    P0    36W / 300W |     11MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   2  Tesla V100-SXM2...  Off  | 00000000:00:1D.0 Off |                    0 |
| N/A   40C    P0    54W / 300W |    376MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   3  Tesla V100-SXM2...  Off  | 00000000:00:1E.0 Off |                    0 |
| N/A   36C    P0    39W / 300W |     11MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|    2     51838      C   ...conda3/envs/d2l-pt-release-1/bin/python   369MiB |
+-----------------------------------------------------------------------------+

Você deve ter notado que um tensor MXNet parece quase idêntico a um NumPy ndarray. Mas existem algumas diferenças cruciais. Um dos principais recursos que distinguem o MXNet da NumPy é o seu suporte para diversos dispositivos de hardware.

No MXNet, cada array possui um contexto. Até agora, por padrão, todas as variáveis e computação associada foram atribuídos à CPU. Normalmente, outros contextos podem ser várias GPUs. As coisas podem ficar ainda mais complicadas quando nós implantamos trabalhos em vários servidores. Ao atribuir matrizes a contextos de forma inteligente, podemos minimizar o tempo gasto transferência de dados entre dispositivos. Por exemplo, ao treinar redes neurais em um servidor com uma GPU, normalmente preferimos que os parâmetros do modelo residam na GPU.

Em seguida, precisamos confirmar que a versão GPU do MXNet está instalada. Se uma versão de CPU do MXNet já estiver instalada, precisamos desinstalá-lo primeiro. Por exemplo, use o comando pip uninstall mxnet, em seguida, instale a versão MXNet correspondente de acordo com sua versão CUDA. Supondo que você tenha o CUDA 10.0 instalado, você pode instalar a versão MXNet que suporta CUDA 10.0 via pip install mxnet-cu100.

!nvidia-smi
Sat Dec 11 06:56:32 2021
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 418.67       Driver Version: 418.67       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla V100-SXM2...  Off  | 00000000:00:1B.0 Off |                    0 |
| N/A   43C    P0    37W / 300W |     11MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   1  Tesla V100-SXM2...  Off  | 00000000:00:1C.0 Off |                    0 |
| N/A   58C    P0    45W / 300W |     11MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   2  Tesla V100-SXM2...  Off  | 00000000:00:1D.0 Off |                    0 |
| N/A   44C    P0    41W / 300W |     11MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   3  Tesla V100-SXM2...  Off  | 00000000:00:1E.0 Off |                    0 |
| N/A   61C    P0    62W / 300W |     11MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

No PyTorch, cada array possui um dispositivo, frequentemente o referimos como um contexto. Até agora, por padrão, todas as variáveis e computação associada foram atribuídos à CPU. Normalmente, outros contextos podem ser várias GPUs. As coisas podem ficar ainda mais complicadas quando nós implantamos trabalhos em vários servidores. Ao atribuir matrizes a contextos de forma inteligente, podemos minimizar o tempo gasto transferência de dados entre dispositivos. Por exemplo, ao treinar redes neurais em um servidor com uma GPU, normalmente preferimos que os parâmetros do modelo residam na GPU.

Em seguida, precisamos confirmar que a versão GPU do PyTorch está instalada. Se uma versão CPU do PyTorch já estiver instalada, precisamos desinstalá-lo primeiro. Por exemplo, use o comando pip uninstall torch, em seguida, instale a versão correspondente do PyTorch de acordo com sua versão CUDA. Supondo que você tenha o CUDA 10.0 instalado, você pode instalar a versão PyTorch compatível com CUDA 10.0 via pip install torch-cu100.

!nvidia-smi
Sat Dec 11 07:57:09 2021
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 418.67       Driver Version: 418.67       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla V100-SXM2...  Off  | 00000000:00:1B.0 Off |                    0 |
| N/A   40C    P0    50W / 300W |     11MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   1  Tesla V100-SXM2...  Off  | 00000000:00:1C.0 Off |                    0 |
| N/A   36C    P0    50W / 300W |    584MiB / 16130MiB |      5%      Default |
+-------------------------------+----------------------+----------------------+
|   2  Tesla V100-SXM2...  Off  | 00000000:00:1D.0 Off |                    0 |
| N/A   73C    P0   276W / 300W |   8892MiB / 16130MiB |     98%      Default |
+-------------------------------+----------------------+----------------------+
|   3  Tesla V100-SXM2...  Off  | 00000000:00:1E.0 Off |                    0 |
| N/A   51C    P0    48W / 300W |     11MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|    1     80341      C   ...conda3/envs/d2l-pt-release-0/bin/python   573MiB |
|    2     90676      C   ...conda3/envs/d2l-pt-release-0/bin/python  8881MiB |
+-----------------------------------------------------------------------------+

Para executar os programas desta seção, você precisa de pelo menos duas GPUs. Observe que isso pode ser extravagante para a maioria dos computadores desktop mas está facilmente disponível na nuvem, por exemplo, usando as instâncias multi-GPU do AWS EC2. Quase todas as outras seções * não * requerem várias GPUs. Em vez disso, isso é simplesmente para ilustrar como os dados fluem entre diferentes dispositivos.

5.6.1. Dispositivos Computacionais

Podemos especificar dispositivos, como CPUs e GPUs, para armazenamento e cálculo. Por padrão, os tensores são criados na memória principal e, em seguida, use a CPU para calculá-lo.

No MXNet, a CPU e a GPU podem ser indicadas por cpu () e gpu(). Deve-se notar que cpu() (ou qualquer número inteiro entre parênteses) significa todas as CPUs físicas e memória. Isso significa que os cálculos do MXNet tentará usar todos os núcleos da CPU. No entanto, gpu() representa apenas uma carta e a memória correspondente. Se houver várias GPUs, usamos gpu(i) para representar a \(i^\mathrm{th}\) GPU (\(i\) começa em 0). Além disso, gpu(0) e gpu() são equivalentes.

from mxnet import np, npx
from mxnet.gluon import nn

npx.set_np()

npx.cpu(), npx.gpu(), npx.gpu(1)
(cpu(0), gpu(0), gpu(1))

No PyTorch, a CPU e a GPU podem ser indicadas por torch.device('cpu') e torch.cuda.device('cuda'). Deve-se notar que o dispositivo cpu significa todas as CPUs físicas e memória. Isso significa que os cálculos de PyTorch tentará usar todos os núcleos da CPU. No entanto, um dispositivo gpu representa apenas uma placa e a memória correspondente. Se houver várias GPUs, usamos torch.cuda.device(f'cuda: {i}') para representar a \(i^\mathrm{th}\) GPU (\(i\) começa em 0). Além disso, gpu:0 e gpu são equivalentes.

import torch
from torch import nn

torch.device('cpu'), torch.cuda.device('cuda'), torch.cuda.device('cuda:1')
(device(type='cpu'),
 <torch.cuda.device at 0x7f5ec461f9d0>,
 <torch.cuda.device at 0x7f5ec4645ca0>)
import tensorflow as tf

tf.device('/CPU:0'), tf.device('/GPU:0'), tf.device('/GPU:1')
(<tensorflow.python.eager.context._EagerDeviceContext at 0x7ff644556550>,
 <tensorflow.python.eager.context._EagerDeviceContext at 0x7ff644556ee0>,
 <tensorflow.python.eager.context._EagerDeviceContext at 0x7ff6445566d0>)

Podemos consultar o número de GPUs disponíveis.

npx.num_gpus()
2
torch.cuda.device_count()
2
len(tf.config.experimental.list_physical_devices('GPU'))
2

Agora definimos duas funções convenientes que nos permitem para executar o código mesmo que as GPUs solicitadas não existam.

def try_gpu(i=0):  #@save
    """Return gpu(i) if exists, otherwise return cpu()."""
    return npx.gpu(i) if npx.num_gpus() >= i + 1 else npx.cpu()

def try_all_gpus():  #@save
    """Return all available GPUs, or [cpu()] if no GPU exists."""
    devices = [npx.gpu(i) for i in range(npx.num_gpus())]
    return devices if devices else [npx.cpu()]

try_gpu(), try_gpu(10), try_all_gpus()
(gpu(0), cpu(0), [gpu(0), gpu(1)])
def try_gpu(i=0):  #@save
    """Return gpu(i) if exists, otherwise return cpu()."""
    if torch.cuda.device_count() >= i + 1:
        return torch.device(f'cuda:{i}')
    return torch.device('cpu')

def try_all_gpus():  #@save
    """Return all available GPUs, or [cpu(),] if no GPU exists."""
    devices = [torch.device(f'cuda:{i}')
             for i in range(torch.cuda.device_count())]
    return devices if devices else [torch.device('cpu')]

try_gpu(), try_gpu(10), try_all_gpus()
(device(type='cuda', index=0),
 device(type='cpu'),
 [device(type='cuda', index=0), device(type='cuda', index=1)])
def try_gpu(i=0):  #@save
    """Return gpu(i) if exists, otherwise return cpu()."""
    if len(tf.config.experimental.list_physical_devices('GPU')) >= i + 1:
        return tf.device(f'/GPU:{i}')
    return tf.device('/CPU:0')

def try_all_gpus():  #@save
    """Return all available GPUs, or [cpu(),] if no GPU exists."""
    num_gpus = len(tf.config.experimental.list_physical_devices('GPU'))
    devices = [tf.device(f'/GPU:{i}') for i in range(num_gpus)]
    return devices if devices else [tf.device('/CPU:0')]

try_gpu(), try_gpu(10), try_all_gpus()
(<tensorflow.python.eager.context._EagerDeviceContext at 0x7ff59d9e6b20>,
 <tensorflow.python.eager.context._EagerDeviceContext at 0x7ff59004d040>,
 [<tensorflow.python.eager.context._EagerDeviceContext at 0x7ff59004d070>,
  <tensorflow.python.eager.context._EagerDeviceContext at 0x7ff59004d1c0>])

5.6.2. Tensores e GPUs

Por padrão, tensores são criados na CPU. Podemos consultar o dispositivo onde o tensor está localizado.

x = np.array([1, 2, 3])
x.ctx
cpu(0)
x = torch.tensor([1, 2, 3])
x.device
device(type='cpu')
x = tf.constant([1, 2, 3])
x.device
'/job:localhost/replica:0/task:0/device:CPU:0'

É importante notar que sempre que quisermos para operar em vários termos, eles precisam estar no mesmo dispositivo. Por exemplo, se somarmos dois tensores, precisamos ter certeza de que ambos os argumentos estão no mesmo dispositivo — caso contrário, a estrutura não saberia onde armazenar o resultado ou mesmo como decidir onde realizar o cálculo.

5.6.2.1. Armazenamento na GPU

Existem várias maneiras de armazenar um tensor na GPU. Por exemplo, podemos especificar um dispositivo de armazenamento ao criar um tensor. A seguir, criamos a variável tensorial X no primeiro gpu. O tensor criado em uma GPU consome apenas a memória desta GPU. Podemos usar o comando nvidia-smi para ver o uso de memória da GPU. Em geral, precisamos ter certeza de não criar dados que excedam o limite de memória da GPU.

X = np.ones((2, 3), ctx=try_gpu())
X
array([[1., 1., 1.],
       [1., 1., 1.]], ctx=gpu(0))
X = torch.ones(2, 3, device=try_gpu())
X
tensor([[1., 1., 1.],
        [1., 1., 1.]], device='cuda:0')
with try_gpu():
    X = tf.ones((2, 3))
X
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[1., 1., 1.],
       [1., 1., 1.]], dtype=float32)>

Supondo que você tenha pelo menos duas GPUs, o código a seguir criará um tensor aleatório na segunda GPU.

Y = np.random.uniform(size=(2, 3), ctx=try_gpu(1))
Y
array([[0.67478997, 0.07540122, 0.9956977 ],
       [0.09488854, 0.415456  , 0.11231736]], ctx=gpu(1))
Y = torch.rand(2, 3, device=try_gpu(1))
Y
tensor([[0.0183, 0.8929, 0.2991],
        [0.5415, 0.1923, 0.6762]], device='cuda:1')
with try_gpu(1):
    Y = tf.random.uniform((2, 3))
Y
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[0.9953705 , 0.2957828 , 0.80567956],
       [0.11053193, 0.7893543 , 0.5817497 ]], dtype=float32)>

5.6.2.2. Copiando

Se quisermos calcular X + Y, precisamos decidir onde realizar esta operação. Por exemplo, como mostrado em Fig. 5.6.1, podemos transferir X para a segunda GPU e realizar a operação lá. Não simplesmente adicione X eY, pois isso resultará em uma exceção. O mecanismo de tempo de execução não saberia o que fazer: ele não consegue encontrar dados no mesmo dispositivo e falha. Já que Y vive na segunda GPU, precisamos mover X para lá antes de podermos adicionar os dois.

../_images/copyto.svg

Fig. 5.6.1 Copiar dados para realizar uma operação no mesmo dispositivo.

Z = X.copyto(try_gpu(1))
print(X)
print(Z)
[[1. 1. 1.]
 [1. 1. 1.]] @gpu(0)
[[1. 1. 1.]
 [1. 1. 1.]] @gpu(1)
Z = X.cuda(1)
print(X)
print(Z)
tensor([[1., 1., 1.],
        [1., 1., 1.]], device='cuda:0')
tensor([[1., 1., 1.],
        [1., 1., 1.]], device='cuda:1')
with try_gpu(1):
    Z = X
print(X)
print(Z)
tf.Tensor(
[[1. 1. 1.]
 [1. 1. 1.]], shape=(2, 3), dtype=float32)
tf.Tensor(
[[1. 1. 1.]
 [1. 1. 1.]], shape=(2, 3), dtype=float32)

Agora que os dados estão na mesma GPU (ambos são Z eY), podemos somá-los.

Y + Z
array([[1.6747899, 1.0754012, 1.9956977],
       [1.0948886, 1.415456 , 1.1123173]], ctx=gpu(1))

Imagine que sua variável Z já esteja em sua segunda GPU. O que acontece se ainda chamarmos Z.copyto(gpu(1))? Ele fará uma cópia e alocará nova memória, mesmo que essa variável já resida no dispositivo desejado. Há momentos em que, dependendo do ambiente em que nosso código está sendo executado, duas variáveis podem já estar no mesmo dispositivo. Então, queremos fazer uma cópia apenas se as variáveis atualmente vivem em dispositivos diferentes. Nestes casos, podemos chamar as_in_ctx. Se a variável já estiver viva no dispositivo especificado então este é um ambiente autônomo. A menos que você queira especificamente fazer uma cópia, as_in_ctx é o método de escolha.

Z.as_in_ctx(try_gpu(1)) is Z
True
Y + Z
tensor([[1.0183, 1.8929, 1.2991],
        [1.5415, 1.1923, 1.6762]], device='cuda:1')

Imagine que sua variável Z já esteja em sua segunda GPU. O que acontece se ainda chamarmos Z.cuda(1)? Ele retornará Z em vez de fazer uma cópia e alocar nova memória.

Z.cuda(1) is Z
True
Y + Z
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[1.9953705, 1.2957828, 1.8056796],
       [1.1105319, 1.7893543, 1.5817497]], dtype=float32)>

Imagine que sua variável Z já esteja em sua segunda GPU. O que acontece se ainda chamarmos Z2 = Z no mesmo escopo de dispositivo? Ele retornará Z em vez de fazer uma cópia e alocar nova memória.

with try_gpu(1):
    Z2 = Z
Z2 is Z
True

5.6.2.3. Informações extra

As pessoas usam GPUs para fazer aprendizado de máquina porque eles esperam que ela seja rápida. Mas a transferência de variáveis entre dispositivos é lenta. Então, queremos que você tenha 100% de certeza que você deseja fazer algo lento antes de deixá-lo fazer. Se a estrutura de Deep Learning apenas fizesse a cópia automaticamente sem bater, então você pode não perceber que você escreveu algum código lento.

Além disso, a transferência de dados entre dispositivos (CPU, GPUs e outras máquinas) é algo muito mais lento do que a computação. Também torna a paralelização muito mais difícil, já que temos que esperar que os dados sejam enviados (ou melhor, para serem recebidos) antes de prosseguirmos com mais operações. É por isso que as operações de cópia devem ser realizadas com muito cuidado. Como regra geral, muitas pequenas operações são muito piores do que uma grande operação. Além disso, várias operações ao mesmo tempo são muito melhores do que muitas operações simples intercaladas no código a menos que você saiba o que está fazendo. Este é o caso, uma vez que tais operações podem bloquear se um dispositivo tem que esperar pelo outro antes de fazer outra coisa. É um pouco como pedir seu café em uma fila em vez de pré-encomendá-lo por telefone e descobrir que ele está pronto quando você estiver.

Por último, quando imprimimos tensores ou convertemos tensores para o formato NumPy, se os dados não estiverem na memória principal, o framework irá copiá-lo para a memória principal primeiro, resultando em sobrecarga de transmissão adicional. Pior ainda, agora está sujeito ao temido bloqueio de intérprete global isso faz tudo esperar que o Python seja concluído.

5.6.3. Redes Neurais e GPUs

Da mesma forma, um modelo de rede neural pode especificar dispositivos. O código a seguir coloca os parâmetros do modelo na GPU.

net = nn.Sequential()
net.add(nn.Dense(1))
net.initialize(ctx=try_gpu())
net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=try_gpu())
strategy = tf.distribute.MirroredStrategy()
with strategy.scope():
    net = tf.keras.models.Sequential([
        tf.keras.layers.Dense(1)])
INFO:tensorflow:Using MirroredStrategy with devices ('/job:localhost/replica:0/task:0/device:GPU:0', '/job:localhost/replica:0/task:0/device:GPU:1')

Veremos muitos mais exemplos de como executar modelos em GPUs nos capítulos seguintes, simplesmente porque eles se tornarão um pouco mais intensivos em termos de computação.

Quando a entrada é um tensor na GPU, o modelo calculará o resultado na mesma GPU.

net(X)
array([[0.04995865],
       [0.04995865]], ctx=gpu(0))
net(X)
tensor([[1.5026],
        [1.5026]], device='cuda:0', grad_fn=<AddmmBackward>)
net(X)
<tf.Tensor: shape=(2, 1), dtype=float32, numpy=
array([[-0.97097045],
       [-0.97097045]], dtype=float32)>

Vamos confirmar se os parâmetros do modelo estão armazenados na mesma GPU.

net[0].weight.data().ctx
gpu(0)
net[0].weight.data.device
device(type='cuda', index=0)
net.layers[0].weights[0].device, net.layers[0].weights[1].device
('/job:localhost/replica:0/task:0/device:GPU:0',
 '/job:localhost/replica:0/task:0/device:GPU:0')

Resumindo, contanto que todos os dados e parâmetros estejam no mesmo dispositivo, podemos aprender modelos com eficiência. Nos próximos capítulos, veremos vários desses exemplos.

5.6.4. Sumário

  • Podemos especificar dispositivos para armazenamento e cálculo, como CPU ou GPU. Por padrão, os dados são criados na memória principal e então usa-se a CPU para cálculos.

  • A estrutura de Deep Learning requer todos os dados de entrada para cálculo estar no mesmo dispositivo, seja CPU ou a mesma GPU.

  • Você pode perder um desempenho significativo movendo dados sem cuidado. Um erro típico é o seguinte: calcular a perda para cada minibatch na GPU e relatando de volta para o usuário na linha de comando (ou registrando-o em um NumPy ndarray) irá disparar um bloqueio global do interpretador que paralisa todas as GPUs. É muito melhor alocar memória para registrar dentro da GPU e apenas mover registros maiores.

5.6.5. Exercícios

  1. Tente uma tarefa de computação maior, como a multiplicação de grandes matrizes, e veja a diferença de velocidade entre a CPU e a GPU. Que tal uma tarefa com uma pequena quantidade de cálculos?

  2. Como devemos ler e escrever os parâmetros do modelo na GPU?

  3. Meça o tempo que leva para calcular 1000 multiplicações matriz-matriz de \(100 \times 100\) matrizes e registrar a norma de Frobenius da matriz de saída, um resultado de cada vez vs. manter um registro na GPU e transferir apenas o resultado final.

  4. Meça quanto tempo leva para realizar duas multiplicações matriz-matriz em duas GPUs ao mesmo tempo vs. em sequência em uma GPU. Dica: você deve ver uma escala quase linear.