异构节点、GPU 与加速器放置
训练任务申请了 vendor.example/gpu: 1,集群也有四块空闲卡,Pod 却持续 Pending。节点上的驱动已经安装,但 Device Plugin 没有成功向 kubelet 注册,Node 的 status.allocatable 根本没有该扩展资源。扩容系统看到的是“缺少一种资源”,不是“CPU 节点不够”,继续增加通用节点只会制造更多账面容量。
另一支团队解决 Pending 后又遇到更隐蔽的问题:任务确实拿到一块 GPU,却落到了显存不足、指令集不兼容或拓扑距离很差的型号上。一个整数扩展资源只回答“有几份”,不能完整表达型号、显存、NUMA、互联、切片和共享方式。节点标签能补充粗粒度事实,但标签、设备健康与真实驱动状态一旦漂移,调度成功仍可能在容器启动或训练过程中失败。
先区分节点可调度、设备可分配与应用可运行
异构放置至少有三层成功。第一层是节点满足 Pod 的 CPU、内存、标签、污点、卷和拓扑约束;第二层是设备管理路径能够分配健康设备;第三层是容器内驱动、运行库、设备节点和应用框架真正兼容。PodScheduled=True 只证明前两层的调度决策完成,不能证明 CUDA、ROCm、FPGA bitstream 或高速网卡初始化成功。
传统 Device Plugin 框架自 Kubernetes 1.26 起为稳定能力,但 kubelet 与插件之间的 Device Plugin API 本身仍未标为稳定,升级前仍要核对插件支持的 API 与 kubelet 版本。厂商插件以 DaemonSet 运行,在 /var/lib/kubelet/device-plugins/kubelet.sock 注册 vendor-domain/resource,通过 ListAndWatch 报告设备与健康,kubelet 再把设备总数与健康可分配数写入 Node capacity/allocatable。Pod 在容器 resources 中请求整数扩展资源;扩展资源不可超卖,也不能用小数表达。官方 Device Plugin 文档描述了注册、健康更新与 Allocate 调用链,GPU 调度任务页则给出厂商驱动与插件的启用入口。
Dynamic Resource Allocation(DRA)在 Kubernetes 1.35 达到稳定基础能力。驱动通过 ResourceSlice 发布设备及属性,集群管理员定义 DeviceClass,工作负载通过 ResourceClaim 或模板请求设备,scheduler 在分配后选择能够访问该设备的节点。DRA 适合属性筛选、共享、网络设备、可分区设备及更丰富的分配语义;它不是“自动让所有旧 GPU 插件升级”,具体驱动支持和 feature gate 必须逐项核对。DRA 概念页给出了稳定与仍处于 beta/alpha 的能力边界。
驱动、Device Plugin 与节点准备必须按顺序启用
节点镜像先安装与内核、固件和容器运行时兼容的厂商驱动,再部署对应 Device Plugin。插件通常需要访问宿主机设备目录、驱动库或 kubelet socket,属于高权限 DaemonSet;不要从未知仓库复制 privileged 清单。安装前记录节点架构、内核、驱动、运行时与 Kubernetes 版本:
kubectl version
kubectl get nodes -o wide
kubectl describe node <accelerator-node> | grep -A8 -E 'Capacity:|Allocatable:'
kubectl auth can-i create daemonsets -n kube-system
kubectl auth can-i patch nodes厂商插件应使用其官方 chart、Operator 或固定 manifest,并锁定镜像 digest。安装后先看 DaemonSet 是否只落到目标硬件节点,再看日志中的注册与设备发现:
kubectl -n kube-system get daemonset,pod -l app.kubernetes.io/component=device-plugin -o wide
kubectl -n kube-system logs daemonset/<device-plugin> --tail=200
kubectl get node <accelerator-node> -o jsonpath='{.status.capacity.vendor\.example/gpu}{"\n"}'
kubectl get node <accelerator-node> -o jsonpath='{.status.allocatable.vendor\.example/gpu}{"\n"}'示例资源名 vendor.example/gpu 必须替换为厂商真实资源名。capacity 表示节点报告的总量,allocatable 是可供新 Pod 请求的数量;插件把设备标记为 unhealthy 后,allocatable 会下降,但已有 Pod 不一定自动迁移。容器运行时还需配置对应 runtime、CDI 或厂商 hook;Node 显示 GPU 不代表容器已经能访问驱动库。
本地单节点集群若没有真实设备,只适合验证“资源不存在导致 Pending”,不能伪造设备成功路径。云托管集群还要确认加速器节点镜像、驱动安装方式、节点池污点和云配额;创建节点会产生费用,实验结束必须删除节点池或缩容到零。
标签、污点与扩展资源组成粗粒度放置契约
整数资源负责“需要几份”,标签负责“需要哪类节点”,污点负责“哪些工作负载可以进入昂贵节点”。例如:
apiVersion: v1
kind: Pod
metadata:
name: accelerator-demo
namespace: accelerator-lab
spec:
restartPolicy: Never
nodeSelector:
accelerator.example.com/family: compute-x
kubernetes.io/arch: amd64
tolerations:
- key: accelerator.example.com/dedicated
operator: Equal
value: gpu
effect: NoSchedule
containers:
- name: probe
image: <vendor-runtime-image>
resources:
requests:
cpu: "1"
memory: 2Gi
limits:
vendor.example/gpu: "1"
command: ["sh", "-c", "<vendor-device-query-command>"]对扩展资源,设备数量通常写在 limits,Kubernetes 会把 request 视为相同值;不能设置 request 小于 limit,也不能申请 0.5 块设备。CPU 与内存 requests 仍然重要:GPU 空闲而 CPU、内存、临时盘或 Pod 数上限不足,Pod 一样 Pending。
标签必须由可信的节点发现组件或供应系统维护。业务用户不应拥有 Node patch 权限,否则可以伪造型号、区域或合规标签。通用工作负载应被 NoSchedule 污点挡在加速器池外,目标 Pod 同时带 toleration 与 required node affinity,形成双向契约。toleration 只表示允许进入,不会把 Pod 吸引到 GPU 节点。
DRA 用设备属性与 Claim 表达更细的分配意图
DRA 将“设备是什么”和“谁占用它”从 Node 整数计数中拆出来。驱动发布 ResourceSlice,其中包含节点可访问设备、属性和容量;管理员创建 DeviceClass,用 CEL selector 表达可接受设备;namespace 中的 ResourceClaim 或 ResourceClaimTemplate 记录请求与分配结果。scheduler 负责把 Claim 分配与 Pod 放置协调起来,kubelet 与驱动在目标节点准备设备。
一个概念化的 DeviceClass 可以按驱动和设备属性筛选:
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
name: training-gpu
spec:
selectors:
- cel:
expression: >-
device.driver == "gpu.example.com" &&
device.attributes["gpu.example.com"].memoryGiB >= 40字段与属性命名由实际驱动决定,不能把示例直接用于生产。查看集群是否提供 DRA API、DeviceClass 与 ResourceSlice:
kubectl api-resources | grep -E 'deviceclasses|resourceclaims|resourceslices'
kubectl get deviceclass
kubectl get resourceslice -o wide
kubectl -n accelerator-lab get resourceclaim -o yamlResourceClaim 的 status 能显示 allocation 与 reservedFor;驱动实现对应状态上报时,还能在 status.devices 提供设备状态,因此它是排障的核心对象,但不能把驱动可选状态当成必有事实。普通租户只能创建经批准的 Claim,不应修改 DeviceClass 或 ResourceSlice。DRA 的 adminAccess 可访问在用设备,属于维护特权;Kubernetes 1.36 要求调用者既有创建 Claim/ClaimTemplate 的权限,目标 namespace 又带精确标签 resource.kubernetes.io/admin-access: "true",绝不能把这个标签和权限授予普通训练任务。Kubernetes 1.36 的设备健康状态、设备污点、可分区设备和扩展资源兼容处在不同成熟度并受不同 feature gate 约束,启用前必须逐项确认默认状态与驱动实现。
正向实验:证明资源申请、落点与容器可见性一致
选择一台非生产加速器节点,确认 allocatable 至少为一,并创建隔离 namespace:
kubectl create namespace accelerator-lab
kubectl get nodes -L accelerator.example.com/family
kubectl describe node <accelerator-node> | grep -A12 Allocatable把上一节 Pod 保存为 accelerator-demo.yaml,替换厂商镜像、资源名和查询命令后应用:
kubectl apply -f accelerator-demo.yaml
kubectl -n accelerator-lab get pod accelerator-demo -w
kubectl -n accelerator-lab describe pod accelerator-demo
kubectl -n accelerator-lab logs accelerator-demo
kubectl -n accelerator-lab get pod accelerator-demo \
-o jsonpath='{.spec.nodeName}{"\t"}{.status.phase}{"\n"}'预期 Pod 只落到目标 family 节点,PodScheduled=True,容器退出码为零,日志中的设备型号和数量与请求一致。运行期间再次读取 Node allocatable 可能仍显示节点总可分配上限;“已分配多少”应结合 Pod resources、kubelet PodResources API、厂商指标或 DRA Claim status 判断,不能简单用 capacity 减 allocatable 推导占用。
DRA 路径还要记录 ResourceClaim.status.allocation、reservedFor、关联 Pod UID 与目标节点;Device Plugin 路径要记录 Node 扩展资源、Pod limit、插件日志和容器内查询。四层证据一致,才能排除“调度到 GPU 节点但设备未注入”这类假成功。
反向实验:制造错误型号与资源不存在两种失败
第一种反例把资源名改成集群从未公布的 vendor.example/missing-gpu:
kubectl -n accelerator-lab run missing-device \
--image=registry.k8s.io/pause:3.10 \
--overrides='{"apiVersion":"v1","spec":{"containers":[{"name":"pause","image":"registry.k8s.io/pause:3.10","resources":{"limits":{"vendor.example/missing-gpu":"1"}}}]}}'
kubectl -n accelerator-lab describe pod missing-device
kubectl -n accelerator-lab get events --field-selector involvedObject.name=missing-device --sort-by=.lastTimestamp预期 spec.nodeName 为空、PodScheduled=False,FailedScheduling 指向不足的扩展资源。新增 CPU 节点不会改变结论;修复路径是恢复插件注册、改正资源名或供给能公布该资源的节点。
第二种反例请求真实 GPU,却要求一个不存在的型号标签。Pod 的调度字段不可原地改写,因此创建一个独立反例对象:
kubectl -n accelerator-lab run wrong-family \
--image=registry.k8s.io/pause:3.10 \
--overrides='{"apiVersion":"v1","spec":{"nodeSelector":{"accelerator.example.com/family":"does-not-exist"},"containers":[{"name":"pause","image":"registry.k8s.io/pause:3.10","resources":{"limits":{"vendor.example/gpu":"1"}}}]}}'
kubectl -n accelerator-lab describe pod wrong-family预期事件显示标签不匹配或资源不足;这证明调度求的是 CPU、内存、设备、标签、污点与拓扑的交集。若插件在运行后把设备标为 unhealthy,Node allocatable 会下降而 capacity 保持不变;已绑定 Pod 不会因此自动迁移,应用可能 CrashLoop 或任务失败。Kubernetes 1.36 中 ResourceHealthStatus 为 beta 且默认启用,可从容器状态的 allocatedResourcesStatus 获取设备健康线索;若集群显式关闭该 gate、版本更旧或驱动没有上报,字段缺失不能反推设备健康。
Pending、启动失败与运行中故障要按阶段分型
Pending 且没有 spec.nodeName,先查 scheduler Events、Node allocatable、标签、污点、requests、卷拓扑和 DRA Claim allocation。已经绑定但卡在 ContainerCreating,重点查 kubelet、Device Plugin Allocate、CDI/运行时、设备节点权限和镜像拉取。容器已启动却退出,则检查驱动与用户态库兼容、设备型号、显存、固件、应用框架和厂商健康日志。
kubectl -n accelerator-lab get pod <pod> -o yaml
kubectl -n accelerator-lab describe pod <pod>
kubectl describe node <node>
kubectl -n kube-system logs daemonset/<device-plugin> --since=10m
kubectl get events -A --sort-by=.lastTimestamp | grep -Ei 'FailedScheduling|Allocate|device'错误资源名和资源耗尽都可能显示 insufficient extended resource,但前者在所有节点 capacity 中都不存在,后者至少有节点公布该资源且已被其他 Pod 请求。标签错误表现为候选节点被硬约束过滤。设备注入失败发生在 Bind 之后,通常不能靠扩容修复。DRA 则要增加 Claim 是否 allocated、reservedFor 是否包含当前 Pod、ResourceSlice 是否仍公布目标设备这条判断链。
监控至少关联 Node 设备 capacity/allocatable、Pending 原因、Claim 分配延迟、插件重启、设备 unhealthy、容器启动延迟、任务失败和设备利用率。单看 GPU utilization 会漏掉已申请未使用、初始化过慢和型号碎片;单看 requests 又无法说明任务真的在计算。
碎片、共享与 NUMA 让“一块 GPU”不是统一容量单位
不同型号、显存、切片规格和互联拓扑不能简单相加。两台各剩一块卡,不一定能承载需要同机双卡高速互联的任务;一张大卡切成多个实例后,剩余切片形状可能无法满足新请求。CPU、内存、HugePages、本地盘和网卡也要与 GPU 同时装箱,任何维度都可能成为碎片瓶颈。
传统扩展资源只计整数,型号通常依赖 label/affinity,多卡拓扑依赖厂商调度扩展、DRA 属性或批调度器。时间切片、MPS、MIG 等共享机制的隔离、故障域、显存边界和计费语义各不相同;“一个资源单位”必须在平台目录中写明它代表整卡、切片还是共享份额。否则 requests、配额和账单无法比较。
容量模型应按可替代设备类统计:可行节点数、可用设备形状、最大可并行任务、队列等待、冷启动、失败率、装箱碎片和跨区供给。预留昂贵节点可降低启动时延但提高空闲成本,按需供给降低空闲成本却受到云配额、库存、驱动启动和镜像下载影响。Spot 设备还要把训练检查点与中断恢复时间算进有效成本。
选 Device Plugin、DRA 还是厂商调度扩展
工作负载只需要“某类设备的整数数量”,厂商 Device Plugin 成熟、节点池型号单一时,扩展资源加可信标签最简单,生态兼容也最好。需要按显存、固件、网络能力等属性筛选,设备可共享或可跨节点访问,或者希望通过 Claim 管理生命周期时,评估 DRA 驱动更合适。驱动是否生产可用比 Kubernetes API 的成熟度更关键。
需要 gang scheduling、队列、租户配额或多 Pod 同时获得设备时,引入 Kueue、Volcano 等批处理能力,但它们不能替代设备发现和注入。Node Feature Discovery 可以维护硬件标签,却不能分配设备。厂商 Operator 可以安装驱动、runtime 和插件,却不自动解决业务 requests、PDB、队列与成本治理。
选择标准应包括 Kubernetes 与驱动兼容矩阵、设备型号覆盖、升级路径、健康上报、拓扑表达、共享隔离、多租户安全、可观测性、云与本地可移植性、供应商支持和退出成本。不要因为 DRA 能力更丰富就同时让同一设备被两个驱动路径重复公布;字段所有权和设备唯一身份必须明确。
权限、镜像与设备数据都属于高敏感边界
Device Plugin 通常挂载 kubelet socket 与宿主机设备目录,可能运行 privileged;被攻破后影响整台节点。只允许平台身份部署,镜像固定 digest 并做签名与漏洞验证,ServiceAccount 不授予无关 Secret 和工作负载写权限。节点驱动安装器与业务容器身份分离。
DRA 的 DeviceClass 和 ResourceSlice 属于集群级供给事实,普通 namespace 用户只创建受策略约束的 Claim。adminAccess 能接触在用设备,应由独立维护身份按工单临时获得,并审计创建、批准、使用和撤销。云节点供应器使用工作负载身份获取最小的实例、网络与标签权限,不在 DaemonSet 环境变量中保存长期 AK/SK。
设备序列号、PCI 地址、节点名、区域、任务标签、模型名和显存用量可能暴露资产与业务信息。日志、海报、工单和指标导出时使用脱敏节点与任务标识。训练数据凭证通过 Secret 或外部密钥系统注入,不能因为容器需要 GPU 就给它宿主机文件系统或云管理员权限。
升级、回滚和退出先保护正在使用的设备
驱动、固件、Device Plugin、容器运行时、Kubernetes 和用户态库组成兼容矩阵。升级先建 canary 节点池,cordon 后安装新栈,运行设备查询、最小训练、故障注入和性能基线,再允许少量业务进入。不要在有长任务的节点上原地重启驱动;先按应用的检查点能力 drain,并遵守 PDB 与任务队列语义。
kubectl cordon <canary-node>
kubectl get pods -A --field-selector spec.nodeName=<canary-node>
kubectl drain <canary-node> --ignore-daemonsets --delete-emptydir-data
# 由节点镜像或厂商 Operator 升级驱动与插件
kubectl uncordon <canary-node>回滚必须恢复整套兼容组合,不只回退插件镜像。若 Node 已公布新资源名或新标签,先让工作负载同时接受旧、新契约,再迁移模板;回退前确认旧版本能读取现有对象。DRA 迁移应并行建立新 DeviceClass 和 canary Claim,验证资源不会被 Device Plugin 与 DRA 双重计算,然后逐批切换。
退出某节点池时,先停止新任务准入,等待或中断可恢复任务,撤销标签与设备类供给,确认 ResourceClaim、Pod、卷和云实例都归零,再卸载插件与驱动。实验清理示例:
kubectl delete namespace accelerator-lab --wait=true
kubectl get resourceclaim -A
kubectl get pods -A -o json | grep -F 'vendor.example/gpu' || true
kubectl get nodes -L accelerator.example.com/familynamespace 删除会级联清理由实验 Pod 模板生成的 namespaced ResourceClaim,但不会删除集群级 DeviceClass、厂商驱动或节点标签。若实验确实创建了专用 DeviceClass,应先确认没有其他 Claim 引用,再单独执行 kubectl delete deviceclass <lab-device-class>;共享 DeviceClass、ResourceSlice 和 Device Plugin 必须由平台 owner 按驱动退役流程处理,不能作为文章实验清理的一部分顺手删除。
平台团队拥有驱动、Device Plugin/DRA 与节点标签;应用团队拥有设备请求、运行库和检查点;容量团队拥有节点池、配额和碎片模型;安全团队拥有 privileged 例外与 admin access;财务 owner 依据设备类而不是笼统“GPU 小时”核算。每个设备类都要有兼容矩阵、健康判据、容量单位、升级窗口和退役动作,才能避免昂贵而不可解释的异构孤岛。
