MindSpore运行模式与PyNative内存调优分析

发布于:2022-12-15 ⋅ 阅读:(376) ⋅ 点赞:(0)

MindSpore运行模式与内存

MindSpore下有2种运行模式,一种是GRAPH模式(静态图),一种是PyNative模式(动态图)。
GRAPH下的内存使用分为2种:

  1. 通过somas算法控制的内存,称为动态内存(Tensor使用的内存地址可以被其它Tensor使用),如算子的输出;

  2. 从内存池申请的内存,称为静态内存(Tensor的内存地址不会被其它Tensor使用),如Weights Parameter, 图的output等。

PyNative模式下使用的内存均从内存池申请,算子运行完成后,如果输出不被算子反向图使用,那就可以释放。也就是说,PyNative下内存全部是动态使用的。

PyNative的内存问题分析

如何提高PyNative下的内存使用率呢?可以从2个方面考虑。

  1. 内存池的设计。PyNative下的内存均是从内存池申请的,那么内存池的设计就直接关系到内存的利用率。MindSpore下内存池使用的是Best-Fit算法,优先按照大小去划分。具体是,先划分block(默认大小1GB),然后在block中划分buf块,Tensor使用的内存是buf块。那么,这里就存在2个问题。
    – block之间碎片。默认按照1GB的大小去分配,如果是30GB的内存,一般情况下,肯定会有30个内存碎片。碎片与碎片的内存无法合并,也就无法被再利用,造成内存利用率低。如图1中,灰色的块表示碎片。

    图1 block之间碎片
    – block内部碎片。block内部各个buf所属的Tensor,它们的生命周期可能是不相同的,那么就有可能有的buf被回收是空闲的,有的buf还在使用中,那么buf之间的内存就不能被有效合并,也会出现很多碎片,造成利用率低。如图2中,灰色的块表示碎片。

    图2 block内部碎片

  2. 正向内存使用优化。算子正向过程的输出,如果不被反向图使用到,那么算子正向运行完成的时,就可以释放;但是,如果正向输出被反向图使用到,那么就只能在反向算子运行完成后才能释放。如何确保在反向算子运行完成后及时释放?

PyNative内存优化手段

通过以上分析,PyNative下的内存优化,具体可以从3个方面入手,

  1. 优化block之间的碎片。如果block只有一个,是不是就没有block之间的碎片了?通过在context中添加mempool_block_size,可以使用block的大小,例如,Device设备给内存池使用为29GB,PyNative下通过设置context.set_context(mempool_block_size=“29GB”),那么内存池就只会有1个block了,也就不存在block之间的碎片了。如图3中,黄色块表示已经分配占用。

    图3 内存池只有1个block

  2. 优化block内部碎片。将内存池划分为2类block。一类称为common block, 另一类称为persistent block。将图的输入,输出,Weigths Parameter和ValueNode这些生命周期较长的Tensor划分到这个persistent block。common block被其它类型的内存申请使用,主要是各个算子的输出。如此一来,common block将大幅提高buf之间的合并成功率,图执行完一个step后,common block将完全有可能合并为一整块内存。如图4中,黄色块表示已经分配占用。


    图4 内存池区分为common block和persistent block

  3. 优化正向输出内存。通过添加对正向输出的内存的所属Tensor的引用计数,当引用计数为0时,直接释放该内存。反向图在执行过程中,先遍历整图获取每个正向输出Tensor的引用计数,当反向算子执行完成后,检查Tensor的引用计数是否为0,如果是,就直接释放该Tensor的内存,不需要等到整个反向图执行完成后再释放。释放的内存被内存回收,可能会与其它空闲buf合并成大块内存,方便后续算子使用,这样内存利用率就提高了。

PyNative内存调试案例

以上3个优化手段,第1个是需要用户设置的,后面2个已经在代码中添加优化,无需用户考虑。那么如何使用呢?
注意:当前PyNative下的persistent block的默认大小是1GB,不需要设置。通过mempool_block_size接口设置的大小是common block的大小,该大小与Device设备给内存池的可用大小有关,结果取2个值的最小值。GPUAscend上调整可用内存大小接口为max_device_memory=“XXGB”。如Ascend设备,总共大小为32GB,默认可以给内存池30GB,剩下的内存给HCCL组件或者算子运行时内存使用,如下示例:

  • context.set_context(mempool_block_size=“10GB”),默认设备可用内存大小为30GB,去掉persistent block的1GB,取min(mempool_block_size, max_device_memory - 1GB)值,那么实际生效的common block大小就是10GB。

  • context.set_context(mempool_block_size=“30GB”),默认设备可用内存大小为30G,去掉persistent block的1GB,取min(mempool_block_size, max_device_memory - 1GB)值,那么实际生效的common block大小就是29GB。

  • context.set_context(mempool_block_size=“30GB”, max_device_memory=“31GB”),通过max_device_memory接口,提供给内存池的设备内存可用大小为31GB,去掉persistent block的1GB,取min(mempool_block_size, max_device_memory - 1GB)值,那么实际生效的common block大小就是30GB。

  • context.set_context(mempool_block_size=“31GB”, max_device_memory=“31GB”),通过max_device_memory接口,提供给内存池的设备内存可用大小为31GB,去掉persistent block的1GB,取min(mempool_block_size, max_device_memory - 1GB)值,那么实际生效的common block大小就是30GB。

通过shufflenetv2网络具体说明

下载MindSpore models,进入official/cv/shufflenetv2目录。如下做了运行模式,batch size与epoch size修改:

运行:bash run_standalone_train_for_gpu.sh /home/workspace/mindspore_dataset/ImageNet_Original/train;
目前该网络没有默认没有添加mempool_block_size的设置,是跑不通的,如下:

可以看到,common block的大小是1GB,有19个;persistent block的大小是1GB,有1个。

解决步骤如下:

第一步,先直接设置context.set_context(mempool_block_size=“29GB”)或者context.set_context(mempool_block_size=“30GB”),通过上述分析,该2种设置方法生效的common block都是29GB,如下:

再次,运行测试结果如下:

PyNative能跑起来了,每一行“Run start cell id 140427040045680_”日志表示执行了一个step
第二步,如果上述设置后,内存还是出现不足,那么就需要调整设备可用内存的大小,当前最大调整为31G,即设置:

这样生效的common block大小是30GB,运行结果会出现:


DataSetQueue报错内存不足,而且是在AllocDeviceMem时。但是,调用接口AllocDeviceMem时,先判断了current free memory size大小,不应该报错。那么这里报错,只能说明是该次内存的申请不是向内存池申请的,而是算子本身运行过程种需要的内存。既然这样,那就需要将内存池的大小改小,以便多些内存给其它场景使用。这里申请的内存大小是1849700352B,大小约为1.7GB,而当前只空闲1073741824B,大小是1GB,所以至少要多放出0.7GB的内存。那么,内存池的大小设置为29GB即可。


网站公告

今日签到

点亮在社区的每一天
去签到