DockOne微信分享(二四一):Volcano介绍及其在深度学习场景下的应用


【编者的话】随着Kubernetes技术成熟度的逐渐加强和企业使用率的不断提升,Kubernetes已经成为新一代人工智能(AI)、大数据处理和分布式存储的基础,越来越多的学习、科研和商业计算开始拥抱Kubernetes。作为一款通用的容器编排系统,Kubernetes在计算任务的批量管理和调度方面仍然存在一些不足。为了弥补Kubernetes在深度学习、大数据计算场景下的不足,Volcano社区提出构建于Kubernetes之上的增强型高性能计算任务批量处理系统Volcano。Volcano在原有Kubernetes能力基础上对计算任务的批量创建及生命周期管理、批量调度等方面做了增强。本次分享主要针对Volcano的架构和核心算法进行介绍,并介绍了主流深度学习框架与Volcano结合的案例。详细介绍了如何使用Volcano系统运行深度学习计算任务,最终分析使用Volcano运行计算任务的优势。希望通过本次分享,给大家提供一种新的运行深度学习计算任务的方式。

计算任务与Kubernetes的结合

深度学习(DL)对于商业和科研的重要性众所周知,为资源敏感型的深度学习任务提供计算平台是每个商业和科研机构都需要解决的挑战。计算资源上云成为解决上述问题的首要选择。作为一款通用的容器编排管理平台,Kubernetes满足深度学习计算任务的上云需求,使深度学习任务可以借助云原生应用的灵活性和架构快速构建和拓展。

Kubernetes是一个平台无关的容器管理工具,自Kubernetes诞生以来,它在云原生环境中的生态不断发展壮大并逐渐走向成熟。随着Kubernetes成熟度的逐渐加深和企业采用率的不断提升,Kubernetes已经成为云原生环境中新一代人工智能(AI)、深度学习(DL)、大数据处理和分布式存储的基础。越来越多的学习、科研和商业计算开始拥抱Kubernetes。

搭建基于Kubernetes的深度学习计算平台

深度学习大致将涉及数据获取和处理,模型训练和演进,模型部署,模型评测四个阶段。一个企业开始涉猎深度学习之初,因团队缺乏深度学习的相关经验和资源,通常,团队人员选择手工逐步完成上述步骤,以期快速满足项目发展期的即时需求。然而,不管深度学习专家具有多么专业的领域内知识,没有基础设施平台的支撑,项目规模的扩大都变得异常困难。相较于作业的本地运行,生产环境的大规模运行,对产品的快速复制和迭代升级要求更高。生产环境对数据的可靠性,模型训练的可复现,训练模型的扩展性,自动化部署以及运维的可靠性有更高的要求。从而,当企业发展到一定的规模,企业将希望构建稳定的科学计算平台,从而让上层业务人员能够专注于算法优化和业务的发展。
1.png

通常,深度学习平台的搭建,将遇到诸多挑战,主要体现在以下方面:数据管理和自动化,资源的有效利用和屏蔽底层技术的复杂性。

在探索深度学习的过程中,从业人员将不可避免的花费大量的时间,获取和转换建立模型需要的数据,随着组织规模的壮大,数据转换的自动化对于模型构建的效率提升至关重要。数据处理过程中还将产生新的数据,这些数据将不单单应用于本次训练,还可能应用于后续的推理过程。然而,这些新生成的数据并不需要回传给数据源,而是希望能够放置到新的存储空间。这需要深度学习平台提供可扩展的存储系统。灵活,可扩展且安全的数据存储系统将极大的促进数据管理能力的提升。

深度学习是资源密集型应用,并且资源使用量波动较大,存在波峰和波谷。在应用开始运行的时候,快速获取到计算资源,在应用运行结束后,回收不使用的计算资源对于资源利用率的提升相当重要。数据处理,模型训练和推理所使用的计算资源类型和资源使用时长均有所不同,这需要计算平台能够提供弹性的资源供应机制。深度学习任务需要占用大量计算资源,不可能为每个用户单独构建计算资源,计算平台应能保证资源使用的多租性,允许多个用户同时使用计算资源池,而不应出现资源被少数用户独占的现象。

深度学习的从业人员专注于模型构建和产品研发,忽略基础设施的架构对于深度学习的发展非常重要。深度学习构建在不断发展进步的复杂技术栈上,其中包括TensorFlow等深度学习计算框架,Spark数据处理引擎和CUDA等底层驱动程序。手动管理这些依赖将消耗大量的资源。
2.png

基于上述目标和挑战,深度学习和其他AI计算选择使用容器和Kubernetes来管理和构建深度学习平台。
3.png

容器镜像提供了标准化的可执行程序包,提供运行中依赖的资源和信息,包括代码、库、工具和配置文件。与虚拟机相比,容器更加的轻量化,并易于移植。但是单个容器并不能解决复杂的深度学习方案,实际生产中,将需要使用多个容器,并且,多个容器之间存在数据和网络的交互,这需要一个平台有效的管理容器并实现容器与外部的交互,从而引入Kubernetes。Kubernetes可以提供完整的容器生命周期管理功能,并具有高可靠性和扩展性。容器和Kubernetes可以解决上述机器学习平台构建中的挑战。
4.png

Kubernetes可以提供灵活、可扩展的存储系统。Kubernetes提供挂载卷机制,将存储卷与应用绑定,使计算任务可以读取到数据源,并能将生成的中间数据和模型保存到存储卷所对应的硬件存储中。另外,Kubernetes负载可以共享分布式存储系统,保证数据可以在不同的模型,项目和团队之间共享和传输。
5.png

Kubernetes支持集群的弹性扩缩容,只要硬件资源充足,可以随时为集群增加或删减节点。Kubernetes支持包括CPU、Memory、GPU在内的多种资源的调度。Kubernetes支持任务配置资源申请量和资源使用上限,在为任务提供资源保障的同时,限制单个任务可以使用的集群资源。集群通过namespace将资源划分为不同的资源池,每个namespace可以配置资源配额,限制单个namespace使用的集群资源,确保集群资源使用的多租性。
6.png

容器与编程语言和平台无关,通过将任务容器化,可以保证业务免受复杂的基础技术栈的影响,确保任务的依赖和所需资源正确,使开发人员专注于自身业务的研究。
B1.jpg

Kubernetes在深度学习场景下的不足

尽管Kubernetes原生能力对深度学习运算提供了很大支持,构建于容器之上的深度学习计算任务,在Kubernetes的编排管理下可以应对传统模式下的很多挑战。但是,因为Kubernetes是一个通用容器管理平台,在特定领域的支持方面仍然存在一些不足。尤其,作为资源敏感型计算任务,深度学习计算对于资源的隔离,多租性,任务的管理和调度要求更高。然而,针对深度学习计算领域,Kubernetes在任务管理,资源管理,调度方面仍然有需要加强的地方。

Kubernetes欠缺不同功能Pod的批量管理功能。Kubernetes实现了容器的全生命周期管理,通过Deployment、StatefulSet、Job等对象实现了对Pods的管理。为了保证Kubernetes管理Pod的通用性,无一例外,上述三种传统的Pod管理对象均主要针对于具有同种配置而具有不同实例个数的Pod对象管理,在批量管理具有不同配置Pod方面并没有提供原生对象支持。在深度学习计算中,同一批次的计算任务,往往含有多个具有不同功能的Pod,同类型计算Pod的实例个数也并不唯一,这需要Kubernetes加强批量任务管理能力。以TensorFlow计算框架为例,任务中需要包含“ps”和“worker”两种不同类型的任务,其中“ps”用于执行模型相关的工作,包括模型参数的存储、分发、汇聚和更新,“worker”用于执行训练相关的工作,包括推理和梯度计算。功能不同,“ps”和“worker”的配置自然也有差异,但同时又隶属于同一批计算任务,即使是同为“worker”,其实例个数也需要可配置,kubernetes原生并没有提供可以覆盖这种场景的对象。
7.png

Kubernetes在Pod管理和批量Pod的生命周期管理方面仍需加强。Kubernetes是声明式的架构,只要在Deployment、StatefulSet或者Job对象中指明期待的Pod状态,声明式架构可以保证集群下的Pod列表维护在期待的状态,其中包括Pod的实例个数和状态。Kubernetes提供了一些Pod的生命周期管理能力。在Pod描述文件中,“restartPolicy”字段控制Pod的重启策略,可配置参数为“Always”、“OnFailure”和“Never”,指示在何种场景下,Pod需要重启。Kubernetes Job对象中“parallelism”字段指明Job下并行执行的Pod数量,“completions”字段指明Job下有多少Pod运行完成,可认为整个Job任务结束,“backoffLimit”字段指明Job下Pod失败后的重启次数。上述Pod生命周期管理能力涵盖了通用的Pod管理能力,但是在应对场景复杂的深度学习计算,仍存在不足。深度学习计算任务,希望在任务管理时,统筹考虑计算任务下的所有Pod。在深度学习计算中,不同的计算任务往往需要先形成一个计算集群,才能开始执行计算任务,当其中一些任务结束或异常时,在对这些成功或异常的任务处理的同时,还需要一并对集群下的其他计算节点做处理。对于没有错误容忍的计算任务,当其中一个Pod失败后,Pod所代表的计算节点IP发生变更,计算集群可能被破坏,计算过程将会中止。此时,需要计算集群内的所有计算节点重启,形成新的计算集群,开始新一轮的计算过程。以MXNet使用的ps-lite计算框架为例,计算集群中存在三种角色的Pod:“worker”、“server”和“scheduler”。“worker”从事梯度计算任务,“server”用于存储模型参数,“scheduler”用于监控节点运行状态并维护计算集群。当“worker”和“server”启动后,会逐个向“scheduler”注册自身的IP,“scheduler”在接收到所有“worker”和“server”的IP后,维护一个计算集群,并向各个“worker”和“server”发送通知,通知计算任务可以开始进行,集群下的“worker”和“server”通过计算集群知道彼此在集群中的位置。当其中一个“worker”或“server”失败后,Pod重启,失败Pod的IP发生变更,但是“scheduler”仍然维护原来的计算集群,发现其中有些计算节点不可达后,计算过程将被迫中止。出现这种情况后,需要集群下的所有计算节点都重启,以形成新的计算集群,并重新开始计算任务。在深度学习任务的执行中,还存在很多需要满足的生命周期管理场景,又比如对于一些计算任务,当其中的一些任务结束后,就表明整个计算任务结束,并需要开始清理部分计算节点。对于MPI作业,当作为分发任务的“master”节点成功并安全退出后,就表明整个计算任务成功,计算节点就可以被清理掉。原生Kubernetes并没有满足以上所述深度学习场景中复杂的Pod生命周期管理需求。
8.png

Kubernetes不满足Pod的批量调度需求。Kubernetes提供了丰富的调度策略,可以满足CPU、Memory、GPU等多种资源的调度能力,调度过程中可以充分考虑集群资源现状和任务的资源请求量,以及任务和任务之间的亲和性和反亲和性要求,将任务调度到最合适的集群节点上。Kubernetes默认调度器支持多种调度插件,并支持用户自定义调度算法,可以满足大多数场景的调度需求,但是在批量调度方面仍然有需要加强的地方。在深度学习计算中,往往需要“All or nothing”的调度策略,对于一批计算任务,只有当任务都被调度下去,并处于运行中时,计算过程才能开始。当只有其中一部分任务调度下去,虽然这些Pod处于运行中,但是,计算集群的条件并没有得到满足,计算任务仍无法开始。而这些已经调度下去的Pod将占用集群资源,导致集群资源的浪费。因此,在深度学习计算中,往往希望同一批计算任务要么全部调度下去,要么都不要调度下去,以避免部分任务调度,但并没有形成计算集群而无法工作,浪费集群资源。对于深度学习场景,不同的计算任务之间通常有数据和网络的交互,将这些具有网络和数据交互需求的Pod调度到网络临近的节点上,将非常明显的提升计算效率。比如,对于Tensorflow应用,“ps”和“worker”之间存在模型参数的交互,对比将“ps”和“worker”调度到同一台节点上和调度到两台节点上,前者计算效率更高。对于含有GPU资源的调度场景,调度过程中,则需要考虑GPU的拓扑结构,不同拓扑结构下的GPU通信差异非常明显,实现GPU亲和性调度对于计算资源的效率提升同样非常重要。
9.png

Kubernetes在集群资源细粒度分配和租户隔离方面仍有提升空间。Kubernetes通过namespace划分集群资源,并使用namespace配额限制namespace下Pod使用集群资源数量,从而保证资源使用的多租性。Namespace的配额是一个硬性的限定,当集群下资源已经按照约定俗成的方式,通过namespace划分给不同的用户,那么这些用户所能使用的集群资源上限也被限定了。当集群下仅有一个用户(用户a)在执行计算任务,用户a使用集群资源的数量达到namespace配额后,该用户便无法再继续下发计算任务,即便是当前集群下还存在空闲的集群资源。Kubernetes在资源划分上面粒度较大,不能满足资源使用的灵活性。假如在上述场景下,该用户被允许使用分配给其他用户namespace下的资源,并往用户b所分配namespace下发任务,并占用了用户b namespace下的部分资源。当用户b开始投放任务时,会因为用户a投放的任务占用了部分集群资源配额,资源使用需求无法得到满足。使用Kubernetes时,在希望兼顾资源使用灵活性的时候,Kubernetes无法保证用户资源配额一定得到满足。
10.png

Kubernetes不支持计算集群参数自动配置。很多深度学习应用在构建计算集群的过程中,都需要用户为各计算节点做依赖配置,以方便计算节点之间识别和通信。比如MPI应用,需要用户在各个计算节点之间设置ssh免密登录,需要通过—host配置集群下所有节点信息。再比如TensorFlow应用,每个计算任务中都需要通过TF_CONFIG指定计算集群信息,以及当前计算节点在计算集群中的序号。使用Kubernetes平台运行上述应用,用户需要考虑如何处理这些计算集群准备工作。
B2.jpg

Volcano基于Kubernetes的batch任务管理及调度系统

Volcano是一款构建于Kubernetes之上的增强型高性能计算任务批量处理系统。作为一个面向高性能计算场景的平台,它弥补了Kubernetes在机器学习、深度学习、HPC、大数据计算等场景下的基本能力缺失,其中包括gang-schedule的调度能力、计算任务队列管理、task-topology和GPU亲和性调度。另外,Volcano在原生Kubernetes能力基础上对计算任务的批量创建及生命周期管理、fair-share、binpack调度等方面做了增强。
11.png

Volcano架构

Volcano在原生Kubernetes Job对象之上,引入一个新的CRD对象“job.batch.volcano.sh”,以下统称为“vcjob”,其中包含了对任务执行信息配置的描述和任务执行过程中生命周期的控制参数。作为一款通用的基于kubernetes的对象管理工具,volcano在支持“vcjob”及其所管理Pod的生命周期管理的同时,同样可以处理原生Kubernetes Pod对象的管理。

Volcano由三个组件组成,分别是volcano-admission,volcano-controllers,volcano-scheduler。Volcano-admission是Kubernetes的一个admission webhook,包括Mutating控制器和Validating控制器两部分,其中Validating控制器负责创建、更新“vcjob”对象之前的校验工作;Mutating控制器负责创建“vcjob”对象时修改对象的部分参数,为部分对象参数添加默认值。Volcano-controllers是针对自定义CRD对象“vcjob”、queue、podgroup和command的Kubernetes Controller控制器,用于监听和处理上述对象的创建,销毁等生命周期。其中queue是volcano中资源管理对象,podgroup是volcano中资源调度的单位,command是封装了对“vcjob”和queue进行生命周期管理的对象。Volcano-schedules负责kubernetes pod的调度。Volcano-scheduler中的调度策略均是通过插件形式注入到volcano中,以保证调度通用性。为了便于管理“vcjob”,queue,podgroup和command对象,volcano提供vcctl工具用于操作上述对象。

一个“vcjob”的处理流程大致如下:当用户通过vcctl工具或者kubectl工具创建了一个“vcjob”对象。Volcano-controllers监听到“vcjob”的创建,并开始处理“vcjob”对象,按照“vcjob”内的配置先后创建Pod,并根据“vcjob”中配置的插件为“vcjob”创建对应的依赖资源,依赖的资源可能是ConfigMap、Secret、Service等,同时在创建Pod的时候,为Pod挂载对应资源卷。Volcano-scheduler监听到集群中有新的Pod创建,开始为Pod执行调度动作,并最终为Pod选择合适的节点。最后,kubelet检测到节点上有Pod需要运行,开始运行Pod。至此,“vcjob”生命周期的处理过程结束。
12.png

Volcano Job 解析

Volcano在原生Kubernetes Job对象基础上扩展引入新的CRD对象“vcjob”。“vcjob”是一个更强大的batch job管理对象,在Kubernetes原生Job基础上做了很多加强。其中,在配置管理方面,支持多task模式,用于将同一批次计算任务中不同类型Pod划分到一个Kubernetes对象内进行管理,task是对Pod描述信息的封装。在Job生命周期管理方面,支持用户自定义Job级别和task级别生命周期管理策略,提升计算过程的自动化程度。在数据传输和共享方面,支持Job下Pod共享存储卷。支持为“vcjob”配置插件来适配不同的计算框架,降低用户使用不同框架进行计算的学习成本和减少搭建框架依赖资源的重复工作。在资源管理方面,支持用户将任务放置在不同的queue下,细化资源划分粒度,确保资源使用的多租性。

一个“vcjob”配置描述如下所示:
apiVersion: batch.volcano.sh/v1alpha1  
kind: Job  
metadata:  
name: demo-vc-job  
spec:  
# minAvailable配置,用于标识Job下Pod调度过程中的最小调度单位  
minAvailable: 3  
# schedulerName用于标明Job下Pod的调度器  
# 选择volcano调度器可享用volcano调度器在计算领域的加强功能  
# 选择default-scheduler将使用Kubernetes默认调度器调度  
schedulerName: volcano  
# 插件列表,用于在兼容不同计算框架时,配置使用公共资源  
plugins:  
ssh: []  
svc: []  
# policies定义Job级别的生命周期管理  
policies:  
  # event定义生命周期中的事件名称  
- event: TaskCompleted  
  # action定义生命周期中的action名称  
  action: CompleteJob  
# queue配置Job下Pod的queue名称,用于集群资源划分  
queue: default  
# tasks列表,支持多task模式  
tasks:  
  # replicas定义同类型计算Pod的副本数  
- replicas: 1  
  name: mpimaster  
  # policies配置,定义task级别生命周期管理  
  policies:  
    - event: TaskCompleted  
      action: CompleteJob  
  # template是Kubernetes podTemplate对象  
  template:
    ······  
- replicas: 2  
  name: mpiworker  
  template:
    ······  

Task是Kubernetes podTemplate的封装,“vcjob”支持多task模式。“vcjob”支持一个Job中配置多个具有不同配置的podTemplate,并且,每个podTemplate可以分别配置副本数。Tasks的引入,提供了对Pod的批量管理能力,满足了深度学习场景下不同角色Pod的创建和管理。

“minAvailable”字段,标识当前Job下Pod调度过程中需要保证的最小调度单位。“minAvailable”需要大于等于1并且小于等于Job下所有实例个数和。该字段与volcano-scheduler中的gang-scheduler调度策略组合,可以实现调度过程中的“All or nothing”调度策略。如果“minAvailable”的数值为2,则表明当调度器为Job下的Pod执行调度的时候,只有集群资源满足其中任何两个Pod的调取要求,调度动作才可以执行,这些满足调度要求的Pod可以被调度。否则,将跳过对该Job下Pod的调度流程,即使Job下某些Pod的调度要求得到满足,调度器也不会为这些Pod执行调度动作。

“policies”用于定义任务生命周期管理的参数,它是一个数组,支持同时定义多种不同的生命周期管理策略。“policy”由“event”和“action”两部分组成,其中“event”表明Pod上报的事件,“action”表明Pod事件期望触发的动作。目前支持定义的Pod “event”包括“PodFailed”,“PodEvicted”,“TaskCompleted”。支持配置的Job “action”包括“AbortJob”,“RestartJob”,“TerminateJob”,“CompleteJob”, “ResumeJob”。支持配置的task “action”为“RestartTask”。通过以上配置,“vcjob”支持在Pod失败、Pod被驱逐或者task完成后,触发Job停止、重启和完成等动作。“vcjob”支持在Job级别和task级别分别配置policies,其中Job级别policies配置对Job下的所有Pod生效,task级别的policies对当前task下的Pod生效。“vcjob”的生命周期管理模式契合了深度学习场景下对计算任务的生命周期管理述求。

另外,“vcjob”中的policies使用,并不局限于在“policies”中做对应配置,还可以通过volcano引入的CRD对象command触发。“command”是辅助管理其他对象生命周期的一个对象,它封装了对象target和针对target实施的policies两部分。一个command对象实体如下所示:
apiVersion: bus.volcano.sh/v1alpha1  
kind: command  
metadata:  
name: command-job  
# action定义将对target对象的处理动作  
action: RestartJob  
# target定义command的处理对象  
target:  
apiVersion: job.batch.volcano.sh/v1alpha1  
controller: true  
kind: Job  
name: demo-job  
uid: d7e5c85f-e1e0-11e9-a589-fa163e5227bc  

Volcano-controllers将负责处理针对“vcjob”和queue的command对象,解析其中的target并实施对target的action。此外,volcano vcctl工具对“vcjob”和queue的管理也将通过command施加于对应的“vcjob”对象上。

“vcjob”支持配置使用插件,兼容不同计算框架任务的运行。插件可以辅助创建除Pod资源之外的其他资源,并为Pod挂载所需物料。目前volcano支持“ssh”,“svc”,“env”三种插件。其中“ssh”插件为Job下的Pod配置ssh免密认证证书。配置使用“ssh”插件,可以实现Pods之间的免密ssh互访,这对于mpi类作业至关重要。“svc”插件为job创建headless service,并为Job下的Pod配置hostName和subDomain名称,其中Service名称、Job名称和subDomain名称一致,这样Kubernetes CoreDNS将为Job下的每个Pod映射一个podName.subDomain的域名,并指向Pod的IP地址。为每个计算节点暴露访问地址,这是Tensorflow和MXNet等计算框架计算节点形成计算集群的必要条件。“env”插件为每个Pod设置“VK_TASK_INDEX”环境变量,通过该环境变量,可以获取Pod在同类型计算任务Pod列表中的序号。这可以满足Tensorflow计算中,获取每个计算任务序号的需求。

提供priorityClassName配置,用来指明Job级别的优先级。Pod调度中有多个优先级维度,其中Job级别的priorityClassName决定了Job下Pod的整体调度顺序,在调度过程中,具有高优先级的Job下的Pod将会被优先调度。

Queue解析

Queue是一种划分集群资源的对象,用户可以通过queue分割集群资源,达到平衡任务优先级和集群资源的目的。Queue是集群级别,它可以跨越多个namespace,不同的namespace可以共用同一个queue,不同的pod也可以共享同一个queue下的资源。Queue根据自身配置和集群现状分得资源配额,其下的Job根据优先级逐个获取queue的资源,当queue下的资源被占满后,其下未获取到集群资源的Pod将无法再继续被调度,直到queue下已经在运行的Pod结束并释放资源。

在“vcjob”中,queue的配置为Job级别,通过为“vcjob”配置“queue”参数指明Job所在的queue。Queue的配置对Job下的所有Pod生效,不支持Job下的Pod使用不同的queue。在安装部署volcano时,系统为集群创建default queue,用于为没有明确指定queue的Job提供默认queue,集群下发的“vcjob”,如果没有指定queue名称,将使用默认default queue。对于集群下非“vcjob”对象创建的pod,比如Deployment对应的Pod,默认也放置到default queue下。

系统根据queue的配置和集群可分配的资源为queue分配资源。Queue有两个配置,weight和capability,其中weight表示queue占有资源的权重,capability为queue可以占有的资源上限。理论上,queue可以分配的资源为(weight/totalWeight)*allocatableResource和queue下所有Pod请求资源总和的最小值,weight是当前queue的权重,totalWeight为集群下所有含有运行中Pod或待调度pod queue的权重总和,allocatableResource为集群下可分配资源。在实际资源分割中,queue的资源分割将经历多个轮次,直到集群下的资源分割完毕,这样可以保证当集群下weight值比较高的queue下任务比较少,但是weight值比较低的queue下任务比较多时,weight值比较低的queue仍然可以使用集群下剩余的资源,即使这部分资源已经超出了严格按照queue权重算出的queue资源配额。当queue配置了capability参数后,实际queue可以使用的资源由上述计算的数值和capability两者的最小值决定。使用Queue分割集群资源,不仅可以为每个租户提供资源配额的保证,还可以保证集群下租户使用集群资源的弹性。正常情况下,租户可以使用的集群资源上限为queue分得的资源配额,当租户使用的资源达到资源配额后,如果此时集群下仍有剩余资源,租户仍然可以使用这部分资源。

根据queue的weight为queue分配集群资源的逻辑如下图所示。分配配额的过程是一个循环,首先计算所有queue的weight之和,在计算queue weight之和时,剔除已经meet的queue,queue meet表示queue下的资源请求已经得到满足。根据queue的weight所占比重计算初始queue配额值,当计算所得queue配额值大于queue下资源请求量时,queue的配额被调整为queue下的资源请求量,并标记该queue为meet。当所有的queue都已经被分配了配额后,判断计算集群剩余资源是否为0,如果为0则资源分配终止,否则继续新一轮的资源分配。当所有的queue都meet时,集群资源分配也会终止。
13.png

Queue的优先级决定queue下pod在调度过程中的调度顺序,Pod所在queue的优先级越高,queue下的Pod越先获取到集群资源。Queue的优先级由queue的share值确定,share值越小,queue的优先级越高。对于一个特定的queue,其share值的计算方式为queue下已经分配的资源与queue可以获取的集群资源的比值,queue下已经分配的资源包括已经被绑定到节点的Pod占用的资源,已经被“allocate”资源的Pod占用的资源和已经处于Running的Pod占用的资源,queue可以获取的集群资源则根据queue的weight和queue下pod的资源申请量决定。

当Queue下已经分配的资源大于或等于queue可支配的资源,此时queue处于“overUsed”的状态,queue下的Pod将不会再被调度,Queue下已经调度的Pod不会受到影响。当queue的配置中配置了capability,如果Pod所在Job需求的最小资源与queue下已经分配的资源的和大于queue可支配的资源总量,Pod将因为没有足够的资源而不能被调度。

Queue按照weight分配集群资源,正常情况下,queue下Pod所使用的资源不能超过queue所分得的配额,保证了集群资源使用的多租性。Queue灵活分配集群资源,有任务运行或调度的queue动态瓜分集群资源,保证了集群下用户可以灵活使用剩余资源。无论是何种场景,配额并不代表实际可以使用的资源量,这还取决于集群下空闲资源的数量,尽管queue分得了资源配额,但是当集群下空闲资源不足时,queue下的Pod仍然无法被调度,尤其是当集群下划分资源的对象发生增删变化时,配额和实际可使用的计算资源并不能保证一一对应。那么,当集群下出现queue的增删,queue下租户的资源配额所对应的实际资源是如何得到保障的呢?Volcano引入“reclaim” action,用于保障集群下queue的配额所代表的资源即是queue下任务所能使用的资源。当集群中新增queue,新增的queue按照weight分得资源配额,如果集群下空闲资源小于queue的资源配额,这个新增的queue会尝试通过“reclaim”行为从那些资源使用量大于queue配额的queue中抢占资源。通过“reclaim”的过程,保证集群下所有的queue可以使用的资源都在配额左右。

PodGroup解析

PodGroup是volcano引入的一个新的CRD对象,它是volcano-scheduler调度过程中的一个单位,与Job对应,在Pod调度之上,并贯穿于Pod调度的整个过程中。PodGroup与Pod绑定在一起,实现Pod的整体调度,比如与gang-scheduler插件一起实现Pod的“All or nothing”调度策略。PodGroup在调度中的角色是不可或缺的,Pod通过在annotation中指定“scheduling.k8s.io/group-name”与对应的podgroup绑定在一起。Podgroup的配置中包含“minMember”、“queue”、“priorityClassName”和“minResources”字段。其中“minMember”和“minResources”用于表明Job下的Pod整体调度时的最小资源申请量,只有当Pod所在Job下的最小资源申请量得到满足,Job下的Pod才能被调度。“queue”字段表明Pod所在的queue。“priorityClassName”字段表明Pod所在Job的优先级,具有高优先级的Job下的Pod具有高的调度优先级。在“vcjob”的处理过程中,volcano会为其创建podgroup,并将Job所对应的Pod与该podgroup绑定。对于普通的Pod,如果指定了volcano调度器,volcano-controllers也会为其创建一个podgroup,并将创建的podgroup与该pod绑定。同时,volcano支持先创建podgroup,并将Pod与已有的podgroup绑定,这只需要在Pod的annotation中做对应配置。

另外,podgroup的状态也决定了Job下的Pod是否能够被调度,为了防止往集群下恶意投放多个Pod,导致在集群资源不足时,volcano-scheduler调度器仍然需要反复处理多个未调度的Pod,造成调度器的空跑,性能下降。只有当podgroup的状态为非Pending状态时,podgroup下的Pod才允许被调度。

Volcano调度框架

Volcano的调度过程以action和plugin为基础。其中,action中定义了调度过程中将对Pod实施的调度阶段,plugin中注册了各种调度算法,调度算法包括节点的预选算法,优选算法或者资源管理控制和计算的其他逻辑。Plugin中注册的调度算法将分散到action的处理过程中执行。Volcano的调度会无限循环进行多个轮次,在每个调度轮次中,volcano open一个新的session,并在session中遍历注册的action并执行,在每个action中,按照各层级优先级,遍历需要调度的Pod,并逐个为Pod执行对应的action。在Pod调度过程中,将会调用plugin中注册的函数。在本轮次调度结束后,session将被closed。在下一个调度轮次中开启新的session进行调度。
14.png

Volcano目前支持“enqueue”、“allocate”、“backfill”、“preempt”、“reclaim”五个action。在一个调度过程中执行哪些action,这取决于调度器的配置,但是一般来说,“enqueue”、“allocate”和“backfill”三个action是必不可少的。

“enqueue” action用于刷新podgroup的状态,将podgroup的状态由“Pending”刷新成“Inqueue”。当一个Job下的最小资源申请量不能得到满足时,这表明,即使为Job下的Pod执行调度动作,Pod也会因为gang约束没有达到而无法调度。因此,在这种场景下,volcano-scheduler不会刷新podgroup的状态,podgroup的状态保持为“Pending”,对于状态为“Pending”的podgroup下的Pod,后续action都不再处理。即是,如果一个Pod所在Job的podgroup状态为“Pending”,那么这个Pod将不会被调度。当集群下剩余资源满足Job的最小资源述求,调度器会刷新Job的podgroup状态为“Inqueue”,表明podgroup下的Pod可以被尝试调度。“enqueue”action用于防止集群下有大量不能调度的Pod,影响scheduler的调度性能。

“allocate” action用于处理待调度Pod列表中具有资源申请量的Pod调度,即non-besteffort pod的调度。与Kubernetes默认default-scheduler类似,allocate action在为Pod选择节点调度时,也需要经过“predicate”和“prioritize”两个阶段,在经历节点预选后,从预选节点中选择一个最优的节点,并将Pod调度上去。在预选和优选的过程中,调度器将会调用plugin中注册的预选和优选函数。在allocate action的执行过程中,在预选节点的阶段,单单从节点对Pod资源请求量的满足方面来看,只要节点上空闲资源或者releasing资源大于Pod的资源申请量,就认为该节点的资源状况满足Pod的资源调度请求,当其他预选条件也同时满足时,该节点将作为优选阶段的候选节点供Pod调度。节点的releasing资源是指节点上正在结束或被驱逐的Pod所占用的资源。理论上,当Pod结束或被驱逐后,这部分节点资源可以被释放,供其他Pod使用。在为Pod选择合适的节点后,如果Pod的资源申请量小于节点的空闲资源,将会为Pod执行绑定动作,如果Pod的资源申请量大于Node的空闲资源,但是小于节点的releasing资源,Pod会被pipelined到节点上。Pipelined是指Pod作为候选Pod调度到这个节点上,一旦节点上有了空闲的资源,被pipelined的Pod将会被绑定到这个节点上。“allocate”过程遵循commit机制,当一个Pod的调度请求得到满足后,最终并不一定会为该Pod执行绑定动作,这还取决于Pod所在Job的gang约束是否得到满足,只有Pod所在Job的gang约束得到满足,Pod才可以被调度,否则,Pod不能够被调度。

“backfill” action处理待调度Pod列表中没有指明资源申请量的Pod调度,即besteffort pod的调度。在对单个Pod执行调度动作的时候,遍历所有的节点,只要节点满足了Pod的调度请求,就将Pod调度到这个节点上。

“preempt” action用于处理高优先级pod的调度问题。当集群比较繁忙,集群下已经没有空闲资源可供新Pod使用,此时,如果有更高优先级的Pod下发到集群中,那么volcano-scheduler会尝试驱逐这个集群中已经处于运行中的并且优先级比待调度Pod低的Pod,希望通过驱逐低优先级的Pod,使更高优先级的Pod得以调度。当然考虑到驱逐将可能对已经处于运行中的任务有破坏性的影响,对于一个Pod是否可以驱逐其他的Pod,或者一个Pod是否可以被其他的pod驱逐都有严格的限制。比如在一个Pod是否可以驱逐其他Pod的约束中,只有当Pod驱逐了其他Pod后,这个Pod所在Job的gang约束可以得到满足,Pod才可以驱逐其他Pod。对于一个Pod是否可以被驱逐的约束将包括,Pod被驱逐后,Pod所在Job的gang约束不能被破坏,在kube-system下的Pod不能被驱逐等。当一个Pod驱逐其他的Pod成功后,这个Pod将会pipelined到这个节点上,预示着,当节点上有空闲资源时,Pod将会被调度到这个节点上。

“reclaim” action用于在各个queue之间均衡集群资源。queue在瓜分集群资源时,只会考虑现有集群下有任务在运行或待调度的queue。当集群中现有的queue瓜分完集群资源后,集群下新增了queue,这个queue将希望得到集群资源。集群资源划分需要打破原来的形势,建立新的分割形势。当这个新加入的queue分割到集群配额后,部分原有queue的配额将可能会降低。然而,新queue分到配额后,并不表明queue下的Pod可以正常调度了,因为queue在此时分到的配额只是使用集群资源的上限,并不是使用集群的担保。假如此时旧有queue下的Pod已经占尽了集群资源,尽管此时这些queue下pod的资源使用量已经大于queue分得的配额,但是因为这些Pod已经处于运行中,并不会主动释放资源。这时候,新的queue虽然有配额,但是苦于集群下没有资源,queue下的Pod仍然无法调度。这个时候就需要reclaim action在不同的queue之间做资源均衡。“reclaim” action尝试驱逐那些资源使用量已经大于配额的queue下的Pod,并把这部分资源分配给资源使用量还没有得到满足的queue。同样在Pod驱逐过程中,对于是否可以驱逐和是否可以被驱逐都有严格的定义。只有当Pod被驱逐后,其所在Job下的资源使用量仍然大于配额,这个Pod才可以被驱逐,以防止queue之间出现互相驱逐的震荡。同样,只有当Pod所在Job的gang约束没有得到破坏时,Pod才可以被驱逐。

下图展示一个Pod在调度周期内将可能会经历的过程。对于pending的Pod,调度器开始调度该Pod,等调度器为该Pod找到合适的调度节点后,Pod被allocated或者pipelined到节点上,如果此时,Pod所在的Job的gang约束得到满足,Pod被bind到这个节点上,否则,Pod仍然退回到pending的状态,等待下一个action或下一个调度周期的处理。
15.png

Binpack调度

Binpack的调度策略是尽量的将容器调度到主要负载节点上,优先将集群下某些节点的资源占满,以提高资源使用率。同时,避免资源碎片化,在空闲的机器上为申请了更大资源请求的Pod预留足够的资源空间,使集群下空闲资源得到最大化的利用。

Binpack算法以插件的形式,注入到volcano-scheduler调度过程中,将会应用在Pod优选节点的阶段。Volcano-scheduler在计算binpack算法时,会考虑Pod请求的各种资源,并根据各种资源所配置的权重做平均。每种资源在节点分值计算过程中的权重并不一样,这取决于管理员为每种资源配置的权重值。同时不同的插件在计算节点分数时,也需要分配不同的权重,scheduler也为binpack插件设置了分数权重。binpack插件的资源和插件级别权重配置如下:
- plugins:    
- name: binpack    
arguments:  
  # binpack插件权重  
  binpack.weight: 10  
  # cpu资源权重    
  binpack.cpu: 5  
  # memory资源权重    
  binpack.memory: 1  
  # gpu等其他资源类型    
  binpack.resources: nvidia.com/gpu, example.com/foo  
  # gpu等其他资源权重配置  
  binpack.resources.nvidia.com/gpu: 2    
  binpack.resources.example.com/foo: 3  

Binpack算法的流程如下图所示:
16.png

首先,遍历Pod请求中的所有资源类型,分别计算资源类型对应的节点分数。在为单一资源类型计算节点分数时,先计算节点上该资源的已经使用量和Pod对该资源的请求量的和与节点该资源的可分配资源的比值作为初始节点分数值,然后上述计算的值与系统配置的针对于该资源权重weight的乘积作为最终节点在这个资源类型上所得分数。当所有资源类型所对应的节点分数都计算完毕,将所有的资源类型对应的分数相加得到节点总分,并与Pod所申请的所有资源类型权重的总值相除得到对于这个Pod的调度,节点的分数值。最终将Node分值重新规划到0~10*binpackingweight之间。

选择含有两个节点的集群进行测试,其中节点资源规格均为4c8g,分别往集群下发两个MXNet job,Pod数量分别为16和24,每个Pod的资源请求为0.2c。查看Pod调度情况。测试结果显示,当Pod数量较少时,Pod全部调度到其中一个节点上,优先占满集群下某个节点的资源;当Pod数量较多时,该节点资源被占满,开始往其他的节点调度。
B3.jpg

Task-topology调度

Task-topology算法是一种根据Job内task之间亲和性和反亲和性配置计算task优先级和Node优先级的算法。通过在Job内配置task之间的亲和性和反亲和性策略,并使用task-topology算法,可优先将具有亲和性配置的task调度到同一个节点上,将具有反亲和性配置的Pod调度到不同的节点上。

同样是处理亲和性和反亲和性配置对Pod调度的影响,task-topology算法与Kubernetes默认调度器处理的不同点在于,Kubernetes默认调度器在调度Pod过程中,仅会检查Pod与现有集群下所有已经处于运行状态Pod的亲和性和反亲和性配置是否冲突或吻合,并不会考虑接下来可能会调度的Pod造成的影响;而task-topology将待调度的Pods作为一个整体进行亲和性和反亲和性考虑,在批量调度Pod的时候,考虑未调度Pod之间的亲和性和反亲和性影响,并通过优先级施加到Pod的调度进程中。

Task-topology对于提升深度学习计算场景下的计算效率非常重要。以TensorFlow计算为例,配置“ps”和“worker”之间的亲和性,以及“ps”与“ps”之间的反亲和性,task-topology算法,可使“ps”和“worker”尽量调度到同一台节点上,从而提升“ps”和“worker”之间进行网络和数据交互的效率,进而提升计算效率。

Volcano团队使用task-topology算法对TensorFlow任务进行性能测试。同时下发3组Tensorflow计算任务,每组TensorFlow任务包含2个“ps”和4个“worker”,对上述测试执行多次,并取平均完成时间,测试结果如下图所示。测试结果显示,当使用Kubernetes default-scheduler进行测试时,测试时间波动较大,而使用volcano调度器,测试时间相对稳定。最终测试平均时间显示,使用volcano调度器,其性能提升了33%。
17.png

研究结果表明对于worker/parameter server的调度结果,存在如下图所示的多种组合,相比较三种调度结果,(c)场景所描述的调度结果,“ps”和“worker”之间网络互访速度更快,是最优的调度结果。Volcano配置使用task-topology调度算法可以实现TensorFlow计算任务的最优调度,提升计算效率。
18.png

Fair Share调度

当集群资源不足,但运行了多个Job,并且每个Job下有不等数量的Pod等待被调度的时候,如果使用Kubernetes默认调度器,那么最终,具有更多Pod数量的Job将分得更多的集群资源。在这种情况下,volcano-scheduler提供算法支持不同的Job以fair-share的形式共享集群资源。
19.png

Fair-Share调度过程中使用Dominant Resource Fairness(DRF)的方法,DRF即主导资源公平性,volcano-scheduler观察每个Job请求的主导资源,并将其作为对集群资源使用的一种度量,根据Job的主导资源,计算Job的share值,在调度的过程中,具有较低share值的Job将具有更高的调度优先级。

Volcano支持多个维度的fair-share,包括queue与queue之间,namespace与namespace之间,queue内部Job与job之间。选择MXNet计算任务,分别使用Kubernetes default调度器和volcano调度器对比分析queue内部Job和Job之间的fair-share体现。投放两组MXNet计算任务,其中一组计算任务的Pod数量为300,一组计算任务的Pod数量为60,其中任何一组计算任务均可占满集群资源,查看最终调度成功的Pod数量。测试结果显示,使用volcano调度器,两组Job被调度的Pod数量相等,而使用Kubernetes默认调度器,Pod数量多的Job被调度的Pod数量也多,volcano调度器可以实现Job与Job之间的公平调度。
B4.jpg

通过在namespace的ResourceQuota中配置“volcano.sh/namespace.weight”可以为namespace配置资源使用权重。调度过程中,具有更高权重的namespace下的Pod将具有更高的优先级获取集群资源。测试不同namespace之间的fair-share,集群下存在两个namespace:“vc-test-1”和“vc-test-2”。其中“vc-test-1” namespace的“volcano.sh/namespace.weight”值为3,“vc-test-2” namespace的“volcano.sh/namespace.weight”值为1。分别往“vc-test-1”和“vc-test-2” namespace下发含有60个Pod的Job。观察最终每个namespace下调度Pod的数量。测试结果显示,Job下所能调度的Pod数量与namespace的权重成正比。
B5.jpg

Gang调度

Gang调度策略是volcano-scheduler的核心调度算法之一,它满足了调度过程中的“All or nothing”的调度需求,避免Pod的任意调度导致集群资源的浪费。Gang调度遵循commit机制,在调度过程中,观察Job下的Pod已调度数量是否满足了最小运行数量,当Job的最小运行数量得到满足,为Job下的所有Pod执行调度动作,否则,跳过Job下Pod的调度。

Gang的约束不单单体现在调度阶段,也会影响Job下的Pod是否可以被驱逐。在计算Pod是否可以被驱逐时,如果驱逐后,Pod所在Job的gang约束被破坏,那么Pod不可被驱逐。

当集群下资源不足时,gang的调度策略对于集群资源的利用率的提升是非常明显的。
20.png

Volcano团队针对gang的调度策略做了一项测试。当集群资源不能同时满足两组2ps+4worker的TensorFlow作业并行计算的资源请求时,分别使用Kubernetes默认调度器和volcano调度器,验证并行下发一个上述TensorFlow作业,两个上述TensorFlow作业和五个上述TensorFlow作业,所消耗的时间。其结果显示,在资源充足的场景下,使用Kubernetes调度器和volcano调度器运行一组TensorFlow作业,作业运行时间一致。在集群资源不足的场景下,使用Kubernetes调度器和volcano调度器运行多组TensorFlow作业,使用Kubernetes调度器,会造成集群资源死锁,导致部分计算任务无法顺利完成,使用volcano调度器任务则可以顺利完成,volcano解决了Kubernetes默认调度器死锁问题。

测试结果如下图所示,其中case1 1 job with 2ps+4workder,case2 2jobs with 2ps+4workder case3 5jobswith 2ps+4worker。当集群资源不能满足多组计算任务同时运算时,如果多个计算任务下都有Pod在调度,那么会造成集群资源虽然被占用,但是TensorFlow作业并没有形成计算集群,而无法开始计算,造成集群资源的浪费。更有甚者,可能造成资源死锁,导致计算任务无法进行。
21.png

深度学习计算框架在Volcano上的运行

Volcano平台可以弥补Kubernetes在深度学习计算领域的不足。Volcano的批量创建批量调度计算任务为深度学习计算作业提供计算任务的自动化生命周期管理。Gang调度策略可以满足server、worker以及scheduler “all or nothing”的业务调度约束。使用queue管理划分集群资源,不仅可以保证集群资源使用的多租性,还能保证资源使用的弹性。

Volcano调度器支持插入多种调度算法,提供更适合深度学习计算任务运行模式的调度结果,提高计算任务的执行效率。在集群资源不足时,gang调度策略可以避免集群资源浪费和死锁,提升计算性能。另外,binpack和task-topology等调度算法的应用使计算任务的调度更贴合计算集群对节点资源和网络拓扑结构的要求而提升任务计算效率。
B6.jpg

Volcano与深度学习框架的结合

深度学习计算框架与volcano的结合非常方便,volcano “vcjob”支持定义插件,通过在“vcjob”中配置插件,可以实现计算框架与volcano计算平台的结合。目前,volcano已经实现了对几乎全部深度学习领域主流计算框架的支持,其中包括TensorFlow、MPI、MXNet以及国内第一款开源的深度学习框架PaddlePaddle。
B7.jpg

Volcano社区和贡献

Volcano在深度学习领域和大数据场景下的强势表现,赢得了很多公司的青睐,volcano的前景普遍看好。目前,已有十多家企业考虑使用volcano作为计算任务管理工具。详细的企业使用现状如下表所示:
B8.jpg

总结与展望

自诞生以来,volcano已经成长为一个成熟、稳定、高性能的服务,无论是在技术交流上还是商业运行上,均取得了显著的成绩。在技术融合上,Volcano已经实现了与包括TensorFlow、MPI、MXNet、PaddlePaddle在内的诸多主流深度学习平台的结合。在企业交流上,volcano与“caicloud”、“Baidu”、 “Huawei Cloud”等多家企业接洽合作,其中有些企业已经将volcano运行到生产环境中。

不仅在深度学习计算领域,在大数据场景下,volcano的表现同样优异。Volcano已经完成了对Spark计算任务的支持,并且经过测试,使用volcano运行Spark任务,可使计算任务效率提升20%。另外,在Volcano上运行Flink作业已经处于测试中。

未来volcano将会支持更多的计算场景,并在计算领域提供更多的优化算法和工具。同时,也会有更多的公司和个人参与到volcano的发展中。

参考文献

  1. Kubernetes for Machine Learning Deep Learning &AI
  2. Optimus: An Efficient Dynamic Resource Scheduler for Deep Learning Clusters
  3. Job scheduling of kubeflow
  4. Volcano introduction
  5. 百度飞浆(PaddlePaddle)分布式训练在Volcano系统上的实践
  6. Volcano在Kubernetes中运行高性能实践
  7. Kubernetes增强型调度器Volcano算法分析
  8. 华为云Volcano:让企业AI算力像火山一样爆发
  9. Volcano官网 https://volcano.sh/
  10. Volcano社区 https://github.com/volcano-sh/volcano
  11. Namespace fair share of volcano
  12. Queue of volcano
  13. Reclaim action
  14. Adopters of volcano


Q&A

Q:针对KubeFlow这种工作流有没有计划做出针对性优化?#274号PR提出要做拓扑优化,最后也close了,这是为什么?
A:针对volcano与KubeFlow的结合,volcano社区一直在推动,希望KubeFlow下各个operator对接volcano,现在,这一推动在KubeFlow中的一些计算框架已经取得了比较明显的进展。task-topology的算法已在内网实现,推入GitHub的计划正在制定中,如您有切实需求,可到volcano社区留言讨论https://github.com/volcano-sh/volcano/issues

Q:请问分布式TensorFlow训练,worker节点的GPU型号不同,会不会有问题,比如v100和p40混用?
A:v100和p40均为扩展资源,在调度过程中均同等对待,是否指定多个GPU卡对调度进程无影响,欢迎您使用volcano进行相关测试。

Q:在使用Kubernetes中,对于IB网络和RDMA的有什么问题?
A:没有问题,支持过程中,Kubernetes需要做一些适配。据我所知,一些云厂商使用Kubernetes对IB网络和RDMA的支持已经商用。

Q:volcano是并行调度多个Pod吗?调度过程中会不会发生冲突?
A:是并行调度,不会冲突。batch调度,主要针对于同一批计算任务下任务的批量调度。调度过程中,仍然存在多个维度的优先级。优先级内有先后。

以上内容根据2019年12月10日晚微信群分享内容整理。 分享人张经辉,Volcano贡献者,云原生技术开发工程师。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiese,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

0 个评论

要回复文章请先登录注册