CANN pyasc 算子编程接口详解:asc.language.basic.sub 按元素求差全指南
【免费下载链接】pyasc本项目为Python用户提供算子编程接口,支持在昇腾AI处理器上加速计算,接口与Ascend C一一对应并遵守Python原生语法。项目地址: https://gitcode.com/cann/pyasc
asc.language.basic.sub是 CANN pyasc 为 Python 开发者提供的矢量向量减法接口,与 Ascend C 中的Sub一一对应,用于在昇腾 AI 处理器的 Vector 核上按元素计算dst = src0 - src1。本文以该接口的官方 API 文档为骨架,结合 vec_binary.py 等仓库源码与单测用例,系统讲解三种重载签名的使用方式、参数语义、底层 IR 分发机制与约束条件,帮助读者在算子内核(Kernel)中正确、高效地完成向量减法运算。
接口概述:一次调用,三种形态
sub接口将 Ascend C 的Sub指令封装为符合 Python 原生语法的函数,核心能力是按元素求差:dst[i] = src0[i] - src1[i]。它在源码 vec_binary.py 中通过@overload声明了三种重载形态,分别覆盖"整段连续计算"与"高维切分计算"两类场景:
# 形态一:tensor 前 n 个数据计算 asc.language.basic.sub(dst: LocalTensor, src0: LocalTensor, src1: LocalTensor, count: int, is_set_mask: bool = True) -> None # 形态二:tensor 高维切分计算(mask 连续模式) asc.language.basic.sub(dst: LocalTensor, src0: LocalTensor, src1: LocalTensor, mask: int, repeat_times: int, repeat_params: BinaryRepeatParams, is_set_mask: bool = True) -> None # 形态三:tensor 高维切分计算(mask 逐 bit 模式) asc.language.basic.sub(dst: LocalTensor, src0: LocalTensor, src1: LocalTensor, mask: List[int], repeat_times: int, repeat_params: BinaryRepeatParams, is_set_mask: bool = True) -> None三种形态的运行期实现统一收敛到op_impl("sub", ...)(见 utils.py),Python 层会根据实参形态自动完成分派,无需开发者手动判断。
对应的 Ascend C 函数原型
sub接口与昇腾原生算子开发接口中的Sub模板函数一一对应,其 C++ 原型如下:
template <typename T> __aicore__ inline void Sub(const LocalTensor<T>& dst, const LocalTensor<T>& src0, const LocalTensor<T>& src1, const int32_t& count);template <typename T, bool isSetMask = true> __aicore__ inline void Sub(const LocalTensor<T>& dst, const LocalTensor<T>& src0, const LocalTensor<T>& src1, uint64_t mask[], const uint8_t repeatTimes, const BinaryRepeatParams& repeatParams);template <typename T, bool isSetMask = true> __aicore__ inline void Sub(const LocalTensor<T>& dst, const LocalTensor<T>& src0, const LocalTensor<T>& src1, uint64_t mask, const uint8_t repeatTimes, const BinaryRepeatParams& repeatParams);其中第一个原型对应 Python 的count形态,第二个对应mask: List[int]逐 bit 模式,第三个对应mask: int连续模式。这组 C++ 原型同样由 set_binary_docstring 统一生成,与add、mul、max、min等二元矢量运算接口保持一致。
参数说明
| 参数 | 类型 | 语义 |
|---|---|---|
dst | LocalTensor | 目的操作数,保存减法结果。支持的 TPosition 为VECIN/VECCALC/VECOUT |
src0, src1 | LocalTensor | 源操作数。支持的 TPosition 为VECIN/VECCALC/VECOUT |
count | int | 参与计算的元素个数(形态一) |
mask | int/List[int] | 控制每次迭代内参与计算的元素。int为连续模式,List[int]为逐 bit 模式(形态二、三) |
repeat_times | int | 重复迭代次数(形态二、三) |
repeat_params | BinaryRepeatParams | 控制操作数地址步长的参数(形态二、三) |
is_set_mask | bool | 是否在接口内部设置 mask,默认True |
关于 LocalTensor
dst、src0、src1均为 LocalTensor 类型,即算子内核中位于本地内存(UB)的矢量张量。三种 TPosition 的含义为:VECIN表示矢量输入、VECCALC表示矢量计算中间缓冲、VECOUT表示矢量输出。
关于 BinaryRepeatParams:地址步长控制
BinaryRepeatParams是形态二、三中控制地址步长的核心结构体,其定义位于 types.py。构造签名为:
asc.BinaryRepeatParams(dst_blk_stride=1, src0_blk_stride=1, src1_blk_stride=1, dst_rep_stride=8, src0_rep_stride=8, src1_rep_stride=8)六个字段的职责可归纳为两组:
- blk_stride(块内步长):
dst_blk_stride、src0_blk_stride、src1_blk_stride控制单次迭代内相邻数据块(DataBlock)之间的地址步长。设为 1 表示迭代内数据连续读取/写入。 - rep_stride(迭代间步长):
dst_rep_stride、src0_rep_stride、src1_rep_stride控制相邻两次迭代之间的地址步长。默认值 8 恰好对应 fp16 场景下一次迭代 256 字节的数据跨度(128 个 fp16 元素 × 2 字节 = 256 字节 = 8 个 32 字节 DataBlock)。
从源码看,BinaryRepeatParams最终通过builder.create_asc_ConstructOp构造为 IR 值,字段统一以uint8类型编码(见 types.py)。正是因为该结构承载了地址编排信息,高维、非连续张量才能通过"迭代 + 步长"的方式分片参与运算。
约束说明
- 地址对齐约束:操作数地址对齐要求遵循昇腾社区《Ascend C 算子开发接口》中"通用说明和约束-通用地址对齐约束"的规定,涉及矢量指令对起始地址的字节对齐要求。
- 地址重叠约束:操作数地址重叠约束同样参考《Ascend C 算子开发接口》中"通用说明和约束-通用地址重叠约束"章节,使用时应避免
dst与src0/src1出现非法的读写重叠。 - 符号重载运算量:使用整个 tensor 参与计算的接口(对应
count形态)时,运算量为目的LocalTensor的总长度。 - 数据类型一致性(来自源码校验,见 utils.py):
sub的源操作数与目的操作数支持float16、float32、int16、int32四种数据类型;src0与src1必须同类型,且dst与二者类型必须一致,否则抛出TypeError。调用前请确保三个 tensor 使用同一 dtype。
调用示例
以下示例均可在内核函数中直接使用(asc为import asc或import asc.language as asc后的命名空间)。
1. tensor 前 n 个数据计算(count 形态)
# 一次性对 dst 的前 512 个元素执行 src0 - src1 asc.sub(dst, src0, src1, count=512)这是最简洁的形态,适合连续内存块的整体运算,运算量为count指定的元素个数。
2. tensor 高维切分计算- mask 连续模式
mask = 128 # repeat_times = 4,一次迭代计算128个数,共计算512个数 # dst_blk_stride, src0_blk_stride, src1_blk_stride = 1,单次迭代内数据连续读取和写入 # dst_rep_stride, src0_rep_stride, src1_rep_stride = 8,相邻迭代间数据连续读取和写入 params = asc.BinaryRepeatParams(1, 1, 1, 8, 8, 8) asc.sub(dst, src0, src1, mask=mask, repeat_times=4, repeat_params=params)连续模式下,mask为整数,表示每次迭代从起始位置连续参与计算的元素个数。示例中一次迭代处理 128 个元素,重复 4 次,共计算 512 个数。
3. tensor 高维切分计算- mask 逐 bit 模式
mask = [uint64_max, uint64_max] # repeat_times = 4,一次迭代计算128个数,共计算512个数 # dst_blk_stride, src0_blk_stride, src1_blk_stride = 1,单次迭代内数据连续读取和写入 # dst_rep_stride, src0_rep_stride, src1_rep_stride = 8,相邻迭代间数据连续读取和写入 params = asc.BinaryRepeatParams(1, 1, 1, 8, 8, 8) asc.sub(dst, src0, src1, mask=mask, repeat_times=4, repeat_params=params)逐 bit 模式下,mask为uint64列表,列表中每个 bit 对应一个元素,bit 为 1 表示该元素参与计算,bit 为 0 则跳过。示例中mask = [uint64_max, uint64_max]即两个 64 位全 1 掩码,共覆盖 128 个元素(64 + 64 bit),配合repeat_times = 4同样完成 512 个元素的计算,但元素筛选粒度更细。
源码级实现剖析:三种形态如何映射到底层算子
sub的 Python 实现非常精简,真正的逻辑在公共函数op_impl中(见 utils.py)。调用asc.sub(dst, src0, src1, ...)后发生如下过程:
- 类型校验:进入
check_type,校验dst/src0/src1的数据类型是否落在{float16, float32, int16, int32}集合内,且三者类型一致(utils.py 中"sub": valids即指向该集合)。 - 重载分派:
OverloadDispatcher依据关键字参数匹配三种注册分支:mask=RuntimeInt+repeat_times+repeat_params→ 调用builder.create_asc_SubL0Op(连续模式);mask=list+repeat_times+repeat_params→ 调用builder.create_asc_SubL1Op(逐 bit 模式,mask 列表逐项转换为uint64IR 值);count=RuntimeInt→ 调用builder.create_asc_SubL2Op(count 被转换为int32IR 值)。
- IR 构建:三种形态最终在 vec_binary.py 的
sub函数中,通过create_asc_SubL0Op/create_asc_SubL1Op/create_asc_SubL2Op构造出对应的 Asc IR 算子节点,进入后续编译流水线。
也就是说,L0/L1/L2后缀对应了昇腾矢量指令的三种编程视图:连续 mask、逐 bit mask、以及按 count 的整段计算。这一设计在add、mul、max、min等二元矢量接口中完全复用,因此理解sub的实现即可触类旁通。
另外值得留意的是,接口的 docstring(即 asc.language.basic.sub.md 的生成来源)由 set_binary_docstring 以cpp_name="Sub"统一模板化生成,这也是sub与add等接口文档结构高度一致的原因。
测试用例佐证
仓库单测 test_vector_binary.py 中的test_sub_kernel覆盖了sub的全部三种调用形态:
@asc.jit def sub_kernel(): x_local = asc.LocalTensor(dtype=asc.float16, pos=asc.TPosition.VECIN, addr=0, tile_size=512) y_local = asc.LocalTensor(dtype=asc.float16, pos=asc.TPosition.VECIN, addr=0, tile_size=512) z_local = asc.LocalTensor(dtype=asc.float16, pos=asc.TPosition.VECOUT, addr=0, tile_size=512) asc.sub(z_local, x_local, y_local, count=512) params = asc.BinaryRepeatParams(1, 1, 1, 8, 8, 8) asc.sub(z_local, x_local, y_local, mask=512, repeat_times=1, repeat_params=params) uint64_max = 2**64 - 1 mask = [uint64_max, uint64_max] asc.sub(z_local, x_local, y_local, mask=mask, repeat_times=1, repeat_params=params) sub_kernel[1]()该用例给出了一个可直接套用的完整内核骨架:以asc.LocalTensor声明VECIN源张量与VECOUT目的张量,在@asc.jit修饰的核函数中依次调用三种形态的sub。开发者可参照此结构,结合 examples 目录下的算子样例(如 01_add/add.py)快速搭建自己的减法算子。
相关接口与延伸
- sub_relu:按元素求差后再与 0 比较取较大值(ReLU),实现见 vec_binary.py,对应 Ascend C
SubRelu。 - sub_relu_cast:求差 + ReLU + 类型转换的组合运算,见 vec_vconv.py。
- 同一族二元接口:
add、mul、max、min、div等共享相同的重载结构与op_impl分发机制,掌握sub后可直接套用其调用约定。
在算子开发实践中,建议优先使用count形态处理连续整块数据以获取最佳可读性;当张量维度较高或需要按块跳过部分元素时,再选用mask+repeat_times+BinaryRepeatParams的高维切分形态,并通过调整blk_stride与rep_stride精确控制地址布局。
【免费下载链接】pyasc本项目为Python用户提供算子编程接口,支持在昇腾AI处理器上加速计算,接口与Ascend C一一对应并遵守Python原生语法。项目地址: https://gitcode.com/cann/pyasc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考