参数服务器(Parameter Server)逐段精读【论文精读】

Scaling Distributed Machine Learning with the Parameter Server
如果说 AI 相对于计算机别的领域,比如编程领域已经算比较小的i情况下,那么系统领域相对于 AI 来讲就小了十倍不止了,作为 AI 和系统相结合的交叉方向就更小了,因此受众也比较少
这篇文章发表于 2014 年,会议的名称叫做操作系统的设计和实现(Operating Systems Design and Implementation,OSDI)
这里的系统指的是操作系统,类似的方向还有编译器方向、编程语言方向、高性能计算方向或者是体系结构方向等,对于 AI 来讲,这些方向可能看起来都是偏编程、偏底层的方向,但是实际上领域和领域之间的区分度还是比较大的
操作系统
- 一开始大家所关注的是 Linux、Windows 这样的操作系统的实现
- 但是多年之后,现在能做的东西也不多了,这个领域的论文也在过去一些年发生了一些变化:二零零年比较有名的论文包括互联网公司的一些技术架构,比如 Google 的文件系统 GFS 或者是 Google 的计算框架 MapReduce
OSDI 会议
- 当时是两年开一次
- 整个系统领域两大顶级会议之一,另外一个顶级会议叫做 SOSP(OS 也是操作系统的意思,也是两年开一次),OSDI 和 SOSP 这两个会议是每年轮着开的,现在随着人数的增加也改成一年开一次了
标题
使用参数服务器来拓展分布式机器学习
学术界对大数据的理解和工业界对大数据的理解的差别:
学术界对大数据的理解是一个数据不能用一个邮件来发,而是要通过 U 盘来拷贝了。这也就意味着数据其实是能够拷贝在 U 盘上面的,可能最大也就几个 GB
工业界的大数据是 TB 起步的,不可能一下子就拷贝到 U 盘上面,而且耗时比较长
本文的写作目的旨在介绍什么是真正的大规模机器学习
论文结构
摘要
- 比较短
引言
- 引言比较长,大概有两页多
- 虽然里面包含了相关工作,但是为了把整个故事讲得足够清楚,所以引言花的篇幅比较长
机器学习
- 也是比较长,大概有两页半
- 为什么叫机器学习?因为当时系统领域对机器学习是不太熟悉的,所以需要花很长的篇幅用系统领域研究人员能懂的语言来解释机器学习是干什么的,以此来给读者介绍一下背景知识
系统架构
- 大概两页多一点的篇幅
实现细节
- 差不多两页
实验单元
总结
一共 14 页,相对于机器学习 NeurIPS 或者 ICML 来说,篇幅相对来说是比较长的,这也是系统领域的论文比较难写的一个原因
- 之所以系统领域的论文比较长,是因为一个系统的工作通常需要写几千行的代码将系统搭建出来,然后才能做实验。在系统中存在很多模块的情况下,在论文中需要将这些模块描述清楚,用文字进行描述所以篇幅会比较长
- 机器学习的算法,如果还不是深度学习的算法,仅仅是传统一点的机器学习的话,整个模型可能实现起来只有几行代码,所以写出来相对来讲比较容易。就算是深度学习,就算是将代码附到文章中,可能篇幅也不会太长
这篇文章几乎是没有什么公式的,在但是深度学习还不是特别流行的年代,一篇机器学习的文章不讲公式,描述起来会比较困难,所以这篇文章的机器学习部分的难点在于需要将机器学习领域相关的内容用系统领域研究人员能够理解的语言描述出来,而且尽量不要涉及太多的专业知识
摘要
本文提出了一个参数服务器的框架,用来针对分布式的机器学习任务
- 数据和任务都分布在一些任务节点上或者工作节点上
- 用一些服务器节点来维护一个全局共享的参数,这些参数通常会表示成一个稠密或者稀疏的向量或者矩阵
这个框架用来管理异步的数据通讯,并且支持一些灵活的一致性模型,具有弹性的可扩展性和持续的容灾
为了展示系统的性能,使用了一些 pb 级别的真实数据做了一些实验,这些任务中都含有 10 亿级别的样本和参数
- 这些实验包括稀疏的 Logist 回归、LDA 聚类算法和分布式的 Sketching
- 就算这个工作是在八年前,在当时来讲,数据量和和可学习的参数量也已经非常大了,都是在 10 亿级别以上
所以在深度学习出来之后,它的分布式的问题是一个很简单的问题,直到最近的比较大的语言模型出来之前,深度学习的分布式都没有特别好做的地方,但是现在因为任务已经足够大了,所以对于深度学习的分布式来讲,又有了新的挑战
摘要中所用的句式都是很短的句式,系统方向的人的写作喜欢用比较短的句式,这样整体来讲写起来简单而且强有力
导言
所研究的问题以及这个问题的重要性
分布式的优化和推理现在已经成为了解决大规模机器学习问题的一个前置条件。当规模很大的时候,没有一台机器能够在足够快的情况下解决这个问题(大规模机器学习问题)
- 理论上讲,一台机器慢慢跑总是能够跑完的,但是实际上在现实生活中还是需要在几个小时或者是几天的时间里面完成(因为数据在不断地增加,模型的复杂度也在不停地增长,而且复杂的模型通常会导致参数地变动)
但是实现一个非常有效的分布式算法是非常难的
- 实现一个分布式算法是不难的,但是实现一个性能很高的分布式算法是非常难的
- 摩尔(英特尔的创始人)曾经说过做一个 CPU 并不难,但是做一个性能很好的 CPU 是相当困难的
- 主要是因为计算的复杂度比较高,而且所带来的数据通讯量也会比较大,所以如何有效地处理这两点是一个高效的分布式系统所要考虑的事情
具体的痛点
在现实情况下,这些大规模机器学习的训练数据大小一般是在 1 TB ~ 1 PB
- 虽然这个数据是在 8 年前论文所发表的时候,但是现在看来,数据量也是在这个规模,但很多时候深度学习得数据压缩完可能还不到 1 TB
大量的数据允许创造更加复杂的模型,这些模型可能有 10 亿 ~ 1 千亿个参数。现在最大的语言模型的参数量大概也是在 10 亿到 1 千亿。这些模型需要被全局共享,所有的计算节点都要去访问这些模型,这样就会带来很多的问题
- 所有的计算节点都会频繁地访问这些参数,就会导致大量的网络通讯
- 机器学习算法都是一个顺序的模型,算完第一个小批量之后才能计算第二个小批量,这就会导致大量的全局同步,从而影响模型的性能
- 当计算规模比较大,需要使用成百上千的计算机来做运算的时候,容灾就显得比较重要了
- 容灾:一台机器宕机或者有其他任务将其从当前任务中挪走的情况下,整个训练过程还是能够继续计算下去
表 1

为了展现最后一点,作者在一个公司的数据中心收集了它过去三个月所有的训练日志,并将这些任务按照机器 × 时间(如果 100 个机器跑 1 个小时就是 100 个小时)并分成了三档:
- 100 小时:小规模
- 1000 小时:中规模
- 10000 小时:大规模
从上表中可以看出:虽然绝大部分的任务都是 100 小时级别的,失败的概率为 7.8% ,但是随着规模的增大,到 10000 小时级别的时候( 3 个月中有 77 个任务有 10000 个小时),失败的概率就从 7.8% 提高到了 24.7%,也就是说四分之一的任务都会失败
- 任务的规模越大,任务失败的概率就越高。因为随着做运算的机器和训练时间的增加,这些机器出现问题的概率还是比较大的
- 比机器出现问题的概率更高的是软件层面上的一些问题,比如某一台机器的磁盘突然满了、机器被其他任务抢走了或者整个模型的架构本身不稳定等
虽然现在整个系统机器的稳定性和模型基础架构的稳定性已经比之前有很大的进步了,但是实际上如今在跑大规模的任务(用 1000 块或者 10000 块 GPU 去训练一个特别大的语言模型)时,任务失败的概率依然是非常高的,主要存在两个问题:
- 机器容易过热。显卡在持续工作的过程中,在电量要求比较大的时候,由于风扇的转速没有跟上导致过热降频
- Nvidia 的驱动偶尔会出现一些问题。在分布式中,驱动可能在通讯的时候会出现问题
这些问题都是可以被解决的,但是一旦系统变得更加复杂,就可能会有新的模块出现问题,导致在大规模训练时,任务的成功率依旧不高
这个表格主要讲述的是为什么要在机器学习里面做容灾
不像许多实验室里的那样,所有的任务都是独占一个集群在运行。在工业界真是的应用场景中,容灾是非常重要的
引言的前半部分讲的是所需要解决的三大痛点
- 网络带宽的利用
- 机器学习算法需要不断地做全局通讯
- 容灾
1、贡献
参数服务器框架在参考文献【43】这篇文章出来之后,在工业界和学术界都没有被使用
- 当时这个框架主要是给 LDA 一个聚类的算法所使用的,所以之前关注的不是很多
- 直到一两年之后 Jeff Dean 写了一篇文章,提出了 TensorFlow 的前身,使用的是参数服务器实现的,后续才有了大量的研究者跟进
如果将参考文献【43】称为第一代,Jeff Dean 所提出的工作称为第二代的话,那么本文所提出的就是第三代的开源实现,主要有两个好处
1、将整个框架中共享的一些模块抽象出来之后,易于实现任务相关的代码,而不像几天做深度学习的时候可能跑一个 SGD 就可以了
- 八年前当时的机器学习比较多样化,优化算法的种类繁多,因此需要做到框架设定要比较简单,而且能够适配到各种不同的算法上面,所以提供了高性能的实现,同时能够处理很多不一样的算法,包括了稀疏的 Logistic 回归、topic model(LDA,可以认为是一个话题模型、聚类算法或者是分布式的 sketching)
本文所提出的框架是根据真实的系统所设计的
- 当时在学校中关于系统和机器学习结合的研究都存在一个问题:并不真正清楚机器学习在工业界的具体实现,只是将一些论文中的机器学习算法盲目地扩大 10 倍或者 100 倍,然后再看如何去设计,但是实际上在在现实生活中并不会盲目地进行扩大,所以导致在设计上会出现偏差
- 这里所要强调地是根据真实的计算任务来进行系统级别的抽象
本文所提出的参数服务器有五个关键性的特征:
1、有效通讯。使用异步通讯的方法,同时针对机器学习算法进行了大量的压缩,从而降低通讯量的规模
2、灵活的一致性的模型。一致性是说每一个计算节点对同样一个参数(比如说参数 W0)在任何时刻是不是存在一致性。
- 比如说两台机器访问同一个值的时候,一台机器可能拿到的值是上一个时间点的,另一台机器拿到的值可能会更新一点,他们之间所存在的差异就是一致性模型。
- 强一致性表示在任何时刻都应该拿到一样的值,强一致性对优化算法和机器学习来说更好一些
- 弱一致性表示允许一定的程度上的延后,弱一致性对系统的高效性来说更好一点
- 当时由于通讯量实在是太大了,所以需要牺牲一点机器学习算法上的好处去换取系统的好处,去关注一些弱一致性的模型
3、弹性的可扩展性:在训练的时候,新的节点是不是可以在不停掉整个任务的情况下加进来。
- 当时这个特质对数据库、各种计算框架(如 MapReduce)来说可能还行,但是对机器学习这种有非常强的一致性的模型算法来讲还是比较先进的,就算是 8 年后的今天似乎也没有几个框架真正能做到在训练的时候进行机器的增减
4、容灾:当一台机器或者几台机器出现问题的时候,花多少时间能够恢复过来
- 现在大型的互联网服务容灾都做的比较好,比如说微信或者说某个网站如果下线几个小时的话,将会造成非常大的灾难
- 但是对于机器学习来讲,对于容灾的关注度还不是很高,但是在本文中还是将容灾做到了比较高的程度:当一台机器或者几台机器停机,只要不争整个系统所有的机器都停机的情况下,可以在一秒钟之内恢复运行,所使用的技术叫做 vector clock(向量钟,一个年代比较久远的技术)
5、操作简单:8 年前,python 的应用还不是很广泛,特别是在工业界大规模的机器学习平台中,几乎很少使用 python
- 当时主要是使用 C++ 来进行开发,C++ 不像 python 有 numpy ,没有那么好的矩阵计算相关的东西
- 当时一个矩阵或者一个 vector 就认为是一个数组,所以这里强调全局参数可以抽象称为一个稀疏的向量或者矩阵,所以可以调用一些向量和矩阵
- 在当时 C++ 中的库做的都不是很好(当时做的比较好的有 ALGOL ,一种早期的计算机算法语言,但是使用起来比较困难)
- 反过来讲,如果所提供的抽象的数据结构是向量或者是矩阵的情况下,开发机器学习会简单很多。在现在大家已经习惯了使用各种深度学习框架的情况下不会觉得困难,但在当时来说这确实代表了易用性
novelty
本文所提出的系统找到了合适的系统技术,然后将其适配到机器学习算法中,并且改变这些机器学习的算法使其对系统更加友好
- 本文所做的工作就是系统和机器学习的交叉。对于像这种由两部分构成的内容,如果只是单纯地将这两个东西放在一起,也就谈不上 novelty 了;如果说这两个东西本身就耦合得比较好的话,也就谈不上工作量了;如果说强行将两个东西很生硬地结合到一起,那所做的工作也就没有任何意义了;所以,如果将两个东西都进行适当的调整之后能够很好地融合在一起的话,其实可以说是有 novelty 的
具体来说,本文所提出的系统放弃了一些分布式系统中要求特别高的如一致性等这些内容,同时也对机器学习的算法进行了一些修改,使其能够容忍所丢弃掉的一致性。最终得到了第一个通用的机器学习的系统,并且能够做到工业界的大小
- 虽然这里说是”第一个“,但是有两个前置的条件:1、general purpose,作为一般性的使用;2、industrial scale size,工业级别的大小,当时工业界对自己的大系统都是做特殊开发,也有一些通用的系统(比如 spark ),但是实际上这些通用的系统只能做相对来说比较小(几个 GB 、几十 GB 或者几百个 GB )的东西。所以如果想做一个比较通用而且能够达到当时工业界最大规模的系统的话,在当时来讲还是一个比较困难的事情
2、工程上的挑战
如果说想要处理一些分布式数据分析的问题的话,就需要不断地读写全局的参数。
参数服务器提供了一个有效的汇聚和同步计算节点和统计信息的机制。
- 对于每一个参数的服务节点,参数服务器会维护一部分全局共享的参数(因为参数可能会非常大,,一台机器可能放不下,所以会有很多台机器一起来维护,每台机器维护一部分
- 每一个计算节点(work node)每次会拿整个参数中的一部分(有可能是全部,也可能是其中的一块),再读取一些数据来进行计算
这里有两个关键性的挑战:
1、通讯
计算节点需要不断地向服务器索要和回传数据,在分布式中,key/value 系统有很多这样的工具,市面上这些 key/value 的 datastore(datastore 不是一个数据库,就是一个纯数据的东西)并没有提供足够的抽象
- key 可以认为是某一个参数的下标或者是深度神经网络中的某一个层对应的 W 中的一个元素,但是如果说对每一个元素都将下标拿出来进行浮点运算的话,效率并不是很高(因为每一个 key 发送出去之后只能带上一个浮点运算的话,开销比较大)
对这些机器学习的算法来讲,通常来说数据结构是一个结构化的数学物体(比如向量矩阵或者张量),所以每次更新的话不是发送一个一个的浮点数,而是发一段一段的内容(segment)
- 对于深度学习来讲,每次发送一层的东西,这在现在看来可能就是一个比较正常的东西。但是如果考虑随更广阔的机器学习来讲,话有很多的算法并不是这样的(比如说稀疏的 Logistic 回归,它的整个权重可以认为是一个很稀疏的东西,每次不需要将整个 W 发送出去,而是将 W 中的一些非零元素发送出去),所以这里使用 segment 更好一点,可能是一个向量中间的一段或者是一个矩阵的一整行,也可以是整个矩阵,所以在这种情况下可以批量地发送和更新,而不是对每一个向量逐一更新,而且这个抽象也可以覆盖到大量的机器学习算法
2、容灾
如果一台机器挂掉的话,不需要重启整个任务(从上一个保存点重启)
- 这里所使用到的技术是对每一个服务器节点进行实时复制,使得即使一台机器挂掉了,在另外一个地方还有数据的备份,这是分布式系统中使用比较广泛的技术,虽然在机器学习中使用的得并不是很多
- 对于计算节点来讲的话,因为不用存储全局共享的参数,所以相对来讲实现起来更加容易一些。如果一台机器挂掉了,可以直接拿掉它或者重新增加几台机器,从而进行动态的调整
图 1

图 1 中对比了当时(2014 年 4 月)的一些系统中最大的任务用了多少个核
- 这里指的是 CPU 的核,因为在 2014 年的时候,使用 GPU 做大量的机器学习还没有广泛应用
- y 轴表示的是任务对应的可学习的参数的个数
- 蓝色方框表示稀疏的 Logistic 回归
- 红色方框表示 LDA(无监督的聚类算法)
- 紫色的表示 Distbelief ,它是 TensorFlow 的上一代,使用的是 DNN 的任务
- 在当时看来,稀疏的 Logistic 回归是最主流的,接下来是 LDA ,然后是 DNN 的兴起
这篇文章所使用的两个实验都是非常大的,LDA 使用了将近 10 万个核,稀疏的 Logistic 回归也使用了将近 1000 亿
- 现在的文章一般都使用 1000 个 GPU来跑很多天
- 这篇文章虽然是 8 年前的,但是它的规模放到现在来讲仍然是非常大的
表 2

表 2 对比的是当时之前的几个工作之间的区别(数据结构)
参数服务器:
- 使用的是稀疏的向量或者矩阵
- 一致性模型(允许计算节点之间有多少的不一致性,始终保持一致或者是仅需要在停止之前能够达到一致):这里做的比较灵活,因为需要支持大量的不一样的算法
- 容灾(最简单的就是进行备份,每一次扫完数据或者每隔过少分钟或者每过多少个小批量之后,就将整个模型的参数存下来,这样做的坏处是需要浪费很多的磁盘空间):重新开始计算的话会浪费掉一部分的计算资源,所以本文中使用的是持续的容灾
3、相关工作
这篇文章的名字叫做 Parameter Sever
- 这个词最早出现在文献【43】中,但是真正让这个名字出名的是 Jeff Dean 的 paper(Large Scale Distributed Deep Networks,NIPS 2012)
- 作为区分,本文将最早的工作称为第一代,Jeff 相关的工作称为第二代(他的系统是针对某一个特定的任务,Distbelief 是针对深度学习来做的),本文将自己的工作称为第三代(因为本文尝试了去做一个更加通用的机器学习的解决方案)
小结
整个导言的部分篇幅比较长,主要是因为对各种上下文花了很多的笔墨进行交代,因为当时的系统界对于整个机器学习没有特别深刻的了解。之前的一些文章更多的是像描述研究的机器学习系统,本文所尝试的是从工业界实用的角度来讲,机器学习系统应该具备的特征与难点,也是一个相对来说比较新的角度,所以作者花了很多的时间去讲清楚是什么和为什么,也是为了给后文讲述为什么设计成这样子铺路
机器学习
- 这部分可以认为是背景知识介绍,讲述了训练、特征提取、特征向量、目标函数与学习等相关概念
- 这部内容可以借鉴学习一下:如何针对某一个领域的读者介绍什么是机器学习
算法 1
- 算法一描述的是如何将机器学习中比较常见的梯度下降方法变成分布式的算法

算法 1 中将一个任务切分成了三块:
1、任务调度器
- 算法的调度器可以认为是整个优化算法中的 for 循环:告诉所有的计算节点将数据 load 回来,然后开始迭代(迭代 T 次,在每个 t 时刻告诉所有的工作节点进行第 t 个小批量的更新)
2、计算节点
- 可以认为是一个机器,但本质上是一个进程,是一个抽象出来的概念
- 这部分可以在同一台机器上出现,没有必要真的使用很多台机器,一台机器可以跑多个工作的进程
- 这样做的好处是:假设这部分中的多线程实现不是很好的话,则可以跑多个工作的进程,使得更有效地使用所有的计算资源(或者如果一台机器上有很多张 GPU 的时候,可以在每个 GPU 上跑一个计算节点;server 也是一样的,server 其实不怎么耗费计算资源,server 也可以放在同一台机器的不同进程上)
- 这里主要是一个抽象,实际实现来说可以不用做那么大,但是抽象是很重要的,对于一个系统来讲,抽象决定了 API
工作节点的两个函数
LoadData 函数
- 这个函数的作用是找到所需要的训练数据
- 这里所使用的是数据并行:假设有 m 个工作节点,则会将数据分成 m 块,每个工作节点读取它所对应的那一块训练数据,读到数据之后,它会将自己要的那些权重从 server 上面下载下来
- 最简单的就是将整个 server 的权重下载下来,但是有时候如果数据量特别大,一个 server 根本放不下要很多 server 才能放得下的情况下,内存可能不够用,这时通常来说不会将机器学习的模型设的无限大,如果确实 W 特别大导致一个机器的内存中放不下的情况下,一定是有内在的一些结构(比如说稀疏的结构)使得每一个计算节点只需要其中的一小块就行了,因为只用了其中的一块数据
- 程序中的 working set 并不一定指的是完整的 W
WorkingIterate 函数
- 这是一个迭代函数,描述的是第 t 次迭代
- 函数所完成的功能是:拿到第 t 时刻对应的 W ,然后和自己所有的样本计算梯度(每一个样本的梯度求和就得到了总梯度),计算完总梯度之后 push 给服务器节点,然后再从服务器节点将更新后的权重 W(t+1)pull 下来,这样接下来就能做下一次的迭代了
- 这里每一次没有放缩一致性,所以每次拿到的都是全局的新的 Wt (表示每次使用的都是最新的正确的 W 进行的计算,从而不会影响最终的计算结果)
3、服务节点
ServerIterate 函数
- Ω :目标函数中的正则项
- 函数所完成的功能是:将 t 时刻所有工作节点的梯度汇总得到总梯度,然后通过计算更新权重(这里梯度的更新也是原始梯度的更新)
- 这里是将一个正常的梯度下降的优化算法切开,然后分到不同的地方去
- 这里只是做了物理上的切割,并不会影响真实的计算结果,也就是说不管使用多少个工作节点和服务节点都不会影响最终的计算结果,因为梯度不管是分到多少个计算节点上,最后的梯度是所有样本的梯度求和,所以不管有多少个 m ,最后都是求和
图 2
- 图二直观上描述了任务的划分以及整个计算的流程

- 图中一个圆角矩形框就是一个节点,节点是一个抽象的概念,并不是指的是物理上面的机器,本质上就是一个进程
- 图中画出了很多的工作节点
- 这里的数据是稀疏的,每一行表示样本,列表示特征,“X” 表示非零;这里的数据可能是千亿或者是万亿级别的样本数,因为数据是稀疏的,比如某一个计算节点所拿到的权重数据中有的列为全零,没有任何的非零元素,这些列的的非零元素可能是分到其他计算节点上,所以当前计算节点其实不需要这些列对应的 W
- 计算流程:当拿到一大块数据之后,让每个节点拿到其中的一部分,这里也叫数据并行,当计算节点从 server 端拿到自己所需要的那部分 W 之后,它会根据数据和 W 计算出梯度(梯度也是稀疏的),然后传回 server 端(server 进程),当 server 端拿到所有来自计算节点传回来的梯度之后,将它们相加然后更新 W(这里其实是一个循环,多台机器的情况下进行多种循环,所以当任务特别复杂或者数据特别大的时候,可以增加机器的数量,使得每台机器的计算量大大减小,这样就能缩短整体的计算时间)
- 因为当前计算节点所需要的 W 只是完整的 W 中的一部分,这样就可以将 W 做的特别大,每台机器只拿到 W 的一部分就可以了
图 3

为什么服务器节点需要多个才能存储全局共享的参数,但是每个工作节点就能够拿到想要的那部分 W ?
- 假设有一个一千亿个特征的数据(假设数据是稀疏的),如果只有一台机器的话,那么这台机器需要拿到数据的完整特征
- 假设能够将这些数据分到 100 台机器的情况下,那么这时候每台机器因为稀疏性的原因只需要 7.8% 的 W 就可以了(也就是只需要 78 亿的特征就可以了),这是完全可以放进内存中的
- 假设进一步增加计算节点数,增加到 10000 个计算节点的时候,每台机器就只需要 0.15% 的 W 了,也就是说每台机器只需要保存 1.5 亿个参数就行了
- 通过以上数据,也解释了这种划分方式能够支持特别大的计算任务
参数服务器的架构
----to be continued----