第 15 环 · K/V 分离(投影接进多层块)
同一个圆心:一台开机只有一个 AI 的手机。上一环注意力的 Q/K/V 终于由残差流投影出来了,但还有两处「简化」撑着:一是历史 K/V 挤在同一条槽位载荷里——算分引擎(读 K)和加权引擎(读 V)只能分时抢同一个读端口,而且「K 的长度」和「V 的长度」被迫绑成一个数;二是投影权重只有一套,多层堆叠时每一层只能共用同一组权重。真模型里 K 和 V 是两份不同的张量:各自投影、各自缓存、各自被不同的引擎消费;而每一层都有自己的一套 Wq/Wk/Wv。这一环把这两处补齐——K/V 分离 + 投影按层驻留,接进多层块。
延续「端上紧凑、全定点、权重 INT4、激活 INT8」的主线,新增一个引擎 kv_split,并给多层投影另开一页寄存器:
· K bank / V bank 两份独立存储——各有自己的追加端口与写指针(k_len / v_len),也各有自己的读端口。算分引擎只读 K bank,加权引擎只读 V bank,两个引擎可以真的同时读,不再分时抢同一个端口;K 的长度和 V 的长度也就解绑了;
· 两个 bank 都按「层」分区(每层一套独立指针)——第 ℓ 层追加的 K/V 只进第 ℓ 层那一格,第 ℓ 层算分只看得到第 ℓ 层的 K,层与层互不串扰;
· 投影权重按层驻留——新增 ql_w[层][矩阵](每层一套 Wq/Wk/Wv),核给一个层号 + 一条残差流,硬件就用那一层的权重算出那一层的 Q/K/V;
· 只扩不推翻——新增一页寄存器(0x300 起),用页码守卫把它和前 14 圈的老映射隔开,老地址一个字不动;KVS_MODE=1 时两个引擎才改读分离后的 bank,默认还是老行为。
新增 rtl/kv_split.sv:K/V 双 bank,独立指针 + 独立读端口 + 按层分区:
K bank:append → k_mem[层][槽] ,写指针 k_len[层] 自增(满则丢弃)
读端口 → 1 拍延迟(算分引擎专用)
V bank:append → v_mem[层][槽] ,写指针 v_len[层] 自增(与 K 完全独立)
读端口 → 1 拍延迟(加权引擎专用,与 K 端口彼此独立)
min_bus.sv 挂载并新映射(K/V 分离页,0x300 起):
0x300 → KVS_K_APPEND 写 1 → K 进本层 K bank
0x304 → KVS_K_DATA 待追加的 K(D 个 INT4 打包)
0x308 → KVS_V_APPEND 写 1 → V 进本层 V bank
0x30C → KVS_V_DATA 待追加的 V
0x310 → KVS_LAYER 当前层号(决定读写哪一层的 bank)
0x314 → KVS_MODE 1 = 引擎改读分离后的 K/V bank
0x318/0x31C → KVS_K_LEN0/1 逐层 K 长度(与 V 各自独立计数)
0x320/0x324 → KVS_V_LEN0/1 逐层 V 长度
0x328/0x32C → KVS_K_RD/RDATA 核读回 K bank
0x330/0x334 → KVS_V_RD/RDATA 核读回 V bank
0x338/0x33C → QL_W_LO/HI 待提交的半矩阵(行 0~1 / 行 2~3)
0x340 → QL_W_SEL 提交到 ql_w[层][矩阵]({层, 矩阵})
0x344 → QL_X 本层投影输入:残差流
0x348 → QL_LAYER 用哪一层的投影权重
0x34C → QL_START 写 1 → 用本层权重投影 Q/K/V
0x350/0x354/0x358 → QL_Q4/K4/V4 投影结果(INT4 饱和打包)
0x35C/0x360/0x364 → QL_Q8/K8/V8 投影结果(8bit 全精度)
0x36C → QL_BUSY 多层投影引擎忙闲(含刚下发那拍,无空窗)
最小 OS:两层块,每层都走完整的一圈(自己投影 → 自己 K/V → 自己注意力 → 残差 → FFN → 残差):
层 ℓ:LST_X(本层输入残差流)
① 历史 token 投影(本层权重)→ K 进 K bank、V 进 V bank(各自指针)
② 查询 token 投影(本层权重)→ Q 喂算分引擎,K/V 也各进各的 bank
③ QK^T(读 K bank)→ Softmax → 加权 V(读 V bank)
④ RSN(a=注意力输出, b=本层输入) # 残差 + LayerNorm #1
⑤ FFN(归一化输出) # 前馈两层
⑥ RSN(a=FFN 输出, b=LN#1) # 残差 + LayerNorm #2
LST_ADV(残差流锁回 XSR,层号 +1;K bank / V bank 各报长度)
最后读回 XSR → 屏幕
这一环最直观的是「K 和 V 真的是两份东西,而且每一层真的是自己投影的」,每一步都能手算对得上:
· 层 0 输入残差流 x=[6,-4,3,-5],历史 token [1,2,0,-1];投影 Q=[3,5,6,2](P=0x2653)、K=[6,-4,3,-5](P=0xB3C6)、V=[2,2,2,2](P=0x2222)。算分只读 K bank:分数 [11,6],Softmax 权重 [254,1](分歧极大,注意力几乎全压在历史那条上),加权 V(读 V bank)得 [3,3,3,3];
· 残差 + LN #1 → [5,-4,2,-5],FFN → [7,7,7,7],残差 + LN #2 → 层 0 输出 [5,-3,2,-4],锁回 XSR;
· 层 1 用自己那套权重从新的残差流再投影:Q=[3,4,5,2]、K=[5,-3,2,-4]、V=[7,7,7,7];分数 [6,5],权重 [187,68],加权 V 得 [5,5,5,5];再走两个残差 + LN 与 FFN,得到最终残差流 [7,-5,3,-6];
· 屏幕 0xFA03FB07 = 4194564871,与软件参考模型逐位一致;K bank / V bank 的长度各自独立计数(层 0 与层 1 各 2 条,互不串扰)。
单元仿真另外验证了 kv_split 的四条铁律:K 追加 2 条、V 追加 1 条时两份长度各算各的;往层 1 追加不改动层 0;K 端口与 V 端口同一拍同时读各自取回各自的数据;写满 DEPTH 后继续追加被丢弃、长度不变。
本环走完一整圈:目标 → 真模型里 K/V 是两份张量、每层有自己的一套投影权重,共用一份载荷和一套权重是「假的多层」 → 硬件把 KV 存储拆成 K bank / V bank(独立指针、独立端口、按层分区),并把投影权重按层驻留、接进多层循环 → 两层端到端跑通、单元仿真 + 协同仿真 + 回归 16 项全绿 → 回到目标。这块芯片上的多层块第一次「每层都是完整的」:自己投影、自己 K/V、自己注意力。离一台本地跑大模型的手机,又近一圈。下一环补最后一块:现在残差流跑完还是要靠核读回屏幕——下一步让芯片自己从残差流算出下一个 token(LM 头 + 采样),把生成的字追加回 KV bank,真正「一个字一个字自己往下写」。
第 16 环:自回归解码 · LM 头出下一个 token——在多层块之后接一个 LM 头(词表投影 + argmax 采样),让芯片从最终残差流自己吐出下一个 token,并把它追加回 KV bank 继续往下生成,形成「逐字生成」的推理闭环。