基于 Kubernetes 的 GPU 资源调度与成本优化系统实践
最近做了一个 Kubernetes 方向的小项目:GPU 资源调度与成本优化系统。
项目地址:https://github.com/ycx666994/gpu-k8s-scheduler
然后这个项目的目标呢,不是训练模型,也不是写一个复杂的调度器,而是围绕 Kubernetes 原生能力,做一个可以演示 GPU 资源隔离、配额控制、节点亲和性、优先级调度和成本可视化的 MVP。
主要是我想去验证几个问题:
1.多个团队共用 GPU 集群时,怎么避免互相抢资源?
2.GPU 配额超了以后,Kubernetes 是怎么处理的?
3.训练任务和推理任务能不能调度到不同类型的 GPU 节点?
4.没有真实 GPU 的本地 kind 集群,能不能模拟出调度现象?
5.能不能把这些状态导出来,用一个简单 Dashboard 展示?
最终做出来的效果是:用 Namespace、ResourceQuota、Node Affinity、PriorityClass 搭了一套 GPU 调度实验环境,并写了一个本地 Web 仪表盘展示当前集群状态。
那么下面开始整个项目的讲解。
一、项目整体架构
项目整体分成三层:
1. 控制层
使用 Kubernetes YAML 定义 Namespace、ResourceQuota、LimitRange、PriorityClass 和 GPU workload。
2. 集群层
Kubernetes 负责 admission 和 scheduling。这里能观察到两类关键现象:
FailedScheduling:Pod 已创建,但调度器找不到合适节点。
FailedCreate:Pod 还没创建出来,就被 ResourceQuota 拒绝。
3. 可观测层
用 PowerShell 脚本调用 kubectl 导出当前集群状态,再由本地 Web Dashboard 读取 JSON/JS 文件展示。
我画了个架构图,如下图所示:

二、项目目录
项目目录如下图所示:

其中:
k8s/ 放 Kubernetes 资源清单。
scripts/ 放状态查看、导出和清理脚本。
web/ 是本地仪表盘。
docs/ 放报告、架构图和截图。
三、Namespace 隔离设计
项目里设计了三个 Namespace,对应三类 GPU 使用场景:

示例 YAML:
apiVersion: v1
kind: Namespace
metadata:
name: ai-training
labels:
gpu.platform/team: training
这样做的好处是后续所有资源管理都可以按 Namespace 维度展开,包括 GPU 配额、Pod 数量、CPU、内存等。
四、ResourceQuota:限制每个团队能用多少 GPU
GPU 资源比较贵,所以不能让某一个团队无限制申请。
所以我给三个 Namespace 分别设置了 GPU 配额:

以 ai-inference 为例:
apiVersion: v1
kind: ResourceQuota
metadata:
name: inference-gpu-quota
namespace: ai-inference
spec:
hard:
requests.cpu: "32"
requests.memory: 128Gi
limits.cpu: "64"
limits.memory: 192Gi
requests.nvidia.com/gpu: "4"
limits.nvidia.com/gpu: "4"
pods: "40"
这里最关键的是:requests.nvidia.com/gpu: "4"
它表示这个 Namespace 最多只能申请 4 张 GPU。
五、PriorityClass:区分推理和训练优先级
GPU 集群里,不同任务的重要性是不一样的。
在线推理一般比离线训练更敏感。如果资源紧张,生产推理应该优先保证。
所以项目里定义了三个 PriorityClass:

示例:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: gpu-critical-inference
value: 100000
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "Production GPU inference workloads that should win during contention."
在 Pod 里使用时,只需要指定:
priorityClassName: gpu-critical-inference
六、Node Affinity:训练和推理调度到不同节点池
真实 GPU 集群里,通常会有不同类型的 GPU 节点,比如:
A100/H100 更适合训练
低延迟节点更适合在线推理
便宜卡可以跑实验或 batch 任务
我在 kind 节点上用 label 模拟 GPU 节点池:
kubectl label node llm-platform-control-plane accelerator=nvidia-a100 --overwrite
kubectl label node llm-platform-control-plane gpu.platform/pool=inference --overwrite
kubectl label node llm-platform-control-plane topology.kubernetes.io/zone=local-a --overwrite
训练任务要求:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: gpu.platform/pool
operator: In
values:
- training
推理任务要求:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: gpu.platform/pool
operator: In
values:
- inference
这样就可以模拟训练池和推理池的隔离效果。
七、本地 kind 没有 GPU,为什么还能做实验?
这个项目是在本地 kind 集群上做的,没有真实 GPU。开始我也考虑过这个问题:没有 GPU,是不是就没法做 GPU 调度实验?实际做下来发现,还是可以做出很多有价值的现象。
原因是 Kubernetes 调度链路里有多个阶段:
1. Namespace 是否存在
2. ResourceQuota 是否允许
3. Node Affinity 是否匹配
4. 节点是否有足够 CPU/内存/GPU
5. Pod 是否能最终调度
即使没有真实 GPU,也可以观察到:
0/1 nodes are available: 1 Insufficient nvidia.com/gpu.
这说明 Pod 已经通过了前面的约束,最后卡在节点没有 GPU 可分配资源。
八、实验一:Node Affinity 不匹配
当训练 Pod 要求 gpu.platform/pool=training,但节点是 pool=inference 时,会看到:
0/1 nodes are available: 1 node(s) didn't match Pod's node affinity/selector.
这说明调度器首先会检查节点标签是否匹配。
如果标签不匹配,Pod 根本不会进入后续 GPU 资源检查。
九、实验二:节点标签匹配,但没有 GPU
当我把节点标签改成符合推理任务要求后:
kubectl label node llm-platform-control-plane gpu.platform/pool=inference --overwrite
推理 Pod 的失败原因变成:
0/1 nodes are available: 1 Insufficient nvidia.com/gpu.
这就说明 Node Affinity 已经通过了,剩下的问题是 kind 节点没有可分配的 GPU。
这是一个很清晰的调度链路:
Node Affinity 不匹配
↓
修改节点标签
↓
Node Affinity 通过
↓
Insufficient nvidia.com/gpu
十、实验三:ResourceQuota 超限
这个实验比较关键。
当前 ai-inference 已经申请了 3 张 GPU,配额是 4 张:
requests.nvidia.com/gpu: 3/4
然后我再提交一个请求 2 张 GPU 的 Deployment:
resources:
requests:
nvidia.com/gpu: "2"
limits:
nvidia.com/gpu: "2"
结果不是 Pending,而是直接被拒绝:
exceeded quota: inference-gpu-quota,
requested: requests.nvidia.com/gpu=2,
used: requests.nvidia.com/gpu=3,
limited: requests.nvidia.com/gpu=4
这里可以看到 ResourceQuota 和调度器的区别:

这也是我觉得这个项目最有价值的地方之一:它能把 Kubernetes 不同控制点的行为区分清楚。
十一、导出集群状态
为了让项目不只是命令行输出,我写了一个导出脚本:
.\scripts\export-cluster-status.ps1
它会调用 kubectl 收集:
Namespace
ResourceQuota
Pod
Node
Event
然后生成:
web/data/cluster-status.json
web/data/cluster-status.js
仪表盘直接读取 cluster-status.js,这样不需要后端服务,也不用部署数据库。
导出的数据大概长这样:
{
"context": "kind-llm-platform",
"summary": {
"gpuRequested": 5,
"gpuQuota": 12,
"pendingPods": 5,
"estimatedIdleCostPerHour": 12
}
}
十二、本地 Dashboard
项目里有一个简单的 Web Dashboard:
web/index.html
打开后可以看到:
GPU Requested / Quota
Pending Pods
Estimated Idle Cost
Namespace GPU 使用情况
调度失败事件
Quota 拒绝事件
截图如下:

还有一张专门整理实验结果的证据页:

十三、GitHub Actions 校验
项目上传 GitHub 后,我又加了一个简单的 CI:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
它主要用来检查:
1.JavaScript 语法
2.JSON 格式
3.Kubernetes YAML 语法
4.PowerShell 脚本语法
我其实踩了一个小坑:Windows PowerShell 默认写 UTF-8 时可能带 BOM,所以GitHub Actions 里的 Python 解析 JSON 会报:Unexpected UTF-8 BOM
最后我改成无 BOM 写入:
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($jsonPath, $json, $utf8NoBom)
这个问题不大,但挺真实。很多跨平台项目都会遇到类似编码问题。
十四、当前项目的进度
目前这个项目算是一个比较完整的 MVP。
目前大概做了:
Namespace 隔离
ResourceQuota / LimitRange
PriorityClass
Node Affinity 训练池实验
Node Affinity 推理池实验
Quota 超限实验
kubectl 状态导出
本地 Dashboard
项目报告和截图
GitHub Actions CI
我之后打算补充的:
接入真实 GPU 节点
安装 NVIDIA device plugin
接入 DCGM Exporter
接入 Prometheus
展示真实 GPU 利用率
更精确的 GPU 成本计算模型
十五、总结
这个项目最大的收获是:GPU 调度不一定一开始就要写复杂调度器,Kubernetes 原生能力已经能做很多事情。通过 Namespace、ResourceQuota、PriorityClass 和 Node Affinity,可以先搭出一套可解释、可复现的 GPU 资源管理机制。
即使本地没有真实 GPU,也可以用 kind 做出很多关键实验,比如说:
1.节点亲和性不匹配
2.GPU 资源不足
3.Quota 超限拒绝
4.Pending 和 FailedCreate 的区别
总而言之,对学习 Kubernetes 调度、平台工程和 AI Infra 来说,这个项目比较适合作为入门级但有完整链路的实践项目。
项目地址:
https://github.com/ycx666994/gpu-k8s-scheduler
后面我会继续尝试把它接到 Prometheus 和 DCGM Exporter 上,做成一个更接近真实生产环境的 GPU 资源监控和成本优化系统。
欢迎大家评论留言以及欢迎大家来我仓库做开源贡献。谢谢!
更多推荐


所有评论(0)