简介
第一视角:深度学习框架这几年 作者深度参与了tf 等几个框架,对很多事情有独到的了解
大模型有什么用
目前为止,大模型主要是以NLP为主,因为NLP抛弃了RNN序列依赖的问题,采用了Attention is All you need的Transformer结构,使得NLP能够演变出更多大模型。图像领域也不甘示弱,CNN大模型也开始陆续涌现。
- 模型碎片化,大模型提供预训练方案。目前AI面对行业、业务场景很多,人工智能需求正呈现出碎片化、多样化的特点。从开发、调参、优化、迭代到应用,AI模型研发成本极高,且难以满足市场定制化需求,所以网上有的人会说现阶段的AI模型研发处于手工作坊式。基本上一个公司想要用AI赋能自身的业务,多多少少也得招聘懂AI的研发人员。为了解决手工作坊式走向工厂模式,大模型提供了一种可行方案,也就是“预训练大模型+下游任务微调”的方式。大规模预训练可以有效地从大量标记和未标记的数据中捕获知识,通过将知识存储到大量的参数中并对特定任务进行微调,极大地扩展了模型的泛化能力。例如,在NLP领域,预训练大模型共享了预训任务和部分下游任务的参数,在一定程度上解决了通用性的难题,可以被应用于翻译,问答,文本生成等自然语言任务。PS:手工作坊式 ==> 工厂模式,一个大模型替代之前几十个专门小模型。
- 大模型具备自监督学习功能,降低训练研发成本。大模型的自监督学习方法,可以减少数据标注,在一定程度上解决了人工标注成本高、周期长、准确度不高的问题。
- 大模型有望进一步突破现有模型结构的精度局限。从深度学习发展前10年的历程来看,模型精度提升,主要依赖网络在结构上的变革。 例如,从AlexNet到ResNet50,再到NAS搜索出来的EfficientNet,ImageNet Top-1 精度从58提升到了84。但是,随着神经网络结构设计技术,逐渐成熟并趋于收敛,想要通过优化神经网络结构从而打破精度局限非常困难。近年来,随着数据规模和模型规模的不断增大,模型精度也得到了进一步提升,研究实验表明,模型和数据规模的增大确实能突破现有精度的一个局限。
什么是大模型
TensorFlow在推荐系统中的分布式训练优化实践随着美团业务的发展,推荐系统模型的规模和复杂度也在快速增长,具体表现如下:
- 训练数据:训练样本从到百亿增长到千亿,增长了近10倍。
- 稀疏参数:个数从几百到几千,也增长了近10倍;总参数量(也就是tf.Variable)从几亿增长到百亿,增长了10~20倍。
- 模型复杂度:越来越复杂,模型单步计算时间增长10倍以上。 对于大流量业务,一次训练实验,从几个小时增长到了几天,而此场景一次实验保持在1天之内是基本的需求。
深度学习分布式训练的现状及未来大模型主要分为两类:
- 搜索、推荐、广告类任务,它的特点是海量样本及大规模稀疏参数(sparse embeddings),适合使用 CPU/GPU 参数服务器模式(PS);参数服务器模式从第一代 Alex Smola 在 2010 年提出的 LDA(文本挖掘领域的隐狄利克雷分配模型),到第二代 Jeff Dean 提出的 DistBelief,接着到第三代李沐提出的相对成熟的现代 Parameter Server 架构,再到后来的百花齐放:Uber 的 Horvod,阿里的 XDL、PAI,Meta 的 DLRM,字节的 BytePs、美团基于 Tensorlow 做的各种适配等等。参数服务器的功能日趋完善,性能也越来越强,有纯 CPU、纯 GPU,也有异构模式。
- CV、NLP 任务,它的特点是常规样本数据及大规模稠密参数,它适合用纯 GPU 集合通信模式(Collective)。基于纯 GPU 的集合通信模式的分布式训练框架,伴随着 Nvidia 的技术迭代,特别是 GPU 通信技术(GPU Direct RDMA)的进步,性能也变得愈来愈强。
广告推荐中大规模分布式模型 为啥一两百类的特征,我们却总是听说大规模特征?举个例子,用户 userid 这一维特征,比如系统中用户有1亿个,那么每个 id 实际上也可以当做一个独立的特征对待。这样一算,特征规模就上去了。这里就要重新理解 embedding 的概念了。对于模型而言,id 查了embedding表后得到向量,输入进来进行计算,是对数据进行抽特征。如果类比到图像分类,抽取 rgb 特征来分类 (一个值变成 3个255)
参数量卷到一百万亿!华人团队开源史上最大的推荐训练系统Persia 一般来说,推荐系统模型首先需要将不同的ID特征(如用户ID和session ID)映射到一个固定长度的低维向量,而系统中的用户ID、交叉特征数量都特别多,就需要更大规模的模型来捕获特征和映射。但更大规模的embedding layer也需要更大的内存来载入,不得不说大模型太费钱了!
有了embedding后,剩下的工作就简单了,设计后续layer来适配不同的任务。通常只占据整个模型的0.1%,无需大内存,主要是一些计算密集型的工作。
在实现上
- 推理服务在运行时 也会访问ps (distributed inference),根据 ID feature 查询对应的 embedding 向量。当然,有的框架直接将 ps 组件的功能内嵌到各个worker 上了。
- 针对 大模型 包含 embedding layer的场景,input 层和 embedding 层之间不是全连接的,而是一个 embedding_lookup 的Op
- 常规的dense 模型,input是一个一维向量。 针对多个id feature,为了 确保与模型的input 层对齐,input 实际变成了一个
map<string,tensor>
,key 为id feature 名字,value 为id feature 值对应的 tensor。
大了难在哪
- 内存墙。在计算过程中,神经网络模型每一层的卷积或者全连接计算,都会把权重W_m长期保存下来,用作网络的权重参数更新(静态内存)。另外针对诸如ADAM的优化器,会存储优化器的动量等信息,用于优化器计算(动态内存)。一块有16G显存的AI芯片,最大能塞满20+亿参数的模型,但是这时候已经没有额外空间,留给动态内存进行分配啦。静态内存和动态内存都可能造成内存墙的问题。
- 通讯墙。大模型通过模型并行、流水线并行切分到AI集群后,通讯便成了主要的性能瓶颈。随着机器规模的扩大,基于同步的All Reduce通讯聚合方式,会因为大量的AI芯片和服务器之间频繁进行同步,出现水桶效应,也就是最慢的一路通讯,将会决定整个AI集群的通讯的高度。如果采用目前比较流行的Ring-AllReduce的通信聚合方式,当通讯的环越大,通讯的延长将会不断地被扩大。另外网络协议的多次握手的方式,诸如此类的开销会导致训练无法有效利用带宽。
- 性能墙。性能墙呢主要是指计算资源利用率的问题。随着大模型的提出,对算力需求更加迫切,理论上在4K的集群上每块卡快1分钟,总体就快了68个小时。大模型会增加对算力的需求,但是随着大模型引入各项分布式并行技术的同时,会降低计算资源的利用率。
- 算子层(Operator Level):小算子过多,可以通过算子融合进行优化;子实现不够高效,类似于卷积CONV算子针对2x2和3x3 Kernel大小的使用Winograd算法代替;内存局部性差,对算子和内存的开销进行分析,可以对计算时算子输出有相同的shape进行复用。
- 图层(Graph Level):如何搜索和切分处计算效率更高的子图,分配到不同的机器上进行计算;数据在通讯和内存之间如何增加overlay重叠部分,提高单卡整体的计算率。
- 任务层(Task Level):在任务层级,性能的瓶颈从计算转移到了通信,如何降低通信量,缩减通信域规模,尽可能把通信时延隐藏在计算中,是大规模训练的核心关注点。
- 调优墙。所以在数千节点的集群上,需要考虑到提升算法工程师分布式调试调优的效率,另外还要考虑降低工程师对大模型进行并行切分的难度。除了对人的考虑,还要对硬件集群的管理,需要保证计算的正确性、性能、可用性。要是有一台机器坏了,如何快速恢复训练中的参数。
CV和NLP场景
浅谈工业界分布式训练(一) 除了上述的数据量级大,不同场景下分布式训练的痛点 对CV和NLP场景
- CV和NLP场景模型复杂,单机性能要求高,比如卷积的计算时间在CPU上和 GPU上相差十倍到几十倍。 ==> 业界主要使用高性能的GPU进行计算,并采用All-reduce的通信拓扑进行参数的同步更新。
- 模型大(DenseNet部分),比如NLP领域,GPT-3这样的模型高达1750亿参数,显存占用高达2.8 TB,单机内存无法容纳。而Bert-Large虽然只有3.4亿参数规模,但由于各种内存占用,在16G V100上,训练也仅能使用batch Size=8。 ==> 当面对GPT-3这种DenseNet部分大的模型,Allreduce 单卡内存无法容纳,我们需要采用模型并行(model parallelism)的方式将计算图划分到不同的设备上构建有向无环图(DAG)进行分布式训练,其中Gpipe, Megatron, Oneflow和Whale都提出模型并行的相关解决方案。相比于数据并行每个worker只有一部分数据,模型并行下每个node使用所有数据.
- Intra-layer parallelism(Tensor Parallelism) 。主要是将一层Layer中的矩阵计算分别拆分到不同的机器上进行运算,比如简单的Y_1=W_1 X_1这一次矩阵乘法中,我们将模型参数W_1或者输入数据X_1,按某个维度分别拆分到不同设备上计算,比如1D Megatron。
- Inter-layer parallelism(Pipeline Parallelism)。而Inter-Layer Parallism会将模型的layers拆分到不同的机器上,则一次forward就需要跨过不同的机器串行地进行运算,而流行并行通过将batch size切分为更小的mirco batch,减少数据依赖,从而将整个计算过程异步起来,最大化资源利用率。举例来说,在一个简单的三层MLP中(的Y_i = W_i X_i, i=1,2,3)会存在三次矩阵乘法 W_i X_i,流水线并行会把W_i X_i分别分配到三台机器上进行运算。
AI for Science的出现,让高性能计算与AI融合成为刚需:
- 数据并行。假如整个模型设两个节点,一个模型节点0、另一个模型做的节点1,整个模型都做了数据并行,数据各一半要拿去训练学习,但是要注意训练完了以后不是最终的结果,因为只输入了一半的数据。因此这中间要AII-Reduce
- 模型并行。将整个模型切成一半,一半模型节点0,一半模型节点1,数据是整个拿去训练。训练完了以后出来的结果也不是最终结果,因为只训练了一半的模型,出来还有AII-Gather,也是做通信的。 究竟用哪种并行方法呢?实际上这跟计算机结构有关。如果每台计算机之间通信都非常快,那么用数据并行就可以;如果你的通信比较慢,就要考虑模型并行。因此,这些模型如何跟数据、机器实际情况匹配?这就涉及到软硬件协
推广搜场景
大模型:id 化导致模型变大,模型变大需要更多的数据才能收敛。
- 模型小,词表大。模型中的DenseNet部分,不像BERT是模型巨大词表小,往往一台机器的内存就可以容纳,但是其特征量级可能高达成百上千亿,造成Sparsenet部分或者Embedding lookup table高达TB级别,使得单机无法容纳。
- 一个batch的embedding lookup量级大,造成查询耗时大。由于特征数量多,一个batch可能包含几十万个ID类特征,tf原生的embedding lookup查询耗时大,造成训练和inference性能低。尤其在线上inference的时候,无法在给定RT内完成服务响应。
- 数据具有大规模稀疏的特点。不同于CV和NLP场景,数据是稠密的图像和文本,搜广推的数据非常稀疏的,第一这来源于很多数据无法对所有用户和场景有效采集到,第二是因为建模使用的特征量级大造成的高维稀疏性。这会影响了数据的存储格式和计算效率。
TensorFlow在美团外卖推荐场景的GPU训练优化实践 推荐系统深度学习模型特点
- 读取样本量大:训练样本在几十TB~几百TB,而CV等场景通常在几百GB以内。
- 模型参数量大:同时有大规模稀疏参数和稠密参数,需要几百GB甚至上TB存储,而CV等场景模型主要是稠密参数,通常在几十GB以内。
- 模型计算复杂度相对低一些:推荐系统模型在GPU上单步执行只需要10~100ms,而CV模型在GPU上单步执行是100~500ms,NLP模型在GPU上单步执行是500ms~1s。 GPU服务器特点
- GPU卡算力很强,但显存仍有限:如果要充分发挥GPU算力,需要把GPU计算用到的各种数据提前放置到显存中。而从2016年~2020年,NVIDIA Tesla GPU卡计算能力提升了10倍以上,但显存大小只提升了3倍左右。
- 其它维度资源并不是很充足:相比GPU算力的提升速度,单机的CPU、网络带宽的增长速度较慢,如果遇到这两类资源负载较重的模型,将无法充分发挥GPU的能力,GPU服务器相比CPU服务器的性价比不会太高。
外卖广告大规模深度学习模型工程实践在数据规模、模型规模增长的情况下,所对应的“时长”变得会越来越长。这个“时长”对应到离线层面,体现在效率上;对应到在线层面,就体现在Latency上。
“时长”变长,主要会体现在以下几个方面:
- 在线时延:特征层面,在线请求不变的情况下,特征量的增加,带来的IO、特征计算耗时增加等问题尤为突出,需要在特征算子解析编译、特征抽取内部任务调度、网络I/O传等方面重塑。在模型层面,模型历经百M/G到几百G的变化,在存储上带来了2个数量级的上升。此外,单模型的计算量也出现了数量级的上涨(FLOPs从百万到现在千万),单纯的靠CPU,解决不了巨大算力的需求,建设CPU+GPU+Hierarchical Cache推理架构来支撑大规模深度学习推理势在必行。
- 离线效率:随着样本、特征的数倍增加,样本构建,模型训练的时间会被大大拉长,甚至会变得不可接受。如何在有限的资源下,解决海量样本构建、模型训练是系统的首要问题。
- 在数据层面,业界一般从两个层面去解决,一方面不断优化批处理过程中掣肘的点,另一方面把数据“化批为流”,由集中式转到分摊式,极大提升数据的就绪时间。
- 在训练层面,通过硬件GPU并结合架构层面的优化,来达到加速目的。
- 其次,算法创新往往都是通过人来驱动,新数据如何快速匹配模型,新模型如何快速被其他业务应用,如果说将N个人放在N条业务线上独立地做同一个优化,演变成一个人在一个业务线的优化,同时广播适配到N个业务线,将会有N-1个人力释放出来做新的创新,这将会极大地缩短创新的周期,尤其是在整个模型规模变大后,不可避免地会增加人工迭代的成本,实现从“人找特征/模型” 到“特征/模型找人”的深度转换,减少“重复创新”,从而达到模型、数据智能化的匹配。
- Pipeline其他问题:机器学习Pipeline并不是在大规模深度学习模型链路里才有,但随着大模型的铺开,将会有新的挑战,比如:① 系统流程如何支持全量、增量上线部署;② 模型的回滚时长,把事情做正确的时长,以及事情做错后的恢复时长。简而言之,会在开发、测试、部署、监测、回滚等方面产生新的诉求。
大模型的参数主要分为两部分:Sparse参数和Dense参数。
- Sparse参数:参数量级很大,一般在亿级别,甚至十亿/百亿级别,这会导致存储空间占用较大,通常在百G级别,甚至T级别。其特点:
- 单机加载困难:在单机模式下,Sparse参数需全部加载到机器内存中,导致内存严重吃紧,影响稳定性和迭代效率;
- 读取稀疏:每次推理计算,只需读取部分参数,比如User全量参数在2亿级别,但每次推理请求只需读取1个User参数。
- Dense参数:参数规模不大,模型全连接一般在2~3层,参数量级在百万/千万级别。特点:
- 单机可加载:Dense参数占用在几十兆左右,单机内存可正常加载,比如:输入层为2000,全连接层为[1024, 512, 256],总参数为:2000 * 1024 + 1024 * 512 + 512 * 256 + 256 = 2703616,共270万个参数,内存占用在百兆内;
- 全量读取:每次推理计算,需要读取全量参数。
因此,解决大模型参数规模增长的关键是将Sparse参数由单机存储改造为分布式存储,改造的方式包括两部分:
- 模型网络结构转换。业界对于分布式参数的获取方式大致分为两种:外部服务提前获取参数并传给预估服务;预估服务内部通过改造TF(TensorFlow)算子来从分布式存储获取参数。为了减少架构改造成本和降低对现有模型结构的侵入性,我们选择通过改造TF算子的方式来获取分布式参数。正常情况下,TF模型会使用原生算子进行Sparse参数的读取,其中核心算子是GatherV2算子。算子的作用是从Embedding表中读取ID列表索引对应的Embedding数据并返回,本质上是一个Hash查询的过程。遍历模型网络,将需要替换的GatherV2算子替换为自定义分布式算子MtGatherV2,同时修改上下游节点的Input/Output。MtGatherV2 算子:从本地Embedding表中查询,改造为从分布式KV中查询。
- Sparse参数导出。
模型保存
- 在模型比较小的时候,比如100G以下,模型还有可能单机存储。这个时候的方案是tensorflow分布式训练+savedmodel,分布式训练可以用多个ps(tensorflow自带的),资源管理可以用yarn。用分布式是由于样本数大,同时多ps也能异步加速训练。saved_model一般由chief worker保存,但存下来以后,会抹掉ps的痕迹,保存的模型跟单机训练的一模一样。
- 当模型比较大的时候,这个时候要求的样本数也更大,训练完dump出来的模型会很大,一个单机放不下,尤其是embedding table。这个时候怎么办?一个自然的思路就是,把训练时候的ps拷贝同步一份给serving ps,线上由该ps做serving。注意后面谈到的serving ps,都是自己开发或者根据某个开源软件修改而成(比如ps-lite)。如果是天级模型,可以用tensorflow原生的worker和train ps,但依然用saved model方式把模型存放到hdfs,然后从hdfs读入另外一个serving ps。如果是实时训练,则serving ps还得跟训练ps进行实时的网络连接,在内存就解决掉weight同步的处理,这个时候就不能用tensorflow原生的ps了,因为原生的ps没有实现同步接口。ps变了,worker也得跟着变,worker大多数都是基于tensorflow的c++底层接口开发,底层用到tf的session接口。
解决思路
针对上述的问题,各个大厂的训练框架进行很多相关优化,目前总结下来,核心的两点,一个在于分布式通信拓扑的设计,还有一个在于Embedding Lookup的性能优化。
只要单卡放的下,走数据并行,ps 或allreduce 都行,allreduce 通信成本小一些。若规模变大
- 稀疏模型,稀疏参数特殊处理
- 使用ps,加上一些稀疏tensor 的优化,且将 embedding 存储和更新 负担转嫁到 ps
- 稠密参数allreduce,想办法解决 稀疏tensor 的存储、通信成本。 比如 HybridBackend架构中参数放在 worker 上:稠密参数 replication 存放,每个 worker 都有副本,梯度更新时进行 allreduce;稀疏参数 partition 存放,每个 worker 只有部分分片,梯度更新时进行 alltoall。allreduce 和 alltoall 都会使用 nccl 进行同步通信,效率较高。hb 进行 alltoall 时,通信的是稀疏梯度,而不是完整的参数,通信量上和 ps 是一致的,但是通信效率会更高。
- 稠密模型,单卡无论如何也放不下了,就只能采取模型并行 及附属的一些优化方案
- 算子拆分 单个矩阵乘法可以分到两个device上计算
Y = WX = [W1,W2]X = [W1X,W2X]
。我们在工程上要做的就是:将切分到两个device上,将复制到两个device上,然后两个device分别做矩阵乘法即可。有的时候,切分会带来额外的通信,比如矩阵乘法切到了reduction维度上,为了保持语义正确,就必须再紧跟一个AllReduce通信。 这里复杂之处在于,你不能无脑地将所有算子全部做拆分,因为拆分可能会引入一些额外通信,降低总体吞吐。所以你得做些分析,决定哪些算子被拆分,现在大部分框架都不支持这种全自动化策略,要么是半自动或纯手工,要么是针对某种模型把它的拆分方案写死。所以只能造轮子解决这个事 - 流水并行 不切算子,而是将不同的Layer切分到不同的Device上,就可以形成Pipeline方案,GPipe就是这样一种方案,提出了将一个batch拆分成若干个micro-batch,依次推入到Pipeline系统中,即可消除Bubble time。和算子拆分类似,全自动化方案工作量不小,比如Pipeline怎么切,才能让通信量小,计算还能均匀,这需要一定的算法和工程量
我们的模型可能会很大,或者数据量会很大。仅仅用一块GPU卡可能连模型都放不下,或者batch size只能设置的很小,但是我们知道有些情况下大的batch size往往会提供更好的效果。
- 假设我们只有一个GPU,我们的模型一次只能输入batch size为8的数据,那么我们怎么样实现batch size为32的更新呢?那就需要时间换空间了,即我们训练32/8=4步才去更新模型,也就是所谓的梯度累积。
- Gradient-Checkpointing, 那么如果你的GPU连batch size为1都跑不了怎么办?我们在训练深度学习模型的时候,需要先做前向传播,然后将中间得到的激活值存储在内存中,然后反向传播的时候再根据loss和激活值计算梯度。也就是说内存消耗其实跟模型的层数线性相关。那么怎么减少这个内存消耗呢?最简单的想法就是我不存这些中间信息,计算梯度的时候,到了某一层我重新计算它的激活值,这个方法虽然可以让内存消耗是个常量,但是运行时间会是
O(n^2)
,这是没法接受的。那么就有一个折中的办法,我不存全部的中间数据,只存部分,那么我们在计算梯度的时候不需要从头计算了,只需要从最近的checkpoint点计算就好。 - 我们训练模型一般都是用单精度(FP32)的参数,但是其实我们还使用半精度(FP16)。半精度可以降低内存消耗,从而训练更大的模型或者使用更大的batch size;同时运算时间受内存和算术带宽的限制,在有些gpu(Tensor cores)上可以为半精度提供更大的算术带宽,从而提高训练效率,减少inference用时。