CANN pyasc 内存管理:深入解析 asc.language.core.LocalMemAllocator.alloc 的 LocalTensor 分配机制
【免费下载链接】pyasc本项目为Python用户提供算子编程接口,支持在昇腾AI处理器上加速计算,接口与Ascend C一一对应并遵守Python原生语法。项目地址: https://gitcode.com/cann/pyasc
asc.language.core.LocalMemAllocator.alloc是 CANN pyasc 在 Ascend C 静态 Tensor 编程方式下,从指定物理内存位置分配 LocalTensor 的核心接口。本文基于 官方 API 文档,结合 memory_allocator.py 源码与 MLIR 测试用例,完整讲解其函数签名、Ascend C 原型、参数语义、内存约束与调用示例,帮助你避开"内存越界"与"混用 TPipe"等典型陷阱,写出可直接上板运行的静态 Tensor 算子。
一、接口定位:静态 Tensor 编程方式的内存管理入口
在 core.md 中,LocalMemAllocator被明确定义为:
LocalMemAllocator 是在使用 Ascend C 静态 Tensor 编程方式时用于内存管理的类,用户无需构建 TPipe/TQue,而是直接创建 LocalTensor 对象并开发算子,从而减少运行时的开销,实现更优的性能。
这意味着它与传统基于TPipe/TQue的流水线内存管理模式是互斥的。源码 memory_allocator.py 的类注释也强调:LocalMemAllocator仅支持在 Ascend C 静态 Tensor 编程方式中使用,不可以与 TPipe 等接口混用。
从源码结构看,LocalMemAllocator的构造器通过builder.create_asc_LocalMemAllocatorOp将 Hardware 物理位置固化为 IR 的一部分(memory_allocator.py),因此一个 allocator 实例一旦创建,其绑定的物理内存位置(默认Hardware.UB)便确定下来,后续的alloc与get_cur_addr都在该位置上工作。
二、函数签名与对应的 Ascend C 原型
alloc接口提供两个重载形式,对应tile_size是编译期常量还是动态参数两种场景:
LocalMemAllocator.alloc(pos: TPosition, data_type: DataType, tile_size: ConstExpr[int]) → LocalTensor LocalMemAllocator.alloc(pos: TPosition, data_type: DataType, tile_size: int) → LocalTensor其对应的 Ascend C 函数原型分别为:
// 原型1:tileSize为模板参数 // 当tileSize为常量时,建议使用此接口,以获得更优的性能 template <TPosition pos, class DataType, uint32_t tileSize> __aicore__ inline LocalTensor<DataType> Alloc() // 原型2:tileSize为接口入参 // 当tileSize为动态参数时使用此接口 template <TPosition pos, class DataType> LocalTensor<DataType> __aicore__ inline Alloc(uint32_t tileSize)两个原型的取舍非常明确:
- 常量 tileSize:编译期即可确定内存布局,编译器可以做静态优化,官方建议优先使用以获得更优性能;
- 动态 tileSize:仅在运行时才能确定长度(例如随输入 shape 变化的场景)时使用。
这一分支逻辑在 memory_allocator.py 中有精确的对应实现:当tile_size是ConstExpr实例时,通过ConstExpr.unwrap取到常量值并调用create_asc_LocalMemAllocatorAllocOp(对应模板原型);否则通过materialize_ir_value将运行时值物化为 i32 IR 值,调用create_asc_LocalMemAllocatorAllocDynamicOp(对应动态原型)。ConstExpr的 unwrap 逻辑见 constexpr.py,它会递归剥开ConstExpr外壳取到内层真实值。
三、参数说明与语义
| 参数 | 类型 | 语义与约束 |
|---|---|---|
pos | TPosition | 逻辑位置,必须符合LocalMemAllocator中指定的 Hardware 物理位置 |
data_type | DataType | LocalTensor 的数据类型,只支持基础数据类型,当前不支持 TensorTrait 类型 |
tile_size | ConstExpr[int]/int | LocalTensor 的元素个数,其数量不应超过当前物理位置剩余的内存空间 |
3.1 pos:逻辑位置 TPosition
TPosition在 enums.py 中定义,包括GM、A1、A2、B1、B2、C1、C2、CO1、CO2、VECIN、VECOUT、VECCALC、MAX。其中VECIN(向量输入区)、VECOUT(向量输出区)、VECCALC(向量计算区)是静态 Tensor 编程中最常用的向量逻辑位置,而 A/B/C 系列位置对应 Cube 矩阵运算的搬运缓冲区。
pos必须与 allocator 绑定的 Hardware 物理位置匹配。例如在 test_local_mem_allocator.py 中,默认 UB allocator 上以asc.TPosition.VECIN分配asc.float32、32 个元素的 tensor 是合法用法;而若在 UB 上指定属于 GM 的语义位置则不符合接口约束。
3.2 data_type:基础数据类型
data_type只支持基础数据类型(如asc.float32、float等),不支持 TensorTrait 类型。IR 层通过data_type.to_ir()将 Python 数据类型映射为 MLIR 类型(memory_allocator.py)。
3.3 tile_size:元素个数与内存边界
tile_size是元素个数而非字节数。其上限由"当前物理位置剩余的内存空间"决定——剩余空间 = 物理内存最大值 − 当前可用内存地址(get_cur_addr的返回值)。
四、返回值与约束说明
返回值
根据用户输入构造的LocalTensor对象。IR 构造完成后通过LocalTensor.from_ir(handle)包装返回(memory_allocator.py),该 tensor 的类型为LocalTensor<data_type>,长度为tile_size。
约束:切勿超出剩余内存
tile_size 不应超过当前物理位置剩余的内存空间。剩余内存空间通过如下公式计算:
剩余内存空间 = 物理内存最大值 - get_cur_addr() 返回值get_cur_addr是配套的查询接口(get_cur_addr.md),返回当前物理位置空闲的起始地址,范围为[0, 物理内存最大值),其 Ascend C 原型为__aicore__ inline uint32_t GetCurAddr() const。由于 allocator 从 0 地址开始分配,初始状态下get_cur_addr()返回 0。
注意:
alloc是静态 Tensor 编程的"一次性规划"接口,连续调用alloc会从当前位置继续向后分配地址,因此需要自行累计已分配长度,避免后续分配的 tile_size 越界。
五、调用示例与完整用法
5.1 文档中的标准示例
allocator = asc.LocalMemAllocator() # 用户指定逻辑位置 VECIN,float 类型,Tensor 中有 1024 个元素 tensor1 = allocator.alloc(asc.TPosition.VECIN, float, 1024) # 用户指定逻辑位置 VECIN,float 类型,Tensor 中有 tileLength 个元素 tile_length = 512 tensor2 = allocator.alloc(asc.TPosition.VECIN, float, tile_length)这里tensor1对应常量路径(tile_size 为字面量 1024,编译期常量),tensor2对应动态路径(tile_size 由 Python 变量tile_length携带)。需要说明的是,文档示例中的字面量会被 pyasc 的 JIT 编译管线识别为常量路径;而来自运行时入参的长度则走动态原型。若希望显式表达"编译期常量"语义,可以将其包装为ConstExpr,例如asc.ConstExpr(1024)(具体用法见 constexpr.py)。
5.2 结合 get_cur_addr 做剩余空间校验
import asc @asc.jit def kernel() -> None: allocator = asc.LocalMemAllocator() # 默认物理位置为 UB # 初始从 0 地址开始分配,下面的打印结果为 0 addr = allocator.get_cur_addr() asc.printf("current addr is %u\n", addr) tensor1 = allocator.alloc(asc.TPosition.VECIN, asc.float32, 1024) # 分配后当前地址前进,可依据剩余空间判断下一次 alloc 是否安全 tensor2 = allocator.alloc(asc.TPosition.VECIN, asc.float32, 512) kernel[1]()5.3 源码级验证:MLIR 到 Ascend C 的发射路径
local_mem_allocator.mlir 的测试用例完整展示了三个关键场景的降级结果:
- get_cur_addr:
ascendc.local_mem_allocator.get_cur_addr发射为AscendC::LocalMemAllocator<AscendC::Hardware::GM> v1; uint32_t v2 = v1.GetCurAddr(); - 常量 alloc:
AscendC::LocalTensor<float> v2 = v1.Alloc<AscendC::TPosition::VECIN, float, c32_i32>();——tileSize 成为模板参数,印证原型 1; - 动态 alloc:
AscendC::LocalTensor<float> v3 = v2.Alloc<AscendC::TPosition::VECIN, float>(v1);——tileSize 作为运行时入参传入,印证原型 2。
同时该文件还验证了Hardware枚举(GM/UB/L1/L0A/L0B/L0C/BIAS/FIXBUF)全部 8 种物理位置都能正确构造 allocator 类型。IR 层的 Op 定义位于 MemoryAllocator.td,其中AscendC_LocalMemAllocatorAllocOp与AscendC_LocalMemAllocatorAllocDynamicOp分别对应两个 Python 重载。
六、工程实践要点
- 不要与 TPipe/TQue 混用:
LocalMemAllocator是静态 Tensor 编程专属,混用会破坏内存管理的一致性。 - 常量优先:只要 tile_size 在编译期可确定,就应走常量路径(字面量或
ConstExpr),以获得模板化后的静态优化收益。 - 自行管理地址游标:allocator 从 0 地址顺序分配,多次
alloc后应通过get_cur_addr()主动跟踪空闲起始地址,确保累计分配长度不超过物理内存最大值。 - 区分元素数与字节数:
tile_size的单位是元素个数,估算内存占用时需乘以data_type的字节宽度。 - 数据类型的边界:只使用基础数据类型,TensorTrait 类型不在支持范围内,避免类型不匹配导致的编译错误。
七、参考依据
- API 文档:asc.language.core.LocalMemAllocator.alloc、asc.language.core.LocalMemAllocator.get_cur_addr、core.md
- Python 实现:python/asc/language/core/memory_allocator.py
- 枚举定义:python/asc/language/core/enums.py(
Hardware、TPosition) - 常量封装:python/asc/language/core/constexpr.py
- IR Op 定义:include/ascir/Dialect/Asc/IR/Core/MemoryAllocator.td
- MLIR 降级测试:test/Target/AscendC/local_mem_allocator.mlir
- 单元测试:python/test/unit/language/core/test_local_mem_allocator.py
【免费下载链接】pyasc本项目为Python用户提供算子编程接口,支持在昇腾AI处理器上加速计算,接口与Ascend C一一对应并遵守Python原生语法。项目地址: https://gitcode.com/cann/pyasc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考