研究过深度学习的学生,一定熟悉以下CUDA显存溢出错误。
RuntimeError: CUDA out of memory. Tried to allocate 916.00 MiB (GPU 0; 6.00 GiB total capacity; 4.47 GiB already allocated; 186.44 MiB free; 4.47 GiB reserved in total by PyTorch)。
本文探讨了CUDA的内存管理机制,并对问题的解决方法进行总结。
2.1 CUDA固有显存。在实验开始前,先清空环境终端输入。
nvidia-smi。
接下来,向GPU存储一个小的张量。import。 torchdevice。=torch。.。device。(。'cuda'if。torch。.。cuda。.。is_available。(。)。else。'cpu')。torch。.。randn。(。(。2。,3。)。,device。=device。
)。占用显存的情况如下c;共计。
448M。
当我们增加张的尺寸时c;例如。torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。
)。此时GPU占用也随之上升c;共计。
1362M。这表明:GPU显存占用率与存储的数据尺寸成正相关,显存占用的数据越多。
,这其实是废话,但是这句话反过来:数据占用越小,显存越小吗?做个实验。torch。.。randn。(。(。1。,1。)。,device。=device。
)。仍然占用。
448M。事实上,这是因为CUDA运行时,其固件将占用一定的显存,在本机软硬件环境下。
448M。,不同的CUDA版本或显卡型号固件显存不同。换句话说,#xff0c;只要使用GPU,至少会占。 x x。x。显存,而且这部分显存无法释放。
2.2 激活和失活显存。
代码A。 x1。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x2。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x3。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x4。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x5。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x6。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。
代码B。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。
)。答案可以猜到,代码A报错了,显存为CUDA。激活机制。有关。CUDA目前的数据空间可以看作是队列,队列中有两种内存。激活内存(Activate Memory。)和。失活内存(Unactivate Memory)。
。当一个内存不再被变量引用时,,内存由激活内存转变为失活内存,但它仍然存在于这个数据队列中。
接下来,添加了一个新的数据,CUDA将释放部分失活内存,用于存储新数据。如果新数据占用的空间大于队列中的所有失活内存,从显存到队列,再申请一部分空间c;相当于队列的容量扩展;如果新数据占用空间约等于队列中的失活内存,然后CUDA显存的占用率几乎保持不变。
可以实验证运行。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x。=torch。.。randn。(。(。200。,300。)。,device。=device。
)。显存占用为。
1364M。
,单独运行。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。
)。的。
1362M。
相比相似但当新数据占用的空间大于队列中的所有失活内存时。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x。=torch。.。randn。(。(。300。,300。,300。,20。)。,device。=device。
)。显存占用率飙升至。
3422M。。当数据队列达到一定阈值时,,CUDA触发垃圾回收机制,清理失活的内存。
。
上述实验解释了深度学习中非常常见的代码。for。images。, labels。in。train_bar。:。images。, labels。=images。.。to。(。config。.。device。)。,labels。.。to。(。config。.。device。)。# 梯度清零。opt。.。zero_grad。(。)。# 正向传播。 outputs。=model。(。images。)。# 计算损失。 loss。=F。.。cross_entropy。(。outputs。,labels。)。# 反向传播。loss。.。backward。(。)。# 模型更新。opt。.。step。(。
)。
2.3 释放GPU显存。
操作下面的命令可以手动清理GPU数据队列中的失活内存。torch。.。cuda。.。empty_cache。(。
)。
需要注意的是,,上述命令可能需要多次才能释放空间,比如。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x。=torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。 x。=
1。此时。
x。指向了。
int。类型,因此,GPU数据队列中的空间没有被变量引用,说明所有的队列都是失活内存,但此时运行。
nvidia-smi。仍有。
2278M。占用,进一步运行。
torch.cuda.empty_cache()。然后可以恢复。
448M。
3 问题总结。关于CUDA GPU。显存管理。
- 总结:
- GPU显存占用率与存储的数据尺寸成正相关,显存占用的数据越多。只要使用GPU,至少会占。 x x。x。
- 显存,而且这部分显存无法释放。
- 当一个内存不再被变量引用时,,内存由激活内存转变为失活内存,但它仍然存在于这个数据队列中。
- 当数据队列达到一定阈值时,,CUDA触发垃圾回收机制,清理失活的内存。
运行。
torch.cuda.empty_cache()。
失活的内存可以手动清理。
然后根据上述理论可以得到相应的问题解决方案。调小。
batch_size。
本质上是为了防止GPU数据队列向显存申请的空间大于显存本身。
检查GPU中是否有数据在未发布的情况下继续存储。
例如: app。=[。]。for。 _。in。range。(。1000。)。:。app。.。append。(。torch。.。randn。(。(。200。,300。,200。,20。)。,device。=device。)。
)。这里。
append。函数相当于获得张量。
torch.randn((200, 300, 200, 20), device=device)。
存储在列表中的副本,因此,每次存储的张量都会被隐式引用,GPU继续增加激活内存而不释放,导致崩溃。在测试阶段和验证阶段之前插入代码。
with torch.no_grad()。
4 告别bug。本文收录了《告别bug》专栏,本专栏记录了人工智能领域的各种bug,以备复习,文章形式:问题背景 + 问题探索 + 问题解决。
,订阅栏目+关注博主后,可以通过下面的名片联系我,进入AI技术交流小组帮助解决问题。 🔥更多精彩的专栏。