第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,里面记录了这台节点可分配的资源:

yaml 复制代码
# 一个普通节点的 Allocatable 示例
status:
  allocatable:
    cpu: "32"
    memory: 128Gi
    storage: 200Gi
    pods: "110"
  capacity:
    cpu: "32"
    memory: 130Gi
    storage: 200Gi
    pods: "110"

调度器只认这三类核心资源:cpumemoryephemeral-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/gpuintel.com/sgxcambricon.com/mlu

扩展资源有两个关键限制:

  1. 整数计量:只能是整数(不能声明 0.5 个 GPU),调度器按整数扣减。
  2. 不能超卖 :默认情况下 Extended Resource 不可 overcommit,调度器严格按 requests 匹配 Allocatable。

注册扩展资源靠的是向 Node 对象打一个 patch:

bash 复制代码
# 给 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。它的核心职责有三:

  1. 盘点:检测本机有哪些设备,每张卡一个唯一 DeviceID。
  2. 上报 :通过 gRPC 把设备列表告诉 kubelet,kubelet 再上报到 API Server,变成 nvidia.com/gpu 的 Allocatable。
  3. 分配 :当 Pod 被调度到本节点后,kubelet 调用 Device Plugin 的 Allocate 接口,Device Plugin 返回该 Pod 应挂载的设备文件、驱动库、环境变量等,kubelet 把这些信息合入容器 spec。

#mermaid-svg-LdZjuz3FxjLC9dGm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LdZjuz3FxjLC9dGm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LdZjuz3FxjLC9dGm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LdZjuz3FxjLC9dGm .error-icon{fill:#552222;}#mermaid-svg-LdZjuz3FxjLC9dGm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LdZjuz3FxjLC9dGm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LdZjuz3FxjLC9dGm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LdZjuz3FxjLC9dGm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LdZjuz3FxjLC9dGm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LdZjuz3FxjLC9dGm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LdZjuz3FxjLC9dGm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LdZjuz3FxjLC9dGm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LdZjuz3FxjLC9dGm .marker.cross{stroke:#333333;}#mermaid-svg-LdZjuz3FxjLC9dGm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LdZjuz3FxjLC9dGm p{margin:0;}#mermaid-svg-LdZjuz3FxjLC9dGm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LdZjuz3FxjLC9dGm .cluster-label text{fill:#333;}#mermaid-svg-LdZjuz3FxjLC9dGm .cluster-label span{color:#333;}#mermaid-svg-LdZjuz3FxjLC9dGm .cluster-label span p{background-color:transparent;}#mermaid-svg-LdZjuz3FxjLC9dGm .label text,#mermaid-svg-LdZjuz3FxjLC9dGm span{fill:#333;color:#333;}#mermaid-svg-LdZjuz3FxjLC9dGm .node rect,#mermaid-svg-LdZjuz3FxjLC9dGm .node circle,#mermaid-svg-LdZjuz3FxjLC9dGm .node ellipse,#mermaid-svg-LdZjuz3FxjLC9dGm .node polygon,#mermaid-svg-LdZjuz3FxjLC9dGm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LdZjuz3FxjLC9dGm .rough-node .label text,#mermaid-svg-LdZjuz3FxjLC9dGm .node .label text,#mermaid-svg-LdZjuz3FxjLC9dGm .image-shape .label,#mermaid-svg-LdZjuz3FxjLC9dGm .icon-shape .label{text-anchor:middle;}#mermaid-svg-LdZjuz3FxjLC9dGm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LdZjuz3FxjLC9dGm .rough-node .label,#mermaid-svg-LdZjuz3FxjLC9dGm .node .label,#mermaid-svg-LdZjuz3FxjLC9dGm .image-shape .label,#mermaid-svg-LdZjuz3FxjLC9dGm .icon-shape .label{text-align:center;}#mermaid-svg-LdZjuz3FxjLC9dGm .node.clickable{cursor:pointer;}#mermaid-svg-LdZjuz3FxjLC9dGm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LdZjuz3FxjLC9dGm .arrowheadPath{fill:#333333;}#mermaid-svg-LdZjuz3FxjLC9dGm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LdZjuz3FxjLC9dGm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LdZjuz3FxjLC9dGm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LdZjuz3FxjLC9dGm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LdZjuz3FxjLC9dGm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LdZjuz3FxjLC9dGm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LdZjuz3FxjLC9dGm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LdZjuz3FxjLC9dGm .cluster text{fill:#333;}#mermaid-svg-LdZjuz3FxjLC9dGm .cluster span{color:#333;}#mermaid-svg-LdZjuz3FxjLC9dGm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LdZjuz3FxjLC9dGm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LdZjuz3FxjLC9dGm rect.text{fill:none;stroke-width:0;}#mermaid-svg-LdZjuz3FxjLC9dGm .icon-shape,#mermaid-svg-LdZjuz3FxjLC9dGm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LdZjuz3FxjLC9dGm .icon-shape p,#mermaid-svg-LdZjuz3FxjLC9dGm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LdZjuz3FxjLC9dGm .icon-shape .label rect,#mermaid-svg-LdZjuz3FxjLC9dGm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LdZjuz3FxjLC9dGm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LdZjuz3FxjLC9dGm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LdZjuz3FxjLC9dGm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GPU 节点
gRPC: ListAndWatch
gRPC: Allocate
读取
上报 capacity/allocatable
watch Node/Pod
绑定 Pod→Node
kubelet
NVIDIA Device Plugin

(DaemonSet)
GPU 0 /dev/nvidia0
GPU 1 /dev/nvidia1
GPU 2 /dev/nvidia2
GPU 3 /dev/nvidia3
NVIDIA Driver + NVML
API Server
kube-scheduler

注意一个常见误区:Device Plugin 不参与调度决策。它只负责「设备上报」和「设备分配」,调度器才是决定 Pod 落在哪台机器的角色。

2.2 Device Plugin 的 gRPC 接口

Device Plugin 与 kubelet 之间通过 Unix Socket /var/lib/kubelet/device-plugins/kubelet.sock 和反向 socket 通信。核心服务定义在 plugin.proto

protobuf 复制代码
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 (...) {}
}

整个生命周期是:

  1. Device Plugin 启动 → 扫描 /dev/nvidia*、调用 NVML 拿到 GPU 数量与 UUID。
  2. 通过 Register 告诉 kubelet:「我管理 nvidia.com/gpu 这类资源,socket 是 nvidia.sock」。
  3. 进入 ListAndWatch 长连接,持续推送形如 [{id: "GPU-uuid-0", health: "Healthy"}, ...] 的设备清单。
  4. kubelet 把清单转为 Node.Status.Capacity / Allocatable 上报 API Server。
  5. 当一个请求 nvidia.com/gpu: 2 的 Pod 被调度到本节点,kubelet 通过 Allocate 调用 Device Plugin,传入要分配的两个 DeviceID。
  6. Device Plugin 返回 ContainerResponse:列出要挂载的设备文件路径(/dev/nvidia0/dev/nvidia1)、必要的库(/usr/lib/x86_64-linux-gnu/libnvidia-ml.so)、环境变量(NVIDIA_VISIBLE_DEVICES)。
  7. kubelet 把这些挂载点注入容器,Pod 启动后容器内 nvidia-smi 就能看到对应的卡。

2.3 分配流程时序

物理 GPU Device Plugin kubelet kube-scheduler API Server 用户/kubectl 物理 GPU Device Plugin kubelet kube-scheduler API Server 用户/kubectl #mermaid-svg-B5g3VJmozLotqdUL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-B5g3VJmozLotqdUL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-B5g3VJmozLotqdUL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-B5g3VJmozLotqdUL .error-icon{fill:#552222;}#mermaid-svg-B5g3VJmozLotqdUL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-B5g3VJmozLotqdUL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-B5g3VJmozLotqdUL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-B5g3VJmozLotqdUL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-B5g3VJmozLotqdUL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-B5g3VJmozLotqdUL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-B5g3VJmozLotqdUL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-B5g3VJmozLotqdUL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-B5g3VJmozLotqdUL .marker.cross{stroke:#333333;}#mermaid-svg-B5g3VJmozLotqdUL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-B5g3VJmozLotqdUL p{margin:0;}#mermaid-svg-B5g3VJmozLotqdUL .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-B5g3VJmozLotqdUL text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-B5g3VJmozLotqdUL .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-B5g3VJmozLotqdUL .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-B5g3VJmozLotqdUL .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-B5g3VJmozLotqdUL .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-B5g3VJmozLotqdUL #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-B5g3VJmozLotqdUL .sequenceNumber{fill:white;}#mermaid-svg-B5g3VJmozLotqdUL #sequencenumber{fill:#333;}#mermaid-svg-B5g3VJmozLotqdUL #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-B5g3VJmozLotqdUL .messageText{fill:#333;stroke:none;}#mermaid-svg-B5g3VJmozLotqdUL .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-B5g3VJmozLotqdUL .labelText,#mermaid-svg-B5g3VJmozLotqdUL .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-B5g3VJmozLotqdUL .loopText,#mermaid-svg-B5g3VJmozLotqdUL .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-B5g3VJmozLotqdUL .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-B5g3VJmozLotqdUL .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-B5g3VJmozLotqdUL .noteText,#mermaid-svg-B5g3VJmozLotqdUL .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-B5g3VJmozLotqdUL .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-B5g3VJmozLotqdUL .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-B5g3VJmozLotqdUL .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-B5g3VJmozLotqdUL .actorPopupMenu{position:absolute;}#mermaid-svg-B5g3VJmozLotqdUL .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-B5g3VJmozLotqdUL .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-B5g3VJmozLotqdUL .actor-man circle,#mermaid-svg-B5g3VJmozLotqdUL line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-B5g3VJmozLotqdUL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 容器内 nvidia-smi 只看到 2 张卡 提交 Pod(requests: nvidia.com/gpu: 2) watch 到新 Pod 查询各节点 Allocatable 过滤:选 nvidia.com/gpu≥2 的节点 绑定 Pod → node-gpu-03 Pod 到达本节点 Allocate(deviceIDs=GPU-uuid-5, GPU-uuid-6) 查询设备状态 健康 ContainerResponse{devices:/dev/nvidia1, /dev/nvidia3, envs:{...}} 注入挂载点与 Env 到容器 spec 启动容器,挂载指定设备

这里的关键设计是 Device Plugin 决定「挂哪几张卡」。调度器只保证数量够,但具体是 node-gpu-03 的 GPU-5 还是 GPU-6,由 Device Plugin 的 Allocate 决定(默认按上报顺序)。这也是后面 GPU 拓扑感知调度要解决的问题------默认分配可能把两张卡放到不同的 NVLink 域,影响通信性能。

2.4 自定义 Device Plugin 的最小骨架

理解了原理,我们写一个最小的 Device Plugin 骨架,管理一种叫 example.com/fakegpu 的虚拟资源。这是一个 Go 程序,演示框架的核心交互:

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:

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:

bash 复制代码
$ 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:

yaml 复制代码
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

几点必须严格遵守的规则:

  1. 必须同时出现在 requests 和 limits:K8s 规定 Extended Resource 的 requests 必须等于 limits,否则调度失败。
  2. 不能写小数nvidia.com/gpu: 0.5 无效。要做 GPU 共享见下一篇。
  3. 被调度后无法迁移:GPU 是本地设备,一旦绑定到某节点,Pod 重启也只会在同一节点重建。
  4. 资源类型只能是 limits 控制:调度器只看 limits(Extended Resource 视为 Guaranteed 等级)。

容器启动后执行 nvidia-smi

text 复制代码
+-----------------------------------------------------------------------------+
| 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 节点是这样的:

  1. 安装操作系统。
  2. 下载并安装对应内核版本的 NVIDIA 驱动(apt install nvidia-driver-535,匹配内核 header,重启)。
  3. 安装 NVIDIA Container Toolkit(nvidia-runtime / nvidia-ctk)。
  4. 配置 containerd / docker 的 runtime class,加 nvidia runtime。
  5. 给节点打 nvidia.com/gpu.present=true 标签。
  6. 部署 NVIDIA Device Plugin DaemonSet。
  7. 部署 DCGM Exporter 用于监控。
  8. 配置 MIG(如果用 A100/H100)。

任何一步版本不匹配都会导致 GPU 不可用。集群规模到几十台节点时,这是噩梦。GPU Operator 把这一切收敛到一个 Operator 里。

4.2 GPU Operator 架构

#mermaid-svg-tnAGWqJSjvAqg2j9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tnAGWqJSjvAqg2j9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tnAGWqJSjvAqg2j9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tnAGWqJSjvAqg2j9 .error-icon{fill:#552222;}#mermaid-svg-tnAGWqJSjvAqg2j9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tnAGWqJSjvAqg2j9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tnAGWqJSjvAqg2j9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tnAGWqJSjvAqg2j9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tnAGWqJSjvAqg2j9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tnAGWqJSjvAqg2j9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tnAGWqJSjvAqg2j9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tnAGWqJSjvAqg2j9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tnAGWqJSjvAqg2j9 .marker.cross{stroke:#333333;}#mermaid-svg-tnAGWqJSjvAqg2j9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tnAGWqJSjvAqg2j9 p{margin:0;}#mermaid-svg-tnAGWqJSjvAqg2j9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tnAGWqJSjvAqg2j9 .cluster-label text{fill:#333;}#mermaid-svg-tnAGWqJSjvAqg2j9 .cluster-label span{color:#333;}#mermaid-svg-tnAGWqJSjvAqg2j9 .cluster-label span p{background-color:transparent;}#mermaid-svg-tnAGWqJSjvAqg2j9 .label text,#mermaid-svg-tnAGWqJSjvAqg2j9 span{fill:#333;color:#333;}#mermaid-svg-tnAGWqJSjvAqg2j9 .node rect,#mermaid-svg-tnAGWqJSjvAqg2j9 .node circle,#mermaid-svg-tnAGWqJSjvAqg2j9 .node ellipse,#mermaid-svg-tnAGWqJSjvAqg2j9 .node polygon,#mermaid-svg-tnAGWqJSjvAqg2j9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tnAGWqJSjvAqg2j9 .rough-node .label text,#mermaid-svg-tnAGWqJSjvAqg2j9 .node .label text,#mermaid-svg-tnAGWqJSjvAqg2j9 .image-shape .label,#mermaid-svg-tnAGWqJSjvAqg2j9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-tnAGWqJSjvAqg2j9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tnAGWqJSjvAqg2j9 .rough-node .label,#mermaid-svg-tnAGWqJSjvAqg2j9 .node .label,#mermaid-svg-tnAGWqJSjvAqg2j9 .image-shape .label,#mermaid-svg-tnAGWqJSjvAqg2j9 .icon-shape .label{text-align:center;}#mermaid-svg-tnAGWqJSjvAqg2j9 .node.clickable{cursor:pointer;}#mermaid-svg-tnAGWqJSjvAqg2j9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tnAGWqJSjvAqg2j9 .arrowheadPath{fill:#333333;}#mermaid-svg-tnAGWqJSjvAqg2j9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tnAGWqJSjvAqg2j9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tnAGWqJSjvAqg2j9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tnAGWqJSjvAqg2j9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tnAGWqJSjvAqg2j9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tnAGWqJSjvAqg2j9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tnAGWqJSjvAqg2j9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tnAGWqJSjvAqg2j9 .cluster text{fill:#333;}#mermaid-svg-tnAGWqJSjvAqg2j9 .cluster span{color:#333;}#mermaid-svg-tnAGWqJSjvAqg2j9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tnAGWqJSjvAqg2j9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tnAGWqJSjvAqg2j9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-tnAGWqJSjvAqg2j9 .icon-shape,#mermaid-svg-tnAGWqJSjvAqg2j9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tnAGWqJSjvAqg2j9 .icon-shape p,#mermaid-svg-tnAGWqJSjvAqg2j9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tnAGWqJSjvAqg2j9 .icon-shape .label rect,#mermaid-svg-tnAGWqJSjvAqg2j9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tnAGWqJSjvAqg2j9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tnAGWqJSjvAqg2j9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tnAGWqJSjvAqg2j9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} DaemonSet 组件(每节点一份)
reconcile
reconcile
reconcile
reconcile
reconcile
reconcile
注入内核模块
生成
注册 nvidia.com/gpu
metrics
GPU Operator

(ClusterRole + CRD: ClusterPolicy)
nvidia-driver

容器化驱动(容器内编译 .ko)
nvidia-container-toolkit

配置 containerd runtime
nvidia-device-plugin

设备上报与分配
dcgm-exporter

监控指标采集
gpu-operator-validator

启动期健康自检
nvidia-mig-manager

MIG 配置动态切换
VFIO-PCI / NVIDIA Driver 容器
CDI 生成器
Kubernetes API Server

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 大致长这样:

yaml 复制代码
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 节点打上详细的标签:

text 复制代码
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 节点」:

yaml 复制代码
nodeSelector:
  nvidia.com/gpu.product: A100-SXM4-40GB

或者「至少 80GB 显存的卡」用 NodeAffinity

yaml 复制代码
affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
            - key: nvidia.com/gpu.memory
              operator: Gt
              values: ["80000"]

五、节点选择与拓扑感知

5.1 GPU 节点的标签与污点策略

生产集群通常给 GPU 节点打污点(taint),防止非 GPU Pod 误占:

bash 复制代码
# 给所有 GPU 节点打污点
kubectl taint node node-gpu-01 nvidia.com/gpu=true:NoSchedule

GPU Pod 必须显式 toleration 才能调度上去。这一步防的是:一个普通的 Java 微服务 Pod 不小心 requests 超大内存,被调度到 GPU 节点,把宝贵的 GPU 资源挤掉。结合 nodeSelectortolerations,可以把 GPU 节点变成「专用资源池」。

到这里调度器只知道「数量够」,不知道「卡之间是否在同一个 NVLink 域」。看一个 A100 8 卡节点的拓扑:

text 复制代码
$ 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 社区有几种做法:

  1. NUMA-aware scheduling :通过 Topology Manager 在 kubelet 侧约束。
  2. Device Plugin 的 GetPreferredAllocation:让 Device Plugin 反向推荐「优先分配哪些设备」。
  3. 第三方调度器扩展:Volcano / Kueue / Run:AI 等支持 GPU 拓扑优选。
  4. 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 nodenvidia.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

一个万能排查组合拳:

bash 复制代码
# 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 节点纳入集群的标准化流水线:
#mermaid-svg-IGemNE0WGlBYqNGw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IGemNE0WGlBYqNGw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IGemNE0WGlBYqNGw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IGemNE0WGlBYqNGw .error-icon{fill:#552222;}#mermaid-svg-IGemNE0WGlBYqNGw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IGemNE0WGlBYqNGw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IGemNE0WGlBYqNGw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IGemNE0WGlBYqNGw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IGemNE0WGlBYqNGw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IGemNE0WGlBYqNGw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IGemNE0WGlBYqNGw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IGemNE0WGlBYqNGw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IGemNE0WGlBYqNGw .marker.cross{stroke:#333333;}#mermaid-svg-IGemNE0WGlBYqNGw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IGemNE0WGlBYqNGw p{margin:0;}#mermaid-svg-IGemNE0WGlBYqNGw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IGemNE0WGlBYqNGw .cluster-label text{fill:#333;}#mermaid-svg-IGemNE0WGlBYqNGw .cluster-label span{color:#333;}#mermaid-svg-IGemNE0WGlBYqNGw .cluster-label span p{background-color:transparent;}#mermaid-svg-IGemNE0WGlBYqNGw .label text,#mermaid-svg-IGemNE0WGlBYqNGw span{fill:#333;color:#333;}#mermaid-svg-IGemNE0WGlBYqNGw .node rect,#mermaid-svg-IGemNE0WGlBYqNGw .node circle,#mermaid-svg-IGemNE0WGlBYqNGw .node ellipse,#mermaid-svg-IGemNE0WGlBYqNGw .node polygon,#mermaid-svg-IGemNE0WGlBYqNGw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IGemNE0WGlBYqNGw .rough-node .label text,#mermaid-svg-IGemNE0WGlBYqNGw .node .label text,#mermaid-svg-IGemNE0WGlBYqNGw .image-shape .label,#mermaid-svg-IGemNE0WGlBYqNGw .icon-shape .label{text-anchor:middle;}#mermaid-svg-IGemNE0WGlBYqNGw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IGemNE0WGlBYqNGw .rough-node .label,#mermaid-svg-IGemNE0WGlBYqNGw .node .label,#mermaid-svg-IGemNE0WGlBYqNGw .image-shape .label,#mermaid-svg-IGemNE0WGlBYqNGw .icon-shape .label{text-align:center;}#mermaid-svg-IGemNE0WGlBYqNGw .node.clickable{cursor:pointer;}#mermaid-svg-IGemNE0WGlBYqNGw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IGemNE0WGlBYqNGw .arrowheadPath{fill:#333333;}#mermaid-svg-IGemNE0WGlBYqNGw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IGemNE0WGlBYqNGw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IGemNE0WGlBYqNGw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IGemNE0WGlBYqNGw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IGemNE0WGlBYqNGw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IGemNE0WGlBYqNGw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IGemNE0WGlBYqNGw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IGemNE0WGlBYqNGw .cluster text{fill:#333;}#mermaid-svg-IGemNE0WGlBYqNGw .cluster span{color:#333;}#mermaid-svg-IGemNE0WGlBYqNGw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IGemNE0WGlBYqNGw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IGemNE0WGlBYqNGw rect.text{fill:none;stroke-width:0;}#mermaid-svg-IGemNE0WGlBYqNGw .icon-shape,#mermaid-svg-IGemNE0WGlBYqNGw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IGemNE0WGlBYqNGw .icon-shape p,#mermaid-svg-IGemNE0WGlBYqNGw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IGemNE0WGlBYqNGw .icon-shape .label rect,#mermaid-svg-IGemNE0WGlBYqNGw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IGemNE0WGlBYqNGw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IGemNE0WGlBYqNGw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IGemNE0WGlBYqNGw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 裸机上架
OS 安装

(标准 Ubuntu 22.04)
kubelet + containerd
加入 K8s 集群
打 label

nvidia.com/gpu.present=true
打 taint

nvidia.com/gpu=true:NoSchedule
GPU Operator 自动接管
驱动容器化加载
Toolkit 配置 runtime
Device Plugin 上报资源
DCGM Exporter 上报指标
GFD 打产品标签
节点 Ready, 可调度

从步骤 G 开始全部由 GPU Operator 自动完成。运维只负责 A--F,剩下交给 Operator。一台新 GPU 节点上线时间可以从原来的 2 小时压缩到 15 分钟。

6.2 资源配额与多租户

在多租户场景下,通过 ResourceQuota 限制每个 Namespace 可使用的 GPU 总量:

yaml 复制代码
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 推荐的灰度策略:

  1. 先升级一个节点(按 label 选 canary 节点)。
  2. 该节点驱逐所有业务 Pod(kubectl drain)。
  3. Operator 触发 nvidia-driver DaemonSet 滚动更新。
  4. 等待新驱动加载完成,跑 validator 自检。
  5. 解除 cordon,灰度观察 24 小时。
  6. 全量滚动升级。

驱动容器化的好处之一:版本回滚就是切回旧 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 决定环境。三者分工明确,缺一不可。


下篇预告

第 29 篇:GPU共享与MIG分区-算力的精细化切分

GPU 调度解决了「整卡分配」的问题,但实际业务里大量场景只需要 GPU 的一小部分算力------比如一个小模型的推理服务,整张 A100 给它纯属浪费。下一篇我们讨论 GPU 共享与 MIG 分区,看 K8s 上如何把一张 GPU 切成多份给不同 Pod 共用,涉及 Time-Slicing、MPS、HAMi 显存隔离和 MIG 硬件分区四种方案及其选型。


如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。

相关推荐
IvanCodes1 小时前
RAG 实战教程(二):向量相似度、向量数据库与 Chroma 实战
人工智能·agent
阿维的博客日记1 小时前
什么是unigram语言模型
人工智能·语言模型·自然语言处理
碧海银沙音频科技研究院2 小时前
ONNX 的全称Open Neural Network Exchange(开放神经网络交换格式)
人工智能·音视频·语音识别
OpsEye2 小时前
直连大模型API、开源网关、商用AI管理平台,该如何抉择
人工智能·开源
杀生丸学AI2 小时前
【世界模型】Lyra 2.0:可探索的生成式3D世界(NVIDIA)
人工智能·深度学习·3d·数据挖掘·transformer·世界模型·高斯泼溅
xushichang123_2 小时前
工业企业推动自动化产线向自主化产线演进,云上物理 AI 与工业 Agent 方案如何选型?:优先采用 “云上智能底座+数字孪生+工业本体” 的分层架构
人工智能
hkNaruto2 小时前
【AI】规范驱动开发(SDD)团队实践指南 v2
人工智能·驱动开发
bittersuite2 小时前
BERT,fine-tuning
人工智能·深度学习·bert
武汉星际互动2 小时前
政务大厅引入AI数字人,需要关注哪些核心能力?
人工智能·政务