深入解析 K8s GPU 复用:从通用方案到 HAMi 异构架构全解

在云原生 AI 时代,GPU 作为算力核心资产,其高昂的成本与常常不到 40% 的实际利用率,让"显卡复用"成为企业降本增效的必答题。本文将全景解析当前主流的 GPU 复用方案,并深度剖析国产开源项目 HAMi 如何通过底层 Hook 技术与云原生调度,实现多厂商异构 GPU 的精细化隔离。


一、 GPU 复用全景:8 大主流方案对比

当前主流的显卡复用方案可按技术层次划分为五大类,不同路线在隔离强度、性能损耗、硬件门槛和部署复杂度上呈现明显的梯度差异:从硬到软,灵活性递增、隔离性递减

方案 技术层次 隔离级别 性能损耗 硬件门槛 典型场景
MIG 硬件分区 物理硬隔离 几乎无 仅 A100/A30/H100 等 多租户推理、确定 QoS
NVIDIA vGPU 驱动/Hypervisor 级 驱动级硬隔离 较低 VGX 系列卡 + IOMMU VDI、图形桌面云、虚机推理
SR-IOV PCIe 硬件虚拟化 硬件级 极低 Ampere 及以后数据中心卡 HPC/AI 多虚机直通
CUDA MPS API/运行时级 共享上下文 所有 NVIDIA GPU 同机多进程推理、MPI 作业
Time-Slicing 容器/调度级 无隔离 所有 NVIDIA GPU 开发/测试、轻量推理
HAMi / libvgpu.so CUDA API Hook 软隔离 较低 不挑硬件,支持国产卡 K8s 推理共享、显存配额超卖
GPU 池化 远程调用 软隔离+网络隔离 较高 CPU/GPU 节点分离 跨节点池化、CPU 节点远程用卡
DRA (K8s 1.34+) K8s 原生设备 API 取决于策略 取决于实现 通用 K8s 原生 GPU 分配统一入口

选型建议:

  • 若硬件支持且需硬隔离 → MIG
  • 需 VM 级隔离与热迁移 → NVIDIA vGPU
  • 同一 K8s 集群内多 Pod 共享、需显存配额与超卖 → HAMi
  • 快速验证或不支持 MIG 的卡 → Time-Slicing
  • 面向未来 K8s 原生统一调度 → DRA

二、 HAMi 的黑魔法:零侵入 API Hooking 原理

在上述方案中,HAMi(Heterogeneous AI Computing Virtualization Framework) 因其不挑硬件、免费开源、支持显存超卖的特性,成为国内 K8s 推理集群提升利用率的主流开源方案。而 HAMi 实现零侵入隔离的核心,依赖于 Linux 系统的两大底层机制:LD_PRELOADdlsym

整体拦截拓扑

在展开两个机制前,先看 HAMi 拦截一次显存申请的完整数据流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
AI 程序(PyTorch/tf)进程
│ 调用 cuMemAlloc(申请显存)

┌─────────────────────────────────────────────┐
│ 动态链接器加载共享库(按优先级) │
│ │
│ ① LD_PRELOAD 指定的库 ← 最高优先级 │
│ └─ libvgpu.so (HAMi 的替身库) │
│ ├─ cuMemAlloc() ← 假函数,拦截 │
│ │ 1. 检查配额(current+bytes>limit?)│
│ │ 超限 → 返回 OUT_OF_MEMORY │
│ │ 2. dlsym(RTLD_NEXT,"cuMemAlloc")│
│ │ 找到"排在我后面"的真函数地址 │
│ │ 3. 转发给真驱动 │
│ │ 4. 更新 current_usage │
│ ▼ │
│ ② 标准库/驱动库 │
│ └─ libcuda.so (NVIDIA 真驱动) │
│ └─ cuMemAlloc() ← 真身,真正分配 │
└─────────────────────────────────────────────┘


物理显卡(实际分配显存)

关键点:AI 程序以为自己调的是 NVIDIA 的 cuMemAlloc,实际先进了 HAMi 的替身;替身查配额、放行后才把请求转给真驱动。全程不改 AI 程序代码、不改驱动,这就是"零侵入"。

1. LD_PRELOAD:强行插队的"替身乘务员"

LD_PRELOAD 是 Linux 系统中的一个环境变量。程序运行时,动态链接器会按特定顺序加载共享库,而 LD_PRELOAD 拥有最高优先级

通俗比喻:假设你要坐高铁(运行程序),正常会去找 3 号车厢的乘务员(标准库 libcuda.so)。但设置了 LD_PRELOAD 后,就像系统在检票口给你派了一个"替身乘务员"(你自定义的库 libvgpu.so)。你一喊乘务员,响应你的是替身,而不是真身。

在 GPU 复用中的用途:HAMi 将自己编译的动态库 libvgpu.so 通过 LD_PRELOAD 注入容器。当 AI 程序调用 CUDA 库申请显存时,会最先跳入 libvgpu.so 的同名函数中,从而被 HAMi 拦截。

2. dlsym 与 RTLD_NEXT:寻找真身的"暗号"

如果替身乘务员只会拦车不会发车,系统就崩了。假函数在检查完配额后,必须把请求转发给真正的显卡驱动。

dlsym 是 Linux 动态加载库提供的 API,用于在运行时根据函数名查找真实内存地址。其中有一个特殊的伪句柄 RTLD_NEXT

作用:当调用 dlsym(RTLD_NEXT, "cuMemAlloc") 时,它告诉动态链接器:

“在当前加载的库列表中,找名为 cuMemAlloc 的函数,但跳过我自己,找排在我后面的第一个真正的 cuMemAlloc。”

这巧妙地避免了假函数调用自己导致的死循环。

3. cuMemAlloc:向显卡申请显存的底层动作

PyTorch 中的 torch.cuda.malloc 底层最终调用的都是 NVIDIA 驱动层的 cuMemAlloc。拦截了这个函数,就掐住了所有 AI 框架申请显存的咽喉。

核心拦截逻辑伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define _GNU_SOURCE
#include <dlfcn.h>
#include <cuda.h>

typedef CUresult (*real_cuMemAlloc_t)(CUdeviceptr *dptr, size_t bytesize);

// 被 LD_PRELOAD 拦截后执行的假函数
CUresult cuMemAlloc(CUdeviceptr *dptr, size_t bytesize) {
// 1. 拦截:检查显存配额
if (current_usage + bytesize > CONTAINER_LIMIT) {
return CUDA_ERROR_OUT_OF_MEMORY; // 超限,直接拒绝
}
// 2. 放行:使用 RTLD_NEXT 寻找真正的函数地址
real_cuMemAlloc_t real_func = (real_cuMemAlloc_t)dlsym(RTLD_NEXT, "cuMemAlloc");
// 3. 调用真正的底层驱动
CUresult result = real_func(dptr, bytesize);
// 4. 更新使用量
if (result == CUDA_SUCCESS) {
current_usage += bytesize;
}
return result;
}

三、 坚守当下:HAMi 的 Device Plugin 异构调度机制

由于 DRA 要求较高的 K8s 版本,当前生产环境主流仍是 Device Plugin 模式。HAMi 在此模式下通过「统一的 MutatingWebhook + 统一的 Scheduler-Extender + 每厂商一个 DevicePlugin + 每厂商一套资源名 + 每厂商一个 Hook 库」实现异构支持。

整体异构调度拓扑

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
29
30
31
32
33
34
35
36
37
            用户提交 Pod(声明多厂商资源)


┌───────────────────────────────┐
│ ① MutatingWebhook (统一入口) │
│ 遍历所有厂商 MutateAdmission │
│ 命中 → 改写 schedulerName │
│ = hami-scheduler │
└───────────────┬───────────────┘

┌───────────────────────────────┐
│ ② Scheduler-Extender │
│ (hami-scheduler) │
│ Filter: 解析节点注解建全局设备表│
│ 累加运行中 Pod 用量 │
│ 按厂商检查剩余显存/算力│
│ Bind: Patch 分配决策到 Pod 注解│
└───────────────┬───────────────┘

┌───────────────────────────────┐
│ ③ Kubelet (节点上) │
│ 调 DevicePlugin gRPC Allocate │
└───┬───────────┬───────────┬───┘
│ │ │
┌─────────▼─┐ ┌─────▼─────┐ ┌──▼──────────┐
│ NVIDIA DP │ │ 昇腾 DP │ │ MLU/海光 DP │
│ libvgpu.so │ │ libvnpu.so│ │ 厂商 Hook 库│
│ gpu=on节点 │ │ ascend=on │ │ │
└─────────┬───┘ └─────┬────┘ └──────┬──────┘
│ │ │
读 Pod 注解的配额,注入:
• 环境变量(HAMI_CONTAINER_MEM 等)
• 挂载 Hook 库 + 设 LD_PRELOAD


Pod 容器跑起来
(AI 程序被 LD_PRELOAD 拦截,按配额用卡)

三个统一 + 每厂商一份:Webhook 和 Scheduler-Extender 是全局唯一的;每个厂商有自己的 DevicePlugin、资源名、Hook 库(libvgpu.so/libvnpu.so/…)。这样加新厂商只需加一个 DP + 一套资源名 + 一个 Hook 库,不用动 Webhook 和调度器。

1. 架构与资源映射

不同厂商在 HAMi 中的适配方式:

厂商 扩展资源名 隔离实现 节点标签
NVIDIA nvidia.com/gpu / gpumem / gpucores libvgpu.so (LD_PRELOAD Hook) gpu=on
华为昇腾 huawei.com/Ascend910B3 / -memory / -core libvnpu.so + Limiter ascend=on
寒武纪 MLU cambricon.com/vmlu / vmemory / vcore 厂商 SMLU 硬件级 + HAMi 调度 mlu=on
海光 DCU hygon.com/dcunum / dcumem / dcucores libvgpu-control.so 变体 dcu=on

2. 核心调度链路(五步走)

Step 1: 节点注册 — 每厂商 DevicePlugin 每 30 秒 Patch 节点注解,把设备规格写入 hami.io/node-{device-type}-register。异构节点示例(同时有 NVIDIA 和 MLU):

1
2
hami.io/node-nvidia-register: GPU-00552014-...,10,32768,100,NVIDIA-V100,0,true
hami.io/node-mlu-register: MLU-45013011-...,10,23308,0,MLU-370-X4,0,false

Step 2: MutatingWebhook 拦截 — Webhook 遍历所有已注册设备类型,调用每个厂商的 MutateAdmission 判断 Pod 是否请求了该厂商资源。命中则把 schedulerName 改写为 hami-scheduler

Step 3: Filter 阶段(全局记账) — Scheduler-Extender 持续解析所有节点注解构建全局设备表,并累加运行中 Pod 的已用量。在 Filter 阶段按厂商分别检查剩余显存/算力是否满足。

Step 4: Bind 阶段(结果传递) — 由于原生 Bind 无法传递配额,HAMi 在 Bind 阶段把完整分配决策 Patch 到 Pod 注解:

1
hami.io/vgpu-devices-allocated: GPU-0fc3eda5-...,NVIDIA,3000,0:;

Step 5: Allocate 阶段(环境注入) — Kubelet 调用 DevicePlugin 的 gRPC Allocate 接口,HAMi 从 Pod 注解读取配额,注入环境变量(如 HAMI_CONTAINER_MEM)并挂载 Hook 库(如 libvgpu.so 设置 LD_PRELOAD)。

3. 配置示例:异构集群 Helm 部署

通过 Helm values.yaml 启用多厂商:

1
2
3
4
5
6
7
8
9
10
devicePlugin:
nvidiaNodeSelector: {gpu: "on"}
devices:
ascend:
enabled: true
nodeSelector: {ascend: "on"}
customresources:
- huawei.com/Ascend910B3
- huawei.com/Ascend910B3-memory
- huawei.com/Ascend910B3-core

提交异构任务时,只需声明不同厂商的扩展资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Ascend 910C 推理,软切分 28Gi 显存 + 40% 算力
apiVersion: v1
kind: Pod
metadata:
name: ascend-infer
annotations:
huawei.com/vnpu-mode: "hami-core" # 必须显式声明软切分
spec:
containers:
- name: npu
image: ascend-pytorch:24.0.RC2
resources:
limits:
huawei.com/Ascend910B3: "1"
huawei.com/Ascend910B3-memory: "28672" # 28 GiB
huawei.com/Ascend910B3-core: "40" # 40% 算力

四、 迈向未来:HAMi 的 DRA 模式与异构支持

Kubernetes 1.34 开始,DRA(Dynamic Resource Allocation)正式 GA,它突破了 Device Plugin 只能上报整数数量的限制,支持显存级申请。HAMi 也顺势推出了 HAMi-DRA 架构。

DRA 与 Device Plugin 模式对比

1
2
3
4
5
6
7
8
9
10
11
12
Device Plugin 模式(当下):                DRA 模式(未来):
┌──────────────┐ ┌──────────────┐
│ Webhook 改 │ │ 原生 DRA │
│ schedulerName│ │ Scheduler │
│ + Extender │ │ (无需 Extender)│
│ + 节点注解协议│ │ + ResourceSlice│
│ + Pod 注解传配额│ │ + ResourceClaim│
└──────┬───────┘ └──────┬───────┘
│ 自定义链路,绕过原生 API │ 原生 API
▼ ▼
每厂商一个 DevicePlugin 每厂商一个 DRA Driver
(上报整数 + 注解存规格) (上报显存级 capacity)

DRA 把 HAMi 原来靠 Webhook+Extender+注解协议"绕"出来的能力(显存级申请、异构统一调度)变成了 K8s 原生 API,链路更短、更标准。DRA 模式下每个厂商需一个独立 DRA Driver,目前 HAMi 支持:

  • NVIDIA GPU: hami-core-gpu.project-hami.io (GA)
  • 华为昇腾 910C: ascend.project-hami.io (GA,HAMivNPUCore 模式)

异构集群部署示例(NVIDIA + Ascend)

在 DRA 模式下,异构集群采用"每个厂商一个独立 DRA Driver + 一个统一 Webhook"的架构:

1
2
3
4
5
6
7
8
9
10
11
# 1. 前置要求:K8s >= 1.34,开启 FeatureGate DRAConsumableCapacity
# kube-apiserver/controller-manager/scheduler 添加 --feature-gates=DRAConsumableCapacity=true

# 2. 安装 NVIDIA 侧 DRA Driver
helm repo add hami-dra https://project-hami.github.io/HAMi-DRA
helm install hami-dra hami-dra/hami-dra -n hami-system --create-namespace

# 3. 安装昇腾侧 DRA Driver
git clone https://github.com/Project-HAMi/ascend-dra-driver.git
cd ascend-dra-driver
helm upgrade --install ascend-dra-driver deployments/helm/ascend-dra-driver -n ascend-dra-system --create-namespace

原生 DRA 异构任务 YAML——一个 Pod 可以同时申请 NVIDIA 和 Ascend 设备:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: resource.k8s.io/v1
kind: ResourceClaim
metadata:
name: heterogeneous-claim
spec:
devices:
requests:
- name: nvidia-partition
exactly:
deviceClassName: hami-core-gpu.project-hami.io
capacity:
requests:
cores: 50
memory: "10Gi"
- name: ascend-partition
exactly:
deviceClassName: ascend.project-hami.io
capacity:
requests:
cores: 30
memory: "16Gi"

五、 总结

显卡复用的本质是在"隔离性"与"弹性"之间寻找平衡点。

  • 底层技术:LD_PRELOADRTLD_NEXT 构成了 HAMi 零侵入拦截的基石,让显存配额限制成为可能。
  • 当下生产:HAMi 的 Device Plugin 模式通过 Webhook + 节点注解协议 + 厂商级 DevicePlugin,巧妙绕过了 K8s 原生 API 的限制,实现了对 12+ 厂商异构 AI 加速器的统一调度。
  • 未来趋势:随着 K8s 1.34 DRA 的 GA,HAMi 正逐步将异构调度逻辑迁移至原生的 ResourceSliceResourceClaim API。DRA 模式将简化调度链路,但 Device Plugin 模式在短时间内仍是兼容多厂商与旧版本集群的中流砥柱。

一句话:HAMi 的精髓是用 LD_PRELOAD 这把"软刀子"在不改程序、不改驱动的前提下给显存配额套上枷锁,再用 Device Plugin + Webhook + Extender 把这套机制织进 K8s 调度——当下靠注解协议扛异构,未来靠 DRA 原生化