Kubernetes 中的 GPU:其底层工作原理
前言:
Kubernetes中的GPU几乎是所有AI基础设施预算中最昂贵的项目,而Kubernetes对GPU的管理能力却远逊于其调度的其他资源。CPU请求可以拆分份额、进行限流,并在容器闲置时立即回收;而GPU请求则是对整个物理设备的独占式占用,且Kubernetes原生无法判断该设备是否实际执行了任何任务。
这种管理盲区之所以存在,是因为 GPU 并不属于 Kubernetes 用来管理 CPU 和内存的那些 Linux 内核底层机制的管辖范围。控制组(cgroups)负责计量和限制容器可使用的资源,命名空间则隔离其可见范围,但对GPU而言这两者均不透明。因此Kubernetes只能退而求其次地统计整个设备数量,并将实际分配工作交由NVIDIA驱动栈处理。Kubernetes能确认GPU已分配给某个Pod,却无法判断该Pod究竟使用了设备3%还是99%的算力:
在 K8s 中管理 CPU 和 GPU 的体验有着天壤之别:

打个比方: CPU 就像酒店的共享办公工位,你可以按小时租一个角落,没人用时立刻收回;而 GPU 就像豪华套房,只要你申请了,哪怕你整天睡大觉,整间房也只能为你空着,别人一概进不去。
已分配和已使用之间的区别并非无关紧要。GPU节点的常规成本是同等CPU节点的数倍,而一个所有设备都显示为已被占用的集群,其实际运行能力可能仍远未达到真正的容量,且没有原生信号提示任何人存在问题。
本指南是五部分系列中关于在Kubernetes上运行GPU工作负载的第一篇,它涵盖了整个系列所依赖的基础机制:将原始GPU转化为nvidia.com/gpu可调度资源的四层堆栈、该资源请求实际承诺与未承诺的内容、一个可用的Pod清单,以及DCGM填补之前导致利用率不可见的观测空白。下一部分将由此展开,展示DCGM遥测数据如何从工程师手动检查的数字转变为集群可自动响应的指标。
要点总结
1、Kubernetes通过NVIDIA驱动程序和设备插件管理GPU,而非借助cgroups或命名空间,因此无法像处理CPU或内存那样对GPU进行限流、细分或回收。
2、一个正常运行的GPU节点需要四个独立层次:内核驱动程序、用户空间的CUDA、NVIDIA容器工具包,以及向kubelet声明nvidia.com/gpu资源的设备插件。3、大多数GPU Pod规范仅设置limits.nvidia.com/gpu。Kubernetes会自动将requests填充为相同值。不存在独立默认值,若这两个字段未同时出现该数值,则无法调度GPU资源。
4、资源计数跟踪的是分配状态而非使用情况。从调度器视角看,占用GPU但利用率为0%的Pod与满载运行的Pod毫无区别。
5、原生Kubernetes工具(包括kubectl top、水平Pod自动扩缩容和垂直Pod自动扩缩容)缺乏读取GPU指标的信号。
6、DCGM通过NVML(Kubernetes实现自动GPU利用率管控所需的遥测层)提供真实的单GPU算力、内存和功耗指标。
一、为什么 Kubernetes 中的 GPU 与 CPU 不同
在Kubernetes上运行工作负载时,实际上是在要求Linux内核将一台机器的CPU和内存资源分配给多个进程共享。Kubernetes依赖两个内核实现这一功能:首先是控制组(cgroup),它负责计量并限制每个容器可获取的CPU时间和内存;其次是命名空间(namespaces),用于隔离各容器可见的范围。由于这些控制机制存在于内核层面,Kubernetes能够对CPU和内存实现其根本无法对GPU实施的操作——例如为Pod分配500毫核CPU资源、在其超额使用时立即限流、节点压力增大时回收内存,以及将数十个容器压缩到单个物理核心上运行。对内核而言,CPU本质上是可分割且完全可回收的资源。而GPU则完全不具备这些特性。内核并不负责调度NVIDIA GPU的工作负载,这项工作由NVIDIA驱动完成——这个驱动是闭源的二进制模块,拥有自有的编程层CUDA,其全链路技术栈均由厂商掌控。控制组无法计量GPU算力,命名空间也不能划分GPU显存,因为Kubernetes所依赖的内核从根本上就不具备对该设备的管控权限。
就Kubernetes而言,/dev/nvidia0文件只是一个它完全不了解的不透明字符设备。这就是GPU在Kubernetes中常被称为二等公民的原因。该平台构建于Linux基础架构之上,而GPU完全处于这套体系之外,因此无法像对待CPU那样对GPU进行细分、限流或回收。它只能退而求其次,将GPU视为不透明的可计数资源,并将实际分配工作交给外部组件处理。这个组件就是NVIDIA设备插件,下一节将具体阐述其运作机制。当你首次在同一个pod配置中同时放置CPU请求和GPU请求时,差异就会变得非常明显:
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
nvidia.com/gpu: 1
CPU: 500m是一个动态请求,内核会时刻强制实施。nvidia.com/gpu: 1是一个二元声明:Pod要么获得一整块物理GPU,要么根本不被调度。该字段不支持半块GPU的语法,两个Pod无法通过它共享设备,Kubernetes仅记录GPU已分配,而从不追踪是否有单个CUDA内核实际在其上运行。"已分配"与"实际使用"之间的差距是本系列文章探讨的主线,这条线索直指硬件本质。CPU设计用于上下文切换,能在指令执行中途暂停一个进程、运行另一个进程,并在几微秒后恢复第一个进程。而GPU则专为原始吞吐量设计,而非交错处理。提交到设备上的计算通常要运行到完成,内存也是以大块连续形式提交到GPU显存,而非按需分页交换。该芯片的工程目标是以最快速度完成海量并行任务,而非被精细切割分配给多个租户。当Kubernetes在1.8版本通过设备插件框架添加GPU支持时,就继承了这种硬件约束,本指南后续介绍的几乎所有模式都是为了解决这个问题而存在的。
二、完整的 GPU 堆栈,逐层解析
GPU节点在加入集群时并不可用。从芯片到能够实际调用torch.cuda.is_available()的容器之间,存在四个独立的层级,每一层都需要单独安装和操作。只有当这四层全部就位时,申请nvidia.com/gpu: 1的请求才会成功。按顺序梳理这些层级,是理解Kubernetes中"GPU支持"真正含义的最清晰方式。
第一层:内核驱动。NVIDIA驱动作为Linux内核模块安装在主机上。加载该驱动会创建上层所有功能依赖的字符设备,包括用于第一块GPU的/dev/nvidia0、用于控制操作的/dev/nvidiactl,以及用于统一内存的/dev/nvidia-uvm。若这些设备文件不存在,用户空间将无法访问GPU。该层还将硬件绑定至特定驱动版本,这对下一层的兼容性规则至关重要。
第二层:用户空间中的CUDA。CUDA是PyTorch和TensorFlow等框架实际调用的编程层;当模型在GPU上运行时,它发出的是CUDA调用,而非直接与内核通信。对于操作者而言,驱动版本与CUDA工具包版本无需严格匹配,但必须保持兼容。NVIDIA发布了前向兼容性矩阵,定义了给定驱动程序支持的CUDA版本。只有当该矩阵允许时,基于新版CUDA工具包构建的容器才能在旧版主机驱动程序上运行。首次GPU故障的很大一部分可追溯至不符合此矩阵要求的版本配对。
第三层:NVIDIA容器工具包。默认情况下,调度到GPU节点上的容器仍然无法识别GPU,因为容器隔离机制会隐藏主机的设备。NVIDIA容器工具包通过向容器运行时(通常是containerd)添加运行时钩子来填补这一缺口。当GPU容器启动时,该钩子会将驱动库和相关的/dev/nvidia*设备挂载到容器的命名空间中。若缺少这一层,即使Pod已申请并获分配GPU,运行时仍可能因找不到驱动程序而报错——这是初次使用时最常见的错误之一。该工具包通过Pod引用的RuntimeClass进行配置:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: nvidia
handler: nvidia
第四层:设备插件。Kubernetes仍然没有原生的GPU概念,因此需要某种机制告知其GPU的存在及数量。这一职责由NVIDIA设备插件承担,这是一个运行在每个GPU节点上的DaemonSet。它通过NVIDIA的管理库NVML查询驱动程序,统计节点上的GPU数量,随后通过设备插件API向kubelet注册这些GPU。kubelet继而将它们作为名为nvidia.com/gpu的扩展资源上报给控制平面,这就是GPU节点在其容量中报告这些资源的原因:
$ kubectl get node gpu-node-1 -o jsonpath='{.status.capacity}'
{"cpu":"48","memory":"353814036Ki","nvidia.com/gpu":"4","pods":"110"}
那四个nvidia.com/gpu单元是调度器唯一感知的实体。它处理请求nvidia.com/gpu: 1的Pod与处理任何扩展资源的方式相同:找到一个尚未耗尽广告计数的节点,然后绑定。当Pod落地时,实际的交接通过设备插件完成。kubelet调用Allocate接口,插件选择具体的物理GPU并返回其设备ID,kubelet将这些ID传递给containerd,第三层的运行时钩子设置NVIDIA_VISIBLE_DEVICES环境变量并将对应的/dev/nvidiaN设备挂载到容器中。直到这一刻,GPU才对Pod内的CUDA可见。
实践中几乎没人会手动安装这四层组件。NVIDIA GPU Operator将它们打包集成,作为整体单元在每个GPU节点上管理:包括驱动程序、容器工具包、设备插件、用于标记GPU硬件的节点特征发现功能,以及后续指南中将涉及的指标导出器DCGM。该Operator是将全新GPU节点池转化为Kubernetes可调度资源的标准方案,也是大多数托管服务在添加GPU节点池时自动安装的组件。
以上四层简单总结就是:
你把一张昂贵的 GPU 显卡插进服务器里,Kubernetes(K8s)是根本没办法直接使用的。必须接通 4 层“管道”,AI 代码才能真正用到显卡。
如果把 K8s 节点 比作一个大型工厂,把 容器(Pod) 比作工厂里的封闭隔音间,把 GPU 比作一台刚运进厂里的超级大机器,这 4 层做的事情如下:
1. 第一层:给机器通电、装上控制面板(内核驱动)
显卡插进服务器后,操作系统必须先装 NVIDIA 驱动。驱动装好,系统里才会生成控制文件(比如 /dev/nvidia0)。没有这一步,操作系统本身都是个“瞎子”,根本不知道这台机器里装了显卡。2. 第二层:配一本两头都能看懂的说明书(CUDA)
像 PyTorch、TensorFlow 这些 AI 框架,它们不识字,不会直接操作底层硬件,它们只认 CUDA 语言。这层负责把 AI 的指令翻译成驱动能听懂的话。新手最常踩的坑: 容器里的说明书版本(CUDA 版本)和宿主机上的机器控制面板(驱动版本)对不上,导致 AI 直接崩溃报错。
3. 第三层:在封闭隔音间的墙上打个孔、拉进电缆(容器工具包)
容器默认是完全封闭隔离的,根本看不到外面宿主机上的显卡。这个工具包(NVIDIA Container Toolkit)的作用,就是在容器启动的瞬间,在容器墙上凿个洞,把显卡的控制接口直接拉进容器内部。没这一步,容器里的代码就会报错说“找不到显卡”。4. 第四层:去厂房前台登记“本店有大机器”(设备插件)
K8s 默认是个“卡盲”,它只认识 CPU 和内存,根本不知道世界上有 GPU 这回事。Device Plugin 就像个登记员,负责数清楚这台服务器有几张卡,然后跑去跟 K8s 汇报:“这台机器有 4 张显卡!”K8s 调度器这才知道可以把 AI 任务往这台机器上发。现实中大家是怎么做的?
因为一层层手动去配这 4 个东西极其折磨人,所以现在业界没人这么干。大家都是直接安装 NVIDIA 官方提供的 GPU Operator——这相当于一个“全家桶一键安装包”,自动帮你在所有 GPU 节点上把这 4 层全套搞定。
核心总结
打通这 4 层,只是让 AI 容器“能够看到并用上显卡”。但正如前文所说,K8s 依然只负责把显卡“整张交出去”,至于容器拿到显卡后是在疯狂计算还是在白白挂机浪费钱,K8s 依然一概不知。
三、nvidia.com/gpu : 1 的真正含义
为 pod 分配 GPU 的请求只有一行代码,它的简洁性掩盖了它与 pod 规范中其他所有资源的不同之处:
resources:
limits:
nvidia.com/gpu: 1
该行的三个特性决定了后续所有配置,每个特性都值得精确说明。
首先,该数值代表整块设备的数量。nvidia.com/gpu是一种仅支持整数的扩展资源,因此唯一有效值为0、1、2及以上。请求0.5并不会部分分配GPU,而是直接导致验证失败。一个单位即代表一整块物理GPU,该GPU将在Pod的整个生命周期内被分配给唯一一个容器。
其次,请求值同时就是限制值。Kubernetes要求扩展资源的请求值和限制值必须相等,这消除了CPU和内存所依赖的可突发中间状态。对于CPU,你可以请求半个核心并突发突破到更高限制,内核会实时仲裁争用并通过节流指标反映。但GPU完全不具备这种弹性。你请求固定数量的整块设备,就会获得完全相同的数量,不存在超额分配、突发机制或平台需要执行的软限制。
第三,该数字并未说明使用情况。Kubernetes仅将nvidia.com/gpu视为分配计数。它既不限制GPU内存,也不限制GPU计算,对容器内共享设备的进程间未提供任何隔离,且完全未记录GPU是否处于工作状态。一个加载200MB模型且每小时处理一次请求的Pod,与一个占满全部80GB显存并以满负荷计算的Pod,在GPU占用强度上完全相同。对于调度器及所有Kubernetes原生指标而言,二者完全无法区分:
$ kubectl describe node gpu-node-1 | grep -A2 "Allocated resources"
Allocated resources:
Resource Requests Limits
nvidia.com/gpu 4 4
已分配四个GPU,可用数为零。无论这四块设备的使用率是3%还是持续高达99%,Kubernetes能感知的只有这个数字,而正是这个数字驱动着调度决策、容量规划以及云账单上的GPU费用项。
GPU可以实现共享。时间切片、MPS、MIG以及更新的动态资源分配技术都能让单个物理GPU服务于多个工作负载,这些将是本系列下一篇文章的全部主题。但所有这些技术都未改变基础nvidia.com/gpu整数字段的含义。通过该字段,GPU始终遵循全有或全无原则,而Kubernetes分配量与硬件实际运行情况之间的差异,需要依靠Kubernetes之外的机制来监测。这个差异正是本文剩余部分要探讨的核心。
四、你的第一个GPU POD
最小而有用的实验是一个请求一个GPU、打印其可见内容并退出的Pod。以下是完整实现这一功能的清单:
apiVersion: v1
kind: Pod
metadata:
name: gpu-probe
spec:
restartPolicy: Never
runtimeClassName: nvidia
nodeSelector:
nvidia.com/gpu.present: "true"
containers:
- name: cuda
image: nvidia/cuda:12.4.1-base-ubuntu22.04
command: ["nvidia-smi"]
resources:
limits:
nvidia.com/gpu: 1
有三个字段承载了GPU特定的行为。resources.limits.nvidia.com/gpu: 1表示设备插件所满足的资源分配,这与前一节所述完全一致。nodeSelector确保Pod不会被调度到仅支持CPU的节点上,这些节点不会宣告nvidia.com/gpu的容量,否则Pod将无限期处于Pending状态。runtimeClassName: nvidia选择了注入驱动的容器运行时,而这个字段在初次使用时经常被遗漏:即使Pod被分配了GPU,如果运行时钩子从未运行,启动时仍可能因找不到驱动而失败。
在托管平台上,节点选择和运行时配置略有不同。您需要通过加速器标签而非通用选择器来选择硬件,并且通常会完全省略runtimeClassName,通过其托管的安装程序安装驱动,并自动为您将NVIDIA运行时配置到节点中:
nodeSelector:
cloud.google.com/gke-accelerator: nvidia-l4
Pod 调度完成后,日志会显示容器实际可以看到的内容:
$ kubectl logs gpu-probe
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.183.01 Driver Version: 535.183.01 CUDA Version: 12.4 |
|--------------------------------+-------------------------------+------------------------|
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
|==================================+==============================+========================|
| 0 NVIDIA L4 Off | 00000000:00:04.0 Off | 0 |
| N/A 38C P8 12W / 72W | 0MiB / 23034MiB | 0% Default |
+----------------------------------+------------------------------+------------------------+
| No running processes found |
+-----------------------------------------------------------------------------------------+
容器检测到一个NVIDIA L4显卡、535系列驱动、CUDA 12.4环境以及约23GB的显存——由于这个探测程序在退出前未加载任何内容,所有显存均处于空闲状态。若在真实推理容器内执行相同nvidia-smi命令,数据就会发生变化:占用两三GB显存的模型在请求间隔期间GPU利用率显示为0%,却独占整个设备而仅使用其微小部分。
这个简单命令也揭示了监控能力的边界。nvidia-smi虽然能实时反馈精确数据,但仅限于容器内部且仅反映运行瞬间状态。这些数据无一传递至Kubernetes系统。无论设备处于闲置还是满载状态,控制平面始终记录着"已分配1个GPU,可用0个"的静态信息。要将这种容器级快照转化为集群可响应的指标,需要Kubernetes原生不具备的功能层,这也正是下一章节要探讨的内容。
五、可观测性差距
Kubernetes 向你提供的关于正在运行的工作负载的所有信息都来自同一条流水线。kubelet 内置的 cAdvisor 从 cgroups 中收集每个容器的 CPU 和内存使用情况,metrics-server 对其进行聚合,kube-state-metrics 暴露对象状态,并kubectl top读取聚合结果。这条流水线基于与第一部分相同的内核原语构建,因此它只报告内核控制的两种资源:CPU 和内存,除此之外别无其他。如果你询问它关于 GPU 的信息,它不会返回任何相关字段。
$ kubectl top pod vllm-server
NAME CPU(cores) MEMORY(bytes)
vllm-server 240m 14620Mi
GPU始终在持续运行。前一章节中的nvidia-smi输出证明了设备在任意时刻处于忙碌或空闲状态。这些信息仅存在于Pod内部,从未进入指标管道,因此Kubernetes只能告知GPU是否被分配,却无法判断该GPU是否在执行任务。这一缺陷直接影响到Kubernetes本应响应负载的各个组件:默认调度器仅根据分配数量部署GPU Pod,一旦节点上的GPU被申领即视为满载,不论这些GPU是否实际工作;水平Pod自动扩缩容仅依据CPU、内存或自定义指标进行,除非自行构建监控管道,否则这些指标均不包含GPU利用率;垂直Pod自动扩缩容仅针对CPU和内存资源提出建议,对GPU资源完全无感知。所有本应对闲置GPU作出反应的原生控制循环,都对其最关键的行动信号视而不见。
在真实集群规模下,这会导致可预见的资源错配模式:由于整数型资源类型限制,每个工作负载必须占用整块GPU,而这些GPU的实际利用率普遍远低于其容量上限,同时分配视图始终显示集群处于满载状态。最终可能出现所有GPU均被申领,而整体硬件仅执行了其潜在工作负载的一小部分,且没有任何Kubernetes原生视图能揭示这种差异。这种差异代价高昂——GPU节点是大多数AI集群中成本最高的项目(往往远超其他资源),当平台无法感知实际利用率时,为已分配却闲置的算力资源付费将成为常态而非例外。
FinOps基金会《2026年FinOps现状调查报告》(涵盖从业者管理的年度云支出超830亿美元)指出,细粒度GPU利用率监控是目前从业者最迫切需要的缺失功能。CNCF关于生产环境AI工作负载的专项报告同样从基础设施角度确认了这一根本性问题。
六、接下来会怎样:共享单个GPU
本指南中的所有内容皆假设一个Pod独占整块GPU,因为这是直接请求`nvidia.com/gpu`时的强制要求。这也是运行大多数推理负载时效率最低的方式——当一个模型仅占用24GB显存显卡中的3GB时,只要Pod存在,该模型就会一直占据整个设备。
本系列下一篇文章将探讨Kubernetes实现单块物理GPU被多个工作负载共享的四种方式:时间切片、NVIDIA多进程服务(MPS)、支持该功能的硬件上的多实例GPU(MIG),以及会改变Kubernetes在表达部分结构化设备声明时固有局限的动态资源分配(DRA)。这些方式均不影响本指南所述内容。无论采用哪种共享模式,都必须先构建四层技术栈(驱动程序、CUDA、容器工具包和设备插件),且必须通过DCGM监控设备指标,否则无法判断共享配置是真正提升了利用率还是仅仅增加了资源争用。
七、常见问题解答
1、可以在 Kubernetes 中申请半个 GPU 吗?
并非通过普通
nvidia.com/gpu资源。它是一种仅限整数的扩展资源,因此请求失败会导致0.5验证失败,而不是资源分配不足。对 GPU 的部分或共享访问需要使用下一篇文章中介绍的共享机制之一:时间片轮转、MPS、MIG 或动态资源分配。
2、为什么 Kubernetes 不能像查看 CPU 利用率那样查看 GPU 利用率?
CPU 和内存指标来自 cAdvisor 读取 cgroups,cgroups 是 Kubernetes 构建时所依赖的内核级记账机制。GPU 没有等效的内核原语。GPU 的运行情况由驱动程序和 NVML(而非内核)跟踪,而 Kubernetes 本身也没有读取 NVML 的原生管道。
3、Kubernetes 中一个 Pod 最多可以请求多少个 GPU?
一个 Pod 可以请求任意整数数量的
nvidia.com/gpu设备,但不能超过单个节点上实际存在的设备数量。例如,一个拥有 4 个物理 GPU 的节点可以满足 Pod 请求的 1、2、3 或 4 个设备;它不能满足超过节点实际拥有数量的请求。
4、DCGM 实际执行哪些操作?
NVIDIA 数据中心 GPU 管理器通过 NVML 直接从硬件读取每个 GPU 的计算能力、显存、温度和功耗,并将这些数据作为集群可以抓取的指标公开。它并非由设备插件安装,需要单独添加,通常通过 NVIDIA GPU Operator 添加。
5、pod 被调度到了一个 GPU 节点上,但却出现了找不到驱动程序的错误。这是怎么回事?
常见的原因是缺少或配置错误
runtimeClassName。如果没有它,容器运行时就不会运行将驱动程序库和设备挂载到容器中的钩子/dev/nvidia*,因此 GPU 会被分配,但实际上在 Pod 中不可见
6、需要分别安装驱动程序、CUDA、容器工具包和设备插件吗?
NVIDIA GPU Operator 将所有四个层打包并作为一个整体进行管理,同时还包括节点特性发现和 DCGM 导出器,大多数托管 Kubernetes 服务在添加 GPU 节点池时都会自动安装它。
更多推荐
所有评论(0)