当一个AI训练任务向Kubernetes请求GPU时,真正发生的事情远比“调度器找到一台有GPU的服务器”复杂。
从物理GPU到最终运行的Pod,中间至少经过GPU驱动、NVIDIA Device Plugin、kubelet、Kubernetes Scheduler、容器运行时以及NVIDIA Container Toolkit等多个层次。
理解这条链路非常重要,因为GPU调度和CPU调度并不是完全相同的机制。
一、Kubernetes一开始并不知道服务器有几块GPU
假设一台Linux服务器安装了8张NVIDIA GPU。
操作系统和NVIDIA驱动可以识别这些GPU,但刚刚加入Kubernetes集群时,Kubernetes并不会凭空知道这台机器有8张可以调度的GPU。
这时候需要NVIDIA Kubernetes Device Plugin。
Device Plugin运行在节点上,通过Kubernetes Device Plugin API向kubelet注册设备资源。Kubernetes官方的Device Plugin机制就是为了让GPU、FPGA、RDMA等特殊硬件能够向kubelet提供设备资源。(kubernetes.io)
对于NVIDIA GPU,集群中通常可以看到类似:
nvidia.com/gpu: 8
这样的扩展资源。
这里需要特别区分:nvidia.com/gpu不是简单意义上的NodeLabel,而是Kubernetes的扩展资源名称。
标签可以用来描述“这台机器是什么GPU”,扩展资源则用来表达“这台机器有多少GPU资源可以分配”。
二、Device Plugin到底向谁注册
原文把Device Plugin说成“通过CRI向调度器注册GPU”,这个过程不准确。
Device Plugin首先与节点上的kubelet进行通信。
kubelet负责与Device Plugin建立设备管理关系,Device Plugin向kubelet提供设备列表以及设备健康状态等信息。
随后kubelet将这些设备资源反映到节点资源信息中。
Kubernetes Scheduler看到的,是Node层面的可调度资源。
所以整个关系可以简化为:
NVIDIA GPU → NVIDIA驱动 → Device Plugin → kubelet → Node资源 → Scheduler。
Scheduler并不是直接调用NVIDIA驱动去扫描GPU。
三、Pod到底怎样提出GPU需求
用户在Pod或者Deployment中声明GPU资源。
例如:
resources:
limits:
nvidia.com/gpu: 1
对于GPU这类扩展资源,Kubernetes通常通过limits声明资源需求,并由调度器根据节点的可分配资源进行调度。Kubernetes官方GPU调度文档也采用这种资源请求方式。(kubernetes.io)
如果某个节点还有足够的GPU资源,调度器就可以把Pod安排到这个节点。
这里最重要的一点是:
Scheduler首先决定“Pod应该去哪个节点”,而不是在调度阶段直接把物理GPU 0、GPU 1或者GPU 7插入Pod。
四、Scheduler到底知道什么
假设节点A拥有8个GPU,已经有6个GPU资源被其他Pod分配,剩下2个可分配GPU。
一个只请求1张GPU的新Pod,可以满足资源条件。
如果新Pod请求4张GPU,那么这个节点就不满足资源条件。
Scheduler根据Node上的资源信息以及Pod的调度约束进行筛选和打分,然后选择目标节点。
除了GPU数量,还可以进一步使用nodeSelector、nodeAffinity、taints和tolerations等机制选择特定GPU节点。
例如一个模型只支持某种GPU,可以给节点添加描述GPU型号的标签,再让Pod通过Node Affinity选择对应节点。
但这里仍然要注意:
Node Label负责选择节点,nvidia.com/gpu等扩展资源负责表达GPU资源需求。
两者不能混为一谈。
五、什么时候才真正决定“给Pod哪一块GPU”
这是GPU调度流程中非常容易被忽略的一步。
Scheduler选定节点以后,Pod还没有直接拿到某一个 /dev/nvidiaX。
Pod会被发送到目标节点,由kubelet负责后续启动流程。
kubelet根据Pod的资源请求与已经注册的Device Plugin进行设备分配。
Device Plugin的Allocate流程会返回该Pod运行所需要的设备信息和相关配置。
然后kubelet把这些信息交给容器运行时。
这时候才真正进入“设备注入容器”的阶段。
所以应该把两个动作分开:
Scheduler:决定Pod去哪个节点。
Device Plugin/kubelet:在这个节点上决定把哪些GPU设备提供给这个Pod。
六、容器运行时如何把GPU交给Pod
当kubelet要求启动容器时,它会通过CRI与容器运行时通信,例如containerd或CRI-O。
随后GPU相关设备和运行环境需要被正确提供给容器。
在NVIDIA环境中,NVIDIA Container Toolkit可以与容器运行时集成,把容器访问NVIDIA GPU所需要的设备和用户态组件配置进去。
最终容器里面可能可以看到类似:
/dev/nvidia0
/dev/nvidiactl
/dev/nvidia-uvm
等设备节点,具体节点取决于驱动和运行环境。
这时候容器里的CUDA应用才能通过NVIDIA驱动访问真实GPU。
因此完整链路可以理解成:
Pod请求GPU → Scheduler选择Node → kubelet处理Pod → Device Plugin分配设备 → 容器运行时创建容器 → NVIDIA Container Toolkit提供GPU运行环境 → CUDA程序访问NVIDIA驱动 → GPU执行计算。
七、为什么不是简单地用cgroups限制GPU
原文把GPU分配和cgroups直接联系起来,这容易造成误解。
Linux cgroups非常适合管理CPU和内存等资源,但普通NVIDIA GPU整卡分配并不是简单地通过“给容器设置一个GPU cgroup百分比”实现。
标准Kubernetes GPU extended resource通常代表一个可分配的设备。
例如:
nvidia.com/gpu: 1
通常意味着这个Pod请求一个GPU设备。
如果希望多个工作负载共享一张物理GPU,则需要其他机制。
NVIDIA环境中可以采用MIG、Time-Slicing、MPS等不同方案。不同方案提供的资源隔离能力也不同。
因此:
GPU设备分配 ≠ GPU利用率限制。
GPU可见性 ≠ GPU计算资源隔离。
这两个概念必须分开。
八、如果我要“指定某一种GPU”怎么办
很多AI服务器并不是所有GPU都一样。
一台机器可能同时存在不同型号的GPU,或者不同GPU具有不同的MIG配置。
这时候可以先给节点添加GPU型号等标签。
例如通过节点标签表达:
gpu-model=A100
或者:
gpu-model=H100
Pod再通过Node Affinity选择满足条件的节点。
这样解决的是“我要什么类型的GPU节点”。
如果问题变成“我要节点上的某一块物理GPU”,情况就不同了。
标准Kubernetes调度模型通常不会让用户直接写:
“我要GPU 3。”
设备编号属于节点内部设备标识,而不是普通Scheduler资源选择接口。
具体哪一个设备分配给Pod,通常由Device Plugin完成。
这也是GPU设备插件存在的重要原因之一。
九、为什么GPU编号可能让人产生误解
假设服务器有:
GPU 0
GPU 1
GPU 2
GPU 3。
Pod A请求一张GPU。
Device Plugin可能分配其中一块设备,并通过容器环境让应用看到相应GPU。
在容器内部,应用看到的CUDA设备编号甚至不一定等于宿主机物理编号。
NVIDIA运行环境可以通过GPU可见性配置改变容器能够看到的设备集合。
因此,“容器里的cuda:0”并不一定意味着它就是宿主机上的物理GPU 0。
排查GPU任务时,必须区分:
宿主机物理GPU编号;
Device Plugin分配的设备;
容器内CUDA看到的逻辑设备编号。
十、Kubernetes为什么不直接根据GPU利用率调度
这是另一个经常被误解的问题。
假设某张GPU当前利用率只有10%,另一张GPU利用率90%。
不能因此就认为Kubernetes会自动把新Pod安排到10%那张GPU。
普通GPU资源调度主要依据的是Kubernetes记录的可分配GPU资源,而不是实时GPU utilization。
例如一张GPU已经被一个Pod分配,即使这个Pod实际上只使用了20%的计算能力,对于普通整卡extended resource模型来说,它仍然可能被认为已经被占用。
如果企业希望根据GPU利用率、显存、队列长度或者tokens/s动态调整服务,就需要额外的监控、指标和调度机制。
这属于GPU-aware scheduling或autoscaling的进一步问题,而不是标准GPU Device Plugin自动完成的功能。
十一、GPU资源碎片化为什么比较特殊
CPU可以比较自然地切成多个核心或者CPU时间片。
普通整卡GPU则不同。
假设服务器有4张GPU。
一个任务需要4张GPU,另一个任务需要1张GPU。
如果4张GPU已经被三个不同任务以不合理方式占用,即使总剩余GPU数量看起来足够,也可能无法满足一个要求特定数量GPU并且具有拓扑要求的任务。
多GPU训练还会进一步考虑GPU之间的通信拓扑。
例如4张GPU虽然数量足够,但如果它们之间的NVLink或者PCIe拓扑不理想,NCCL通信性能可能受到影响。
因此大型AI集群不能只看:
GPU还有几张。
还需要看:
是什么型号的GPU;
是否支持MIG;
GPU之间如何互联;
GPU与NIC如何连接;
NUMA如何布局;
任务是否要求多GPU协同。
十二、Pod删除以后GPU发生了什么
当Pod结束生命周期时,kubelet负责处理容器生命周期,并通过设备管理机制完成相应的资源释放。
之后GPU资源重新成为节点上的可分配资源。
这并不是简单地由Device Plugin维护一个“GPU使用次数”然后减一。
整个生命周期涉及:
Scheduler的资源状态;
kubelet;
Device Plugin;
容器运行时;
GPU设备状态。
如果某个组件异常,例如设备插件失联、节点状态不一致或者GPU发生错误,就可能出现资源状态与实际硬件状态不一致的情况。
这也是为什么GPU节点通常需要持续监控,而不能只依靠Scheduler判断GPU健康程度。
十三、真正复杂的是GPU拓扑感知
对于普通推理任务,一张GPU可能就够了。
对于大模型训练,一个Pod可能同时请求8张GPU。
这时候“8张GPU”只是第一步。
还需要考虑这8张GPU是否位于同一个高速互联域,GPU之间是否具有良好的NVLink/NVSwitch连接,NIC是否靠近相应GPU,以及CPU和内存的NUMA布局是否合理。
否则Scheduler即使成功找到8张GPU,也可能得到一个通信性能并不理想的组合。
因此现代AI基础设施越来越重视Topology Manager、GPU device plugin、MIG配置以及更高级的GPU-aware调度方案。
Kubernetes本身提供拓扑管理机制,而具体GPU厂商和设备插件还需要提供相应的拓扑信息。
十四、从一条Pod命令看完整过程
假设用户提交一个需要一张NVIDIA GPU的Pod。
第一步,API Server接收到Pod对象。
第二步,Scheduler读取Pod资源需求,看到它请求:
nvidia.com/gpu: 1
第三步,Scheduler检查各节点的可分配资源和调度约束。
第四步,Scheduler选择满足条件的GPU节点。
第五步,kubelet发现这个Pod被分配到本节点。
第六步,kubelet根据Pod的GPU请求与Device Plugin进行设备分配。
第七步,Device Plugin返回对应GPU设备信息。
第八步,kubelet通过CRI要求containerd或CRI-O创建容器。
第九步,NVIDIA Container Toolkit等运行时组件把GPU设备和相关用户态运行环境配置给容器。
第十步,容器中的CUDA程序启动,通过NVIDIA驱动访问真实GPU。
这时候,一个看似简单的:
nvidia.com/gpu: 1
实际上已经经过了一整套资源管理链。
GPU进入Kubernetes以后,真正发生的变化不是“Scheduler学会了识别显卡”,而是GPU被转换成了Kubernetes能够管理的一类扩展资源。
Device Plugin负责告诉kubelet“这个节点有哪些GPU设备可以提供”,Scheduler负责决定“Pod应该去哪个节点”,kubelet和Device Plugin再完成节点上的设备分配,容器运行时负责最终创建容器环境,NVIDIA Container Toolkit则把容器与宿主机GPU驱动连接起来。
到了大型AI集群,问题又从“一张GPU能不能用”进一步发展成“什么型号的GPU、几张GPU、怎样共享、怎样保持拓扑、怎样扩容以及怎样保证GPU之间的通信效率”。
这也是Kubernetes进入AI基础设施以后真正有技术含量的地方:它并没有把GPU变成普通CPU那样简单的资源,而是在容器编排系统中建立了一套从物理GPU到集群调度,再到容器进程的完整资源管理链。
Kubernetes 管理 GPU 节点时发生了什么,Pod 如何获得指定 GPU 资源
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP