设备插件与设备抽象层
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"设备插件生命周期
启动流程
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
}核心方法说明
| 方法 | 调用方 | 功能 |
|---|---|---|
GetNodeDevices | Scheduler | 获取节点上的设备列表和状态 |
NodeClean | Scheduler | 清理已终止 Pod 占用的设备资源 |
ResourceName | Scheduler/Plugin | 返回注册的资源名(如 nvidia.com/gpu) |
Discovery | Device Plugin | 发现物理设备并生成设备列表 |
Lock/Unlock | Scheduler | 获取/释放节点级互斥锁 |
PatchPodAnnotations | Scheduler | 将分配决策写入 Pod 注解 |
ScoreNode | Scheduler | 根据策略对节点评分 |
Fit | Scheduler | 检查设备是否满足 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
}类图
多厂商适配
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 与准入控制的工作机制。