CANN/asc-devkit原子操作关键特性
关键特性说明确定性计算是指在相同输入条件下无论执行次数或执行环境如何变化总能产生完全一致输出结果的计算过程。确定性计算为系统稳定性和实验可验证性提供保障。确定性计算概述为引出原子操作场景下非确定性计算的问题我们构建如下常见的确定性计算场景首先通过单组浮点数数据搬运完成GM的初始化随后启动原子累加操作最后经由多次数据搬运在GM上对多组浮点数数据进行累加。具体伪代码如下1. 向GM搬运数据data0; // 数据搬运覆盖GM原有随机值期望GM数据为data0。 2. SetAtomicAdd(); // 开启原子累加后续从UB/L0C Buffer/L1 Buffer到GM的搬运均执行原子累加。 3. 向GM搬运data1; // 带随路原子操作的数据搬运期望GM数据为data0 data1。 4. 向GM搬运data2; // 带随路原子操作的数据搬运期望GM数据为data0 data1 data2。 5. 向GM搬运data3; // 带随路原子操作的数据搬运期望GM数据为data0 data1 data2 data3。如下图1所示开发者的预期结果指令发射的顺序能够严格对应实际指令执行顺序多次执行该段代码无论执行多少次最终GM数据均为data0 data1 data2 data3结果完全一致实现确定性计算。图 1确定性计算场景GM上数据变化过程但实际情况是若开发者不做干预程序每次运行时这些指令的执行顺序都可能发生变化最终导致GM数据与预期结果不一致。下面列举两种可能的指令执行顺序及其对应的执行流程。非确定性计算结果1图 2非确定性计算场景1GM上数据变化过程如图2所示该场景中指令执行流程如下初始状态GM数据为随机值向GM搬运data0GM数据被初始化为data0执行SetAtomicAdd为后续搬运指令开启原子累加GM数据为data0三次带随路原子操作的搬运指令乱序实际执行顺序为搬出data2→搬出data3→搬出data1最终GM上数据为data0 data2 data3 data1。非确定性计算的产生原因1带随路原子操作的搬运指令乱序由于浮点数加法不满足结合律即(ab)c!a(bc)使得最终GM数据data0 data2 data3 data1与预期的data0 data1 data2 data3存在偏差。带随路原子操作的搬运指令乱序导致最终结果产生偏差的前提条件有以下三条原子操作类型为原子累加最大值、最小值运算满足结合律原子操作数据类型为浮点数整数加法满足结合律带随路原子操作的搬运指令达到3条及以上浮点数加法满足交换律但是不满足结合律。非确定性计算结果2图 3非确定性计算场景2GM上数据变化过程如图3所示该场景中指令执行流程如下初始状态GM数据为随机值执行SetAtomicAdd为后续搬运指令开启原子累加GM数据为随机值先后执行两次带随路原子操作的搬运指令执行顺序为“搬出data1→搬出data2”GM数据为随机值 data1 data2向GM搬运data0GM上累加的结果被data0覆盖GM数据为data0最后执行data3的搬运最终GM上数据为data0 data3。非确定性计算的产生原因2开启原子累加前的普通搬运指令与开启原子操作的搬运指令之间发生乱序会导致GM上已完成原子操作的数据被data0错误覆盖进而产生非确定性计算结果。此类乱序导致结果偏差无需满足任何前提条件开发者无需再区分原子操作类型、原子操作数据类型也无需考虑带随路原子操作的搬运指令数量是否达到3条及以上。确定性计算实现方案根据导致非确定性计算的两个根因下面也从解决这两个方面描述确定性计算实现的方案。核心思想是在指令之间插入适当的同步使每次程序运行时相关指令都按照预期确定的顺序执行最终保证每次执行程序输出的结果都相同。具体来说包含以下两个方面开启原子累加前的搬运指令与开启原子操作的指令之间插入同步如下伪代码所示在指令1与2之间插入同步能够确保开始原子操作前GM的初始值符合预期。开启原子累加操作后多条搬运指令之间的同步指令3与4、4与5之间插入同步能够确保浮点数累加的顺序符合预期。[!CAUTION]注意 开启原子操作的指令与后续搬运指令之间不需要开发者插入同步。// 整个原子累加在同一个核内执行控制5个指令的执行顺序为1→2→3→4→5。 1. 向GM搬运数据data0; // 数据搬运覆盖GM原有随机值期望GM数据为data0。 核内同步 2. SetAtomicAdd(); // 开启原子累加后续从UB/L0C Buffer/L1 Buffer到GM的搬运均执行原子累加。 // 指令2与3间无需插入同步。 3. 向GM搬运data1; // 开启原子累加后的数据搬运期望GM数据为data0 data1。 核内同步 4. 向GM搬运data2; // 开启原子累加后的数据搬运期望GM数据为data0 data1 data2。 核内同步 5. 向GM搬运data3; // 开启原子累加后的数据搬运期望GM数据为data0 data1 data2 data3。如下伪代码所示当上述指令都在不同核中执行时需要将上述的核内同步替换为核间同步。// 整个原子累加在4个不同核中执行控制4个核执行的顺序为“核0→核1→核2→核3”。 if (GetBlockIdx 0) { 向GM搬运数据data0; 核间同步 } else if (GetBlockIdx 1) { 核间同步 SetAtomicAdd(); 向GM搬运data1; 核间同步 } else if (GetBlockIdx 2) { 核间同步 SetAtomicAdd(); 向GM搬运data2; 核间同步 } else if (GetBlockIdx 3) { 核间同步 SetAtomicAdd(); 向GM搬运data3; }下面介绍如何基于硬件同步指令实现核内同步以及如何基于软件同步方案实现核间同步。核内同步搬运指令和开启原子操作的指令流水类型如下表所示当上述指令在同一个核内执行时开发者按需插入单流水同步或者多流水同步可参考样例中scenarioNum1的分支。表 1原子操作确定性计算相关指令的流水类型指令名称流水类型DataCopyPIPE_MTE3FixpipePIPE_FIXSetAtomicAdd/SetAtomicMax/SetAtomicMinPIPE_S核间同步由于当前未提供用于控制不同核之间执行顺序的硬件同步接口因此确定性计算场景下的核间同步需通过软件仿真实现。针对纯Vector算子、纯Cube算子及Mix同时包含Vector与Cube计算算子三种场景所采用的软件同步方案存在差异具体如下表所示。表 2核间同步软件实现方案算子类型软件同步方案说明纯Vector算子方案1通过多对IBSet和IBWait的接口组合可实现多个AIV间的同步可参考样例中scenarioNum2的分支。方案1支持指定部分AIV参与同步并可控制各AIV的执行顺序。纯Vector算子方案2通过InitDetermineComputeWorkspace、NotifyNextBlock和WaitPreBlock三个接口配合使用确保所有AIV核按照blockIdx升序执行。方案2要求所有AIV必须参与同步且执行顺序固定为blockIdx升序。纯Cube算子通过GM中的信号量实现核间同步先建立一对AIC核之间的同步进而扩展至多个AIC核之间的同步可参考样例中scenarioNum3的分支。通过Scalar单元访问GM时需考虑多核间数据一致性问题。Mix算子通过GM中的信号量实现核间同步先建立一对核之间的同步进而扩展至多个核之间的同步。通过Scalar单元访问GM时需考虑多核间数据一致性问题。此处的“核”可以是AIV或者AIC。下图展示了两个核之间如何通过GM中的信号量进行核间同步上一个核完成数据搬运或开启原子操作后会通过Scalar单元向核间共享的GM中的信号量写入值1表示自己的任务已完成。上一个核中也需要插入核内同步当上一个核内有多条搬运指令时它们之间需要插入核内同步1。Scalar单元向GM写数据之前必须保证前序所有搬运指令都已经执行完成因此它们之间也需要插入核内同步2。当前核在执行搬运任务前会通过Scalar单元不断读取该信号量的值。如果信号量不等于1当前核会进入阻塞等待状态当检测到信号量等于1时当前核会解除阻塞开始执行自己的数据搬运或原子操作。为确保信号量等于1之前当前核不会执行搬运指令需要在搬运指令之前插入核内同步3。图 4一对核之间软件同步方案流程图Scalar单元访问GM上的信号量存在两种访问方式经过DCache访问使用GlobalTensor的成员函数GetValue和SetValue进行操作。这种情况下开发者需要手动调用DataCacheCleanAndInvalid接口以确保多核间数据的一致性。不通过DCache访问使用WriteGmByPassDCache和ReadGmByPassDCache。这种方式无需额外操作即可保证多核间数据的一致性。两种方案在性能上的差异不经过DCache性能会较差但是如果读写GM数据较少可以考虑使用不经过DCache的方法。如图4所示核间同步方案中也需要与核内同步配合使用现将三处核内同步作用说明如下核内同步1可选核内存在多条数据搬运指令时通过该同步保证各搬运操作严格按顺序执行。核内同步2必备等待前一个核全部任务执行完毕后才允许Scalar单元向全局内存信号量写入1。核内同步3必备等待Scalar单元检测到信号量更新为1后当前核再启动后续任务执行。调用示例核内同步确定性计算对应scenarioNum1所有指令在同一个AIV内执行// 在原子累加之前以src0Local初始化GM。 AscendC::DataCopy(dstGlobal, src0Local, DATA_LENGTH); // 确保在原子累加之前gm的初始化完成。 AscendC::PipeBarrierPIPE_MTE3(); // 开启原子累加。 // 预期累加执行的顺序是src2Local src3Local src1Local。 AscendC::SetAtomicAddfloat(); AscendC::DataCopy(dstGlobal, src2Local, DATA_LENGTH); // 两条搬运指令之间需要调用PipeBarrierPIPE_MTE3()确保src2Local和src3Local的搬运顺序正确。 AscendC::PipeBarrierPIPE_MTE3(); AscendC::DataCopy(dstGlobal, src3Local, DATA_LENGTH); AscendC::PipeBarrierPIPE_MTE3(); AscendC::DataCopy(dstGlobal, src1Local, DATA_LENGTH); AscendC::DisableDmaAtomic(); // GM结果data0 data1 data2 data3。纯Vector算子核间同步对应scenarioNum2四次搬运分别在四个不同的AIV内执行// 预期的4个核执行的顺序是核0--核2--核3--核1。 // 核0负责在原子累加之前以src0Local初始化GM核2、3、1负责按顺序做原子累加。 if (blockIdx 0) { AscendC::LocalTensorfloat src0Local inQueueSrc0.AllocTensorfloat(); AscendC::DataCopy(src0Local, src0Global, DATA_LENGTH); AscendC::SetFlagAscendC::HardEvent::MTE2_MTE3(0); AscendC::WaitFlagAscendC::HardEvent::MTE2_MTE3(0); // 在原子累加之前以src0Local初始化GM。 AscendC::DataCopy(dstGlobal, src0Local, DATA_LENGTH); // 核0通知核2核0的src0Local初始化已完成。 auto sync_buf vecIn.AllocTensorint32_t(); AscendC::IBSet(src4Global, sync_buf, 0, 0); // 最后两个参数int32_t blockIdx, int32_t eventID。 inQueueSrc0.FreeTensor(src0Local); vecIn.FreeTensor(sync_buf); AscendC::printf(”核的下标%d\n“, blockIdx); } else if (blockIdx 1) { AscendC::LocalTensorfloat src1Local inQueueSrc1.AllocTensorfloat(); // 核1等待核3直到核3数据已完成累加。 auto sync_buf vecIn.AllocTensorint32_t(); AscendC::IBWait(src4Global, sync_buf, 3, 0); // 最后两个参数int32_t blockIdx, int32_t eventID。 AscendC::DataCopy(src1Local, src1Global, DATA_LENGTH); AscendC::SetFlagAscendC::HardEvent::MTE2_MTE3(0); AscendC::WaitFlagAscendC::HardEvent::MTE2_MTE3(0); AscendC::DisableDmaAtomic(); AscendC::SetAtomicAddfloat(); AscendC::DataCopy(dstGlobal, src1Local, DATA_LENGTH); AscendC::DisableDmaAtomic(); vecIn.FreeTensor(sync_buf); inQueueSrc1.FreeTensor(src1Local); AscendC::printf(”核的下标%d\n“, blockIdx); } else if (blockIdx 2) { AscendC::LocalTensorfloat src2Local inQueueSrc2.AllocTensorfloat(); // 核2等待核0直到核0数据已完成累加。 auto sync_buf vecIn.AllocTensorint32_t(); AscendC::IBWait(src4Global, sync_buf, 0, 0); // 最后两个参数int32_t blockIdx, int32_t eventID。 AscendC::DataCopy(src2Local, src2Global, DATA_LENGTH); AscendC::SetFlagAscendC::HardEvent::MTE2_MTE3(0); AscendC::WaitFlagAscendC::HardEvent::MTE2_MTE3(0); AscendC::DisableDmaAtomic(); AscendC::SetAtomicAddfloat(); AscendC::DataCopy(dstGlobal, src2Local, DATA_LENGTH); AscendC::DisableDmaAtomic(); // 核2通知核3核2数据已经累加完成。 AscendC::IBSet(src4Global, sync_buf, 2, 0); vecIn.FreeTensor(sync_buf); inQueueSrc2.FreeTensor(src2Local); AscendC::printf(”核的下标%d\n“, blockIdx); } else if (blockIdx 3) { // 核3等待核2直到核2数据已完成累加。 auto sync_buf vecIn.AllocTensorint32_t(); AscendC::IBWait(src4Global, sync_buf, 2, 0); // 最后两个参数int32_t blockIdx, int32_t eventID。 AscendC::LocalTensorfloat src3Local inQueueSrc3.AllocTensorfloat(); AscendC::DataCopy(src3Local, src3Global, DATA_LENGTH); // AscendC::DumpTensor(src3Local,0,8); AscendC::SetFlagAscendC::HardEvent::MTE2_MTE3(0); AscendC::WaitFlagAscendC::HardEvent::MTE2_MTE3(0); AscendC::DisableDmaAtomic(); AscendC::SetAtomicAddfloat(); AscendC::DataCopy(dstGlobal, src3Local, DATA_LENGTH); AscendC::DisableDmaAtomic(); // 核3通知核1核3数据已经累加完成。 AscendC::IBSet(src4Global, sync_buf, 3, 0); vecIn.FreeTensor(sync_buf); inQueueSrc3.FreeTensor(src3Local); AscendC::printf(”核的下标%d\n“, blockIdx); } // GM结果data0 data1 data2 data3。纯Cube核间同步对应scenarioNum3四次搬运分别在四个不同的AIC内执行// 预期的4个核执行的顺序是核0--核2--核3--核1。 // 核0负责在原子累加之前以src0Local初始化GM核2、3、1负责按顺序做原子累加。 if (blockIdx 0) { AscendC::DataCopy(src0Local, src0Global, DATA_LENGTH); AscendC::SetFlagAscendC::HardEvent::MTE2_MTE3(0); AscendC::WaitFlagAscendC::HardEvent::MTE2_MTE3(0); // 在原子累加之前以src0Local初始化GM。 AscendC::DataCopy(dstGlobal, src0Local, DATA_LENGTH); // 核0通知核2核0的src0Local初始化已完成。 // 在通知下一个核之前等待当前核的任务完成。 AscendC::SetFlagAscendC::HardEvent::MTE3_S(0); AscendC::WaitFlagAscendC::HardEvent::MTE3_S(0); AscendC::WriteGmByPassDCacheint32_t(addr blockIdx * 32, 1); } else if (blockIdx 1) { int32_t preblockIdx 3; // 核1等待核3直到核3数据已完成累加。 while (true) { int32_t value AscendC::ReadGmByPassDCacheint32_t(addr preblockIdx * 32); if (value 1) { AscendC::WriteGmByPassDCacheint32_t(addr preblockIdx * 32, 0); break; } } // 开始当前核的任务之前等待上一个核的通知。 AscendC::SetFlagAscendC::HardEvent::S_MTE2(0); AscendC::WaitFlagAscendC::HardEvent::S_MTE2(0); AscendC::DataCopy(src1Local, src1Global, DATA_LENGTH); AscendC::SetFlagAscendC::HardEvent::MTE2_MTE3(0); AscendC::WaitFlagAscendC::HardEvent::MTE2_MTE3(0); AscendC::DisableDmaAtomic(); AscendC::SetAtomicAddfloat(); AscendC::DataCopy(dstGlobal, src1Local, DATA_LENGTH); AscendC::DisableDmaAtomic(); } else if (blockIdx 2) { int32_t preblockIdx 0; // 核2等待核0直到核0数据已完成累加。 while (true) { int32_t value AscendC::ReadGmByPassDCacheint32_t(addr preblockIdx * 32); if (value 1) { AscendC::WriteGmByPassDCacheint32_t(addr preblockIdx * 32, 0); break; } } // 开始当前核的任务之前等待上一个核的通知。 AscendC::SetFlagAscendC::HardEvent::S_MTE2(0); AscendC::WaitFlagAscendC::HardEvent::S_MTE2(0); AscendC::DataCopy(src2Local, src2Global, DATA_LENGTH); AscendC::SetFlagAscendC::HardEvent::MTE2_MTE3(0); AscendC::WaitFlagAscendC::HardEvent::MTE2_MTE3(0); AscendC::DisableDmaAtomic(); AscendC::SetAtomicAddfloat(); AscendC::DataCopy(dstGlobal, src2Local, DATA_LENGTH); AscendC::DisableDmaAtomic(); // 在通知下一个核之前等待当前核的任务完成。 AscendC::SetFlagAscendC::HardEvent::MTE3_S(0); AscendC::WaitFlagAscendC::HardEvent::MTE3_S(0); AscendC::WriteGmByPassDCacheint32_t(addr blockIdx * 32, 1); } else if (blockIdx 3) { int32_t preblockIdx 2; // 核3等待核2直到核2数据已完成累加。 while (true) { int32_t value AscendC::ReadGmByPassDCacheint32_t(addr preblockIdx * 32); if (value 1) { AscendC::WriteGmByPassDCacheint32_t(addr preblockIdx * 32, 0); break; } } // 开始当前核的任务之前等待上一个核的通知。 AscendC::SetFlagAscendC::HardEvent::S_MTE2(0); AscendC::WaitFlagAscendC::HardEvent::S_MTE2(0); AscendC::DataCopy(src3Local, src3Global, DATA_LENGTH); AscendC::SetFlagAscendC::HardEvent::MTE2_MTE3(0); AscendC::WaitFlagAscendC::HardEvent::MTE2_MTE3(0); AscendC::DisableDmaAtomic(); AscendC::SetAtomicAddfloat(); AscendC::DataCopy(dstGlobal, src3Local, DATA_LENGTH); AscendC::DisableDmaAtomic(); // 在通知下一个核之前等待当前核的任务完成。 AscendC::SetFlagAscendC::HardEvent::MTE3_S(0); AscendC::WaitFlagAscendC::HardEvent::MTE3_S(0); AscendC::WriteGmByPassDCacheint32_t(addr blockIdx * 32, 1); } // GM结果data0 data1 data2 data3。完整样例请参考数据搬运随路原子操作做确定性计算的样例。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考