第 35 环 · 段表流式取(表在内存里、边扫边取 · 表与权重共用流式通道)
还是同一个圆心:一台开机只有一个 AI 的手机。上一环硬件确实会自己读段表了,可读法是「停在边界上等表」——每到一个段边界,引擎先把扫描停下,进 ST_TBLD/ST_TBLW 两个专用状态等表字从 SRAM 回来(发地址 + 采样,实打实两拍)。真芯片上表的读取(主存访问)是几十上百拍的延迟,这么「停一次等一次」会把延迟整个暴露在关键路径上,段越多停越多次。
而段表本来就住在内存里、长得可以很长(大矩阵分几十上百段),正确的做法是边扫边取:扫当前这一段的时候,就把下一条表字从(与权重共用的)流式通道里取回来、存进一个前瞻缓冲;走到段边界时直接换段,不停下扫描等表。
· 前瞻缓冲:lk_data 一个表字 + lk_valid 一位——缓冲里装的是「下一条」表字(当前段还在扫,下一段已经到手);
· 借一拍:预取就借 ST_COMP 那一拍——部分积算在片上(累加发生在寄存器里),SRAM 读口本来空着,同一拍把表地址摆上、读使能拉高,表字下一拍就落进缓冲;扫描一个额外周期都不花(读使能因此改成组合:寄存的取权重 + 组合的预取,两者永不重叠);
· 边界三档:缓冲有货 → 零额外周期直接换段(lk_use_now);缓冲空着(比如本段只有一块、来不及预取)→ 边界那一拍当场取,下一拍装上开扫(1 拍);都没赶上 → 退回第 34 环的阻塞式读表(2 拍);
· 只扩不推翻:RB_TBLP=0 = 第 34 环老语义(边界阻塞两拍读表),一个状态没删;开/关预取跑同一张表,算出来的整行必须逐位一致;前 34 圈地址映射一字不改;
· 硬证据探针:0x3F4 RB_TBLPF(走流式通道取回几条)、0x3F8 RB_TBLUS(边界零额外周期换段的段数——这个数 > 0 就是「边扫边取」真发生了)、0x3FC RB_TBLST(退回阻塞读表的条数,预取路径应为 0)、0x634 RB_TBLCYC(本次启动周期数)、0x638 SCREEN10。
改 rtl/row_blk.sv(新增 tbl_pf 输入 + lk_data/lk_valid/lk_req 前瞻缓冲与预取通路 + 新状态 ST_LKW + 读使能改为组合 w_rd_en_r | tbl_rd_now + 表字装载抽成公共块,三条来路共用),新增 tb/tb_row_blk35.sv(单元仿真,两个不同大小的矩阵 + 开/关预取周期对照);min_bus 挂 0x3F0 RB_TBLP / 0x3F4 RB_TBLPF / 0x3F8 RB_TBLUS / 0x3FC RB_TBLST / 0x634 RB_TBLCYC / 0x638 SCREEN10 并新增第十观察点 screen10_out;circle35/gen_boot35.py 生成最小 OS(第 35 环试验段);tb/tb_min_soc35.sv 协同仿真。
// rtl/row_blk.sv(节选):段表流式取——借一拍空闲的流式通道,边界不再等表
// 预取就发生在 ST_COMP 那一拍(部分积算在片上,SRAM 读口空着)
wire lk_use_now = (st == ST_COMP) && !seg_next && tbl_r && tbl_more
&& tbl_pf_r && lk_valid;
wire lk_issue_now = (st == ST_COMP) && tbl_r && tbl_pf_r && tbl_more
&& !lk_valid && !lk_req;
assign w_rd_en = w_rd_en_r | lk_issue_now; // 组合读使能:借一拍,不花额外周期
ST_COMP: begin
... 部分积累加进 acc ...
if (seg_next) begin // 本段还有下一块
blk_r <= blk_r + 1'b1;
if (lk_issue_now) lk_req <= 1'b1; // 顺手把下一条表字取回来
end else if (bnd_now) begin // 段边界:该翻下一条表了
if (lk_use_now) begin // 缓冲有货 → 零额外周期换段
lk_valid <= 1'b0;
tbl_idx_r <= tbl_idx_r + 8'd1;
st <= ST_FETCH;
end else if (lk_issue_now) begin // 缓冲空 → 当场取(只花 1 拍)
lk_req <= 1'b1; st <= ST_LKW;
end else st <= ST_TBLD; // 兜底:第34环阻塞读表(2 拍)
end
end
// 预取回来的表字下一拍落进前瞻缓冲(表与权重共用同一条流式通道)
if (lk_req && w_rd_valid && (st != ST_LKW)) begin
lk_data <= w_rd_data; lk_valid <= 1'b1; lk_req <= 1'b0;
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]。同一张段表(起 0 长 2 / 起 2 长 1 / 起 3 长 1)先关预取、再开预取各跑一遍,最小 OS 在芯片上真跑:
| 动作 | 结果 | 说明 |
|---|---|---|
| 关预取(第 34 环老路) | acc = [-6, 11, 3, 4] | = W·x;RB_TBLST = 2(两个段边界都停下等表) |
| 开预取(本环主路) | acc = [-6, 11, 3, 4] | 与关预取逐位一致(只改了「谁等谁」) |
| 流式通道取回的表条数(RB_TBLPF) | 2 | 段 0 扫着时预取段 1;段 1 边界当场取段 2 |
| 零额外周期换段(RB_TBLUS) | 1 | 边扫边取的硬证据:有一段是直接从前瞻缓冲换过去的 |
| 退回阻塞读表(RB_TBLST) | 0 | 一次都没停下来等表 |
| 周期数(RB_TBLCYC) | 开预取 省 5 拍 | 一个边界命中缓冲省 3 拍 + 一个当场取省 2 拍 |
| 改写表第 0 条 → 起 1、长 1(开预取) | acc = [-5, 13, 3, 4] | 预取回来的表字真从内存来(改表结果就变) |
单元仿真 tb_row_blk35 把这条通路钉死(两个实例、开/关预取逐组对照):4×4 矩阵 2 段表 → 开预取 31 拍 < 关预取 34 拍(省 3)、RB_TBLUS=1、RB_TBLST=0;3 段表 → 32 < 37(省 5);每段只有一块的短段表 → 34 < 40(省 6,来不及预取也当场取、一条都没退回阻塞读表);8×8、16 块大矩阵 3 段(0..5/6..11/12..15)→ 开预取 115 拍 < 关预取 121 拍(省 6),RB_TBLPF=2、RB_TBLUS=2、RB_TBLST=0,3 段累加与一次整扫逐位一致([-44,13,25,-44,13,25,-44,13]);改写表第 2 条 → 结果跟着变(预取回来的表字真从内存来);空表不挂死;RB_TBLP=0 时前 34 圈老路径逐位一致。协同仿真 tb_min_soc35:SCREEN = 0xF905FD04、SCREEN2 = 0x02040404、SCREEN3 = 0x04000204、SCREEN4 = 0x10200707、SCREEN5 = 0x04081807、SCREEN6 = 0x0402FFFA、SCREEN7 = 0x0403FA04、SCREEN8 = 0x0102FAFB、SCREEN9 = 0x03E3FAFB 与第 25~34 环逐位一致(本环只给行块引擎加了一条流式取表通路,主通路一个字没动);采样序列 [0,2,0,0,0,3]、第 6 步吐停止符 3 而停;全量回归 55 项全绿。
从「硬件自己读表」走到「边扫边取、边界不等表」——表读的延迟被扫描本身盖住了:扫这一段的时候,下一段已经到手。真芯片上表的读取是几十上百拍的主存访问,「停下等」和「边走边取」差的就是 NPU 的利用率;段越多,省得越多。段表住在内存里、被硬件一条条流进来,核从「扫描的执行者」彻底退成「声明者」。芯片离「跑真 DeepSeek 权重」又近一层。
段表已经住在内存里、读它的是硬件不是软件;可核现在还是一条条 MMIO 配置(写开关、写基址、写段数)+ 轮询 busy。下一环把「扫完整块分块矩阵」也收成一条 NPU 指令(复用第 27 环 npu.wait 的核内冻结握手):核只说「激活在哪、段表在哪、扫哪些段」,剩下的硬件自己做完——行块矩阵扫 ISA 化 · 一条指令扫完整块矩阵。