第 30 环 · 权重分块(一层权重分多拍取)
还是同一个圆心:一台开机只有一个 AI 的手机。上一环的双缓冲能把「算本层、取下层」并行起来,但它的前提很硬:一层权重得整块装进片上缓冲(第 29 环的两个 bank 各 16 字)。真模型一层就有几十万到上百万个权重,片内那点存储连一层都放不下——「整层 bank」这套一碰真模型就崩。
真芯片的做法是分块(tiling):一层权重切成一堆小块,算一块的同时取下一块,块块流过片上的小缓冲,层再大也只是流过去。这一环把片上的权重缓冲缩成一块(CKW = 8 字),一层的 16 字分 2 块取,取满一块就派发给该块的消费寄存器。
· 缓冲从「整层」缩成「一块」:新增片内分块缓冲 ck_buf[0..CKW-1](8 × 32bit),一个块装一层的一部分(CKW < WS);
· 一层分 NC = WS/CKW = 2 块:第 0 块 = 投影矩阵(Wq/Wq2/Wk/Wv),第 1 块 = FFN/LN(W1/W2/gamma/beta)——切法正好对上计算的相位(先投影、后整层块);
· 取满一块就派发:新增分块装载子引擎(CK_IDLE / CK_REQ / CK_WAIT / CK_DISP),一块填满 → 按块号派发给消费寄存器(pj_rdy/pj_lay = 投影块就绪、ff_rdy/ff_lay = FFN 块就绪),不必等整层;
· 调度优先级:A 本层 FFN 块(LB 等着用)> B 下层投影块(预取,趁本层在算)> C 本层投影块(冷启动兜底)——层与层之间、块与块之间都在往前赶;
· 开算的门槛降下来了:ST_WLD 只等第 0 块(8 字)就进投影,ST_LB 才等第 1 块——投影那一段完全不等 FFN 权重;
· 只扩不推翻:w_ck 选择取法——1 = 分块(本环新增、主路默认),0 = 第 28/29 环的整层 bank(串行/乒乓,一字未删);w_src=0 的寄存器驻留路径、前 29 圈的地址映射一个字没动;
· 探针:0x2E4 NET_W_CK_CNT(取了多少块)、0x2E8 NET_W_ATPROJ(首次进投影那一刻缓冲里已有多少字)——把「层大过缓冲也算得下去」钉在寄存器上。
rtl/layer_net.sv:片上一块缓冲 + 分块派发 + 分块调度:
// 分块调度:本层 FFN 块 > 下层投影块(预取)> 本层投影块(兜底)
if (w_src && w_ck && ck_idle && !ck_go) begin
if (sch_a) begin ck_layer <= lcnt; ck_ck <= (NC-1); ck_go <= 1'b1; end
else if (sch_b) begin ck_layer <= li_nxt; ck_ck <= 4'h0; ck_go <= 1'b1; end
else if (sch_c) begin ck_layer <= lcnt; ck_ck <= 4'h0; ck_go <= 1'b1; end
end
// 一块填满 → 派发给该块的消费寄存器(第 0 块→投影、第 1 块→FFN/LN)
CK_DISP: if (ck_ck == 4'h0) begin
wq_b[0] <= {ck_buf[1], ck_buf[0]}; ... pj_rdy <= 1'b1;
end else begin
w1_b[0] <= {ck_buf[1], ck_buf[0]}; g_b[0] <= ck_buf[4]; ff_rdy <= 1'b1;
end
// 开算的门槛:只等第 0 块(8 字 < 16)
ST_WLD: if (w_ck ? (pj_rdy && pj_lay == lcnt) : (ld_ready && ld_layer == lcnt)) ...
// 字地址 = 层号×WS + 块号×CKW + 块内序号 c_addr <= (ck_layer * WS) + (ck_ck * CKW) + ck_i;
rtl/min_bus.sv:新接口 + 探针接到核可见的寄存器(权重 SRAM 布局、前 29 圈地址映射一个字没动):
// 0x2E0 NET_W_CK 1 = 分块取(片上缓冲只装一块)
// 0x2E4 NET_W_CK_CNT 取了多少块(探针)
// 0x2E8 NET_W_ATPROJ 首次进投影时缓冲里已取到的字数(探针)
// 0x2EC SCREEN5 权重分块 digest
6'd56: net_w_ck_r <= dmem_wdata[0]; // 写
6'd57: dmem_rdata_reg = {24'b0, net_w_ck_cnt}; // 读
6'd58: dmem_rdata_reg = {24'b0, net_w_atproj};
OS 只在开机时多一句 sw 1, 0x2E0(NET_W_CK=1)——此后一层多大、怎么切块、哪块什么时候到,全是硬件自己的事。
单元仿真 tb_layer_net30(引擎 + 行为级权重 SRAM 模型,5 组用例):分块路径 y=0xFA03FC07、取块数 4(2 层 × 2 块)、首次进投影只取到 8 字、重叠 24;老路径(w_ck=0)同一个 y、取块数 0、首次进投影已取满 16 字、重叠 0;单层分块 16 字 / 2 块;改 SRAM 里层 1 的 W2 → 分块路径变 0xF703F90A;寄存器路径不受影响仍 0xFA03FC07。
协同仿真 tb_min_soc30(整机:核 → OS → 解码循环 → 收尾十项试验):
| 探针 / 断言 | 结果 | 说明 |
|---|---|---|
| 分块路径 NET_W_CNT | 32 | 每层 16 字 × 2 层,真从 SRAM 取 |
| 分块路径 NET_W_CK_CNT | 4 | 2 层 × 2 块(8 字/块)流过缓冲 |
| 分块路径 NET_W_ATPROJ | 8 | 开算那一刻缓冲里只有一块(不是 16) |
| 分块路径 NET_W_PF_CNT | 24 | 后 3 块全在算前一块期间取回 |
| 老路径 NET_W_ATPROJ | 16 | 整层 16 字到齐才开算(对比鲜明) |
| 两条路径整网输出 | 逐位一致 | 层大过缓冲也算得下去,计算一个字没动 |
SCREEN2 = 0x02040404(整网 digest,同第 26~29 环)、SCREEN3 = 0x04000204(ISA 契约 digest,同第 27~29 环)、SCREEN4 = 0x10200707(预取 digest,同第 29 环)、SCREEN5 = 0x04081807(分块 digest:取块数 4 | 首投影已取字数 8 | 重叠 24 字 | 分块路径 lane0 = 7 —— 中间那位 8 < 16 就是「层大过片上缓冲也算得下去」的硬证据)。全量回归 45 项(新增 layer_net30 与 min_soc30)全部通过。
调试中顺带想清一处会被漏掉的状态耦合:分块调度里「本层 FFN 块」必须在 ST_LB 也允许发起——否则一旦第 1 块还没取到时已走到 LB,调度器会去取下层的块,而 LB 永远等不到自己的块(兜底死锁);优先级 A 覆盖到 ST_LB 后闭环才真正闭合。
这一环之后,「一层权重必须一次全部装进片上」这个前提没了:层再大,也只是一块块流过缓冲;投影只等第一块就开算,后面的块在算前一块的时候已经取回来了。
对「开机只有一个 AI」的手机来说,这是从玩具模型走向真模型的一步:真 DeepSeek 的一层权重是几 MB 到几 GB,任何片上都装不下——能分块流过的引擎,才有资格谈「在这块自研芯片上跑真的 AI」。
块的形状与数据通路对齐——现在一层切成的是「投影 / FFN」两块,切法跟着计算相位走;下一环让块内权重按矩阵行块切(一层 = 若干行块 × 若干列块),把块号也做成核可编程的(NET_W_TILE),并让累加器跨块累加——同样的片上缓冲,能装下任意大的层。