Skip to content

02 · PR !1174:L0C 同步与事件栅栏 ​

结论先行

  1. !1174 的实测 diff 规模比"8 处替换"大得多:src/88d8b965 → src/048d0427 共 17 个 hunk、40 删 + 52 增 = 92 行, 净 +12 行(2462 → 2474)。它由三类改动组成,不是两类: (a) 新增 CopyL0CToUb 封装(1 hunk,+12 行);(b) 8 处 DataCopy → CopyL0CToUb(8 hunk,各 1 行); (c) 32 行 Hardware:: → AscendC::Hardware:: 限定名改写(8 hunk,各 4 行)—— 第三类在 docs 中完全没有记载。
  2. CopyL0CToUb 定义在 048d0427 的 :116,调用点恰好 8 处: :511、:1183、:1298、:1734、:1823、:1909、:2058、:2271(全部 grep -n 实测)。
  3. 完备性可机器核查:修复后文件里已经没有一处裸的 DataCopy(<UB>, c1Local, ...); L0C 分配点 8 处、封装调用点 8 处,一一对应。
  4. 最硬的一条实测数字:HardEvent::M_V 与 HardEvent::V_M 在 d0906c3e / 88d8b965 中的出现次数都是 0, 而同期有 21 次 Mmad(、8 次 L0C 分配、8 次 BLOCK_MODE_MATRIX 搬运。 也就是说 !1135 的 Cube 代码从未建立过 M↔V 这条事件边;!1174 之后两者各出现 3 次,且全部位于封装内部。 这不是"漏了一处栅栏",是"整条同步边不存在"。
  5. 症状是崩溃而非算错,因为竞争发生在 L0C 这块硬件缓冲区上,硬件对 VEC 与 CUBE 同址读写直接抛异常 (真机报错串见 §5,逐字引用)。
  6. 修复零性能开销:栅栏等的两件事本来就要发生(V 反正要等数据、M 反正要等上一轮 Mmad), 把隐式依赖显式化不新增等待;实测数据见 §8。

锚点:src/048d0427/op_kernel/chunk_gated_delta_rule.h:113-130


1. diff 实测基线 ​

锚点:src/88d8b965/op_kernel/chunk_gated_delta_rule.h:115、474-477、499;src/048d0427/op_kernel/chunk_gated_delta_rule.h:116-127

diff src/88d8b965/op_kernel/chunk_gated_delta_rule.h src/048d0427/op_kernel/chunk_gated_delta_rule.h 的 hunk 头全部 17 条(实查原样):

115a116,127
474,477c486,489
499c511
1072,1075c1084,1087
1171c1183
1244,1247c1256,1259
1286c1298
1637,1640c1649,1652
1722c1734
1766,1769c1778,1781
1811c1823
1847,1850c1859,1862
1897c1909
1958,1961c1970,1973
2046c2058
2196,2199c2208,2211
2259c2271

分类清点:115a116,127 = 1 个纯新增 hunk(+12 行,即封装本体); 499c511 / 1171c1183 / 1286c1298 / 1722c1734 / 1811c1823 / 1897c1909 / 2046c2058 / 2259c2271 = 8 个 1 行替换(DataCopy → CopyL0CToUb); 474,477c486,489 等 = 8 个 4 行替换(Hardware:: → AscendC::Hardware::)。1 + 8 + 8 = 17 ✓。 行数核对:删除 8 + 32 = 40,新增 8 + 32 + 12 = 52 ✓(grep -c '^<' / '^>' 实测)。

影响面(diff -rq src/88d8b965 src/048d0427 的全部输出,仅此一行):

Files src/88d8b965/op_kernel/chunk_gated_delta_rule.h and src/048d0427/op_kernel/chunk_gated_delta_rule.h differ

两修复合起来的净改动(diff src/d0906c3e ... src/048d0427):18 hunk、41 删 + 54 增 = !1158 的 1 hunk + !1174 的 17 hunk,无相互覆盖。

1.1 第三类改动:AscendC::Hardware:: 限定名(docs 未载) ​

锚点:src/88d8b965/op_kernel/chunk_gated_delta_rule.h:474-477;src/048d0427/op_kernel/chunk_gated_delta_rule.h:486-489

diff 原文(以第一处为例,其余 7 处逐字符同构):

-    LocalMemAllocator<Hardware::L1> l1Allocator;
-    LocalMemAllocator<Hardware::L0A> l0aAllocator;
-    LocalMemAllocator<Hardware::L0B> l0bAllocator;
-    LocalMemAllocator<Hardware::L0C> l0cAllocator;
+    LocalMemAllocator<AscendC::Hardware::L1> l1Allocator;
+    LocalMemAllocator<AscendC::Hardware::L0A> l0aAllocator;
+    LocalMemAllocator<AscendC::Hardware::L0B> l0bAllocator;
+    LocalMemAllocator<AscendC::Hardware::L0C> l0cAllocator;

实查:LocalMemAllocator<Hardware:: 在 88d8b965 出现 32 次,在 048d0427 出现 0 次; LocalMemAllocator<AscendC::Hardware:: 在 048d0427 出现 32 次。分配的对象、顺序、数量完全未变。

判读:文件 :23 本来就是 using namespace AscendC; // NOLINT(build/namespaces), 所以这是语义等价的名字限定(编译期写法,不改变任何运行行为)。它的价值要与本文件 :25-33 作者自己写下的 那段注释连读 —— 那段注释记录的正是"全局命名空间里的非限定名字被 CANN autogen 宏展开搞坏过" ("unknown type 'ChunkGatedDeltaRuleTilingData'"、"removes the namespace as an autogen variable")。 同属"把名字来源写死、消除工具链变量"这一类修补。 报告意义:PR 标题只讲稳定性,但 commit 里搭车了一处与稳定性无关的编译加固; 引用"!1174 做了什么"时必须把这两件事分开归因,否则会把一次命名空间修补也记成栅栏修复的成果。


2. CopyL0CToUb:封装本体 ​

锚点:src/048d0427/op_kernel/chunk_gated_delta_rule.h:116-127

逐字(新增的全部 12 行):

cpp
  __aicore__ inline void CopyL0CToUb(LocalTensor<float> dst, LocalTensor<float> src,
                                     const DataCopyParams &copyParams,
                                     const DataCopyEnhancedParams &copyEnhanced) {
    event_t mToV = static_cast<event_t>(pipe_->FetchEventID(HardEvent::M_V));
    event_t vToM = static_cast<event_t>(pipe_->FetchEventID(HardEvent::V_M));
    SetFlag<HardEvent::M_V>(mToV);
    WaitFlag<HardEvent::M_V>(mToV);
    DataCopy(dst, src, copyParams, copyEnhanced);
    SetFlag<HardEvent::V_M>(vToM);
    WaitFlag<HardEvent::V_M>(vToM);
  }

位置:Init(:105-114)与 SetGlobalTensors(:128)之间 —— 放在所有 Cube 函数之前,纯工具函数。

三点对读代码最重要的结构事实:

  1. 它只新增事件,不删除任何东西。8 处调用点其后仍保留原有的 PipeBarrier<PIPE_ALL>() (diff 里每处只改 1 行即可证明)。所以 !1174 是纯加法,不可能改变原有时序中"已经正确"的部分。
  2. 形参是 LocalTensor<float> dst, LocalTensor<float> src,与 8 处调用实参 cNz(UB fp32)/ c1Local(L0C fp32)严格同型。它不是"通用的 UB→UB 搬运",签名本身就锁定了 L0C→UB 这一个方向。
  3. 事件 ID 由 pipe_->FetchEventID(...) 向 pipe 申请,而非硬编码数字 —— 与文件里封装之外的 23 处 FetchEventID 用法同式(全文件共 25 处,覆盖 9 种事件类型: V_MTE2 6、MTE2_V 6、V_MTE3 5、MTE3_MTE2 2、MTE3_V 2、MTE2_S/S_MTE2/M_V/V_M 各 1)。 HardEvent:: 在全文件共出现 205 次,其中 65 次 SetFlag 直接用字面事件号 0 —— 那 65 次全部属于 MTE2_MTE1(23) / MTE1_M(22) / M_MTE1(20) 这三条五件套内部边; 跨 UB/GM 的边以及本次新增的 M↔V 边一律走 FetchEventID。ID 取法的这条分界线在 §03 报告里展开。

2.1 封装前后的语义差分 ​

锚点:src/88d8b965/op_kernel/chunk_gated_delta_rule.h:499;src/048d0427/op_kernel/chunk_gated_delta_rule.h:511-512

封装前(d0906c3e/88d8b965)封装后(048d0427)
L0C→UB 搬运DataCopy(cNz, c1Local, copyParams, copyEnhanced)同一条 DataCopy,被包进 CopyL0CToUb
搬运之前无事件:上一次 Mmad 是否写完 L0C 未知SetFlag/WaitFlag<HardEvent::M_V>
搬运之后无事件:紧跟的 V 侧 NZ→ND 读取无人授权SetFlag/WaitFlag<HardEvent::V_M>
其后PipeBarrier<PIPE_ALL>()PipeBarrier<PIPE_ALL>()(原样保留)

3. 八处替换:全部实测 ​

锚点:src/88d8b965/op_kernel/chunk_gated_delta_rule.h:495、499、505;src/048d0427/op_kernel/chunk_gated_delta_rule.h:507、511、517

3.1 全景表(行号、函数、被搬出的 UB 缓冲、紧随其后的 V 侧读者) ​

锚点:src/048d0427/op_kernel/chunk_gated_delta_rule.h:511、1183、1298、1734、1823、1909、2058、2271

cNz 是 L0C 落地的 UB 目的板;"V 侧读者"是竞争的另一造,全部实测自 grep -n 'cNz'(048d0427 行号)。

#函数048d0427 调用行88d8b965 原行cNz 绑定紧随其后的 V 侧读
1MatmulBlockFp32Compensated:511:499= residualA(:507):517 DataCopy(dst[row*kBlock], cNz[row*16], nzToNdParams)
2CopyAttnCubeResult:1183:1171= chunkVFp32(:1179):1189 DataCopy(dst[row*kMatmulN], cNz[row*16], ...)
3ComputeKCumdecayCube:1298:1286= chunkVFp32(:1294):1304 DataCopy(chunkKFp32[row*kMatmulN], cNz[row*16], ...)
4ComputeValueAndVNewCube:1734:1722= chunkKFp32(:1730):1741 DataCopy(chunkVFp32[row*kMatmulN], cNz[row*16], ...)
5ComputeValueNewCube:1823:1811= kCumdecayFp32(:1819):1830 DataCopy(chunkAttnOutFp32[row*kMatmulN], cNz[row*16], ...)
6ComputeVNewCube:1909:1897= chunkKFp32(:1903):1918 DataCopy(chunkVFp32[row*kMatmulN], cNz[row*16], ...)
7ComputeOutputCube:2058:2046= kCumdecayFp32(:2054):2065 DataCopy(chunkAttnOutFp32[row*kMatmulN], cNz[row*16], ...)
8ComputeStateUpdateCube:2271:2259= chunkKFp32(:2262),分支内改指 tmpBuff 偏移 0(:2266):2282 / :2296 Add(stateInFp32[...], stateInFp32[...], cNz[...], 16, ...)

缩进本身也携带信息:第 1-7 处是 4 空格(函数体层级),第 8 处是 6 空格 —— 它在 ComputeStateUpdateCube 的 m-tile 循环体内,cCopyParams 的 M 维用运行期值 static_cast<uint16_t>(curTileM / 16)(:2268),是 8 处中唯一 blockLen 非常量的一个。

每处的替换都是同一形式的单行改动,实查 diff 原文(以 #1 为例):

-    DataCopy(cNz, c1Local, cCopyParams, cCopyEnhanced);
+    CopyL0CToUb(cNz, c1Local, cCopyParams, cCopyEnhanced);

#2-#7 的删除行与上完全相同(4 空格缩进),#8 的两个版本都多 2 个空格。 cCopyParams 的构造式各不相同,是区分 8 处的可靠指纹: #1 用 {kBlock/16, kBlock/16, 0, 0},#2-#7 用 {kMatmulN/16, kMatmulM/16, 0, 0}, #8 用 {kMatmulN/16, curTileM/16, 0, 0};8 处的 cCopyEnhanced.blockMode 一律是 BlockMode::BLOCK_MODE_MATRIX(grep -c BLOCK_MODE_MATRIX = 8)。

3.2 完备性核查(3 条可复跑命令) ​

锚点:src/048d0427/op_kernel/chunk_gated_delta_rule.h:489、1087、1259、1652、1781、1862、1973、2211

  1. 无残留裸搬:grep -n 'DataCopy(.*c1Local' src/048d0427/op_kernel/chunk_gated_delta_rule.h → 0 命中。L0C→UB 的读取已 100% 收口到封装。
  2. 一一对应:grep -n 'Alloc<TPosition::C1\|Hardware::L0C>' → L0C 分配器出现在 8 个行号 (:489、1087、1259、1652、1781、1862、1973、2211),与 8 个调用点同序同行号区间。
  3. 事件边从 0 到 1:
快照HardEvent::M_VHardEvent::V_MMmad(L0C 分配BLOCK_MODE_MATRIX
a9c66ddb(!1135 初版 Cube)002188
d0906c3e(!1135 终态)002188
88d8b965(+!1158)002188
048d0427(+!1174)332188

(L0C 分配口径 = grep -c 'Hardware::L0C>'。第一行很关键: 初版 Cube 化 a9c66ddb 就已经是"21 次 Mmad / 8 次 L0C 落地 / M↔V 事件边 0", 这与 docs/PR-1135-d0906c3e/02-实验复现.md:217-221 记录的"a9c66ddb 首跑即崩溃"独立互证 —— 缺陷自 Cube 化第一天就在,中间 commit 只是搬运它。)

(计数用 grep -o 'HardEvent::M_V\>' | wc -l,必须带词边界 —— 裸 grep -c HardEvent::V_M 会被 V_MTE2 / V_MTE3 污染成 33,是个容易踩的统计陷阱。) 048d0427 的 3 次 = FetchEventID + SetFlag + WaitFlag 各 1,全部在 CopyL0CToUb 内; V_MTE2(18) 与 V_MTE3(15) 在三个快照中一字不变,进一步证明 !1174 只在 M↔V 这一条边上动刀。

这张表是本 PR 最重要的单点证据:不是"某一处忘了加栅栏", 而是"21 次 Mmad、8 次 L0C 落地,M↔V 事件边计数为 0"。修 1 处当然不够。


4. PipeBarrier<PIPE_ALL> 挡不住什么 ​

锚点:src/048d0427/op_kernel/chunk_gated_delta_rule.h:511-517;src/d0906c3e/op_kernel/chunk_gated_delta_rule.h:1822-1834

4.1 两种原语的语义差别 ​

锚点:src/048d0427/op_kernel/chunk_gated_delta_rule.h:116-127、121、124;src/048d0427/op_kernel/chunk_gated_delta_rule.cpp:28;src/88d8b965/op_kernel/chunk_gated_delta_rule.h:499;src/d0906c3e/op_kernel/chunk_gated_delta_rule.h:1885-1897

PipeBarrier<PIPE_ALL>()SetFlag<H> / WaitFlag<H>(H = HardEvent::M_V 等)
表达的东西本指令流上的一个对齐点一条有名的跨管依赖(源管→宿管)
作用域只治理"我这条流"前后下发的顺序点名到具体管道对,别的管不受影响
能否表达"对端管已经退休"不能:它不携带源→宿语义,M 管与 V 管各自独立流水能:WaitFlag 阻塞到 SetFlag 点之前的源管工作真的执行完
方向性无方向(全管对齐)有方向:M_V 只管 M→V,管不了 V→M
代价打断流水,最贵一对事件寄存器操作,标量级
文件里用量PIPE_ALL 29 次、PIPE_V 92 次FetchEventID 25 处(23 处原有 + 本封装 2 处)

两条可在快照里复核的证据支撑这个判断,不需要借助任何外部说法:

  1. src/048d0427/op_kernel/chunk_gated_delta_rule.cpp:28 是 KERNEL_TASK_TYPE_DEFAULT(KERNEL_TYPE_AIC_ONLY); —— Cube 与 Vector 的指令由同一条流下发, 但在各管上独立退休。这种模式下"下发到哪"与"哪条管跑完了"是两件事,能表达后者的只有具名事件边。
  2. 修复前那处代码里,栅栏的位置就不对。d0906c3e ComputeVNewCube 的原文顺序是: :1885 最后一条 Mmad(c1Local, ...) → :1887-1888SetFlag<HardEvent::M_MTE1>(0); WaitFlag<HardEvent::M_MTE1>(0); → :1896 DataCopy(cNz, c1Local, cCopyParams, cCopyEnhanced); → :1897 PipeBarrier<PIPE_ALL>(); → :1904-1906 NZ→ND 读回循环。 也就是说:Mmad 与 L0C→UB 复制之间只有 M_MTE1 这一对(Cube↔L1 的完成点,语义是 "Cube 腾出了 L1,MTE1 可以继续搬下一块",完全不携带"VEC 可以来读 L0C"); 唯一的 PIPE_ALL 落在复制之后,治理的是"复制 → 读回"的本地顺序,而不是报错点名的 L0C 读写冲突。 CopyL0CToUb 补的正是复制前那条 M_V 和复制后那条 V_M。

事件才是唯一携带"完成 + 方向"语义的原语:SetFlag<HardEvent::M_V> 把 M 管当下的完成点登记成事件, WaitFlag<HardEvent::M_V> 让 V 侧在该事件上阻塞。方向必须成对: M→V("我写完了 L0C,你可以来读")与 V→M("我读完了,下一轮 Mmad 才能覆写") 是两条不同的事件边,HardEvent::M_V 与 HardEvent::V_M,不能互相代用。

4.2 竞争时序(以 docs 用的同一个函数 ComputeVNewCube 为例,行号实测) ​

锚点:src/d0906c3e/op_kernel/chunk_gated_delta_rule.h:1822、1843、1896、1905、1910

d0906c3e 中该函数起于 :1822,五件套 → L0C 落地 → V 侧读取的顺序是:

V    : StageHalf(stateInFp32, bStageGm, kBElements)          // :1843
MTE2 : Nd2Nz DataCopy a1Local <- aStageGm
MTE1 : LoadData  a2Local <- a1Local
M    : Mmad(c1Local <- a2 x b2, init = true)                    (1)
M    : Mmad(c1Local <- ... , init = false)  续累同一 L0C        (2)
M    : DataCopy(cNz, c1Local, ..., BLOCK_MODE_MATRIX)  L0C->UB   (3)  :1896
V    : for (row) DataCopy(chunkVFp32[row*kMatmulN], cNz[row*16], nzToNd)   (4)  :1905
V    : Add(chunkVFp32, chunkVFp32, chunkAttnOutFp32, kCElements)            (5)  :1910

((3) 与 (4) 的原文,逐字:src/d0906c3e/...:1896 的 DataCopy(cNz, c1Local, cCopyParams, cCopyEnhanced); 与 :1905 的 DataCopy(chunkVFp32[row * kMatmulN], cNz[row * 16], nzToNdParams);; 中间只夹着一个 PipeBarrier<PIPE_ALL>();。同一处在修复前快照 88d8b965 是 :1897 / :1906, 在修复后 048d0427 是 :1909 / :1918。)

竞争在 (3) 与 (4) 之间,且 (3) 与 (2) 之间也有:

  • (3) 未搬完,(4) 就读 cNz → V 读 L0C 数据通路 / CUBE 写 L0C 撞在同一地址;
  • (2) 的 Mmad 还在往 c1Local 累加,(3) 已经开始把它往外读 → 同一 L0C 地址同时被 VEC 读、被 CUBE 写。

CopyL0CToUb 的两个事件对正好各自堵住这两条:搬前的 M_V 让"上一轮 Mmad 写完 L0C"成为 "VEC 读 L0C"的前置条件;搬后的 V_M 让"本轮 VEC 读完 L0C / UB"成为"下一轮 Mmad 再写 L0C"的前置条件。

4.3 为什么 !1108 时代不存在这条边,因而没暴露 ​

锚点:src/048d0427/op_kernel/chunk_gated_delta_rule.h:342、413、540、547、660、883、909、950、1017、1169、1201、1220、1243、1403、1530、1541、2149、2411(全部 18 处 TQueSync)

!1108 的世界只有 V / S / MTE2 / MTE3 四条管,同步模式(TQueSync<PIPE_S,PIPE_V>、 V_MTE2/MTE2_V/V_MTE3)已足够,根本没有 M 管。 本文件里那 18 处 TQueSync 全是 PIPE_S↔PIPE_V 方向(TQueSync<PIPE_S,PIPE_V> 6 处 + TQueSync<PIPE_V,PIPE_S> 12 处),没有一处涉及 M 管或 L0C。 !1135 上 AIC 后新增的 M 管与 L0C 引入了新的跨管边,但 L0C→UB 的写法沿用了 !1108 时代 "UB 上写读 + PIPE_ALL"的惯性 —— 换硬件单元 = 新增同步边,这是移植类 bug 的通用形态。 TQue 的同步语义锚在队列位置(VECIN/VECOUT,V↔MTE),生产者/消费者是 V 管与 MTE 管, 它压根不感知 M 管,所以此处无法用队列替代裸事件。


5. 为什么症状是崩溃而不是算错 ​

锚点:docs/PR-1135-d0906c3e/10-Commit演进史.md:349-351

真机抓到的硬件报错,一字不差(出处 docs/PR-1135-d0906c3e/10-Commit演进史.md:349-351):

text
error code = 0x100000000020000, errorStr: L0C read/write conflict
(vec read operation or cube write operation). VEC read/write L0C and
cube read/write L0C addresses are the same.

这条报错本身就是机理证据,三个信息点全都在里面:

  1. 冲突发生在 L0C 上,不是 UB。UB 是普通 SRAM、有读写口仲裁,同址读写最坏算错; L0C 是 Cube 的累加器存储,不做 VEC/CUBE 同址仲裁,一旦撞上去就直接抛异常 → 这就是"症状是 exception 而不是数值偏差"的直接原因。
  2. 括号里点名了两造:vec read operation 或 cube write operation —— 与 §4.2 的 (3)V 读 L0C / (2)CUBE 写 L0C 一一对应,不是推测。
  3. addresses are the same:判据是地址相等,不是数据依赖。所以只要两条指令的 L0C 地址重叠即为非法, 与时序裕量共同决定"撞不撞得上"——这解释了为什么同一个 kernel 会时崩时不崩。

外层表现:runtime result = 507015(aicore exception)、 fault kernel_name=ChunkGatedDeltaRule_...(直接指认肇事 kernel), npu-smi 前后健康、重启自愈(出处 docs/PR-1135-d0906c3e/10-Commit演进史.md:346、 docs/PR-1174-048d0427/02-实验复现.md:69-72)。 另需注意崩溃会污染设备:a9c66ddb 首跑出现过"1 failed + 2 errors", 后两条 error 是崩溃后设备脏导致下一条用例的数据生成算子报错,不是三个独立 bug (出处 docs/PR-1135-d0906c3e/10-Commit演进史.md:340-342)。

对照 §01 那一类 bug 的表征:!1158 是 UB 内的地址别名,硬件不管,所以静默算错; !1174 是 L0C 上的跨管同址读写,硬件硬拦,所以间歇崩溃。同一个"重叠"关键词, 一个走数值通道、一个走异常通道,这是两类 bug 最根本的分野。


6. 为什么长序列才崩 ​

锚点:src/048d0427/op_kernel/chunk_gated_delta_rule.h:511-517(竞争窗口在稳态热循环里的位置)

竞争是时序窗口问题:崩溃 ⟺ V 管执行到 (4) 的时刻早于 M 管完成 (3) 的时刻。 平时不炸,是因为 (3) 的完成延迟通常短于 V 管从发射 (4) 到真正读之间要做的准备工作 —— 靠裕量掩盖。 裕量被吃光的两个条件,都随负载单调变薄:

  1. chunk 数 × head 数增加 → ProcessOneVTile 里的 Cube 往返次数线性增加 (表 #2/#3/#5/#6/#7/#8 所在函数几乎每个 chunk 都要走一遍), 连续多个 Cube 函数背靠背:V 管刚发完 (4)(5) 就被下一个函数调走,cNz 的读还悬着;
  2. 长序列 → 流水被填满:V 管在两次 Cube 往返之间的"闲活"(准备下一 stage 的 GM 暂存) 被 !1135 的双 slot 流水优化越压越薄,可掩盖延迟的空隙随之消失。

实测印证(出处与性质标明): 2×8×512×128×128(8 chunks × 8 heads × 满流水)约 1/3 概率崩溃, 单跑 3 次为 pass/crash/pass;1×16×128(2 chunks)从不崩 (docs/PR-1174-048d0427/01-L0C同步与裸Cube编程.md:44、PLAN.md:106,蓝区 310P3,2026-09-08)。 后续中间 commit 矩阵又给出两个互补的观察:long_t2048 跑 3 次 pass/crash/pass、 long_t512_batch2 2 次 crash/pass(docs/PR-1135-d0906c3e/10-Commit演进史.md:344-345)。 T 越长崩得越频繁,与"裕量变薄"的模型同向。

另一条独立的加固证据(同一 bug 谱系,非本文推演):崩溃 kernel 名后缀随 tilingKey 个数变化 (3 桶版 _2、4 桶版 _3),但崩溃 shape 簇(dk=128 长序列)、概率量级(~1/3)、 硬件机理(L0C 读写冲突)在 a9c66ddb → 69300ffc → d0906c3e 整条分支上完全一致 (docs/PR-1135-d0906c3e/02-实验复现.md:222-226)。 这排除了"某个中间 commit新引入竞争"的解释,也与本文 §3.2 的"M↔V 边计数一直为 0"完全吻合。


7. 按不变式修复,而不是按症状打补丁 ​

锚点:src/048d0427/op_kernel/chunk_gated_delta_rule.h:511、1183、1298、1734、1823、1909、2058、2271

不变式:任何"由 Cube 侧写 L0C、随后被 Vector 侧读"的搬运,两侧必须各有一道 M↔V 事件栅栏。

论证链条,每一环都是实查:

  1. 竞争是概率性的(§4、§6)。一次崩溃日志只能指认发生崩溃的那一个 kernel 实例, 无法指认"只有这一处有窗口"。测试能覆盖的 shape 簇是有限的: 实测崩溃集中在 dk=128 长序列(docs/PR-1135-d0906c3e/02-实验复现.md:225), 而表里 8 处分布在 8 个不同函数、由不同 fast-path 判定进入 —— 慢路径函数(#5 ComputeValueNewCube、#6 ComputeVNewCube)在快路径可用的 shape 下根本不被调用, 崩溃日志永远不可能指认它们。
  2. 逐点数竞争窗口,8 处都在,且宽度不同(grep -n 'Mmad(' 实测 21 处的分布):
    • #1 MatmulBlockFp32Compensated:L0C 目的由 MmadMatmulBlock(:452-459)里的单条 Mmad 按 init 开关续累(:456 的 init 是形参),一次调用内被反复累加多次再落地 —— 窗口中等;
    • #4 ComputeValueAndVNewCube:函数体内有 4 次 Mmad(:1673、1686、1713、1726, 两组 kAttnK / kStateK 链共享同一 c1Local)却只有 1 处 L0C 落地(:1734)—— 紧邻复用 + 落地次数少于累加次数,竞争窗口最长,与 docs 的判断一致;
    • #7 ComputeOutputCube:同样 4 次 Mmad(:1994、2007、2037、2050)对 1 处落地(:2058),结构与 #4 同构;
    • #2 CopyAttnCubeResult:本体只 1 处落地(:1183),但它被 ComputeAttnProductsCube 调用 2 次(:1127 目的 chunkScoresFp32、:1156 目的 chunkAttnOutFp32), 对应 attn 的两个矩阵 —— 单 chunk 内该封装执行 2 次;
    • #8 ComputeStateUpdateCube:落地在 m-tile 循环体内(:2271,6 空格缩进), M-tile 越多则触发次数越多,且 cNz 还在分支里换绑到 tmpBuff 偏移 0(:2266), 是 8 处中缓冲身份最不稳定的一个。
  3. 因此"只修测试崩的那一处"会在下一个 shape / 下一个 CANN 版本上以同样的形态复发; 8 处一次性替换的成本只是 8 行,收益是不变量成立。
  4. 可验证性:正因为做成了不变式,§3.2 的三条 grep 才能作为验收判据 (残留裸搬 0、L0C 分配 8 ↔ 调用 8、事件边 0→3+3)。 按症状打补丁的修复给不出这种可复跑的完备性证明 —— 这是两种修法的工程差别。

顺带一条对 review 的可操作建议(从 §3.2 的"事件边为 0"直接得出): 读任何 AIC 算子代码,先 grep -c 'Mmad(' 与 grep -c 'HardEvent::M_V\|HardEvent::V_M' 对比 —— 前者远大于 0 而后者为 0,就是同一形态的缺陷。


8. 为什么修复零性能开销 ​

锚点:src/048d0427/op_kernel/chunk_gated_delta_rule.h:116-127(封装体即全部新增工作)

实测(出处 docs/PR-1174-048d0427/02-实验复现.md:80-87,与 PLAN.md:113 一致; "修复前"列即 d0906c3e / !1135,蓝区 310P3、CANN 8.3.RC1,2026-09-08):

shape!1135 ms048d0427 ms
1×8×64×64×640.9280.858
1×16×128×128×1281.1511.141
1×1×128×128×2560.8310.819
2×8×512×128×1284.3554.373
1×16×1024×128×1288.7468.757
1×16×2048×128×12818.95417.357

6 个 shape 全部落在 ±1.5% 内(最大相对变化出现在 t2048:18.954→17.357,方向还是变快)。 正确性侧:d0906c3e 12 passed / 1 failed → 048d0427 15 passed / 0 failed / 13 skipped; long_t512_batch2 修复后连续通过(4.363 / 4.376 ms),无崩溃 (docs/PR-1174-048d0427/02-实验复现.md:76、89,PLAN.md:112、114)。 本文未复核这些数字,它们来自上述文档记录的蓝区实验。 注意一个二手数字的自相矛盾:同为 d0906c3e 的 6 shape 性能, PLAN.md:96-103(2026-09-08)给 0.928/1.151/0.831/4.355/8.746/18.954, 而 PLAN.md 中间 commit 矩阵(2026-09-14 重跑,:132-138)给 0.869/1.132/0.794/4.340/8.737/~17.4。 两个时点相差可达 6%(0.928 vs 0.869),量级与"零开销"结论同向,但引用时必须带时点。

零开销的机理(两条,都可在 §2 的代码上直接验证):

  1. 栅栏等待的事件本来就要发生。V 管反正必须等 cNz 搬完才能算 NZ→ND; M 管反正必须等上一轮 Mmad 写完 L0C 才能安全读它。SetFlag/WaitFlag 只是把这两个"本来存在的依赖" 从"靠时序运气满足"改成"靠硬件事件满足",没有引入任何新的等待关系。
  2. 新增指令的代价是标量级的。封装里除了那条原有 DataCopy 只多了 2 次 FetchEventID + 2 对 SetFlag/WaitFlag,都是寄存器/事件号操作;被等待的对象 Mmad 是微秒级。 更关键的是它没有用 PipeBarrier<PIPE_ALL>:文件里 29 次 PIPE_ALL 才是打断流水的贵原语, 而这次修复刻意选择了方向明确的事件(并且把 PIPE_ALL 原样留着), 用最小约束换正确性 —— 这也解释了为什么它连"变慢一点点"都几乎没有。

反面判据(本 PR 给出的可迁移结论):代价高昂的栅栏是加在不会自然发生的依赖上的那种。 比如把两个并无数据依赖的 Cube 调用用事件串起来,才是真实降速。 所以"加栅栏的判据是数据依赖,不是保守程度"。


9. 与既有文档的差异(以实查为准) ​

锚点:docs/PR-1174-048d0427/01-L0C同步与裸Cube编程.md:42、44、49、72-87;docs/PR-1158-88d8b965/01-缓冲重叠Bug深度剖析.md:81-82;PLAN.md:93、106

#既有说法(出处)实查结果判定
1全篇只描述"新增封装 + 8 处替换"(docs/PR-1174-048d0427/01:72-87、02 篇全篇)!1174 还含 32 行 Hardware:: → AscendC::Hardware:: 限定名改写(8 个 4 行 hunk)。grep -c 实测 32→0 与 0→32;docs 全文 grep 'AscendC::Hardware|限定名|namespace' 0 命中遗漏一类改动,本文 §1.1 补
28 处行号为 :508、1180、1295、1731、1820、1906、2055、2268(docs/PR-1174-048d0427/01:76-85 表)实测调用行是 :511、1183、1298、1734、1823、1909、2058、2271,每处恰好 +3系统偏差:那组数字是 diff 的 hunk 头起始行(含 3 行上文),不是改动行
3"封装在 :116-127"(同文件 :49)grep -n CopyL0CToUb → 定义确在 :116,SetGlobalTensors 起于 :128,故函数体 :116-127一致 ✓
4"修复把 8 处 DataCopy(cNz, c1Local, ...) 换成 CopyL0CToUb"(同文件 :74)成立,且更强:修复后无任何残留裸 DataCopy(..., c1Local, ...)(0 命中)一致,本文 §3.2 补完备性
5表 #1 频次写"64×64 里 3 次"、#8 写"128 桶调 2 次"(同文件 :78、:85)源码无法验证运行期次数(无调用计数器、无真机)。可验证的结构事实是:#1 的单条 Mmad 由 init 形参决定续累(:452-456),#8 在 m-tile 循环内(:2271)不可复核,本文只给结构事实
6机理表述为"④读到半新半旧的 UB → 数据错;更严重时 L0C 被下一个 Mmad 覆写而 UB 端还在读"(同文件 :42)真机报错点名的是 L0C 地址(VEC 读 L0C vs CUBE 写 L0C 同址),不是 UB本文 §5 以硬件报错为准,把冲突面钉在 L0C
7崩溃概率"~1/3"、shape 依赖(docs/PR-1174/01:44;PLAN.md:106)快照层面无法复核;两处记录一致,另有 2026-09-14 的 t2048/t512 复测记录补充二手,全部标出处
8grep -c 'HardEvent::V_M' 得 33(若有人这样统计)V_MTE2 / V_MTE3 会吞进前缀匹配;带词边界后 d0906c3e = 0、048d0427 = 3统计陷阱,本文 §3.2 已注明正确写法

另需指出(不指责任何一方):!1158 与 !1174 的作用点完全不相交 —— 前者改 :819-820(Vector 管 UB 别名),后者改 M↔V 事件边。 两 PR 互不依赖,谁先合入都不会让另一个失效;把 d0906c3e 的唯一精度 fail 同时归给两者 (PLAN.md:93)与归给"没被任何用例抓到"(docs/PR-1158/01:81-82)是矛盾的,见 §01 报告 §8 第 7 行。