HAMi 架构设计解析:MutatingWebhook、Scheduler、DevicePlugin 与容器内控制的四层协作
HAMi 架构设计解析MutatingWebhook、Scheduler、DevicePlugin 与容器内控制的四层协作【免费下载链接】HAMiHeterogeneous GPU Sharing on Kubernetes项目地址: https://gitcode.com/GitHub_Trending/ha/HAMi导读本文以 docs/develop/design.md 为主线系统拆解 HAMiHeterogeneous GPU Sharing on Kubernetes的整体架构。HAMi 以图表chart的形式组织四大核心组件MutatingWebhook准入变更、Scheduler调度扩展器、DevicePlugin定制设备插件与 InContainer Control容器内硬限制控制共同实现对 NVIDIA、Cambricon、Hygon、Ascend 等异构 AI 设备在 Kubernetes 上的切分与共享调度。读完本文你将掌握 HAMi 各组件的职责边界、设备注册与调度决策的注解协议、Pod 从创建到运行的完整生命周期以及 Helm 部署时的关键配置项。一、架构总览以chart形式组织的四层设计HAMi 的整体架构采用chart图表/层次式组织设计文档 中的架构图直观展示了各组件与 Kubernetes 控制平面的协作关系整个系统由四个层次构成每一层解决一个明确的子问题组件职责关键点MutatingWebhook准入校验与改写识别 HAMi 资源请求改写schedulerNameScheduler调度决策以 extender 方式挂接 kube-scheduler / volcano-scheduler实现 Filter 与 ScoreDevicePlugin设备发现与分配落地生成环境变量、挂载设备向节点注解注册设备规格InContainer Control容器内硬限制按设备类型注入不同实现hami-core、libvgpu-control.so 等从源码结构看这四层分别对应 pkg/scheduler/webhook.go准入、pkg/scheduler/scheduler.go 与 pkg/scheduler/routes/route.go调度、cmd/device-plugin/nvidia/main.go 及pkg/device-plugin/nvidiadevice/设备插件、pkg/device/*/各设备后端容器内控制与资源建模。二、MutatingWebhook准入阶段的门卫MutatingWebhook 是 Pod 进入集群后的第一道关卡其实现位于 pkg/scheduler/webhook.go。它的核心逻辑可以概括为检查任务有效性对每个 Pod 的 InitContainers 与普通 Containers 逐一调用各设备后端的MutateAdmission识别请求中是否包含 HAMi 可识别的设备资源。改写调度器如果资源请求已被 HAMi 识别则将pod.Spec.SchedulerName改写为hami-scheduler即 Helm 配置中的schedulerName见 charts/hami/values.yaml否则不做任何改动交由 default-scheduler 处理。除了设计文档描述的这两点从源码还可以看到准入阶段还承担了更严格的安全与正确性校验webhook.go拒绝无容器的 Podpod has no containers。拒绝预置调度注解schedulerOwnedAnnotation会检查hami.io/vgpu-nodeAssignedNodeAnnotations、hami.io/vgpu-timeBindTimeAnnotations、DeviceBindPhase以及所有设备后端的-devices-to-allocate注解。这些注解只能由调度器在调度完成后写入用户在创建 Pod 时手工预置会被直接拒绝防止绕过调度器核算私自指定 GPU。不同调度器的 Pod 放行若 Pod 已显式指定了其他调度器且forceOverwriteDefaultScheduler未开启则放行不干预。NUMA 对齐模式校验util.GetNumaAlignmentModeByPod会在准入阶段拒绝非法的hami.io/numa-alignment取值让拼写错误在用户侧直接暴露。设备评分权重校验对携带 HAMi 资源的 Pod 校验GetDeviceScoringWeightsByPod异常配置在调度前即被拦截。拒绝 privileged 容器同时请求设备资源且容器设置了privileged: true的 Pod 会被拒绝container %s is privileged。资源配额校验fitResourceQuota依据各设备后端的GenerateResourceRequests结果包含记忆因子、默认值、模板取整逻辑核算 init 容器峰值与 sidecar 容器用量调用FitQuota判断是否超出 namespace 级 ResourceQuota。也就是说webhook 不仅在业务层决定 Pod 走 HAMi 调度器还是默认调度器还在安全层杜绝了注解伪造、privileged 逃逸与配额超卖等风险。三、SchedulerFilter / Score 双阶段调度3.1 作为 extender 的接入方式HAMi 支持默认的 kube-scheduler 与 volcano-scheduler。它实现了一个scheduler extender扩展器通过 HTTP 端点注册Filter与Score方法来处理可共享设备。相关路由定义在 pkg/scheduler/routes/route.go/filter对应PredicateRoute接收 kube-scheduler 传入的候选节点返回过滤后的可用节点ExtenderFilterResult。/bind对应Bind由 HAMi 负责将 Pod 绑定到选定节点。/webhook对应WebHookRoute承载 MutatingWebhook 的 admission 请求。/healthz、/readyz健康检查与就绪检查。/refitNumaRefit供设备插件在 kubelet 的 NUMA 受限设备集合上请求重新执行 Fit见 docs/develop/numa_refit.md 相关实现与 issue #2080 的设计背景。注意 Helm 部署中/filter与/bind监听在scheduler.extenderPort默认 9444绑定回环地址只允许与调度器同 Pod 网络命名空间的 kube-scheduler 容器访问/webhook与/refit则由 Service 发布到集群。3.2 Filter找出可用节点当携带共享设备请求的 Pod 到达时Filterpkg/scheduler/scheduler.go执行以下流程提取资源请求device.Resourcereqs(pod)遍历 init 容器与普通容器调用每个设备后端的GenerateResourceRequests生成请求列表。快速放行如果 Pod 不请求任何 HAMi 资源直接回传全部候选节点不参与 HAMi 核算。构建节点用量快照getNodesUsage将每个节点的设备总量减去已分配量Used/Usedmem/Usedcores并考虑 MIG 分配、节点 taint、nodeSelector、nodeAffinity 与不可调度标记。评分并排序calcScore对每个候选节点计算可容纳度sort.Sort后取最高分的节点。写入调度决策将选中的节点写入 Pod 注解hami.io/vgpu-node、hami.io/vgpu-time并调用各设备后端的PatchAnnotations写入设备分配明细最后把唯一可用节点回传给 kube-scheduler。3.3 Score为节点打分Score的核心实现在 pkg/scheduler/policy/gpu_policy.go 的ComputeScoregpu_policy.go对同一类型的设备将请求量 已用量分别对设备数Slot、总核心数Core、总显存Memory求占用率再按可配置权重加权Score Weight * (W_slot * usedSlot W_core * usedCore W_mem * usedMem)排序策略支持binpack装箱倾向已用设备、spread打散倾向空闲设备、numaNUMA 亲和以及mutex互斥设备先挑繁忙 GPU 以保证空闲卡优先还支持逗号分隔的策略链如binpack,spread,numa详见DeviceUsageList.Less的多分支逻辑。Helm 中通过scheduler.defaultSchedulerPolicy.gpuSchedulerPolicy与nodeSchedulerPolicy配置默认分别为spread与binpack。3.4 Bind加锁、打注解、绑定Bindscheduler.go在绑定前通过verifyBindTarget校验请求中的 Pod UID 与当前对象一致、Pod 未被分配、目标节点与 Filter 阶段决策一致然后对目标节点的所有设备后端加节点锁acquireNodeLocks失败则回滚已获取的锁并释放将hami.io/vgpu-devices-assigned阶段标记DeviceBindPhaseallocating与绑定时间写入注解调用 kube-apiserver 的Bind完成绑定成功后记录EventReasonBindingSucceed事件。节点锁机制配合NodeLockExpire默认 5m与NodeLockRetryTimeout保证了多副本/多调度并发下同一节点设备分配的一致性。四、设备注册协议让调度器看见异构设备设计文档明确指出HAMi 需要知道集群中每台 AI 设备的规格才能正确调度。这是通过设备插件持续把设备规格 patch 进节点注解实现的完整协议见 docs/develop/protocol.md。4.1 注册注解格式设备插件每 30 秒向节点注解写入一次设备注册信息hami.io/node-handshake-{device-type}: Reported_{device_node_current_timestamp} hami.io/node-{device-type}-register: {Device 1}:{Device2}:...:{Device N}每台设备的定义格式为{Device UUID},{device split count},{device memory limit},{device core limit},{device type},{device numa},{healthy}一个同时存在 NVIDIA 与 Cambricon 设备的节点示例hami.io/node-handshake-nvidia: Reported 2024-01-23 04:30:04.434037031 0000 UTC m1104711.777756895 hami.io/node-handshake-mlu: Requesting_2024.01.10 04:06:57 hami.io/node-mlu-register: MLU-45013011-2257-0000-0000-000000000000,10,23308,0,MLU-MLU370-X4,0,false:MLU-54043011-2257-0000-0000-000000000000,10,23308,0, hami.io/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:该示例表示节点拥有 2 块 NVIDIA V100 与 2 块 Cambricon 370-X4。字段含义与源码中的DeviceInfo结构pkg/device/devices.go一一对应Count对应 split count可切分数、Devmem对应显存上限、Devcore对应核心上限、Numa对应 NUMA 节点、Health对应健康状态。NVIDIA 设备插件在注册时还会根据deviceSplitCount默认 10与deviceMemoryScaling/deviceCoreScaling计算注册显存与核心数详见 register.go。4.2 心跳与unavailable判定设备可能因硬件或网络故障变得不可用因此协议定义了两级心跳设备节点侧设备插件每 30 秒上报Reported_心跳若某节点超过 5 分钟未注册调度器将该节点标记为 unavailable。调度器侧由于调度器节点与设备节点系统时钟可能不同步调度器每 30 秒向节点注解写入hami.io/node-handshake-{device-type}: Requesting_{scheduler_node_current_timestamp}若注解停留在Requesting_xxxx且scheduler 当前时间 5 分钟 注解内时间戳则该节点上的该设备在调度器侧被标记为 unavailable。调度器的CheckHealth实现pkg/device/devices.go正是解析Requesting_时间戳并按 60 秒粒度做健康刷新同时检查节点 Allocatable 是否仍存在设备资源以避免误清理。4.3 调度决策注解协议调度完成后HAMi scheduler 将决策 patch 进 Pod 注解hami.io/vgpu-devices-to-allocate: {ctr1 request};{ctr2 request};...{Last ctr request}; hami.io/vgpu-node: {schedule decision node} hami.io/vgpu-time: {timestamp}需要特别说明的是hami.io/vgpu-devices-to-allocate是NVIDIA GPU 的 key。每个设备后端在InRequestDevices中注册自己的 key并不遵循统一的{device-type}命名例如Cambricon MLUhami.io/cambricon-mlu-devices-to-allocateMoore Threadshami.io/mthreads-vgpu-devices-to-allocateHygon HCUhami.io/hcu-devices-to-allocate调度器按设备类型各写一条-devices-to-allocate注解一个 Pod 同时请求多种设备如 NVIDIA MLU时每种类型一条注解且每条只列出该类型的设备。每个容器请求列出该容器的设备每台设备是 4 个逗号分隔字段并以:结尾每个容器以;结尾{device UUID},{device type keyword},{device memory request},{device core request}:示例一个 Pod 有 2 个容器第一个容器请求 1 张 GPU 的 3G 显存第二个请求 1 张 GPU 的 5G 显存hami.io/vgpu-devices-to-allocate: GPU-0fc3eda5-e98b-a25b-5b0d-cf5c855d1448,NVIDIA,3000,0:;GPU-0fc3eda5-e98b-a25b-5b0d-cf5c855d1448,NVIDIA,5000,0:; hami.io/vgpu-node: node67-4v100 hami.io/vgpu-time: 1705054796这套注解的编解码逻辑集中在 pkg/device/devices.go 的EncodePodDevices/DecodePodDevices与EncodeContainerDevices/DecodeContainerDevices中。解码时不会跳过空容器条目以保证注解条目与容器含 init 容器的索引一一对应——这正是多容器 Pod 正确回收资源的前提。五、DevicePlugin定制版设备插件5.1 为什么不能用官方设备插件调度决策做出后调度器会调用目标节点上的设备插件依据 Pod 注解生成环境变量与挂载。设计文档明确警告这里使用的 DP 是定制版本你需要根据该设备的 README 文档进行安装。大多数官方 DP 无法适配 HAMi强行使用会产生意外行为。原因在于 HAMi 的设备插件必须完成三件官方插件不做的事持续注册设备规格到节点注解30 秒心跳 register 注解见上文协议读取调度器写入的-devices-to-allocate注解把注解中的虚拟设备UUID、显存、核心转换为真实设备的环境变量与 DeviceSpec在 Allocate 阶段按 Pod 请求的切分参数挂载设备例如 NVIDIA 场景下注入hami-core的预加载库路径。NVIDIA 设备插件入口在 cmd/device-plugin/nvidia/main.go其WatchAndRegister循环register.go在成功时每 30 秒、失败时每 5 秒尝试一次RegisterInAnnotation先通过 NVML 发现设备UUID、显存、型号、NUMA再MarshalNodeDevices序列化为hami.io/node-nvidia-register注解写入节点若设备信息未变化deviceCache比对则跳过注解更新以降低 API 压力。当环境变量ENABLE_TOPOLOGY_SCOREtrue时还会额外计算 GPU 间 P2P 拓扑得分并写入hami.io/node-nvidia-score注解供拓扑感知调度使用。5.2 关键参数与节点配置设备插件的核心切分参数charts/hami/values.yaml参数默认值说明deviceSplitCount10每张物理 GPU 可切分的虚拟设备数deviceMemoryScaling1注册显存缩放系数deviceCoreScaling1注册核心缩放系数migStrategynoneMIG 策略none/single/mixedpassDeviceSpecsEnabledtrue是否在 Allocate 时传递 DeviceSpecdeviceListStrategyenvvar设备列表传递方式envvar/volume-mounts/cdi-annotationsnvidiaDriverRootauto支持读取 GPU Operator 的 driver-ready 契约自动解析驱动根目录这些参数通过nodeConfiguration.config可做按节点覆盖优先级外部 ConfigMap 内联 config 默认值例如operatingmodehami-core或 MIG、filterdevices按 UUID/索引过滤设备、enablegetpreferredallocation等。六、InContainer Control容器内硬限制的差异化实现调度与分配只是上半场真正把显存/算力限制落到容器内部的是 InContainer Control。设计文档强调不同设备的容器内硬限制实现各不相同NVIDIA 设备由HAMi-Core负责通过注入libvgpu/libcudart替换层预加载机制见 lib/nvidia/ld.so.preload 与 docker/Dockerfile.withlib拦截 CUDA 运行时调用实现显存上限与算力配额。Iluvatar天数智芯设备由libvgpu-control.so负责。其余设备后端Cambricon、Hygon、Metax、Ascend、Kunlun 等各有自己的实现。设备插件必须在 Allocate 阶段传入正确的环境变量如设备列表、hami-core的.so路径、显存/核心配额等InContainer Control 才能按调度决策生效。这也是环境变量错误会导致限制不生效的原因所在。用户侧的资源声明examples/nvidia/default_use.yaml对应了这套限制模型resources: limits: nvidia.com/gpu: 1 # 物理 GPU 数量 nvidia.com/gpumem: 3000 # 每张物理 GPU 分配 3000M 显存可选整数 nvidia.com/gpucores: 30 # 每张物理 GPU 分配 30% 算力可选整数HAMi 还支持百分比形式examples/nvidia/use_memory_fraction.yamlresources: limits: nvidia.com/gpu: 2 nvidia.com/gpumem-percentage: 50 # 每张物理 GPU 分配 50% 显存 nvidia.com/gpucores: 30这些资源名nvidia.com/gpu、nvidia.com/gpumem、nvidia.com/gpumem-percentage、nvidia.com/gpucores、nvidia.com/priority可在 charts/hami/values.yaml 与 pkg/scheduler/config/config.go 的默认配置中统一调整调度器在fitResourceQuota中会按设备的MemoryFactor对齐配额计算口径记录用量为缩放后单位ResourceQuota 限制为未缩放单位。七、Pod 全生命周期从创建到运行设计文档以流程图总结了 Pod 的完整流转路径结合前文分析一次典型的 HAMi 调度可以归纳为如下时序创建用户提交声明了nvidia.com/gpu等资源的 Pod。准入MutatingWebhookwebhook 校验资源请求、拒绝预置注解/privileged/超配额等非法 Pod识别到 HAMi 资源后把schedulerName改写为hami-scheduler。过滤Filterkube-scheduler 调用 extender/filterHAMi 依据节点注解中的设备注册信息与当前占用返回可用节点列表。打分Score按 binpack/spread/numa 等策略对节点打分选出最优节点。写决策Patch调度器把hami.io/vgpu-node、hami.io/vgpu-time与各设备后端的-devices-to-allocate注解写入 Pod。绑定Bindextender/bind加节点锁、校验身份后完成绑定。分配DevicePlugin Allocate节点上的设备插件读取注解生成环境变量与挂载向 kubelet 返回分配结果。运行InContainer Control容器启动预加载的 hami-core / libvgpu-control.so 等按环境变量执行显存与算力硬限制。回收调度器通过 Pod informer 监听删除/终止事件TakeAndDeletePodRmUsage释放设备用量与配额scheduler.goinit 容器完成后还会通过CollapseInitContainerUsage/SteadyStateDeviceUsage把临时占用收缩为稳态占用实现 init 容器资源复用。八、部署与验证8.1 Helm 安装要点使用 charts/hami 部署时几个与架构强相关的开关scheduler.admissionWebhook.enabled默认true控制是否安装 MutatingWebhook关闭后 Pod 必须显式配置schedulerName: hami-scheduler等。scheduler.kubeScheduler.enabled默认true在调度器 Pod 内以 sidecar 方式运行 kube-scheduler 并挂接 extender 配置。scheduler.leaderElect/replicas多副本高可用配合 PodDisruptionBudget。scheduler.forceOverwriteDefaultScheduler默认true即 webhook 会把SchedulerName default-scheduler的 Pod 改写为 HAMi 调度器。devicePlugin.enabled默认true以 DaemonSet 方式在每个 GPU 节点运行定制设备插件。schedulerName默认hami-scheduler与 webhook 改写值一致。8.2 快速验证部署完成后可通过以下方式验证架构是否按设计工作提交示例负载examples/nvidia/example.yaml 或 examples/nvidia/default_use.yamlkubectl get pod -o yaml观察 Pod 注解中是否出现hami.io/vgpu-node、hami.io/vgpu-time与hami.io/vgpu-devices-to-allocate且spec.schedulerName: hami-schedulerkubectl get node -o yaml观察hami.io/node-handshake-nvidia与hami.io/node-nvidia-register注解是否持续刷新30 秒周期结合 pkg/scheduler/router_test.go、pkg/scheduler/scheduler_test.go 与 pkg/device/devices_test.go 等测试用例理解各路由与编解码协议的期望行为。总结HAMi 通过MutatingWebhook 准入改写 → Scheduler 双阶段调度 → DevicePlugin 注解落地 → InContainer Control 硬限制的四层协作把异构 AI 设备的共享调度完全融入 Kubernetes 原生工作流webhook 决定谁该走 HAMiextender 决定调度到哪、切多少注解协议充当调度器与设备插件之间的契约而容器内控制层保证说好的限制真正生效。理解这套以图表形式组织的架构是正确部署、排障乃至二次开发 HAMi 的基础。【免费下载链接】HAMiHeterogeneous GPU Sharing on Kubernetes项目地址: https://gitcode.com/GitHub_Trending/ha/HAMi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考