前言
为什么需要 GPU 共享、切分等方案?
在使用GPU的过程中我们会发现,直接在裸机环境使用,都可以多个进程共享 GPU,怎么到 k8s 环境就不行了?
1. 资源感知
在 k8s 中资源是和节点绑定的,对于 GPU 资源,我们使用 NVIDIA 提供的 device-plugin 进行感知,并上报到 kube-apiserver,这样我们就能在 Node 对象上看到对应的资源。
bash
kubectl describe node gpu01|grep Capacity -A 7
Capacity:
cpu: 128
ephemeral-storage: 879000896Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 1056457696Ki
nvidia.com/gpu: 8
pods: 110
该节点除了基础的 cpu、memory 之外,还有一个nvidia.com/gpu: 8
信息,表示该节点上有 8 个 GPU 。
2. 资源申请
bash
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
containers:
- name: gpu-container
image: nvidia/cuda:11.0-base # 一个支持 GPU 的镜像
resources:
limits:
nvidia.com/gpu: 1 # 申请 1 个 GPU
command: ["nvidia-smi"] # 示例命令,显示 GPU 的信息
restartPolicy: OnFailure
kube-scheduler 在调度该 Pod 时就会将其调度到一个拥有足够 GPU 资源的 Node 上。同时该 Pod 申请的部分资源也会标记为已使用,不会再分配给其他 Pod。
总结一下:
1)device-plugin 感知到节点上的物理 GPU 数量,上报到 kube-apiserver
2)kube-scheduler 调度 Pod 时会根据 pod 中的 Request 消耗对应资源
即:Node 上的 GPU 资源被 Pod 申请之后,在 k8s 中就被标记为已消耗了,后续创建的 Pod 会因为资源不够导致无法调度。
实际上:可能 GPU 性能比较好,可以支持多个 Pod 共同使用,但是 k8s 中的调度限制导致多个 Pod 无法正常共享。因此,我们才需要 GPU 共享、切分等方案。
什么是 HAMi?
https://github.com/Project-HAMi/HAMi
HAMi 全称是:Heterogeneous AI Computing Virtualization Middleware,HAMi 给自己的定位或者希望是做一个异构算力虚拟化平台。原第四范式 k8s-vgpu-scheduler, 这次改名 HAMi 同时也将核心的 vCUDA 库 libvgpu.so 也开源了。但是现在比较完善的是对 NVIDIA GPU 的 vGPU 方案,因此我们可以简单认为他就是一个 vGPU 方案。
整体架构
特性
使用 HAMi 最大的一个功能点就是可以实现 GPU 的细粒度的隔离,可以对 core 和 memory 使用 1% 级别的隔离。
bash
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
containers:
- name: ubuntu-container
image: ubuntu:18.04
command: ["bash", "-c", "sleep 86400"]
resources:
limits:
nvidia.com/gpu: 1 # 请求1个vGPUs
nvidia.com/gpumem: 3000 # 每个vGPU申请3000m显存 (可选,整数类型)
nvidia.com/gpucores: 30 # 每个vGPU的算力为30%实际显卡的算力 (可选,整数类型)
----------------------
nvidia.com/gpu:请求一个 GPU
nvidia.com/gpumem:只申请使用 3000M GPU Memory
nvidia.com/gpucores:申请使用 30% 的 GPU core,也就是该 Pod 只能使用到 30% 的算力
设计
HAMi 实现 GPU core 和 memory 隔离、限制是使用的 vCUDA 方案
HAMi 使用的是软件层面的 vCUDA 方案,对 NVIDIA 原生的 CUDA 驱动进行重写(libvgpu.so),然后挂载到 Pod 中进行替换,然后在自己的实现的 CUDA 驱动中对 API 进行拦截,实现资源隔离以及限制的效果。
例如:原生 libvgpu.so 在进行内存分配时,只有在 GPU 内存真的用完的时候才会提示 CUDA OOM,但是对于 HAMi 实现的 libvgpu.so 来说,检测到 Pod 中使用的内存超过了 Resource 中的申请量就直接返回 OOM,从而实现资源的一个限制。
然后在执行 nvidia-smi 命令查看 GPU 信息时,也只返回 Pod Resource 中申请的资源,这样在查看时也进行隔离。
HAMi 部署
HAMi 提供了 Helm Chart 安装
- 部署 GPU Operator
HAMi 会依赖 NVIDIA 的那一套,因此推荐先部署 GPU-Operator
此处留着补充
部署好 GPU Operator 之后再部署 HAMi。
- 部署 HAMi
bash
# 添加repo仓库
helm repo add hami-charts https://project-hami.github.io/HAMi/
# 获取k8s版本
kubectl version
# 在安装过程中须根据集群服务端版本(上一条指令的结果)指定调度器镜像版本,例如集群服务端版本为 v1.27.4,则可以使用如下指令进行安装
helm install hami hami-charts/hami --set scheduler.kubeScheduler.imageTag=v1.27.4 -n kube-system
# 通过 kubectl get pods 指令看到 vgpu-device-plugin 与 vgpu-scheduler 两个 pod 状态为Running 即为安装成功
kubectl get pods -n kube-system|grep hami
hami-device-plugin-b6mvj 2/2 Running 0 42s
hami-scheduler-7f5c5ff968-26kjc 2/2 Running 0 42s
- 自定义配置
官方文档:
HAMi-config-cn.md: https://github.com/Project-HAMi/HAMi/blob/master/docs/config_cn.md
在安装过程中,通过-set
来修改以下的客制化参数,例如
bash
helm install vgpu vgpu-charts/vgpu --set devicePlugin.deviceMemoryScaling=5 ...
devicePlugin.deviceSplitCount
:整数类型,预设值是 10。GPU 的分割数,每一张 GPU 都不能分配超过其配置数目的任务。若其配置为 N 的话,每个 GPU 上最多可以同时存在 N 个任务。
devicePlugin.deviceMemoryScaling:
浮点数类型,预设值是 1。NVIDIA 装置显存使用比例,可以大于 1(启用虚拟显存,实验功能)。对于有 M 显存大小的 NVIDIA GPU,如果我们配置devicePlugin.deviceMemoryScaling
参数为 S ,在部署了我们装置插件的 Kubenetes 集群中,这张 GPU 分出的 vGPU 将总共包含S * M
显存。
devicePlugin.migStrategy:
字符串类型,目前支持"none"与"mixed"两种工作方式,前者忽略 MIG 设备,后者使用专门的资源名称指定 MIG 设备,使用详情请参考 mix_example.yaml,默认为"none"
devicePlugin.disablecorelimit:
字符串类型,"true"为关闭算力限制,"false"为启动算力限制,默认为"false"
scheduler.defaultMem:
整数类型,预设值为 5000,表示不配置显存时使用的默认显存大小,单位为 MB
scheduler.defaultCores:
整数类型(0-100),默认为 0,表示默认为每个任务预留的百分比算力。若设置为 0,则代表任务可能会被分配到任一满足显存需求的 GPU 中,若设置为 100,代表该任务独享整张显卡
scheduler.defaultGPUNum:
整数类型,默认为 1,如果配置为 0,则配置不会生效。当用户在 pod 资源中没有设置 nvidia.com/gpu 这个 key 时,webhook 会检查 nvidia.com/gpumem、resource-mem-percentage、nvidia.com/gpucores 这三个 key 中的任何一个 key 有值,webhook 都会添加 nvidia.com/gpu 键和此默认值到 resources limit 中。
resourceName:
字符串类型, 申请 vgpu 个数的资源名, 默认: "nvidia.com/gpu"
resourceMem:
字符串类型, 申请 vgpu 显存大小资源名, 默认: "nvidia.com/gpumem"
resourceMemPercentage:
字符串类型,申请 vgpu 显存比例资源名,默认: "nvidia.com/gpumem-percentage"
resourceCores:
字符串类型, 申请 vgpu 算力资源名, 默认: "nvidia.com/cores"
resourcePriority:
字符串类型,表示申请任务的任务优先级,默认: "nvidia.com/priority"
除此之外,容器中也有对应配置
GPU_CORE_UTILIZATION_POLICY:
字符串类型,"default", "force", "disable" 代表容器算力限制策略, "default"为默认,"force"为强制限制算力,一般用于测试算力限制的功能,"disable"为忽略算力限制
ACTIVE_OOM_KILLER:
字符串类型,"true", "false" 代表容器是否会因为超用显存而被终止执行,"true"为会,"false"为不会
- 验证
查看 Node GPU 资源
环境中只有一个物理 GPU,但是 HAMi 默认会扩容 10 倍,理论上现在 Node 上能查看到 1*10 = 10 个 GPU。
默认参数就是切分为 10 个,可以设置
bash
kubectl get node xxx -oyaml|grep capacity -A 7
capacity:
cpu: "4"
ephemeral-storage: 206043828Ki
hugepages-1Gi: "0"
hugepages-2Mi: "0"
memory: 15349120Ki
nvidia.com/gpu: "10"
pods: "110"
验证显存和算力限制
使用以下 yaml 来创建 Pod,注意 resources.limit 除了原有的 nvidia.com/gpu 之外还新增了 nvidia.com/gpumem 和 nvidia.com/gpucores,用来指定显存大小和算力大小。
nvidia.com/gpu:请求的 vgpu 数量,例如 1
nvidia.com/gpumem :请求的显存数量,例如 3000M
nvidia.com/gpumem-percentage:显存百分百,例如 50 则是请求 50%显存
nvidia.com/priority: 优先级,0 为高,1 为低,默认为 1。
对于高优先级任务,如果它们与其他高优先级任务共享 GPU 节点,则其资源利用率不会受到
resourceCores
的限制。换句话说,如果只有高优先级任务占用 GPU 节点,那么它们可以利用节点上所有可用的资源。对于低优先级任务,如果它们是唯一占用 GPU 的任务,则其资源利用率也不会受到
resourceCores
的限制。这意味着如果没有其他任务与低优先级任务共享 GPU,那么它们可以利用节点上所有可用的资源。
bash
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
containers:
- name: ubuntu-container
image: ubuntu:18.04
command: ["bash", "-c", "sleep 86400"]
resources:
limits:
nvidia.com/gpu: 1 # 请求1个vGPUs
nvidia.com/gpumem: 3000 # 每个vGPU申请3000m显存 (可选,整数类型)
nvidia.com/gpucores: 30 # 每个vGPU的算力为30%实际显卡的算力 (可选,整数类型)
bash
kubectl exec -it gpu-pod -- bash
root@gpu-pod:/# nvidia-smi
[HAMI-core Msg(16:139711087368000:libvgpu.c:836)]: Initializing.....
Mon Apr 29 06:22:16 2024
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.54.14 Driver Version: 550.54.14 CUDA Version: 12.4 |
|-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 Tesla T4 On | 00000000:00:07.0 Off | 0 |
| N/A 33C P8 15W / 70W | 0MiB / 3000MiB | 0% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
+-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=========================================================================================|
| No running processes found |
+-----------------------------------------------------------------------------------------+
[HAMI-core Msg(16:139711087368000:multiprocess_memory_limit.c:434)]: Calling exit handler 16
最后的日志就是 HAMi 的 CUDA 驱动打印
bash
[HAMI-core Msg(16:139711087368000:multiprocess_memory_limit.c:434)]: Calling exit handler 16
HAMi 大致实现原理
通过替换容器中的 libvgpu.so 库,实现 CUDA API 拦截,最终实现对 GPU core 和 memory 的隔离和限制。
参考资料:
第四范式 k8s-vgpu-scheduler: https://github.com/4paradigm/k8s-vgpu-scheduler