资讯动态

Kubernetes GPU调度全链路解析:从设备插件到Pod拿到显卡

发布时间:2026/9/12 1:24:05 来源:尧图企业网站定制
先说一个真实场景上个月运维群里突然炸了新买的一批带GPU的计算节点加进集群kubectl get nodes看节点状态是Ready但用户的训练任务全部卡在Pending。大家第一反应是驱动没装好登录节点一看nvidia-smi正常再查镜像没问题最后翻了半天才发现设备插件根本没有把nvidia.com/gpu这段资源报上去。这类问题在Kubernetes GPU集群里太典型了——GPU从节点上架到Pod真正拿到显卡中间隔着好几层组件任何一层断了表面现象都一样Pod起不来。1. 整体链路拆解GPU资源靠谁一步步“搬”到Pod里1.1 一条GPU调度请求经历了多少个环节Kubernetes集群里跑GPU任务难点往往不在“调度”这一步而在于链路太长。从物理节点上架到容器里能运行nvidia-smi至少得经过四个环节环节核心组件职责节点准备宿主机驱动、nvidia-container-toolkit、containerd让宿主机内核认识GPU让容器运行时具备GPU设备注入能力资源注册nvidia-device-plugin、kubelet把物理GPU数量上报为节点可分配资源调度决策kube-scheduler根据资源请求挑选满足GPU数量的节点设备注入kubelet、设备插件、容器运行时在Pod创建时把对应设备文件和环境变量注入容器只要其中一个环节配合不好最终体现都是PodPending或者容器里看不到GPU。这跟CPU、内存的调度有本质区别CPU和内存是kubelet原生统计的资源而GPU完全依赖设备插件动态上报所以每次排查问题都得沿着上面四个环节一层层拆。1.2 GPU为什么不能像CPU内存那样直接被调度Kubernetes原生的资源模型是CPU和内存调度器只需要看节点上的已分配量就能判断是否满足请求。GPU是外部设备有两个关键问题kubelet本身不知道节点上有几张卡、每张卡显存多少、能不能被容器使用。GPU卡不是普通文件设备容器要看到它必须由运行时挂载/dev/nvidia0、/dev/nvidiactl等设备文件还要注入CUDA相关环境变量。所以Kubernetes引入了设备插件Device Plugin机制。GPU的数量天然适合用Extended Resource扩展资源表达——它是一个整数比如nvidia.com/gpu: 8调度器做数量匹配kubelet记录分配量。但显存大小、SM算力这些属性没有原生字段所以后面会讲到默认情况下调度器只认“卡数”不认“显存”。2. 节点上架驱动、运行时和kubelet的准备2.1 装驱动与nvidia-container-toolkit为什么一个都不能少新节点上架第一步是装NVIDIA GPU驱动。这句话说起来简单但很多人会在“驱动装在宿主机内核”这件事上踩坑。Kubernetes的容器运行时不会替宿主机加载内核驱动所以驱动必须装在宿主机上。装好之后确认nvidia-smi能正常输出nvidia-smi如果能看到GPU型号和驱动版本说明驱动OK。常见Linux发行版装驱动方式不同推荐用NVIDIA官方runfile或者发行版包管理器但生产环境千万不要随意升级驱动。原因很直接CUDA容器镜像里的CUDA版本一般向下兼容驱动但驱动升级可能引起内核模块重新编译尤其是和gcc、kernel-devel版本不匹配的时候折腾起来很麻烦。接下来是nvidia-container-toolkit它的作用是给容器运行时提供GPU支持。只装驱动不装它containerd底层的runc根本不会把GPU设备送进容器。# 以Ubuntu/Debian为例 distribution$(. /etc/os-release;echo $ID$VERSION_ID) curl -s -L https://nvidia.github.io/nvidia-container-runtime/gpgkey | sudo apt-key add - curl -s -L https://nvidia.github.io/nvidia-container-runtime/$distribution/nvidia-container-runtime.list | sudo tee /etc/apt/sources.list.d/nvidia-container-runtime.list sudo apt-get update sudo apt-get install -y nvidia-container-toolkit装完之后会多出几个关键组件nvidia-container-runtime和nvidia-container-cli。前者是一个符合OCI规范的运行时垫片后者负责实际读取设备信息、修改容器配置。后面配置containerd时全靠它们。2.2 修改containerd运行时为nvidia默认的/etc/containerd/config.toml不认识GPU需要把默认运行时配置成nvidia-container-runtime。这里有一个我建议的配置段基于Kubernetes社区标准配置文件修改version 2 [plugins.io.containerd.grpc.v1.cri] [plugins.io.containerd.grpc.v1.cri.containerd] default_runtime_name nvidia [plugins.io.containerd.grpc.v1.cri.containerd.runtimes] [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.nvidia] runtime_type io.containerd.runc.v2 runtime_engine runtime_root privileged_without_host_devices false [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.nvidia.options] BinaryName /usr/bin/nvidia-container-runtime改完必须重启containerdsudo systemctl restart containerd重启之后立刻做一个验证拉一个CUDA基础镜像用ctr手动跑一个容器看看里面能不能看到GPUctr image pull docker.io/nvidia/cuda:12.2.0-base-ubuntu20.04 ctr run --rm --runtime io.containerd.runc.v2 \ --env NVIDIA_VISIBLE_DEVICESall \ docker.io/nvidia/cuda:12.2.0-base-ubuntu20.04 gpu-test \ nvidia-smi如果容器里能正常输出GPU信息说明containerd的nvidia运行时已经生效。这一步看似简单但很多人改完配置不重启containerd结果还是用的默认runc后面设备插件怎么启动都没用。2.3 kubelet参数与节点标签、污点节点侧的最后一步是让kubelet能接受设备插件注册的扩展资源。默认情况下只要kubelet没有禁用设备插件功能你不需要额外加启动参数。但有两个建议在生产环境做给GPU节点打标签比如gpu-nodetrue、gpu-typeampere方便业务通过nodeSelector定向调度。如果这批节点是GPU专用节点给节点加污点比如nvidia.com/gputrue:NoSchedule防止普通业务Pod随机跑上来占用CPU和内存。打标签和加污点可以这样操作kubectl label node gpu-node-01 gpu-nodetrue gpu-typeampere kubectl taint node gpu-node-01 nvidia.com/gputrue:NoSchedule打标签的好处是调度可控加污点则是资源隔离的兜底。实际用下来GPU节点不加污点的话很容易被一些无状态微服务Pod占满等真正要跑训练任务时CPU和内存反而成了瓶颈。3. 设备插件GPU资源如何注册成为Extended Resource3.1 Extended Resource机制设备插件已经注册到kubelet的gRPC接口上它做的事情就是向kubelet“汇报”这个节点有几张GPU。kubelet把这些GPU数量合并到节点的Capacity和Allocatable里资源名就叫nvidia.com/gpu。扩展资源有几个使用限制只能上报整数不能是0.5张卡。节点资源是“非超卖”的即调度器只能根据请求量匹配不能像CPU那样压缩共享。Pod的requests和limits必须一致。正因为扩展资源模型简单调度器只能做数量匹配这就导致了默认情况下GPU是整卡独占的。后面第5节会展开讲各种共享方案。3.2 部署nvidia-device-pluginNVIDIA官方提供nvidia-device-plugin通常以DaemonSet方式部署保证每个GPU节点都有一个Pod来上报资源。我平时用的YAML模板是apiVersion: apps/v1 kind: DaemonSet metadata: name: nvidia-device-plugin-daemonset namespace: kube-system spec: selector: matchLabels: name: nvidia-device-plugin-ds updateStrategy: type: RollingUpdate template: metadata: labels: name: nvidia-device-plugin-ds spec: priorityClassName: system-node-critical tolerations: - key: nvidia.com/gpu operator: Exists effect: NoSchedule containers: - name: nvidia-device-plugin-ctr image: nvcr.io/nvidia/k8s-device-plugin:v0.16.2 args: - --fail-on-init-errorfalse env: - name: NVIDIA_VISIBLE_DEVICES value: all securityContext: allowPrivilegeEscalation: false capabilities: drop: [ALL] volumeMounts: - name: device-plugin mountPath: /var/lib/kubelet/device-plugins - name: nvidia-driver mountPath: /var/lib/nvidia - name: nvidia-smi mountPath: /usr/local/nvidia/bin volumes: - name: device-plugin hostPath: path: /var/lib/kubelet/device-plugins - name: nvidia-driver hostPath: path: /var/lib/nvidia - name: nvidia-smi hostPath: path: /usr/local/nvidia/bin这里有几个参数值得解释--fail-on-init-errorfalse如果节点上驱动暂时没就绪设备插件不会疯狂崩溃重启而是等待后续重启恢复。NVIDIA_VISIBLE_DEVICESall让设备插件看到所有GPU。toleration对应前面加的节点污点否则设备插件Pod会被这个节点“挡住”。部署完之后查看日志确认设备插件正常注册kubectl logs -n kube-system -l namenvidia-device-plugin-ds日志里会出现类似Starting device plugin with ...、Registered device plugin with kubelet的内容。3.3 设备插件的工作流程设备插件启动时会通过Unix Socket/var/lib/kubelet/device-plugins/nvidia.sock向kubelet注册。注册成功后kubelet会调用它的ListAndWatch接口获取可用设备列表。ListAndWatch不是一次性调用而是持续监听。如果某张卡发生故障设备插件需要把设备从列表里移除kubelet会同步更新节点可分配资源。当Pod调度到该节点并需要GPU时kubelet会调用设备插件的Allocate接口。对于nvidia-device-pluginAllocate主要做两件事返回该容器要挂载的设备文件列表比如/dev/nvidia0、/dev/nvidiactl、/dev/nvidia-uvm。返回环境变量最重要的是NVIDIA_VISIBLE_DEVICESGPU-xxx或0,1。容器运行时接着根据这些设备文件和环境变量结合nvidia-container-toolkit完成真正的设备注入。所以说设备插件是“调度计数”和“容器设备可见”之间的桥梁。3.4 验证节点GPU资源部署完成并等待一段时间后查看节点资源kubectl describe node gpu-node-01 | grep -A5 nvidia.com/gpu预期输出类似Capacity: nvidia.com/gpu: 8 Allocatable: nvidia.com/gpu: 8如果Capacity和Allocatable里没有nvidia.com/gpu基本可以断定设备插件没注册成功。此时先去查设备插件日志再检查宿主机/var/lib/kubelet/device-plugins/下有没有socket文件。常见问题在第6节展开。4. 从调度器到Pod拿到显卡的完整旅程4.1 调度器如何理解GPU需求默认的kube-scheduler并不认识“GPU”是什么它只认识nvidia.com/gpu这个扩展资源名。调度时它会去检查每个节点上nvidia.com/gpu的Allocatable量、已分配量以及当前Pod的请求量。如果Pod请求nvidia.com/gpu: 1节点可用量为0该节点就会被过滤掉。由于扩展资源不能超卖requests和limits必须一致否则调度器会拒绝调度。这句话我见过很多人误解以为只写limits就行实际上最好写清楚resources: requests: nvidia.com/gpu: 1 limits: nvidia.com/gpu: 1同时如果节点有污点而Pod没有对应的容忍度调度器也会跳过该节点。4.2 写一个需要GPU的Pod一个最简单的GPU测试Pod可以这样写apiVersion: v1 kind: Pod metadata: name: cuda-vector-add spec: restartPolicy: OnFailure containers: - name: cuda image: nvcr.io/nvidia/k8s/cuda-vector-add:v0.1 resources: limits: nvidia.com/gpu: 1这是NVIDIA官方提供的一个向量加法测试镜像它会申请一张GPU并跑一段CUDA代码。如果想用nvidia-smi验证可以换用apiVersion: v1 kind: Pod metadata: name: gpu-smi-test spec: restartPolicy: Never containers: - name: cuda image: nvidia/cuda:12.2.0-base-ubuntu20.04 command: [nvidia-smi] resources: requests: nvidia.com/gpu: 1 limits: nvidia.com/gpu: 1创建之后观察调度状态kubectl apply -f gpu-smi-test.yaml kubectl get pod gpu-smi-test -o wide kubectl logs gpu-smi-test正常情况下日志会显示该Pod被调度到某个GPU节点容器内能识别到一张显卡。4.3 调度绑定、容器创建、设备注入调度器选出节点后会把Node IP写入Pod的spec.nodeName这个过程叫绑定。绑定之后kubelet收到Pod的更新事件开始干活Admit阶段kubelet检查Pod请求的资源是否满足节点剩余资源包括nvidia.com/gpu。调用设备插件的Allocate拿到设备列表和环境变量。通过CRI调用containerd创建sandbox和容器。containerd找到nvidia-container-runtimeruntime读取NVIDIA_VISIBLE_DEVICES用nvidia-container-cli把/dev/nvidia0等设备挂载进容器。这一步常常被误解为“设备插件把设备塞进容器”准确来说设备插件只负责告诉kubelet“哪些设备可用、需要哪些环境变量”真正执行挂载的是容器运行时和toolkit。这也解释了为什么第2节的环境准备这么重要。4.4 多卡Pod与指定GPU如果训练任务需要两张卡直接把nvidia.com/gpu的请求量写成2resources: requests: nvidia.com/gpu: 2 limits: nvidia.com/gpu: 2设备插件默认会从节点空闲GPU里挑两个设备。但注意它不会保证一定给你“显存最大的两块”也不会保证物理上相邻调度器更不会感知PCIe拓扑。所以对高性能场景尤其是多机多卡通信敏感的任务单纯靠K8s默认调度是不够的还需要结合nvidia-device-plugin的--device-list-strategy配合NVIDIA的Topology-aware allocation或者额外的调度器扩展。业务侧如果想控制容器里具体看到哪块卡可以在容器启动脚本里设置CUDA_VISIBLE_DEVICES不过有个前提设备插件已经通过NVIDIA_VISIBLE_DEVICES限制了容器可见设备集合所以业务看到的是“调度器/设备插件分配后的卡”不一定是宿主机的物理序号。5. 进阶调度能力GPU共享、时间切片与MIG5.1 整卡调度的浪费默认情况下一个GPU请求就独占一张物理卡。模型推理任务、批量图像处理任务往往显存和算力都用不满比如一张24GB的卡只用了4GB这时整卡调度就是巨大的成本浪费。我见过不少团队为了“省事”一直用整卡调度结果GPU利用率长期在10%以下资源成本却很高。这就有了下面几种优化方案。5.2 Time-Slicing最简单的共享方式NVIDIA官方nvidia-device-plugin支持通过配置文件把一张物理卡“虚拟”成多张逻辑卡。比如一张40GB的A100配置成4个10GB的“虚拟GPU”上报给K8s的nvidia.com/gpu就是4个Pod只看见其中的一个“设备”。配置文件示例apiVersion: v1 kind: ConfigMap metadata: name: nvidia-device-plugin-config namespace: kube-system data: time-slicing-config.yaml: | version: v1 sharing: timeSlicing: resources: - name: nvidia.com/gpu replicas: 4然后在DaemonSet中挂载这个ConfigMap并指定配置文件路径args: - --config-file/config/time-slicing-config.yaml volumeMounts: - name: nvidia-device-plugin-config mountPath: /configTime-Slicing方案的优点是真的“零硬件门槛”老卡也能用缺点是显存不隔离多个Pod共享一块物理卡一个任务的显存OOM可能把整张卡搞挂算力也是时间片抢占对需要稳定资源的训练任务不友好。5.3 MIG硬件级隔离A100、H100等卡支持MIGMulti-Instance GPU可以把一张物理GPU切成多个实例每个实例拥有独立的显存和计算单元。比如A100 40GB可以切成1g.5gb、1g.10gb、2g.10gb等不同规格。启用MIG需要宿主机上操作nvidia-smi -mig 1 nvidia-smi mig -cgi 1g.10gb,1g.10gb,2g.10gb -C同时要让device-plugin以MIG策略运行args: - --mig-strategysingleMIG的好处是硬件级隔离显存和算力真正分开适合把GPU池按规格拆分给不同业务缺点是必须用新卡配置复杂而且不同规格的MIG实例调度策略需要和业务匹配。从运维角度看如果业务分为“大模型训练”和“小模型推理”两大类MIG是很理想的选择物理卡切完每个实例是独立资源互不干扰。5.4 显存维度调度方案HAMI与Volcano如果业务需要“按显存申请”而不是“按卡申请”MIG和Time-Slicing都不够灵活。社区有几个方案HAMI一款开源GPU共享调度器核心思路是替换设备插件把显存作为可调度资源Pod可以申请如nvidia.com/gpu-mem: 4Gi动态分配物理卡和显存比例。Volcano更偏向批量任务调度支持GPU显存调度、公平调度等适合AI训练和推理混部场景。这类方案相当于在调度层和设备插件层做了增强需要额外部署和控制面组件。我的建议是当GPU共享和显存维度的需求成为刚需时再引入单纯追求“用满显存”而引入一套新组件排查问题成本也会上升。默认设备插件加Time-Slicing能覆盖大部分团队需求。6. 常见问题排查与避坑实录6.1 节点Ready但Pod一直Pending这是最典型的“假正常”问题。看到节点Ready认为节点没问题但Pod就是调度不上去。快速排查三板斧kubectl describe node node-name | grep -A5 nvidia.com/gpu kubectl logs -n kube-system -l namenvidia-device-plugin-ds --tail50 kubectl describe pod pod-name | grep -A5 Events如果节点上根本没有nvidia.com/gpu优先检查设备插件是否被部署到该节点。比如我给GPU节点打了污点但DaemonSet没有对应容忍度设备插件Pod会被调度到其他节点资源上报自然也就没了。其次是设备插件启动失败原因可能是驱动未加载或者宿主机/var/lib/kubelet/device-plugins目录权限不对。事件里一般会写明。6.2 容器里nvidia-smi报错could not select device driver这个报错出现时Pod能创建成功nvidia-smi执行却报错说明容器运行时并用没有真正使用nvidia runtime。最常见的两个原因containerd的config.toml里把默认运行时写成了runc设备注入没生效。改了config.toml后没有重启containerd。排查方法crictl info | grep runtime确认当前默认运行时是不是nvidia。如果不对检查配置并重启containerd然后重新创建Pod。还有一个点要小心有的发行版nvidia-container-runtime路径不是/usr/bin/nvidia-container-runtime用which nvidia-container-runtime确认。6.3 同节点多个Pod挤在同一张卡默认情况下如果节点有4张卡4个Pod各自申请1张理论上是分散的。但某些配置或版本异常时可能出现多个Pod都用同一张卡的情况。优先看设备插件日志里Allocate送的设备IDkubectl logs -n kube-system -l namenvidia-device-plugin-ds --tail100 | grep -i allocate如果日志显示分配到不同设备ID那容器里看到的卡片数仍不对多半是业务镜像里自己设置了CUDA_VISIBLE_DEVICES0把设备插件之前设置的环境变量覆盖了。这里要提醒业务团队镜像内不要硬编码GPU设备号。6.4 内核升级后GPU节点反复异常很多集群在节点维护时升级内核升级后驱动模块和内核不匹配nvidia-smi会报类似Failed to initialize NVML: Driver/library version mismatch。设备插件会反复重启节点上的nvidia.com/gpu资源会掉。这类问题一般需要重建驱动模块比如使用DKMS方式安装驱动sudo dkms autoinstall sudo modprobe nvidia节点维护的正确姿势是kubectl cordon先把节点上的Pod排空再升级内核和驱动确认nvidia-smi恢复后再kubectl uncordon。不要在大批量任务正在运行时直接重启节点。6.5 避免在同一节点上部署多套设备插件有些团队试过在GPU集群里同时部署官方nvidia-device-plugin和第三方的HAMI、Volcano插件想着能兼具两种功能结果往往出现nvidia.com/gpu资源重复上报、调度混乱的问题。设备插件机制本质上要求同一资源名只能有一个提供者。如果需要切到HAMI等方案完整卸载官方device-plugin清理/var/lib/kubelet/device-plugins/下的旧socket文件重启kubelet再部署新方案。这是我在一个客户环境里亲眼见过的问题两套插件同时存在调度器看到8个GPU实际只有4张卡大量Pod卡在等待资源。6.6 问题排查速查表现象优先检查项大概率原因节点无nvidia.com/gpu设备插件日志、节点污点插件未被调度、驱动未加载Pod Pendingdescribe node、describe pod节点GPU资源不足或没有容忍污点容器内看不到GPUcontainerd运行时配置默认运行时仍是runc多Pod挤一张卡设备插件Allocate日志镜像硬编码设备号内核升级后资源掉nvidia-smi、dkms驱动模块未重新编译结尾回头再看GPU调度这件事我最大的体会是Kubernetes的GPU管理远不只是“调度器认资源”那么简单它依赖驱动、运行时、设备插件、kubelet四层配合。新节点上架时严格按照“驱动 - toolkit - containerd运行时 - 设备插件注册 - 验证资源”的顺序来能省掉后面大量排查时间。另外我的个人习惯是每次动节点前先cordon和drain每次改配置后确认重启服务每次部署新设备插件前先清理旧socket文件。这些小细节看起来不起眼但真的能帮你少熬好几个通宵。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价