🎯 圆心 · 目标(每环都从这里出发)
造一台 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 化 · ㊲描述符链 · ㊳链槽直喂 · ㊴主路走链 · ㊵链预投下层 · ㊶跨趟预投 · ㊷权重池三 bank · ㊸权重池 ISA 化 · ㊹一步生成 ISA 化 → ㊺…

本环:一步解码,一条指令说完

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

第 16~18 环把「一步解码」的每个零件都做进了硬件:词嵌入查表、整网推理、LM 头、采样器、生成控制器。可把零件串成一步这件事,仍在核的软件里——每吐一个 token,核要:

sw EMB_TOK → lw EMB_X → npu.net → npu.wait → npu.rd
→ sw LM_X → sw LM_START → 轮询 LM_BUSY
→ sw SMP_START → 轮询 SMP_BUSY → lw SMP_TOKEN
→ sw GEN_STEP_W → 轮询 GEN_DONE

十几条访存,每一次「谁先落地、忙闲什么时候看」的时序都压在软件肩上。真手机每吐一个字都走一遍,核就成了那条「串零件的传送带」。本环把「一步」本身收进硬件:核给一个 token、一条 npu.step,引擎在片内自己把词嵌入 → 整网 N 层 → LM 头 → 采样器 → 生成控制器登记串完。

② 实现(做了什么)

新增 rtl/step_gen.sv:一个固定顺序的状态机 EMB → NET → LM → SMP → COMMIT → FIN。每一级都是「先发单拍 start,再等 busy 落下」;忙闲采样在发令后若干拍才判完成,避免「引擎还没忙起来就被当成做完」。片内直连:词嵌入查表结果直接当整网输入,整网输出直接当 LM 头输入,采样出的 token 直接交给生成控制器登记——核一个字节都不搬。

ISA:核升 v12,在 custom-1 下加 funct3=001:

npu.step  rs1, rs2        rs1 = 这一步的输入 token,rs2 = 整网层数
npu.wait                 一步没跑完,整条流水线冻在这条指令上
npu.rd rd, sel=8         这一步吐出的 token
npu.rd rd, sel=9         自开新生成以来跑了几步
npu.rd rd, sel=10        状态 {gen_count, gen_done, …}

npu.rd 的选源从 funct7[2:0] 放宽到 funct7[3:0](8/9/10 是新增的一步生成读回;0~7 编码一位不动)。npu.wait 的忙闲一并覆盖一步生成(step_busy | step_go_pulse | 指令拍),所以「一步没完,核不往下走」是硬保证。

min_bus 新页 0x890~0x8B8:STEP_TOK(MMIO 路径输入)、STEP_LAYERS、STEP_GO、STEP_BUSY、STEP_N(本次生成跑了几步)、STEP_TOKOUT、STEP_STATUS、STEP_CORE(核写入口寄存器的次数)、STEP_RST、STEP_ST(启动次数)、SCREEN19。前 43 圈地址映射一字不改,老路径一条不删。

③ 怎么验证(硬证据)

把主路解码循环整段换成「六步、每步一条 npu.step」,其余一字不动——如果一步生成与「十几条访存」有任何一位不同,后面那一长串 SCREEN 立刻对不上:

证据结果说明
六步采样 token[0,2,0,0,0,3]每步与第 17~43 圈老路径逐位对表,全中
token 序列打包0x0203六步依次 <<2 塞入,序列错一位都不对
步数STEP_N = 6六步都真走了一步生成引擎,不是核在算
核搬运入口寄存器次数STEP_CORE = 0六步里核一次没写 EMB_TOK/LM_X/LM_START/SMP_START/GEN_STEP_W/NET_X/NET_LAYERS/NET_GO
最终残差流0xF905FD04与第 25~43 圈逐位一致(主通路没被动过)
整网 digest / ISA digest0x02040404 / 0x04000204层号、环条数、落点、读回全部不变
SCREEN4~18全部逐位一致权重预取/分块/行块/链/池 15 个观察点一个没漂
采样仍会在第 6 步停停止符 3生成控制器照旧在第六步 done
SCREEN19 = 0x06000203
STEP_N = 6 | STEP_CORE = 0 | token 序列 = 0x0203([0,2,0,0,0,3])

单元仿真 tb_step_gen:三个替身引擎(整网/LM/采样)按拍数忙闲——一步只发一次每级脉冲、忙时再发 start 不重入、词嵌入/LM 输入片内直连、busy 期间不早退、clear 归零、多步串联 token 链与模型逐位一致。

协同仿真 tb_min_soc44:六步 npu.step 跑完整段生成,token 序列 [0,2,0,0,0,3] 与老路径逐位相同,SCREEN/2/3/4/…/18 与第 25~43 圈逐位一致,SCREEN19=0x06000203。全量回归 75 项全绿。

④ 调试中钉住的钉子

其一(脉冲默认值,第 38 环那颗钉子的同一面)。step_rst_pulse 只写在复位分支里,没进「每拍默认拉低」的块——于是 STEP_RST 写过一次之后,它永远高,step_gen 的 clear 每拍都在清,tok_out/steps 永远是 0。偏偏第一步的期望 token 就是 0,自检通过了;到第二步才炸。教训:单拍脉冲必须在每拍默认块里拉低,只写复位分支 = 常高;而且「第一步恰好为 0」会让假绿看起来像真绿。

其二(12 bit 有符号偏移)。0x8A0 起的新页超过 0x7FF,0x8A0(x0) 汇编会被当成负数偏移。必须 li a6, 0x890 再用页内偏移——与第 43 环 a6=0x870 是同一颗钉子。

其三(老程序的隐含寄存器约定)。收尾段一直假设 t1 == 1(sw t1, WC_START / MHA_START 都靠它)。本环的 SCREEN19 计算把 t1 用来装 STEP_CORE,算完停在 0——滑窗引擎再也没被启动,两个窗口读到全 0、指纹相同,在第 2 阶段自曝。教训:接手老程序时,寄存器是有隐式约定的;改完要还原(li t1, 1)。

其四(一个寄存器两个驱动)。把核的 npu_step_tok 输出直接接到 step_tok_r(MMIO 写的同一个 reg),等于让核和总线同时驱动它——Verilator 直接报 BLKANDNBLK。正确做法:指令操作数是独立 wire,引擎入口做二选一(指令拍用操作数,MMIO 用寄存器)。

其五(读回位宽)。npu.rd 的选源原来只有 3 bit,8/9/10 会在译码时被截成 0/1/2——一步生成的读回会静默变成「整网输出」。位宽要跟着选源一起放宽到 4 bit(funct7[3:0]),且 0~7 的旧编码保持不变。

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

到这一环,一次解码对软件只剩一句话:「给这个 token,跑一步」。词嵌入怎么查、整网跑几层、LM 头什么时候启动、采样怎么抽、生成控制器什么时候登记——全在片内按固定时序自己走。核从「串零件的人」退成「发一步、等一步、读一个 token」的调用者。

对一台手机的意义:生成一个 token 的固定开销,从十几条总线访问缩成三条指令;驱动不再需要记住「先写谁、再等谁」,也不会因为顺序写错而静默变慢。更重要的是一步的语义边界清楚了——「一步生成」是一个不可分割的硬件动作,这正是后面把「整段生成」也收进一条指令的地基。