控制台 退出登录

HAMi 整体工作原理与指标产生原理

一、HAMi 整体工作原理

HAMi 由三个核心组件构成,形成一个完整的 GPU 虚拟化链路。

组件概览


阶段 1:Device Plugin — GPU 设备注册

Device Plugin 作为 DaemonSet 运行于每个 GPU 节点,通过 WatchAndRegister 每 30 秒循环调用 RegistrInAnnotation,将节点 GPU 信息(UUID、显存、算力等)编码后写入 Node Annotation hami.io/node-nvidia-register。 register.go:186-222

设备信息由 getAPIDevices() 通过 NVML 获取,包括 GPU 显存大小(支持超分配系数 DeviceMemoryScaling)、算力上限、NUMA 拓扑等。 register.go:114-184


阶段 2:Admission Webhook — 资源请求改写

Scheduler 内嵌一个 Webhook,拦截 Pod 创建请求,对每个容器调用各设备的 MutateAdmission 方法改写资源请求(如将 nvidia.com/gpu 等原生资源转换为 HAMi 内部资源),并在必要时将 pod.Spec.SchedulerName 修改为 HAMi 调度器。 webhook.go:52-104


阶段 3:Scheduler Extender — 调度与设备分配决策

Scheduler 以 Kubernetes Extender 形式运行,对外暴露 /filter 和 /bind 接口。

Filter 阶段:getNodesUsage 读取调度器内存中维护的节点设备状态与已调度 Pod 信息,通过 calcScore 打分选出最优节点与 GPU,并将分配结果(具体哪块 GPU、分配多少显存和算力)写入 Pod Annotation hami.io/vgpu-devices-to-allocate 和 hami.io/vgpu-node。 scheduler.go:481-553

节点信息采集: Scheduler 通过 RegisterFromNodeAnnotations 定期从 Node Annotation 读取设备信息,结合 Pod informer 维护 overviewstatus 内存状态。 scheduler.go:185-281

Bind 阶段: 设置 Node Lock 防止并发分配冲突,调用 Kubernetes Bind API 完成 Pod 绑定。 scheduler.go:421-479


阶段 4:Device Plugin Allocate — 关键注入机制

Kubelet 调用 Device Plugin 的 Allocate() gRPC 方法。这是 GPU 虚拟化的核心环节:

  1. 从 Pod Annotation 读取调度器已决定的设备分配信息(GetNextDeviceRequest)

  2. 设置关键环境变量:CUDA_DEVICE_MEMORY_LIMIT_x(显存限制)、CUDA_DEVICE_SM_LIMIT(算力限制)

  3. 将 libvgpu.so 和 ld.so.preload 以 Volume Mount 方式注入容器,实现对 CUDA API 的透明拦截

  4. 在宿主机创建专属缓存目录 {HOOK_PATH}/vgpu/containers/{podUID}_{containerName},挂载进容器,用于共享内存通信 server.go:455-586


阶段 5:容器内 libvgpu.so 运行时拦截

libvgpu.so 通过 ld.so.preload 在容器进程启动时自动加载,拦截所有 CUDA 调用,对显存分配和 SM 利用率进行限制,并将实时使用数据写入共享内存 .cache 文件(sharedRegionT 结构体)。


二、HAMi 指标系统

HAMi 有两套独立的 Prometheus 指标服务,服务于不同层级。


Scheduler 指标(:9395)

Scheduler 的 initMetrics 启动一个独立 HTTP 服务,注册 ClusterManagerCollector,每次 Prometheus 抓取时触发 Collect()。 metrics.go:298-310

设备级指标(来自 InspectAllNodesUsage(),即调度器内存状态):

指标名

含义

GPUDeviceMemoryLimit

GPU 总显存(MiB→Bytes)

GPUDeviceCoreLimit

GPU 总算力(%)

GPUDeviceMemoryAllocated

GPU 已分配显存

GPUDeviceSharedNum

共享该 GPU 的容器数

GPUDeviceCoreAllocated

GPU 已分配算力

nodeGPUOverview

GPU 综合概况

nodeGPUMemoryPercentage

已用显存/总显存

nodeGPUMigInstance

MIG 实例使用情况

用户(Pod/容器)级分配指标(来自 GetScheduledPods(),即已调度 Pod 的分配记录):

指标名

含义

标签

vGPUMemoryAllocated

容器分配的显存量

podnamespace, nodename, podname, containeridx, deviceuuid

vGPUCoreAllocated

容器分配的算力

同上

QuotaUsed

ResourceQuota 使用量

quotanamespace, quotaName, limit


vGPUmonitor 指标(:9394)— 用户级实时指标的产生原理

这是最核心的部分。vGPUmonitor 的 initMetrics 启动 HTTP 服务并注册 ClusterManagerCollector。 main.go:136-170

指标定义 metrics.go:83-127

第一步:共享内存的数据结构

libvgpu.so 和 monitor 共享同一个内存映射文件,其核心结构 sharedRegionT 包含:

  • uuids[16]:容器使用的 GPU UUID 列表

  • limit[16]:每个 GPU 的显存限额

  • procs[1024]:每个进程的使用数据槽位(shrregProcSlotT),每个槽位含 used[16]deviceMemory 和 deviceUtil[16]deviceUtilization

  • utilizationSwitch、recentKernel、priority:反馈控制字段

  • lastKernelTime:最近一次 kernel 执行时间戳 spec.go:57-81

第二步:ContainerLister 扫描与映射

ContainerLister.Update() 扫描 {HOOK_PATH}/containers/ 目录,对每个子目录(命名格式为 {podUID}_{containerName})调用 loadCache(),通过 syscall.Mmap 将 .cache 文件以读写共享模式映射到内存,并根据文件大小/版本号区分 v0/v1 格式,调用 CastSpec() 将字节切片直接转换为结构体指针(零拷贝)。 cudevshr.go:164-221 cudevshr.go:223-288

第三步:指标采集与聚合

collectPodAndContainerInfo() 的工作流程:

  1. 通过 Pod Lister 获取当前节点(hami.io/vgpu-node = nodeName)所有 GPU Pod

  2. 将 ContainerLister.ListContainers() 的结果按 podUID 建立 containerMap

  3. 对每个 Pod 的每个容器调用 collectContainerMetrics() metrics.go:305-354

collectContainerMetrics() 遍历容器使用的每块 GPU,调用 UsageInfo 接口聚合所有进程(procs[0..procnum])的显存(total/contextSize/moduleSize/bufferSize/offset)和 SM 利用率(smUtil),并带上 {podnamespace, podname, ctrname, vdeviceid, deviceuuid} 标签发送到 Prometheus channel。 metrics.go:363-426

数据聚合实现(以显存 Total 为例,累加容器所有进程的用量): spec.go:123-137

第四步:反馈控制(闭环)

watchAndFeedback 每 5 秒触发一次 Observe(),读取共享内存中各容器的 recentKernel 和 priority,向共享内存写回 utilizationSwitch(控制是否限速)和 recentKernel(控制是否阻塞),libvgpu.so 读取这些字段来调整容器的 GPU 使用行为,实现多容器间的优先级调度。 feedback.go:186-214 feedback.go:128-184


总结

关键设计思路:

  • 用户级实时指标(vGPU_device_memory_usage_in_bytes, Device_utilization_desc_of_container 等)通过 libvgpu.so ↔ mmap共享内存 ↔ vGPUmonitor 三层联动产生,数据完全来自容器内部运行时,精度高且无侵入

  • 用户级分配指标(vGPUMemoryAllocated, vGPUCoreAllocated)由 Scheduler 从调度决策记录中产生,反映的是调度时承诺的资源量,而非实际使用量

  • 两套指标互补:前者反映实际运行时状态,后者反映资源配额分配情况

Notes

  1. 两套指标端口不同:vGPUmonitor 暴露 :9394,Scheduler 暴露 :9395,部署时需要分别配置 Prometheus scrape job。

  2. 共享内存版本兼容:v0 格式(文件大小恰好为 1197897 字节)和 v1 格式(majorVersion == 1)共存,loadCache() 会自动区分。v0 的 LastKernelTime() 返回 0(不支持),v1 支持该字段。 cudevshr.go:272-287

  3. MIG 指标:MigInfo 指标由 collectPodAndContainerMigInfo() 从 Pod Annotation 解码产生,而非共享内存,因为 MIG 切分的是物理设备,其 UUID 格式为 {GPU-UUID}[templateIdx-pos]。 metrics.go:428-473

  4. vGPUPodsDeviceAllocated、vGPUMemoryPercentage、vGPUCorePercentage 已标记为在 v2.8.0 弃用,应使用 vGPUMemoryAllocated 和 vGPUCoreAllocated 替代。 metrics.go:172-196

本文采用 CC BY-NC-SA 4.0 协议发布

相关文章

鹊桥计划

鹊桥计划

日志模板提取与 Drain 算法

日志模板提取与 Drain 算法

Thin Pool

Thin Pool

镜像层与可写层原理

镜像层与可写层原理