先简单和大家介绍一下腾讯内部的业务及相关组织架构的现状,有助于帮助大家理解为什么我们会基于后面的架构来设计整套方案。
下图的应用大多数人经常会用到,比如微信、腾讯视频、游戏等等APP,其背后承载的技术也不尽相同,涉及了NLP、计算机视觉、强化学习、语音等不同的AI技术。
比如我们玩的《王者荣耀》或者下围棋,背后所对应的就是用强化学习训练出来的一个机器人,玩游戏没有队友陪同时,机器人可以满足我们对战合作等游戏需求。
不同的业务部门,APP对外需求也不同,均会针对自己的业务场景做一些AI平台的定制。我们做的是底层算力,给业务部门提供服务时,在考虑整体资源利用率的情况下,也要为各业务便捷地去做些定制服务,这就是腾讯内部的一个多租户的现状。
接下来,介绍一下腾讯内部业务的一些特点以及大概规模。
目前的环境是基于开源项目TKEStack,TKEStack是腾讯公有云TKE的开源版本,是一个开源的容器云平台解决方案,用的KubernetesV1.14版本,操作系统是腾讯自研的Linux操作系统,已经为GPU或者腾讯内部业务做了一层性能的调优和bugfix。
GPU节点是NVIDIA的V100、P100,个别会有一些M40的机器。网络联通使用100G的RoCE,既能够提供以太网的支持,又能够提供RDMA的网络协议支持,对于用户去做一些多机通信的优化有事半功倍的效果,也从硬件层面保证了整体的使用效率。
接下来介绍一下我们是怎么完善、开发以及设计这一整套流程的。
这里先介绍一下关于Kubeflow以及Kubeflow里面一些主要的组件,帮助大家理解其中的一些具体业务,或者设计。
Kubeflow是什么呢?
Kubeflow自从2017年底发布,目前逐渐成为主流的在Kubernetes上面跑机器学习、深度学习等训练或者推理任务的主要工具。
Kubeflow包含非常多的组件,比较多的像Operator ,或者像自动调参的工具。
先介绍一下Operator,它是Kubernetes中的一种概念,主要是用来打包、部署以及管理用户的任务。但在Kubeflow里面,Operator主要用来管理机器学习或者深度学习里面的任务。
那么它能帮用户做什么呢?
比如在多机的任务下面,如何去管理和维护一个任务的多个节点,以及之前通信是怎么做的,怎么能够帮助用户管理整个Pod以及任务的生命周期,比如某一个Pod失败了,整个任务是终止还是有一些其他的容错办法,这个都是由Operator来完成。
目前主流的Operator有几种,会对应每一种框架。比如用的最多的TF-Operator,主要对应的是tensorflow,MPI-Operator主要对应Horovod,Pytorch和Caffe2的Operator,它们针对各自的框架都有一些定制的场景。
这里着重介绍两种Operator以及我们对它做的一些重点优化,也是我们用的比较多的两个Operator以及框架任务。
MPI-Operator是为了MPI的任务或者Horovod的任务提供一个多机的管理工作。它有V1 α1、V1 α2以及V1的版本,V1正在大规模开发中,所以我们推荐用V1 α2 。未来V1 release之后,可以推荐使用。
在这种版本下面,它有两个概念:一个Launcher,另一个是Worker。Launcher相当于一个启动器的角色,它会等Worker都就位之后,去启动MPI的任务。但是在这里,Launcher本身就会占用一些CPU的资源,其实我们是可以将它合并到一起的,也确实做了一些改进并提交到社区,去掉一些无用的CPU的Pod,让某一个GPU节点起到这种角色。
此外,为了提升Pod的创建速度,以及提升Job的启动速度,我们也做了一些优化。
比如Launcher在等待其他Worker就位的时候,是从其他Shell脚本的版本变成了多线程的工具,可以极大提升整体的等待时间。
除此之外,比如再增加一个额外的init container去下载用户的docker镜像,这样来做docker镜像类似于并行加载这种方式。
我们推荐是直接参照Kubeflow的MPI Operator的GitHub去查看更多的详情,大体的流程也在上图右侧展示出来,主要是将MPI Job换成对应的Kubernetes可以识别的一些资源,比如使用CRD、使用Configmap以及RBAC控制权限等等。
除了MPI-Operator,还有另外一个用得更多的是TF-Operator,TF-Operator主要就是帮助用户启动多机的一个PS-Worker这种架构的情况下的一个多机的任务,因为这个集群全部是GPU相关的集群,所以我们推荐用户是将PS和Worker放在一个Pod里面启动,减少一些通信的成本。
除此之外,我们还做了一些额外的优化。比如像就近部署的方案以及脱库调度的方案。为了加快image加载的时间,也是用IfNotPresent这种方式去加快整个的速度。
目前TF-Operator还是比较完善的,其他各家公司也都会有比较多的投入。
介绍完Kubeflow目前的一些Operator,言归正传,今天的主题也是多租户的场景下面使用Kubeflow构建一个训练平台。
先来看一下目前Kubeflow的多租户平台是如何构建的。
目前,在这个平台有两种方式,一种是用原生的Kubernetes RBAC,就是Role based access control去做权限的管控。另外一种是使用Istio,Istio这种方式更偏向于推理场景。通过这种方式对用户的访问进行一个控制。
可以简单来看这张图,当我们为用户提供一个集群,一般有两种方式。一种是通过命令行的方式使用,另外一种就是提供Web端,Web端通过Istio的gateway接入整个集群,然后Istio的RBAC去做权限管控、去做流量的分发、去维护整个集群的权限。
另外一点,如果是客户端,除了集成的客户端,另外还引入Kubernetes的RBAC去做允许或禁止,帮助用户解决权限管控的问题。
但它有一个小的问题,就是所有的用户都会共用所有这些资源,像Operator、Controller,用户只能使用定义好的资源,比如我们设计好一种Job的类型或者Operator的类型,那么用户就必须这么使用。
但是对于多个事业群,大家会有一些自定义的需求或者定制的Operator,在这种场景下,就会有些捉襟见肘。为此,我们也引入了一些其他的方式改善这种需求。
现在我们做的多租户的Kubeflow训练平台,首先在资源层将GPU资源聚集到一个或多个集群,在此GPU集群之上提供多个用户的集群,也是K8s的集群。用户可以通过Virtual Kubelet,然后接入到底层的算力集群,分成两层:
在用户的集群,这些租户的管理员去管理或者去创建,去自己定义一些Operator或者Controller,用户接入的是这些租户的K8s原生的KPI;
在底层算力集群 ,我们统一集中调度,提升资源利用率。
当有任务下发的时候,通过Virtual Kubelet,或者用Kubernetes实现的Virtual Node转发到算力集群,在底层算力层面而言,可以通过Namespace隔离不同的租户以及不同租户的任务,将一些特定需求的节点划分形成一些小的资源池,更多就是通过Namespace进行隔离。
但是对于上层用户而言,他们有一些自定义的权限,也可以自己开发一些自己的组件,相当于将权限分离开。
我们这里实现的Virtual node,相当于用Virtual Kubelet实现的,Virtual Kubelet相当于Kubernetes上面一个开源的Kubernetes kubelet实现,最开始是由微软的一个团队开发并维护,后捐献给了CNCF成为sandbox的项目。Virtual Kubelet后面是可以接入像ACI、AWS forgate、IOT等等可扩展的组件或者服务。比如OpenStack也是可以接入的。
而我们这里其实相当于接入了一个新的功能,它是一个比较简单的,主要定义为用户有一个Pod下发请求或者有一个其他资源下发请求,我们直接转发到底层的一个K8s,同时Virtual Kubelet也会监控底层它关注的资源状态,并将状态汇报给上层,它相当于一个桥梁的作用,承上启下去同步整体的状态。
这张图就是一个比较完善的用户的集群或者用户的一个架构图。在左侧有用户的集群,对应的是通过Virtual Kubelet连接到底层不同的算力集群,这些算力集群都是有GPU资源的算力集群,也是K8s的集群。
如果用户比较简单,可以直接接入我们推荐的组件,形成一个整体的简单管控策略。比如用户想跑一些任务,以及有自己的controller去定义整个的规则,其实可以接入像MPI-Operator、TF-Operator等operator资源。
这里也多提一句,当用户下发一个MPI的Job,MPI-Operator就会转换成像Configmap、Secret,像RollBinding ,以及像多个Pod这种资源。
当这些转换发生完成之后,Pod经过调度器到具体的某一个Virtual Kubelet,Virtual Kubelet发现资源到自己的节点后,会将其转发到具体的某一个Kubernetes集群,并关注这些Pod或者其他资源的状态,形成整个的一个转发和传递的效果。
对于上层集群的管理员,不再需要关注底层集群的情况以及权限管控等,只要关注上一层即可。底层的管理员需要关注更多整体资源的使用情况,形成上下层的分离。
介绍完上下层分离以及怎么做多租户这种场景,转到我们之前提到的集群全部资源归集到一起,主要目标是为了提升资源的利用率。当整个资源打通以后,怎么提升资源利用率?而且通过前面介绍的多租户的机制,让用户也不用感知到。
在深度学习或者机器学习场景下,大部分任务都需要批量调度功能,也就是需要保证多个Pod同时地调度。它主要算法就是all or nothing的算法,保证整个资源要么可以调度,要么就不要调度,如果无法调度那么就排队,保证整个资源不会被饿死。这是一个比较常见的需求。
这里面主要是用volcano去做gang-scheduling。
除此之外,我们也引入了一个优先级的任务。顾名思义,就是我们给每个用户或者每个租户都开放高优先级的任务,他们可以在一个固定时间之内被调度。当整个集群的利用率不太高的时候或者分配还有一些空间的时候,就可以开发一些低优的任务给用户,用户可以提交整个的弹性任务或者叫低优的任务。
这种任务下发下来之后,就可以以低优的任务占据这些空闲资源,当高优任务下发的时候,就可以抢占这些低优资源,保证整个资源池是最满的状态。
最后一点是一些优化的策略,如基于网络拓扑架构或者GPU的拓扑架构的拓扑调度或者使用binpack,减少底层集群的碎片,保证更多的资源是可以尽快被调度的。
其他的优化,如提升MPIJob的一个启动速度,能够尽快地将任务下发下去,将底层空闲的算力资源变得越来越少。
除此之外,众所周知,我们跑GPU任务一般都会用nvidia—docker2版本,我们分析了一下,其实nvidia—docker2对应的版本,它的Pod启动速度相比于一般的runC的这种启动速度还是慢不少,分析原因,主要集中在每次启动的Pod或者container,它都会去查询CUDA或者Nvidia驱动的版本对应的信息,然后供Nvidia Container的CLI Prehook去操作。
但是在我们场景里面,其实不太能用得上,因为我们是一个私有云的平台,私有云的平台里面不涉及特别多的经常更换硬件或者驱动版本经常变化的场景。所以我们可以简化。
怎么简化呢?最简单一种办法,就相当于将CUDA、INVIDA这种驱动信息全部缓存下来,然后保存到某一个固定的文件,当机器重启或者将驱动变化,CUDA的版本发生变化的时候,才会重新触发这个动作,获取更新的信息缓存下来,这样可以大大减少每一次Pod创建的时候都会获取这些信息的次数。
这个信息的时间,如图中所示大约在几百毫秒。
经过这样一个优化,我们是可以将整个Pod的启动时间提升大概30%到40%的时间。对于用户体验还是非常友好的。
当然这个只能说做几百毫秒的优化,像深度学习的场景,CUDA的版本、Nvidia的版本,Nvidia驱动本身就比较大,所以如何能够优化这个docker image的加载,或者能够减少它的镜像拉取,做一些预分发、预部署,这个也是我们非常关注的一点。除了一些调度层面可以做的事情,其实业界也有一个比较流行的方式就是做一种延迟加载。
docker image是分多层的,以及它分metadata。调查发现,基本上大多数的镜像里面的内容一般不会被用上,能用上的也就10到20%。
我们做一些延迟加载,当它在用的时候才去加载,当然这个也是一个比较前沿或者时间性质的功能,我们也在重度参与。后面有一些进展,大家可以持续关注我们,会不断和大家来分享。
基于kubeflow目前的架构或者一些现有的组件支持多租户以及一些后面优化的策略,为了提升整个用户体验,我们其实还是有很多工作去要去做。比如现在比较流行的弹性训练任务,像基于kubeflow、基于horovod本身的可以动态伸缩,去占用更多的资源,能够减少用户的训练时间,都是非常关键的。
另外一点,MPI-Operator本身V1.0的版本我们也在重点参与,希望它能尽快被release。