11 · 成员变量与缓冲生命周期总表(2410-2461)
0. 结论先行
锚点:chunk_gated_delta_rule.h:2410-2461
- 声明区相对 !1108 只增加了两行:
:2421 GM_ADDR workspaceAddr_;与:2437 LocalTensor<float> betaFp32;。其余 50 行逐字节相同(diff可证,见 §6)。 "只有两行变化"与"整个 kernel 的内存模型已经换了"同时成立, 因为改变的是这些句柄的用法(ReinterpretCast借用点从 3 处涨到 21 处),不是声明。 - 语义变化最大的不是新字段,而是
: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当模板常量用的前提。 :2423 stateOutQueue_的名字已经骗人:它是TQue<QuePosition::VECOUT, 1>的单槽队列, 在 !1135 里被当成全 kernel 通用的 FP16 中转板用了 8 个不同用途 (state 输出、StageHalfhi、StageHalfWithResidualhi/lo、A 转置源、final_stateCast 目标……),BUFFER_NUM = 1使每一次AllocTensor都必须配一次FreeTensor—— 漏掉一次 FreeTensor 就是死锁,多做一次就是踩数据。: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 空闲。- chunkVFp32 是全场最忙的板:声明注释只写了
// cs * avFp32 (v_beta, then v_new)两个身份, 实际快照里它至少承担 11 种身份(§5.2 逐条给锚点), 包括被ReinterpretCast成 FP16 staging 4 次、被切成 5 块 scratch 2 次、当 L0C 落地板 2 次。 - 风险清单 (c) 里唯一真实爆过的是
:819+:827:betaLocal与 Cast 目的端是同一块 chunkVFp32,FP16→FP32 是原地增宽, 目标区间覆盖源区间且向后扩张,V 管按块前推时先写的 FP32 会毁掉尚未读的 FP16。 !1158(88d8b965:820)的修法正是把betaLocal换成借chunkAttnOutFp32; 本仓库 048d0427(!1174)的:832/:840也是同一修法。 - 为什么纯 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
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_;:2410private::从这里到文件尾只有数据成员,没有函数(函数全在:246-2408)。:2411-:2413queryGm_/keyGm_/valueGm_:三个GlobalTensor<inType>(FP16), 在SetGlobalTensors:117-119绑定;被LoadPaddedRows(:272-340)与 fallback 的标量读消费。:2414gGm_:唯一的GlobalTensor<float>输入(g 是 log-decay,FP32),:123-125仅在hasGamma_ != 0时绑定——未绑定时任何对它的访问都是空指针, 所以:840的canUseGDma把hasGamma_ != 0放在第一位。:2415-:2416betaGm_(FP16)、initStateGm_(FP16,[B,Nv,Dv,Dk])。:2417actualSeqLengthsGm_(int32_t):变长 mask,ComputeAvgload:207-210与Process:217+用它算负载均衡。:2418-:2419finalStateGm_/attnOutGm_:两个GlobalTensor<outType>输出。:2420stateWorkspaceGm_:GlobalTensor<float>,既是跨 chunk 的 FP32 state 存放区, 又被GetCubeStageBase:249-255当成 Cube staging 的基址——同一段 GM 两种用途, 靠"workspace 在前、stage 在B_*NV_*realK_*stateWorkspaceStrideV_*4之后"分开(:250-253)。:2421GM_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
TPipe *pipe_;
TQue<QuePosition::VECOUT, 1> stateOutQueue_;
TBuf<TPosition::VECCALC> tmpBuff;:2422TPipe *pipe_;:!1135 里它不只用来InitBuffer, 还承担pipe_->FetchEventID(HardEvent::V_MTE3)这类取事件 ID 槽的调用 (:2163/:2168/:2322/:2341/:2389等), 这是裸 Mmad 编程下自己管理跨管事件的必需品。:2423TQue<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 清单里"队列槽位泄漏"一类风险都挂在这一行上。:2424TBuf<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
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空行,:2426chunkKFp32 // cs * alignK (original K):出生是"原始 K", 第二次身份是ComputeKCumdecay:1198/1203的 k_cumdecay 落点, Cube 化之后又被借为 FP16 Q 板(:1946)、FP16 K 板(:2133)、FP32 残差板(:2171-2173)、 L0C 落地板(:1717/:1890/:2249)——注释里一个都没写。:2427kCumdecayFp32 // cs * alignK (k_beta then k_cumdecay): 实际还兼任 attn 的 NZ 落地板(:2041)、A 转置板(:2146)、final_state转置板(:2374)、 state 载入的stateNd(:1481)。:2428decayMaskFp32 // cs * cs:唯一"只被写两次读多次"的板(:917-935生产,:993/:1009/:2000/:2081消费),它和:2253的tmpBuff.GetWithOffset<float>(kStateElements, 0)有重叠关系(§7 风险 #21)。:2429chunkVFp32 // cs * avFp32 (v_beta, then v_new):全场最忙,见 §5.2 与 §7。:2430chunkScoresFp32 // cs * cs (attn matrix):与:2431同基址(:175-176)。:2431stateInFp32 // dk * avStepAligned (state [DK, vStep]): 注释里的dk实际是stateStrideK_(= 桶宽),不是realK_。:2432chunkAttnOutFp32 // cs * avFp32 (output):还兼任 V 的 FP16 staging(:1344)、 beta 广播块(:1374)、K 的 FP32 板(:2135)、StageHalfWithResidual的 scratch(:1954/:2006)。:2433gCumsumFp32 // cs:还是前缀和 ping-pong 的一端(:791-807)。:2434deltaFp32 // temp: max(stateStrideK_, chunkSize_, vStepAligned_): 注释是三者取最大,代码:191-193用两次二值 max 实现同一件事; 身份最多:残差列(:524)、前缀和对端(:792)、衰减系数向量(:2124-2128)、 标量逐元素缓冲(:643/:1002)。:2435dotProductFp32 // dot-product multiply/reduction scratch: 还被ReinterpretCast<uint32_t>当 gather 偏移表(:525/:815)。:2436expGCumFp32 // precomputed exp(gCumsum) [cs]:只被读(:1185/:1950/:2065/:2122/:2298), 唯一一个从生到死身份不变的 FP32 板。:2437betaFp32 // 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
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-:2441B_/T_/NK_:批次、总序列长、QK 头数(:78-80)。:2442alignK_——桶化后的语义改变者。: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_;而必须靠本篇把它的作用域列出来。:2443realK_ = tilingData->dk(:81):真实头维,一切%16、<= 桶宽判断的左值 (:259/:2361/:2372)。:2444-:2446NV_/realV_/vStep_:value 头数、真实 Dv、每片 V 步长(:82-83/:88)。:2447-:2448chunkSize_/numChunks_:块长(当前恒 64)与块数。chunkSize_是kMatmulM/kMatmulK = 64这些"魔数"的真实来源 (ComputeOutputCube:1926-1927把 64 写成了constexpr,与chunkSize_是重复表达, 靠IsCubeFastPath:259的chunkLen == 64保证一致)。:2449hasGamma_:可选 g 的存在位(:87)。:2450restUbSize_ = tilingData->ubRestBytes(:89): host 侧SolveVStep算出的ubSize - outQueueMax(02b 章 §SolveVStep), tmpBuff 的容量就是它,多出来的 14592 B(典型形状)是GetWithOffset借用的安全垫。:2451vStepAligned_:Ceil(vStep_, 8)*8,在InitLocalBuffers:135-136才赋值, 构造里置 0(:100)。全 kernel 用avFp32/vStepAligned_两个名字指它 (:719/:1133等),是本篇最容易读错的一个。:2452stateStrideK_——语义被简化成alignK_的别名(:92stateStrideK_ = alignK_;)。 !1108 里它是独立的Ceil(dk,8)*8(00e39ab5:89)。 现在它的用途是:stateInFp32的行数(:176/:181)、alignedElem(:2348)、outQueueBytes的stateTileBytes(:139)。 注意它不影响 workspace 布局(那是stateWorkspaceStrideV_), 所以桶化不会改变 GM 上 FP32 state 的字节数,只改变 UB 里的行宽。:2453stateWorkspaceStrideV_ = Ceil(dv,8)*8(:93):workspace 的行宽,:2329/:2336的dstGapBlocks依赖它。:2454-:2456load_/usedblk_/avgload_:负载均衡游标(IsCurrentBlock:342-350、ComputeAvgload:205+),与 !1108 完全一致,Cube 化没有动调度。:2457scale_:1/sqrt(dk)类 attr(:84),在:1950与:959被吸收进 Q 的行缩放。:2458blockIdx_:GetBlockIdx()(:107),Cube staging 槽位分配的唯一依据 (:253blockIdx_*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 :2426 | cs * alignK | 8192 | 32768 | 0 | 20480 |
kCumdecayFp32 :2427 | cs * alignK | 8192 | 32768 | 32768 | 20480 |
decayMaskFp32 :2428 | cs * cs | 4096 | 16384 | 65536 | 16384 |
chunkVFp32 :2429 | cs * avFp32 | 8192 | 32768 | 81920 | 20480 |
chunkScoresFp32 :2430 | cs * cs(重叠分支下句柄按 overlapSize 申请) | 逻辑 4096 / 句柄 16384 | (65536,与 stateIn 同址) | 114688 | (25600,句柄 6400) |
stateInFp32 :2431 | dk * avStepAligned | 16384 | 65536 | 114688 | 25600 |
chunkAttnOutFp32 :2432 | cs * avFp32 | 8192 | 32768 | 180224 | 20480 |
gCumsumFp32 :2433 | cs | 64 | 256 | 212992 | 256 |
deltaFp32 :2434 | max(stateStrideK_, cs, vStepAligned_) | 128 | 512 | 213248 | 320 |
dotProductFp32 :2435 | max(stateStrideK_, cs) | 128 | 512 | 213760 | 320 |
expGCumFp32 :2436 | cs | 64 | 256 | 214272 | 256 |
betaFp32 :2437 | cs | 64 | 256 | 214528 | 256 |
| tmpBuff 合计 | — | — | 214784 | 0 … 214784 | 125312 |
stateOutQueue_ :2423 | max(dk*avStep, cs*avStep)*sizeof(outType) | 16384 half | 32768 | 独立 buffer | 12800 |
| 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-926 | 2411-2419 | 逐字节同 | Q/K/V 的读法从标量 GetValue 变成 LoadPaddedRows 带跨距 DMA(:272-340);betaGm_/gGm_ 变成"整 slab 一次 DMA + Gather"(:812-864) |
stateWorkspaceGm_ | 927 | 2420 | 同 | 被劈成两半用:前半仍是 FP32 state(:2332),后半被 GetCubeStageBase:249-255 拿去当 Cube staging |
workspaceAddr_ | — | 2421(新增) | 新字段 | 为字节级 slot 寻址而生;!1108 完全不需要 |
pipe_ | 928 | 2422 | 同 | 多出 FetchEventID 用法(裸 Cube 自管事件) |
stateOutQueue_ | 929 | 2423 | 同(含 BUFFER_NUM=1) | 从"状态输出队列"退化为通用 FP16 单槽中转板(8 类用途,见 §2) |
tmpBuff | 930 | 2424 | 同 | 由 GetWithOffset 派生的借用点从 3 处涨到 21 处 |
chunkKFp32 | 932 | 2426 | 同(含注释) | 注释仍写 "original K";实际新增 5 个非声明身份(§5.2) |
kCumdecayFp32 | 933 | 2427 | 同 | 新增 4 个非声明身份,且是 final_state 转置板 |
decayMaskFp32 | 934 | 2428 | 同 | 不变(仍只做 decay mask);但被 :2253 并入借用区 |
chunkVFp32 | 935 | 2429 | 同 | 变化最剧烈:!1108 里它只做 v_beta/v_new 与 5 块递归 scratch;!1135 加了 4 次 FP16 staging + 2 次 cNz |
chunkScoresFp32 | 936 | 2430 | 同 | 不变(与 state 同址的规则在 !1108 已存在,见 00e39ab5:170-171) |
stateInFp32 | 937 | 2431 | 同 | 装载路径新增转置快路(:1478-1515);衰减从 realK_ 条 Muls 变 1 条 |
chunkAttnOutFp32 | 938 | 2432 | 同 | 新增 6 个非声明身份(含 K 的 FP32 板) |
gCumsumFp32 / deltaFp32 / dotProductFp32 / expGCumFp32 | 939-942 | 2433-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_ | 947 | 2442 | 同 | Ceil(dk,16)*16(00e39ab5:88)→ 桶宽(:91):桶内行宽变成编译期常量 |
stateStrideK_ | 957 | 2452 | 同 | 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-523 | inverse / coefficientBlocks | chunkVFp32 | 上一 V-tile 的 v_new | :2308(上一轮) | ✓ 跨 chunk 首尾相接 | 低 |
| 2 | :524 | column | deltaFp32 | 未初始化 | — | ✓ | 低 |
| 3 | :525 | gatherOffsets(uint32) | dotProductFp32 | 点积 scratch | :410 | ✓ 但类型宽度不同,SetValue 写 32 位 | 低 |
| 4 | :563-567 / :599-603 | 5 块 srcA/srcB/residualA/residualB/product | chunkVFp32 | inverse(#1 的产物) | :552(Muls(buf…, inverse…)) | ✓ 同一函数内顺序;但 kBlock 在 32/64 两层不同,5 块切片尺寸随递归层变化 | 中:切片总量 5*kBlock*kBlock,64 层是 20480 float > 16×16 层的 1280,靠"先做完内层再做外层"串行 |
| 5 | :765 | keyLocal(FP16) | chunkVFp32 | 未定义 | — | ✓ | 低;容量守卫 :760-763(2*chunkSize_*vStepAligned_ ≥ chunkLen*alignK_) |
| 6 | :819 | betaLocal(FP16) | chunkVFp32 | #5 的 K 板 | 在 :338 的 Cast 里已消费 | ✓ | 低 |
| 7 | :827 | Cast(chunkVFp32, betaLocal, …) | chunkVFp32 | 同一个 betaLocal | — | ✗ 源与目的同一地址,且 FP16→FP32 原地增宽 | 高:这就是 !1158 修的 bug;88d8b965:820 与 048d0427:832 把 betaLocal 改借 chunkAttnOutFp32 |
| 8 | :845 | g slab 直写 chunkVFp32 | chunkVFp32 | #7 的 beta FP32 slab | :831 Gather 已完成 | ✓ | 低 |
| 9 | :874 | betaBlocks | chunkVFp32 | #8 的 g slab | :852 Gather 已完成 | ✓ | 低 |
| 10 | :921 | gBlocks | chunkVFp32 | #9 的 betaBlocks | :888/:892 | ✓ | 低 |
| 11 | :956 | queryLocal(FP16) | chunkVFp32 | #10 的 gBlocks | :926 | ✓ 中间有 :928 PipeBarrier | 低 |
| 12 | :1046 | keyTransposed(FP16) | chunkVFp32 | #11 的 Q 板 | :1068 之前无人读 Q 板?Q 板其实在 :1068 被当作 scratch 再次覆写 | ✓ 但连续两次借用 | 中:#11 与 #12 相隔 90 行、身份完全不同,靠 IsCubeFastPath 保证 :956 的缓存一定没被消费 |
| 13 | :1068/:1113/:1239/:1954/:2006 | scratch 形参 | chunkVFp32 或 chunkAttnOutFp32 | 各自 | 各自 | ✓ StageHalfWithResidual:1566-1568 的 MTE3_V 等待保证 MTE3 读空后才写 scratch | 低(但正确性完全依赖那 3 行事件) |
| 14 | :1166 / :1281 | cNz | chunkVFp32 | #11/#12 | :1066 | ✓ | 中:cNz 需要 64*64/64*128 float,128 桶下正好等于 chunkVFp32 全容量 32768 B,贴边无余量 |
| 15 | :1344 / :1374 | valueLocal(FP16) / betaBlocks | chunkAttnOutFp32 | 注释声称"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 / :1482 | stateNd / stateTransposed(FP16) | kCumdecayFp32 / chunkVFp32 | attn 板 / #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 / :2249 | cNz | chunkKFp32 | 残差 / Q 板 | :1680 / :1842 / :2173 | ✓ 都在 staging A 之后 | 中:三处都用"整块 chunkK",贴边 |
| 18 | :1806 / :2041 / :2146 / :2374 | cNz / transposedLocal / stateTransposed | kCumdecayFp32 | k_cumdecay 或 attn | :1682 / :2000 / :2190 | ✓ | 低-中(同一块板在 4 个函数里 4 种身份) |
| 19 | :1946 | queryLocal(FP16) | chunkKFp32 | k_cumdecay(已死) | :1682(融合路径)/ :1842(分立路径) | ✓ | 低;只用到前一半字节(16384 B / 32768 B) |
| 20 | :2133 / :2135 | keyLocal(FP16) / dst FP32 | chunkKFp32 / chunkAttnOutFp32 | #19 的 Q 板 / 已写出的 attnOut | — / :657 之后 | ✓ 由 ProcessOneVTile:1326 → :1327 的顺序保证 | 中:跨函数依赖调用顺序,函数内部看不出来 |
| 21 | :2253 | cNz = 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 | 队列 stageLocal | stateOutQueue_ | 上一个用途的残留 | — | ✓ FreeTensor 在 :2191 | 中::2145 与 :2191 之间跨度 46 行、内含 3 次 DMA 与 2 轮转置,任何提前 return 都会漏 FreeTensor(BUFFER_NUM=1 → 下一 chunk 死锁) |
| 23 | :2351/:2354-2355 | 队列 stateLocal | stateOutQueue_ | — | — | ✓,但 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。 本篇从声明区把它们重新推导了一遍。