课程目录(第 32 章 / 共 33 章)
GPU 与 AI 工作负载:调度、共享与推理平台
把 GPU 变成集群里可调度、可共享、可观测的资源:扩展资源模型、Device Plugin、MIG 与时间片共享,以及训练任务与推理服务的工程细节。
学完这一章,你将能够
- ✓说清 nvidia.com/gpu 为什么不能超卖,以及 requests 与 limits 为什么必须相等
- ✓用节点标签、nodeSelector 与污点容忍把 AI 任务固定到 GPU 节点,并排查 GPU 类 Pending
- ✓在整卡独占、MIG、时间片与 MPS 之间按场景做出取舍
- ✓给推理服务配上合理的探针、HPA 与监控,解释「GPU 利用率低但账单高」
为什么 GPU 要单独讲一遍
CPU 和内存是节点自带的:内核一启动就知道有多少核、多少内存,kubelet 直接把它们写进 Capacity 与 Allocatable,调度器也天然会按它们过滤和打分。GPU 不是这样——它是一张挂在 PCIe 上的卡,内核默认不认识它,kubelet 也不会自己去数。
所以「让集群能用 GPU」这件事,本质上是要把一张设备变成一种资源:装驱动、注册插件、上报数量,然后才能像申请 CPU 一样写进 Pod。这中间少一环,Pod 就只是「被调度到了有卡的机器上,但容器里看不见卡」。
更麻烦的是,GPU 贵、不可超卖、而且经常要多人共享。CPU 可以 requests: 500m 挤一挤,GPU 不能这么干。这一章就按「资源模型 → 安装验证 → 调度 → 共享 → 训练与推理 → 监控成本」的顺序,把这条链路走完。
GPU 的资源模型:扩展资源与 Device Plugin
Kubernetes 里除了 cpu、memory、ephemeral-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: 500m、limits: 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 等)和驱动库挂进容器。
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-smi 报 no 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 排错)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安装完先确认组件状态,再确认资源真的上报到了节点:
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 node 的 Capacity 和 Allocatable 两张表里都能看到 nvidia.com/gpu: 2。Allocatable 一般等于 Capacity,但如果 kubelet 配了 --system-reserved 且预留了设备,两者可能不同——取决于你的 kubelet 配置。
最后跑一次真实计算,确认「容器里真的能用卡」而不只是「数量对上了」:
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-checknvidia-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: Completed 且 Status: Success | 缺 nvidia-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.product | NVIDIA-H100-SXM5-80GB | 按型号挑:新卡跑训练、老卡跑推理 |
nvidia.com/gpu.count | "8" | 把大机器留给多卡任务 |
nvidia.com/gpu.memory | "81920"(MiB) | 显存是硬约束,先按它过滤 |
nvidia.com/mig.capable | "true" | 挑支持 MIG 的卡做多租户推理 |
# 列出所有节点的卡型、卡数与显存
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 自动打,或者你自己打一个实例类型标签:
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 里的真实名字):
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 节点的污点:
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第三步,应用并观察:
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-demoPod 变成 Completed、日志里能看到显卡信息,就算成功。顺手把污点删掉,别留给下一个实验:kubectl taint nodes "$NODE" nvidia.com/gpu=true:NoSchedule-。
如果 Pod 卡在 Pending,事件里的原因分两种,先看清再动手:
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 | 硬件级切分,接近独占 | 是(每实例独立显存) | 推理多租户、稳定 SLA | GPU Operator migManager.enabled=true + 节点标签 nvidia.com/mig.config | 切分规格固定、不是所有卡/驱动都支持、重配要先清空任务 |
| time-slicing | 弱,无隔离 | 否(共用同一块显存) | 开发/实验环境共享 | device plugin 的共享 ConfigMap + 节点标签 | 一个容器 OOM 会影响同卡其它容器、延迟抖动、计量不准 |
| MPS | 进程级并行,算力利用率高 | 部分(可按进程限制显存) | 小模型高并发推理 | 节点层面配置,device plugin 的共享配置里有实验性支持 | 配置复杂、异常可能连带、具体能力取决于版本 |
MIG 是最推荐的多租户方案,因为它把卡在硬件层面切成独立实例,每个实例有独立的显存和算力,容器之间互不可见。开启后资源名会变成 nvidia.com/mig-1g.10gb 这种,节点上会出现 7 个可用实例:
# 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 份:
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 Operator(TrainJob CRD,屏蔽 PyTorchJob/MPIJob 差异)和 Ray(RayCluster/RayJob)是社区两个主流选择,从单卡实验走到多卡训练还要补上排队、通信验证与检查点,这些见 AI 平台:批调度、分布式训练与模型服务。
推理服务:Deployment + Service + HPA
推理是长期在线的服务,用 第 6 章 Deployment + 第 7 章 Service 这一套标准组合,但有三处必须为 GPU 调整:
探针要放宽。 大模型加载几 GB 甚至几十 GB 权重,冷启动可能几十秒到几分钟。用 startupProbe 给足时间,readinessProbe 检查模型真正可服务(比如 /health 返回 200),livenessProbe 千万不要检查下游依赖——模型加载慢导致的误杀会让 Pod 陷入重启循环,每次都重新加载一遍,永远起不来。细节见第 11 章 探针与资源管理。
HPA 默认看不了 GPU。 HorizontalPodAutoscaler 的 type: 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/gpu 与 limits.nvidia.com/gpu,值必须是整数),这样某个团队的命名空间最多只能用 N 张卡,超过就直接创建失败;这比事后看账单有效得多,配合第 12 章 命名空间与 RBAC 一起用。再往前一步是优先级与抢占:给关键训练任务设高 PriorityClass,资源不够时调度器可以抢占(驱逐)低优先级任务,注意被抢占的任务要能靠检查点恢复。
常见坑与排错
GPU 的问题排查顺序是固定的:先确认节点有没有资源 → 再确认 Pod 有没有资格上去 → 最后确认容器里看不看得见卡。
| 现象 | 原因 | 怎么确认 | 怎么办 |
|---|---|---|---|
容器里没有 GPU,nvidia-smi 报 no CUDA-capable device is detected | device plugin 未就绪,或 Pod 没申请 GPU | kubectl -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 memory | batch 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 json 看 nvidia.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 json 加 jq 看 .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 节点的容量规划与机型选择见生产集群部署。
练习
- 在你所在的环境里执行
kubectl get nodes -o json | jq '.items[].status.allocatable',回答:有几个节点上报了nvidia.com/gpu?每台几张?如果一张都没有,按「节点有卡但集群看不到」这一行去排查,找出缺的是哪一层。 - 给 GPU 节点加上
nvidia.com/gpu=true:NoSchedule污点,然后写一个不带容忍的 nginx Pod 和一个带容忍的 Pod,分别观察它们的调度结果与事件,解释为什么前者不会跑到 GPU 节点上。 - 假设你有一个 7B 模型的推理服务,单副本常驻 16GB 显存、冷启动 90 秒,要部署在 2 张 80GB 的卡上。请写出你会选哪种共享方案、副本数怎么定、
startupProbe的阈值怎么设,并说明如果流量翻倍时你打算怎么扩缩容(提示:先想清楚用什么指标)。
下一章我们把视角从「怎么跑起来」转到「怎么持续交付」:用 GitOps 让集群状态和代码仓库保持一致。