简介
Kubernetes容器平台下的 GPU 集群算力管控 梳理了下gpu虚拟化(如何对gpu 进行细粒度切分、隔离,毕竟gpu还没有linux cgroup/namespace这些机制),gpu资源纳入k8s 体系,自然也会有常见的k8s 资源调度问题(碎片化等等)。
上报/调度/容器创建
Kubernetes GPU管理与Device Plugin机制对于云的用户来说,在 GPU 的支持上,他们最基本的诉求其实非常简单:我只要在 Pod 的 YAML 里面,声明某容器需要的 GPU 个数,那么 Kubernetes 为我创建的容器里就应该出现对应的 GPU 设备,以及它对应的驱动目录。以 NVIDIA 的 GPU 设备为例,上面的需求就意味着当用户的容器被创建之后,这个容器里必须出现如下两部分设备和目录:
- GPU 设备,比如 /dev/nvidia0;
- GPU 驱动目录,比如 /usr/local/nvidia/*。
apiVersion: v1
kind: Pod
metadata:
name: cuda-vector-add
spec:
restartPolicy: OnFailure
containers:
- name: cuda-vector-add
image: "k8s.gcr.io/cuda-vector-add:v0.1"
resources:
limits:
nvidia.com/gpu: 1
在 kube-scheduler 里面,它其实并不关心nvidia.com/gpu
的具体含义,只会在计算的时候,一律将调度器里保存的该类型资源的可用量,直接减去 Pod 声明的数值即可。为了能够让调度器知道这个自定义类型的资源在每台宿主机上的可用量,宿主机节点本身,就必须能够向 API Server 汇报该类型资源的可用数量。在 Kubernetes 里,各种类型的资源可用量,其实是 Node 对象 Status 字段的内容。为了能够在上述 Status 字段里添加自定义资源的数据,你就必须使用 PATCH API 来对该 Node 对象进行更新,加上你的自定义资源的数量。这个 PATCH 操作,可以简单地使用 curl 命令来发起 curl --header "Content-Type: application/json-patch+json" \--request PATCH \--data '[{"op": "add", "path": "/status/capacity/nvidia.com/gpu", "value": "1"}]' \http://localhost:8001/api/v1/nodes//status
apiVersion: v1
kind: Node
...
Status:
Capacity:
cpu: 2
memory: 2049008Ki
nvidia.com/gpu: 1
- 对于每一种硬件设备,都需要有它所对应的 Device Plugin 进行管理,这些 Device Plugin,都通过 gRPC 的方式同 kubelet 连接起来。以 NVIDIA GPU 为例,它对应的插件叫作NVIDIA GPU device plugin。DevicePlugin 注册一个socket 文件到
/var/lib/kubelet/device-plugins/
目录下,Kubelet 通过这个目录下的socket 文件向对应的 DevicePlugin 发送gRPC 请求。PS: 通过目录做服务发现。 - Device Plugin 会通过一个叫作 ListAndWatch 的 API,定期向 kubelet 汇报该 Node 上 GPU 的列表。比如,在上图的例子里,一共有三个 GPU(GPU0、GPU1 和 GPU2)。这样,kubelet 在拿到这个列表之后,就可以直接在它向 APIServer 发送的心跳里,以 Extended Resource 的方式,加上这些 GPU 的数量,比如nvidia.com/gpu=3。
- 当 kubelet 发现这个 Pod 的容器请求一个 GPU 的时候,kubelet 就会从自己持有的 GPU 列表里,为这个容器分配一个 GPU。此时,kubelet 就会向本机的 Device Plugin 发起一个
Allocate()
请求。这个请求携带的参数,正是即将分配给该容器的设备 ID 列表。 - 当 Device Plugin 收到 Allocate 请求之后,它就会根据 kubelet 传递过来的设备 ID,从 Device Plugin 里找到这些设备对应的设备路径和驱动目录。比如,在 NVIDIA Device Plugin 的实现里,它会定期访问 nvidia-docker 插件,从而获取到本机的 GPU 信息。而被分配 GPU 对应的设备路径和驱动目录信息被返回给 kubelet 之后,kubelet 就完成了为一个容器分配 GPU 的操作。接下来,kubelet 会把这些信息追加在创建该容器所对应的 CRI 请求当中。这样,当这个 CRI 请求发给 Docker 之后,Docker 为你创建出来的容器里,就会出现这个 GPU 设备,并把它所需要的驱动目录挂载进去。
service DevicePlugin {
// ListAndWatch returns a stream of List of Devices
// Whenever a Device state change or a Device disappears, ListAndWatch
// returns the new list
rpc ListAndWatch(Empty) returns (stream ListAndWatchResponse) {}
// Allocate is called during container creation so that the Device
// Plugin can run device specific operations and instruct Kubelet
// of the steps to make the Device available in the container
rpc Allocate(AllocateRequest) returns (AllocateResponse) {}
}
目前 Kubernetes 本身的 Device Plugin 的设计,实际上能覆盖的场景是非常单一的,属于“可用”但是“不好用”的状态。一旦你的设备是异构的、不能简单地用“数目”去描述具体使用需求的时候,比如,“我的 Pod 想要运行在计算能力最强的那个 GPU 上”,Device Plugin 就完全不能处理了。在很多场景下,我们其实希望在调度器进行调度的时候,就可以根据整个集群里的某种硬件设备的全局分布,做出一个最佳的调度选择。
- 调度器扮演的角色,仅仅是为 Pod 寻找到可用的、支持这种硬件设备的节点
- GPU 等硬件设备的调度工作,实际上是由 kubelet 完成的。即kubelet 会负责从它所持有的硬件设备列表中,为容器挑选一个硬件设备,然后调用 Device Plugin 的 Allocate API 来完成这个分配操作。
- 不支持多个pod共享一个gpu GPU拓扑感知调度概述
上报细节
kuebctl describe node xx
查看对应节点的gpu数据
Capacity:
cpu: 48
ephemeral-storage: 2239684580Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 131661580Ki
nvidia.com/gpu: 4
pods: 110
Allocatable:
cpu: 46
ephemeral-storage: 2058724596391
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 126418700Ki
nvidia.com/gpu: 4
pods: 110
nvidia gpu operator
如果在k8s 上支持gpu 设备调度,需要做
- 节点上安装 nvidia 驱动 (对物理机)
- 节点上安装 nvidia-docekr (对接docker)
- 集群部署 gpu device plugin (对接kubelet)
- 部署 dcgm-exporter 监控gpu 使用 为此, nvidia 开源了 nvidia-gpu-exporter 自动化管理上述组件
云原生方式管理GPU资源
- gpu share。阿里GPU Share Device Plugin。不支持共享资源的隔离
- 截获CUDA库转发,如vCUDA。
- 截获驱动转发,如阿里云cGPU、腾讯云qGPU。
- 截获GPU硬件访问,如NVIDIA GRID vGPU。
如果哪一天,gpu 也可以支持类似于cpu的cgroup 接口,gpu的池化就很爽了。现在的各种vgpu 方案,直接对接k8s 有点不太统一了。如果有一个通用的gpu cgroup 配置标准,大家都去对接 cgroup。k8s/kubelet 只是对接 cgroup。就可以上层调度 和 下层虚拟化方案 分开发展了。难点:gpu 驱动不统一,所以在 cg 这层很难统一。k8s 社区现在搞了个 dynamic resource manager,想在 k8s 侧统一 extender resource。
使用 Elastic GPU 管理 Kubernetes GPU 资源在 GPU 场景,还是存在以下不足:
- 集群 GPU 资源缺少全局视角。没有直观方式可获取集群层面 GPU 信息,比如 Pod / 容器与 GPU 卡绑定关系、已使用 GPU 卡数等。
- 不能很好支持多 GPU 后端。各种 GPU 技术(nvidia docker、qGPU、vCUDA、gpu share、GPU 池化)均需独立部署组件,无法统一调度和管理。
每种方案都有自己独立的一套 Kubernetes 集成实现方式,通常是由调度器 + device plugin 组成。这些方案相互独立,没有统一标准,无法共通。这导致用户在单个集群中很难同时使用多种 GPU 后端技术,同时也没有一个全局的视角获取集群层面 GPU 信息。腾讯云参考 PV/PVC/StorageClass 提出了ElasticGPU/ElasticGPUClaim/EGPUClass。
- ElasticGPU:ElasticGPU 是集群中一个实际可使用的 GPU 资源,可以是一块本地 GPU 物理卡、一个 GPU 切片资源( GPU 算力 / 显存 的组合)、一个远端 GPU 设备。
- ElasticGPUClaim:ElasticGPUClaim 是用户对 ElasticGPU 资源的申领,可以申请整卡数量,申请 GPU 核数 / 显存,或者申请 TFLOPS 算力。
- EGPUClass:EGPUClass 提供了生产和挂载 ElasticGPU 的方式,可以使用 qGPU 虚拟化、vCUDA、或是 GPU 远端池化的技术。
mGPU 技术揭秘 :新一代 Kubernetes GPU 共享调度方案
- 两层调度:mGPU 虚拟化方案则为 GPU 增加了算力和显存两个维度的属性,不再是一个简单的设备个数。对于单机上的 kubelet 来说,算力和显存会被视为两种独立的扩展资源。在 mGPU 场景下,如果由 kubelet 进行 GPU 级别的调度,可能会导致一个容器被分配到的算力和显存是在两个 GPU 上,实际上无法使用。因此,算力和显存两种资源的“撮合”需要由调度器来完成。也就是说,调度器不仅需要决策将 Pod 调度到哪个节点,还需要进一步决策将该 Pod 中的各个容器分别调度到该节点的哪些 GPU 上。
- 卡级别的 Binpack/Spread 策略:在引入了两层调度后,GPU 也将作为一种调度的拓扑域。调度器不仅需要考虑节点级别的 Binpack/Spread 策略,也需要支持卡级别的 Binpack/Spread 策略,从而减少卡级别的资源碎片,或者提升卡级别的故障隔离性。
- 基于 Scheduling Framework 扩展 GPUShare Plugin,实现 GPU 共享调度。
- 将 Pod 调度到合适的节点。
- 将 Pod 中的各个 Container 调度到合适的 GPU 组合上(并将结果记录到 Pod Annotation 中)。
- mGPU Device Plugin: 单机上的 mGPU 资源管理插件。在本功能中负责:
- 发布 mGPU 资源 (最终将由 kubelet 上报到 Node 对象中)。
- 根据调度器的分配结果,将相应的环境变量注入到容器中。