资讯详情

Kubernetes GPU调度全解析:从驱动、容器运行时到Device Plugin

📅 2026/9/12 8:17:39 | 华诺云谱 👁 阅读
Kubernetes GPU调度全解析:从驱动、容器运行时到Device Plugin
1. 先搞清楚一张 GPU 卡是怎么“进”集群的节点上架的完整链路很多人第一次在 Kubernetes 里跑 GPU 任务时都会有一个想当然的假设把显卡插进 PCIe 槽装好驱动节点上自然就会出现nvidia.com/gpuPod 里自然就能nvidia-smi。这套直觉在单机环境下成立在 K8s 集群里却差了整整三层。一段真实经历我接手过一个集群运维新上了一批 A100 节点硬件装好、驱动装好、nvidia-smi在宿主机上能看到 8 张卡但kubectl describe node里nvidia.com/gpu一直是 0调度器永远提示Insufficient nvidia.com/gpu。问题最后定位在容器运行时没有接入 NVIDIA 的 prestart hook——GPU 物理上在机器里但 Kubernetes 和容器运行时根本“不知道”怎么把它交给容器。所以这条链路的完整顺序是物理设备 → 内核驱动 → NVIDIA Container Toolkit → 容器运行时 → Device Plugin → Kubelet Device Manager → kube-scheduler → Pod。这篇文章就从这条链路往下拆把每一层为什么存在、做什么事、出问题怎么查一次讲清楚。1.1 驱动层宿主机 nvidia-smi 能出卡只是起点GPU 节点上架第一件事当然是装驱动。但这里有一个很容易被忽略的分工宿主机驱动解决的是“操作系统能访问 GPU”和“容器能访问 GPU”是两码事。驱动装好后你应该确认这些内核模块都加载了nvidia、nvidia_uvm、nvidia_modeset、nvidia_drm。其中nvidia_uvm尤其关键它负责 Unified Virtual MemoryDevice Plugin 做健康检查和显存分配时都依赖它。如果只有nvidia加载而nvidia_uvm没加载宿主机nvidia-smi还能看到卡但容器里 CUDA 初始化会直接失败。另一个常见坑是驱动版本和容器内 CUDA 版本的关系。很多人以为容器里跑 CUDA 12宿主机就必须装 CUDA 12 的驱动其实不是。容器镜像里的 CUDA Toolkit 是用户态库宿主机驱动是内核态和用户态驱动两者通过驱动暴露的接口通信。只要宿主机驱动版本 ≥ 容器内 CUDA 要求的最低驱动版本就能跑。比如 CUDA 12.4 要求驱动 ≥ 550.54.14你装 550 或更高版本都行。反过来就不行驱动版本低了容器内应用启动时会报CUDA driver version is insufficient for CUDA runtime version这个错误几乎不用怀疑就是驱动太老。如果你用 Deb/RPM 或 runfile 装驱动建议直接选发行版仓库里带 DKMS 的包或者自行保证内核升级后驱动要重新编译。GPU 集群里因为内核安全升级导致驱动模块丢失、节点直接失去 GPU 能力的事我见过太多次了。驱动装完用nvidia-smi -L确认所有卡都识别正常然后用nvidia-smi -q -d ECC看显存 ECC 状态。如果 ECC 错误数激增这张卡要么返修要么隔离不要直接丢进集群。1.2 NVIDIA Container Toolkit容器运行时认不认识 GPU由它说了算宿主机驱动搞定了下面这一步是整个上架过程中最容易出错、也最不被理解的NVIDIA Container Toolkit旧名 nvidia-container-runtime / nvidia-docker2。它的本质不是一个完整的容器运行时而是一个 OCI Runtime 的 prestart hook在容器创建前截获请求检测容器是否请求 GPU如果需要就把宿主机上的 GPU 设备节点、驱动库、二进制工具链比如nvidia-smi注入到容器的 runtime spec 里同时设置好环境变量。为什么没有它就不行因为容器默认只有一份最小化的设备白名单/dev/nvidia* 这些设备不会被自动暴露进容器。CUDA 程序在容器里找不到/dev/nvidia0、/dev/nvidiactl、/dev/nvidia-uvm自然认为自己跑在一台没有 GPU 的机器上。以 containerd 为例你需要修改/etc/containerd/config.toml给 CRI 增加一个nvidiaruntime然后重启 containerd[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.nvidia] privileged_without_host_devices false runtime_engine runtime_root runtime_type io.containerd.runc.v2 [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.nvidia.options] BinaryName /usr/bin/nvidia-container-runtime这里有一个容易翻车的点如果你只改了 containerd 配置但 Kubelet 创建 Pod 时用的 runtimeClass 仍然是默认的runcGPU 设备还是进不了容器。较新的 NVIDIA Container Toolkit 支持把 nvidia 设为默认 runtime或者你为 GPU Pod 显式指定runtimeClassName: nvidia。两套模式各有取舍全节点默认 nvidia runtime简单直接但相当于所有容器都走了 hook用 runtimeClass 隔离更干净但需要业务方 yaml 里写好。自己内部集群我建议直接默认 runtime 设成 nvidia少一层扯皮。验证这一步是否配置正确最快的方法是先不要上 Device Plugin直接手动跑一个测试容器ctr image pull docker.io/library/ubuntu:22.04 ctr run --rm --runtime io.containerd.runc.v2 --gpus 0 docker.io/library/ubuntu:22.04 test-nvidia nvidia-smi如果容器里能看到卡说明 runtime hook 已经通了再往后走 Device Plugin。1.3 标签、污点与“可调度”的边界GPU 节点上架的最后一步是让集群知道这张卡有什么用、给谁用。我通常会做三件事给节点打标签gpu.vendornvidia、gpu.modelA100-SXM4-40GB、gpu.memory40Gi。标签不是为了装饰而是给调度器和业务方一个明确的路由依据。没有标签业务方想只调度到 A100 节点就得靠nodeName硬编码非常难维护。给节点打污点dedicatedgpu:NoSchedule同时给 GPU 工作负载加对应 toleration。这一步能把普通 CPU 业务挡在 GPU 节点外面。GPU 节点通常很贵CPU 和内存配置也比较高如果不打污点一些无状态业务很可能被调度上去把节点 CPU 挤爆影响 GPU Pod 的通信线程。确认 kubelet 启动参数里没有--register-with-taints和自定义--node-labels冲突。很多发行版用 systemd 管理 kubelet改配置的姿势不对kubelet 一重启标签就丢了。做到这一步节点只是“准备好了”Kubernetes 还不会报告它有 GPU 资源。下一节就是整个 GPU 调度体系里最关键的一块Device Plugin 和 Extended Resource。2. Device Plugin 与 Extended Resource调度器凭什么“看见” GPU有一个基本事实要先说清楚kube-scheduler 本身对 GPU 一无所知。它既不认识 NVIDIA也不认识 CUDA更不理解“显存”“算力”“SM 利用率”这些概念。Kubernetes 之所以能调度 GPU靠的是 Extended Resource 机制加 Device Plugin 插件二者配合才让 GPU 变成一个调度器能处理、kubelet 能分配的“资源”。2.1 为什么 K8s 把 GPU 当成一个“黑盒整数”在 K8s 里nvidia.com/gpu属于 Extended Resource。Extended Resource 有一系列硬性限制其中一个关键限制是只能申报整数数量不能像 CPU 那样写500m也不能写0.5。所以从调度器视角看一张 A100 和一张 T4 没有区别都只是“1”这个数字。它不会关心你的 Pod 需要 40GB 显存还是 16GB 显存也不在乎这张卡能不能满足你的算力需求。这个设计的妥协是有历史原因的。Extended Resource 的目标是提供一种通用扩展机制让各种硬件设备都能被调度器纳入计算但又要保持调度器足够简单和高效。真正理解显存、拓扑、MIG 这些复杂属性的只有对应硬件厂商或者运维团队的扩展组件。所以你会看到官方 Device Plugin 只负责数量维度如果你想要“按显存调度”就得自己去写调度插件或者用第三方解决方案。这也引出第一个运维经验不要试图用 Extended Resource 管理显存。如果你把显存建模成nvidia.com/gpu-mem23000会遇到两个问题一是 Extended Resource 不允许小数显存单位只能以整数 MiB 上报二是调度器不知道自己分配出去的 GPU 卡到底是哪一张很容易出现“调度到 40GB 显存的卡但实际分配到的卡只剩 20GB 可用”的状态。后续的 NVIDIA 官方方案在 MIG 场景用nvidia.com/mig-profile这样的独立资源名本质上仍然是数量匹配而不是显存大小匹配。2.2 Device Plugin 的三件事注册、上报、分配Device Plugin 是 kubelet 的一个插件机制通常以 DaemonSet 方式部署到每个 GPU 节点。它是运行在宿主机上的一个二进制通过一套 gRPC 接口和 kubelet 通信。NVIDIA 官方实现叫 k8s-device-plugin 社区也有 AMD、Intel 等其他实现。它的核心生命周期可以概括为三步注册插件启动后通过 Unix socket/var/lib/kubelet/device-plugins/kubelet.sock向 kubelet 注册自己声明资源名是nvidia.com/gpu。kubelet 会为这个资源名建立独立的设备管理器表。上报与健康检查插件通过ListAndWatch接口持续向 kubelet 上报当前节点上的设备列表比如 8 个 GPU 的 UUID。如果某个 GPU 出现 Xid 错误、驱动异常或者无法访问插件会标记这个设备为Unhealthykubelet 就不会再把它分配给新 Pod。设备变成 Unhealthy 后已运行在该设备上的容器不会被强制杀掉但新的调度会避开它。分配当调度器把 Pod 调度到某个节点且这个 Pod 请求了nvidia.com/gpukubelet 会在创建容器前调用插件的Allocate接口传入请求的设备 ID。插件返回一组信息需要注入的环境变量典型是CUDA_VISIBLE_DEVICES、需要挂载的设备文件、需要做的 mount、需要附加的 annotation 等。kubelet 把这些信息交给 CRI 运行时最终形成容器可感知的 GPU 环境。所以 Device Plugin 的本质是“翻译官”把底层硬件的复杂性翻译成 kubelet 能执行的分配指令。这里有个经常被误解的地方Device Plugin 不做调度决策它只负责本节点的设备分配。真正决定 Pod 落在哪个节点的是调度器而调度器依赖的是插件上报的数量。2.3 节点上 GPU 时有时无的常见原因节点上nvidia.com/gpu不可用或者数量不对排障时先别急着怀疑调度器按这个顺序查检查 Device Plugin Pod 是否正常运行kubectl logs -n kube-system -l namenvidia-device-plugin。如果插件一直 CrashLoopBackOff大概率是宿主机驱动和插件版本不匹配或者 socket 路径不对。看插件日志里设备上报的数量。一个健康节点应该类似Found 8 GPU devices。看kubectl describe node里Allocatable和Capacity的nvidia.com/gpu。如果 Capacity 有但 Allocatable 是 0说明设备被标记为 Unhealthy 了查健康检查日志和 Xid。如果状态一直不刷新重启 kubelet 通常能解决。Device Plugin 注册是一次性的kubelet 和插件之间断连后插件一般会重试注册但有时重试逻辑不够健壮重启 kubelet 是最快的兜底方案。还要检查节点上是否已经有其他 Pod 占用了这些 GPU 资源kubectl get pods --all-namespaces -o wide看看有没有悬空的 Pod 占据了资源。这一点经常被忽视——旧 Pod 卡在 Terminating资源没释放新 Pod 自然调度不进来。注册机制还有个小坑设备插件删除后重新部署如果新 Pod 的 socket 文件还残留插件可能注册失败。所以在更新 Device Plugin 时最好用 DaemonSet 滚动更新方式让插件走完整的启动、注册流程。3. 从调度到绑卡Pod 请求 GPU 后到底发生了什么前面说了调度器只知道“整数”这一节我们详细走一遍一个 GPU Pod 从提交到真正拿到卡系统内部做了哪些决策。这个过程中有两类角色分工很容易混淆调度器负责决定“去哪台节点”kubelet 负责决定“用哪张卡”。3.1 过滤与打分调度器对 GPU 只做数量匹配一个 GPU Pod 的 yaml 通常长这样apiVersion: v1 kind: Pod metadata: name: gpu-pod spec: containers: - name: pytorch image: pytorch/pytorch:2.2.0-cuda12.1-cudnn8-runtime resources: limits: nvidia.com/gpu: 1注意这里我只写了limits没写requests。K8s 的语义是如果只写 limitsrequests 默认等于 limits。这点在 GPU 场景很重要因为像 CPU/内存你可以只写 requests 不写 limits但在 GPU 上必须让请求和限制一致否则调度器无法保证资源不超卖。调度器处理这个 Pod 时会先执行过滤。NodeResourcesFit插件会检查候选节点的nvidia.com/gpu可分配数量是否满足请求。这里有一个关键的内部逻辑调度器计算的是“已调度但可能尚未创建的 Pod 的请求总量”而不是“已经实际分配的 GPU 数量”。所以它是基于声明式资源模型做判断不看真实负载。这带来一个常见问题节点上的 GPU 明明有 Pod 在跑但显存、算力都很空闲调度器却认为资源满了——因为它统计的是请求量不是使用量。反过来如果 GPU 已经被某个进程吃满显存但 K8s 层面没有任何 Pod 请求该资源调度器还是会把它当成空闲资源。后面我们会聊到这是超卖/共享方案要解决的根源。过滤通过后进入打分环节。默认策略对 CPU 和内存是LeastAllocated倾向于把负载打散。但对 GPU 来说这种打散策略并不总是好事。你可能希望 GPU 任务尽量堆满一张卡再开下一张降低碎片也可能希望分散到不同节点避免单点故障影响面扩大。官方 Device Plugin 不提供任何打分策略所以如果你对 GPU 调度有拓扑、共享、显存维度的需求要么自己写调度插件要么上 Volcano、Kueue、scheduler-plugins 这类扩展。Volcano 的 binpack 插件、scheduler-plugins 的拓扑感知插件都是针对这类场景的成熟实现。3.2 kubelet 的设备管理器节点侧才是真正的“发卡”环节Pod 被调度到节点后kubelet 开始创建工作负载。在创建容器之前kubelet 的 Device Manager 会检查这个 Pod 请求了哪些 Device Plugin 资源如果存在就进入Allocate流程。这是整个链路中最容易出问题、也最值得深入理解的一环。Device Manager 在 kubelet 内部维护了一张“已分配设备”的表。当同一个节点上多个 Pod 都请求 GPU 时Device Manager 会按设备 ID 去重分配Pod A 拿到GPU-123Pod B 拿到GPU-456不会出现同一张卡被两个 Pod 同时分配的情况这是 K8s 默认语义除非你用的是共享/超卖方案。分配完成后Device Manager 调用对应 Device Plugin 的Allocate接口。插件返回的内容会包含环境变量CUDA_VISIBLE_DEVICESGPU-123Mounts比如需要把宿主机/usr/local/nvidia/lib64/下的驱动库挂进容器Devices比如/dev/nvidia0:/dev/nvidia0Annotations插件自定义信息然后 kubelet 把这份附加信息传给容器运行时运行时在创建容器时执行这些挂载和环境变量注入。这里有个细节Allocate是 kubelet 在创建 Pod sandbox 或容器时调用的所以 Device Plugin 必须事先已经注册且设备健康。如果插件没注册成功kubelet 会创建一个状态为Failed的沙箱Pod 一直卡在ContainerCreating。关于同一个 Pod 内多个容器请求 GPU 的场景NVIDIA 官方插件的实现允许 Pod 内多个容器共享同一张卡。比如一个 Pod 里有两个容器各请求 1 个 GPU总共请求 2 个 GPU。这时Allocate调用会在容器级别分别传入设备。如果你只申请了 1 个 GPU两个容器都请求nvidia.com/gpu: 1插件会返回同一个设备给两个容器吗答案取决于插件实现。官方插件在这种场景下会为每个容器分配独立的设备所以实际上你申请 1 个 GPU、两个容器都要用K8s 会认为资源不足Pod 会被拒绝。正确做法是每个容器都显式请求 GPU 数量总体请求等于容器请求之和。3.3 CUDA_VISIBLE_DEVICES 与容器里“看到”的卡队列里最经典的问题为什么在容器里跑nvidia-smi只看到一张卡道理很简单所有被分配的卡并不是全部物理暴露给容器而是通过CUDA_VISIBLE_DEVICES环境变量做了一层“过滤”。在宿主机上你用nvidia-smi -L看到的是物理 GPU 的 UUID在容器里CUDA 库启动时会读取CUDA_VISIBLE_DEVICES只让程序枚举到这个变量指定的设备。所以即使宿主机有 8 张卡容器里nvidia-smi也只会显示被塞进这个变量的那一张或那几张。这里有个不容忽视的运维细节CUDA_VISIBLE_DEVICES的值不是 GPU 索引而是 UUID。为什么用 UUID因为索引会随驱动加载顺序、设备顺序变化而 UUID 是硬件级的、稳定的。如果插件按索引分配那么宿主机重启后设备顺序一变容器里看到的卡可能就换了。用 UUID每次重启后分配依然稳定。如果你遇到“容器里看不到 GPU”这类问题第一步就是手动进容器用env | grep CUDA看看CUDA_VISIBLE_DEVICES是否存在。如果存在说明 Device Plugin 分配逻辑正常如果不存在说明容器根本没走 Device Plugin 的分配流程优先怀疑容器运行时没配好或者 Pod 的 resources 里没写 GPU 请求。还有一类场景容器里能看到多个 GPU但程序只用到其中一个这通常是应用代码没有感知到设备数量写死了cuda:0。分布式训练里这类问题很常见后面在排障部分专门说。4. 显存、算力与共享GPU 资源管理的进阶玩法默认情况下K8s 只能把一个 GPU 整卡分配一个 Pod不能把一个 GPU 分配给多个 Pod。这在显存小、任务多、碎片化严重的推理场景里非常浪费。A100 80GB 跑一个小模型显存可能只用 10GB但一个 Pod 独占之后其他 Pod 都挤不进来。所以真实生产环境里GPU 资源管理一定绕不开“共享”和“切分”两件事。这一节聊聊我实际用过的方案MIG、时间切片、第三方共享组件以及多卡任务的拓扑优化。4.1 MIG把一整张卡拆成多个互相隔离的实例MIGMulti-Instance GPU是 NVIDIA 在 A100、H100、A30 等数据中心 GPU 上提供的硬件级切分能力。它能将一张物理 GPU 切分成最多 7 个实例每个实例有独立的显存、独立的内存控制器、独立的 SM 分区实例之间在硬件层面隔离。这意味着一个 MIG 实例上跑的任务显存爆掉原则上不会影响同一物理卡上的其他实例也有边界情况后面会提到。MIG 开启后K8s 的资源模型会发生根本变化。物理 GPU 不再上报nvidia.com/gpu: 1而是上报一组 MIG profile比如nvidia.com/mig-1g.10gb: 2 # 这张卡可以创建 2 个 1g.10gb 实例 nvidia.com/mig-2g.20gb: 1 # 还可以创建 1 个 2g.20gb 实例Device Plugin 会把这些 profile 作为独立的 Extended Resource 上报。调度器依然只做整数匹配但这里的“整数”对应的是 MIG 实例数不再是物理卡数。Pod 申请nvidia.com/mig-1g.10gb: 1调度器确保节点上该 profile 还有可用实例kubelet 在Allocate阶段会告诉容器运行时使用哪个 MIG UUID。MIG 的好处是隔离性和稳定性特别适合多租户场景。但它有几个硬约束不是所有卡都支持 MIG。T4、V100、A40 等经典卡不支持。MIG 开启时物理 GPU 上的nvidia.com/gpu资源不会上报所以需要明确选择使用哪种资源模型。MIG 实例的显存是固定的比如1g.10gb只能用 10GB如果你想跑一个需要 12GB 的模型它就是用不了。资源粒度偏粗容易造成内部碎片。驱动、CUDA 版本、container toolkit 版本都需要匹配 MIG 支持。我个人的建议是如果集群里主要是推理业务、租户隔离要求高、卡是 A100/H100 这类支持 MIG 的新卡优先上 MIG如果跑大模型训练一张卡 80GB 经常都不够用再切 MIG 反而浪费直接用整卡更合理。4.2 时间切片与显存共享超卖和隔离的权衡MIG 是硬件切分另一条路线是软件层面的共享。官方比较轻量的做法是 GPU Time Slicing本质上是利用 CUDA 的时间片调度让多个上下文共享同一物理 GPU。NVIDIA 官方 device plugin 提供了 time-slicing 配置可以在一张物理卡上虚拟出多个“虚拟 GPU”资源。配置之后节点上报的nvidia.com/gpu数量可以超过物理卡数量比如 8 张物理卡、每张虚拟成 4 个节点就有 32 个nvidia.com/gpu。时间切片的好处是部署简单适合把小模型批量打包到一张大卡上。但它的隔离性很差显存不隔离。两个 Pod 共享同一张卡各自申请 16GB 显存实际物理显存可能只有 24GB。如果两个 Pod 同时把显存吃满会触发显存 OOM甚至产生 Xid 错误导致整卡掉卡。算力不隔离。时间片调度依赖 CUDA 层如果一个 Pod 里跑满算力另一个 Pod 的延迟会显著上升。故障影响面大。一个 Pod 的异常 CUDA 上下文可能影响同卡上的其他 Pod。所以时间切片只适合“空闲资源利用”和“低敏感推理”不适合生产环境的租户隔离。如果你需要同时隔离显存和算力可以有更重的方案。腾讯 qGPU、阿里 cGPU 这类基于内核和 Device Plugin 的共享方案能在单卡上创建多个虚拟 GPU每个虚拟 GPU 有独立的显存上限和算力权重。它们通过劫持 CUDA 的显存申请和 kernel 下发来实现隔离。这类方案的引入成本高通常还需要专门的驱动模块和研发维护适合中大型平台团队。另一种很实用的轻量方案是给同一张卡上的 Pod 分别设置 MIG 或者使用 MPSMulti-Process Service限制算力。MPS 是 NVIDIA 提供的进程级共享机制可以在普通卡上把多个 CUDA 上下文合并提交配合显存限制能实现一定程度的隔离。但 MPS 的配置和管理也不简单而且和容器调度结合不如 MIG 那么优雅。我的一段实战经验在一个 10 多张 A100 的推理集群里我用 MIG 时间切片组合。核心业务用 MIG 保证隔离非核心的批量离线推理用时间切片跑把空闲算力利用起来。前提是运维侧有清晰的业务分级和严格的资源 Request 规范。4.3 多卡任务与拓扑感知为什么 8 卡任务会“莫名”变慢GPU 调度还有一个比共享更隐蔽的问题多卡任务的设备拓扑。以一个常见的 8 卡节点为例物理上这 8 张卡通过 NVLink 或 NVSwitch、PCIe Switch、甚至 NUMA 节点相连。有的卡之间走高速 NVLink有的卡之间要走 PCIe通信带宽和延迟完全不同。默认调度器不关心这个问题。当你的 Pod 请求 8 个 GPU 时Device Manager 会从节点上任意挑 8 张可用的卡。如果节点上已经有其他 Pod 占用了部分卡你的 8 张卡可能分布在不同的 PCIe Switch 下面跨 NUMA 访问集合通信性能会有一段断崖式下降。最典型的场景是分布式训练NCCL 的 allreduce 性能直接决定训练吞吐。我遇到过训练速度慢 30%排查到最后发现是 4 张卡在 NUMA node 0、4 张在 NUMA node 1通信大量走 QPI/UPI。要解决这个问题有两条路开启 kubelet 的 Topology Manager 和 CPU Manager让 GPU、CPU、内存三者尽量在同一 NUMA 节点上对齐。这要求 Pod 在 resources 里显式请求 CPU并且节点 CPU 策略配置为静态。部署拓扑感知调度插件或调度器。比如 scheduler-plugins 的NodeResourceFit扩展、NVIDIA 官方的 GPU topology 感知方案可以在调度阶段就根据 GPU 之间的 NVLink 拓扑计算最优设备组合并为 Pod 打上最合适的节点标签或模拟资源。如果你的集群规模不大业务也比较单一可以先用节点选择器粗粒度控制把 8 卡训练任务固定调度到“整节点空闲”的节点上同时用反亲和避免和其他 GPU Pod 混部。这种方式牺牲了调度密度但换来了稳定性在中小集群里是最划算的选择。4.4 资源测算怎么判断一张卡到底够不够很多业务方提交 GPU 申请时对“需要几张卡”完全没有概念只管写 4 卡、8 卡。平台侧如果也不做校验就会出现一堆 Pod 申请了 8 卡但实际只用了 1 卡的情况。所以我要单独提一下 GPU 资源测算。推理场景的显存需求主要由几个部分组成模型权重、KV Cache、激活值activation、CUDA context。以大模型推理为例7B 参数 FP16 权重大约 14GB如果开启 vLLM 这类优化KV cache 会动态分配通常还需要预留 20%-40% 的额外空间。你可以用 vLLM 启动时的--max-model-len、batch size 去估算峰值显存也可以直接看nvidia-smi或 DCGM 指标来反向校准。我的习惯是先按最乐观情况给一个初值再拿真实请求压低至少观察一周的显存峰值再做资源规格模板。训练场景的显存需求更复杂但有一个粗略的公式模型参数权重假设参数量为 PFP16 训练时权重占用2P字节梯度同样2P字节优化器状态Adam 优化器通常需要8P到12P字节主副本、动量项、方差项取决于实现激活值取决于 batch size、序列长度、层数这部分经常比权重还大所以一个 7B 模型用 Adam 混合精度训练光模型状态就需要大概(2 2 12) * 7GB ≈ 112GB。这意味着单张 80GB 的 A100 根本放不下通常需要 ZeRO 等并行策略或者调低 batch size、开启激活重计算。很多训练任务一直 OOM不是代码问题是资源测算在第一步就错了。平台侧可以把常见任务的资源需求做成模板比如“chat 模型 7B 推理1×A100 40G / 2×A100 80G”减少业务方反复试错。5. 排障实录GPU 相关问题的定位链路这一节不讲理论全部是我在 GPU 集群运维中真实踩过、解决过的问题。我按现象分类并给出一条可以复现的排查链路方便你下次直接套用。5.1 Pod Pending先看调度器怎么说GPU Pod 最常见的问题就是一直 Pending。第一步永远是kubectl describe pod pod-name看 Events 里的调度消息。常见的几种0/8 nodes available: 4 Insufficient nvidia.com/gpu, 2 node(s) didnt match node selector, 2 node(s) had taint ...这种信息非常直白对应三种原因。Insufficient 说明 GPU 资源不足node selector 不匹配通常是你用了节点选择器但打的标签不对taint 相关说明 Pod 没有加 toleration。如果 Events 里只有FailedScheduling没有其他细节可能是调度器本身异常或者 webhook 拦截。进 scheduler 日志看一下。还有一种隐蔽情况一个节点上 Capacity 显示有 GPU但 Allocatable 是 0。这通常是因为节点上的 GPU 设备被标记为 Unhealthy或者 Device Plugin 上报设备数为 0。这时候去查 Device Plugin 的日志。资源请求写错也会导致 Pending。比如业务方在 yaml 里写了nvidia.com/gpu但集群里根本没有安装 NVIDIA Device Plugin调度器当然找不到任何节点满足条件。这类问题的排查顺序是先确认集群里确实有该资源再看资源数量是不是够再看 Pod 的调度约束最后才考虑调度器配置。5.2 ContainerCreating 与 Runtime Hook 失败如果 Pod 已经调度到节点但一直卡在ContainerCreating说明 kubelet 在创建容器阶段出了问题。先看事件kubectl describe pod pod-name -n namespace比较常见的是failed to run Kubelet container ... nvidia-container-cli: detection error: ...这种是宿主机 NVIDIA Container Toolkit 或驱动问题。“detection error”一般翻译过来是“找不到 GPU 设备或者驱动库”在容器运行时 hook 被调用、但设备没暴露时就会出现。优先检查宿主机nvidia-smi是否正常再检查容器运行时配置。另一个高频报错是Error: failed to get device ... failed to add device ... /dev/nvidia-uvm does not exist这基本确认是nvidia_uvm内核模块没有加载。直接modprobe nvidia-uvm然后重建 Pod。如果重启后模块又消失你需要把模块加载写成持久化配置比如/etc/modules-load.d/nvidia.conf。还有一类 ContainerCreating 与 runtime 无关是 Pod 内部镜像启动时 CUDA 初始化失败。比如镜像里带的是 CUDA 12.1宿主驱动只支持到 CUDA 11.8。启动日志会出现CUDA driver version is insufficient。这时候要么换一个与驱动匹配的 CUDA 镜像要么升级宿主机驱动。强烈推荐把“宿主机驱动版本管理”做成节点池维度比如 gpu-driver-535、gpu-driver-550 两种节点池不同业务选择不同池子避免升级驱动时全集群一起遭殃。5.3 显存 OOM、Xid 错误与掉卡显存 OOM 在 GPU 集群里属于“日常暴力”级别的故障。最直观的现象是容器内日志报CUDA out of memory但 K8s 层面可能没有任何异常事件因为 K8s 根本不感知显存用量。如果显存 OOM 只导致进程退出可能只影响这一个 Pod但如果 OOM 发生在共享卡上、或者直接触发了驱动级的 Xid 错误风险就大了。Xid 错误是 NVIDIA 驱动报告的错误码常见的有Xid 79/80GPU 显存或者内部错误经常由显存超频、供电不稳、硬件故障导致。Xid 33通常是驱动和 GPU 固件之间发生异常处理不当会演变成整卡“掉卡”。Xid 45一般是预取错误多和 PCIe 链路有关比如卡没有插稳。排查掉卡的方法dmesg | grep -i nvidia nvidia-smi # 如果卡已经掉这里看不到对应卡掉卡后节点上的 Device Plugin 健康检查会立刻把这个设备标记为 UnhealthyK8s 侧体现为Allocatable数量减少。运维处理要果断先隔离该节点然后尝试nvidia-smi --gpu-reset如果还在掉就报修。不要抱侥幸心理把坏卡留在节点上它会不断吞噬排障精力。对于显存 OOM另一个常见原因是显存碎片。哪怕总量看起来够但碎片导致连续显存不足。这类问题只能靠减小 batch size、重启进程、或者换用 vLLM 等对显存管理更友好的推理框架。运维侧无法根治。5.4 一套可复用的 GPU 排障命令组合我把常用的排查命令总结成一张速查表遇到 GPU 相关问题时按顺序执行现象排查命令关键信息Pod Pendingkubectl describe pod podEvents 里的 FailedScheduling节点 GPU 数量不对kubectl describe node nodeCapacity / Allocatable / Unhealthy 数量Device Plugin 异常kubectl logs -n kube-system ds/nvidia-device-plugin设备数量、健康检查结果容器内看不到 GPUcrictl exec ctr nvidia-smienv | grep CUDACUDA_VISIBLE_DEVICES 是否存在运行时 hook 失败journalctl -u kubeletnvidia-container-cli 相关错误驱动/内核模块异常lsmod | grep nvidiadmesg | grep -i nvidianvidia_uvm 是否加载、Xid硬件故障nvidia-smi -anvidia-smi -q -d ECCECC 错误计数、温度、掉卡有一个容易忽略的点kubectl logs只能看到容器 stdout/stderr但很多 CRI 错误发生在容器创建前。所以排查 ContainerCreating 时一定不要只看 Pod 事件要看 kubelet 日志journalctl -u kubelet --since 10 minutes ago | grep pod-name这条命令能看到 CRI 调用失败的具体原因比 describe pod 有用得多。6. 长期维护GPU 集群的监控、升级和一些容易忽略的细节GPU 集群和普通 CPU 集群的运维节奏差别很大。GPU 是高价值、高故障率、高能耗设备如果不做监控故障基本要靠业务方来报。这一节谈谈我长期维护 GPU 集群的经验。6.1 DCGM 监控与告警指标NVIDIA 官方的 DCGMData Center GPU ManagerExporter 是目前主流的 GPU 监控方案。它通过 Prometheus 暴露一组 GPU 相关的指标再由 Grafana 展示。我建议告警至少覆盖以下指标DCGM_FI_DEV_GPU_UTILSM 利用率。不要只看这一个指标判断 GPU 忙不忙它衡量的是计算单元占用显存搬数据但没计算时利用率可能很低。DCGM_FI_DEV_FB_USED/DCGM_FI_DEV_FB_FREE显存使用率。这个是所有 GPU 告警里最该盯的显存打满离 OOM 只差一步。DCGM_FI_DEV_TEMP温度。A100 这类卡长期超过 85°C 就有风险H100 甚至需要更保守。DCGM_FI_DEV_XID_ERRORSXid 错误计数。这个指标突然升高基本就是硬件问题的前兆。DCGM_FI_DEV_MEM_CLOCK/DCGM_FI_DEV_SM_CLOCK频率。如果手动降频或热节流这里能看出来。NVLink 流量和带宽DCGM_FI_PROF_PCIE_TX_BYTES、DCGM_FI_PROF_NVLINK_TX_BYTES多卡任务性能异常时重点查这里。告警阈值要按卡类型和业务形态调整。比如 T4 的散热能力和 A100 完全不同不能设同一个温度阈值。GPU 利用率也不是越高越好长期 100% 利用率且伴随温度高企可能导致寿命缩短或可靠性下降。在共享/超卖场景下监控的另一个难点是“这张卡上有哪个 Pod”。建议在添加 DCGM 指标时把 Pod 的 namespace、name、GPU UUID 关联上否则出现显存突然打满时你根本不知道是哪个租户干的。6.2 驱动和容器运行时的升级怎么不做成大事故GPU 驱动升级是最容易引发大面积故障的操作。因为一旦驱动版本变化所有节点上的 Device Plugin、容器运行时 hook、甚至已运行 Pod 的 CUDA 上下文都可能受影响。我建议的流程是先在测试节点上验证升级驱动、重启节点、确认nvidia-smi正常、跑一个真实业务 Pod观察日志和监控。正式升级时先kubectl drain node --ignore-daemonsets --delete-emptydir-data把节点排空。非 GPU 业务先迁走。卸载旧驱动安装新驱动建议同时重启节点确保内核模块干净加载。重启 kubelet、containerd确认 Device Plugin 已重新注册。用kubectl describe node确认nvidia.com/gpu数量恢复。kubectl uncordon node恢复调度。逐个节点滚动不要并行操作太多节点。容器运行时containerd 版本升级相对低风险但也要注意 NVIDIA Container Toolkit 和 containerd 的兼容性矩阵。containerd 大版本升级可能改变 CRI 插件的配置结构导致config.toml里的 nvidia runtime 配置不生效。升级后一定要先用测试容器验证 GPU hook再批量推广。6.3 几个容易被忽略的运维细节最后分享几个我长期维护 GPU 集群后总结出来的细节每个都踩过坑GPU Pod 的 CPU 资源不要卡得太死。CUDA 运行时、NCCL 通信、数据加载都会占用大量 CPU 线程。Linux 下 CUDA 默认会为每个 GPU 创建独立的 polling 线程如果 Pod 的 CPU limit 只有 1 核或者不到 2 核可能出现 GPU 利用率上不去、训练/推理吞吐不如预期。给 GPU Pod 预留至少 4 核是比较稳妥的做法。系统内存要和显存成比例预留。大模型训练时pinned memory、CUDA context、数据预处理 buffer 都在系统内存里。如果节点只有 128GB 内存却插 8 张 80GB 的卡业务一上来就会 OOM。常规划分是每张 GPU 至少搭配 16GB 到 32GB 系统内存A100/H100 集群建议更高。HugePages 和 IPC Lock 限制。NCCL 在分布式训练时会使用大量共享内存和锁如果limits.memory过小或者/dev/shm空间不足NCCL 初始化会失败。有时候报错是 “unable to allocate memory for receive buffers”其实不是显存问题是容器内共享内存不够。资源配额别忘了 Extended Resource。ResourceQuota 是支持扩展资源的如果你在多租户集群里只限制了 CPU/内存、没限制nvidia.com/gpu业务方可能在一个 namespace 里把全集群的 GPU 申请完。务必加上配额apiVersion: v1 kind: ResourceQuota metadata: name: gpu-quota spec: hard: nvidia.com/gpu: 4不要把所有 GPU 节点打成一个池子。如果业务同时有 A100 和 T4建议按卡型拆分节点池并打上明确的标签。否则一个训练任务调度到 T4 上性能直接不可用业务方还以为是代码问题。把“不匹配”在调度阶段就挡掉比事后优化代码省事得多。最后再说一个我自己的习惯。我会在每个 GPU 节点上固定放一个 minimal 的 DeepSpeed / vLLM 测试 Pod 模板节点上架验收、驱动升级、故障切换后都先跑一遍这个模板确认从调度到容器内nvidia-smi、再到真实 CUDA 矩阵运算都正常再放业务进来。这个操作成本很低但能把大概率会在凌晨爆发的问题提前到下午茶时间解决。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。