跳转至

InsertCommFence Pass

概述

InsertCommFence 实现最新 PTOAS 在其 pto-memory-consistency pass 中定义、并下放给 编译器的 data-before-signal 内存一致性契约。「下放给编译器」是字面意思:ptoas 要求这些 标记,但不检查它们是否存在,因此满足契约完全是本 pass 的职责 (见 ptoas 不做守门)。该契约是双向的:

  • 发布侧。 pto.comm.tnotify 要求在其匹配的 pto.cmo.cacheinvalid 释放标记之后、 信号之前,显式插入一条 pto.fence.barrier_all #pto.fence_scope<gm> —— 跨 rank 的写 必须在释放它的 notify 之前对 peer 可见。
  • 消费侧。 pto.comm.twait(或成功的 pto.comm.ttest)之后的可缓存 GM load 之前, 要求显式插入一条 pto.cmo.cacheinvalid all #pto.address_space<gm>,使读方看到 peer 的最新写。

消费侧里 ttest 那一半描述的是硬件契约,而非本 pass 的行为:PyPTO 没有 ttest 算子, 因此 pld.system.wait 是本 pass 唯一能识别的消费侧点。将来若新增 ttest 算子, StmtEffect 也必须把它归为 Effect::kWait

两处缓存标记都是同一个 system.cacheinvalid op,按参数个数分两种形态:

形态 IR 降级为
区域 system.cacheinvalid(tensor, shapes, offsets) pto.cmo.cacheinvalid … single_cache_line
全 GM system.cacheinvalid()(无参) pto.cmo.cacheinvalid all #pto.address_space<gm>

system.fence 降为 pto.fence.barrier_all #pto.fence_scope<gm> —— 带 DDR 可见性的 GM 屏障,强于裸的 pto.barrier <PIPE_ALL>

本 pass 插入什么

在 ptoas 0.50 上实测,该契约可归结为两条纯局部规则 —— notify 本身无需任何标记。 一趟结构遍历(InsertCommMarkers),不带任何控制流状态,插入:

  • 每个本地发布写之后 —— 写入 window-bound 目标的 tile.storetensor.write, 或写入 window-bound 本地目标的 get:一条覆盖整张量的区域 system.cacheinvalid紧跟一条 system.fence
  • 每个远端发布写之后 —— remote_store / put:只插一条 system.fence(其 peer 区域 cacheinvalid 由 codegen 发,见下);
  • 每个不透明发布写之后 —— 一个 Submit,或对未注册用户函数(其函数体不在本 pass 内分析, 没有单一可寻址区域)的调用:保守地插一条全 GM system.cacheinvalid() + system.fence
  • 每个 wait 之后 —— 一条全 GM system.cacheinvalid(消费侧在下一次可缓存读之前 的失效)。会做批量合并:连续的一段 wait,或一个纯 wait 循环for/while 的循环体 经 if/seq 嵌套后只含 wait 且至少含一条,例如 mesh composite 的 for src: if src != me: wait(...)),在 wait 之间不会有任何内存访问,因此只在循环/序列 之后一条全 GM cacheinvalid,而不是每个 wait 一条——每个屏障代次从 (P-1) 次整缓存刷新降为 1 次。遍历仍保持结构化(无数据流分析),用一个局部的纯 wait 循环判定; 同时含 load 的循环(如 ring 逐 step 的 wait; load)不算纯循环,仍保留逐 wait 失效。 无 wait 的循环同样不算纯:空 body 是空真地“只含 wait”,因此判定要求 ≥ 1 条 wait—— 否则 pass 会给一个原本什么都不发的循环追加全 GM cacheinvalid(在一个以移除刷新为目标的 pass 里反而引入回归);
  • notify —— 什么都不插。

区域 system.cacheinvalid(target) 寻址的是 target本地 base,这对本地窗口写是对的。 但远端写 remote_store / put 写到的是 peer 偏移 GM 地址(local_ptr + delems(peer)),本地 target view 寻址不到。peer 偏移只有在 codegen 里才知道 (EmitCommRemoteView)、目前无法在 IR 上表示,所以那条 peer 区域 pto.cmo.cacheinvalid <peer_view> single_cache_line 是 codegen 发的规避手段;而配对的 GM 释放 fence 始终由本 pass 插一条显式 system.fence(codegen 不得嵌入 fence),让释放排序在 IR 上可见、与本地写路径统一。(后续:给 peer 区域 cacheinvalid 一个 IR 表达,让 pass 拥有完整标记。)

store(win_a); store(win_b); notify        (本地窗口写)
  -> store(win_a); cacheinvalid(win_a); fence; store(win_b); cacheinvalid(win_b); fence; notify

for c: store; for p: notify                 (写与 notify 在不同循环)
  -> for c: (store; cacheinvalid; fence);
     for p: notify

wait; read
  -> wait; cacheinvalid(); read

notify 为何无需标记、fence 为何落在写:ptoas 把所需的释放 fence 关联到发布写的 cacheinvalid,而非 notify。因此发布「先前(哪怕在另一个循环里)写入」数据的 tnotify 已由该写的 cacheinvalid; fence 满足 —— fence 不必紧挨 notify。纯 barrier notify (完全无数据)什么都不需要。(此结论经实测验证:从 ring-allreduce 的 .pto 删掉 notify 侧 标记后 ptoas 0.50 仍接受;而删掉 wait 侧的 cacheinvalid all 在 0.50 上则被拒绝。)

ptoas 不做守门 —— 插标记是编译器的义务

ptoas 0.52 接受缺失标记的模块。 在 0.52 上重跑上述检查,用的是 tests/st/distributed/collectives/test_l3_allreduce_ring.py 里同一个 ring_step kernel:

变体 ptoas 0.52
原样 接受
删掉 wait 侧 cacheinvalid all(0.50 上会被拒) 接受
删光所有 cacheinvalid system.fence 接受

没有任何诊断信息,只是对应的指令不再生成:

IR 标记 缺失时丢掉的指令
区域 cacheinvalid PTOAS__DCCI_SINGLE_CACHE_LINE(<GlobalTensor>)
全 GM cacheinvalid() dcci((__gm__ void*)0, cache_line_t::ENTIRE_DATA_CACHE)
system.fence pipe_barrier(PIPE_MTE2/MTE3/FIX); dsb(DSB_DDR)

所以不存在编译期的安全网。缺一条标记不会构建失败,而是一个数据竞争:读方可能读到 陈旧的 cache 行,是否发生取决于时序、cache 状态、传输大小和 rank 数。不要把测试全绿 当作某条标记不必要的证据 —— 本 pass 自身的历史就说明这种失败是静默的:pin 还在 ptoas 0.50 期间,发布侧的区域 cacheinvalid 压根没有生成任何调用(hw-native-sys/PTOAS#995), 从未到达设备,而分布式测试套件全程保持绿色。

区域 cacheinvalid 目前覆盖整个目标张量(以全 0 offset 覆盖完整 shape),复用类型的 dim 表达式。收窄为精确写入子区域(写自身的 (shapes, offsets) 就在写入点旁边)是后续 升级项。

标记落在写 / wait / notify 所在处(必在作用域内)

把区域 cacheinvalid 紧插在其写之后,目标 Var 天然在作用域内(写刚用过它)—— 无论它是 window 参数、别名(dv = pl.tensor.view(win); remote_store(dv))、循环携带的 iter_arg, 还是分支内定义的值。不需要任何跨作用域跟踪,也绝不会 silent drop:每条标记都落在需要 它的 op 旁边,任何嵌套层级皆然。裸单语句的分支/循环体会就地包裹 (body -> { body; markers });首次运行后该体成为 SeqStmts,故本 pass 幂等。

在流水线中的位置

... -> ClassifyIterArgCarry -> InsertCommFence -> MaterializeValidShapeSymbols   (最后)

它在 Default 流水线中运行于所有会重排语句的 pass (SkewCrossCorePipelineLowerPipelineLoopsCanonicalizeIOOrder ...)之后。插入的 op 无操作数、无依赖边,若更早插入可能被挪离其 notify/wait;放在这里可让它们在 codegen 前 保持相邻。它之前的 pass 只改动编排体(OrchestrationGraph),因此本 pass 看到的 InCore IR 正是 codegen 最终降级的 IR。

本 pass 标记哪些写

情形 cacheinvalid fence
写入 window-bound DistributedTensorTypetile.store(peer 可 remote_load pass(本地区域,dst arg 2) pass
写入 window-bound DistributedTensorTypetensor.write(标量写,ConvertTensorToTileOps 有意不转换它) pass(本地区域,dst arg 0) pass
目标为 window-bound DistributedTensorTypepld.tile.get / pld.tensor.get pass(本地区域,dst arg 0) pass
pld.tile.remote_store / pld.tile.put / pld.tensor.put(peer 偏移写) codegen(peer 区域,IR 规避) pass
Submit,或对未注册用户函数的调用(没有单一可寻址区域) pass(全 GM,无参) pass

remote_load(结果是 tile、不写 GM),以及目标是普通 Tensor 而非 window-bound DistributedTensortile.store / tensor.write / get不是发布写 —— 完全不插标记。

算法 —— 一趟结构遍历,带消费侧批处理

本 pass 只携带一项控制流状态 —— 一个标志位:在访问纯 wait 循环for/while 的 body 经 seq/if 嵌套后只含 pld.system.wait —— 至少一条 —— 且控制表达式不触达内存;无 wait 的循环不算纯 wait 循环)的 body 时抑制逐 wait 的 invalidate:

  • 每个本地发布写处追加 region cacheinvalid; fence
  • 每个远端发布写remote_store / put)处只追加 fence(peer 区域 cacheinvalid 由 codegen 发,见下);
  • 每个 wait 处追加 cacheinvalid() —— 批量:纯 wait 循环在循环后共享一条全 GM cacheinvalid(),连续 wait 序列在序列后共享一条。wait 之间没有内存访问,因此最后一次 wait 之后 invalidate 等价于每次 wait 之后都 invalidate —— 在 mesh composite 中每个屏障代际的 (P-1) 次整缓存冲刷变为 1 次。
  • notify 保持不动。

if/for/while 的 body 正常递归访问;唯一的特殊处理是包裹裸单语句 body(作为 if/for 唯一 body、且无外层 SeqStmts 的写/wait),使其标记也能落上 —— 裸纯 wait 循环 body 同样获得 抑制,并在循环后落一条 cacheinvalid()。由于其余规则都是局部且只追加,控制流无关紧要:某个 循环内的写会被正确标记,无论其 notify 是否在另一个循环。读取 GM 的控制表达式(tensor.read 降级为缓存加载)会使循环不再“纯”,从而 invalidate 绝不会被推迟到可能读到陈旧 peer 数据的读取之后。

store(win); notify                   -> store(win); cacheinvalid(win); fence; notify
store(win); for: notify               -> store(win); cacheinvalid(win); fence; for: notify
for: { notify; store(win) }           -> for: { notify; store(win); cacheinvalid(win); fence }

写之后紧跟一条 fence 的已存在区域 cacheinvalid、以及紧接 wait 之后已存在的全 GM cacheinvalid,都会被识别且不重复插入,故本 pass 幂等。

与 Codegen 的关系

remote_storeput 的 codegen 各自在 store 之后发一条 peer 区域 pto.cmo.cacheinvalid <peer_view>(peer 偏移只有在这里才知道、且目前无法在 IR 上表示 —— 一种规避手段;配对的 GM fence 由本 pass 以 system.fence op 提供,所以这里发 fence)。 put 额外在 TPUT 与其 cacheinvalid 之间保留一条尾部 pto.barrier <PIPE_ALL>:TPUT 是 DMA, GM fence 只排序内存、并不 drain 发起 DMA 的 MTE 管线,缺这条 barrier 时后面的 notify 可能在 (原子)TPUT 真正落到 peer 之前就发出 —— test_l3_put 的 atomic-add / 子区域用例在真机上会 flaky;且实测 MTE3 级 barrier 不够(只有 PIPE_ALL 稳)。PTOAS#872 的 workaround,待 PTOAS 自行 drain tput 后移除。TPUT/TGET 的前置屏障与 TGET 的尾部屏障同样保留不动。

后续: 远端写的 peer 区域 cacheinvalid 是唯一还留在 codegen(而非 IR op)的标记,因为 peer 地址(local_ptr + delems(peer))暂无 IR 表达。有了一等的 IR 表达后,InsertCommFence 就能像本地写 那样拥有远端写的完整标记(cacheinvalid + fence)。

消费者

流水线下游无消费者。PTO codegen 通过既有的 op handler 降级插入的 system.cacheinvalid (按参数个数选区域或全 GM)与 system.fence;没有其它 pass 需要理解它们。