Skip to content

01 · 数学原理:ChunkGatedDeltaRule 在算什么 ​

在看任何 AscendC 代码之前,必须先弄清楚算子的数学语义。否则 kernel 里每个循环为什么是三角形的、为什么有一个"递归修正"步骤、为什么 state 要转置,全都看不懂。

1. 算子的来源 ​

ChunkGatedDeltaRule 是 Gated DeltaNet(门控 Delta 网络)类线性注意力的 chunkwise(分块)训练算子。它对应的推理/训练公式来自 DeltaNet 一脉的线性注意力模型(Gated DeltaNet,也是 Qwen3-Next 等模型使用的线性注意力层)。

与标准 softmax attention 不同,DeltaNet 维护一个循环状态矩阵 S,逐 token 递推更新:

St=αtSt−1+βtkt(vt−kt⊤St−1)⊤

拆开看这个递推的每一项:

记号含义对应算子输入
S∈Rdk×dv循环状态("记忆"矩阵)initialState (FP16),最终写回 finalState
kt∈Rdkkey 向量key,布局 [T, Nk, dk]
vt∈Rdvvalue 向量value,布局 [T, Nv, dv]
βt∈(0,1)写入强度(决定这步"写多少"进记忆)beta,布局 [T, Nv]
αt=egt遗忘门(g 是 log 域衰减,负数,所以 eg≤1)g(可选输入,FP32),布局 [T, Nv]
qtquery 向量(读记忆)query,布局 [T, Nk, dk],带缩放 scale
actualSeqLengths每个 batch 的真实序列长度变长序列支持

每一步 t:

  1. 用 query 读旧记忆:qt⊤St−1(这就是"注意力"输出的一部分);
  2. 计算"应该写入的增量":vt−kt⊤St−1(当前记忆能解释多少 vt,补差值——这就是 Delta Rule 名字的由来);
  3. 先遗忘(乘 egt),再按强度 βt 把增量写进记忆。

输出:

  • out[t] = 该步的注意力输出(由 qt 读历史记忆 + 块内互注意力组成,见下文);
  • finalState = 序列结束时的状态 ST。

注意 βt 和 gt 是 per-value-head 的(布局 [T, Nv]),而 k,q 是 per-k-head([T, Nk, dk])。通常 Nv 是 Nk 的整数倍,每 Nv/Nk 个 V-head 共享一个 K/Q-head —— kernel 里 nvPerNk = NV_/NK_、qkHead = head_i / nvPerNk 就是干这个的(见 04 篇)。

2. 为什么分块(chunkwise)? ​

朴素递推是 O(T) 串行的,GPU/NPU 上没法并行。chunkwise 算法把长度 T 的序列切成 ⌈T/64⌉ 个块(chunkSize=64,与 910B 版本对齐,是硬编码常量),然后:

  • 块内:64 个时间步之间的"互注意力"用一个小矩阵乘一次性并行算出(递推在块内被展开成矩阵形式);
  • 块间:块与块之间只传一个状态矩阵 S,仍然串行,但串行次数从 T 次降到 T/64 次。

这是 flash-linear-attention 系列的标准做法,块内矩阵大小 64×64,正好可以放进片上存储。

3. 块内数学:把递推展开成矩阵 ​

对一个块内的 64 步递推做代数展开(这是整个 kernel 的数学骨架)。记块内第 i 步(i=0..L−1,L≤64 是 chunkLen),定义:

  • γi=egi(遗忘因子),gi 的块内前缀和 Gi=∑j≤igj(代码里的 gCumsumFp32),跨步衰减 egi−gj=eGi−Gj(这就是 decayMaskFp32);
  • ki(β)=βiki(kCumdecayFp32 的初值,Phase 2 ComputeKBeta);
  • vi(β)=βivi(chunkVFp32 的初值,Step 1 LoadVBeta)。

3.1 块内互注意力矩阵 A(含 delta rule 的递归修正) ​

块内 j<i 时,第 i 步看到的"第 j 步写入的残余"不是简单的前缀和,因为每步写入的量本身被后面步骤的读取-擦除修正过。展开后 Aij 满足一个下三角递推:

Aij=−eGi−Gj⋅βiki⊤kj+∑j<m<iAim⋅(eGm−Gjβjkm⊤kj)

即:直接衰减项 + 间接路径修正。代码里分两步实现:

  1. 先算"直接项"矩阵(Phase 4 ComputeAttnMatrix 前半段): A[i][j] = -(k_beta[i] · k[j]) * decayMask[i][j],其中 k_beta[i]·k[j] 用向量点积 DotFp32 算出,乘上 decayMask(即 eGi−Gj)取负。
  2. 再做递归修正(ComputeRecursiveAttn):按行迭代,row_i += A[i][k] * row_k(k 从小到大),把间接路径补上。这是纯下三角递推,串行但每步是一条向量指令 Axpy。

最后对角线置 1(A[i][i] = 1):写入自己这一步的增量是完整保留的。

3.2 块内输出与状态更新(kernel 的 7 个公式) ​

有了 A 和 γ,块的输入输出关系可以全部写成矩阵形式:

(1) k_cumdecay(Phase 5 ComputeKCumdecay,结果覆盖进 chunkKFp32):

Kcd=A⋅diag(γ)Kβ

含义:每步的 key 按块内衰减路径"衰减到块尾"之后的等效 key。它用于把旧状态的贡献从写入里扣除。

(2) 块内新值(Step 2 ComputeValueNew → chunkAttnOutFp32):

value_new=A⋅Vβ

即块内经典的因果注意力输出(对角线为 1,所以包含"自己")。

(3) 减去旧状态贡献(Step 4 ComputeVNew → 覆盖回 chunkVFp32):

vnew=value_new−Kcd⋅Sin

含义:递推里每步写入的是 v−k⊤S,把"旧状态已经解释的部分"从 v 里减掉,剩下的才是真正要写进记忆的新增信息。

(4) 跨块注意力(Step 5 ComputeAttnInter → 写入 chunkAttnOutFp32):

attn_inter=(diag(eGi)qi⋅scale)⋅Sin

含义:第 i 步用 query 读块开始时刻的记忆,读到的量已经按 eGi 衰减。

(5) 最终输出(Step 6 AccumOutput,累加进 chunkAttnOutFp32):

outi=attn_interi+∑j≤ieGi−Gj(qi⊤q??kj)⋅scale⋅vnew,j

准确写法:out = attn_inter + tril(QK^T ⊙ decay) · v_new。其中 tril(QK^T ⊙ decay) 就是 Phase 4 后半段重写进 decayMaskFp32 的矩阵(原来存 eGi−Gj 的缓冲被原地覆盖成 (qi⋅kj)eGi−Gj⋅scale)。

(6) 状态更新(Step 7 UpdateAndWriteState):

Sout=eGL−1Sin+∑ieGL−1−Giki⊗vnew,i

含义:旧状态衰减一个整块(eGL−1 = 块内所有 γ 的乘积),再把每步的 vnew(已经扣除了旧状态贡献)按"衰减到块尾"的权重外积累加进去。这就是下一块的 Sin。

3.3 七个公式 ↔ kernel 函数对照表 ​

数学kernel 函数位置产出缓冲
Gi 前缀和ComputeGCumsumchunk_gated_delta_rule.h:486gCumsumFp32
eGi、eGi−GjPrepareDecayAndExpchunk_gated_delta_rule.h:514expGCumFp32、decayMaskFp32
A 直接项 + 递归修正 + 对角线ComputeAttnMatrix + ComputeRecursiveAttnchunk_gated_delta_rule.h:576/317chunkScoresFp32
Kcd=Adiag(γ)KβComputeKCumdecaychunk_gated_delta_rule.h:617覆盖 chunkKFp32
Vnew=AVβComputeValueNewchunk_gated_delta_rule.h:732chunkAttnOutFp32
vnew=Vnew−KcdSComputeVNewchunk_gated_delta_rule.h:789覆盖 chunkVFp32
attn_inter=eGQSComputeAttnInterchunk_gated_delta_rule.h:811覆盖 chunkAttnOutFp32
out=attn_inter+tril(QK⋅decay)vnewAccumOutputchunk_gated_delta_rule.h:832累加 chunkAttnOutFp32
Sout=eGLS+∑eGL−Giki⊗viUpdateAndWriteStatechunk_gated_delta_rule.h:849stateInFp32 → GM

建议:把这张表打印出来放在手边。读 05/06 两篇时,每进一个函数先回来看它对应哪个公式、输入输出是什么,就不会迷路。