文章目录
-
- [K8s GPU 调度:从 apply 到跑起来](#K8s GPU 调度:从 apply 到跑起来)
- [1. 先看完整链路](#1. 先看完整链路)
- [2. 第一层:准入控制------还没进调度,先被搜身](#2. 第一层:准入控制——还没进调度,先被搜身)
-
- [2.1 ResourceQuota:配额就是预算](#2.1 ResourceQuota:配额就是预算)
- [2.2 LimitRange:强制声明,不许裸奔](#2.2 LimitRange:强制声明,不许裸奔)
- [2.3 自定义 Admission Webhook:平台自己的土规矩](#2.3 自定义 Admission Webhook:平台自己的土规矩)
- [3. 第二层:调度------选节点比选对象还累](#3. 第二层:调度——选节点比选对象还累)
-
- [3.1 Device Plugin:给调度器配个导盲犬](#3.1 Device Plugin:给调度器配个导盲犬)
- [3.2 Filter:先刷掉不合适的](#3.2 Filter:先刷掉不合适的)
- [3.3 Score:矮子里拔将军](#3.3 Score:矮子里拔将军)
- [3.4 Reserve:先占个座](#3.4 Reserve:先占个座)
- [4. 第三层:kubelet + Device Plugin------把"决定"变成"设备号"](#4. 第三层:kubelet + Device Plugin——把"决定"变成"设备号")
-
- [4.1 Allocate:甲乙方对话现场](#4.1 Allocate:甲乙方对话现场)
- [4.2 分配策略:整卡、MIG、Time-Slicing、MPS](#4.2 分配策略:整卡、MIG、Time-Slicing、MPS)
- [4.3 分配失败:你看到的和实际的不一样](#4.3 分配失败:你看到的和实际的不一样)
- [5. 第四层:容器运行时------把 GPU 焊进容器](#5. 第四层:容器运行时——把 GPU 焊进容器)
-
- [5.1 挂载链](#5.1 挂载链)
- [5.2 NVIDIA_VISIBLE_DEVICES:你只能看到你该看的](#5.2 NVIDIA_VISIBLE_DEVICES:你只能看到你该看的)
- [6. 第五层:GPU Operator------把前四层打包成"一键安装"](#6. 第五层:GPU Operator——把前四层打包成"一键安装")
-
- [6.1 驱动管理:告别手动挡](#6.1 驱动管理:告别手动挡)
- [6.2 节点全生命周期](#6.2 节点全生命周期)
- [7. 常见故障:三个翻车现场](#7. 常见故障:三个翻车现场)
-
- [7.1 故障一:Pod 一直 Pending,Events 为空](#7.1 故障一:Pod 一直 Pending,Events 为空)
- [7.2 故障二:Events 显示 "0/10 nodes are available"](#7.2 故障二:Events 显示 "0/10 nodes are available")
- [7.3 故障三:调度成功了,启动失败了](#7.3 故障三:调度成功了,启动失败了)
- [8. 一句话总结](#8. 一句话总结)

P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看, 传送门https://blog.csdn.net/H1727548
K8s GPU 调度:从 apply 到跑起来
先讲个残酷的事实:你敲下 kubectl apply 的时候,以为自己只是提交了一个 YAML。其实你相当于在 GPU 机房门口按了一下门铃,后面一整条流水线的人开始为你忙活。
这条流水线比外卖平台的链条还长,而且这份"外卖"不能退款------Pod 一旦 Pending,你就只能蹲在 kubectl describe 前面干瞪眼。
今天就把这条链路从头捋一遍。一共五层,每一层都有自己的脾气,少一层,你的 GPU 就只是个价值几十万的摆件。
1. 先看完整链路
一个 Pod 从提交到真正跑起来,大概长这样:
kubectl apply -f pod.yaml
│
▼
┌─────────────────────────────────────────────┐
│ 第一层:准入控制 (Admission) │
│ GPU 请求是否合法?配额是否够? │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 第二层:kube-scheduler 调度 │
│ Filter(过滤节点)→ Score(打分)→ Reserve(预留)│
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 第三层:kubelet + Device Plugin │
│ 分配具体 GPU 设备 → 注入环境变量 │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 第四层:容器运行时 (nvidia-container-runtime) │
│ 挂载 GPU 设备 → 设置 cgroup → 启动容器 │
└─────────────────────────────────────────────┘
│
▼
容器内可以调用 CUDA → nvidia-smi 看到 GPU
看懂这张图,你就明白了一件事:GPU 不是 K8s 天生认识的,是人家一层层"抬"上来的。跟公司里推诿扯皮的流程很像,区别是------它们真的能把事办成。
2. 第一层:准入控制------还没进调度,先被搜身
Pod 进了 API Server,别急着上桌,先过安检。
这一层专门干一件事:在 Pod 还没被持久化之前,先检查它是不是个"正经 Pod"。
2.1 ResourceQuota:配额就是预算
yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-ml-gpu
namespace: team-ml
spec:
hard:
requests.nvidia.com/gpu: "16"
limits.nvidia.com/gpu: "16"
规则很简单:你租户申请的总 GPU 数超过 16 张,API Server 直接拒单,连排队的资格都不给。
像不像你刷爆信用卡?银行连催收短信都懒得给你发,直接冻结。
2.2 LimitRange:强制声明,不许裸奔
yaml
apiVersion: v1
kind: LimitRange
metadata:
name: gpu-limits
namespace: team-ml
spec:
limits:
- type: Container
min:
nvidia.com/gpu: "1" # 必须至少声明 GPU
max:
nvidia.com/gpu: "8" # 单 Pod 最多 8 张
LimitRange 的存在意义,是防两类人:
- 忘了声明 GPU 的------上了车才发现没买票。
- 喝多了声明 1000 张 GPU 的。这种需求一般出现在两种情况:跑大模型的,或者刚被裁的。
2.3 自定义 Admission Webhook:平台自己的土规矩
到这一层,GPU 平台就可以玩一些自己的校验了:
- 检查 Pod 是不是既声明了 GPU 又写了 nodeName------想绕过调度器直接点菜?没门。
- 根据 GPU 型号自动注入 toleration,减轻用户心智负担。
- 强制 GPU Pod 必须 limit=request,GPU 不支持超分,谁也别想白嫖。
- 检查选的 GPU 型号在集群里有没有货。
这里头最贴心的是第二条:自动帮你把"请假条"提前写好,省得每次都被 taint 拦在门外。像不像你妈提前帮你把行李收拾好,你只管出门挨骂。
3. 第二层:调度------选节点比选对象还累
3.1 Device Plugin:给调度器配个导盲犬
kube-scheduler 不会自己扫描 PCIe 总线,它就是个瞎子。全靠 Device Plugin 这个导盲犬跟它报数:这个节点有 8 张卡,型号 A100,显存 80G。
yaml
$ kubectl get node gpu-node-1 -o yaml
status:
capacity:
cpu: 64
memory: 512Gi
nvidia.com/gpu: 8 # Device Plugin 上报
allocatable:
nvidia.com/gpu: 8 # 可分配数 = capacity - 系统预留
顺便说一句,GPU 型号、显存这些信息也不是 Device Plugin 报的,是 gpu-feature-discovery 打的 label。这俩一个报数、一个贴标签,配合得像相声里的捧哏逗哏,谁也不抢谁的活。
3.2 Filter:先刷掉不合适的
Filter 检查清单(针对 GPU Pod):
1. nvidia.com/gpu 资源够吗?
Node.allocatable[nvidia.com/gpu] - Node.requested[nvidia.com/gpu] ≥ Pod.request
2. Taint 匹配吗?
GPU 节点通常有 taint: nvidia.com/gpu=true:NoSchedule
Pod 必须有对应的 toleration,否则被挡
3. NodeSelector / NodeAffinity 匹配吗?
Pod 要求 nvidia.com/gpu.product=A100
节点 label 必须精确匹配
4. CPU/内存/临时存储够吗?同普通 Pod,没有特殊逻辑
5. PodAffinity / PodAntiAffinity 满足吗?
分布式训练 Pod 通常要求 PodAntiAffinity 分散到不同节点
Filter 就是相亲软件里的左滑右滑:GPU 不够,左滑;taint 没 toleration,左滑;label 不匹配,左滑。一个 Pod 的恋爱,全在这一层结束了。
分布式训练这里有个狠要求:四个 Pod 必须分到四个不同节点。
yaml
apiVersion: v1
kind: Pod
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
training-job: llama-70b-epoch3
topologyKey: kubernetes.io/hostname # 同节点只能有一个
不是它们关系不好,是 NVLink 就一条。抢带宽比抢最后一块排骨还狠。
3.3 Score:矮子里拔将军
过了 Filter 的节点进入打分环节,0 到 100,谁分高选谁。
CPU 集群的哲学是共同富裕:资源用得越少的节点分越高,Pod 撒得到处都是,谁也别饿着。
GPU 集群不行,GPU 的哲学是钱要花在刀刃上:卡要堆在一个节点上,把整台空闲机器留给后来的多卡任务。
yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: gpu-scheduler
plugins:
score:
enabled:
- name: NodeResourcesFit
weight: 10
pluginConfig:
- name: NodeResourcesFit
args:
scoringStrategy:
type: MostAllocated # 对 GPU 资源用 Bin Packing
resources:
- name: nvidia.com/gpu
weight: 10 # GPU 权重最高
- name: cpu
weight: 1
- name: memory
weight: 1
看这个配置,GPU 权重 10,CPU 和内存权重 1。调度器的偏心肉眼可见,像极了老师只给主科划重点。
碎片化的痛,经历过的人都懂:每个节点都被占走一两张卡,你手上有个 8 卡任务,放眼全集群愣是凑不齐------就像你想买三室一厅,结果满城都是只剩一间房的楼盘。
3.4 Reserve:先占个座
# 调度器缓存(Reserve 后):
Node gpu-a100-01:
nvidia.com/gpu: 8 allocatable, 6 requested(含当前 Pod 占的 2 张)
# 实际节点上:GPU 还是 8 张全空闲(kubelet 还没收到指令)
注意,这里只是调度器在自己的小本本上记了一笔,节点上的 GPU 其实还是全空闲的。
这个机制叫乐观锁,翻译成人话就是:调度器觉得这事稳了,但最终成不成,得看 kubelet 那边翻台。
万一等位期间 GPU 被别人抢了,Pod 就会进入 UnexpectedAdmissionError------你约好了人,到地方人家跟你说:我已经跟别人走了。
4. 第三层:kubelet + Device Plugin------把"决定"变成"设备号"
4.1 Allocate:甲乙方对话现场
调度器把 Pod 绑定到节点后,kubelet 开始干活。对 GPU 资源,它会调 Device Plugin 的 Allocate() gRPC 接口,现场大概是这样的:
kubelet:
"DevicePlugin,这个 Pod 请求了 nvidia.com/gpu: 2,
给我分配 2 张可用的 GPU"
nvidia-device-plugin:
1. 查看本节点哪些 GPU 还没被分配
2. 选出 2 张 GPU(如 GPU-0 和 GPU-3)
3. 返回:
- 设备路径: /dev/nvidia0, /dev/nvidia3
- 环境变量: NVIDIA_VISIBLE_DEVICES=GPU-xxx,GPU-yyy
- 挂载点: /usr/local/nvidia 等驱动文件路径
这段对话是整条链路里最像甲乙方的一环:一个说"给我 2 张卡",另一个翻翻库存,回一句"GPU-0 和 GPU-3,拿走,别客气"。
这些设备路径和环境变量会被写进容器的 OCI spec,然后传给容器运行时。
4.2 分配策略:整卡、MIG、Time-Slicing、MPS
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: device-plugin-config
data:
config.yaml: |
version: v1
flags:
migStrategy: mixed
sharing:
timeSlicing:
replicas: 8
resources:
- name: nvidia.com/gpu
replicas: 8
这张 ConfigMap 管两件事:MIG 怎么上报,以及要不要把一张卡切成时间片。示例里俩开关都开了,实际按需配一个就行。
| 分配策略 | Pod 侧声明 | 含义 | 隔离级别 |
|---|---|---|---|
| 默认(整卡) | nvidia.com/gpu: 1 |
整张卡 | 物理独占 |
| MIG | nvidia.com/mig-1g.10gb: 1 |
一个 MIG 实例:1/7 算力 + 10GB 显存 | 硬件隔离 |
| Time-Slicing | nvidia.com/gpu: 1 |
逻辑 1/8 张卡(8 来自上面配置的 replicas) | 无隔离 |
| MPS | nvidia.com/gpu: 1 |
共享 GPU 算力 | 显存不隔离 |
MIG 是隔断房,每间都有自己的锁和门,切完就是真隔离,故障都互不影响;Time-Slicing 是合租客厅,谁都能进,性能全靠自觉。
这里有个特别容易踩的坑:MIG 的资源名(比如 nvidia.com/mig-1g.10gb)不是 ConfigMap 写出来的。插件不造名字,只翻译硬件现状------像前台只负责念号,不负责给你造号。
那资源名哪来的?硬件上切出来的。
nvidia-smi -mig-mode 1 # 开启 MIG 模式(需重启 GPU/驱动后生效)
nvidia-smi mig -cgi 1g.10gb,1g.10gb -C # 创建实例
MIG 要用起来,得打通三层,缺一不可:
| 层 | 在哪配 | 做什么 |
|---|---|---|
| ① 硬件层 | GPU 节点上 nvidia-smi | 开 MIG 模式、建实例------资源名真正的"来源"在这 |
| ② 插件层 | ConfigMap 的 migStrategy | 只决定插件怎么上报,不产生资源名 |
| ③ 调度层 | Pod 的 resources.requests | 按资源名请求,与 ① 建出来的实例对得上才能调度 |
还有个经典疑问:为什么 MIG 的 1g 是 1/7,Time-Slicing 却是 1/8?
因为 7 是硬件写死的。A100 的计算部分按物理单元组边界切,上限就是 7 份,老祖宗定的规矩,改不了;而 8 是你 ConfigMap 里自己写的,想改 4、改 16 都行,跟外卖备注一样自由。
显存又是另一套独立的切片:40GB 的卡每档 5GB,80GB 的卡每档 10GB。所以同样是 1g 算力,40GB 卡上叫 1g.5gb,80GB 卡上叫 1g.10gb,两段拼一起才是完整资源名。
写 Pod 之前先 nvidia-smi mig -lgi 看一眼这张卡上实际有什么,别凭记忆写。不然调度器会用一个 Pending 教你做人。
4.3 分配失败:你看到的和实际的不一样
# kubelet 日志中分配失败的典型输出:
"Failed to allocate GPU: no available GPUs meeting request:
requested 2 GPUs with label nvidia.com/gpu.product=A100,
found 1 available"
典型的抢单事故:调度器刚说有 2 张,kubelet 真去拿的时候只剩 1 张。其他 Pod 在调度窗口内先下手了。
像极了双十一抢购:购物车里有,结算时没了。
遇到这种情况不用手动干预,调度器会自动重新调度,而且重试间隔会越来越长------像追人被拒后自我安慰的冷静期。
5. 第四层:容器运行时------把 GPU 焊进容器
5.1 挂载链
K8s 默认用 containerd 做容器运行时,containerd 通过 runc 创建容器。GPU 挂载的关键是 nvidia-container-runtime------它是 runc 的一个包装:
kubelet
→ CRI Plugin (containerd)
→ containerd-shim
→ nvidia-container-runtime ← 拦截容器创建
→ nvidia-container-runtime-hook ← OCI prestart hook
→ 调用 libnvidia-container ← 挂载 GPU 设备 + 驱动库
→ runc 启动容器
nvidia-container-runtime-hook 在容器启动前偷偷干了一堆活,自动注入这些东西:
# 容器内被自动注入:
/dev/nvidia0 ← GPU 设备文件
/dev/nvidiactl ← NVML 控制设备
/dev/nvidia-modeset ← 显示模式内核驱动
/dev/nvidia-uvm ← 统一虚拟内存
/usr/lib/x86_64-linux-gnu/libcuda.so ← CUDA 驱动库
/usr/lib/x86_64-linux-gnu/libnvidia-ml.so ← NVML 库
NVIDIA_VISIBLE_DEVICES=GPU-xxx,GPU-yyy # 容器内 nvidia-smi 只看得到分配的 GPU
NVIDIA_DRIVER_CAPABILITIES=compute,utility
你什么都没做,容器里就什么都有了------比亲妈还周到。
5.2 NVIDIA_VISIBLE_DEVICES:你只能看到你该看的
# 节点上有 8 张 GPU
$ kubectl exec gpu-pod -- nvidia-smi -L
GPU 0: NVIDIA A100-SXM4-80GB (UUID: GPU-xxx) # 只看到分配的 2 张
GPU 1: NVIDIA A100-SXM4-80GB (UUID: GPU-yyy)
# 其余 6 张不可见
节点上有 8 张卡,容器里 nvidia-smi 只显示分给你的那 2 张。
这不是抠门,这叫权限边界。像极了老板只让你看到你该看的报表------你不知道的,就当你不需要知道。
6. 第五层:GPU Operator------把前四层打包成"一键安装"
前面说了这么多,你要是手动挨个部署,光组件就能装一晚上。NVIDIA GPU Operator 用一个 Helm Chart 搞定整个 GPU 节点的生命周期管理:
bash
helm install gpu-operator nvidia/gpu-operator \
--namespace gpu-operator --create-namespace \
--set driver.enabled=true \ # 自动安装 GPU 驱动(DaemonSet)
--set mig.strategy=mixed \ # 启用 MIG
--set devicePlugin.config.name=device-plugin-config
一条命令下去,组件清单长这样:
| 组件 | 类型 | 作用 |
|---|---|---|
| nvidia-driver-daemonset | DaemonSet | 在 GPU 节点上安装/更新驱动 |
| nvidia-device-plugin | DaemonSet | 向 kubelet 注册 GPU 扩展资源 |
| gpu-feature-discovery | DaemonSet | 自动检测 GPU 型号并打 Node Label |
| nvidia-dcgm-exporter | DaemonSet | 导出 GPU 指标(温度/功率/ECC 错误等) |
| nvidia-mig-manager | DaemonSet | MIG 分区策略管理 |
| nvidia-operator-validator | Job | 部署后验证 GPU 环境是否可用 |
| sandbox-validator | DaemonSet | 持续运行时验证(GPU 健康检查) |
6.1 驱动管理:告别手动挡
传统方式:
新 GPU 节点上架 → SSH 登录 → apt install nvidia-driver-535
→ 版本不对 → 卸载重装 → 重启
→ 90% 的节点上架时间花在驱动问题上
GPU Operator 方式:
新 GPU 节点上架 → 打 label: nvidia.com/gpu.present=true
→ Operator 自动派 Driver DaemonSet Pod 到该节点
→ Pod 内安装/编译驱动 → 自动加载内核模块 → Done
→ 不需要 SSH,不需要重启(大部分情况下)
以前上架一台 GPU 节点,90% 的时间耗在装驱动上:SSH 登录、apt install、版本不对、卸载重装、重启。
现在呢?打个 label,Operator 自动干活。从手动挡到自动挡,驾校教练看了都想转行。
6.2 节点全生命周期
节点上架:
1. 服务器到货,装好 OS 和 K8s
2. 打 label: nvidia.com/gpu.present=true
3. GPU Operator 自动部署驱动 + Device Plugin + DCGM
4. nvidia-operator-validator 跑验证
5. 验证通过 → node 自动变为 Ready,GPU 资源可分配
节点运行中:
1. DCGM Exporter 持续采集指标
2. Node Problem Detector 监听 DCGM 指标 → 异常自动打 taint
3. MIG Manager 监控 MIG 配置(被人手动改了 → 自动恢复)
节点下架:
1. kubectl drain gpu-node-1 → Pod 迁移到其他 GPU 节点
2. drain 完 → 节点可以安全关机/送修
上架、运行、下架,三个阶段的活儿它全包了,连 MIG 配置被人手动改了都会偷偷改回来------比物业管家还尽责。
7. 常见故障:三个翻车现场
7.1 故障一:Pod 一直 Pending,Events 为空
$ kubectl describe pod training-pod-0
Events: <none>
# 调度器根本没考虑这个 Pod
Events 是空的,说明调度器压根没考虑过你这个 Pod。
比被拒绝更伤人的是根本没被看见------像极了相亲对象已读不回。
# 检查 Device Plugin 是否正常运行
kubectl get pods -n gpu-operator -l app=nvidia-device-plugin
# 检查节点上是否有 nvidia.com/gpu
kubectl get nodes -o custom-columns=NAME:.metadata.name,GPU:.status.capacity.'nvidia\.com/gpu'
# 如果没有 → 检查 Device Plugin 日志
kubectl logs -n gpu-operator nvidia-device-plugin-xxx
# "NVML initialized. Number of devices: 0" → 驱动问题,检查驱动是否加载
大概率是 Device Plugin 没起来,或者驱动没加载。日志里那句 Number of devices: 0 就是典型症状:显卡明明插着,驱动就是不认。
7.2 故障二:Events 显示 "0/10 nodes are available"
Events:
Type Reason Age Message
---- ------ ---- -------
Warning FailedScheduling 2m 0/10 nodes are available:
3 node(s) had untolerated taint {nvidia.com/gpu: true}
4 node(s) didn't match Pod's node affinity/selector
3 Insufficient nvidia.com/gpu
这条报错特别好,它把每个节点拒绝你的原因都列得清清楚楚。
比前任的分手理由还详细,而且人家是讲道理的:
- 3 个节点被 taint 挡住 → Pod 缺少 toleration,请假条没写。
- 4 个节点 label 不匹配 → nodeSelector 或 GPU 型号要求不对,门牌号找错了。
- 3 个节点 GPU 不够 → 空闲数 < 请求数,卡确实不够分。
7.3 故障三:调度成功了,启动失败了
Events:
Type Reason Age Message
---- ------ ---- -------
Warning UnexpectedAdmissionError 5s failed to allocate GPU devices
这是调度器和 kubelet 之间的状态不一致:调度器算的时候有 2 张空闲,kubelet 分配的时候已经被别人抢了。
解决方式也很硬核:不用管。kube-scheduler 会自动重新调度,重试间隔会越来越长,就当它自己在冷静。
8. 一句话总结
Kubernetes 的 GPU 调度不是单一机制,是五层协同:准入控制管合法性和配额,Device Plugin 管上报,调度器管 Filter + Score 选节点,kubelet 管分配具体设备号,容器运行时管挂载。
GPU Operator 再把驱动安装、Device Plugin 部署、MIG 管理、健康监控全打包进 Helm Chart,节点上架打一个 label 就完事。
下次你的 GPU Pod 再 Pending,先别急着骂人。想想这条链路------它比你想象中勤奋,只是偶尔运气不太好。
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看,传送门https://blog.csdn.net/H1727548