第 14 环 · QKV 投影(从残差流算 Q/K/V)
同一个圆心:一台开机只有一个 AI 的手机。前面十三圈,芯片上已经有了一个「能摞起来的 Transformer 块」,残差流也能贯穿多层。但注意力的最前面一直缺一块拼图:Q/K/V 是核直接手塞进去的常数和预填向量。真模型里,查询 Q、钥匙 K、值 V 全都要由当前输入的隐藏状态 x(残差流)经三个权重矩阵投影出来——Q = Wq·x、K = Wk·x、V = Wv·x。这一环把这三个投影做成片上一个引擎:残差流一进来,Q/K/V 由硬件一次算出,注意力从此端到端自洽,软件再也不用手塞常数。
延续「端上算力紧凑、全部定点、权重 INT4、激活 INT8」的主线,新增一个很小的模块 qkv_proj:
· 输入是残差流 x(4 lane × 8bit);配三个 4×4 INT4 权重矩阵 Wq/Wk/Wv;
· 一次算出 Q/K/V 三组向量,每组同时吐两种精度:8bit 全精度(可读回、供后续层用)+ INT4 饱和打包(直接喂注意力算分与 KV Cache——算分引擎的 Q 是 INT4 契约,KV 槽位载荷也是 INT4 打包);
· 投影输出按 INT4 饱和,等价于把「投影后量化」这一步落在硬件数据通路上,不额外吃乘加器;
· QKV 寄存器另开一页(0x200 起),一行守卫把新页和前面 13 圈的地址映射隔开,老映射一个字不动——只扩不推翻。
新增 rtl/qkv_proj.sv:三路投影 + 8bit/INT4 双精度输出,组合逻辑一次算完:
投影: q[d] = Σ_i Wq[d][i]·x[i] (INT4 权重 × INT8 激活,32bit 累加)
k[d] = Σ_i Wk[d][i]·x[i]
v[d] = Σ_i Wv[d][i]·x[i]
输出: q8/k8/v8 = 饱和到 [-128,127] (8bit 全精度)
q4/k4/v4 = 饱和到 [-8,7] 后打包 (INT4,喂注意力 / KV Cache)
min_bus.sv 挂载 qkv_proj,新映射(QKV 页,0x200 起):
0x200 → QKV_X 投影输入:残差流 x(4×8bit) 0x204/0x208 → QKV_WQ_LO/HI Wq(4×4 INT4) 0x20C/0x210 → QKV_WK_LO/HI Wk 0x214/0x218 → QKV_WV_LO/HI Wv 0x21C → QKV_START 写 1 → 触发三路投影 0x220~0x22C → QKV_Q0~3 读 Q 各 lane(8bit 全精度,符号扩展) 0x230~0x23C → QKV_K0~3 读 K 各 lane 0x240~0x24C → QKV_V0~3 读 V 各 lane 0x250 → QKV_Q4 读 Q 的 INT4 饱和打包 → 直接喂 ATTN_Q 0x254 → QKV_K4 读 K 的 INT4 饱和打包 → 直接喂 KVC_DATA 0x258 → QKV_V4 读 V 的 INT4 饱和打包 0x25C → QKV_BUSY 投影引擎忙闲(含刚下发那拍,无空窗)
最小 OS:残差流进 → 投影出 Q/K/V → 投影出的 K 作为一条新 token 追加进 KV Cache、投影出的 Q(INT4 饱和打包)直接喂算分引擎,接着走完一个完整块:
LST_X(x0) → QKV 投影(x→Q/K/V)
ATTN_Q ← Q4(投影出的 Q) KVC_APPEND(K4)(投影出的 K 进历史)
QK^T → Softmax → 加权 V
RSN(a=注意力输出, b=当前残差流) # 残差 + LayerNorm #1
FFN(LN 输出) # 前馈两层
RSN(a=FFN 输出, b=LN#1) # 残差 + LayerNorm #2
LST_ADV # 块输出锁回 XSR
读回 XSR → 屏幕
这一环最直观的是Q/K/V 真的由残差流算出来了,而且每一步都能手算对得上:
· 残差流 x=[6,-4,3,-5],配 Wq(混合阵)、Wk(单位阵)、Wv(每行 [1,1,0,0])→ 硬件一次算出 Q=[3,5,6,2](P=0x2653)、K=[6,-4,3,-5](P=0xB3C6)、V=[2,2,2,2](P=0x2222)——Q ≠ x,证明它真是被矩阵「投」出来的;
· 投影出的 K 追加成第 3 条历史(KV 槽 0x0001 / 0x0010 / 0xB3C6),投影出的 Q 直接进算分引擎:分数 [3,5,6],Softmax 权重 [9,66,180](三条历史都参与加权),加权 V 得 [4,-3,2,-4];
· 再走残差+LN → FFN → 残差+LN:LN1=[13,-8,7,-11],FFN=[20,20,20,20],最终残差流 [16,-11,8,-14];
· 屏幕 0xF208F510 = 4060673296,与软件参考模型逐位一致。
单元仿真另外验证了饱和路径(8bit 不饱和、4bit 饱和:q8=[12,-8,6,-10] → q4=[7,-8,6,-8])。至此注意力的 Q 不再由核手塞常数,Q/K/V 全部来自残差流投影,一个块的注意力和它的输入真正自洽。
本环走完一整圈:目标 → 大模型里 Q/K/V 必须由隐藏状态投影得到,手塞常数的注意力是假的自洽 → 硬件只加一个小引擎(三个 4×4 INT4 矩阵乘 + 饱和打包),Q/K/V 全在片上算出 → 投影-注意力-残差-FFN 端到端跑通、单元仿真 + 回归全绿 → 回到目标。这块芯片上的注意力第一次「长在自己身上」:输入残差流进来,Q/K/V 自己投出来,不用外面喂。离一台本地跑大模型的手机,又近一圈。下一层要补两块:现在 K/V 还共用一个槽位载荷(最小系统的简化),且投影还没接进多层块——下一圈把 K/V 拆开,让每一层都自己投影。
第 15 环:K/V 分离 · 投影接进多层块——把 KV Cache 每个槽位的单字载荷拆成 K、V 两份,真正的 K 和 V 各存各的;同时把投影接进多层循环,让每一层都从自己的残差流投影出 Q/K/V,注意力完全不再依赖预填向量。