PaaS 平台 GPU 资源复用与超卖探索
PaaS 平台 GPU 资源复用与超卖探索
背景&动机
PaaS平台用户使用上的需求:
- 部分推理服务的实例占用的资源较少,即使只占用1卡也存在浪费,因此希望将多个推理服务的实例部署到同一个GPU上,从而节约成本。
- 由于租户内资源有限,往往只有一两个节点,但有十几个用户的开发机。因此希望支持多个用户的开发机共用一个节点的GPU。
PaaS平台侧从提高集群利用率、增加收入和毛利率的角度出发,也存在下面的需求:
- 通过对未分配/已分配空闲GPU超卖进一步增加收入
- 在节点GPU资源全部调度分配后,以CPU任务的方式继续超卖节点的CPU和内存资源
为了满足用户需求,需要支持GPU复用功能,吸引用户使用平台,同时为了提升集群资源利用率和毛利率,需要进一步探索闲时复用方案,对于已经售卖但处于空闲状态的资源进行超卖。
本文将介绍PaaS侧在解决上面的问题过程中,在GPU资源的复用与超卖方面的一些探索。所谓复用或者超卖,都建立在把同一个GPU同时分配给多个实例的基础上,属于一种GPU共享技术。
- GPU共享一般是指在多个用户或应用程序之间共享GPU资源的技术。这种技术通过虚拟化、资源管理和调度算法,使多个任务能够在同一GPU上运行,从而提高GPU的利用率。
- GPU共享在大模型训练中的用处不大,因为大模型训练往往都是整节点分配资源,所以它的应用场景主要是在推理、开发机。
市场现有方案对比
| 方案类型 | 代表方案 | 局限性 |
|---|---|---|
| 厂商官方解决方案 | Nvidia MIG、Nvidia MPS、Nvidia vGPU | 授权成本高;使用场景局限性大;不好灵活配置;不利于整合不同GPU厂商功能;不利于扩展超卖等业务功能 |
| 基于API劫持的隔离共享方案 | 用户态vCUDA、OrionX,内核态cGPU | 有侵入业务层;开发维护难度大;存在性能损失;整套复杂系统ROI不符合预期 |
| 轻量开源调度共享方案 | Aliyun gpushare-device-plugin、gpushare-scheduler-extender | 依赖调度逻辑变更;与已有体系存在冲突;不利于进一步扩展 |
我们的选择:自研轻量化共享方案
在K8s原生的框架和现有的调度体系内,基于自定义的 Container Runtime、Device Plugin、节点资源管理组件 等,实现一种通用的容器间的GPU设备共享。
核心优势:
- 对调度逻辑的影响更小
- 设备类型无关,更加通用且便于扩展
- 核心功能:支持将一块卡同时挂载到多个容器里
从容器间共享开始
Dind的问题
目前线上开发机的Dind(docker in docker)功能,是通过在开发机pod中设置了一个运行docker服务的Dind容器实现的。
问题:用户常常会有需要在 Dind 中使用 GPU 的需求,但是由于 K8s 中资源是以容器为单位进行调度的,不同容器之间的资源互不相干,一个 GPU 设备要么分配给主容器,要么分配给 Dind 容器。当前,如果开发机开启了Dind功能,在创建 pod 的时候是将所有GPU都挂载给Dind容器的,这就会导致用户在主容器中看不到显卡,也没法使用显卡。
解决思路
虽然 K8s 不能给不同容器分配同一个 GPU,但是从容器的角度来讲,同一个 GPU 是可以被不同容器挂载的,因此如果从更底层去实现,是完全可行的。
问题转化:
- 怎样从底层把一个容器中的设备同步挂载到另一个容器中?
- 在哪一步/哪一个组件中去完成上面的同步挂载的操作?
容器创建 Hacking
K8s容器创建基本流程:
1 | kubelet → containerd → spec文件(OCI标准) → runc → 容器 |
containerd在创建容器时会根据kubelet请求中的信息(如 Pod 的配置、容器的镜像、环境变量、挂载卷等)生成容器的规范文件(spec 文件)- 这个 spec 文件符合 OCI标准,描述了容器的配置和运行时环境
runc实际创建容器时就是参考这个spec文件进行创建的
关键发现:只要将容器A的spec中的设备挂载信息拷贝到容器B的spec中,就可以实现将同一块GPU同时挂载给容器A和容器B。
自定义Runtime方案
查看 nvidia-container-runtime 的源码,会发现它定义了很多 spec-modifier,用于修改容器的 spec 文件,在修改完成后,最终再调用 runc 去创建容器。
我们的方案:仿照 nvidia-container-runtime “包一层"的方式,自定义runtime"再包一层”,在真正创建容器之前修改它的spec文件,将另一个容器的设备、挂载、环境变量等信息同步到当前容器里。
用annotation传递共享关系
利用容器的 annotation 作为接口:
1 | "annotations": { |
annotation格式说明:
"gpushare.k8s.io/container-devices-from.dind":代表需要进行设备同步,接收设备的容器名称为dind"_/_/main":代表来源容器的信息,2个下划线分别代表来源容器属于当前namespace和当前pod,main代表容器名称- 这样的设计为以后租户间、pod间的显卡复用提前留好接口
Runtime执行逻辑:
- 首先对annotation进行检查
- 如果没有检查到上述的annotation,就什么都不做
- 否则就将来源容器的设备挂载信息拷贝到目标容器的spec中,再调用下一层的runtime创建容器
小结
- 上层组件在创建pod的时候,只需要向pod中写入上述格式的annotation,就可以完成main容器与dind容器之间的设备共享
- 这种容器间的设备共享不局限于特定的GPU设备类型,对所有类型的GPU设备都可以生效,是一种比较通用的实现方式
向Pod间显卡复用拓展
新的共享需求
基于在容器间共享GPU的能力,支持推理服务和开发机GPU共享的需求。
以推理服务为例:
- 最初需求:支持1卡规格的推理服务实例两两共享GPU
- 用户在创建推理服务时,不需要指定共享的对象,只增加一个"显卡复用开关"
带来的问题:如何区分和处理第一个开启共享的pod?因为基于runtime的设备同步的共享机制永远需要一个"来源容器"。
ShadowPod和虚拟设备
由于在k8s的框架里无法将同一张卡同时分配给两个pod,为了实现上述需求,并且最小化对已有调度逻辑的侵入,设计了一种基于 ShadowPod 和 虚拟设备 的共享方案。
核心概念
| 概念 | 说明 |
|---|---|
| ShadowPod | 隐藏的pod,在系统中仅在后台可见,作用是创建出用来占住GPU资源,作为设备同步中的"来源容器" |
| SharePod | 开启了"显卡复用"开关的推理服务pod,作为"目标容器"接收设备同步 |
| 虚拟资源 | gpushare/share-gpu,在k8s的调度框架内实现对应关系 |
工作流程
1 | 1. 根据SharePod的需求,动态创建ShadowPod |
探索进一步超卖
扩大spot任务的活动范围
当前spot任务机制:
- 一种服务质量不被保证的低优先级任务
- 在运行过程中随时可以被高优先级的reserved任务驱逐抢占
- 可调度范围:集群中所有空闲未分配的资源(未售出的 + 已售出但暂未分配的)
问题场景:
某个用户以包年包月的方式购买了一个8卡节点,同时起了一个8卡的开发机在上面,但是这个开发机只有白天工作时间会运行一些GPU任务,大部分休息时间则保持空闲,GPU利用率为0。我们能否将这部分空闲的GPU时也作为可以调度的资源超卖呢?
"偷卡"机制设计
核心思路:
- 利用已有的"同一块GPU挂载给多个pod"的能力
- 准确判断用户的"离开"状态 → 把开发机中的GPU重复挂载给其他spot类型的pod
- 检测到用户的"回归" → 立刻驱逐偷卡的spot任务,释放显卡资源
用户体验目标:让用户感觉离开的这段时间好像什么事情都没发生一样。
如何判断用户离开与回归
技术原理
"回归"意味着对 GPU有所操作,无论是用户执行一个torch脚本,还是执行一个 nvidia-smi命令,最终在系统层面上都会转化成对GPU设备的系统调用。
实现方案
1 | 参考audit、falco等监控方案 → 在内核中劫持对GPU设备的ioctl系统调用 |
判断逻辑
| 状态 | 判断条件 |
|---|---|
| 离开 | 容器中超过一段时间没有对GPU设备的访问,并且设备利用率在过去这段时间一直是0 |
| 回归 | 处于"离开"状态的容器中出现了对GPU设备的访问 |
技术演进:eBPF → 内核模块
问题:当检测到"回归"事件并立刻驱逐偷卡的负载,还是来不及。因为负载的驱逐需要时间,显卡资源的释放也需要时间。
解决方案:
- 在内核中检测到用户回归的同时,将其系统调用阻塞一小段时间,直到显卡资源彻底释放为止
- 由于eBPF技术的限制无法进行阻塞操作,最终将内核逻辑重新实现为一个内核模块
新的spot调度视图
引入"偷卡"机制后,为spot任务引入新的虚拟资源类型:gpushare/spot-gpu
资源映射关系
1 | 真实GPU ←→ gpushare/spot-gpu (一一对应) |
状态流转
| 场景 | 真实GPU状态 | gpushare/spot-gpu状态 | 行为 |
|---|---|---|---|
| reserved任务分配 | 已分配 | Unhealthy | 不可用,如有spot负载则驱逐 |
| 开发机"离开"状态 | 已分配但空闲 | Healthy | 可分配给新的spot负载 |
| 用户"回归" | 需要使用 | 触发驱逐 | 内核模块信号→驱逐spot负载 |
CPU和内存超卖
数据处理任务超卖
背景:当前节点上CPU、内存和显卡资源是捆绑成固定的规格统一分配的,在节点上所有资源按照规格分配完毕后,理论上就不能再调度其他CPU规格的任务到节点上了。
方案:通过引入虚拟资源 vCPU 和 vMem,实现将数据处理任务调度到已经全部分配的节点上,以此实现CPU和内存的超卖。
技术架构
通用性和可扩展性分析
当前容器间设备共享是基于底层runtime实现的,具有很好的通用性和可扩展性。
业务需求扩展示例
场景:节点上只剩1卡空闲,另外还有一个1卡的处于"离开"状态的开发机,此时启动一个2卡spot负载。
方案:意味着它需要同时接收来自另外2个容器的GPU设备,只需要扩展上文runtime的annotation接口即可。
国产芯片适配
优势:底层runtime进行容器间设备同步的机制是设备无关的,适用于所有类型的设备。
已适配芯片:
- 国际:NVIDIA、AMD
- 国产:燧原、摩尔线程、天数、沐曦、壁仞等
扩展方式:对于后续可能新增的芯片类型,可以通过增加配置项,而不用修改代码来实现。
🎉 总结
1 | 技术演进路线: |
当前成果:
- 随着需求的导向,基于容器间设备同步这一技术对PaaS中GPU资源的复用与超卖进行了一系列探索
- 当前方案在产品、研发和测试同学的共同努力下,已经能够稳定地在线上环境运行
后续规划:
- 完善可观测性等产品功能和技术实现
- 不断探索其他更多的调度优化方式,以实现集群利用率和平台效益的最大化
📋 文档信息:PaaS中GPU资源的复用与超卖探索
