CUDA 语义
3.1 设备分配
torch.cuda
用于设置和运行 CUDA
操作。它会跟踪当前选定的 GPU,并且您分配的所有 CUDA 张量将默认在该设备上创建。所选设备可以使用 torch.cuda.device
环境管理器进行更改。
一旦分配了张量,您就可以对其执行操作而必在意所选的设备如何,并且结果将总是与张量一起放置在相同的设备上。
默认的情况下不允许进行交叉 GPU 操作,除了 copy_()
和其他具有类似复制功能的方法(如 to()
和 cuda()
)之外。除非启用端到端的存储器访问,否则任何尝试将张量分配到不同设备上的操作都会引发错误。
下面可以用一个小例子来展示:
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()
),否则每个流内的操作都按照它们创建的顺序进行序列化,但是来自不同流的操作可以以任意相对顺序并发执行。例如,下面的代码是不正确的:
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。
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
,我们可以使用它在所需的设备上创建一个张量。
x = torch.empty((8, 42), device = args.device)
net = Network().to(device = args.device)
这可以在许多情况下用于生成设备不可知代码。以下是使用 dataloader
的例子:
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
上下文管理器。
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 上下文和所传递的属性参数。
这是建立模块时推荐的做法,在前向传递期间需要在内部创建新的张量
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
的张量)。
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)存在重要的注意事项;除非准确的满足了数据处理的要求,否则很可能您的程序将具有不正确或未定义的行为。
译者署名
用户名 | 头像 | 职能 | 签名 |
---|---|---|---|
风中劲草 | 翻译 | 人生总要追求点什么 |