使用 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 代表 NVidia TOP,是一个类似于 (h)top 的 NVIDIA GPU 任务监视器。它可以处理多个 GPU,并以 htop 熟悉的方式打印有关它们的信息。
它显示进程,并直观地显示内存和 GPU 统计信息。
此应用程序需要从源代码构建(需要
gcc
、make
等),但说明易于遵循且构建速度很快。 -
类似于
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()
。
峰值内存使用量
如果您在像 Learner
的 fit()
这样的函数上运行 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 时,您可能会发现 pytorch
和 nvidia-smi
对它们的排序方式不同,因此 nvidia-smi
报告为 gpu0
的,可能被 pytorch
分配给 gpu1
。pytorch
使用 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)