第 34 环 · 段表连扫(一次启动走完多段 · 段起始/段长硬件自读)
还是同一个圆心:一台开机只有一个 AI 的手机。上一环把「起点与段长」做成了核可写寄存器,但每段仍要核发一次 start:分 3 段扫一块矩阵 = 核写 3 次起点、写 3 次段长、发 3 次 RB_GO,软件得自己把每段的起点/段长记住。
真 NPU 上一块大矩阵要分几十上百段搬进片上算,每段的起点/段长本来就住在内存里的段表(一段一行);让软件一趟趟读出来再写进 MMIO,软件又一次成了扫描的瓶颈。这一环把「读表」也交给硬件:给一个段表基址与段数,一条 start,硬件自己把段表走完——段与段之间累加器自动接续。
· 段表:摆在权重 SRAM 里,一段一个字 {标签[31:24], 段长[15:8], 起始块号[7:0]}——起点段长本来就是内存里的数据;
· 三个寄存器:0x3D4 RB_TBLM(1 = 段表连扫)、0x3D8 RB_TBLS(段表起始字地址)、0x3DC RB_TBLN(段数);
· 跑法:RB_CLR 一次、一条 RB_GO;硬件先发段表读地址(与取权重共用同一条 SRAM 读端口,分时复用、不额外开口子),把表字读回来解出「起始块号 + 段长」,自增扫完这一段,再自读下一条……扫完 n 段才 done;
· 只扩不推翻:RB_TBLM=0 = 第 31/32/33 环老语义(核写块号/起点/段长、核发 start),引擎一个字没删,两条路径必须逐位一致;前 33 圈地址映射一字不改;
· 硬证据探针:0x3E0 RB_TBLC(按表扫过几段)、0x3E4 RB_TBLT(当前段标签——核从没写过这个寄存器,标签对得上就说明表字真是硬件从内存读来的)、0x3E8 SCREEN9。
改 rtl/row_blk.sv(新增 tbl_mode/tbl_base/tbl_n 输入 + tblcnt_out/tbltag_out/tblent_out 探针 + 两个新状态 ST_TBLD/ST_TBLW + ST_COMP 的「还有下一条就接着翻表」判据 + 读地址在读表两拍都得稳住),新增 tb/tb_row_blk34.sv(单元仿真,两个不同大小的矩阵);min_bus 挂 0x3D4 RB_TBLM / 0x3D8 RB_TBLS / 0x3DC RB_TBLN / 0x3E0 RB_TBLC / 0x3E4 RB_TBLT / 0x3E8 SCREEN9 并新增第九观察点 screen9_out;circle34/gen_boot34.py 生成最小 OS(第 34 环试验段);tb/tb_min_soc34.sv 协同仿真。
// rtl/row_blk.sv(节选):段表连扫——硬件自己读表、自己翻页
// 表字 = {标签[31:24], 保留[23:16], 段长[15:8], 起始块号[7:0]}
wire tbl_rd_phase = (st == ST_TBLD) || (st == ST_TBLW);
wire [31:0] waddr_i = tbl_rd_phase
? (tbl_base_r + tbl_idx_r) // 读段表:表地址
: (BASE + (blk_r * RB) + f_i32); // 扫段:权重地址
ST_TBLD: begin w_rd_en <= 1'b1; st <= ST_TBLW; end // 发段表读地址
ST_TBLW: if (w_rd_valid) begin
// 起点/段长全从这一条表字解出——核没写过这两个寄存器
blk_r <= w_rd_data[NB_W-1:0];
seg_r <= w_rd_data[15:8];
tbltag_out <= w_rd_data[31:24]; // 标签回显(表字真从内存读来的硬证据)
segcnt_r <= 8'd0;
st <= ST_FETCH; // 开始扫这一段
end
ST_COMP: begin
...
end else if (tbl_r && ((tbl_idx_r + 8'd1) < tbln_r)) begin
tbl_idx_r <= tbl_idx_r + 8'd1; // 这一段扫完 → 自己翻到表下一条
tblcnt_r <= tblcnt_r + 8'd1;
st <= ST_TBLD; // 核一个字没写、一条 start 没发
end else begin
busy <= 1'b0; done <= 1'b1; // 表里最后一段也扫完才放手
end
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]。段表写进权重 SRAM 字 40~42,最小 OS 在芯片上真跑(全程只发一条 RB_GO):
| 动作 | 结果 | 说明 |
|---|---|---|
| 段表 3 条:起 0(长 2) / 起 2(长 1) / 起 3(长 1) | acc = [-6, 11, 3, 4] | = W·x == 一次整扫;RB_TBLC = 3(硬件自己走了三段) |
| 末段标签(RB_TBLT) | 0xE3 | 直接从表字读来,核没写过这个寄存器 |
| 累计扫过块数(RB_BLKC) | 4 | 三段加起来 4 块,累加器跨段保持 |
| 改写表第 0 条 → 起 1、长 1 | acc = [-5, 13, 3, 4] | 表字真从内存读来(改表结果就变,≠ 整扫的 -6) |
| 段数写 2(RB_TBLN=2) | [-5, 13, -3, 8] | RB_TBLC = 2 → 连扫范围真受段数限制 |
| RB_TBLM=0 老路径 | [-6, 11, 3, 4] | 两条路径逐位一致(递增不推翻) |
单元仿真 tb_row_blk34 用一个「大矩阵」实例把「连扫」钉死:8×8 矩阵、16 块(D=8、NB=16),段表 3 条 0..5 / 6..11 / 12..15 连扫完,3 段累加与一次整扫逐位一致([-44,13,25,-44,13,25,-44,13]);段数写 2 → 只走前两条([-44,13,25,-44,13,25,0,0]);改写表第 2 条(起 9、长 1)→ 结果跟着变(行 4/5 上多长一遍块 9 的部分积 16/19);空表(段数 0)立刻完成、busy 不挂死;换个槽位放表照样连扫(段表基址可编程)。协同仿真 tb_min_soc34:SCREEN = 0xF905FD04、SCREEN2 = 0x02040404、SCREEN3 = 0x04000204、SCREEN4 = 0x10200707、SCREEN5 = 0x04081807、SCREEN6 = 0x0402FFFA、SCREEN7 = 0x0403FA04、SCREEN8 = 0x0102FAFB 与第 25~33 环逐位一致(本环只给行块引擎加了一条读表通路,主通路一个字没动);采样序列 [0,2,0,0,0,3]、第 6 步吐停止符 3 而停;全量回归 53 项全绿。
从「一段一次 start」走到「一张段表、一次 start、硬件自己走完多段」——段表住在内存里,读它的是硬件不是软件。这正是真 NPU 上权重分块搬运的样子:几十上百段的起点/段长本来就写在内存的表里,核只说「表在哪、有几条」,剩下的硬件自己做完。软件从「扫描的执行者」退成「扫描的声明者」,矩阵再大也不会把核拖成瓶颈。芯片离「跑真 DeepSeek 权重」又近一层。
现在段表还整整齐齐摆在片内 SRAM 里。真模型上段表会跟权重一样大到放不下片上——表本身也得从外部内存边算边流式取,甚至与权重抢同一条内存带宽。下一环:段表流式取 · 表在内存里、边扫边取(表与权重共用流式通道)。