第 26 环 · 整网一块(层控制器串起整层引擎跑完 N 层)
还是同一个圆心:一台开机只有一个 AI 的手机,它要能把一句话一层层想完整。上一环「一整层」已经长成一块了(LB_GO 一声,片内跑完注意力块 → FFN → 残差 → LN),可整张网还没长成一块:层与层之间的边界仍要交回核——核得逐层做「写本层输入的残差流 → 切本层 K/V 环 → 本层 Q/K/V 投影 → K/V 追加进环 → 两头各自的 Q → 多头 → LB_GO → 读回整层输出 → 当作下一层的输入」,两层就是两趟,N 层就是 N 趟。核又成了层与层之间那根搬运线。
这一环把整张网收进一个引擎:一次 NET_GO,片内自己 for L = 0..N-1 跑完整网。
继续「端上紧凑、全定点、每步硬件自洽、只扩不推翻」的主线:
· 新增 rtl/layer_net.sv——一次 NET_GO,片内自己把 N 层串成一条通路,每层五步:① 本层投影(复用第 14/24 环 qkv_proj:Wq/Wq2/Wk/Wv 按层选,从残差流算 Q/K/V)② K/V 追加进片内环(复用第 19 环 kv_ring:按层分区、满则覆盖最旧)③ 多头注意力(复用第 21 环 mha:两头各自的 Q、同一份环、同一窗口)④ 整层一块(复用第 25 环 layer_block)⑤ 残差流就地更新 x ← 本层输出;
· 层控制器在片内——层号硬件自增(核可读回 NET_IDX)、层权重以层号为地址自己切换、每层 K/V 由硬件 append(核可读回 NET_KCNT);核一轮解码只发一声、只等一次;
· 前 25 圈的引擎一个不删——qkv_proj / kv_ring / mha / layer_block 都还在、单元仿真照跑,整网一块算出的最终残差流与第 25 圈「核串两层」逐位一致;
· 只扩不推翻——整网引擎的寄存器开在投影页 0x200 尾部未占的 slot(0x280~0x2BC),前 25 圈地址映射一个字不动。
新增 rtl/layer_net.sv,rtl/min_bus.sv 挂载并把整网引擎接到 0x280~0x2BC,改写 circle26/gen_boot.py 的解码循环:
// layer_net:一次 start,片内 for L = 0..N-1
ST_PROJ: proj_start <= 1'b1; // ① 本层投影(权重按层选)
ST_PW: if (pr_done) st <= ST_APP;
ST_APP: k_app <= 1'b1; v_app <= 1'b1; // ② K/V 进本层环(硬件 append)
ST_MHA: mha_start <= 1'b1; // ③ 多头(两头各自的 Q)
ST_MW: if (m_done) st <= ST_LB;
ST_LB: lb_start <= 1'b1; // ④ 整层一块
ST_LW: if (lb_done) begin // ⑤ 残差流就地更新 + 层号自增
xs <= lb_y[..7:0];
lcnt <= li_nxt;
if (li_nxt >= layers) st <= ST_DONE; // 跑完 N 层
else st <= ST_PROJ; // 下一层
end
# OS 主路:一轮解码 = 一声 NET_GO + 一次 NET_BUSY
sw s4, 0x280(x0) # NET_X = 本轮输入残差流(token 隐藏状态)
li t0, 2 ; sw t0, 0x284(x0) # NET_LAYERS = 2
sw t1, 0x288(x0) # NET_GO(整网一个引擎)
poll NET_BUSY
lw a0, 0x2A0(x0) # NET_IDX ← 片内层号(硬件自增,必须 == 2)
lw s0.., 0x2A4..(x0) # NET_Y0..Y3 = 整网输出(最终残差流)
整网一块页:0x280 NET_X、0x284 NET_LAYERS、0x288 NET_GO、0x28C/0x290/0x294 NET_W_SEL/LO/HI(每层 FFN 权重 + LayerNorm 参数,槽 0/1/2/3 = W1/W2/gamma/beta)、0x298 NET_BUSY、0x29C NET_DONE、0x2A0 NET_IDX、0x2A4~0x2B0 NET_Y0..Y3、0x2B4/0x2B8 NET_KCNT0/1、0x2BC NET_SLOT。
这一环要证明「整张网真的长成一块了」,现场五条硬证据:
· 证据一:一轮解码只发一声——OS 主路从「每层 QL_START → K/V 追加 → MHA_START → LB_GO」的一长串调用,缩成 NET_X / NET_LAYERS / NET_GO 三个寄存器写 + 一次 NET_BUSY 轮询;
· 证据二:层循环在片内——每轮 NET_IDX 必须等于 2(层号是硬件自增的,核没插过手);NET_GO 带 layers=1 时片内层号只停在 1(层数真由硬件控制);
· 证据三:K/V 是硬件自己 append 的——每层环条数 NET_KCNT 解码 6 步(> 环深 4)后必须满 4,而核在整个主路里没碰过 KR_K_APPEND;
· 证据四:与第 25 环逐位一致——整网一块算出的最终残差流 0xF905FD04 与第 25 环「核串两层」完全相同,采样序列也一样([0,2,0,0,0,3])。结构换了、计算没变(递增不推翻);
· 证据五:层权重真按层生效——只把层 1 的 FFN 第 2 层权重换成单位阵,同一个输入下整网输出必须变;多头仍在主路(末层两头落点累计出现过分歧,否则自曝);
· 屏幕值:SCREEN = 0xF905FD04;SCREEN2 = 0x02040404 = 片内层号 2 | 层 0 环条数 4 | 层 1 环条数 4 | 最终残差流 lane0 4;
· 单元仿真新增 tb_layer_net:四组用例逐项对表(layers=1 只跑层 0;同一输入再跑一次、片内环自己涨到 2 条;layers=2 片内跑完两层;只换层 1 的 W2 整网输出必变)。
这一环踩了一个很像「玄学」的坑:整网引擎在单元仿真里全对,一进协同仿真输出却整整齐齐全是 0。查下来是权重寄存器提交时的位段编码写反了——层号与槽号取错了位,于是每层权重都落进了隔壁格子,LayerNorm 的 gamma/beta 从头到尾没被写过。gamma 是 0,LayerNorm 的输出就是 0,网络于是「完美地」输出全零。
结论:静默的全零比报错更危险。也正因为这一环把「片内环条数」「片内层号」都做成可读回,才一眼看出引擎其实跑完了两层、只是 LN 没参数——可观测性本身就是验证的一部分。
本环走完一整圈:目标 → 一台只有一个对话窗口的手机,它得把一句话一层层想完 → 硬件把「层内的块」再往外串成「整张网」:层控制器在片内自己切层、切权重、追加 K/V、更新残差流 → OS 主路一轮解码只发一声 NET_GO → 单元仿真 + 协同仿真 + 回归 37 项全绿 → 回到目标。芯片的 Transformer 从「一块一层」正式长成「一个引擎跑完整张网」。下一环让这件事从「写寄存器启动」变成「一条指令启动」——ISA 契约落地。
第 27 环:ISA 契约 · 把整网启动做成一条 NPU 指令——现在启动整网还要往 NET_GO 写 1、再轮询 NET_BUSY。下一环把「启动整网」这件软硬件握手的事写进指令集:核里加一条自定义指令(ISA 层的接口契约),整网启动/等待由一条指令完成,OS 从「操作寄存器」升级成「执行指令」。