赞
踩
在人工智能大模型训练的过程中,常常会面临显存资源不足的情况,其中包括但不限于以下两个方面:1.经典错误:CUDA out of memory. Tried to allocate ...;2.明明报错信息表明显存资源充足,仍然发生 OOM 问题。为了深入理解问题的根源并寻求解决方案,必须对系统内存架构以及显存管理机制进行进一步的探究。本文将为读者带来对这些基础知识的全面学习与了解。
系统内存与两块GPU设备的交互示意图
系统存储:
GPU设备存储:
传输通道: 存储与存储之间通过传输协议/通道进行数据交换。
模型训练涉及到多个关键概念,下面是显存和内存之间的关系以及在训练过程中的作用:
在AI模型训练过程中,显存和内存之间的交互是关键的:
显存和内存在AI模型训练中扮演着关键角色,它们之间的高效协同工作有助于加速训练过程并降低资源消耗。同时,合理的显存管理和数据处理策略可以提高训练效率和性能。
在模型训练阶段和推理阶段,优化内存使用都是非常重要的,因为内存是有限资源,合理管理内存可以提高性能和效率。以下是在这两个阶段分别优化内存的一些方法:
模型训练阶段优化内存:
推理阶段优化内存:
总之,在模型训练和推理阶段都有许多策略可以优化内存使用。选择适合您应用场景的策略,可以提高性能、减少资源消耗,并确保您的计算资源得到了最大程度的利用。
最重要的两个指标:
显存占用和GPU利用率是两个不一样的东西,显卡是由GPU计算单元和显存等组成的,显存和GPU的关系有点类似于内存和CPU的关系。显存可以看成是空间,类似于内存。
GPU计算单元类似于CPU中的核,用来进行数值计算。衡量GPU计算量的单位通常是FLOPS,即每秒浮点运算次数(Floating Point Operations Per Second):每秒能执行的flop数量。FLOPS值越大,计算能力越强大、速度越快。
神经网络模型占用显存的包括:
只有有参数的层,才会占用显存。这部份的显存占用和输入无关,模型加载完成之后就会占用。
有参数的层:
无参数的层:
模型的参数数目:
参数占用显存 = 参数数目 × n
Float32 是在深度学习中最常用的数值类型,称为单精度浮点数,每一个单精度浮点数占用 4 Byte显存。
在PyTorch中,执行 model = MyModel().cuda() 之后就会输出占用显存大小,占用的显存大小基本与上述分析的显存差不多(会稍大一些,因为其它开销)。
模型中与输入无关的显存占用包括:
输入输出的显存主要看输出的feature map 的形状:
模型输出的显存占用:
显存占用 = 模型显存占用 + batch\_size × 每个样本的显存占用
可以看出显存不是和batch-size简单的成正比,尤其是模型自身比较复杂的情况下:比如全连接很大,Embedding层很大。
另外需要注意:
在深度学习中,一般占用显存最多的是卷积等层的输出,模型参数占用的显存相对较少,而且不太好优化。
节省显存方法:
尤其是batch-size,假定GPU处理单元已经充分利用的情况下:
在模型整个训练过程中中,占用显存大概分以下几类:
除了算法层的优化,最基本的优化方式如下:
了解显存管理机制主要是为了减少显存碎片化带来的影响。也就解决了我们经常遇到的一些问题:为什么报错信息里提示显存够,但还是遇到了 OOM?
malloc 分配失败的情况下的错误信息:
CUDA out of memory. Tried to allocate 1.24 GiB (GPU 0; 15.78 GiB total capacity; 10.34 GiB already allocated; 456.50 MiB free; 14.21 GiB reserved in total by PyTorch)
注意: reserved + free 并不等同于 total capacity,因为 reserved 只记录了通过 PyTorch 分配的显存,如果用户手动调用 cudaMalloc 或通过其他手段分配到了显存,是没法在这个报错信息中追踪到的(又因为一般 PyTorch 分配的显存占大部分,分配失败的报错信息一般也是由 PyTorch 反馈的)。
在这个例子里,device 只剩 456.5MB,不够 1.24GB,而 PyTorch 自己保留了 14.21GB(储存在 Block 里),其中分配了 10.3GB,剩 3.9GB。那为什么不能从这 3.9GB 剩余当中分配 1.2GB 呢?原因肯定是显存碎片化了。
实现自动碎片整理的关键特性:split
通过环境变量会指定一个阈值 max\_split\_size\_mb,实际上从变量名可以看出,指定的是最大的可以被 "split" 的 Block 的大小。
被 split 的操作很简单,当前的 Block 会被拆分成两个 Block,第一个大小正好为请求分配的 size,第二个则大小为 remaining,被挂到当前 Block 的 next 指针上(这一过程见源码 L570~L584)。这两个 Block 的地址自然而然成为连续的了。随着程序运行,较大的 Block(只要仍小于阈值 max\_split\_size\_mb)会不断被分成小的 Block。值得注意的是,由于新 Block 的产生途径只有一条,即通过步骤三中的 alloc\_block 函数经由 cudaMalloc 申请,无法保证新 Block 与其他 Block 地址连续,因此所有被维护在双向链表内的有连续地址空间的 Block 都是由一个最初申请来的 Block 拆分而来的。
一段连续空间内部(由双向链表组织的 Blocks)如图所示:
当 Block 被释放时,会检查其 prev、next 指针是否为空,及若非空是否正在被使用。若没有在被使用,则会使用 try\_merge\_blocks (L1000) 合并相邻的 Block。由于每次释放 Block 都会检查,因此不会出现两个相邻的空闲块,于是只须检查相邻的块是否空闲即可。这一检查过程见 free\_block 函数(L952)。又因为只有在释放某个 Block 时才有可能使多个空闲块地址连续,所以只需要在释放 Block 时整理碎片即可。
关于阈值 max\_split\_size\_mb ,直觉来说应该是大于某个阈值的 Block 比较大,适合拆分成稍小的几个 Block,但这里却设置为小于这一阈值的 Block 才进行拆分。个人理解是,PyTorch 认为,从统计上来说大部分内存申请都是小于某个阈值的,这些大小的 Block 按照常规处理,进行拆分与碎片管理;但对大于阈值的 Block 而言,PyTorch 认为这些大的 Block 申请时开销大(时间,失败风险),可以留待分配给下次较大的请求,于是不适合拆分。默认情况下阈值变量 max\_split\_size\_mb 为 INT\_MAX,即全部 Block 都可以拆分。可能有的人有开始有了疑问:如果全部 Block 都拆分了,碎片整理后还是OOM怎么办?
在训练规模庞大的模型训练时,GPU显得至关重要,然而,GPU资源的可用性常常面临严重不足的局面。这种情况可能由于模型尺寸过大,导致显存空间不足,进而影响训练进程的顺利进行。为了克服这一难题,我们迫切需要深入探究其根本原因,并对其背后的工作原理有深入的理解。只有这样,才能针对具体情况施以恰当的策略,实现对GPU资源的有效利用,确保训练任务能够高效进行。
转载自智源社区老刘说NLP
更多技术文档请访问365文档
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。