virt

A survey of GPU sharing for DL

当前机器学习训练中,使用 GPU 提供算力已经非常普遍,对于 GPU-based AI system 的研究也如火如荼。在这些研究中,以提高资源利用率为主要目标的 GPU 共享(GPU sharing)是当下研究的热点之一。GPU 共享涉及到的技术面较广,包括 GPU 架构(计算,存储等),CUDA,IO(内存,显存),机器学习框架(Tf,Pytorch),集群&调度,ML/DL 算法特性,通信(单机内和多机间),逆向工程等等,是一个自上而下的工作。本篇文章希望能提供一个对 GPU 共享工作的分享,希望能和相关领域的研究者们共同讨论。限于笔者能力有限,可能会出现一些错漏,希望能多多指正,感谢。

GPU 共享,是指在同一张 GPU 卡上同时运行多个任务。优势在于:

  • 集群中可以运行更多任务,减少抢占。
  • 资源利用率(GPU/显存/e.t.c.)提高;GPU 共享后,总利用率接近运行任务利用率之和,减少了资源浪费。
  • 可以增强公平性,因为多个任务可以同时开始享受资源;也可以单独保证某一个任务的 QoS。
  • 减少任务排队时间。
  • 总任务结束时间下降;假设两个任务结束时间分别是 x,y,通过 GPU 共享,两个任务全部结束的时间小于 x+y。

想要实现 GPU 共享,需要完成的主要工作有:

  • 资源隔离,是指共享组件有能力限制任务占据算力(线程/SM)及显存的比例,更进一步地,可以限制总线带宽。
  • 并行模式,主要指时间片模式和 MPS 模式。

资源隔离

资源隔离是指共享组件有能力限制任务占据算力/显存的比例。限制的方法就是劫持调用。图一是在 Nvidia GPU 上,机器学习自上而下的视图。由于 CUDA 和 Driver 不开源,因此资源隔离层一般处在用户态。在内核态做隔离的困难较大,但也有一些工作。顺带一提,Intel 的 Driver 是开源的,在 driver 层的共享和热迁移方面有一些上海交大和 Intel 合作的工作。

img
img

图一/使用 Nvidia GPU 机器学习自上而下视图

来自腾讯的 Gaia(ISPA'18)[1]共享层在 Cuda driver API 之上,它通过劫持对 Cuda driver API 的调用来做到资源隔离。劫持的调用如图二所示。具体实现方式也较为直接,在调用相应 API 时检查:

  • 对于显存,一旦该任务申请显存后占用的显存大小大于 config 中的设置,就报错。
  • 对于计算资源,存在硬隔离和软隔离两种方式
    • 共同点是当任务使用的 GPU SM 利用率超出资源上限,则暂缓下发 API 调用。
    • 不同点是如果有资源空闲,软隔离允许任务超过设置,动态计算资源上限。而硬隔离则不允许超出设置量。
  • 该项目代码开源在[2]。实测即使只跑一个任务也会有较大 JCT 影响,可能是因为对资源的限制及守护程序的资源占用问题。
  • KubeShare(HPDC ‘20)[3]的在资源隔离方面也是类似的方案。

img
img
图二/ Gaia 限制的 CUDA driver API

发了 44 篇论文(截止 2020 年 3 月)的 rCuda[4]和 Gaia 有相似之处,他们都是在 Cuda driver API 之上,通过劫持调用来做资源隔离。不同的是,rCuda 除了资源隔离,最主要的目标是支持池化。池化简单来讲就是使用远程访问的形式使用 GPU 资源,任务使用本机的 CPU 和另一台机器的 GPU,两者通过网络进行通信。也是因为这个原因,共享模块需要将 CPU 和 GPU 的调用分开。然而正常情况下混合编译的程序会插入一些没有开源的 Cuda API,因此需要使用作者提供的 cuda,分别编译程序的 CPU 和 GPU 部分。图三显示了 rCuda 的架构。如果使用该产品,用户需要重新编译,对用户有一定的影响。该项目代码不开源。另外 vCUDA(TC ‘12)[5]和 qCUDA(CloudCom ‘19)[18]也采用了和 rCuda 相似的技术。

img
img
图三/rCuda 架构

GPUShare(IPDPSW’ 16)[6]也是劫持的方式,但不同的是,它采用预测执行时间的方式来实现计算资源的公平性。作者认为比切换周期还小的短 kernel 不会影响公平使用,因此只限制了较大的 kernel。

来自阿里的 cGPU[7],其共享模块在 Nvidia driver 层之上,也就是内核态。由于是在公有云使用,相对于用户态的共享会更加安全。它也是通过劫持对 driver 的调用完成资源隔离的,通过设置任务占用时间片长度来分配任务占用算力,但不清楚使用何种方式精准地控制上下文切换的时间。值得一提的是,由于 Nvidia driver 是不开源的,因此需要一些逆向工程才可以获得 driver 的相关 method 的 name 和 ioctl 参数的结构。该方案在使用上对用户可以做到无感知,当然 JCT 是有影响的。代码没有开源,也没有论文。图四是 cGPU 的架构图。

img
img
图四/cgpu 架构图

来自 Nvidia 的 vGPU[8]其共享模块在 Nvidia driver 里面。vGPU 通过 vfio-mdev 提供了一个隔离性非常高的的硬件环境,主要面向的是虚拟机产品,无法动态调整资源比例。来自 Nvidia 的产品当然没有开源。图五是 vGPU 的架构图。

img
img
图五/vGPU 架构图

Fractional GPU(RTAS’ 19)[9]是一篇基于 MPS 的资源隔离方案。其共享模块在 Nvidia driver 里面。该方案的隔离非常硬,核心就是绑定。在计算隔离方面,它通过给任务绑定一定比例的可使用 SM,就可以天然地实现计算隔离。MPS 的计算隔离是通过限制任务的 thread 数,相较于 Fractional GPU 会限制地更加不准确。在显存隔离方面,作者深入地研究 Nvidia GPU 内存架构(包括一些逆向工程)图六是 Fractional GPU 通过逆向得到的 Nvidia GPU GTX 970 的存储体系架构。通过页面着色(Page Coloring)来完成显存隔离。页面着色的思想也是将特定的物理页分配给 GPU SM 分区,以限制分区间互相抢占的问题。该隔离方案整体上来说有一定损耗,而且只能使用规定好的资源比例,不能够灵活地检测和使用全部空闲资源。另外使用该方案需要修改用户代码。代码开源在[10]。

img
img
图六/Fractional GPU 通过逆向得到的 Nvidia GPU GTX 970 的存储体系架构

Mig( MULTI-INSTANCE GPU)[21]是今年 A100 机器支持的资源隔离方案,Nvidia 在最底层硬件上对资源进行了隔离,可以完全地做到计算/通信/配置/错误的隔离。它将 SM 和显存均匀地分给 GPU instance,最多支持将 SM 分 7 份(一份 14 个),显存分 8 份(1 份 5GB)。顺带一提 A100 有 SM108 个,剩下的 10 个将无法用上。它可选的配置也是有限制的,如图七所示。

img
img

img
img
图七/Mig GPU Instance 配置

并行模式

并行模式指任务是以何种方式在同一个 GPU 上运行的。目前有两种方式:

  • 分时复用。指划分时间片,让不同的任务占据一个独立的时间片,需要进行上下文切换。在这种模式下,任务实际上是并发的,而不是并行的,因为同一时间只有一个任务在跑。
  • 合并共享。指将多个任务合并成一个上下文,因此可以同时跑多个任务,是真正意义上的并行
  • 在生产环境中,更多使用分时复用的方式。

分时复用

分时复用的模式大家都较为熟悉,CPU 程序的时间片共享已经非常常见和易用,但在 GPU 领域还有一些工作要做。

如果在 Nvidia GPU 上直接启动两个任务,使用的就是时间片共享的方式。但该模式存在多任务干扰问题:即使两个机器学习任务的 GPU 利用率和显存利用率之和远小于 1,单个任务的 JCT 也会高出很多。究其原因,是因为计算碰撞,通信碰撞,以及 GPU 的上下文切换较慢。

计算碰撞

计算碰撞很好理解,如果切换给另一个任务的时候,本任务正好在做 CPU 计算/IO/通信,而需要 GPU 计算时,时间片就切回给本任务,那么就不会有 JCT 的影响。但两个任务往往同时需要使用 GPU 资源。

通信碰撞

通信碰撞,是指任务同时需要使用显存带宽,在主机内存和设备显存之间传输数据。

GPU 上下文切换慢

GPU 上下文切换慢,是相对 CPU 而言的。CPU 上下文切换的速度是微秒级别,而 GPU 的切换在毫秒级别。在此处也会有一定的损耗。下图是分时复用模式的常见架构。

分时复用架构图
分时复用架构图

上文提到的 Gaia,KubeShare,rCuda,vCuda,qCuda,cGPU,vGPU 均为分时复用的模式。由于上文所述的问题,他们的单个任务完成时间(JCT)都会受到较大的影响。V100 测试环境下,两个任务同时运行,其 JCT 是单个任务运行时的 1.4 倍以上。因此在生产环境下,我们需要考虑如何减少任务之间互相影响的问题。上述方案都没有考虑机器学习的特性,只要共享层接收到 kernel 下发,检查没有超过设置上限,就会继续向下传递。另外也限制任务显存的使用不能超过设置上限,不具备弹性。因此针对特定的生产场景,有一些工作结合机器学习任务的特性,进行了资源的限制及优化。

服务质量(QoS)保障

在生产环境的 GPU 集群中常会有两类任务,代称为高优先级任务和低优先级任务:

  • 高优任务是时间敏感的,在它需要资源时需要立刻提供给它。
  • 而低优任务是时间不敏感的,当集群有资源没被使用时,就可以安排它填充资源缝隙以提高集群利用率。
  • 因此共享模块需要优先保障高优先级任务的 JCT 不受影响,以限制低优任务资源占用的方式。

Baymax(ASPLOS ‘16)[11]通过任务重排序保障了高优任务的 QoS。Baymax 作者认为多任务之间的性能干扰通常是由排队延迟和 PCI-e 带宽争用引起的。也就是说,当高优任务需要计算或 IO 通信时,如果有低优的任务排在它前面,高优任务就需要等待,因此 QoS 无法保障。针对这两点 Baymax 分别做了一些限制:

  • 在排队延迟方面,Baymax 利用 KNN/LR 模型来预测持续时间。然后 Baymax 对发给 GPU 的请求进行重新排序。简单来说,就是共享模块预测了每个请求的执行时间,当它认为发下去的请求 GPU 还没执行完时,新下发的请求就先进入队列里。同时将位于队列中的任务重排序,当需要下发请求时,先下发队列中的高优任务请求
  • 在 PCI-e 带宽争用方面,Baymax 限制了并发数据传输任务的数量。Baymax 作者在第二年发表了 Prophet(ASPLOS ‘17)[12],用于预测多任务共置时 QoS 的影响程度。在论文最后提到的实验中,表示如果预测到多个任务不会影响 QoS,就将其共置,但此处共置使用的是 MPS,也就是没有使用分时复用的模式了。在该研究中,预测是核心。预测准确性是否能适应复杂的生产环境,预测的机器负载是否较大,还暂不清楚。

来自阿里的 AntMan(OSDI ‘20)[13]也认为排队延迟和带宽争用是干扰的原因,不同的是,它从 DL 模型的特点切入,来区分切换的时机。

在算力限制方面,AntMan 通过限制低优任务的 kernel launch 保证了高优任务的 QoS。图九是 AntMan 算力共享机制的对比。AntMan 算力调度最小单元,在论文中描述似乎有些模糊,应该是 Op(Operator),AntMan“会持续分析 GPU 运算符的执行时间“,并在空隙时插入另一个任务的 Op。但如何持续分析,论文中并没有详细描述。在显存隔离方面,AntMan 没有限制显存的大小,而是尽力让两个任务都能运行在机器上。但两个或多个任务的显存申请量可能会造成显存溢出,因此 AntMan 做了很多显存方面的工作。首先需要了解任务在显存中保存的内容:首先是模型,该数据是大小稳定的,当它在显存中时,iteration 才可以开始计算。论文中表示 90%的任务模型使用 500mb 以内的显存。其次是 iteration 开始时申请的临时显存,这部分显存理论上来说,在 iteration 结束后就会释放。但使用的机器学习框架有缓存机制,申请的显存不会退回,该特性保障了速度,但牺牲了共享的可能性。因此 AntMan 做了一些显存方面最核心的机制是,当显存放不下时,就转到内存上。在此处论文还做了很多工作,不再尽述。

论文描述称 AntMan 可以规避总线带宽争用问题,但似乎从机制上来说无法避免。除此之外,按照 Op 为粒度进行算力隔离是否会需要大量调度负载也是一个疑问,另外 Op 执行时间的差异性较大,尤其是开启 XLA 之后,这也可能带来一些不确定性。该方案需要修改机器学习框架(Tensorflow 和 Pytorch),对用户有一定的影响。代码开源在[20],目前还是 WIP 项目(截止 2020/11/18),核心部分(local-coordinator)还没能开源。

img
img
图九/AntMan 算力共享机制的对比

如果最小调度单元是 iteration,则会更加简单。首先需要了解一下 DL 训练的特征:训练时的最小迭代是一个 iteration,分为四个过程:IO,进行数据读取存储以及一些临时变量的申请等;前向,在此过程中会有密集的 GPU kernel。NLP 任务在此处也会有 CPU 负载。后向,计算梯度更新,需要下发 GPU kernel;更新,如果非一机一卡的任务,会有通信的过程。之后更新合并后的梯度,需要一小段 GPU 时间。可以看出前向后向和通信之后的更新过程,是需要使用 GPU 的,通信和 IO 不需要。因此可以在此处插入一些来自其他任务的 kernel,同时还可以保证被插入任务的 QoS。更简单的方式是,通过在 iteration 前后插入另一个任务的 iteration 来完成共享。当然这样就无法考虑通信的空隙,可以被理解是一种 tradeoff。另外也因为 iteration 是最小调度单元,避免了计算资源和显存带宽争用问题。另外,如果不考虑高优任务,实现一个退化版本,贪心地放置 iteration 而不加以限制。可以更简单地提高集群利用率,也可以让任务的 JCT/排队时间减小。

针对推理的上下文切换

在上文中描述了分时复用的三个问题,其中上下文切换是一个耗时点。来自字节跳动的 PipeSwitch(OSDI ‘20)[14]针对推理场景的上下文切换进行了优化。具体生产场景是这样的:训练推理任务共享一张卡,大多数时候训练使用资源。当推理请求下发,上下文需要立刻切换到推理任务。如果模型数据已经在显存中,切换会很快,但生产环境中模型一般较大,训练和推理的模型不能同时加载到显存中,当切换到推理时,需要先传输整个模型,因此速度较慢。

在该场景下,GPU 上下文切换的开销有:(1)任务清理,指释放显存。(2)任务初始化,指启动进程,初始化 Cuda context 等。(3)Malloc。(4)模型传输,从内存传到显存。

在模型传输方面,PipeSwitch 作者观察到,和训练不同的是推理只有前向过程,因此只需要知道上一层的输出及本层的参数就可以开始计算本层。目前的加载方式是,将模型数据全部加载到显存后,才会开始进行计算,但实际上如果对 IO 和计算做 pipeline,只加载一层就开始计算该层,就会加快整体速度。当然直接使用层为最小粒度可能会带来较大开销,因此进行了 grouping 合并操作。图十显示了 pipeline 的对比。在任务清理和初始化方面,设置了一些常驻进程来避免开销。最后在 Malloc 方面也使用了统一的内存管理来降低开销。可以说做的非常全面。由于需要获知层级结构,因此需要对 Pytorch 框架进行修改,对用户有一定影响。代码开源在[19].

img
img
图十/PipeSwitch pipeline 的对比

合并共享

合并共享是指,多个任务合并成一个上下文,因此可以共享 GPU 资源,同时发送 kernel 到 GPU 上,也共同使用显存。最具有代表性的是 Nvidia 的 MPS[15]。

  • 优势:当任务使用的资源可以同时被满足时,其 JCT 就基本没有影响,性能可以说是最好的。可以充分利用 GPU 资源。
  • 缺点:错误会互相影响,如果一个任务错误退出(包括被 kill),如果该任务正在执行 kernel,那么和该任务共同 share IPC 和 UVM 的任务也会一同出错退出。目前还没有工作能够解决这一问题,Nvidia 官方也推荐使用 MPS 的任务需要能够接受错误影响,比如 MPI 程序。因此无法在生产场景上大规模使用。另外,有报告称其不能支持所有 DL 框架的所有版本。

在资源隔离方面,MPS 没有显存隔离,可以通过限制同时下发的 thread 数粗略地限制计算资源。它位于 Nvidia Driver 之上。图十一是 MPS 的架构图。

MPS 架构图
MPS 架构图

Salus(MLSys ‘20)[16]也采取了合并共享的方式,作者通过 Adaptor 将 GPU 请求合并到同一个 context 下,去掉了上下文切换。当然,和 MPS 一样会发生错误传播,论文中也没有要解决这一问题,因此无法在生产环境中使用。但笔者认为这篇论文中更大的价值在显存和调度方面,它的很多见解在 AntMan 和 PipeSwitch 中也有体现。调度方面,以 iteration 为最小粒度,并且诠释了原因:使用 kernel 为粒度,可以进一步利用资源,但会增加调度服务的开销。因此折中选择了 iteration,可以实现性能最大化。显存方面,一些观察和 AntMan 是一致的:显存变化具有周期性;永久性显存(模型)较小,只要模型在显存中就可以开始计算;临时性显存在 iteration 结束后就应该释放。也描述了机器学习框架缓存机制的死锁问题。不过 Salus 实现上需要两个任务所需的显存都放到 GPU 显存里,没有置换的操作。论文中也提到了推理场景下的切换问题:切换后理论上模型传输时间比推理延迟本身长几倍。除此之外论文中也有一些其他的观察点,值得一看。图十二展示了 Salus 架构。该项目代码开源在[17]。Salus 也需要修改 DL 框架。作者也开源了修改后的 tensorflow 代码。

Salus 架构图
Salus 架构图

如果在合并共享模块之上做分时复用,应可以绕过硬件的限制,精准地控制时间片和切换的时机,也可以去除上下文切换的开销。但在这种情况下是否还会有错误影响,还需要进一步验证。

场景展望

目前 GPU 共享已经逐渐开始进入工业落地的阶段了,若需要更好地满足用户对使用场景的期待,除了更高的性能,笔者认为以下几点也需要注意。

  • 能够提供稳定的服务,运维便捷。比如 MPS 的错误影响是不能被接受的,另外对于带有预测的实现,也需要更高的稳定性。共享工作负载尽量降低。
  • 更低的 JCT 时延,最好具有保障部分任务 QoS 的能力。对于一个已有的 GPU 集群进行改造时,需要尽量减少对已有的用户和任务的影响。
  • 不打扰用户,尽量不对用户的代码和框架等做修改,另外也需要考虑框架和其他库的更新问题。

GPU 虚拟化方案

Nvidia vGPU

nvidia-vgpu

NVIDIA Virtual GPU 允许多虚拟机能够同时直接访问单个物理 GPU 的能力,只需要在虚拟机上装上与宿主机相同的驱动设备。通过这种方式,NVIDIA vGPU 给多个虚拟机非并行化图形性能,以及应用的兼容性,在不同负载间来共享一个 GPU。

Diagram showing the high-level architecture of NVIDIA vGPU
Diagram showing the high-level architecture of NVIDIA vGPU

NVIDIA 在 vGPU 技术上提供了 2 种模式,GPU passthrough 和 Bare-Metal Deployment。GPU passthrough 模式相当于独占,不允许虚拟机之间共享设备,Bare-Metal 相当于共享模式。GRID 技术的 Bare-Metal 通过 vfio-mdev 提供了一个隔离性非常高的的硬件环境(不是模拟简单的模拟硬件),这个虚拟化技术并不会对性能有很大的伤害,对多租户需要强隔离的平台是一个很好的选择。

但是这个技术目前来看主要针对的是虚拟机平台,在技术特性方面也有明确写出某些功能不支持,其次 NVIDIA GRID 技术需要购买 NVIDIA 公司的软件授权才能使用,这个授权费相当昂贵。

Time-Sliced Nvidia vGPU Internel Architecture

通过分时复用实现对于 GPU 的共享:

Diagram showing the internal architecture of a time-sliced NVIDIA vGPU
Diagram showing the internal architecture of a time-sliced NVIDIA vGPU

Since 11.1 MIG-Backend Nvidia vGPU Internal Architecture

Diagram showing the internal architecture of a MIG-backed NVIDIA vGPU
Diagram showing the internal architecture of a MIG-backed NVIDIA vGPU

Nvidia MPS

nvidia-mps

NVIDIA MPS 技术 NVIDIA 对 GPU 共享的最早的一种支持模式,通过 MPS server 和 MPS client 就可以让多个 GPU 任务共享 GPU 的计算能力。对于容器平台,这种共享 GPU 的方式是一种可行性的选择。

不过,这种指令代理技术有一个弊端,就是如果 MPS Server 挂掉或者其他 MPS client 端造成的非正常性退出,会导致处于同一个 MPS server 下的所有 MPS client 都受到影响,这种影响对于提供共享服务的平台来说是灾难性的。

Gaia vCUDA

它通过劫持对 Cuda driver API 的调用来做到资源隔离。劫持的调用如图二所示。具体实现方式也较为直接,在调用相应 API 时检查:(1)对于显存,一旦该任务申请显存后占用的显存大小大于 config 中的设置,就报错。(2)对于计算资源,存在硬隔离和软隔离两种方式,共同点是当任务使用的 GPU SM 利用率超出资源上限,则暂缓下发 API 调用。不同点是如果有资源空闲,软隔离允许任务超过设置,动态计算资源上限。而硬隔离则不允许超出设置量。该项目代码开源在[2]。实测即使只跑一个任务也会有较大 JCT 影响,可能是因为对资源的限制及守护程序的资源占用问题。

vCUDA 的系统架构与 NVIDIA 的 GRID 架构类似,采用一个 Manager 来管理 GPU,Manager 负责配置容器的 GPU 计算能力和显存资源,做到使用者无法使用多余申请的显存,GPU 的平均使用率不会大幅超出申请值。vCUDA 的设计采用零入侵设计,用户的程序无需重新编译就可以运行在 GaiaStack 平台进行 GPU 共享。

img
img

vCUDA 使用修改后 cuda library 来达到资源控制,vCUDA 分别修改了计算操作,显存操作和信息获取 3 个方面的 API。

Alibaba cGPU

来自阿里的 cGPU[7],其共享模块在 Nvidia driver 层之上,也就是内核态。由于是在公有云使用,相对于用户态的共享会更加安全。它也是通过劫持对 driver 的调用完成资源隔离的,通过设置任务占用时间片长度来分配任务占用算力,但不清楚使用何种方式精准地控制上下文切换的时间。值得一提的是,由于 Nvidia driver 是不开源的,因此需要一些逆向工程才可以获得 driver 的相关 method 的 name 和 ioctl 参数的结构。该方案在使用上对用户可以做到无感知,当然 JCT 是有影响的。代码没有开源,也没有论文。图四是 cGPU 的架构图。

Alibaba AntMan

来自阿里的 AntMan(OSDI ‘20)[13]也认为排队延迟和带宽争用是干扰的原因,不同的是,它从 DL 模型的特点切入,来区分切换的时机。

在算力限制方面,AntMan 通过限制低优任务的 kernel launch 保证了高优任务的 QoS。图九是 AntMan 算力共享机制的对比。AntMan 算力调度最小单元,在论文中描述似乎有些模糊,应该是 Op(Operator),AntMan“会持续分析 GPU 运算符的执行时间“,并在空隙时插入另一个任务的 Op。但如何持续分析,论文中并没有详细描述。在显存隔离方面,AntMan 没有限制显存的大小,而是尽力让两个任务都能运行在机器上。但两个或多个任务的显存申请量可能会造成显存溢出,因此 AntMan 做了很多显存方面的工作。首先需要了解任务在显存中保存的内容:首先是模型,该数据是大小稳定的,当它在显存中时,iteration 才可以开始计算。论文中表示 90%的任务模型使用 500mb 以内的显存。其次是 iteration 开始时申请的临时显存,这部分显存理论上来说,在 iteration 结束后就会释放。但使用的机器学习框架有缓存机制,申请的显存不会退回,该特性保障了速度,但牺牲了共享的可能性。因此 AntMan 做了一些显存方面最核心的机制是,当显存放不下时,就转到内存上。在此处论文还做了很多工作,不再尽述。

论文描述称 AntMan 可以规避总线带宽争用问题,但似乎从机制上来说无法避免。除此之外,按照 Op 为粒度进行算力隔离是否会需要大量调度负载也是一个疑问,另外 Op 执行时间的差异性较大,尤其是开启 XLA 之后,这也可能带来一些不确定性。该方案需要修改机器学习框架(Tensorflow 和 Pytorch),对用户有一定的影响。代码开源在[20],目前还是 WIP 项目(截止 2020/11/18),核心部分(local-coordinator)还没能开源。

AntMan算力共享机制的对比
AntMan算力共享机制的对比

如果最小调度单元是 iteration,则会更加简单。首先需要了解一下 DL 训练的特征:训练时的最小迭代是一个 iteration,分为四个过程:IO,进行数据读取存储以及一些临时变量的申请等;前向,在此过程中会有密集的 GPU kernel。NLP 任务在此处也会有 CPU 负载。后向,计算梯度更新,需要下发 GPU kernel;更新,如果非一机一卡的任务,会有通信的过程。之后更新合并后的梯度,需要一小段 GPU 时间。可以看出前向后向和通信之后的更新过程,是需要使用 GPU 的,通信和 IO 不需要。因此可以在此处插入一些来自其他任务的 kernel,同时还可以保证被插入任务的 QoS。更简单的方式是,通过在 iteration 前后插入另一个任务的 iteration 来完成共享。当然这样就无法考虑通信的空隙,可以被理解是一种 tradeoff。另外也因为 iteration 是最小调度单元,避免了计算资源和显存带宽争用问题。另外,如果不考虑高优任务,实现一个退化版本,贪心地放置 iteration 而不加以限制。可以更简单地提高集群利用率,也可以让任务的 JCT/排队时间减小。

ByteDance PipeSwitch

在上文中描述了分时复用的三个问题,其中上下文切换是一个耗时点。来自字节跳动的 PipeSwitch(OSDI ‘20)[14]针对推理场景的上下文切换进行了优化。具体生产场景是这样的:训练推理任务共享一张卡,大多数时候训练使用资源。当推理请求下发,上下文需要立刻切换到推理任务。如果模型数据已经在显存中,切换会很快,但生产环境中模型一般较大,训练和推理的模型不能同时加载到显存中,当切换到推理时,需要先传输整个模型,因此速度较慢。

在该场景下,GPU 上下文切换的开销有:(1)任务清理,指释放显存。(2)任务初始化,指启动进程,初始化 Cuda context 等。(3)Malloc。(4)模型传输,从内存传到显存。

在模型传输方面,PipeSwitch 作者观察到,和训练不同的是推理只有前向过程,因此只需要知道上一层的输出及本层的参数就可以开始计算本层。目前的加载方式是,将模型数据全部加载到显存后,才会开始进行计算,但实际上如果对 IO 和计算做 pipeline,只加载一层就开始计算该层,就会加快整体速度。当然直接使用层为最小粒度可能会带来较大开销,因此进行了 grouping 合并操作。图十显示了 pipeline 的对比。在任务清理和初始化方面,设置了一些常驻进程来避免开销。最后在 Malloc 方面也使用了统一的内存管理来降低开销。可以说做的非常全面。由于需要获知层级结构,因此需要对 Pytorch 框架进行修改,对用户有一定影响。代码开源在[19].

img
img

GPU 并行模式

并行模式指任务是以何种方式在同一个 GPU 上运行的。目前有两种方式:

  • 分时复用:指划分时间片,让不同的任务占据一个独立的时间片,需要进行上下文切换。在这种模式下,任务实际上是并发的,而不是并行的,因为同一时间只有一个任务在跑。
  • 合并共享:指将多个任务合并成一个上下文,因此可以同时跑多个任务,是真正意义上的并行。在生产环境中,更多使用分时复用的方式。

分时复用

分时复用的模式大家都较为熟悉,CPU 程序的时间片共享已经非常常见和易用,但在 GPU 领域还有一些工作要做。如果在 Nvidia GPU 上直接启动两个任务,使用的就是时间片共享的方式。但该模式存在多任务干扰问题:即使两个机器学习任务的 GPU 利用率和显存利用率之和远小于 1,单个任务的 JCT(单个任务完成时间)也会高出很多。

究其原因,是因为计算碰撞,通信碰撞,以及 GPU 的上下文切换较慢。

  • 计算碰撞很好理解,如果切换给另一个任务的时候,本任务正好在做 CPU 计算/IO/通信,而需要 GPU 计算时,时间片就切回给本任务,那么就不会有 JCT 的影响。但两个任务往往同时需要使用 GPU 资源。
  • 通信碰撞,是指任务同时需要使用显存带宽,在主机内存和设备显存之间传输数据。
  • GPU 上下文切换慢,是相对 CPU 而言的。CPU 上下文切换的速度是微秒级别,而 GPU 的切换在毫秒级别。在此处也会有一定的损耗。

图八是分时复用模式的常见架构:

media/image3.png
media/image3.png

上文提到的 Gaia,KubeShare,rCuda,vCuda,qCuda,cGPU,vGPU 均为分时复用的模式。由于上文所述的问题,他们的单个任务完成时间(JCT)都会受到较大的影响。V100 测试环境下,两个任务同时运行,其 JCT 是单个任务运行时的 1.4 倍以上。因此在生产环境下,我们需要考虑如何减少任务之间互相影响的问题。上述方案都没有考虑机器学习的特性,只要共享层接收到 kernel 下发,检查没有超过设置上限,就会继续向下传递。另外也限制任务显存的使用不能超过设置上限,不具备弹性。因此针对特定的生产场景,有一些工作结合机器学习任务的特性,进行了资源的限制及优化。

QoS 保障

合并共享

合并共享是指,多个任务合并成一个上下文,因此可以共享 GPU 资源,同时发送 kernel 到 GPU 上,也共同使用显存。最具有代表性的是 Nvidia 的 MPS[15]。该模式的好处是显而易见的,当任务使用的资源可以同时被满足时,其 JCT 就基本没有影响,性能可以说是最好的。可以充分利用 GPU 资源。但坏处也是致命的:错误会互相影响,如果一个任务错误退出(包括被 kill),如果该任务正在执行 kernel,那么和该任务共同 share IPC 和 UVM 的任务也会一同出错退出。目前还没有工作能够解决这一问题,Nvidia 官方也推荐使用 MPS 的任务需要能够接受错误影响,比如 MPI 程序。因此无法在生产场景上大规模使用。另外,有报告称其不能支持所有 DL 框架的所有版本。

在资源隔离方面,MPS 没有显存隔离,可以通过限制同时下发的 thread 数粗略地限制计算资源。它位于 Nvidia Driver 之上

media/image4.png
media/image4.png

参考资料