第 23 环 · 多头块入残差流(W_o 输出接回残差 + LN)
还是同一个圆心:一台开机只有一个 AI 的手机,它得聊得住。上一环给 8 lane 接了一张 W_o,把各头的结论融成一条——可到这儿,多头吐出的这 4 lane 还只是一格结果:它没回到残差流里,也没经过 LayerNorm。Transformer 块的真形态是 x = LN(x + Attn(x)):子层输出要加回子层输入(残差连接),再归一化(LayerNorm),才能一层层稳地摞起来。
上一环末尾也是核自己动手:把 4 lane 一条条读回来、拼好、写进残差加法寄存器,再调一次 LN——多头块还是「块外的东西」。这一环就把这最后一段接上:让多头块自己在硬件里走回残差流,一次调用完成「W_o → 残差连接 → LayerNorm」。
继续「端上紧凑、全定点、每步硬件自洽、只扩不推翻」的主线,不新造轮子:
· 复用已经验过的两块硬件——W_o 融合直接用第 22 环的 out_proj,归一化直接用第 12 环的 resid_ln;本环只新增「把它们串起来的那根线」和一个收口引擎;
· 8 lane 由硬件直连——多头引擎(第 21 环 mha)的输出直接接进新引擎,核不搬运:核只写「子层输入残差流」和「开始」两个寄存器;
· 残差连接在硬件里做——s[d] = sat8(x[d] + wo_y8[d]):子层输出加回子层输入,8bit 饱和,不溢出;
· 归一化接着做——把残差和喂给 LN 引擎,减均值、除标准差(rsqrt 查表)、乘 gamma、加 beta,吐出可以直接写回残差流的 4 lane;
· 三拍时序,接口干净——拍 0 投影、拍 1 残差、拍 2 归一化;结果一直保持到下次开始;
· 只扩不推翻——新寄存器挂在第 22 环的输出投影页里没用到的 slot(0x560~0x5A4),前 22 圈的地址映射一个字不动。
新增 rtl/mh_block.sv:多头块收口引擎,内部实例化第 22 环 out_proj 与第 12 环 resid_ln。
// ① 多头 8 lane ── W_o ──▶ 4 lane(复用第 22 环引擎)
out_proj #(...) wop (.start(op_start_w), .x_in(x_in), .w_o(w_o), .y8_out(wo_y8_w), ...);
// ② 残差连接:s[d] = sat8( x[d] + wo_y8[d] )(子层输出加回子层输入)
assign s_c[g] = $signed(resid_in[g*OUT_W +: OUT_W])
+ $signed(wo_y8_w[g*OUT_W +: OUT_W]);
assign s_sat[g*OUT_W +: OUT_W] = sat8(s_c[g]);
// ③ LayerNorm(复用第 12 环引擎:a = 残差和,b = 0)
resid_ln #(...) rsn (.start(rsn_start_w), .a_in(s_sat), .b_in('0), .y_out(ln_out), ...);
// 状态机三拍:投影 → 残差 → 归一化
2'd0: if (start) begin st <= 2'd1; busy_r <= 1'b1; end
2'd1: begin s_reg <= s_sat; st <= 2'd2; end // wo_y8 此刻有效
default: begin busy_r <= 1'b0; done_r <= 1'b1; st <= 2'd0; end
映射:MB_X(子层输入残差流)、MB_GO;读回 MB_Y0..Y3(W_o 输出)、MB_S0..S3(残差和)、MB_L0..L3(LN 输出)、MB_MEAN/MB_VAR/MB_RSTD、MB_BUSY。多头 8 lane 直连 mha,W_o 复用 WO_W0..W3、位移复用 WO_SHIFT、LN 仿射参数复用 RSN_GAMMA/RSN_BETA。
这一环的关键是「证明多头块真的自己走回了残差流」,所以用两条硬证据钉死:
· 证据一:核没搬运 8 lane——OS 先把「多头 8 lane 输入」那几个寄存器全部写成垃圾(0x7FFFFFFF),再喊开始。结果照样对([6,1,6,2]):说明这块读的真是多头引擎的直连输出,不是核一条条搬进去的。要是它偷偷读垃圾,屏幕立刻自曝;
· 证据二:残差真的加进去了——同一个 8 lane、同一张 W_o,把子层输入残差流从 [-5,1,7,-3] 换成 [0,0,0,0],LN 输出就从 [-2,-1,7,-3] 变成 [5,-4,5,-2]:输入一动输出就变,说明「加回子层输入」这一步真在硬件里发生;
· 残差和逐 lane 对表——[1,2,13,-1](-5+6 / 1+1 / 7+6 / -3+2),与软件参考模型逐位一致;
· 屏幕 SCREEN = 0xFD0701FB = 4245094907(最后一步残差流,与第 19/20/21/22 环同值:主通路一个字没动),SCREEN2 = 0xFD07FFFE(多头块 LN 输出的四条 lane);
· 单元仿真(不接核、直接拿真 kv_ring+mha 跑出的真 8 lane)覆盖「W_o 跨头融合」「残差连接真生效」「饱和 [-128,127] 夹紧」「全零 W_o → 残差和 = x」「重量化位移在块内同样生效」「同配置复现逐位一致」,全部与参考模型对表。
本环最花时间的不是写新引擎,而是被 OS 咬出来的核的两个隐患——这正是「OS 和芯片一起设计」的意思:软件跑到足够深,才会指出硬件的毛病。
· 核的 LUI 真的把立即数当寄存器了:lui rd, imm 的指令位 [19:15] 是立即数的低 5 位,不是 rs1 字段;旧核按 rs1 去读寄存器文件,于是 lui 的结果 = imm<<12 + regfile[imm[4:0]]。本环 OS 用到常量 0xFD0701FB(低 5 位=16 → 选中 a4)和 0xDEADBEEF(低 5 位=28 → 选中 t3),两个非零寄存器被静默加了进去,常量当场失真。修法:LUI / AUIPC 的 A 操作数强制为 0(核升到 v6),并把这条写进核的单元测试(专门造一个「立即数低 5 位选中非零寄存器」的用例);
· ALU → 分支要吃一条 nop:核只对「load → 分支」做了冻结(v5),紧接着 li 后就用它分支会拿到旧值。OS 里的自检比较器统一改成 li → nop → bne,分支才拿到刚写进的值。
本环走完一整圈:目标 → 一台只有一个对话窗口的手机,它得聊得住,而多头块得能一层层摞起来 → 硬件把「多头 → W_o → 残差 → 归一化」串成一个块,8 lane 直连、核不搬运,三拍闭环 → OS 用「WO_X 写垃圾不影响结果 + 换子层输入 LN 就变」两条自检把「块真的自己走回残差流」钉死,顺手修掉核的两颗钉子 → 单元仿真 + 协同仿真 + 回归 33 项全绿 → 回到目标。多头注意力到此才真正成为残差流里的一个块:不是摆在外面的一格输出,而是层里能摞起来的一块。下一环把生成主路改成走这个多头块——解码循环里,每一层都用多头看上下文。
第 24 环:多头块进解码循环 · 生成主路走多头块——块已经完整了,下一环让连续生成真的跑在它上面:解码循环里每一层都用「多头 → W_o → 残差 → LN」,单头那套退居备用,芯片的注意力主路从「一个角度看上下文」正式升级成「几个头一起看」。