动态图是边计算边搭建计算图,中间结果一目了然。
静态图是先搭建计算图,最后喂入输入,直接出结果。
因为静态图在编译的时候进行了一些优化,比如改变了代码的计算过程,以利于GPU更好的并行计算,那么想debug看中间结果就很困难了,有时候看到的和期望的可能会有些出入。另外就是每次当我们搭建完一个动态计算图,然后在反向传播结束之后,整个计算图就在内存中被释放了。如果想再次使用的话,必须从头再搭一遍。
对于大网络结构的训练场景,在静态图上的显存优化主要可以分为三个方向:
动态图无法提前获得全局的计算图信息。因为无法得到每个 tensor 的生命周期,所以静态显存分配不再可用

如上图所示,交换耗时比计算耗时高出很多,因此用带宽换显存不合理。

如上图所示,在前向传播中(第一行从左到右),蓝色圆圈表示模型的中间计算结果开始占用显存。一直到前向传播完成,第一行完全变为蓝色圆圈,前面计算所占用的显存都不能释放。
等到反向传播开始(第二行从右到左),随着梯度的计算与完成应用,前向传播保留在显存中的张量才可以释放。
很明显,如果要降低显存占用,就要拿前向传播保存的中间计算结果开刀,这也正是 MegEngine 动态图显存优化的主要方向。

如上为梯度检查点技术原理示意,前向传播中第三个点为检查点,它会一直保存在显存中。第四个点在完成计算后即可释放显存,在反向传播中如果需要第四个点的值,可以从第三个点重新计算出第四个点的值。
引入LRU cache的机制,选择代价最低的tensor进行释放,在需要用到的时候进行重计算。
LRU cache:距离上次访问时间间隔最长的,进行释放
另外,DTR 论文中还提出,除了重计算带来的开销之外,其他的额外开销主要用于寻找应该被释放掉的最优 tensor。因为在显存中,tensor 停留的时长是不断在变化的,所以只能在需要释放的时候现场计算最优的 tensor。
对此,论文中提出了两个运行时的优化技巧:
不考虑小的 tensor,当 tensor 大小小于候选集中的 tensor 的平均大小的 1% 时,不加入候选集;
每次在需要释放 tensor 的时候,随机采样 sqrt(N) 个 tensor 进行遍历(N 为目前可释放的 tensor 候选集的大小)

释放的显存不连续,形成的显存碎片,无法容纳新的tensor
例如,新的tensor需要100M的显存,为此释放了两个tensor,但是这两个tensor不是连续的,不能被使用,根据释放机制,就会一直释放下去,直到释放出一段连续可用的显存



in-place op 失效
重计算实质上把in-place op变成了非in-place op
in-place op:模型权重会被修改利用,以此来节省显存和cache,原地修改会造成后续的值改变。
DTR:模型权重不可被修改,额外申请资源进行计算,生成新的tensor,分散在显存池中,很难形成连续大显存。

对显存的排列方式进行优化,一次性找出可以生成足够大的连续空闲显存、并且总代价最低的tensor集合。


对于不再变化的tensor,进行共享存储。

根据op进行显存池中位置的分配,相同的放在一起。(原先是按照计算顺序来的)

将显存池中空闲显存当做代价为0的tensor。
利用滑动窗口找出代价最小的连续tensor,进行释放。

下图是Coop和同样在OneFlow中实现的DTR、DTE策略在八种不同的网络、不同显存阈值下的对比,可以看到Coop均超过了其它两种方法:
横轴为显存,纵轴为时间倍数

同时,Coop 将显存碎片率减少了一个量级(注意 BiLSTM 的 y 轴为对数坐标)
在大部分网络中,显存碎片率最低

Coop 的搜索过程时间复杂度为 O(N) 而不是 O(N^2)(N 为显存池中的 tensor 个数),在
绝大多数场景下也取得了最快的搜索速度(注意 BiLSTM 和 BERT Large 的 y 轴为对数坐
标)
