文章目录
- [HAMI-core 技术解析](#HAMI-core 技术解析)
-
- [一、HAMI-core 简介](#一、HAMI-core 简介)
- [二、HAMI-core 代码框架解析](#二、HAMI-core 代码框架解析)
- [三、HAMI-core 核心原理深度拆解](#三、HAMI-core 核心原理深度拆解)
-
- [3.1 核心机制:CUDA 与 NVML API 拦截](#3.1 核心机制:CUDA 与 NVML API 拦截)
- [3.2 核心功能:GPU 内存与核心算力限制](#3.2 核心功能:GPU 内存与核心算力限制)
- [3.3 关键步骤:替换原生 libvgpu.so 驱动](#3.3 关键步骤:替换原生 libvgpu.so 驱动)
HAMI-core 技术解析
一、HAMI-core 简介
HAMi-core 是一款基于软件层面的 vCUDA 解决方案,核心目标是实现 GPU 资源的精细化隔离与限制,适配容器化(K8s + Pod)场景下的 GPU 资源调度需求。其核心实现逻辑的是:重写 NVIDIA 原生 CUDA 驱动(libvgpu.so),通过 Pod 挂载的方式替换原生驱动,再通过修改后的驱动拦截 CUDA 与 NVML 核心 API 接口,从而实现对 GPU 资源(内存、核心算力)的精准管控,解决容器场景下 GPU 资源共享冲突、分配不均的痛点。
HAMI-core 的核心原理可通过以下架构图清晰理解:
HAMI-Core Interception Stack
Native CUDA Stack
API Interception & Forward
CUDA Driver
Nvidia Driver
Nvidia GPU
CUDA Application
CUDA Library
CUDA Runtime
HAMI-Core
补充说明:架构图中涉及的核心组件(CUDA Application、CUDA Driver、Nvidia Driver、Nvidia GPU、CUDA Library、CUDA Runtime、HAMi-Core),其层级关系为:HAMi-Core 处于 CUDA 应用与原生 CUDA 驱动之间,通过拦截 API 调用,成为 GPU 资源管控的中间层,实现"应用无感知、资源可管控"。
二、HAMI-core 代码框架解析
HAMi-core 的代码结构清晰,各目录/文件职责明确,核心代码集中在 src 目录下,具体框架如下(补充各目录核心作用,便于读者快速理解代码分工):
plain
- src
- allocate 目录:核心负责 GPU 相关资源(主要是内存)的分配与释放逻辑实现,是内存限制功能的核心依赖。
- cuda 目录:负责 CUDA 库函数的初始化、核心 API 拦截逻辑实现,包含 hook 钩子、内存操作等核心代码。
- include 目录:存放所有模块的头文件,统一管理函数声明、宏定义、数据结构,保证代码的可复用性与一致性。
- multiprocess 目录:聚焦 GPU 核心(core)算力的限制实现,通过速率限制等机制,管控不同 Pod 对 GPU 核心的占用。
- nvml 目录:负责 NVML(NVIDIA Management Library)库函数的初始化与 API 拦截,配合 CUDA 拦截实现全方位资源管控。
- libvgpu.c 文件:核心入口文件,实现 dlsym 函数重写、全局初始化等逻辑,是 HAMI-core 生效的关键。
三、HAMI-core 核心原理深度拆解
3.1 核心机制:CUDA 与 NVML API 拦截
HAMi-core 的核心实现依赖"API 拦截"技术,其核心思路是:重写动态链接函数 dlsym,劫持 NVIDIA 动态链接库(CUDA、NVML)中关键函数的调用,尤其是以 cu(CUDA 相关)和 nvml(NVML 相关)开头的核心函数,在不修改上层应用代码的前提下,插入资源管控逻辑。
首先,HAMI-core 会预先定义需要拦截的 CUDA 和 NVML 函数列表,明确拦截范围:
c
// src/cuda/hook.c
typedef void* (*fp_dlsym)(void*, const char*);
extern fp_dlsym real_dlsym;
// 定义需要拦截的 CUDA 核心函数,涵盖初始化、设备操作等场景
cuda_entry_t cuda_library_entry[] = {
/* Init Part */
{.name = "cuInit"}, // CUDA 初始化函数,是拦截的第一个关键节点
/* Deivce Part */
{.name = "cuDeviceGetAttribute"}, // 获取设备属性,用于资源状态判断
{.name = "cuDeviceGet"}, // 获取 GPU 设备,关联资源分配
... // 省略其他核心 CUDA 函数
// src/nvml/hook.c
// 定义需要拦截的 NVML 核心函数,涵盖 NVML 初始化、关闭等场景
entry_t nvml_library_entry[] = {
{.name = "nvmlInit"}, // NVML 初始化,用于获取 GPU 硬件信息
{.name = "nvmlShutdown"}, // NVML 关闭,释放相关资源
... // 省略其他核心 NVML 函数
其次,在 HAMI-core 启动初始化阶段(preInit 函数调用时),会通过 load_cuda_libraries 和 load_nvml_libraries 两个函数,加载上述定义的函数指针地址,建立"拦截函数-原生函数"的映射关系,为后续拦截做准备:
c
// src/cuda/hook.c
void load_cuda_libraries() {
for (i = 0; i < CUDA_ENTRY_END; i++) {
LOG_DEBUG("LOADING %s %d",cuda_library_entry[i].name,i);
// 优先从指定动态链接表中加载原生函数指针
cuda_library_entry[i].fn_ptr = real_dlsym(table, cuda_library_entry[i].name);
if (!cuda_library_entry[i].fn_ptr) {
// 若未找到,从后续动态链接库中查找
cuda_library_entry[i].fn_ptr=real_dlsym(RTLD_NEXT,cuda_library_entry[i].name);
if (!cuda_library_entry[i].fn_ptr){
LOG_INFO("can't find function %s in %s", cuda_library_entry[i].name,cuda_filename);
// 尝试查找该函数的兼容版本,提升兼容性
memset(tmpfunc,0,500);
strcpy(tmpfunc,cuda_library_entry[i].name);
while (prior_function(tmpfunc)) {
cuda_library_entry[i].fn_ptr=real_dlsym(RTLD_NEXT,tmpfunc);
if (cuda_library_entry[i].fn_ptr) {
LOG_INFO("found prior function %s",tmpfunc);
break;
}
}
}
}
}
// src/nvml/hook.c
void load_nvml_libraries() {
for (i = 0; i < NVML_ENTRY_END; i++) {
LOG_DEBUG("loading %s:%d",nvml_library_entry[i].name,i);
// 加载 NVML 原生函数指针
nvml_library_entry[i].fn_ptr = real_dlsym(table, nvml_library_entry[i].name);
if (!nvml_library_entry[i].fn_ptr) {
LOG_INFO("can't find function %s in %s", nvml_library_entry[i].name,
driver_filename);
}
}
}
最后,对上述定义的函数进行重写,重写后的函数会先调用原生函数,再插入 HAMI-core 的资源管控逻辑,实现"原生功能保留+资源限制"的双重效果。以内存分配函数 cuMemAllocHost_v2 为例,清晰展示拦截逻辑:
c
// src/cuda/memory.c
CUresult cuMemAllocHost_v2(void** hptr, size_t bytesize) {
LOG_DEBUG("cuMemAllocHost_v2 hptr=%p bytesize=%ld",hptr,bytesize);
ENSURE_RUNNING(); // 确保 HAMI-core 处于运行状态
// 1. 调用 CUDA 原生内存分配函数,保证应用正常功能
CUresult res = CUDA_OVERRIDE_CALL(cuda_library_entry,cuMemAllocHost_v2, hptr, bytesize);
if (res != CUDA_SUCCESS) {
return res; // 若原生函数调用失败,直接返回错误
}
// 2. 插入 HAMI-core 内存限制逻辑:检查是否超出预设内存限制
if (check_oom()) {
// 若超出限制,释放已分配内存,并返回内存不足错误
CUDA_OVERRIDE_CALL(cuda_library_entry,cuMemFreeHost, *hptr);
return CUDA_ERROR_OUT_OF_MEMORY;
}
return res;
}
补充说明:dlsym 函数的重写是整个拦截机制的核心------dlsym 用于运行时查找动态链接库中的函数符号,HAMI-core 重写该函数后,当应用调用 CUDA/NVML 函数时,会优先被 HAMI-core 的拦截函数捕获,再转发至原生函数,从而实现"无侵入式拦截":
c
// src/libvgpu.c
FUNC_ATTR_VISIBLE void* dlsym(void* handle, const char* symbol) {
pthread_once(&dlsym_init_flag,init_dlsym); // 确保 dlsym 只初始化一次
// 拦截所有以 "cu" 开头的函数(CUDA 相关函数)
if (symbol[0] == 'c' && symbol[1] == 'u') {
pthread_once(&pre_cuinit_flag,(void(*)(void))preInit); // 初始化拦截相关逻辑
// 查找重写后的拦截函数
void* f = __dlsym_hook_section(handle, symbol);
if (f != NULL)
return f; // 若存在拦截函数,返回拦截函数地址(实现拦截)
}
// 若未命中拦截规则,返回原生 dlsym 查找结果
return real_dlsym(handle, symbol);
}
3.2 核心功能:GPU 内存与核心算力限制
基于上述 API 拦截机制,HAMI-core 实现了 GPU 两大核心资源的精准限制,解决容器场景下资源抢占问题:
-
GPU 内存限制:通过拦截内存相关 API(如 nvmlDeviceGetMemoryInfo、cuMemoryAllocate 等),实时监控 Pod 的 GPU 内存占用,当超出预设限制时,直接返回内存不足错误(如上述 cuMemAllocHost_v2 函数中的 check_oom 逻辑),避免单个 Pod 耗尽 GPU 内存,影响其他容器。
-
GPU 核心算力限制:通过拦截核心算力相关 API(如 cuLaunchKernel,GPU 内核启动函数),结合 multiprocess 目录下的 rate_limiter(速率限制器)逻辑,限制单个 Pod 对 GPU 核心的占用率,实现多个 Pod 公平共享 GPU 核心算力。
3.3 关键步骤:替换原生 libvgpu.so 驱动
HAMI-core 要实现全局生效,核心是将重写后的 libvgpu.so 替换掉 NVIDIA 原生驱动,且保证容器优先加载该驱动。其实现依赖 K8s Device Plugin 的 Allocate 方法,通过 hostPath 挂载方式,将 HAMI-core 的 libvgpu.so 和相关配置文件挂载到 Pod 中,具体实现如下:
首先,挂载 /etc/ld.so.preload 文件(系统级动态链接库加载配置文件),确保容器启动时优先加载 HAMI-core 的 libvgpu.so,覆盖原生驱动:
bash
# 容器内查看 ld.so.preload 配置(优先加载 HAMI-core 驱动)
# cat /etc/ld.so.preload
/usr/local/vgpu/libvgpu.so
其次,HAMI Device Plugin 的 Allocate 方法中,通过代码逻辑实现驱动和配置文件的挂载,核心代码如下(补充关键代码注释,便于理解挂载逻辑):
go
func (plugin *NvidiaDevicePlugin) Allocate(ctx context.Context, reqs *kubeletdevicepluginv1beta1.AllocateRequest) (*kubeletdevicepluginv1beta1.AllocateResponse, error) {
log.InfoS("Allocate", "request", reqs)
...
// 1. 挂载 HAMI-core 重写后的 libvgpu.so 驱动(只读,防止被篡改)
response.Mounts = append(response.Mounts,
&kubeletdevicepluginv1beta1.Mount{
ContainerPath: fmt.Sprintf("%s/vgpu/libvgpu.so", hostHookPath),
HostPath: GetLibPath(), // 主机上 HAMI-core libvgpu.so 的路径
eadOnly: true
// 2. 挂载 HAMI-core 缓存目录(可写,用于存储资源限制配置、运行日志等)
&kubeletdevicepluginv1beta1.Mount{
ContainerPath: fmt.Sprintf("%s/vgpu", hostHookPath),
HostPath: cacheFileHostDirectory,
dOnly: false
3. 挂载锁文件目录(用于解决多进程资源竞争问题)
beletdevicepluginv1beta1.Mount{
ContainerPath: "/tmp/vgpulock",
HostPath: "/tmp/vgpulock",
ReadOnly: false
},
...
// 若未找到已挂载的 ld.so.preload,挂载 HAMI 配置的 ld.so.preload(保证优先加载)
if !found {
response.Mounts = append(response.Mounts, &kubeletdevicepluginv1beta1.Mount{
ContainerPath: "/etc/ld.so.preload",
HostPath: hostHookPath + "/vgpu/ld.so.preload",
ReadOnly: true
})
...
} } / &ku // },
Rea }, R / k
最后,可通过 docker inspect 命令验证挂载效果:
bash
# 查看容器挂载信息,验证 HAMI-core 驱动和配置文件是否挂载成功
# docker inspect -f '{{.Mounts}}' <container_id> # 替换 <container_id> 为实际容器ID
"Mounts": [
{
"Type": "bind",
"Source": "/usr/local/vgpu/ld.so.preload", # 主机上的配置文件
"Destination": "/etc/ld.so.preload", # 容器内的配置文件(优先加载)
"Mode": "ro",
"RW": false,
"Propagation": "rprivate"
},
{
"Type": "bind",
"Source": "/usr/local/vgpu/libvgpu.so", # 主机上 HAMI 重写的驱动
"Destination": "/usr/local/vgpu/libvgpu.so", # 容器内的驱动路径
"Mode": "ro",
"RW": false,
"Propagation": "rprivate"
},
... # 省略其他挂载项
]