资讯动态

Kubernetes GPU调度全链路拆解:从设备上报到Pod拿到显卡的排查指南

发布时间:2026/9/20 6:47:14 来源:尧图企业网站定制
前段时间给生产环境加了一批 GPU 节点装好驱动、配好容器运行时、也顺手把 nvidia-device-plugin 部署上去了。节点状态倒是 Ready可训练任务里的 Pod 一直卡在 Pending。最后定位下来是 containerd 没有加载 NVIDIA runtime导致设备插件上报设备失败。这类问题在 Kubernetes GPU 调度里太典型了表面现象都差不多但实际上链路很长——从驱动、容器运行时、设备插件、调度器再到最后 runc 创建容器时的设备注入任何一环断了Pod 都拿不到卡。这篇文章我把“从节点上架到 Pod 拿到 GPU”这条完整链路拆开讲。不是只讲概念而是把每一步背后的机制说清楚也会把我踩过的坑、排查思路原样摆出来。适合集群管理员、平台工程师以及任何在 Kubernetes 上用 GPU 跑训练或推理的团队参考。看完之后你会清楚地知道一张 GPU 卡是怎么从物理设备变成 Pod 里那个/dev/nvidia0的。1. GPU 节点上架驱动、容器运行时和 kubelet 之间的三角关系1.1 驱动安装和验证这一步决定后面所有工作先说结论GPU 节点的上架流程驱动是第一步也是问题潜藏最多的一步。很多人以为装上驱动、nvidia-smi能输出就算完事但驱动选型没匹配上 GPU 型号和操作系统内核的情况我见过太多次。在 Ubuntu/Debian 这类系统上常规做法是装 NVIDIA 官方驱动。装完后第一时间验证nvidia-smi能正常列出 GPU 型号、驱动版本、CUDA 版本指驱动支持的 CUDA 版本不是容器里的 CUDA 运行时版本说明驱动基本就绪。但这真的只是“基本”。我遇到过驱动版本太老导致后面 nvidia-container-toolkit 正常工作但容器里 CUDA 版本兼容不了的情况。NVIDIA 驱动的版本和 CUDA 版本之间有严格的上向兼容关系驱动版本过旧容器镜像里的 CUDA toolkit 版本再新也跑不起来。还有个很容易忽视的点驱动安装时nvidia-modprobe、nvidia-persistenced这些配套组件也建议装上。生产环境长时间跑 GPU 任务如果没启用nvidia-persistenced卡在空闲和忙绿之间切换时可能会出一些诡异的性能问题比如显存不释放、功耗状态异常。这个不是 Kubernetes 层面的问题但会直接体现为 Pod 里的任务异常变慢。1.2 容器运行时必须能操作 GPUkubelet 才能把 GPU 交给 Pod驱动装好后下一步的关键是让容器运行时一般是 containerd具备把 GPU 设备挂载进容器的能力。这一步被很多人忽略也是我上文提到的那个 Pending 事故的根源。Kubernetes 里跑 GPU Podkubelet 本身读不读卡不重要重要的是容器运行时得能“碰”GPU。NVIDIA 提供的 nvidia-container-toolkit 就是这个桥梁。它做的事情本质上是往 runc 创建容器的流程里插入一个 prestart hook在容器启动前把 GPU 设备、驱动库、必要的环境变量注入进去。安装和配置 containerd 的步骤很简单# 安装 nvidia-container-toolkit apt install -y nvidia-container-toolkit # 将 nvidia runtime 配置写入 containerd sudo nvidia-ctk runtime configure --runtimecontainerd # 重启 containerd 使配置生效 sudo systemctl restart containerd配置完成后检查一下 containerd 的配置里是否真的带上了 nvidia runtimegrep -A20 \[plugins\].*nvidia /etc/containerd/config.toml这一步的坑在于很多部署脚本只装了 toolkit没执行nvidia-ctk runtime configure或者执行了但没重启 containerd。结果是 device plugin 能启动但它尝试向 containerd 发起创建容器的请求时runtime 里根本没有 nvidia 这个条目设备注入失败Pod 就会一直 ContainerCreating 或者 Pending。注意到这一步为止kubelet 还没有感知到任何 GPU。它只知道这个节点 Ready但不知道这个节点上有几张卡、卡是否健康。这个“感知”步骤由设备插件来完成。2. 设备插件GPU 是怎么变成 Kubernetes 可分配资源的2.1 kubelet 不原生认识 GPU设备插件是唯一的信息源Kubernetes 原生认识 CPU、内存、临时存储这些内置资源但 GPU 完全不在原生资源列表里。它属于扩展资源Extended Resource命名通常带域名前缀比如nvidia.com/gpu。这里就有个关键机制扩展资源的数量不是 kubelet 扫描硬件自动获取的而是由一个叫设备插件Device Plugin的组件上报的。设备插件通过 Kubernetes 的 Device Plugins 接口和 kubelet 通信通信方式是一根 Unix socket。这个机制在 2018 年的 Kubernetes 1.8 版本里引入一直用到现在。设备插件的生命周期是这样的设备插件启动后向/var/lib/kubelet/device-plugins/kubelet.sock发起 gRPC 注册请求。kubelet 验证通过后插件通过ListAndWatch接口持续上报设备 ID 列表和每个设备的健康状态。当调度器把 Pod 调度到这个节点且 Pod 需要某个扩展资源时kubelet 会调用插件的Allocate接口插件返回一组环境变量、设备路径和挂载点kubelet 把这些信息附加到容器运行时配置中。以 nvidia-device-plugin 为例它部署成 DaemonSet 后每个 GPU 节点上跑一个 Pod。这个 Pod 会扫描节点上的物理 GPU把每张卡上报为一个nvidia.com/gpu资源单位。所以当你在节点上执行kubectl describe node gpu-node | grep -A5 nvidia.com/gpu如果输出里有类似nvidia.com/gpu: 8这样的内容说明设备插件已经把 8 张卡上报给 kubelet 了。如果这一行不存在或者显示为 0问题基本出在设备插件这一层。2.2 资源单位是“张”不是“G”这是理解一切调度问题的起点设备插件上报完成后kubelet 的 capacity 和 allocatable 里就多了一个值。但这个值单位是“张”或者叫“个”不是显存容量。调度器看到的不是一个“可分割的 GPU 资源池”而是nvidia.com/gpu: 8这种整数。这种设计直接决定了 Kubernetes 原生的 GPU 调度粒度整卡调度。每张卡被视为一个不可分割的原子单位一个 Pod 请求 1 就是指一整张卡。至于这张卡的显存是 40G、80G还是 96G调度器完全不知道。这带来两个直接后果。第一请求 GPU 的数量必须是整数不能写nvidia.com/gpu: 0.5。第二显存利用率无法精细控制。比如节点上有 8 张 40G 卡两个训练任务各 request 1 卡调度器会认为节点还有 6 卡可用但实际上每张卡 40G 显存里可能只被任务用了 20G剩下 20G 全浪费了。这个问题在后面专门讲共享 GPU 时再展开。这里先记住一个核心认知Kubernetes 原生的 GPU 调度单位是物理卡不是显存。3. 调度器视角Pod 请求的 GPU 是如何匹配到节点的3.1 Filter 阶段只有具备足够 GPU 的节点才进候选池当用户创建一个请求 GPU 的 Pod调度器就开始干活了。调度器的运行逻辑分两个大阶段Filter 和 Score。Filter 是硬性条件筛选Score 是在满足条件的节点里打分排序。对于 GPU 这类扩展资源Filter 阶段的关键是看节点allocatable里nvidia.com/gpu的数量是否大于等于 Pod 请求的数量。举个例子apiVersion: v1 kind: Pod metadata: name: gpu-train spec: containers: - name: train image: nvidia/cuda:12.3.1-base-ubuntu22.04 command: [nvidia-smi] resources: limits: nvidia.com/gpu: 2调度器遍历所有节点只把allocatable.nvidia.com/gpu 2的节点纳入候选池。其他没有 GPU、或者 GPU 不够的节点直接排除。这一步对应的事件信息是0/5 nodes are available: 2 insufficient nvidia.com/gpu, 3 node(s) didnt match Pods node affinity/selector.看到insufficient nvidia.com/gpu说明调度器认为这个节点 GPU 数量不够。可能是真的不够也可能是节点根本没上报 GPU 资源。这里有个特别值得注意的细节对于扩展资源Kubernetes 有一个硬性语义——只能写limits不能单独只写requests。你写了limits.nvidia.com/gpu调度器会自动把它同步为 request 和 limit。原因是扩展资源被认为“不可压缩、不可超卖”必须用上限来限制使用量。理解了这一点你就不会再去纠结为什么配置里 GPU 没有独立的 request 字段。3.2 Score 阶段和节点选择GPU 会参与打分吗Filter 之后是 Score。Kubernetes 默认调度器的打分主要基于节点资源水位哪个节点在分配完这些资源后剩余资源更均衡哪个节点分数更高。GPU 资源本身也参与打分。但说实话在纯 GPU 调度的场景里Score 阶段的作用没有 CPU/内存那么明显。如果集群里只有两台 GPU 节点Filter 过了就基本定了。真正复杂的是 GPU 资源和其他资源比如内存、CPU的联合匹配。GPU 计算任务通常对 CPU 也有要求如果节点 GPU 够但 CPU 核不足、内存不够Filter 阶段同样会被刷掉。这也就解释了为什么有些生产环境里GPU 节点明明有 8 张卡任务却调度不上去——不是因为 GPU 不够而是因为节点内存或者 CPU 已经被其他 Pod 占了。3.3 显存不够但卡数够原生调度的一个明显盲区这是我工作中最常被问到的问题也是原生调度最让人头疼的地方。假设节点上有 4 张 40G 的 A100某个推理服务请求了 1 张卡调度器一看可用卡数 4够调度通过。但问题是这个推理服务的模型实际只需要 12G 显存另外 28G 就被锁死了。更极端的情况一个 Pod 被调度到某张卡上但卡上已经在跑别的任务显存已经被占了大半新 Pod 一启动就 OOM。为什么会这样因为原生调度的资源模型里只有“张”这个单位没有“显存”这个概念。调度器无法回答“这张卡还剩多少显存”这个问题因为设备插件上报的只是设备 ID 列表不包括设备容量属性。社区对这个盲区的解决方案是引入 GPU 调度器插件例如 scheduler-plugins 里的 capacity 调度、或者各家云厂商的自研调度器它们通过读取 GPU 卡显存信息、结合 Pod 请求的显存大小把显存维度引入调度决策。但这种做法不是 Kubernetes 标准能力而且需要和设备插件、甚至 NVIDIA MPS/MIG 机制联动复杂度会明显上升。所以如果你的业务明显存在“多任务共享 GPU 显存”的需求建议一开始就在容量规划和调度器选型上想清楚不要指望原生 Kubernetes 帮你解决显存问题。4. 最后一公里Pod 是如何在运行时拿到 GPU 的4.1 从 Allocate 调用到 NVIDIA_VISIBLE_DEVICES调度器选好了节点Pod 被绑定到节点上接下来就轮到 kubelet 和容器运行时干活了。kubelet 创建 Pod 里的容器时发现容器请求了nvidia.com/gpu就会走设备插件通道调用插件的Allocate接口。nvidia-device-plugin 的 Allocate 逻辑很直接根据 kubelet 传过来的设备 ID构造一组环境变量和挂载点返回给 kubelet。其中最核心的是NVIDIA_VISIBLE_DEVICES环境变量。这个变量的值是分配给 Pod 的 GPU UUID 或索引。kubelet 拿到这个环境变量后会把它写进容器运行时配置里。此时 containerd 创建的容器 runtime spec 中已经包含了这样的环境变量NVIDIA_VISIBLE_DEVICESGPU-18f7b1a5-...然后重点来了containerd 在启动容器时会调用我们之前配置好的 nvidia-container-runtime这个 runtime 会看到NVIDIA_VISIBLE_DEVICES环境变量于是调用 nvidia-container-cli把对应的 GPU 设备节点比如/dev/nvidia0、NVIDIA 驱动库libcuda、libnvidia-ml、libnvidia-ptxjitcompiler 等、以及必要的二进制文件挂载进容器。容器里的进程看到的就是一个个可见的设备文件而感知不到宿主机上还有其他 GPU。这就是隔离的实质不是虚拟化而是通过设备的可见性控制实现资源隔离。4.2 容器里看不到 GPU 的常见原因容器启动后在容器里执行nvidia-smi正常情况下应该能看到被分配的 GPU。但如果看不到问题多半出在这几个地方。第一是 containerd 没加载 nvidia runtime。这是最高频的原因。表现是 Pod 事件里有failed to create containerd task: failed to create shim task之类的报错或者容器能启动但里面没有任何 NVIDIA 设备相关文件。第二是设备插件分配正常但容器镜像里缺少 NVIDIA 的库。这种情况常见于用非官方 CUDA 镜像跑 GPU 任务的场景。nvidia-smi能执行但报类似error while loading shared libraries: libnvidia-ml.so的错误。解决方式是使用官方nvidia/cuda镜像或者自己把 CUDA 运行时库打进镜像。第三是驱动版本和容器 CUDA 版本匹配不上。前面说过驱动对 CUDA 是上向兼容关系驱动版本越新能支持的 CUDA 版本越新。如果驱动的 CUDA 版本太老而容器镜像里的 CUDA 太新应用就会在真正调用 CUDA API 时报版本不兼容的错误。这类问题调起来是有点拐弯的因为nvidia-smi可能正常但程序一跑就崩。我通常的排查顺序是先在宿主机跑nvidia-smi再进容器跑nvidia-smi最后跑一个真正的 CUDA 小程序比如deviceQuery。每一步都能通过才能证明驱动、运行时、镜像这条链路是通的。5. 实战排错Pod 一直 Pending 的完整排查链路5.1 先看 describe pod区分是调度失败还是设备分配失败遇到 GPU Pod 起不来我第一步永远是kubectl describe pod pod-name看最下面的 Events。这一步能帮你快速定位问题在上游还是下游。如果看到的是0/8 nodes are available: 8 insufficient nvidia.com/gpu说明是调度器这一层的问题——要么节点真的没 GPU要么节点根本没上报 GPU 资源。如果看到的是 Pod 已经 assigned 到某个节点、但容器一直 ContainerCreating那问题出在 kubelet 或者容器运行时这一层。这两种情况的排查方向完全不同。调度失败优先查资源上报ContainerCreating 优先查驱动和运行时。用这个简单的二分法能省掉大量瞎试的时间。5.2 检查节点资源上报nvidia.com/gpu 到底有没有出现如果是调度失败下一步查节点kubectl describe node gpu-node | grep -A5 nvidia.com/gpu如果这个输出为空或者写的是 0说明节点上的设备插件没有上报成功。这时候先看设备插件 Pod 的状态kubectl get pods -n kube-system -o wide | grep nvidia-device-plugin kubectl logs -n kube-system nvidia-device-plugin-pod最常见的日志报错是error while creating plugin: failed to get device: ...或者是无法连接 kubelet socket。前者通常意味着宿主机上nvidia-smi有问题后者通常意味着 kubelet 没重启、或者插件启动得太早 socket 还没建好。还有一个非常隐蔽的坑kubelet 在重启之后旧的设备插件 svc socket 会失效。如果设备插件不具备自动重注册能力它就会一直连不上 kubelet。解决办法是删掉插件 Pod 让它重建或者等 device plugin 自身的重试机制生效。5.3 设备插件正常但 Pod 还 Pendingcheck kubelet 的日志如果节点上nvidia.com/gpu出现了数量也对但 Pod 依然 Pending这时候需要看 kubelet 侧的信息。因为设备已经上报调度器也能感知但 kubelet 在运行时调用设备插件 Allocate 时可能失败。查 kubelet 日志通常能直接看到原因。常见的一种是Error allocating device: unknown device nvidia.com/gpu这个报错看起来像设备不存在其实很多时候是 device plugin 上报的设备信息里缺少了真正可用的设备比如 MIG 模式下实例 ID 不匹配。另一种是 socket 权限问题设备插件 Pod 因为安全上下文配置问题访问不了/var/lib/kubelet/device-plugins/导致注册失败。遇到这种问题建议先检查设备插件挂载的宿主机路径权限ls -l /var/lib/kubelet/device-plugins/正常应该能看到kubelet.sock和每个插件的 svc sock 文件。如果目录下啥都没有说明设备插件根本没注册成功问题大概率在插件侧而不是 kubelet 侧。5.4 一个实测过的经典故障MIG 模式切换后资源数没变有一次我们把一批 A100 节点从整卡模式切到 MIG 模式每个物理卡切成 3 个实例。切完之后设备插件自动扫描到了 24 个 MIG 实例但在 Kubernetes 里可分配数测试还是 8。原因是设备插件在检测到 GPU 型号变更后需要重新注册而它默认的注册是“只在启动时检测一次”不会动态监听设备状态变化。解决办法就是滚动重启设备插件 DaemonSet让它在新的 MIG 拓扑下重新启动、重新注册。这个坑的启示是修改 GPU 底层拓扑后一定要重启设备插件而不是等它自动感知。同样的情况也出现在驱动升级之后强烈建议驱动升级和生产变更都带上设备插件的滚动重启步骤。6. 再进一步MIG、共享调度和未来的 DRA6.1 MIG 是物理切分不是调度器的能力A100、H100 这些卡支持 MIGMulti-Instance GPU能把一张物理 GPU 切分成多个独立的 GPU 实例每个实例有独立的显存和计算单元。比如一张 80G 的 A100 可以切成 2 个 40G 实例或者 7 个 10G 实例。MIG 之后nvidia-device-plugin 会把每个 MIG 实例当成一个独立的nvidia.com/gpu资源来上报。调度器看到的就是更多更小的 GPU 单位每个单位对应一个 MIG 实例。但要注意MIG 是在物理层面做分割不是调度器帮你切的。先要在宿主机上配置好 MIG 模式、创建好实例Kubernetes 里的资源数量才会变化。而且 MIG 模式通常要求同一批实例的显存大小一致mixed 模式做起来会很复杂这在容量规划时要想清楚。6.2 共享 GPU 的三条路时间片、MPS、vGPU如果不想用 MIG又希望多个 Pod 共享一张卡有另外几条路可以走。时间片共享是最常见的方案。原理是让设备插件把一张物理卡上报为 N 个虚拟资源每个虚拟资源仍然对应同一张卡。多个 Pod 可以在不同时间片里交替执行 GPU 任务。实现上有很多开源方案核心是让Allocate返回同一个物理设备、或者完全不做设备隔离。这种方式简单但没有显存隔离两个任务可能互相挤占显存导致 OOM。MPSMulti-Process Service是 NVIDIA 官方的进程级并发机制。它可以限制每个进程的计算份额适合显存够但计算能力不够的场景。但 MPS 和 Kubernetes 原生调度结合的复杂度不低通常需要额外的 controller 参与。vGPU比如虚拟化平台上的 vGPU 方案隔离性最好但也有成本且往往依赖特定虚拟化平台。三种方案的取舍用一个表看更清楚方案隔离级别显存隔离计算隔离实现复杂度适用场景MIG物理实例强隔离强隔离中多租户、显存敏感时间片进程级无隔离弱隔离低训练小任务、推理MPS进程级无隔离中隔离中高计算密集型小任务vGPU虚拟化强隔离强隔离高虚拟化环境6.3 DRA 正在改写 GPU 资源的描述方式最后聊一个 2023 年以来很值得关注的方向Dynamic Resource AllocationDRA。这是一个比设备插件更通用的新机制从 Kubernetes 1.26 开始作为 alpha 特性引入核心思路是把“资源设备”作为一等公民来管理。DRA 引入了 ResourceClass、ResourceClaim、ResourceSlice 这些新对象。ResourceClaim 可以表达非常精细的诉求比如“我要一张显存不小于 32GB 的 GPU”“我要一张和某个网卡在同一个 NUMA 节点上的 GPU”。这些信息不再像扩展资源那样只能用一个整数表达而是通过结构化的资源属性来描述。调度器在 Filter 阶段就能根据 ResourceSlice 里的属性来做匹配而不是只能看数量够不够。这意味着未来 GPU 调度的精细度会大幅提升显存信息、设备拓扑、设备亲缘性都能参与调度决策。虽然目前 DRA 在生产环境里用得还没那么广但方向已经非常明确用结构化的资源描述替代简单的整数计数。如果你正在规划新的集群或者准备做大规模 GPU 池化建议关注一下这个方向。Kubernetes 1.28 之后 DRA 的 API 演进速度很快而 nvidia-device-plugin 的官方实现也已经在往 DRA 上靠。实际操作中我最大的体会是Kubernetes GPU 调度并不复杂难的是把“物理设备”“设备插件上报”“调度器语义”“运行时注入”这几层串起来理解。遇到 Pod 拿不到卡的问题不要急着改调度器配置先按“驱动→运行时→设备插件→调度→注入”这条链路逐层排查绝大多数问题都能定位到具体哪一环断了。最后再分享一个小技巧生产环境里给 GPU 工作负载的 Pod 加上资源模板和事件告警把insufficient nvidia.com/gpu和FailedCreatePodSandBox这两个事件单独拿出来做监控很多 GPU 资源问题能在用户反馈之前就被发现。

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

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

免费获取报价