Skip to main content

16. GPU管理与调度

GPU 不是"一块更快的 CPU"​

前面整个调度器讲的都是 CPU 和内存——可压缩资源、数量多、Node 间基本同构。GPU 打破了所有假设:

  1. 不可压缩——你没法给 GPU 分"时间片"。一块 GPU 同一时刻基本上只能做一个大计算。
  2. 异构——你的集群里可能有 A100、V100、T4 混着跑,它们的能力完全不一样。
  3. 稀缺——一个 Node 插不了几块 GPU,不能像 CPU 那样 96 核随便分。
  4. 拓扑敏感——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 切两片,互不干扰
MPSCUDA 进程级并发——多个进程用同一块 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_UTILGPU 利用率——低则浪费
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 场景有几个实际的点要注意:

  1. 边缘推理——K3S 常跑在边缘 Node 上(摄像头、工控机),如果 Node 上有 Jetson、嵌入式 GPU(Orin NX 这类),Nvidia Device Plugin 需要 Jetson 版本(nvcr.io/nvidia/k8s-device-plugin:jetson),不是 x86 版本。
  2. 单节点 GPU——K3S 的默认架构是单节点(一个 server),如果 GPU 也在这台机器上,调度和 GPU 分配的逻辑在同一个进程里完成,延迟最小。
  3. 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-SlicingMIG 把训练和推理的显存隔开,推理侧 time-slice
Nvidia GPU Operatornvidia.com/gpu(整卡) 和 nvidia.com/mig-3g.20gb(MIG 片)不同资源名Scheduler 把它们当不同的资源,训练申请整卡,推理申请 MIG 片

不同 GPU SKU 的申请资源名不一样——Scheduler 不会把整卡申请和 MIG 申请混在一起分配。

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 阶段做决策:

  1. 从 Node 上的 Nvidia Topology Manager 拿到 GPU 之间的拓扑距离矩阵(NVSwitch id、PCIe 交换层级)
  2. 选择 NVLink 带宽最高的一组 GPU
  3. 同 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 集群装不下更多设备,或者需要在不同地区、不同机房分别管理——多集群调度。