第 25 环 · 整层一块(注意力块与 FFN 块串成一个引擎)
还是同一个圆心:一台开机只有一个 AI 的手机,它得一层一层地看上下文。上一环生成主路的注意力子层已经是多头块了(多头 → W_o → 残差 → LN),可一整层还没「长成一块」:注意力块算完,要由核一趟趟把结果读出来、写进 FFN 引擎、再读出来、再写进残差 LN 引擎……一层里核要串 4 次引擎调用(块 LN → FFN → 残差 LN → 残差流回写),层内的中间量全靠核当搬运线。
这一环把整层收进一个引擎:一次 LB_GO,片内自己把「注意力块 → FFN 块 → 残差连接 + LayerNorm」串成一条通路跑完。
继续「端上紧凑、全定点、每步硬件自洽、只扩不推翻」的主线:
· 新增 rtl/layer_block.sv——一层 = ① 注意力块(复用第 23 环 mh_block:多头 8 lane → W_o → 残差 → LN₁)② FFN 块(复用第 11 环 ffn:LN₁ 低 8bit/lane 直喂 → W1 → ReLU → W2)③ 残差连接 + LayerNorm(复用第 12 环 resid_ln:s = sat8(FFN + LN₁) → LN₂)。中间量片内直连,根本不出片、不经过核;
· 残差流也在片内自流——层跑完那一刻(lb_done),硬件顺手把本层输出写回残差流寄存器堆 XSR 并推进层号。于是「一次调用既跑完整层、又把残差流推一层」,核不必再写 LST_ADV;
· 前 23 圈的引擎一个不删——mh_block / ffn / resid_ln 都还在、单元仿真照跑,老分步链与整层一块逐位一致;
· 只扩不推翻——整层引擎的寄存器开在输出投影页尾部的未占 slot(0x5A8~0x5FC),前 24 圈地址映射一个字不动。
新增 rtl/layer_block.sv,rtl/min_bus.sv 挂载并把整层引擎接到 bit10 页内的 0x5A8~0x5FC,重写 circle25/gen_boot.py 的 layer_body:
// layer_block:一条通路串到底(注意力块 → FFN → 残差 LN) wire mb_start_w = (st == S_IDLE) && start; // ① 注意力块起跑 wire ffn_start_w = (st == S_ATTN) && mb_done; // LN₁ 有效那一拍直接喂 FFN wire rsn_start_w = (st == S_FFN) && ffn_done; // FFN 输出有效那一拍进残差 LN // LN₁ 与 FFN 输出都片内直连,核一个字不搬 assign ln_bytes[g*8 +: 8] = mb_ln[g*32 +: 8]; // LN₁ → W1 assign fy_bytes[g*8 +: 8] = ffn_y_w[g*32 +: 8]; // FFN → 残差 a # OS 主路:一层 = 一声 LB_GO + 一次 LB_BUSY sw s4, 0x560(x0) # MB_X = 本层输入残差流(整层的子层输入) sw t1, 0x5B8(x0) # LB_GO:一次调用跑完 注意力块→FFN→残差→LN poll LB_BUSY lw s0, 0x5E0(x0) # LB_Y0 = 整层输出(四个阶段一次拿齐) ... # 硬件残差流写回自检:LB 跑完那一刻 XSR 必须就是本层输出 lw ... 0x0E4(x0) # XSR lane0 bne s0, s11, L_bad # XSR != 整层输出 → 自曝 0xDEADBEEF
整层一块页:0x5A8/0x5AC/0x5B0/0x5B4 FFN 两层权重、0x5B8 LB_GO、0x5BC LB_BUSY、0x5C0~0x5CC LB_A0..A3(注意力块 LN₁)、0x5D0~0x5DC LB_F0..F3(FFN 输出)、0x5E0~0x5EC LB_Y0..Y3(整层输出)、0x5F0~0x5FC LB_S0..S3(注意力块残差和)。
这一环要证明「整层真的长成一块了」,现场四条硬证据:
· 证据一:一次调用四个阶段全对表——同一份 8 lane、同一份子层输入残差流,一次 LB_GO 拿到 注意力块残差和 [1,2,13,-1]、LN₁ [-2,-1,7,-3]、FFN 输出 [7,7,7,7]、整层输出 [-3,-2,7,-4],逐 lane 断言;
· 证据二:与老分步链逐位一致——OS 现场再跑一遍第 24 圈核手工串的那条路(MB_GO → FFN_START → RSN_START 三次单独调用),LN₁ / FFN 输出 / 整层输出全部逐位相同。结构换了、计算没变(递增不推翻);
· 证据三:残差流写回是硬件干的——每层 LB_GO 之后,残差流 XSR 必须等于整层输出(核一个字没搬);对不上就自曝 0xDEADBEEF;
· 证据四:FFN 真在整层通路上——只换 FFN 第 2 层权重(单位阵),FFN 输出从 [7,7,7,7] 变成 [0,0,7,0]、整层输出跟着变,而 LN₁ 一个字不动(前向通路没有被回改);
· 屏幕值:SCREEN = 0xF905FD04(与第 24 环同值 —— 同一份计算,只是从「核串 4 次调用」换成「整层一个引擎」);SCREEN2 = 0x01FE07FD = 一次调用里 s / LN₁ / FFN / 整层输出 各取 lane0 一个字节的 digest;
· 单元仿真新增 tb_layer_block:独立跑一遍老分步链(mh_block → ffn → resid_ln)与整层一块对表,另有「残差置零 → LN₁ 跟着变」「shift=2 重量化在块内生效」「换 FFN 权重输出变、LN₁ 不动」「同配置复现」五组用例。
这一环最容易被忽略、也最容易做错的地方是「一致性」不是「新旧都跑一遍就算数」:新引擎(整层一块)必须在同一份输入下与老路径逐位一致,否则「只扩不推翻」只是一句口号。所以单元仿真和 OS 收尾都专门现场跑老分步链来对表——引擎留着不只是为了兼容,更是为了当参照物。
本环走完一整圈:目标 → 一台只有一个对话窗口的手机,它得一层层看上下文,而每一层都应该是一个「块」 → 硬件把注意力块、FFN 块、残差归一化串成一个引擎,层内中间量片内直连、残差流片内自流 → OS 主路一层只发一声、只等一次,并用「四阶段对表 + 与老分步链逐位一致 + XSR 自检 + 换 FFN 权重输出变」四条自检钉死 → 单元仿真 + 协同仿真 + 回归 35 项全绿 → 回到目标。芯片的一层 Transformer,从「核手工串的一串调用」正式长成「一块」。下一环让层控制器驱动这块引擎一口气跑完 N 层——整网一块。
第 26 环:整网一块 · 层控制器串起整层引擎跑完 N 层——层内已经是一块了,下一环把「层与层之间」也收进硬件:层控制器驱动整层引擎,残差流片内自流,核一次启动就让整网自己跑完 N 层。