Skip to content

11 · 成员变量与缓冲生命周期总表(2410-2461) ​

0. 结论先行 ​

锚点:chunk_gated_delta_rule.h:2410-2461

  1. 声明区相对 !1108 只增加了两行::2421 GM_ADDR workspaceAddr_; 与 :2437 LocalTensor<float> betaFp32;。其余 50 行逐字节相同(diff 可证,见 §6)。 "只有两行变化"与"整个 kernel 的内存模型已经换了"同时成立, 因为改变的是这些句柄的用法(ReinterpretCast 借用点从 3 处涨到 21 处),不是声明。
  2. 语义变化最大的不是新字段,而是 :2442 alignK_ 与 :2452 stateStrideK_: !1108 里两者是两套对齐规则(Ceil(dk,16)*16 与 Ceil(dk,8)*8,见 00e39ab5:88-89), !1135 里变成 alignK_ = 桶宽、stateStrideK_ = alignK_(:91-92), 即"K 的行宽"与"state 的行数"被强制统一到同一个编译期桶。 这一步是 ComputeStateUpdateCube<kSpecializedDk, kSpecializedDk>(:2096) 能把 kStateRows 当模板常量用的前提。
  3. :2423 stateOutQueue_ 的名字已经骗人:它是 TQue<QuePosition::VECOUT, 1> 的单槽队列, 在 !1135 里被当成全 kernel 通用的 FP16 中转板用了 8 个不同用途 (state 输出、StageHalf hi、StageHalfWithResidual hi/lo、A 转置源、final_state Cast 目标……), BUFFER_NUM = 1 使每一次 AllocTensor 都必须配一次 FreeTensor—— 漏掉一次 FreeTensor 就是死锁,多做一次就是踩数据。
  4. :2424 tmpBuff 是唯一的 UB 大 buffer,12 个成员句柄全是它上面的视图 (:161-202 的 12 次 GetWithOffset),因此"两个成员同时存活"是假象: 真正的约束是偏移区间是否重叠 + 时间是否错开。 典型形状(B=1,H=16,T=128,dk=dv=128)下 tmpBuff 实占 214784 B、 队列 32768 B,合计 247552 B(≈241.8 KiB,UB 256 KiB 的 94.5%),只剩 14592 B 空闲。
  5. chunkVFp32 是全场最忙的板:声明注释只写了 // cs * avFp32 (v_beta, then v_new) 两个身份, 实际快照里它至少承担 11 种身份(§5.2 逐条给锚点), 包括被 ReinterpretCast 成 FP16 staging 4 次、被切成 5 块 scratch 2 次、当 L0C 落地板 2 次。
  6. 风险清单 (c) 里唯一真实爆过的是 :819 + :827: betaLocal 与 Cast 目的端是同一块 chunkVFp32,FP16→FP32 是原地增宽, 目标区间覆盖源区间且向后扩张,V 管按块前推时先写的 FP32 会毁掉尚未读的 FP16。 !1158(88d8b965:820)的修法正是把 betaLocal 换成借 chunkAttnOutFp32; 本仓库 048d0427(!1174)的 :832/:840 也是同一修法。
  7. 为什么纯 Vector 时代这种借用安全、Cube 化后开始破(§8 展开): Vector 时代的每一步都自带 PipeBarrier<PIPE_V>,借用者与被借用者在同一条管上顺序执行; Cube 化引入了 M/MTE1/MTE2/MTE3 四条新流水与 L1/L0A/L0B/L0C 四级私有存储, 同一块 UB 现在可能被两个不同的管在两个不同的时刻读写,而代码里只写了同管栅栏。

1. GM 句柄区(2410-2421):十个输入输出指针 + 一个裸地址 ​

锚点:chunk_gated_delta_rule.h:2410-2421

cpp
 private:
  GlobalTensor<inType> queryGm_;
  GlobalTensor<inType> keyGm_;
  GlobalTensor<inType> valueGm_;
  GlobalTensor<float> gGm_;
  GlobalTensor<inType> betaGm_;
  GlobalTensor<inType> initStateGm_;
  GlobalTensor<int32_t> actualSeqLengthsGm_;
  GlobalTensor<outType> finalStateGm_;
  GlobalTensor<outType> attnOutGm_;
  GlobalTensor<float> stateWorkspaceGm_;
  GM_ADDR workspaceAddr_;
  • :2410 private::从这里到文件尾只有数据成员,没有函数(函数全在 :246-2408)。
  • :2411-:2413 queryGm_/keyGm_/valueGm_:三个 GlobalTensor<inType>(FP16), 在 SetGlobalTensors:117-119 绑定;被 LoadPaddedRows(:272-340)与 fallback 的标量读消费。
  • :2414 gGm_:唯一的 GlobalTensor<float> 输入(g 是 log-decay,FP32), :123-125 仅在 hasGamma_ != 0 时绑定——未绑定时任何对它的访问都是空指针, 所以 :840 的 canUseGDma 把 hasGamma_ != 0 放在第一位。
  • :2415-:2416 betaGm_(FP16)、initStateGm_(FP16,[B,Nv,Dv,Dk])。
  • :2417 actualSeqLengthsGm_(int32_t):变长 mask,ComputeAvgload:207-210 与 Process:217+ 用它算负载均衡。
  • :2418-:2419 finalStateGm_/attnOutGm_:两个 GlobalTensor<outType> 输出。
  • :2420 stateWorkspaceGm_:GlobalTensor<float>,既是跨 chunk 的 FP32 state 存放区, 又被 GetCubeStageBase:249-255 当成 Cube staging 的基址——同一段 GM 两种用途, 靠"workspace 在前、stage 在 B_*NV_*realK_*stateWorkspaceStrideV_*4 之后"分开(:250-253)。
  • :2421 GM_ADDR workspaceAddr_;(新增字段):workspaceGm_ 是类型化的 GlobalTensor<float>, 做字节级偏移要绕;Cube staging 的布局是按 字节 排的(64 KiB slot × 2), 所以额外存一份裸地址(:129 绑定、:102 构造里置空)。 这是"Cube 化只需要加一个字段"的直接体现。

2. 队列与 UB 宿主(2422-2424) ​

锚点:chunk_gated_delta_rule.h:2422-2424

cpp
  TPipe *pipe_;
  TQue<QuePosition::VECOUT, 1> stateOutQueue_;
  TBuf<TPosition::VECCALC> tmpBuff;
  • :2422 TPipe *pipe_;:!1135 里它不只用来 InitBuffer, 还承担 pipe_->FetchEventID(HardEvent::V_MTE3) 这类取事件 ID 槽的调用 (:2163/:2168/:2322/:2341/:2389 等), 这是裸 Mmad 编程下自己管理跨管事件的必需品。
  • :2423 TQue<QuePosition::VECOUT, 1> stateOutQueue_;: QuePosition::VECOUT = "向量管输出方向的队列",模板参数 1 = BUFFER_NUM(:34), 只有一个槽。InitLocalBuffers:138-143 给它 max(stateTileBytes, chunkOutBytes); 典型形状 max(128*128*2, 64*128*2) = 32768 B。 它在快照里的 8 类用途::1559/:1582(StageHalf 系列的中转)、:2145(A_hi/A_lo 转置源)、 :2351(final_state 的 Cast 目标)、:1045(K 的 FP16 转置源)、 以及 :1562/:1563、:1585/:1586、:2354/:2355 的 EnQue+DeQue 当跨管栅栏用。 命名与用途已经分离,03 章 Bug 清单里"队列槽位泄漏"一类风险都挂在这一行上。
  • :2424 TBuf<TPosition::VECCALC> tmpBuff;:UB 计算区的唯一宿主,:158 用 restUbSize_ 开。 TPosition::VECCALC 表明它占的是"向量计算"分区(与 VECIN/VECOUT 相对), 但 !1135 把 MTE2/MTE3 的 DMA 目的/源也指到这里(:823/:845/:1493/:2166 等), 分区语义被当成了纯粹的地址空间用。

3. 12 个 UB 视图声明(2425-2437):注释只写了"第一身份" ​

锚点:chunk_gated_delta_rule.h:2425-2437

cpp

  LocalTensor<float> chunkKFp32;        // cs * alignK (original K)
  LocalTensor<float> kCumdecayFp32;     // cs * alignK (k_beta then k_cumdecay)
  LocalTensor<float> decayMaskFp32;     // cs * cs
  LocalTensor<float> chunkVFp32;        // cs * avFp32 (v_beta, then v_new)
  LocalTensor<float> chunkScoresFp32;   // cs * cs (attn matrix)
  LocalTensor<float> stateInFp32;       // dk * avStepAligned (state [DK, vStep])
  LocalTensor<float> chunkAttnOutFp32;  // cs * avFp32 (output)
  LocalTensor<float> gCumsumFp32;       // cs
  LocalTensor<float> deltaFp32;         // temp: max(stateStrideK_, chunkSize_, vStepAligned_)
  LocalTensor<float> dotProductFp32;    // dot-product multiply/reduction scratch
  LocalTensor<float> expGCumFp32;       // precomputed exp(gCumsum) [cs]
  LocalTensor<float> betaFp32;          // beta for the current chunk/head [cs]

逐行一句(容量公式与 §5 表 (a) 一致;"实际身份数"见 §5.2):

  • :2425 空行,:2426 chunkKFp32 // cs * alignK (original K):出生是"原始 K", 第二次身份是 ComputeKCumdecay:1198/1203 的 k_cumdecay 落点, Cube 化之后又被借为 FP16 Q 板(:1946)、FP16 K 板(:2133)、FP32 残差板(:2171-2173)、 L0C 落地板(:1717/:1890/:2249)——注释里一个都没写。
  • :2427 kCumdecayFp32 // cs * alignK (k_beta then k_cumdecay): 实际还兼任 attn 的 NZ 落地板(:2041)、A 转置板(:2146)、final_state 转置板(:2374)、 state 载入的 stateNd(:1481)。
  • :2428 decayMaskFp32 // cs * cs:唯一"只被写两次读多次"的板(:917-935 生产, :993/:1009/:2000/:2081 消费),它和 :2253 的 tmpBuff.GetWithOffset<float>(kStateElements, 0) 有重叠关系(§7 风险 #21)。
  • :2429 chunkVFp32 // cs * avFp32 (v_beta, then v_new):全场最忙,见 §5.2 与 §7。
  • :2430 chunkScoresFp32 // cs * cs (attn matrix):与 :2431 同基址(:175-176)。
  • :2431 stateInFp32 // dk * avStepAligned (state [DK, vStep]): 注释里的 dk 实际是 stateStrideK_(= 桶宽),不是 realK_。
  • :2432 chunkAttnOutFp32 // cs * avFp32 (output):还兼任 V 的 FP16 staging(:1344)、 beta 广播块(:1374)、K 的 FP32 板(:2135)、StageHalfWithResidual 的 scratch(:1954/:2006)。
  • :2433 gCumsumFp32 // cs:还是前缀和 ping-pong 的一端(:791-807)。
  • :2434 deltaFp32 // temp: max(stateStrideK_, chunkSize_, vStepAligned_): 注释是三者取最大,代码 :191-193 用两次二值 max 实现同一件事; 身份最多:残差列(:524)、前缀和对端(:792)、衰减系数向量(:2124-2128)、 标量逐元素缓冲(:643/:1002)。
  • :2435 dotProductFp32 // dot-product multiply/reduction scratch: 还被 ReinterpretCast<uint32_t> 当 gather 偏移表(:525/:815)。
  • :2436 expGCumFp32 // precomputed exp(gCumsum) [cs]:只被读(:1185/:1950/:2065/:2122/:2298), 唯一一个从生到死身份不变的 FP32 板。
  • :2437 betaFp32 // beta for the current chunk/head [cs](新增字段): !1108 的 beta 是逐 token 现取(LoadBeta),!1135 为了把 Brcb + 高维 Mul 用起来(:876-894)必须先有一整块 64 元素的 beta 向量, 这就是新增这个成员的全部理由。

4. 20 个标量成员(2438-2458):几何、桶宽与负载均衡 ​

锚点:chunk_gated_delta_rule.h:2438-2458

cpp

  uint32_t B_;
  uint32_t T_;
  uint32_t NK_;
  uint32_t alignK_;
  uint32_t realK_;
  uint32_t NV_;
  uint32_t realV_;
  uint32_t vStep_;
  uint32_t chunkSize_;
  uint32_t numChunks_;
  uint32_t hasGamma_;
  uint32_t restUbSize_;
  uint32_t vStepAligned_;
  uint32_t stateStrideK_;
  uint32_t stateWorkspaceStrideV_;
  uint32_t load_;
  uint32_t usedblk_;
  uint32_t avgload_;
  float scale_;
  uint64_t blockIdx_;
  • :2438 空行。
  • :2439-:2441 B_/T_/NK_:批次、总序列长、QK 头数(:78-80)。
  • :2442 alignK_——桶化后的语义改变者。 :90-91:naturalAlignK = Ceil(dk,16)*16; alignK_ = (kSpecializedDk != 0 && dk <= kSpecializedDk) ? kSpecializedDk : naturalAlignK。 即桶内:行宽 = 桶宽(64/80/96/128);桶 0 或 dk 超过桶宽:退回 16 对齐。 它同时决定:LoadPaddedRows 的目的行宽(:283/:296)、 高维 Mul 的 rowStride(:884)、cs*alignK 的缓冲容量(:161/:164)、 以及 :2348 的 alignedElem。四个互不相关的算法共用一个数, 这就是为什么它的注释只写 uint32_t alignK_; 而必须靠本篇把它的作用域列出来。
  • :2443 realK_ = tilingData->dk(:81):真实头维,一切 %16、<= 桶宽 判断的左值 (:259/:2361/:2372)。
  • :2444-:2446 NV_/realV_/vStep_:value 头数、真实 Dv、每片 V 步长(:82-83/:88)。
  • :2447-:2448 chunkSize_/numChunks_:块长(当前恒 64)与块数。 chunkSize_ 是 kMatmulM/kMatmulK = 64 这些"魔数"的真实来源 (ComputeOutputCube:1926-1927 把 64 写成了 constexpr,与 chunkSize_ 是重复表达, 靠 IsCubeFastPath:259 的 chunkLen == 64 保证一致)。
  • :2449 hasGamma_:可选 g 的存在位(:87)。
  • :2450 restUbSize_ = tilingData->ubRestBytes(:89): host 侧 SolveVStep 算出的 ubSize - outQueueMax(02b 章 §SolveVStep), tmpBuff 的容量就是它,多出来的 14592 B(典型形状)是 GetWithOffset 借用的安全垫。
  • :2451 vStepAligned_:Ceil(vStep_, 8)*8,在 InitLocalBuffers:135-136 才赋值, 构造里置 0(:100)。全 kernel 用 avFp32/vStepAligned_ 两个名字指它 (:719/:1133 等),是本篇最容易读错的一个。
  • :2452 stateStrideK_——语义被简化成 alignK_ 的别名(:92 stateStrideK_ = alignK_;)。 !1108 里它是独立的 Ceil(dk,8)*8(00e39ab5:89)。 现在它的用途是:stateInFp32 的行数(:176/:181)、 alignedElem(:2348)、outQueueBytes 的 stateTileBytes(:139)。 注意它不影响 workspace 布局(那是 stateWorkspaceStrideV_), 所以桶化不会改变 GM 上 FP32 state 的字节数,只改变 UB 里的行宽。
  • :2453 stateWorkspaceStrideV_ = Ceil(dv,8)*8(:93):workspace 的行宽, :2329/:2336 的 dstGapBlocks 依赖它。
  • :2454-:2456 load_/usedblk_/avgload_:负载均衡游标(IsCurrentBlock:342-350、 ComputeAvgload:205+),与 !1108 完全一致,Cube 化没有动调度。
  • :2457 scale_:1/sqrt(dk) 类 attr(:84),在 :1950 与 :959 被吸收进 Q 的行缩放。
  • :2458 blockIdx_:GetBlockIdx()(:107),Cube staging 槽位分配的唯一依据 (:253 blockIdx_*CUBE_STAGE_SLOT_COUNT + slot)——每 core 2 个 64 KiB slot。
  • :2459 }; 类结束;:2460 空行;:2461 #endif // CHUNK_GATED_DELTA_RULE_KERNEL_H_ 文件结束。

5. 表 (a):容量公式、实字节与生老病死 ​

5.1 容量与偏移(128 桶为典型形状,80 桶用于交叉验证) ​

锚点:chunk_gated_delta_rule.h:132-203(InitLocalBuffers)

典型形状 B=1, H=16(T=128 时 NK_=NV_=16), T=128, dk=dv=128: 桶 = SelectCubeDk(128) = 128,alignK_ = stateStrideK_ = 128,chunkSize_ = 64, numChunks_ = 2,vStep_ = 128(单片),vStepAligned_ = 128,stateWorkspaceStrideV_ = 128。

成员(行号)注释公式元素数实字节(128 桶)tmpBuff 内偏移80 桶字节
chunkKFp32 :2426cs * alignK819232768020480
kCumdecayFp32 :2427cs * alignK8192327683276820480
decayMaskFp32 :2428cs * cs4096163846553616384
chunkVFp32 :2429cs * avFp328192327688192020480
chunkScoresFp32 :2430cs * cs(重叠分支下句柄按 overlapSize 申请)逻辑 4096 / 句柄 16384(65536,与 stateIn 同址)114688(25600,句柄 6400)
stateInFp32 :2431dk * avStepAligned163846553611468825600
chunkAttnOutFp32 :2432cs * avFp3281923276818022420480
gCumsumFp32 :2433cs64256212992256
deltaFp32 :2434max(stateStrideK_, cs, vStepAligned_)128512213248320
dotProductFp32 :2435max(stateStrideK_, cs)128512213760320
expGCumFp32 :2436cs64256214272256
betaFp32 :2437cs64256214528256
tmpBuff 合计——2147840 … 214784125312
stateOutQueue_ :2423max(dk*avStep, cs*avStep)*sizeof(outType)16384 half32768独立 buffer12800
UB 实占——247552—138112

三点核对说明:

  • scores 与 state 同基址::173-177 的 if (vStep_ >= realV_ || ShouldSplitVTiles()) 在 vStep_ == realV_ == 128 时成立,两块都用未自增的 off(:175 与 :176), 只推进 overlapSize;overlapSize = max(cs*cs, stateStrideK_*avStepAligned) = max(4096,16384) = 16384。 这是全表唯一一对静态重叠,理由是"scores 的最后一次读在 ComputeVNew, 而 state 的第一次写在 LoadStateTile"(注释 :150-152)。
  • 80 桶的 125312 与实测日志一致:docs/PR-1135-d0906c3e/02-实验复现.md 记录 d0906c3e: dk=80, vStep=80, tbuf=125312。本表逐列相加 20480+20480+16384+20480+25600+20480+256+320+320+256+256 = 125312 ✓, 说明本表不是估算而是与 host 侧 ComputeTmpBuffBytes(02b 章)同构的重算。
  • 余量:restUbSize_ = ubSize - outQueueBytes,128 桶下 tmpBuff 被要求 214784 B, 而 InitBuffer(tmpBuff, restUbSize_)(:158)给的是全部余量; 表内偏移累加到 214784,剩下的字节没有被任何具名句柄占用, 只被 :2253 的 tmpBuff.GetWithOffset<float>(kStateElements, 0) 这类"从 0 号偏移借三块板"的写法引用。

5.2 身份链:出生 → 每次变身份 → 死亡 ​

锚点:chunk_gated_delta_rule.h:161-202(出生)与全文件(变身份)

只列有实际读写动作的锚点;"死亡"指最后一次被读,"再出生"指被别的用途重新写入。

成员出生(:161-202)身份链(行号:身份 / 读者)死亡
chunkKFp32:161:767 原始 K(LoadPaddedRows 目的)→ :888/:892 被 ComputeKBeta 读 → :980/:992/:1008 被 attn 读 → :1047 被 ComputeAttnProductsCube Cast 成 FP16 读 → :1181/:1198/:1291 写成 k_cumdecay → :1544 被 ComputeVNew 读 → :1680/:1717 被 ComputeValueAndVNewCube 当 A、当 cNz → :1842/:1890 同上(另一条链)→ :1946 变 FP16 Q 板 → :2133 变 FP16 K 板 → :2171-2175 变 FP32 残差板 → :2249 变 L0C 落地板:2250-2253(≤96 桶被 tmpBuff 头 0 号偏移整体覆盖);128 桶死于 :2283 之后
kCumdecayFp32:164:881/:888 写 k_beta → :980/:1068/:1186/:1203 读 → :1240/:1632/:1761 当 A 源 → :1481 变 stateNd(FP16) → :1682/:1842 当残差 scratch → :1948 变 Q 的 FP32 板 → :2000 attn 副本 → :2041 变 cNz → :2146 变 A 转置板 → :2374 变 final_state 转置板下一个 chunk 的 :881 重新写
decayMaskFp32:167:917/:926/:931/:935 写 decay_mask → :964/:981/:993/:994/:1009/:1010 attn 逐元素读 → :1145/:1148/:1153 Cube 化的读 → :2000 再读(10 章 §7 依据)→ :2081 fallback 读:2251-2253 被并入"三块板"覆盖
chunkVFp32:170:522-523 切 inverse + coefficientBlocks → :563-567/:599-603 切 5 块 scratch → :765 变 FP16 K 板 → :819 变 FP16 beta 板 → :827 原地增宽 Cast(!1158 爆点) → :845 变 FP32 g slab → :874 变 betaBlocks → :921 变 gBlocks → :956 变 FP16 Q 板 → :1046 变 K 转置板 → :1068/:1113/:1239 当残差 scratch → :1166/:1281 变 cNz → :1368/:1396/:1404 写成 v_beta → :1482 变 stateTransposed → :1540/:1423 被读 → :1728/:1905/:1908/:1910 写成 v_new → :2007/:2192 当 B₂ → :2082/:2308 被 fallback 读下一个 V-tile 的 :1368 重新写
chunkScoresFp32:175/:179:971/:973/:984/:986 写 attn → :1114/:1145/:1147/:1151 被读 → :1202/:1422 fallback 逐元素读 → :1233/:1626/:1755 当 A 源(Muls(..., chunkScoresFp32, 1.0f, ...) 的复制读)与 stateInFp32 同址:LoadStateTile 执行即死
stateInFp32:176/:181:1429/:1433 写(workspace 读入 / Duplicate 清零)→ :1447/:1452/:1512 装载 → :1523/:1545 被读 → :1683/:1843 当 B₁ → :2068/:2300/:2311 fallback → :2123 衰减 → :2269/:2283 累加 Cube 结果 → :2332/:2337 写 workspace → :2349/:2352 Cast 成 final_state末块之后无读者
chunkAttnOutFp32:185:657/:660 写 attnOut → :958 变 Q 的 FP32 缓存 → :990 被 attn 读 → :1113/:1143/:1233/:1236/:1632/:1761 当 A 源或残差 scratch → :1344/:1374 变 V 的 FP16 staging + betaBlocks → :1410/:1417/:1420 写成 attn_inter → :1551 注释指它 dead → :1910 读 → :1954/:2006 当残差 scratch → :2052 变 cNz 的 ND 目的 → :2057/:2060/:2079 被 fallback 写 → :2135 变 K 的 FP32 板 → :2141/:2147/:2173 读写:2249 之后无人读
gCumsumFp32:188:805/:852/:856/:861 写 → :905/:907/:917/:923 读 → :2121 末元素读 → :2307 fallback 读下一 chunk :852
deltaFp32:193:524 变 column → :635/:643 递归 attn 标量读 → :792 前缀和 ping-pong 对端 → :1002 变 query 标量缓冲 → :2124-2140 变衰减系数向量下一个用途覆盖,无固定死亡点
dotProductFp32:196:391/:398/:406/:410 点积 scratch → :525/:815 变 uint32 gather 偏移表同上
expGCumFp32:199:907 唯一写;:1185/:1950/:2065/:2122/:2298 读下一 chunk :907
betaFp32:202:831/:835 写;:876 Brcb 读;:1376/:1401 读下一 chunk :831

一句话总结:12 个具名句柄里,只有 expGCumFp32 与 betaFp32 的身份数与注释一致; chunkVFp32 注释写 2 个身份、实际 11 个;chunkAttnOutFp32 注释写 1 个、实际 7 个。

6. 表 (b):与 !1108(src/00e39ab5/...:917-966)的逐字段对比 ​

锚点:chunk_gated_delta_rule.h:2410-2461 对 00e39ab5/op_kernel/chunk_gated_delta_rule.h:917-966

核对命令(已执行): diff <(sed -n '918,964p' src/00e39ab5/op_kernel/chunk_gated_delta_rule.h) <(sed -n '2411,2459p' src/d0906c3e/op_kernel/chunk_gated_delta_rule.h) → 输出只有 2 行新增(>),0 行删除(两个区间都取到 private: 之后第一行至 };; !1108 的 :965-966 文件尾空行 + #endif 不参与比较,否则 diff 会多出 2 行伪删除)。

成员!1108 行!1135 行声明是否变化语义变化
queryGm_ … attnOutGm_918-9262411-2419逐字节同Q/K/V 的读法从标量 GetValue 变成 LoadPaddedRows 带跨距 DMA(:272-340);betaGm_/gGm_ 变成"整 slab 一次 DMA + Gather"(:812-864)
stateWorkspaceGm_9272420同被劈成两半用:前半仍是 FP32 state(:2332),后半被 GetCubeStageBase:249-255 拿去当 Cube staging
workspaceAddr_—2421(新增)新字段为字节级 slot 寻址而生;!1108 完全不需要
pipe_9282422同多出 FetchEventID 用法(裸 Cube 自管事件)
stateOutQueue_9292423同(含 BUFFER_NUM=1)从"状态输出队列"退化为通用 FP16 单槽中转板(8 类用途,见 §2)
tmpBuff9302424同由 GetWithOffset 派生的借用点从 3 处涨到 21 处
chunkKFp329322426同(含注释)注释仍写 "original K";实际新增 5 个非声明身份(§5.2)
kCumdecayFp329332427同新增 4 个非声明身份,且是 final_state 转置板
decayMaskFp329342428同不变(仍只做 decay mask);但被 :2253 并入借用区
chunkVFp329352429同变化最剧烈:!1108 里它只做 v_beta/v_new 与 5 块递归 scratch;!1135 加了 4 次 FP16 staging + 2 次 cNz
chunkScoresFp329362430同不变(与 state 同址的规则在 !1108 已存在,见 00e39ab5:170-171)
stateInFp329372431同装载路径新增转置快路(:1478-1515);衰减从 realK_ 条 Muls 变 1 条
chunkAttnOutFp329382432同新增 6 个非声明身份(含 K 的 FP32 板)
gCumsumFp32 / deltaFp32 / dotProductFp32 / expGCumFp32939-9422433-2436同deltaFp32 新增"衰减系数向量"身份(:2124-2128),是 Step7 批量化的关键
betaFp32—2437(新增)新字段为 Brcb + 高维 Mul 的 beta 广播(:876-894)与 Gather(:831)服务
B_ T_ NK_ realK_ NV_ realV_ vStep_ chunkSize_ numChunks_ hasGamma_ restUbSize_ vStepAligned_ stateWorkspaceStrideV_ load_ usedblk_ avgload_ scale_ blockIdx_944-963(缺 2451 同名项一致)2439-2458 中除 2442/2452逐字节同—
alignK_9472442同Ceil(dk,16)*16(00e39ab5:88)→ 桶宽(:91):桶内行宽变成编译期常量
stateStrideK_9572452同Ceil(dk,8)*8(00e39ab5:89)→ = alignK_(:92):两个不同对齐规则合并成一个,state 行数与 K 行宽从此永远相等

这张表最该记住的两件事: (1) 新增字段只有 2 个,说明 !1135 的架构侵入度极低; (2) 真正承重的是 :91-92 两行赋值,声明区里那两个字段一个字节都没改。 桶化的全部代价被藏在"语义变了但签名没变"的两个 uint32_t 里—— 这也是读 !1135 diff 时最容易漏掉的地方。

7. 表 (c):缓冲借用风险清单 ​

锚点:chunk_gated_delta_rule.h 全文件(借用点)+ InitLocalBuffers:132-203(地址关系)

"是否真错开"的判据:借用发生时,出借方最后一次读的行号是否严格早于借用行号; 同管时看中间有没有 PipeBarrier<PIPE_V/PIPE_ALL>,跨管时看有没有对应的 SetFlag/WaitFlag 或队列 EnQue/DeQue。

#锚点借用者(别名)出借方出借方当时的身份最后读者是否真错开风险
1:522-523inverse / coefficientBlockschunkVFp32上一 V-tile 的 v_new:2308(上一轮)✓ 跨 chunk 首尾相接低
2:524columndeltaFp32未初始化—✓低
3:525gatherOffsets(uint32)dotProductFp32点积 scratch:410✓ 但类型宽度不同,SetValue 写 32 位低
4:563-567 / :599-6035 块 srcA/srcB/residualA/residualB/productchunkVFp32inverse(#1 的产物):552(Muls(buf…, inverse…))✓ 同一函数内顺序;但 kBlock 在 32/64 两层不同,5 块切片尺寸随递归层变化中:切片总量 5*kBlock*kBlock,64 层是 20480 float > 16×16 层的 1280,靠"先做完内层再做外层"串行
5:765keyLocal(FP16)chunkVFp32未定义—✓低;容量守卫 :760-763(2*chunkSize_*vStepAligned_ ≥ chunkLen*alignK_)
6:819betaLocal(FP16)chunkVFp32#5 的 K 板在 :338 的 Cast 里已消费✓低
7:827Cast(chunkVFp32, betaLocal, …)chunkVFp32同一个 betaLocal—✗ 源与目的同一地址,且 FP16→FP32 原地增宽高:这就是 !1158 修的 bug;88d8b965:820 与 048d0427:832 把 betaLocal 改借 chunkAttnOutFp32
8:845g slab 直写 chunkVFp32chunkVFp32#7 的 beta FP32 slab:831 Gather 已完成✓低
9:874betaBlockschunkVFp32#8 的 g slab:852 Gather 已完成✓低
10:921gBlockschunkVFp32#9 的 betaBlocks:888/:892✓低
11:956queryLocal(FP16)chunkVFp32#10 的 gBlocks:926✓ 中间有 :928 PipeBarrier低
12:1046keyTransposed(FP16)chunkVFp32#11 的 Q 板:1068 之前无人读 Q 板?Q 板其实在 :1068 被当作 scratch 再次覆写✓ 但连续两次借用中:#11 与 #12 相隔 90 行、身份完全不同,靠 IsCubeFastPath 保证 :956 的缓存一定没被消费
13:1068/:1113/:1239/:1954/:2006scratch 形参chunkVFp32 或 chunkAttnOutFp32各自各自✓ StageHalfWithResidual:1566-1568 的 MTE3_V 等待保证 MTE3 读空后才写 scratch低(但正确性完全依赖那 3 行事件)
14:1166 / :1281cNzchunkVFp32#11/#12:1066✓中:cNz 需要 64*64/64*128 float,128 桶下正好等于 chunkVFp32 全容量 32768 B,贴边无余量
15:1344 / :1374valueLocal(FP16) / betaBlockschunkAttnOutFp32注释声称"dead until V-tile"(:1334-1335、:949-951):990(attn 读 Q 缓存)✓ 但完全靠调用顺序:PrepareAttnQueryCache:729 → ComputeAttnMatrix:730 都在 ProcessVTiles:732 之前,所以第 2 个 V-tile 的 :1344 覆写时 Q 缓存早已消费完中:守卫 CanCacheAttnQuery:942-946 只查容量、不查"Q 缓存是否已消费",正确性外包给了 ProcessChunk 的行序;把 PrepareAttnQueryCache 挪进 V-tile 循环就会立刻破
16:1481 / :1482stateNd / stateTransposed(FP16)kCumdecayFp32 / chunkVFp32attn 板 / #12:1240 / :1066✓ 注释 :1466-1467 明说"kCumdecayFp32 and chunkVFp32 are dead at this point"中:容量守卫 :1478-1480 要求 realK_*vStepAligned_ ≤ 2*chunkSize_*alignK_ 且 ≤ 2*chunkSize_*vStepAligned_,128 桶下前者 16384 ≤ 16384 贴边
17:1717 / :1890 / :2249cNzchunkKFp32残差 / Q 板:1680 / :1842 / :2173✓ 都在 staging A 之后中:三处都用"整块 chunkK",贴边
18:1806 / :2041 / :2146 / :2374cNz / transposedLocal / stateTransposedkCumdecayFp32k_cumdecay 或 attn:1682 / :2000 / :2190✓低-中(同一块板在 4 个函数里 4 种身份)
19:1946queryLocal(FP16)chunkKFp32k_cumdecay(已死):1682(融合路径)/ :1842(分立路径)✓低;只用到前一半字节(16384 B / 32768 B)
20:2133 / :2135keyLocal(FP16) / dst FP32chunkKFp32 / chunkAttnOutFp32#19 的 Q 板 / 已写出的 attnOut— / :657 之后✓ 由 ProcessOneVTile:1326 → :1327 的顺序保证中:跨函数依赖调用顺序,函数内部看不出来
21:2253cNz = tmpBuff.GetWithOffset<float>(kStateElements, 0)chunkK + kCumdecay + decayMask 三块拼接三块都已死(注释 :2251-2252):2000 / :2190 / :2081✓ 前提是本 chunk 不再走 fallback::2081 的 decayMaskFp32.GetValue 若发生在 :2253 之后就会被读脏高:ProcessOneVTile 里 Step5/6 的 fallback(:1322-1325)与 Step7 的 Cube 分支(:2295)用的是同一个谓词(IsCubeFastPath),所以二者不会同时发生;将来若把谓词拆成两个,#21 立刻变成静默数据破坏
22:2145/:2159 → :2191队列 stageLocalstateOutQueue_上一个用途的残留—✓ FreeTensor 在 :2191中::2145 与 :2191 之间跨度 46 行、内含 3 次 DMA 与 2 轮转置,任何提前 return 都会漏 FreeTensor(BUFFER_NUM=1 → 下一 chunk 死锁)
23:2351/:2354-2355队列 stateLocalstateOutQueue_——✓,但 FreeTensor 出现在两条出口(:2394 与 :2407)中:新增第三条出口时必须记得归还

8. 结论:为什么这套借用在纯 Vector 时代安全、Cube 化之后开始破 ​

纯 Vector 时代(!1108)的隐含契约有两条,且都自动成立: (1) 所有算术都在 V 管上顺序执行,任意两次指令之间只要有一条 PipeBarrier<PIPE_V>(本文件的 fallback 几乎每条 Mul/Axpy/Exp 后都跟一条, 如 :895/:906/:908/:933/:1200/:1204/:2302/:2313),就不存在乱序可见性; (2) UB 上任何一个时刻只有一条流水在写,所以"借用哪块板"只取决于程序顺序上的最后一次读—— 这是一个纯文本可判定的条件,人眼 review 就够用。 !1108 里 chunkVFp32 的借用点只有 3 处(:522/:563/:599),全部在同一个函数族内部, 所以它的声明注释"v_beta, then v_new"与实际用法是一致的。

!1135 把这两个前提同时打断了:

  • 流水变多:V 之外多了 M、MTE1、MTE2、MTE3。 同一块 UB 现在既可能是 V 的算术目的地,也可能是 MTE2 的 DMA 目的、MTE3 的 DMA 源、 甚至 M 管 L0C 的读回落地点(:2258)。 PipeBarrier<PIPE_V> 对 MTE3 完全无效,必须换成 SetFlag/WaitFlag<V_MTE3>(:2163-2165) 或 PipeBarrier<PIPE_ALL>(:2259)——这正是 10b §9 里 L0C 读回只能用全管栅栏的原因, 也是 !1174 的 L0C read/write conflict 事故(docs/PR-1174-048d0427/01 §1)的根因形态。
  • 借用点从 3 处涨到 21 处,且横跨函数边界:chunkKFp32 在 ComputeOutputCube 里是 Q 板、 在 ComputeStateUpdateCube 里是 K 板再到残差板再到 cNz。 "最后一次读"从"往上看 30 行"变成"要跨 500 行、4 个函数、逆着 ProcessOneVTile 的调用序去推", 人眼 review 不再可靠——表 (c) 的 #7 就是这次失效的实证, 它在 !1135 初版(a9c66ddb:768/:776)就已存在,直到 !1158 才被独立的精度事故暴露。
  • 增宽写成为可能:Vector 时代的借用类型不变(FP32 板借给 FP32 scratch), 覆写窗口 = 读取窗口。Cube 时代要 ReinterpretCast 成 FP16 当 staging、 再 Cast 回 FP32(:819 → :827),同一次借用把目标撑到源的 2 倍宽, 于是"源区间是目的区间的真子集"这种在 Vector 时代不可能出现的拓扑出现了。 这是 !1158 的机制级原因,而不是"某行写错了"。
  • 不变式没有被写下来:容量守卫分散在 :763/:946/:1478/:2372, 每处只检查自己那一块的地址范围,没有任何一处检查"这块板现在属于谁"。 全文件唯一写成注释的借用声明是 :1466-1467 与 :2251-2252 两处, 其余 19 处借用点没有一行注释。

因此本篇的三条可执行结论: ① 任何新增借用必须像 :1466-1467 那样写明出借方已死 + 最后一次读的行号; ② 跨管复用(V 写 → MTE3 读 → V 再写)必须有 SetFlag/WaitFlag 或 PIPE_ALL, 同管复用只能靠 PIPE_V——判据是"借用者与被借用者在不在同一条管上"; ③ 禁止在同一块 UB 上做类型增宽的原地写(表 #7 的唯一根治法), 需要临时增宽就换一块板,代价只是多一次 Cast。 本篇从声明区把它们重新推导了一遍。