使用 GPU

GPU 监控

以下是如何通过终端以多种方式轮询 GPU 状态的方法

  • 查看使用 GPU 的进程以及 GPU 的当前状态

    watch -n 1 nvidia-smi
  • 监测使用情况统计信息的变化

    nvidia-smi --query-gpu=timestamp,pstate,temperature.gpu,utilization.gpu,utilization.memory,memory.total,memory.free,memory.used --format=csv -l 1

    这种方式很有用,因为您可以看到变化的轨迹,而不仅仅是执行不带任何参数的 nvidia-smi 时显示的当前状态。

    • 要查看可以查询的其他选项,请运行:nvidia-smi --help-query-gpu

    • -l 1 将每隔 1 秒更新一次(`–loop)。您可以增加这个数字以减少更新频率。

    • -f filename 会将日志写入文件,但您将无法看到输出。因此最好使用 nvidia-smi ... | tee filename,它会显示输出并同时记录结果。

    • 如果您想让程序运行 3600 秒后停止记录,请运行:timeout -t 3600 nvidia-smi ...

    欲了解更多详情,请参阅 有用的 nvidia-smi 查询

    您很可能只想跟踪内存使用情况,因此这可能就足够了

    nvidia-smi --query-gpu=timestamp,memory.used,memory.total --format=csv -l 1
  • 与上面类似,但以百分比显示统计信息

    nvidia-smi dmon -s u

    它显示了基本信息(使用率和内存)。如果您想要所有统计信息,请不带参数运行:nvidia-smi dmon 要了解其他选项,请使用:nvidia-smi dmon -h

  • nvtop

    Nvtop 代表 NVidia TOP,是一个类似于 (h)top 的 NVIDIA GPU 任务监视器。它可以处理多个 GPU,并以 htop 熟悉的方式打印有关它们的信息。

    它显示进程,并直观地显示内存和 GPU 统计信息。

    此应用程序需要从源代码构建(需要 gccmake 等),但说明易于遵循且构建速度很快。

  • gpustat

    类似于 nvidia-smi 的监视器,但更紧凑。它依赖于 pynvml 与 nvml 层通信。

    安装:pip3 install gpustat

    这是一个使用示例

    gpustat -cp -i --no-color

以编程方式访问 NVIDIA GPU 信息

在终端中监视 nvidia-smi 的运行很方便,但有时您想做的更多。这时 API 访问就派上用场了。以下工具提供了这种能力。

pynvml

nvidia-ml-py3 为 nvml c-lib(NVIDIA 管理库)提供了 Python 3 绑定,允许您直接查询该库,而无需通过 nvidia-smi。因此,这个模块比围绕 nvidia-smi 的封装器快得多。

绑定使用 Ctypes 实现,因此此模块是 noarch - 它只是纯 Python。

安装

  • Pypi
pip3 install nvidia-ml-py3
  • Conda
conda install nvidia-ml-py3 -c fastai

这个库现在是 fastai 的一个依赖项,因此您可以直接使用它。

示例

打印第一个 GPU 卡的内存统计信息

from pynvml import *
nvmlInit()
handle = nvmlDeviceGetHandleByIndex(0)
info = nvmlDeviceGetMemoryInfo(handle)
print("Total memory:", info.total)
print("Free memory:", info.free)
print("Used memory:", info.used)

列出可用的 GPU 设备

from pynvml import *
nvmlInit()
try:
    deviceCount = nvmlDeviceGetCount()
    for i in range(deviceCount):
        handle = nvmlDeviceGetHandleByIndex(i)
        print("Device", i, ":", nvmlDeviceGetName(handle))
except NVMLError as error:
    print(error)

这是一个通过示例模块 nvidia_smi 的使用示例

import nvidia_smi

nvidia_smi.nvmlInit()
handle = nvidia_smi.nvmlDeviceGetHandleByIndex(0)
# card id 0 hardcoded here, there is also a call to get all available card ids, so we could iterate

res = nvidia_smi.nvmlDeviceGetUtilizationRates(handle)
print(f'gpu: {res.gpu}%, gpu-mem: {res.memory}%')

py3nvml

这是 nvidia-ml-py3 的另一个分支,补充了一些 额外有用的工具

注意:主通道中没有 py3nvml 的 conda 包,但它可以在 pypi 上获得。

GPUtil

GPUtil 是围绕 nvidia-smi 的一个包装器,需要先让 nvidia-smi 工作才能使用它。

安装:pip3 install gputil

这是一个使用示例

import GPUtil as GPU
GPUs = GPU.getGPUs()
gpu = GPUs[0]
    print("GPU RAM Free: {0:.0f}MB | Used: {1:.0f}MB | Util {2:3.0f}% | Total {3:.0f}MB".format(gpu.memoryFree, gpu.memoryUsed, gpu.memoryUtil*100, gpu.memoryTotal))

欲了解更多详情,请参阅:https://github.com/anderskm/gputil

欲了解更多详情,请参阅:https://github.com/nicolargo/nvidia-ml-py3

https://github.com/FrancescAlted/ipython_memwatcher

GPU 内存注意事项

每个进程不可用的 GPU RAM

一旦开始使用 CUDA,您的 GPU 会因每个进程损失约 300-500MB RAM。具体大小似乎取决于显卡和 CUDA 版本。例如,在 GeForce GTX 1070 Ti (8GB) 上,运行在 CUDA 10.0 的以下代码会消耗 0.5GB GPU RAM

import torch
torch.ones((1, 1)).cuda()

这部分 GPU 内存您的程序无法访问,并且不能在进程间重用。如果您运行两个进程,每个进程都在 cuda 上执行代码,那么每个进程一开始就会消耗 0.5GB GPU RAM。

这块固定大小的内存被 CUDA context 使用。

缓存内存

pytorch 通常会缓存它以前使用过的 GPU RAM,以便稍后重用。因此,nvidia-smi 的输出可能不准确,您可能拥有比它报告的更多可用的 GPU RAM。您可以使用以下方法回收此缓存

import torch
torch.cuda.empty_cache()

如果有多个进程使用同一个 GPU,一个进程的缓存内存对另一个进程是不可访问的。第一个进程执行上述代码将解决此问题,使释放的 GPU RAM 可供其他进程使用。

此外,可能需要注意 torch.cuda.memory_cached() 并不显示 pytorch 缓存在中有多少可用内存,它仅指示当前已分配的内存量,其中一部分正在使用,一部分可能是空闲的。要衡量缓存中可用的空闲内存量,请执行:torch.cuda.memory_cached()-torch.cuda.memory_allocated()

重用 GPU RAM

如何在一个给定的 jupyter notebook 中进行大量实验而无需一直重启内核?您可以删除持有内存的变量,可以调用 import gc; gc.collect() 来回收带有循环引用的被删除对象占用的内存,并且可选地(如果您只有一个进程)调用 torch.cuda.empty_cache(),这样您就可以在同一个内核中重用 GPU 内存了。

为了自动化这个过程并获取各种内存消耗统计信息,您可以使用 IPyExperiments。除了帮助您回收普通 RAM 和 GPU RAM,它还有助于有效调整 notebook 参数,避免 CUDA: out of memory 错误并检测各种其他内存泄漏。

另外,请务必阅读有关 learn.purge 及相关功能的教程 此处,它们提供了更好的解决方案。

GPU RAM 碎片化

如果您遇到类似以下的错误

RuntimeError: CUDA out of memory.
Tried to allocate 350.00 MiB
(GPU 0; 7.93 GiB total capacity; 5.73 GiB already allocated;
324.56 MiB free; 1.34 GiB cached)

您可能会问自己,如果还有 0.32 GB 空闲和 1.34 GB 缓存(即总共有 1.66 GB 未使用的内存),怎么会无法分配 350 MB?这是由于内存碎片化造成的。

为了这个例子,我们假设您有一个函数,它可以分配与其参数指定的 GB 数相等的 GPU RAM

def allocate_gb(n_gbs): ...

并且您有一张 8GB 的 GPU 卡,没有任何进程正在使用它,所以当一个进程启动时,它是第一个使用它的进程。

如果您按以下顺序分配 GPU RAM

                    # total used | free | 8gb of RAM
                    #        0GB | 8GB  | [________]
x1 = allocate_gb(2) #        2GB | 6GB  | [XX______]
x2 = allocate_gb(4) #        6GB | 2GB  | [XXXXXX__]
del x1              #        4GB | 4GB  | [__XXXX__]
x3 = allocate_gb(3) # failure to allocate 3GB w/ RuntimeError: CUDA out of memory

尽管总共有 4GB 的空闲 GPU RAM(缓存和可用),最后一个命令仍将失败,因为它无法获得 3GB 的连续内存。

除了这个例子不太完全有效之外,因为在底层,CUDA 会重新定位物理页面,并使它们对 pytorch 看起来像是连续的内存类型。因此,在上面的例子中,只要没有其他东西占用这些内存页,它就会重用这些碎片中的大部分或全部。

因此,为了使此示例适用于 CUDA 内存碎片化的情况,它需要分配内存页面的片段,目前大多数 CUDA 显卡的内存页面大小为 2MB。因此,如果在此示例的相同场景中分配的内存小于 2MB,就会发生碎片化。

鉴于 GPU RAM 是一种稀缺资源,最好在使用完 CUDA 上的任何东西后立即释放,然后再将新对象移动到 CUDA。通常,简单的 del obj 就可以解决问题。但是,如果您的对象包含循环引用,即使调用了 del(),在 python 调用 gc.collect() 之前,它也不会被释放。而在后者发生之前,它仍将占用已分配的 GPU RAM!这也意味着在某些情况下,您可能需要自己调用 gc.collect()

如果您想了解 python 垃圾收集器如何以及何时自动调用,请参阅 gc此处

峰值内存使用量

如果您在像 Learnerfit() 这样的函数上运行 GPU 内存分析器,您会注意到在第一个 epoch 时会导致 GPU RAM 使用量急剧增加,然后稳定在低得多的内存使用模式。这是因为 pytorch 内存分配器尝试以最有效的方式为加载的模型构建计算图和梯度。幸运的是,您无需担心这种峰值,因为分配器足够智能,能够识别内存紧张的情况,并能够以少得多的内存做同样的事情,尽管效率较低。通常,以 fit() 示例为例,分配器至少需要与第二个及后续 epoch 正常运行所需的内存量相同。您可以在 此处 阅读关于此主题的一篇非常棒的帖子。

pytorch Tensor 内存追踪

显示所有当前已分配的 Tensor

import torch
import gc
for obj in gc.get_objects():
    try:
        if torch.is_tensor(obj) or (hasattr(obj, 'data') and torch.is_tensor(obj.data)):
            print(type(obj), obj.size())
    except: pass

注意,gc 不会包含 autograd 内部消耗内存的一些 Tensor。

此处有一篇关于此主题的精彩讨论,其中包含更多相关的代码片段。

GPU 重置

如果由于某种原因,python 进程退出后 GPU 没有释放内存,您可以尝试重置它(将 0 更改为所需的 GPU ID)

sudo nvidia-smi --gpu-reset -i 0

使用多进程时,有时一些客户端进程会卡住并变成僵尸进程,并且不会释放 GPU 内存。它们也可能对 nvidia-smi 不可见,因此 nvidia-smi 报告没有内存使用,但显卡无法使用,即使尝试在该卡上创建一个微小的 tensor 也会因 OOM 失败。在这种情况下,使用 fuser -v /dev/nvidia* 定位相关进程,并使用 kill -9 杀死它们。

这篇博客 文章 建议以下技巧来按需安排进程干净退出

if os.path.isfile('kill.me'):
    num_gpus = torch.cuda.device_count()
    for gpu_id in range(num_gpus):
        torch.cuda.set_device(gpu_id)
        torch.cuda.empty_cache()
    exit(0)

将此代码添加到训练迭代后,一旦您想停止它,只需进入训练程序的目录并运行

touch kill.me

多 GPU

GPU 顺序

当拥有多个 GPU 时,您可能会发现 pytorchnvidia-smi 对它们的排序方式不同,因此 nvidia-smi 报告为 gpu0 的,可能被 pytorch 分配给 gpu1pytorch 使用 CUDA GPU 排序,这是根据 计算能力 进行的(计算能力较高的 GPU 排在前面)。

如果您想让 pytorch 使用 PCI 总线设备顺序,以匹配 nvidia-smi,请设置

export CUDA_DEVICE_ORDER=PCI_BUS_ID

在启动您的程序之前(或者放在您的 ~/.bashrc 中)。

如果您只想在特定的 GPU ID 上运行,可以使用 CUDA_VISIBLE_DEVICES 环境变量。它可以设置为单个 GPU ID 或列表

export CUDA_VISIBLE_DEVICES=1
export CUDA_VISIBLE_DEVICES=2,3

如果您不在 shell 中设置环境变量,可以在程序开头通过以下方式在代码中设置:import os; os.environ['CUDA_VISIBLE_DEVICES']='2'

一种灵活性较低的方法是在代码中硬编码设备 ID,例如将其设置为 gpu1

torch.cuda.set_device(1)