PTO-ISA TPOP 指令详解:从 TPipe FIFO 弹出消费者 Tile 的 Cube-Vector 数据通路编程
【免费下载链接】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
TPOP 是 PTO(Parallel Tile Operation)虚拟指令集架构中用于Cube-Vector(CV)跨核通信的关键消费者侧指令:它从TPipe环形 FIFO 中弹出生产者写入的 tile,并在等待数据就绪同步后将其交给消费者计算单元。本文以 docs/isa/TPOP.md 为骨架,结合 include/pto/common/pto_instr.hpp 中的 intrinsic 声明、include/pto/cpu/TPop.hpp 的 CPU_SIM 实现以及 NPU 各后端TPush.hpp源码,系统讲解 TPOP 的操作语义、重载形式、切分模式、约束条件与实战用法。读完本文,你将掌握 C2V/V2C 场景下 TPOP 的完整编程套路,能够结合TPUSH、TFREE、TALLOC编写正确的 CV FIFO 数据流。
TPOP 在 CV FIFO 通信中的位置
在 PTO 的 Cube-Vector 流水线通信模型里,TPipe是一个带环形槽位(Ring FIFO)的跨核 FIFO,生产者与消费者通过三条指令协同:
TPUSH(Pipe&, TileData&, Split):生产者把 tile 存入当前 FIFO 槽位,并为消费者记录数据就绪同步;生产者索引在槽位地址计算完成后递增。TPOP(Pipe&, TileData&, Split):消费者等待生产者的数据就绪同步,把当前 FIFO 槽位加载进消费者 tile;消费者索引在槽位地址计算完成后递增。TFREE(Pipe&, Split):释放 FIFO 槽位空间,供生产者复用。
TPOP是这条链路中唯一由消费者侧执行的"取数"动作,其指令名即 "Tile Pop" 的缩写。它支持两类数据视图:
- TileData 流程(本地 tile 缓冲):
TPOP把槽位数据加载进本地Tile(向量 tile 或矩阵 tile),随后消费者可直接对 tile 计算; - GlobalData 流程(全局内存槽位视图):
TPOP只把GlobalTensor视图绑定到槽位基地址,不做数据搬运,消费者用TLOAD等指令按需读取。
操作语义:TileData 与 GlobalData 两条数据流
TileData 流程(本地缓冲)
对于TPOP(Pipe&, TileData&, Split),完整的生命周期是"生产者 push → 消费者 pop → 空间释放"三步:
TPUSH(Pipe&, TileData&, Split)将生产者 tile 存入当前 FIFO 槽位并记录数据就绪同步;生产者 tile 索引在槽位地址计算完成后递增。TPOP(Pipe&, TileData&, Split)等待数据就绪同步,将当前 FIFO 槽位加载到消费者 tile;消费者 tile 索引在槽位地址计算完成后递增。每次TPOP都会执行一次数据就绪等待。TFREE(Pipe&, Split)释放 FIFO 槽位空间。需要注意平台差异:A2A3 平台上该接口为空操作(no-op),因为TPOP已在内部执行空闲空间通知;A5 平台上TFREE才会真正释放TPOP使用的 FIFO 槽位空间。
这条平台差异可以直接从源码中得到印证:A2A3 的TPipe与 A5 的TPipe都定义了shouldWaitFree(tileIndex)与SyncPeriod,但 A5 后端由于引入了显式的本地 FIFO 槽位管理(LocalSlotNum),需要TFREE配合归还槽位。
GlobalData 流程(GM 槽位视图)
当通信双方不希望把数据搬进本地 tile,而是直接在全局内存槽位上读写时,使用GlobalData重载。其完整生命周期为:
TALLOC(Pipe&, GlobalData&)从TPipe分配一个生产者 FIFO 槽位,并暴露为GlobalTensor视图;生产者可通过TSTORE等指令向该槽位写入数据。TPUSH(Pipe&, GlobalData&)为已由TALLOC分配的槽位记录数据就绪同步,把该槽位提交给消费者。注意:它本身不存储 tile 数据。TPOP(Pipe&, GlobalData&)等待数据就绪,把gmTensor赋值为当前 FIFO 槽位地址,并递增消费者 tile 索引。它不把数据加载进本地 tile,也不释放槽位;消费者可通过TLOAD等指令从槽位读取数据。TFREE(Pipe&, GlobalData&)释放由TPOP(Pipe&, GlobalData&)返回的 FIFO 槽位视图,通知生产者该槽位空间已空闲。
GlobalData 流程的价值在于:生产者与消费者之间的数据始终驻留全局内存(GM),TPOP只做地址赋值与同步,避免了一次无谓的本地缓冲拷贝,适合大 tile 或需要多消费者共享数据的场景。注意 CPU_SIM 目前不支持 GlobalData 重载,该流程仅在 NPU 后端可用。
C++ Intrinsic 声明与重载形式
TPOP的 C++ intrinsic 声明位于 include/pto/common/pto_instr.hpp,共四个重载:
// 重载 1:TileData,显式指定 TileSplitAxis template <typename Pipe, typename TileCons, TileSplitAxis Split, std::enable_if_t<is_tile_data_v<TileCons>, int> = 0, typename... WaitEvents> PTO_INST RecordEvent TPOP(Pipe &pipe, TileCons &tile, WaitEvents &... events); // 重载 2:TileData,显式指定 subblock ID(按核切分场景) template <typename Pipe, typename TileCons, TileSplitAxis Split, typename... WaitEvents> PTO_INST RecordEvent TPOP(Pipe &pipe, TileCons &tile, int32_t subBlockId, WaitEvents &... events); // 重载 3:参数反转、不指定 Split(等效 TILE_NO_SPLIT) template <typename TileData, typename Pipe, typename... WaitEvents> PTO_INST RecordEvent TPOP(TileData &tile, Pipe &pipe, WaitEvents &... events); // 重载 4:GlobalData 槽位视图 template <typename Pipe, typename GlobalData, TileSplitAxis Split, std::enable_if_t<is_global_data_v<GlobalData>, int> = 0, typename... WaitEvents> PTO_INST RecordEvent TPOP(Pipe &pipe, GlobalData &gmTensor, WaitEvents &... events);从 include/pto/common/pto_instr.hpp 的实现可以看到,每个重载都先调用detail::PtoWaitEvents(events...)处理等待事件,再转发到对应后端的TPOP_IMPL;参数反转重载在 CPU 实现中直接转发为TPOP_IMPL<Pipe, TileCons, TileSplitAxis::TILE_NO_SPLIT>(见 include/pto/cpu/TPop.hpp),即TPOP(dst, pipe)等价于不切分的标准写法。所有重载返回RecordEvent,可用于与异步事件链组合。
其中WaitEvents变参允许在 pop 前插入事件等待(例如等待上一次TLOAD完成),便于在流水线中做依赖管理。
TPipe 模板参数详解
Pipe通常是各后端TPush.hpp中声明的TPipe模板类,三端声明完全一致,分别是 include/pto/cpu/TPush.hpp、include/pto/npu/a2a3/TPush.hpp、include/pto/npu/a5/TPush.hpp:
template <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;各参数含义与取值要点:
| 参数 | 含义 | 说明 |
|---|---|---|
FlagID | 同步标志 ID | 用于 CV 核间同步,FlagID + 1 <= MAX_SYC_ID(15);DIR_BOTH场景下硬件限制要求FlagID + 1 < 15 |
DirType | 通信方向 | Direction::DIR_C2V(1)、DIR_V2C(2)、DIR_BOTH(3)、DIR_V2C_CTRL(4);A5 额外支持DIR_C2V_GM(5)、DIR_V2C_GM(6)、DIR_BOTH_GM(7) |
SlotSize | 单个 GM 槽位字节数 | 决定 ring 中每个槽位可承载的数据量 |
SlotNum | GM 槽位个数 | 决定 FIFO 深度与SyncPeriod |
LocalSlotNum | 本地 FIFO 槽位数 | 默认 2,用于本地缓冲轮转 |
IsNoSplit | 是否不切分模式 | true时配合TILE_NO_SPLIT使用 |
EN_UNIT_FLAG | 是否启用单元标志 | 默认false |
Direction与TileSplitAxis枚举定义在 include/pto/common/fifo.hpp。TPipe内部通过DIR_MASK & DirType提取方向位,并以此派生is_c2v、is_v2c、is_both等编译期常量;A2A3 的 TPush.hpp 中static_assert只允许 C2V/V2C/Both/V2C_CTRL 四种方向,而 A5 的 TPush.hpp 额外支持 GM 系方向(C2V_GM、V2C_GM、BOTH_GM)。
TPipe内部通过RingFIFO<SlotSize, SlotNum, LocalSlotNum>(定义于 include/pto/common/fifo.hpp)组织槽位:GM_SLOT_BUFFER指向全局内存槽位基址,C2V_CONSUMER_BUF是向量侧(UB)消费者缓冲基址,V2C_CONSUMER_BUF是矩阵侧(L1)消费者缓冲基址,V2C_CONTROL_BUF是标量控制信号缓冲。
约束条件与切分行为
方向与消费者类型约束
- TileData 消费者:
TileCons::Loc必须是TileType::Vec、TileType::Mat或TileType::Ctrl。 - 方向语义:
Direction::DIR_C2V:向量核消费 Cube 核生产的数据(Cube 是生产者,Vector 是消费者);Direction::DIR_V2C:Cube 核消费向量核生产的数据(Vector 是生产者,Cube 是消费者);Direction::DIR_BOTH:同一 pipe 类型同时支持 C2V 与 V2C 两个方向的消费者。
- 本地消费者缓冲区:C2V 的 vector 消费者,
TPipe把 tile 分配到C2V_CONSUMER_BUF并做本地 FIFO 轮转;V2C 的 matrix 消费者,tile 被分配到V2C_CONSUMER_BUF并做本地 FIFO 轮转。
切分模式(TileSplitAxis)
TileSplitAxis枚举完整定义于 include/pto/common/fifo.hpp:
| 枚举值 | 数值 | 语义 |
|---|---|---|
TILE_NO_SPLIT | 0 | 1:1 模式不切分,仅使用 AIV0;A2A3 上要求 AIV0/AIV1 参与陪跑核间同步 |
TILE_UP_DOWN | 1 | 沿行方向切分:AIV0 取上半区、AIV1 取下半区,行数必须为偶数 |
TILE_LEFT_RIGHT | 2 | 沿列方向切分:AIV0 取左半区、AIV1 取右半区,列数必须为偶数 |
TILE_UP_DOWN_ODD | 3 | 沿行方向奇行切分:AIV0 取 rows/2 + 1、AIV1 取 rows/2,行数必须为奇数 |
TILE_LEFT_RIGHT_ODD | 4 | 沿列方向奇列切分:AIV0 取 cols/2 + 1、AIV1 取 cols/2,列数必须为奇数 |
文档聚焦的三种切分语义为:
TILE_NO_SPLIT:不施加子向量偏移。在 A2A3 上此模式需要 AIV0/AIV1 参与陪跑核间同步操作;TILE_UP_DOWN:vector 子块消费上、下两个行半区;TILE_LEFT_RIGHT:vector 子块消费左、右两个列半区。
NPU 同步模型
- 数据就绪等待:每次
TPOP都会执行一次数据就绪等待,即"每次 pop 必等一次 producer 就绪"; - 空闲空间通知:空闲空间通知是稀疏的,由
Pipe::SyncPeriod控制。SyncPeriod的推导规则在 include/pto/npu/a2a3/TPush.hpp 与 include/pto/npu/a5/TPush.hpp 中一致:(SlotNum <= 2) ? SlotNum : SlotNum / 2,即深 FIFO 时按一半槽位数周期性发空闲通知。
GlobalData 槽位视图约束
gmTensor被赋值为pipe.cons.tileIndex选择的 FIFO 槽位基地址;- 对于 C2V 切分模式,会按照
Split施加向量子块偏移; - 从槽位视图完成所有加载之后,调用者必须调用
TFREE(Pipe&, GlobalData&)归还槽位,否则生产者将无法复用该槽位。
CPU_SIM FIFO 模型约束
CPU_SIM 是 PTO 在 CPU 上的全功能模拟后端,其TPOP实现位于 include/pto/cpu/TPop.hpp。关键约束:
TPOP使用主机 FIFO 的互斥锁与条件变量等待生产者提交槽位;- TileData 数据从主机 FIFO 状态拥有的存储加载;即使
TPipe携带非空 NPU GM workspace,CPU_SIM 的 TileData 流程也不会访问该 workspace(GlobalData 重载当前在 CPU_SIM 中不可用); - 切分模式根据当前 subblock 上下文选择 lane。对于启用
IsNoSplit的 C2V 管道,TILE_NO_SPLIT会根据运行时 subblock 数量协调一个或两个 vector 消费者 subblock;其他 no-split vector 场景中,非参与 lane 按需填零; - 显式传入
int32_t subBlockId的重载在 CPU_SIM 中未实现,应通过模拟 subblock 执行上下文选择 split lane; DIR_BOTH下,消费者在其方向的独立环形队列中等待;no-split 与 V2C 消费者按生产者提交序号取出最早已提交槽位,在延迟释放造成环内空隙时仍保持 FIFO 顺序;重叠的 pop 会预留不同槽位,TFREE释放调用方消费者对应方向最早未释放的 pop;- 每次启用的
TFREE在 CPU_SIM 中都会释放一个具体占用的槽位,不模拟NPU 的SyncPeriod通知节奏; - TileData 数据按消费者 tile 的声明形状读回,因此该形状必须与生产者推送的窗口一致(
TILE_NO_SPLIT下即其有效形状); - CPU_SIM 按逻辑坐标把 TileData 复制到目标布局;A5 本地 FIFO 的
TPOP则是把 tile 绑定到 FIFO 地址、不做布局转换。要使 V2C 行为与 NPU 一致,应使用匹配的生产者与消费者布局(如 NZ Vec 到 NZ Mat),并在TPUSH前完成所需布局转换。
从 include/pto/cpu/TPop.hpp 的实现可以看到 CPU_SIM 的完整 pop 路径:首先通过ShouldNoSplitC2VConsumerLaneParticipate判断当前 lane 是否参与(双 lane C2V 协议激活时第二个 vector lane 才参与 pop;单 subblock 时走 inactive-lane 路径——FillTile填零并直接返回,不执行 pop,管道退化为单消费者);随后若pipe.cons.getWaitStatus()为真则调用wait<TileCons, Split>()等待数据;最后pipe.cons.pop<TileCons, Split>(pipe.fifo, tile)完成地址计算、TASSIGN与数据传输。
GlobalData 的 CPU_SIM 实现(include/pto/cpu/TPop.hpp)展示了槽位寻址的通用公式:slotIndex = pipe.cons.getTileId() % SLOT_NUM,entryBase = slotIndex * SLOT_SIZE + pipe.cons.entryOffset,再把GM_SLOT_BUFFER + entryBase通过TASSIGN_IMPL赋给gmTensor。
实战示例
以下三个示例完整继承自 docs/isa/TPOP.md,分别覆盖 C2V 向量消费、V2C 矩阵消费与 GlobalData 槽位弹出,可直接作为手写 CV FIFO 内核的模板。
C2V Vector Pop:向量核消费 Cube 产出的数据
#include <pto/pto-inst.hpp> using namespace pto; template <typename T> AICORE void example_c2v(__gm__ void *fifoMem) { constexpr uint32_t M = 128; constexpr uint32_t N = 128; constexpr uint32_t FlagID = 0; constexpr uint32_t FifoDepth = 2; constexpr uint32_t LocalBase = 0x0; using Pipe = TPipe<FlagID, Direction::DIR_C2V, M * N * sizeof(T), FifoDepth>; using VecTile = Tile<TileType::Vec, T, M / 2, N, BLayout::RowMajor, M / 2, N>; Pipe pipe(fifoMem, LocalBase, 0x0); // C2V:第二参数为 C2V 消费者缓冲基址,第三参数为 V2C 消费者缓冲基址(此处为 0) VecTile tile; TPOP<Pipe, VecTile, TileSplitAxis::TILE_UP_DOWN>(pipe, tile); }要点:TPipe构造参数依次为(GM_SLOT_BUFFER, C2V_CONSUMER_BUF, V2C_CONSUMER_BUF),对应 include/pto/common/fifo.hpp 中RingFIFO构造函数。C2V 场景下第二参数传入向量侧消费者缓冲基址LocalBase,第三参数置 0。VecTile声明为M/2 x N的行主序 tile,与TILE_UP_DOWN的上半区尺寸匹配——即消费者 tile 的有效形状必须与生产者推送窗口一致(CPU_SIM 会按声明形状读回数据)。
V2C Matrix Pop:Cube 核消费向量产出的数据
#include <pto/pto-inst.hpp> using namespace pto; template <typename T> AICORE void example_v2c(__gm__ void *fifoMem) { constexpr uint32_t M = 128; constexpr uint32_t N = 128; constexpr uint32_t FlagID = 0; constexpr uint32_t FifoDepth = 2; constexpr uint32_t LocalBase = 0x0; using Pipe = TPipe<FlagID, Direction::DIR_V2C, M * N * sizeof(T), FifoDepth>; using MatTile = Tile<TileType::Mat, T, M, N, BLayout::ColMajor, M, N, SLayout::RowMajor, 512>; Pipe pipe(fifoMem, 0x0, LocalBase); // V2C:第二参数为 C2V 消费者缓冲基址(此处为 0),第三参数为 V2C 消费者缓冲基址 MatTile tile; TPOP<Pipe, MatTile, TileSplitAxis::TILE_NO_SPLIT>(pipe, tile); }要点:V2C 场景下TPipe构造参数中第三位传入LocalBase(L1 消费者缓冲基址),第二位置 0。MatTile为列主序(BLayout::ColMajor)、行主序 S 布局(SLayout::RowMajor, 512)的矩阵 tile。若要 V2C 行为与 NPU 一致,生产者与消费者的布局必须匹配(如 NZ Vec → NZ Mat),布局转换需在TPUSH前完成,因为 A5 本地 FIFO 的TPOP只做地址绑定、不做布局转换。
GlobalData Slot Pop:GM 槽位视图直达
#include <pto/pto-inst.hpp> using namespace pto; template <typename T> AICORE void example_globaldata(__gm__ void *fifoMem) { constexpr uint32_t M = 128; constexpr uint32_t N = 128; constexpr uint32_t FlagID = 0; constexpr uint32_t FifoDepth = 2; using Pipe = TPipe<FlagID, Direction::DIR_C2V, M * N * sizeof(T), FifoDepth>; using SlotGlobal = GlobalTensor<T, Shape<1, 1, 1, M / 2, N>, Stride<1, 1, 1, N, 1>>; using VecTile = Tile<TileType::Vec, T, M / 2, N, BLayout::RowMajor, M / 2, N>; Pipe pipe(fifoMem, 0x0, 0x0); SlotGlobal slot; VecTile tile; TASSIGN(tile, 0x0); TPOP<Pipe, SlotGlobal, TileSplitAxis::TILE_UP_DOWN>(pipe, slot); TLOAD(tile, slot); TFREE<Pipe, SlotGlobal, TileSplitAxis::TILE_UP_DOWN>(pipe, slot); }要点:SlotGlobal是五维GlobalTensor(Shape<1,1,1,M/2,N>、Stride<1,1,1,N,1>),TPOP只把它绑定到槽位基地址(C2V 切分模式下施加向量子块偏移),随后用TLOAD(tile, slot)显式加载数据,最后必须用TFREE<Pipe, SlotGlobal, ...>归还槽位视图。该模式适合数据驻留 GM、避免本地缓冲拷贝的场景。
测试用例与验证路径
TPOP 的正确性在仓库中有成体系的 ST 测试覆盖,可用于对照学习和验证:
- CPU 后端:tests/cpu/st/testcase/tpushpop/main.cpp 覆盖了 C2V/V2C 双方向、
TILE_NO_SPLIT/TILE_UP_DOWN/TILE_LEFT_RIGHT乃至 ODD 切分、DIR_BOTH 并发、参数反转写法TPOP(dst, pipe)等多种场景; - A2A3 NPU 后端:tests/npu/a2a3/src/st/testcase/tpushpop_cv/tpushpop_cv_kernel.cpp、tests/npu/a2a3/src/st/testcase/tpushpop_cv_nosplit/tpushpop_cv_nosplit_kernel.cpp、tests/npu/a2a3/src/st/testcase/tpushpop_dir_both/tpushpop_dir_both_kernel.cpp 覆盖了 A2A3 平台上的 C2V/V2C 切分与非切分、DIR_BOTH 并发 pop;
- 固定管道:tests/cpu/st/testcase/tpushpop_fixpipe/tpushpop_fixpipe_kernel.cpp 与 tests/npu/a2a3/src/st/testcase/tpushpop_fixpipe/tpushpop_fixpipe_kernel.cpp 验证固定 FlagID 管道下的 push/pop 配对。
阅读这些测试时,可以重点观察"每次 pop 前生产者如何 push、pop 后如何 free"的配对关系,以及切分模式下 AIV0/AIV1 两个子块如何各取一半。
关于 ASM 形式
当前公开的 PTO 汇编参考(见 docs/isa 目录)尚未为TPOP定义稳定的 PTO-AS 拼写。因此手写 CV FIFO 程序时应使用 C++ intrinsic 形式进行编程,汇编形式留待后续版本补充。
小结
TPOP 是 PTO CV 通信模型的核心消费者指令,它把"数据就绪等待、槽位地址计算、本地 tile 绑定/数据加载"封装为一次调用。编程时需要重点把握三件事:方向与缓冲区的匹配(C2V 用C2V_CONSUMER_BUF、V2C 用V2C_CONSUMER_BUF)、切分模式与 tile 形状的对应(消费者 tile 有效形状必须等于生产者推送窗口)、以及槽位归还时机(A5 上TFREE必须配对调用,GlobalData 流程在全部TLOAD完成后必须归还视图)。结合 docs/isa/TPUSH.md、docs/isa/TFREE.md 与 docs/isa/TALLOC.md 可以拼出完整的 CV FIFO 生命周期;更完整的指令背景可参考 PTO-Virtual-ISA-Manual.md。
【免费下载链接】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),仅供参考