跳转至

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 限制插件容器的设备视图:

examples/chapter-01/10-gpu-device/single-gpu-device-plugin.yaml
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:

examples/chapter-01/12-vllm/02-deployment.yaml
resources:
  limits:
    nvidia.com/gpu: "1"

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 分别标记资源注册与运行时设备配置的交接位置。

相关资料