GPU 设备:Device Plugin 与设备注入¶
PodSpec 中的 nvidia.com/gpu: 1 是一份整数扩展资源申请。Scheduler 根据这项申请选择 GPU 数量充足的 Node;Pod 绑定后,Kubelet Device Manager 选择具体 GPU UUID,调用 NVIDIA Device Plugin 的 Allocate,再将 CDI 设备信息交给 containerd。
完整路径依次经过裸机 Driver、nvkind、Device Plugin、Scheduler、Kubelet Device Manager 和 containerd。Pod 停在 Pending 时,证据集中在资源发布和调度;Pod 已绑定节点后出现 CreateContainerError,证据集中在 Allocate、CDI 和容器运行时。
A100 资源发布链路¶
当前环境将一张空闲且未启用 MIG 的 A100 80 GB 交给 nvkind worker。worker 的设备视图和 Node Capacity 都以这张 GPU 为边界。
下图展示了物理 GPU 进入 worker 并注册为 nvidia.com/gpu 的过程。
flowchart TB
H["Ubuntu 裸机\nNVIDIA Driver ≥ 580"]
T["NVIDIA Container Toolkit\nDocker/CDI 支持"]
N["nvkind\n选择一个 GPU 注入 worker"]
W["kind worker\n选中的 GPU 可见"]
P["NVIDIA Device Plugin\nListAndWatch"]
A["Node Allocatable\nnvidia.com/gpu = 1"]
H --> T --> N --> W --> P --> A
Driver 负责 Linux 内核与物理设备通信,Container Toolkit 配置容器运行时的 NVIDIA 设备支持,nvkind 将宿主机 GPU 传入 kind worker,Device Plugin 再向 Kubelet 注册扩展资源。Node Status 中的 nvidia.com/gpu 在 Device Plugin 完成注册和上报后出现。
GPU Operator v26.3.3 运行在集群中,配置关闭 Operator 对 Driver、Toolkit 和 Device Plugin 的管理。裸机提供 Driver 与 Toolkit,单卡 Device Plugin DaemonSet 负责资源发布。该 DaemonSet 固定到 worker,并通过 ConfigMap 中的 GPU UUID 限制插件容器的设备视图:
env:
- name: NVIDIA_VISIBLE_DEVICES
valueFrom:
configMapKeyRef:
name: single-gpu-device-plugin
key: selected-gpu-uuid
- name: MIG_STRATEGY
value: none
- name: DEVICE_LIST_STRATEGY
value: cdi-annotations,cdi-cri
- name: DEVICE_ID_STRATEGY
value: uuid
这份清单面向单 worker、单 GPU 的 nvkind 环境,Node 最终发布一个 nvidia.com/gpu。
Device Plugin 注册与健康状态¶
Kubelet 在 /var/lib/kubelet/device-plugins 下提供注册 socket。NVIDIA Device Plugin 启动自己的 Unix socket 后,向 Kubelet 发送 Register,声明 API 版本、socket 名和资源名 nvidia.com/gpu。Kubelet 随后连接插件并调用 ListAndWatch。
下图展示了资源注册、Node Status 更新和容器分配的调用顺序。
sequenceDiagram
autonumber
participant P as NVIDIA Device Plugin
participant K as Kubelet Device Manager
participant A as API Server
participant S as Scheduler
participant C as containerd
P->>K: Register(socket, v1beta1, nvidia.com/gpu)
K->>P: ListAndWatch
P-->>K: GPU UUID + Healthy
K->>A: Node capacity/allocatable = 1
S->>A: Bind 请求 1 GPU 的 Pod 到 worker
K->>P: Allocate(具体 device ID)
P-->>K: CDI device names/annotations
K->>C: CreateContainer
ListAndWatch 持续报告设备 ID 和 Health,Kubelet 据此更新内部 checkpoint 与 Node Status。Healthy 设备进入可分配集合;设备变为 Unhealthy 后,Kubelet 将其移出新的分配候选。已经运行的容器仍由硬件状态和进程行为决定,Device Plugin 不负责应用迁移。
Kubelet 重启后,Device Plugin 会重新注册。Device Plugin DaemonSet 处于 Running 而 Node Capacity 没有 GPU 时,应检查 socket、注册版本、插件初始化和设备可见性;这些步骤发生在 Scheduler 核算资源之前。
Scheduler 的 GPU 数量核算¶
vLLM 容器声明以下 GPU limit,并省略同名 request:
GPU 扩展资源使用整数,普通 Device Plugin 模式要求 request 与 limit 相等。Pod 省略 GPU request 时,Kubernetes 使用 limit 值作为 request,因此 Scheduler 按一张 GPU 核算资源。它读取 Node Allocatable,减去已绑定 Pod 的 GPU request,判断 worker 是否还能容纳新 Pod。节点过滤与绑定机制见 Scheduler:Pod 的节点选择。
nvidia.com/gpu request 同时构成节点约束。当前 Deployment 省略 nodeSelector,Scheduler 保留已发布该资源且可分配数量不少于 1 的节点。
Scheduler 完成 Binding 后,PodSpec 的 spec.nodeName 记录目标节点;Kubelet Device Manager 在节点本地保存 GPU UUID 映射。下表列出了各层证据对应的结论。
| 证据 | 结论 |
|---|---|
| Scheduler 日志中的 Node 过滤与 Binding | Pod 的目标节点及选择原因 |
| Node Capacity/Allocatable | Device Plugin 注册的资源总量和可分配上限 |
Device Plugin Allocate 日志或 Kubelet checkpoint |
Kubelet 为容器分配的 device ID |
容器内 nvidia-smi |
运行时最终暴露了哪张 GPU |
Node Status 中的 Allocatable 记录节点可供调度的资源上限。一个 Pod 占用 GPU 后,该字段通常仍为 1;Scheduler 从上限中减去已绑定 Pod 的 request,计算当前可用数量。
Allocate 与 CDI 设备注入¶
Device Plugin API 允许 AllocateResponse 返回环境变量、mount、设备节点、annotation 和 CDI device name。当前配置使用 cdi-annotations,cdi-cri,插件同时返回 CDI annotation 和 CRI CDIDevices 字段所需的信息;Kubernetes v1.36 与当前 containerd 消费这些 CDI 信息。
CDI(Container Device Interface)用规范文件给设备命名,并描述容器需要加入的设备节点、mount、环境变量或 hook。containerd 收到 Kubelet 的 CreateContainer 后解析 CDI 设备名,把对应编辑合并到 OCI Runtime Spec,runc 再按最终配置创建进程。
下图展示了 GPU request 从节点选择进入设备分配,再转换为 OCI Runtime Spec 的过程。
flowchart LR
R["Pod request\nnvidia.com/gpu=1"]
S["Scheduler\n选择 worker"]
D["Device Manager\n选择 GPU UUID"]
A["Device Plugin\nAllocate"]
CDI["CDI spec\n设备节点、mount、env"]
CRI["containerd\n修改 OCI spec"]
RUN["runc\n启动 vLLM"]
R --> S --> D --> A --> CDI --> CRI --> RUN
DEVICE_ID_STRATEGY=uuid 让 Kubelet 和插件使用稳定的 GPU UUID。MIG_STRATEGY=none 发布整张 A100;vLLM 获得完整设备,Kubernetes 按一张 GPU 进行分配。
RuntimeClass 与 CDI¶
Deployment 的 runtimeClassName: nvidia 选择节点上名为 nvidia 的 runtime handler。CDI 描述目标设备以及需要合并到 OCI Runtime Spec 的编辑。RuntimeClass 选择运行时配置,CDI 提供设备配置,两者共同进入 containerd 的容器创建路径。容器创建过程见 容器运行时:从 CRI 到容器进程。
容器内的 nvidia-smi 输出证明 NVIDIA 管理库和设备可见,一次成功的 vLLM CUDA 推理则证明应用已经使用该设备完成计算。
DRA 设备分配¶
Dynamic Resource Allocation(DRA)使用 ResourceSlice、DeviceClass、ResourceClaim 和 ResourceClaimTemplate 表达设备及其申请,Scheduler 可以根据设备属性参与选择。当前 Deployment 使用 NVIDIA Device Plugin 的整数扩展资源路径,清单和验证流程均以 nvidia.com/gpu 为准。DRA 的版本状态见 Kubernetes Dynamic Resource Allocation。
GPU 设备链路排查¶
| 现象 | 检查对象 |
|---|---|
worker 内 nvidia-smi 失败 |
裸机 Driver、Container Toolkit、nvkind 注入 |
worker 能看见 GPU,Node 没有 nvidia.com/gpu |
Device Plugin 注册、ListAndWatch、socket |
| Capacity 为 1,Allocatable 为 0 | 插件报告的设备不再健康,检查 ListAndWatch;现有 Pod 占用不会把该字段直接减成 0 |
Pod 一直 Pending 且 Insufficient nvidia.com/gpu |
Scheduler 资源账本与现有 GPU Pod |
Pod 已绑定,出现 Allocate failed |
Kubelet Device Manager 与 Device Plugin |
CreateContainerError 提到 CDI/runtime |
CDI spec、containerd handler、RuntimeClass |
| 容器内有设备,vLLM CUDA 初始化失败 | Driver/用户态兼容、vLLM 参数和应用日志 |
GPU 状态检查¶
核对 worker 的设备列表是否与选中的 A100 一致:
docker exec k8s-ai-infra-worker \
nvidia-smi --query-gpu=index,uuid,name,mig.mode.current \
--format=csv,noheader
检查 Device Plugin 和 Node 资源:
kubectl get daemonset single-gpu-device-plugin \
-n nvidia-device-plugin -o wide
kubectl logs -n nvidia-device-plugin \
daemonset/single-gpu-device-plugin --tail=100
kubectl get node k8s-ai-infra-worker -o json | jq '{
capacity: .status.capacity["nvidia.com/gpu"],
allocatable: .status.allocatable["nvidia.com/gpu"]
}'
PodSpec 记录请求数量、RuntimeClass 和目标节点:
pod_name="$(kubectl get pod -n default \
-l app=vllm-demo \
-o jsonpath='{.items[0].metadata.name}')"
kubectl get pod "$pod_name" -n default -o json | jq '{
node: .spec.nodeName,
runtimeClassName: .spec.runtimeClassName,
requests: .spec.containers[0].resources.requests,
limits: .spec.containers[0].resources.limits
}'
比较 worker、插件 socket、CDI spec 和业务容器的设备视图:
docker exec k8s-ai-infra-worker \
ls -l /var/lib/kubelet/device-plugins
docker exec k8s-ai-infra-worker \
sh -c 'find /var/run/cdi -maxdepth 1 -type f -print'
kubectl exec -n default "$pod_name" -- \
nvidia-smi --query-gpu=uuid,name,memory.total \
--format=csv,noheader
worker 与业务容器中的 UUID 用于确认两层容器看到同一张卡;Device Plugin socket 和 CDI spec 分别标记资源注册与运行时设备配置的交接位置。