用 NVIDIA mock GPU 测试 HAMi
没有 GPU 服务器,也可以先测试 HAMi 的调度行为:是否选对设备、是否拒绝超额请求、Pod 删除后是否释放资源。
这次在一台 Linux CPU 主机上,用 NVIDIA 的 k8s-test-infra 模拟了 4 张 A100,部署 HAMi 后测试共享、多卡、显存与算力预算、设备筛选、调度策略、资源回收、监控和重启恢复。33 项检查中,31 项通过,另两项分别遇到了显存视图兼容性问题和 mock 的 CUDA 功能限制。
这些结果能说明调度和配额注入的行为,真实 GPU 上的显存、算力隔离仍需单独测试。
HAMi 解决什么问题
假设一张 GPU 有 40 GiB 显存,一个小推理服务只需要 4 GiB。若以整张卡作为分配单位,服务会占住整卡;若只允许多个容器访问同一设备,又没有显存预算与运行时限制,服务之间容易互相影响。
HAMi 负责设备共享、资源分配和设备感知调度。它支持多种加速器,但具体能力取决于厂商、硬件和后端。HAMi v2.10.0 的项目说明 介绍了这些功能;这次测试使用 NVIDIA 的 hami-core 后端。
常用功能
| 功能 | 解决的问题 | 本文验证程度 |
|---|---|---|
| GPU 共享 | 多个 Pod 使用同一张物理卡,各自请求部分资源 | 验证到 Running Pod 与实际 UUID 分配 |
| 显存配额 | 按绝对容量或百分比分配显存 | 验证预算、配额注入;显存视图有一项异常 |
| 算力配额 | 为工作负载配置算力比例,并参与调度预算计算 | 验证预算与环境变量,未验证执行精度 |
| 多卡分配 | 一个容器申请多张物理卡,每张卡分配所请求的资源 | 验证 2 个不同 UUID |
| 设备选择 | 按型号、UUID、节点约束选择或排除设备 | 验证正向与反向用例 |
| binpack / spread | 尽量集中到少量节点/卡,或分散到不同节点/卡 | 两个层级都验证 |
| 配额与回收 | 拒绝无法满足的请求,释放后让等待的 Pod 继续运行 | 验证设备预算与 namespace 配额 |
| 显存超卖 | 放大调度器可分配的逻辑显存容量 | 只验证超卖调度,不代表增加了物理显存 |
| 监控 | 查看设备资源分配与运行指标 | 验证 scheduler 与 monitor 指标 |
| MIG、拓扑感知调度 | 利用支持的硬件划分实例或选择合适的设备组合 | 未测试 |
| 异构设备后端 | 用统一的 Kubernetes 工作流管理不同加速器 | 本文只覆盖 NVIDIA |
HAMi-core 通过容器内的库拦截实现部分控制,MIG 则依赖 GPU 硬件划分实例。HAMi 动态 MIG 的实例创建、记录和回收流程需要另行测试,见 v2.10.0 的动态 MIG 说明。
资源请求怎么写
一个典型请求是:
|
|
在本文固定的版本和配置中,它表示:
- 需要 1 张物理 GPU 的资源。
- 在该卡上分配 4096 MiB 显存。
- 申请 25 的算力配额,作为 100 分制的比例参与预算计算。
gpucores: 25 表示算力配额,不对应 25 个 CUDA core 或固定的 SM 分区。它与 MIG 的硬件划分不同,实际吞吐量也未必是整卡的 25%,需要用真卡测量。
Node 上的 nvidia.com/gpu 表示可分配的共享名额,Pod 中的同名字段表示需要几张物理卡。本次每个节点有 2 张卡,deviceSplitCount=10,Node 因而公布 20 个名额;Pod 请求 nvidia.com/gpu: 2 时,会分配到 2 张不同的物理卡。固定版本的 README 中也有这个说明。
假 GPU 替换的是哪一层
本文使用的 k8s-test-infra 提供 mock NVML、假设备节点、驱动目录和 CDI 注入配置,上层组件可以据此发现设备、读取属性,但无法执行真实的 GPU 计算。
本次保留了真实的 HAMi scheduler、webhook、NVIDIA device-plugin、HAMi-core 和 monitor,没有启用 HAMi 自带的 mock-device-plugin。
|
|
沿着这条调用链,分别检查:
- 控制面:Pod 是否被正确修改、调度和绑定;拒绝请求是否有 FailedScheduling 事件。
- 容器配置:实际设备节点、UUID、配额环境变量和 hook 是否存在。
- 执行效果:CUDA 分配是否被限制,超额是否失败,计算速度是否受控。
这个环境可以测试前两层。执行效果需要完整的 CUDA Driver API 和真实计算能力,超出了本次 mock 的能力范围。上游的 CUDA mock 文档 列出了 vectorAdd 等 CUDA 工作负载的限制。
搭建实验环境
实验日期是 2026 年 9 月 6 日,宿主机为 Linux amd64 CPU 主机。使用一个 control-plane、两个 worker 的独立 kind 集群,不连接已有业务集群。
| 组件 | 实测版本 |
|---|---|
| k8s-test-infra | main 分支的 874ab0319732981bc79cbc9b01c22c8fd00656e6 |
| HAMi | v2.10.0,提交 4707fb02c91c545bc7343ce26dba4c32919f9a3e |
| HAMi-core | HAMi 子模块固定的 b216ba1be1b8e21488d1c7370ed3357b3049aad1 |
| kind / Kubernetes | v0.31.0 / v1.35.0 |
| NVIDIA container toolkit | test-infra 的 Kind 镜像中固定为 1.19.1-1 |
| 模拟设备 | 每个 worker 2 张 A100-SXM4-40GB,每卡 40960 MiB |
完整版本和镜像标识见 versions.json。复现时使用当时的主分支提交,避免版本变化影响结果。
准备 Docker、kind、kubectl、Helm 3、Git、Python 3,确保当前用户可操作 Docker,并能拉取 GitHub、Docker Hub、registry.k8s.io、nvcr.io 和 NVIDIA 软件源中的内容。宿主机不需要安装 Go,构建在 Docker 内完成。NVIDIA toolkit 安装在 kind 节点镜像里。
配置、脚本和结果摘要放在 k8sdev/hami-mock-gpu,每个实验使用独立目录。克隆仓库后进入该目录:
|
|
初始化脚本 会拒绝覆盖非空目录或同名 hami-test 集群。脚本由实测命令整理而来,增加了目录检查,尚未在新集群中完整重跑。
下面是脚本中的部署步骤。已经运行 setup.sh 时无需重复执行。
构建 mock 和节点镜像
在实验工作目录执行:
|
|
第二个镜像在 kind 节点中配置 NVIDIA runtime 与 CDI,负责向 Pod 注入设备和库。配置来自 固定提交中的 Kind NVIDIA CDI 部署目录。
创建三节点集群
|
|
|
|
两个 worker 用来区分节点层面的 binpack 与 spread。每个 worker 配置两张卡,则能继续区分卡层面的策略。
为两个节点准备不同 UUID
上游 A100 profile 中的 UUID 是固定值。若两个节点直接复用同一份 profile,会出现重复 UUID。本次为每个 worker 安装一个 mock release,第二份 profile 改用不同前缀:
|
|
worker1 的 values 如下,worker2 则把 hostname 改为 hami-test-worker2:
|
|
|
|
完整配置见 worker1 values 和 worker2 values。四张卡使用不同 UUID,便于核对共享、筛选和跨节点分配结果。
安装 HAMi
HAMi 的 chart 和镜像都使用 v2.10.0:
|
|
hami-values.yaml 中几个关键参数是:
|
|
本实验设置 passDeviceSpecsEnabled=false,由 NVIDIA runtime 通过 CDI spec 注入假设备。真卡部署需要按所用运行时配置。完整 values 还把 kube-scheduler 镜像版本对齐到 Kubernetes v1.35.0,并将实验服务设为 ClusterIP。
部署完成后,先检查设备注册:
|
|
本次两个 worker 分别公布 20 份资源。HAMi 节点注解中,每卡记录 count=10、devmem=40960、devcore=100、health=true。检查这些值,可以确认 HAMi 读到的设备数量、容量和健康状态是否符合配置。
跑通第一个 Pod
先建立专用 namespace:
|
|
下面是简化后的测试 Pod,完整版本见 smoke.yaml:
|
|
这里需要额外挂载 CUDA 库。上游 mock CDI spec 注入了 NVML,却没有注入已构建的 libcuda.so.1。HAMi 注入的 libvgpu.so 依赖该库,最初连 bash 都无法启动,报 libcuda.so.1: cannot open shared object file。增加这个只读挂载后,原测试 Pod 可以运行。
|
|
日志中应该有配额:
|
|
继续检查分配注解:
|
|
本次得到的一个实际分配是:
|
|
同时核对 spec.schedulerName=hami-scheduler、spec.runtimeClassName=nvidia 和 hami.io/bind-phase=success,确认 Pod 经过了 HAMi 调度并完成分配。
功能测试
脚本提供 33 项检查,在 k8sdev/hami-mock-gpu 目录执行:
|
|
脚本会清理 hami-tests 中的 Pod、重启实验用 HAMi 组件,并短暂调整显存 scaling 后恢复,因而应只在独立实验集群中运行。它会保留逐项输入、Pod JSON、事件和日志。有检查项失败时,脚本返回非零退出码;本次固定版本有后文记录的两个已知失败。
证据保存在 $HAMI_LAB_DIR/evidence/。入口脚本会拒绝覆盖已有的 matrix 结果目录,复跑前应先备份已有测试结果。
以下各组分别检查共享、预算、选卡和资源回收。
同卡共享和份数限制
连续提交 3 个 Pod,每个请求 4096 MiB、25 cores,并指定同一张卡:
|
|
三个 Pod 都应 Running,分配注解中的 UUID 也应相同,以确认它们共享同一张卡。
再用每个 Pod 64 MiB、1 core 的小请求验证份数:前 10 个运行,第 11 个因共享名额耗尽而等待;删除一个已运行 Pod 后,第 11 个自动运行。即使还有剩余显存和算力,分配也受 deviceSplitCount 限制。
超额请求与资源回收
显存测试固定同一个 UUID:
- 第一个 Pod 请求 30000 MiB。
- 第二个请求 12000 MiB,两者相加超过单卡 40960 MiB。
- 第二个应没有
nodeName,并出现 FailedScheduling。 - 删除第一个,第二个应在不重新提交的情况下运行。
算力测试用 60 与 50 的请求重复相同步骤,两组都通过了预算检查和资源回收测试。容器没有执行 CUDA 分配或计算,所以这里没有测量实际显存占用和算力利用率。
测试独占时,先请求目标卡的 100% 显存和 100 cores,再提交一个很小的同卡请求,后者应被阻塞。
另外,v2.10.0 的 gpucores: 101 并没有被拒绝。实测分配注解与容器环境变量都变为 100;NVIDIA Fit 源码 中有截为 100 的逻辑,因此该版本的测试预期应是按 100 分配。
多卡与显存百分比
把 nvidia.com/gpu 改为 2,应在同一节点分配两个不同 UUID;每张卡都分配请求的显存与算力。本文还验证了两个容器各自请求 GPU 的 Pod,两者均有分配并正常运行。
百分比请求使用:
|
|
在本次 40960 MiB 的卡上,分配结果应为 10240 MiB。测试百分比时移除绝对显存字段,避免把两个输入的优先级混入这个用例。
节点与 GPU 调度策略
HAMi 支持分别指定节点与 GPU 策略:
|
|
测试卡级策略时,先用 nodeSelector 固定节点,再提交两个小请求:binpack 应选择同一 UUID,spread 应选择不同 UUID。测试节点级策略时解除节点约束:binpack 应选择同一节点,spread 应选择不同节点。
每组测试前清理前一组的 Pod,避免已有负载影响预期。本次四组测试的节点名和 UUID 都符合预期。上游策略示例 给出了这两个注解。
设备选择也应有正反两组:use-gputype: A100 应通过,use-gputype: T4 应等待;nouse-gputype: A100 应排除当前节点上的卡。UUID 白名单和黑名单则核对最终选中的具体 UUID。
显存超卖
本次将 deviceMemoryScaling 临时改为 2:
|
|
等待设备重新注册后,每卡逻辑显存变为 81920 MiB。再提交 61440 MiB 请求,Pod 可以分配并运行。测试容器只执行 sleep,因此这里验证了超卖调度,没有实际申请 60 GiB 显存。
测试结束后,重新使用原始 hami-values.yaml 升级,等待注册值恢复到每卡 40960 MiB。脚本将恢复步骤放在 finally 中,并再次检查注册值。
监控与重启恢复
scheduler 的指标可以经 Kubernetes API 的 service proxy 读取,无需对宿主机额外开放端口:
|
|
本次固定一张卡分配 4096 MiB 后,hami_gpu_memory_allocated_bytes 对应值为 4294967296;删除 Pod 后变为 0。指标更新有延迟,脚本会等待它变为预期值。
device-plugin monitor 的服务端点也返回了 hami_host_gpu_* 指标。不过它读取的是 mock 数据,不能把利用率、已用显存等读数当作实际 GPU 负载。
重启恢复则检查两件事:重启 HAMi scheduler 和 device-plugin 后,已有 Pod 的 UID、分配保持不变,容器没有重启;再提交的新 Pod 仍能调度。这两项都通过了;节点掉电、驱动重载和 GPU 故障恢复未测试。
测试中遇到的问题
GPU 0 显示 4 GiB,GPU 1 却仍显示 40 GiB
在本次 runtime 下,初始容器进程拥有 CDI 注入的 MOCK_NVML_CONFIG,但 kubectl exec 新建的进程没有继承这部分变量。直接 exec 查询可能得到 mock 的默认配置。查询时需要显式带上配置:
|
|
即使补上配置,两个物理索引仍有差异。同样请求 4096 MiB:
|
|
原始查询分别见 GPU 0 与 GPU 1。两个 Pod 的 HAMi 分配和 CUDA_DEVICE_MEMORY_LIMIT_0=4096m 都正确,异常出在显存查询结果。
从源码看,可能与设备索引映射有关:mock 过滤设备后,容器中的设备数量是 1,但 GPU 1 的 nvmlDeviceGetIndex 仍返回物理索引 1;HAMi-core 的 NVML 初始化使用初始的 CUDA/NVML 索引映射,映射函数只遍历设备数量范围。找不到物理索引 1 时,显存查询返回原始容量。
对应代码是 mock 的设备实现、HAMi-core NVML hook 和 索引映射函数。本次将它记为 mock 与 HAMi-core 的兼容性异常,尚未在真卡上复现。
vectorAdd 无法运行
CUDA 测试使用 NVIDIA 官方样例镜像:
|
|
带 HAMi-core hook 时,Pod 失败,错误为:
|
|
再设置 CUDA_DISABLE_CONTROL=true,让 device-plugin 不注入 HAMi preload,样例仍然失败:
|
|
无 hook 的控制实验 也未通过,与该版本 mock 缺少完整 CUDA Driver API 的限制一致。这一项记为「不支持验证」,调度本身已完成。
后续测试与清理
本次通过的 31 项主要涉及资源请求、调度、Allocate 注入和资源回收,另外记录了两项 mock 兼容性或功能限制。
运行时隔离需要用真卡补测:让 CUDA 程序申请低于和高于配额的显存,检查超限行为;并发运行计算任务,比较不同算力配额下的利用率、吞吐和延迟;再测试多进程、进程异常退出、不同物理索引和 MIG。Pod 状态、分配注解和 nvidia-smi 输出只能作为其中一部分检查。
实验结束后可按需删除独立集群:
|
|
该命令保留构建镜像、源码和证据目录。
实验文件:
仓库中的脚本整理了路径、检查入口和重复用例;测试结果取自整理前的实际运行。