第28篇-Kubernetes-GPU调度机制-Device-Plugin与GPU-Operator
【AIaaS 全栈架构师】第 28 篇:Kubernetes GPU 调度机制——Device Plugin 与 GPU Operator
系列定位:AIaaS 全栈架构师教程,技术栈以 Go 为主。本篇进入模块五——GPU 集群与 Kubernetes 编排,聚焦 K8s 如何感知并调度 GPU 这种异构硬件。
本篇你将学到
- 为什么 K8s 原生调度器无法直接管理 GPU,需要引入 Device Plugin 机制
- K8s Extended Resource(扩展资源)的语义与注册流程
- NVIDIA Device Plugin 的工作原理:如何把物理 GPU 上报为
nvidia.com/gpu - GPU Operator 的全套自动化能力:驱动 / Runtime / Device Plugin / DCGM Exporter 一键部署
- Pod 如何在 YAML 里声明 GPU 请求、节点选择与设备隔离
- GPU 调度链路的典型故障排查思路
从本篇开始,我们离开单机视角。前面四个模块讲的都是「一台机器内」发生的事——训练并行、推理引擎、Go 网关。但真实的 AIaaS 平台动辄上百上千张 GPU,它们必须被一个统一调度器管理。这个调度器在云原生世界里的事实标准就是 Kubernetes。然而,K8s 原生只懂 CPU / 内存 / 本地存储三类资源,它根本不知道「GPU」为何物。这一篇,我们就来拆解 K8s 是如何一步步学会管理 GPU 的。
一、K8s 为什么管不了 GPU:原生调度的盲区
1.1 原生资源模型的三件套
Kubernetes 的调度器(kube-scheduler)本质上是一个「为 Pod 匹配 Node」的循环。它依据的核心数据结构是 Node.Status.Allocatable,里面记录了这台节点可分配的资源:
# 一个普通节点的 Allocatable 示例
status:
allocatable:
cpu: "32"
memory: 128Gi
storage: 200Gi
pods: "110"
capacity:
cpu: "32"
memory: 130Gi
storage: 200Gi
pods: "110"
调度器只认这三类核心资源:cpu、memory、ephemeral-storage。它们由 kubelet 在节点启动时从 cgroup / /proc/meminfo 自动采集并上报。Pod 在 resources.requests 里声明多少,调度器就扣减对应节点的 Allocatable,直到不够为止。
这套机制对 CPU/内存这种「可任意切分」的资源非常合适——10 个 Pod 各要 2 核,32 核节点还能再塞 6 个。但 GPU 是完全不同的硬件抽象:
| 维度 | CPU/内存 | GPU |
|---|---|---|
| 切分粒度 | 可毫核级切分 | 整卡(或 MIG 切片,粒度粗) |
| 设备暴露 | 无物理设备 | /dev/nvidia0 等设备文件 |
| 驱动依赖 | 内核通用 | 厂商私有驱动 + 用户态库 |
| 容器访问 | 无需特殊处理 | 需挂载设备 + 库 + 环境变量 |
| 状态采集 | 内核标准接口 | 厂商 SDK(NVML/DCGM) |
调度器就算知道「这台机器有 4 张 GPU」,也不知道该把哪张卡的设备文件挂进哪个容器、怎么避免两张卡被分给两个 Pod 却指向同一个 /dev/nvidia0。原生模型完全没有「设备」这个概念。
1.2 Extended Resource:K8s 的补丁方案
Kubernetes 给出的答案是 Extended Resource(扩展资源)。它允许任何外部组件向 K8s 注册一种新的资源类型,资源名遵循 域名/资源名 的格式,比如 nvidia.com/gpu、intel.com/sgx、cambricon.com/mlu。
扩展资源有两个关键限制:
- 整数计量:只能是整数(不能声明 0.5 个 GPU),调度器按整数扣减。
- 不能超卖:默认情况下 Extended Resource 不可 overcommit,调度器严格按
requests匹配 Allocatable。
注册扩展资源靠的是向 Node 对象打一个 patch:
# 给 node-gpu-01 注册 4 个 nvidia.com/gpu
kubectl patch node node-gpu-01 --type=json -p='[{
"op": "add",
"path": "/status/capacity/nvidia.com~1gpu",
"value": "4"
}]'
但「注册」只是告诉调度器「这台机器有 4 个」。真正把 GPU 设备挂进容器的工作,由 Device Plugin 完成。
二、Device Plugin:让 K8s 学会感知设备
2.1 Device Plugin 框架总览
Device Plugin 是 K8s 提供的一套 gRPC 接口框架,运行在每个节点上的 DaemonSet。它的核心职责有三:
- 盘点:检测本机有哪些设备,每张卡一个唯一 DeviceID。
- 上报:通过 gRPC 把设备列表告诉 kubelet,kubelet 再上报到 API Server,变成
nvidia.com/gpu的 Allocatable。 - 分配:当 Pod 被调度到本节点后,kubelet 调用 Device Plugin 的
Allocate接口,Device Plugin 返回该 Pod 应挂载的设备文件、驱动库、环境变量等,kubelet 把这些信息合入容器 spec。
注意一个常见误区:Device Plugin 不参与调度决策。它只负责「设备上报」和「设备分配」,调度器才是决定 Pod 落在哪台机器的角色。
2.2 Device Plugin 的 gRPC 接口
Device Plugin 与 kubelet 之间通过 Unix Socket /var/lib/kubelet/device-plugins/kubelet.sock 和反向 socket 通信。核心服务定义在 plugin.proto:
service DevicePlugin {
// DevicePlugin 注册自己,声明能管理的资源名(如 nvidia.com/gpu)
rpc Register(RegisterRequest) returns (Empty) {}
// DevicePlugin 向 kubelet 推送设备列表与状态
rpc ListAndWatch(Empty) returns (stream ListAndWatchResponse) {}
// kubelet 询问:这个 Pod 要的设备,要挂哪些东西进容器
rpc Allocate(AllocateRequest) returns (AllocateResponse) {}
// 可选:GetPreferredAllocation,用于拓扑优选
rpc GetPreferredAllocation(...) returns (...) {}
}
整个生命周期是:
- Device Plugin 启动 → 扫描
/dev/nvidia*、调用 NVML 拿到 GPU 数量与 UUID。 - 通过
Register告诉 kubelet:「我管理nvidia.com/gpu这类资源,socket 是 nvidia.sock」。 - 进入
ListAndWatch长连接,持续推送形如[{id: "GPU-uuid-0", health: "Healthy"}, ...]的设备清单。 - kubelet 把清单转为 Node.Status.Capacity / Allocatable 上报 API Server。
- 当一个请求
nvidia.com/gpu: 2的 Pod 被调度到本节点,kubelet 通过Allocate调用 Device Plugin,传入要分配的两个 DeviceID。 - Device Plugin 返回
ContainerResponse:列出要挂载的设备文件路径(/dev/nvidia0、/dev/nvidia1)、必要的库(/usr/lib/x86_64-linux-gnu/libnvidia-ml.so)、环境变量(NVIDIA_VISIBLE_DEVICES)。 - kubelet 把这些挂载点注入容器,Pod 启动后容器内
nvidia-smi就能看到对应的卡。
2.3 分配流程时序
这里的关键设计是 Device Plugin 决定「挂哪几张卡」。调度器只保证数量够,但具体是 node-gpu-03 的 GPU-5 还是 GPU-6,由 Device Plugin 的 Allocate 决定(默认按上报顺序)。这也是后面 GPU 拓扑感知调度要解决的问题——默认分配可能把两张卡放到不同的 NVLink 域,影响通信性能。
2.4 自定义 Device Plugin 的最小骨架
理解了原理,我们写一个最小的 Device Plugin 骨架,管理一种叫 example.com/fakegpu 的虚拟资源。这是一个 Go 程序,演示框架的核心交互:
package main
import (
"context"
"log"
"net"
"os"
"path/filepath"
"time"
"github.com/NVIDIA/go-nvlib/pkg/nvlib/device"
"github.com/NVIDIA/k8s-device-plugin/internal/plugin"
"k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1"
)
const (
socketDir = "/var/lib/kubelet/device-plugins"
resourceName = "example.com/fakegpu"
)
type FakeGPUPlugin struct {
devices map[string]v1beta1.Device // DeviceID → Device
}
func (p *FakeGPUPlugin) GetDevicePluginOptions(ctx context.Context, e *v1beta1.Empty) (*v1beta1.DevicePluginOptions, error) {
return &v1beta1.DevicePluginOptions{PreferRequiredChecks: true}, nil
}
// ListAndWatch 持续向 kubelet 推送设备列表
func (p *FakeGPUPlugin) ListAndWatch(e *v1beta1.Empty, srv v1beta1.DevicePlugin_ListAndWatchServer) error {
resp := new(v1beta1.ListAndWatchResponse)
for id := range p.devices {
resp.Devices = append(resp.Devices, &p.devices[id])
}
_ = srv.Send(resp)
ticker := time.NewTicker(5 * time.Second)
for range ticker.C {
// 实际生产中这里要重新探测设备健康度
_ = srv.Send(resp)
}
return nil
}
// Allocate 返回容器需要的挂载信息
func (p *FakeGPUPlugin) Allocate(ctx context.Context, req *v1beta1.AllocateRequest) (*v1beta1.AllocateResponse, error) {
resp := new(v1beta1.AllocateResponse)
for range req.ContainerRequests {
// 每个 Container 要的设备,告诉 kubelet 挂哪些东西
containerResp := v1beta1.ContainerAllocateResponse{
Devices: []*v1beta1.DeviceSpec{
{ContainerPath: "/dev/fakegpu0", HostPath: "/dev/fakegpu0", Permissions: "rw"},
},
Mounts: []*v1beta1.Mount{
{ContainerPath: "/usr/local/lib/fakedrv.so", HostPath: "/usr/local/lib/fakedrv.so", ReadOnly: true},
},
Envs: map[string]string{
"FAKE_GPU_VISIBLE_DEVICES": "0",
},
}
resp.ContainerResponses = append(resp.ContainerResponses, &containerResp)
}
return resp, nil
}
func main() {
// 1. 盘点设备
p := &FakeGPUPlugin{
devices: map[string]v1beta1.Device{
"fake-0": {ID: "fake-0", Health: v1beta1.Healthy},
"fake-1": {ID: "fake-1", Health: v1beta1.Healthy},
},
}
// 2. 注册(向 kubelet.sock 发送 RegisterRequest)
// 省略 socket 拨号代码,实际使用 client-go 或 v1beta1 的 Register 接口
// 3. 监听自己的 socket
os.MkdirAll(socketDir, 0755)
socket := filepath.Join(socketDir, "fakegpu.sock")
listener, _ := net.Listen("unix", socket)
s := v1beta1.NewDevicePluginServer(p)
log.Println("FakeGPU Device Plugin serving on", socket)
log.Fatal(s.Serve(listener))
}
这段代码省略了真实的 Register 与 socket 拨号细节(实际工程里通常基于 client-go 封装),但它清晰展示了 Device Plugin 的两件事:告诉 kubelet 我有哪些设备、告诉我把哪些设备怎么挂。NVIDIA 官方的 Device Plugin 就是这个模型的完整工业级实现,多了 GPU 健康度检测、MIG 切片管理、拓扑优选等能力。
三、NVIDIA Device Plugin:工业级实现
3.1 部署形态
NVIDIA Device Plugin 以 DaemonSet 形式部署,只跑在带 GPU 的节点上。最简部署用 Helm 或一份 DaemonSet 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:
nodeSelector:
nvidia.com/gpu.present: "true" # 只调度到 GPU 节点
tolerations:
- key: nvidia.com/gpu
operator: Exists # 容忍 GPU 节点的 taint
priorityClassName: system-node-critical
containers:
- name: nvidia-device-plugin
image: nvcr.io/nvidia/k8s-device-plugin:v0.14.5
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
volumes:
- name: device-plugin
hostPath:
path: /var/lib/kubelet/device-plugins
部署后,所有带 GPU 的节点会自动出现 nvidia.com/gpu 这条 Allocatable:
$ kubectl describe node node-gpu-01 | grep -A3 Allocatable
Allocatable:
cpu: 31
memory: 128Gi
nvidia.com/gpu: 4
3.2 关键启动参数
NVIDIA Device Plugin 通过命令行参数控制行为,常用的几个:
| 参数 | 作用 | 典型值 |
|---|---|---|
--fail-on-init-error=true | 初始化失败(驱动未装)是否直接退出 | true |
--mig-strategy=none | MIG 设备暴露策略(none/single/mixed) | none / mixed |
--device-list-strategy=envvar | Allocate 时如何告诉容器可见设备 | envvar / cdi |
--pass-device-specs=false | 是否返回 DeviceSpec(用于额外挂载) | false |
--device-id-strategy=uuid | 设备 ID 使用 UUID 还是索引 | uuid |
mig-strategy 是下一篇(MIG 分区)的重点,这里先记住它有三种取值。device-list-strategy 决定了 NVIDIA_VISIBLE_DEVICES 如何传递给容器——较新的 K8s 推荐使用 CDI(Container Device Interface),更安全。
3.3 Pod 声明 GPU 资源
设备注册好之后,Pod 这样请求 GPU:
apiVersion: v1
kind: Pod
metadata:
name: gpu-test
spec:
restartPolicy: Never
containers:
- name: cuda-container
image: nvcr.io/nvidia/cuda:12.4.0-base-ubuntu22.04
resources:
limits:
nvidia.com/gpu: 2 # 申请 2 张 GPU(必须放在 limits)
requests:
nvidia.com/gpu: 2 # requests 与 limits 必须相等
command: ["nvidia-smi"]
nodeSelector:
nvidia.com/gpu.present: "true"
tolerations:
- key: nvidia.com/gpu
operator: Exists
几点必须严格遵守的规则:
- 必须同时出现在 requests 和 limits:K8s 规定 Extended Resource 的 requests 必须等于 limits,否则调度失败。
- 不能写小数:
nvidia.com/gpu: 0.5无效。要做 GPU 共享见下一篇。 - 被调度后无法迁移:GPU 是本地设备,一旦绑定到某节点,Pod 重启也只会在同一节点重建。
- 资源类型只能是 limits 控制:调度器只看 limits(Extended Resource 视为 Guaranteed 等级)。
容器启动后执行 nvidia-smi:
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 535.104.05 Driver Version: 535.104.05 CUDA Version: 12.2 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC|
| 0 A100-SXM4-40GB Off | 00000000:07:00.0 Off | 0|
| 1 A100-SXM4-40GB Off | 00000000:08:00.0 Off | 0|
+-------------------------------+----------------------+----------------------+
虽然节点上有 8 张卡,容器内只能看到分配给它的 2 张。这正是 Device Plugin 的 Allocate 接口设置 NVIDIA_VISIBLE_DEVICES=0,1 的效果。
四、GPU Operator:驱动与组件的全自动部署
4.1 痛点:裸装一台 GPU 节点要做多少事
没有 GPU Operator 之前,运维一台 GPU 节点是这样的:
- 安装操作系统。
- 下载并安装对应内核版本的 NVIDIA 驱动(
apt install nvidia-driver-535,匹配内核 header,重启)。 - 安装 NVIDIA Container Toolkit(nvidia-runtime / nvidia-ctk)。
- 配置 containerd / docker 的 runtime class,加
nvidiaruntime。 - 给节点打
nvidia.com/gpu.present=true标签。 - 部署 NVIDIA Device Plugin DaemonSet。
- 部署 DCGM Exporter 用于监控。
- 配置 MIG(如果用 A100/H100)。
任何一步版本不匹配都会导致 GPU 不可用。集群规模到几十台节点时,这是噩梦。GPU Operator 把这一切收敛到一个 Operator 里。
4.2 GPU Operator 架构
GPU Operator 本质是一个 Operator(Controller + CRD)。它定义了 ClusterPolicy CRD,用户创建一份 ClusterPolicy 实例,Operator 按照这份声明的期望状态去 reconcile 各个 DaemonSet。
4.3 驱动容器化:最大的工程突破
最革命性的一点是 驱动容器化。传统方式驱动装在宿主机,要严格匹配内核版本。GPU Operator 允许把 NVIDIA 驱动打包进容器镜像,启动时在容器内编译 .ko 内核模块并加载到宿主机内核。这意味着:
- 节点只需一个标准 OS,无需预装驱动。
- 驱动版本可以随镜像升级,与 K8s 滚动更新统一。
- 不同节点可以使用不同驱动版本(按节点标签划分)。
代价是容器需要特权模式(必须能 modprobe 和访问 /lib/modules)。如果安全策略不允许特权容器,可以关闭驱动容器化,改用预装驱动模式。
4.4 ClusterPolicy 关键字段
一份生产可用的 ClusterPolicy 大致长这样:
apiVersion: nvidia.com/v1
kind: ClusterPolicy
metadata:
name: gpu-cluster-policy
spec:
driver:
enabled: true
image: nvidia/driver
repository: nvcr.io
version: 535-5.4.0-ubuntu22.04
useNvidiaDriverCRD: true
toolkit:
enabled: true
image: nvidia/container-toolkit
version: 1.15.0-ubuntu22.04
devicePlugin:
enabled: true
image: nvidia/k8s-device-plugin
version: v0.14.5
config:
name: device-plugin-config # 引用 ConfigMap
default: default
dcgmExporter:
enabled: true
image: nvidia/dcgm-exporter
version: 3.2.6-ubuntu22.04
serviceMonitoring: false
migManager:
enabled: true
config:
name: mig-config # MIG 分区配置
gfd:
enabled: true # GPU Feature Discovery
nodeStatusExporter:
enabled: true
validator:
image: nvidia/cloud-native/k8s-logical-edge-validator
version: v22.10.0
mig:
strategy: single # single / mixed
Operator 启动后,按字段逐项 reconcile。如果某节点没装驱动,nvidia-driver DaemonSet 在该节点启动一个驱动容器,编译内核模块、modprobe nvidia;接着 nvidia-container-toolkit 配置 containerd 的 nvidia runtime;device-plugin 上报设备;dcgm-exporter 开始吐指标。整个过程全自动,运维只需关心 ClusterPolicy 这一份数据。
4.5 GPU Feature Discovery:节点的自我介绍
GPU Operator 还会自动跑一个叫 GFD(GPU Feature Discovery) 的组件,给 GPU 节点打上详细的标签:
nvidia.com/gpu.count=8
nvidia.com/gpu.product=A100-SXM4-40GB
nvidia.com/gpu.memory=40536
nvidia.com/gpu.family=ampere
nvidia.com/mig.capable=true
nvidia.com/cuda.driver.major=535
nvidia.com/cuda.runtime.major=12
这些标签让调度可以更精细。比如要求「只要 A100 节点」:
nodeSelector:
nvidia.com/gpu.product: A100-SXM4-40GB
或者「至少 80GB 显存的卡」用 NodeAffinity:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: nvidia.com/gpu.memory
operator: Gt
values: ["80000"]
五、节点选择与拓扑感知
5.1 GPU 节点的标签与污点策略
生产集群通常给 GPU 节点打污点(taint),防止非 GPU Pod 误占:
# 给所有 GPU 节点打污点
kubectl taint node node-gpu-01 nvidia.com/gpu=true:NoSchedule
GPU Pod 必须显式 toleration 才能调度上去。这一步防的是:一个普通的 Java 微服务 Pod 不小心 requests 超大内存,被调度到 GPU 节点,把宝贵的 GPU 资源挤掉。结合 nodeSelector 和 tolerations,可以把 GPU 节点变成「专用资源池」。
5.2 NUMA 与 NVLink 拓扑:默认调度的盲区
到这里调度器只知道「数量够」,不知道「卡之间是否在同一个 NVLink 域」。看一个 A100 8 卡节点的拓扑:
$ nvidia-smi topo -m
GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7
GPU0 X NV4 NV4 NV4 SYS SYS SYS SYS
GPU1 NV4 X NV4 NV4 SYS SYS SYS SYS
GPU2 NV4 NV4 X NV4 SYS SYS SYS SYS
GPU3 NV4 NV4 NV4 X SYS SYS SYS SYS
GPU4 SYS SYS SYS SYS X NV4 NV4 NV4
GPU5 SYS SYS SYS SYS NV4 X NV4 NV4
GPU6 SYS SYS SYS SYS NV4 NV4 X NV4
GPU7 SYS SYS SYS SYS NV4 NV4 NV4 X
NV4 表示 4 NVLink(最高带宽),SYS 表示跨 NUMA 节点(最慢)。如果 Device Plugin 默认把 Pod 分到 GPU0 和 GPU4,那这个 Pod 拿到的 2 张卡跨了 QPI,NVLink 完全用不上。对训练任务这是致命的通信瓶颈。
解决方案是 拓扑感知调度。K8s 社区有几种做法:
- NUMA-aware scheduling:通过
Topology Manager在 kubelet 侧约束。 - Device Plugin 的
GetPreferredAllocation:让 Device Plugin 反向推荐「优先分配哪些设备」。 - 第三方调度器扩展:Volcano / Kueue / Run:AI 等支持 GPU 拓扑优选。
- NVIDIA Network Operator + gpu-feature-discovery:把 NVLink 拓扑也作为节点标签暴露给调度器。
最简单有效的是用 NVIDIA 维护的 nvidia.com/gpu.sharing-strategy 等扩展标签 + 在 Pod 上声明 podMetricsResource 让调度器感知。
5.3 故障排查清单
实际运维中最常见的几类 GPU 调度问题:
| 现象 | 可能原因 | 排查命令 |
|---|---|---|
| Pod 一直 Pending | 节点 Allocatable 为 0(Device Plugin 未启动) | kubectl describe node 看 nvidia.com/gpu 是否存在 |
| Pod 调度上去了但容器起不来 | 宿主机无驱动 / Toolkit 未配 | 节点上 nvidia-smi 是否正常 |
容器内 nvidia-smi 看不到卡 | NVIDIA_VISIBLE_DEVICES 错误 / runtime 没用 nvidia | 看 containerd 配置是否含 nvidia runtime |
| 节点 GPU 数量与实际不符 | Device Plugin 没扫到部分卡(硬件故障) | 看 Device Plugin 日志、dmesg |
| Pod 卡在 ContainerCreating | 设备文件挂载失败 | 看 kubelet 日志、/dev/nvidia* 是否齐全 |
| 多 Pod 抢同一张卡 | Device Plugin 状态错乱 | 重启 Device Plugin DaemonSet |
一个万能排查组合拳:
# 1. 节点级
nvidia-smi # 驱动是否正常
ls /dev/nvidia* # 设备文件是否齐全
cat /proc/driver/nvidia/version # 驱动版本
# 2. K8s 级
kubectl describe node node-gpu-01 | grep -A20 Allocatable
kubectl get ds -n kube-system | grep nvidia
kubectl logs -n kube-system <device-plugin-pod>
kubectl describe pod <pod-name> # 看 Events 段
# 3. 容器级
kubectl exec <pod> -- nvidia-smi
kubectl exec <pod> -- env | grep NVIDIA
六、生产落地:从一台到一千台
6.1 节点初始化流水线
把 GPU 节点纳入集群的标准化流水线:
从步骤 G 开始全部由 GPU Operator 自动完成。运维只负责 A–F,剩下交给 Operator。一台新 GPU 节点上线时间可以从原来的 2 小时压缩到 15 分钟。
6.2 资源配额与多租户
在多租户场景下,通过 ResourceQuota 限制每个 Namespace 可使用的 GPU 总量:
apiVersion: v1
kind: ResourceQuota
metadata:
name: gpu-quota
namespace: tenant-a
spec:
hard:
requests.nvidia.com/gpu: "8" # 租户 A 最多同时占 8 张 GPU
limits.nvidia.com/gpu: "8"
注意 ResourceQuota 只能控制「同时占用量」,不能控制「累计使用时长」。后者需要外部记账(GPU 时长计费),是模块六的内容。
6.3 升级与回滚
驱动升级是高风险操作。GPU Operator 推荐的灰度策略:
- 先升级一个节点(按 label 选 canary 节点)。
- 该节点驱逐所有业务 Pod(
kubectl drain)。 - Operator 触发
nvidia-driverDaemonSet 滚动更新。 - 等待新驱动加载完成,跑
validator自检。 - 解除 cordon,灰度观察 24 小时。
- 全量滚动升级。
驱动容器化的好处之一:版本回滚就是切回旧 DaemonSet 镜像,无需重新 apt 卸装。
本篇小结
| 主题 | 核心结论 |
|---|---|
| 原生资源模型 | K8s 原生只懂 CPU/内存/存储,不识别 GPU 这种设备 |
| Extended Resource | 域名/资源名 格式的扩展资源,整数计量、不可超卖 |
| Device Plugin 框架 | 三步:盘点设备 → ListAndWatch 上报 → Allocate 返回挂载信息 |
| NVIDIA Device Plugin | 工业级实现,DaemonSet 部署,把 GPU 上报为 nvidia.com/gpu |
| GPU Operator | 自动化部署驱动/Toolkit/Device Plugin/DCGM/Validator,收敛到一份 ClusterPolicy |
| 驱动容器化 | 把 NVIDIA 驱动打包进容器,启动时编译 .ko 加载,节点免预装驱动 |
| GPU Feature Discovery | 自动给节点打产品/显存/MIG 能力等标签,支撑精细化调度 |
| 节点选择 | nodeSelector + tolerations + 污点组合,把 GPU 节点做成专用池 |
| 拓扑感知 | 默认调度器不考虑 NVLink/NUMA,需要 GetPreferredAllocation 或第三方调度器 |
| 故障排查 | 三层排查:节点级(nvidia-smi)→ K8s 级(describe/logs)→ 容器级(exec) |
核心心智模型:调度器决定数量,Device Plugin 决定设备,GPU Operator 决定环境。三者分工明确,缺一不可。
下篇预告
GPU 调度解决了「整卡分配」的问题,但实际业务里大量场景只需要 GPU 的一小部分算力——比如一个小模型的推理服务,整张 A100 给它纯属浪费。下一篇我们讨论 GPU 共享与 MIG 分区,看 K8s 上如何把一张 GPU 切成多份给不同 Pod 共用,涉及 Time-Slicing、MPS、HAMi 显存隔离和 MIG 硬件分区四种方案及其选型。
如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。
更多推荐
所有评论(0)