1. 核心结论
HIP 内存分配链路的核心,是看清楚用户态指针 API 如何逐层转换成 ROCclr memory object、ROCr HSA memory/SVM 操作,以及最终的 libhsakmt/KFD 资源管理动作。
主路径可以概括为:
cpp
hipMalloc / hipMallocManaged / hipHostMalloc
-> HIP Runtime API 层
-> ihipMalloc / ihipMallocManaged / ihipHostMalloc
-> hip::Device / amd::Context
-> amd::SvmBuffer / amd::Buffer / amd::Memory
-> roc::Memory / roc::Device
-> Hsa::memory_pool_allocate / Hsa::svm_attributes_set
-> ROCr Runtime
-> hsaKmtAllocMemory / hsaKmtMapMemoryToGPUNodes / hsaKmtSVMSetAttr
-> KFD ioctl
这条链路里有三个关键转换:
| 阶段 | 看到的对象 | 说明 |
|---|---|---|
| HIP API | void* |
用户只看到裸指针 |
| HIP/ROCclr | amd::Memory / amd::Buffer |
runtime 用内存对象保存大小、flags、context、device 等元数据 |
| ROCr/libhsakmt | HSA memory pool / SVM range / KFD allocation | 真正和 KFD 交互的底层资源 |
text
根据当前 hip::Device 找到 amd::Context
-> 创建 ROCclr memory object
-> 在 ROCm 后端创建 roc::Memory
-> 通过 ROCr/HSA 分配真实内存或建立 SVM range
-> 把返回的裸指针登记到 runtime 的内存对象表
HIP 内存相关 API 很多,本文先把它们按语义分组:
| 类型 | HIP API | 典型语义 |
|---|---|---|
| Device memory | hipMalloc / hipFree |
分配和释放 GPU device memory |
| Managed memory | hipMallocManaged / hipMemAdvise / hipMemPrefetchAsync |
分配和控制 HMM/SVM 管理内存 |
| Host memory | hipHostMalloc / hipHostRegister |
pinned host memory / registered memory |
| Copy path | hipMemcpy / hipMemcpyAsync |
不一定分配,但依赖内存对象查询 |
| Memory query | hipPointerGetAttributes / hipMemGetInfo |
查询 runtime 元数据或后端状态 |
| Memory pool | hipMemPool* / async malloc |
HIP runtime 侧的池化分配路径 |
本文主线先关注这些接口:
text
hipMalloc
hipFree
hipMallocManaged
hipMemAdvise
hipMemPrefetchAsync
hipHostMalloc / hipHostRegister
其中 hipMemAdvise 的完整属性翻译链放在了下一篇:HIP hipMemAdvise 到 libksamkt中的 hsaKmtSVMSetAttr 调用链分析;本文只把它放到内存分配体系里,说明它和 SVM range、libhsakmt 的关系。
2. 前置背景:Topology 发现之后有什么
内存分配发生在 HIP runtime 初始化之后。前一篇 topology 文档已经说明,初始化后 HIP 层具备这些对象:
text
hip::g_devices[N]
-> hip::Device
-> amd::Context
-> amd::Device
-> roc::Device
-> hsa_agent_t
-> HSA memory pools
-> CPU agent / NUMA 信息
-> P2P/link 信息
这一步对内存分配很重要,因为 hipMalloc 后续需要回答几个问题:
- 当前线程的 current device 是哪个
hip::Device? - 这个
hip::Device对应哪个amd::Context? - 这个 context 里的
amd::Device在 ROCm 后端下是哪一个roc::Device? - 这个
roc::Device有哪些 GPU memory pool、fine-grain pool、CPU agent、NUMA 信息? - 如果分配的是 managed/host memory,是否需要 HMM/SVM 属性初始化?
所以 topology 发现结束后,不只是"知道有几个 GPU",而是为后续内存分配准备好了:
text
当前 HIP device
-> ROCclr context
-> ROCclr device
-> ROCm backend device
-> HSA agent + memory pool
3. HIP Runtime API 入口模式
HIP 内存 API 入口一般分成两层:
text
公共 HIP API
-> HIP_INIT_API / 参数检查 / stream capture 检查
-> ihip* 内部实现
以 hipMalloc 为例:
cpp
hipError_t hipMalloc(void** ptr, size_t sizeBytes) {
HIP_INIT_API(hipMalloc, ptr, sizeBytes);
CHECK_STREAM_CAPTURE_SUPPORTED();
HIP_RETURN_DURATION(ihipMalloc(ptr, sizeBytes, 0), ReturnPtrValue(ptr));
}
HIP_INIT_API 的意义是:如果 runtime 还没有初始化,就先触发 hip::init(),完成 device/topology/context 初始化。真正分配逻辑在 ihipMalloc()。
hipFree 也类似:
cpp
hipError_t hipFree(void* ptr) {
HIP_INIT_API(hipFree, ptr);
CHECK_STREAM_CAPTURE_SUPPORTED();
HIP_RETURN(ihipFree(ptr));
}
所以调试时,公共 API 适合确认用户参数和初始化是否发生,ihip* 函数才是看业务逻辑的主要入口。
4. Device Memory 主路径:hipMalloc
hipMalloc 的核心实现是 ihipMalloc()。
简化后的逻辑是:
cpp
hipError_t ihipMalloc(void** ptr, size_t sizeBytes, unsigned int flags) {
bool useHostDevice = (flags & CL_MEM_SVM_FINE_GRAIN_BUFFER) != 0;
amd::Context* curDevContext = hip::getCurrentDevice()->asContext();
amd::Context* amdContext = useHostDevice ? hip::host_context : curDevContext;
const auto& dev_info = amdContext->devices()[0]->info();
hip::getCurrentDevice()->SetActiveStatus();
*ptr = amd::SvmBuffer::malloc(*amdContext, flags, sizeBytes,
dev_info.memBaseAddrAlign_,
useHostDevice ? curDevContext->svmDevices()[0] : nullptr);
amd::Memory* memObj = getMemoryObject(hip::getCurrentDevice(), *ptr, offset);
memObj->getUserData().deviceId = hip::getCurrentDevice()->deviceId();
return hipSuccess;
}
这里有几个关键点。
第一,hipMalloc 默认使用当前 device 的 per-device context:
text
hip::getCurrentDevice()
-> asContext()
-> 当前 device 对应的 amd::Context
第二,真正创建内存对象的入口是 ROCclr 的:
cpp
amd::SvmBuffer::malloc(...)
SvmBuffer::malloc() 再转到 context:
cpp
void* SvmBuffer::malloc(Context& context, cl_svm_mem_flags flags,
size_t size, size_t alignment,
const amd::Device* curDev, void* hostptr) {
void* ret = context.svmAlloc(size, alignment, flags, curDev, hostptr);
Add(ret_u, ret_u + size);
return ret;
}
第三,context.svmAlloc() 会根据 context 里的 device 选择后端实现。ROCm 后端是:
text
amd::Context::svmAlloc
-> amd::Device::svmAlloc 虚接口
-> roc::Device::svmAlloc
roc::Device::svmAlloc() 会创建一个隐藏的 ROCclr buffer:
cpp
mem = new (context) amd::Buffer(context, flags, size, svmPtrUsed);
mem->create(nullptr);
amd::MemObjMap::AddMemObj(mem->getSvmPtr(), mem);
return mem->getSvmPtr();
到这里,HIP 用户拿到的是 void*,但 runtime 内部已经有了:
text
用户指针 ptr
-> amd::MemObjMap
-> amd::Memory / amd::Buffer
-> roc::Memory
-> HSA allocation
4.1 roc::Memory 如何走到 HSA memory pool
amd::Buffer::create() 会为具体 device 创建后端 memory。ROCm 后端对应 roc::Memory::Buffer::create()。
对于普通 device local memory,最终会走到:
cpp
deviceMemory_ = dev().deviceLocalAlloc(size(), flags);
roc::Device::deviceLocalAlloc() 会选择一个 HSA memory pool:
cpp
const hsa_amd_memory_pool_t& pool =
flags.pseudo_fine_grain_ && gpu_ext_fine_grained_segment_.handle
? gpu_ext_fine_grained_segment_
: flags.atomics_ && gpu_fine_grained_segment_.handle
? gpu_fine_grained_segment_
: gpuvm_segment_;
然后调用 ROCr/HSA 扩展 API:
cpp
hsa_status_t stat = Hsa::memory_pool_allocate(pool, size, hsa_mem_flags, &ptr);
Hsa::memory_pool_allocate() 是 ROCclr 对动态加载 ROCr 符号的包装,实际符号是:
text
hsa_amd_memory_pool_allocate
因此 hipMalloc 的 device memory 主路径可以写成:
text
hipMalloc
-> ihipMalloc
-> amd::SvmBuffer::malloc
-> amd::Context::svmAlloc
-> roc::Device::svmAlloc
-> amd::Buffer::create
-> roc::Memory::Buffer::create
-> roc::Device::deviceLocalAlloc
-> Hsa::memory_pool_allocate
-> hsa_amd_memory_pool_allocate
-> ROCr Runtime::AllocateMemory
-> hsaKmtAllocMemory
-> hsaKmtMapMemoryToGPUNodes
-> KFD
5. amd::Memory 和内存对象登记
HIP API 对外暴露的是裸指针,但 runtime 必须知道这个指针对应的内存类型、大小、context、device、flags 等信息。这个信息由 amd::Memory 保存。
分配完成后,ROCm 后端会把内存对象登记到全局映射表:
cpp
amd::MemObjMap::AddMemObj(mem->getSvmPtr(), mem);
后续很多 API 都会通过用户指针反查:
cpp
amd::Memory* memObj = getMemoryObject(hip::getCurrentDevice(), ptr, offset);
这个设计解释了几个现象。
第一,hipFree(ptr) 不能直接释放裸指针,它必须先找到 amd::Memory:
cpp
ptr
-> getMemoryObject
-> amd::Memory
-> 判断是否来自 memory pool / SVM / external memory
-> 选择正确释放路径
第二,hipMemcpy 也需要查内存对象,因为它要判断 src/dst 是 host、device、managed、registered host memory,还是普通 CPU 指针。
第三,hipPointerGetAttributes、hipMemRangeGetAttribute、hipMemAdvise 这类 API 本质上都是围绕这个 runtime metadata 工作。
所以 amd::Memory 是 HIP 指针式 API 和 ROCclr 对象式内存模型之间的桥:
text
HIP 用户模型:void* ptr
ROCclr 内部模型:amd::Memory / roc::Memory
底层驱动模型:HSA allocation / SVM range / KFD BO
6. Managed Memory / SVM 路径
hipMallocManaged 不走普通 per-device context,而是使用 hip::host_context:
cpp
amd::Context& ctx = *hip::host_context;
const amd::Device& dev = *ctx.devices()[0];
*ptr = amd::SvmBuffer::malloc(ctx,
CL_MEM_SVM_FINE_GRAIN_BUFFER | CL_MEM_ALLOC_HOST_PTR,
size,
dev.info().memBaseAddrAlign_);
这里的关键点是:managed memory 需要表达"系统内存 / SVM / 多 GPU 可访问 / 可迁移"的语义,因此它使用包含所有可见 GPU 的 host_context,而不是某个 hip::Device 的单设备 context。
简化路径是:
text
hipMallocManaged
-> ihipMallocManaged
-> hip::host_context
-> amd::SvmBuffer::malloc
-> amd::Context::svmAlloc
-> roc::Device::svmAlloc
-> amd::Buffer / roc::Memory
-> HMM/SVM 初始化
-> Hsa::svm_attributes_set
-> hsa_amd_svm_attributes_set
-> ROCr Runtime::SetSvmAttrib
-> hsaKmtSVMSetAttr
-> AMDKFD_IOC_SVM
在 HMM 支持路径下,ROCm 后端会对系统内存做 SVM 初始化:
cpp
if (dev().info().hmmSupported_) {
deviceMemory_ = dev().reserveMemory(size(), amd::Os::pageSize());
dev().SvmAllocInit(deviceMemory_, size());
}
SvmAllocInit() 内部会调用:
text
roc::Device::SetSvmAttributesInt(..., first_alloc = true)
-> Hsa::svm_attributes_set
-> hsa_amd_svm_attributes_set
这一步不是普通的 VRAM allocation,而是给一段 SVM/HMM 地址范围建立初始访问属性,例如让 GPU agent 可以访问这段系统内存。
6.1 hipMemAdvise
hipMemAdvise 不负责分配新内存,但它会修改已有 SVM range 的属性。它的核心路径是:
text
hipMemAdvise / hipMemAdvise_v2
-> ihipMemAdvise
-> getMemoryObject
-> amd::Device::SetSvmAttributes
-> roc::Device::SetSvmAttributesInt
-> Hsa::svm_attributes_set
-> hsa_amd_svm_attributes_set
-> ROCr Runtime::SetSvmAttrib
-> hsaKmtSVMSetAttr
-> AMDKFD_IOC_SVM
属性会经历几次翻译:
text
hipMemoryAdvise
-> amd::MemoryAdvice
-> HSA_AMD_SVM_ATTRIB_*
-> HSA_SVM_ATTR_* / HSA_SVM_FLAG_*
-> KFD_IOCTL_SVM_ATTR_*
这条路径和 managed memory 关系很近,因为它操作的是同一类 SVM/HMM 地址范围。
6.2 hipMemPrefetchAsync
hipMemPrefetchAsync 也不分配新内存,但它会触发 managed/SVM range 向目标位置迁移或建立目标访问倾向。
简化路径是:
text
hipMemPrefetchAsync
-> ihipMemPrefetchAsync
-> getMemoryObject
-> 解析目标 location / CPU 或 GPU agent
-> ROCclr command 或 ROCr SVM prefetch/attribute 路径
-> hsa_amd_svm_prefetch_async 或 hsaKmtSVMSetAttr(prefetch loc)
-> KFD SVM range 迁移 / 预取
不同 ROCm 版本和 HMM 配置下,prefetch 可能表现为显式 prefetch API,也可能体现为 SVM 属性更新和迁移请求。调试时要同时关注 ROCr 的 hsa_amd_svm_prefetch_async 和 libhsakmt 的 hsaKmtSVMSetAttr。
7. Host Memory 路径:hipHostMalloc / hipHostRegister
hipHostMalloc 最终也是调用 ihipMalloc(),但会传入 fine-grain / atomics / NUMA / uncached 等 flags:
cpp
unsigned int ihipFlags = CL_MEM_SVM_FINE_GRAIN_BUFFER;
if (flags & hipHostMallocUncached) {
ihipFlags |= ROCCLR_MEM_HSA_UNCACHED;
}
if (flags == 0 || flags & hipHostMallocMapped || HIP_HOST_COHERENT) {
ihipFlags |= CL_MEM_SVM_ATOMICS;
}
if (flags & hipHostMallocNumaUser) {
ihipFlags |= CL_MEM_FOLLOW_USER_NUMA_POLICY;
}
hipError_t status = ihipMalloc(ptr, sizeBytes, ihipFlags);
因为带有 CL_MEM_SVM_FINE_GRAIN_BUFFER,ihipMalloc() 会选择:
cpp
amd::Context* amdContext = hip::host_context;
所以 hipHostMalloc 的核心路径是:
text
hipHostMalloc
-> ihipHostMalloc
-> ihipMalloc(..., CL_MEM_SVM_FINE_GRAIN_BUFFER | ...)
-> hip::host_context
-> amd::SvmBuffer::malloc
-> amd::Context::svmAlloc
-> roc::Device::svmAlloc
-> roc::Memory::Buffer::create
-> hostAlloc / HMM reserve / SVM init
-> ROCr/libhsakmt/KFD
hipHostRegister 的语义不同:它不是分配一块新 host memory,而是把用户已有的 host pointer 注册成 GPU 可访问。它通常会涉及:
text
用户已有 host pointer
-> HIP runtime 创建/登记 memory object
-> ROCclr/ROCm 后端 lock/register host memory
-> ROCr HSA memory_register / memory_lock / SVM attributes
-> libhsakmt pin/map userptr
-> KFD 建立 GPU 可访问映射
因此,hipHostMalloc 更像"runtime 分配 pinned/SVM host memory",hipHostRegister 更像"把已有 CPU 地址范围纳入 GPU 可访问管理"。
8. Free 路径:hipFree
hipFree 的关键不是直接释放 ptr,而是先通过 ptr 找到 runtime 内部的 amd::Memory:
cpp
hipError_t ihipFree(void* ptr) {
if (ptr == nullptr) {
return hipSuccess;
}
amd::Memory* memory_object = getMemoryObject(hip::getCurrentDevice(), ptr, offset);
if (memory_object != nullptr) {
auto device_id = memory_object->getUserData().deviceId;
if (!g_devices[device_id]->FreeMemory(memory_object, nullptr)) {
g_devices[device_id]->SyncAllStreams();
if (memory_object->getSvmPtr() == nullptr) {
amd::MemObjMap::RemoveMemObj(ptr);
memory_object->release();
} else {
amd::SvmBuffer::free(memory_object->getContext(), ptr);
}
}
return hipSuccess;
}
return hipErrorInvalidValue;
}
释放路径大致是:
text
hipFree
-> ihipFree
-> getMemoryObject
-> 判断是否属于 HIP memory pool
-> 必要时同步相关 stream
-> amd::SvmBuffer::free / amd::Memory::release
-> amd::Context::svmFree
-> roc::Device / roc::Memory free
-> hsa_amd_memory_pool_free / hsa_memory_free
-> hsaKmtFreeMemory
-> KFD
这里要注意,释放时使用的是 allocation 时记录在 memObj->getUserData().deviceId 里的 device,而不是简单使用当前线程 current device。这是为了避免用户切换 device 后释放指针时找错设备上下文。
9. Copy / Prefetch 为什么也依赖内存对象
hipMemcpy 本身不是分配接口,但它强依赖内存对象表。原因是 hipMemcpyDefault、managed memory、host registered memory、device pointer 都要通过 runtime metadata 判断。
典型逻辑是:
text
hipMemcpyAsync
-> 解析 stream
-> getMemoryObjectPairs(src, dst)
-> 判断 src/dst 分别是什么内存
-> 创建 ROCclr copy command
-> 提交到 hip::Stream / amd::HostQueue
-> ROCm 后端生成 SDMA 或 kernel/blit 命令
也就是说,分配阶段建立的 ptr -> amd::Memory 映射,会直接影响后续 copy 路径:
text
hipMalloc / hipHostMalloc / hipMallocManaged
-> MemObjMap 登记
-> hipMemcpy / hipFree / hipMemAdvise / hipPointerGetAttributes 反查
hipMemPrefetchAsync 更接近 managed memory 的控制路径:它先通过 getMemoryObject 找到 SVM allocation,再根据目标 location 决定是迁移到 GPU、迁移到 CPU,还是设置 NUMA/agent 相关属性。
10. 到 libhsakmt 的边界在哪里
这一节重点看 ROCr Runtime 进入 libhsakmt 的边界:上层已经完成 HIP/ROCclr 对象转换之后,底层会根据内存类型进入 allocation/map/free 或 SVM attribute 路径。
不同 HIP 语义到 libhsakmt 的落点大致如下:
| HIP 语义 | ROCclr/ROCr 落点 | libhsakmt/KFD 可能涉及 |
|---|---|---|
hipMalloc device memory |
hsa_amd_memory_pool_allocate |
hsaKmtAllocMemory、hsaKmtMapMemoryToGPUNodes |
hipFree |
hsa_amd_memory_pool_free / hsa_memory_free |
hsaKmtFreeMemory |
hipMallocManaged |
SVM/HMM allocation + hsa_amd_svm_attributes_set |
hsaKmtSVMSetAttr、AMDKFD_IOC_SVM |
hipHostMalloc |
fine-grain/SVM host allocation | hsaKmtAllocMemory、map/pin、SVM attributes |
hipHostRegister |
host memory register/lock | userptr pin/map、GPU mapping |
hipMemAdvise |
hsa_amd_svm_attributes_set |
hsaKmtSVMSetAttr |
hipMemPrefetchAsync |
SVM prefetch / prefetch location attribute | hsaKmtSVMSetAttr 或 prefetch ioctl 相关路径 |
以 device memory 为例,ROCr 的 hsa_amd_memory_pool_allocate() 最终调用:
cpp
core::Runtime::AllocateMemory
-> hsaKmtAllocMemory
-> hsaKmtMapMemoryToGPUNodes
libhsakmt 的 memory path 会进入:
c
libhsakmt/src/memory.c
-> hsaKmtAllocMemoryCtx
-> hsaKmtAllocMemoryAlignCtx
-> ioctl(KFD allocation/map memory path)
以 SVM 属性为例,ROCr 的 hsa_amd_svm_attributes_set() 最终调用:
cpp
core::Runtime::SetSvmAttrib
-> hsaKmtSVMSetAttr
-> libhsakmt/src/svm.c
-> hsakmt_ioctl(AMDKFD_IOC_SVM)
所以如果目标是观察"HIP 内存 API 什么时候到 libhsakmt",断点不应该只打在 HIP 层,还要打在 ROCr 和 libhsakmt 边界。
11. 调试建议
可以按层级设置断点。
HIP 层:
text
hipMalloc
hipFree
hipHostMalloc
hipMallocManaged
hipMemAdvise
hipMemPrefetchAsync
ihipMalloc
ihipFree
ihipHostMalloc
ihipMallocManaged
ihipMemAdvise
ihipMemPrefetchAsync
getMemoryObject
getMemoryObjectPairs
ROCclr 层:
text
amd::SvmBuffer::malloc
amd::SvmBuffer::free
amd::Context::svmAlloc
amd::Context::svmFree
amd::Buffer::create
amd::MemObjMap::AddMemObj
amd::MemObjMap::FindMemObj
roc::Device::svmAlloc
roc::Device::deviceLocalAlloc
roc::Device::hostAlloc
roc::Device::SetSvmAttributes
roc::Device::SetSvmAttributesInt
roc::Device::SvmAllocInit
roc::Memory::Buffer::create
ROCr 层:
text
hsa_amd_memory_pool_allocate
hsa_amd_memory_pool_free
hsa_amd_svm_attributes_set
hsa_amd_svm_attributes_get
hsa_amd_svm_prefetch_async
core::Runtime::AllocateMemory
core::Runtime::SetSvmAttrib
libhsakmt 层:
text
hsaKmtAllocMemory
hsaKmtAllocMemoryAlign
hsaKmtMapMemoryToGPU
hsaKmtMapMemoryToGPUNodes
hsaKmtFreeMemory
hsaKmtSVMSetAttr
hsaKmtSVMGetAttr
hsakmt_ioctl
如果只想看 hipMalloc 主路径,推荐第一轮断点是:
text
hipMalloc
-> ihipMalloc
-> amd::SvmBuffer::malloc
-> roc::Device::svmAlloc
-> roc::Memory::Buffer::create
-> roc::Device::deviceLocalAlloc
-> hsa_amd_memory_pool_allocate
-> core::Runtime::AllocateMemory
-> hsaKmtAllocMemory
-> hsaKmtMapMemoryToGPUNodes
如果只想看 managed/SVM 路径,推荐第一轮断点是:
text
hipMallocManaged
-> ihipMallocManaged
-> amd::SvmBuffer::malloc
-> roc::Device::svmAlloc
-> roc::Device::SvmAllocInit
-> roc::Device::SetSvmAttributesInt
-> hsa_amd_svm_attributes_set
-> core::Runtime::SetSvmAttrib
-> hsaKmtSVMSetAttr
-> hsakmt_ioctl(AMDKFD_IOC_SVM)
12. 总结
HIP 内存分配接口到 libhsakmt 的链路可以压缩成一张图:
text
HIP API
-> HIP runtime object model
-> ROCclr context/device/memory model
-> ROCm backend roc::Device / roc::Memory
-> ROCr HSA memory/SVM API
-> libhsakmt thunk
-> KFD ioctl
其中最重要的是两个对象桥接:
text
hip::Device
-> amd::Context
-> amd::Device / roc::Device
和:
text
void* ptr
-> amd::MemObjMap
-> amd::Memory / roc::Memory
前者决定"这次分配属于哪个 device/context",后者决定"后续拿着这个裸指针还能不能找回 runtime 内部的内存对象"。
所以,hipMalloc 到 libhsakmt 不是一条简单的函数转发链,而是一条分层资源建模链:
- HIP 层提供 CUDA-like 指针 API。
- ROCclr 层创建 context-scoped memory object。
- ROCm 后端选择 HSA memory pool 或 SVM/HMM 路径。
- ROCr Runtime 翻译成底层 allocation / map / SVM attribute 操作。
- libhsakmt 把这些操作提交给 KFD。
理解这条链路后,再看 hipFree、hipMemcpy、hipMemAdvise、hipMemPrefetchAsync 就会顺很多:它们都是围绕同一个 amd::Memory 元数据和同一组 ROCr/libhsakmt 底层能力展开。