# CUDA 语义

*   [异步执行](#asynchronous-execution)
    *   [CUDA 流](#cuda-streams)
*   [内存管理](#memory-management)
*   [最佳实践](#best-practices)
    *   [设备无关代码](#device-agnostic-code)
    *   [使用固定的内存缓冲区](#use-pinned-memory-buffers)
    *   [使用 nn.DataParallel 替代 multiprocessing](#use-nn-dataparallel-instead-of-multiprocessing)

### 3.1 设备分配

`torch.cuda` 用于设置和运行 `CUDA` 操作。它会跟踪当前选定的 GPU，并且您分配的所有 CUDA 张量将默认在该设备上创建。所选设备可以使用 `torch.cuda.device` 环境管理器进行更改。

一旦分配了张量，您就可以对其执行操作而必在意所选的设备如何，并且结果将总是与张量一起放置在相同的设备上。

默认的情况下不允许进行交叉 GPU 操作，除了 `copy_()` 和其他具有类似复制功能的方法（如 `to()` 和 `cuda()` ）之外。除非启用端到端的存储器访问，否则任何尝试将张量分配到不同设备上的操作都会引发错误。

下面可以用一个小例子来展示：

```py
cuda = torch.device("cuda")  # 默认为 CUDA 设备
cuda0 = torch.device("cuda:0")
cuda2 = torch.device("cuda:2") # GPU 2

# x, y 的设备是 device(type='cuda', index=0)
x = torch.tensor([1., 2.], device=cuda0)
y = torch.tensor([1., 2.], cuda())

# 在 GPU 1 上分配张量
with torch.cuda.device(1):
    # GPU 1
    a = torch.tensor([1., 2.], device=cuda)

    # 从 CPU 传递到 GPU 1
    # a、b 所在的设备都是 device(type='cuda', index=1)
    b = torch.tensor([1., 2.]).cuda()

    # 也可以用 Tensor.to 来传递张量
    # b2 的设备与 a 同
    b2 = torch.tensor([1., 2.]).to(device=cuda)

    # 即使是在环境中，你也可以指定设备
    d = torch.randn(2, device=cuda2)
    e = torch.randn(2).to(cuda2) 
```

### 3.2 异步执行

默认情况下，GPU 操作是异步的。当你调用一个使用 GPU 的函数时，这些操作会在特定的设备上排队，但不一定会在稍后执行。这允许我们并行更多的计算，包括 CPU 或其他 GPU 上的操作。

一般情况下，异步计算的效果对调用者是不可见的，因为（1）每个设备按照它们排队的顺序执行操作，（2）在 CPU 和 GPU 之间或两个 GPU 之间复制数据时，PyTorch 自动执行必要的同步。因此，计算将按每个操作同步执行的方式进行。

您可以通过设置环境变量 `CUDA_LAUNCH_BLOCKING = 1` 来强制进行同步计算。当 GPU 发生错误时，这可能非常方便。 （使用异步执行，只有在实际执行操作之后才会报告此类错误，因此堆栈跟踪不会显示请求的位置。）

作为一个例外，`copy_()` 等几个函数允许一个显式的异步参数 `async`，它允许调用者在不必要时绕过同步。另一个例外是 CUDA 流，解释如下：

#### 3.2.1 CUDA 流

CUDA 流是属于特定设备的线性执行序列。您通常不需要明确创建一个，默认情况下，每个设备都使用其自己的“默认”流。

除非显式的使用同步函数（例如 `synchronize()` 或 `wait_stream()` ），否则每个流内的操作都按照它们创建的顺序进行序列化，但是来自不同流的操作可以以任意相对顺序并发执行。例如，下面的代码是不正确的：

```py
cuda = torch.device("cuda")
s = torch.cuda.stream()  # 在当前流中创建一个新的流
A = torch.empty((100,100), device = cuda).normal_(0.0, 1.0)

with torch.cuda.stream(s):
    # sum()可能在 normal_()执行完成前就开始执行
    B = torch.sum(A) 
```

当“当前流”是默认流时，如上所述，PyTorch 在数据移动时自动执行必要的同步。但是，使用非默认流时，用户有责任确保正确的同步。

### 3.3 内存管理

`PyTorch`使用缓存内存分配器来加速内存分配。这允许在没有设备同步的情况下快速释放内存。但是，由分配器管理的未使用的内存仍将显示为在 `nvidia-smi` 中使用。您可以使用 `memory_allocated()` 和 `max_memory_allocated()` 来监视张量占用的内存，并使用 `memory_cached()` 和 `max_memory_cached()` 来监视由缓存分配器管理的内存。调用 `empty_cache()` 可以从 `PyTorch` 中释放所有未使用的缓存内存，以便其他 GPU 应用程序可以使用这些内存。但是，被张量占用的 GPU 内存不会被释放，因此它不能为 `PyTorch` 增加可用的 GPU 内存量。

#### 3.4.1 设备诊断

由于 PyTorch 的结构，您可能需要明确编写与设备无关的（CPU 或 GPU）代码；比如创建一个新的张量作为循环神经网络的初始隐藏状态。

第一步是确定是否应该使用 GPU。一种常见的模式是使用 Python 的 `argparse` 模块来读入用户参数，并且有一个标志可用于禁用 CUDA，并结合 `is_available()` 使用。在下面的内容中，`args.device` 会生成一个 `torch.device` 对象，该对象可用于将张量移动到 CPU 或 CUDA。

```py
import argparse
import torch

parser = argparse.ArgumentParser(description='PyTorch Example')
parser.add_argument('--disable-cuda', action='stroe_true', help='Disable CUDA')
args = parser.parse_args()
args.device = None
if not args.disable_cuda and torch.cuda.is_available():
    args.device = torch.device('cuda')
else:
    args.device = torch.device('cpu') 
```

现在我们有了 `args.device`，我们可以使用它在所需的设备上创建一个张量。

```py
x = torch.empty((8, 42), device = args.device)
net = Network().to(device = args.device) 
```

这可以在许多情况下用于生成设备不可知代码。以下是使用 `dataloader` 的例子：

```py
cuda0 = torch.device('cuda:0')  # CUDA GPU 0
for i, x in enumerate(train_loader):
    x = x.to(cuda0) 
```

在系统上使用多个 GPU 时，您可以使用 `CUDA_VISIBLE_DEVICES` 环境标志来管理 PyTorch 可用的 GPU。如上所述，要手动控制在哪个 GPU 上创建张量，最佳做法是使用 `torch.cuda.device` 上下文管理器。

```py
print("外部的设备是 0") # 在设备 0 上
with torch.cuda.device(1):
    print("内部的设备是 1")  # 设备 1
print("外部的设备仍是 0")  # 设备 0 
```

如果您有一个张量，并且想要在同一个设备上创建一个相同类型的张量，那么您可以使用 `torch.Tensor.new_*` 方法（请参阅 `torch.Tensor` ）。`torch.Tensor.new_*` 方法保留了设备和张量的其他属性，而前面提到的 `torch.*` 工厂函数（Creation Ops）创建的张量则取决于当前的 GPU 上下文和所传递的属性参数。

这是建立模块时推荐的做法，在前向传递期间需要在内部创建新的张量

```py
cuda = torch.device("cuda")
x_cpu = torch.empty(2)
y_gpu = torch.empty(2, device = cuda)
x_cpu_long = torch.empty(2, dtype=torch.int64)

y_cpu = x_cpu.new_full([3,2], fill_value=0.3)
print(y_cpu)
    tensor([[ 0.3000,  0.3000],
            [ 0.3000,  0.3000],
            [ 0.3000,  0.3000]])
y_gpu = x_gpu.new_full([3,2], fill_value=-5)
print(y_gpu)
    tensor([[-5.0000, -5.0000],
            [-5.0000, -5.0000],
            [-5.0000, -5.0000]], device='cuda:0')

y_cpu_long = x_cpu_long.new_tensor([[1,2,3]])
print(y_cpu_long)
    tensor([[ 1,  2,  3]]) 
```

如果要创建与另一个张量相同类型和大小的张量，并将其填充为 1 或 0，则可以使用 `ones_like()` 或 `zeros_like()` 作为便捷的辅助函数（也可以保留 `torch.device` 和 `torch.dtype` 的张量）。

```py
x_cpu = torch.empty(2,3)
x_gpu = torch.empty(2,3)

y_cpu = torch.ones_like(x_cpu)
y_gpu = torch.zeros_like(x_gpu) 
```

### 3.4 使用固定的内存缓冲区

当数据源自固定（页面锁定）内存时，主机在 GPU 上的数据副本运行速度会更快。 CPU Tensors 和存储器暴露了一个 `pin_memory()` 方法，该方法返回对象的一个副本，并将数据放入固定区域。

一旦您固定(pin)一个张量或存储器，您就可以使用异步 GPU 副本。只需将一个额外的 `non_blocking = True` 参数传递给 `cuda()` 调用即可。这可以用于计算与数据传输的并行。

通过将 `pin_memory = True`传递给其构造函数，可以将 `DataLoader` 返回的批量数据置于固定内存(pin memory)中。

### 3.5 使用 nn.DataParallel 代替多线程处理

大多数涉及批量输入和多个 GPU 的使用案例应默认使用 `DataParallel`来利用多个 GPU。即使使用 GIL，单个 Python 进程也可以使多个 GPU 饱和。

从版本 0.1.9 开始，大量的 GPU（8+）可能未被充分利用。但是，这是一个正在积极开发中的已知问题。像往常一样，测试你的用例。

使用 CUDA 模型进行多线程处理(multiprocessing)存在重要的注意事项；除非准确的满足了数据处理的要求，否则很可能您的程序将具有不正确或未定义的行为。

### 译者署名

| 用户名 | 头像 | 职能 | 签名 |
| --- | --- | --- | --- |
| 风中劲草 | ![](img/2018033000352689884.jpeg) | 翻译 | 人生总要追求点什么 |