第 37 环 · 矩阵描述符链(一条指令扫完整网层权重)
上一环把「扫完一块矩阵」收成了一条 npu.rbmat:操作数在 rs1/rs2 里、忙闲一起等、结果线直接回来。可一层的权重不止一块矩阵——真 Transformer 一层里有 Wq/Wk/Wv/Wo、FFN 的 W1/W2、LN 的 gamma/beta,一层的权重是一串矩阵,每块各有自己的段表、自己的住处、自己该去的去处。
于是核又坐回了驾驶座:一块矩阵一条指令、一块一块往下发;块与块之间的「换矩阵、结果摆哪、要不要清累加器」仍是软件在管。硬件越强、驱动越长——这正是第 27 环给整网解掉的病,在「一层权重」这条路上又长回来了。这一环要把一整层权重也收进一条指令。
做一条矩阵描述符链:内存里排一串矩阵描述符(一条就是一块矩阵的全部交代),核只给「链在哪、链多长」,硬件一条条走完——中间不发第二个 start、不读一次累加器、不写一个寄存器。
链条目(32bit)= { 标签[31:24] | 段数[23:16] | 段表基址[15:10]
| 权重基址[9:4] | 结果槽[3:2] | 保留 | 清累加器[0] }
每条描述符说清四件事:①这块矩阵住在哪(权重字基址——真芯片上就是内存里这块矩阵的地址)②它的段表在哪、有几段 ③算完把结果摆到哪个槽 ④算之前要不要清累加器。
硬件侧(rtl/row_blk.sv)在原来的段表扫描上加了三态:读链描述符(ST_CHND/ST_CHNW)→ 按条目里的段表把这块矩阵扫完(第 34/35 环那套连扫+流式取表一个字没改)→ 把累加器收口进结果槽(ST_SNAP)→ 翻下一条。四个结果槽 × 四个 lane 同时留在片上,核跑完一次性读走。多花的那一拍收口是必要的:最后一块的部分积在 ST_COMP 那一拍的边沿才落进累加器,此刻读到的才是完整的一行。
ISA 契约(核升 v9):npu.rbchain rs1, rs2(rs1=激活向量、rs2={链长[15:8], 链基址[5:0]})、npu.rbchain.s(段表走阻塞读,对照);npu.wait 一并覆盖链(min_bus 把链启动脉冲并进忙闲);npu.rd 选源再取一档 sel=7 = 走完的链条目数。新页 0x750~0x7AC(RB_CHNM/RB_CHNS/RB_CHNN/RB_CHN_GO + 探针 + 16 个槽寄存器 + SCREEN12),前 36 圈地址映射一字不改。
| 证据 | 结果 | 说明 |
|---|---|---|
MMIO 开关全关,一条 npu.rbchain | 槽0=[-6,11,3,4]、槽1=[3,-2,5,-4]、槽2=[2,2,2,2]、槽3=[6,-11,-3,-4] | 四块矩阵各自的 W·x 一次到位(结果只可能来自指令通路) |
npu.rd sel=7 vs MMIO RB_CHNC | 都是 4 | 两条读回通路逐位一致:一条指令真走了四条 |
RB_CHNT(末条目标签) | 0xC3 | 描述符真从内存读来——核没写过这个字段 |
| 改链条目 2 的权重基址(16→24) | 槽2 变、槽0 一个字不动 | 描述符字段真管事(换的是这块矩阵的住处) |
npu.rbchain.s(段表阻塞读) | 四个槽逐位一致 | 取表方式由指令选,结果不变 |
| 老路径(MMIO 四寄存器 + RB_CHN_GO) | 四个槽逐位一致 | 递增不推翻:两条来路同一套硬件 |
单元仿真 tb_row_blk37(35 个用例):四条目链 == 四条单跑、描述符字段(权重基址/段数)改哪个槽就变哪个、条目里的「清累加器」位管事(不清则累加器跨矩阵继续长,槽1 = 槽0 + 下一块的结果)、单条目链 == 第 34/35/36 环段表连扫老路径逐位一致、空链立刻完成不挂死(start 后那一拍 done 就拉高)、某条目段数为 0 时该槽被清成 0 且链继续往下走、开/关段表预取四个槽逐位一致。
ISA 单元仿真 tb_isa_rbchain 只验契约本身:四块矩阵与段表写进 SRAM,MMIO 开关全写 0,一条 npu.rbchain → 四个槽对表全中;改条目 2 的住处 → 只有槽 2 变;npu.rbchain.s 与流式取逐位一致;老路径逐位一致;全通 SCREEN = 0x0000C5C5。
协同仿真 tb_min_soc37:SCREEN = 0xF905FD04、SCREEN2~11 与第 25~36 环逐位一致(本环只加一条链通路,主通路一个字没动);采样序列仍是 [0,2,0,0,0,3]、第 6 步吐停止符 3 而停。全量回归 60 项全绿。
其一:报忙要报在第一拍。链启动脉冲在 EX 拍才置起来,引擎下一拍才看到 start、再下一拍 busy 才起来——核走 MMIO 轮询时会在引擎忙起来的前一拍把忙闲看成空、直接走人(与第 36 环 npu.wait 踩到的是同一颗钉子,只是这次出现在 MMIO 侧)。把链启动脉冲也算进 RB_BUSY 读回之后,忙闲首尾不断。
其二:换矩阵要把前瞻缓冲丢掉。第 35 环的段表预取会在扫描途中把「下一条表字」取进缓冲;一换矩阵,缓冲里那条就是上一块矩阵的表字。装载链条目时清掉缓冲(lk_valid/lk_req 归零),否则会拿着上块的表继续扫本块。
其三:老路径的清零在核手里,链的清零在描述符里。同一次「新矩阵乘」,MMIO 老路径靠核发 RB_CLR 脉冲,链路径靠条目里的清标志——这也是为什么条目要带这一位:一条指令里没有「先清再发」两个动作的位置。
从「一条指令扫一块矩阵」走到「一条指令扫完一整层权重」:一层的权重是一串矩阵,每块各有住处/段表/结果槽/清标志——这些交代全写进内存里的描述符链,核只给「链在哪、链多长」。整层的结论同时留在片上四个槽里,核跑完一次性读走,中间不发第二个启动、不读一次累加器、不写一个寄存器。
这正是软硬协同往深处走的样子:上一环把「一块矩阵的扫描」收进接口契约,这一环把「一层权重的调度」也收进去——核从「扫描的声明者」再退一步,成了「整层搬运的声明者」。真芯片上一层权重有几十上百块矩阵、每块几十上百段,软件看到的仍然只是一条指令。
现在一条指令能把整层权重扫进片上四个结果槽。<可槽里的结论还要靠核一趟趟读出来、再摆进投影/注意力通路。下一环把四个槽直接接进注意力子层:链扫完即用——一层权重扫下来的 Q/K/V 投影结果在片上直连进多头块,核不再当搬运线——链槽直喂投影 · 四个槽接进注意力子层(扫完即用,核不搬运)。