如何在K8s集群中管理与使用GPU

embedded/2024/11/20 6:24:23/

背景

随着人工智能的兴起,GPU作为重要的智算算力类型愈发受到重视,而Kubernetes(k8s)作为业界主流的集群管理系统,如何方便管理、使用GPU也是其需要解决的一大问题,故此收集整理了K8s管理与使用GPU的相关资料以学习。

物理机如何使用GPU

如果给一台普通的物理机,例如我们日常用的笔记本电脑应该如何使用GPU呢。其主要涉及到两个插件的安装,分别是Nvidia DriverCUDA Toolkit

Nvidia Driver

Nvidia Driver就是GPU驱动,其与其他驱动类似,其主要作用是作为操作系统与GPU硬件之间沟通的桥梁,它需要负责将GPU复杂的硬件功能抽象为标准化接口,方便操作系统和软件调用,并能把GPU硬件的反馈结果传递给操作系统或应用程序。

Cuda Toolkit

Cuda toolkit是NVIDIA提供的一个开发工具集,包含了一系列用于GPU编程的工具和库。其主要由以下组件组成:

  • Compiler: CUDA-C和CUDA-C++编译器NVCC位于bin/目录中。它建立在NVVM优化器之上,而NVVM优化器本身构建在LLVM编译器基础结构之上。希望开发人员可以使用nvm/目录下的Compiler SDK来直接针对NVVM进行开发。
  • Tools: 提供一些像profiler,debuggers等工具,这些工具可以从bin/目录中获取
  • Libraries: 下面列出的部分科学库和实用程序库可以在lib/目录中使用(Windows上的DLL位于bin/中),它们的接口在include/目录中可获取。
    • cudart: CUDA Runtime
    • cudadevrt: CUDA device runtime
    • cupti: CUDA profiling tools interface
    • nvml: NVIDIA management library
    • nvrtc: CUDA runtime compilation
    • cublas: BLAS (Basic Linear Algebra Subprograms,基础线性代数程序集)
    • cublas_device: BLAS kernel interface
  • Runtime Api:提供GPU访问的接口,包括:
    • CUDA Runtime API: 提供简单易用的高层接口,简化GPU的初始化和资源管理。
    • CUDA Driver API: 更底层的接口,提供对GPU的精细控制,适合需要自定义优化的高级用户。
  • CUDA Samples: 演示如何使用各种CUDA和library API的代码示例。可在Linux和Mac上的samples/目录中获得,Windows上的路径是C:\ProgramData\NVIDIA Corporation\CUDA Samples中。在Linux和Mac上,samples/目录是只读的,如果要对它们进行修改,则必须将这些示例复制到另一个位置。

说明

安装完以上插件后就可以使用GPU了,我们可以直接使用CUDA来编程也可以利用Pytorch、TensorFlow等机器学习库来间接使用GPU,在使用GPU时,其整体的调用链如下图所示:

请添加图片描述

Docker如何使用GPU

配置nvidia-container-runtime

正常创建一个容器的流程是这样的:

请添加图片描述

简单来说主要有以下这些步骤:

  1. 用户命令传递: CLI 将用户命令解析并使用 HTTP 或 Unix Socket 与 dockerd 通信。
  2. 调度与管理: dockerd 解析命令并检查、拉取镜像,再调用 containerd 创建一个新的容器任务,准备容器的元数据和配置(如挂载点、网络设置、环境变量等),并为每个任务创建一个 containerd-shim 进程
  3. 隔离与启动: containerd-shim 启动并调用 runc 创建隔离环境,runc 从 containerd 提供的配置中读取容器规格,包括文件系统挂载、网络命名空间、cgroups 配置(限制 CPU、内存等资源),并配置PID、Network、Mount Namespace级别的隔离,再配置 Cgroups,限制资源使用,设置 rootfs,将镜像内容挂载为容器的根文件系统。
  4. 容器运行: runc 启动用户指定的进程,容器进入运行状态。

而为了能够让容器也能直接使用GPU,我们就需要修改创建容器的关键runtime为nvidia-container-runtime,而我们一般都通过NVIDIA Container Toolkit来安装nvidia-container-runtime。旧版本修改runtime为nvidia-container-runtime是需要手动在etc/docker/daemon.json中增加配置,指定使用 nvidia 的 runtime,如下:

    "runtimes": {"nvidia": {"args": [],"path": "nvidia-container-runtime"}}

新版 toolkit 带了一个nvidia-ctk 工具,执行以下命令即可一键配置:

sudo nvidia-ctk runtime configure --runtime=docker

然后重启 Docker 即可,再创建使用GPU的容器时,只需要加入--gpu参数即可,如docker run --rm --gpus all nvidia/cuda:12.0.1-runtime-ubuntu22.04 nvidia-smi

说明

修改runtime后,如下图所示,containerd-shim会调用指定的运行时nvidia-container-runtime。nvidia-container-runtime相比于默认的runc多实现了nvidia-container-runime-hook,该hook是在容器启动后(Namespace已创建完成),容器自定义命令(Entrypoint)启动前执行。当检测到NVIDIA_VISIBLE_DEVICES环境变量时,会调用libnvidia-container挂载GPU Device和CUDA Driver。如果没有检测到NVIDIA_VISIBLE_DEVICES就直接执行默认的runc。

请添加图片描述

在Docker 环境中的 CUDA 调用的整体层级如下图所示,NVIDIA 将原来 CUDA 应用依赖的API环境划分为两个部分:

  • 驱动级API:由libcuda.so.major.minor动态库和内核module提供支持,图中表示为CUDA Driver,它必须在宿主机上就配置好,且只能有一个版本。
  • 非驱动级API:由动态库libcublas.so等用户空间级别的API(算是对驱动级API的一种更高级的封装)组成,图中表示为CUDA Toolkit,直接存在在各个容器中,各个容器中的CUDA Toolkit的版本也可以不同。

请添加图片描述

K8s如何使用GPU

通过上述说明,我们可以手动在宿主机中起一个使用GPU的容器,但是对于k8s管理的大规模集群,我们还需要做到可以让k8s感知到有哪些GPU可以使用,可以通过k8s的pod来创建使用GPU的容器

手动配置

在k8s中使用GPU资源涉及到的一个关键组件就是NVIDIA Device Plugin

Device plugin是k8s 用于管理和调度容器中设备资源的一种插件机制,它可以将物理设备(如 GPU、FPGA 等)暴露给容器,从而提供更高级别的资源管理和调度能力。它由各个硬件对应的厂商提供,其主要是通过DeamonSet部署到各个主机上,然后上报给kubelet对应的硬件资源的情况,再上报给master。

当我们安装了NVDIA的device plugin后再次查看node的可分配资源就可以看到GPU相关的信息:

root@test:~# k describe node test|grep Capacity -A7
Capacity:cpu:                48ephemeral-storage:  460364840Kihugepages-1Gi:      0hugepages-2Mi:      0memory:             98260824Kinvidia.com/gpu:     2pods:               110

可以看到,除了常见的 cpu、memory 之外,还有nvidia.com/gpu, 这个就是GPU资源,数量为 2 说明我们有两张 GPU。当我们要为pod分配GPU资源的时候也比较简单,只需要在resources.limits中加入nvidia.com/gpu: 1,就可以为pod申请一块GPU资源了。

具体实现

而具体来说NVDIA device plugin主要是通过实现ListAndWatch 接口来上报节点上的GPU数量,实现Allocate接口, 支持分配GPU的行为。

这部分的关键源码如下:

// ListAndWatch lists devices and update that list according to the health status
func (plugin *NvidiaDevicePlugin) ListAndWatch(e *pluginapi.Empty, s pluginapi.DevicePlugin_ListAndWatchServer) error {s.Send(&pluginapi.ListAndWatchResponse{Devices: plugin.apiDevices()})for {select {case <-plugin.stop:return nilcase d := <-plugin.health:// 收到某个设备有健康问题,标志该设备不健康// FIXME: there is no way to recover from the Unhealthy state.d.Health = pluginapi.Unhealthylog.Printf("'%s' device marked unhealthy: %s", plugin.rm.Resource(), d.ID)// 重新发送新的可用的device列表s.Send(&pluginapi.ListAndWatchResponse{Devices: plugin.apiDevices()})}}
}// Allocat主要是分配显卡,给容器指定要附加的NVIDIA_VISIBLE_DEVICES环境变量
func (plugin *NvidiaDevicePlugin) Allocate(ctx context.Context, reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {responses := pluginapi.AllocateResponse{}// 为每个请求分配设备for _, req := range reqs.ContainerRequests {if plugin.config.Sharing.TimeSlicing.FailRequestsGreaterThanOne && rm.AnnotatedIDs(req.DevicesIDs).AnyHasAnnotations() {if len(req.DevicesIDs) > 1 {return nil, fmt.Errorf("request for '%v: %v' too large: maximum request size for shared resources is 1", plugin.rm.Resource(), len(req.DevicesIDs))}}// 判断一下申请的设备ID是不是自己所管理的,也就是所拥有的设备,也就是校验是不是自己注册的那些设备for _, id := range req.DevicesIDs {if !plugin.rm.Devices().Contains(id) {return nil, fmt.Errorf("invalid allocation request for '%s': unknown device: %s", plugin.rm.Resource(), id)}}response := pluginapi.ContainerAllocateResponse{}// 将注册时的设备ID转换为具体的gpu idids := req.DevicesIDsdeviceIDs := plugin.deviceIDsFromAnnotatedDeviceIDs(ids)// 将分配的设备信息保存到Env里面去,后续docker的runC将设备信息以环境变量的形式注入到容器if *plugin.config.Flags.Plugin.DeviceListStrategy == spec.DeviceListStrategyEnvvar {response.Envs = plugin.apiEnvs(plugin.deviceListEnvvar, deviceIDs)}if *plugin.config.Flags.Plugin.DeviceListStrategy == spec.DeviceListStrategyVolumeMounts {response.Envs = plugin.apiEnvs(plugin.deviceListEnvvar, []string{deviceListAsVolumeMountsContainerPathRoot})response.Mounts = plugin.apiMounts(deviceIDs)}if *plugin.config.Flags.Plugin.PassDeviceSpecs {response.Devices = plugin.apiDeviceSpecs(*plugin.config.Flags.NvidiaDriverRoot, ids)}if *plugin.config.Flags.GDSEnabled {response.Envs["NVIDIA_GDS"] = "enabled"}if *plugin.config.Flags.MOFEDEnabled {response.Envs["NVIDIA_MOFED"] = "enabled"}responses.ContainerResponses = append(responses.ContainerResponses, &response)}return &responses, nil
}

整个Kubernetes调度GPU的过程如下:

  1. GPU Device plugin 部署到GPU节点上,通过ListAndWatch接口,上报注册节点的GPU信息和对应的DeviceID。
  2. 当有声明nvidia.com/gpu的GPU Pod创建出现,调度器会综合考虑GPU设备的空闲情况,将Pod调度到有充足GPU设备的节点上。
  3. 节点上的kubelet启动Pod时,根据request中的声明调用各个Device plugin的allocate接口,由于容器声明了GPU。kubelet根据之前ListAndWatch接口收到的Device信息,选取合适的设备,DeviceID作为参数,调用GPU DevicePlugin的allocate接口。
  4. GPU device plugin接收到调用,将DeviceID转换为NVIDIA_VISIBLE_DEVICES环境变量,返回给kubelet
  5. kubelet收到返回内容后,会自动将返回的环境变量注入到容器中,并开始创建容器
  6. 容器创建时,nvidia-container-runtime调用gpu-containers-runtime-hook根据容器的NVIDIA_VISIBLE_DEVICES环境变量,来决定这个容器是否为GPU容器,并且可以使用哪些GPU设备。如果没有携带NVIDIA_VISIBLE_DEVICES这个环境变量,那么就会按照普通的docker启动方式来启动。

使用GPU Operator安装

GPU Operator旨在简化在Kubernetes环境中使用GPU的过程,通过自动化的方式处理GPU驱动程序安装、Controller Toolkit、Device-Plugin 、监控等组件。
NVIDIA GPU Operator总共包含如下的几个组件:

  • NFD(Node Feature Discovery): 用于给节点打上某些标签,这些标签包括 cpu id、内核版本、操作系统版本、是不是GPU节点等,其中需要关注的标签是nvidia.com/gpu.present=true,如果节点存在该标签,那么说明该节点是GPU节点。
  • GFD(GPU Feature Discovery): 用于收集节点的GPU设备属性(GPU驱动版本、GPU型号等),并将这些属性以节点标签的方式透出。在k8s集群中以DaemonSet方式部署,只有节点拥有标签nvidia.com/gpu.present=true时,DaemonSet控制的Pod才会在该节点上运行。
    • 新版本 GFD迁移到了NVIDIA/k8s-device-plugin
  • NVIDIA Driver Installer:基于容器的方式在节点上安装 NVIDIA GPU驱动,在k8s集群中以DaemonSet 方式部署,只有节点拥有标签nvidia.com/gpu.present=true时,DaemonSet 控制的 Pod 才会在该节点上运行。
  • NVIDIA Container Toolkit Installer:能够实现在容器中使用GPU设备,在k8s集群中以DaemonSet 方式部署,同样的,只有节点拥有标签nvidia.com/gpu.present=true时,DaemonSet 控制的 Pod 才会在该节点上运行。
  • NVIDIA Device Plugin:NVIDIA Device Plugin 用于实现将GPU设备以 Kubernetes 扩展资源的方式供用户使用,在k8s集群中以DaemonSet 方式部署,只有节点拥有标签nvidia.com/gpu.present=true时,DaemonSet 控制的 Pod 才会在该节点上运行。
  • DCGM Exporter:周期性的收集节点GPU设备的状态(当前温度、总的显存、已使用显存、使用率等)并暴露Metrics,结合Prometheus和Grafana使用。在k8s集群中以DaemonSet 方式部署,只有节点拥有标签nvidia.com/gpu.present=true时,DaemonSet 控制的Pod才会在该节点上运行。

首先是 GFD、NFD,二者都是用于发现 Node 上的信息,并以label形式添加到k8snode对象上,特别是GFD会添加nvidia.com/gpu.present=true标签表示该节点有GPU,只有携带该标签的节点才会安装后续组件。
然后则是Driver Installer、Container Toolkit Installer用于安装GPU驱动和container toolkit。
接下来这是device-plugin让k8s能感知到GPU资源信息便于调度和管理。
最后的exporter则是采集GPU监控并以Prometheus Metrics格式暴露,用于做GPU监控。

这里着重提及一下NVIDIA Driver InstallerNVIDIA Container Toolkit Installer是如何通过容器的方式来给主机安装对应的内容的。
其安装的主要方法还是通过hostPath挂载的方式来将相关的目录挂载进容器中,然后控制容器将对应的内容添加进目录里。而如果要将相关内容卸载,也是只需要将对应的容器删除,容器就会自动移除相应安装的内容。

GPU Operator虽然方便了安装但是仍然存在一些缺点:

  • Driver Installer 以DaemonSet 方式运行的,每个节点上运行的 Pod 都一样,但是镜像由 驱动版本+内核版本+操作系统版本拼接而成,因此需要集群中所有节点操作系统一致
  • NVIDIA Container Toolkit Installer 同样是以DaemonSet 方式运行的,另外安装时需要指定 Runtime,这也造成了集群的节点必须安装相同的 Container Runtime

思考

目前学习下来的一大感受就是GPU管理的粒度很粗,都是以整卡为单位进行划分,这势必会造成资源的浪费,如何结合GPU卡虚拟化来进行细粒度的分配感觉是一个很大的问题。同时它也没有刻画GPU卡之间、CPU与GPU卡之间的拓扑关系,而当前对于大模型训练其网络管理与优化是非常重要的。

参考资料

  1. https://www.lixueduan.com/posts/ai/01-how-to-use-gpu
  2. https://www.lixueduan.com/posts/ai/02-gpu-operator/
  3. https://www.aneasystone.com/archives/2023/12/scheduling-gpus-in-kubernetes.html
  4. https://blog.csdn.net/qq_43684922/article/details/127024933
  5. https://kubernetes.io/zh-cn/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/
  6. https://kubernetes.io/zh-cn/docs/tasks/manage-gpus/scheduling-gpus/
  7. https://blog.csdn.net/qq_43684922/article/details/127025776
  8. https://www.lixueduan.com/posts/kubernetes/21-device-plugin/#

http://www.ppmy.cn/embedded/138990.html

相关文章

第十五届蓝桥杯JAVA的B组题目详情解析

(第一个填空太简单&#xff0c;就不写了,根本不用代码&#xff0c;直接excel计算) 目录 蓝桥杯第二个填空&#xff0c;类斐波那契循环数 蓝桥杯JAVA.b组第三题 -分布式队列(模拟) 食堂(蓝桥杯D题) ​编辑 星际旅行(Floyd佛洛依德) 其余的有点变态&#xff0c;感觉学了好像…

PNG图片隐写之IDAT

IDAT 结构 在PNG文件中&#xff0c;每个块&#xff08;包括IDAT块&#xff09;的结构是固定的&#xff0c;CRC校验码总是位于每个块的末尾。具体来说&#xff0c;每个块的结构如下&#xff1a; 长度&#xff08;4字节&#xff09;&#xff1a;表示数据部分的长度。 类型&…

如何通过统计来反映工业新产业发展情况

工业战略性新兴产业对经济全局和长远发展具有重大引领带动作用&#xff0c;如何通过统计来反映工业新产业发展情况&#xff1f; 战略性新兴产业是以重大技术突破和重大发展需求为基础&#xff0c;对经济社会全局和长远发展具有重大引领带动作用&#xff0c;知识技术密集、物质…

在openi平台 基于华为顶级深度计算平台 openmind 动手实践

大家可能一直疑问&#xff0c;到底大模型在哪里有用。 本人从事的大模型有几个方向的业务。 基于生成式语言模型的海事航行警告结构化解析。 基于生成式语言模型的航空航行警告结构化解析。 基于生成式生物序列&#xff08;蛋白质、有机物、rna、dna、mrna&#xff09;的多模态…

国产RestApi工具Apifox使用介绍

常见RestApi工具介绍 常见的接口工具有Postman、Swagger等&#xff0c;当然还有其他很多种&#xff0c;就不列举了&#xff0c;在遇到Apifox之前&#xff0c;我一直都使用的Postman&#xff0c;但是Postman有个弊端&#xff0c;就是网络问题&#xff0c;还有就是免费有限制&…

HBase 开发:使用Java操作HBase

1、实战简介 HBase和Hadoop一样&#xff0c;都是用Java进行开发的&#xff0c;本次实训我们就来学习如何使用Java编写代码来操作HBase数据库。 实验环境&#xff1a; hadoop-2.7 JDK8.0 HBase2.1.1 2、任务 1、第1关&#xff1a;创建表 package step1; import java.io.IOE…

JavaScript 高级—求数组的最大值与最小值

JavaScript 数组操作&#xff1a;求数组的最大值与最小值 在编程中&#xff0c;处理数组是一个常见的任务。JavaScript 提供了一些内建的方法和技巧&#xff0c;可以方便地找到数组中的最大值和最小值。今天&#xff0c;我们就来详细探讨一下如何使用 JavaScript 来实现这一功…

读懂top后显示内容

第一行&#xff1a;系统信息 top - 06:33:12 up 42 min, 1 user, load average: 0.04, 0.02, 0.00 06:33:12&#xff1a;当前时间。up 42 min&#xff1a;系统已经启动了 42 分钟。1 user&#xff1a;当前有 1 个用户登录。load average: 0.04, 0.02, 0.00&#xff1a;这三个…