CANN PTO-ISA TSUB 指令深度解析:Tile 逐元素减法(Elementwise Subtract)的语义、约束与后端实现
【免费下载链接】pto-isaParallel Tile Operation (PTO) is a virtual instruction set architecture designed by Ascend CANN, focusing on tile-level operations. This repository offers high-performance, cross-platform tile operations across Ascend platforms.项目地址: https://gitcode.com/cann/pto-isa
导读
TSUB 是 CANN PTO(Parallel Tile Operation)虚拟指令集中最基础的向量二元算术指令,实现两个 Tile 的逐元素减法(dst = src0 - src1),广泛用于神经网络推理与训练中的残差连接、差分计算、归一化减均值等场景。本文以 TSUB 指令参考文档 为主体,结合仓库内 NPU(A2A3/A5)、CPU 仿真三套实现与 ST 测试用例,系统讲解其数学语义、三种汇编层级、C++ 内建接口、平台约束、事件同步用法以及自动/手动两种编程模式,帮助读者在真实工程中正确使用并验证 TSUB。
指令语义与有效区域
数学语义
TSUB 对两个输入 Tilesrc0、src1执行逐元素(elementwise)减法,结果写入dst。对有效区域内的每个元素(i, j):
$$ \mathrm{dst}{i,j} = \mathrm{src0}{i,j} - \mathrm{src1}_{i,j} $$
该操作要求三个 Tile 元素类型一致(见下文"约束"小节),且不涉及任何广播或维度变换——src0、src1、dst在有效区域内按同一坐标一一对应。
有效区域(valid region)语义
TSUB 的迭代域完全由目标 Tile 决定:指令使用dst.GetValidRow()/dst.GetValidCol()作为计算范围,src0/src1假定与该范围兼容(本指令不通过显式运行时检查来验证输入的兼容性)。这与 PTO ISA 通用约定 中"默认使用valid_row = dst.GetValidRow()、valid_col = dst.GetValidCol()作为迭代域"的规则完全一致:
- 数学语义仅对
0 <= i < valid_row且0 <= j < valid_col的dst[i, j]做出定义; - 有效区域之外的元素值为未指定,不要假设其一定清零或保持不变;
- 物理形状
Rows/Cols与有效形状GetValidRow()/GetValidCol()相互独立:修改有效形状不会改变物理行步长RowStride(行主序下为Cols)。
汇编语法:从同步形式到两级抽象
TSUB 在不同抽象层级有对应的文本表示,均围绕同一操作tsub展开。
同步形式(PTO 汇编)
%dst = tsub %src0, %src1 : !pto.tile<...>AS Level 1(SSA)
SSA 形式显式标注操作数类型与返回类型,操作数类型为!pto.tile<...>:
%dst = pto.tsub %src0, %src1 : (!pto.tile<...>, !pto.tile<...>) -> !pto.tile<...>AS Level 2(DPS)
DPS(Data-Parallel Semantics)形式将输入与输出操作数分离,使用ins(...)/outs(...)子句,操作数类型为!pto.tile_buf<...>:
pto.tsub ins(%src0, %src1 : !pto.tile_buf<...>, !pto.tile_buf<...>) outs(%dst : !pto.tile_buf<...>)三种形式描述的是同一个操作在不同抽象层级的投影:同步形式面向最终指令编码,SSA 形式面向编译器中间表示,DPS 形式则显式表达"输入/输出缓冲"的资源语义。
C++ 内建接口
TSUB 的 C++ 内建函数声明于 include/pto/common/pto_instr.hpp,公共包含头为<pto/pto-inst.hpp>:
template <typename TileDataDst, typename TileDataSrc0, typename TileDataSrc1, typename... WaitEvents> PTO_INST RecordEvent TSUB(TileDataDst &dst, TileDataSrc0 &src0, TileDataSrc1 &src1, WaitEvents &... events);从源码看(include/pto/common/pto_instr.hpp#L209-L215),该接口的实际执行分为两步:
detail::PtoWaitEvents(events...):消费传入的等待事件,建立与前序指令(如TLOAD)的顺序关系;MAP_INSTR_IMPL(TSUB, dst, src0, src1):将调用映射到各平台的具体实现(A2A3、A5 或 CPU 仿真),并返回RecordEvent供后续指令(如TSTORE_VEC)继续链式依赖。
因此TSUB返回的事件对象可无缝衔接 TLOAD→TSUB→TSTORE 的流水线依赖链,这是 NPU 上保证内存流水线与向量流水线数据一致性的关键机制(事件模型详见 docs/coding/Event_zh.md)。
后端实现与平台差异
TSUB 是典型的"一次编写、多端执行"指令:同一套 C++ 接口在 A2A3、A5 与 CPU 仿真三套后端上有不同的底层实现,支撑跨平台可移植。
Atlas A2/A3(a2a3 后端)
实现位于 include/pto/npu/a2a3/TSub.hpp。核心要点:
- 通过
SubOp<T>模板定义二元算子,底层发射 CCE 向量指令vsub(dst, src0, src1, repeats, ...),默认 repeat 参数为(1, 1, 1, 8, 8, 8),即每次处理 8 个 block、间隔 8 个 block; TSub模板根据dstRowStride、src0RowStride、src1RowStride是否一致选择不同的BinaryInstr特化路径,支持三者在物理形状不同时仍按各自行步长正确寻址;TSubCheck通过static_assert在编译期检查:三者的DType必须一致("TSUB the data type of dst must be consistent with of src0 and src1")、必须是受支持的类型、布局必须行主序;运行时通过PTO_ASSERT检查src0/src1的有效形状与dst完全一致。
Ascend 950PR / Ascend 950DT(a5 后端)
实现位于 include/pto/npu/a5/TSub.hpp。与 A2A3 相比存在两点显著差异:
- 类型支持更广:新增
uint8_t、uint16_t、uint32_t、uint64_t、int8_t、int64_t、bfloat16_t等无符号与 64 位类型支持,而 A2A3 仅支持int32_t、int16_t、half、float; - 64 位类型走专用路径:当元素类型为
int64_t/uint64_t时,不直接走vsub寄存器路径,而是调用Int64Binary<Int64Op::Sub, ...>专用实现;其余类型则通过SubOp<T>发射vsub(reg_dst, reg_src0, reg_src1, preg, MODE_ZEROING),并支持通过VFImplKind version参数选择不同的向量函数(VF)实现。
CPU 仿真后端
实现位于 include/pto/cpu/TSub.hpp,用于无昇腾硬件的开发与调试环境。其语义与 NPU 完全对齐:
- 通过
cpu::parallel_for_rows(validRow, validCol, ...)按行并行遍历有效区域; - 循环体为
dst.SetElement(r, c, src0.GetElement(r, c) - src1.GetElement(r, c)),且标注了PTO_CPU_VECTORIZE_LOOP以支持向量化; - 同样通过
static_assert与PTO_ASSERT保证三个 Tile 类型一致、有效形状一致。
三套实现共享同一套约束与行为语义,这正是 PTO 虚拟指令集"一次编写、跨平台高效执行"的体现。
约束与检查
实现检查(Atlas A2/A3 训练/推理系列产品)
TileData::DType必须是以下之一:int32_t、int16_t、half、float(从源码结构看,A2A3 的TSubCheck还接受int、float16_t、float32_t等别名类型);- Tile 布局必须是行主序(
TileData::isRowMajor); - Tile 位置必须是向量(
TileData::Loc == TileType::Vec); - 静态有效边界:
TileData::ValidRow <= TileData::Rows且TileData::ValidCol <= TileData::Cols; - 运行时:
src0、src1与dst应具有相同的validRow/validCol。
实现检查(Ascend 950PR / Ascend 950DT)
TileData::DType必须是以下之一:uint32_t、int32_t、int64_t、uint64_t、uint16_t、int16_t、uint8_t、int8_t、bfloat16_t、float、half(Ascend 950PR/Ascend 950DT 架构新增无符号整型支持,Atlas A2/A3 系列仅支持有符号及浮点类型);- Tile 布局必须是行主序(
TileData::isRowMajor); - Tile 位置必须是向量(
TileData::Loc == TileType::Vec); - 静态有效边界:
TileData::ValidRow <= TileData::Rows且TileData::ValidCol <= TileData::Cols; - 运行时:
src0、src1与dst应具有相同的validRow/validCol。
有效区域
该操作使用dst.GetValidRow()/dst.GetValidCol()作为迭代域;src0/src1假定是兼容的(此操作中不通过显式运行时检查进行验证)。
编程示例
自动(Auto)模式
自动模式下,Tile 的资源放置与调度由编译器/运行时负责,用户只需声明 Tile 并调用指令:
#include <pto/pto-inst.hpp> using namespace pto; void example_auto() { using TileT = Tile<TileType::Vec, float, 16, 16>; TileT src0, src1, dst; TSUB(dst, src0, src1); }手动(Manual)模式
手动模式下,用户需要先用TASSIGN将 Tile 显式绑定到片上缓冲区地址,再发射指令,以精确控制数据布局:
#include <pto/pto-inst.hpp> using namespace pto; void example_manual() { using TileT = Tile<TileType::Vec, float, 16, 16>; TileT src0, src1, dst; TASSIGN(src0, 0x1000); TASSIGN(src1, 0x2000); TASSIGN(dst, 0x3000); TSUB(dst, src0, src1); }汇编示例(ASM)
自动模式的汇编形式(由编译器/运行时负责资源放置与调度):
# 自动模式:由编译器/运行时负责资源放置与调度。 %dst = pto.tsub %src0, %src1 : (!pto.tile<...>, !pto.tile<...>) -> !pto.tile<...>手动模式的汇编形式(先显式绑定资源,再发射指令):
# 手动模式:先显式绑定资源,再发射指令。 # 可选(当该指令包含 tile 操作数时): # pto.tassign %arg0, @tile(0x1000) # pto.tassign %arg1, @tile(0x2000) %dst = pto.tsub %src0, %src1 : (!pto.tile<...>, !pto.tile<...>) -> !pto.tile<...>PTO 汇编形式的完整对照:
%dst = tsub %src0, %src1 : !pto.tile<...> # AS Level 2 (DPS) pto.tsub ins(%src0, %src1 : !pto.tile_buf<...>, !pto.tile_buf<...>) outs(%dst : !pto.tile_buf<...>)实战调用链:TLOAD → TSUB → TSTORE
在真实 NPU kernel 中,TSUB 通常与TLOAD、TSTORE配合使用。以 A5 平台 ST 用例 tests/npu/a5/src/st/testcase/tsub/tsub_kernel.cpp 为例:
Event<Op::TLOAD, Op::TSUB> event0; Event<Op::TSUB, Op::TSTORE_VEC> event1; TLOAD(src0Tile, src0Global); event0 = TLOAD(src1Tile, src1Global); event1 = TSUB(dstTile, src0Tile, src1Tile, event0); TSTORE(dstGlobal, dstTile, event1);该示例展示了三条关键实践:
- 事件依赖链:
TLOAD返回的event0作为TSUB的等待事件传入,TSUB返回的event1又传递给TSTORE,确保加载完成后才执行减法、减法完成后才写回全局内存; - 显式地址绑定:手动模式下通过
TASSIGN(src0Tile, 0x0)等为每个 Tile 指定片上地址,并通过block_idx偏移实现多核分片; - 动态形状支持:
Tile<TileType::Vec, T, dstTileH, dstTileW, BLayout::RowMajor, -1, -1>配合GlobalTensor的动态Shape/Stride,允许物理 Tile 与有效区域(vRows/vCols)不同,例如 src0 为 16x128 而 dst 为 16x64 的场景。
测试与验证
仓库为 TSUB 提供了覆盖多平台、多数据类型的完整测试矩阵:
- NPU A5 平台(tests/npu/a5/src/st/testcase/tsub/main.cpp):覆盖
float、int32_t、int16_t、half(aclFloat16)、int64_t、uint64_t共 6 种数据类型,包含"三 Tile 物理形状一致"与"三 Tile 物理形状不同"两类用例,并专门构造了有效区域小于物理形状的场景(如case_half_16x64_16x128_16x128_16x63,即 dst 有效列为 63 而物理列为 64)。校验时对int64_t/uint64_t使用精确比较(ResultCmpExact),浮点类型使用容差比较(ResultCmp<T>,容差 0.001f); - A2A3 平台:对应用例位于 tests/npu/a2a3/src/st/testcase/tsub;
- CPU 仿真平台:位于 tests/cpu/st/testcase/tsub,包含
main.cpp、tsub_kernel.cpp与gen_data.py数据生成脚本,可在无 NPU 环境下验证算法正确性; - 代价模型:tests/costmodel/st/testcase/tsub/main.cpp 用于验证性能仿真/代价模型对 TSUB 的建模。
上述用例与 include/pto/npu/a2a3/TSub.hpp、include/pto/npu/a5/TSub.hpp、include/pto/cpu/TSub.hpp 三套实现相互印证,共同保障 TSUB 在跨平台语义上的一致性。
相关指令与扩展阅读
- TSUBS(标量减法):从 Tile 中逐元素减去一个标量(
dst = src - scalar),与 TSUB 互为补充,用于偏置消除、减均值等场景; - TADD(加法)、TMUL(乘法):与 TSUB 同属向量二元算术指令族,接口形态与约束体系一致;
- PTO ISA 通用约定:定义有效区域、行主序、事件同步等贯穿全部指令页的通用语义;
- Tile 编程模型:docs/coding/Tile_zh.md;事件模型:docs/coding/Event_zh.md;
- 公共头文件入口:include/pto/pto-inst.hpp,指令接口声明:include/pto/common/pto_instr.hpp。
小结
TSUB 是 PTO-ISA 中最基础的向量二元减法指令,其核心要点可概括为四点:一是以dst的有效区域为唯一迭代域;二是 A2A3 与 Ascend 950 系列在数据类型支持上存在差异(后者新增无符号与 64 位整型支持);三是通过WaitEvents事件参数天然融入 TLOAD→TSUB→TSTORE 流水线;四是自动/手动两种模式分别面向编译器托管与精细资源控制两类场景。掌握 TSUB 的语义与约束,是进一步理解 TADD、TMUL、TSUBS 等整个向量二元指令族的基础。
【免费下载链接】pto-isaParallel Tile Operation (PTO) is a virtual instruction set architecture designed by Ascend CANN, focusing on tile-level operations. This repository offers high-performance, cross-platform tile operations across Ascend platforms.项目地址: https://gitcode.com/cann/pto-isa
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考