CUDA语义
一旦分配了张量,您就可以对其执行操作而必在意所选的设备如何,并且结果将总是与张量一起放置在相同的设备上。
默认的情况下不允许进行交叉 GPU 操作,除了 copy_()
和其他具有类似复制功能的方法(如 to()
和 cuda()
)之外。除非启用端到端的存储器访问,否则任何尝试将张量分配到不同设备上的操作都会引发错误。
下面可以用一个小例子来展示:
3.2 异步执行
默认情况下,GPU 操作是异步的。当你调用一个使用 GPU 的函数时,这些操作会在特定的设备上排队,但不一定会在稍后执行。这允许我们并行更多的计算,包括 CPU 或其他 GPU 上的操作。
一般情况下,异步计算的效果对调用者是不可见的,因为(1)每个设备按照它们排队的顺序执行操作,(2)在 CPU 和 GPU 之间或两个 GPU 之间复制数据时,PyTorch 自动执行必要的同步。因此,计算将按每个操作同步执行的方式进行。
您可以通过设置环境变量 CUDA_LAUNCH_BLOCKING = 1
来强制进行同步计算。当 GPU 发生错误时,这可能非常方便。 (使用异步执行,只有在实际执行操作之后才会报告此类错误,因此堆栈跟踪不会显示请求的位置。)
作为一个例外,copy_()
等几个函数允许一个显式的异步参数 async
,它允许调用者在不必要时绕过同步。另一个例外是 CUDA 流,解释如下:
3.2.1 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):
B = torch.sum(A)
当“当前流”是默认流时,如上所述,PyTorch 在数据移动时自动执行必要的同步。但是,使用非默认流时,用户有责任确保正确的同步。
PyTorch
使用缓存内存分配器来加速内存分配。这允许在没有设备同步的情况下快速释放内存。但是,由分配器管理的未使用的内存仍将显示为在 nvidia-smi
中使用。您可以使用 和 max_memory_allocated()
来监视张量占用的内存,并使用 memory_cached()
和 max_memory_cached()
来监视由缓存分配器管理的内存。调用 empty_cache()
可以从 PyTorch
中释放所有未使用的缓存内存,以便其他 GPU 应用程序可以使用这些内存。但是,被张量占用的 GPU 内存不会被释放,因此它不能为 PyTorch
增加可用的 GPU 内存量。
3.4 最佳实践
3.4.1 设备诊断
由于 PyTorch 的结构,您可能需要明确编写与设备无关的(CPU 或 GPU)代码;比如创建一个新的张量作为循环神经网络的初始隐藏状态。
第一步是确定是否应该使用 GPU。一种常见的模式是使用 Python 的 argparse
模块来读入用户参数,并且有一个标志可用于禁用 CUDA,并结合 is_available()
使用。在下面的内容中,args.device
会生成一个 torch.device
对象,该对象可用于将张量移动到 CPU 或 CUDA。
现在我们有了 args.device
,我们可以使用它在所需的设备上创建一个张量。
x = torch.empty((8, 42), device = args.device)
net = Network().to(device = args.device)
这可以在许多情况下用于生成设备不可知代码。以下是使用 dataloader
的例子:
print("外部的设备是0") # 在设备0上
with torch.cuda.device(1):
print("内部的设备是1") # 设备1
如果您有一个张量,并且想要在同一个设备上创建一个相同类型的张量,那么您可以使用 torch.Tensor.new_*
方法(请参阅 torch.Tensor
)。torch.Tensor.new_*
方法保留了设备和张量的其他属性,而前面提到的 torch.*
工厂函数(Creation Ops)创建的张量则取决于当前的 GPU 上下文和所传递的属性参数。
这是建立模块时推荐的做法,在前向传递期间需要在内部创建新的张量
如果要创建与另一个张量相同类型和大小的张量,并将其填充为1或0,则可以使用 ones_like()
或 zeros_like()
作为便捷的辅助函数(也可以保留 torch.device
和 torch.dtype
的张量)。
x_cpu = torch.empty(2,3)
x_gpu = torch.empty(2,3)
y_gpu = torch.zeros_like(x_gpu)
当数据源自固定(页面锁定)内存时,主机在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 饱和。
使用 CUDA 模型进行多线程处理(multiprocessing)存在重要的注意事项;除非准确的满足了数据处理的要求,否则很可能您的程序将具有不正确或未定义的行为。