第 21 环 · 多头注意力(多个头并行看不同角度)
同一个圆心:一台开机只有一个 AI 的手机,它得聊得住。上一环注意力有了「看多远」的旋钮——窗口锚点可编程,上下文真的进了输出。可它仍然只有一个头:一组 Q,一次只能从一个角度看上下文。真模型不是这样的。同一句话,模型会同时问好几个问题:谁在指代谁?哪几个词是搭配?语气是升还是降?每个问题就是一个头——同一段上下文,不同的 Q、不同的 K/V 子空间,各自看各自的重点,最后把各头的结论拼起来。一个头只能给一个答案,几个头才看得又全又细,这也是 Transformer 真正强的地方。这一环就把「头」做成硬件:同一份上下文,几个头同时看,看不同的角度。
继续「端上紧凑、全定点、每步硬件自洽、只扩不推翻」的主线,不新造轮子,而是把上一环的引擎并列起来用:
· 并列两个头——新引擎里摆 2 个第 20 环的 win_ctx(滑窗注意力),同一个窗口、同一份 K/V 环,每人带自己的 Q,同一个 start 脉冲同时开跑;
· 真并行,不是排队——两头的遍历/归一化/加权逻辑与窗口参数完全一致,timing 一模一样,所以硬件只发一次 K 环地址、只发一次 V 环地址,同一拍的数据同时喂给两个头:一路端口喂两头,不额外占端口、也不轮流算;
· 各自看各自的——每个头各自吐出自己的最高分、落点、窗口内加权输出:同一个窗口只换 Q,两个头就盯上不同的 token、吐出不同的输出向量;
· 片上拼接——各头输出按 lane 拼接成 8 lane(头 0 的 4 lane 在前、头 1 的 4 lane 在后),得到多头输出(下一环再给这 8 lane 接一个输出投影 W_o,把多头压回 4 lane);
· 只扩不推翻——多头引擎接在 bit10 页里新开的 0x700 起(用 addr[9]/addr[8] 在页内再分页),前 20 圈的地址映射一个字不动;K/V 环读端口的仲裁再加一级:多头引擎优先,其次滑窗引擎,再次算分/加权引擎,闲时归核。
新增 rtl/mha.sv:多头注意力引擎(内含 2 个第 20 环 win_ctx,同一窗口、同一份 K/V,每人一份 Q)。
// 并列 H 个头(generate),每人一份自己的 Q
for (gi = 0; gi < H; gi = gi + 1) begin : g_head
win_ctx wc ( .start(start), .q_in(q_in[gi*16 +: 16]),
.w_start(w_start), .w_len(w_len), .avail(avail),
.best_score(h_best[gi*16 +: 16]),
.best_slot (h_slot[gi*2 +: 2]),
.out (h_out[gi*128 +: 128]), ... );
end
// 两头 timing 一致 → 地址相同:只发一次地址,同一拍数据喂两头
assign k_rd_en = |h_k_rd_en; // 任一头发读即读
assign k_rd_addr = h_k_rd_en[0] ? k_addr0 : k_addr1;
assign digest = h_dig[31:0]; assign win_n = h_n[0 +: L_W];
min_bus.sv 挂载并新映射(bit10 页内新开一页,0x700 起):
0x700 → MHA_Q0 头 0 的查询 Q(4×INT4 打包) 0x704 → MHA_Q1 头 1 的查询 Q(4×INT4 打包) 0x708 → MHA_WSTART 窗口起点(所有头共用) 0x70C → MHA_WLEN 窗口长度 0x710 → MHA_START 写 1 → 两个头同时在锚定窗口上跑(用 KR_LAYER 选层) 0x714 → MHA_BEST0 头 0 最高分 0x718 → MHA_SLOT0 头 0 落点(逻辑槽号) 0x71C → MHA_BEST1 头 1 最高分 0x720 → MHA_SLOT1 头 1 落点 0x724..0x730 → MHA_OUT0_L0..L3 头 0 窗口内加权输出 4 lane 0x734..0x740 → MHA_OUT1_L0..L3 头 1 窗口内加权输出 4 lane 0x744 → MHA_DIGEST 窗口指纹(各头共用同一份上下文) 0x748 → MHA_N 窗口实际参与条数 0x74C → MHA_BUSY 忙闲
最小 OS 收尾在既有解码循环与第 20 环滑窗试验之后,多跑一段多头试验:往层 1 的环里放 4 条已知 K/V,让两个头在同一份上下文上比落点,落点相同就把屏幕写成 0xDEADBEEF 自曝。
层1 环里放 4 条(环深 4 → 正好一整窗): K0=(4,0,0,0) V0=(7,0,0,0) K1=(0,0,0,0) V1=(0,7,0,0) K2=(0,2,0,0) V2=(0,0,7,0) K3=(0,0,0,2) V3=(0,0,0,7) MHA_Q0=0x1111 → 分数 [4,0,2,2] → 盯槽0 输出 lane0=5 # 头 0:四个 lane 等权 MHA_Q1=0x0010 → 分数 [0,0,2,0] → 盯槽2 输出 lane0=1 # 头 1:只看 lane1 两头落点相同 → SCREEN=0xDEADBEEF(多头没看不同角度,当场自曝)
这一环最直观的是「同一份上下文,不同的头看不同的地方」:
· 层 1 环里就那 4 条已知 K/V,两个头跑的是同一个窗口(起点 0、长 4)、同一份上下文;
· 头 0(Q=0x1111,四个 lane 等权):分数 [4,0,2,2],最高分 4、落点槽 0、输出 lane0 = 5——它看重那条打 4 分的上下文;
· 头 1(Q=0x0010,只看 lane1):分数 [0,0,2,0],最高分 2、落点槽 2、输出 lane0 = 1——它盯的是另一条,答案也不一样;
· 两头落点不同、输出不同,OS 的自检因此不会触发;要是两头一样,屏幕会写成 0xDEADBEEF 当场自曝;
· 屏幕 SCREEN = 0xFD0701FB = 4245094907(最后一步残差流,与前三环同值:主通路一个字没动),SCREEN2 = 0x04220501(头0落点/头0最高分/头1落点/头1最高分/头0输出/头1输出),与软件参考模型逐位一致;
· 单元仿真覆盖「两头同窗不同落点、不同输出」「拼接顺序(低 128bit 是头 0、高 128bit 是头 1)」「窗口超出环里条数被裁剪(两头都只看 1 条)」「空窗不挂死、输出清零」「同窗复现逐位一致」,并配一棵真 kv_ring 与参考模型对表。
本环走完一整圈:目标 → 一个开机只有一个对话窗口的手机,它得聊得住、还得同时听懂好几层意思 → 硬件把「头」并列起来:同一份上下文、同一路 K/V 端口,几个头同时看、各看各的角度,各头输出再拼回一条;OS 让两个头在同一份上下文上比落点,落点一样就自曝 → 单元仿真 + 协同仿真 + 回归 28 项全绿 → 回到目标。这块芯片的注意力第一次有了「好几个视角」——一个头只看一件事,几个头才看得又全又细。离一个真能聊得住、聊得深的 AI 手机,又近一圈。下一环把多头收口:现在 8 lane 只是并排摆着,得给它们接一个输出投影 W_o,把几个头的结论融合回一条 4 lane 残差流,多头才真正接得进下一层。
第 22 环:输出投影 W_o · 把 8 lane 多头融合回 4 lane——各头看的角度不同,得有一层把它们的结论学出来、揉成一条再往下走;多头到此才算长进 Transformer 块里。