如你安好

时间就像海绵里的水,只要愿挤,总还是有的。 -- 鲁迅
如你安好 ,

便是晴天 。

RuntimeError: CUDA out of memory. Tried to allocate 20.00 MiB (GPU 0; 4.00 GiB total capacity; 2.44

先说结论: 这个问题的出现就是显存不足导致的,物理上让显存扩大是最有效的解决方法。要是没有条件,就试试下面的方法,希望能够帮到你。

方法一:调整batch_size大小

1、完整报错

RuntimeError: CUDA out of memory. Tried to allocate 20.00 MiB (GPU 0; 4.00 GiB total capacity; 2.41 GiB already allocated; 5.70 MiB free; 2.56 GiB reserved in total by PyTorch) If reserved memory is >> allocated memory try setting max_split_size_mb to avoid fragmentation. See documentation for Memory Management and PYTORCH_CUDA_ALLOC_CONF

2、原因

batch_size过大,导致内存容量撑不住了。
通过打开任务管理器,我们很清楚的发现,原因就是内存容量不足,导致报错。
dec47319314a4c0c8077e6a849f06862.jpg

3、解决方法

减小batch_size
正常运行:
05dbd84839c94f10a016a042fa5243a2.jpg
任务管理器检测情况:
5f1497e4e6984e55b2db03d5f3aea56d.jpg

方法二:不计算梯度

使用with torch.no_grad():
在2080ti上运行分类模型时遇到了该问题,检查模型本身没有发现错误,最终确认是验证集评估阶段的张量计算非常占用空间。

1.可以对利用torch.tensor().detach().cpu().numpy()转为numpy,在cpu上进行loss和acc的计算
2.直接对评估阶段使用with torch.no_grad():

for step, (img, label) in enumerate(dataloader):
......
if (step + 1) % opt.print_interval_steps == 0:
with torch.no_grad():
'''验证集上评估模型'''
print("evaluate the performance on validate data")
total_loss_val = torch.zeros(opt.batch_size).to(device)
total_acc_val = torch.zeros(opt.batch_size).to(device)
for img_val, label_val in tqdm(val_dataloader):
img_val = img_val.to(device)
label_val = label_val.to(device)
y_pred_val = resnet18_model(img_val)
total_loss_val += loss_func(y_pred_val, label_val)
y_pred_class = torch.argmax(y_pred_val)
total_acc_val += (y_pred_class == label_val)
loss_val = torch.sum(total_loss_val) / len(val_db)
acc_val = torch.sum(total_acc_val) / len(val_db)

方法三:释放内存

在报错代码前加上以下代码,释放无关内存:

if hasattr(torch.cuda, 'empty_cache'):
torch.cuda.empty_cache()

方法四:max_split_size_mb设为较小值

注:本文使用的就是方法四解决了问题。
1.原因:
将注意力转回到报错消息本身:

RuntimeError: CUDA out of memory. Tried to allocate 6.18 GiB (GPU 0; 24.00 GiB total capacity; 11.39 GiB already allocated; 3.43 GiB free; 17.62 GiB reserved in total by PyTorch) If reserved memory is >> allocated memory try setting max_split_size_mb to avoid fragmentation. See documentation for Memory Management and PYTORCH_CUDA_ALLOC_CONF Time taken: 4.06sTorch active/reserved: 17917/18124 MiB, Sys VRAM: 24576/24576 MiB (100.0%)

注意到reserved - allocated = 17.62 - 11.39 = 6.23 > 6.18,而free = 3.43,符合消息中的reserved memory is >> allocated memory.显然显存是够的,但是因为碎片化无法分配。所以开始调查PYTORCH_CUDA_ALLOC_CONF这个环境变量设置。 首先检索到的是一文中的这一段:

关于阈值max_split_size_mb,直觉来说应该是大于某个阈值的 Block 比较大,适合拆分成稍小的几个 Block,但这里却设置为小于这一阈值的 Block 才进行拆分。个人理解是,PyTorch 认为,从统计上来说大部分内存申请都是小于某个阈值的,这些大小的 Block 按照常规处理,进行拆分与碎片管理;但对大于阈值的 Block 而言,PyTorch 认为这些大的 Block 申请时开销大(时间,失败风险),可以留待分配给下次较大的请求,于是不适合拆分。默认情况下阈值变量max_split_size_mb为 INT_MAX,即全部 Block 都可以拆分。

当时的理解是,既然默认值就是最大值,此前没有专门设置过这个变量,那么所有的显存请求都可以被拆分,应该不会出现OOM才对( 注意,这里的理解是错误的! 文末会有反思环节,这是后话 )。于是进一步调查,按照文章的思路,在每次Generate的逻辑前后执行empty_cache操作并回显显存使用情况(webui.py,L49):

def wrap_gradio_gpu_call(func, extra_outputs=None):
    def f(*args, **kwargs):
        os.system(nvidia-smi -i 0)
        print(torch.cuda.memory_allocated())
        print(torch.cuda.max_memory_allocated())
        print(torch.cuda.memory_reserved())
        print(torch.cuda.max_memory_reserved())
        print(torch.cuda.memory_stats())
        print(torch.cuda.memory_snapshot())
        torch.cuda.empty_cache()
        time.sleep(1)
        os.system(nvidia-smi -i 0)
        print(torch.cuda.memory_allocated())

        shared.state.begin()

        with queue_lock:
            res = func(*args, **kwargs)

        shared.state.end()

        return res

    return modules.ui.wrap_gradio_call(f, extra_outputs=extra_outputs, add_stats=True)

观察结果是一切正常,没有什么建设性的发现。empty_cache操作前后显存用量基本没有变化,维持在载入模型后的7~8G左右。 苦思冥想不得解,这时候基友提示把这个PYTORCH_CUDA_ALLOC_CONF环境变量也记录下来确认一下:

os.system("echo %PYTORCH_CUDA_ALLOC_CONF%")

得到的结果是:

%PYTORCH_CUDA_ALLOC_CONF%
……

也就是说Pytorch、gradio和sdweb三者的逻辑中都没有设置这个变量。抱着试试看的心态,在启动脚本前手动设置此变量值为int32类型的上限值,也就是官网和文献1中提到的默认值:

@echo off
set PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:2147483647
set PYTHON=
set GIT=
set VENV_DIR=
set COMMANDLINE_ARGS=--no-half --api --theme dark
call webui.bat

此时再使用同样的参数,逐步提高分辨率测试了多次,都可以正常运行。正当笔者满心欢喜以为问题得解的时候,OOM又出现了。之后再测试,OOM开始高概率随机出现,极少数时候可以正常运行。这时才开始怀疑可能对max_split_size_mb参数值和实际行为的关系理解有误,开始尝试将其值调小为32MB(),竟然就可以稳定正常运行了,OOM不再出现,问题解决了。

2.解决方法:

TLDR:对于显存碎片化引起的CUDA OOM,解决方法是将PYTORCH_CUDA_ALLOC_CONF的max_split_size_mb设为较小值。

set PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:32

3.逻辑探究

问题虽然解决了,但对于其中的逻辑还是完全一头雾水。由于是改小了才可以正常运行,遂怀疑实际行为与参数的字面意思相反,即大于该值的显存请求才会被拆分。然后就使用参数值逐级逼近报错消息中的显存请求大小6.18GB的方式来测试其实际行为,对一系列参数值进行测试:

max_split_size_mb 测试结果 4096 正常 5120 正常 6144 正常 7168 OOM 8192 OOM 2147483647 OOM

现在拿到了确凿的事实:当参数值小于显存请求的大小时就可以正常运行。
这和之前的理解明显是不符的,因为小的显存请求明显是不用分割就可以分配,大的请求才需要分割。
这时才回去文章详细阅读前文的分析,发现文中提及的Block是指空闲Block,而不是显存请求。max_split_size_mb分割的对象也是空闲Block(这里有个暗含的前提:pytorch显存管理机制中,显存请求必须是连续的)。
这里实际的逻辑是:由于默认策略是所有大小的空闲Block都可以被分割,所以导致OOM的显存请求发生时,所有大于该请求的空闲Block有可能都已经被分割掉了。
而将max_split_size_mb设置为小于该显存请求的值,会阻止大于该请求的空闲Block被分割。
如果显存总量确实充足,即可保证大于该请求的空闲Block总是存在,从而避免了分配失败的发生。
笔者的情况,显存总量是24G,那么最理想的条件下,大于6.18G的空闲Block最多也只能有3个,这就解释了为什么OOM是高概率随机出现。
而报错信息中的”3.43 GiB free”实际上是指pytorch所能找到的最大的空闲Block的大小,而非总的空闲空间大小,所以会出现reserved - allocated > free的现象。一文中将max_split_size_mb称为“一次分配的最大单位”也是错误的,这个值实际上决定了最大的空闲Block可能的最小值,这个最小值为显存总量 max_split_size_mb。所以这个变量应该命名为最小空闲Block保留大小语义才更为明确。

4.进一步优化

有了上述结论,就可以导出最优设置策略:将max_split_size_mb设置为小于OOM发生时的显存请求大小最小值的最大整数值,就可以在保证跑大图的可行性的同时最大限度照顾性能。 笔者的观测OOM请求最小值是6.18GB,所以最终选择了6144作为最优设置:

set PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:6144

5.总结:

这个故事告诉我们:

TLDR(太长不看)不可取,很容易断章取义。快速提取关键信息固然重要,但对于复杂系统还是需要更谨慎一些;
进行逻辑和机制分析的时候尽量使每个段落的内容自洽,尽量不依赖上下文和概念定义。这有助于读者快速检索时得到正确的信息;
亲手验证之前不能相信默认值。只有确认了实际行为才能消除表述和理解之间可能存在的差异,不能想当然。

愿望集合地

(仅限登陆用户评论)

这篇文章获得了个赞!