第 39 环 · 主路走链(解码循环的层权重改走描述符链与槽直喂)
前几环把「扫一整层权重」「把扫完的槽直喂子层」都收进了硬件。可这些本事只在收尾试验里跑:解码主路的每层权重,走的还是第 28~30 环那条老路——整网引擎自己按写死的公式「层号 × 16 + 层内序号」从权重 SRAM 流式取。
公式把权重的住处钉死了:想搬到别处不行、想让两层共用一份矩阵不行、想按别的顺序摆不行。真芯片上权重就是内存里一堆矩阵,谁住哪、投到哪个用途该由内存里的描述符说了算,不该由一条乘法公式说了算。这一环让主路走链。
新增 rtl/wei_chain.sv 权重描述符链引擎:内存里排一串权重描述符,每条说清「这张矩阵是哪一层的、投到哪个用途槽、几个字、住在哪」:
描述符(32bit) = { 层标签[31:24], 用途槽[23:20], 字数[19:8], 基址[7:0] }
用途槽 = 整网引擎里那几个消费寄存器的组号:
0=Wq(头0查询) 1=Wq2(头1查询) 2=K/V投影 4=FFN两层 6=LN仿射
槽 s 的第 j 个字 → 消费寄存器序号 = 起始序号(槽) + j
(与第 28 环「层 × 16 + 序号」的字布局一一对齐 → 链投的字与装载器取的字落在同一个寄存器)
整网引擎在等本层权重时把 req 拉高、同时给出层号;引擎按描述符一条条取字、边取边直投进层权重寄存器(wr_en/wr_idx/wr_data 片上直连),别的层的条目硬件自己跳过——「哪些条目属于本层」是硬件判的,核没参与筛选。本层投完 rdy 拉高。
ISA 上不加新指令:主路启动仍是第 27 环那三条 npu.net / npu.wait / npu.rd——本环换的是「权重从哪来」,不是「核怎么发指令」,解码循环一行未改。核只写三个寄存器:WC_MODE(主路走链)/ WC_TBLS(链条目起始字)/ WC_N(链长)。新页 0x800~0x838(bit11 页,前 38 圈未用过),前 38 圈地址映射一字不改。
| 证据 | 结果 | 说明 |
|---|---|---|
| 解码循环整个改走链 | SCREEN / SCREEN2 / SCREEN3 与第 25~38 环逐位一致 | 换了权重的来路、算出来的东西一个字没变 |
NET_CHN_WRD(引擎侧) | 28 字 / 趟(2 层 × 14 字) | 本层权重真是链直投进来的 |
NET_CHN_WAIT | > 0 | 等权重的那些拍里,等的是链,不是装载器 |
WC_WRD / WC_SKIP / WC_ENT(链侧) | 168 字 / 跳 60 条 / 走 120 条 | 6 步 × 2 层 = 12 次请求;每次跳过另一层 5 条——核没筛过层 |
WC_TAG(末条目标签) | 0x01 | 直接从内存里的描述符读来,核没写过这个字段 |
| 尾巴 A(链)vs B(公式) | 逐位一致 | 同一份计算,只是「谁把权重送进来」不同 |
| 尾巴 C(改一条描述符:层0 LN 住处 12→14) | ≠ A | 描述符的基址真管着权重——不是摆设 |
单元仿真 tb_wei_chain:一层权重按内存里的描述符投进消费寄存器、别的层的条目硬件自己跳过(skip=5)、槽号说了算(描述符顺序打乱,每个字照样落回自己的槽)、别名(层1 的 Wq 指向层0 的地址 → 两层共用一张矩阵)、空链不挂死 / clr 清探针 / 重放逐位一致——全通。
协同仿真 tb_min_soc39:整机跑完解码循环(层权重全程由链直投)→ 采样序列仍是 [0,2,0,0,0,3],第 6 步吐停止符 3 而停;SCREEN = 0xF905FD04 与第 25~38 环逐位一致。全量回归全绿。
其一:新页的位选要抢在老的页内再分页之前。新页 0x800 的 addr[10:8] 全是 0,会被前几圈那条 sel_r0 分支先接走——写进去石沉大海。bit11 页的解码必须放在最前面。
其二:链的 rdy 是电平,不是脉冲。核在 ST_WLD 看到它才开算;可等跑到 ST_LB 要用「本层 FFN 块到齐」时,req 早已落下、rdy 也跟着落下——不把它锁进一个标志(chn_ok_r),ST_LB 会永远等一个已经消失的握手(死锁)。
其三(参考模型里的同名覆盖)。本环的尾巴对照在参考模型里也重放了一遍,用的变量名也叫 yA/yB/yC——正好把第 28~30 环那套同名的模型值覆盖掉了。于是第 38/56 子阶段的期望值被悄悄换成了尾巴对照的值:硬件给 0xF703F90A,生成器却写 0xFB04FB07,仿真报了一个假失败。修法很土很有效:尾巴那套前缀 r39_。
其四(尾巴的寄存器其实没摆对)。npu.net 的两个操作数是 s5/s6(第 27 圈起的约定),不是注释里写的 s10——尾巴只写了 s10,于是悄悄沿用了第 37/38 段留下的 s5=0x4E / s6=1072(层数被截成 0)。跑的既不是对照输入也不是 2 层,A 趟输出自然对不上。教训:注释说的不等于指令真吃的,摆操作数要对着编码核一遍。
第 37 环让「扫一整层」成了一条指令,第 38 环让「扫完即用」不用回核里绕一圈;这一环让主路本身也走这条路——解码循环的每一层权重,都由内存里的描述符链决定住处与去处。核在权重这件事上从「取数的人」退成「给个链在哪、多长的人」。
离目标更近的一点在于:真模型的一层权重是几 MB 到几 GB、切块摆在不同区域,甚至多层共用同一份矩阵(权重共享)——这些都能被描述符表达,而公式永远表达不了。当主路也走链,整台机器就从「按公式找权重」变成了「按内存里的清单取权重」,这才装得下真正的大模型。
主路走链之后,链是一层投完、等下一层再来要的:引擎在 ST_WLD 上真要停下来等(NET_CHN_WAIT 就是这段等待的拍数)。下一环把第 29 环的乒乓预取搬到链上——算本层的时候,链已经把下层投进另一个 bank,让等链的那些拍也被计算盖住:链预投下层 · 算本层时下层权重已在路上。