双引擎GPU容器虚拟化,用户态和内核态技术解析和实践分享
发布时间:2023-01-07 09:03 所属栏目:125 来源:互联网
导读:如何让硬件算力发挥最大效率,是所有资源运营商和用户非常关注的问题。百度作为一家领先的 AI 公司,拥有可能是业界最全的 AI 应用场景。 在这篇文章中,将和大家分享和讨论 GPU 容器虚拟化在复杂AI场景中的解决方案和厂内的最佳实践。 下面这张图片的左右两
如何让硬件算力发挥最大效率,是所有资源运营商和用户非常关注的问题。百度作为一家领先的 AI 公司,拥有可能是业界最全的 AI 应用场景。 在这篇文章中,将和大家分享和讨论 GPU 容器虚拟化在复杂AI场景中的解决方案和厂内的最佳实践。 下面这张图片的左右两部分,在不同场合下已经多次展示过,放到这里主要想强调算力需求 —— 硬件算力的指数型增长,与真实应用场景中利用率偏低资源浪费之间的矛盾。 左边的部分是 OpenAI 统计的数据,从 2012 年以来,模型训练所需的算力每 3.4 个月翻一倍,截止到 AlphaGoZero 这类的大模型,训练算力已经增长了 30 万倍,并且这种趋势还在继续。一方面,随着算力需求的增长,主流 AI 加速单元计算性能也在以每两年翻一倍的速率增加。另一方面,资源利用效率却制约着硬件效能的充分发挥。 右边的部分是 Facebook 在 2021 年对数据中心 Machine Learning 负载分析的结果。大量的 AI 算力损失在故障、调度、时间片浪费、空间单元浪费等环节,真正的算力利用率不到 30%。我们相信,这也是国内各大基础设施运营商所面临的现状。 图片 刚才提到在线集群不到 30% 利用率可能不符合很多同学的认知。在线的很多同学可能是模型和算法的开发者。我们普遍的认知是,在训练和测试过程中利用率可以保持很高的水平,甚至可以达到 100% 利用率。 但模型在生产环境上线,会受到很多约束,这些约束导致利用率远远达不到我们的预期。 下面我们用有限的篇幅总结一下主要的制约因素: 模型特点:每个模型网络不同,调用的底层算子组合不同,很大程度上会影响 GPU 的利用率。 服务 SLA:不同场景下的服务需要不同的 SLA,有的服务实时性要求较高,甚至需要严格控制在 10ms 以内,那么这些服务就不能通过增加 batchsize 的方式提升利用率,甚至 batchsize 只能为 1。 流量模式:不同模型算法服务于不同的应用场景,比如 OCR 识别,可能在工作期间被频繁调用。而语音识别则更多的在通勤时间或者娱乐休闲时才会被调用,这样就导致了一天中 GPU 利用率的峰谷波动。 优化效果:根据模型的迭代频率以及覆盖场景的不同,模型的优化粒度也不尽相同。可想而知,一个未经充分优化的模型利用率也很难达到较高的水平。 容量冗余:模型上线前都要经过详细的容量规划,最大流量是多少,是否需要多地域,在此过程中会预留难以忽略的容量冗余,这些冗余在平时也造成了算力的浪费。 图片 在上面种种约束条件的制约下,真实生产环境的利用率可能是接下来我们要展示的。我们从复杂多变的在线生产环境中抽象出这几种利用率模式。 均值偏低型:如左上图,为一个真实的在线推理业务,由于模型特点和服务 SLA 的限制,GPU 的峰值利用率只有 10%,平均利用率会更低。 峰谷波动型:如左下图,是典型的在线推理业务的利用率模式,服务在白天会达到高峰,在深夜至第二天早上是利用率的低谷,全天平均利用率只有 20% 左右,低谷利用率只有 10% 不到。 短时激增型:如右上图,利用率曲线基本与左下图一致,但在夜间黄金时段会有两个明显的利用率高峰,高峰阶段的利用率高达 80%,为了满足高峰阶段的服务质量,该服务在部署过程中会预留不小的 buffer,资源平均利用率也刚刚超过 30%。 周期触发型:如右下图,是典型在线训练场景的利用模式,在线训练任务介于离线训练和在线推理之间,这是一种周期性批处理的任务。例如每 15 分钟会有一批数据到达,但这批数据的训练只需要 2-3 分钟,大量的时间 GPU 处于闲置状态。 AI应用场景复杂多变,上面只是列举了四种典型场景。如何在复杂场景中,平衡业务性能与资源效率,是我们在 GPU 虚拟化中遇到的第一个挑战。 图片 GPU 虚拟化过程中我们面临的第二个挑战就是缺乏完善的 GPU 隔离与混布机制。 我们以目前主流的 NVIDIA GPU 为例。典型的 AI 软硬件生态都分为这样几个层次 ——应用 & 框架层,运行时层,驱动层,硬件层。 首先最上层是用户的应用,这里包含了各种常见的框架 PaddlePaddle、TensorFlow、PyTorch 等等。在应用层之下是硬件提供商封装的 API 接口层,包含各类常用算子库与硬件运行时访问接口。在这层 API 接口之下,是与硬件沟通的驱动层,该层位于内核态,是直接与设备沟通的软件接口层。位于最底层是真正的 AI 加速硬件,负责算子的执行。 传统的虚拟化方案,都会结合驱动内核态以及硬件虚拟化逻辑实现。这两个层次是硬件提供商最核心的 IP,一般是闭源的。后续会提到,当前 GPU 原生的的隔离机制在灵活性和分配力度上都无法满足云原生场景下的使用需求。 除了隔离机制,现有的混布机制也很难满足复杂场景的需求,我们看到业界有很多共享调度的开源方案,这些开源方案只是从资源的层面把两个任务简单的调度到一张卡上。在实际场景中,简单的共享会造成业务之间相互影响,长尾延迟甚至吞吐的恶化导致简单共享无法真正应用于生产环境。 在上文利用率模式分析一节我们看到不同的业务,不同的场景下,利用率模式都不尽相同。如何抽象业务场景,定制混布方案,是生产环境落地的关键。 图片 为了让大家更全面地了解 GPU 的发展以及虚拟化历史,这里我们用一张图来展示 GPU 虚拟化发展史。 GPU 应用于通用计算最早可以追溯到 G80 时代的 Tesla 架构,是第一代实现统一着色器的架构,用通用处理器 SM 替代了原来的顶点、像素管线分离的图形图像处理器。 百度最早引进的 GPU 可以追溯到 Fermi 架构。从这个时间点开始,业界就出现了一批虚拟化方案,其中大部分以 API 劫持为主。这里的典型代表是 rCUDA,该项目最初由学术团体维护,直到近期,还保持一定频率的更新和迭代,但看起来以学术研究为主,并没有在生产环境大范围使用。 百度大规模引入 GPU 是在 Kepler 架构,Kepler 架构开启了百度自研的超级AI计算机X-MAN 时代。X-MAN 1.0 首次实现单机 16 卡配置,可以在 PCIe 硬件层面实现 CPU 和 GPU 的动态绑定和灵活配比。受限于单卡性能,当时更多的考虑是扩展,而不是切分。 随后的 Pascal 架构、Volta 架构、Turing 架构性能有了飞速提升,这时虚拟化的需求日益显著起来。我们看到,最早从 Kepler 架构,NV 官方提供了 GRID vGPU 虚拟化方案,最开始主要是面向图形渲染和远程桌面场景。在 2019 年前后,针对 AI 和高性能计算场景也提供了解决方案。但这些方案都是基于虚机的,在 AI 场景中很少使用。 在 Ampere 这一代,NV 推出了 MIG 实例切分方案,该方案在硬件层面实现了 SM、MEM、L2 Cache 等多种硬件资源的切分,提供了良好的硬件隔离性能。但该方案从Ampere 架构开始支持,且对于卡的型号还有一定的限制。只有 A100、A30 少数几个型号可以支持。而且即使切分之后,单个实例的性能也超过了 T4 算力,并无法很好地解决当前生产环境的效率问题。 图片 大家对 GPU 架构与虚拟化历史脉络有了一些印象之后,我们来详细介绍下实现 GPU虚拟化的几个主要层次,或者说是技术路线。 实现资源虚拟化隔离,首先需要资源在时间或空间维度是可分的,在用户视角看来就是多个任务可以并发(concurrent)或并行(parallel)地执行。 这里我们在用户态、内核态、硬件多个层次上讨论一下并行或并发空间。 由于 NV 的软硬件生态是闭源的,这里的示意图是我们综合架构白皮书,逆向论文和我们自己的理解而绘制的,不准确的地方还希望大家及时指正。 用户态方案 我们从上至下来看这张图,首先多个进程在 GPU 来看,天然就是并发的,也就是时分复用的。驱动和硬件负责以时间片轮转的方式进行任务的切换。利用这层机制,我们可以在 API 层面上实现计算资源、显存资源的限制,达到虚拟化的效果。这里的 API 可以分为两层,一层是驱动 API,这层 API 紧贴驱动,是所有上层调用访问 GPU 必经之路,只要控制了这层 API,就相当于控制了用户的资源访问。这里先提一句, NV 提供的 MPS 技术可以实现空分复用,这也为业务性能的进一步优化提供了可能。在后续落地实践部分我们会详细展开。 内核态方案 再往下一层是内核态,无论是虚机层面的全虚拟化、半虚拟化,还是近两年各大云厂商的容器方案,都是在内核层实现了系统调用拦截和 MMIO 劫持,内核态最大的困难在于很多寄存器和 MMIO 行为没有很好的文档说明,这些都需要复杂的逆向工程。 硬件方案 内核态之下是硬件层,真正的并行是在这一层进行保证的,无论是 NV 的 MIG 技术还是百度昆仑的 SR-IOV 技术,都在硬件逻辑上进行了算力切分,实现了真正的并行和空分复用。如昆仑可以实现 1/3,1/2 的硬件划分,A100 可以实现最小 1/7 粒度的资源划分。 上面我们花了不小的篇幅向大家介绍了 GPU 虚拟化的挑战和现状,接下来我们看百度内部是如何应对这些挑战的。 这张图展示了百度智能云 —— 双引擎 GPU 容器虚拟化架构。 这里强调容器,因为我们相信,未来 AI 全链路应用会逐步收敛到云原生平台,实现全容器化开发、训练、推理。据 Gartner 调研显示,2023 年 70% 的 AI 任务将会容器化部署。百度内部容器化从 2011 年就开始了,目前已经有 10 余年的部署和优化经验,我们也致力于将这部分真刀真枪打磨出来的产品能力和优化经验贡献给社区和广大的用户。 这里还强调了双引擎。在整体架构中我们采用了用户态和内核态两套隔离引擎,以满足用户对隔离性、性能、效率等多方面不同侧重的需求。 在隔离引擎之上,是资源池化层,该层次基于我们对软硬件体系深刻理解,逐步实 AI 加速资源的解耦、拉远和池化,是我们面向未来基础设施打造的池化抽象层。 在资源池化层之上,是 Matrix / k8s 统一资源调度层(这里的 Matrix 是百度厂内的容器化调度系统),在调度机制之上,我们会根据不同业务场景,抽象出来多种混布策略,包括共享混布,抢占混布,分时混布,潮汐混布等。这些混布策略,后续实践部分会详细展开。 依托于资源隔离和资源调度之上的是 AI 业务的全链路场景,包括模型开发、模型训练、在线推理。 接下来会分别给大家分享用户态和内核态隔离引擎的实现。 下图是用户态隔离引擎核心架构示意图。位于架构图最上的是用户应用,这里包含了各类常用框架,如PaddlePaddle、TensorFlow、PyTorch等。 位于用户应用之下的是一系列的 API Hook 接口,也是基于这套接口我们可以实现 GPU 资源的本地使用和远程挂载。通过替换框架依赖的底层动态库,实现资源的控制和隔离。需要重点说明的是,该方案对于应用是完全透明的,必要的库替换操作已经由容器引擎和调度部分自动完成。 CUDA API 在 Hook 之后会通过两个通路最终到达执行器。在这里,绝大多数 API ,如设备管理 API 经过 Hook 之后不做任何操作直接 pass-through 给执行器执行。少数和资源申请相关的 API 会经过一层拦截,通过这层拦截实现用户态虚拟化的一系列功能。这层的逻辑实现得足够高效,对性能的影响几乎忽略不计。 目前用户态隔离引擎可以提供丰富的隔离和控制功能,包括基础的显存隔离、算力隔离。我们还扩展了很多高级功能:编码器隔离、高优抢占、显存超发、显存池化等等。 用户态方案的优点是性能好,长尾延迟低,适合追求机制性能、极致效率的业务场景,如延迟敏感的在线推理业务。 在隔离的基础上,我们提供远程功能,远程的引入将大大提升资源配置的灵活度和使用效率,这一点我们将在本文最后展开。 本次分享是一次技术分享,这里用少量篇幅展开一下远程技术的重点和难点,希望能激发大家的业务思路和技术讨论。 根据我们在前文虚拟化挑战中讲到的软硬件技术栈, GPU 的远程访问大致上也可以在硬件链路层、驱动层、运行时层和用户层实现,但经过深入的技术分析并结合对业务场景的理解,我们认为目前最适合的还是运行时层。 确定运行时层技术路线,如何实现?技术的重点是什么?我们认为主要是语义一致性问题。基于运行时的远程,需要把原始的 local 进程拆分为 client、 server 两个进程。CUDA 运行时是闭源的,内部实现逻辑无从探究。如何保证拆分进程后仍保持原有的程序逻辑和 API 语义,这里我们用一对一线程模型保证 API 内部的逻辑和语义对齐。 远程实现的难点是 API 繁多的问题,运行时除了 libcudart.so 这个动态库,还涉及cuDNN、cuBLAS、cuFFT 等一系列动态库和 API,涉及数千个不同的 API 接口。我们用编译技术实现了头文件的自动解析和代码的自动生成,并通过逆向技术完成了隐藏API的解析。 解决远程方案 0-1 适配之后,接下来的向后兼容性其实是比较好解决的。目前看来 CUDA API 相对稳定,新版本只需要少量增量适配即可。 上面多次提到空分复用和时分复用。这里做一下详细的解释: 时分复用:顾名思义,是时间片层面的复用。这里与 CPU 的进程调度类似,在单一时间片内,只有一个 GPU 进程在运行。多个 GPU 进程之间在微观层面上是交替运行的,只能成为并发(concurrent)。这也导致,在某一时间片内,如果该进程无法很好的利用计算资源,这些计算资源就是浪费掉的。 空分复用:与时分复用不同,空分复用时,在某一微观时刻,多个进程是可以同时运行在一个 GPU 上的,只要这个 GPU 的资源没有用满,其它进程的 Kernel 就可以发射上来,两个进程的 Kernel 在微观层面上是交织运行的,真正实现了并行(parallel),进一步利用 GPU 资源。 如综述部分介绍,当前常见的虚拟化方式,包括内核态虚拟化、NVIDIA vGPU虚拟化,在底层实际都是基于时间片轮转的时分复用方案。 NV 面向多进程并发场景推出了 MPS ——多进程服务解决方案,该方案可以做到空分复用,是目前看到同时兼顾效率与性能的方案。 这里简单介绍一下 MPS,MPS 相当于把两个进程的上下文融合成了一个进程,融合后的进程将之前两个进程的 Kernel 交织到一起进行发射。这样做有两个好处: 进程之间无需上下文切换,减少了上下文切换的开销。 同一时刻,不同进程的 kernel 交织,提升了资源空间利用率。 说到 MPS,不得不提被人诟病的一个缺点——故障隔离问题。 如何解决这个 MPS 稳定性问题的呢?百度智能云结合调度、容器引擎、业务保活提出一整套进程融合共享方案。 通过 kill 命令重定向实现业务进程优雅退出 通过 MPS 状态检测机制实现健康检查和假死检测 通过服务保活实现用户进程自动重启 该方案已经覆盖商业(延迟敏感型重要业务)90%+ 资源,并长期运行超两年的时间,在提供极致性能的同时,相信能够满足绝大多数用户对稳定性的需求。 随着 MPS 的接受程度越来越高,NV 也不断增强 MPS 的稳定性。这里可以提前透露一个好消息,NV 在今年下半年会在 MPS 稳定性上大幅度增强,包括假死状态检测,进程优雅退出这些功能都会成为 MPS 产品的一部分,MPS 的稳定性和易用性会进一步提升。 在介绍高优抢占功能之前,先和大家分享一下高优抢占的业务场景。根据我们和厂内外不同用户的讨论,大多数 AI 应用生产环境中按延迟敏感程度可以分为在线、近线、离线这三类任务。 在线任务,对延迟最高,一般是实时响应用户请求的推理任务; 近线任务,是一般是批处理任务,对单条日志的延迟没有要求,但对一批数据的完成时间有小时到分钟级不等的要求; 离线任务,对延迟无要求,只关注吞吐,一般是模型训练类任务。 如果我们把延迟敏感型任务定义为高优任务,把延迟不敏感的近线离线任务定义为低优任务。并在两类任务混布时根据任务优先级不同定义不同的 kernel 发射优先级,就是我们上面提到的高优抢占功能。 实现原理如下图所示,用户态隔离引擎为高优任务和低优任务各自维护了一个逻辑上的 kernel 队列。当整体负载较低时,允许两个队列同时发射 kernel,这时两个队列的 kernel 是交织在一起运行的。一旦负载增高,分级发射模块就会第一时间 pending 低优队列的发射,从而保证高优任务的执行延迟。 该功能的优势是保证离线吞吐的同时,减少甚至避免了在线任务的影响。 同理,我们先介绍分时混布的定义和场景。 分时混布,在混布模式上有点像时间片轮转的共享混布。不同之处在于分时混布针对显存不提出了显存 swap 方案,这样在显存长期占用但算力间歇使用或偶尔触发的场景就派上了用场。当进程需要算力时获取显存的访问权限,当进程完成运算后释放显存的访问权限,让其它等待该权限的进程获得运行机会,让间歇闲置的 GPU 资源得到充分利用。 分时混布的核心技术是显存 swap。我们可以类比 CPU 的内存 swap,当某一进程的内存不够用了,系统会根据一定的策略换出一部分系统内存资源到磁盘,从而腾挪出空间给在运行的进程使用。 显存 swap 的实现原理如下图所示。我们在显存的物理地址上维护了一个显存池,上层通过资源锁来确定哪个进程有权限使用 GPU。当进程获得锁时,显存便会从内存或磁盘搬运到物理显存池中,进一步映射到虚拟地址空间供进程使用。当进程释放锁时,会保留进程的虚拟显存空间,将物理显存搬移到内存或磁盘上。该锁是互斥的,只有一个进程可以获得锁,其它的进程 pending 在等待队列上,以 FIFO 的方式依次获得资源锁。 上面介绍了用户态隔离引擎的功能实现,在实际应用中,性能如何,对用户的影响如何?这里我们直接上测试数据。 下图是我们在公开测试集 MLPerf 上选择典型模型 ResNet-50 Server 场景下的数据对比。图中的柱子从左至右依次表示独占、裸混、用户态隔离、内核态隔离下的性能。 左图是平均吞吐对比,在推理场景下请求是间歇触发的,我们可以看到,无论何种方案在吞吐下都能直接达到发压值。这里想说明,推理场景下吞吐并不能很好的展示虚拟化性能,在生产环境中落地时应该更多的关注延迟。 右图是 P99 分位延迟的对比。可以看到,在低压力下(QPS = 40)用户态,裸混对长尾延迟的影响基本一致,内核态由于采用了时分复用,对长尾延迟影响稍大。我们继续增大压力,在 QPS = 60 时,用户态的优势就显现了,空分复用大大降低了对长尾延迟的影响。随着压力的进一步加大,用户态进程融合方案甚至比其它混布方式有数量级的提升。 尽管长尾延迟控制不如用户态,但在隔离性方面,内核态具备优势,更侧重于对隔离要求有强诉求的场景。 下面我们来了解下内核态隔离引擎的技术实现。 首先来看内核态虚拟化实现的特点,包括如下: 内核态实现;隔离性好:支持显存,算力和故障隔离;显存 MB 级隔离;算力 1% 级分配;支持 P4,V100,T4,A100/A10/A30 等主流 GPU;支持 410 到 510 GPU 驱动版本;用户态运行环境无需任何改变;支持容器化部署。 不同于用户态的实现,内核态虚拟化对 GPU 的隔离功能都是在内核态实现。下图的左半部分是我们内核态虚拟化实现的一个架构图,从底层到上层,分别是 GPU 硬件,内核层,用户层。 硬件层面就是我们的 GPU,这个 GPU 可以裸机的 GPU,也可以是透传的 GPU。 内核层的底下是 GPU 原有的驱动,它实际控制着 GPU 的功能,真正操作 GPU 的都是这个驱动,然后 GPU 驱动上面就是我们实现的 GPU 虚拟化的一个内核模块,也就是 GPU 拦截驱动,就是黄色的部分,包含三部分功能,包括显存拦截,算力拦截和算力调度。分别实现的显存隔离,算力隔离。 用户层,首先是拦截接口。这个接口是由拦截模块提供的,分为两部分:一部分是设备文件接口,一部分是配置拦截模块的接口。设备文件是提供给容器的,我们先来看容器。容器上面是应用,底下是 cuda runtime,在下面是cuda 底层库,包括 driver api/nvml api 等。通过把我们的设备文件提供给容器作为假的设备文件,那么上层 cuda 访问时,就访问的是我们的设备文件,这样就完成了 cuda 底层库对访问 GPU驱动的拦截。 我们在内核的拦截模块,会拦截所有访问的系统调用,拦截并解析,然后把真正的访问,重定向到真正的 GPU 底层驱动。GPU底层驱动处理完后,把结果返回给我们的拦截模块,拦截模块再次进行处理,最后把结果返回给容器里的底层库。 简单来说,就是通过模拟设备文件来拦截底层库对 GPU 驱动的访问,通过拦截、解析和注入等操作,完成显存和算力等拦截。 (编辑:ASP站长网) |
相关内容
网友评论
推荐文章
热点阅读