主题
第四部分 Kubernetes GPU 调度与管理(第 13~17 章)
第三部分我们学会了 MIG、MPS、时间片共享等单机 GPU 虚拟化技术。但生产环境 GPU 服务器动辄几十上百台,靠人工逐台
docker run --gpus不现实。本部分进入 Kubernetes 世界:K8s 如何"看见"GPU(Device Plugin)、如何一键装好整套 GPU 软件栈(GPU Operator)、如何在集群中使用 MIG、如何实现共享超卖(时间片与 HAMi),以及如何用 Volcano 批调度器支撑分布式训练。
前置知识:默认你已掌握 K8s 基础(Pod、Deployment、DaemonSet、Node 管理、resources requests/limits),并了解 MIG、MPS、时间片共享的概念(第三部分已讲解)。
第 13 章 Device Plugin 机制
本章目标
- 理解 K8s 如何"发现"并暴露 GPU 资源(Extended Resource)
- 讲清 Device Plugin 与 kubelet 的 gRPC 交互流程
- 手动部署 NVIDIA k8s-device-plugin,编写申请 GPU 的 Pod,并实验多 Pod 抢卡与 Pending 排查
13.1 Extended Resource:GPU 是如何出现在节点上的
先看现象:在装好 GPU 软件栈的节点上执行 kubectl describe node gpu-node-01,你会看到:
Capacity:
cpu: 64
memory: 512Gi
nvidia.com/gpu: 8 # ← GPU 在这里!
pods: 110
Allocatable:
nvidia.com/gpu: 8
...这是什么? nvidia.com/gpu 是一种 Extended Resource(扩展资源)。CPU 和内存是 K8s 内置认识的资源,GPU、RDMA 网卡这类硬件 K8s 本身并不认识,它提供了一套扩展机制:
- 节点上的组件(就是 Device Plugin)向 kubelet 汇报:"我这里有 8 个叫
nvidia.com/gpu的资源" - kubelet 把它写入 Node 对象的
status.capacity,上报给 API Server - 调度器看到 Pod 申请
nvidia.com/gpu: 1时,只把它调度到剩余 GPU ≥ 1 的节点
重要规则:
- 扩展资源名必须是
域名/资源名格式(如nvidia.com/gpu) - 扩展资源只支持整数,不能申请
0.5——这是 K8s 层面的硬限制,共享要靠其他手段(第 16 章) - 调度器只负责数数,它不知道也不关心"这块 GPU 还有多少显存空闲"
13.2 Device Plugin 工作原理
为什么需要 Device Plugin? 调度器只决定"Pod 去哪个节点",但容器启动时还需要把 /dev/nvidia0 等设备文件、驱动库挂载进容器,并设置 NVIDIA_VISIBLE_DEVICES。这些"节点本地"的操作由 kubelet 完成,kubelet 通过 Device Plugin 这套标准接口把具体工作委托给厂商插件。
插件(如 NVIDIA k8s-device-plugin)以常驻 Pod 运行在每个 GPU 节点上,通过 Unix Socket 与 kubelet 用 gRPC 通信。完整流程:
约定目录:/var/lib/kubelet/device-plugins/
① 插件启动,创建 nvidia.sock
② 插件 → kubelet 的 kubelet.sock 调用 Register:"我管理 nvidia.com/gpu"
③ kubelet 连接 nvidia.sock,调用 ListAndWatch(长连接):
插件持续推送设备列表 [gpu0...gpu7] 及健康状态
④ kubelet 更新 Node status.capacity:nvidia.com/gpu: 8
⑤ 用户提交 Pod(limits: nvidia.com/gpu: 1),调度器把 Pod 绑定到该节点
⑥ kubelet 启动 Pod 前,调用插件的 Allocate(gpu0),插件返回:
设备文件 /dev/nvidia0、/dev/nvidiactl、/dev/nvidia-uvm
环境变量 NVIDIA_VISIBLE_DEVICES=<GPU-uuid>
⑦ kubelet 把信息交给 containerd,由 nvidia-container-toolkit 完成实际挂载,容器启动
⑧ GPU 掉卡/故障时,插件经 ListAndWatch 上报 Unhealthy,
kubelet 将 Allocatable 减 1,新 Pod 不再调度到该卡需要记住的三个 gRPC 接口:Register(插件 → kubelet,上线时注册资源名)、ListAndWatch(kubelet → 插件,长连接推送设备列表与健康状态)、Allocate(kubelet → 插件,容器启动前分配设备并返回挂载信息)。
运维视角:节点上
nvidia.com/gpu显示为 0 或消失,90% 是 Device Plugin Pod 没跑起来或注册失败。排查顺序:插件 Pod 状态 → 插件日志 → kubelet 日志。
13.3 手动部署 NVIDIA k8s-device-plugin
生产一般用 GPU Operator(第 14 章)自动部署,但手动部署一遍是理解原理的最佳方式,也是排查 Operator 故障时的必备知识。
前提条件(在 GPU 节点上已完成,对应第二部分内容):驱动已装好(nvidia-smi 正常)、toolkit 已配置好 containerd:
bash
sudo nvidia-ctk runtime configure --runtime=containerd --set-as-default
sudo systemctl restart containerd
# 验证:裸容器能跑 GPU
ctr run --rm --gpus 0 docker.io/nvidia/cuda:12.4.1-base-ubuntu22.04 test nvidia-smi部署 DaemonSet:
yaml
# nvidia-device-plugin.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nvidia-device-plugin-daemonset
namespace: kube-system
spec:
selector:
matchLabels:
name: nvidia-device-plugin-ds
template:
metadata:
labels:
name: nvidia-device-plugin-ds
spec:
priorityClassName: system-node-critical
nodeSelector: # 只调度到 GPU 节点(需预先打该标签)
nvidia.com/gpu.present: "true"
tolerations: # 容忍 GPU 节点上常见的 taint
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
containers:
- name: nvidia-device-plugin-ctr
image: nvcr.io/nvidia/k8s-device-plugin:v0.16.2
volumeMounts: # 挂载与 kubelet 约定的 device-plugins 目录
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
volumes:
- name: device-plugin
hostPath:
path: /var/lib/kubelet/device-plugins部署与验证:
bash
kubectl apply -f nvidia-device-plugin.yaml
# 确认插件 Pod 在每个 GPU 节点上 Running
kubectl get pods -n kube-system -l name=nvidia-device-plugin-ds -o wide
# 确认节点上出现了 GPU 资源
kubectl describe node gpu-node-01 | grep nvidia.com/gpu
# 插件正常注册的日志标志:Registered device plugin for 'nvidia.com/gpu' with Kubelet
kubectl logs -n kube-system -l name=nvidia-device-plugin-ds | grep -i register13.4 Pod 申请 GPU
在 resources.limits 中声明扩展资源即可:
yaml
# gpu-test-pod.yaml
apiVersion: v1
kind: Pod
metadata: {name: gpu-smi-test}
spec:
restartPolicy: Never
containers:
- name: cuda
image: nvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04
command: ["nvidia-smi"]
resources:
limits: {nvidia.com/gpu: 1} # 申请 1 张卡bash
kubectl apply -f gpu-test-pod.yaml
kubectl logs gpu-smi-test # 能看到 nvidia-smi 输出即成功必须知道的规则:
- GPU 只能写在
limits里(只写requests时自动对齐为相同值);不能申请0.5张卡,API Server 直接报错:must be an integer - CPU/内存与 GPU 是独立维度,申请 GPU 不意味着自动获得 CPU/内存,常规资源仍建议显式声明
- 申请 1 张卡,容器里就只能看到 1 张卡(
NVIDIA_VISIBLE_DEVICES隔离)——注意这是"可见性隔离",不是显存/算力配额;整卡资源不会被超卖
13.5 实验:多 Pod 抢卡与 Pending 排查
实验目标:在只有 1 张 GPU 的节点上,观察第 2 个 GPU Pod 的调度行为。
yaml
# gpu-claim.yaml
apiVersion: apps/v1
kind: Deployment
metadata: {name: gpu-hog}
spec:
replicas: 2
selector: {matchLabels: {app: gpu-hog}}
template:
metadata: {labels: {app: gpu-hog}}
spec:
containers:
- name: cuda
image: nvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04
command: ["sleep", "3600"]
resources:
limits: {nvidia.com/gpu: 1}bash
kubectl apply -f gpu-claim.yaml
kubectl get pods -l app=gpu-hog -o wide
# 期望:一个 Running,一个 PendingPending 排查三板斧:
bash
# 1. 看事件(最重要):期望看到 Insufficient nvidia.com/gpu
kubectl describe pod <pending-pod>
# 2. 看每个节点的 GPU 余量
kubectl get nodes -o json | jq '.items[] | {name: .metadata.name,
gpu: .status.allocatable["nvidia.com/gpu"]}'
# 3. 看该节点上 GPU 被谁占了
kubectl describe node <node-name> | grep -A20 "Allocated resources"常见 Pending 原因对照表:
| 现象(Events 信息) | 根因 | 解决 |
|---|---|---|
Insufficient nvidia.com/gpu | 节点 GPU 已分配完 | 等待/扩容/换共享方案(第 16 章) |
didn't match node affinity / untolerated taint | 标签、亲和性或容忍配置错误 | 检查 nodeSelector、label、toleration |
| 无任何事件,一直 Pending | PVC 未绑定 / 调度器异常 | 检查 PVC、scheduler 日志 |
本章小结
- K8s 通过 Extended Resource(
nvidia.com/gpu)感知 GPU,由 Device Plugin 经 kubelet 上报,只支持整数 - Device Plugin 的核心是 gRPC 三步:
Register→ListAndWatch→Allocate,插件真正做的是"上报数量 + 告诉 kubelet 挂载什么" - Pod 通过
resources.limits: nvidia.com/gpu: N申请整卡;排查 Pending 先看kubectl describe pod事件
动手实验
- 删除 Device Plugin DaemonSet,观察节点
nvidia.com/gpu何时消失、重新部署后何时恢复(体会 ListAndWatch 的作用)。 - 提交申请
nvidia.com/gpu: 0.5的 Pod,记录 API Server 的报错原文。 - 申请 2 张卡运行
nvidia-smi -L,与申请 1 张卡对比容器内可见设备数量。
第 14 章 NVIDIA GPU Operator
本章目标
- 说清楚 GPU Operator 解决什么问题、管理哪些组件
- 用 Helm 完成安装与常用参数定制,理解
nvidia.com/*节点标签体系 - 掌握已有驱动时跳过 Operator 装驱动的配置,学会升级与排查驱动 Pod 卡 init 故障
14.1 GPU Operator 是什么:一键管理整套 GPU 软件栈
回顾第 13 章,让一个 GPU 节点能被 K8s 使用需要:装驱动 → 装 toolkit → 改 containerd 配置 → 部署 Device Plugin → 部署 DCGM 监控……几十台节点逐台操作是运维噩梦。GPU Operator 是 NVIDIA 官方的 K8s Operator,把上述所有组件做成"声明式管理":装好 Operator 后,它自动在每个 GPU 节点拉起组件 Pod,完成安装、配置、升级全生命周期管理。
组件清单表(安装后 gpu-operator 命名空间下的典型 Pod):
| 组件 | 作用 |
|---|---|
nvidia-driver-daemonset | 以容器方式在节点上安装/加载 NVIDIA 驱动 |
nvidia-container-toolkit-daemonset | 配置 containerd/Docker 使用 nvidia runtime |
nvidia-device-plugin-daemonset | 向 kubelet 注册 nvidia.com/gpu(第 13 章) |
nvidia-dcgm-exporter | 采集 GPU 指标供 Prometheus 抓取(第六部分) |
nvidia-mig-manager | 按节点标签自动配置 MIG(第 15 章) |
node-feature-discovery (NFD) | 自动探测节点硬件(PCI 设备)并打标签 |
nvidia-operator-validator | 校验整套栈就绪状态,打 nvidia.com/gpu.deploy.* 标签 |
gpu-operator-xxxxx (Deployment) | Operator 本体,调协上述所有组件 |
关键理念转变:驱动也跑在容器里。
nvidia-driver-daemonset是特权容器,启动时在宿主机编译/加载内核模块。驱动版本变成 YAML 里的一行参数,升级驱动 = 改版本号 + 滚动更新。
14.2 Helm 安装 GPU Operator
bash
# 添加 NVIDIA 官方 Helm 仓库并安装(全部组件默认启用)
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update
helm install gpu-operator nvidia/gpu-operator \
--namespace gpu-operator --create-namespace
# 观察组件 Pod 逐个起来(driver → toolkit → device-plugin → validator ...)
# 全部就绪的标志:节点 Capacity 中出现 nvidia.com/gpu
kubectl get pods -n gpu-operator -w
kubectl describe node <gpu-node> | grep nvidia.com/gpu常用 values 参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
driver.enabled | true | 是否由 Operator 安装驱动(已有驱动时设 false) |
driver.version | 随 chart | 指定驱动版本,如 550.127.08 |
toolkit.enabled / toolkit.version | true / 随 chart | 是否安装 toolkit、指定其版本 |
devicePlugin.enabled / nfd.enabled | true | 是否部署 device plugin / node-feature-discovery |
dcgmExporter.enabled | true | 是否部署 DCGM 监控采集器 |
migManager.enabled / mig.strategy | true / single | mig-manager 开关与 MIG 资源暴露策略(第 15 章) |
operator.defaultRuntime | containerd | 集群容器运行时 |
生产定制安装示例:helm install gpu-operator nvidia/gpu-operator -n gpu-operator --create-namespace --set driver.version=550.127.08 --set toolkit.version=v1.17.4-ubuntu20.04 --set mig.strategy=single
14.3 节点标签体系
GPU Operator 就绪后,节点上会出现大量 nvidia.com/ 前缀标签,它们是调度、MIG 配置、组件部署控制的统一语言:
bash
kubectl get node <gpu-node> --show-labels | tr ',' '\n' | grep nvidia.com| 标签 | 示例值 | 含义与用途 |
|---|---|---|
nvidia.com/gpu.present | true | 检测到 NVIDIA GPU,是组件 nodeSelector 依据 |
nvidia.com/gpu.product | NVIDIA-A100-SXM4-80GB | GPU 型号,可按型号调度任务 |
nvidia.com/gpu.count / .memory | 8 / 81920 | GPU 数量 / 单卡显存(MiB) |
nvidia.com/gpu.family | ampere | GPU 架构代次 |
nvidia.com/cuda.driver.major | 550 | 驱动主版本 |
nvidia.com/mig.capable / mig.config | true / all-disabled | 是否支持 MIG / mig-manager 的输入标签(第 15 章) |
nvidia.com/gpu.deploy.driver | true/false | 控制该节点是否由 Operator 部署驱动(14.4 节) |
NFD 的作用:这些"硬件探测类"标签不是手打的,而是 NFD 自动探测 PCI 设备(NVIDIA 厂商 ID 0x10de)、读取驱动信息后生成的,GPU Operator 据此决定"这台节点需不需要装 GPU 组件"。按型号调度:nodeSelector: {nvidia.com/gpu.product: NVIDIA-A100-SXM4-80GB} 可让任务只用 A100-80G 节点。
14.4 集群中已有驱动时:跳过 Operator 装驱动
很多生产节点驱动已预装(云镜像自带、安全部门统一分发、定制内核),让 Operator 再装一套会冲突,必须跳过:
bash
# 方式一:全局关闭(安装时指定)
helm install gpu-operator nvidia/gpu-operator -n gpu-operator --create-namespace \
--set driver.enabled=false
# 方式二:按节点粒度控制(标签优先于全局配置)
kubectl label node <gpu-node> nvidia.com/gpu.deploy.driver=false同样的模式适用于其他组件:nvidia.com/gpu.deploy.container-toolkit=false、nvidia.com/gpu.deploy.device-plugin=false 等,可精细控制"哪些节点装哪些组件"。
注意:跳过驱动后仍需 Operator 部署 toolkit、device-plugin、dcgm 等组件,整体才能工作。
nvidia-operator-validator会逐项检查并在日志里报告哪一环没就绪。
14.5 升级与故障排查
升级 Operator:
bash
helm repo update
helm search repo nvidia/gpu-operator --versions # 查看新版本
# 升级时带上原有 values,避免参数丢失
helm upgrade gpu-operator nvidia/gpu-operator -n gpu-operator -f my-values.yaml
kubectl get pods -n gpu-operator -w # 滚动观察组件重建驱动 Pod 滚动更新时会短暂驱逐节点上的 GPU Pod,升级前建议先把训练任务 checkpoint 或主动迁移。
驱动 Pod 卡在 Init 的常见原因(GPU Operator 最高频故障):
bash
kubectl get pods -n gpu-operator -l app=nvidia-driver-daemonset -o wide
kubectl logs -n gpu-operator <driver-pod> -c nvidia-driver-ctr --tail=100| 现象/日志关键词 | 根因 | 解决方案 |
|---|---|---|
Could not resolve host / 拉包超时 | 节点无法访问外网下载编译依赖 | 配置代理,或预编译驱动镜像推私有仓库 |
kernel headers not found / gcc 版本不匹配 | 缺内核头文件、编译环境与内核不一致 | 安装 kernel-headers,或用匹配内核的预编译驱动镜像 |
Secure Boot / Key was rejected | 安全启动拒绝未签名内核模块 | 关闭 Secure Boot,或配置 MOK 签名 |
nouveau 相关报错 | 开源 nouveau 驱动未禁用 | 节点禁用 nouveau 并重建 initramfs |
kernel module appears to already be loaded | 节点已有驱动在运行 | 停掉占用进程;已有驱动场景改用 driver.enabled=false |
通用排查顺序:gpu-operator 本体日志 → 各组件 Pod 状态 → 卡住组件的日志 → 节点 nvidia.com/gpu.deploy.* 状态标签 → validator 汇总日志。
本章小结
- GPU Operator 把驱动、toolkit、device-plugin、dcgm、mig-manager 等组件做成声明式管理,一个
helm install交付整套 GPU 软件栈 nvidia.com/*节点标签(多数由 NFD 自动生成)是调度与组件部署控制的统一语言;已有驱动时用driver.enabled=false或节点标签跳过- 驱动 Pod 卡 init 高发原因:无外网、缺内核头、Secure Boot、nouveau 未禁用、已有驱动冲突
动手实验
- 安装 GPU Operator 后,列出
gpu-operator命名空间所有 Pod,逐一对应 14.1 节组件表。 - 查看一个 GPU 节点的全部
nvidia.com/标签,解释其中 5 个的含义。 - 模拟故障:给节点打
nvidia.com/gpu.deploy.driver=false,观察该节点 driver Pod 的终止过程。
第 15 章 MIG 在 Kubernetes 中的使用
本章目标
- 理解 mig-manager "打标签即配置" 的工作方式与内置策略
- 区分 MIG 的 single 与 mixed 两种资源暴露策略
- 掌握变更 MIG 的标准流程(drain → 打标签 → 验证 → uncordon),了解与时间片混用的限制
15.1 mig-manager:用标签驱动 MIG 配置
第三部分讲过,MIG 配置命令(nvidia-smi mig -cgi ...)是节点本地操作。集群中,mig-manager(GPU Operator 组件)把 MIG 配置变成了声明式:只需给节点打标签 nvidia.com/mig.config=<策略名>,mig-manager 就自动完成 MIG 模式切换和实例切分。
内置策略(定义在 gpu-operator 命名空间的 default-mig-parted-config ConfigMap 中,以 A100-40GB 为例):
| 策略名 | 含义 |
|---|---|
all-disabled | 关闭所有 GPU 的 MIG,恢复整卡 |
all-enabled | 开启 MIG 模式,但不创建实例 |
all-1g.5gb / all-2g.10gb / all-3g.20gb | 每卡切成 7×1g.5gb / 3×2g.10gb / 2×3g.20gb |
all-balanced | 均衡切法:2×1g.5gb + 1×2g.10gb + 1×3g.20gb |
也支持自定义 ConfigMap 实现"同一节点不同卡不同切法"(mig-parted 语法),生产最常用 all-* 系列。打标签后 mig-manager 自动执行:驱逐节点 GPU Pod(state=pending)→ 确保无 GPU 客户端占用 → 切 MIG 模式/重建实例 → 重启 device-plugin 重新上报 → 恢复可调度(state=success)。
15.2 MIG 策略:single vs mixed
MIG 切片配好后,device-plugin 怎么暴露给 K8s?两种策略(安装时 --set mig.strategy=single|mixed):
| 对比项 | single(默认,推荐) | mixed |
|---|---|---|
| 资源命名 | 按规格暴露:nvidia.com/mig-1g.5gb 等 | 整卡与切片统一暴露为 nvidia.com/gpu |
| 节点选择 | 直接申请对应资源名 | 用 nodeSelector 按 nvidia.com/gpu.product(如 ...-MIG-1g.5gb)筛选 |
| 适用场景 | 大多数场景,语义清晰 | 整卡与多种切片混在同一集群、用标签区分 |
single 策略下调度 MIG 切片:
yaml
apiVersion: v1
kind: Pod
metadata: {name: mig-inference}
spec:
restartPolicy: Never
containers:
- name: inference
image: nvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04
command: ["nvidia-smi", "-L"]
resources:
limits: {nvidia.com/mig-1g.5gb: 1} # 申请一个 1g.5gb 切片调度成功后,节点 Allocatable 出现 nvidia.com/mig-1g.5gb: 7(单卡切 7 份),容器内 nvidia-smi -L 只能看到分给自己的那一个 MIG 设备。MIG 切片之间是硬件级隔离(显存、SM、L2 缓存都有保障),这是与第 16 章时间片共享的本质区别。
15.3 修改 MIG 配置的标准流程
变更 MIG 切分需要重建 GPU 实例,必须保证节点上没有 GPU 任务在跑,否则 mig-manager 会一直等待(或任务被杀):
bash
NODE=gpu-node-01
# 1. 驱逐业务 Pod(同时 cordon 防止新 Pod 进入)
kubectl drain $NODE --ignore-daemonsets --delete-emptydir-data
# 2. 打标签,声明期望的 MIG 配置
kubectl label node $NODE nvidia.com/mig.config=all-1g.5gb --overwrite
# 3. 观察执行过程(pending → success)
kubectl get node $NODE -L nvidia.com/mig.config.state -w
# 4. 验证:实例已切好 + K8s 资源已更新
ssh $NODE nvidia-smi mig -lgi
kubectl describe node $NODE | grep mig-1g.5gb
# 5. 恢复调度
kubectl uncordon $NODE常见坑:状态卡在 pending(节点还有 GPU Pod 或占用 GPU 的进程没清掉);部分卡开启 MIG 需重启节点才生效;忘记 uncordon 导致节点一直 SchedulingDisabled。
15.4 MIG 与时间片共享混用的注意事项
- device-plugin 的时间片配置对 MIG 资源不生效:time-slicing 只作用于整卡
nvidia.com/gpu,不能对nvidia.com/mig-1g.5gb做 replicas 超分——MIG 切片本身就是最小调度单元,再切分要靠 HAMi 这类方案 - MIG 与整卡并存时(如 8 卡中 2 卡开 MIG),mixed 策略下整卡和切片都叫
nvidia.com/gpu,务必用产品标签区分,否则 Pod 可能拿到不符合预期的资源 - MIG 硬件隔离优于时间片软件复用:有 SLO 要求的在线业务优先 MIG,容忍抖动的开发测试再考虑时间片(决策见 16.5 节);变更 MIG 会重启 device-plugin,期间该节点 GPU 资源短暂消失、排队 Pod 瞬间 Pending,属正常现象
本章小结
- mig-manager 通过节点标签
nvidia.com/mig.config声明式管理 MIG,内置all-disabled、all-1g.5gb、all-balanced等策略 - single 策略按规格暴露
nvidia.com/mig-1g.5gb等资源名(推荐);mixed 统一暴露为nvidia.com/gpu靠标签区分 - 变更 MIG 标准流程:drain → label → 验证 state=success → uncordon;MIG 是硬件级隔离,时间片超分不作用于 MIG 资源
动手实验
- 在 A100/H100 节点上按 15.3 流程切换到
all-balanced,用nvidia-smi mig -lgi画出实际切分布局。 - 提交两个分别申请
nvidia.com/mig-1g.5gb和nvidia.com/mig-3g.20gb的 Pod,验证调度结果。 - 故意留一个 GPU Pod 不驱逐就给节点打 MIG 标签,观察 mig-manager 卡在 pending 的日志。
第 16 章 GPU 共享调度:时间片与 HAMi
本章目标
- 配置 device-plugin 时间片共享,实现多 Pod 复用同一张物理卡
- 说清时间片共享"无显存隔离"的局限与风险
- 部署 HAMi 实现按显存、按算力的细粒度分配,并按场景做选型决策
16.1 时间片共享配置:一张卡当八张用
为什么需要共享? 很多推理、开发、教学任务用不满一张卡(利用率 < 10%),整卡调度浪费巨大。时间片共享的思路:让 K8s 把 1 张物理卡"看成"N 张,N 个 Pod 调度到同一张卡上,由 GPU 以时间片轮转方式执行。
device-plugin 通过 ConfigMap 配置:
yaml
# time-slicing-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nvidia-device-plugin-config
namespace: kube-system
data:
config.yaml: |
version: v1
sharing:
timeSlicing:
resources:
- name: nvidia.com/gpu
replicas: 8 # 每张物理卡对外暴露 8 个可调度资源手动部署模式下给 device-plugin 容器加 --config-file 参数并挂载该 ConfigMap;GPU Operator 模式下:
bash
kubectl create configmap nvidia-device-plugin-config \
-n gpu-operator --from-file=config.yaml
helm upgrade gpu-operator nvidia/gpu-operator -n gpu-operator \
--reuse-values --set devicePlugin.config.name=nvidia-device-plugin-config验证效果:
bash
kubectl describe node <gpu-node> | grep nvidia.com/gpu # 资源从 1 变成 8
kubectl apply -f gpu-claim.yaml # replicas 改为 8,全部 Running 在同一节点
nvidia-smi # 节点上可见 8 个容器进程共享同一张物理卡16.2 时间片共享的局限
时间片配置简单,但隔离性几乎为零:
| 局限 | 后果 |
|---|---|
| 无显存隔离:每个容器都看到整卡显存 | 一个 Pod 爆显存(CUDA OOM)可能挤垮同卡所有 Pod |
| 无算力配额:靠 CUDA 上下文切换轮转 | 一个重任务拖慢同卡所有任务;共享越多切换开销越大 |
| 无故障隔离:一个 Pod 触发 GPU 异常(XID) | 同卡 Pod 全部受牵连 |
监控混淆:nvidia-smi 看到的是整卡利用率 | 难以定位是哪个 Pod 在消耗资源 |
结论:时间片只适合开发测试、教学、低负载推理等容忍抖动的场景。生产多租户共享请用 MIG(硬件隔离)或 HAMi(软件限额)。
16.3 HAMi 部署与使用:按显存切分 GPU
HAMi(Heterogeneous AI Computing Virtualization Middleware,原 4Paradigm k8s-vgpu,CNCF 沙箱项目)是最流行的开源 GPU 切分方案,它在 K8s 层做了两件事:①自定义 device-plugin + 调度器扩展:识别 nvidia.com/gpumem(显存,MiB)等资源,按"整卡剩余显存"做 binpack/spread 调度;②容器内显存硬隔离:注入库限制容器可用显存——容器内 nvidia-smi 只显示分到的额度,超额申请直接 OOM,不影响其他 Pod。
安装与使用:
bash
helm repo add hami-charts https://project-hami.github.io/HAMi/
helm install hami hami-charts/hami -n kube-system
# HAMi 自带 scheduler extender 接管 GPU 调度,无需替换默认调度器
kubectl get pods -n kube-system | grep hami
# 期望:hami-device-plugin-*、hami-scheduler-* Runningyaml
# 申请 8GB 显存(而不是整卡)
apiVersion: v1
kind: Pod
metadata: {name: hami-demo}
spec:
restartPolicy: Never
containers:
- name: app
image: nvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04
command: ["bash", "-c", "nvidia-smi && sleep 3600"]
resources:
limits:
nvidia.com/gpu: 1 # 需要 1 个 GPU 设备(个数)
nvidia.com/gpumem: 8000 # 只给 8000MiB 显存
nvidia.com/gpucores: 30 # 限制最多用 30% 算力(可选)显存隔离验证:kubectl exec hami-demo -- nvidia-smi,容器内显存上限显示为 8000MiB(物理卡可能是 80GB);用 PyTorch 申请超限张量会直接 CUDA OOM,同卡其他 Pod 不受影响。
16.4 HAMi 进阶
按核数限制算力:nvidia.com/gpucores: 30 限制容器最多用 30% SM 算力(HAMi-core 对 CUDA kernel 限速实现)。显存 + 算力双限额,接近 vGPU 体验。
节点 GPU 超卖:安装时通过 values 控制资源放大:
bash
helm install hami hami-charts/hami -n kube-system \
--set devicePlugin.deviceMemoryScaling=1.5 \ # 显存超卖:物理 80G 按 120G 可分配
--set devicePlugin.deviceSplitCount=10 # 单卡默认最多切 10 份超卖适合"多数任务显存用不满"的场景,但要评估峰值叠加风险,配合监控观察真实显存水位。
与监控对接:HAMi 暴露 Prometheus 指标(节点显存分配量、容器显存/算力用量等),配合第六部分的 DCGM + Prometheus + Grafana 可实现"容器级 GPU 利用率"面板——这正是原生时间片方案做不到的。
16.5 选型建议
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 大模型训练、多卡分布式训练 | 整卡调度 | 需要全部显存与算力,共享只引入抖动 |
| 在线推理(有 SLO 要求,卡支持 MIG) | MIG | 硬件级隔离,性能可预期 |
| 在线推理(卡不支持 MIG,如 T4) | HAMi | 显存/算力双限额,隔离性优于时间片 |
| 开发、测试、教学、Notebook | 时间片 或 HAMi | 成本优先;多人共用优先 HAMi 防互挤 |
| 小规模推理混部、追求密度 | HAMi 超卖 | 密度最高,配监控控制峰值风险 |
本章小结
- 时间片共享设
replicas即可让 1 卡变 N 个可调度资源,配置最简单,但无显存/算力/故障隔离 - HAMi 提供
nvidia.com/gpumem/gpucores细粒度申请,容器内显存硬隔离,支持超卖与监控对接,是生产共享的主流开源方案 - 选型口诀:训练整卡、在线推理 MIG/HAMi、开发测试时间片/HAMi
动手实验
- 配置
replicas: 4时间片共享,提交 4 个 Pod,确认 4 进程共卡,并验证一个 Pod 打满显存对其他 Pod 的影响。 - 安装 HAMi,提交两个各申请
gpumem: 8000的 Pod 到同一张 24G 卡,容器内验证显存上限;再让一个 Pod 超限分配,记录 OOM 行为及同卡 Pod 是否存活(对比实验 1)。 - 设置
deviceMemoryScaling=2.0,观察节点可分配显存变化,思考超卖风险。
第 17 章 AI 任务的批调度:Volcano
本章目标
- 说清默认调度器为什么撑不住分布式训练(Gang Scheduling 问题)
- 掌握 Volcano 核心概念:vcjob、Queue、PodGroup、Gang Scheduling
- 提交 8 卡 PyTorch 分布式训练 vcjob,用 Queue 实现多团队配额、公平共享与抢占
17.1 为什么默认调度器不适合 AI 训练
默认调度器(kube-scheduler)逐 Pod 独立调度:它认为每个 Pod 都是无状态服务,先来先调度。这对 Web 服务没问题,对分布式训练是灾难。
典型死锁场景:一个 PyTorch 分布式训练任务需要 8 个 Pod(每 Pod 1 卡)全部启动后才能开始训练(互相等待 rendezvous)。假设集群只剩 5 张空闲卡:Pod1~Pod5 调度成功,占住 5 张卡空等另外 3 个兄弟;Pod6~Pod8 一直 Pending。结果 5 张卡被白白占着、训练永远开始不了;若另一个任务也以同样方式占了剩下的卡,两个任务互相死锁,集群被"等死"的任务占满。
这就是 Gang Scheduling(成组调度) 问题:一组 Pod 必须"要么全部调度,要么一个都不调度"(All-or-Nothing)。AI 任务还需要队列、配额、优先级、抢占等批处理能力——这正是 Volcano(CNCF 毕业项目,K8s 生态最主流的批调度器)要解决的。
17.2 Volcano 核心概念
| 概念 | 说明 |
|---|---|
| Job(vcjob) | Volcano 的 CRD,一个 Job 含多个 Task(如 master、worker),自带生命周期与失败策略 |
| PodGroup | 一组 Pod 的"调度单元",核心字段 minMember(最少一起调度的 Pod 数),提交 vcjob 时自动创建 |
| Gang Scheduling | 检查剩余资源能否满足 minMember 个 Pod,满足才整组绑定,否则整组等待——从机制上杜绝死锁 |
| Queue | 资源队列,多团队共享的基本单位:weight 公平分配、capability 配额上限、guarantee 预留 |
| 插件体系 | 调度行为由插件组合:gang、priority、proportion、drf、reclaim(回收)、preempt(抢占)等 |
使用方式:Pod/Job 指定 schedulerName: volcano 由 Volcano 接管;不指定的仍走默认调度器,两者可共存。
17.3 实战:提交 8 卡 PyTorch 分布式训练任务
安装 Volcano:
bash
# 方式一:官方清单(快)
kubectl apply -f https://raw.githubusercontent.com/volcano-sh/volcano/master/installer/volcano-development.yaml
# 方式二:Helm(生产推荐)
helm install volcano volcano-sh/volcano -n volcano-system --create-namespace
# 期望 volcano-admission、volcano-controllers、volcano-scheduler 全部 Running
kubectl get pods -n volcano-system提交 vcjob(1 个 master + 8 个 worker,每 worker 1 卡,共 8 卡):
yaml
# pytorch-8gpu-vcjob.yaml
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
name: pytorch-dist-8gpu
spec:
minAvailable: 9 # 关键:9 个 Pod 全凑齐才调度(Gang)
schedulerName: volcano # 关键:交给 Volcano 调度
queue: team-a # 提交到 team-a 队列(17.4 节创建)
policies:
- event: PodEvicted
action: RestartJob # 任一 Pod 被驱逐/失败则整体重启
tasks:
- name: master
replicas: 1
template:
spec:
restartPolicy: OnFailure
containers:
- name: pytorch
image: nvcr.io/nvidia/pytorch:24.05-py3
command: ["bash", "-c", "torchrun --nnodes=9 --node_rank=0 \
--nproc_per_node=1 --master_addr=pytorch-dist-8gpu-master-0 train.py"]
resources:
limits: {cpu: "8", memory: 32Gi}
- name: worker
replicas: 8
template:
spec:
restartPolicy: OnFailure
containers:
- name: pytorch
image: nvcr.io/nvidia/pytorch:24.05-py3
# RANK 用 Pod 序号注入(实际平台多由 Training Operator 处理)
command: ["bash", "-c", "torchrun --nnodes=9 --node_rank=$RANK \
--nproc_per_node=1 --master_addr=pytorch-dist-8gpu-master-0 train.py"]
env:
- name: RANK
valueFrom:
fieldRef:
fieldPath: metadata.annotations['volcano.sh/task-index']
resources:
limits: {nvidia.com/gpu: 1, cpu: "8", memory: 32Gi}bash
kubectl apply -f pytorch-8gpu-vcjob.yaml
kubectl get podgroup # 资源不足时整组 Pending
kubectl describe podgroup pytorch-dist-8gpu # Unschedulable 原因清晰可见
kubectl get pods -l volcano.sh/job-name=pytorch-dist-8gpu -o wide验证 Gang 行为:故意只留 5 张空闲卡再提交,可以看到 9 个 Pod 全部 Pending——而不是像默认调度器那样先跑 5 个占住资源。
17.4 Queue 配额管理:多团队公平共享
yaml
# queues.yaml
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
name: team-a
spec:
weight: 2 # 公平分配权重:空闲资源约能分到 B 队的 2 倍
reclaimable: true # 允许超占的资源被其他队列回收
capability: {nvidia.com/gpu: 16} # 硬上限:本队列最多用 16 张卡
guarantee: # 预留保障:始终为本队列留 8 张卡
resource: {nvidia.com/gpu: 8}
---
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
name: team-b
spec:
weight: 1
reclaimable: true
capability:
nvidia.com/gpu: 8行为解读:weight 决定空闲资源的公平分配比例(A:B=2:1 则 A 能拿约 2 倍于 B);capability 是硬上限防吃光集群,guarantee 为核心业务预留资源;reclaim/preempt:team-a 超占份额时,team-b 提交任务而资源不足,Volcano 会驱逐 team-a 的部分任务还资源(插件默认开启)。
被抢占的训练 Pod 会被杀掉——所以训练任务必须做 checkpoint,这是 GPU 集群运维要给算法团队立的第一条规矩。
17.5 常见 AI 平台与调度框架简介
Volcano 解决"怎么调度",实际平台上往往还叠一层"怎么编排任务"的框架:
| 框架 | 定位 |
|---|---|
| Kubeflow Training Operator | 提供 PyTorchJob、TFJob、MPIJob 等 CRD,自动生成分布式训练拓扑,可指定 schedulerName: volcano 与 Volcano 搭配 |
| Ray on K8s(KubeRay) | 用 CRD 管理 Ray 集群(head + worker),适合 RL、数据预处理、分布式推理等灵活拓扑 |
| Run:ai(已并入 NVIDIA) | 商业 GPU 调度平台:显存超分、动态配额、公平共享、可视化,开箱即用 |
| Platform9 | 商业托管 K8s 平台,提供 GPU 集群托管运维与多集群管理 |
17.6 其他调度增强简介
- Koordinator:阿里开源 QoS 调度器,主打在离线混部——在线服务与批任务同节点运行,保在线 SLO 的同时用空闲资源跑训练
- Kueue:K8s 官方批任务队列项目(SIG-Scheduling),专注任务排队与配额准入,不改变 Pod 级调度逻辑
本章小结
- 默认调度器逐 Pod 调度,无法保证分布式训练"成组启动",会造成占卡死锁——AI 训练必须用支持 Gang Scheduling 的批调度器
- Volcano 核心:vcjob 定义任务、PodGroup
minMember实现 All-or-Nothing、Queue 的 weight/capability/guarantee 实现配额与公平、reclaim 实现抢占 - 提交任务关键:
schedulerName: volcano+minAvailable+ 指定 queue;被抢占是常态,训练必须 checkpoint;Kubeflow 管编排、Volcano 管调度,Kueue/Koordinator 是另外两条路线
动手实验
- 只留 5 张空闲卡,提交
minAvailable: 9的 vcjob,确认所有 Pod Pending;再用普通 Deployment(8 副本各 1 卡)对比观察占卡现象,直观理解死锁。 - 创建 weight 为 2:1 的两个队列,同时提交两个大队任务,观察资源分配比例;再让 team-a 占满集群后提交 team-b 任务,观察 reclaim 抢占过程。
- 用
kubectl describe podgroup查看一次调度的完整事件链,解释每条事件。
下一部分预告:第五部分进入高速网络与分布式训练——RDMA、InfiniBand、RoCE、NCCL、GPUDirect,学习为多机多卡训练保障通信性能。