第 31 环 · 矩阵行块(块号可编程 · 累加器跨块累加)
还是同一个圆心:一台开机只有一个 AI 的手机。上一环已经能「一层权重分块流过片上缓冲」,可那个分块是写死的——第 0 块恒定是投影矩阵、第 1 块恒定是 FFN/LN,块号由位置决定,核没法说「我要算哪一块」。
真 NPU 上,权重是一个大矩阵(比如 4096×4096),要按矩阵行块切成很多块,由核(或 DMA)给一个可编程的块号;硬件自己去取那一块、算那一块的部分积,并把它累加进一个跨块保持的累加器——块块下来,累加器里才长出一整行输出。这一环把「分块」升级成「矩阵行块 + 可编程块号 + 跨块累加」。
· 矩阵按行切、按内维切:W(4×4 INT4)按行切 RB=2 行一块、按内维切 CB=2 列一块,共 NB = (D/RB)·(D/CB) = 4 块;
· 块号是可编程寄存器:新增页面寄存器 0x37C RB_BLK——核写块号,硬件把它译码成 (行基址 r0, 列基址 c0),只取那一块的行、只算那几行在那几列上的部分积;
· 累加器跨块累加:新增持久累加器 acc[0..3],不清零——同一行块的不同列块(块 0、块 1)会把部分积一段段加进同一行里;直到核写 0x380 RB_CLR 才归零,开始一次新的矩阵乘;
· 权重仍住内存:行块的行权重从权重 SRAM 取(w_addr = BASE + 块号·RB + 行内序号,放在字 32~39,不碰 0~31 字的层权重)——延续第 28 环「权重住内存」;
· 只扩不推翻:新引擎 row_blk.sv 与前 30 圈的引擎并列,主通路的整网(layer_net)一个字没动;新增 0x378~0x3B0 页,前 30 圈地址映射一字不改;
· 探针:0x3A0 RB_BLKO(块号回显)、0x3B0 RB_BASE(块号译出的行/列基址)、0x388~0x394 RB_ACC0..3(跨块累加器)、0x398 RB_PART0(本块部分积)。
新增 rtl/row_blk.sv(矩阵行块累加引擎)+ tb/tb_row_blk.sv(单元仿真);min_bus 挂第 31 环页 0x378~0x3B0 + 只读 0x3AC SCREEN6;circle31/gen_boot31.py 生成最小 OS(第 31 环试验段);tb/tb_min_soc31.sv 协同仿真。
// rtl/row_blk.sv(节选):块号 → (行基址, 列基址),部分积累加进跨块 acc
function [31:0] dec_rc(input [NB_W-1:0] b, input integer row_sel);
integer t; begin
t = b;
if (row_sel != 0) dec_rc = (t / NCB) * RB; // 行基址 r0
else dec_rc = (t % NCB) * CB; // 列基址 c0
end
endfunction
ST_COMP: begin
for (n0 = 0; n0 < RB; n0 = n0 + 1) begin
part_out[n0*ACC_W +: ACC_W] <= partial_c[n0];
acc_out[(r0_i + n0)*ACC_W +: ACC_W] <=
acc_out[(r0_i + n0)*ACC_W +: ACC_W] + partial_c[n0]; // 跨块累加
end
busy <= 1'b0; done <= 1'b1; st <= ST_IDLE;
end
测试矩阵 W = [1,2,-1,0 / 0,1,1,-2 / -1,0,2,1 / 2,-1,0,1]、x = [3,-2,5,-4],手算 y = W·x = [-6,11,3,4]。最小 OS 在芯片上真跑:
| 块号序列 | acc 累加器 | 说明 |
|---|---|---|
| RB_CLR + 块 0 | [-1, -2, 0, 0] | 只算行 0/1 的列 0/1 → 部分积 |
| 接块 1 | [-6, 11, 0, 0] | 同行块、另列块 → 累加成整行(-1→-6、-2→11) |
| 接块 2、3 | [-6, 11, 3, 4] | 全 4 块跑完 == W·x;行 0/1 一路不变 |
| RB_CLR + 2,3,0,1 | [-6, 11, 3, 4] | 换顺序结果逐位一致(块号可编程) |
| RB_CLR + 只跑块 1 | [-5, 13, 0, 0] | 块号一变,行/列与部分积全变 |
| RB_CLR | [0, 0, 0, 0] | 累加器可复位 |
单元仿真 tb_row_blk 7 组用例通过;协同仿真 tb_min_soc31:SCREEN = 0xF905FD04、SCREEN2 = 0x02040404、SCREEN3 = 0x04000204、SCREEN4 = 0x10200707、SCREEN5 = 0x04081807 与第 25~30 环逐位一致(本环只加了一个新引擎,主通路一个字没动);采样序列 [0,2,0,0,0,3]、第 6 步吐停止符 3 而停。
从「一层权重按位置切两块」走到「块号由核编程、按矩阵行块取、部分积跨块累加」——这正是真 NPU 把大矩阵切成成百上千块流式算完的那套机制:块号可编程 = 计算可以被调度;累加器跨块累加 = 一块块拼出整行输出。芯片离「跑真 DeepSeek 权重」又近了一层:层再大、矩阵再大,也只是块号一个个给、累加器一格格长。
现在块号要核一块一块手动给(写 RB_BLK → RB_GO → 等 → 再写)。下一环把块号做成硬件自增:核写一次起始块号与块数,引擎自己一块块扫完整个矩阵、自动累加,跑完给一个 done——从「核喂每块」升级成「核发一声、硬件扫完」。同样的片上缓冲,一次启动走完整层权重。