深度学习或者 AI 的呈现,扭转了咱们以往的解决问题的编程形式,不再是代码上直观的表白。
举一个简略的例子,咱们如何辨认一个数字(图片)是数字 9 呢?十分直观的办法就是下面有一小圆圈,上面有一个竖线。然而如果写的歪斜了一些呢?如果下面的圈没有闭合呢?如果竖线蜿蜒了呢?感觉咱们日常的程序判断(switch)无奈收敛,咱们只能用一种可能自我演进的形式来意识这个“看起来像 9”的数字,而这也正是咱们大脑的学习行为,咱们第一个看到这个数字的时候,被告知这是 9,那么图片就有了一个标签;下次再看到相似的,还是属于标签 9,见多识广,最初见到一个兴许写得更加不像的咱们也可能辨认出是 9,这个过程正是由咱们大脑中的上千亿的神经细胞长时间的学习后果。
人类大脑的真正运行形式,仍旧是神秘所在,但从这个过程中咱们倒退出了神经网络算法,能够从已有的常识中进行学习。勤能补拙,既然算法不如人脑,就通过学习大量的材料来放慢学习的过程。MNIST 数据有 60,000 张手写数字图片,ImageNet 数据有靠近 1500 万张图片,Youtube-8M 的视频数据无数 TB,Google 的 Open Image dataset, 仅仅是在 Open Images Challenge 中应用的数据集就达到了 18TB。
AI 中有三大外围:算法,算力,数据(存储)。 算法自有成熟的框架,由数学科学家去解决;计算能力由 CPU 甚至 GPU 去解决。面对如此大量的数据,一台机器的内存、硬盘去承载根本不太可能,而对于 CPU/GPU 计算能力强悍的组件,频繁的去远端获取数据期待 IO 又是资源的节约。有没有既能满足数据间隔计算近、又能承载大量数据的计划呢?缓存是银弹!前面的次要篇幅从论文解析的角度来逐渐论述,论文来自Fast20 Quiver: An Informed Storage Cache for Deep Learning。
后续的探讨中,有个比拟重要的概念,就是 mini-batch, 如果没有实战的经验过,不是很容易了解这个概念。
深度学习的优化算法,实质就是梯度降落。每次的参数更新有两种形式。
第一种,遍历全副数据集计算一次损失函数,而后计算函数对各个参数的梯度,更新梯度。这种办法每更新一次参数都要把数据集里的所有的样本数据遍历一遍,计算量开销大,计算速度慢,不反对在线学习,这称为 Batch gradient descent(BGD),批梯度降落。
另一种,每训练一个数据就算一下损失函数,而后求梯度更新参数,这个称为随机梯度降落,stochastic gradient descent。这个办法速度比拟快,然而收敛性能不太好,可能在最长处左近稳定,达不到最长处。两次参数的更新也有可能相互对消掉,造成指标函数震荡的比拟激烈。
为了克服两种办法的毛病,当初个别采纳的是一种折中伎俩,mini-batch gradient decent,小批的梯度降落,这种办法把数据分为若干个批,按批来更新参数,这样,一个批中的一组数据独特决定了本次梯度的方向,降落起来就不容易跑偏,缩小了随机性。
用一个示意图示意如下:
蓝色:为 batch 梯度降落,即 mini batch size = m,紫色:为 stochastic 梯度降落,即 mini batch size = 1,绿色:为 mini batch 梯度降落,即 1 < mini batch size < m。
以下图为例,执行了 3 轮训练(epoch), 每轮外面定义 mini-bach size=5, 其中数据集为 1 -20 个数字,咱们看到通过 torch.DataLoader, 每次取得了 5 个数据(batch x)。
01 深度学习训练的基本知识
深度学习训练任务 (Deep Learning Training DLT) 会将训练数据作为输出,从千头万绪的线索中通过学习并失去一个输入模型来代表训练数据。
为了实现训练,DLT 会应用一个较小的随机样本(mini-batch, 通常是 32 到 512 个),并利用 SGD 来缓缓的学习各种参数进而进步准确率。
训练数据:通常咱们能够认为是一个列表,列表中的每一个元素都是一个二元组 <input,label>, input 可能是一张图片或者一段语音,而 label 则代表着 input 的语义,而这也正是深度学习网络所须要学习的并可能正确区分 input 的指标。例如 ImageNet 的全副数据集大略有 150 万张图片,每张图片在 200KB 左右。
为了可能以随机形式拜访训练数据,DLT 框架会应用索引序列来遍历数据。假如训练数据有 100 万个文件,那么会保护一个蕴含每一个文件索引的列表,并对它进行随机的排列,随后依据 mini-batch 的数据量向后端存储取得数据,当全副的数据都残缺遍历训练一次,一个 epoch 实现。对于下一个 epoch, 再次对索引进行随机排列,反复下面的过程。一个典型的 DLT 工作会运行很多轮训练,例如 50-200。
数据转换:从存储取得原始数据会被 DLT 框架进行转换,例如彩色图片变成黑白图片,同时将图片转换为像素数矩阵等等。当然这部分工作通常由 CPU 来实现。
多任务:因为 DLT 工作是一个试错的过程,所以理论运行过程中,用户总是会应用不同的参数来同时运行不同的工作,所有的这些工作都会拜访雷同的残缺数据集,不同的就是以不同的随机程序来进行拜访。
02 深度学习的 IO 特点
咱们从 DLT 工作 I / O 拜访的角度看来列举一下它的次要特点:
可共享性:在 DLT 训练任务中,无论是一个训练任务本身,还是多个训练任务之间,都存在很大水平的 I / O 重叠性。在一个工作内,它会针对同一个数据集进行屡次的遍历(例如多个 epoch),所以如果可能在第一个 epoch 的时候就对数据进行缓存,会大幅晋升后续训练的效率。更重要的是,这种共享性甚至能够扩大到多任务之间,例如针对同一份训练数据集,配置不同的参数,利用同一个训练模型运行多个不同的工作。这些工作可能运行在不同的机器上,然而拜访的都是雷同的底层数据集。
随机拜访:因为数据的可共享性,这使得 DLT 具备十分的缓存敌对性,但只有在全副数据可能被残缺缓存的状况下才有成果,否则,DLT 随机拜访数据的形式又使得局部数据缓存很容易被穿透。例如只可能缓存 20% 的数据,那么这些数据马上就会被后续的随机拜访刷掉。
局部数据缓存对于 DLT 来说很重要,因为训练数据通常曾经足够大,并且会越来越多,例如前文提到过即便只是 ImageNet 这样的百万级规模的数据集,总体也曾经达到了数 TB 的大小。
可替换性 :从 I / O 的角度来说,一个训练任务(epoch) 次要关注以下两点即可:a)每一个训练数据必须且仅被拜访一次;b)而对于每次的 mini-batch, 必须是随机的序列。乏味的是,一组数据的准确程序并不会对训练任务的精确或者精确性产生影响,这就意味着 I / O 是能够被替换的。对于特定若干文件的拜访,DTL 工作能够替换为一组其余的随机的且没有被拜访过的数据。从缓存的角度来说,这是一个独特的个性来晋升缓存的命中率,即便缓存只能承载 20% 的数据,也能够在拜访一个不存在于缓存中的数据,通过替换的形式返回一个存在的内容,同时并没有毁坏随机以及唯一性的训练要求。
可预测性:因为每一个 mini-batch 的运行工夫,能够当时取得,这样就能够用于评估一个训练任务对 I / O 性能的敏感性,进而能够进行策略调整以可能使那么 I / O 敏感的工作从缓存获益。
03 缓存的设计
总结起来深度学习的特点:
- 须要的数据量大
- 多台机器多个训练并行
- 每个训练要运行屡次
- 在每次训练中,所有的数据须要遍历一遍
- 针对不同的训练参数,以及在不同的机器上运行的训练任务,数据集绝对放弃固定
针对以上的特点,当咱们思考缓存的时候,不禁会有如下的疑难:缓存毕竟容量无限,穿透如何解决?缓存的过期置换策略是如何的?当不同的用户拜访不同的数据,安全性如何保障?等等。
Quiver、分布式缓存,通过与 DLT 框架深度整合,缓存客户端集成到训练任务的 IO 过程中,进而为缓存服务端提供更多的策略信息。
系统结构
以私有云虚拟机环境举例,每一个 GPU VM 带有一个本地 SSD 硬盘,每一个 DLT job 会运行在本人的容器内,这样即便是多用户运行,也是在一个隔离的环境内。
每个用户的数据存储在各自账号的云存储内,这样保障了隐衷以及拜访权限。通过分布式缓存,即便训练任务因为调度等起因在各个宿主之间切换,缓存数据仍旧是可能进步训练效率。
数据安全
Quiver 的设计是一个共享式的散布缓存,无论是不同的工作,还是不同的用户之间,在共享的模式下如何保证数据的平安就是一个重要因素。Quiver 保障了用户只能看到他有权限拜访的数据,但这样仿佛又与缓存的重用产生了抵触。如果针对某一个数据集,例如 ImageNet, 两个不同用户别离在各自的存储账号内各自保留了一份,那么逻辑上来讲,缓存要别离为每个用户各自缓存一份。这将导致缓存的效率升高,Quiver 通过内容寻址 (content-addressed) 的形式来解决重用与隔离的问题。
内容寻址缓存
对于缓存,根本的行为就是通过一个 <key, value> 的映射关系,在咱们通过 key 查问时,可能疾速的返回所对应的 value。在 Quiver 中,缓存并不是利用文件名以及偏移量在作为缓存的关键字,而是利用缓存内容的 hashes。缓存内容的粒度是由具体的 DLT 工作决定的,可能是一张图片,无论是对它的插入还是寻址,都以它的 hash(例如 SHA1)来惟一定位。用 hash 来定位的益处是对于一个雷同内容的文件,不论它来自于何处以及文件名是否雷同,在缓存中都仅须要保留一份即可,这样也就可能达到即便在不同的用户之间也可能共享目标。
同时为了保证数据的隔离性,Quiver 利用摘要索引来拜访训练数据,对于每一份数据,摘要索引将蕴含 <content_hash: file_location>,因而,在多用户领有雷同内容的数据集时,因为数据是存在在各自的存储系统内,每个用户将领有不同的 file_location,然而所有的 content_hash 是雷同的。
缓存服务器
利用本地 SSD 作为介质的 KV 存储,通过一致性 Hash 的形式将 key space 散布在多个缓存服务器上。
缓存治理(Cache Manager)
因为 Quiver 是分布式缓存,那么针对所有的缓存服务器,缓存的插入、清理须要一个协调者 Cache manager。
Cache manager 同时会评估每一个计算工作从缓存的受害状况,次要通过让缓存服务器针对训练任务所须要的若干 mini-batch 数据做 cache misses, 而后与其余的缓存命中的训练人耗时机型比照,进而对缓存进行优先级调整。
缓存客户端
缓存客户端作为训练任务的一部分,通过干涉 DLT 框架,例如 PyTorch 等的接口层来拜访训练数据。在 PyTorch 中,DataSet 会用来遍历所有的训练数据,并且外部保护一个随机的文件索引列表,其中 Next 的接口就能够用来取得下一个 mini-batch 数据。Quiver 通过调整这个接口,利用一个摘要文件,当下层拜访一组文件时,它会先对缓存进行数据的拜访。
客户端会将训练任务的一些信息反馈给 Cache Manager, 例如每一个 mini-batch 的训练工夫,Cache Manager 能够据此来优化缓存的策略。
替换命中率
在惯例的缓存中,如果一个 mini-batch 蕴含了 512 个文件,那么 Dataset 会提供 512 个文件索引用来从后端存储取得文件内容,如果这其中只有局部缓存命中,那么将仍然存在近程的 I / O 操作。在 Quiver 中,会从 Cache 中加载更多的(例如 10 倍的 mini-batch 数量)数据,而只有其中有 512 个数据可能被命中,那么就返回给下层训练任务,这样训练任务就不会被 Cache miss 阻塞。同时 Quiver 会标记 Cache miss 的数据为 pending 状态,周而复始,直到数据被遍历了一遍,这时将重头来过仅仅去关注之前 pending 的数据。
假如目前只有 10% 的数据在缓存中,为了简略起见,咱们能够认为就是间断的原始数据的 10%,因为 DLT 工作会随机的查找数据,所以每一个长度为 k 的 mini-batch 序列,缓存的命中率应该为 k /10, 因而如果咱们查找一个长度为 10* k 的序列,那么正好可能命中取得 mini-batch 所须要的数据。当下一轮查找 pending 数据的时候,另外的 10% 的数据可能曾经在缓存中了,这也意味着可能 1 / 9 的命中率。须要留神的是,在多个工作的训练中,这仍旧实用,因而多个训练任务只管每个都拜访随机的训练数据,从整体来看,他们能够做到以全缓存命中的形式来运行。
训练准确性
因为上述的 I / O 可替换性,咱们有理由狐疑最终训练后果的准确性。这里借用原文的数据来阐明。
04 缓存的治理
在之前的形容中,当只有局部数据被缓存时,Quiver 会在一个 epoch 的训练过程中,再次遍历文件索引。为了能在这后续的遍历中取得更好的命中率,另一部分数据必须被 pre-fetch 到缓存中。
Quiver 通过缓存整个数据集的 2 个 chunks 来解决这个问题。首先数据集的摘要索引文件会被分成固定数据的 chunks, 例如每个 chunk 蕴含 10% 的数据,同时每个 chunk 代表着 striped partition。例如咱们定义数据集中间断的 10% 为一个 partition, 每个 partition 被分成 10 个 stripe units. 这样每个 chunk 将蕴含所有的 partition 中的一个 unit。这样当训练任务操作第一个 chunk 的过程中,第二个 chunk 将被加载到缓存中,所以当局部训练任务实现第一次遍历开始第二次的时候,数据曾经在缓存内,训练任务以递进的形式运行。
这外面潜在的一个问题就是什么时候将第一个 chunk 置换进来。如果置换太快,局部工作还没有实现将导致缓存生效,如果保留太长时间,那么第三个 chunk 将无奈加载进来。在 Quiver 中,当第二个 chunk 被加载到缓存后,第一个 chunk 会被标记为能够革除,同时新的工作能够从第二个 chunk 中取得命中的数据。原有的工作仍旧利用第一个 chunk 来运行,当所有的工作都曾经遍历了第一个 chunk 数据,这部分数据才会真的从缓存中革除,同时第三局部数据将开始加载。
在上述的过程中,如果某一个训练任务相比于其余的要慢很多,那么将导致前一个 chunk 迟迟不能开释,通常来说,在同一个训练模型的多个工作中,每个工作的训练工夫根本是雷同的,但无奈防止在多个不同的训练模型训练同一个数据集的场景。不过如果一个工作显著的耗时很长,那么将意味着每一个 mini-batch 在 GPU 上的训练工夫都很长,也就是它对 I / O 的性能没那么敏感,所以缓存的不命中并不会影响多少这个训练的效率,因而 Quiver 会设定一个临界值来强制第一个 chunk 生效并革除。
05 缓存成果
论文作者通过如下的配置环境来进行成果的比照,从理论数据来看,训练性能的确有较大的进步。
Timeline of Mini-batches
吞吐晋升
06 论断
深度学习场景中,更多的注意力放在了进步计算以及网络的性能上,而对于存储,则是利用现有的计划来解决,例如提前手动将数据加载到离 GPU 较近的 SSD 上。论文作者通过 Quiver,提供了自动化的伎俩来打消存储的瓶颈。当然无奈防止对训练框架的侵入性,但也正是因为如此,Quiver 能力感知到训练 I / O 的特点,进而达到即便缓存只能承载局部数据也能够大幅调高缓存利用率的成果。