HAMI-core 技术解析

文章目录

  • [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"
            },
            ...  # 省略其他挂载项
        ]
相关推荐
密瓜智能4 个月前
HAMi Meetup-贝壳找房 回顾:vGPU 推理集群的实践经验
云原生·性能优化·开源·gpu算力·hami·密瓜智能
狂奔solar1 年前
HAMi + prometheus-k8s + grafana实现vgpu虚拟化监控
prometheus·hami