摘要:libhsakmt 最初的设计是"一个进程 = 一个 KFD 上下文"。全局变量
hsakmt_primary_kfd_ctx持有进程唯一的/dev/kfd文件描述符,所有线程共享同一份拓扑、内存、队列、事件状态。这个模型简单高效,但无法满足多租户 / 容器化 / 调试器隔离等新兴需求。本篇分析 KFD Context 机制的引入动机、数据结构设计以及新旧 API 的衔接策略。
1. 原始模型:单进程单上下文
在 1.1 篇中我们已经详细分析过,hsaKmtOpenKFDCtx()对/dev/kfd只执行一次open()调用,后续所有请求都通过引用计数返回同一个全局上下文:
// openclose.cHsaKFDContext hsakmt_primary_kfd_ctx={.fd=-1,.hsakmt_is_primary_ctx=true,.hsakmt_is_svm_api_supported=false,};这个全局变量就是整个进程的 GPU “身份证”。所有 ioctl 都通过hsakmt_primary_kfd_ctx.fd发往内核,所有子系统(拓扑、内存、队列、事件、调试、性能计数器)的状态都挂在这个结构体下。
在这个模型中,进程 = KFD 上下文 = 内核侧kfd_process,三者是一一对应的。这也是 KFD 内核驱动长期以来的假设 —— 它以pid为粒度管理 GPU 资源:每个进程拥有独立的 GPU 虚拟地址空间(GPUVM)、页表、队列和事件。
这个设计干净利落,但有一个根本限制:同一个进程内无法拥有多个相互隔离的 GPU 资源域。
2. 为什么需要多上下文
2.1 容器化与多租户 GPU 共享
在云计算和容器化场景中,多个"租户"(容器 / 虚拟机 / 用户进程)可能通过同一个宿主进程间接访问 GPU。例如,一个 GPU 虚拟化代理进程需要同时为多个容器提供隔离的 GPU 服务。如果 libhsakmt 只有一个全局上下文,所有容器的 GPU 资源就混在一起 —— 队列共享、内存可互相访问、事件可互相干扰。这显然不能满足隔离需求。
2.2 调试器隔离
GPU 调试器(如 ROCgdb)需要在不干扰目标进程正常 GPU 操作的前提下,建立自己的 KFD 通信通道来设置断点、检查队列状态、读取 GPU 内存。如果调试器和被调试程序共享同一个上下文,调试操作与计算操作之间的状态干扰将难以避免。
2.3 DRM 层面的 VM 隔离
当一个进程内的多个"应用"需要独立的 GPU 虚拟地址空间时,每个应用需要自己的 DRM file handle。这在 GPU 虚拟化(VirtIO-GPU)场景中尤为关键 —— 宿主侧的代理进程需要为每个 Guest 维护独立的 VM 空间,以实现 Guest 之间的地址空间隔离和隐式同步。
3. HsaKFDContext:上下文数据结构
为了支持多上下文,AMD 工程师引入了HsaKFDContext结构体,将原本散落在全局变量中的所有子系统状态收归其下:
// kfdcontext.htypedefstruct_HsaKFDContext{intfd;// KFD 文件描述符bool hsakmt_is_primary_ctx;// 是否为 Primary 上下文bool hsakmt_is_svm_api_supported;// SVM API 支持标志structhsa_kfd_topology_context*topology_context;// 拓扑信息structhsa_kfd_queue_context*queue_context;// 队列资源structhsa_kfd_fmm_context*fmm_context;// 内存管理(FMM)structhsa_kfd_event_context*event_context;// 事件structhsa_kfd_debug_context*debug_context;// 调试structhsa_kfd_perf_context*perf_context;// 性能计数器}HsaKFDContext;这是一个典型的聚合根设计 ——HsaKFDContext本身只持有文件描述符和几个标志位,实际的资源状态通过六个指针分别指向各子系统的上下文结构。
每个子上下文都是按需分配的。以 FMM 上下文为例:
// fmm.cstructhsa_kfd_fmm_context{gpu_mem_t*gpu_mem;// 每个 GPU 节点的内存 apertureunsignedintgpu_mem_count;gpu_mem_t*first_gpu_mem;uint32_tall_gpu_id_array_size;uint32_t*all_gpu_id_array;void*dgpu_shared_aperture_base;// dGPU 共享 aperture 基址void*dgpu_shared_aperture_limit;svm_tsvm;// SVM 管理manageable_aperture_tcpuvm_aperture;// CPU VM aperturemanageable_aperture_tmem_handle_aperture;intdrm_render_fds[128];// DRM render 节点 fdstructamdgpu_device*amdgpu_handle[128];// AMDGPU 设备句柄};这意味着每个HsaKFDContext拥有自己独立的 GPU 内存视图、DRM 设备句柄和 aperture 地址空间 —— 不同上下文之间的内存分配互不可见。
头文件中的注释明确了隔离原则:
Multiple HsaKFDContext instances can coexist simultaneously, each maintaining its own independent set of resources. These contexts are fully isolated from one another and must not have their resources mixed. If resources need to be shared between contexts, they must be explicitly exported and imported using the appropriate APIs.
多个上下文可以同时存在,各自维护独立的资源集。上下文之间完全隔离,资源不得混用。如需跨上下文共享,必须通过显式的导出/导入 API。
4. Primary vs Secondary:两种上下文类型
4.1 Primary Context
Primary Context 就是原来的全局上下文,由hsaKmtOpenKFDCtx()创建,全进程唯一:
// openclose.c — hsaKmtOpenKFDCtx()if(hsakmt_kfd_open_count==0){fd=open(kfd_device_name,O_RDWR|O_CLOEXEC);hsakmt_kfdcontext_init_context(fd,&hsakmt_primary_kfd_ctx);hsakmt_kfd_open_count=1;*pCtx=&hsakmt_primary_kfd_ctx;}它的特点:
- 全局静态分配,生命周期与进程相同
- 引用计数管理,支持多次 Open/Close
- 支持 SVM、调试等全部功能
- DRM 设备初始化使用
amdgpu_device_initialize()(启用设备去重)
4.2 Secondary Context
Secondary Context 由hsaKmtOpenSecondaryKFDCtx()创建,可以有多个:
// openclose.c — hsaKmtOpenSecondaryKFDCtx()kfd_fd=open(kfd_device_name,O_RDWR|O_CLOEXEC);// 新的 fdstructkfd_ioctl_create_process_argsargs={};hsakmt_ioctl(kfd_fd,AMDKFD_IOC_CREATE_PROCESS,&args);// 内核侧创建新进程上下文new_ctx=calloc(1,sizeof(HsaKFDContext));hsakmt_kfdcontext_init_context(kfd_fd,new_ctx);new_ctx->hsakmt_is_primary_ctx=false;new_ctx->hsakmt_is_svm_api_supported=false;*pCtx=new_ctx;关键区别在三处:
① 独立的文件描述符。每个 Secondary Context 都会执行一次open("/dev/kfd"),获得自己的 fd。加上AMDKFD_IOC_CREATE_PROCESSioctl,内核侧会为这个 fd 建立独立的进程上下文。
② 独立的 DRM VM 空间。初始化 DRM 设备时使用amdgpu_device_initialize2(fd, false, ...),第二个参数false禁用设备去重,使得每个上下文获得自己的 DRM file handle 和独立的 GPU 虚拟地址空间:
// fmm.c — 初始化 DRM 设备if(ctx->hsakmt_is_primary_ctx)dev_init_ret=amdgpu_device_initialize(fd,...);elseif(hsakmt_fn_amdgpu_device_initialize2)dev_init_ret=hsakmt_fn_amdgpu_device_initialize2(fd,false,...);③ 功能限制。Secondary Context 不支持调试操作和 userptr 映射:
这个可能是暂时的,后面有可能被支持。实现与primay context一样的效果。
// debug.c — 调试接口拒绝 Secondary Contextif(!ctx->hsakmt_is_primary_ctx)returnHSAKMT_STATUS_NOT_SUPPORTED;// fmm.c — Secondary 禁用 userptrif(!ctx->hsakmt_is_primary_ctx)fmm_ctx->svm.userptr_for_paged_mem=false;4.3 生命周期对比
| 维度 | Primary Context | Secondary Context |
|---|---|---|
| 创建 | hsaKmtOpenKFDCtx() | hsaKmtOpenSecondaryKFDCtx() |
| 数量 | 全进程唯一 | 可创建多个 |
| 存储 | 全局静态变量 | calloc动态分配 |
| 引用计数 | 有(hsakmt_kfd_open_count) | 无 |
| fd 关闭 | 进程退出或 fork 后清理 | hsaKmtCloseSecondaryKFDCtx()显式关闭 |
| DRM 初始化 | amdgpu_device_initialize | amdgpu_device_initialize2(fd, false) |
| 调试支持 | 完整支持 | 不支持 |
| SVM / userptr | 支持 | 禁用 |
销毁 Secondary Context 时,资源清理更加彻底 —— 因为没有引用计数保护,关闭即意味着完全释放:
// openclose.c — hsaKmtCloseSecondaryKFDCtx()hsakmt_clear_events_page(ctx);hsakmt_destroy_counter_props(ctx);hsakmt_destroy_device_debugging_memory(ctx);hsakmt_fmm_clear_all_aperture(ctx);close(ctx->fd);// 关闭 fd → 内核侧释放进程上下文hsakmt_kfdcontext_clear_context(ctx);// 释放六个子上下文free(ctx);// 释放结构体本身5. 新旧 API 的衔接:Ctx 后缀模式
引入多上下文后,每个操作都需要知道"在哪个上下文上执行"。但 libhsakmt 已有大量不带上下文参数的遗留 API,不能破坏兼容性。
解决方案是双层 API 设计:
┌──────────────────────────────────┐ │ hsakmt.h(遗留 API,无上下文参数)│ ← 上层调用者使用 ├──────────────────────────────────┤ │ hsakmtctx.h(Ctx API,带上下文) │ ← 新场景使用 ├──────────────────────────────────┤ │ 具体实现(各模块 .c 文 │ └──────────────────────────────────┘Ctx API(定义在hsakmtctx.h)是真正的实现入口,每个函数的第一个参数都是HsaKFDContext *ctx:
// hsakmtctx.hHSAKMT_STATUS HSAKMTAPIhsaKmtAllocMemoryCtx(HsaKFDContext*ctx,HSAuint32 PreferredNode,HSAuint64 SizeInBytes,HsaMemFlags MemFlags,void**MemoryAddress);HSAKMT_STATUS HSAKMTAPIhsaKmtCreateQueueExtCtx(HsaKFDContext*ctx,HSAuint32 NodeId,HSA_QUEUE_TYPE Type,unsignedintQueuePercentage,...);遗留 API(定义在hsakmt.h)变成了简单的转发层,自动传入 Primary Context:
// queues.cHSAKMT_STATUShsaKmtCreateQueueExt(...){returnhsaKmtCreateQueueExtCtx(&hsakmt_primary_kfd_ctx,...);}HSAKMT_STATUShsaKmtDestroyQueue(HSA_QUEUEID QueueId){returnhsaKmtDestroyQueueCtx(&hsakmt_primary_kfd_ctx,QueueId);}// topology.cHSAKMT_STATUShsaKmtAcquireSystemProperties(HsaSystemProperties*SystemProperties){returnhsaKmtAcquireSystemPropertiesCtx(&hsakmt_primary_kfd_ctx,SystemProperties);}这个设计的优雅之处在于:
- 完全向后兼容—— 不修改任何现有调用者的代码,遗留 API 行为不变
- 无代码重复—— 实现逻辑只存在于
*Ctx()函数中,遗留 API 是零逻辑的转发 - 渐进式迁移—— 新代码可以直接使用 Ctx API,老代码保持不变,自然过渡
测试框架也通过一个巧妙的宏来在两套 API 之间切换:
// KFDTestUtil.hpp#ifdefHSAKMT_CTX#defineHSAKMT_CALL(func,ctx,...)func##Ctx(ctx,##__VA_ARGS__)#else#defineHSAKMT_CALL(func,ctx,...)func(__VA_ARGS__)#endif编译时定义HSAKMT_CTX,所有测试自动切换到 Ctx API 路径;再加上HSAKMT_SECONDARY_CTX,测试可以运行在 Secondary Context 上,验证隔离性。
6. 小结
| 要素 | 说明 |
|---|---|
| 原始模型 | 单进程单上下文,全局hsakmt_primary_kfd_ctx,所有线程共享 |
| 新需求 | 容器化多租户隔离、调试器隔离、DRM VM 空间隔离 |
| 核心数据结构 | HsaKFDContext聚合 fd + 六个子上下文指针(topology / queue / fmm / event / debug / perf) |
| Primary Context | 全局唯一,引用计数管理,功能完整 |
| Secondary Context | 动态分配,独立 fd +AMDKFD_IOC_CREATE_PROCESS,独立 VM 空间,部分功能受限 |
| API 兼容策略 | 双层设计 ——hsakmtctx.h(Ctx API)为实现层,hsakmt.h(遗留 API)为转发层 |
| 隔离粒度 | 内存 aperture / DRM 设备句柄 / GPU VM 空间 / 队列 / 事件,各上下文完全独立 |
KFD Context 的引入是 libhsakmt 架构的一次重要演进。它在保持向后兼容的前提下,将原本"进程 = 上下文"的刚性绑定松开,使得同一进程内可以划分出多个相互隔离的 GPU 资源域。
ROCm的虚拟化是一个很长的技术栈,涉及到guest/host的双系统的栈。如果大家对此感兴趣,可以关注下AMD工程师的相关其他软件库的提交。
下一篇 11.2 将深入分析六个子上下文的内部结构与按需初始化机制。文章审核中…