☰
K8s GPU 调度测试:用 Mokka 模拟设备插件验证链路
2026/10/14 10:52:24 网站建设 项目流程

做 Kubernetes GPU 调度平台的开发,最难熬的不是写代码,而是等一块真实的 GPU。我们组之前做资源调度优化,几个模块都写好了,测试却卡在一台共享的老旧 GPU 服务器上——每次跑用例都要排队,一个用例跑三分钟,排队排一天。后来我们才反应过来:相当一部分测试根本不依赖 GPU 算力,只是想验证 NVIDIA Device-Plugin 是否把资源报上去了、调度器是否按配额分配、资源不足时 Pod 是否留在 Pending 状态。这些场景完全可以在软件层面模拟出来。于是我们引入了 Mokka(nvml-mock)来与 Device-Plugin 做对接,在一块真实 GPU 都没有的节点上,硬是把“GPU 资源”这条链路完整跑通了。这篇文章就从实际项目出发,把对接思路、操作步骤和过程中踩到的坑完整写出来,给同样在搞 K8s GPU 调度的朋友一个参考。

1. 一张“假” GPU 能带来什么:说清楚为什么要折腾 Mokka

1.1 从一次资源排队说起

真实开发中有个很尴尬的现状:跑调度测试的机器比写代码的人还紧张。某次我们要验证“GPU 资源不足时新 Pod 应该保持在 Pending”这个场景,按真机逻辑得先占住一张卡,再提交一个申请 GPU 的 Pod,观察它不调度。听起来很简单,但在共享节点上做这件事就要跟别人协调时间,一等就是一两天。后来有同事提出,这类测试跟算力没关系,本质上只需要让 Kubernetes 相信“节点上有 GPU”就行了,为什么非要真卡?

这句话点醒了我。仔细盘点一下,GPU 平台开发里大量测试属于这一类:验证节点资源上报、验证 Device-Plugin 启动、验证 Pod 调度约束、验证配额与抢占逻辑。这些都是控制面和调度链路的行为,跟 CUDA Kernel 能不能跑、显卡算力多少没有任何关系。只要有一个能在软件层面模拟出“GPU 存在”的组件,这些测试就能在任意一台普通节点上跑。Mokka(nvml-mock)就是干这个的。

1.2 Mokka 能做什么、不能做什么

Mokka 是 NVIDIA 开源的 NVML mock 实现,我把它的能力边界整理成了下面这张表,方便大家做选型判断:

能力维度Mokka(nvml-mock)真实 GPU
设备枚举与信息上报支持,可配置数量和型号支持
显存、利用率、温度等状态读取支持,数据可预设支持
注册为 K8s 可调度资源支持,配合 Device-Plugin支持
CUDA 实际计算不支持支持
生成 /dev/nvidia* 设备节点不生成生成
模拟驱动异常、掉卡、故障恢复一般不支持,需看版本天然发生

这张表帮我省了不少时间。刚开始也有人担心“没有真机验证,后面上了生产环境会不会翻车”。实际上我们的使用很克制:凡涉及算力验证、显存 OOM、驱动故障恢复的场景,一律走真机;凡涉及资源注册、调度约束、配额限制的场景,全在 Mokka 模拟环境里跑。这套分工执行了半年,效果非常稳定。

2. 原理必须先懂:NVML、Device Plugin 与 Mokka 三者之间怎么分工

2.1 NVML 是 GPU 管理的事实标准

NVML 的全称是 NVIDIA Management Library,是 NVIDIA 提供的一套 C 语言 API 集合。你可以把它理解为 GPU 管理领域的“事实协议”——几乎所有与 GPU 打交道的系统工具都建立在它之上,比如 nvidia-smi、各类监控采集器、GPU 资源调度器。它做的事情包括设备发现、查询设备状态、读取温度功耗、控制显存时钟等,通常以动态库 libnvidia-ml.so.1 的形式存在于系统中。

Device-Plugin 之所以依赖 NVML,是因为它需要回答两个最基础的问题:节点上有几块 GPU?每块 GPU 的 UUID、显存、型号是什么?这两个答案都来自 NVML。如果 NVML 层给出的信息是假的、但接口行为是合理的,Device-Plugin 就会以为自己面对的是真实硬件。

2.2 Device Plugin 在 Kubernetes 里扮演什么角色

Kubernetes 自身不认识 GPU,它只认识一种标准化的扩展机制:设备插件协议。kubelet 通过 Unix Socket(默认路径 /var/lib/kubelet/device-plugins/)与插件通信,插件负责把硬件资源转译成 K8s 可调度的资源对象。

NVIDIA Device-Plugin 的工作流程大致如下:启动后初始化 NVML,枚举全部 GPU 设备;通过 gRPC 向 kubelet 注册自己,报告资源名和数量,资源名通常写作 nvidia.com/gpu;kubelet 把资源数量同步到 API Server,调度器便能感知节点上有多少 GPU;当 Pod 申请 GPU 并调度到该节点时,kubelet 再请求 Device-Plugin 返回具体的设备分配列表和环境变量。整个过程里,插件是“GPU 驱动”和“K8s 控制面”之间的翻译官。

2.3 Mokka 切入的位置

Mokka 做的事情很纯粹:实现了 NVML 公开的 C API 函数签名,但内部数据来自内存中的模拟结构,而不是 PCIe 总线上的硬件。比如nvmlDeviceGetCount()返回的不是真实设备数,而是你预设的模拟数量;nvmlDeviceGetUUID()返回的是形如 mock-gpu-0 的模拟字符串。

它切入的关键位置在动态链接层。Device-Plugin 在启动时通过动态链接加载 NVML,如果进程运行环境里有一个提前指定好的库目录,且该目录下存在名为 libnvidia-ml.so.1 的 mock 版本,那么动态链接器就会优先命中 Mokka。用一句通俗的话说:电话线那头的真人被换成了语音机器人,打电话的人完全不知道对面换了对象,因为对话内容始终符合预期。

理解了这一层,后面所有配置就都顺理成章了。我们要做的,无非是保证 mock 库出现在动态链接器能找到的位置。

3. 动手准备:构建模拟库并规划节点环境

3.1 构建 Mokka

构建 Mokka 需要一台 Linux 节点,安装好 CMake 和 gcc 工具链。环境干净的话,编译过程非常快,几乎不会遇到依赖问题。

git clone https://github.com/NVIDIA/nvml-mock.git cd nvml-mock mkdir -p build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j$(nproc)

编译完成后,在 build 目录下能看到以 libnvidia-ml 开头的共享库文件,核心产物通常叫 libnvidia-ml-mock.so。不同版本可能会附带额外的测试程序,不用管它们。

提示:如果你只是想快速验证,也可以跳过独立编译,直接用容器方式打包 mock 库。但我更建议先本地编译一遍,亲眼确认库文件路径和结构。后面排错时,你能少猜不少问题。

3.2 布置库目录与符号链接

我建议在节点上单独建一个目录放 mock 库,比如 /opt/nvidia/mock。这样配置清晰,也方便后续用 hostPath 挂载进 Pod。

mkdir -p /opt/nvidia/mock cp build/libnvidia-ml-mock.so /opt/nvidia/mock/ ln -sf /opt/nvidia/mock/libnvidia-ml-mock.so /opt/nvidia/mock/libnvidia-ml.so ln -sf /opt/nvidia/mock/libnvidia-ml-mock.so /opt/nvidia/mock/libnvidia-ml.so.1

这里有个关键细节:为什么两个符号链接都要建?因为动态链接器在搜索共享库时,依赖的是 SONAME 和文件名,开发链接名和运行时链接名常常不一样。Device-Plugin 底层通过 go-nvml 绑定库加载 NVML,实际查找的文件是 libnvidia-ml.so.1。如果你只放了 libnvidia-ml.so,加载就会失败。这两条链接就是堵住这个坑最直接的手段。

3.3 准备 Kubernetes 节点

Mock 库就绪后,还需要保证 Kubernetes 侧满足基本条件。首先,给测试节点打一个专门的标签,方便 Device-Plugin 的 DaemonSet 精确调度:

kubectl label node simnode-01 gpu-sim=true

其次,确认 kubelet 的 device-plugins 目录存在,并且 kubelet 开启了设备插件特性。主流 Kubernetes 版本默认开启,不需要额外改配置,但在老版本上值得验证一下:

ls -ld /var/lib/kubelet/device-plugins/

最后,确认节点上没有真实的 NVIDIA 驱动库干扰。如果节点曾经装过驱动,系统中可能也存在 libnvidia-ml.so.1。只要我们的 LD_LIBRARY_PATH 优先级排在系统路径之前,一般不会冲突。为了保险起见,测试节点建议选一台真正干净的普通机器。

4. 实战对接:让 Device Plugin 心甘情愿加载 Mokka

4.1 从官方 DaemonSet 改出适配版本

NVIDIA Device-Plugin 以 DaemonSet 形式部署到每个 GPU 节点。我们要做的,是在官方模板基础上做三处修改:加节点选择器、挂载 mock 库目录、注入 LD_LIBRARY_PATH。这是完整的 YAML:

apiVersion: apps/v1 kind: DaemonSet metadata: name: nvidia-device-plugin namespace: kube-system spec: selector: matchLabels: app: nvidia-device-plugin template: metadata: labels: app: nvidia-device-plugin spec: nodeSelector: gpu-sim: "true" tolerations: - key: nvidia.com/gpu operator: Exists effect: NoSchedule containers: - name: nvidia-device-plugin image: nvidia/k8s-device-plugin:latest env: - name: LD_LIBRARY_PATH value: /opt/nvidia/mock volumeMounts: - name: mock-lib mountPath: /opt/nvidia/mock - name: device-plugin mountPath: /var/lib/kubelet/device-plugins volumes: - name: mock-lib hostPath: path: /opt/nvidia/mock - name: device-plugin hostPath: path: /var/lib/kubelet/device-plugins

几个字段值得单独解释。nodeSelector 限制插件只在带 gpu-sim=true 标签的模拟节点上运行,避免在真实 GPU 节点上误用 mock 库。tolerations 容忍了 nvidia.com/gpu 污点,因为实际生产环境里 GPU 节点通常带这个污点。环境变量 LD_LIBRARY_PATH 是整个方案的核心,它让动态链接器优先到 /opt/nvidia/mock 去找 libnvidia-ml.so.1。

4.2 配置模拟参数:设备数量、显存、GPU 名称

Mokka 允许通过环境变量控制模拟行为,我在容器 env 中加入这几个参数:

env: - name: LD_LIBRARY_PATH value: /opt/nvidia/mock - name: NVML_MOCK_DEVICE_COUNT value: "4" - name: NVML_MOCK_MEMORY_SIZE_MB value: "16384"

简单解释一下配置意图。NVML_MOCK_DEVICE_COUNT 决定模拟出几张 GPU,直接决定节点上报的 nvidia.com/gpu 数量,是我最常用的变量。NVML_MOCK_MEMORY_SIZE_MB 控制每张卡的显存大小,主要影响后续排查时的显存相关逻辑。GPU 名称也可以用变量改,但默认值足够,我一般不动。

需要提醒的是,Mokka 不同版本的环境变量命名有过调整。如果你手头的版本不识别这些变量,会用 strings 命令很快确认。

4.3 部署并观察插件启动

应用上述 YAML 后,等待 Pod 进入 Running 状态:

kubectl apply -f nvidia-device-plugin-ds.yaml kubectl get pods -n kube-system -o wide | grep nvidia kubectl logs -n kube-system nvidia-device-plugin-xxxxx

日志里如果能找到 NVML 初始化成功、设备数量为 4 之类的信息,说明 mock 库已经真正生效。至少从代码路径上讲,Device-Plugin 已经相信节点上有 4 块 NVIDIA GPU 了。

5. 从设备注册到 Pod 调度的完整验证

5.1 从插件日志解读设备发现

插件启动后的日志会非常有信息量,核心步骤大致如下:

  • 初始化 NVML:此时如果 mock 库生效,日志里的版本信息就不是真实驱动的版本
  • 枚举设备:会显示发现了多少个设备
  • 注册到 kubelet:向 kubelet 报告资源名和数量
  • 等待分配请求:进入正常监听状态

我习惯用“设备数量是否为预期值”来快速判断 mock 配置是否正确。如果设置的是 4 却只有 2,往往是环境变量没传进去或变量名不对。

5.2 节点 GPU 资源出现

插件注册成功后,节点上的资源状态会立刻变化:

kubectl describe node simnode-01 | grep -A8 "Capacity"

输出中应当出现 nvidia.com/gpu: 4,Allocatable 区也应保持一致。这里有个小知识点:Capacity 是节点资源总量,Allocatable 是调度器实际可分配的配额。如果两处都是 4,说明资源上报成功。

5.3 提交一个真正请求 GPU 的 Pod

资源上报成功只是第一步,真正有意义的是调度链路。我准备了一个最小的测试 Pod,它不跑 CUDA,只验证 GPU 资源能被正常请求和分配:

apiVersion: v1 kind: Pod metadata: name: gpu-request-pod spec: restartPolicy: Never containers: - name: main image: busybox:1.36 command: ["/bin/sh", "-c"] args: ["echo scheduled-to-gpu-node && sleep 10"] resources: limits: nvidia.com/gpu: "1"

这里有个细节:在 Kubernetes 里,GPU 作为扩展资源只能写进 limits,且 requests 会默认等同 limits,所以无需重复写 requests。提交后观察:

kubectl apply -f gpu-request-pod.yaml kubectl get pod gpu-request-pod -o wide

Pod 应当快速进入 Running。如果一直是 Pending,说明调度器认为节点没有足够的 nvidia.com/gpu 资源,需要回到上一步检查节点资源上报。

5.4 用资源配额反向校验模拟数量

调度链路验证还有一种更严格的玩法:反向校验。把模拟数量改成 2,然后一次性提交 3 个各申请 1 块 GPU 的 Pod,预期结果是前两个 Running,第三个 Pending。

这个测试在真实 GPU 节点上做成本极高,但在模拟环境里几乎是零成本。它能验证调度器是否正确读取了节点资源上报,也能验证资源不足时的行为是否符合预期。这类用例非常适合放进 CI 流程,代码变更提交后自动跑一遍,比人工在真机上测效率高得多。

6. 我踩过的坑:模拟环境下独有的故障排查

6.1 “明明挂载了库,为什么加载的还是真驱动”

这个坑我印象最深,现象是:/opt/nvidia/mock 目录挂载了,LD_LIBRARY_PATH 也设置了,但插件日志显示的 NVML 版本还是真实驱动的版本。排查思路是这样的:先进入容器,看进程实际加载了哪个共享库:

kubectl exec -it -n kube-system nvidia-device-plugin-xxxxx -- sh cat /proc/1/maps | grep nvidia-ml

观察发现加载路径根本不是 /opt/nvidia/mock。最终定位到两个原因:一是动态链接器寻找的是 libnvidia-ml.so.1,而目录里只有 libnvidia-ml.so;二是容器镜像内部自带了一版 NVML 库,且被其他依赖间接引用。解决办法就是前面已经提到的那条符号链接命令,再配合 LD_LIBRARY_PATH 的优先级,问题彻底消失。

提示:动态链接器的搜索顺序是 LD_LIBRARY_PATH 优先于系统缓存路径,但前提是文件名和 SONAME 完全匹配。别只做了一半就走人。

6.2 设备节点缺失:Pod 起来了,容器内访问 GPU 失败

调度链路跑通后,有同事兴奋地往 Pod 里塞了一个 nvidia-smi 命令,结果容器内报找不到设备。原因很直接:Mokka 是用户态模拟,不产生内核设备节点。Device-Plugin 在分配阶段返回的 /dev/nvidia0 等路径,在节点上根本不存在,容器自然无法挂载真实设备文件。

这个坑本质上是使用边界没有提前对齐。我现在的做法是:在需求阶段就明确“调度链路验证”和“容器内 GPU 算力验证”是两种不同的测试,前者用 Mokka 模拟环境,后者必须在真机。别试图在模拟环境里装 nvidia-smi 然后期待它输出一张假卡的信息,路径不同,行为也不一致。

6.3 设备数量与环境变量不生效

有次调参,不管怎么改 NVML_MOCK_DEVICE_COUNT,节点上报数量都是 2。第一反应是变量名写错了,但查了文档是对的。最后发现环境变量虽然写进了 DaemonSet 的 env,但 mock 库初始化时读取的并不是当前进程环境里最新的值,而是库里内部缓存的一版默认配置。重新检查库版本后发现,该版本根本不支持通过环境变量改数量,需要换一个更新版本的构建产物。

这个经历告诉我:Mokka 也是持续演进的项目,不同版本能力差距不小。遇到诡异问题时,先 strings 查库:

strings libnvidia-ml-mock.so | grep NVML_MOCK

看一眼到底支持哪些变量,比自己盲猜快得多。

6.4 Kubelet 与插件版本兼容性

还有一次,插件 Pod 一直在 CrashLoopBackOff,kubelet 日志里报设备插件注册失败。原因是测试集群的 Kubernetes 版本比较旧,而插件镜像版本太新,两者用的 Device Plugin API 版本不一致。解法也很直接:换一个与集群版本匹配的插件版本,同时确认 socket 路径一致。

这类兼容性问题在真实 GPU 节点上同样会遇到,但在模拟环境里更容易被误判为“mock 库有问题”。我的建议是,模拟环境也要严格按生产版本的兼容矩阵来选型,不要因为是测试就随手拉 latest。

7. 模拟环境的边界与我的选型建议

7.1 适合用 Mokka 接 Device Plugin 的场景

排过这些坑之后,我基本确定了 Mokka 的适用清单:

  • 调度链路回归测试:资源上报、配额限制、Pending 行为
  • CI/CD 快速验证:每次代码提交后,无 GPU 环境自动跑一遍核心用例
  • 多卡规模模拟:模拟 8 卡、16 卡节点,验证调度器在大规模 GPU 节点下的表现
  • 多团队并行开发:不抢硬件,每个团队一个独立模拟集群

7.2 不适合的场景

相反,下面这些场景我坚持走真机:真正的 CUDA 算力验证;GPU 显存溢出、驱动异常、掉卡恢复等故障演练;性能调优、显存带宽测试;容器运行时与驱动的完整协作链路。Mokka 毕竟只是功能级模拟,不是故障注入器,别指望它模拟驱动阶段的复杂行为。

7.3 落地经验:把模拟配置参数化

搭建一次模拟环境不难,难的是让团队都愿意用它。我最后做的一件事,是把模拟 DaemonSet 的配置参数化,用 Helm 管理起来,GPU 数量和显存大小都变成 values 参数:

gpuSim: enabled: true deviceCount: 4 memoryMB: 16384

这样每个需求分支都能用一套独立参数部署,互不干扰。我还单独划了一个 namespace 叫 gpu-sim,所有模拟相关的组件都放进去,看资源使用情况时一目了然。

最后聊一点个人体会。Mokka 对接 NVIDIA Device-Plugin 这件事,本质上不是用假硬件骗系统,而是把 K8s GPU 调度链路的验证门槛降到了零。对我个人来说,最大的收获是:以后再碰到“没有 GPU 环境”的抱怨,可以先问一句——你到底需要算力,还是只需要验证资源调度?如果是后者,一条模拟链路十分钟就能搭起来。如果你也在做类似的事情,尤其是打算在 CI 里把 GPU 调度用例跑起来,我建议尽早把这套 Mock 配置沉淀到部署模板里。它未必能替代真机,但绝对能让你在真机面前更有底气。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询