Featured image of post HAMI 虚拟化原理与资源超卖机制

HAMI 虚拟化原理与资源超卖机制

HAMI vGPU 虚拟化原理笔记

HAMI 虚拟化原理与资源超卖机制

HAMI(HAMi)是一个开源的 vGPU 方案,本文从调度器原理、资源超卖两个方面做个总结。

一、调度器原理

1.1 架构概述

Hami-scheduler 采用 Scheduler Extender 机制实现,而非直接扩展 default-scheduler:

  • 使用默认 kube-scheduler 镜像启动服务,通过配置将调度器名称指定为 hami-scheduler
  • 为该调度器配置 Extender,Extender 服务由同一 Pod 中的另一个 Container 启动的 HTTP 服务提供

1.2 部署架构

1
2
3
4
5
Deployment (kube-system/vgpu-hami-scheduler)
├── Container 1: kube-scheduler(原生调度器)
│   └── 使用 KubeSchedulerConfiguration 配置 Extender
└── Container 2: vgpu-scheduler-extender(HAMi 调度逻辑)
    └── 提供 /filter 和 /bind HTTP 接口

1.3 关键配置

KubeSchedulerConfiguration 配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
profiles:
- schedulerName: hami-scheduler
extenders:
- urlPrefix: "https://127.0.0.1:443"
  filterVerb: filter      # 对应 /filter 接口
  bindVerb: bind          # 对应 /bind 接口
  nodeCacheCapable: true
  weight: 1
  httpTimeout: 30s
  enableHTTPS: true
  tlsConfig:
    insecure: true
  managedResources:
  - name: nvidia.com/vgpu
    ignoredByScheduler: true
  # ... 其他 vGPU 相关资源

managedResources 作用:

  • 指定 HAMi 管理的资源类型(nvidia.com/vgpunvidia.com/gpumem 等)
  • ignoredByScheduler: true 表示原生调度器忽略这些资源,完全由 Extender 处理
  • 只有 Pod 申请了这些资源时,调度器才会请求 Extender

1.4 资源感知机制

1.4.1 感知节点上的 GPU 资源信息

HAMi 通过 Node Annotations 获取 GPU 信息:

1
2
# Node Annotation 示例
hami.io/node-nvidia-register: 'GPU-03f69c50-207a-2038-9b45-23cac89cb67d,10,46068,100,NVIDIA-NVIDIA A40,0,true:...'

格式解析:GPU-ID,索引,显存(MB),核心数,厂商型号,NUMA节点,健康状态

数据来源:

  • DevicePlugin 中的后台 Goroutine 定时上报并写入 Node Annotations
  • Scheduler 通过 RegisterFromNodeAnnotations() 方法定时(每 15 秒)从 Annotations 解析 GPU 信息

1.4.2 感知节点上 GPU 使用情况

通过 Informer 机制 Watch Pod 和 Node 变化:

1
2
3
4
5
6
// Pod 事件处理
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
    AddFunc:    s.onAddPod,
    UpdateFunc: s.onUpdatePod,
    DeleteFunc: s.onDelPod,
})

从 Pod Annotations 解析 GPU 使用情况:

1
2
# Pod Annotation 示例
hami.io/vgpu-devices-allocated: 'GPU-03f69c50-207a-2038-9b45-23cac89cb67d,NVIDIA,3000,30:;'

格式解析:GPU-UUID,设备类型,已用显存(MB),已用核心百分比

1.5 调度核心算法

1.5.1 Filter 接口(节点过滤与打分)

流程:

1
2
3
4
1. 检查 Pod 是否申请 vGPU 资源 → 未申请则返回全部节点
2. 获取所有节点的 GPU 使用情况 (getNodesUsage)
3. 计算每个节点的得分 (calcScore)
4. 选择得分最高的节点进行调度

得分计算算法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// pkg/scheduler/policy/node_policy.go
func (ns *NodeScore) ComputeScore(devices DeviceUsageList) {
    // 统计已使用资源
    used, usedCore, usedMem := int32(0), int32(0), int32(0)
    for _, device := range devices.DeviceLists {
        used += device.Device.Used
        usedCore += device.Device.Usedcores
        usedMem += device.Device.Usedmem
    }

    // 统计总资源
    total, totalCore, totalMem := int32(0), int32(0), int32(0)
    for _, deviceLists := range devices.DeviceLists {
        total += deviceLists.Device.Count
        totalCore += deviceLists.Device.Totalcore
        totalMem += deviceLists.Device.Totalmem
    }

    // 计算得分:资源使用率越高,得分越高
    useScore := float32(used) / float32(total)
    coreScore := float32(usedCore) / float32(totalCore)
    memScore := float32(usedMem) / float32(totalMem)

    ns.Score = float32(Weight) * (useScore + coreScore + memScore)
}

核心逻辑: 节点上 GPU Core 和 GPU Memory 资源剩余越少,得分越高(Binpack 策略)。

1.5.2 Bind 接口(完成调度)

核心逻辑: 调用 Kubernetes API 创建 Binding 对象将 Pod 绑定到目标节点

1
2
3
4
5
binding := &corev1.Binding{
    ObjectMeta: metav1.ObjectMeta{Name: args.PodName, UID: args.PodUID},
    Target:     corev1.ObjectReference{Kind: "Node", Name: args.Node},
}
err = s.kubeClient.CoreV1().Pods(args.PodNamespace).Bind(context.Background(), binding, ...)

1.6 内存数据结构

1
2
3
4
5
type Scheduler struct {
    nodes map[string]*util.NodeInfo    // 节点 GPU 信息缓存
    cachedstatus map[string]*NodeUsage // 节点资源使用情况
    overviewstatus map[string]*NodeUsage // 全局节点概览
}

二、资源超卖机制

2.1 超卖配置参数

参数默认值超卖场景
device-memory-scaling1.0显存超卖(如设为 1.5 表示 100GB 可虚拟出 150GB)
device-cores-scaling1.0核心超卖(如设为 1.5 表示 100% 核心可虚拟出 150%)

2.2 资源充足情况

场景: 节点有足够的物理 GPU 资源满足 Pod 需求

处理方式:

  1. 正常计算节点得分
  2. 根据 Binpack 策略选择得分最高(资源利用率最高)的节点
  3. Pod 分配到目标节点后,Device Plugin 进行资源绑定

调度流程:

1
Pod 申请资源 → Filter 阶段计算得分 → 选择得分最高节点 → Bind 阶段绑定 → Device Plugin 分配

2.3 资源不足情况

场景: 节点剩余资源无法完全满足 Pod 需求

处理方式:

场景处理方式
资源充足正常计算得分,选择得分最高(资源利用率最高)的节点
资源不足fitInDevices() 判断节点剩余资源是否满足 Pod 需求,不满足则忽略该节点
所有节点都不满足返回错误:“no available node, all node scores do not meet”

核心判断逻辑(伪代码):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func fitInDevices(node *NodeUsage, request ResourceRequest) bool {
    // 检查显存是否满足
    if node.AvailableMem < request.Memory {
        return false
    }
    // 检查核心是否满足
    if node.AvailableCores < request.Cores {
        return false
    }
    // 检查设备数量是否满足
    if node.AvailableDevices < request.DeviceCount {
        return false
    }
    return true
}

2.4 显存超卖原理

device-memory-scaling > 1.0 时启用显存超卖:

  1. 注册阶段:将物理显存按比例放大写入 Node Annotations

    1
    2
    
    实际显存: 46068 MB
    缩放后显存: 46068 * 1.5 = 69102 MB
    
  2. 调度阶段:按虚拟显存进行调度,允许调度总量超过物理显存

  3. 运行时限制:通过 libvgpu.so 拦截 CUDA 内存分配请求

    • 环境变量 CUDA_DEVICE_MEMORY_LIMIT_* 设置显存上限
    • 超额部分通过共享缓存文件(CUDA_DEVICE_MEMORY_SHARED_CACHE)模拟

2.4.1 运行时显存分配失败处理机制

当 Pod 在 limit 配额内申请更多显存,但整个 GPU 已经没有余下可用显存时,HAMi-core 通过以下机制处理,类似应用接收到显存分配失败异常,而不是被OOM Kill:

1. 内存分配拦截与检查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// libvgpu/cuda_memory.c - cuMemAlloc_v2_hook
CUresult cuMemAlloc_v2_hook(CUdeviceptr *dptr, size_t bytesize)
{
    log_debug("Intercepted cuMemAlloc_v2: size=%zu", bytesize);

    // 检查内存限制
    if (check_memory_limit(bytesize) != 0) {
        log_error("Memory allocation exceeds limit: %zu bytes", bytesize);
        return CUDA_ERROR_OUT_OF_MEMORY;
    }

    // 调用真实的 cuMemAlloc
    CUresult result = cuMemAlloc_v2_real(dptr, bytesize);

    if (result == CUDA_SUCCESS) {
        // 更新内存使用统计
        update_memory_usage(bytesize);
        // 记录分配信息
        record_allocation(*dptr, bytesize);
    }

    return result;
}

2. 共享内存协调机制

HAMi-core 使用共享内存实现多进程间的内存使用统计和协调:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// libvgpu/cuda_shm.c - 共享内存结构
struct shm_info {
    size_t memory_limit;      // 内存限制(来自环境变量)
    size_t used_memory;       // 当前已使用内存
    int process_count;        // 进程计数
    pid_t process_pids[MAX_PROCESSES];      // 进程PID数组
    size_t process_memory[MAX_PROCESSES];   // 每个进程的内存使用
};

// 检查内存限制
int check_memory_limit(size_t size)
{
    struct shm_info info;

    // 从共享内存获取当前内存使用情况
    if (get_shm_info(&info) != 0) {
        return -1;
    }

    // 检查是否超过限制
    if (info.used_memory + size > info.memory_limit) {
        log_error("Memory limit exceeded: %zu + %zu > %zu",
                  info.used_memory, size, info.memory_limit);
        return -1;
    }

    return 0;
}

3. 处理流程总结

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌─────────────────────────────────────────────────────────────────┐
│              显存分配失败处理流程                                 │
├─────────────────────────────────────────────────────────────────┤
│  1. CUDA 应用调用 cudaMalloc()                                   │
│     └── 被 libvgpu.so 拦截                                       │
│                                                                  │
│  2. 检查内存限制 (check_memory_limit)                           │
│     ├── 从共享内存读取当前使用量                                 │
│     ├── 检查是否超过 CUDA_DEVICE_MEMORY_LIMIT_*                 │
│     └── 如果超过,返回 CUDA_ERROR_OUT_OF_MEMORY                 │
│                                                                  │
│  3. 如果未超过限制,调用真实 cuMemAlloc()                       │
│     ├── 成功 → 更新共享内存使用统计                              │
│     └── 失败 → 返回 CUDA_ERROR_OUT_OF_MEMORY                    │
│                                                                  │
│  4. CUDA 错误处理                                                │
│     ├── 记录错误日志(当前使用量、限制值)                       │
│     ├── 触发 OOM 处理函数                                        │
│     └── 尝试清理缓存(如果有)                                   │
│                                                                  │
│  5. 应用程序收到 CUDA_ERROR_OUT_OF_MEMORY                       │
│     └── 应用程序自行处理 OOM(如释放内存、降低 batch size 等)   │
└─────────────────────────────────────────────────────────────────┘

2.5 核心超卖原理

device-cores-scaling > 1.0 时启用核心超卖:

  1. 配置方式:设置 CUDA_DEVICE_SM_LIMIT 环境变量限制 SM 使用比例

    1
    2
    
    物理核心: 100%
    缩放后虚拟核心: 150%
    
  2. 实现原理:通过 libvgpu.so 周期性采样和限制

    • 采样周期内如果 SM 利用率超过限制,则触发限流
    • recentKernellastKernelTime 用于平滑超卖场景下的资源分配

2.6 调度策略配置

节点选择策略(通过 Annotations 配置):

  • hami.io/node-scheduler-policy: 节点级调度策略(binpack / spread
  • hami.io/gpu-scheduler-policy: GPU 级调度策略(binpack / spread
策略说明
binpack优先将 Pod 调度到资源使用率高的节点,减少碎片
spread优先将 Pod 调度到资源使用率低的节点,提高容错

三、调度流程总结

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────────────────┐
│                     HAMI 调度器工作流程                              │
├─────────────────────────────────────────────────────────────────────┤
│  1. 用户创建 Pod 并申请 vGPU 资源                                    │
│                                                                     │
│  2. Webhook 修改 SchedulerName 为 hami-scheduler                    │
│                                                                     │
│  3. hami-scheduler 接收调度请求                                      │
│     ├── 获取 Node Annotations → 解析 GPU 资源总量                   │
│     ├── 获取 Pod Annotations  → 解析 GPU 使用量                     │
│     └── 计算各节点剩余可用资源                                       │
│                                                                     │
│  4. Filter 阶段:                                                    │
│     ├── fitInDevices() 检查资源是否满足                             │
│     ├── 根据资源使用率计算得分(Binpack/Spread)                    │
│     └── 选择得分最高的节点                                          │
│                                                                     │
│  5. Bind 阶段:将 Pod 绑定到目标节点                                 │
│                                                                     │
│  6. Kubelet 启动 Pod,Device Plugin 进行资源绑定                    │
│     ├── 设置 NVIDIA_VISIBLE_DEVICES(原生逻辑)                    │
│     ├── 挂载 libvgpu.so,设置资源限制环境变量(HAMi 逻辑)          │
│     └── libvgpu.so 运行时拦截 CUDA API 实现资源隔离                 │
└─────────────────────────────────────────────────────────────────────┘

四、关键环境变量汇总

环境变量作用
NVIDIA_VISIBLE_DEVICES指定容器可见的 GPU 设备
CUDA_DEVICE_MEMORY_LIMIT_*限制对应 GPU 的显存使用量
CUDA_DEVICE_SM_LIMIT限制 GPU 核心(SM)使用比例
CUDA_DEVICE_MEMORY_SHARED_CACHE共享内存缓存文件路径
CUDA_OVERSUBSCRIBE启用显存超额订阅
CUDA_DISABLE_CONTROL禁用 libvgpu.so 控制(跳过 ld.so.preload 替换)
CoreLimitSwitch是否关闭算力限制

五、总结

HAMI vGPU 方案的核心设计要点:

  1. 调度器层:采用 Extender 机制实现调度逻辑,根据节点资源使用率进行打分和选择

  2. 资源超卖:通过 device-memory-scalingdevice-cores-scaling 参数启用,配合 libvgpu.so 实现运行时隔离

  3. 核心隔离:依赖 libvgpu.so 拦截 CUDA API,通过环境变量控制显存和核心使用上限

往日已经不在,未来尚未开始
使用 Hugo 构建
主题 StackJimmy 设计