
干 GPU 集群运维这几年最常被问的一句话就是我的 Pod 明明提交了为什么一直 Pending十个里面有一半最后查出来都是 GPU 资源的问题。Kubernetes 默认只认识 CPU 和内存GPU 这种稀缺资源你想让任务真正跑起来中间隔着一整套完整的调度与管理机制节点要上架、驱动要匹配、设备插件要上报资源、调度器要会数数、容器运行时还得把 GPU 正确塞给容器。今天就把这条链路从头到尾拆一遍从一张裸卡到你的 Pod 拿到 GPU每一步到底发生什么为什么会有这些坑都写清楚。1. 拆链路GPU 是怎么被 Kubernetes 认识并分配的1.1 Kubernetes 原生只认识 CPU 和内存Kubernetes 调度器本质上是一个非常死板的“资源会计”。默认情况下它管理节点上的资源类型只有 CPU 和内存最多再加上一些临时存储。节点加入集群后会通过 kubelet 上报自己的可分配资源调度器看到的就是某节点有 8 核 CPU、16Gi 内存这种信息它根本不知道这台机器插了四张 A100 还是两张 4090。为了解决这个问题Kubernetes 引入了扩展资源Extended Resource。这类资源的名字必须带域名前缀比如nvidia.com/gpu、amd.com/gpu、huawei.com/Ascend910。扩展资源有非常关键的三个特性第一数量只能是整数你不能声明 0.5 张 GPU第二不能超卖调度器只会在节点当前可分配的 GPU 数量大于等于你请求的数量时才放行第三它会被计入节点的可分配容量参与调度判定。简单说扩展资源是一种“只会数数”的资源调度器只关心你有几个不关心你是 A100 还是 V100也不关心显存大小、卡之间的拓扑关系。这里就容易踩第一个坑很多人以为给节点装上 NVIDIA 驱动Kubernetes 就会自动看到 GPU其实完全不是这么回事。驱动装好之后节点在 Kubernetes 眼里仍然是一个只有 CPU 和内存的普通节点想让 GPU 出现在资源清单里得靠下一步的 Device Plugin。1.2 Device Plugin 是连接 kubelet 与底层驱动的翻译官单靠 kubelet 自己它不知道如何枚举 GPU 设备也不知道某个容器该分配哪一块 GPU。Kubernetes 为此设计了一套官方接口叫 Device Plugin。每个节点上跑一个设备插件这个插件以 Unix Socket 方式跟本节点的 kubelet 通信。插件的核心逻辑分两段。第一段是 ListAndWatch插件启动后把节点上所有的 GPU 设备枚举出来上报给 kubelet之后持续监听设备状态一旦有卡坏掉或者被拔掉就通过这个接口通知 kubelet 同步更新。第二段是 Allocate当某个 Pod 被调度到该节点并请求 GPU 时kubelet 会调用这个接口告诉插件“这个容器需要几块 GPU”插件决定具体给哪几块卡然后把分配结果返回。返回的内容很有讲究包括三类东西一是环境变量最典型的就是NVIDIA_VISIBLE_DEVICES指定这个容器能“看见”哪几个 GPU二是挂载点比如宿主机的 CUDA 库、驱动目录需要以 volume 形式挂载进容器三是 device 文件比如/dev/nvidia0、/dev/nvidiactl让容器能真正打开设备。kubelet 拿到这些信息后会在创建容器时注入到 OCI spec 里由容器运行时最终落实。你可以把 kubelet 想象成操作系统内核Device Plugin 就是显卡驱动这个类比虽然不严谨但新同事基本都能秒懂。2. 节点上架实操从物理卡到节点资源清单2.1 第一步先把驱动和容器运行时备齐新节点上架的流程比很多人想象得繁琐。物理插卡只是开始后面每一步都要验证。刚插上卡后先用lspci | grep -i nvidia确认系统认得到这张卡然后装驱动装完一定跑一遍nvidia-smi能看到所有卡的型号、驱动版本、显存才说明宿主机的 GPU 环境是通的。这一步看着基础其实特别容易翻车尤其是服务器上还有别的老卡时驱动版本容易冲突。我自己的经验是优先用官方驱动包配合 DKMS 方式安装这样内核升级后驱动还能自动重新编译装完用nvidia-smi -pm 1开启持久化模式避免频繁加载驱动上下文导致偶发故障。驱动装完容器运行时这一步不能省。容器本身没有设备的访问能力你的容器镜像里也没有显卡驱动它只是用户态程序真正和硬件打交道的是宿主机内核驱动。因此需要把宿主机的/dev/nvidia*设备节点和用户态库libcuda、libnvidia-ml 这些透传进容器再把环境变量设好这个工作就由 NVIDIA Container Toolkit 完成。安装完 Toolkit 之后重点来了必须把你的容器运行时切换到 NVIDIA 的运行时上。Docker 时代比较简单配好 nvidia-container-runtime 后docker run --gpus all ...就能用。但 Kubernetes 1.24 之后很多集群都默认用 containerd这时需要手动修改 containerd 配置。在/etc/containerd/config.toml里注册一个名为nvidia的 runtime[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.nvidia] runtime_type io.containerd.runc.v2 [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.nvidia.options] BinaryName /usr/bin/nvidia-container-runtime改完后重启 containerd再用一条命令验证容器内能否看到 GPUctr run --rm --runtime nvidia docker.io/nvidia/cuda:12.0-base nvidia-smi nvidia-smi如果能正常输出 GPU 列表说明 runtime 层已经打通。这里有个非常常见的坑只装了 Toolkit 但 containerd 里没有配置nvidiaruntime那么 Kubernetes 里跑起来的所有容器都看不了 GPU即使 Device Plugin 正常工作也没用。2.2 Device Plugin 部署与资源上报验证宿主机和运行时准备好之后才算有资格部署 Device Plugin。通常用 DaemonSet 方式部署 NVIDIA 官方维护的 k8s-device-pluginyaml 的核心就是给每个节点起一个 Pod这个 Pod 会挂载宿主机的/var/lib/kubelet/device-plugins目录和/dev目录并持有宿主机的/var/log权限方便插件读取日志。部署完成后验证节点是否上报了 GPU 资源kubectl describe node gpu-node-01 | grep nvidia.com/gpu如果输出类似nvidia.com/gpu: 4说明节点已经正确上报了 4 张 GPU。同时还能看到nvidia.com/gpu.memory这样的附加字段这是较新版本插件通过 GPU Feature DiscoveryGFD组件一起上报的用于给节点补充型号、显存、显存大小等标签。这一步我只强调两件事。第一Device Plugin 必须以 DaemonSet 方式跑因为 kubelet 只和本机上的插件通过 socket 通信远程无效。第二节点数量多的时候一定要给 Device Plugin 配上资源请求和健康检查避免插件 OOM 或异常退出之后无人处理否则你那 8 张卡会一夜之间从集群里“消失”调度器只看得到资源不够根本不知道是卡坏了还是插件死了。3. 从 Pod 声明到 GPU 注入调度与分配全过程拆解3.1 Pod 侧的正确声明姿势应用想用 GPU必须在容器资源声明里写清楚。常规写法是这样的resources: requests: nvidia.com/gpu: 1 limits: nvidia.com/gpu: 1对扩展资源来说requests 和 limits 必须保持一致如果你只写 limitsKubernetes 会自动把 requests 补成同一个值。这不是什么“推荐姿势”而是硬性约定。你也没办法只写 requests 不写 limits调度系统根本不认。这里有个新手容易误解的地方nvidia.com/gpu: 1表示分配 1 块物理 GPU 设备而不是“1G 显存”。你拿到的是一整张卡的全部显存和算力除非用了后面讲的 MIG。如果任务只需要 2GB 显存但节点上只有 40GB 的 A100你也得整卡申请。这是 Kubernetes 默认 GPU 调度最浪费的地方也是后面很多优化方案要解决的核心痛点。另外Pod 声明了 GPU 之后镜像里不需要装显卡驱动但必须装对应版本的 CUDA 用户态库因为容器内运行的程序是跟 CUDA 库链接的。经常有人把宿主机驱动和容器内 CUDA 的关系搞混导致容器起来后nvidia-smi正常但程序跑不起来报找不到libcudart.so这就是镜像里 CUDA 环境缺失。3.2 调度器怎么从“够数”到“选中节点”调度器的工作分几个阶段。过滤阶段它会遍历所有节点把满足资源要求的节点筛出来。对 GPU 而言主要检查的就是节点上nvidia.com/gpu的剩余数量是否大于等于 Pod 的请求数量。如果找不到任何节点能满足Pod 就进入 Pending事件里明确写着0/8 nodes are available: 8 Insufficient nvidia.com/gpu。如果过滤后只剩一个节点那没什么好说的直接选中。如果有多个节点都满足调度器会进入打分阶段。麻烦的是默认调度器的打分插件比如 NodeResourcesLeastAllocated只对 CPU 和内存做加权计算对扩展资源基本不参与打分。换句话说两个节点都有足够 GPU 时调度器大概率是“看着顺眼”随便选一个它根本不会考虑每个节点现在还剩几张卡、这些卡是不是离散的。这导致生产集群里经常出现一种现象节点 A 的 GPU 已经快被打满了节点 B 还有很多空闲卡但新的 GPU Pod 还是被调度到了节点 A因为 A 的 CPU 和内存余量更大、打分更高。解决这个问题没有银弹常见做法是给不同队列或项目的节点打标签配 nodeSelector 或 nodeAffinity或者干脆替换调度器社区里的 Volcano、Kueue 这类组件对 GPU 分配策略的控制更强适合多团队共享大集群的场景。调度优先级也值得留意。如果集群开启了 PriorityClass高优先级 Pod 排队等 GPU 时会尝试抢占低优先级 Pod。这意味着你的训练任务可能不是自己跑完了才结束而是被“借卡”了。之前不少人半夜收到任务中断告警查了半天就是被高优队列抢占。3.3 设备注入的最后一公里调度完成、Pod 被绑到节点后剩余步骤都是在节点本地发生的。kubelet 拿到 Pod 的 spec发现有容器声明了nvidia.com/gpu就会调用本机 Device Plugin 的 Allocate 接口把“这个容器需要 1 块卡”的请求交给插件。插件内部根据当前节点上哪些卡已被占用挑出一块空闲卡返回一堆注入信息。这些注入信息最终被 kubelet 翻译成容器的运行时配置。以 NVIDIA 生态为例注入后的容器环境变量里会有NVIDIA_VISIBLE_DEVICESGPU-xxxxxx这个变量对 nvidia-container-runtime 来说是关键开关它只把变量的那几张卡挂进容器并在 cgroup device controller 里放行对应的设备文件。这就是硬隔离的基础容器 A 不会看到容器 B 正在占用的卡。这个阶段如果出问题通常表现为 Pod 反复重启或者 Ready 不起来。一个很隐蔽的坑是 Device Plugin 返回了设备但容器 runtime 没走 NVIDIA 运行时导致NVIDIA_VISIBLE_DEVICES被当成普通环境变量设备没有真正注入。你kubectl exec进去发现连/dev/nvidia0都没有多半是 containerd 配置还停在默认 runtime。4. 进阶玩法MIG、共享 GPU 与多卡拓扑4.1 MIG把大卡硬切成小份对于 A100、H100 这类大显存卡跑小模型或者并发推理业务时整卡分配太浪费于是 NVIDIA 提供了 MIGMulti-Instance GPU技术。开启 MIG 后一张物理卡可以被划分为多个独立的 GPU 实例每个实例拥有独立的流处理器和显存切片隔离性比软件共享硬得多而且可以独立重启一个实例故障不会影响同卡其他实例。MIG 在 Kubernetes 里使用起来并不复杂但配置链路很繁琐。你需要在宿主机上先通过nvidia-smi mig -ci 0 -gi 1这类命令把卡切好然后在 Device Plugin 的配置里启用 MIG 策略插件就会把每个 MIG 实例当成一个独立的nvidia.com/gpu设备上报。你可以看到同一个节点上报的 GPU 数量从 4 变成了 16每份只有原卡四分之一的算力和显存。这里必须强调一个限制MIG 模式下节点上所有卡最好都切成相同的规格否则调度器还是只会数数量不知道哪块卡对应多大显存。你有可能排到一个小实例但任务需要大显存起来之后直接被显存不足卡住。实践中通常配合 GFD 上报的标签给节点打上nvidia.com/mig-1g.5gb这样的标注再用 nodeSelector 把不同规格的任务送到不同节点。4.2 共享 GPU 的几种玩法与隔离代价MIG 需要硬件支持老一点的 V100、T4 中的一部分不支持 MIG那还想共享 GPU 怎么办最常见的方案是 time-slicing也就是时间片共享。Device Plugin 支持通过环境变量让同一张卡被多个 Pod 共享每个 Pod 依然声明nvidia.com/gpu: 1但底层它们拿到的可能是同一张物理卡。调度器眼里节点有 4 张卡却可能跑着 8 个 GPU Pod。听着很美代价也很大。time-slicing 没有显存隔离几个容器共享同一块显存只要有一个任务显存超限 OOM整个卡上所有任务都遭殃。算力同样是轮流用如果两个训练任务挤在同一张卡上各自的迭代速度都会明显下滑。最适合 time-slicing 的场景是大量小模型推理、开发调试最不适合的是多个大模型训练任务混跑。另外一个方向是 MPSMulti-Process Service它对算力共享做得更精细但也需要在容器里配置相应的 CUDA 环境并且仍然解决不了显存硬隔离问题。至于 vGPU 这类方案往往要额外的许可证或驱动支持成本高除非业务强需求我一般不推荐小团队立刻上。选哪种方案本质是在“利用率”和“隔离性”之间做取舍没有全都要。4.3 多卡任务怎么保证落在合适的拓扑上单机多卡训练或者跨节点分布式训练对 GPU 调度提出了更高要求。NCCL 这类通信库对卡与卡之间的物理拓扑非常敏感同一张卡上的 NVLink 带宽几百 GB/s同一节点通过 PCIe 通信也有几十 GB/s跨节点就得走网卡带宽和延迟完全是两个数量级。如果你的 Pod 请求 4 张卡但调度器把 2 张放在节点 A、2 张放在节点 B训练框架可能会慢到你怀疑人生。默认调度器完全不知道这些事情。它只知道节点 A 有 4 张卡、节点 B 有 4 张卡完全可能把一个需要 4 卡的 Pod 拆到两个节点去。要解决你至少要做三件事中的一件一是给节点打标签并强约束 Pod 必须落在单个节点二是利用较新版本 Device Plugin 配合 GFD 上报的 GPU 索引、产品型号等扩展标签配合自定义调度逻辑三是直接引入 Volcano 这类支持拓扑感知的高级调度器它会把请求 4 卡的 Pod 看成一个整体优先选择能一次性凑齐 4 卡且拓扑最优的节点。我自己的实践是在共享集群里训练类任务一律要求申请整节点可用的卡数再用 nodeAffinity 绑定到指定节点池虽然调度灵活性变差了但省心不用天天去跟业务方解释为什么跨节点的分布式训练这么慢。5. 高频故障排查与实战避坑5.1 Pod 一直 Pending先看 Events 再动手遇到 GPU Pod 一直 Pending别急着猜先执行kubectl describe pod xxx看 Events 列。调度器拒绝节点时会把原因写得很直白我按实际频率排个序Pending 原因说明最常见的坑Insufficient nvidia.com/gpu节点 GPU 数量不够你以为是资源不足其实是 Device Plugin 挂了节点上报资源数量是 00/8 nodes available所有节点都被过滤掉了没加节点亲和性但节点都打了特殊污点node(s) had taint节点有污点Pod 未容忍新节点没清掉控制面污点或者运维手动加了污点Failed to allocate resource调度已成功但节点上分配失败Device Plugin Allocate 接口报错驱动异常或插件版本与 runtime 不匹配这里特别提醒一句我见过不少案例kubectl get nodes看着正常kubectl describe node却发现nvidia.com/gpu一栏直接消失了。大多数是 Device Plugin 异常退出或者 kubelet 重启后插件没有重新注册。看节点状态不如直接看 Device Plugin Pod 日志来得快。5.2 容器里看不到 GPU最经典的故障现象Pod 正常 Runningkubectl exec进去执行nvidia-smi却提示找不到设备。很多人第一反应是镜像问题其实镜像只要装了 CUDA 基础镜像通常没问题问题大多出在运行时注入。第一步检查环境变量有没有注入kubectl exec pod-name -- env | grep NVIDIA如果NVIDIA_VISIBLE_DEVICES变量不存在或为空说明 kubelet 向 Device Plugin 申请设备时就没拿到东西。接下来看 Device Plugin 日志确认是否正常执行了 Allocate同时检查 kubelet 日志里有没有和 device-plugin 相关的报错journalctl -u kubelet -f | grep -i device如果变量正常但容器里还是看不到设备那大概率是 containerd 没有用 NVIDIA runtime 启动这个容器。这个问题我再强调一次装了 Toolkit 不等于配置了nvidiaruntime必须确保 containerd 的default_runtime_name或 Pod 的 runtimeClass 指向了nvidia。验证方式是去 Pod 所在节点的容器运行时配置里确认。5.3 显存 OOM、资源碎片与排队体验GPU 的显存管理是另一个隐蔽问题。常规 GPU 设备没有显存配额限制Pod 声明了 1 块卡就能用满这张卡的全部显存MIG 除外。如果业务侧显存控制不当进程直接被 OOM Killer 干掉表现在 Kubernetes 里就是容器 exited 重启最后变成 CrashLoopBackOff。这时候别光看 Kubernetes 日志要去dmesg或者容器日志里找是否出现了CUDA OOM或Killed。集群层面还有个现象叫 GPU 资源碎片化。比如节点 A 剩 1 张卡节点 B 剩 1 张卡来了一个需要 2 卡的新任务调度器会直接说资源不足即使两个节点加起来的空闲卡数完全够。这种情况在混部场景特别常见因为大家总是申请不同数量的卡卡片越分越碎。要避免的话要么严格按规格拆分节点池要么在调度策略上把“剩余卡数要满足任务规模”作为硬约束尽量避免一个节点上既跑单卡任务又跑多卡任务。最后再分享一个我个人的习惯。我每次给集群接入一批新 GPU 节点一定会固化成一套自检脚本核心就是四个验证宿主机nvidia-smi能看到全部卡、容器运行时能通过 NVIDIA runtime 执行nvidia-smi、节点上 Device Plugin 上报的数量等于物理卡数、创建一个请求 1 张卡的测试 Pod 能正常拿到 GPU 并执行 CUDA 程序。四步全部跑通才把节点交给业务方。Kubernetes GPU 调度这条链路环环相扣任何一个环节偷懒最后都会变成深夜的告警不如把上架这一步做到标准化。