第 24 环 · 多头块进解码循环(生成主路走多头块)
还是同一个圆心:一台开机只有一个 AI 的手机,它得聊得住——而「聊」就是一层一层地看上下文。上一环多头块已经完整了(多头 → W_o → 残差 → 归一化),可它只被单独调用过一次做验证;真正在生成主路上跑的,还是老的单头那套:投影 → 算分 → Softmax → 加权 → 残差 LN。多头块只是摆在那儿。
更要紧的是:第 21 环把两个「头」并列起来时,两个头拿的其实是同一个 Q——那只是「同一双眼睛看两遍」,算不上多头。真多头的每个头有自己的查询投影 Wq。这一环把两件事一起做完:每个头投影出自己的 Q,然后把生成主路整条换成「多头 → 块」。
继续「端上紧凑、全定点、每步硬件自洽、只扩不推翻」的主线:
· 投影引擎加一路 Wq2——第 14 环的 qkv_proj 原本只算一个 Q;本环给它加第二路查询投影(Wq2),四个矩阵乘并行:头 0 的 Q、头 1 的 Q、共用的 K、共用的 V。每个头自己的「看问题的角度」落在硬件数据通路上;
· 头 1 的 Q 也是 INT4 契约——和头 0 一样同时吐 8bit 全精度与 INT4 饱和打包,后者直接喂多头引擎;
· 主路换轨——解码循环里每层的注意力子层改成:两头各带自己的 Q,在同一份 K/V 环上同时开跑 → 8 lane 直连 mh_block → W_o → 残差连接 → LayerNorm → 前馈 → 残差 → 残差流。单头那套(算分/Softmax/加权的单独调用)退居备用,引擎都还在、单元仿真照跑;
· V 投影改成单位阵——这是本环最不起眼却最关键的一处:第 19~23 环的 V 每行相同(加权出来是个常量向量),常量加进残差流后被 LayerNorm 的平移不变性抹掉,注意力对主路等于「使不上力」。V 改成逐 lane 不同(v = x)之后,多头加权出来的才是真正的逐 lane 向量;
· 只扩不推翻——Wq2 用已有的 QL_W_SEL 矩阵槽 3,头 1 的 Q 读口开在 0x370/0x374,前 23 圈的地址映射一个字不动。
改 rtl/qkv_proj.sv(加第二路查询投影)、rtl/min_bus.sv(挂 Wq2 与 QL_Q4B/QL_Q8B),重写 circle24/gen_boot.py 的 layer_body:
// qkv_proj:四路投影并行(每个头自己的 Q + 共用的 K/V) assign qc[d] = dot4(wq [(d*D)*W_W +: D*W_W], x_in); // 头 0 的 Q assign q2c[d] = dot4(wq2[(d*D)*W_W +: D*W_W], x_in); // 头 1 的 Q(本环新增) assign kc[d] = dot4(wk [(d*D)*W_W +: D*W_W], x_in); assign vc[d] = dot4(wv [(d*D)*W_W +: D*W_W], x_in); # 生成主路:每层的注意力子层 = 多头块 lw t0, 0x354(x0) # QL_K4 → KR_K_APPEND(K 进本层环) lw t0, 0x358(x0) # QL_V4 → KR_V_APPEND(V 进本层环) lw t0, 0x350(x0) # QL_Q4 = 头 0 的 Q → MHA_Q0 lw t0, 0x370(x0) # QL_Q4B = 头 1 的 Q → MHA_Q1(本环新增的读口) sw t1, 0x710(x0) # MHA_START:两头在同一份 K/V 环上同时开跑 poll MHA_BUSY lw t2, 0x718(x0) # 头 0 落点 lw t3, 0x720(x0) # 头 1 落点 sub t2, t2, t3 or s9, s9, t2 # 累计:主路上两头到底看过不同角度没有 sw s4, 0x560(x0) # MB_X = 本层输入残差流 sw t1, 0x564(x0) # MB_GO:8 lane 直连 → W_o → 残差连接 → LayerNorm poll MB_BUSY # 块的 LN 输出就是注意力子层的输出 → 前馈 → 残差 → 残差流
新增读口:0x370 QL_Q4B(头 1 的 Q,INT4)、0x374 QL_Q8B(8bit 全精度)。
这一环要证明「多头块真的在主路上跑」,三条硬证据:
· 证据一:主路上两头真的看不同角度——解码 6 步里,层 0 有 3 步、层 1 有 3 步两个头的最高分落在不同的 token 上(OS 用 s9 全程累计,一次都没出现就把屏幕写成 0xDEADBEEF 自曝);
· 证据二:主路被换掉了——最终残差流从第 19~23 环的 0xFD0701FB 变成 0xF905FD04([4,-3,5,-7])。参考模型里直接断言「若等于旧值就报错」,防止有人把主路悄悄改回去;
· 证据三:喂给前馈层的就是块吐出的那份——SCREEN2 = 0xFA05FE04 = 最后一轮解码里末层多头块的 LayerNorm 输出 [4,-2,5,-6],与软件参考模型逐位一致(单头通路永远算不出这份「8 lane → W_o → 残差 → LN」的值);
· 采样序列 [0,2,0,0,0,3]:第 6 步(超过环深 4)吐停止符 3 而停,采样器的抽签翻盘 3 次;
· 单元仿真新增 tb_qkv_proj 的用例 3:同一笔残差流、两张不同的查询矩阵,头 0 的 Q 不变([3,5,6,2])、头 1 的 Q 另外算([-4,-4,-4,-4]),两头查询互不干扰。
跑全量回归时,第 23 环留下的核单元测试一直假失败:断言写成 32'h0BEF_F000(Verilog 忽略下划线,等于 0x0BEFF000),可 lui x9, 0xBEEF 的正确结果就是显示里的 0x0BEEF000——测试期望值自己写错了位。修好后回归才真正全绿。这也说明「每一圈必须真跑一遍全量回归」不是形式:上一次的「33 项通过」其实有一项是红的。
本环走完一整圈:目标 → 一台只有一个对话窗口的手机,它得一层层看上下文 → 硬件给每个头各自的 Q 投影,再把生成主路的注意力子层整条换成「多头 → W_o → 残差 → LN → 前馈」 → OS 用「两头落点全程有差异 + 屏幕值与旧环不同 + 块 LN 输出逐位对表」三条自检钉死,顺手修掉一条假失败的测试 → 单元仿真 + 协同仿真 + 回归 33 项全绿 → 回到目标。芯片的注意力主路,从「一个角度看上下文」正式升级成「几个头一起看」。下一环把前馈块也串进同一个引擎——整层一块。
第 25 环:整层一块 · 注意力块与 FFN 块串成一个引擎——注意力块已经进了主路,下一环把前馈那两步(两层乘加 + 中间的残差归一化)也收进同一个硬件块,核不必再在两次引擎调用之间逐 lane 搬运,一整层 Transformer 块变成一次调用。