CANN Runtime 流序内存池(Stream-Ordered Memory Pool)实战指南:创建、属性配置与异步分配
【免费下载链接】runtime本项目提供CANN运行时组件和维测功能组件。项目地址: https://gitcode.com/cann/runtime
本文围绕 CANN Runtime 中流序内存池(Stream-Ordered Memory Pool,即 SOMA 机制)这一高级内存管理能力展开,讲解内存池的创建、属性配置、异步申请/释放以及与常规aclrtMalloc/aclrtFree的适用场景对比。读者将掌握aclrtMemPoolCreate、aclrtMemPoolSetAttr/GetAttr、aclrtMemPoolMallocAsync、aclrtMemPoolFreeAsync和aclrtMemPoolTrimTo一套完整的 API 用法,理解内存池阈值、保留内存与已用内存等关键指标的真实含义,并能在算子高频下发的场景中直接落地使用。
背景:为什么需要流序内存池
在常规编程模型下,设备内存管理依赖同步接口aclrtMalloc与aclrtFree。典型的使用方式是:申请内存 → 异步拷贝数据到 Device 侧 → 下发算子任务 →同步 Stream 确认内存使用完毕→ 释放内存。示例代码如下:
#include "acl/acl_rt.h" #include "acl/acl.h" int main() { typedef struct { ...... } ArgsInfo; void *ptr0 = nullptr; aclrtStream stream1; // 申请内存 aclrtMalloc(&ptr0, sizeof(ArgsInfo), ACL_MEM_MALLOC_HUGE_FIRST); ...... // 配置任务下发 ArgsInfo usrArgs; // 拷贝信息到device侧申请内存 error = aclrtMemcpyAsync(ptr0, sizeof(ArgsInfo), (void *)&usrArgs, sizeof(ArgsInfo), ACL_MEMCPY_HOST_TO_DEVICE, stream1); // 下发任务 uint32_t blockDim = 32; aclrtLaunchKernelV2(funcHandle, blockDim, (void *)&usrArgs, sizeof(ArgsInfo), nullptr, stream1); // 流同步以同步释放申请内存 aclrtSynchronizeStream(stream1); // 释放内存,释放前需要流同步 aclrtFree(ptr0); ...... // 流同步 aclrtSynchronizeStream(stream1); return 0; }如上代码所示,aclrtFree之前必须显式执行aclrtSynchronizeStream来保证内存不再被异步任务使用,否则可能产生数据竞争或非法内存访问。在算子下发数量少、内存调整频率低的场景下,这种模式是可接受的;但面对大量算子下发与高频内存申请/释放的负载时,会暴露两个突出问题:
- 同步瓶颈:任务下发过程中一旦涉及内存分配或释放的调整,就不得不引入 Stream 同步,打断异步流水,拖累整体执行效率。
- 累积延迟:内存申请与释放本身耗时,频繁操作会累积出可观的额外延迟,进一步压缩性能空间。
流序内存池(Stream-Ordered Memory Pool)正是为打破这一瓶颈而设计:它把内存的分配与释放动作融入 Stream 的调度序列,由 Stream 自身的有序执行机制保证内存操作安全,无需显式同步即可进行内存管理;同时 Runtime 提供内存复用能力,能够全面支撑复杂的内存管理场景。
本功能在仓库中的落点见 example/3_memory_advanced/memory_pool/README_en.md(关联文档)、完整 API 手册见 docs/zh/api_ref/11-10_ordered_stream_memory_allocation.md。
流序内存池核心模型:一次申请/释放的完整生命周期
流序内存池的核心思路是将内存管理任务化。从源码实现看,其运行时链路分为三层:
- ACL 封装层:
src/acl/aclrt_impl/memory.cpp中的aclrtMemPoolCreateImpl等函数完成参数校验、类型映射与运行时调用; - Runtime API 层:
src/runtime/api/api_c_soma.cc中的rtMemPoolCreate等 C 接口经ApiSoma分发到ApiImplSoma(见 src/runtime/api/impl/api_impl_soma.cc); - SOMA 内核层:真正的内存管理逻辑位于 src/runtime/feature/soma/soma.cc,通过
SomaApi::AllocFromMemPool/FreeToMemPool与SegmentManager段管理器协同,并借助 AICore 侧的SomaMemMng内核(以AicpuOpType::MALLOC/FREE任务形式下发到指定 Stream)完成异步语义。
以异步申请为例,ApiImplSoma::MemPoolMallocAsync(api_impl_soma.cc)的执行路径可以概括为:
- 校验
devPtr、memPoolId、stm及当前 Context/Stream 匹配关系; - 将请求大小按
DEVICE_POOL_MIN_BLOCK_SIZE对齐(源码中(size + alignMask) & ~alignMask),并做溢出检查; - 通过
SomaApi::QueryMemPool定位内存池,失败时返回RT_ERROR_MEM_POOL_NULL; - 调用
SomaApi::AllocFromMemPool从池中切出内存块,得到虚拟地址va; - 通过
SomaAicpuKernelLaunch("SomaMemMng", ...)在指定 Stream 上下发 MALLOC 管理任务,将该分配动作纳入 Stream 保序序列;若下发失败则回滚归还。
对应的MemPoolFreeAsync则先通过SomaApi::InMemPoolRegion判断指针是否属于 SOMA 池:若是,则归还池中并下发 FREE 任务;若不是(例如指针来自同步分配),则退化为注册 Host 回调(LaunchHostFunc+MemPoolFreeAsyncCallback)执行DevFreeStatic。从源码结构看,这种"池内异步归还 + 池外回调兜底"的双路径设计,正是为了兼容不同来源指针的释放语义。
异步申请/释放与流同步的关系
这是理解流序内存池的关键点:aclrtMemPoolFreeAsync只是把内存归还给内存池,并不释放物理内存(物理释放由阈值控制,见下文),因此归还动作天然安全——物理块仍归属池内,不会被其他实体占用。而"内存已归还、可被后续任务复用"这一事实,通过下发到同一 Stream 的管理任务天然保序:后续任务在同一 Stream 中执行时,必然晚于归还任务,从而安全地复用该内存块。这正是"无需显式同步即可管理内存"的底层保证。
对照传统方式的对比代码如下:
#include "acl/acl_rt.h" #include "acl/acl.h" int main() { typedef struct { ...... } ArgsInfo; void *ptr0 = nullptr; aclrtStream stream1; // 异步申请内存,testReusePool为用户创建的内存池 aclrtMemPoolMallocAsync(&ptr0, sizeof(ArgsInfo), testReusePool, stream1); ...... // 配置任务下发 ArgsInfo usrArgs; // 拷贝信息到device侧申请内存 error = aclrtMemcpyAsync(ptr0, sizeof(ArgsInfo), (void *)&usrArgs, sizeof(ArgsInfo), ACL_MEMCPY_HOST_TO_DEVICE, stream1); // 下发任务 uint32_t blockDim = 32; aclrtLaunchKernelV2(funcHandle, blockDim, (void *)&usrArgs, sizeof(ArgsInfo), nullptr, stream1); // 异步释放内存,无需进行流同步 aclrtMemPoolFreeAsync(ptr0, stream1); ...... // 流同步 aclrtSynchronizeStream(stream1); return 0; }可以看到,两次异步操作之间不再需要任何aclrtSynchronizeStream,编程模型大幅简化。
内存池创建与销毁:aclrtMemPoolCreate / aclrtMemPoolDestroy
接口签名与功能
aclError aclrtMemPoolCreate(aclrtMemPool *memPool, const aclrtMemPoolProps *poolProps) aclError aclrtMemPoolDestroy(const aclrtMemPool memPool)aclrtMemPoolCreate:创建内存池。memPool为输出参数,返回内存池实例句柄;poolProps为输入参数,描述内存池配置。aclrtMemPoolDestroy:销毁通过aclrtMemPoolCreate创建的内存池。memPool为输入参数。
两个接口均返回 0 表示成功,其他值表示失败(错误码参见aclError)。
须知:本组接口均为试验特性,后续版本可能变更,不支持应用于生产环境。
产品支持情况
从 docs/zh/api_ref/11-10_ordered_stream_memory_allocation.md 的产品支持矩阵看,当前仅Atlas A2 训练系列产品/Atlas A2 推理系列产品支持该功能,其余产品(如 Atlas A3 系列、Atlas 训练系列、Ascend 950PR/950DT、IPV350 等)均不支持,使用前务必确认目标平台。
内存池属性结构 aclrtMemPoolProps
aclrtMemPoolProps定义于 include/external/acl/acl_rt_memory_soma.h:
typedef struct { aclrtMemAllocationType allocType; aclrtMemHandleType handleType; aclrtMemLocation location; size_t maxSize; unsigned char reserved[32]; } aclrtMemPoolProps;各字段含义如下:
| 字段 | 说明 |
|---|---|
allocType | 内存分配类型,当前仅支持ACL_MEM_ALLOCATION_TYPE_PINNED(0)。 |
handleType | 内存句柄类型,当前支持ACL_MEM_HANDLE_TYPE_NONE(0,默认)与ACL_MEM_HANDLE_TYPE_POSIX(2)。 |
location | 内存位置,为aclrtMemLocation结构体,包含id(设备逻辑 ID)与type(内存位置类型)。 |
maxSize | 内存池最大容量,单位 Byte,即创建池时一次性预留的物理内存上限。 |
reserved | 保留字段,必须全 0。 |
相关枚举定义在 include/external/acl/acl_rt.h:
typedef enum aclrtMemLocationType { ACL_MEM_LOCATION_TYPE_HOST = 0, /**< reserved enum, current version not support */ ACL_MEM_LOCATION_TYPE_DEVICE, ACL_MEM_LOCATION_TYPE_UNREGISTERED, ACL_MEM_LOCATION_TYPE_MANAGED, ACL_MEM_LOCATION_TYPE_HOST_NUMA = 4, /*alloc host memeory via NUMA ID */ } aclrtMemLocationType; typedef struct aclrtMemLocation { uint32_t id; aclrtMemLocationType type; } aclrtMemLocation; typedef enum aclrtMemAllocationType { ACL_MEM_ALLOCATION_TYPE_PINNED = 0, } aclrtMemAllocationType; typedef enum aclrtMemHandleType { ACL_MEM_HANDLE_TYPE_NONE = 0, ACL_MEM_HANDLE_TYPE_POSIX = 2, } aclrtMemHandleType;参数校验规则(源码级)
ACL 封装层aclrtMemPoolCreateImpl(src/acl/aclrt_impl/memory.cpp)在创建前执行如下强校验:
allocType必须为ACL_MEM_ALLOCATION_TYPE_PINNED,否则返回ACL_ERROR_INVALID_PARAM;location.type必须为ACL_MEM_LOCATION_TYPE_DEVICE(即 1),即当前仅支持设备内存池;reserved字段必须全零,否则拒绝创建。
运行时层SomaApi::StreamMemPoolCreate(src/runtime/feature/soma/soma.cc)会进一步:
- 将用户 Device ID 转换为物理 Device ID(
ChgUserDevIdToDeviceId); - 通过
MemGetInfoEx查询 HBM 空闲/总大小; - 通过
GetAllocationGranularity获取设备内存分配粒度(推荐粒度,失败时回退到DEVICE_POOL_ALIGN_SIZE),并对maxSize做对齐与上限校验(对齐后不得超过设备可用内存,否则报错提示); - 调用驱动
StreamMemPoolCreate在驱动侧建立池并返回起始虚拟地址outVa,随后PoolRegistry::InitializeMemPool初始化段管理器,最后RegisterMemPool注册到全局池注册表中。
一个完整的创建示例
结合 docs/zh/api_ref/11-10_ordered_stream_memory_allocation.md 的示例代码,一个完整的内存池创建流程如下:
uint32_t devid = 0; aclInit(NULL); aclrtSetDevice(devid); // 创建Context和Stream aclrtContext context; aclrtStream stream1; aclrtCreateContext(&context, 0); aclrtCreateStream(&stream1); // 设置内存池属性 aclrtMemLocation testLoc = { 0, // id ACL_MEM_LOCATION_TYPE_DEVICE // type }; aclrtMemPoolProps testProp = { ACL_MEM_ALLOCATION_TYPE_PINNED, // allocType ACL_MEM_HANDLE_TYPE_NONE, // handleType testLoc, // location 14UL << 30, // maxSize = 14GB, 内存池大小为14GB {0} // reserved }; // 创建内存池 aclrtMemPool testReusePool; auto ret = aclrtMemPoolCreate(&testReusePool, &testProp); if (ret != ACL_SUCCESS) { fprintf(stderr, "Failed to create memory pool\n"); return -1; }创建完成后,内存池便以独立于普通堆的实体存在:池内物理内存由驱动在创建时一次性预留,后续分配/释放均在该池内进行,从源码结构看这正是"避免频繁向驱动申请/释放物理内存"的性能前提。
内存池属性配置与关键指标:aclrtMemPoolSetAttr / aclrtMemPoolGetAttr
接口签名与功能
aclError aclrtMemPoolSetAttr(aclrtMemPool memPool, aclrtMemPoolAttr attr, void *value) aclError aclrtMemPoolGetAttr(aclrtMemPool memPool, aclrtMemPoolAttr attr, void *value)aclrtMemPoolSetAttr:设置属性值,多次对同一属性设置以最后一次为准。aclrtMemPoolGetAttr:获取属性值;若从未设置,则返回该属性的默认值。
属性枚举 aclrtMemPoolAttr 全解
aclrtMemPoolAttr定义于 include/external/acl/acl_rt_memory_soma.h,取值如下:
typedef enum aclrtMemPoolAttr { ACL_RT_MEM_POOL_REUSE_FOLLOW_EVENT_DEPENDENCIES = 0x1, ACL_RT_MEM_POOL_REUSE_ALLOW_OPPORTUNISTIC = 0x2, ACL_RT_MEM_POOL_REUSE_ALLOW_INTERNAL_DEPENDENCIES = 0x3, ACL_RT_MEM_POOL_ATTR_RELEASE_THRESHOLD = 0x4, ACL_RT_MEM_POOL_ATTR_RESERVED_MEM_CURRENT = 0x5, ACL_RT_MEM_POOL_ATTR_RESERVED_MEM_HIGH = 0x6, ACL_RT_MEM_POOL_ATTR_USED_MEM_CURRENT = 0x7, ACL_RT_MEM_POOL_ATTR_USED_MEM_HIGH = 0x8 } aclrtMemPoolAttr;各属性的完整语义(依据 docs/zh/api_ref/25-02_Enumerations.md 的aclrtMemPoolAttr小节整理):
| 属性 | 类型 | 读写 | 语义 |
|---|---|---|---|
ACL_RT_MEM_POOL_REUSE_FOLLOW_EVENT_DEPENDENCIES | uint32_t | 写 | 事件依赖内存复用开关。执行某 Stream 任务时,系统查找与该 Stream 通过 Event 关联的其他 Stream,复用其中任务已归还的内存。适用于以 Event 实现 Stream 间同步的应用程序。1=启用,0=关闭。 |
ACL_RT_MEM_POOL_REUSE_ALLOW_OPPORTUNISTIC | uint32_t | 写 | 机会主义内存复用开关。执行某 Stream 任务时,系统检索池中可复用内存,但不保证一定成功;复用失败时程序报错停止。1=启用,0=关闭。 |
ACL_RT_MEM_POOL_REUSE_ALLOW_INTERNAL_DEPENDENCIES | uint32_t | 写 | 隐式依赖内存复用开关。若待复用内存曾被其他无任务依赖关系的 Stream 使用,系统自动在相关 Stream 间增加 Event 同步等待逻辑,确保前序访问结束,实现安全复用。1=启用,0=关闭。 |
ACL_RT_MEM_POOL_ATTR_RELEASE_THRESHOLD | uint64_t | 读写 | 释放空闲物理内存时池中要保留的内存大小阈值,单位 Byte,默认值为 0。当池内空闲物理内存超过该阈值时,在下一次 Stream 同步(如aclrtSynchronizeStream)时系统尝试真正释放超出的空闲内存。 |
ACL_RT_MEM_POOL_ATTR_RESERVED_MEM_CURRENT | uint64_t | 只读 | 内存池中当前被申请(Reserved)的内存总量。 |
ACL_RT_MEM_POOL_ATTR_RESERVED_MEM_HIGH | uint64_t | 读写 | 池中当前被申请内存总量的历史峰值。设置时属性值只能为 0(即清零峰值)。 |
ACL_RT_MEM_POOL_ATTR_USED_MEM_CURRENT | uint64_t | 只读 | 内存池中实际正在使用(Used)的内存总量。 |
ACL_RT_MEM_POOL_ATTR_USED_MEM_HIGH | uint64_t | 读写 | 池中实际正在使用内存总量的历史峰值。设置时属性值只能为 0(即清零峰值)。 |
关键指标区分:Reserved vs Used、Current vs High
关联文档强调需要理解"内存池阈值、保留内存与已用内存等关键指标",这里给出精确区分:
- Reserved(保留/被申请):表示已从池中划出、被应用"占用"的内存总量。
aclrtMemPoolMallocAsync成功一次,RESERVED_MEM_CURRENT就增加对应对齐后大小;aclrtMemPoolFreeAsync归还一次则减少。 - Used(已用):表示当前实际被使用的内存。它与 Reserved 的差异主要出现在池外回调路径:从源码实现看,当
aclrtMemPoolFreeAsync释放的指针不属于 SOMA 池区域时,会注册 Host 回调真正释放物理内存,此时 Reserved 与 Used 的统计口径就会产生分化。 - Current(当前值):实时快照,用于监控。
- High(历史峰值):自创建或清零以来达到的最大值,用于评估内存池容量规划是否合理,可通过写入 0 重置。
从驱动侧实现(src/runtime/driver/npu_driver_standard_soc.cc 的ConvertMemPoolAttr)可以看到,RELEASE_THRESHOLD、RESERVED_MEM_CURRENT/HIGH、USED_MEM_CURRENT/HIGH会原样映射为驱动侧MEM_POOL_ATTR_*枚举,即这些指标由驱动侧统一维护,Runtime 通过halMemPoolSetAttr/halMemPoolGetAttr驱动接口透传。
阈值与物理释放的联动机制
- 释放时机:默认(阈值 0)情况下,空闲内存超过 0 即触发"下一次 Stream 同步时尝试真正释放空闲物理内存"的逻辑。合理设置阈值可以保留一部分空闲内存避免频繁向驱动申请/归还,是性能与内存占用之间的权衡旋钮。
- 手动收缩:
aclrtMemPoolTrimTo可直接主动收缩(见下文)。 - 优先级:若同时通过
aclrtMemPoolSetAttr配置了ACL_RT_MEM_POOL_ATTR_RELEASE_THRESHOLD阈值,又通过aclrtMemPoolTrimTo的minBytesToKeep指定保留大小,后者(TrimTo)优先级更高。
异步分配与内存复用:aclrtMemPoolMallocAsync / aclrtMemPoolFreeAsync
接口签名
aclError aclrtMemPoolMallocAsync(void **ptr, size_t size, aclrtMemPool memPool, aclrtStream stream) aclError aclrtMemPoolFreeAsync(void *ptr, aclrtStream stream)aclrtMemPoolMallocAsync:从内存池异步申请size(单位 Byte,须大于 0)内存,输出到ptr;stream指定承载该分配任务的 Stream。aclrtMemPoolFreeAsync:异步释放ptr指向的内存。仅将内存归还内存池,并不实际释放物理内存,以便后续任务复用。
两者的"异步"语义均体现在:操作被包装为 Stream 上的任务(申请侧为SomaMemMngAICore 管理内核,释放侧为归还 + 管理内核/回调),随 Stream 保序执行,调用方无需提前aclrtSynchronizeStream。
内存复用机制
调用aclrtMemPoolFreeAsync归还内存后,物理内存并不归还给驱动,而是留在池内供后续任务复用,从而避免频繁申请/释放物理内存。复用时会按本次任务所需大小,从池中选取大小最接近的空闲内存块,尽可能降低碎片与扩容开销。
复用场景分为三类,与上述三个REUSE_*属性开关一一对应:
- 单个 Stream 内复用:执行某 Stream 任务时,系统查找该 Stream 前序任务已归还到池中的内存并复用,是最基础的复用路径。
- Stream 间复用,细分为:
- 事件依赖复用(对应
REUSE_FOLLOW_EVENT_DEPENDENCIES):复用通过 Event 与该 Stream 关联的其他 Stream 已归还的内存,适用于用 Event 做 Stream 间同步的应用。 - 机会主义复用(对应
REUSE_ALLOW_OPPORTUNISTIC):尽力检索可复用内存,不保证成功;失败时报错停止。 - 隐式依赖复用(对应
REUSE_ALLOW_INTERNAL_DEPENDENCIES):若待复用内存曾被无依赖关系的其他 Stream 使用,系统自动插入 Event 同步等待,保证前序访问结束后再复用,兼顾安全与效率。
- 事件依赖复用(对应
从实现层面看,运行时通过SomaApi::AllocFromMemPool(devPtr, alignedSize, memPoolId, streamId, flag)传入复用标志ReuseFlag,结合MemPoolAsyncConfig对驱动的异步配置(见 src/runtime/driver/npu_driver_standard_soc.cc 的StreamMemPoolAsyncConfig),将"该块可复用/被复用"的状态同步到驱动,从而让驱动在后续分配时做出复用决策。
完整实战示例
以下示例完整串联创建池、异步申请、算子下发、异步归还与销毁(摘自 docs/zh/api_ref/11-10_ordered_stream_memory_allocation.md,仅作参考,需根据实际环境调整):
#include <stdio.h> #include <string.h> #include <unistd.h> #include <time.h> #include <gtest/gtest.h> #include "acl/acl_rt.h" #include "acl/acl.h" int main() { uint32_t devid = 0; aclInit(NULL); aclrtSetDevice(devid); // 创建Context和Stream aclrtContext context; aclrtStream stream1; aclrtCreateContext(&context, 0); aclrtCreateStream(&stream1); // 设置内存池属性 aclrtMemLocation testLoc = { 0, // id ACL_MEM_LOCATION_TYPE_DEVICE // type }; aclrtMemPoolProps testProp = { ACL_MEM_ALLOCATION_TYPE_PINNED, // allocType ACL_MEM_HANDLE_TYPE_NONE, // handleType testLoc, // location 14UL << 30, // maxSize = 14GB, 内存池大小为14GB {0} // reserved }; // 创建内存池 aclrtMemPool testReusePool; auto ret = aclrtMemPoolCreate(&testReusePool, &testProp); if (ret != ACL_SUCCESS) { fprintf(stderr, "Failed to create memory pool\n"); return -1; } const size_t GB_TO_B = 1024ULL * 1024 * 1024; // 定义算子信息结构体 typedef struct { void *input_x; void *input_y; void *output_z; } ArgsInfo; aclrtBinHandle bin_handle = nullptr; aclrtFuncHandle func_handle; aclError aclrtBinaryGetFunction(binHandle, "add_custom", &funcHandle); void *ptr0 = nullptr; void *ptr1 = nullptr; void *ptr2 = nullptr; void *ptr3 = nullptr; // 异步申请内存 aclrtMemPoolMallocAsync(&ptr1, sizeof(uint64_t), testReusePool, stream1); aclrtMemPoolMallocAsync(&ptr2, sizeof(uint64_t), testReusePool, stream1); aclrtMemPoolMallocAsync(&ptr3, sizeof(uint64_t), testReusePool, stream1); aclrtMemPoolMallocAsync(&ptr0, sizeof(ArgsInfo), testReusePool, stream1); // 配置任务下发 ArgsInfo usrArgs; usrArgs.input_x = ptr1; usrArgs.input_y = ptr2; usrArgs.output_z = ptr3; error = aclrtMemcpyAsync(devPtr, sizeof(ArgsInfo), (void *)&usrArgs, sizeof(ArgsInfo), ACL_MEMCPY_HOST_TO_DEVICE, stream1); // 下发任务 uint32_t blockDim = 32; aclrtLaunchKernelV2(funcHandle, blockDim, (void *)&usrArgs, sizeof(ArgsInfo), nullptr, stream1); // 异步释放内存,此前无需进行流同步 aclrtMemPoolFreeAsync(ptr0, stream1); aclrtMemPoolFreeAsync(ptr1, stream1); aclrtMemPoolFreeAsync(ptr2, stream1); aclrtMemPoolFreeAsync(ptr3, stream1); // 流同步 aclrtSynchronizeStream(stream1); // 销毁内存池、Stream和Context aclrtMemPoolDestroy(testReusePool); aclrtDestroyStream(stream1); aclrtDestroyContext(context); aclrtResetDevice(devid); aclFinalize(); return 0; }与常规 aclrtMalloc/aclrtFree 的适用场景对比
| 维度 | aclrtMalloc / aclrtFree | aclrtMemPoolMallocAsync / aclrtMemPoolFreeAsync |
|---|---|---|
| 同步语义 | 同步接口,释放前必须aclrtSynchronizeStream保证内存空闲 | 异步接口,操作进入 Stream 调度序列,依赖 Stream 保序,无需显式同步 |
| 物理内存行为 | 每次申请/释放都直接作用于物理内存 | 归还仅回池,物理内存延迟释放,由阈值/TrimTo 控制 |
| 内存复用 | 无 | 支持池内复用(单 Stream 内及 Stream 间三种模式) |
| 使用复杂度 | 简单直观 | 需先建池、配置属性,复杂度更高 |
| 适用场景 | 低频、一次性分配 | 大量算子下发、高频申请/释放、追求流水线性能的场景 |
主动收缩内存池:aclrtMemPoolTrimTo
接口签名
aclError aclrtMemPoolTrimTo(aclrtMemPool memPool, size_t minBytesToKeep)功能说明
收缩内存池,保留minBytesToKeep字节的物理内存,释放其余空闲物理内存,不影响当前正在使用的内存。
由于aclrtMemPoolFreeAsync只归还、不释放,空闲内存会持续占用池容量,极端情况下可能导致aclrtMemPoolMallocAsync无法申请到新内存(池容量已满)。此时调用aclrtMemPoolTrimTo可主动回收未使用的物理内存。它与ACL_RT_MEM_POOL_ATTR_RELEASE_THRESHOLD的差别在于:
- 阈值属性是"惰性释放"——在下次 Stream 同步时按阈值判断是否释放;
TrimTo是"主动收缩"——调用即刻生效,且minBytesToKeep优先级高于阈值配置。
运行时内部:与普通内存池的协同
值得注意的是,仓库中还维护了一套非 SOMA 的普通设备内存池机制(src/runtime/core/src/pool/memory_pool_manager.cc),两者并存但职责不同:
MemoryPoolManager服务于常规aclrtMalloc/aclrtFree等同步分配路径,按需创建MemoryPool实例(Allocate时若无可用池则AddMemoryPool),单块分配超过 2MB(POOL_SIZE_2M)时直接不走池;- 它采用"空闲池超过
maxFreePools_阈值即回收"的策略(CheckAndReleasePools),并用读写锁保护并发(memory_pool_manager.cc); - 而流序内存池由
PoolRegistry统一管理SegmentManager,走驱动halMemPoolCreate/halMemPoolAsyncConfig接口(soma.cc、npu_driver_standard_soc.cc),二者分别服务同步与异步两条内存管理通路。
理解这一区分有助于在排查问题时快速定位:同步接口的池化行为看MemoryPoolManager,流序池的行为看 SOMA 栈。
总结与建议
流序内存池将内存管理"任务化 + 池化 + 复用化",是针对高频异步任务场景的高阶内存方案。落地时的要点可归结为:
- 先确认平台:当前仅 Atlas A2 训练/推理系列产品支持,其他平台调用会返回功能不支持错误。
- 合理设定
maxSize:创建时即预留物理内存,过大浪费显存,过小则复用价值有限,需结合业务峰值与USED_MEM_HIGH/RESERVED_MEM_HIGH指标校准。 - 配置复用开关与释放阈值:根据 Stream 间是否存在 Event 依赖选择合适的
REUSE_*开关,并用RELEASE_THRESHOLD平衡内存占用与释放频率;需要即刻回收时调用aclrtMemPoolTrimTo。 - 遵循异步语义:申请/归还均按 Stream 保序,无需在归还前显式同步,但最终仍需一次
aclrtSynchronizeStream完成整条流水线的收尾。 - 监控指标:通过
aclrtMemPoolGetAttr读取RESERVED_MEM_CURRENT/HIGH与USED_MEM_CURRENT/HIGH,用 Current 观察实时水位、用 High 评估容量规划。
相关文档与源码入口:功能索引 example/3_memory_advanced/memory_pool/README_en.md(及 中文版)、API 手册 docs/zh/api_ref/11-10_ordered_stream_memory_allocation.md、属性枚举说明 docs/zh/api_ref/25-02_Enumerations.md、头文件 include/external/acl/acl_rt_memory_soma.h、实现源码 src/runtime/feature/soma/soma.cc 与 src/runtime/api/impl/api_impl_soma.cc。
【免费下载链接】runtime本项目提供CANN运行时组件和维测功能组件。项目地址: https://gitcode.com/cann/runtime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考