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

1.5节中, 我们回顾了过去20年计算能力的快速增长。 简而言之,自2000年以来,GPU性能每十年增长1000倍。

本节,我们将讨论如何利用这种计算性能进行研究。 首先是如何使用单个GPU,然后是如何使用多个GPU和多个服务器(具有多个GPU)。

我们先看看如何使用单个NVIDIA GPU进行计算。 首先,确保你至少安装了一个NVIDIA GPU。 然后,下载NVIDIA驱动和CUDA 并按照提示设置适当的路径。 当这些准备工作完成,就可以使用nvidia-smi命令来查看显卡信息。

!nvidia-smi
Sun Jul 31 00:43:33 2022
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 460.106.00   Driver Version: 460.106.00   CUDA Version: 11.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  Tesla V100-SXM2...  Off  | 00000000:00:1B.0 Off |                    0 |
| N/A   52C    P0    54W / 300W |      1MiB / 16160MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
|   1  Tesla V100-SXM2...  Off  | 00000000:00:1C.0 Off |                    0 |
| N/A   51C    P0    52W / 300W |      0MiB / 16160MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
|   2  Tesla V100-SXM2...  Off  | 00000000:00:1D.0 Off |                    0 |
| N/A   53C    P0    55W / 300W |      0MiB / 16160MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
|   3  Tesla V100-SXM2...  Off  | 00000000:00:1E.0 Off |                    0 |
| N/A   51C    P0    56W / 300W |      3MiB / 16160MiB |      4%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

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

之前,你可能已经注意到MXNet张量看起来与NumPy的ndarray几乎相同。 但有一些关键区别,其中之一是MXNet支持不同的硬件设备。

在MXNet中,每个数组都有一个上下文(context)。 默认情况下,所有变量和相关的计算都分配给CPU。 有时上下文可能是GPU。 当我们跨多个服务器部署作业时,事情会变得更加棘手。 通过智能地将数组分配给上下文, 我们可以最大限度地减少在设备之间传输数据的时间。 例如,当在带有GPU的服务器上训练神经网络时, 我们通常希望模型的参数在GPU上。

接下来,我们需要确认是否安装了MXNet的GPU版本。 如果已经安装了MXNet的CPU版本,我们需要先卸载它。 例如,使用pip uninstall mxnet命令, 然后根据你的CUDA版本安装相应的MXNet的GPU版本。 例如,假设你已经安装了CUDA10.0, 你可以通过pip install mxnet-cu100安装支持CUDA10.0的MXNet版本。

!nvidia-smi
Sun Jul 31 03:18:46 2022
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 460.106.00   Driver Version: 460.106.00   CUDA Version: 11.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  Tesla V100-SXM2...  Off  | 00000000:00:1B.0 Off |                    0 |
| N/A   61C    P0   207W / 300W |   2876MiB / 16160MiB |     87%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
|   1  Tesla V100-SXM2...  Off  | 00000000:00:1C.0 Off |                    0 |
| N/A   47C    P0    44W / 300W |      0MiB / 16160MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
|   2  Tesla V100-SXM2...  Off  | 00000000:00:1D.0 Off |                    0 |
| N/A   56C    P0   258W / 300W |   2646MiB / 16160MiB |     82%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
|   3  Tesla V100-SXM2...  Off  | 00000000:00:1E.0 Off |                    0 |
| N/A   48C    P0    50W / 300W |      0MiB / 16160MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A     19432      C   ...l-zh-release-1/bin/python     2873MiB |
|    2   N/A  N/A     19432      C   ...l-zh-release-1/bin/python     2643MiB |
+-----------------------------------------------------------------------------+

在PyTorch中,每个数组都有一个设备(device), 我们通常将其称为上下文(context)。 默认情况下,所有变量和相关的计算都分配给CPU。 有时上下文可能是GPU。 当我们跨多个服务器部署作业时,事情会变得更加棘手。 通过智能地将数组分配给上下文, 我们可以最大限度地减少在设备之间传输数据的时间。 例如,当在带有GPU的服务器上训练神经网络时, 我们通常希望模型的参数在GPU上。

!nvidia-smi
Sun Jul 31 03:58:18 2022
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 460.106.00   Driver Version: 460.106.00   CUDA Version: 11.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  Tesla V100-SXM2...  Off  | 00000000:00:1B.0 Off |                    0 |
| N/A   44C    P0    51W / 300W |    660MiB / 16160MiB |      3%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
|   1  Tesla V100-SXM2...  Off  | 00000000:00:1C.0 Off |                    0 |
| N/A   56C    P0    54W / 300W |      0MiB / 16160MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
|   2  Tesla V100-SXM2...  Off  | 00000000:00:1D.0 Off |                    0 |
| N/A   46C    P0    54W / 300W |    724MiB / 16160MiB |      4%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
|   3  Tesla V100-SXM2...  Off  | 00000000:00:1E.0 Off |                    0 |
| N/A   48C    P0    55W / 300W |      0MiB / 16160MiB |      4%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A     94556      C   ...l-zh-release-0/bin/python      657MiB |
|    2   N/A  N/A    100458      C   ...l-zh-release-0/bin/python      721MiB |
+-----------------------------------------------------------------------------+

要运行此部分中的程序,至少需要两个GPU。 注意,对于大多数桌面计算机来说,这可能是奢侈的,但在云中很容易获得。 例如,你可以使用AWS EC2的多GPU实例。 本书的其他章节大都不需要多个GPU, 而本节只是为了展示数据如何在不同的设备之间传递。

5.6.1. 计算设备

我们可以指定用于存储和计算的设备,如CPU和GPU。 默认情况下,张量是在内存中创建的,然后使用CPU计算它。

在MXNet中,CPU和GPU可以用cpu()gpu()表示。 需要注意的是,cpu()(或括号中的任意整数)表示所有物理CPU和内存, 这意味着MXNet的计算将尝试使用所有CPU核心。 然而,gpu()只代表一个卡和相应的显存。 如果有多个GPU,我们使用gpu(i)表示第\(i\)块GPU(\(i\)从0开始)。 另外,gpu(0)gpu()是等价的。

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))

在PyTorch中,CPU和GPU可以用torch.device('cpu')torch.device('cuda')表示。 应该注意的是,cpu设备意味着所有物理CPU和内存, 这意味着PyTorch的计算将尝试使用所有CPU核心。 然而,gpu设备只代表一个卡和相应的显存。 如果有多个GPU,我们使用torch.device(f'cuda:{i}') 来表示第\(i\)块GPU(\(i\)从0开始)。 另外,cuda:0cuda是等价的。

import torch
from torch import nn

torch.device('cpu'), torch.device('cuda'), torch.device('cuda:1')
(device(type='cpu'), device(type='cuda'), device(type='cuda', index=1))
import tensorflow as tf

tf.device('/CPU:0'), tf.device('/GPU:0'), tf.device('/GPU:1')
(<tensorflow.python.eager.context._EagerDeviceContext at 0x7f6e5572f040>,
 <tensorflow.python.eager.context._EagerDeviceContext at 0x7f6e54b79040>,
 <tensorflow.python.eager.context._EagerDeviceContext at 0x7f6e54b79680>)

我们可以查询可用gpu的数量。

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

现在我们定义了两个方便的函数, 这两个函数允许我们在不存在所需所有GPU的情况下运行代码。

def try_gpu(i=0):  #@save
    """如果存在,则返回gpu(i),否则返回cpu()"""
    return npx.gpu(i) if npx.num_gpus() >= i + 1 else npx.cpu()

def try_all_gpus():  #@save
    """返回所有可用的GPU,如果没有GPU,则返回[cpu()]"""
    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
    """如果存在,则返回gpu(i),否则返回cpu()"""
    if torch.cuda.device_count() >= i + 1:
        return torch.device(f'cuda:{i}')
    return torch.device('cpu')

def try_all_gpus():  #@save
    """返回所有可用的GPU,如果没有GPU,则返回[cpu(),]"""
    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
    """如果存在,则返回gpu(i),否则返回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
    """返回所有可用的GPU,如果没有GPU,则返回[cpu(),]"""
    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 0x7f6e54b91b00>,
 <tensorflow.python.eager.context._EagerDeviceContext at 0x7f6e54b91e40>,
 [<tensorflow.python.eager.context._EagerDeviceContext at 0x7f6e54b91e80>,
  <tensorflow.python.eager.context._EagerDeviceContext at 0x7f6e54b91f00>])

5.6.2. 张量与GPU

我们可以查询张量所在的设备。 默认情况下,张量是在CPU上创建的。

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:GPU:0'

需要注意的是,无论何时我们要对多个项进行操作, 它们都必须在同一个设备上。 例如,如果我们对两个张量求和, 我们需要确保两个张量都位于同一个设备上, 否则框架将不知道在哪里存储结果,甚至不知道在哪里执行计算。

5.6.2.1. 存储在GPU上

有几种方法可以在GPU上存储张量。 例如,我们可以在创建张量时指定存储设备。接 下来,我们在第一个gpu上创建张量变量X。 在GPU上创建的张量只消耗这个GPU的显存。 我们可以使用nvidia-smi命令查看显存使用情况。 一般来说,我们需要确保不创建超过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)>

假设你至少有两个GPU,下面的代码将在第二个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.1379, 0.4532, 0.9869],
        [0.4779, 0.7426, 0.9622]], device='cuda:1')
with try_gpu(1):
    Y = tf.random.uniform((2, 3))
Y
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[0.92645   , 0.7453978 , 0.50585294],
       [0.70877016, 0.39366913, 0.8184483 ]], dtype=float32)>

5.6.2.2. 复制

如果我们要计算X + Y,我们需要决定在哪里执行这个操作。 例如,如 图5.6.1所示, 我们可以将X传输到第二个GPU并在那里执行操作。 不要简单地X加上Y,因为这会导致异常, 运行时引擎不知道该怎么做:它在同一设备上找不到数据会导致失败。 由于Y位于第二个GPU上,所以我们需要将X移到那里, 然后才能执行相加运算。

../_images/copyto.svg

图5.6.1 复制数据以在同一设备上执行操作

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)

现在数据在同一个GPU上(ZY都在),我们可以将它们相加。

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

假设变量Z已经存在于第二个GPU上。 如果现在我们还是调用Z.copyto(gpu(1))会发生什么? 即使该变量已经存在于目标设备(第二个GPU)上, 它仍将被复制并保存在新分配的显存中。 有时,我们只想在变量存在于不同设备中时进行复制。 在这种情况下,我们可以调用as_in_ctx。 如果变量已经存在于指定的设备中,则这不会进行任何操作。 除非你特别想创建一个复制,否则选择as_in_ctx方法。

Z.as_in_ctx(try_gpu(1)) is Z
True
Y + Z
tensor([[1.1379, 1.4532, 1.9869],
        [1.4779, 1.7426, 1.9622]], device='cuda:1')

假设变量Z已经存在于第二个GPU上。 如果我们还是调用Z.cuda(1)会发生什么? 它将返回Z,而不会复制并分配新内存。

Z.cuda(1) is Z
True
Y + Z
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[1.92645  , 1.7453978, 1.5058529],
       [1.7087702, 1.3936691, 1.8184483]], dtype=float32)>

假设变量Z已经存在于第二个GPU上。 如果我们仍然在同一个设备作用域下调用Z2 = Z会发生什么? 它将返回Z,而不会复制并分配新内存。

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

5.6.2.3. 旁注

人们使用GPU来进行机器学习,因为单个GPU相对运行速度快。 但是在设备(CPU、GPU和其他机器)之间传输数据比计算慢得多。 这也使得并行化变得更加困难,因为我们必须等待数据被发送(或者接收), 然后才能继续进行更多的操作。 这就是为什么拷贝操作要格外小心。 根据经验,多个小操作比一个大操作糟糕得多。 此外,一次执行几个操作比代码中散布的许多单个操作要好得多(除非你确信自己在做什么)。 如果一个设备必须等待另一个设备才能执行其他操作, 那么这样的操作可能会阻塞。 这有点像排队订购咖啡,而不像通过电话预先订购: 当你到店的时候,咖啡已经准备好了。

最后,当我们打印张量或将张量转换为NumPy格式时, 如果数据不在内存中,框架会首先将其复制到内存中, 这会导致额外的传输开销。 更糟糕的是,它现在受制于全局解释器锁,使得一切都得等待Python完成。

5.6.3. 神经网络与GPU

类似地,神经网络模型可以指定设备。 下面的代码将模型参数放在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')

在接下来的几章中, 我们将看到更多关于如何在GPU上运行模型的例子, 因为它们将变得更加计算密集。

当输入为GPU上的张量时,模型将在同一GPU上计算结果。

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

让我们确认模型参数存储在同一个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')

总之,只要所有的数据和参数都在同一个设备上, 我们就可以有效地学习模型。 在下面的章节中,我们将看到几个这样的例子。

5.6.4. 小结

  • 我们可以指定用于存储和计算的设备,例如CPU或GPU。默认情况下,数据在主内存中创建,然后使用CPU进行计算。

  • 深度学习框架要求计算的所有输入数据都在同一设备上,无论是CPU还是GPU。

  • 不经意地移动数据可能会显著降低性能。一个典型的错误如下:计算GPU上每个小批量的损失,并在命令行中将其报告给用户(或将其记录在NumPy ndarray中)时,将触发全局解释器锁,从而使所有GPU阻塞。最好是为GPU内部的日志分配内存,并且只移动较大的日志。

5.6.5. 练习

  1. 尝试一个计算量更大的任务,比如大矩阵的乘法,看看CPU和GPU之间的速度差异。再试一个计算量很小的任务呢?

  2. 我们应该如何在GPU上读写模型参数?

  3. 测量计算1000个\(100 \times 100\)矩阵的矩阵乘法所需的时间,并记录输出矩阵的Frobenius范数,一次记录一个结果,而不是在GPU上保存日志并仅传输最终结果。

  4. 测量同时在两个GPU上执行两个矩阵乘法与在一个GPU上按顺序执行两个矩阵乘法所需的时间。提示:你应该看到近乎线性的缩放。