在 PyTorch 中固定内存实际上更慢?

Zis*_*Zis 11 python pytorch

我想知道为什么在 PyTorch 中固定内存会使事情变得更慢。通过阅读 的代码torch.utils.data.dataloader,我找到了在返回之前简单地调用每个批次的pin_memory=True选项。返回的张量仍在 CPU 上,此后我必须手动调用。因此,整个过程将是DataLoader.pin_memory().cuda(non_blocking=True)

for x in some_iter:
    yield x.pin_memory().cuda(non_blocking=True)
Run Code Online (Sandbox Code Playgroud)

我将其性能与

for x in some_iter:
    yield x.cuda()
Run Code Online (Sandbox Code Playgroud)

这是实际的代码

a = torch.rand(1024, 655360)

%%time
for i in a:
    i.pin_memory().cuda(non_blocking=True)
# CPU times: user 1.35 s, sys: 55.8 ms, total: 1.41 s
# Wall time: 396 ms

%%time
for i in a:
    i.pin_memory().cuda()
# CPU times: user 1.6 s, sys: 12.2 ms, total: 1.62 s
# Wall time: 404 ms

%%time
for i in a:
    i.cuda(non_blocking=True)
# CPU times: user 855 ms, sys: 3.87 ms, total: 859 ms
# Wall time: 274 ms

%%time
for i in a:
    i.cuda()
# CPU times: user 314 ms, sys: 12 µs, total: 314 ms
# Wall time: 313 ms
Run Code Online (Sandbox Code Playgroud)

因此,不固定内存既使用更少的 CPU 时间,而且在实际时间方面更快。固定内存不应该使数据传输异步从而更快吗?如果不是这样,我们为什么要进行 pin memory?


附注。我考虑了TensorDataset提前固定一个整体的可能性(而不是每次固定批次)。但这不能固定比 GPU 内存大的张量:

a = np.memmap('../dat/R/train.3,31,31B', '3,31,31B', 'r')
a.nbytes // 2**30
## 68

torch.from_numpy(a).pin_memory()
## ---------------------------------------------------------------------------
## RuntimeError                              Traceback (most recent call last)
## <ipython-input-36-d6f2d74da8e7> in <module>
## ----> 1 torch.from_numpy(a).pin_memory()
##
## RuntimeError: cuda runtime error (2) : out of memory at /tmp/pip-req-build-58y_cjjl/aten/src/THC/THCCachingHostAllocator.cpp:296
Run Code Online (Sandbox Code Playgroud)

如果我确实想固定一个小张量,为什么不提前将整个张量直接移动到 GPU 内存中?

Fir*_*ger 8

翻译:博士

您的代码速度较慢,因为每次调用生成器时都会分配一个新的固定内存块。每次分配新内存都需要同步,这使得它比非固定内存慢得多。您可能正在测量此开销。

您在编辑中的代码示例在THCCachingHostAllocator.cpp中失败。这并不是 GPU 内存不足,而是您的主机拒绝您分配 68GB 固定物理内存。


PyTorch 中固定内存实际上更慢?

创建或释放固定内存(cudaHostAlloc()/cudaFreeHost()通过CUDA 运行时malloc)比/慢得多free,因为它涉及设备(GPU 和主机)之间的同步。很可能,您正在测量的在很大程度上是这种开销,因为您正在增量分配固定内存。

固定内存不应该使数据传输异步从而更快吗?如果不是这样,我们为什么要做 pin 内存呢?

它可以,但如果您在每次传输之前停止/加入同步以分配内存,则不行。

固定内存最终的作用是防止内存块被操作系统换出;它保证保留在 RAM 中。这种保证使 GPU 的 DMA 能够在该块上进行操作,而无需通过 CPU(CPU 必须检查数据是否需要换回)。因此,CPU 可以同时自由地执行其他操作。

这不是一个完美的类比,但您可以将固定内存视为 GPU 和主机之间的共享内存。双方均可对其进行操作,无需通知对方;有点像进程中的多个线程。如果您实现非阻塞代码,这会快得多。join然而,如果聚会总是结束的话,速度也会慢得多。

将此与非固定方法进行对比,在非固定方法中,CPU 从 RAM 加载数据(必要时交换),然后将其发送到 GPU。它不仅速度较慢(需要经过北桥两次),而且还会使线程(以及一个 CPU 核心)保持忙碌。Python 还具有臭名昭著的 GIL,因此您的整个应用程序可能正在等待同步 I/O。

如果您想使用固定内存将批量数据混入 GPU,那么一种方法是将固定内存用作(循环)缓冲区。CPU 可以从磁盘加载数据,应用预处理,并将批处理放入缓冲区。然后,GPU 可以在自己的时间内从缓冲区中获取批次并进行推理。如果实现得好,那么GPU不会闲置超过必要的时间,并且主机和GPU之间不再需要同步。

如果我确实想固定一个小张量,为什么不直接将整个张量提前移动到 GPU 内存中呢?

如果您不需要从 CPU 访问张量并且它适合 GPU,那么确实不需要将其放入固定内存中。

在您的示例中,您正在打开一个内存映射的 numpy array memmap,然后要求将其传输到固定内存。内存映射文件的工作原理与分页内存非常相似,即不再适合 RAM 的数据将刷新到磁盘,并在再次访问时加载回磁盘。

对于固定内存来说,这种“交换”是不可能发生的,因为我们需要保证整个块始终驻留在 RAM 中。因此,我们需要首先将整个数组加载到主机内存中 - 一个 68 GB 的连续块 - 可能会在进程中创建数组的副本,以免破坏对象memmap,然后我们需要固定该内存块,告诉主机放弃 68GB 的​​托管物理内存给我们的应用程序。这两个步骤中的任何一个都可能被操作系统拒绝并引发错误OutOfMemory

THCCaching当您在Host中失败时,这几乎就是您所看到的Allocator.cpp


dpe*_*ini 3

来自 Pytorch 开发者的回答:

“固定内存是页锁定内存。如果用户为所有内容启用页锁定内存,那么用户很容易搬起石头砸自己的脚,因为它无法被抢占。这就是为什么我们没有将其设置为默认 True”来自 这里

这意味着根据您当前的内存情况(RAM 量、碎片等),它可能会延迟您的系统。