k8s-vgpu-scheduler 设备注册协议逆向图解:Node/Pod 注解握手全流程,一篇看懂
【免费下载链接】k8s-vgpu-schedulerOpenAIOS vGPU device plugin for Kubernetes is originated from the OpenAIOS project to virtualize GPU device memory, in order to allow applications to access larger memory space than its physical capacity. It is designed for ease of use of extended device memory for AI workloads.项目地址: https://gitcode.com/gh_mirrors/k8s/k8s-vgpu-scheduler
前言:vGPU 调度器靠什么"看见"GPU?🔍
k8s-vgpu-scheduler(HAMi)是一个 Kubernetes vGPU 设备调度项目,核心能力是GPU 显存虚拟化:让多个 Pod 共享一张物理 GPU,并按显存/算力比例硬限制每个容器的用量,还可以用主机内存做 swap 实现显存超卖。
它和 kube-scheduler 最大的不同在于:GPU 是"可拆分、可记账"的共享资源,调度器必须实时掌握每张卡的分片数、显存上限、算力上限和健康状态。它是怎么做到不依赖任何私有 API 的?答案很"Kubernetes"——全程只用 Node 和 Pod 的 annotations(注解)完成注册、心跳、握手和调度决策。
本文从源码层面逆向这套协议:从节点上的 device-plugin 如何把设备规格"贴"到 Node 注解,到 scheduler 如何用"握手"判断节点存活,再到调度结果如何写回 Pod 注解交给设备插件执行分配,全流程图解如下。
上图即 HAMi 的整体架构:MutatingWebhook 负责把请求 GPU 的 Pod 切换给 HAMi scheduler,scheduler 作为 extender 注册 Filter/Score 方法并写出调度决策,定制版 DevicePlugin 按决策执行分配(见 docs/develop/design.md)。
一、协议总览:两条"注解信道"
整套协议只依赖两类对象上的注解,可以概括为两条信道:
| 信道 | 载体 | 方向 | 作用 |
|---|---|---|---|
| 设备注册信道 | Node annotations | device-plugin → scheduler | 上报每张卡的规格、心跳握手 |
| 调度决策信道 | Pod annotations | scheduler → device-plugin | 下发"分到哪张卡、分多少" |
设备注册时序图(出自官方协议文档 docs/develop/protocol.md):
二、Node 侧:device-plugin 如何上报设备规格
每个 GPU 节点上的 HAMi device-plugin 会每 30 秒patch 两条 Node 注解(以 NVIDIA 为例):
HAMi.sh/node-handshake-nvidia: Reported_{本机当前时间戳} HAMi.sh/node-register-nvidia: {设备1}:{设备2}:...:{设备N}每条设备信息的编码格式为 7 个逗号分隔字段:
{设备UUID},{分片数 Count},{显存上限 Devmem},{算力上限 Devcore},{设备型号 Type},{Numa},{是否健康 Health}实际例子(该节点有 2 张 V100-32G 和 2 张寒武纪 MLU370-X4):
HAMi.sh/node-nvidia-register: GPU-00552014-5c87-89ac-b1a6-7b53aa24b0ec,10,32768,100,NVIDIA-Tesla V100-PCIE-32GB,0,true:GPU-0fc3eda5-e98b-a25b-5b0d-cf5c855d1448,10,32768,100,NVIDIA-Tesla V100-PCIE-32GB,0,true:各字段正好映射到 pkg/api/device_register.go 中的DeviceInfo结构体:Id / Count / Devmem / Devcore / Type / Numa / Health。编解码逻辑在 pkg/util/util.go 的EncodeNodeDevices/DecodeNodeDevices中实现。
📌多设备厂商怎么区分?每种芯片有独立的注解前缀,定义在各厂商 device 包中,并注册进全局映射表 pkg/device/devices.go:
| 厂商 | 握手注解 (handshake) | 注册注解 (register) |
|---|---|---|
| NVIDIA | 4pd.io/node-handshake | 4pd.io/node-nvidia-register |
| 寒武纪 MLU | 4pd.io/node-handshake-mlu | 4pd.io/node-mlu-register |
| 海光 DCU | 4pd.io/node-handshake-dcu | 4pd.io/node-dcu-register |
device-plugin 侧的写入动作,例如 NVIDIA 插件在 pkg/device-plugin/nvidiadevice/nvinternal/plugin/register.go 中写入"Reported " + 当前时间;MLU 与 DCU 插件分别在 pkg/device-plugin/mlu/register.go、pkg/device-plugin/hygon/dcu/register.go 中做同样的事。
三、握手机制:Reported ↔ Requesting 的"对表"
节点和调度器分布在两台机器上,时钟不可能完全对齐。如果只看 register 注解的时间戳判断存活,时钟漂移就会导致节点被误判离线。HAMi 的解法是双向互打时间戳:
- device-plugin 每 30s 把握手注解置为
Reported_节点时间; - scheduler 每 30s 遍历节点,发现
Reported后,用调度器自己的时钟把该注解改写成Requesting_调度器时间; - 下一轮 device-plugin 发现被改成
Requesting,再回写Reported_节点时间,完成一次"回合"。
于是 scheduler 判断存活的标准变成:如果注解停留在Requesting_xxx,且"当前调度器时间"比注解里的时间戳超出了阈值(文档中为 5 分钟),说明节点侧的 device-plugin 早已不在应答,该节点设备被标记为 unavailable(超时清理逻辑见 pkg/scheduler/scheduler.go 的RegisterFromNodeAnnotatons)。
节点设备彻底消失时(如 GPU 掉卡、插件退出),scheduler 还会把注解改写为Deleted_时间戳并从本地设备表删除,形成Reported → Requesting → Deleted的完整状态机。
💡 逆向视角:这套"谁最后写入谁说了算 + 时间戳仲裁"的无锁设计,本质上是把lease 租约思想压进了两条字符串注解里,不需要 etcd watch 长连接,重启后自愈。
四、Scheduler 侧:从注解构建集群 GPU 视图
HAMi scheduler 启动后在 pkg/scheduler/scheduler.go 中建立两个内存账本:
- nodeManager(pkg/scheduler/nodes.go):
map[节点名] -> NodeInfo{Devices []DeviceInfo},数据源就是上一节解析出的 Node 注册注解; - podManager(pkg/scheduler/pods.go):
map[Pod UID] -> podInfo{NodeID, Devices},通过 informer 监听所有带4pd.io/vgpu-node注解的 Pod 维护。
每当有 Pod 请求 GPU 资源进入 Filter/Score 时,getNodesUsage会做两件事:
- 从 nodeManager 拉出每个节点每张卡的总容量(Count / Totalmem / Totalcore / Health);
- 遍历 podManager 中已分配 Pod 的注解,把每份"已分走的显存/算力"累加到对应卡的 Used 字段上,得到剩余可用量。
也就是说,GPU 余量 = Node 注解的总量 − Pod 注解中已承诺的量,两条信道在此汇合,调度决策完全建立在注解数据之上。
五、Pod 侧:调度决策如何写回 Pod 注解
节点选出来后(Filter 过滤 + Score 打分,打分策略见 pkg/scheduler/score.go),scheduler 一次性 patch 三条 Pod 注解(常量定义见 pkg/util/types.go):
4pd.io/vgpu-node: {选中的节点名} 4pd.io/vgpu-time: {unix 时间戳} 4pd.io/devices-to-allocate: {容器1请求}:{容器2请求}:...其中每段容器请求的格式是{设备UUID},{设备类型关键字},{显存请求MB},{算力请求}。例如双容器 Pod 分别申请 3G 和 5G 显存:
4pd.io/devices-to-allocate: GPU-0fc3eda5-...,NVIDIA,3000,0:GPU-0fc3eda5-...,NVIDIA,5000,0: 4pd.io/vgpu-node: node67-4v100 4pd.io/vgpu-time: 1705054796Pod 调度决策时序图(同样出自 docs/develop/protocol.md):
Bind 阶段:上锁与 bind-phase
scheduler 在 Bind 时(pkg/scheduler/scheduler.go 的Bind方法)还会追加两条注解:
4pd.io/bind-phase: allocating—— 标记进入分配中;4pd.io/bind-time—— 记录绑定时间,防止重复处理。
同时通过节点锁(pkg/util/nodelock/nodelock.go)对该节点加锁,避免同一节点上并发分配互相踩脚。
六、Device-plugin 侧:按注解"逐容器"消费配额
Pod 落到节点后,kubelet 调 device-plugin 的 Allocate。HAMi 的定制版插件并不"一次全给",而是:
- 读取
4pd.io/devices-to-allocate注解,DecodePodDevices按容器顺序解码; - 为每个容器取它名下的设备段(
GetNextDeviceRequest),据此生成环境变量(显存/算力上限)和挂载; - 每成功分配一个容器,就把该段从注解中划掉(
EraseNextDeviceTypeFromAnnotation); - 注解中所有段清空后,把
4pd.io/bind-phase改为success并释放节点锁;失败则改为failed(逻辑见 pkg/device/devices.go)。
bind-phase字段因此构成第三重状态机:allocating → success / failed,配合 scheduler 的 informer 可以实时观察分配进度。
七、端到端全流程:一张图串起来
把上面六步连起来,一次完整的注解握手如下:
[节点 device-plugin] [HAMi scheduler] [kubelet/DP] │ 每30s: Reported_节点时间 │ │ │──node 注解──► scheduler 读注册注解 │ │ │ 改写 Requesting_调度时间 │ │◄──心跳回合── 建立 Node 设备视图 │ │ │ │ Pod 请求 GPU 到达 │ │ │ Filter/Score: 节点总量-已承诺量 │ │ │──Pod 注解: vgpu-node + │ │ │ devices-to-allocate + bind-phase │ │ │◄────────────────────────────────── │ │ │ Bind: 上节点锁、创建 Binding │ │ 按容器解码注解、设置限额环境变量 │ │ │◄────────────────────────────┼──────── 逐容器 Allocate ───────────┤ │ 划掉已分配段 │ bind-phase = success │对应运行的真实系统效果,可以看这张 HAMi 共享 GPU 使用示例:
八、逆向要点小结 🧩
| 机制 | 注解 Key(以 NVIDIA 为例) | 值形态 | 逆向结论 |
|---|---|---|---|
| 设备注册 | 4pd.io/node-nvidia-register | 7 字段逗号串,:分隔多卡 | 静态规格,30s 刷新 |
| 心跳握手 | 4pd.io/node-handshake | Reported_/Requesting_/Deleted_+ 时间戳 | 双方互写时钟,超时判离线 |
| 调度决策 | 4pd.io/vgpu-node | 节点名 | Filter/Score 的输出去向 |
| 分配清单 | 4pd.io/devices-to-allocate | 按容器分段的 UUID+限额 | DP 逐容器消费并划掉 |
| 分配状态 | 4pd.io/bind-phase | allocating/success/failed | 节点级串行化 + 进度可观测 |
这套协议最值得借鉴的三点:
- 无状态存储、注解即协议——所有跨组件信息都压缩进可 patch 的字符串,重启任意组件都能从 etcd 全量重建;
- 时间戳互打解决时钟漂移——存活判断只依赖"同侧时钟差",永远不直接比较两台机器的绝对时间;
- 容量 = 注册总量 − 已承诺量——用 Pod 注解做"承诺账本",天然支持超卖与记账。
延伸阅读
- 协议文档:docs/develop/protocol.md
- 设计文档与流程图:docs/develop/design.md
- 配置项说明(含调度策略):docs/config.md
- 调度核心源码:pkg/scheduler/
- 注解编解码:pkg/util/util.go、pkg/util/types.go
- 各厂商 device-plugin 注册逻辑:pkg/device-plugin/nvidiadevice/nvinternal/plugin/register.go
【免费下载链接】k8s-vgpu-schedulerOpenAIOS vGPU device plugin for Kubernetes is originated from the OpenAIOS project to virtualize GPU device memory, in order to allow applications to access larger memory space than its physical capacity. It is designed for ease of use of extended device memory for AI workloads.项目地址: https://gitcode.com/gh_mirrors/k8s/k8s-vgpu-scheduler
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考