接上一章:
分布式训练在机器学习中是一个不可缺少的部分,模型训练大都伴随着大量的数据需要进行计算,单机的资源往往是有限的,利用多机资源分布式进模型训练是加速模型训练的一个重要的手段
自定义开发环境-秒级创建Jupyter开发环境提升工作效率

(Jupyter界面图)
为什么需要Jupter+自定义容器环境:
1: 提供易用的IDE工具,辅助开发,提高开发效率(在很多的开发测试环境依赖于linux等机器环境,很多情况下的同学要么通过linux无图形化界面进行开发,或者本地开发,然后再上传代码、配置等,这样的开发效率非常低下)
2: 隔离用户空间,减少开发过程中相互影响
3: 隔离环境,满足不同开发需求对不同环境的要求(如一些库对GCC版本要求较低,有些库要求高,他们之间相互影响)
4: 对环境做镜像,达到秒级创建新环境,相比起在实体机器上重建环境少则一天多则可能一周都搞不定一个复杂的环境(比如机器迁移、故障等等环境重建,比如工作交接、新同学入职开发环境搭建,只要一键化就可以做到)
我们通过docker将用户的开发环境进行隔离,针对于不同种类型的环境构建出一个基于jupter基础镜象的开发镜像环境,这样使用者就可以通过平台选择一个自已相适应用开发镜像,一键创建出一个容器环境,用户只需要通过WEB页面就可以打开Jupyter进行代码开发了。这样即能做到环境的隔离,也能做到用户之间开发空间的隔离,每个用户都可以创建自已的jupyter容器,大家开发上互不干扰,各种环境的依赖之间也是互不干扰,如果机器迁移或者机房迁移,用户只需要一键重建环境就可以了

对于前台用户只需选择镜像、填写机器要求,比如CPU\GPU\内存\磁盘信息后就能创建一个环境,对于后台来说要解决的问题如下:

1: 根据jupter的镜像,创建一个pod
2: 构建jupyter启动脚本,在容器启动时将jupyter的进程启动起来
3: pod启动后用户需要能够访问到这个pod中的jupyter,所以需要构建一套网络访问的服务或者叫CRD,最后将让upyter的访问地址能够在办公网络进行访问
首先我们来看一下整个机器学习平台的网络访问结构

从外部网络需要访问到k8s内部的服务需要通过外部的负载均衡负载到k8s的一些结点上,这些结点绑定着一个静态的端口(Nodeport),通过这个端口能将请求通过kube-proxy转发到对应的istio-ingressgateway最后由istio配置的网关及VirtualService将流量转到对应的service上,最终通过service后就通访问到容器中Jupter的服务了

Gateway的配置是静态的,平台需为了保证每个用户创建的每个jupyter pod都能独立进行访问,所以需要针对每个jupyter pod 动态创建service\VirtualService进行绑定,最终达到可以动态创建jupter的效果。
分布式存储-分布式训练的基石
在分布式训练过程中,训练的容器次源是由K8S进行调度分配置,工作容器被分布在集群中的哪一台机器使用者是预先不知道的,这样我们就需要有一种介质来存储训练过程中所需要的代码、配置、数据等等,以便于在训练过程中任何一个容器都可以访问它。

在系统框架中已经介绍过了,平台采用的是ceph做为平台的分布式存储,同时与rook进行集成部署在k8s上,ceph包含了包括对象存储、块设备、文件系统,显然这三种模式中文件系统存储便适合平台的使用方式,主要有如下几个原因:
1: ceph文件系统能通过系统内核的方式进行挂载,使用者能像使用本地文件系统一样访问分布式文件系统,对于使用者来说无感知,使用成本几乎为0,对于那种以前都是单机模式开发的程序迁移成本会大大降低
2: 文件通过操作系统内核挂载,后期如果更换文件系统对于整个平台及平台的用户是无感知的,系统扩展方便
平台按分类在分布式文件目录下创建子目录,同时按分类创建静态存储卷,比如用户空间存储目录:/xxx/xx2/user,会在k8s上创建一个PV及PVC,在woker容器创建时将容器下的目录mount到这个pvc上

mount的目录主要分成几种模式,一种是用户级别的目录,这个目录下的文件只有用户 自已可以访问,还有一种目录是项目组共享目录,这个目录是同属一个项目组下的用户才可以访问,另一种目录是全局共享目录,这个目如下的数据是所有用户都可以访问,每个运行的任务都会规属到个人、项目组,这样每个运行的task的容器在创建时都会将当前task所归属的项目组、用户所属的目录挂载到运行容器中去

解决了存储的问题后,我们就能在任何容器中像访问本地文件一样访问分布式文件系统上相同文件了,这样我们写一份代码,我们不用关心容器在创建在哪台实体机器上都可以进行访问了。
分布式训练-为百G以上级别数据进行模型训练护航
分布式训练基础知识介绍
本文所说的训练,指的是利用训练数据通过计算梯度下降的方式迭代地去优化神经网络参数,并最终输出网络模型的过程。在单次模型训练迭代中,会有如下操作:
首先利用数据对模型进行前向的计算。所谓的前向计算,就是将模型上一层的输出作为下一层的输入,并计算下一层的输出,从输入层一直算到输出层为止。其次会根据目标函数,我们将反向计算模型中每个参数的导数,并且结合学习率来更新模型的参数。
而并行梯度下降的基本思想便是:多个处理器分别利用自己的数据来计算梯度,最后通过聚合或其他方式来实现并行计算梯度下降以加速模型训练过程。比如两个处理器分别处理一半数据计算梯度 g_1, g_2,然后把两个梯度结果进行聚合更新,这样就实现了并行梯度下降。
训练并行机制
模型训练并行机制有三种,但是我们最常见的方式有2种:数据并行与模型并行,其中目前工业界中基本的训练框架实现都是基于数据并行的方式。
分布式训练最大的优势就是可以利用集群多机的资源,并行的进行计算,每一台机器承载着整个计算的一部分,也就是说一份大体量的工作由一堆人来做,每个人同时做其中的一小块事情,目前最常见的并行计算方式有2种:

模型并行:集运行的集群中,每台机器上计算着相同的数据,但是每台机器上运行模型中的不同计算部分
数据并行:所有机器上的模型是相同的,但是是需要训练的数据按机器进行拆分,每台机器计算数据中的一部分,计算完后再将结果进行合并
目前工业界最主流运用最广泛的模式是数据并行计算
模型并行计算的实现架构
Parameter server 模式

PS架构下所有的参数信息都存放在参数服务器中,参数服务(PS)在集群中可以是多台,Worker机器为工作结点,Worker结点首先从PS上获取参数信息,然后根据训练数据计算梯度值,计算完成后将计算的梯度更新到PS上,PS获取Worker过来的梯度值后对梯度求平均,最后返回给到Worker。
Allreduce 模式

AllReduce模式是所有的机器上都具有相同的模型参数信息,每台机器计算一部分数据得到一个梯度值,然后执行allreduce操作使得所有node结点都得到其它结点上的所有梯度值,最终更新本地的梯度值,AllReduce每轮迭代都需要同步所有参数,对于网络来说是一个大的冲击,后来在2017年百度在tensorflow 上实现了基于 Ring-Allreduce 的深度学习分布式训练Rring-AllReduce,大大减少了网络的压力

参数服务器适合的是高纬稀疏模型训练,它利用的是维度稀疏的特点,每次 pull or push 只更新有效的值。但是深度学习模型是典型的dense场景,embedding做的就是把稀疏变成稠密。所以这种 pull or push 的不太适合。而 网络通信上更优化的 all-reduce 适合中等规模的深度学习。又比如由于推荐搜索领域模型的 Embedding 层规模庞大以及训练数据样本长度不固定等原因,导致容易出现显存不足和卡间同步时间耗费等问题,所以 all-reduce 架构很少被用于搜索推荐领域
分布式模型训练

上面介绍完分布式训练的一些基础知识后,我们来看平台是如何与这些框架结合进行模型训练,在机器学习平台上主要选取如下2种模式来支持深度学习模型的分布式训练:
基于RingAllReduce分布式训练
Horovod主要是基于RingAllReduce的架构来进行分布式训练,Horovod 支持TF/Pytorch/MXNet等训练框架进行模型训练,在图像、音视频、文本等等分布式训练场景下使用非常广泛,对原框架(TF/Pytorch等等)的入侵很小,使用起来简单方便,对原代码做很小的改动就能进行分布式训练。

选择了使Horovod进行训练后需要有一套机制来组成RingAllreduce通讯结构,可以看下图,这时我们需要有一套机制去创建容器,同时让他们组成一个环境环形的通讯结构

我们首先来看一下Horovod的运行示例,如果是在实体机上执行的话只需要设置分布式下多台机器的SSH免登录,然后在其中一台机器上执行下面的代码,整个分布式就能正常的运行起来了

但是在K8S上我们的容器是动态创建的,IP地址是动态变化的,也不好去动态设置这几台容器间的SSH(我个人是没有试过如果在容器上设置多台机器的SSH免登录,同时能在多台机器间去通过SSH执行脚本),执行完成或者异常后还需要对这一批容器进行回收等等操作,这时我们就需要一套这样的机制来实现上面说的这些功能,这时KubeFlow的MPI-Operator就能派上用场了
MPI-Operator

MPI-Operator根据用户定义的CRD文件生成一个ConfigMap

我们可以看到这个ConfigMap里边主要是生成了三部分,我们现在主要关注的是hostfile和kubexec.sh,MPI-Operator会创建2种角色的容器: launcher、worker,这launcher在所有的worker容器启动后调用horovodrun命令,在上面官方广档中默认是通过SSH方式向集群中的其它容器发出执行远程命令,在launcher中MPI-Operator会设置launcher的环境变量OMPI_MCA_plm_rsh_agent

这样最终在执行过程中会在launcher执行kubeexec.sh向worker发起命令执行用户脚本,同时MPI-Operator还管理运行过程当中成功与异常时容器的退出等等,这样在机器学习平台侧则需要构建MPI-Operator的CRD:
1: 构建文件挂载信息,将分布式存储挂载到Horovod的容器中去,以保证在任何容器中能访问到训练脚本代码和配置、训练数据等等
2:构建资源调度规则,如结点分配规则信息(如如果有申请到GPU的资源,那则设置worker容器都分布到GPU的结点上去,如果只需要CPU资源则设置worker分配到CPU的结点上去,同时会按照平台的资源隔离策略,如资源有按照分组进行隔离测将worker分布到当前分组所在的资源结点上去运行)
3:设置pod之间的亲和策略,比如是GPU机器的话尽量将容器分布到相同的结点上,减少中间的一些网络损耗

平台要解决的问题是通过上面一个简单的配置,就能实现复杂的分布式训练过程

开始提交训练任务运行分布式任务
CPU任务执行

GPU任务执行

提交后的效果如上,平台会设置将launcher 尽量调度到cpu机器,如果没有CPU机器则调度到GPU的机器,同时只分配到CPU的资源,
基于PS架构的分布式模型训练
虽然基于RingAllReduce的模式在训练的性能方面会比PS架构要好很多,但是上面我也有提到过在推荐、广告、搜索等这种超大规模场景及需要做在线实时训练场景下PS架构是很适应的,所以在机器学习平台对这种分布式训练场景的支持是非常有必要的。

Multi-machine multi-GPU distributed parallel training.

PS架构下所有的训练参数信息保存在参数服务上,参数服务是集群进行部署的,这样的话在超大规模参数下单机的内存资源是无法满足训练的要求的,特别像是在一些广告场景中,大量的embedding造成参数规模很大。
PS架构的实现是基于Kubeflow的TF Operator来实现的,在TF的PS训练模式下

从上面的图我们可以看到整个训练过程中会创建如下几种角色:
PS: 所有的参数存储的地方
Worker: 根据训练参数计算出梯度值,然后把梯度传递给PS之后拿到了PS返回的最新参数并更新到本地并进行多轮的迭代计算
Chief:一般来说可以用来单独保存模型、代码执行点(比如执行构建Graph)、日志记录等等。比如下下部分代码只会在Chief上运行

这样我们就可以推断出TF Operator需要做的事情如下:
1:创建PS\WORKER\CHIEF等角色的容器
2:根据这些角色创建的POD的IP信息创建TF_CONFIG
3:在创建容器时候设置TF_CONFIG为容器的环境变量
4:在容器启动时执行用户脚本
TF-OPRATOR 的整个处理流程并不是太复杂,对于机器学习平台来说就是创建TF Operator 对应的CRD:
1: 构建文件挂载信息,将分布式存储挂载到Horovod的容器中去,以保证在任何容器中能访问到训练脚本代码和配置、训练数据等等
2:设置PS及chief容器的信息,这2种容器只需要分配到CPU的机器上即可,对于worker容器根据用户设置,如果需要cpu则设置cpu资源信息,如果需要GPU则设置GPU资源需求
4:设置Node结点亲和度信息,如当前项目组下有资源,测将当前任务的容器设置要调到到当前分组的资源结点下
3:设置pod之间的亲和策略
对于使用者来说只需要通过如下简单配置加上代码中的训练脚本配合就能就行分布式的训练了,对于资源的创建、回收,网络的管理都交由平台来管理,用户只需要关注自已的训练逻辑就可以了。

资源调度
在分布式计算下,我们需要申请大批量的机器进行训练,但是在大部的场景情况,无论是MPI+Horovod 或者是TF PS架构下都是需要等容器创建完后整个训练过程才会开始

如上图,有一部分的worker申请到了机器了,但是另外几个worker申请不到机器,还一直处于Pending状态


这里我们查看luncher的状态还一直处理Init状态,等待所有的worker Ok了后才开始作业,这些如果一直申请不到机器,已经起来的worker的资源就一直占用并浪费掉了,特别是GPU的资源,所以我们就需要一套资源调度框架来处理这些事情,原官方有kube-batch但是kube-batch已经很多年不更新了,对于目前很多的计算框架或者一些组件会有不兼容,volcano是目录最火而且比较完善的调度框架

Volcano由scheduler、controllermanager、admission和vcctl组成:
Scheduler Volcano scheduler通过一系列的action和plugin调度Job,并为它找到一个最适合的节点。与Kubernetes default-scheduler相比,Volcano与众不同的 地方是它支持针对Job的多种调度算法。
Controllermanager Volcano controllermanager管理CRD资源的生命周期。它主要由Queue ControllerManager、 PodGroupControllerManager、 VCJob ControllerManager构成。
Admission Volcano admission负责对CRD API资源进行校验。
Vcctl Volcano vcctl是Volcano的命令行客户端工具。
未完待续




