内存管理
内存管理总述
在昇腾异构计算架构中,系统由主机(Host)和设备(Device)组成。Host和Device各自拥有独立的内存,Host内存是指AI处理器所在服务器的主机内存(即CPU内存),而Device内存则是指AI处理器自带的设备内存。
内存管理中要做好的两件事是:
- 可以访问内存: Runtime提供了一套内存管理API,使开发者能够高效便捷地编写应用程序中的内存管理代码。由于Host和Device的内存相互独立,Runtime提供了专门的接口来分别申请和释放Host内存及Device内存。例如,申请和释放Host内存的接口为aclrtMallocHost和aclrtFreeHost,而申请和释放Device内存的接口为aclrtMalloc和aclrtFree。
- 高效访问内存: 为了实现最佳的内存访问性能,需要将数据存储在对应的内存中,例如算子在Device上执行过程中,访问Device上的数据性能要远高于访问Host侧的。Host与Device、Device与Device之间的数据搬运请参见数据复制。
Device内存使用
在昇腾异构计算编程中,典型的使用场景是:通过aclrtMallocHost接口申请Host内存,通过aclrtMalloc接口申请Device内存,通过内存复制接口将数据从Host拷贝到Device上,算子执行过程中使用Device内存进行计算并保存结果。
以下是一段简单的示例代码。在示例代码中,两个张量从Host内存被拷贝到Device内存,在Device侧完成计算,再将结果从Device内存拷贝到Host内存:
int main(void)
{
int32_t deviceId = 0;
int64_t N = 16;
const size_t bytes = static_cast<size_t>(N) * sizeof(float);
aclrtStream stream = nullptr;
// STEP 1: 初始化、Stream创建
aclInit(nullptr);
aclrtSetDevice(deviceId);
aclrtCreateStream(&stream);
// STEP 2: 申请Host内存
void* hostA = nullptr;
void* hostB = nullptr;
void* hostOut = nullptr;
aclrtMallocHost(&hostA, bytes);
aclrtMallocHost(&hostB, bytes);
aclrtMallocHost(&hostOut, bytes);
// 输入数据初始化
...
// STEP 3: 申请Device内存
void* deviceA = nullptr;
void* deviceB = nullptr;
void* deviceOut = nullptr;
// 第三个参数aclrtMemMallocPolicy表示申请内存时的内存页分配策略
// ACL_MEM_MALLOC_HUGE_FIRST 表示大页内存优先,其余定义参考API文档
aclrtMalloc(&deviceA, bytes, ACL_MEM_MALLOC_HUGE_FIRST);
aclrtMalloc(&deviceB, bytes, ACL_MEM_MALLOC_HUGE_FIRST);
aclrtMalloc(&deviceOut, bytes, ACL_MEM_MALLOC_HUGE_FIRST);
// STEP 4: 将输入数据从Host内存传输到Device内存
aclrtMemcpy(deviceA, bytes, hostA, bytes, ACL_MEMCPY_HOST_TO_DEVICE);
aclrtMemcpy(deviceB, bytes, hostB, bytes, ACL_MEMCPY_HOST_TO_DEVICE);
// STEP 5: 执行计算操作,比如aclnnAdd,将计算结果保存在Device内存
// ...
// STEP 6: 将计算结果从Device内存传输到Host内存
aclrtMemcpy(hostOut, bytes, deviceOut, bytes, ACL_MEMCPY_DEVICE_TO_HOST);
// STEP 7: 在Host侧处理计算结果
// ...
// STEP 8: 资源清理
// STEP 8.1: 释放Host和Device内存
if (deviceA) (void)aclrtFree(deviceA);
if (deviceB) (void)aclrtFree(deviceB);
if (deviceOut) (void)aclrtFree(deviceOut);
if (hostA) (void)aclrtFreeHost(hostA);
if (hostB) (void)aclrtFreeHost(hostB);
if (hostOut) (void)aclrtFreeHost(hostOut);
// STEP 8.2: 清理设备和进程资源
if (stream) (void)aclrtDestroyStream(stream);
(void)aclrtResetDevice(deviceId);
(void)aclFinalize();
return 0;
}
Device内存页策略
调用aclrtMalloc、aclrtMallocWithCfg等接口申请Device内存时,需要通过aclrtMemMallocPolicy参数指定内存分配策略。该策略主要解决两类问题:一是使用普通页还是大页,二是在性能优先、容量利用率和申请失败处理之间如何取舍。
普通页和大页的区别如下:
- 普通页:也称小页,页表粒度通常按4K处理,适合小块、短生命周期或数量较多的内存申请。普通页可以降低大页对齐带来的空间浪费,内存碎片压力也更容易控制。
- 2M大页:内存申请粒度为2M,不足2M的倍数会向上按2M对齐。大页可以减少页表项数量,扩大TLB覆盖范围,适合较大、长期驻留、访问频繁的输入、输出、workspace等Device内存。
- 1G大页:内存申请粒度为1G,仅部分产品支持。与2M大页相比,1G大页能进一步减少页表项数量,例如1G内存使用2M大页需要512个页表项,而使用1G大页只需要1个页表项。该策略适合超大块、长期驻留并且对访问性能敏感的内存,但会带来更明显的容量占用和对齐浪费。
aclrtMemMallocPolicy的设计理念是把“页大小”“兜底策略”和“访问拓扑”拆开表达:
- 优先还是强制:ACL_MEM_MALLOC_HUGE_FIRST表示大页优先,申请不到大页时可退回普通页;ACL_MEM_MALLOC_HUGE_ONLY表示只申请大页,申请不到则报错;ACL_MEM_MALLOC_NORMAL_ONLY表示只申请普通页。
- 默认性能与容量平衡:ACL_MEM_MALLOC_HUGE_FIRST适合多数大块Device内存。申请大小小于等于1M时,即使配置该策略,Runtime也会申请普通页;申请大小大于1M时,Runtime优先申请2M大页,大页不足时退回普通页。这种设计避免小内存过度占用大页,同时尽量让大块数据获得更好的TLB命中表现。
- 跨Device传输:带P2P后缀的策略用于两个Device之间内存复制场景,例如ACL_MEM_MALLOC_HUGE_FIRST_P2P、ACL_MEM_MALLOC_HUGE_ONLY_P2P、ACL_MEM_MALLOC_NORMAL_ONLY_P2P。若内存主要用于Device间复制,应选择P2P策略,使内存属性与访问路径匹配。
- 大页粒度选择:ACL_MEM_MALLOC_HUGE_ONLY使用2M大页;ACL_MEM_MALLOC_HUGE1G_ONLY使用1G大页。1G大页更关注大容量连续访问性能,2M大页在性能和容量占用之间更均衡。
- 带宽类型提示:ACL_MEM_TYPE_LOW_BAND_WIDTH、ACL_MEM_TYPE_HIGH_BAND_WIDTH可与部分页策略按位或组合,但系统最终会根据硬件支持情况选择合适的物理内存类型。未明确需要时,一般无需单独配置。
使用建议如下:
- 大多数普通计算输入、输出和workspace,优先使用ACL_MEM_MALLOC_HUGE_FIRST。
- 大量小块内存、临时内存或需要降低对齐浪费时,使用ACL_MEM_MALLOC_NORMAL_ONLY。
- 对大页性能有强依赖,且希望资源不足时尽早暴露问题时,使用ACL_MEM_MALLOC_HUGE_ONLY或ACL_MEM_MALLOC_HUGE1G_ONLY。
- 跨Device复制场景下,优先选用带P2P后缀的策略。
- 频繁调用aclrtMalloc/aclrtFree会影响性能。对于固定大小或可预估大小的内存,建议在初始化阶段预分配并在业务侧复用。
Host锁页内存使用
在CANN编程框架中,Host内存可以是Pageable内存,也可以是Page-Locked内存(也称锁页内存或Pinned内存):
-
Pageable内存,由操作系统统一管理。开发者可使用malloc、mmap等传统接口申请内存,使用free、munmap等接口释放内存。当内存压力较大时,Pageable内存会被换出到后备存储提供的交换空间中。当Pageable内存中的数据被传输到Device时,数据首先会被复制到缓冲区,然后通过DMA(Direct Memory Access)通道传输到Device。
-
Page-Locked内存,即锁页内存。开发者需要用Runtime提供的API进行锁页内存的申请和释放,例如aclrtMallocHost、aclrtFreeHost等接口。对于锁页内存,虚拟页与物理页的映射关系固定,在其生命周期内不会被换出至交换空间。当锁页内存中的数据被传输至Device时,直接通过DMA通道传输,无需经过缓冲区,传输性能更优。
在Runtime中,使用锁页内存的好处如下:
- 设备可以直接通过DMA访问主机内存,无需经过缓冲区,可以提供更好的传输性能;
- 数据搬运过程无需CPU参与,可以实现数据的异步传输;
- 数据的异步传输,使传输过程和计算过程可以相互掩盖,减少传输+计算的整体时长。
锁页内存可直接通过aclrtMallocHost接口申请,示例代码如下。如果需要在申请时指定内存的其他配置,例如自定义模块ID、配置VA(virtual address)一致性等,也可以使用aclrtMallocHostWithCfg接口申请。
// 资源初始化
......
// 申请锁页内存
void *hostPtr = NULL;
aclrtMallocHost(&hostPtr, size);
// 申请Device内存
void *devicePtr = NULL;
aclrtMalloc(&devicePtr, size, ACL_MEM_MALLOC_NORMAL_ONLY);
// 内存初始化
......
// 异步H2D
aclrtMemcpyAsync(devicePtr, size, hostPtr, size, ACL_MEMCPY_HOST_TO_DEVICE, stream);
// 等待异步拷贝完成
aclrtSynchronizeStream(stream);
// 资源释放
if (devicePtr) (void)aclrtFree(devicePtr);
if (hostPtr) (void)aclrtFreeHost(hostPtr);
......
若使用malloc/mmap等内存管理接口申请Pageable内存,当前Runtime提供了aclrtHostRegisterV2接口,用于将Pageable内存转换为锁页内存,供Device访问。请注意,当OS内核版本为5.10或更低时,此方法会导致异常,此时应通过aclrtMallocHost接口申请锁页内存。
// 申请Pageable内存
void *hostPtr = malloc(size);
// 注册为锁页内存(ACL_HOST_REG_PINNED),并映射到Device(ACL_HOST_REG_MAPPED)
aclrtHostRegisterV2(hostPtr, size, ACL_HOST_REG_PINNED|ACL_HOST_REG_MAPPED);
// 获取Device地址
void *devicePtr = NULL;
aclrtHostGetDevicePointer(hostPtr, &devicePtr, 0);
// 任务通过Device地址访问对应内存
...
// 资源释放
aclrtHostUnregister(hostPtr);
free(hostPtr);
若涉及Host内存的VA(virtual address)一致性:
- Host内存可以通过aclrtHostRegister接口注册到Device,根据Host指针映射得到Device指针,供Device使用;默认情况下,对于同一块Host内存,Host和Device看到的虚拟地址是不同的;
- 如果需要Host和Device的虚拟地址保持一致,可以使用aclrtMallocHostWithCfg接口申请锁页内存,并在aclrtMallocConfig参数中指定ACL_RT_MEM_ATTR_VA_FLAG属性,后续再注册获取到的Device虚拟地址即可与Host虚拟地址一致。
虚拟内存管理
Runtime提供了一套虚拟内存管理API,开发者可以更加精细化地管理内存。例如,可以提前申请大块连续的虚拟内存,以简化后续管理;也可以在使用过程中按需申请物理内存,并与虚拟内存建立地址映射,既能精细化管理内存,也能最大化利用物理内存。此外,还可以在进程之间利用虚拟内存管理中的handle实现物理内存共享。
以下是一个虚拟内存的基本使用示例,申请好物理内存以及虚拟内存后,通过aclrtMapMem接口进行映射,即可通过虚拟地址访问对应的数据:
void SampleTest()
{
// 1. 查询内存申请粒度,并对size做对齐处理
// 设置要申请的物理内存属性
aclrtPhysicalMemProp prop = {};
prop.handleType = ACL_MEM_HANDLE_TYPE_NONE; // handle类型,当前仅支持 ACL_MEM_HANDLE_TYPE_NONE
prop.allocationType = ACL_MEM_ALLOCATION_TYPE_PINNED; // 内存分配类型,当前仅支持分配锁页内存
prop.memAttr = ACL_HBM_MEM_NORMAL; // 内存属性,这里申请HBM内存
prop.location.type = ACL_MEM_LOCATION_TYPE_DEVICE; // 内存所在位置,这里申请Device内存,且deviceId为0
prop.location.id = 0;
// 查询申请最小内存粒度
size_t granularity = 0UL;
aclrtMemGetAllocationGranularity(&prop, ACL_RT_MEM_ALLOC_GRANULARITY_MINIMUM, &granularity);
// 根据查询到的内存申请粒度做内存对齐,以便节约内存
const size_t dataSize = 1024 * sizeof(float);
size_t alignedSize = ((dataSize + granularity - 1U) / granularity) * granularity;
// 2. 申请物理内存
// 获取存放物理内存信息的handle,用于后续申请虚拟内存使用,需要注意的是handle并不能直接当虚拟地址访问
aclrtDrvMemHandle handle = nullptr;
aclrtMallocPhysical(&handle, alignedSize, &prop, 0); // 最后一个flag参数当前默认填0
// 3. 申请预留虚拟内存
void *virPtr;
// virPtr即为申请预留的虚拟地址指针,alignedSize为申请预留的内存大小
aclrtReserveMemAddress(&virPtr, alignedSize, 0, nullptr, 0);
// 4. 映射虚拟内存到物理内存上
// 将申请物理内存时获取的handle与申请预留的虚拟地址空间关联起来
// 需要注意的是,映射的内存大小alignedSize要与申请物理内存时设置的size大小一致
aclrtMapMem(virPtr, alignedSize, 0, handle, 0);
// 5. 设置虚拟内存访问权限
aclrtMemAccessDesc accessDesc = {};
accessDesc.flags = ACL_RT_MEM_ACCESS_FLAGS_READWRITE;
accessDesc.location.type = ACL_MEM_LOCATION_TYPE_DEVICE;
accessDesc.location.id = 0;
aclrtMemSetAccess(virPtr, alignedSize, &accessDesc, 1);
// 6. 内存访问
// 这里以Device->Host的内存拷贝为例,展示正常访问映射后的内存地址
int *hostPtr;
aclrtMallocHost(reinterpret_cast<void**>(&hostPtr), alignedSize);
aclrtMemcpy(hostPtr, alignedSize, virPtr, alignedSize, ACL_MEMCPY_DEVICE_TO_HOST);
aclrtFreeHost(hostPtr);
// 7. 取消虚拟内存与物理内存的映射
aclrtUnmapMem(virPtr);
// 8. 释放虚拟内存
aclrtReleaseMemAddress(virPtr);
// 9. 释放物理内存
aclrtFreePhysical(handle);
}