CANN pyasc 算子编程:GlobalTensor.set_global_buffer 全局内存绑定原理与实战
【免费下载链接】pyasc本项目为Python用户提供算子编程接口,支持在昇腾AI处理器上加速计算,接口与Ascend C一一对应并遵守Python原生语法。项目地址: https://gitcode.com/cann/pyasc
GlobalTensor 是 CANN pyasc(Python 算子编程接口)中表示 Global Memory(全局内存/外部存储)数据的核心类型。set_global_buffer负责把 Host 侧传入的全局数据地址绑定到 GlobalTensor 上,是几乎每个算子 kernel 的"开场动作"。本文以该 API 为线索,结合仓库源码(tensor.py、Tensor.td、test_global_tensor.py 与 examples/01_add)讲解它的签名语义、参数约定、底层 IR 实现与典型实战用法,帮助你写对每一个"从全局内存出发"的算子。
一、GlobalTensor 与全局内存
在昇腾 AI Core 的编程模型中,数据存放在两类存储上:
- Global Memory(全局内存,GM):AI Core 外部的片外存储,容量大、带宽相对有限,对应代码中的
GlobalTensor; - Local Memory(本地内存,LM):AI Core 内部存储,按逻辑位置划分为 VECIN、VECOUT、VECCALC、A1、A2、B1、B2、CO1、CO2 等,对应代码中的
LocalTensor。
pyasc 中GlobalTensor类的定位在源码注释中写得很清楚:"GlobalTensor 用来存放 Global Memory(外部存储)的全局数据"(python/asc/language/core/tensor.py)。一个典型的 kernel 流程是:先用set_global_buffer把 Host 传入的全局地址绑定到 GlobalTensor,再用data_copy等指令把数据搬运到 LocalTensor 参与计算,最后把结果写回全局内存。
set_global_buffer就是这条链路的起点——它是 GlobalTensor 生命周期中第一个、也是最重要的初始化动作。
二、函数签名与语义
asc.language.core.GlobalTensor.set_global_buffer提供两个重载(见原 API 文档 asc.language.core.GlobalTensor.set_global_buffer.md):
GlobalTensor.set_global_buffer(buffer: GlobalAddress | None = None) -> None GlobalTensor.set_global_buffer(buffer: GlobalAddress | None = None, buffer_size: int | None = None) -> None其语义为:传入全局数据地址,初始化 GlobalTensor。
两种重载的区别仅在于是否显式指定buffer_size:
- 只传
buffer:仅完成地址绑定,不携带长度信息; - 同时传
buffer与buffer_size:在绑定的同时声明该 GlobalTensor 所包含的元素个数,供依赖长度信息的指令使用。
从源码看,这两个重载在 python/asc/language/core/tensor.py 中通过@overload声明,实际实现是同一个函数体:
@overload def set_global_buffer(self, buffer: Optional[GlobalAddress] = None) -> None: ... @overload def set_global_buffer(self, buffer: Optional[GlobalAddress] = None, buffer_size: Optional[int] = None) -> None: ... @require_jit @set_tensor_docstring(tensor_name="GlobalTensor", api_name="set_global_buffer") def set_global_buffer(self, buffer: Optional[GlobalAddress] = None, buffer_size: Optional[RuntimeInt] = None) -> None: if buffer is None or buffer.dtype is None: raise ValueError("Either DataType or typed GlobalAddress must be provided to instantiate GlobalTensor") dtype = buffer.dtype super().__init__(dtype) builder = global_builder.get_ir_builder() ir_type = ir.get_global_tensor_type(dtype.to_ir()) handle = builder.create_asc_GlobalTensorOp(ir_type) self.dtype = dtype self.handle = handle if buffer_size is not None: builder.create_asc_GlobalTensorSetGlobalBufferOp(self.to_ir(), buffer.to_ir(), _mat(buffer_size).to_ir()) else: builder.create_asc_GlobalTensorSetGlobalBufferOp(self.to_ir(), buffer.to_ir())实现要点(可从源码结构确认):
- 校验:
buffer为None或缺少dtype时抛出ValueError,提示必须提供带数据类型的GlobalAddress; - 类型推导:GlobalTensor 的元素类型直接取自
buffer.dtype,因此传入的GlobalAddress必须携带正确的数据类型信息; - IR 构造:先创建
asc_GlobalTensorOp得到全局张量句柄,再按是否传buffer_size分别生成带/不带 size 参数的asc_GlobalTensorSetGlobalBufferOp; @require_jit:该 API 只能在 JIT 编译的 kernel 上下文中调用,对应 pyasc 的@asc.jit编程模型。
三、参数详解
buffer:全局数据指针
- 类型:
GlobalAddress | None,即 Host 侧传入的全局数据指针,元素类型为 PrimType(pyasc 中的DataType)。 - 说明:
GlobalAddress是 pyasc 对全局地址的抽象,定义在 python/asc/language/core/ir_value.py。它支持__add__指针偏移运算(内部生成arith.IndexCast与emitasc.ptr_offset操作),因此可以写出x_gm.set_global_buffer(x + offset, length)这种按核/按块切分全局内存的惯用法(见下文实战示例)。 - 注意:在
set_global_buffer的实现中,若传入的地址缺少dtype信息,会直接抛异常;调用前应确保地址是"带类型的"GlobalAddress(如从 kernel 参数传入的asc.GlobalAddress)。
buffer_size:数据个数
- 类型:
int | None,可选。 - 含义:GlobalTensor 所包含的、类型为 PrimType 的数据个数(注意是元素个数,不是字节数)。
- 约束:需自行保证不会超出实际数据的长度,即
buffer_size不得超过 Host 侧为该地址分配的实际元素数量。这是开发者需要负责的边界约定,框架不做越界校验。 - 可选性:省略
buffer_size时,GlobalTensor 仍可正常绑定地址,但依赖长度信息的操作(如data_copy的搬运长度、get_size返回的元素数等)需要由调用方另行保证。
四、与 Ascend C 的对应关系
set_global_buffer与 Ascend C 的GlobalTensor::SetGlobalBuffer一一对应。原 API 文档给出两个 C++ 原型:
__aicore__ inline void SetGlobalBuffer(__gm__ PrimType* buffer, uint64_t bufferSize) __aicore__ inline void SetGlobalBuffer(__gm__ PrimType* buffer)在 pyasc 的 MLIR 方言层,这一对应关系被定义在 TableGen 文件中(include/ascir/Dialect/Asc/IR/Core/Tensor.td):
def AscendC_GlobalTensorSetGlobalBufferOp : APIOp<"global_tensor.set_global_buffer", "SetGlobalBuffer", [AscMemberFunc]> { let summary = "Set data buffer of global tensor"; let arguments = (ins AscendC_GlobalTensor:$tensor, AnyRankedOrUnrankedMemRef:$buffer, Optional<AnyInteger>:$size); ... }从该定义可以确认三点实现事实:
- 操作名
global_tensor.set_global_buffer直接映射到 Ascend C 的SetGlobalBuffer成员函数; buffer参数在 IR 中建模为AnyRankedOrUnrankedMemRef,即任意秩的 memref 全局地址;size为Optional<AnyInteger>,印证了buffer_size可选这一 API 设计。
整个链路为:Python 层set_global_buffer→ pyasc IR 的asc_GlobalTensorSetGlobalBufferOp→ MLIR 方言层global_tensor.set_global_bufferOp → Ascend C 代码生成SetGlobalBuffer。
五、典型实战用法
5.1 最小示例(来自原 API 文档)
data_size = 256 input_global = asc.GlobalTensor() input_global.set_global_buffer(src_gm, data_size) input_local = in_queue_x.alloc_tensor(asc.int32) asc.data_copy(input_local, input_global, data_size)该示例展示的标准三步:
asc.GlobalTensor()创建空全局张量;set_global_buffer(src_gm, data_size)把源地址src_gm绑定进来,并声明长度为data_size个元素;asc.data_copy(input_local, input_global, data_size)将其搬运到本地队列张量参与后续计算。
5.2 多核分块:buffer + offset惯用法
在真实的算子 kernel 中,通常需要按核切分全局数据。仓库示例 examples/01_add/add.py 给出了完整范式:
@asc.jit def vadd_kernel(x: asc.GlobalAddress, y: asc.GlobalAddress, z: asc.GlobalAddress, block_length: int): offset = asc.get_block_idx() * block_length x_gm = asc.GlobalTensor() y_gm = asc.GlobalTensor() z_gm = asc.GlobalTensor() x_gm.set_global_buffer(x + offset, block_length) y_gm.set_global_buffer(y + offset, block_length) z_gm.set_global_buffer(z + offset, block_length) ...要点解析:
asc.get_block_idx()获取当前 AI Core 的块索引,乘以block_length得到本核负责的数据偏移;x + offset利用GlobalAddress.__add__做指针偏移,得到本核的起始地址;- 每个 GlobalTensor 绑定各自核内的地址与长度,实现多核并行下各核互不越界的全局内存视图;
- 后续配合
x_gm[i * tile_length:]这类切片(GlobalTensor 支持__getitem__子索引,见 python/asc/language/core/tensor.py)逐 tile 搬运计算。
同样的用法还出现在 examples/03_matmul_mix/matmul_mix.py、examples/07_swiglu/swiglu.py 等示例中,是 pyasc 算子开发的通用模式。
六、与其他 GlobalTensor 接口的联动
set_global_buffer完成初始化后,GlobalTensor 的其他成员方法才有意义(各接口文档见 docs/python-api/language/core.md 及其 generated 目录):
| 接口 | 作用 | 依赖关系 |
|---|---|---|
get_phy_addr([offset]) | 获取全局物理地址 | 依赖已绑定 buffer |
get_size() | 返回元素个数(有 shape 时返回 shape 乘积,否则由 IR 查询) | 依赖绑定时的长度信息 |
get_value(offset) | 按偏移读取单个元素 | 依赖已绑定 buffer |
set_value(offset, value) | 按偏移写入单个元素 | 依赖已绑定 buffer |
set_l2_cache_hint(mode, rw_mode) | 设置 L2 Cache 命中模式 | 依赖已绑定 buffer |
set_shape_info(shape_info) | 设置形状信息 | 依赖已绑定 buffer |
从实现上看,get_size在无 shape 时会生成asc_GlobalTensorGetSizeOp由 IR 层面查询,这也说明在编译期声明buffer_size有助于编译器推导出更准确的长度信息。
七、测试验证
仓库单元测试 python/test/unit/language/core/test_global_tensor.py 直接覆盖了该接口的两种调用形态:
def test_set_global_buffer(mock_launcher_run): @asc.jit def kernel_set_global_buffer(x: asc.GlobalAddress) -> None: x_gm = asc.GlobalTensor() x_gm.set_global_buffer(x) x_gm.set_global_buffer(x, 8192) data = MockTensor(asc.float32) kernel_set_global_buffer1 assert mock_launcher_run.call_count == 1该测试验证了:不传buffer_size与传buffer_size(如 8192)两种重载均可在 JIT kernel 内正常编译运行。同文件中的test_get_phy_addr、test_get_size、test_get_value、test_operator、test_set_l2_cache_hint、test_set_shape_info等用例都遵循"先set_global_buffer再调用其他方法"的结构,进一步印证它是 GlobalTensor 一切操作的初始化前提。
八、注意事项与最佳实践
- 先绑定、后使用:GlobalTensor 的读写、搬运、查址接口全部依赖
set_global_buffer完成初始化,忘记调用会在后续指令生成或运行时暴露问题; - 类型一致性:
buffer必须携带正确的DataType,GlobalTensor 的元素类型由它推导,类型不一致会导致后续指令语义错误; - 长度边界自担:
buffer_size声明的是元素个数,且"需自行保证不会超出实际数据的长度",这是开发者需要严格遵守的内存安全约定,框架不负责越界检查; - 多核场景善用指针偏移:结合
asc.get_block_idx()与GlobalAddress.__add__做核级分块,是 pyasc 标准的多核数据处理模式,可参考 examples/01_add/add.py 等示例; - 无长度绑定的场景:如果后续操作不依赖长度信息,可以省略
buffer_size,但建议在需要data_copy、get_size等长度相关操作时显式传入,帮助编译期推导; - JIT 上下文限制:该 API 带
@require_jit标记,只能在@asc.jit装饰的 kernel 函数中调用,不能在普通 Python 代码中直接使用。
掌握set_global_buffer,就等于掌握了 pyasc 算子从"全局内存"出发的第一步——正确绑定地址与长度,后续的搬运、计算、写回流程才能在此基础上可靠展开。
【免费下载链接】pyasc本项目为Python用户提供算子编程接口,支持在昇腾AI处理器上加速计算,接口与Ascend C一一对应并遵守Python原生语法。项目地址: https://gitcode.com/cann/pyasc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考