mGPU 技术揭秘 :新一代 Kubernetes GPU 共享调度方案

技术

picture.image

上一篇文章中,我们详细介绍了 mGPU 作为一个解决方案,所具有的能力、特性和优势。本文是 mGPU 系列文章的第二篇, 将重点介绍提升 GPU 资源利用率的关键一环——新一代 GPU 共享调度技术。

来源 | 火山引擎云原生团队

AI 时代,企业大模型的落地离不开能提供大规模 AI 算力的基础设施。为了更好地支持模型开发、训练和推理等场景,当下的云原生基础设施已不再局限于传统的硬件,也包含了 GPU、RDMA 等各种新兴的异构设备,以及精细化的设备管理方式。

第一篇文章所述,原生 Kubernetes 虽然提供了标准化的异构资源管理能力,但对于最关键的 GPU 资源,仅支持整卡粒度的调度,容器会独占整个 GPU。在一些场景下往往会浪费大量昂贵的 GPU 资源:

  • AI 推理场景: 通常一次只处理一个或一小批输入样本;
  • 高性能计算场景: 一些 HPC 应用会因为 CPU 的瓶颈而对 GPU 的利用率不高;
  • 开发机场景: 研发人员使用 Jupyter Notebook 进行交互式的模型开发,有时只需要较低规格的机器;
  • CI/CD 场景: 流水线往往只需要有限的 GPU 资源运行测试用例。

虽然业界已经有一些 GPU 共享的方案,比如 Time-slicing、MPS、MIG 等,但其在显存与算力的隔离性、故障隔离性、使用的灵活性上或多或少都存在一些问题。

因此,火山引擎 VKE 基于 Kubernetes 原生的 Scheduling Framework 自研了一种 新的 GPU 共享调度方案 ,支持 1% 算力粒度和 1 MiB 显存粒度的容器调度。该 GPU 共享调度方案可以结合火山引擎 mGPU 技术提供性能和故障的隔离,此外也兼容其他多种底层 GPU 虚拟化实现以及整卡分配方式。

问题分析

两层调度

原生的 Kubernetes 调度是指将 Pod 调度到合适的节点上,对于 GPU 资源仅对其数量进行标量的比较。而 mGPU 虚拟化方案则为 GPU 增加了算力和显存两个维度的属性,不再是一个简单的设备个数。

对于单机上的 kubelet 来说,算力和显存会被视为两种独立的扩展资源。在 mGPU 场景下,如果由 kubelet 进行 GPU 级别的调度,可能会导致一个容器被分配到的算力和显存是在两个 GPU 上,实际上无法使用。

因此,算力和显存两种资源的“撮合”需要由调度器来完成。也就是说,调度器不仅需要决策将 Pod 调度到哪个节点,还需要进一步决策将该 Pod 中的各个容器分别调度到该节点的哪些 GPU 上。

卡级别的 Binpack/Spread 策略

原生 Kubernetes 调度器支持节点级别的 Binpack/Spread 策略:Binpack 策略有助于提升节点的分配率,减少节点级别的资源碎片;而 Spread 策略倾向于将 Pod 打散,提升节点级别的故障隔离性。

在引入了两层调度后,GPU 也将作为一种调度的拓扑域。调度器不仅需要考虑节点级别的 Binpack/Spread 策略,也需要支持卡级别的 Binpack/Spread 策略,从而减少卡级别的资源碎片,或者提升卡级别的故障隔离性。

解决方案

系统架构

mGPU 的整体架构如下图所示,各组件的职责如下:

picture.image

  • Scheduler: 中心调度器,基于 Scheduling Framework 扩展 GPUShare Plugin,实现 GPU 共享调度。在本功能中负责:
  • 将 Pod 调度到合适的节点。
  • 将 Pod 中的各个 Container 调度到合适的 GPU 组合上(并将结果记录到 Pod Annotation 中)。
  • mGPU Device Plugin: 单机上的 mGPU 资源管理插件。在本功能中负责:
  • 发布 mGPU 资源 (最终将由 kubelet 上报到 Node 对象中)。
  • 根据调度器的分配结果,将相应的环境变量注入到容器中。

API 定义

节点可用的 mGPU 资源量以 Extended Resources 的形式上报到 node.stastus 字段中,分为算力和显存两个资源维度。例如,一个节点上有 4 个 V100 GPU,每个 V100 的显存为 32 GiB,则该节点对应的 Node 对象如下:


          
apiVersion: v1
          
kind: Node
          
metadata:
          
  name: 10.xx.yy.zz
          
spec:
          
  ...
          
status:
          
  allocatable:
          
    vke.volcengine.com/mgpu-core: "400" # 节点可分配的 GPU 算力,单位为百分比 
          
    vke.volcengine.com/mgpu-memory: "130040" # 节点可分配的 GPU 显存,单位为 MiB
          
  capacity:
          
    vke.volcengine.com/mgpu-core: "400" # 节点总的 GPU 算力,单位为百分比 
          
    vke.volcengine.com/mgpu-memory: "130040" # 节点总的 GPU 显存,单位为 MiB
          
  ...
      

如果一个 Pod 需要使用 mGPU 资源,则需要在 .spec.containers[i].resources 字段中进行申请。例如,一个 Pod 需要 30% 的算力和 1 GiB 显存,则其定义如下:


          
apiVersion: v1
          
kind: Pod
          
metadata:
          
  name: test-mgpu
          
  namespace: default
          
spec:
          
  containers:
          
  - name: app
          
    resources:
          
      limits:
          
        vke.volcengine.com/mgpu-core: "30" # 容器申请的 GPU 算力,单位为百分比
          
        vke.volcengine.com/mgpu-memory: "1024" # 容器申请的 GPU 显存,单位为 MiB
          
      requests:
          
        vke.volcengine.com/mgpu-core: "30" # 容器申请的 GPU 算力,单位为百分比
          
        vke.volcengine.com/mgpu-memory: "1024" # 容器申请的 GPU 显存,单位为 MiB
          
...
      

当 Pod 被调度成功后,调度结果将被填充到以下字段中:


          
apiVersion: v1
          
kind: Pod
          
metadata:
          
  annotations:
          
    vke.volcengine.com/assumed: "true" # 标识 Pod 被调度成功
          
    vke.volcengine.com/gpu-index-container-app: "3" # 容器调度结果,表示名称为 app 的容器被调度到序号为 3 的 GPU 上
          
  name: test-mgpu
          
  namespace: default
          
spec:
          
  containers:
          
  - name: app
          
    resources:
          
      limits:
          
        vke.volcengine.com/mgpu-core: "30"
          
        vke.volcengine.com/mgpu-memory: "1024"
          
      requests:
          
        vke.volcengine.com/mgpu-core: "30"
          
        vke.volcengine.com/mgpu-memory: "1024"
          
  nodeName: 10.xx.yy.zz # Pod 调度结果,表示该 Pod 被调度到 10.xx.yy.zz 节点
          
...
      

调度器 插件

picture.image

在 GPUShare Plugin 中,我们对以下 5 个阶段进行了扩展:

  • PreFilter: 初始化 CycleState,通过快照尽量实现后续阶段中读操作的无锁化。
  • Filter: 进行 GPU 卡级别的算力、显存的资源量准入。
  • Reserve: 当挑选出最优节点后,对该节点上的各个 GPU 组合进行打分,为得分最高的 GPU 组合中的各个 GPU 扣除本次分配的资源量,并将该分配结果缓存在 CycleState 中。
  • Unreserve: 当 Reserve 之后的阶段失败时,调用 Unreserve 插件进行缓存状态的回滚。
  • PreBind: 当最终确定了调度结果后,在 PreBind 扩展点调用 APIServer,将容器级别的调度结果更新到 Pod Annotation 中。

调度算法

GPUShare 插件在 Filter 阶段对各个节点分别进行准入,并在 Reserve 阶段对最优节点上的各个 GPU 组合进行打分。整个过程可以理解为:为一个 Pod 中的各个 Container,判断各个节点上的 GPU 是否可以满足其对算力和显存两种资源的需求;并依据卡级别的 Binpack/Spread 策略,为其分配一个最优的 GPU 组合。本质上是一个 最优化问题

(一)目标函数

Kubernetes 调度器的调度单元是 Pod,一个 Pod 只能调度到一个节点,而一个 Pod 中不同的 Container 可以调度到多张 GPU 卡的组合上。因此,Binpack/Spread 策略在节点级别、卡级别有不同的定义:

  • 节点级别的定义
  • Binpack: 优先选择分配率高的节点。
  • Spread: 优先选择分配率低的节点。
  • 卡级别的定义
  • Binpack: 优先选择分配率高的 GPU 组合
  • Spread: 优先选择分配率低的 GPU 组合

在定义卡级别的 Binpack 策略时,考虑到为了减少碎片,我们优先填满一张卡,从而为剩下的卡留出尽可能多的资源。也就是说,让不同卡之间的资 源 分配率的差别 尽可能的大 (Spread 策略相反)。

方差即为描述一组变量间差异的指标。相比于极差仅体现最大值和最小值之间的差异,方差可以将中间的变量也考虑在内,因此我们选择节点上各 GPU 间资源分配率的方差 作为描述 Binpack/Spread 程度的指标:

Var(X) = E[X^2] - E[X]^2

又因为存在显存和算力两个资源维度,我们分别对其计算得分,然后加权平均。考虑到算力可以压缩,而显存不可压缩,我们把显存维度的默认权重设为 0.7,算力维度的默认权重设为 0.3 (可配置)。最终得分的计算公式为:


        
            

          Score = 0.7 * 显存维度得分 + 0.3 * 算力维度得分
        
      

(二)约束条件

在对各个 GPU 组合进行搜索时,需要满足以下约束条件:

  • GPU 的组合需要在同一个节点上,即一个 Pod 的各个容器需要调度到同一个节点的 GPU 上。
  • GPU 的组合需要能够满足 Pod 的各个容器对显存和算力的需求,即资源量的准入。也就是说,只有能够遍历到树的最底层的路径,才满足约束条件。
  • 除 GPU 外其他调度因素的约束,因此对 GPU 组合的挑选应当在挑选出最优节点之后。

(三)搜索算法

为了求解这个最优化问题,我们需要分别对每个节点上所有可能的 GPU 组合进行搜索。DFS 与 BFS 均可解决,考虑到实现复杂度,我们采用回溯法 (即 DFS),并进行剪枝优化。

以下图为例,假设待调度的 Pod 有 3 个 Container,某个节点上有 3 个 GPU:

picture.image

  • 状态定义
  • 树的每一层代表一个 Container。
  • 节点中的数字代表 GPU 的序号。
  • 根节点代表初始未开始分配时的状态。
  • 搜索方式
  • 以 DFS 的方式进行遍历,尝试为每个 Container 分配一个 GPU,即进行资源量的准入。
  • 在进行下一层的准入时,需考虑该路径上之前所有层的分配结果,作为当前的状态。
  • 结束条件
  • 如果在某个节点上资源量准入失败,则进行剪枝,停止该路径后续的搜索,进行回溯。
  • 如果搜索到最底层,则代表所有的 Container 均可分配足够的 GPU 资源,该路径即为一种可行的 GPU 组合。当遍历到最底层时,对该路径对应的 GPU 组合进行打分,并在遍历过程中动态维护最高分及对应的 GPU 组合。

当整棵树搜索完成时,即可得知该节点上是否存在可行的 GPU 组合。如果存在,亦可得知该节点上最优的 GPU 组合,即得到了该节点对应的容器级别的调度结果。

总结与展望

当前,GPU 共享调度功能和 mGPU 虚拟化技术已经上线火山引擎容器服务 VKE。 经落地验证,其可以帮助用户将 GPU 部署密度提升 500% 以上,将资源利用率提升超过 50%。

此外,VKE 的 GPU 共享调度功能当前已经支持将一个容器调度到多张 GPU 卡上,并且已对社区主流的 Batch 调度器进行了支持。 相关的技术细节将在本系列后续的文章中进行解析,敬请期待。

未来,火山引擎 VKE 将进一步支持 GPU 拓扑感知调度、GPU 混部等更多功能,更好地帮助用户提升 GPU 利用率和 AI 大模型的训练效率。

  • END -

相关链接

[1] Scheduling Framework: https://kubernetes.io/docs/concepts/scheduling-eviction/scheduling-framework/

[2] 节点级别的 Binpack/Spread 策略:https://kubernetes.io/docs/concepts/scheduling-eviction/resource-bin-packing/

[3] 火山引擎 mGPU: www.volcengine.com/docs/6460/132501

[4] 火山引擎 VKE: www.volcengine.com/product/vke

近期活动

时间:8 月 26 日

地点:深圳线下/线上直播

线下参会报名: https://www.bagevent.com/event/8659508

picture.image

301
0
0
0
关于作者
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论