GaiaGPU:Sharing GPUs in Container Clouds

613次阅读
没有评论

2018年腾讯和北大发的一篇论文,一种容器云平台上的GPU虚拟化解决方案。对于GPU虚拟化关键的vCUDA部分讲得比较含糊,但是系统架构方面还是有很多值得学习的地方。

GaiaGPU:Sharing GPUs in Container Clouds

论文内容

摘要

容器技术由于其轻量级和可伸缩的优势而被广泛使用。GPU也因为其强大的并行计算能力被用于应用程序加速。在云计算环境下,容器可能需要一块或者多块GPU计算卡来满足应程序的资源需求,但另一方面,容器独占GPU计算卡常常会带来资源利用率低的问题。因此,对于云计算资源提供商而言,如何解决在多个容器之间共享GPU计算卡是一个很有吸引力的问题。本文中我们提出了一种称为GaiaGPU的方法,用于在容器间共享GPU存储和GPU的计算资源。GaiaGPU会将物理GPU计算卡分割为多个虚拟GPU并且将虚拟GPU按需分配给容器。同时我们采用了弹性资源分配动态资源分配的方法来提高资源利用率。实验结果表明GaiaGPU平均仅带来1.015%的性能损耗并且能够高效的为容器分配和隔离GPU资源。

关键词:GPU虚拟化,Kubernetes,Container

1. Introduction

先是在介绍容器的概念,与虚拟机的区别,和GPU的特点。

讲到GPU虚拟化技术,GPU虚拟化技术就是在诸如VM或者容器这样被隔离的虚拟环境之间共享GPU的技术。当前现存的GPU虚拟化技术都被应用于VM,基于容器的GPU虚拟化技术还处于起步阶段。当前现存的基于容器的GPU虚拟化技术有以下的短板:

  • 不能共享,只能单个容器独占一块GPU计算卡(Nvidia Docker
  • 只能在多个容器之间共享GPU内存(2017 IEEE《ConVGPU: GPU Management Middleware in Container Based Virtualized Environment》)
  • 仅支持单个GPU(2017 IEEE 《ConVGPU:……》)

接下来讲到GaiaGPU,能够透明地(transparently)在多个容器之间共享GPU的内存和计算资源。用户无需因为要共享GPU个修改容器镜像,为了实现这一目标本文基于了Kubernetes的Device Plugin框架讲物理GPU分割为多个vGPU。每一个容器都可以按需被分配一个或者多个vGPU。还提供了两种方法用于在运行时修改容器的资源,一种是弹性资源分配(Elastic Resource Allocation),能够临时的改变容器资源,另一种是动态资源分配(Dynamic Resource Allocation),能够永久的改变容器资源。

接下来解释了共享GPU内存(Memory)和共享GPU计算资源(Computer Resource)的含义。共享GPU内存就是说一块GPU计算卡的内存被划分为多块,每个容器都占有一部分。共享GPU计算资源就是说每一个容器都占有GPU的线程资源(GPU’s thread)的一部分来进行并行计算。一个vGPU(抽象概念,实际上并不存在)由GPU的内存和计算资源构成。vGPU的内存就是被实际分配的物理显存,vGPU的计算资源通过GPU的使用率来计量,GPU使用率被定义为在过去一个采样周期内某个容器使用GPU进行计算的时间百分比。

本文的贡献:

  • 提出GaiaGPU,一种在容器之间透明的共享GPU内存和计算资源的方法
  • 提出弹性资源分配动态资源分配用于提高资源利用率
  • 进行了4组实验来评估GaiaGPU的表现,表明性能损耗低,仅1.015%

……

3. Design And Implementation

这一部分介绍了整个虚拟化框架的架构,和两种利用率优化策略(弹性资源分配和动态资源分配)。

A. 系统设计

如下图是GaiaGPU的系统架构:

GaiaGPU:Sharing GPUs in Container Clouds

有四个主要模块:GPU Manager,GPU Scheduler,vGPU Manager,vGPU Library

总的来看可以两层,在主机层(host level)GPU Manager负责创建vGPU,GPU Scheduler负责将物理GPU计算卡的资源分配给vGPU,在容器层(container level)vGPU Library模块会负责管理某一个具体容器的GPU资源。

a) GPU Manager

GPU Manger是Device Plugin接口的实现,运行在宿主机上负责创建vGPU和通过gRPC通信向Kubelet报告GPU的情况。下图是GPU Manager与Kubelet的通信方式:

GaiaGPU:Sharing GPUs in Container Clouds

首先GPU Manager向Kubelet发起Register请求,在Kubelet中注册自己,注册成功后Kubelet会向GPU Manager发起一个ListAndWatch请求,GPU Manager需要向Kubelet返回一个自己所管理的设备的列表,但是这里不是直接将物理GPU计算卡的信息返回给Kubelet,而是vGPU的信息,物理的GPU计算卡被虚拟化为两个资源维度:内存和计算资源,就是说原来的GPU计算卡被虚拟化成了两种设备:

  • 存储维度:将256M的内存为一个单元叫做vmemory
  • 计算维度:将一个物理GPU分为100个vprocessor,每个vprocessor拥有1%的GPU使用率

GPU Manger将一个由所有vmemoryvprocessor的信息组成的list返回给Kubelet。

当用户为某个容器申请GPU资源时,Kubelet会直接(arbitrarily 不知道是不是要表达“直接”的意思)从GPU Manager返回的list中选择相应数量的虚拟设备,然后向GPU Manager发起Allocate请求,请求将这些虚拟设备分配给该容器使用。但是由于list中的信息实际上都是虚拟的设备,所以还需要建立起一个虚拟设备到物理设备的映射,就是说需要决定究竟从哪一个物理设备上分割实际的资源

映射流程举例:一个容器申请了50个vprocess和10个vmemory,首先Kubelet会随机的从上文提到的list中选择对应数量的Device ID发送给GPU Manager,然后GPU Manager会根据接收到的Device ID计算所需的物理GPU资源,然后给GPU Scheduler发送一个请求,最终GPU Schduler返回具体分配给容器的物理GPU计算卡信息。

在完成了上述的映射过程后GPU Manager会向Kubelet返回一个含有设备配置信息的allocateResponse,包含以下内容:1)容器的环境变量,2)需要挂载到容器中的目录(如NVIDIA驱动,CUDA库),3)被分配的物理设备信息。Kubelet会将这些信息返回给容器运行时(container runtime)用于创建容器。

b) GPU Scheduler

GPU Scheduler负责处理来自GPU Manager的调度请求,如果调度成功GPU Scheduler会返回被分配的物理GPU计算卡信息。

GPU Scheduler的调度基于拓扑学(topology),GPU拓扑是一个树形拓扑,书的根节点是一个物理机,叶子节点是物理GPU计算卡,调度策略如下:

  • 如果请求的资源少于一整块GPU计算卡(<1),则按照“最小化树上的资源碎片”的原则进行分配
  • 如果请求的资源等于一整块GPU计算卡(=1),则按照”使得单叶子节点(即没有兄弟节点的叶子节点)数量最少”的原则进行分配
  • 如果请求的资源大于一整块GPU计算卡(>1),则按照“最小化GPU间通信成本”的原则进行分配,两个GPU间的通讯成本取决于两个GPU之间的连接模式(这里好像并没有说清楚这个通信成本到底怎么衡量)
c) vGPU Manager

vGPU Manager运行在宿主机上,负责分发容器的配置信息和监控被分配了vGPU资源的容器。当一个容器申请GPU资源时,GPU Manager会发送容器的配置信息给vGPU Manager,配置信息包括请求的GPU资源和该容器的name等。根据收到的这些容器的配置信息,vGPU Manager会为这个容器在宿主机上创建一个独立的目录,这个目录以容器的name命名,这个目录的路径也会包含在GPU Manager返回给Kubelet的allocateResponse中。容器的配置信息会被保存在这个目录中,并且通过Kubelet分发给容器。(将这个目录挂载到容器中?)

vGPU Manager和vGPU Library以“服务器-客户端”模式进行通信。vGPU Manager会维护一个被分配了GPU资源且存活的容器的list,并且周期性的检查这些容器是否仍然存活,如果容器退出,vGPU Manager会从list中移除这个容器的信息,并且删除对应的目录。

d) vGPU Library

vGPU Library运行在容器中,用于管理其所在容器的GPU资源,vGPU Library会在容器中第一次执行GPU程序时被加载(launched),启动后vGPU Library会想vGPU Manager注册自身。

vGPU Library利用LD_LIBRARY_PATH机制拦截CUDA库中与内存和计算相关的APILD_LIBRARY_PATHLinux系统中的一个环境变量,能够影响程序的运行时链接(runtime link,这里不知道该怎么翻译合适),LD_LIBRARY_PATH中包含的目录会在加载标准库目录之前加载。下表展示了所有被拦截的CUDA API:

GaiaGPU:Sharing GPUs in Container Clouds

两种资源限制策略

  • 硬限制(hard limiit):如果容器资源消耗量超过配额,就不再给该容器分配资源
  • 弹性限制(elastic limit):如果容器资源消耗量超过配额,但是系统中还有空闲资源,那么该容器仍然能够得到更多的资源

内存资源的限制采用硬限制策略,因为 1)被分配的内存资源大小能够决定一个程序能否运行,但是对程序运行的效率影响较小,2)GPU是运算设备,采用一次性资源分配策略(one-time resource allocation strategy),就是说一个程序只有在获得了所有的资源之后才开始运行,并且只有在程序运行完成后才回释放内存资源,如果采用弹性限制策略限制应用程序的内存使用,可能会导致对内存需求量大的应用程序无法运行,3)回收一个程序超额(over-allocated)资源的唯一方法就是通过抛出内存溢出异常(out-of-memory exception)杀死进程。

计算资源的限制采用弹性限制策略,因为计算资源对程序运行效率影响很大。并且与内存不同,计算资源(也就是GPU线程)会在执行之后立即释放,而不是等待整个程序的完成再释放。

系统工作流程

总的来看,GaiaGPU的工作流程如下,对应系统架构图中的标号:

GaiaGPU:Sharing GPUs in Container Clouds
  • Step 1:GPU Manager向Kubelet注册自身,并报告vGPU的信息(ListAndWatch)
  • Step 2:Kubelet接收到来自Master的创建一个GPU容器的请求
  • Step 3:Kubelet发送一个allocateRequest到GPU Manager
  • Step 4:GPU Manager发送一个vGPU调度请求到GPU Scheduler,GPU Scheduler根据调度策略选择实际提供资源的物理GPU。如果调度成功返回一个包含该物理GPU的信息的reponse
  • Step 5:GPU Manager将容器配置信息发送到vGPU Manager
  • Step 6:GPU Manager将容器环境变量,挂载信息和设备信息通过allocateResponse返回给Kubelet
  • Step 7:Kubelet根据allocateResponse*创建并且初始化一个容器
  • Step 8:vGPU Library向vGPU Manager注册自身并且管理其所在容器的GPU资源
  • Step 9:vGPU Manager持续监控GPU容器状态

B. 优化策略

弹性资源分配(Elastic Resource Allocation)

算法描述:

GaiaGPU:Sharing GPUs in Container Clouds
  • nanosleep()是Linux内核函数,会挂起当前线程等待一段时间或者直到接收到调用当前线程的信号(Line 2)
  • 为了防止过载,GU max默认的最大值是90%(Line 3)
  • 如果物理GPU计算卡仍然有空闲资源,也就是说GU free > 0,即使容器的资源请求已经超出其配额,vGPU Library也会继续给容器分配计算资源(Line 4-5)。如果系统没有剩余的空闲资源(GU free <= 0)并且容器的消耗的资源大于其配额,vGPU Library会逐渐收回超额(over-allocated)资源(Line 6-7)
  • 超额资源的回收采用非抢占式策略(non-preemptive strategy),就是说会等到容器中占用线程的核函数执行完后再回收线程资源。CU cores可以被理解为一种token,容器执行核函数时需要消耗该token,当核函数执行完成释放线程资源时容器又回重新拥有该tokenCU cores的初始值等于容器的计算资源配额,CU cores为零时(系统没有空闲的资源,并且该容器的计算资源配额都正在被用于执行核函数),vGPU Library不会再给容器分配任何计算资源,直到容器的CU cores大于零(其他容器释放了空闲资源,或者该容器有核函数完成释放了资源,容器重新获得CU cores

弹性资源分配举例:

GaiaGPU:Sharing GPUs in Container Clouds

a)首先,容器A申请了0.3个GPU,并且被GPU Scheduler调度到了一个完全空闲的GPU上

b)由于GPU完全空闲,所以容器A会逐渐占用所有的空闲资源,默认最大可占用90%

c)此时容器B申请了0.7个GPU,也被调度到了此GPU上,但是由于容器A占用了所有的空闲资源,所以需要从容器A回收超额线程资源并分配给容器B

d)重复经过几次资源的重新分配,容器A和容器B所占用的资源与其资源配额相同

(这里有一个问题,这个资源分配的线程何时会被调用,周期性的调用还是等待容器有资源请求时才被调用。如果是周期性的调用容器的资源分配在临界值时会不会出现振荡?因为算法逻辑中一旦flag要么是1要么是-1,要么是分配资源要么是回收资源,不会出现既不分配也不回收的情况。如果是容器有资源请求时才被调用,那么资源回收又是如何触发的?感觉没有说清楚,也有可能是在临界值出现振荡不会有影响)

(还有就是GU free只在分配超额资源时会有限制,如果容器消耗的资源还没有达到配额,还是会将剩下的10%分配给容器)

动态资源分配(Dynamic Resource Allocation)

这个比较简单,就是说在容器运行过程中允许修改容器的资源配额

4. Experiment

……

5.Conclusion

……

总结

主要的关键点有三个

  • 整个系统的架构和模块功能的划分
  • vGPU Library的实现方法,如何通过拦截CUDA API实现对GPU内存和线程使用的限制
  • 弹性资源分配策略

系统架构和模块功能阐述的比较清楚的。但是CUDA API拦截和拦截后如何进行资源管理好像没有讲明白。弹性资源分配策略还是比较巧妙的,用于线程资源的管理应该是可行的。

参考资料

论文地址 https://ieeexplore.ieee.org/abstract/document/8672318

腾讯云论坛GaiaStack介绍 https://cloud.tencent.com/developer/article/1389547

腾讯云市场GaiaStack https://market.cloud.tencent.com/products/3966?productId=3966#

Github开源代码 https://github.com/tkestack/gpu-manager

正文完
可以使用微信扫码关注公众号(ID:xzluomor)
post-qrcode
 
评论(没有评论)