10c · UpdateAndWriteState:一次算、两条出口写(2290-2410)
本篇是 1918-2410 这段的第三块。拆分过程:1918-2410 单文件必然超 700 行, 先在
ComputeStateUpdateCubeDispatch(:2094)处切出 10 章(1918-2093)/10b 章(2094-2289), 10b 章含正文仍达 587 行,再接着在UpdateAndWriteState(:2292)这个函数边界切出本篇(2290-2410)。 三篇合起来对 1918-2410 逐行全覆盖、无缺口。 正文所有代码块由sed -n 'A,Bp' src/d0906c3e/op_kernel/chunk_gated_delta_rule.h逐字切出, 段内省略以单独一行...标示。文中10b §n指10b-StateUpdateCube与finalState写回.md的第 n 节。
0. 结论先行
锚点:chunk_gated_delta_rule.h:2290-2410(本篇)
UpdateAndWriteState自己不做乘法,它只做三件事: 按IsCubeFastPath分派(:2295-2296)或走 Vector fallback(:2297-2316); 非末块把 FP32 结果写进 workspace 并return(:2320-2344); 末块 Cast 成 FP16、转置、写final_state(:2347-2407)。 10b 章全部篇幅(:2100-2289)都是它的被调方。- 三条出口互斥且都必须存在:Cube 计算 → FP32 workspace(中间块)、 Cube 计算 → FP16
final_state转置快写(末块、16 对齐)、 标量逐元素SetValue(末块、守卫失败)。:2398-2406的兜底不是死代码,dk非 16 倍数、curV ≠ vStepAligned_且桶 0 时唯一可行。 - "跨 chunk 保持 FP32"是 !1135 与前作共同的既有约束,注释
:2317-2319把它写成了三条理由(误差累积 / 结果依赖 chunk_size),本篇 §2 逐行核对DataCopyParams的双向 gap 如何做到"一条指令搬完整块"。 transposeCapacity的系数 2 是 fp32→fp16 的元素翻倍,128 桶代入后是"恰好相等" (128*128 = 2*64*128 = 16384),不是留一倍余量;推导见 §3。- 快慢两条写回路径写出的字节序列必须逐位相同(
:2392与:2402同一偏移式、:2393的裁剪长度与:2401-2404的循环范围一致), 这是"守卫只挑性能、不改语义"的可验证锚点,也是 03 章 Bug 清单里的回归点。 - 本篇还留了一个结构性事实:
stateOutQueue_的EnQue立刻DeQue(:2354-2355) 是仓库里最便宜的跨管栅栏,StageHalf:1585-1586用的是同一招; 它同时完成"UB 槽位换主",所以BUFFER_NUM=1的单槽队列没有被浪费。
1. UpdateAndWriteState 的分派与 Vector fallback
锚点:chunk_gated_delta_rule.h:2290-2316
// Step 7: decay state, add (K * exp(gLast - g))^T @ v_new, then write the state tile back to GM.
__aicore__ inline void UpdateAndWriteState(int32_t t_start, uint32_t chunkLen, uint32_t v_i, uint32_t curV,
uint64_t qkHead, uint64_t stateBaseOffset,
uint64_t workspaceStateBaseOffset, uint32_t avFp32, bool isLastChunk) {
if (likely(IsCubeFastPath(chunkLen, avFp32))) {
ComputeStateUpdateCubeDispatch(t_start, qkHead);
} else {
float gLastExp = expGCumFp32.GetValue(chunkLen - 1);
for (uint32_t d = 0; d < realK_; d++) {
LocalTensor<float> stRow = stateInFp32[d * vStepAligned_];
Muls(stRow, stRow, gLastExp, vStepAligned_);
PipeBarrier<PIPE_V>();
}
for (uint32_t i = 0; i < chunkLen; i++) {
int32_t t = t_start + i;
uint64_t qkOff = (static_cast<uint64_t>(t) * NK_ + qkHead) * realK_;
float diffExp = ScalarExp(gCumsumFp32.GetValue(chunkLen - 1) - gCumsumFp32.GetValue(i));
LocalTensor<float> vRow = chunkVFp32[i * avFp32];
for (uint32_t d = 0; d < realK_; d++) {
float coeff = static_cast<float>(keyGm_.GetValue(qkOff + d)) * diffExp;
LocalTensor<float> stRow = stateInFp32[d * vStepAligned_];
Axpy(stRow, vRow, coeff, vStepAligned_);
PipeBarrier<PIPE_V>();
}
}
}:2290空行;:2291Step 7 的单行注释(与 10 章:2089-2093的五行块不重复, 这行是UpdateAndWriteState自己的:decay state, add (...)^T @ v_new, then write the state tile back to GM, 明确"本函数负责写回 GM",Cube 函数只负责算)。:2292-22949 参数签名:t_start, chunkLen, v_i, curV, qkHead, stateBaseOffset, workspaceStateBaseOffset, avFp32, isLastChunk。 与:1327-1328调用点逐一对应;v_i/curV是 V-tile 维度的切分位置,stateBaseOffset指向final_state/initial_state的[B,Nv,Dv,Dk]展平基址,workspaceStateBaseOffset指向内部 FP32 workspace(:690/:704算好传入)。:2295if (likely(IsCubeFastPath(chunkLen, avFp32))):谓词在:257-262, 四个条件realK_ <= kSpecializedDk && chunkLen == 64 && vStepAligned_ == kSpecializedDk && avFp32 == kSpecializedDk。likely是给编译器的分支布局提示,不影响语义。:2296进 Cube;:2297else。:2298gLastExp = expGCumFp32.GetValue(chunkLen - 1):fallback 用运行期长度。:2299-2303逐d衰减 state:realK_条Muls,每条长vStepAligned_。 对照 Cube 路径:2123的一条Muls(kStateElements)。:2304-2315双重循环:外层 64 个 token(:2304),:2307ScalarExp(gCumsumFp32.GetValue(chunkLen-1) - gCumsumFp32.GetValue(i))—— 标量管 8 项泰勒展开(:364-385,项数常数kExpTaylorTerms见:40-41);:2310keyGm_.GetValue(qkOff+d)逐元素读 GM;:2312Axpy(stRow, vRow, coeff, vStepAligned_)原地累加。 成本:chunkLen*realK_ = 64*128 = 8192次标量 GM 读 + 8192 条 Axpy + 64 次标量 Exp。 这正是:2332(Cube 路径的DataCopy)与:2393(一次转置+一条 DataCopy)省掉的东西。:2316结束 else。
2. 非末块:FP32 workspace 写回(双向 gap 的一条 DataCopy)
锚点:chunk_gated_delta_rule.h:2317-2345
// Preserve the state in FP32 between chunks. Casting to final_state (FP16)
// after every chunk accumulates quantization error and makes the result
// depend on chunk_size.
if (!isLastChunk) {
PipeBarrier<PIPE_ALL>();
event_t vectorToMte3 = static_cast<event_t>(pipe_->FetchEventID(HardEvent::V_MTE3));
SetFlag<HardEvent::V_MTE3>(vectorToMte3);
WaitFlag<HardEvent::V_MTE3>(vectorToMte3);
uint32_t alignedV = Ceil(curV, FP32_NUM_PER_BLOCK) * FP32_NUM_PER_BLOCK;
if constexpr (kSpecializedDk != 0) {
uint16_t rowBlocks = static_cast<uint16_t>(alignedV / FP32_NUM_PER_BLOCK);
uint16_t srcGapBlocks = static_cast<uint16_t>((vStepAligned_ - alignedV) / FP32_NUM_PER_BLOCK);
uint16_t dstGapBlocks = static_cast<uint16_t>((stateWorkspaceStrideV_ - alignedV) / FP32_NUM_PER_BLOCK);
DataCopyParams stateParams{static_cast<uint16_t>(realK_), rowBlocks, srcGapBlocks, dstGapBlocks};
uint64_t rowOff = workspaceStateBaseOffset + v_i;
DataCopy(stateWorkspaceGm_[rowOff], stateInFp32, stateParams);
} else {
DataCopyParams stateParams{1, static_cast<uint16_t>(alignedV / FP32_NUM_PER_BLOCK), 0, 0};
for (uint32_t d = 0; d < realK_; d++) {
uint64_t rowOff = workspaceStateBaseOffset + static_cast<uint64_t>(d) * stateWorkspaceStrideV_ + v_i;
DataCopy(stateWorkspaceGm_[rowOff], stateInFp32[d * vStepAligned_], stateParams);
}
}
PipeBarrier<PIPE_ALL>();
event_t mte3ToMte2 = static_cast<event_t>(pipe_->FetchEventID(HardEvent::MTE3_MTE2));
SetFlag<HardEvent::MTE3_MTE2>(mte3ToMte2);
WaitFlag<HardEvent::MTE3_MTE2>(mte3ToMte2);
return;
}:2317-2319三行注释是全章的"设计说明":跨 chunk 保持 FP32,只在末块 Cast 成 FP16; 否则量化误差逐块累积、结果依赖 chunk_size。这条正是 recurrent 参考实现踩过的坑。:2320if (!isLastChunk)。:2321PipeBarrier<PIPE_ALL>():V 刚改完 state, MTE3 要读它 —— 注意 !1135 在这里用PIPE_ALL而不是V_MTE3事件(下面:2322-2324又补了一对), 属于双保险。:2322-2324V_MTE3成对事件。:2325alignedV = Ceil(curV, FP32_NUM_PER_BLOCK)*FP32_NUM_PER_BLOCK:目的行宽按 8 个 FP32(32 B)对齐。:2326if constexpr (kSpecializedDk != 0)→ 桶化快路径::2327rowBlocks = alignedV/8:2328srcGapBlocks = (vStepAligned_ - alignedV)/8:源(UB)行内 padding 的块数:2329dstGapBlocks = (stateWorkspaceStrideV_ - alignedV)/8:目的(GM)行 stride 与有效长度之差:2330DataCopyParams stateParams{realK_, rowBlocks, srcGapBlocks, dstGapBlocks}: 字段顺序在本仓库固定为{repeatTimes, blockLen, srcStride, dstStride}(块单位,32 B), 证明见CopyToGm:49-50与LoadPaddedRows:289-291。repeatTimes = realK_→ 一条指令搬完 dk 行。:2331rowOff = workspaceStateBaseOffset + v_i;:2332DataCopy(stateWorkspaceGm_[rowOff], stateInFp32, stateParams)。 典型形状(dv=128、vStep=128、dk=128):rowBlocks = 16、srcGap = dstGap = 0, 一次搬128*16*32 = 65536 B=stateInFp32全部 16384 个 FP32 ✓ 与kStateElements一致。:2333-2338桶 0(纯 Vector):逐 d 一次 DataCopy,realK_条指令,stateParams{1, alignedV/8, 0, 0};行偏移d*stateWorkspaceStrideV_ + v_i。 快慢两条路径写的是同一块 GM 布局,这保证 fallback 与 Cube 的结果可逐位比对。
:2340PipeBarrier<PIPE_ALL>()+:2341-2343MTE3_MTE2成对: 出站 DMA 完成之后才允许下一块的 MTE2 入站读同一块 workspace, 否则下一 chunk 的LoadStateTile:1447会读到半新半旧的状态。:2344return;:2345空行。
3. 末块:FP32→FP16 与转置容量算术(含 128 桶算例)
锚点:chunk_gated_delta_rule.h:2346-2373
// Only the final chunk is cast to the public FP16 final_state output.
uint32_t alignedElem = stateStrideK_ * vStepAligned_;
Muls(stateInFp32, stateInFp32, 1.0f, alignedElem);
PipeBarrier<PIPE_V>();
LocalTensor<outType> stateLocal = stateOutQueue_.AllocTensor<outType>();
Cast(stateLocal, stateInFp32, RoundMode::CAST_NONE, alignedElem);
PipeBarrier<PIPE_V>();
stateOutQueue_.EnQue<outType>(stateLocal);
stateLocal = stateOutQueue_.DeQue<outType>();
// The public final_state is [B, Nv, Dv, Dk], while stateLocal is the
// internal [Dk, vStep] tile. For aligned full tiles, transpose in Vector
// and write one contiguous block instead of performing scalar GM stores.
constexpr uint32_t kTransposeBlock = 16;
uint32_t kBlockCount = realK_ / kTransposeBlock;
uint32_t vBlockCount = curV / kTransposeBlock;
uint32_t stateElementCount = realK_ * curV;
bool supportedVTile = curV == vStepAligned_;
if constexpr (kSpecializedDk != 0) {
vBlockCount = vStepAligned_ / kTransposeBlock;
stateElementCount = realK_ * vStepAligned_;
supportedVTile = true;
}
uint32_t outputStateElementCount = realK_ * curV;
uint32_t transposeCapacity = 2 * chunkSize_ * alignK_;
if (likely(realK_ % kTransposeBlock == 0 && vStepAligned_ % kTransposeBlock == 0 && supportedVTile &&
vBlockCount <= UINT8_MAX && stateElementCount <= transposeCapacity)) {:2346空行注释前的一行;:2347注释Only the final chunk is cast to the public FP16 final_state output.:2348alignedElem = stateStrideK_ * vStepAligned_:stateStrideK_ == alignK_(:92), 所以这是"含 padding 的整块 state 元素数"。:2349Muls(stateInFp32, stateInFp32, 1.0f, alignedElem):乘 1 的空操作,目的是把 padding 区 也过一遍 V 管,让:2352的 Cast 覆盖整块、结果确定(同一手法见:795/:805/:905/:1233)。:2351从队列取outType板;:2352Cast(stateLocal, stateInFp32, CAST_NONE, alignedElem);:2353V 栅栏。:2354-2355EnQue后立即DeQue:这是"用队列当跨管栅栏"的写法 (VECOUT队列的入队/出队隐含 MTE3/V 与 UB 槽位的归属交接), 比手写PipeBarrier<PIPE_ALL>便宜,并且同时完成了"槽位换主"。 与StageHalf:1585-1586同一模式。:2357-2359注释:对外final_state是[B, Nv, Dv, Dk],对内 tile 是[Dk, vStep], 所以对齐鲁棒时"转置 + 一次连续写"替代逐元素标量写。:2360kTransposeBlock = 16(fractal 边长);:2361kBlockCount = realK_/16;:2362vBlockCount = curV/16;:2363stateElementCount = realK_*curV;:2364supportedVTile = (curV == vStepAligned_)。:2365-2369桶化快路径放宽:vBlockCount = vStepAligned_/16、stateElementCount = realK_*vStepAligned_、supportedVTile = true。 即桶内允许尾块curV < vStepAligned_:padding 列照样转置,最后按outputStateElementCount(:2370,仍用真实curV)截断写出。这是"桶内固定形状 + 出口裁剪"的通用套路。:2371transposeCapacity = 2 * chunkSize_ * alignK_:为什么是 2?:2374的落地板stateTransposed借的是kCumdecayFp32, 它的声明容量是chunkSize_*alignK_个 FP32(:164), 按outType = half复述就变成2*chunkSize_*alignK_个 half。 系数 2 是元素翻倍的算术结果,不是安全余量。:2372-2373四条件likely守卫:realK_%16==0 && vStepAligned_%16==0 && supportedVTile && vBlockCount <= UINT8_MAX && stateElementCount <= transposeCapacity(vBlockCount <= UINT8_MAX与:1479的kBlockCount <= UINT8_MAX不同, 因为两个方向的repeatTimes取自不同维度,见 §4)。 128 桶 + B=1,H=16,T=128,dk=dv=128 的逐项代入:量 表达式 值 判定 realK_ % 16128 % 16 0 ✓ vStepAligned_ % 16128 % 16 0 ✓ supportedVTile:2368强制true ✓ vBlockCount128/16 8 ≤255 ✓ stateElementCount128·128 16384 half = 32768 B 需 ≤ capacity transposeCapacity2·64·128 16384 half = 32768 B 16384 ≤ 16384,贴边相等 alignedElem(:2348)128·128 16384 float = stateInFp32容量 ✓队列容量( :139-142)max(128·128·2, 64·128·2) 32768 B = alignedElem字节数 ✓80 桶:
stateElementCount = 80·80 = 6400,transposeCapacity = 2·64·80 = 10240✓ 有余量。 结论:128 桶是四桶里唯一"刚好塞满"的,任何一次把 kCumdecay 换小的改动都会立刻打穿这条守卫。
4. 转置与一次连续 GM 写
锚点:chunk_gated_delta_rule.h:2374-2396
LocalTensor<outType> stateTransposed = kCumdecayFp32.template ReinterpretCast<outType>();
TransDataTo5HDParams transposeParams;
transposeParams.repeatTimes = static_cast<uint8_t>(vBlockCount);
transposeParams.srcRepStride = (vBlockCount == 1) ? 0 : 1;
transposeParams.dstRepStride = (vBlockCount == 1) ? 0 : realK_;
for (uint32_t block = 0; block < kBlockCount; block++) {
LocalTensor<outType> srcRows[kTransposeBlock];
LocalTensor<outType> dstRows[kTransposeBlock];
for (uint32_t row = 0; row < kTransposeBlock; row++) {
srcRows[row] = stateLocal[block * kTransposeBlock * vStepAligned_ + row * vStepAligned_];
dstRows[row] = stateTransposed[block * kTransposeBlock + row * realK_];
}
TransDataTo5HD<outType>(dstRows, srcRows, transposeParams);
}
PipeBarrier<PIPE_V>();
event_t vectorToMte3 = static_cast<event_t>(pipe_->FetchEventID(HardEvent::V_MTE3));
SetFlag<HardEvent::V_MTE3>(vectorToMte3);
WaitFlag<HardEvent::V_MTE3>(vectorToMte3);
uint64_t stateOffset = stateBaseOffset + static_cast<uint64_t>(v_i) * realK_;
DataCopy(finalStateGm_[stateOffset], stateTransposed, outputStateElementCount);
stateOutQueue_.FreeTensor(stateLocal);
return;
}:2374stateTransposed = kCumdecayFp32.ReinterpretCast<outType>():第三次借 kCumdecay (10b §4 的转置板、10 章:2041的cNz、此处final_state转置板)。:2375-2378转置参数:repeatTimes = vBlockCount(沿源矩阵的列方向铺开 16 列块,所以 uint8 上界约束的是curV/16, 与LoadStateTile:1499的repeatTimes = kBlockCount(约束realK_/16)方向相反, 这正是两处UINT8_MAX守卫字段不同的原因);srcRepStride = (vBlockCount==1) ? 0 : 1(源每次进 1 块 = 16 个 half = 16 列);dstRepStride = (vBlockCount==1) ? 0 : realK_(目的每次进realK_块 = 16 个目的行)。=1时把 stride 写成 0 是防御性写法:单 repeat 时 stride 不被使用, 填 0 可避免超出uint8_t表示范围(realK_最大 128 ✓,但 8 桶的kMatmulK=64等也一样安全)。:2379-2387外层kBlockCount = realK_/16次,每次 16 个源行指针stateLocal[block*16*vStepAligned_ + row*vStepAligned_]([dk, vStep] 的第 block 组行) → 16 个目的行指针stateTransposed[block*16 + row*realK_]([vStep, dk] 的第 block 组列)。 与 10b §5 的:2157-2158是同一个寻址式,只是元素类型换成 outType、行宽换成vStepAligned_。:2388V 栅栏;:2389-2391V_MTE3成对。:2392stateOffset = stateBaseOffset + v_i*realK_:对外布局[B,Nv,Dv,Dk]里 本 V-tile 的起始(v_i是 Dv 维偏移,行宽realK_)。:2393DataCopy(finalStateGm_[stateOffset], stateTransposed, outputStateElementCount): 一条 MTE3 写realK_*curV个 half,且用outputStateElementCount(真实curV)裁掉 padding 列, 所以:2366-2367放宽出来的转置余量不会污染输出。:2394FreeTensor;:2395return;:2396大if右括号。
5. 标量兜底:只在守卫失败时执行
锚点:chunk_gated_delta_rule.h:2397-2410
TQueSync<PIPE_V, PIPE_S> finalStateSync;
finalStateSync.SetFlag(0);
finalStateSync.WaitFlag(0);
for (uint32_t v = 0; v < curV; v++) {
uint64_t stateRowOffset = stateBaseOffset + static_cast<uint64_t>(v_i + v) * realK_;
for (uint32_t d = 0; d < realK_; d++) {
finalStateGm_.SetValue(stateRowOffset + d, stateLocal.GetValue(d * vStepAligned_ + v));
}
}
stateOutQueue_.FreeTensor(stateLocal);
}
private::2397空行。:2398-2400TQueSync<PIPE_V, PIPE_S> finalStateSync成对: 10b §4 说的 V→S 不变式的又一实例(:2404要用标量GetValue读stateLocal, 而它是:2352V 管 Cast 的产物)。:2401-2406三重标量循环:curV × realK_ = 128×128 = 16384次finalStateGm_.SetValue,每次一个元素、无 DMA。:2402的stateRowOffset = stateBaseOffset + (v_i+v)*realK_与:2392一致,:2404的stateLocal.GetValue(d*vStepAligned_ + v)就是[dk, vStep]的转置读法。 两条路径写出的字节序列必须完全相同,这是"守卫只挑性能、不改语义"的验证锚点。:2407FreeTensor(stateLocal);:2408函数右括号。:2409空行;:2410private:—— 本章到此结束,11 章从成员声明区继续。
6. 本章的账
| 项目 | Cube 快路径(:2296 起) | !1108 纯 Vector(:2298-2315) |
|---|---|---|
| 状态衰减 | 1 条 Muls(:2123,长 kStateElements) | realK_ 条 Muls(:2301) |
| 衰减系数 | 3 条批量(:2124/2126/2128)+ 64 条宽 Muls(:2141) | 64 次 ScalarExp(:2307) |
| K 读取 | 1 条带跨距 DMA(:2135 → :289-292) | 8192 次标量 GetValue(:2310) |
| 主乘积 | 4 条 Mmad(:2230/2245 × 2 tile) | 8192 条 Axpy(:2312) |
| NZ→ND | 8 条融合 Add(:2269,64/80/96 桶)/ 1024 条 Add(:2283,128 桶) | 无(累加即结果) |
| workspace 写回 | 1 条 DataCopy(:2332) | realK_ 条(:2337,桶 0) |
| final_state 写回 | 转置 + 1 条 DataCopy(:2386/2393) | 16384 次 SetValue(:2404) |
Step7 的净收益全部来自"把 8192 次标量 GM 读和 8192 条 Axpy 换成 4 条 Mmad", 而代价是 L0C→UB 的一次全管停顿(:2259)、A 的两轮转置(:2153-2161/:2177-2185)、 以及 10b §9 那条只能服务一个 M-tile 的融合 Add——128 桶付了两次分段 Add 的账。