课程目录(第 32 章 / 共 33 章)
课程/平台化与规模化

GPU 与 AI 工作负载:调度、共享与推理平台

32 章 / 共 33·22 分钟·进阶GPUDevicePluginMIG推理服务调度

把 GPU 变成集群里可调度、可共享、可观测的资源:扩展资源模型、Device Plugin、MIG 与时间片共享,以及训练任务与推理服务的工程细节。

学完这一章,你将能够

  • 说清 nvidia.com/gpu 为什么不能超卖,以及 requests 与 limits 为什么必须相等
  • 用节点标签、nodeSelector 与污点容忍把 AI 任务固定到 GPU 节点,并排查 GPU 类 Pending
  • 在整卡独占、MIG、时间片与 MPS 之间按场景做出取舍
  • 给推理服务配上合理的探针、HPA 与监控,解释「GPU 利用率低但账单高」

为什么 GPU 要单独讲一遍

CPU 和内存是节点自带的:内核一启动就知道有多少核、多少内存,kubelet 直接把它们写进 CapacityAllocatable,调度器也天然会按它们过滤和打分。GPU 不是这样——它是一张挂在 PCIe 上的卡,内核默认不认识它,kubelet 也不会自己去数。

所以「让集群能用 GPU」这件事,本质上是要把一张设备变成一种资源:装驱动、注册插件、上报数量,然后才能像申请 CPU 一样写进 Pod。这中间少一环,Pod 就只是「被调度到了有卡的机器上,但容器里看不见卡」。

更麻烦的是,GPU 贵、不可超卖、而且经常要多人共享。CPU 可以 requests: 500m 挤一挤,GPU 不能这么干。这一章就按「资源模型 → 安装验证 → 调度 → 共享 → 训练与推理 → 监控成本」的顺序,把这条链路走完。

GPU 的资源模型:扩展资源与 Device Plugin

Kubernetes 里除了 cpumemoryephemeral-storage 这几个内置资源,其它都叫扩展资源(extended resource),写法是 <厂商域名>/<名称>,GPU 就是 nvidia.com/gpu。它有三条和 CPU 完全不同的规矩:

规矩说明
只能是整数不能写 nvidia.com/gpu: 0.5,也不支持 milli 单位
必须写 limits只有 limits 里的扩展资源才会被识别,写进 requests 而不写 limits 时会被补成相等
requests 必须等于 limits两者写不同值会被 API Server 直接拒绝,Pod 根本创建不出来

第三条正是「GPU 不能超卖」的直接体现。CPU 允许 requests: 500mlimits: 1000m,因为它靠时间片轮转,多个容器可以「轮流用」同一个核。GPU 不行:显存是一块物理内存,算力是一次内核调用,调度器按「张数」记账时,如果允许 2 个 Pod 各申请 0.5 张而实际只有 1 张,两个进程会同时往同一块显存里写,结果就是不可预测的 CUDA out of memory 甚至崩溃。要共享,就必须在更下层(MIG、时间片、MPS)把卡真的切开,而不是在调度层把数字拆开。

nvidia.com/gpu: 2 这个数字是谁上报的?答案是 Device Plugin。它是 kubelet 的一个插件框架:厂商写一个 DaemonSet 跑在每个节点上,通过 gRPC 向 kubelet 注册自己管理的资源名,并持续上报可用设备数;Pod 被调度到该节点后,kubelet 让插件把具体设备(/dev/nvidia0 等)和驱动库挂进容器。

text
kubectl apply ──► kube-scheduler
                    │ ① 过滤:nodeSelector / nodeAffinity / 污点容忍
                    │ ② 过滤:节点 Allocatable 里还有 nvidia.com/gpu 吗

              GPU 节点(标签 node-role=ai,污点 nvidia.com/gpu=true:NoSchedule)
              ┌──────────────────────────────────────────────┐
              │ kubelet  ◄── gRPC ──  nvidia-device-plugin    │ 上报 nvidia.com/gpu: 2
              │   │                                          │
              │   ├── 分配 1 张卡 ──► 容器 A 看到 /dev/nvidia0
              │   └── 分配 1 张卡 ──► 容器 B 看到 /dev/nvidia1
              └──────────────────────────────────────────────┘
                共享方案改变的是「容器看到几张卡、卡怎么分」:
                  整卡独占      → 1 个容器 1 张卡
                  MIG           → 1 张卡切成 nvidia.com/mig-1g.10gb × 7
                  time-slicing  → 4 个容器看到同一张卡(显存不隔离)

这里要分清两个容易混的组件:NFD 负责「描述」节点有什么(feature.node.kubernetes.io/pci-10de.present: "true" 这类硬件标签),Device Plugin 负责「上报成资源」。GPU Operator 还会额外部署 GFD(GPU Feature Discovery),它给节点打上 nvidia.com/gpu.present: "true"nvidia.com/gpu.product: NVIDIA-H100-SXM5-80GB 这类标签,方便你挑机器。NFD 的细节见节点与内核调优

requests 和 limits 不相等会发生什么

你会看到 API Server 直接拒绝,报错形如 spec.containers[0].resources.requests: Invalid value: "1": must be equal to nvidia.com/gpu limit。这不是调度失败,而是对象压根没创建成功——kubectl get pod 里找不到它,所以别去 describe pod 找事件。

装好并验证:GPU Operator

手工装驱动的路子(宿主机装 NVIDIA driver + nvidia-container-toolkit + nvidia-device-plugin + DCGM Exporter)要维护四五个组件,节点换内核就得重来。NVIDIA GPU Operator 把这些都变成 Operator 管理的 DaemonSet:驱动、容器运行时 toolkit、device plugin、GFD、DCGM Exporter、MIG Manager 各自一个组件,由 ClusterPolicy 这个 CRD 统一开关。

四个组件各管什么,哪些可以不装

上生产前要能回答「这一层坏了会怎样」,而不是把所有开关都打开:

组件管什么装不装取舍
driver宿主机内核模块与 nvidia-smi二选一推荐宿主机自己装:版本可控,升级内核不用重建特权容器;交给 Operator 装(driver.enabled=true)节点免手工,但要给 driver DaemonSet 特权,且宿主机上的 nvidia-smi 得用 chroot /run/nvidia/driver nvidia-smi 才看得见
container-toolkit让 containerd/CRI-O 认识 NVIDIA 运行时,把设备与驱动库注入容器必须少了它,容器里只有空的 /dev/nvidia*nvidia-smino CUDA-capable device is detected
device-plugin向 kubelet 上报 nvidia.com/gpu 并完成设备分配必须少了它,节点 allocatable 里没有 GPU,所有申请 GPU 的 Pod 永远 Pending
GFD给节点打硬件标签(型号、显存、卡数、MIG 能力)强烈建议标签是「挑机器」的可靠依据;没有它只能靠人工维护机型标签
dcgm-exporter暴露利用率、显存、温度、功耗指标建议不装就没有可观测性里的看板与告警

驱动版本要三处对齐:宿主机驱动、镜像里的 CUDA 运行时、Operator 的 driver.version。任何一处超前都会出现 CUDA driver version is insufficient。NVLink 机型(H100/H200/B200/B300)还依赖 nvidia-fabricmanager,B200/B300 需要额外的 open 驱动与 nvlink5 包:驱动交给 Operator 管时 fabricmanager 会随驱动一起装,驱动在宿主机上装时它也得在宿主机上装。

参考:reference/k8s-in-action/ai/gpu-operator/README.md(宿主机驱动安装与 fabricmanager 排错)
bash
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update

# 版本号按最新 release 替换
helm install --wait -n gpu-operator --create-namespace gpu-operator nvidia/gpu-operator \
  --version v25.3.0

# 如果宿主机已经装好驱动,别让 Operator 再装一遍
# helm install ... --set driver.enabled=false

安装完先确认组件状态,再确认资源真的上报到了节点:

bash
kubectl -n gpu-operator get pods                          # 各组件应为 Running
kubectl get clusterpolicies.nvidia.com                     # State 应为 ready

kubectl get nodes -o json | jq '.items[].status.allocatable'
kubectl describe node "$NODE" | grep -A10 -E "^Capacity:|^Allocatable:"

成功的样子是:allocatable 里出现 "nvidia.com/gpu": "2"(字符串形式的整数),describe nodeCapacityAllocatable 两张表里都能看到 nvidia.com/gpu: 2Allocatable 一般等于 Capacity,但如果 kubelet 配了 --system-reserved 且预留了设备,两者可能不同——取决于你的 kubelet 配置。

最后跑一次真实计算,确认「容器里真的能用卡」而不只是「数量对上了」:

bash
kubectl run gpu-check --restart=Never --image=nvcr.io/nvidia/cuda:12.6.2-base-ubuntu24.04 \
  --overrides='{"spec":{"containers":[{"name":"gpu-check","image":"nvcr.io/nvidia/cuda:12.6.2-base-ubuntu24.04","command":["nvidia-smi"],"resources":{"limits":{"nvidia.com/gpu":1}}}]}}'
kubectl logs gpu-check
kubectl delete pod gpu-check

nvidia-smi 能打印出显卡型号、驱动版本和显存,说明驱动、toolkit、device plugin 三层都通了。看不到 nvidia-smi: command not found 就是镜像不对;看到 no CUDA-capable device is detected 才是资源没挂进去。

分层验证:从宿主机到容器

nvidia-smi 只是最后一步,出问题时按层往上查最快:

怎么验通过的样子不通说明什么
驱动在节点上直接执行 nvidia-smi打印卡型、驱动版本、右上角 CUDA Version驱动没装好,后面几层都不用看
Fabric Manager(NVLink 机型)nvidia-smi -q -i 0,看输出里的 Fabric 段State: CompletedStatus: Successnvidia-fabricmanager 或 NVSwitch 设备没被识别,多卡通信会直接失败
资源上报kubectl get nodes -o json,用 jq.items[].status.allocatable出现 "nvidia.com/gpu": "2"device-plugin 没就绪
容器内可用用 CUDA 镜像跑 nvidia-smi输出与宿主机一致toolkit 没配好,或镜像里没有 CUDA 运行时

如果 nvidia-cuda-validator 这个 init 容器失败、日志里出现 system not yet initialized,先别怀疑卡:八成是 nvidia-fabricmanager 没起来。宿主机装驱动时用 systemctl status nvidia-fabricmanager 查;驱动由 Operator 装时进 nvidia-driver-daemonset-* Pod 里看 ps auxf | grep fabric。如果 ls /proc/driver/nvidia-nvswitch/devices/ 是空的,那才是硬件没被识别,要查插卡与 BIOS 设置。

参考:reference/k8s-in-action/ai/gpu-operator/README.md

用 GFD 标签挑机器

GFD 打完标签后,nodeSelector 可以写得比「是不是 GPU 节点」精确得多:

标签例子用途
nvidia.com/gpu.present"true"粗筛 GPU 节点
nvidia.com/gpu.productNVIDIA-H100-SXM5-80GB按型号挑:新卡跑训练、老卡跑推理
nvidia.com/gpu.count"8"把大机器留给多卡任务
nvidia.com/gpu.memory"81920"(MiB)显存是硬约束,先按它过滤
nvidia.com/mig.capable"true"挑支持 MIG 的卡做多租户推理
bash
# 列出所有节点的卡型、卡数与显存
kubectl get nodes -L nvidia.com/gpu.product,nvidia.com/gpu.count,nvidia.com/gpu.memory

# 只把 8 卡 H100 节点留给多机训练
kubectl get nodes -l nvidia.com/gpu.product=NVIDIA-H100-SXM5-80GB,nvidia.com/gpu.count=8

两个细节:标签值是字符串nodeSelector 里要写引号(nvidia.com/gpu.count: "8");标签由 GFD 按硬件状态动态维护,手工改会被覆盖。标签只负责「选机器」,能不能用仍然看 `allocatable`,两者不一致时以 allocatable 为准。

调度:把 AI 任务固定到 GPU 节点

集群里通常既有普通业务节点,也有装卡的 AI 节点。两个方向要同时做:让 AI 任务主动挑 GPU 节点(标签 + nodeSelector / nodeAffinity),让 GPU 节点只收 AI 任务(污点 + 容忍)。后者很关键——不加污点,一个普通的 nginx Deployment 也会被调度到最贵的机器上。

节点标签的常规做法是 NFD/GFD 自动打,或者你自己打一个实例类型标签:

bash
kubectl label node "$NODE" node-role.kubernetes.io/ai=true
kubectl taint nodes "$NODE" nvidia.com/gpu=true:NoSchedule

关于多卡任务:一次申请多张卡,只会落在同一台节点上nvidia.com/gpu: 4 表示这台机器上同时给 4 张),调度器不会把 4 张卡拆到 4 台机器。同节点多卡之间通常走 NVLink/NVSwitch,带宽远高于 PCIe;跨节点通信则依赖 RDMA/InfiniBand,是另一套网络配置。

动手练习:把一个一次性训练任务钉到 GPU 节点上

第一步,给目标节点打标签、加污点(把 $NODE 换成 kubectl get nodes 里的真实名字):

bash
kubectl label node "$NODE" node-role.kubernetes.io/ai=true
kubectl taint nodes "$NODE" nvidia.com/gpu=true:NoSchedule
kubectl describe node "$NODE" | grep -iE "Taints|nvidia.com/gpu"

第二步,写一个只跑一次的训练任务。restartPolicy: Never 让它失败就是失败,不要无限重试;nodeSelector 挑节点,tolerations 接受 GPU 节点的污点:

yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: gpu-train-demo
spec:
  backoffLimit: 2
  activeDeadlineSeconds: 600
  template:
    spec:
      restartPolicy: Never
      nodeSelector:
        node-role.kubernetes.io/ai: "true"
      tolerations:
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
      containers:
        - name: train
          image: nvcr.io/nvidia/cuda:12.6.2-base-ubuntu24.04
          command: ["bash", "-c", "nvidia-smi && echo 'train step done'"]
          resources:
            limits:
              nvidia.com/gpu: 1

第三步,应用并观察:

bash
kubectl apply -f gpu-train-demo.yaml
kubectl get pods -w
kubectl logs job/gpu-train-demo
kubectl describe pod -l job-name=gpu-train-demo | grep -A5 "Allocated resources"
kubectl delete job gpu-train-demo

Pod 变成 Completed、日志里能看到显卡信息,就算成功。顺手把污点删掉,别留给下一个实验:kubectl taint nodes "$NODE" nvidia.com/gpu=true:NoSchedule-

如果 Pod 卡在 Pending,事件里的原因分两种,先看清再动手:

bash
kubectl describe pod -l job-name=gpu-train-demo | grep -A5 Events
# 典型输出:
#   0/3 nodes are available: 1 Insufficient nvidia.com/gpu,
#   2 node(s) didn't match Pod's node affinity/selector.

kubectl get pods -A -o json \
  | jq -r '.items[] | select([.spec.containers[].resources.limits["nvidia.com/gpu"]] | any) | "\(.metadata.namespace)/\(.metadata.name)"'

第一条说明「节点选对了,但卡被占满了」,第二条说明「标签/容忍没写对,根本没资格上那些节点」。想知道卡被谁占了,用上面那条 jq 列出所有申请了 GPU 的 Pod;kubectl describe node 在较新的版本里会把这些扩展资源列进 Allocated resources,老版本看不到,以 jq 的汇总为准。

GPU 共享:整卡独占、MIG、时间片与 MPS

一整张 H100 给一个小模型做推理,利用率常年个位数,这是最常见的浪费。共享方案有四种,隔离强度和适用场景差别很大,选错比不共享更糟。

方案隔离强度显存隔离适用场景配置方式风险
整卡独占最强(整卡独占)是(整块显存)训练、性能敏感推理只写 nvidia.com/gpu: 1利用率低、单位成本最高
MIG硬件级切分,接近独占是(每实例独立显存)推理多租户、稳定 SLAGPU Operator migManager.enabled=true + 节点标签 nvidia.com/mig.config切分规格固定、不是所有卡/驱动都支持、重配要先清空任务
time-slicing弱,无隔离否(共用同一块显存)开发/实验环境共享device plugin 的共享 ConfigMap + 节点标签一个容器 OOM 会影响同卡其它容器、延迟抖动、计量不准
MPS进程级并行,算力利用率高部分(可按进程限制显存)小模型高并发推理节点层面配置,device plugin 的共享配置里有实验性支持配置复杂、异常可能连带、具体能力取决于版本

MIG 是最推荐的多租户方案,因为它把卡在硬件层面切成独立实例,每个实例有独立的显存和算力,容器之间互不可见。开启后资源名会变成 nvidia.com/mig-1g.10gb 这种,节点上会出现 7 个可用实例:

bash
# Operator 侧开启 MIG Manager,再用节点标签声明切分规格
# helm upgrade ... --set migManager.enabled=true --set mig.strategy=mixed
kubectl label node "$NODE" nvidia.com/mig.config=all-1g.10gb --overwrite
kubectl get nodes -o json | jq '.items[].status.allocatable | with_entries(select(.key | startswith("nvidia.com/")))'

time-slicing 则是「让多个容器轮流用同一张卡」,改的是 device plugin 的配置。它不改资源名,只是把一个物理 GPU 报成 N 份:

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: time-slicing-config
  namespace: gpu-operator
data:
  any: |-
    version: v1
    flags:
      migStrategy: none
    sharing:
      timeSlicing:
        resources:
          - name: nvidia.com/gpu
            replicas: 4

配好后用 --set devicePlugin.config.name=time-slicing-config --set devicePlugin.config.default=any 让 device plugin 读它,并给节点打上 nvidia.com/device-plugin.config=any 标签。time-slicing 不隔离显存:4 个容器都能看到整张卡的全部显存,谁先用完谁先 OOM,另外三个一起挂——所以它只适合开发环境,不要用在付费的多租户推理上。

训练与推理:两类工作负载

AI 工作负载基本分成两类,工程关注点完全不同。

训练任务:用 Job 跑一次性计算

训练是「跑完就结束」的批处理,用 第 13 章 Job 与 CronJob 最合适。三条实践要点:

  • `restartPolicy: Never` + `backoffLimit`:训练失败往往意味着代码或数据有问题,重试几次就够,别让它无限循环烧卡。配合 activeDeadlineSeconds 给一个总时长上限。
  • 检查点写到 PVC:Pod 重建后本地磁盘就没了,长训练必须定期把 checkpoint 写到 第 10 章 存储与 PVC 上的持久卷。选了 ReadWriteOnce 的卷还要注意它一次只能挂到一个节点上。
  • InitContainer 预热数据集:把「下载/解压数据集到本地或缓存卷」放进 initContainer,主容器启动时数据已就绪,避免每个训练进程重复拉数据;同时可以把数据预热到节点本地盘,减少训练时的 IO 抖动。

多机多卡训练不是手写 Deployment 能管好的:它需要 Gang 调度(一组 Pod 要么都起来、要么都不起)和 rank/world_size 这类环境变量注入。Kubeflow Training OperatorTrainJob CRD,屏蔽 PyTorchJob/MPIJob 差异)和 RayRayCluster/RayJob)是社区两个主流选择,从单卡实验走到多卡训练还要补上排队、通信验证与检查点,这些见 AI 平台:批调度、分布式训练与模型服务

推理服务:Deployment + Service + HPA

推理是长期在线的服务,用 第 6 章 Deployment + 第 7 章 Service 这一套标准组合,但有三处必须为 GPU 调整:

探针要放宽。 大模型加载几 GB 甚至几十 GB 权重,冷启动可能几十秒到几分钟。用 startupProbe 给足时间,readinessProbe 检查模型真正可服务(比如 /health 返回 200),livenessProbe 千万不要检查下游依赖——模型加载慢导致的误杀会让 Pod 陷入重启循环,每次都重新加载一遍,永远起不来。细节见第 11 章 探针与资源管理

HPA 默认看不了 GPU。 HorizontalPodAutoscalertype: Resource 只支持 CPU 与内存(数据来自 metrics-server)。GPU 利用率要扩缩容,必须先把 DCGM 指标经 Prometheus 变成自定义/外部指标(常用 prometheus-adapter 或 KEDA 暴露),否则你写了 averageUtilization 也不会动。这条也是「HPA 不扩」的头号原因。

多副本要算显存。 副本数乘以单副本显存占用不能超过卡的容量;两个副本共享一张卡时(time-slicing)还要额外留余量,否则压测一上来就互相 OOM。

模型缓存决定冷启动时间:权重放在镜像里会导致镜像巨大、拉取慢;放在 PVC 或对象存储上挂载,多个副本可以共享一份,配合节点本地缓存(镜像与缓存)把冷启动压到可接受范围。KServe 在 Deployment/Service 之上补了 InferenceService 这类 CRD 和 Serverless 伸缩,适合把「模型部署」标准化成平台能力。

监控与成本:为什么利用率低账单却很高

GPU 的可观测性不能只看 kubectl top——它只有 CPU 和内存。真正需要的是 DCGM Exporter(GPU Operator 自带,或单独部署)暴露的指标,抓进 Prometheus 或 VictoriaMetrics,再在 Grafana 里看板:

指标看什么
DCGM_FI_DEV_GPU_UTIL算力利用率,判断卡是不是在空转
DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_FREE显存占用,排查 OOM 与「占着不放」
DCGM_FI_DEV_GPU_TEMP温度,异常高温通常意味着散热或功耗问题
DCGM_FI_DEV_POWER_USAGE功耗,用于估算真实能耗成本

Grafana 上导入 NVIDIA DCGM 官方看板即可(社区常用 ID 12239,本地 cookbook 用的是 21362,按你的版本核对);采集与看板链路见可观测性

「利用率低但账单高」几乎总是下面几种原因叠加:整卡独占导致一张卡只服务一个小模型;训练任务排队等资源、等的时候卡也没被别人用;开发环境申请了卡但只占显存不跑计算;time-slicing 下多个容器互相抢导致大家都在等。成本控制的本质是提高卡的时间利用率:给推理上 MIG 或 time-slicing、给训练加排队与抢占、给开发环境设自动回收。

配额层面,ResourceQuota 支持限制扩展资源(写 requests.nvidia.com/gpulimits.nvidia.com/gpu,值必须是整数),这样某个团队的命名空间最多只能用 N 张卡,超过就直接创建失败;这比事后看账单有效得多,配合第 12 章 命名空间与 RBAC 一起用。再往前一步是优先级与抢占:给关键训练任务设高 PriorityClass,资源不够时调度器可以抢占(驱逐)低优先级任务,注意被抢占的任务要能靠检查点恢复。

常见坑与排错

GPU 的问题排查顺序是固定的:先确认节点有没有资源 → 再确认 Pod 有没有资格上去 → 最后确认容器里看不看得见卡

现象原因怎么确认怎么办
容器里没有 GPU,nvidia-smino CUDA-capable device is detecteddevice plugin 未就绪,或 Pod 没申请 GPUkubectl -n gpu-operator get pods 看 device-plugin 是否 Running;kubectl get pod -o yaml 看有没有 limits修好 device plugin;Pod 必须写 resources.limits 才会挂设备
Pod Pending,事件 Insufficient nvidia.com/gpu卡已被其它 Pod 占用,或副本数超过卡的张数kubectl describe pod 的 Events;用 jq 列出所有申请 GPU 的 Pod等资源释放、减少副本、或改用 MIG/时间片提高共享度
训练中途 CUDA out of memorybatch size 过大、多进程共用一张卡、或显存没释放nvidia-smi 看显存占用;DCGM_FI_DEV_FB_USED 曲线调小 batch、减少同卡容器数、给多进程设显存上限
驱动与 CUDA 版本不匹配,CUDA driver version is insufficient镜像里的 CUDA 运行时比节点驱动支持的版本新nvidia-smi 右上角的 CUDA Version 对比镜像要求升级驱动或换用匹配的镜像 tag;Operator 的 driver.version 要与镜像对齐
MIG 配置后卡不可用MIG 规格与卡型/驱动不匹配,或重配时仍有任务在跑kubectl get node -o jsonnvidia.com/mig-* 是否存在;MIG Manager 日志先清空该节点任务再改 nvidia.com/mig.config;确认卡型支持该规格
多副本推理互相抢显存副本数 × 单副本显存 > 卡容量,或 time-slicing 下无隔离nvidia-smi 看同卡进程;看 Pod 所在节点减少副本、换 MIG 切分、或把单副本放到独立卡上
HPA 不扩缩容GPU 利用率不是 HPA 的默认可读指标kubectl describe hpa 看是否 unknown 或缺少指标接 DCGM + prometheus-adapter/KEDA 暴露自定义指标,或先用队列长度做指标
节点上 nvidia-smi 正常但集群里看不到卡驱动装了但 device plugin/toolkit 没装kubectl get nodes -o jsonjq.items[].status.allocatable,里面没有 nvidia.com/gpu补装 GPU Operator 的 device-plugin 与 container-toolkit 组件

三个容易误判的细节

  • 「有卡」不等于「能用」allocatable 里有 nvidia.com/gpu 只说明资源上报成功;容器里能不能跑还要看 container-toolkit 是否配好、镜像里有没有 CUDA 运行时。
  • `nvidia.com/gpu` 的数字是字符串jq 里看到 "2" 不是 2,写脚本比较时别踩坑。
  • 改共享配置会重建 device plugin:切 MIG 或 time-slicing 都需要滚动重启 device plugin,期间新 Pod 可能调度失败;重配 MIG 还要先让节点上没有任务,否则切分不生效。具体行为取决于你的驱动与 Operator 版本。

自测题

自测:为什么 GPU 不能像 CPU 那样超卖?(点击展开答案)

因为 CPU 是「时间维度」上可切分的资源,内核用 CFS 让多个进程轮流占用同一个核,requests: 500m 表示「平均拿到半个核的时间」,即使超卖也只是变慢。GPU 的核心资源是显存算力上下文:显存是一块物理内存,两个进程同时申请同一段显存会互相踩写,结果是数据损坏或 CUDA OOM,而不是「慢一点」。

所以 Kubernetes 对扩展资源的规定是 requests 必须等于 limits,调度器按「整张卡」记账,绝不允许把一张卡算成两份。想提高利用率,正确的做法是在硬件或驱动层真的把卡切开(MIG 切成独立显存实例、time-slicing 让多个容器轮流用同一张卡),而不是在调度层把数字拆小。

自测:time-slicing 和 MIG 到底差在哪?(点击展开答案)

差别在于隔离发生在哪一层、隔开了什么。MIG 是硬件级切分:一张卡被切成若干个独立实例,每个实例有自己的一段显存和算力单元,资源名变成 nvidia.com/mig-1g.10gb,容器之间在物理上就看不到对方,因此适合多租户推理和稳定 SLA。代价是切分规格固定(比如 7 个 1g.10gb),改规格要清空节点重配,而且不是所有卡型都支持。

time-slicing 是软件级时间片轮转:device plugin 只是把一张卡报成 N 份,容器仍然能看到整张卡的全部显存,谁先用完谁先 OOM,同卡其它容器一起遭殃,延迟也会因为抢占而抖动。它便宜、配置简单,适合开发环境让几个人共用一张卡,但不能拿来做需要隔离的生产多租户。一句话:MIG 买的是隔离,time-slicing 买的是便宜。

自测:为什么推理服务的探针要放宽,尤其是 liveness?(点击展开答案)

因为推理服务的「启动慢」和「假死」在探针眼里长得一样。大模型加载权重可能耗时几十秒到几分钟,这段时间里端口还没监听、/health 还不通——如果 livenessProbe 的阈值很紧,kubelet 会判定容器已死并重启它,重启后又从头加载模型,永远卡在启动阶段,表现为 CrashLoopBackOff

正确做法是用 startupProbe 把「启动阶段」单独圈出来(periodSeconds × failureThreshold 就是允许的最长启动时间,按实测留 2 到 3 倍余量),startup 成功之前 liveness 与 readiness 都不执行;启动完成后,liveness 用较严格的阈值去发现真正的假死,readiness 则负责「模型没加载完就先别给流量」。这样三种状态各归各管,不会因为慢启动互相干扰。

小结

  • GPU 是扩展资源 nvidia.com/gpu:只能整数、必须写 limits、且 requests 必须等于 limits——不能超卖,共享必须发生在更下层。
  • Device Plugin 把设备上报成可调度资源,NFD/GFD 把硬件特性变成标签;两者分工不同,标签用来看,资源用来申请。
  • 调度靠 节点标签 + `nodeSelector`/`nodeAffinity` 挑机器,靠污点 + 容忍保护 GPU 节点不被普通业务占用;Insufficient nvidia.com/gpu 和「标签不匹配」是两类不同的 Pending。
  • 共享方案按隔离强度排序:整卡独占 > MIG > MPS > time-slicing;多租户推理选 MIG,开发共享才用 time-slicing。
  • 训练用 Job + PVC 检查点 + InitContainer 预热;推理用 Deployment + Service + 放宽的探针,HPA 要扩 GPU 必须接 DCGM 自定义指标
  • 监控看 DCGM Exporter 的利用率/显存/温度,成本问题几乎都是「卡占着但没在算」;用 ResourceQuota 限制扩展资源比事后看账单有效。

相关章节:硬件特性标签与 Device Plugin 的关系见节点与内核调优;requests/limits 与探针细节见第 11 章 探针与资源管理;标签、污点与 cordon 的完整用法见第 14 章 调度入门;检查点落盘见第 10 章 存储与 PVC;指标采集与看板见可观测性;GPU 节点的容量规划与机型选择见生产集群部署

练习

  1. 在你所在的环境里执行 kubectl get nodes -o json | jq '.items[].status.allocatable',回答:有几个节点上报了 nvidia.com/gpu?每台几张?如果一张都没有,按「节点有卡但集群看不到」这一行去排查,找出缺的是哪一层。
  2. 给 GPU 节点加上 nvidia.com/gpu=true:NoSchedule 污点,然后写一个不带容忍的 nginx Pod 和一个带容忍的 Pod,分别观察它们的调度结果与事件,解释为什么前者不会跑到 GPU 节点上。
  3. 假设你有一个 7B 模型的推理服务,单副本常驻 16GB 显存、冷启动 90 秒,要部署在 2 张 80GB 的卡上。请写出你会选哪种共享方案、副本数怎么定、startupProbe 的阈值怎么设,并说明如果流量翻倍时你打算怎么扩缩容(提示:先想清楚用什么指标)。

下一章我们把视角从「怎么跑起来」转到「怎么持续交付」:用 GitOps 让集群状态和代码仓库保持一致。