自主管理内存与同步的编程范式
在基于Pipe进行算子开发的方式中,由Pipe(TPipe类)统一管理Device端内存等资源,开发者无需感知内存管理、DoubleBuffer流水、同步等处理,只需要按照计算流编写算子即可,但由此也带来了一些运行时开销(如TPipe创建、InitBuffer等)。
基于以上原因,Ascend C提供了静态Tensor编程方式,相比基于Pipe的编程方式,这种方式避免了TPipe内存管理初始化过程(约数百纳秒),从而减少了运行时开销,更有助于开发者实现极致性能。通过直接构造指定地址和存储位置的LocalTensor,并将其传递给计算、搬运等API进行编程,提供了更高的灵活性。然而,这种编程方式也带来了更高的开发复杂性,需要开发者自行管理DoubleBuffer和同步流水,并且只能使用Ascend C的基础API,而非全部功能。
两种编程方式的对比如下:
编程范式
- AI Core包括多种内存单元,比如用于矢量计算的Unified Buffer和用于矩阵计算的L1 Buffer、L0A Buffer、L0B Buffer、L0C Buffer等内存资源。
- AI Core包括多条流水,比如Vector/Cube/Scalar计算流水,MTE1、MTE2、MTE3搬运流水等,每条流水并行执行。
静态Tensor编程方式下,开发者完全自主的管理AI Core上的所有内存资源,调用Ascend C提供的搬运或者计算类API编写算子,并根据数据依赖关系插入对应的同步事件,以达成最优性能。
下图是一个典型矢量算子的示意图,开发者首先根据业务计算量进行数据分块处理,之后根据核内的数据依赖关系完成同步事件的插入:
内存管理
静态Tensor编程方式下,开发者可以使用两种方式创建Tensor:
- 通过LocalMemAllocator指定硬件位置进行Tensor分配。
LocalMemAllocator是一种线性内存分配器,开发者可以调用Alloc方法进行内存分配,地址分配从0开始,根据调用次序依次向后进行线性分配,LocalMemAllocator只是一个简单的线性分配器,并不提供内存释放以及其它内存管理的能力。在不关注Bank冲突场景或者算子初始功能开发时,可以使用LocalMemAllocator简化算子编写,在后续性能优化时切换到使用LocalTensor进行地址分配的方式。
- 通过LocalTensor构造函数创建Tensor,极致性能场景推荐使用此方式。
开发者可以使用LocalTensor构造函数直接指定内存地址,实现内存的完全自主管理(本质上无需申请和释放内存)。使用时,需根据需求合理指定地址(不超过物理存储上限),并在保证功能正确的前提下进行内存复用。如果需要通过规避Bank冲突或者复用内存来获得极致性能时,推荐使用该方式。
1 2 3 4 5 6 7 8 9 10 |
// 方式1:使用LocalMemAllocator进行内存分配 AscendC::LocalMemAllocator<AscendC::Hardware::UB> ubAllocator; AscendC::LocalTensor<float> xLocalPing = ubAllocator.Alloc<AscendC::TPosition::VECCALC, float, TILE_LENGTH>(); AscendC::LocalTensor<float> yLocalPing = ubAllocator.Alloc<AscendC::TPosition::VECCALC, float, TILE_LENGTH>(); AscendC::LocalTensor<float> zLocalPing = ubAllocator.Alloc<AscendC::TPosition::VECCALC, float, TILE_LENGTH>(); // 方式2:直接使用LocalTensor构造函数构造Tensor AscendC::LocalTensor<float> xLocalPing(AscendC::TPosition::VECCALC, xAddrPing, TILE_LENGTH); AscendC::LocalTensor<float> yLocalPing(AscendC::TPosition::VECCALC, yAddrPing, TILE_LENGTH); AscendC::LocalTensor<float> zLocalPing(AscendC::TPosition::VECCALC, zAddrPing, TILE_LENGTH); |
同步管理
根据前文介绍的硬件架构,AI Core内部异步并行计算存在多条流水(包括矢量计算、矩阵计算、数据搬入、数据搬出等),多条流水之间存在数据依赖时,需要插入对应的同步事件。静态Tensor编程方式下,开发者使用SetFlag/WaitFlag(ISASI)和PipeBarrier(ISASI)手动插入同步,事件的类型和事件ID由开发者自行管理,但需要注意事件ID不能使用6和7(可能与内部使用的事件ID出现冲突,进而出现未定义行为)。另外由于需要使用SetFlag/WaitFlag/PipeBarrier底层同步接口(属于ISASI硬件体系结构相关的接口),无法保证跨硬件版本兼容。
在同步依赖中,根据数据依赖和指令执行关系,存在两种依赖关系,即正向同步(循环内依赖)与反向同步(循环间依赖):
- 正向同步
在本次数据搬入和计算之间,插入MTE2_V(矢量计算流水等待MT2搬运流水)同步事件,确保数据搬入之后再进行计算;在本次数据计算和搬出之间,插入V_MTE3(MTE3搬运流水等待矢量计算流水)同步事件,确保数据计算完成后再进行搬出。
- 反向同步
在上一次的数据计算和本次数据搬入之间,插入V_MTE2(MT2搬运流水等待矢量计算流水)同步事件,确保上一次的数据计算完成后,本次的数据再进行搬入。防止本次的数据会覆盖掉上一次未计算完成的数据;在上一次的数据搬出和本次数据计算之间,插入MTE3_V(矢量计算流水等待MT3搬运流水)同步事件,确保上一次的数据搬出后,再进行本次数据的计算。防止本次的数据会覆盖掉上一次未搬出的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
AscendC::LocalTensor<float> xLocal(AscendC::TPosition::VECCALC, xAddr, TILE_LENGTH); AscendC::LocalTensor<float> yLocal(AscendC::TPosition::VECCALC, yAddr, TILE_LENGTH); AscendC::LocalTensor<float> zLocal(AscendC::TPosition::VECCALC, zAddr, TILE_LENGTH); for (int i = 0; i < loopCount; i++) { // dependency of PIPE_V & PIPE_MTE2 caused by xLocal/yLocal between 2 sequential loops if (i != 0) { AscendC::WaitFlag<AscendC::HardEvent::V_MTE2>(EVENT_ID0); } AscendC::DataCopy(xLocal, xGm[i * TILE_LENGTH], TILE_LENGTH); AscendC::DataCopy(yLocal, yGm[i * TILE_LENGTH], TILE_LENGTH); // dependency of PIPE_MTE2 & PIPE_V caused by xLocal/yLocal in one single loop AscendC::SetFlag<AscendC::HardEvent::MTE2_V>(EVENT_ID0); AscendC::WaitFlag<AscendC::HardEvent::MTE2_V>(EVENT_ID0); if (i != 0) { // dependency of PIPE_MTE3 & PIPE_V caused by zLocal between 2 sequential loops AscendC::WaitFlag<AscendC::HardEvent::MTE3_V>(EVENT_ID0); } AscendC::Add(zLocal, xLocal, yLocal, TILE_LENGTH); if (i != (loopCount - 1)) { // dependency of PIPE_V & PIPE_MTE2 caused by xLocal/yLocal between 2 sequential loops AscendC::SetFlag<AscendC::HardEvent::V_MTE2>(EVENT_ID0); } // dependency of PIPE_V & PIPE_MTE3 caused by zLocal in one single loop AscendC::SetFlag<AscendC::HardEvent::V_MTE3>(EVENT_ID0); AscendC::WaitFlag<AscendC::HardEvent::V_MTE3>(EVENT_ID0); AscendC::DataCopy(zGm[i * TILE_LENGTH], zLocal, TILE_LENGTH); if (i != (loopCount - 1)) { // dependency of PIPE_MTE3 & PIPE_V caused by zLocal between 2 sequential loops AscendC::SetFlag<AscendC::HardEvent::MTE3_V>(EVENT_ID0); } } |
流水优化
在基于TPipe的编程范式中,开发者只需要在InitBuffer时指定buffer数量为2,即可自动开启Double Buffer。但是静态Tensor编程方式下,开发者需要手动开启Double Buffer,具体示例如下,完整样例请参考静态Tensor编程样例中的Double Buffer示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// ping AscendC::LocalTensor<float> xLocalPing(AscendC::TPosition::VECCALC, xAddrPing, TILE_LENGTH); AscendC::LocalTensor<float> yLocalPing(AscendC::TPosition::VECCALC, yAddrPing, TILE_LENGTH); AscendC::LocalTensor<float> zLocalPing(AscendC::TPosition::VECCALC, zAddrPing, TILE_LENGTH); // pong AscendC::LocalTensor<float> xLocalPong(AscendC::TPosition::VECCALC, xAddrPong, TILE_LENGTH); AscendC::LocalTensor<float> yLocalPong(AscendC::TPosition::VECCALC, yAddrPong, TILE_LENGTH); AscendC::LocalTensor<float> zLocalPong(AscendC::TPosition::VECCALC, zAddrPong, TILE_LENGTH); // double buffer AscendC::SetFlag<AscendC::HardEvent::MTE3_MTE2>(EVENT_ID0); AscendC::SetFlag<AscendC::HardEvent::MTE3_MTE2>(EVENT_ID1); for (int i = 0; i < loopCount; i++) { int32_t eventID = (i % 2 == 0 ? EVENT_ID0 : EVENT_ID1); AscendC::LocalTensor<float> &xLocal = (i % 2 == 0 ? xLocalPing : xLocalPong); AscendC::LocalTensor<float> &yLocal = (i % 2 == 0 ? yLocalPing : yLocalPong); AscendC::LocalTensor<float> &zLocal = (i % 2 == 0 ? zLocalPing : zLocalPong); // dependency of PIPE_MTE3 & PIPE_MTE2 caused by xLocal/yLocal between 2 sequential loops AscendC::WaitFlag<AscendC::HardEvent::MTE3_MTE2>(eventID); AscendC::DataCopy(xLocal, xGm[i * TILE_LENGTH], TILE_LENGTH); AscendC::DataCopy(yLocal, yGm[i * TILE_LENGTH], TILE_LENGTH); // dependency of PIPE_MTE2 & PIPE_V caused by xLocal/yLocal in one single loop AscendC::SetFlag<AscendC::HardEvent::MTE2_V>(eventID); AscendC::WaitFlag<AscendC::HardEvent::MTE2_V>(eventID); AscendC::Add(zLocal, xLocal, yLocal, TILE_LENGTH); // dependency of PIPE_V & PIPE_MTE3 caused by zLocal in one single loop AscendC::SetFlag<AscendC::HardEvent::V_MTE3>(eventID); AscendC::WaitFlag<AscendC::HardEvent::V_MTE3>(eventID); AscendC::DataCopy(zGm[i * TILE_LENGTH], zLocal, TILE_LENGTH); // dependency of PIPE_MTE3 & PIPE_MTE2 caused by zLocal between 2 sequential loops AscendC::SetFlag<AscendC::HardEvent::MTE3_MTE2>(eventID); } AscendC::WaitFlag<AscendC::HardEvent::MTE3_MTE2>(EVENT_ID0); AscendC::WaitFlag<AscendC::HardEvent::MTE3_MTE2>(EVENT_ID1); |
以下为不使能DoubleBuffer和使能DoubleBuffer的流水示意图。多数情况下,采用DoubleBuffer能有效提升Vector的时间利用率,缩减算子执行时间,详细内容可参考DoubleBuffer。
使用约束和限制
静态Tensor编程方式需要遵循以下约束和限制:
- 开发者不能使用TPipe/TQue/TQueBind/TBufPool等框架接口,和上述框架接口混用可能会出现未定义行为。
- 只能使用部分API。具体支持的API列表见支持的API范围。因为不在列表范围内的API内部依赖TPipe分配事件ID,可能会和开发者定义的事件ID产生冲突。
- 同步事件需要由开发者使用SetFlag/WaitFlag(ISASI)和PipeBarrier(ISASI)手动插入,事件的类型和事件ID由开发者自行管理,但需要注意事件ID不能使用6和7(可能与内部使用的事件ID出现冲突,进而出现未定义行为)。
- 由于需要使用SetFlag/WaitFlag/PipeBarrier底层同步接口(属于ISASI硬件体系结构相关的接口),无法保证算子跨硬件版本兼容。
- Kernel入口处需要开发者手动调用InitSocState接口用来初始化全局状态寄存器。因为全局状态寄存器处于不确定状态,如果不调用该接口,可能导致算子执行过程中出现未定义行为。在TPipe框架编程中,初始化过程由TPipe完成,无需开发者关注。
支持的API范围
接口分类 |
接口名称 |
---|---|
基础API > 标量计算 |
ScalarGetCountOfValue、ScalarCountLeadingZero、ScalarCast、CountBitsCntSameAsSignBit、ScalarGetSFFValue |
基础API > 矢量计算 > 基础算术 |
Exp、Ln、Abs、Reciprocal、Sqrt、Rsqrt、Relu、VectorPadding、Add、Sub、Mul、Div、Max、Min、BilinearInterpolation、Adds、Muls、Maxs、Mins、LeakyRelu、Axpy |
基础API > 矢量计算 > 逻辑计算 |
Not、And、Or |
基础API > 矢量计算 > 复合计算 |
AddRelu、AddReluCast、AddDeqRelu、SubRelu、SubReluCast、MulAddDst、MulCast、FusedMulAdd、FusedMulAddRelu |
基础API > 矢量计算 > 比较指令 |
Compare、Compare(结果存入寄存器)、CompareScalar、GetCmpMask、SetCmpMask |
基础API > 矢量计算 > 选择指令 |
Select、GatherMask |
基础API > 矢量计算 > 精度转换指令 |
Cast、CastDeq |
基础API > 矢量计算 > 归约指令 |
WholeReduceMax、WholeReduceMin、WholeReduceSum、BlockReduceMax、BlockReduceMin、BlockReduceSum、PairReduceSum、RepeatReduceSum、GetReduceMaxMinCount |
基础API > 矢量计算 > 数据转换 |
Transpose、TransDataTo5HD |
基础API > 矢量计算 > 数据填充 |
Duplicate |
基础API > 矢量计算 > 排序组合 |
ProposalConcat、ProposalExtract、RpSort16、MrgSort4、GetMrgSortResult |
基础API > 矢量计算 > 数据分散/数据收集 |
Gather、Scatter |
基础API > 矢量计算 > 掩码操作 |
SetMaskCount、SetMaskNorm、SetVectorMask、ResetMask |
基础API > 矢量计算 > 量化设置 |
SetDeqScale |
基础API > 数据搬运 > DataCopy |
基础数据搬运 |
基础API > 数据搬运 |
InitConstValue、LoadData、SetAippFunctions、LoadImageToLocal、LoadUnzipIndex、LoadDataUnzip、SetLoadDataBoundary、SetLoadDataPaddingValue |
基础API > 内存管理与同步控制 > 核内同步 |
SetFlag/WaitFlag、PipeBarrier |
基础API > 缓存处理 |
DataCachePreload、DataCacheCleanAndInvalid、ICachePreLoad |
基础API > 系统变量访问 |
GetBlockNum、GetBlockIdx、GetDataBlockSizeInBytes、GetArchVersion、GetTaskRation、InitSocState、GetProgramCounter、CheckLocalMemoryIA |
基础API > 原子操作 |
SetAtomicAdd、SetAtomicNone |
基础API > 矩阵计算 |
Mmad |
接口分类 |
接口名称 |
备注 |
---|---|---|
基础API > 标量计算 |
ScalarGetCountOfValue、ScalarCountLeadingZero、ScalarCast、CountBitsCntSameAsSignBit、ScalarGetSFFValue、ToBfloat16、ToFloat |
- |
基础API > 矢量计算 > 基础算术 |
Exp、Ln、Abs、Reciprocal、Sqrt、Rsqrt、Relu、Add、Sub、Mul、Div、Max、Min、BilinearInterpolation、Adds、Muls、Maxs、Mins、LeakyRelu、Axpy |
- |
基础API > 矢量计算 > 逻辑计算 |
Not、And、Or、ShiftLeft、ShiftRight |
- |
基础API > 矢量计算 > 复合计算 |
AddRelu、AddReluCast、AddDeqRelu、SubRelu、SubReluCast、MulAddDst、MulCast、FusedMulAdd、FusedMulAddRelu |
- |
基础API > 矢量计算 > 比较指令 |
Compare、Compare(结果存入寄存器)、CompareScalar、GetCmpMask、SetCmpMask |
- |
基础API > 矢量计算 > 选择指令 |
Select、GatherMask |
- |
基础API > 矢量计算 > 精度转换指令 |
Cast、CastDeq |
- |
基础API > 矢量计算 > 归约指令 |
WholeReduceMax、WholeReduceMin、WholeReduceSum、BlockReduceMax、BlockReduceMin、BlockReduceSum、PairReduceSum、RepeatReduceSum、GetAccVal、GetReduceMaxMinCount |
- |
基础API > 矢量计算 > 数据转换 |
Transpose、TransDataTo5HD |
- |
基础API > 矢量计算 > 数据填充 |
Duplicate、Brcb |
- |
基础API > 矢量计算 > 排序组合 |
Sort32、MrgSort、GetMrgSortResult |
- |
基础API > 矢量计算 > 数据分散/数据收集 |
Gather、Gatherb |
- |
基础API > 矢量计算 > 掩码操作 |
SetMaskCount、SetMaskNorm、SetVectorMask、ResetMask |
- |
基础API > 矢量计算 > 量化设置 |
SetDeqScale |
- |
基础API > 数据搬运 > DataCopy |
基础数据搬运 |
不支持VECIN/VECCALC/VECOUT -> TSCM通路的数据搬运。 |
增强数据搬运 |
不支持VECIN/VECCALC/VECOUT -> TSCM通路的数据搬运。 |
|
切片数据搬运 |
- |
|
随路转换ND2NZ搬运 随路转换NZ2ND搬运 随路量化激活搬运 |
不支持VECIN/VECCALC/VECOUT -> TSCM通路的数据搬运。 |
|
基础API > 数据搬运 |
Copy、DataCopyPad、SetPadValue、SetFixPipeConfig、SetFixpipeNz2ndFlag、SetFixpipePreQuantFlag、InitConstValue、LoadData、LoadDataWithTranspose、SetAippFunctions、LoadImageToLocal、LoadDataWithSparse、SetFmatrix、SetLoadDataBoundary、SetLoadDataRepeat、SetLoadDataPaddingValue、Fixpipe |
- |
基础API > 内存管理与同步控制 > 核内同步 |
SetFlag/WaitFlag、PipeBarrier、DataSyncBarrier |
- |
基础API > 内存管理与同步控制 > 核间同步 |
CrossCoreSetFlag、CrossCoreWaitFlag |
- |
基础API > 缓存处理 |
DataCachePreload、DataCacheCleanAndInvalid、ICachePreLoad、GetICachePreloadStatus |
- |
基础API > 系统变量访问 |
GetBlockNum、GetBlockIdx、GetDataBlockSizeInBytes、GetArchVersion、GetTaskRation、InitSocState、GetProgramCounter、GetSubBlockNum、GetSubBlockIdx、GetSystemCycle、 CheckLocalMemoryIA |
- |
基础API > 原子操作 |
SetAtomicAdd、SetAtomicType、SetAtomicNone、SetAtomicMax、SetAtomicMin、SetStoreAtomicConfig、GetStoreAtomicConfig |
- |
基础API > 矩阵计算 |
Mmad、MmadWithSparse、SetHF32Mode、SetHF32TransMode、SetMMLayoutTransform |
- |
高阶API > C++标准库 > 算法 |
max、min、index_sequence |
- |
高阶API > C++标准库 > 容器函数 |
tuple、get、make_tuple |
- |
高阶API > C++标准库 > 类型特性 |
is_convertible、is_base_of、is_same、enable_if、conditional |
- |
接口分类 |
接口名称 |
备注 |
---|---|---|
基础API > 标量计算 |
ScalarGetCountOfValue、ScalarCountLeadingZero、ScalarCast、CountBitsCntSameAsSignBit、ScalarGetSFFValue、ToBfloat16、ToFloat |
- |
基础API > 矢量计算 > 基础算术 |
Exp、Ln、Abs、Reciprocal、Sqrt、Rsqrt、Relu、Add、Sub、Mul、Div、Max、Min、BilinearInterpolation、Adds、Muls、Maxs、Mins、LeakyRelu、Axpy |
- |
基础API > 矢量计算 > 逻辑计算 |
Not、And、Or、ShiftLeft、ShiftRight |
- |
基础API > 矢量计算 > 复合计算 |
AddRelu、AddReluCast、AddDeqRelu、SubRelu、SubReluCast、MulAddDst、MulCast、FusedMulAdd、FusedMulAddRelu |
- |
基础API > 矢量计算 > 比较指令 |
Compare、Compare(结果存入寄存器)、CompareScalar、GetCmpMask、SetCmpMask |
- |
基础API > 矢量计算 > 选择指令 |
Select、GatherMask |
- |
基础API > 矢量计算 > 精度转换指令 |
Cast、CastDeq |
- |
基础API > 矢量计算 > 归约指令 |
WholeReduceMax、WholeReduceMin、WholeReduceSum、BlockReduceMax、BlockReduceMin、BlockReduceSum、PairReduceSum、RepeatReduceSum、GetAccVal、GetReduceMaxMinCount |
- |
基础API > 矢量计算 > 数据转换 |
Transpose、TransDataTo5HD |
- |
基础API > 矢量计算 > 数据填充 |
Duplicate、Brcb |
- |
基础API > 矢量计算 > 排序组合 |
Sort32、MrgSort、GetMrgSortResult |
- |
基础API > 矢量计算 > 数据分散/数据收集 |
Gather、Gatherb |
- |
基础API > 矢量计算 > 掩码操作 |
SetMaskCount、SetMaskNorm、SetVectorMask、ResetMask |
- |
基础API > 矢量计算 > 量化设置 |
SetDeqScale |
- |
基础API > 数据搬运 > DataCopy |
基础数据搬运 |
不支持VECIN/VECCALC/VECOUT -> TSCM通路的数据搬运。 |
增强数据搬运 |
不支持VECIN/VECCALC/VECOUT -> TSCM通路的数据搬运。 |
|
切片数据搬运 |
- |
|
随路转换ND2NZ搬运 随路转换NZ2ND搬运 随路量化激活搬运 |
不支持VECIN/VECCALC/VECOUT -> TSCM通路的数据搬运。 |
|
基础API > 数据搬运 |
Copy、DataCopyPad、SetPadValue、SetFixPipeConfig、SetFixpipeNz2ndFlag、SetFixpipePreQuantFlag、InitConstValue、LoadData、LoadDataWithTranspose、SetAippFunctions、LoadImageToLocal、LoadDataWithSparse、SetFmatrix、SetLoadDataBoundary、SetLoadDataRepeat、SetLoadDataPaddingValue、Fixpipe |
- |
基础API > 内存管理与同步控制 > 核内同步 |
SetFlag/WaitFlag、PipeBarrier、DataSyncBarrier |
- |
基础API > 内存管理与同步控制 > 核间同步 |
CrossCoreSetFlag、CrossCoreWaitFlag |
- |
基础API > 缓存处理 |
DataCachePreload、DataCacheCleanAndInvalid、ICachePreLoad、GetICachePreloadStatus |
- |
基础API > 系统变量访问 |
GetBlockNum、GetBlockIdx、GetDataBlockSizeInBytes、GetArchVersion、GetTaskRation、InitSocState、GetProgramCounter、GetSubBlockNum、GetSubBlockIdx、GetSystemCycle、 CheckLocalMemoryIA |
- |
基础API > 原子操作 |
SetAtomicAdd、SetAtomicType、SetAtomicNone、SetAtomicMax、SetAtomicMin、SetStoreAtomicConfig、GetStoreAtomicConfig |
- |
基础API > 矩阵计算 |
Mmad、MmadWithSparse、SetHF32Mode、SetHF32TransMode、SetMMLayoutTransform |
- |
高阶API > C++标准库 > 算法 |
max、min、index_sequence |
- |
高阶API > C++标准库 > 容器函数 |
tuple、get、make_tuple |
- |
高阶API > C++标准库 > 类型特性 |
is_convertible、is_base_of、is_same、enable_if、conditional |
- |
高阶API > 模板库函数 > type_traits |
is_convertible、is_base_of、is_same、enable_if、conditional |
- |