跳转到文档内容

实验 5: 使用 nvml-mock 进行 Fake-GPU 调度

进阶时长: 约 40 分钟环境: Linux/macOS 配合 kind · 无需真实 GPU费用: 免费验证于: 2026-06-13作者: @maishivamhoo123

本实验使用 NVIDIA 的 nvml-mock 库在本地 kind 集群中模拟一个高端 GPU 节点——8 张虚拟 A100 GPU。你将直接基于 main 分支构建 HAMi,然后验证 GPU 调度功能:共享、显存/算力限制、百分比显存申请以及多 GPU 分配——全部无需物理硬件。

你将得到什么

完成本实验后,你将得到一个本地 Kubernetes 集群:

  • nvml-mock 让节点上报 8 张虚拟 A100 GPU(HAMi 将每张物理 GPU 切成 10 个虚拟槽位后,节点显示 nvidia.com/gpu: 80
  • HAMi device-plugin 和调度器使用当前 main 分支镜像运行
  • 验证的 Pod 包括:单 GPU、GPU 共享、显存/算力限制、百分比显存以及多 GPU 分配
备注

此环境中不存在真实的 CUDA 运行时。Pod 使用 busybox 并设置 CUDA_DISABLE_CONTROL=true,以阻止 HAMi 的控制库尝试真实设备访问。显存和算力限制的运行时强制执行仍需要物理 GPU。

安装全景图

整个安装过程共 10 步:

步骤 1
创建 kind 集群

步骤 2
构建并部署 nvml-mock

步骤 3
基于 main 构建 HAMi

步骤 4
部署 HAMi

步骤 5
验证 GPU 资源

步骤 6
基础 GPU 调度

步骤 7
GPU 共享

步骤 8
显存与算力限制

步骤 9
百分比显存

步骤 10
多 GPU 分配

Fake-GPU 调度安装全景图
步骤目的解决什么问题
创建 kind 集群引导本地 Kubernetes提供测试环境
构建并部署 nvml-mock模拟 8 张虚拟 A100 GPU无需硬件即可发现 GPU
基于 main 构建 HAMi编译最新的调度器和 device-plugin确保 MIG 修复已包含在内
部署 HAMi安装控制面组件启用 GPU 切分和调度
验证 GPU 资源检查 nvidia.com/gpu: 80确认虚拟 GPU 槽位已注册
基础 GPU 调度单 GPU Pod 分配验证调度器基本功能
GPU 共享在同一 GPU 上时间片共享 4 个 Pod测试并发 GPU 访问
显存与算力限制强制执行 gpumemgpucores验证资源约束
百分比显存申请 30% GPU 显存测试百分比分配
多 GPU 分配单 Pod 绑定 2 张 GPU验证多 GPU 绑定

前提条件

安装前提工具:

brew install kind kubectl helm git go

验证版本:

kind version # 0.20+
kubectl version --client --short # 1.31+
helm version # 3.x
go version # 1.21+
提示

Windows 用户请使用 WSL2 配合 Ubuntu,并按照上面的 Linux 选项卡操作。


步骤 1:创建 kind 集群

kind create cluster --name nvml-mock-test

设置一次 NODE_NAME 变量——后续所有命令都会用到它:

NODE_NAME=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
echo "NODE_NAME=${NODE_NAME}"

示例输出:

NODE_NAME=nvml-mock-test-control-plane

步骤 2:构建并部署 nvml-mock

nvml-mock 提供一个虚假的 libnvidia-ml.so、虚拟的 /dev/nvidia* 设备节点以及 PCI 拓扑条目,让 HAMi 的 device-plugin 在节点上看到 8 张 A100 GPU。

2.1 克隆并构建

git clone https://github.com/NVIDIA/k8s-test-infra.git
cd k8s-test-infra
docker build -t nvml-mock:local -f deployments/nvml-mock/Dockerfile .
备注

首次构建会下载基础镜像层,可能需要 5–10 分钟。后续构建会使用 Docker 层缓存。

2.2 加载到 kind

kind load docker-image nvml-mock:local --name nvml-mock-test

2.3 通过 Helm 安装

helm install nvml-mock oci://ghcr.io/nvidia/k8s-test-infra/chart/nvml-mock \
--set image.repository=nvml-mock \
--set image.tag=local \
--wait --timeout 120s

该 Chart 默认配置 A100 配置文件:每节点 8 张 GPU,驱动版本 550.163.01,虚假驱动根目录位于 /var/lib/nvml-mock/driver。这个驱动根路径会在步骤 4 中传给 HAMi。

2.4 验证 GPU 发现

kubectl get node ${NODE_NAME} \
-o custom-columns=NAME:.metadata.name,GPU_PRESENT:.metadata.labels.nvidia\\.com/gpu\\.present

预期输出:

NAME GPU_PRESENT
nvml-mock-test-control-plane true

步骤 3:基于 main 分支构建 HAMi

main 分支包含一个修复:当未启用 MIG 时,阻止调用 nvidia-mig-parted。从源码构建可确保该修复已包含在内,无需等待正式发布版本。

3.1 克隆并初始化子模块

cd ~
git clone https://github.com/Project-HAMi/HAMi.git
cd HAMi
git submodule update --init --recursive

3.2 构建 Docker 镜像

docker build -t hami:local -f docker/Dockerfile .
备注

HAMi 使用三阶段 Dockerfile:Go 构建阶段、CUDA 库构建阶段以及最终运行时阶段。首次构建需要数分钟,因为它要拉取 CUDA 基础镜像;后续运行会使用层缓存。

3.3 加载到 kind

kind load docker-image hami:local --name nvml-mock-test

调度器和 device-plugin 二进制文件都打包在单个 hami:local 镜像中。


步骤 4:部署 HAMi

4.1 通过 Helm 安装

helm install hami ./charts/hami \
-n kube-system \
--set devicePlugin.image.repository=hami \
--set devicePlugin.image.tag=local \
--set scheduler.image.repository=hami \
--set scheduler.image.tag=local \
--set devicePlugin.nvidiaDriverRoot=/var/lib/nvml-mock/driver \
--set scheduler.kubeScheduler.imageTag=v1.35.0

devicePlugin.nvidiaDriverRoot 让 HAMi 指向由 nvml-mock 安装的虚假驱动库。

4.2 给节点打标签

注意

device-plugin 启动前必须执行此操作 HAMi device-plugin DaemonSet 的 NODE SELECTOR 为 gpu=on。如果没有这个标签,DESIRED 会保持为 0,不会调度任何 Pod,也不会注册任何 GPU。

kubectl label node ${NODE_NAME} gpu=on

确认 DaemonSet 现在调度了一个 Pod:

kubectl -n kube-system get daemonset hami-device-plugin

预期输出:

NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
hami-device-plugin 1 1 0 1 0 gpu=on 4m22s

4.3 设置 NVML 设备发现策略

kubectl -n kube-system set env daemonset/hami-device-plugin \
-c device-plugin \
DEVICE_DISCOVERY_STRATEGY=nvml

这告诉 device-plugin 通过 NVML API 枚举 GPU,而不是扫描 /dev。否则插件默认使用基于文件的策略,无法看到 nvml-mock 的虚拟设备。

4.4 滚动发布并验证

kubectl -n kube-system rollout restart daemonset/hami-device-plugin
kubectl -n kube-system rollout status daemonset/hami-device-plugin --timeout=120s

检查是否有 MIG 相关错误——空响应即为预期输出:

kubectl -n kube-system logs daemonset/hami-device-plugin -c device-plugin | grep -i mig

检查 Pod 整体状态:

kubectl -n kube-system get pods -l app.kubernetes.io/name=hami

预期输出:

NAME READY STATUS RESTARTS AGE
hami-device-plugin-lbctx 1/2 CrashLoopBackOff 6 9m24s
hami-scheduler-7858c744cc-7pb79 2/2 Running 0 13m
备注

vgpu-monitor sidecar 会崩溃,因为它需要真实的 GPU 监控基础设施。device-plugin 容器运行正常——此处 1/2 是预期状态,不影响 GPU 调度。


步骤 5:验证 GPU 资源

HAMi 将每张物理 GPU 切分成 10 个虚拟槽位。节点有 8 张物理 GPU,因此应该对外提供 80 个可分配的虚拟 GPU。

kubectl describe node ${NODE_NAME} | grep nvidia.com/gpu

预期输出:

nvidia.com/gpu.present=true
nvidia.com/gpu: 80
nvidia.com/gpu: 80
nvidia.com/gpu 0 0

CapacityAllocatable 都显示 80,确认 device-plugin 已注册全部虚拟 GPU 槽位。最后一行是 Allocated resources 表——当前为 0,因为还没有 Pod 申请 GPU。


步骤 6:测试基础 GPU 调度

部署一个最小化的 Pod,申请一张 GPU。CUDA_DISABLE_CONTROL=true 阻止 HAMi 注入的 CUDA shim 尝试真实设备访问:

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: gpu-test-1
spec:
containers:
- name: sleep
image: busybox
command: ["sleep", "3600"]
env:
- name: CUDA_DISABLE_CONTROL
value: "true"
resources:
limits:
nvidia.com/gpu: 1
EOF

等待 Pod 运行:

kubectl get pod gpu-test-1 -w

预期输出:

NAME READY STATUS RESTARTS AGE
gpu-test-1 1/1 Running 0 9s

验证分配注解:

kubectl describe pod gpu-test-1 | grep vgpu-devices-allocated

预期输出:

hami.io/vgpu-devices-allocated: GPU-12345678-1234-1234-1234-123456780006,NVIDIA,40960,100:;

注解格式为 <UUID>,<厂商>,<显存MiB>,<算力>。A100 GPU 有 40960 MiB 显存——看到这个注解即确认调度器分配并记录了一个虚拟 GPU。


步骤 7:测试 GPU 共享(时间片)

再部署三个 Pod,每个申请 1 张 GPU:

for i in 2 3 4; do
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: gpu-test-$i
spec:
containers:
- name: sleep
image: busybox
command: ["sleep", "3600"]
env:
- name: CUDA_DISABLE_CONTROL
value: "true"
resources:
limits:
nvidia.com/gpu: 1
EOF
done
注意

循环内请使用 <<EOF,而不是 <<'EOF' 单引号分隔符会抑制 shell 展开。$i 不会被替换,三个 Pod 都会得到相同的名字。

验证所有 Pod 都在运行:

kubectl get pods | grep gpu-test

预期输出:

gpu-test-1 1/1 Running 0 3m19s
gpu-test-2 1/1 Running 0 10s
gpu-test-3 1/1 Running 0 10s
gpu-test-4 1/1 Running 0 9s

四个 Pod 并发运行在 80 个虚拟 GPU 槽位的资源池上。调度器通过各自独立的 vgpu-devices-allocated 注解独立跟踪每次分配。


步骤 8:测试显存和算力限制

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: gpu-limits
spec:
containers:
- name: sleep
image: busybox
command: ["sleep", "3600"]
env:
- name: CUDA_DISABLE_CONTROL
value: "true"
resources:
limits:
nvidia.com/gpu: 1
nvidia.com/gpumem: "10"
nvidia.com/gpucores: "30"
EOF
信息

资源限制格式 nvidia.com/gpumem 接受以 MiB 为单位的绝对值——"10" 表示 10 MiB。nvidia.com/gpucores: "30" 表示在所选 GPU 上申请 30 个计算核心。

验证分配:

kubectl describe pod gpu-limits | grep vgpu-devices-allocated

预期输出:

hami.io/vgpu-devices-allocated: GPU-12345678-1234-1234-1234-123456780002,NVIDIA,10,30:;

注解记录了 10 MiB 和 30 个核心——正是所申请的值。


步骤 9:测试百分比显存申请

除了固定的 MiB 值,nvidia.com/gpumem-percentage 让你申请 GPU 总显存的一个比例。在 A100(40960 MiB)上,申请 30% 会分配约 12288 MiB。

提示

为什么要用百分比分配?当你想让工作负载在不同 GPU 型号上按比例扩展,而不必硬编码绝对大小时,这非常有用。

创建 Pod:

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: gpu-mem-30pct
spec:
containers:
- name: sleep
image: busybox
command: ["sleep", "3600"]
env:
- name: CUDA_DISABLE_CONTROL
value: "true"
resources:
limits:
nvidia.com/gpu: 1
nvidia.com/gpumem-percentage: "30"
EOF

等待 Pod 进入 Running 状态:

kubectl get pod gpu-mem-30pct -w

预期输出:

NAME READY STATUS RESTARTS AGE
gpu-mem-30pct 1/1 Running 0 8s

查看分配注解:

kubectl get pod gpu-mem-30pct \
-o jsonpath='{.metadata.annotations.hami\.io/vgpu-devices-allocated}'

预期输出:

GPU-12345678-1234-1234-1234-123456780003,NVIDIA,12288,100:;

第三个字段显示 12288 MiB——即 40960 MiB 的 30%——确认调度器正确地将百分比转换为本次分配的绝对显存预算。


步骤 10:测试多 GPU 分配

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: gpu-multi
spec:
containers:
- name: sleep
image: busybox
command: ["sleep", "3600"]
env:
- name: CUDA_DISABLE_CONTROL
value: "true"
resources:
limits:
nvidia.com/gpu: "2"
EOF

验证 Pod 正在运行:

kubectl get pod gpu-multi

预期输出:

NAME READY STATUS RESTARTS AGE
gpu-multi 1/1 Running 0 64s

查看调度器事件:

kubectl describe pod gpu-multi | tail -20

预期输出:

Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 70s hami-scheduler Successfully assigned default/gpu-multi to nvml-mock-test-control-plane
Normal FilteringSucceed 70s hami-scheduler find fit node(nvml-mock-test-control-plane), 0 nodes not fit, 1 nodes fit(nvml-mock-test-control-plane:13.63)
Normal BindingSucceed 70s hami-scheduler Successfully binding node [nvml-mock-test-control-plane] to default/gpu-multi
Normal Pulling 69s kubelet spec.containers{sleep}: Pulling image "busybox"
Normal Pulled 67s kubelet Successfully pulled image "busybox" in 3.548s
Normal Created 67s kubelet Container created
Normal Started 67s kubelet Container started

hami-scheduler 事件——FilteringSucceedScheduledBindingSucceed——确认 HAMi 的调度器处理了这个 Pod,并成功将其绑定到带 2 个 GPU 槽位的节点。

查看完整的 vgpu-devices-allocated 注解
kubectl get pod gpu-multi \
-o jsonpath='{.metadata.annotations.hami\.io/vgpu-devices-allocated}'

你会看到两个以分号分隔的设备条目,每个对应一个已分配的 vGPU 槽位。


已验证功能总结

功能测试 Pod如何验证
基础 GPU 调度gpu-test-1注解显示 1 个 vGPU UUID + 40960 MiB
GPU 共享(时间片)gpu-test-1gpu-test-44 个 Pod 并发运行
显存限制(gpumemgpu-limits注解显示 10 MiB
算力限制(gpucoresgpu-limits注解显示 30 个核心
百分比显存(gpumem-percentagegpu-mem-30pct注解显示 12288 MiB(A100 的 30%)
多 GPU 分配gpu-multihami-scheduler 事件显示 BindingSucceed
以下功能仍需要真实 GPU
  • 实际 CUDA 程序执行
  • gpumemgpucores 限制的运行时强制执行
  • 真实的 DCGM GPU 指标(温度、利用率)
  • 显存超卖和显存覆盖功能

清理

删除所有测试 Pod:

kubectl delete pod gpu-test-1 gpu-test-2 gpu-test-3 gpu-test-4 \
gpu-limits gpu-mem-30pct gpu-multi

移除 GPU 节点标签:

kubectl label node ${NODE_NAME} gpu-

卸载 HAMi:

helm uninstall hami -n kube-system

卸载 nvml-mock:

helm uninstall nvml-mock

删除 kind 集群:

kind delete cluster --name nvml-mock-test
提示

如果你想保留环境以便继续实验,可以跳过删除集群这一步。


下一步

CNCFHAMi 是 CNCF Sandbox 项目