Skip to content

第四部分 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 本身并不认识,它提供了一套扩展机制:

  1. 节点上的组件(就是 Device Plugin)向 kubelet 汇报:"我这里有 8 个叫 nvidia.com/gpu 的资源"
  2. kubelet 把它写入 Node 对象的 status.capacity,上报给 API Server
  3. 调度器看到 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 register

13.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 输出即成功

必须知道的规则

  1. GPU 只能写在 limits(只写 requests 时自动对齐为相同值);不能申请 0.5 张卡,API Server 直接报错:must be an integer
  2. CPU/内存与 GPU 是独立维度,申请 GPU 不意味着自动获得 CPU/内存,常规资源仍建议显式声明
  3. 申请 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,一个 Pending

Pending 排查三板斧

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
无任何事件,一直 PendingPVC 未绑定 / 调度器异常检查 PVC、scheduler 日志

本章小结

  • K8s 通过 Extended Resourcenvidia.com/gpu)感知 GPU,由 Device Plugin 经 kubelet 上报,只支持整数
  • Device Plugin 的核心是 gRPC 三步:RegisterListAndWatchAllocate,插件真正做的是"上报数量 + 告诉 kubelet 挂载什么"
  • Pod 通过 resources.limits: nvidia.com/gpu: N 申请整卡;排查 Pending 先看 kubectl describe pod 事件

动手实验

  1. 删除 Device Plugin DaemonSet,观察节点 nvidia.com/gpu 何时消失、重新部署后何时恢复(体会 ListAndWatch 的作用)。
  2. 提交申请 nvidia.com/gpu: 0.5 的 Pod,记录 API Server 的报错原文。
  3. 申请 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.enabledtrue是否由 Operator 安装驱动(已有驱动时设 false
driver.version随 chart指定驱动版本,如 550.127.08
toolkit.enabled / toolkit.versiontrue / 随 chart是否安装 toolkit、指定其版本
devicePlugin.enabled / nfd.enabledtrue是否部署 device plugin / node-feature-discovery
dcgmExporter.enabledtrue是否部署 DCGM 监控采集器
migManager.enabled / mig.strategytrue / singlemig-manager 开关与 MIG 资源暴露策略(第 15 章)
operator.defaultRuntimecontainerd集群容器运行时

生产定制安装示例: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.presenttrue检测到 NVIDIA GPU,是组件 nodeSelector 依据
nvidia.com/gpu.productNVIDIA-A100-SXM4-80GBGPU 型号,可按型号调度任务
nvidia.com/gpu.count / .memory8 / 81920GPU 数量 / 单卡显存(MiB)
nvidia.com/gpu.familyampereGPU 架构代次
nvidia.com/cuda.driver.major550驱动主版本
nvidia.com/mig.capable / mig.configtrue / all-disabled是否支持 MIG / mig-manager 的输入标签(第 15 章)
nvidia.com/gpu.deploy.drivertrue/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=falsenvidia.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 未禁用、已有驱动冲突

动手实验

  1. 安装 GPU Operator 后,列出 gpu-operator 命名空间所有 Pod,逐一对应 14.1 节组件表。
  2. 查看一个 GPU 节点的全部 nvidia.com/ 标签,解释其中 5 个的含义。
  3. 模拟故障:给节点打 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
节点选择直接申请对应资源名nodeSelectornvidia.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 与时间片共享混用的注意事项

  1. device-plugin 的时间片配置对 MIG 资源不生效:time-slicing 只作用于整卡 nvidia.com/gpu,不能对 nvidia.com/mig-1g.5gb 做 replicas 超分——MIG 切片本身就是最小调度单元,再切分要靠 HAMi 这类方案
  2. MIG 与整卡并存时(如 8 卡中 2 卡开 MIG),mixed 策略下整卡和切片都叫 nvidia.com/gpu,务必用产品标签区分,否则 Pod 可能拿到不符合预期的资源
  3. MIG 硬件隔离优于时间片软件复用:有 SLO 要求的在线业务优先 MIG,容忍抖动的开发测试再考虑时间片(决策见 16.5 节);变更 MIG 会重启 device-plugin,期间该节点 GPU 资源短暂消失、排队 Pod 瞬间 Pending,属正常现象

本章小结

  • mig-manager 通过节点标签 nvidia.com/mig.config 声明式管理 MIG,内置 all-disabledall-1g.5gball-balanced 等策略
  • single 策略按规格暴露 nvidia.com/mig-1g.5gb 等资源名(推荐);mixed 统一暴露为 nvidia.com/gpu 靠标签区分
  • 变更 MIG 标准流程:drain → label → 验证 state=success → uncordon;MIG 是硬件级隔离,时间片超分不作用于 MIG 资源

动手实验

  1. 在 A100/H100 节点上按 15.3 流程切换到 all-balanced,用 nvidia-smi mig -lgi 画出实际切分布局。
  2. 提交两个分别申请 nvidia.com/mig-1g.5gbnvidia.com/mig-3g.20gb 的 Pod,验证调度结果。
  3. 故意留一个 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-* Running
yaml
# 申请 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

动手实验

  1. 配置 replicas: 4 时间片共享,提交 4 个 Pod,确认 4 进程共卡,并验证一个 Pod 打满显存对其他 Pod 的影响。
  2. 安装 HAMi,提交两个各申请 gpumem: 8000 的 Pod 到同一张 24G 卡,容器内验证显存上限;再让一个 Pod 超限分配,记录 OOM 行为及同卡 Pod 是否存活(对比实验 1)。
  3. 设置 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 预留
插件体系调度行为由插件组合:gangpriorityproportiondrfreclaim(回收)、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提供 PyTorchJobTFJobMPIJob 等 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 是另外两条路线

动手实验

  1. 只留 5 张空闲卡,提交 minAvailable: 9 的 vcjob,确认所有 Pod Pending;再用普通 Deployment(8 副本各 1 卡)对比观察占卡现象,直观理解死锁。
  2. 创建 weight 为 2:1 的两个队列,同时提交两个大队任务,观察资源分配比例;再让 team-a 占满集群后提交 team-b 任务,观察 reclaim 抢占过程。
  3. kubectl describe podgroup 查看一次调度的完整事件链,解释每条事件。

下一部分预告:第五部分进入高速网络与分布式训练——RDMA、InfiniBand、RoCE、NCCL、GPUDirect,学习为多机多卡训练保障通信性能。