多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

CANN PTO 地址式 GM FIFO 编程指南:用 TALLOC / TPUSH / TPOP / TFREE 构建跨核生产者-消费者数据流

CANN PTO 地址式 GM FIFO 编程指南:用 TALLOC / TPUSH / TPOP / TFREE 构建跨核生产者-消费者数据流 CANN PTO 地址式 GM FIFO 编程指南用 TALLOC / TPUSH / TPOP / TFREE 构建跨核生产者-消费者数据流【免费下载链接】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-isaPTOParallel Tile Operation为 Ascend CANN 提供的跨核Cube / Vector通信中除了常见的数据搬运式TPUSH(pipe, tile)/TPOP(pipe, tile)之外还存在一组地址式的 GM FIFO 接口TALLOC、TPUSH、TPOP、TFREE。它们不负责实际的数据搬运而是把当前 FIFO 槽位的 GM 地址计算并写入一个GlobalTensor对象由调用者自行用TSTORE/TLOAD读写数据——非常适合生产者把多个子块写入同一个槽位、消费者以多个视图分片读取的场景。读完本文你将掌握这四个接口的声明、参数语义、底层实现A2A3 架构的TPipe/RingFIFO、分裂地址计算规则以及如何基于 tests/npu/a2a3/src/st/testcase/tpushpop_subtile 的完整测试用例落地一套 Cube→Vector 的 GM FIFO 流水。从 Tile 式到地址式两种 TPUSH/TPOP 形态的差异PTO 的 FIFO 接口存在两种形态Tile 式TPUSH(pipe, tile)/TPOP(pipe, tile)。接口内部会同步完成等待槽位 → 计算地址 →TSTORE/TLOAD搬运数据 → 信号同步的完整闭环。此时数据形态被限制为单个Tile消费端拿到的就是整块数据。地址式本文主题TALLOC(pipe, gmTensor)/TPUSH(pipe, gmTensor)/TPOP(pipe, gmTensor)/TFREE(pipe, gmTensor)。这四个接口只管理 FIFO 同步与地址分配并把计算出的 GM 槽位地址写入GlobalTensor对象数据移动由调用者用普通TSTORE与TLOAD完成。地址式的价值在于当生产者或消费者需要把同一个 FIFO 槽位当作一个或多个GlobalTensor视图来访问时例如 Cube 核把一个槽位存成多个 sub-tileVector 核之后从该槽位按切片加载Tile 式的一次搬运一块就不够灵活而地址式可以完全交给上层自由组织视图与搬运粒度。接口声明与模板参数声明位置与函数签名四个接口的原生声明位于 include/pto/common/pto_instr.hpp对GlobalData模板参数使用std::enable_if_tis_global_data_vGlobalData, int 0约束确保只接受GlobalTensor类型template typename Pipe, typename GlobalData, TileSplitAxis Split, std::enable_if_tis_global_data_vGlobalData, int 0, typename... WaitEvents PTO_INST RecordEvent TALLOC(Pipe pipe, GlobalData gmTensor, WaitEvents ... events); template typename Pipe, typename GlobalData, TileSplitAxis Split, std::enable_if_tis_global_data_vGlobalData, int 0, typename... WaitEvents PTO_INST RecordEvent TPUSH(Pipe pipe, GlobalData gmTensor, WaitEvents ... events); template typename Pipe, typename GlobalData, TileSplitAxis Split, std::enable_if_tis_global_data_vGlobalData, int 0, typename... WaitEvents PTO_INST RecordEvent TPOP(Pipe pipe, GlobalData gmTensor, WaitEvents ... events); template typename Pipe, typename GlobalData, TileSplitAxis Split, std::enable_if_tis_global_data_vGlobalData, int 0, typename... WaitEvents PTO_INST RecordEvent TFREE(Pipe pipe, GlobalData gmTensor, WaitEvents ... events);四个函数统一返回RecordEvent其函数体内首先执行detail::PtoWaitEvents(events...)处理可选的事件等待随后分派到架构相关实现TALLOC_IMPL/TPUSH_IMPL/TPOP_IMPL/TFREE_IMPL。参数详解参数类型含义PipeTPipeFlagID, Direction, SlotSize, SlotNum, ...FIFO 状态的载体。持有槽位基地址、生产者/消费者计数器以及跨核标志flag的完整状态GlobalDataGlobalTensor类型编译期用于地址计算的类型级描述。其RawDType与静态 shape 决定分裂模式Split下的字节偏移gmTensorGlobalTensor对象运行期TALLOC与TPOP将计算出的 FIFO GM 地址写入该对象TPUSH与TFREE仅将其作为事务描述符不读取地址内容SplitTileSplitAxis枚举分裂模式TILE_NO_SPLIT、TILE_UP_DOWN或TILE_LEFT_RIGHTevents可变参数可选的 PTO 事件在接口体执行前完成同步关键点在于GlobalData是编译期描述符、gmTensor是运行期地址载体模板参数中的 shape/dtype 参与偏移计算而实际 GM 地址在执行TALLOC/TPOP后被写入gmTensor供后续TSTORE、TLOAD、TPUSH、TFREE使用。TPipe 与 RingFIFOFIFO 状态的载体TPipe的定义位于 include/pto/npu/a2a3/TPush.hpptemplate uint8_t FlagID, uint8_t DirType, uint32_t SlotSize, uint32_t SlotNum, uint32_t LocalSlotNum 2, bool IsNoSplit false, bool EN_UNIT_FLAG false struct TPipe { using RingFiFo RingFIFOSlotSize, SlotNum, LocalSlotNum; RingFiFo fifo; Producer prod; Consumer cons; // ... };FlagID跨核同步标志 ID。TPipe内部推导FlagIDPlusOne FlagID 1、FlagIDPlusTwo、FlagIDPlusThree使用DIR_BOTH双向通信时FlagID 1必须小于硬件上限MAX_SYC_ID源码中有对应的static_assert。DirType通信方向取值来自 include/pto/common/fifo.hpp 中的Direction枚举DIR_C2V 1Cube 生产、Vector 消费、DIR_V2C 2Vector 生产、Cube 消费、DIR_BOTH 3双向、DIR_V2C_CTRL 4。A2A3 上TPipe仅支持这四种方向。SlotSize/SlotNum每个槽位的字节大小与槽位数量。运行时基地址通过构造函数TPipe(__gm__ void* GM_SLOT_BUFFER, uint32_t C2V_CONSUMER_BUF, uint32_t V2C_CONSUMER_BUF)传入。RingFIFO结构体include/pto/common/fifo.hpp定义了 GM FIFO 的布局template int SlotSize, int SlotNum, int LocalSlotNum struct RingFIFO { __gm__ void* GM_SLOT_BUFFER nullptr; // Global memory uint32_t C2V_CONSUMER_BUF 0x0; // UB buffer uint32_t V2C_CONSUMER_BUF 0x0; // L1 buffer uint64_t V2C_CONTROL_BUF 0x0; // scalar buffer for control signals static constexpr uint32_t SLOT_SIZE SlotSize; static constexpr uint32_t SLOT_NUM SlotNum; static constexpr uint32_t LOCAL_SLOT_NUM LocalSlotNum; // ... };槽位在 GM 中的定位公式为(tileIndex % SLOT_NUM) * SLOT_SIZE即环形复用SlotNum个等长槽位。同步逻辑由Producer/Consumer两个内部结构完成生产者等待空闲槽位allocateC2V 场景下 Cube 侧执行wait_flag_dev(FlagIDPlusOne)V2C 场景下 Vector 侧执行同样的等待生产者提交数据recordC2V 下 Cube 执行ffts_cross_core_sync(PIPE_FIX, CV_CORES_SYNC, FlagID)V2C 下 Vector 执行ffts_cross_core_sync(PIPE_MTE3, CV_CORES_SYNC, FlagID)消费者等待数据wait执行wait_flag_dev(FlagID)消费者归还槽位free执行ffts_cross_core_sync(PIPE_MTE2, CV_CORES_SYNC, FlagIDPlusOne)。可以看到地址式接口复用了TPipe的同一套跨核 flag 机制只是把搬运这一步留给了调用者。生产者流程TALLOC → TSTORE → TPUSHTALLOC分配槽位并取得 GM 地址TALLOC在 A2A3 的实现位于 include/pto/npu/a2a3/TAlloc.hpp依次执行事件等待等待传入的events完成分配/等待空闲槽位当pipe.prod.getAllocateStatus() Pipe::shouldWaitFree(pipe.prod.tileIndex)为真时调用pipe.prod.allocate()等待消费者归还槽位槽位地址计算entryBase GM_SLOT_BUFFER (tileIndex % SLOT_NUM) * SLOT_SIZE并根据方向附加分裂偏移——C2V 下 Cube 生产者写入整个槽位不加偏移V2C 下 Vector 生产者写入子区域追加getSubAIVOffsetGlobalData, Split()生产者 tile 索引自增pipe.prod.tileIndex地址写入TASSIGN_IMPL(gmTensor, reinterpret_castDType*(entryBase))把计算出的 GM 地址写入gmTensor。TALLOC不写数据、也不通知消费者。调用者应使用TSTORE或其他 GM 写逻辑填充gmTensor所描述的槽位。TPUSH提交并通知消费者地址式TPUSH的实现非常精简include/pto/npu/a2a3/TPush.hpptemplate typename Pipe, typename GlobalData, TileSplitAxis Split, std::enable_if_tis_global_data_vGlobalData, int 0 PTO_INTERNAL void TPUSH_IMPL(Pipe pipe, GlobalData gmTensor) { (void)gmTensor; pipe.prod.record(); }它只做两件事事件等待由外层封装完成与pipe.prod.record()——通过ffts_cross_core_sync通知消费者数据已就绪。必须在槽位全部写入完成之后再调用TPUSH否则消费者可能读到未写完的数据。消费者流程TPOP → TLOAD → TFREETPOP等待数据并取得槽位地址TPOP的地址式实现位于 include/pto/npu/a2a3/TPop.hpp依次执行事件等待等待数据就绪pipe.cons.wait()通过wait_flag_dev(FlagID)阻塞到生产者TPUSH提交槽位地址计算entryBase GM_SLOT_BUFFER (tileIndex % SLOT_NUM) * SLOT_SIZE。C2V 下 Vector 消费者得到子区域视图追加getPopSubAIVOffsetGlobalData, Split()V2C 下 Cube 消费者不加偏移消费者 tile 索引自增pipe.cons.tileIndex地址写入TASSIGN_IMPL(gmTensor, entryBase)。TPOP不加载数据、也不归还槽位。之后调用者可以直接使用gmTensor也可以基于gmTensor.data()构造多个更窄的GlobalTensor视图再用TLOAD分片读取。TFREE归还槽位TFREE的地址式实现位于 include/pto/npu/a2a3/TFree.hpptemplate typename Pipe, typename GlobalData, TileSplitAxis Split PTO_INTERNAL void TFREE_IMPL(Pipe pipe, GlobalData gmTensor) { (void)gmTensor; // ... bool isFree pipe.cons.getFreeStatus() Pipe::shouldNotifyFree(static_castuint32_t(pipe.cons.tileIndex - 1)); if (isFree) { pipe.cons.free(); } return; }它基于上一次TPOP的 tile 索引tileIndex - 1判断是否需要发送归还通知shouldNotifyFree依据SyncPeriod周期性地通知实现稀疏同步优化随后执行pipe.cons.free()通过ffts_cross_core_sync(PIPE_MTE2, CV_CORES_SYNC, FlagIDPlusOne)让生产者可以复用该槽位。必须在槽位所有读取完成之后再调用TFREE。Split 地址计算详解地址式接口的分裂偏移与 include/pto/common/fifo.hpp 中的TileSplitAxis枚举对应分裂模式语义子核偏移TILE_NO_SPLIT不分裂单一写者/读者0TILE_UP_DOWN按行上下分半get_subblockid() * rows * cols * sizeof(dtype)TILE_LEFT_RIGHT按列左右分半get_subblockid() * cols * sizeof(dtype)以TALLOC侧的getSubAIVOffsetinclude/pto/npu/a2a3/TAlloc.hpp为例template typename GlobalData, TileSplitAxis Split PTO_INTERNAL uint64_t getSubAIVOffset() { if constexpr (Split TileSplitAxis::TILE_NO_SPLIT) { return 0; } constexpr int prodM GlobalData::staticShape[pto::GlobalTensorDim::DIM_3]; constexpr int prodN GlobalData::staticShape[pto::GlobalTensorDim::DIM_4]; if constexpr (Split TileSplitAxis::TILE_UP_DOWN) { return get_subblockid() * prodM * prodN * sizeof(typename GlobalData::RawDType); } else { // TILE_LEFT_RIGHT return get_subblockid() * prodN * sizeof(typename GlobalData::RawDType); } }偏移计算依赖GlobalData::staticShape[DIM_3]行数、GlobalData::staticShape[DIM_4]列数与GlobalData::RawDType元素类型。注意TILE_UP_DOWN要求行数为偶数、TILE_LEFT_RIGHT要求列数为偶数fifo.hpp 中枚举注释的硬性约束否则两个 AIV 子核的偏移会重叠或越界。典型用法要点对于DIR_C2VVector 消费者会收到同一个槽位的基地址再自行基于它构造视图。TPOP的GlobalData模板参数通常与生产者槽位描述一致因此TPOP把槽地址写入gmTensor后消费者可以从gmTensor.data()出发显式追加元素偏移构造更小的加载视图。对于DIR_V2C生产者侧分裂偏移计算方式对称Vector 核各自写入槽位的不同子区域供 Cube 消费者读取。当消费者想手动从整个 FIFO 槽位加载 sub-tile 时TALLOC与TPOP应使用同一个整槽GlobalData类型再从gmTensor.data()构造更窄的GlobalTensor加载视图——这正是地址式灵活性的核心。完整示例Cube 写整槽、Vector 分片读取原文档给出了一个自洽可读的示例Cube 生产者把 4 个[128, 128]累积结果写入一个[128, 512]FIFO 槽位2 个 Vector 核各自消费自己[64, 512]的半槽再按[16, 512]切片加载计算constexpr int M 128; constexpr int N 128; constexpr int RepeatN 4; constexpr int FullN N * RepeatN; constexpr int VecCores 2; constexpr int VecM 16; constexpr int VecLoadTimes M / (VecCores * VecM); using Pipe TPipe0, Direction::DIR_C2V, M * FullN * sizeof(float), 2; using SlotGlobal GlobalTensorfloat, Shape1, 1, 1, M, FullN, Stride1, 1, 1, FullN, 1; using StoreGlobal GlobalTensorfloat, Shape1, 1, 1, M, N, Stride1, 1, 1, FullN, 1; // TPOP uses this descriptor for split offset calculation, so each vector receives one [64, 512] half. using PopGlobal GlobalTensorfloat, Shape1, 1, 1, M / VecCores, FullN, Stride1, 1, 1, FullN, 1; // One 3D TLOAD gathers four [16, 128] blocks into one logical [16, 512] vector tile. using VecTileData TileTileType::Vec, float, RepeatN * VecM, N, BLayout::RowMajor, RepeatN * VecM, N; using LoadGlobal3D GlobalTensorfloat, Shape1, 1, RepeatN, VecM, N, Stride1, 1, N, FullN, 1; using OutGlobal3D GlobalTensorfloat, Shape1, 1, RepeatN, VecM, N, Stride1, 1, N, FullN, 1; Pipe pipe(fifoMem, 0x0, 0x0); // Cube producer: fill one [128, 512] FIFO slot with four [128, 128] stores. SlotGlobal pushGlobal; TALLOCPipe, SlotGlobal, TileSplitAxis::TILE_UP_DOWN(pipe, pushGlobal); for (int nTile 0; nTile RepeatN; nTile) { StoreGlobal storeGlobal(pushGlobal.data() nTile * N); TSTORE(storeGlobal, accTile); } TPUSHPipe, SlotGlobal, TileSplitAxis::TILE_UP_DOWN(pipe, pushGlobal); // Vector consumer: each vector consumes four [16, 512] row slices from its [64, 512] half. VecTileData vecTile; VecTileData dstTile; TASSIGN(vecTile, 0x0); TASSIGN(dstTile, 0x10000); PopGlobal popGlobal; TPOPPipe, PopGlobal, TileSplitAxis::TILE_UP_DOWN(pipe, popGlobal); uint32_t subBlockIdx get_subblockid(); for (int rowSlice 0; rowSlice VecLoadTimes; rowSlice) { size_t vecBaseRow static_castsize_t(M / VecCores) * static_castsize_t(subBlockIdx); size_t localRowOffset static_castsize_t(rowSlice * VecM); size_t outRowOffset (vecBaseRow localRowOffset) * static_castsize_t(FullN); LoadGlobal3D loadGlobal(popGlobal.data() localRowOffset * static_castsize_t(FullN)); TLOAD(vecTile, loadGlobal); TADDS(dstTile, vecTile, static_castfloat(3.14)); OutGlobal3D outGlobal(out outRowOffset); TSTORE(outGlobal, dstTile); } TFREEPipe, PopGlobal, TileSplitAxis::TILE_UP_DOWN(pipe, popGlobal);示例要点解读TALLOC使用的SlotGlobal描述整槽[128, 512]生产者通过 4 次TSTORE把[128, 128]切片依次放到列偏移nTile * N处TPOP使用的PopGlobal描述半槽[64, 512]M / VecCores行结合TILE_UP_DOWN分裂get_subblockid()为 0/1 的两个 Vector 核各自拿到上/下半槽地址消费者在popGlobal.data()基础上按rowSlice * VecM * FullN追加行偏移构造[16, 512]的LoadGlobal3D视图加载出方向写入独立 GM 内存out不占用 FIFO 槽位。测试用例验证从文档到可运行代码该文档所配套的实测用例位于 tests/npu/a2a3/src/st/testcase/tpushpop_subtile目录内包含tpushpop_subtile_kernel.cpp实际 kernel 源码通过__DAV_CUBE__/__DAV_VEC__宏区分 Cube/Vector 编译分支main.cpp基于 gtest 的启动与 golden 比对ResultCmp(..., 0.001f)gen_data.py生成输入与 golden 数据CMakeLists.txt用例构建配置。用例规模与参数main.cpp中的用例为case1_half_128x512TEST_F(TPushTpopSubtileTest, case1_half_128x512) { TPushTpopSubtileTestFuncaclFloat16, float, 1(128, 128, 128, 4); }即M128, K128, N128, RepeatN4输入half、输出float。host 侧为 FIFO 分配2 * m * n * repeatN * sizeof(float)的 GM 空间对应TPipe0, DIR_C2V, M * FullN * sizeof(float), 2中2 个[128, 512]槽位FIFO 深度 2。输入输出数据经aclrtMemcpy在 host/device 间搬运kernel 通过LaunchTPushTpopSubtilekey以1, nullptr, stream启动。Kernel 中的真实流程tpushpop_subtile_kernel.cpp 完整还原了文档描述的协议并补充了跨流水同步细节Cube 分支TLOAD加载aMatTile/bMatTile→set_flag/wait_flagMTE2→MTE1→M保证搬入完成 →TMOV到TileMat→TMATMUL(accTile, aTile, bTile)得到[128, 128]累积结果 →TALLOCMatPipe, PushGlobal, TILE_UP_DOWN取得槽地址 → 4 次TSTORE写列切片 →TPUSH通知 →pipe_barrier(PIPE_ALL)Vector 分支TPOPMatPipe, PopGlobal, TILE_UP_DOWN取得半槽地址 → 循环 4 次TLOAD加载[16, 512]→TADDS(dstTile, vecTile, 3.14)计算 →TSTORE写出 → 循环结束后TFREE归还槽位 →pipe_barrier(PIPE_ALL)。注意 kernel 中 Vector 侧通过set_flag/wait_flagPIPE_V/PIPE_MTE2/PIPE_MTE3把加载、计算、存储编排成流水而TPOP/TFREE只负责 GM FIFO 的跨核同步两者职责清晰分离。Golden 数据gen_data.py 生成的 golden 为matmul np.matmul(x1_gm.astype(output_type), x2_gm.astype(output_type)).astype(output_type) vec_tadds (np.tile(matmul, [1, repeat_n]) output_type(3.14)).astype(output_type)即先对[128, 128]的x1 x2结果按列重复 4 次得到[128, 512]再加3.14——与 kernel 中4 次列切片存储 Vector 侧TADDS 3.14一一对应最终在main.cpp中以 0.001 的相对误差阈值完成比对。使用要点与注意事项以下是原文档明确强调、并得到源码印证的使用纪律地址式接口不做数据搬运TALLOC/TPUSH/TPOP/TFREE内部不执行TSTORE或TLOAD搬运由调用者完成配对关系强制TALLOC必须与TPUSH配对TPOP必须与TFREE配对不能混用或漏掉地址回写语义TALLOC与TPOP会把计算出的 GM 地址写入传入的GlobalTensorTPUSH与TFREE则只把它当作事务描述符源码中通过(void)gmTensor;显式忽略其内容GlobalData的双重身份作为模板参数它是类型级 shape/dtype 描述符决定分裂偏移作为运行期对象它携带计算后的 GM 地址供后续TSTORE、TLOAD、TPUSH、TFREE使用同步时序TPUSH必须在槽位写入完成后调用TFREE必须在槽位读取完成后调用跨核通知通过 FFTSffts_cross_core_sync与wait_flag_dev完成flag ID 由TPipe的FlagID推导分裂约束TILE_UP_DOWN要求行数可被 2 整除、TILE_LEFT_RIGHT要求列数可被 2 整除两个 AIV 子核各取一半奇数场景需要TILE_UP_DOWN_ODD/TILE_LEFT_RIGHT_ODD等扩展模式fifo.hpp 中已定义视图构造从整槽加载 sub-tile 时TALLOC与TPOP使用同一个整槽GlobalData类型再基于gmTensor.data()构造窄视图避免让TPOP的分裂偏移与手动偏移叠加出错。延伸阅读ISA 规范文档TALLOC、TPUSH、TPOP、TFREE底层数据结构include/pto/common/fifo.hppRingFIFO、TileSplitAxis、Direction与 include/pto/npu/a2a3/TPush.hppTPipe的 Producer/Consumer 同步原语通用编程模型docs/coding/ProgrammingModel.md 与 docs/coding/GlobalTensor.md与 Tile 式接口的对比可参考TPUSH_IMPLPipe, TileProd, Split数据搬运版与TPUSH_IMPLPipe, GlobalData, Split地址同步版两个重载的实现差异。【免费下载链接】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),仅供参考
返回列表