🎯 圆心 · 目标(每环都从这里出发)
造一台 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环形缓冲 → ⑳…

本环:让 K/V 缓冲「绕圈」,长序列不再被撑爆

① 目标是什么

同一个圆心:一台开机只有一个 AI 的手机,它得能聊下去。上一环芯片已经会按温度与 Top-K 抽签说话,同一句话开头能给出不同说法——「像人说话」这件事在芯片上闭合了。可它还有一种非常不像人的毛病:记性只有几个字。片上的 K/V 缓冲是「线性填满即丢」——容量 4 条,第 5 条起全被丢掉。也就是聊到第五句,芯片就把你前面说的话忘了,而且连自己刚写的那一句都塞不进去。真模型处理长序列靠的是滑窗注意力:永远只留最近的一段上下文,新的进来、最旧的出去,窗口滑着走,既不溢出、也不断档。这一环就把 K/V 缓冲从「填满即丢」升级成环形缓冲。

② 怎么实现(不脱圆心)

延续「端上紧凑、全定点、每步硬件自洽」的主线,新增一个引擎,接在上一环那一页的页尾:
· 环形缓冲(kv_ring)——写指针走到头就绕回 0,覆盖最旧的那一条,头指针前移;容量永远是「最近 DEPTH 条」,长序列不再溢出;
· 读取按逻辑序——逻辑槽 i → 物理槽 (head + i) mod DEPTH,映射由硬件做。于是算分引擎(读 K)与加权引擎(读 V)拿到的永远是按时间排好序的最近若干条,不必知道环形已经绕过几圈;
· 状态可见——逐层暴露 cnt(当前条数,恒 ≤ 深度)/ head(最旧那条的物理槽)/ total(累计写入)/ evict(累计被覆盖),软件一眼就能知道环绕了几格;
· K/V 各自独立绕圈(延续上一环的分离存储),按层分区(每层一个自己的环);
· 只扩不推翻——寄存器接在第十六圈页尾(0x4A8 起,同 bit10 守卫),前 18 圈的地址映射一个字不动;环形关闭(KR_RING_EN=0)时退回「满即丢」的旧语义。

③ 本环做了什么(真代码)

新增 rtl/kv_ring.sv:片上环形 K/V 缓冲(满时覆盖最旧 + 逻辑序读回 + 逐层状态)。

# 追加(单拍脉冲,永不失败)
if (!full)          mem[(head + cnt) & MASK] <= data;  cnt++     # 未满:线性写
else if (ring_en)   mem[head] <= data; head++; evict++           # 满:覆盖最旧、头前移
else                /* 丢弃 */                                    # 关环:退回旧语义

# 读回(1 拍,收的是**逻辑**槽号)
phys  = (head + rd_addr) & MASK        # 逻辑序 → 物理槽(硬件做环形映射)
valid = rd_en && (rd_addr < cnt)

min_bus.sv 挂载并新映射(仍在第十六圈页尾,0x4A8 起),KR_MODE=1 时两个引擎改读环形 K/V:

0x4A8 → KR_K_APPEND  写 1 → K 进本层 K 环(满则覆盖最旧)
0x4AC → KR_K_DATA    K 载荷          0x4B0 → KR_V_APPEND  V 进本层 V 环
0x4B4 → KR_V_DATA    V 载荷          0x4B8 → KR_LAYER     当前层
0x4BC → KR_MODE      1 = 引擎改读环形 K/V
0x4C0 → KR_RING_EN   1 = 满时覆盖最旧(环形);0 = 满时丢弃(旧语义)
0x4C4 → KR_K_RD      写逻辑槽号 → 核读回 K 环   0x4C8 → KR_V_RD
0x4CC → KR_K_RDATA   读回逻辑槽的 K             0x4D0 → KR_V_RDATA
0x4D4/0x4D8 → KR_K_CNT0/1  当前条数   0x4DC/0x4E0 → KR_V_CNT0/1
0x4E4/0x4E8 → KR_K_HEAD0/1 头指针     0x4EC/0x4F0 → KR_K_TOTAL0/1 累计写入
0x4F4/0x4F8 → KR_K_EVICT0/1 累计覆盖  0x4FC → KR_WRAP(bit0/1 = 各层是否已绕过)

最小 OS 只把「K/V 追加」从分离 bank 改到环形 bank,其余解码循环原样不动:

KR_MODE=1、KR_RING_EN=1、GEN_MAX_STEPS=6(> 环深 4)
layer_body(li): KR_LAYER=li → 本层投影 Q/K/V
                → KR_K_DATA/KR_K_APPEND(K 进环,满则覆盖最旧)
                → KR_V_DATA/KR_V_APPEND(V 进环)
                → ATTN_START → SM_START → AV_START → 残差LN → FFN → 残差LN
L_gen_done:     KR_LAYER=0 → KR_K_RD=3(逻辑槽 3 = 窗口里最新那条)
                → 读回 KR_K_RDATA,必须等于本步刚写入的 K,否则屏幕自曝 0xDEADBEEF
④ 怎么算成(闭环验证)
5 步生成 > 环深 4 · 窗口恒 4 条 · 淘汰 1 条 · RESULT=PASS
最后残差流 [-5,1,7,-3] → 屏幕 0xFD0701FB · SCREEN2 = 0x01050104(淘汰=1, 写入=5, 头=1, 窗口=4)· 单元仿真 + 协同仿真 + 全量回归 24 项全部通过

这一环最直观的是「环绕了一圈,窗口还是满的,最新的那条一定在里面」:
· 环深 4,停止符 3、最大步数 6、prompt = token 0;
· 解码一路吐字:采样序列 [0, 2, 0, 2, 3],第 5 步吐到停止符 3 而停下 —— 一共写了 5 条 K/V,比环深多 1 条;
· 第 5 条进来时环满了:硬件覆盖最旧那条(第 1 步的 K),头指针从 0 走到 1,窗口恒为 4 条 —— 旧 bank 在这一步会直接把第 5 条丢掉(新的话根本进不了上下文);
· 收尾时 OS 自己核对:从环里读逻辑槽 3(窗口里最新那条)拿到的 K,必须就是本步刚写进去的那条;对不上就把屏幕写成 0xDEADBEEF 自曝。这一条把「滑窗真的被消费」钉死了 —— 旧语义下这里读到的是被卡在窗口里的旧 K;
· 屏幕 SCREEN = 0xFD0701FB = 4245094907(最后一步残差流,与上一环同值:通路没被动过),SCREEN2 = 0x01050104(淘汰 1 / 写入 5 / 头指针 1 / 窗口 4),与软件参考模型逐位一致;单元仿真还覆盖了「不溢出(写 6 条条数恒 4)」「头指针绕回」「逻辑序读回窗口」「K/V 各自独立绕圈」「按层分区」「读越界无效」「关环退回旧语义」七种情形。

⑤ 回到目标

本环走完一整圈:目标 → 一个开机只有一个对话窗口的手机,它得能一直聊下去、还得记得住最近说的话 → 硬件把 K/V 缓冲从「线性填满即丢」升级成环形缓冲(满时覆盖最旧、头指针绕回、读口按逻辑序映射),OS 的 K/V 追加改走环形 bank,解码跑到 5 步(比环深多 1 步)也不溢出 → 单元仿真 + 协同仿真 + 回归 24 项全绿,OS 自检确认最新那条一定在窗口里 → 回到目标。这块芯片的记忆第一次会「滑窗」:写满了绕回去覆盖最旧的,窗口永远是最近的一段,长序列不再被撑爆。离一个能一直聊下去的 AI 手机,又近一圈。下一环接上更狠的一步:上一环的算例里,窗口里放哪几条 K 对最终输出居然没差别——下一步要让上下文真的影响输出,滑窗注意力才算真的活了。