pyasc 内存池分配接口解析:TBufPool.init_buffer 为 TQue / TBuf 分配片上内存的完整实践
【免费下载链接】pyasc本项目为Python用户提供算子编程接口,支持在昇腾AI处理器上加速计算,接口与Ascend C一一对应并遵守Python原生语法。项目地址: https://gitcode.com/cann/pyasc
TBufPool.init_buffer是 CANN pyasc(Ascend C 的 Python 编程接口)中用于为队列TQue与临时缓冲区TBuf分配片上内存(Unified Buffer / L1 Buffer)的核心接口。本文围绕该接口的两种重载形式、参数语义、约束条件与底层实现展开,并结合仓库源码与单元测试,帮助开发者在多 stage 流水计算、片上内存复用的场景下正确使用TBufPool管理内存。读完本文,你将掌握init_buffer与init_buf_pool/reset的配合方式,并能够编写可运行的 pyasc 内存池初始化代码。
一、为什么需要 TBufPool:从 TPipe 到内存池
在 pyasc 编程模型中,TPipe 负责统一管理 Device 端内存资源,一个 Kernel 函数必须且只能初始化一个TPipe对象。常规场景下,开发者通过TPipe.init_buffer直接为TQue(流水队列)和TBuf(临时变量缓冲区)分配内存。
但当一个算子存在多个 stage 计算、且每段计算都需要独立的输入/输出缓冲时,Unified Buffer / L1 Buffer 的物理内存往往不够用。此时TBufPool便派上用场——正如 python/asc/language/fwk/tpipe.py 中类注释所述:
TPipe 可以管理全局内存资源,而 TBufPool 可以手动管理或复用 Unified Buffer / L1 Buffer 物理内存,主要用于多个 stage 计算中 Unified Buffer / L1 Buffer 物理内存不足的场景。
TBufPool的本质是一块从TPipe整体资源中划分出来的子资源池,它允许开发者:
- 把整块物理内存先划给一个
TBufPool,再在其中细分出多个队列/临时缓冲的分配; - 在多个 stage 之间复用同一片物理内存(通过
share_buf共享),从而在内存受限时完成流水编排。
二、接口签名:两种重载,两种分配目标
TBufPool.init_buffer对应文档见 asc.language.fwk.TBufPool.init_buffer.md,其 Python 侧声明为两个重载:
# 重载一:为队列 TQue 分配 num 块、每块 len 字节的内存 TBufPool.init_buffer(que: TQue, num: int = 0, len: int = 0) → None # 重载二:为临时缓冲 TBuf 分配 len 字节的内存 TBufPool.init_buffer(buf: TBuf, len: int = 0) → None它调用的是 Ascend C 的TBufPool::InitBuffer接口,对应的 C++ 函数原型为:
template <class T> __aicore__ inline bool InitBuffer(T& que, uint8_t num, uint32_t len) template <TPosition pos> __aicore__ inline bool InitBuffer(TBuf<pos>& buf, uint32_t len)两个重载解决两类不同对象的内存初始化:
| 重载 | 分配对象 | 语义 |
|---|---|---|
init_buffer(que, num, len) | 流水队列TQue | 为队列分配num块内存,每块大小为len字节 |
init_buffer(buf, len) | 临时缓冲TBuf | 为临时缓冲区分配一块大小为len字节的内存 |
三、参数说明
接口涉及的参数(含TBufPool构造时传入的pos)语义如下:
- pos:Buffer 的逻辑位置,可选值为
VECIN、VECOUT、VECCALC、A1、B1、C1。这些枚举定义在 python/asc/language/core/enums.py 的TPosition中;其中VECIN/VECOUT/VECCALC对应向量单元(Unified Buffer)上的逻辑位置,A1/B1/C1对应 Cube 单元(L1 Buffer 相关)上的逻辑位置。 - que:需要分配内存的
TQue对象。 - num:分配内存块的个数(块数)。
- len(TQue 重载):每个内存块的大小,单位为 Bytes;非 32 Bytes 对齐时会自动向上补齐至 32 Bytes 对齐。
- buf:需要分配内存的
TBuf对象。 - len(TBuf 重载):为
TBuf分配的内存大小,单位为 Bytes;同样非 32 Bytes 对齐会自动向上补齐至 32 Bytes 对齐。
需要特别注意的是:TBufPool自身的逻辑位置pos与队列que/ 缓冲buf的逻辑位置必须匹配——从源码看,TBufPool构造时通过builder.get_tbuf_pool_type(pos, buf_id_size)生成带位置信息的类型(tpipe.py),其物理内存与后续分配的TQue/TBuf必须一致。
四、约束说明
- 可分配 Buffer 数量上限:声明
TBufPool时,可通过构造参数buf_id_size指定可分配 Buffer 的最大数量,默认上限为 4,最大为 16(见 asc.language.fwk.TBufPool.init.md)。- 非共享模式的资源分配:在本
TBufPool上再次申请子TBufPool时,子池的buf_id_size不能超过原池剩余可用的 Buffer 数量; - 共享模式(
share_buf)的资源分配:子池的buf_id_size不能超过原池设置的 Buffer 数量。
- 非共享模式的资源分配:在本
- 物理内存一致性:
TQue或TBuf的物理内存需要和TBufPool一致,即它们必须来自同一个资源池的分配。 - 长度对齐规则:
len参数非 32 Bytes 对齐时自动向上取整对齐,因此开发者无需手工对齐,但建议按 32B 倍数规划,便于地址计算与get_with_offset等偏移操作。
五、源码级实现:从 Python 重载到底层 IR 算子
TBufPool.init_buffer的 Python 实现位于 python/asc/language/fwk/tpipe.py。它并非一个普通函数,而是借助OverloadDispatcher根据实参类型分发到不同重载:
@require_jit @set_tpipe_docstring(pipe_name="TBufPool", api_name="init_buffer") def init_buffer(self, *args, **kwargs) -> None: dispatcher = OverloadDispatcher(__name__) @dispatcher.register(que=TQue, num=RuntimeInt, len=RuntimeInt) def _(que: TQue, num: RuntimeInt = 0, len: RuntimeInt = 0): global_builder.get_ir_builder().create_asc_TBufPoolInitQueueOp( self.to_ir(), que.to_ir(), _mat(num, KnownTypes.int_).to_ir(), _mat(len, KnownTypes.int_).to_ir()) @dispatcher.register(buf=TBuf, len=RuntimeInt) def _(buf: TBuf, len: RuntimeInt = 0): global_builder.get_ir_builder().create_asc_TBufPoolInitBufferOp( self.to_ir(), buf.to_ir(), _mat(len, KnownTypes.int_).to_ir()) dispatcher(*args, **kwargs)从实现可以看出两点关键信息:
- 编译期约束:
init_buffer被@require_jit装饰,即只能在@asc.jit编译的 kernel 函数体内调用,无法在 Host 侧 Python 环境中直接执行; - 底层 IR 映射:TQue 重载生成
asc_TBufPoolInitQueueOp算子(对应 C++ 的InitBuffer(T& que, uint8_t num, uint32_t len)),TBuf 重载生成asc_TBufPoolInitBufferOp算子(对应 C++ 的InitBuffer(TBuf<pos>& buf, uint32_t len)),num以int(对标uint8_t)、len以int(对标uint32_t)类型下发。这与文档中给出的 C++ 原型一一对应,说明 pyasc 接口与 Ascend C 是严格对齐的。
六、完整调用示例:多 stage 内存复用流水
原文档给出了一个非常典型的多 stage 内存复用场景:先用TPipe.init_buf_pool从整体资源中划出大池tbuf_pool0,再通过TBufPool.init_buf_pool细分出tbuf_pool1(普通子池)与tbuf_pool2(共享tbuf_pool1物理内存的子池),随后两个 stage 交替使用tbuf_pool1/tbuf_pool2为各自的TQue分配内存,并配合reset()完成切换:
@asc.jit def init(src0_gm: asc.GlobalAddress, src1_gm: asc.GlobalAddress, dst_gm: asc.GlobalAddress): src0_global.set_global_buffer(src0_gm) src1_global.set_global_buffer(src1_gm) dst_global.set_global_buffer(dst_gm) # 从 TPipe 整体资源中划分出 131072 字节的大资源池 pipe.init_buf_pool(tbuf_pool0, 131072) # 在大池中为 src0 队列分配 1 块 65536 字节内存 tbuf_pool0.init_buffer(que=src_que0, num=1, len=65536) # Total src0 # 细分出子池 tbuf_pool1(65536 字节) tbuf_pool0.init_buf_pool(tbuf_pool1, 65536) # 细分出子池 tbuf_pool2(65536 字节),并与 tbuf_pool1 共享物理内存 tbuf_pool0.init_buf_pool(tbuf_pool2, 65536, tbuf_pool1) @asc.jit def Process(): # stage 1:使用 tbuf_pool1 为输入/输出队列分配内存 tbuf_pool1.init_buffer(que=src_que1, num=1, len=32768) tbuf_pool1.init_buffer(que=dst_que0, num=1, len=32768) copy_in() compute() copy_out() tbuf_pool1.reset() # stage 2:切换至 tbuf_pool2,复用 tbuf_pool1 的物理内存 tbuf_pool2.init_buffer(src_que2, num=1, len=32768) tbuf_pool2.init_buffer(dst_que1, num=1, len=32768) copy_in1() compute1() copy_out1() tbuf_pool2.reset() tbuf_pool0.reset() pipe.reset()示例中展示的完整生命周期是:
- 建池:
pipe.init_buf_pool(tbuf_pool0, len)从TPipe整体资源中划分整块子资源池; - 细分:
tbuf_pool0.init_buf_pool(tbuf_pool1, len)继续划分更小的池;init_buf_pool(tbuf_pool2, len, tbuf_pool1)则创建与tbuf_pool1共享起始地址及长度的池(相关约束见 asc.language.fwk.TBufPool.init_buf_pool.md:新划分的资源池与被复用资源池物理内存一致、输入长度需小于等于被复用池长度); - 分配:各 stage 内用
init_buffer(que=..., num=..., len=...)为队列分配内存; - 切换:stage 切换前调用
tbuf_pool.reset(),结束当前资源池正在处理的相关事件。如 asc.language.fwk.TBufPool.reset.md 所述,调用后资源池及其分配的 Buffer 仍然存在,只是 Buffer 内容可能被改写;切回该池后可重新使用这些 Buffer,无需再次分配; - 收尾:
pipe.reset()恢复TPipe的初始化状态。
七、TBuf 场景:init_buffer 与 TBuf.get 的配合
当使用临时缓冲时,init_buffer(buf, len)分配的内存后续通过TBuf.get取为可参与计算的LocalTensor。参考 asc.language.fwk.TBuf.get.md 的用法:
# 为 TBuf 初始化分配内存,分配内存长度为 1024 字节 pipe = asc.Tpipe() calc_buf = asc.TBuf(asc.TPosition.VECCALC) byte_len = 1024 pipe.init_buffer(calc_buf, byte_len) # 从 calc_buf 获取 Tensor,Tensor 为 pipe 分配的所有内存大小,为 1024 字节 temp_tensor1 = calc_buf.get(asc.int32) # 从 calc_buf 获取 Tensor,Tensor 为 128 个 int32_t 类型元素的内存大小,为 512 字节 temp_tensor1 = calc_buf.get(asc.int32, 128)在TBufPool场景下,把示例中的pipe.init_buffer(calc_buf, byte_len)替换为tbuf_pool.init_buffer(buf=calc_buf, len=byte_len)即可让临时缓冲从资源池中分配。注意约束:len的数值是 Tensor 中元素的个数,len * sizeof(T)不能超过TBuf初始化时的内存长度。
八、测试验证:仓库中如何验证该接口
仓库在 python/test/unit/language/fwk/test_tbuf_pool.py 中提供了覆盖init_buffer两种重载的单元测试:
def test_init_buffer(mock_launcher_run): @asc.jit def kernel_init_buffer() -> None: que = asc.TQue(asc.TPosition.VECIN, 1) tmp_buf = asc.TBuf(asc.TPosition.VECCALC) buf_pool0 = asc.TBufPool(pos=asc.TPosition.VECIN, buf_id_size=4) buf_pool0.init_buffer(que=que, num=1, len=256) buf_pool0.init_buffer(buf=tmp_buf, len=256) kernel_init_buffer[1]() assert mock_launcher_run.call_count == 1同一测试文件还覆盖了TBufPool构造(test_init)、init_buf_pool的普通/共享两种形态(test_init_buf_pool)以及reset(test_reset),完整验证了资源池从建池、细分、分配到复用的全流程。这些测试同时印证了接口的调用方式:asc.TBufPool(pos=..., buf_id_size=...)构造资源池、asc.TQue(asc.TPosition.VECIN, depth)构造队列、asc.TBuf(asc.TPosition.VECCALC)构造临时缓冲,与文档示例完全一致。
九、实践要点小结
- 分清
TPipe.init_buffer与TBufPool.init_buffer:前者由TPipe统一分配,后者在手动划分的资源池内分配;TPipe.init_buf_pool与TBufPool.init_buf_pool分别用于"从整体划池"与"池内再细分"。 - 内存池适用于多 stage 内存不足场景:通过
share_buf共享物理内存的子池,可以让不同 stage 的队列/缓冲复用同一片 UB/L1 内存,从而在有限片上内存下完成多段流水。 - 切换资源池必须 reset:
tbuf_pool.reset()结束当前池相关事件后再切换到下一个池;切回旧池时无需重新init_buffer,但旧 Buffer 内容可能已被改写,需重新填充数据。 - 注意 32B 对齐与长度约束:
len自动向上补齐 32B;TBuf.get时len * sizeof(T)不得超过初始化长度;子池共享模式下长度不得超过被复用池长度。 - buf_id_size 上限:默认 4、最大 16,细分池时不能超过原池剩余(或设置的)可分配 Buffer 数。
十、延伸阅读
- TBufPool 类总览与相关接口:
init_buf_pool、init_buffer、reset三个接口的完整说明 - TBufPool 构造与 buf_id_size 约束
- TBufPool.init_buf_pool:子池划分与共享内存约束
- TBufPool.reset:资源池切换语义
- 实现源码:
TBufPool/TPipe/TQue/TBuf类的完整 Python 实现 - 单元测试:覆盖建池、细分、分配与 reset 的验证用例
【免费下载链接】pyasc本项目为Python用户提供算子编程接口,支持在昇腾AI处理器上加速计算,接口与Ascend C一一对应并遵守Python原生语法。项目地址: https://gitcode.com/cann/pyasc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考