第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"

调度器只认这三类核心资源:cpu、memory、ephemeral-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/gpu、intel.com/sgx、cambricon.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 资源挤掉。结合 nodeSelector 和 tolerations,可以把 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 node 看 nvidia.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 硬件分区四种方案及其选型。


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

相关推荐
2501_938140072 小时前
做内容很省心,2026好用AI笔记,支持抖音小红书视频无水印保存
人工智能
sell-cloud4 小时前
Prompt Engineering要凉了?Fable 5.5能“猜“意图,但银行正在封杀AI Agent——企业AI的真正护城河是什么
人工智能·prompt
Navicat中国6 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI6 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区6 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
XCDoris8 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy8 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V9 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI9 小时前
AI英语教培智能体的开发
人工智能
物联网软硬件开发-轨物科技9 小时前
【技术白皮书】光伏清检一体化机器人系统解决方案技术白皮书
大数据·运维·网络·人工智能·机器人