内存管理

内存管理总述

在昇腾异构计算架构中,系统由主机(Host)和设备(Device)组成。Host和Device各自拥有独立的内存,Host内存是指AI处理器所在服务器的主机内存(即CPU内存),而Device内存则是指AI处理器自带的设备内存。

内存管理中要做好的两件事是:

  1. 可以访问内存: Runtime提供了一套内存管理API,使开发者能够高效便捷地编写应用程序中的内存管理代码。由于Host和Device的内存相互独立,Runtime提供了专门的接口来分别申请和释放Host内存及Device内存。例如,申请和释放Host内存的接口为aclrtMallocHost和aclrtFreeHost,而申请和释放Device内存的接口为aclrtMalloc和aclrtFree。
  2. 高效访问内存: 为了实现最佳的内存访问性能,需要将数据存储在对应的内存中,例如算子在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);
}