🎯 圆心 · 目标(每环都从这里出发)
造一台 Salt OS 手机:基于 DeepSeek 的 AI 操作系统,iPhone 4S 外形(含底部圆形 Home 键),开机只有一个 AI(无桌面、无图标),软硬协同设计(OS + 芯片 + 主板一起做),为别人搭台、让别人在平台上赚钱。

🧭 完整框架(同心圆)

①文字闭环 → ②技术深入 → ③挂SRAM → ④权重INT4 → ⑤激活INT4 → ⑥NPU.STORE → ⑦KV Cache → ⑧注意力QK^T → ⑨Softmax → ⑩加权V → ⑪前馈FFN → ⑫残差·LayerNorm → ⑬多层堆叠 → ⑭QKV投影 → ⑮K/V分离 → ⑯自回归解码 → ⑰连续生成 → ⑱采样 → ⑲KV环形缓冲 → ⑳滑窗注意力 → ㉑多头注意力 → ㉒输出投影 W_o → ㉓多头块入残差流 → ㉔多头块进解码循环 → ㉕整层一块 → ㉖整网一块 → ㉗ISA契约 → ㉘权重内存化 → ㉙权重预取双缓冲 → ㉚权重分块 → ㉛矩阵行块 → ㉜行块自增 → ㉝起始块号可编程 → ㉞段表连扫 → ㉟段表流式取 → ㊱行块矩阵扫 ISA 化 → ㊲描述符链 → ㊳链槽直喂 → ㊴主路走链 → ㊵链预投下层 → ㊶…

本环:链知道权重住哪了,可它还是「一层投完、停下等下一层」

① 目标(这一环要解决什么)

第 39 环把主路的层权重收进了内存里的描述符链(核只给「链在哪、多长」),可吃法是阻塞的:引擎在 ST_WLD 把 req 举给链,链按描述符把本层 14 个字一个字一个字投完,引擎才开算——算一层、停一次、等链一次。NET_CHN_WAIT 记的就是这段等待。

链整个落在关键路径上。第 29 环早就教过一次怎么治这个病(乒乓双缓冲:算本层、取下层),只是那次治的是「按公式取权重」那条路。这一环把同一个办法搬到链上:算本层时,让链去投下层,落进另一个 bank,走到下层 ST_WLD 时常常一拍换 bank 就开跑。

② 实现(做了什么)

链的握手是电平的(投完停在 ST_DONE,rdy 一直举着,直到 req 落下才回空闲)。所以「要一层权重」这件事不能再由 ST_WLD 那一拍直接举着——那只能表现成「停下等」。本环新增一个链取子引擎专门举这个电平:

CP_IDLE --cp_go--> CP_REQ --链把本层投完(rdy)--> CP_RLS --> CP_IDLE
                       │                            │
                       │  举着 req 走完一整轮        └─ 把「这个 bank 装的是哪一层」
                       │   (投完立刻撤 req 放链回空闲)      记进就绪标志 ld_ready/ld_layer

于是插入点只剩三处:冷启动(ST_IDLE 给第 0 层起一次链取——第 0 层没法预投)、预投(ST_PREK:算本层时给下层 li_nxt 起一次链取,投进 ~bank_r,同时把旧的就绪标志撤下)、换 bank(ST_WLD:就绪标志指着本层 → 一拍换 bank_r 就开算)。

两个 bank 乒乓:链写的是 cp_buf_r(在飞那一轮的目标 bank),计算读的是 rd_bank(阻塞路径读链正在写的那一个、预投路径读本层那一个)——写读永不打架,本层那份在整层跑完前一个字都不会被改。

核这边只多写一个寄存器:WC_PF(0x83C,1=链预投下层)。WC_PF=0 就是第 39 环的老语义(停下等链),一字未删。新探针 NET_CHN_PF(链取了几次)/ NET_CHN_OVL(有多少字是在算别的东西的时候投进来的),新观察点 SCREEN15(0x848)。前 39 圈地址映射一字不改。

③ 怎么验证(硬证据)
证据结果说明
D 趟(预投)vs A 趟(第 39 环阻塞链)逐位一致只改了「谁等谁」,算出来的东西一个字没变
E 趟(WC_PF=0 回到阻塞)== A 趟老路径原样保留、结果不动(递增不推翻)
F 趟(再关预投跑一趟)两个探针都不再长重叠确实来自预投路径,不是碰巧
NET_CHN_PF2 次冷启动第 0 层 + 预投第 1 层,两层各一次链取
NET_CHN_OVL14 字下层的 14 个字全是在算本层的时候投回另一个 bank 的
等链拍数:阻塞趟 → 预投趟98 拍 → 52 拍(省 46)等链的拍被本层的计算盖住了——这是本环唯一想要的东西
改一条描述符(层1 LN 借住层0 LN 地址)预投路径输出跟着变预投拍到的真是内存里那一层的权重
SCREEN15 = 0x620E3407
阻塞等链 98 拍 | 重叠 14 字 | 预投等链 52 拍 | D 趟 lane0 = 7(四个字节全是运行时量到的)

单元仿真 tb_chain_pf:预投路径 y=0xFA03FC07、链投 28 字、链取 2 次、重叠 14 字、166 拍;阻塞路径 210 拍、重叠 0 字;公式路径(第 28~30 环)输出与之逐位一致;改描述符输出跟着变、还原后逐位回到原值;单层只有冷启动 1 次链取、没有下层可预投(诚实口径);预热 8 趟后的 A(链)/B(公式)/C(链) 三趟两两一致。

协同仿真 tb_min_soc40:整机跑完解码循环 → 采样序列仍是 [0,2,0,0,0,3](第 6 步吐停止符 3 而停);SCREEN = 0xF905FD04、SCREEN2 = 0x02040404、SCREEN14 = 0x1C3C0107 与第 25~39 环逐位一致。全量回归全绿。

④ 调试中钉住的钉子

其一(读 1 写 0)。链的阻塞路径一直写 bank 0(第 39 环写死的),本环把「读哪个 bank」也统一成「链正在写的那一个」之后,才发现旧代码是写 0 读 bank_r——而前面第 29/30 圈的收尾试验会把 bank_r 留成 1。于是 A 趟(链)读到的是另一个 bank 里的旧字,A ≠ B,收尾自曝在阶段 27。单元仿真里 bank_r 从头到尾是 0,所以单测全绿、整机才炸:同一个模块,状态是前面几十圈攒下来的——单测跑不出这类缝隙,整机协同仿真才跑得出来。

其二(电平握手的半程不能撤)。链的 rdy 是电平,req 一撤它就回空闲。若把「要一层权重」做成单拍脉冲,撤了以后没人接那半程握手,链会一直停在 ST_DONE。所以必须由一个子引擎举着 req 走完一整轮(CP_REQ→CP_RLS),投完那一拍才撤。

其三(等链拍数怎么量)。NET_CHN_WAIT 在每趟 start 时归零,所以「一趟跑完读到的就是本趟的等链拍数」。我一开始按「跑完减跑前」量(差值),结果在预投趟上得到 52 − 98 = −46,8bit 一看是 210——差点把「预投比阻塞省 46 拍」这条正确结论读成「预投反而多等 210 拍」。先把口径说清,再读数。

其四(页内序号错一位)。SCREEN15 的写译码我写成了 6'd16(= 0x840,那是只读探针槽),屏幕永远是 0——而 RTL 里的自检其实全过了。修正到 6'd18(=0x848)后数字才出来。地址按 [7:2] 编号时,字节地址÷4 要再数一遍。

⑤ 回到目标(这环让目标前进了多少)

第 39 环让「权重住哪」由内存里的描述符说了算;这一环让「链在什么时候把权重送进来」也有了提前量——算本层时下层已经在路上。NET_CHN_WAIT 从 98 拍掉到 52 拍,掉掉的正是那两层里被判为「可以提前」的部分。

离目标更近的一点在于:真模型每层权重是几 MB 到几 GB,取一层的耗时远大于算一层——只要取权重还落在关键路径上,NPU 就有一半时间在等内存。这一环把「等」收进计算背后,是「以权重流带动计算」而不是「以计算迁就权重」的那一步。