16. GPU管理与调度
GPU 不是"一块更快的 CPU"
前面整个调度器讲的都是 CPU 和内存——可压缩资源、数量多、Node 间基本同构。GPU 打破了所有假设:
- 不可压缩——你没法给 GPU 分"时间片"。一块 GPU 同一时刻基本上只能做一个大计算。
- 异构——你的集群里可能有 A100、V100、T4 混着跑,它们的能力完全不一样。
- 稀缺——一个 Node 插不了几块 GPU,不能像 CPU 那样 96 核随便分。
- 拓扑敏感——GPU 之间通过 NVLink 互联,跨 PCIe 交换数据慢很多。你希望同一个训练的 Pod 尽量跑在 NVLink 可达的 GPU 上。
这些特性让 GPU 调度变成了一道融"资源管理 + 拓扑感知 + bin packing"的综合题。
K8S 原生:device plugin 框架
K8S 本身不"懂" GPU。它通过 Device Plugin 框架让外部组件告诉它哪个 Node 上有多少 GPU。
kubelet
│ gRPC: Register + ListAndWatch
│
├─ /var/lib/kubelet/device-plugins/nvidia-gpu.sock
│ │
│ Nvidia Device Plugin
│ │ 启动时调 nvidia-smi,发现 8 块 A100
│ │ 向 kubelet 注册:这个 Node 有 8 个 nvidia.com/gpu
▼
API Server → Node.Status.Capacity["nvidia.com/gpu"] = 8
Scheduler 看到 nvidia.com/gpu: 8,会把它当扩展资源调度——跟 CPU/内存一样的逻辑。Pod 只需要声明:
resources:
limits:
nvidia.com/gpu: 2
Scheduler 找有 2 个空闲 GPU 的 Node,把 Pod 分配过去。Pod 跑起来后,kubelet 告诉 Nvidia Device Plugin "给这个容器分配 GPU 0 和 GPU 3",Plugin 把 GPU 设备(/dev/nvidia0、/dev/nvidia3)暴露进容器。
Device Plugin 的接口
接口很简单,定义在 k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1:
// Device Plugin 需要实现的 gRPC 接口
service DevicePlugin {
rpc GetDevicePluginOptions(Empty) returns (DevicePluginOptions);
rpc ListAndWatch(Empty) returns (stream ListAndWatchResponse);
rpc Allocate(AllocateRequest) returns (AllocateResponse);
rpc PreStartContainer(PreStartContainerRequest) returns (PreStartContainerResponse);
}
// 核心消息
message ListAndWatchResponse {
repeated Device devices = 1;
}
message Device {
string ID = 1; // "gpu-0", "gpu-1"
string health = 2; // "Healthy" / "Unhealthy"
}
Nvidia Device Plugin 的 ListAndWatch 发一个 stream 给 kubelet,告诉它"这个 Node 上有 8 个 GPU"。当 GPU 挂了(nvidia-smi 报错),它重新推送状态,kubelet 更新 Node Status,Scheduler 不再把 Pod 分到这块 GPU 上。
Allocate 是在 Pod 被调度到节点之后、容器启动之前调用的——Plugin 告诉 kubelet "把这个 Pod 需要的 GPU 设备文件、库文件、环境变量挂进容器"。
Nvidia 开源的 device plugin 实现里(NVIDIA/k8s-device-plugin),Allocate 做的就是:
// nvidia-device-plugin/internal/plugin/server.go 简化
func (s *server) Allocate(ctx context.Context, req *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
containerResp := &pluginapi.ContainerAllocateResponse{
Envs: map[string]string{
"NVIDIA_VISIBLE_DEVICES": "0,3", // 让容器只能看到 GPU 0 和 GPU 3
},
Mounts: []*pluginapi.Mount{
{HostPath: "/usr/bin/nvidia-smi", ContainerPath: "/usr/bin/nvidia-smi"},
},
}
return &pluginapi.AllocateResponse{ContainerResponses: []*pluginapi.ContainerAllocateResponse{containerResp}}, nil
}
思考题:Device Plugin 在
ListAndWatch里告诉 kubelet "8 块 GPU 都健康"——但如果 node 上第 3 块 GPU 在运行时挂了(nvidia-smi报 Xid error),kubelet 怎么知道?已经在用这块 GPU 的 Pod 会怎样?
MIG:一块 GPU 拆成多个"实例"
A100/H100 支持 MIG(Multi-Instance GPU),把一块物理 GPU 拆成多个独立的逻辑实例——每个实例有自己的显存、计算单元、L2 缓存,互不干扰。
# 把一块 A100-40GB 切成两片 20GB
nvidia-smi mig -cgi 5,5 -C
对 K8S 来说,原来一个 Node 有 1 个 nvidia.com/gpu,MIG 切了之后变成 2 个 nvidia.com/mig-3g.20gb。Pod 申请的只是一片 20GB——适合推理这种不需要整块 A100 的负载。切完后的调度逻辑不变——Scheduler 还是看 Node 有多少 "nvidia.com/mig-3g.20gb"。
GPU 共享:比 MIG 更细的粒度
GPU 能分时复用的原因跟 CPU 类似——推理任务不总是占满 GPU。一个 7B 模型每秒能处理几百个请求,给一个 Pod 独占就浪费了。如果多个 Pod 共用一块 GPU,性能损失在 5-10% 以内,成本能省一大半。
GPU 共享比 MIG 灵活——不需要重启 Node、不需要出厂时切割、可以切很多份。Nvidia 自己的 Time-Slicing(GPU Operator time-slicing)是最基础的方案,还有很多开源项目在这个基础上做文章。
Time-Slicing:简单但有局限
Nvidia GPU Operator 内置了 time-slicing 支持:
# 通过 ConfigMap 配置
apiVersion: v1
kind: ConfigMap
metadata:
name: time-slicing-config
data:
any: |-
version: v1
sharing:
timeSlicing:
renameByDefault: false
failRequestsGreaterThanOne: false
resources:
- name: nvidia.com/gpu
replicas: 8 # 一块 GPU 冒充 8 个"副本"
配置完之后,Pod 申请的 nvidia.com/gpu: 1 实际上只拿了 1/8 块 GPU 的时间片。Scheduler 看到 8 个可调度单位,调度逻辑不用改。
但 time-slicing 没有显存隔离——8 个 Pod 共用同一块 40G 显存,一个 Pod OOM 会导致同一块 GPU 上的其他 Pod 一起挂。
思考题:训练任务对显存要求高,推理任务对延迟敏感——同一块 A100 上,一个训练 Pod 需要独占,一个推理 Pod 只需要 1/8 时间片。Time-Slicing 能把它们分到同一块 GPU 上吗?如果不能,怎么在"共享"和"独占"之间划清界限?
MPS + MIG + 资源控制:更完整的共享
生产上 GPU 共享的组合拳通常是:
| 方案 | 解决什么 |
|---|---|
| MIG | 硬件级显存隔离——40G 切两片,互不干扰 |
| MPS | CUDA 进程级并发——多个进程用同一块 GPU,硬件调度 |
| 显存限制 | 驱动层面限制每个容器能用的显存上限 |
| 算力 QoS | 限制每个容器的 GPU 利用率百分比 |
这套东西比 CPU/内存的 cgroup 复杂,因为 Nvidia 驱动暴露的接口不一样。Hami(阿里开源)和 Volcano 是这方面比较成熟的方案。
算力的"cgroup":Volcano 的 GPU Share
volcano.sh/devices 走的是 K8S 扩展调度器的路子——它在 Scheduler 里注册自定义 Plugin,支持 fraction 级别的 GPU 调度:
# Volcano Pod 的声明
resources:
requests:
volcano.sh/vgpu-memory: 8000 # 8GB 显存
volcano.sh/vgpu-cores: 30 # 30% 算力
volcano.sh/vgpu-memory 落到 Nvidia 驱动层面,通过修改容器的 CUDA_VISIBLE_DEVICES 环境变量 + 驱动层面的显存限制来实现隔离。volcano.sh/vgpu-cores 则是 GPU 利用率的时间片限制。
思考题:Device Plugin 在
ListAndWatch里告诉 kubelet "8 块 GPU 都健康"——但如果 node 上第 3 块 GPU 在运行时挂了(nvidia-smi报 Xid error),kubelet 怎么知道?已经在用这块 GPU 的 Pod 会怎样?
GPU Operator:一键配齐 K8S GPU 栈
手动装 GPU 支持需要三步:驱动 → Device Plugin → 特性(MIG/Time-Slicing)。GPU Operator 打包成了一键 Helm:
helm install gpu-operator nvidia/gpu-operator \
--set driver.enabled=true \
--set mig.strategy=mixed \
--set tool kit.enabled=true
部署后 kube-system 自动跑起来:
nvidia-device-plugin ← 向 kubelet 注册 GPU
nvidia-dcgm-exporter ← 暴露温度/功耗/SM 利用率 Prometheus 指标
nvidia-container-toolkit ← 容器内 CUDA 运行时
nvidia-driver-daemonset ← 驱动安装(可选)
gpu-feature-discovery ← 发现 Node GPU 特性(MIG/NVLink)
GPU 监控:DCGM
DCGM(Data Center GPU Manager)内嵌在 GPU Operator 中,暴露的关键指标:
| 指标 | 含义 |
|---|---|
DCGM_FI_DEV_GPU_UTIL | GPU 利用率——低则浪费 |
DCGM_FI_DEV_GPU_TEMP | 温度 >85°C 告警 |
DCGM_FI_DEV_XID_ERRORS | 硬件故障,需要 RMA |
DCGM 是 GPU 集群的"眼睛"——没它你只知道 GPU 被分配了,不知道是真在工作还是空转。
训练任务的容错:Checkpoint
GPU Pod 挂了代价极高——几小时的训练白费。PyTorch checkpoint 写到 PV 是最基本的保护:
torch.save({
'epoch': epoch,
'model_state_dict': model.state_dict(),
}, checkpoint_path)
Pod 被驱逐后新 Pod 从 PV 读最近 checkpoint 继续训练。PVC 必须用云盘或 NFS——不能用 hostPath(Pod 漂移后丢失)。
思考题:8 块 A100,两两之间通过 NVLink 互联——第一个 NVSwitch 区的 4 块 GPU 和第 2 个区的 4 块之间带宽低很多。分布式训练 Pod 需要 4 块 GPU——怎么保证选到的 4 块都在同一个 NVSwitch 区而不是跨区?谁在帮 Scheduler 做这个拓扑感知?
K3S 的 GPU 支持
K3S 支持 Device Plugin 框架——因为它用的是同一个 kubelet 和 containerd。你在 K3S 集群上部署 Nvidia Device Plugin 的方式跟在 K8S 上完全一样。
但 K3S 的 GPU 场景有几个实际的点要注意:
- 边缘推理——K3S 常跑在边缘 Node 上(摄像头、工控机),如果 Node 上有 Jetson、嵌入式 GPU(Orin NX 这类),Nvidia Device Plugin 需要 Jetson 版本(
nvcr.io/nvidia/k8s-device-plugin:jetson),不是 x86 版本。 - 单节点 GPU——K3S 的默认架构是单节点(一个 server),如果 GPU 也在这台机器上,调度和 GPU 分配的逻辑在同一个进程里完成,延迟最小。
- GPU 共享在边缘收益最大化——边缘设备 GPU 稀缺(通常只有一块 Jetson),GPU 共享让多个推理服务跑在同一块 GPU 上变得可能。Volcano 的 GPU Share、Hami 都支持 K3S 原生部署。
小结
- GPU 不可压缩、异构、稀缺,调度复杂度远超 CPU/内存
- Device Plugin 框架让外部 GPU "注册"到 K8S 的扩展资源系统
- MIG 硬件切割 GPU 成独立实例,适合推理负载
- GPU 共享(time-slicing + MPS + 显存限制)解决"一块 GPU 不够分"的问题
- K3S 完全支持 GPU(通过同一套 Device Plugin),边缘设备用 Jetson 生态
- GPU 共享在 K3S 边缘场景收益最大
解答
1. GPU 运行时挂掉——kubelet 怎么知道?
Device Plugin 的 ListAndWatch 是一个 gRPC stream——不是一次性调用,而是持续推。Nvidia Device Plugin 内部有个定时器定期调 nvidia-smi 检查 GPU 健康:
// nvidia-device-plugin 内部健康检查逻辑简化
func (p *plugin) healthCheck() {
for _, gpu := range p.devices {
if gpu.hasXidError() { // Xid error → GPU 硬件故障
gpu.health = "Unhealthy"
}
}
// ListAndWatch stream 推送更新后的设备列表
p.grpcStream.Send(&pluginapi.ListAndWatchResponse{Devices: p.devices})
}
GPU 从 Healthy 变成 Unhealthy 后,kubelet 更新 Node.Status.Allocatable——这块 GPU 不再计入可分配资源。已经分配了这块 GPU 的已运行 Pod 不会立即被杀——kubelet 不主动驱逐。但设备挂了之后容器内的 CUDA 调用会失败——如果应用没有 fault-tolerance,大概率自己 crash 重启。
Scheduler 的后续行为:新 Pod 也不会被调度到这块 GPU 上,因为 Scheduler 只看 Node Status 里的 Allocatable。
2. Time-Slicing 能把训练 Pod 和推理 Pod 分到同一块 GPU 上吗?
不能有效区分。 Time-Slicing 根本不区分"这个 Pod 需要独占 GPU"还是"这个 Pod 分 1/8 就够"——它统一把一块 GPU 切成 N 个副本,所有 Pod 平等竞争时间片。
训练 Pod 需要连续的计算(一个 step 跑完才能梯度同步),Time-Slicing 会让它的计算片断断续续——吞吐下降 30-50%。推理 Pod 对延迟敏感,time-slicing 切得越细,每个 Pod 的调度延迟越高。
实际做法是分层隔离:
| 场景 | 方案 | 怎么隔离 |
|---|---|---|
| 训练 Pod 需要独占 | MIG 分区——1 块 GPU 切出一片给训练 | 硬件显存隔离,训练独占一片 |
| 推理 Pod 共享一块 GPU | 同 GPU 的另一个 MIG 分区 + Time-Slicing | MIG 把训练和推理的显存隔开,推理侧 time-slice |
| Nvidia GPU Operator | nvidia.com/gpu(整卡) 和 nvidia.com/mig-3g.20gb(MIG 片)不同资源名 | Scheduler 把它们当不同的资源,训练申请整卡,推理申请 MIG 片 |
不同 GPU SKU 的申请资源名不一样——Scheduler 不会把整卡申请和 MIG 申请混在一起分配。
3. NVLink 拓扑感知调度
GPU Operator 1.12+ 集成了 Nvidia Topology-Aware GPU Scheduler Plugin(基于 K8S Scheduling Framework),在 Filter 和 Score 阶段做拓扑匹配。
Pod 声明需要 NVLink 连接的 GPU:
metadata:
annotations:
nvidia.com/gpu-topology: "nvlink" # 只要 NVLink 连接的 GPU
Scheduler Plugin 在 Score 阶段做决策:
- 从 Node 上的 Nvidia Topology Manager 拿到 GPU 之间的拓扑距离矩阵(NVSwitch id、PCIe 交换层级)
- 选择 NVLink 带宽最高的一组 GPU
- 同 NVSwitch 的 ≥ 跨 NVSwitch 的 > 跨 PCIe switch 的
如果没有 NVLink 连接的 4 块 GPU 空着——Filter 阶段淘汰这个 Node。
Nvidia 的 k8s-dra-driver(Dynamic Resource Allocation driver,K8S 1.26+)也在推进 GPU 拓扑感知的标准化,目标是让拓扑调度成为 K8S 原生功能而不是 Device Plugin 的专有扩展。
下一章,当你的 GPU 集群装不下更多设备,或者需要在不同地区、不同机房分别管理——多集群调度。