一句话结论:Device Plugin 的本质,是把「让 Kubernetes 认识一种新硬件」这件事,从改内核代码降维成了写一个 gRPC 服务。而它的边界与局限——只能表达整数、调度决策黑盒、无拓扑感知、无故障协同——恰好勾勒出了大模型时代算力调度的完整问题域。

今天做 AI Infra,大概率不会从零写一个 device plugin,但一定会在这套机制的边界上做选型:训练集群选 Volcano 还是 KAI、推理集群选 HAMi 还是 MIG、要不要切 DRA、边缘具身用 KubeEdge 还是裸 K8s。

本文的机制描述逐条核对自 Kubernetes 官方文档与 proto 源码、NVIDIA/昇腾/HAMi/KubeEdge 各项目仓库。核对中发现原稿有十余处需要更正,其中两处是硬伤,已在文中以 ⚠️ 标注。


一、设计动机:为什么要有 Device Plugin

Kubernetes 原生只认识 CPU、内存和本地存储。让每一种硬件的枚举、健康检查、分配逻辑都进 kubelet 主干,在工程上是不可持续的——每出一个新硬件就要改一次核心代码。

设计提案里说得很直接:我们需要一个厂商无关的插件化方案,让硬件厂商在不写 Kubernetes 代码的前提下,把自己的资源报给 kubelet,并监控其健康状态。

这条路在 2017 年走通了第一程。三个版本节点的官方记录:

版本 事件 CHANGELOG 原文
1.8 进入 alpha [alpha] Add the Hardware Device Plugins API.
1.10 升 beta,默认开启 The DevicePlugins feature has graduated to beta, and is now enabled by default
1.11 移除内置 GPU 路径 Support for the alpha.kubernetes.io/nvidia-gpu resource, which was deprecated in 1.10, has been removed. Please use the resource exposed by DevicePlugins instead (nvidia.com/gpu).

此后近十年,nvidia.com/gpu 这种扩展资源成了 GPU 集群的事实标准接口。

而 DRA(Dynamic Resource Allocation)在 1.34 正式 GA,并从 1.35 起特性门控锁定(locked,无法再关闭)。官方 Feature Gates 表的原话是:

You can no longer disable or opt out of this feature or behavior (it is locked)

两条技术路线的交接,是这篇文章后半段的主线。

⚠️ 一处引用要改:原稿引了一句「官方文档的表述是 ‘DRA replaces device plugins for accessing resources from workloads’」。这句话在 Kubernetes 官方文档里查无出处(DRA 概念页、dra-api、dra-features、Device Plugins 页、set-up-dra-cluster 全文检索均未命中)。

最接近的两处官方原文是:

  • WG Device Management 官方博客:“The working group’s primary deliverable is Dynamic Resource Allocation (DRA), a new framework that replaces the rigid device plugin model with a flexible, declarative API.”
  • DRA 概念页:“These benefits provide significant improvements in the device allocation workflow when compared to device plugins, which require per-container device requests, don’t support device sharing, and don’t support expression-based device filtering.”

引用时用这两句之一。


二、机制解析:kubelet 与插件之间的 gRPC 握手

Device Plugin 的运行模型非常克制:每个插件是一个部署在节点上的 DaemonSet 进程,通过 Unix socket 与 kubelet 通信,对外暴露一组 gRPC 接口。

2.1 五个方法,不是四个

⚠️ 更正(硬伤之一):原稿写「核心只有四个方法」,实际 proto 里 service DevicePlugin 有 5 个 rpc,漏掉的是 PreStartContainer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
service DevicePlugin {
// 注册时告知 kubelet 支持哪些可选能力
rpc GetDevicePluginOptions(Empty) returns (DevicePluginOptions) {}
// 持续上报设备列表与健康状态(服务端流式 RPC)
rpc ListAndWatch(Empty) returns (stream ListAndWatchResponse) {}
// 可选:向 devicemanager 返回建议的优先分配集合
rpc GetPreferredAllocation(PreferredAllocationRequest)
returns (PreferredAllocationResponse) {}
// 容器创建时被 kubelet 调用,返回设备节点、环境变量、挂载
rpc Allocate(AllocateRequest) returns (AllocateResponse) {}
// 可选:容器启动前调用(由 DevicePluginOptions.pre_start_required 决定)
rpc PreStartContainer(PreStartContainerRequest)
returns (PreStartContainerResponse) {}
}

另外要区分:Register 不在这个服务里——它属于另一个独立的 service Registration。官方文档也强调,GetPreferredAllocation 与 PreStartContainer 都是可选的,其可用性由 DevicePluginOptions 里的标志位声明。

2.2 注册、长连与两个容易忽略的工程细节

细节一:socket 路径是硬编码的。 官方文档原文:

under the host path /var/lib/kubelet/device-plugins/ (this path is hardcoded and is not affected by the kubelet’s --root-dir or any other configuration)

这个「不受 --root-dir 影响」是个常见的排查陷阱——改了 kubelet 的 root-dir,插件 socket 位置不会跟着变。另外 kubelet 启动时会清掉该目录下所有已存在的 socket,所以插件必须能感知 kubelet 重启并自动重新注册。

细节二:NVIDIA 插件靠 fsnotify 监听 kubelet socket 的创建事件来重注册。 源码里是这么写的:

1
2
3
4
5
6
// Detect a kubelet restart by watching for a newly created 'pluginapi.KubeletSocket' file.
case event := <-watcher.Events:
if o.kubeletSocket != "" && event.Name == o.kubeletSocket &&
event.Op&fsnotify.Create == fsnotify.Create {
klog.Infof("inotify: %s created, restarting.", o.kubeletSocket)
}

ListAndWatch 是一条服务端流式长连接(proto 里是 returns (stream ...)),插件持续把设备状态推给 kubelet。GPU 出现 ECC 错误或 Xid 掉卡,插件标记为 Unhealthy,kubelet 就把它从可分配资源里摘掉。

⚠️ 一处措辞要精确:官方原文是「the kubelet will decrease the allocatable count for this resource on the Node… Capacity count for the resource will not change」——下降的是 allocatable,capacity 不变。

所以那个经典现象「nvidia-smi 能看到卡、但 kubectl describe node 里 GPU 数量是 0」,严格说是 Allocatable: nvidia.com/gpu: 0 而 Capacity 仍是原值。

Allocate 并不真的「分配」显存,它返回的是环境变量(NVIDIA_VISIBLE_DEVICES、ASCEND_VISIBLE_DEVICES——两个名字都在各自插件的源码里确认过)、设备节点路径、挂载点和 CDI 设备名。真正的设备注入由容器运行时(配合 CDI 或 nvidia-container-toolkit / Ascend Docker Runtime)完成。

补充一句:time-slicing / MPS 下 Allocate 更不做显存隔离——这一点在第四节展开。

2.3 Bind 严格在前,Allocate 严格在后

这是一个高频疑问,而且前提里藏着一个常见误解需要先澄清:

调度器从头到尾不联系 device plugin。 它只看节点对象上的 Extended Resource 计数;真正调用插件 Allocate() 的是目标节点上的 kubelet,时机是容器创建路径中。

完整时序:

从外部可观察的状态看,这条链对应 Pod 的状态流转:Pending(未调度) → Bind → Pending(ContainerCreating) → Allocate 发生在这里 → Running。

「两个分配」不是一回事,这是最容易混淆的点:

调度器的 Reserve/Assume kubelet 的 Allocate
时机 Bind 之前,调度周期内 Bind 之后,容器创建时
性质 记账式虚拟占用(本地缓存 +1) 真实选定具体设备 ID 并锁定
粒度 只知道「数量够不够」 精确到 /dev/nvidia0 这种实例级
失败回滚 Bind 失败则 un-assume,计数 −1 失败不会解绑,Pod 留在节点上重试
谁执行 kube-scheduler kubelet → 插件 gRPC

调度器在 Filter 阶段看到的 GPU 计数,是节点 status 里 Extended Resource 的 capacity/allocatable——数据源头正是 device plugin 的 ListAndWatch 推给 kubelet、再由 kubelet 汇聚上报的。所以插件间接参与了调度,但调度决策时刻没有任何针对插件的 RPC。

2.4 三个语义细节

Allocate 是按容器粒度的。 一个 Pod 里多个容器各请求多少资源,kubelet 就分别调用多少次——不是每个 Pod 一次。

分配是粘性的。 kubelet 把(Pod UID, 容器, 资源名)→ 设备 ID 列表的映射持久化在 kubelet_internal_checkpoint。容器 crash 重启、甚至 kubelet 自身重启后,再次 Allocate 会命中已有分配、返回同一批设备——训练任务里这至关重要,因为 NCCL 通信组绑定的设备不能漂移。

Allocate 失败的后果是「卡住」而非「重调度」。 假设刚绑定完设备就变 Unhealthy:devicesToAllocate 会从健康集合里挑选,健康集合不足则报错;kubelet 记事件、按 backoff 重试,Pod 长期停在 ContainerCreating。绑定关系不会回退——要打破这种僵局得靠删除 Pod 重新调度,或者上层系统(Volcano 重调度、昇腾插件的故障自愈)介入。

这是 device plugin 模型「调度与分配割裂」的直接代价。

释放发生在 Pod 终止路径:容器停止、Pod 被删除或驱逐时,DeviceManager 才把设备 ID 归还到可分配集合并更新 checkpoint。

2.5 DRA 把时序整个反了过来

这个顺序在 DRA 下发生了根本翻转,也是理解两者差异的最直观切口:

  • Device Plugin:「先定节点,再在节点上选设备」——调度器选完节点、Bind 完成后,设备选择才在 kubelet 本地发生,调度器对「选了哪张卡」完全无感。这正是黑盒、无拓扑感知的根源。
  • DRA + 结构化参数:「选设备和选节点在同一个调度闭环」——scheduler 在调度周期内直接为 ResourceClaim 挑选具体设备并写回 ResourceClaim status,然后才 Bind。设备分配从节点本地事件升级为 API Server 上可观察、可约束的调度原语。

排障时的证据在哪找,就以此分界:

阶段 看什么
Bind 之前 调度器日志、节点 nvidia.com/gpu 计数、FailedScheduling 事件
Bind 之后 kubelet 日志(device_allocate 相关事件)、kubectl describe pod 的 Events、Error Allocating 类报错,最后才是插件自身日志

两个世界以 Bind 为界,泾渭分明。

机制部分的更深细节(注册流程源码、ListAndWatch 实现、Allocate 返回结构)见 NVIDIA device plugin for Kubernetes 原理分析,那里逐节拆过。


三、深水区:扩展资源模型的四个根本局限

理解了机制,才能看清它为什么不够用。Device Plugin 把硬件抽象成「资源名 + 整数数量」,这个模型在 AI 算力场景暴露了四个结构性问题:

第一,只能表达整数个数,无法表达分数与属性。 nvidia.com/gpu: 1 意味着独占一张卡,Pod 没法申请「半张卡」或「20GB 显存」;调度器也看不到卡的型号、显存大小、NVLink 拓扑这些属性,做不了「给我一张显存 ≥ 40GB 的卡」这类请求。

第二,调度决策对插件是黑盒。 kube-scheduler 只看节点上的 Extended Resource 数量做过滤打分,插件无法参与调度决策。

⚠️ 更正(硬伤之二):原稿在这里举的例子是「NVIDIA 插件 time-slicing 配了 replicas: 4 但节点 capacity 仍显示 1,因为 kubelet 只把原始设备数上报为 capacity」。这个论据是反的。

NVIDIA 官方文档明确写着:

On a machine with one GPU, the following config map configures Kubernetes so that the node advertises four GPU resources. A machine with two GPUs advertises eight GPUs, and so on.

也就是说 replicas: 4 会让单卡节点的 capacity/allocatable 变成 4(4 张物理卡就是 16)。

这个更正其实让论点更强——time-slicing 恰恰是插件通过 ListAndWatch 虚增设备数来影响调度的正面例子:kubelet 上报的就是插件通告的数量,插件想报几个就报几个。正确的表述是「time-slicing 靠虚增设备数来实现超卖」,而不是「kubelet 不理插件」。

真正的黑盒在于:插件通告的是数量,不是属性——它能告诉你「有 4 个 GPU」,但没法告诉你「这 4 个是同一张物理卡的时间片,共享显存和故障域」。

第三,没有拓扑感知。 八卡训练任务被调度到两台没有高速互联的机器上,通信效率直接塌方——原生调度器对 NVLink、灵衢总线、RDMA 拓扑一无所知。

第四,无故障协同与重调度语义。 设备坏了只能从可用列表里摘掉,正在跑的 Pod 怎么办?训练任务怎么恢复?这些都需要插件和上层调度系统自己发明约定。

所有后续演进——DRA、GPU Operator、Volcano、HAMi——本质上都是在不同层面补这四个洞。


四、演进:DRA 用 ResourceClaim 重写语义

Dynamic Resource Allocation 是对上述局限的正面回答。它在 Pod 规格里引入了新的原语:ResourceClaim / ResourceClaimTemplate(声明「我要什么」而不是「我要几个」,可带属性选择器)、DeviceClass(厂商预定义的设备模板)、ResourceSlice(DRA 驱动上报设备用),以及配套的 DRA Driver(kubelet 插件 + scheduler 插件)。

最关键的进步是 Structured Parameters(1.32 beta):设备能力以结构化属性暴露给调度器,让 kube-scheduler 能自己做属性过滤和拓扑对齐,而不是把决策黑盒地交给某个节点上的驱动。

⚠️ 两处原语更正:

  1. 原稿把 ResourceClass 列进了 DRA 原语清单——它已经过时。ResourceClass 是 classic DRA(KEP-3063,1.26–1.31) 的集群级 class;structured-parameters DRA(KEP-4381)改用了 DeviceClass。当前官方 API 参考里只有 DeviceClass。
  2. 清单里应补上 ResourceSlice。

DRA 的版本脉络要分两段说(这是很容易搞混的地方):

阶段 设计 KEP 版本
classic DRA PodSchedulingContext 协商模型 KEP-3063 alpha 1.26,1.32 已 withdrawn、代码移除
structured parameters DRA 结构化参数 + DeviceClass KEP-4381 alpha 1.30—1.31,beta 1.32,stable 1.34

KEP-3063 的 README 自己说明了这个角色反转:

Originally, this KEP introduced DRA in Kubernetes 1.26 and the “structured parameters” KEP … added an extension. Now the roles are reversed: #4381 defines the base functionality and this KEP is an optional extension.

两者用的是同一个 feature gate 名 DynamicResourceAllocation,所以只看 gate 名会以为是一条线——实际底下换过一轮设计。

Device Plugin vs DRA 对照:

维度 Device Plugin DRA
引入版本 1.8 alpha(2017),1.26 stable 1.26 alpha(classic)/ 1.30 alpha(structured)→ 1.34 GA
资源模型 Extended Resource,整数计数 ResourceClaim,可带属性选择器
调度参与 kube-scheduler 只看数量,插件不参与 驱动提供结构化参数,调度器原生过滤
分数/属性 不支持 原生支持(按显存、MIG profile 请求)
设备配置 Allocate 返回 env/mount NodePrepareResources 等生命周期钩子
设备选择时机 Bind 之后,节点本地 Bind 之前,调度周期内
共享与拓扑 需借助外部项目(HAMi、Volcano) 机制内建
生态成熟度 NVIDIA/昇腾/网络插件全面支持 NVIDIA DRA driver 已可用,部分能力 alpha

现状是过渡态:NVIDIA GPU Operator 支持 DRA Driver 与 Device Plugin 二选一部署,官方原文是「A cluster can have either a GPUCluster resource for DRA or a ClusterPolicy resource for the Device Plugin, but not both」;昇腾的 Ascend Device Plugin 文档标注支持 Kubernetes 1.17.x—1.36.x,处于跟随 DRA 演进的节奏上。

⚠️ 一处时间线要改:原稿写「1.34 GA 后还补上了 Pod 级的资源健康状态上报(同时覆盖 DRA 和 Device Plugin 两种路径)」。实际是——该能力最早(1.31 alpha)是给 Device Plugin 路径做的(KEP-4680);DRA 路径是随 1.34 一起引入的 alpha 特性(不是 GA 之后补的),且到 1.36 才 beta。

DRA 的完整架构(四大支柱、五个特性、DeviceClass / ResourceClaim 的 YAML 示例、迁移路径)见 告别 Device Plugin:深度解析 DRA。


五、大模型时代的三条实战主线

Device Plugin 及其生态在 AI 场景的分化,可以沿三条主线看:训练要的是「整批资源不空转」,推理要的是「单位成本吞吐最大」,具身要的是「确定性时延」。

场景 核心诉求 代表组件 解决的问题
分布式训练 整批同时启动、拓扑对齐 Volcano、Kueue、KAI Scheduler Gang scheduling、队列、拓扑感知
大模型推理 单卡承载多模型、显存切分 HAMi、time-slicing、MIG GPU 共享、显存/算力限额
高性能网络 RDMA/SR-IOV 直通 sriov-network-device-plugin、k8s-rdma-shared-dev-plugin 训练通信低延迟
异构算力 NPU/GPU/国产卡统一调度 昇腾 Ascend Device Plugin、HAMi 多后端统一接口
边缘具身 云边协同、设备孪生 KubeEdge(DeviceModel/Device CRD + Mapper) 边缘设备管理与数据下行
虚机 GPU Passthrough / vGPU kubevirt-gpu-device-plugin(NVIDIA 出品) VM 场景的 GPU 透传

5.1 主线一:分布式训练——gang scheduling 与故障自愈

原生 kube-scheduler 把每个 Pod 当成独立的放置决策,这在「64 个 H100 worker 必须整体启动」的场景下是灾难:调度器先收下 60 个,剩下 4 个 Pending,已启动的 60 个 rank 在 GPU 上空烧,等一个永远不会完成的 rendezvous。

Volcano(2019 年 3 月由华为云开源,调度器基于 kube-batch 演化)为此引入了 PodGroup 抽象,配合队列、DRF 公平共享、网络拓扑感知调度,把「AI 训练任务」变成一等公民。

⚠️ 一处字段名要精确:原稿写「PodGroup 抽象和 minAvailable 语义」。实际是——PodGroup 的 spec 字段叫 minMember(“minMember indicates the minimum number of pods or tasks running under the PodGroup”),而 minAvailable 是 VolcanoJob 的字段。两者语义相近但归属不同。

昇腾的 Ascend Device Plugin 在这条线上走得更远——它把 Device Plugin 从「资源上报器」升级成了「故障自愈的参与者」:

  • -volcanoType 参数原生存在(源码 main.go 里 flag.Bool("volcanoType", false, "Specifies whether to use volcano for scheduling")),Helm chart 默认开启,文档明确「重调度场景下必须使用 Volcano」;
  • -autoStowing 支持故障芯片自动隔离、恢复后自动回归——但要注意两个限制:文档已标注该字段「已日落」(deprecated),且仅在 volcanoType=true 时生效;
  • NPU 设备重调度:节点或芯片故障后,系统把训练任务重调度到健康设备上、重启训练容器、隔离故障芯片——这是 MindCluster 的断点续训能力,重调度由 Volcano 执行;
  • 故障感知走的是订阅而非轮询:插件启动时调用 DCMI 故障订阅接口注册监测,故障发生时由驱动主动上报(驱动不支持订阅时才退化为轮询),而且芯片故障与网络故障都上报(fault_type 含 CardNetworkUnhealthy)。

KAI Scheduler 与 Kueue 在这条线上继续演进,但分工要分清:

⚠️ 一处归属要改:原稿写「KAI Scheduler、Kueue 把『配额、抢占、多集群』做进了调度语义」。核实结果——KAI 归属 NVIDIA(来自 Run:ai 的调度引擎,现为 CNCF Sandbox),有分层队列/配额、抢占、DRF、拓扑感知,但**「多集群」在 KAI 文档里查无**;「多集群」是 Kueue 的 MultiKueue(“dispatch workloads to other clusters with free capacity”)。

5.2 主线二:推理服务——把「一 Pod 一卡」改成「一卡 N 片」

推理是 GPU 共享需求最强烈的地方。一个 4B 模型只占 8GB 显存,独占一张 80GB 的 A100 是巨大的浪费。当前解法分三层:

Time-slicing:NVIDIA device plugin 原生支持,通过 ConfigMap 定义 replicas,把一张物理卡暴露成 N 个逻辑副本,由 CUDA 驱动做时间片轮转。实现最轻,但节点上的 GPU 数量会被虚增为 replicas × 物理卡数(见第三节的更正),而且没有显存和故障隔离。

MIG:A100/H100 等硬件级切分,每个实例有独立的显存和 SM,隔离由硬件保证。适合强隔离需求,但切分 profile 需要预先配置、实例数有限。

HAMi:在 Device Plugin 之上做了更彻底的改造,四步链路是——Mutating Webhook 改写 Pod(如果 Pod 请求 HAMi 资源且没指定别的调度器,就把 schedulerName 设成 hami-scheduler)→ 自研调度器做 Filter/Bind + 内部设备打分 → 设备插件在 Allocate() 时读 annotations,注入 libvgpu.so、/etc/ld.so.preload 和虚拟化环境变量(如 CUDA_DEVICE_MEMORY_LIMIT_0=3000m)→ 容器内 libvgpu.so 拦截 CUDA/NVML 调用执行实际限额。Pod 可以这样申请:

1
2
3
4
5
resources:
limits:
nvidia.com/gpu: 1 # 1 张物理卡
nvidia.com/gpumem: 3000 # 该卡上占 3000 MiB 显存
nvidia.com/gpucores: 30 # 占 30% 算力

gpumem 单位是 MiB、gpucores 是百分比——两个都在官方文档里核实过。

⚠️ 三处更正:

  1. 删掉「4 张降到 2 张、利用率 50%→100%」这组数字——HAMi 官方 case-studies、文档、博客里都没有这个表述。官方最接近的数字是 SNOW Corp 的「2× fewer GPUs」和 DaoCloud 的「>80% average GPU utilization」,口径与原文不同,不能混用。
  2. 删掉「NVIDIA KAI + HAMi:8 张 A100 跑 16 个 Qwen3-4B,每个半卡」——KAI×HAMi 集成本身真实存在(HAMi-core 自 KAI Scheduler v0.16.4 起被内置用于显存硬隔离),但没有这个实验。HAMi 官方 benchmark 用的是 2×A100-SXM4-40GB 跑 Qwen3-8B,模型和规模都对不上。
  3. 「HAMi 用统一资源键前缀做异构屏蔽」不成立——各厂商用各自的资源名前缀。官方文档明写 “Other vendors use their own resource names”:NVIDIA 是 nvidia.com/gpumem,昇腾是 huawei.com/Ascend910B3-memory,寒武纪是 cambricon.com/mlu.smlu.vmemory。异构的「统一」体现在同一套 API 与管理体验,不是资源键前缀统一。

一处核实为正确、保留:HAMi 支持的设备类型数确实是 14 类——官方 device-supported 表(标注 v2.10.0)正好 14 行:NVIDIA GPU、AMD GPU(ROCm)、AWS Neuron、Biren、Cambricon MLU、Enflame GCU、Huawei Ascend NPU、Hygon HCU、Iluvatar、Kunlunxin XPU、MetaX、Mthreads、Teco DPU、Vastai。

HAMi 的底层原理(LD_PRELOAD 劫持 cuMemAlloc 的完整链路、五步调度链路、DRA 模式)见 深入解析 K8s GPU 复用:从通用方案到 HAMi 异构架构全解。

5.3 主线三:具身智能——边缘设备管理与确定性时延

具身智能把算力需求从数据中心推到了物理世界,其 GPU 负载结构与大模型推理完全不同:不是按用户请求量弹性波动,而是随部署单元数线性扩展、需要确定性时延——一条每分钟处理 1000 个零件的产线机械臂,无法容忍 500ms 的 GPU 调度抖动。

(1)设备接入与数字孪生:KubeEdge 的声明式设备模型

KubeEdge 用 DeviceModel / Device 两个 CRD 描述物理设备(传感器、摄像头、机械臂控制器),通过 Mapper 组件对接具体协议。完整链路是:

云端创建 Device CRD → Device Controller 创建一个 ConfigMap 存放设备属性与 visitor 定义 → 借助 edge controller 既有的 ConfigMap 同步机制下发到边缘 → 边缘 Mapper 容器读取该 ConfigMap,据此与物理设备通信 → DeviceTwin 把孪生状态回报云端

下发通道是 CloudHub(websocket server)→ EdgeHub(websocket client)。

⚠️ 一处协议归类要改:原稿把 MQTT 列为 Mapper 对接的设备协议。MQTT 不是设备侧 mapper 协议——它是 EventBus / 数据推送(pushMethod.mqtt)用的消息通道。Mapper 侧真正对接的协议是官方 mappers-go 实现的那几个:Modbus、Bluetooth、ONVIF、OPC-UA。

(2)边缘 GPU 与仿真算力

边缘节点跑推理需要 GPU 直通(device plugin 标准用法),训练侧则依赖 GPU 集群上的物理仿真——Isaac Sim 这类高保真接触动力学仿真本身就是 GPU 密集型,仿真精度直接影响 sim-to-real 迁移成功率。

(3)云边协同调度

KubeEdge 的核心卖点之一是边缘自治——官方原文:

Edge Autonomy: Ensure edge nodes run autonomously and the applications in edge run normally, when the cloud-edge network is unstable or edge is offline and restarted.

⚠️ 一处表述要收:原稿写「KubeEdge 在边缘节点本地保留完整的 Kubernetes API 能力」。这过度了——边缘侧是 Edged(轻量 kubelet)+ MetaManager(把元数据存到轻量数据库 SQLite),并不是在边缘跑一个完整的 kube-apiserver。官方自己的说法是「Kubernetes-native support … just like a traditional Kubernetes cluster in the Cloud」。

「云断边不断」这个结论是对的,只是机制要说准。

(4)具身场景的另一个延伸:KubeVirt 路线

把 GPU 通过 vfio-pci 直通给虚拟机,配合 kubevirt-gpu-device-plugin 把 VFIO 设备和 vGPU 暴露给 VM 调度。对于需要完整操作系统隔离的具身仿真环境或传统工业软件,这是比容器更现实的选择。

⚠️ 一处归属要改:kubevirt-gpu-device-plugin 属于 NVIDIA(github.com/NVIDIA/kubevirt-gpu-device-plugin),不是 KubeVirt 组织的项目——github.com/kubevirt/kubevirt-gpu-device-plugin 这个仓库不存在。


六、多调度器真的会在哪里冲突?

既然 §2.3 已经确立了「调度器不调用 Allocate」,那么一个自然的追问是:多个调度器共存时,会在哪里冲突?

答案:冲突不在 Allocate,而在 Assume。

  • Allocate 侧不可能冲突:它由目标节点的 kubelet 调用,每节点一个 kubelet、串行处理 SyncPod——节点侧只有一个写者。
  • 冲突发生在 Assume/Reserve 阶段:调度器算「这个节点还剩几张卡」,不是读节点上的实时值,而是拿自己 informer 缓存里的 Pod 列表求和,再用节点 status 里的 capacity 去减。两个调度器各有各的缓存、同步有先后——A 把最后一张卡 assume 掉(本地缓存 +1),B 的缓存还没看到那个 Pod,于是也 assume 了同一张卡,两个都 Bind 成功。

后果比想象的轻,但形态很坑。 kubelet 会兜住这一层:DeviceManager 用 kubelet_internal_checkpoint 记账,devicesToAllocate 会跳过已在 allocatedDevices 里的设备 ID——同一个 /dev/nvidia0 不会分给两个 Pod。

所以失败形态不是「两个 Pod 悄悄共享一张卡」,而是:

第二个 Pod 卡在 ContainerCreating,报 Insufficient nvidia.com/gpu。

而这恰好呼应 §2.4 那条语义——Allocate 失败不会解绑,Pod 留在节点上按 backoff 重试。于是它变成一个僵局:Pod 既没跑起来,也没被重新调度。

这个僵局其实是「调度与分配割裂」的意外好处:

上游允许多写者,下游保证单写者。 无论上面挂几个调度器、缓存多乱,节点侧只有一份权威账本。

⚠️ 一个我没有核实的推论:DRA 把设备分配提前到调度周期内、写回 ResourceClaim.status.allocation——设备是在 API Server 上的对象里定好的,kubelet 的 NodePrepareResources 更像是「按单准备」而不是「从池子里挑」。这意味着 DRA 下那道节点侧的最后防线可能变薄了:同样的陈旧缓存问题,在 Device Plugin 下会退化成「Pod 卡住」,在 DRA 下会不会退化成「两个 ResourceClaim 拿到同一张卡」,取决于 DRA driver 的实现。这一点建议实测,我没有找到明确结论。

四层解法,从浅到深:

层 手段 能解决什么
Pod 层 spec.schedulerName 显式声明 解决「谁处理这个 Pod」——但不解决共享节点视图
配额层 namespace + ResourceQuota 在配额层面划界,防止跨调度器的资源争抢
调度器层 缩短 informer 同步延迟、PriorityClass 全局对齐 缩小冲突窗口,不能消除
节点层(最彻底) 节点池物理隔离:taint + nodeSelector/affinity 两个调度器管的节点集合不重叠 → 视图冲突从根上消失

只有节点池物理隔离能根治,其余都是缩小窗口。

多调度器共存的其他两个坑(抢占互相不知情、拓扑约束不互通)、四种共存模式与组合拳建议,见 Kubernetes 多调度器共存实践指南 与 跨调度器抢占:机制、冲突根因与选型实践。本节只补「从 Bind/Allocate 时序看冲突」这个角度。


七、算力池化的三层解构

把上面所有内容收拢,今天所谓的「AI 算力池化」其实可以解构成三层,每层的成熟度和瓶颈都不同:

设备虚拟化层——把物理卡切成可调度的最小单元。 硬件路线(MIG、vGPU)隔离强但切分粒度粗;软件路线(HAMi vGPU、time-slicing)灵活但算力隔离依赖驱动时间片,硬限制能力因厂商和驱动世代而异。这一层的根本约束是:软件虚拟化无法凭空造出硬件没有提供的隔离边界。

调度语义层——表达「要什么样的资源、怎么放、故障怎么办」。 Device Plugin 把这层做成了黑盒,DRA 用结构化参数把它拉回了调度器原生视野,Volcano/KAI 在其上补齐了 gang、队列、抢占、拓扑。演进方向很清晰:从「数量调度」走向「属性与拓扑调度」。

异构屏蔽层——让 NVIDIA、昇腾、海光、寒武纪在同一套 YAML 下可互换。 HAMi 用统一的管理体验做了一层薄封装,代价是各后端能力差异需要用户自己核对支持矩阵(连资源键前缀都是各厂商自己的)。这一层最难标准化,因为「通用抽象」和「发挥每家硬件特性」天然冲突。

如果要跨域借鉴一个视角:Device Plugin 的 ListAndWatch 是事件驱动的状态上报,但硬件侧的感知大多是周期轮询——这和 TCP 拥塞控制里「ACK 驱动的窗口调整 + 发送方周期性探测带宽」的结构是同构的。算力调度面对的核心张力也一样:

如何用最小的探测开销,在最短时间内逼近真实的资源边界,又不在边界附近反复震荡。

DRA 的结构化参数、Volcano 的拓扑感知、HAMi 的动态 MIG 实例缓存复用,本质上都是在优化这个「探测—收敛—避免震荡」的闭环。


八、写在最后

Device Plugin 用一个 gRPC 接口加一个 Unix socket,撑起了近十年异构算力接入的事实标准;它的局限(整数、黑盒、无拓扑)则催生了 DRA、Volcano、HAMi 这一整代新组件。

从 2017 到 2025,从 alpha 到 GA 到被替代,Device Plugin 的生命周期本身就是云原生基础设施演化的一个缩影:

机制解决「接入」,演进解决「表达」,生态解决「场景」。

理解了这三层的关系,就能在任何新硬件、新调度器出现时,快速找到它在这张地图上的位置。


相关阅读

核实说明:本文的版本脉络核对自 Kubernetes CHANGELOG(1.8/1.10/1.11/1.32)与 KEP 仓库(KEP-3573 Device Plugins、KEP-3063 classic DRA、KEP-4381 Structured Parameters、KEP-4680 Resource Health);gRPC 接口核对自 staging/src/k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1/api.proto;机制细节核对自 Device Plugins 官方概念页与 NVIDIA k8s-device-plugin 源码;DRA 语义核对自官方 DRA 概念页与 GPU Operator 文档;生态部分核对自 HAMi 官方文档与源码、Ascend mind-cluster 仓库、KubeEdge 官方文档、Volcano 官方文档、KAI Scheduler / Kueue 仓库。

原稿更正汇总:gRPC 方法数(5 个而非 4 个,漏了 PreStartContainer)、time-slicing 会让节点 GPU 数虚增为 replicas×物理卡数(而非「仍显示 1」)、「DRA replaces device plugins」引文查无出处、ResourceClass 已被 DeviceClass 取代且应补 ResourceSlice、资源健康状态上报的时间线(DRA 路径是 1.34 alpha 而非 GA 之后)、Unhealthy 下降的是 allocatable 而非 capacity、Volcano 的字段归属(PodGroup 用 minMember,minAvailable 属 VolcanoJob)、「多集群」应归于 Kueue 而非 KAI、昇腾 -autoStowing 已日落且仅 volcanoType=true 时生效、KubeEdge 的 MQTT 归类(是 EventBus 通道非 Mapper 协议)与「完整 K8s API 能力」的过度表述、kubevirt-gpu-device-plugin 归属 NVIDIA。另有两组具体数字因查无出处已删除(HAMi 的「4→2 张 / 50%→100%」与「8×A100 跑 16 个 Qwen3-4B」)。一处核实为正确并保留:HAMi 支持的设备类型确为 14 类。