第一个问题就是缓存的淘汰算法非常低效,因为在 AI 场景中,访问数据的模式跟以往有很大区别。第二个问题是,缓存作为一种资源,与带宽(即远程存储的读取速度)是一个对立的关系。如果缓存大,那么从远端读取数据的机会就小。如果缓存很小,则很多数据都得从远端读取。如何很好地调度分配这些资源也是一个需要考虑的问题。
在讨论缓存的淘汰算法之前,先来看一下 AI 训练中数据访问的过程。在 AI 训练中,会分为很多个 epoch,不断迭代地去训练。每一个训练 epoch,都会读取每一条数据,并且仅读一次。为了防止训练的过拟合,在每一次 epoch 结束之后,下一个 epoch 的时候,读取顺序会变化,会进行一个 shuffle。也就是每次每个 epoch 都会把所有数据都读取一次,但是顺序却不一样。Alluxio 中默认的 LRU 淘汰算法,显然不能很好地应用到AI训练场景中。因为 LRU 是利用缓存的本地性。本地性分为两方面,首先是时间本地性,也就是现在访问的数据,马上可能还会即将访问。这一点,在 AI 训练中并不存在。因为现在访问的数据,在下一轮的时候才会访问,而且下一轮的时候都会访问。没有一个特殊的概率,一定是比其他数据更容易被访问。另一方面是数据本地性,还有空间本地性。也就是,为什么 Alluxio 用比较大的 block 缓存数据,是因为某条数据读取了,可能周围的数据也会被读取。比如大数据场景中,OLAP 的应用,经常会进行表的扫描,意味着周围的数据马上也会被访问。但是在 AI 训练场景中是不能应用的。因为每次都会 shuffle,每次读取的顺序都是不一样的。因此 LRU 这种淘汰算法并不适用于 AI 训练场景。
不仅是 LRU,像 LFU 等主流的淘汰算法,都存在这样一个问题。因为整个 AI 训练对数据的访问是非常均等的。所以,可以采用最简单的缓存算法,只要缓存一部分数据就可以,永远不用动。在一个作业来了以后,永远都只缓存一部分数据。永远都不要淘汰它。不需要任何的淘汰算法。这可能是目前最好的淘汰机制。如上图中的例子。上面是 LRU 算法,下面是均等方法。在开始只能缓存两条数据。我们把问题简单一些,它的容量只有两条,缓存 D 和 B 这两条数据,中间就是访问的序列。比如命中第一个访问的是 B,如果是 LRU,B 存在的缓存中命中了。下一条访问的是 C,C 并不在 D 和 B,LRU 的缓存中,所以基于 LRU 策略,会把 D 替换掉,C 保留下来。也就是这个时候缓存是 C 和 B。下一个访问的是 A,A 也不在 C 和 B 中。所以会把B 淘汰掉,换成 C 和 A。下一个就是 D,D 也不在缓存中,所以换成 D 和 A。以此类推,会发现所有后面的访问,都不会再命中缓存。原因是在进行 LRU 缓存的时候,把它替换出来,但其实在一个 epoch 中已经被访问一次,这个 epoch 中就永远不会再被访问到了。LRU 反倒把它进行缓存了,LRU 不但没有帮助,反倒是变得更糟糕了。不如使用 uniform,比如下面这种方式。下面这种 uniform 的方式,永远在缓存中缓存 D 和 B,永远不做任何的替换。在这样情况下,你会发现至少有 50% 的命中率。所以可以看到,缓存的算法不用搞得很复杂,只要使用 uniform 就可以了,不要使用 LRU、LFU 这类算法。
对于第二个问题,也就是关于缓存和远程带宽之间关系的问题。现在所有主流的 AI 框架中都内置了数据预读,防止 GPU 等待数据。所以当 GPU 做训练的时候,其实是触发了 CPU 预取下一轮可能用到的数据。这样可以充分利用 GPU 的算力。但当远程存储的 IO 成为瓶颈的时候,就意味着 GPU 要等待 CPU 了。所以 GPU 会有很多的空闲时间,造成了资源的浪费。希望可以有一个比较好的调度管理方式,缓解 IO 的问题。