从云原生走向 AI 原生:一套面向未来的架构方法论 → 阅读《AI 原生基础设施》

设备插件与设备抽象层

已发行

HAMi Device Plugin 是 GPU 资源控制平面的执行层,在每个 GPU 节点上管理设备的发现、注册、分配与健康检查,设备抽象层则为多厂商异构设备提供统一接口。

设备插件概述

Kubernetes Device Plugin API

Kubernetes Device Plugin 是一种标准的扩展机制,允许第三方硬件供应商将自定义资源(GPU、FPGA、高速网卡等)暴露给 Kubernetes 调度器。

Device Plugin 通过 gRPC 与 Kubelet 通信,遵循以下接口:

// Kubernetes Device Plugin gRPC 接口(简化)
service DevicePlugin {
    // 获取设备列表和状态(持续流式)
    rpc ListAndWatch(Empty) returns (stream ListAndWatchResponse);
    // 为容器分配设备
    rpc Allocate(AllocateRequest) returns (AllocateResponse);
    // 获取设备分配选项
    rpc GetDeviceAllocationOptions(DeviceAllocationOptionsRequest) returns (DeviceAllocationOptionsResponse);
}

HAMi 自研设备插件

HAMi 实现了自己的 Device Plugin,与 NVIDIA 官方 k8s-device-plugin 不兼容。二者不能同时运行在同一个节点上。

HAMi 自研设备插件的原因:

  • 支持资源切分:官方插件只能整卡分配,HAMi 需要支持显存和算力的精细化切分
  • 支持多厂商:官方插件仅支持 NVIDIA,HAMi 需要统一管理 NVIDIA、昇腾、寒武纪等设备
  • 支持共享隔离:HAMi 需要注入底层库实现多 Pod 共享同一 GPU 时的资源隔离

DaemonSet 部署

Device Plugin 以 DaemonSet 形式部署,每个 GPU 节点运行一个实例:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: hami-device-plugin
  namespace: hami-system
spec:
  selector:
    matchLabels:
      app: hami-device-plugin
  template:
    spec:
      hostNetwork: true
      hostPID: true
      containers:
      - name: device-plugin
        image: projecthami/hami:v2.5.0
        command: ["hami-device-plugin"]
        volumeMounts:
        - name: device-plugin
          mountPath: /var/lib/kubelet/device-plugins
        - name: dev
          mountPath: /dev
        securityContext:
          privileged: true
      volumes:
      - name: device-plugin
        hostPath:
          path: /var/lib/kubelet/device-plugins
      - name: dev
        hostPath:
          path: /dev
      nodeSelector:
        gpu: "true"

设备插件生命周期

图 1: Device Plugin 生命周期
图 1: Device Plugin 生命周期

启动流程

Device Plugin 的启动过程分为三个阶段:

1.加载配置

func (p *Plugin) LoadConfig() error {
    // 从 ConfigMap 加载设备配置
    config := &DeviceConfig{}
    config.NVIDIA.SplitCount = 10        // 每张 GPU 切分为 10 份
    config.NVIDIA.MemoryScaling = 1.0    // 显存缩放因子
    config.NVIDIA.MigStrategy = "single" // MIG 策略
    return nil
}

2.NVML 初始化

func (p *Plugin) InitNVML() error {
    if err := nvml.Init(); err != nil {
        return fmt.Errorf("NVML 初始化失败: %v", err)
    }
    count, err := nvml.GetDeviceCount()
    if err != nil {
        return fmt.Errorf("获取 GPU 数量失败: %v", err)
    }
    klog.Infof("检测到 %d 张 NVIDIA GPU", count)
    return nil
}

3.启动 gRPC 服务器

func (p *Plugin) Start() error {
    // 在 Kubelet device-plugin 目录下创建 Unix Socket
    socketPath := filepath.Join(devicePluginPath, "hami-gpu.sock")
    listener, err := net.Listen("unix", socketPath)

    server := grpc.NewServer()
    pluginapi.RegisterDevicePluginServer(server, p)

    go server.Serve(listener)

    // 注册到 Kubelet
    return p.RegisterWithKubelet()
}

设备发现与注册

使用 NVML 查询物理 GPU

func (p *NvidiaPlugin) DiscoverPhysicalGPUs() ([]PhysicalGPU, error) {
    count, err := nvml.GetDeviceCount()
    if err != nil {
        return nil, err
    }

    gpus := make([]PhysicalGPU, 0, count)
    for i := 0; i < count; i++ {
        handle, err := nvml.NewDeviceByIndex(uint(i))
        if err != nil {
            klog.Warningf("跳过 GPU %d: %v", i, err)
            continue
        }

        uuid, _ := handle.GetUUID()
        name, _ := handle.GetName()
        memory, _ := handle.GetMemoryInfo()
        pciBusID, _ := handle.GetPCIBusID()
        numa, _ := handle.GetNUMANode()

        gpu := PhysicalGPU{
            Index:    i,
            UUID:     uuid,
            Model:    name,
            Memory:   memory.Total,
            PCIBusID: pciBusID,
            NUMANode: numa,
        }
        gpus = append(gpus, gpu)

        klog.V(4).Infof("发现 GPU: index=%d uuid=%s model=%s memory=%dMB numa=%d",
            i, uuid, name, memory.Total/1024/1024, numa)
    }
    return gpus, nil
}

根据配置切分设备资源

HAMi 不预创建固定的切分设备,而是通过资源计数的方式注册可分配能力:

func (p *NvidiaPlugin) GenerateDeviceSpecs(gpus []PhysicalGPU, config *DeviceConfig) []*pluginapi.Device {
    var devices []*pluginapi.Device

    for _, gpu := range gpus {
        // 注册每个物理 GPU 的资源能力
        // Kubelet 通过资源名 nvidia.com/gpu 管理设备计数
        device := &pluginapi.Device{
            ID:     gpu.UUID,
            Health: pluginapi.Healthy,
        }
        devices = append(devices, device)
    }

    // 通过节点注解上报详细的切分信息
    // 调度器读取注解获取每张卡的可用显存和算力
    return devices
}

注册到 Kubelet

func (p *Plugin) RegisterWithKubelet() error {
    conn, err := grpc.Dial(
        filepath.Join(devicePluginPath, "kubelet.sock"),
        grpc.WithInsecure(),
    )
    if err != nil {
        return err
    }
    defer conn.Close()

    client := pluginapi.NewRegistrationClient(conn)
    _, err = client.Register(context.Background(), &pluginapi.RegisterRequest{
        Version:      pluginapi.Version,
        Endpoint:     "hami-gpu.sock",
        ResourceName: "nvidia.com/gpu",
    })
    return err
}

资源分配(Allocate)

读取 Pod 分配注解

Device Plugin 在收到 Kubelet 的 Allocate 请求后,首先从 Pod 注解中读取调度器的分配决策:

func (p *Plugin) Allocate(ctx context.Context, req *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
    resp := &pluginapi.AllocateResponse{}

    for _, containerReq := range req.ContainerRequests {
        containerResp := &pluginapi.ContainerAllocateResponse{}

        // 从环境变量或 Pod 注解中获取分配信息
        allocation := p.getAllocationFromAnnotation()

        for _, device := range allocation.Devices {
            // 准备设备路径
            containerResp.Devices = append(containerResp.Devices,
                &pluginapi.DeviceSpec{
                    HostPath:      device.HostPath,
                    ContainerPath: device.ContainerPath,
                    Permissions:   "mrw",
                },
            )

            // 注入环境变量
            containerResp.Envs["CUDA_VISIBLE_DEVICES"] = device.UUID
            containerResp.Envs["NVIDIA_VISIBLE_DEVICES"] = device.UUID

            // 配置显存限制
            if device.MemoryLimit > 0 {
                containerResp.Envs["CUDA_DEVICE_MEMORY_LIMIT"] =
                    fmt.Sprintf("%d", device.MemoryLimit)
            }

            // 配置算力限制
            if device.CoreLimit > 0 {
                containerResp.Envs["CUDA_DEVICE_CORE_LIMIT"] =
                    fmt.Sprintf("%d", device.CoreLimit)
            }
        }

        // 挂载 CUDA 运行时库和注入库
        containerResp.Mounts = append(containerResp.Mounts,
            &pluginapi.Mount{
                HostPath:      "/usr/local/cuda",
                ContainerPath: "/usr/local/cuda",
                ReadOnly:      true,
            },
            &pluginapi.Mount{
                HostPath:      "/var/lib/hami/vgpu/lib",
                ContainerPath: "/usr/local/vgpu/lib",
                ReadOnly:      true,
            },
        )

        resp.ContainerResponses = append(resp.ContainerResponses, containerResp)
    }

    return resp, nil
}

准备容器运行时环境

Allocate 返回的响应包含三类配置:

设备路径注入:

// /dev/nvidia* 设备节点
Devices: []*pluginapi.DeviceSpec{
    {HostPath: "/dev/nvidia0", ContainerPath: "/dev/nvidia0", Permissions: "mrw"},
    {HostPath: "/dev/nvidiactl", ContainerPath: "/dev/nvidiactl", Permissions: "mrw"},
    {HostPath: "/dev/nvidia-uvm", ContainerPath: "/dev/nvidia-uvm", Permissions: "mrw"},
}

环境变量注入:

Envs: map[string]string{
    "CUDA_VISIBLE_DEVICES":         "GPU-abc123-def456",
    "NVIDIA_VISIBLE_DEVICES":       "GPU-abc123-def456",
    "CUDA_DEVICE_MEMORY_LIMIT":     "4000",
    "CUDA_DEVICE_CORE_LIMIT":       "30",
}

挂载点注入:

Mounts: []*pluginapi.Mount{
    // CUDA 运行时
    {HostPath: "/usr/local/cuda/lib64", ContainerPath: "/usr/local/cuda/lib64", ReadOnly: true},
    // HAMi 注入库(实现资源隔离)
    {HostPath: "/var/lib/hami/vgpu/lib", ContainerPath: "/usr/local/vgpu/lib", ReadOnly: true},
    // GPU 驱动库
    {HostPath: "/usr/lib/x86_64-linux-gnu/libnvidia-ml.so", ContainerPath: "/usr/lib/x86_64-linux-gnu/libnvidia-ml.so", ReadOnly: true},
}

CDI 支持

Container Device Interface 规范

CDI(Container Device Interface)是容器运行时的设备注入标准,由容器运行时(containerd、CRI-O)直接处理设备注入,无需依赖 NVIDIA container runtime。

HAMi 支持 CDI 模式作为传统 Device Plugin 注入方式的替代方案。

CDI 设备生成与管理

// CDI 设备生成伪代码
func (p *Plugin) GenerateCDIDevices() ([]CDIDevice, error) {
    var cdiDevices []CDIDevice

    for _, gpu := range p.physicalGPUs {
        // 生成 CDI 设备描述符
        cdi := CDIDevice{
            Name:  fmt.Sprintf("nvidia.com/gpu=%s", gpu.UUID),
            Type:  "nvidia",
            DeviceSpecs: []DeviceSpec{
                {HostPath: fmt.Sprintf("/dev/nvidia%d", gpu.Index), Permissions: "mrw"},
            },
            Mounts: []MountSpec{
                {HostPath: "/usr/local/cuda/lib64", ContainerPath: "/usr/local/cuda/lib64"},
                {HostPath: "/var/lib/hami/vgpu/lib", ContainerPath: "/usr/local/vgpu/lib"},
            },
            Env: map[string]string{
                "CUDA_VISIBLE_DEVICES": gpu.UUID,
            },
        }
        cdiDevices = append(cdiDevices, cdi)
    }

    // 写入 CDI 规范文件
    return cdiDevices, writeCDISpec(cdiDevices)
}

CDI 模式的优势:

  • 标准化遵循容器行业标准,不依赖特定运行时
  • 简洁设备描述与容器运行时解耦
  • 兼容性支持 containerd 1.7+ 和 CRI-O 1.25+

设备抽象层

HAMi 的设备抽象层为多厂商异构设备提供统一的编程接口,是支持多种 GPU/NPU 的核心设计。

经定义设备接口 Devices

// Devices 是所有设备后端必须实现的统一接口
type Devices interface {
    // 健康检查
    GetNodeDevices(node *v1.Node) ([]*DeviceInfo, error)

    // 节点清理(清理已终止 Pod 的设备占用)
    NodeClean(nodeName string) (*NodeUsage, error)

    // 获取资源名称
    ResourceName() string

    // 设备发现
    Discovery() ([]*DeviceInfo, error)

    // 获取节点级锁
    Lock(nodeName string) error

    // 释放节点级锁
    Unlock(nodeName string) error

    // 写入分配注解
    PatchPodAnnotations(pod *v1.Pod, annotations map[string]string) error

    // 评分
    ScoreNode(node *NodeUsage, policy string) int64

    // 检查资源是否满足需求
    Fit(devices []*DeviceInfo, request *GPURequest) bool
}

核心方法说明

方法调用方功能
GetNodeDevicesScheduler获取节点上的设备列表和状态
NodeCleanScheduler清理已终止 Pod 占用的设备资源
ResourceNameScheduler/Plugin返回注册的资源名(如 nvidia.com/gpu
DiscoveryDevice Plugin发现物理设备并生成设备列表
Lock/UnlockScheduler获取/释放节点级互斥锁
PatchPodAnnotationsScheduler将分配决策写入 Pod 注解
ScoreNodeScheduler根据策略对节点评分
FitScheduler检查设备是否满足 Pod 资源请求

DeviceInfo 数据结构

// DeviceInfo 描述一个物理设备的信息
type DeviceInfo struct {
    ID       string            // 设备唯一标识(UUID)
    Type     string            // 设备类型(NVIDIA/Ascend/Cambricon/Hygon)
    Model    string            // 设备型号(A100/910B/...)
    Memory   uint64            // 总显存(字节)
    Cores    uint32            // 总算力核心数
    NUMANode int               // NUMA 节点编号
    Health   bool              // 健康状态

    // 拓扑信息
    PCIBusID string            // PCI 总线地址
    PCIE     PCITopology       // PCIe 拓扑信息

    // 资源使用
    UsedMemory uint64          // 已分配显存
    UsedCores  uint32          // 已分配算力

    // 运行时信息
    Index    int               // 设备索引
    HostPath string            // 设备路径
}

DeviceUsage 数据结构

// DeviceUsage 描述设备级别的资源使用情况
type DeviceUsage struct {
    DeviceID   string    // 设备 UUID
    DeviceType string    // 设备类型
    Memory     uint64    // 总显存
    UsedMemory uint64    // 已用显存
    Cores      uint32    // 总算力
    UsedCores  uint32    // 已用算力
    NUMANode   int       // NUMA 节点
    Healthy    bool      // 健康状态
}

// NodeUsage 描述节点级别的资源使用汇总
type NodeUsage struct {
    NodeName string
    Devices  []DeviceUsage
    // 节点级别的聚合指标
    TotalMemory uint64
    UsedMemory  uint64
    TotalCores  uint32
    UsedCores   uint32
}

类图

图 2: 设备抽象层类图
图 2: 设备抽象层类图

多厂商适配

NVIDIA GPU 适配

NVIDIA 是 HAMi 支持最完善的设备后端,使用 NVML(NVIDIA Management Library)进行设备管理:

type NvidiaDevices struct {
    config *NvidiaConfig
}

func (n *NvidiaDevices) Discovery() ([]*DeviceInfo, error) {
    count, _ := nvml.GetDeviceCount()
    devices := make([]*DeviceInfo, 0, count)

    for i := 0; i < count; i++ {
        handle, _ := nvml.NewDeviceByIndex(uint(i))
        uuid, _ := handle.GetUUID()
        name, _ := handle.GetName()
        mem, _ := handle.GetMemoryInfo()
        numa, _ := handle.GetNUMANode()

        devices = append(devices, &DeviceInfo{
            ID:       uuid,
            Type:     "NVIDIA",
            Model:    name,
            Memory:   mem.Total,
            NUMANode: numa,
            Health:   true,
        })
    }
    return devices, nil
}

func (n *NvidiaDevices) ResourceName() string {
    return "nvidia.com/gpu"
}

华为昇腾 NPU 适配

type AscendDevices struct {
    config *AscendConfig
}

func (a *AscendDevices) Discovery() ([]*DeviceInfo, error) {
    // 使用华为 Ascend CL SDK 查询 NPU 设备
    count := acl.GetDeviceCount()
    devices := make([]*DeviceInfo, 0, count)

    for i := 0; i < count; i++ {
        info, _ := acl.GetDeviceInfo(i)
        devices = append(devices, &DeviceInfo{
            ID:       info.UUID,
            Type:     "Ascend",
            Model:    info.Model,    // e.g., "Ascend 910B"
            Memory:   info.Memory,
            NUMANode: info.NUMA,
            Health:   info.Healthy,
        })
    }
    return devices, nil
}

func (a *AscendDevices) ResourceName() string {
    return "hami.io/ascend"
}

新增设备后端只需实现统一接口

为 HAMi 添加新的设备后端(如 AMD GPU、Intel GPU),只需实现 Devices 接口:

// 新增设备后端的步骤:
// 1. 实现 Devices 接口
type NewVendorDevices struct {
    // 厂商特定字段
}

func (d *NewVendorDevices) Discovery() ([]*DeviceInfo, error) {
    // 使用厂商 SDK 发现设备
}

func (d *NewVendorDevices) ResourceName() string {
    return "hami.io/newvendor"
}

// 2. 在设备注册表中注册
func init() {
    RegisterDevice("newvendor", &NewVendorDevices{})
}

// 3. 通过 Helm 配置启用
// devicePlugin:
//   newvendor:
//     enabled: true

这种设计使得 HAMi 的设备支持范围可以持续扩展,而无需修改调度器或 Webhook 的核心逻辑。

设备插件配置

以下是通过 Helm values.yaml 配置设备插件的完整示例:

# values.yaml - 设备插件配置
devicePlugin:
  enabled: true
  nameOverride: "hami-device-plugin"

  # 镜像配置
  image:
    repository: projecthami/hami
    tag: v2.5.0
    pullPolicy: IfNotPresent

  # NVIDIA GPU 配置
  nvidia:
    enabled: true
    # 资源切分配置
    splitCount: 10                # 每张 GPU 的切分份数
    memoryScaling: 1.0            # 显存缩放因子(>1.0 为超卖)
    coreScaling: 1.0              # 算力缩放因子
    # MIG 策略
    migStrategy: "none"           # none | single | mixed
    # GPU 计算模式
    computeMode: "Default"        # Default | ExclusiveProcess | ExclusiveThread
    # 驱动配置
    driverRoot: "/run/nvidia/driver"
    # CDI 支持
    cdi:
      enabled: false
      vendor: "hami.io"
      class: "gpu"

  # 华为昇腾 NPU 配置
  ascend:
    enabled: false
    splitCount: 4
    memoryScaling: 1.0

  # 寒武纪 MLU 配置
  cambricon:
    enabled: false
    splitCount: 8
    memoryScaling: 1.0

  # 海光 DCU 配置
  hygon:
    enabled: false
    splitCount: 4
    memoryScaling: 1.0

  # DaemonSet 更新策略
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: "10%"

  # 资源限制
  resources:
    requests:
      cpu: 100m
      memory: 128Mi
    limits:
      cpu: 500m
      memory: 512Mi

  # 容忍度
  tolerations:
  - key: "nvidia.com/gpu"
    operator: "Exists"
    effect: "NoSchedule"

  # 节点选择器
  nodeSelector:
    gpu: "true"

  # 健康检查
  livenessProbe:
    exec:
      command:
      - /bin/sh
      - -c
      - "test -S /var/lib/kubelet/device-plugins/hami-gpu.sock"
    initialDelaySeconds: 30
    periodSeconds: 10
    timeoutSeconds: 5

小结

本章详细介绍了 HAMi Device Plugin 和设备抽象层的设计与实现:

  • 设备插件概述:基于 Kubernetes Device Plugin API,以 DaemonSet 形式运行在每个 GPU 节点
  • 生命周期:Init→Register → ListAndWatch → Allocate 的完整状态机
  • 设备发现与注册:通过 NVML 或厂商 SDK 发现物理设备,注册到 Kubelet
  • 资源分配:读取调度器注解,准备设备路径、环境变量和挂载点
  • CDI:支持通过 Container Device Interface 标准化设备注入
  • 设备抽象层:统一的Devices接口,DeviceInfo/DeviceUsage 数据结构
  • 多厂商适配:NVIDIA、昇腾、寒武纪、海光等实现,新增后端只需实现统一接口

在下一章中,我们将深入 Webhook 与准入控制的工作机制。

创建于 2026/06/04 更新于 2026/06/05 3199 字 阅读约 7 分钟