CANN/pto-isa内核开发者规则与限制
这个文档列出了一些kernel开发者使用auto模式的一些规则和限制。【免费下载链接】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不遵守这些规则可能导致以下任意后果无法编译可能是源码层面编译报错或者编译器挂掉结果错误例如精度问题差劲的性能1 - 控制流规则复杂的控制流尤其是循环会给编译器的自动同步尤其是涉及到double-buffering和不同pipe之间的精准同步带来挑战。 因为PTO AUTO编译器首先需要确保结果正确在遇到复杂控制流的情况下会趋向保守无法插入最优的同步指令导致性能受损。1.1 - 隔离第一个和最后一个循环的迭代任何单独隔离开循环的第一个和最后一个迭代的条件控制都需要使用固定形式的表达来使编译器能够静态分析。这样能让AUTO模式编译器剥离开第一次和最后一次迭代能显著简化自动同步的分析难度。例如for (int tile_id 0; tile_id total_tiles; tile_id) { if (tile_id 0) { TLOAD(srcTile, globalSrc); } ... if (tile_id total_tiles-1) { TSTORE(globalDst, dstTile); } }1.2 - 循环中的循环不变条件需要被外提对于一个在内层循环中的if语句如果其是循环不变条件其判断不依赖循环的induction variable则其应该被外提到这个循环之外。例如对下面的在内层循环中的if语句for (int tile_id 0; tile_id total_tiles; tile_id) { int next_tile tile_id total_tiles-1 ? tile_id 1 : -1; ... for (int subtile_id 0; subtile_id total_subtiles; subtile_id) { if (next_tile ! -1) { ... // computation here } } }应该写成for (int tile_id 0; tile_id total_tiles; tile_id) { int next_tile tile_id total_tiles-1 ? tile_id 1 : -1; ... if (next_tile ! -1) { for (int subtile_id 0; subtile_id total_subtiles; subtile_id) { ... // computation here } } }1.3 - 复杂的条件判断表达式针对一个较复杂的条件判断逻辑如果其用来判断PTO指令的执行与否强烈建议将其统一用一个bool变量表达之后再用此bool变量用作if/else if判断。例如if ((srcTile.GetValidRow() 16 || srcTile.GetValidCol() 16) srcTile.GetKAligned()) { TLOAD(srcTile, globalSrc1); } else { TLOAD(srcTile, globalSrc0); }最好写成如下形式bool cond (srcTile.GetValidRow() 16 || srcTile.GetValidCol() 16) srcTile.GetKAligned(); if (cond) { TLOAD(srcTile, globalSrc1); } else { TLOAD(srcTile, globalSrc0); }1.4 - 目前非常不推荐使用double/multi buffering目前对于double/multi buffering没有完全支持因为一旦kernel稍微变得复杂那使用double buffering往往会涉及到很多动态控制流让自动同步变得极其困难。 编译器正在调研设计专门的抽象接口带上一些约束供程序员使用来使能double/multi buffering从而能让编译器正确分析。2 - 内存分配相关规则AUTO模式下由于不能使用TASSIGN编译器无法自动得知两个tile之间的alias关系因为无法得知程序员的意图所以需要程序员显式告诉编译器两个tile的alias关系。2.1 使用TRESHAPE来告诉编译器两个tile拥有相同的首地址如果你想表达两个tile必须具有相同的首地址你可以使用TRESHAPE指令。这个指令manual和auto模式通用。例如TileSrcTypeA tileA; TileSrcTypeB tileB; // Invalid in auto mode TASSIGN(tileA, 0x0); TASSIGN(tileB, 0x0); // Correct in auto mode TRESHAPE(tileB, tileA);2.2 使用TSUBVIEW来告诉编译器tile B是tile A的一个subview和TRESHAPE目的相同但是用来表达tileB是tileA的一个subview。语义是tileB的地址是在tileA的首地址基础上加上一些rowOffset和colOffset而来。auto模式需要这个接口是因为需要专门的接口来告诉编译器两个tile之间的alias关系。详见docs/isa/TSUBVIEW_zh.md。示例:uint16_t rowOffset, colOffset; // can be runtime variable // addr(tileB) addr(tileA) offsets TileData tileA(...); TileData tileB(...); // Invalid in auto mode TASSIGN(tileA, 0x0); TASSIGN(tileB, 0x0 rowOffset * TileData::Col colOffset * 1 sizeof(T)); // Correct for auto mode TSUBVIEW(tileB, tileA, rowOffset, colOffset);2.3 - 记住auto模式的重要编程思维Tile一旦被定义了其地址不能在运行时被改变PTO AUTO编译器会为每一个定义的Tile变量自动分配内存。这个分配是一次性的意思是一旦被编译器自动分配之后就永远不会改变。 也就是说当你在auto模式下编程时如果你的代码逻辑依赖一个tile需要在运行时改变地址那这样的代码就不能在auto模式下正确运行。例如在manual模式下你可以这样TileData tile; for (int i 0; i N; i) { TASSIGN(tile, 0x100 * i); foo(tile); }manual模式下程序员拥有完全的自由可以在运行时的任何时间地点改变一个tile的地址然而这在auto模式是不允许的因为这样的动态性会给编译器的内存分配带来几乎不可能做到的巨大挑战因此编译器的内存分配是一次性的、静态的。因此对于auto模式来说一个至关重要的思维模式是把每个tile想象成一个C的引用其在被定义的时候它们的内存就已经被绑定了且永远不能再变。2.4 - 正确理解TRESHAPE和TSUBVIEW在auto模式下的语义在manual模式下这两个都是实际上的PTO指令它们在内部都是直接调用TASSIGN。这意味着就像上一条讲的理论上程序员可以使用它们在任何时间地点来改变一个Tile的地址。 然而在auto模式下它们不是可执行的PTO指令而只是单纯的对于编译器的提示用来表达两个tile之间的alias关系用的。因此它们不能用来改变tile的地址所以如果你用TRESHAPE或者TSUBVIEW在同一个tile上重复用作输出那是未定义行为比如TRESHAPE(tile0, tile1); foo(tile0); ... TSUBVIEW(tile0, tile2, 0, 0); bar(tile0);同时因为它们在auto模式下只是单纯的给编译器的hint因此它们在源码中的位置并不太重要。然而为了避免困惑还是建议将它们的调用放在紧接着输入和输出Tile的定义之后。3 - 通用规则3.1 - 不要在destination tile上调用TLOAD如果一个Tile只会被用作输出没有需要从GM copied-in的数据那就不要调用TLOAD。 首先这是没有必要的 其次在auto模式下可能造成数据踩踏。比如TLOAD(dstTile, dstGlobal); // redundant TLOAD(srcTile, srcGlobal); TEXP(dstTile, srcTile); TSTORE(dstGlobal, dstTile);在manual模式下程序员可以给srcTile和dstTile手动分配不同的地址这样的话没有任何问题。 但是在auto模式下这可能会有数据踩踏的问题这里srcTile和dstTile的生命周期不重叠因此编译器会给dstTile复用srcTile的地址。这种情况下由于两个TLOAD在pipe中同时执行就会造成数据踩踏。3.2 - 不要在kernel中直接调用CCE intrinsickernel开发者应该只调用PTO指令避免CCE intrinsics。两点原因CCE intrinsics的入参都是裸指针类型这在auto模式下是无法编译的Tilestruct里的TileDType类型在manual模式下定义是指针类型然而在auto模式下其定义是vector类型无法暴露出指针。PTO编译器的分析和优化都是在tile这个抽象层级进行的无论是manual模式下的优化比如tile fusion和auto模式自动同步和内存分配都是如此PTO编译器无法也不会识别CCE intrinsics因此无法正确自动插入同步。基于这个原因kernel开发者应该避免调用Tile::data()成员函数理论上讲这个接口不是给kernel开发者用的而只是给库开发者在tile function上使用的。3.3 - 尽量使用PtoSetWaitFlag或者TSYNC而不是set_flag和wait_flag在PtoSetWaitFlag和TSYNC的内部实现中存在manual和auto模式的隔离manual模式下是正常调用set_flag和wait_flag而auto模式下是no-op因此其不会对auto模式下编译器自动插入的同步产生冲突。如果kernel开发者直接调用set_flag和wait_flag则他们需要手动使用__PTO_AUTO__宏隔离开auto模式比较麻烦。【免费下载链接】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),仅供参考