Loading

基于HAMi的容器GPU虚拟化

CUDA

​ CUDA英文全称为Compute Unified Device Architecture,中文翻译是“计算统一设备架构”,是英伟达专门为GPU开发的一套统一计算架构,核心思路是利用GPU强大并行计算能力来实现大规模计算任务的加速,使GPU可以像CPU一样能够解决复杂的计算问题。

一句话总结就是CUDA允许开发者编写直接在支持CUDA的GPU上运行的代码,并增加了控制GPU并行处理的功能,可以精确分配和控制数据在GPU核心上的处理,实现高效并行计算。

cuda.png

从CUDA体系结构来看,可概括为三个部分,分别是GPU驱动、运行环境和开发库;具体见下表

CUDA架构 运行的内容 支持开发的功能
开发库 提供标准数学运算库,如CUFFT和CUBLAS,解决大规模并行计算问题 开发人员可以基于这些库快速构建自己的计算应用并在CUDA技术基础上扩展更多的开发库
运行环境 提供应用开发接口和运行期组件,支持程序代码在CPU和GPU上运行 涵盖了数据类型定义、内存管理、设备访问和执行调度等功能,帮助开发人员实现各种计算需求
驱动支持 CUDA应用需要NVIDIA CUDA-enable硬件支持驱动程序提供了不同版本GPU之间的设备抽象层接口 通过这一层,CUDA可以实现硬件设备的各种功能和计算任务的执行

以PyTorch使用CUDA为例,需要先在服务器或PC安装英伟达GPU驱动,根据驱动安装支持的CUDA版本。(还需要安装CUDNN,基于CUDA的深度学习GPU加速库,有了它才能在GPU上完成深度学习的计算)。

在Python中导入torch包,print(torch.cuda.is_available())输出为True即可确认使用CUDA。

然后在PyTorch使用中可以调用CUDA,将训练任务准到GPU上,CUDA起到承上启下的作用。

x = torch.tensor([1, 2, 3])
x = x.cuda(0)  # 使用.cuda()可以将CPU上的Tensor转换(复制)到GPU上,调用第一张GPU。

HAMi

首先看一下k8s中容器如何使用GPU:

首先在 k8s 中资源是和节点绑定的,对于 GPU 资源,使用 NVIDIA 提供的 device-plugin (以DaemonSet形式运行在所有节点)进行感知,并上报到 kube-apiserver,这样就能在 Node 对象上看到对应的资源了。

$ 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  # 该节点有8个GPU
  pods:               110

然后就可以在创建 Pod 时申请对应的资源了,比如申请一个 GPU:

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

apply 该 yaml 之后,kube-scheduler 在调度该 Pod 时就会将其调度到一个拥有足够 GPU 资源的 Node 上。

同时该 Pod 申请的部分资源也会标记为已使用,不会再分配给其他 Pod。即:Node 上的 GPU 资源被 Pod 申请之后,在 k8s 中就被标记为已消耗了,后续创建的 Pod 会因为资源不够导致无法调度

这就造成部分GPU资源的浪费,有些GPU 性能比较好,原本可以支持多个 Pod 共同使用。因此,HAMi这种GPU虚拟化方案应运而生。

HAMi 全称是:Heterogeneous AI Computing Virtualization Middleware,HAMi 给自己的定位或者希望是做一个异构算力虚拟化平台,vGPU是其现在最核心也最完善的功能。

https://img.lixueduan.com/kubernetes/vgpu/hami-arch.png

HAMi 最大的一个功能点就是可以实现 GPU 的细粒度的隔离,可以对 core 和 memory 使用 1% 级别的隔离。

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%实际显卡的算力 (可选,整数类型)

HAMi不是采用的TimeSlicing时分复用方案,因为时分复用没有做隔离会存在资源竞争,而是通过vCUDA实现GPU core 和 memory 隔离、限制。

https://img.lixueduan.com/kubernetes/vgpu/hami-core-design.png

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 中申请的资源,这样在查看时也进行隔离。

K8S部署HAMi

简要介绍下k8s部署HAMi的过程和各种组件。

部署GPU-Opeartor

GPU-Operator 旨在简化在 Kubernetes 环境中使用 GPU 的过程,通过自动化的方式处理 GPU 驱动程序安装、Controller Toolkit、Device-Plugin 、监控等组件。HAMi依赖英伟达的整套驱动。

部署HAMi

通过Helm可以一键部署HAMi,主要是hami-device-plugin和hami-scheduler两个pod。

查看与测试GPU使用

同样查看节点GPU情况

$ 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" # GPU 的分割数,每一张GPU 都不能分配超过其配置数目的任务。若其配置为N的话,每个 GPU 上最多可以同时存在 N 个任务。默认值10。
    pods: "110"

起一个pod验证GPU挂载

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%实际显卡的算力 (可选,整数类型)

进入pod,使用nvidia-smi(一个命令行实用程序,用于查询和控制NVIDIA GPU的状态和配置。它是NVIDIA GPU驱动程序包的一部分,允许用户查看关于系统上安装的GPU、其当前状态、运行的进程以及其他相关信息的详细报告)查看GPU信息。

[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

PyTorch使用HAMi提供的GPU

HAMi会替换CUDA的驱动文件实现CUDA API劫持,但HAMi不会提供完整的CUDA,需要镜像中自带CUDA,如pytorch/pytorch:2.4.1-cuda11.8-cudnn9-runtime

apiVersion: v1
kind: Pod
metadata:
  name: hami-30
  namespace: default
spec:
  containers:
  - name: simple-container
    image: pytorch/pytorch:2.4.1-cuda11.8-cudnn9-runtime
    command: ["python", "/mnt/imagenet/main.py", "-a", "resnet18", "--dummy"]
 # 使用 sleep infinity 保持容器持续运行
    resources:
      requests:
        cpu: "4"
        memory: "32Gi"
        nvidia.com/gpu: "1"
        nvidia.com/gpucores: "30"
        nvidia.com/gpumem: "20000"
      limits:
        cpu: "4"
        memory: "32Gi"
        nvidia.com/gpu: "1" # 1 个 GPU
        nvidia.com/gpucores: "30" # 申请使用 30% 算力
        nvidia.com/gpumem: "20000" # 申请 20G 显存(单位为 MB)
    env:
    - name: GPU_CORE_UTILIZATION_POLICY
      value: "force" # 设置环境变量 GPU_CORE_UTILIZATION_POLICY 为 force,限制算力使用,默认的限制策略是该 GPU 只有一个 Pod 在使用时就不会做算力限制,可以提升 GPU 利用率
    volumeMounts:
    - name: imagenet-volume
      mountPath: /mnt/imagenet # 容器内挂载点
    - name: shm-volume
      mountPath: /dev/shm # 挂载共享内存到容器的 /dev/shm
  restartPolicy: Never
  volumes:
  - name: imagenet-volume
    hostPath:
      path: /root/lixd/hami/examples/imagenet # 主机目录路径,使用 PyTorch 提供的 Examples 作为测试脚本,https://github.com/pytorch/examples.git
      type: Directory
  - name: shm-volume
    emptyDir:
      medium: Memory # 使用内存作为 emptyDir

HAMi组件与原理

HAMi-device-plugin-nvidia

HAMi用自己实现的device plugin取代了原来NVIDIA GPU-Operator部署的device plugin,主要在三个地方进行修改:

  • Register:两个作用:将插件注册到 Kubelet 的;感知 Node 上的 GPU 信息并传给 kube-apiserver ,将这部分信息以 annotations 的形式添加到 Node 对象上,以便后续 hami-scheduler 使用。参数 ResourceName 比较重要,HAMi会将GPU信息呈现为 –resource-name=nvidia.com/vgpu。
  • ListAndWatch:感知节点上的设备并上报给 Kubelet。由于需要将同一个 GPU 切分给多个 Pod 使用,因此 HAMi 的 device plugin 也会有类似 TimeSlicing 中的 Device 复制操作。
  • Allocate(核心):控制device plugin 将 GPU 分配给 Pod ,根据 Pod Resource 中的申请资源数量设置对应的环境变量,以及挂载 libvgpu.so 以替换 Pod 中的原生驱动

HAMi-Scheduler

  1. 用户创建 Pod 并在 Pod 中申请了 vGPU 资源

  2. kube-apiserver 根据 MutatingWebhookConfiguration 配置请求 HAMi-Webhook

  3. HAMi-Webhook 检测 Pod 中的 Resource,如果申请的由 HAMi 管理的 vGPU 资源,就会把 Pod 中的 SchedulerName 改成了 hami-scheduler,这样这个 Pod 就会由 hami-scheduler 进行调度了。

    [!NOTE]

    对于特权模式的 Pod,Webhook 会直接跳过不处理

    对于使用 vGPU 资源但指定了 nodeName 的 Pod,Webhook 会直接拒绝

  4. hami-scheduler 进行 Pod 调度,不过就是用的 k8s 的默认 kube-scheduler 镜像,因此调度逻辑和默认的 default-scheduler 是一样的,但是 kube-scheduler 还会根据 KubeSchedulerConfiguration 配置,调用 Extender Scheduler 插件

    这个 Extender Scheduler 就是 hami-scheduler Pod 中的另一个 Container,该 Container 同时提供了 Webhook 和 Scheduler 相关 API。

    当 Pod 申请了 vGPU 资源时,kube-scheduler 就会根据配置以 HTTP 形式调用 Extender Scheduler 插件,这样就实现了自定义调度逻辑。

  5. Extender Scheduler 插件包含了真正的 hami 调度逻辑, 调度时根据节点剩余资源量进行打分选择节点。

  6. 异步任务,包括 GPU 感知逻辑

    • devicePlugin 中的后台 Goroutine 定时上报 Node 上的 GPU 资源并写入到 Node 的 Annoations
    • 除了 DevicePlugin 之外,还使用异步任务以 Patch Annotation 方式提交更多信息
    • Extender Scheduler 插件根据 Node Annoations 解析出 GPU 资源总量、从 Node 上已经运行的 Pod 的 Annoations 中解析出 GPU 使用量,计算出每个 Node 剩余的可用资源保存到内存供调度时使用

https://img.lixueduan.com/kubernetes/vgpu/hami-scheduler.png

HAMi-Core

HAMi-core(libvgpu.so)项目就是 vCUDA 的核心实现,作用在CUDA-Runtime(libcudart.so) 和 CUDA-Driver(libcuda.so)之间。

libvgpu.so 是怎么生效的?

  • 1)device plugin 在 Allocate 方法中使用 hostPath 方式将宿主机上的 libvgpu.so 挂载到 Pod 中
  • 2)并通过 LD_PRELOAD 方式实现优先加载上一步中挂载的 libvgpu.so 库,使其生效

CUDA API 怎么拦截的?

通过重写 dlsym 函数,以劫持 NVIDIA 动态链接库(如 CUDA 和 NVML)的调用,特别是针对以 cunvml 开头的函数进行拦截。

gpu memory 是怎么限制的?

首先是拦截 NVMLAPI 中的 _nvmlDeviceGetMemoryInfo,实现在执行 nvidia-smi 命令时只展示申请的 Memory(来源于CUDA_DEVICE_MEMORY_LIMIT_X)。

然后是拦截内存分配相关的 CUDA API,比如:cuMemoryAllocatecuMemAlloc_v2

分配内存之前,增加了 oom_check,当前 Pod 的 GPU 内存使用量 超过 限制的内存使用量(来源于CUDA_DEVICE_MEMORY_LIMIT_X)时直接返回 OOM。

gpu core 是怎么限制的?

同理,拦截提交 Kernel 相关的 CUDA API,例如:cuLaunchKernel

提交 Kernel 之前,增加 rate_limit 逻辑,具体算法类似令牌桶,每次提交 kernel 都会消耗 Token,直到某次提交 kernel 发现没有 Token 时就会直接 sleep, 一段时间之后 Token 恢复了,又可以继续提交任务了。

恢复 Token 时就会用到CUDA_DEVICE_SM_LIMIT 环境变量。

posted @ 2025-06-18 17:03  VitoChen  阅读(1060)  评论(0)    收藏  举报