首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

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

  • 25-03-07 04:40
  • 4435
  • 6039
blog.csdn.net

背景

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

物理机如何使用GPU

如果给一台普通的物理机,例如我们日常用的笔记本电脑应该如何使用GPU呢。其主要涉及到两个插件的安装,分别是Nvidia Driver和CUDA 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"
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

sudo nvidia-ctk runtime configure --runtime=docker
  • 1

然后重启 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:                48
  ephemeral-storage:  460364840Ki
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             98260824Ki
  nvidia.com/gpu:     2
  pods:               110
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

可以看到,除了常见的 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 nil
        case d := <-plugin.health:
            // 收到某个设备有健康问题,标志该设备不健康
            // FIXME: there is no way to recover from the Unhealthy state.
            d.Health = pluginapi.Unhealthy
            log.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 id
        ids := req.DevicesIDs
        deviceIDs := 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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

整个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 Installer和NVIDIA 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. http://iyenn.com/rec/1720196.html
  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. http://iyenn.com/rec/1720197.html
  8. https://www.lixueduan.com/posts/kubernetes/21-device-plugin/#
注:本文转载自blog.csdn.net的啊滑滑蛋的文章"https://blog.csdn.net/slipegg/article/details/143882492"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

115
云原生
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top