PyPTO-Pro 跨核同步设计指南:set_cross_core/wait_cross_core 的同步点、pipe 与 event_id 规划
2026/9/19 16:55:57 网站建设 项目流程

PyPTO-Pro 跨核同步设计指南:set_cross_core/wait_cross_core 的同步点、pipe 与 event_id 规划

【免费下载链接】pypto-gymPyPTO-Gym 是基于 PyPTO 编程框架构建的算子与模型样例仓库项目地址: https://gitcode.com/cann/pypto-gym

本篇技术指南以 PyPTO-Pro 融合算子的跨核(cross-core)同步设计为主题,系统讲解在 Cube 执行域与 Vector 执行域(AIC 与 AIV)之间、以及跨 Block/subblock 场景下,如何判断同步需求、规划共享 TileGroup 槽位、确定 set/wait 同步点与 pipe、分配event_id,并完成双向同步协议与事件配对核查。读者学完后可直接参照本文方法,为自己的融合算子设计可验证、可运行的跨核同步方案,并结合仓库中 matmul_softmax、vec_cube_abs_sqrt_matmul、KDA chunk_h 与 Engram 前向 等真实算子核对同步结构。

跨核同步是什么:事件只传递信号,不搬运数据

在 PyPTO-Pro 中,pl.system.set_cross_corepl.system.wait_cross_core用于保证生产者和消费者之间的数据读写顺序。最常见的场景是同一个 AI Core block 内,Cube 执行域与 Vector 执行域协同处理一组中间数据;接口也提供跨 Block 或 subblock 的同步模式。

关键语义必须牢记:同步事件只传递"可以继续执行"的信号,不负责搬运数据,也不分配共享缓冲。数据仍由pl.load/pl.store/pl.move/pl.insert等搬运指令负责,事件只是在这些搬运之间建立 happens-before 顺序。设计跨核同步时,需要依次完成四件事:

  1. 判断是否需要 cross-core 同步;
  2. 确定中间数据存放在 GM workspace、Mat(L1)还是 Vec(UB);
  3. 确定生产者和消费者,放置 set/wait 并规划event_id
  4. 记录方案并逐项核查配对关系。

接口参数细节可查阅$PYPTO_DEVKIT_DIR/docs/pypto_pro/api/SIMD-API/synchronization/下的set_cross_core.mdwait_cross_core.md(该环境变量指向 CANN 开发套件文档目录,本文不再展开)。

从仓库的工程落地看,跨核同步在 PyPTO-Pro 工作流中处于"手动流水"的核心位置。编排层约束文档 performance-constraints.md 明确区分了两条路径:

  • 自动流水:跨核 TileGroup 配置fwd_ids/bwd_ids,由框架生成set_cross_core/wait_cross_coreevent_id[0, 16),需复核生成代码;
  • 手动流水:显式调用set_cross_core/wait_cross_coreevent_id[0, 16)auto_mutex不覆盖跨核依赖,事件位置和 pipe 按实际数据操作设计。

本文聚焦手动流水场景——这是设计者必须完全掌控的部分。

判断是否需要 cross-core 同步

消费者读取本次 Kernel 中由另一个执行域或另一个 Block/subblock 写入的数据时,需要设计 cross-core 同步。可以按下面的顺序判断:

与核内同步的边界:mutex_ids、mem_bar 与 unit_flag

数据依赖没有跨 Cube/Vector 执行域,也没有跨 Block/subblock 时,按普通核内依赖处理,此时不应引入 cross-core 事件:

  • 带非空mutex_ids的 TileGroup 由auto_mutex管理跨 Pipe 依赖。例如 KDA 的 chunk_h_kda_impl.py 中,每个 L1/L0/UB TileGroup 都显式声明了mutex_ids=[0..21]auto_mutex自动处理 Section 内部的跨 Pipe 顺序;
  • 未配置mutex_ids时,按实际数据路径手工插入核内同步(如pl.system.sync_src/sync_dst);
  • VF 函数中的局部内存读写顺序使用vf.mem_bar
  • 配置了matmul phase时,M 与 FIX 之间由unit_flag配对。

这些机制不生成 Cube 与 Vector 之间的事件,mutex_id也不能替代 cross-core 使用的event_id。两者是两套独立的资源体系,数值相同也不会自动建立联系。

需要同步与不需要同步的情形

  • 需要 cross-core 同步:Cube 写出中间结果后由 Vector 读取,或者 Vector 准备数据后由 Cube 读取。典型如矩阵乘结果(Acc/L0C)经 GM workspace 交给 Vector 做 softmax,或 Vector 先计算左操作数再交给 Cube 做 matmul;
  • 不需要同步:多个逻辑 Block 只写各自独立的输出区域时,不存在跨核读写依赖;
  • 跨 Block 共享状态:应根据算法使用原子操作、分阶段 Kernel 或目标产品支持的INTER_BLOCK同步,而不是简单地在两个 Block 之间插事件。

缓冲复用时必须双向同步

Cube 与 Vector 循环复用同一组缓冲时,必须同时建立正向同步和反向同步

  • 正向同步(READY):保证消费者在数据写完后再读取;
  • 反向同步(RELEASE):保证生产者在消费者读完后再覆盖该缓冲。

只建立正向同步无法保护正在被消费的数据,可能导致后续写入覆盖尚未读完的数据。事件的具体放置和配对规则见下文"缓冲复用时的双向同步"。

手动同步:set 在最后一次写之后,wait 在第一次读之前

当前跨核同步统一使用set_cross_corewait_cross_core,放置规则非常明确:

  • 生产者在最后一个写操作之后set;
  • 消费者在第一个读操作之前wait;
  • 循环复用同一槽位时,还要增加消费者到生产者的反向事件(RELEASE)。

wait 可以先执行并阻塞,直至配对的 set 到达;但反过来,set 之后对应的 wait 必须最终在某条执行路径上被执行,否则会造成永久等待或遗留信号。

明确共享 TileGroup 的槽位

TileGroup 既可以通过next()current()previous()访问,也可以用group[i]直接选择槽位。二者语义有重要差别:

  • group[i]不读取也不推进轮转游标
  • i可以是运行时整数表达式,但框架不会自动取模——设计时要明确写出槽位表达式,并证明所有运行时取值都在[0, depth)内。

手动同步复用同一个多槽缓冲时,生产者、消费者以及 READY/RELEASE 事件必须指向同一个逻辑槽位。先定义槽位映射,再让两侧按同一映射访问:

slot_idx = task_idx % depth tile = shared_group[slot_idx] READY = READY_IDS[slot_idx] RELEASE = RELEASE_IDS[slot_idx]

这是一条手动同步的设计关系,不表示mutex_idevent_id变成了同一种资源group[slot_idx]选中的 Tile 仍可携带 mutex 元数据,由auto_mutex处理各 Section 内部的跨 Pipe 依赖;READY 和 RELEASE 仍由 cross-core 事件处理 Section 之间的顺序。

继续使用next()也可以,但设计文档必须证明生产者和消费者的访问次数、初始游标和分支路径会选中同一个物理槽位。存在以下情况时,显式下标通常更容易核对:

  • 预取(prefetch);
  • 尾块分支;
  • 两侧循环次数不同;
  • 同一轮同时访问多个槽位。

下标访问不会改变游标;与next()混用时,两套状态要分别推导。这一要求在 design-template.md 的 §6"核间同步"模板中也被固化为必填字段:每个共享 TileGroup 要记录depth、访问方式(current()/next()/group[i])、槽位表达式(如task_idx % 2)以及"槽位一致性依据"。

根据数据路径确定同步点:pipe 表示硬件流水

pipe表示执行setwait硬件流水,不表示 Section 名称。发送和等待两侧可以使用不同 pipe,取值由同步点紧邻的数据操作决定——这是最常见的出错点之一。

pipe必须与同步点相邻的数据操作一致,仓库中的典型对应关系如下:

数据路径pipe 配对仓库实例
Cube 将 Acc 写入 GM workspace,Vector 再从 GM 加载到 UBFIX → MTE2matmul_softmax_impl.py:Cubeset_cross_core(pipe=FIX, event_id=0),Vectorwait_cross_core(pipe=V, event_id=0)
Cube 将 Acc 搬到 Vec 后由 Vector 计算FIX → VKDA chunk_h_kda_impl.py:Cubeset_cross_core(pipe=FIX, event_id=0),Vectorwait_cross_core(pipe=V, event_id=0)
Vector 通过 MTE3 写入 Mat(L1),Cube 再搬入 L0MTE3 → MTE1vec_cube_abs_sqrt_matmul_impl.py:Vectorset_cross_core(pipe=MTE3, event_id=2),Cubewait_cross_core(pipe=MTE1, event_id=2)

具体接口规定其他 pipe 时,以该接口文档为准。

示例:Cube 整体写完 workspace 后通知 Vector(单向)

以下片段展示 Cube 整体写完 workspace 后通知 Vector 的单向事件位置(Tile 和 Tensor 声明已省略):

with pl.section_cube(): ... pl.store(workspace, acc_tile, [m_off, n_off]) pl.system.set_cross_core( pipe=pl.PipeType.FIX, event_id=0, sync_mode=pl.CrossCoreSyncMode.INTRA_BLOCK, ) with pl.section_vector(): pl.system.wait_cross_core( pipe=pl.PipeType.MTE2, event_id=0, sync_mode=pl.CrossCoreSyncMode.INTRA_BLOCK, ) pl.load(vec_tile, workspace, [m_off, n_off]) ...

仓库中 matmul_softmax_impl.py 就是这一模式的完整实现:Cube 侧完成 matmul 后pl.move(mm1_res, tile_c1, acc_to_vec_mode=pl.AccToVecMode.DualModeSplitM),随后set_cross_core(pipe=pl.PipeType.FIX, event_id=0);Vector 侧每个 subblock 先wait_cross_core(pipe=pl.PipeType.V, event_id=0)再对 mm1_res 做 row_max/exp/row_sum 等 softmax 运算。注意该算子在 Cube 内部还用了多组sync_src/sync_dst(如MTE2→MTE1MTE1→MM→FIX),这些是核内同步,与跨核事件职责不同。

上例是一条单向依赖。如果 Cube 在循环内逐槽位生产,而 Vector 逐槽位消费,事件也必须按相同粒度建立;把唯一的set放在生产循环之后,只能形成"Cube 全部结束后 Vector 整体开始"的阶段屏障,不能实现逐 Tile 流水——这是性能优化的关键取舍。

反向示例:Vector 准备数据后交给 Cube(Vector → Cube 方向)

vec_cube_abs_sqrt_matmul_impl.py 展示了相反的握手方向:Vector 在 UB 中计算sqrt(|x|),转 NZ 后pl.insert(v1_mat, tile_nz, [off, 0])写入 L1(Mat),随后set_cross_core(pipe=pl.PipeType.MTE3, event_id=2);Cube 侧wait_cross_core(pipe=pl.PipeType.MTE1, event_id=2, sync_mode=pl.CrossCoreSyncMode.INTRA_BLOCK)后再pl.move(v1_left, v1_mat)将 L1 数据搬入 L0A 作为 matmul 左操作数。该样例头部注释明确记录了"cross-core event 2 is signalled by vector MTE3 and awaited by cube MTE1",并在 64×64 FP32 用例上以maxdiff=3.815e-06(容差 1e-3)通过 Gate A 验证。

sync_mode 的四种取值

发送和等待两侧的sync_mode必须一致。按参与者拓扑选择:

sync_mode适用场景
INTRA_BLOCKAIC 与两个 AIV 子核协同:AIC 向 Vector 发送的事件供两个 AIV 子核分别等待;AIC 等待 Vector 事件时需要等待两个 AIV 子核都完成
INTER_SUBBLOCK两个 AIV 子核之间同步
INTER_BLOCK跨物理 block 同步(必须同时满足目标产品与算法的适用条件)
UNICAST_BLOCK只与一个 AIV 子核同步(必须同时满足目标产品与算法的适用条件)

INTER_BLOCKUNICAST_BLOCK属于受限模式,使用时需确认目标产品支持且算法语义匹配。

规划 event_id

当前接口的event_id范围为[0, 16)。两条硬性规则:

  1. 一对 set 和 wait 必须使用相同的 ID 与sync_mode
  2. 一个 set 发送后,在对应 wait 消费该信号之前,event_id不得分配给可能产生错配的另一条事件

event_id与 TileGroup 的mutex_id是两套独立机制,应分别规划;二者数值相同不会自动建立联系,也不能相互替代。

单向同步:只有 READY

一次性 workspace 或每轮使用独立地址时,只需建立生产者到消费者的 READY 事件。生产者在完成写操作后发送 READY,消费者在读取数据前等待同一个 READY。wait 可以先执行并阻塞,直至配对的 set 到达。

时间向下 Cube/FIX(生产者) Vector/MTE2(消费者) 写workspace set READY0 ---------------------> wait READY0 读workspace

READY 保证 Vector 不会在 Cube 写完之前读取 workspace。每轮使用不同地址时,Cube 不再覆盖 Vector 正在读取的数据,因此不需要反向事件。

缓冲复用时的双向同步:READY + RELEASE

生产者和消费者循环复用同一组缓冲时,还需要反方向的 RELEASE 事件:

  • READY表示"本轮数据已经写好";
  • RELEASE表示"上一轮数据已经读完,这个槽位可以再次写入"。

下图以槽位 0 为例,展示该槽位每次写入、读取和释放的顺序(时间自上而下):

协议要点:

  • 循环开始前,Vector 发送初始 RELEASE,Cube 等待该事件后获得槽位 0 的首次写入权限;
  • 每次使用该槽位时:Cube 写完后发送 READY(步骤①)→ Vector 等待 READY(步骤②)→ Vector 读取数据(步骤③完成后发送 RELEASE)→ Cube 在下次覆盖前等待 RELEASE(步骤④);
  • 槽位 0仍需复用时,步骤④位于下一次写入之前;槽位 0不再复用时,步骤④位于 Cube 侧结束之前,用于确认最后一次读取已经完成;
  • ping-pong 缓冲的槽位 1 采用相同过程,并使用另一组event_id

循环前的 RELEASE 为生产者提供首次写入权限,循环中的 RELEASE 保护下一次复用。也可以不预发初始 RELEASE:此时生产者第一次写入不等待,从第二次使用该槽位开始等待。两种方案的要点是:初始化、循环主体和循环结束必须采用同一套配对方式,不能中途更换。

事件表按"共享缓冲 + 槽位访问 + 数据方向"记录事件。以下示例把每个物理槽位直接写开;如果源码使用动态下标,表中还应补充统一的slot_idx表达式:

事件组共享缓冲及访问方向槽位event_idset位置/pipewait位置/pipe复用条件
QK_READYqk_group[0]Cube→Vector00FIX写槽位0后V读槽位0前对应wait已消费
QK_READYqk_group[1]Cube→Vector11FIX写槽位1后V读槽位1前对应wait已消费
QK_RELEASEqk_group[0]Vector→Cube02V读槽位0后FIX覆盖槽位0前对应wait已消费
QK_RELEASEqk_group[1]Vector→Cube13V读槽位1后FIX覆盖槽位1前对应wait已消费

前一个信号由配对 wait 消费后,event_id 才可以复用;前一个信号尚未消费时,新的逻辑通道必须使用其他 ID。动态 event 表达式的所有运行时取值都必须位于[0, 16)

源码实例:Engram 前向算子的 READY/ACK 双事件

仓库中的 engram_forward_impl.py 是双向同步协议的完整落地。它定义了K_READY_IDS = (0, 1)ACK_IDS = (2, 3)两组事件 ID,按 head 奇偶h % 2轮转:

  • Cube 侧(section_cube):每个 M-tile 的 head 完成pl.store(key_back, ac, ...)落 GM 后,set_cross_core(pipe=pl.PipeType.FIX, event_id=K_READY_IDS[h % 2], sync_mode=INTRA_BLOCK)通知 Vector 消费;而在消费同槽上一轮数据之前,先wait_cross_core(pipe=pl.PipeType.FIX, event_id=ACK_IDS[h % 2], sync_mode=INTRA_BLOCK)等待 Vector 的背压信号;
  • Vector 侧(section_vector):先wait_cross_core(pipe=pl.PipeType.MTE2, event_id=K_READY_IDS[h % 2])等 Cube 写完,消费完毕后set_cross_core(pipe=pl.PipeType.MTE3, event_id=ACK_IDS[h % 2])回发 ACK。

其中K_READY对应正向 READY,ACK对应反向 RELEASE,正好构成"同一逻辑槽位上的双向事件 + 奇偶轮转的两组 ID"。源码注释还点出了一个冷启动细节:mi == core_id and h < 2时(本核第一轮的前两个 head)不执行 ACK wait,否则 core >= 1 的 head 0 会等待一个永远不来的 ACK 导致死锁——这正是文档"初始化、循环主体必须采用同一套配对方式,且各分支路径都必须能配对"的工程实证。

源码实例:KDA chunk_h 的多事件流水

chunk_h_kda_impl.py 展示了一个循环内多个 Phase 交替、多个event_id并行工作的复杂场景:

  • event_id=3:Vector 侧store(ws_w_f16, ...)set_cross_core(pipe=MTE3, event_id=3),Cube 侧wait_cross_core(pipe=MTE2, event_id=3)后加载w_l1/s_l1WS = W @ S
  • event_id=0:Cube 完成pl.move(cur_ub_ws, cur_ws_acc, acc_to_vec_mode=...)set_cross_core(pipe=FIX, event_id=0),Vector 侧wait_cross_core(pipe=V, event_id=0)后执行ws = kv - ws
  • event_id=1:Vector 计算 v_corr 完成后set_cross_core(pipe=MTE3, event_id=1),Cube 侧wait_cross_core(pipe=MTE2, event_id=1)后加载k_l1/v_l1KV = k_rest^T @ v_corr
  • event_id=2:Cube 完成 KV 搬入 UB 后set_cross_core(pipe=FIX, event_id=2),Vectorwait_cross_core(pipe=V, event_id=2)后做S = exp(g_total)*S + KV

四个事件分别对应四条独立的数据通道(w/s 准备、WS 结果、v_corr 结果、KV 结果),各自在"最后一次写之后 set、第一次读之前 wait",互不复用 ID,形成了 Cube/Vector 在单个 chunk 循环内的多阶段软件流水。这验证了"事件按数据方向与槽位逐一对应,指令总数相等也不能代替这项检查"的设计原则。

记录并验证同步方案:六项核查

同步方案应记录共享数据对象、set/wait 位置、pipe、sync_mode、event_id 和事件复用条件。共享对象是多槽 TileGroup 时,还要记录缓冲深度以及生产者、消费者的槽位访问表达式。design-template.md 的 §6 提供了落地的表格框架:cross_core 涉及判定、共享数据与槽位映射表、同步点表(事件组/共享缓冲/方向/set 位置与 pipe/wait 位置与 pipe/sync_mode/event_id/复用条件)、event_id 分配表。完成设计后按以下顺序检查:

  1. 检查槽位映射。对同一逻辑任务,确认生产者和消费者最终选中同一个物理槽位,READY/RELEASE 也按该槽位选 ID。动态下标的所有取值必须位于[0, depth);使用next()时要把初始游标、调用次数和分支影响写清楚。

  2. 检查事件位置。对照 READY/RELEASE 配对图中的①~④,确认:READY 的 set 位于最后一个生产操作之后,READY 的 wait 位于第一个消费操作之前,RELEASE 的 set 位于最后一个消费操作之后,RELEASE 的 wait 位于下一次覆盖之前。需要确认最后一次消费完成时,对应的 RELEASE wait 位于 Cube 侧结束之前——Section 开头的 wait 只能消费预发信号或上一轮信号,不能代替末尾等待。

  3. 检查配对关系。每个 wait 都应明确对应哪个方向、哪个槽位和哪一轮的 set。set 可以晚于 wait 到达,但在所有执行路径上都必须最终执行,整个等待关系不能形成环。

  4. 检查执行次数。设某槽位使用 n 次:

    • 预发初始 RELEASE 时:初始授权和 n 次消费完成通知共执行n+1次 set;首次写前、n-1次复用前和末尾确认共执行n+1次 wait;
    • 不预发初始 RELEASE 时:RELEASE 方向执行 n 次 set 和 n 次 wait——第一次写不等待,后续n-1次写前等待,末尾再等待最后一次消费完成;
    • 两种方案的 READY 方向都是 n 次 set 和 n 次 wait。
  5. 检查边界分支。零次、一次、整除和尾块迭代中的每个 wait 都必须能够获得配对信号。任一侧因条件分支少执行一次,都可能造成永久等待或遗留信号;零次使用的槽位还要检查预发信号是否会影响后续 event_id 复用。

  6. 检查参与者。Vector 侧有两个 subblock 时,应记录各自访问的地址范围。使用INTRA_BLOCK时,Cube 等待两个 AIV 子核都完成;使用UNICAST_BLOCK时,只等待参与该事件的 AIV 子核;两个 AIV 子核之间的屏障使用INTER_SUBBLOCK

上述计数按每个槽位的源码执行次数统计。使用INTRA_BLOCK时,还要分别核对两个 AIV 子核的实际执行情况——例如 Engram 中 Vector 侧按sub_idx切分vm_start段(偶数段归 subblock 0、奇数段归 subblock 1),两侧的 wait/set 必须与这种切分保持一致。

官方指定算子示例:核对完整同步协议

下面这些示例来自 PyPTO Pro 工作流维护的官方指定算子清单,路径均相对于$PYPTO_DEVKIT_DIR。实际设计时以PRO_MATERIAL_INDEX.md§B 中存在的文件为准,并优先选择与目标算子缓冲数量、数据方向和循环拓扑相近的示例。

示例适合核对的同步结构
pro_ops/lightning_indexer/test_quant_lightning_indexer_vf.pyCube 与 Vector 之间的双向 READY/RELEASE、ping-pong 槽位和 Cube 侧末尾等待
pro_ops/fa/test_fa_tilingkey_attn_mask.pyQK、P、PV 多组跨 Section 数据依赖,以及按槽位分配正反向事件
pro_ops/fa/test_fa_perf_tkv_preload_dn_vf_bufid_dynrank.py动态循环中的 Cube/Vector 协作、预发反向事件、双缓冲复用和最后一批消费确认
pro_ops/fa/test_fa_with_mask.pymask 分支下的多组事件、双缓冲与三缓冲并存,以及不同 pipe 上的 set/wait 位置

这些示例用于核对完整同步协议,不是event_id、pipe 或缓冲深度的固定模板。设计时必须按本算子的生产者、消费者、槽位和循环次数重新推导事件表;单个接口的pipesync_mode约束仍以当前 API 文档为准。

设计流程总结

回到 PyPTO-Pro 迭代式方案设计工作流(见 SKILL.md 的 R6 轮次),跨核同步设计遵循一条可复用的流水线:

  1. 在 R0 模块划分阶段确认是否存在 Cube↔Vector 或跨 Block/subblock 的数据依赖;没有则 §6 填"不涉及 cross_core",不能仅凭 Section 数量判定;
  2. 在 R4 循环与 Section 结构阶段确定自动流水还是手动流水;手动流水时,在 R6 按本文方法:先确定共享数据与槽位映射 → 再确定每个事件的 set/wait 位置与 pipe → 分配event_id→ 填写同步点表和 event_id 分配表;
  3. 完成设计后,逐槽位、逐轮次核对 set/wait 一一对应,检查动态下标落在[0, depth)、各边界分支事件可配对、生产者结束前等待最后一次消费完成。

跨核同步的本质是把"数据就绪"和"缓冲可复用"两个事实,以事件的形式在正确的硬件流水上、以正确的时序传递给正确的参与者。把握住"事件只传信号、不搬数据",以及"READY 保护读、RELEASE 保护覆盖、配对必须按槽位和轮次逐一对应"这三条主线,就能为任何融合算子设计出正确且可验证的同步方案。

【免费下载链接】pypto-gymPyPTO-Gym 是基于 PyPTO 编程框架构建的算子与模型样例仓库项目地址: https://gitcode.com/cann/pypto-gym

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询