第 20 环 · 滑窗注意力(上下文真影响输出)
同一个圆心:一台开机只有一个 AI 的手机,它得聊得住。上一环芯片的记忆学会了滑窗——写满就绕回去覆盖最旧的,窗口永远是最近的一段。可那个窗口是写死的:永远是「最近 4 条」。核既没法说「只看最近两条会怎样」,也没法证明「窗口里那几条内容,真的参与了输出」。而人脑不是这么工作的——聊到关键处你会回头多看几句,闲聊时只看眼前一句。看多远,应该是个旋钮。更重要的一件事:上一环的算例里,窗口里放哪几条 K 对最终输出居然没差别——滑窗注意力「结构上通了」但「作用上还没活」。这一环两件事一起做:把窗口做成核可编程的锚点,并且拿出硬证据证明上下文真的影响输出。
延续「端上紧凑、全定点、每步硬件自洽」的主线,新增一个引擎,接在 bit10 那一页里新开的一页:
· 窗口锚点——核给起点 w_start 与长度 w_len,硬件只在这个窗口(逻辑槽 w_start … w_start+w_len-1)上做注意力,窗口外的 K/V 一概不看;
· 窗口指纹——遍历窗口时顺手把每一条 K 折进一个 32 位指纹 digest = (digest<<1) ^ K,窗口内容一变指纹就变,核据此能证明「这次看的上下文不是上次那份」;
· 窗口内完整注意力——遍历窗口算 QK^T 分数(追最高分与落点)→ 把窗口内分数喂给第 9 环那台 softmax 引擎归一化 → 把 Q8 权重喂给第 10 环那台加权引擎(权重 × 窗口内 V,(acc+128)>>8)→ 直接吐出窗口内 4 lane 输出;
· 窗口被裁剪——room = avail - w_start、eff = min(w_len, room),裁剪后窗口内每一条都有效,核读到的永远自洽;空窗(len=0)不空转,直接给出 n=0 并把输出清零,不留上一窗的残余;
· 只扩不推翻——寄存器接在页内新开的 0x600 起(用 addr[9] 在 bit10 页里再分一页),前 19 圈的地址映射一个字不动;K/V 环读端口的仲裁加一级:滑窗引擎优先,其次算分/加权引擎,闲时归核。
新增 rtl/win_ctx.sv:滑窗上下文引擎(窗口锚点 + 窗口内 QK^T 落点 + 窗口指纹 + 窗口内加权输出)。
# 窗口裁剪 room = avail - w_start; eff = min(w_len, room) # 环里没那么多条就只看有的 # 遍历窗口(K 环 1 拍回数) sc[idx] = Q·K(idx) # 窗口内逐条算分 digest = (digest << 1) ^ K(idx) # 顺手折窗口指纹 best/slot = max/argmax # 最高分与落点(逻辑槽号) # 窗口内完整注意力(复用第 9/10 环的引擎) softmax(sc[0..eff-1]) → Q8 权重 # 窗口内归一化 out[lane] = (Σ w_i · V(i)[lane] + 128) >> 8 # 权重 × 窗口内 V
min_bus.sv 挂载并新映射(bit10 页内新开一页,0x600 起):
0x600 → WC_Q 查询向量 Q(4×INT4 打包) 0x604 → WC_WSTART 窗口起点(逻辑槽号) 0x608 → WC_WLEN 窗口长度 0x60C → WC_START 写 1 → 在锚定窗口上跑一次(用 KR_LAYER 选层) 0x610 → WC_BEST 窗口内最高分 0x614 → WC_SLOT 最高分落点(逻辑槽号) 0x618 → WC_DIGEST 窗口内 K 序列指纹 0x61C → WC_N 窗口实际参与条数 0x620/0x624/0x628/0x62C → WC_OUT0..3 窗口内加权输出 4 lane 0x630 → WC_BUSY 忙闲(含刚下发那拍,无空窗)
顺带修掉一个藏在 softmax 引擎里的数值 bug:差值原本用无符号比较,分数正负混杂时(滑窗注意力里必然出现)会把负数当成大数、差值归 0 → exp 全 256 → 权重全均分,softmax 静默失真。改成有符号比较(此前分数恒正,暴露不出来)。
最小 OS 在收尾多跑一段滑窗试验:核注入同一个探针 Q,先看整窗、再看最新一条,两窗的指纹/输出若相同就把屏幕写成 0xDEADBEEF 自曝。
WC_Q=0x1113(同一个 Q)→ WC_WSTART=0 / WC_WLEN=4 → WC_START # 整窗
→ WC_WSTART=3 / WC_WLEN=1 → WC_START # 只看最新一条
指纹相同 or 输出相同 → SCREEN=0xDEADBEEF(上下文没影响输出,当场自曝)
这一环最直观的是「同一个问题,看多远的上下文决定了答案」:
· 环深 4、同一个探针 Q = 0x1113,环里就那 4 条 K/V;
· 整窗(锚点 0、长 4):n=4、最高分 12、落点槽 0、指纹 0x00078DD5、输出 [2,2,2,2]——它看到了那条打 12 分的上下文,注意力落上去,加权输出是 +2;
· 只看最新一条(锚点 3、长 1):n=1、最高分 −6、落点槽 3、指纹 0x0000E41D、输出 [−2,−2,−2,−2]——同一条 Q,锚点一挪,它只剩这一条可看,输出直接翻号;
· 指纹与输出全都变了,OS 的自检因此不会触发;要是两窗一样,屏幕会写成 0xDEADBEEF 当场自曝;
· 屏幕 SCREEN = 0xFD0701FB = 4245094907(最后一步残差流,与上两环同值:主通路一个字没动),SCREEN2 = 0xD51D02FE(整窗指纹 / 最新窗指纹 / 整窗输出 / 最新窗输出),与软件参考模型逐位一致;
· 单元仿真覆盖「整窗与近窗的落点/指纹」「同一 Q 不同窗输出不同」「窗口超出环里条数被裁剪」「空窗不挂死、输出清零」「同窗复现逐位一致」;softmax 单元素仿真补了「正负混分 [12,−6,12,−6] → 权重 [128,0,128,0]」这一条,把刚修的 bug 钉住。
本环走完一整圈:目标 → 一个开机只有一个对话窗口的手机,它得聊得住、还得知道该回头多看几句 → 硬件给窗口装上核可编程的锚点,顺手折出窗口指纹、并把窗口内的加权输出直接吐出来;OS 用同一个 Q 跑两个窗口,指纹与输出都变 → 单元仿真 + 协同仿真 + 回归 26 项全绿,OS 自检把「上下文没影响输出」这条死路堵上 → 回到目标。这块芯片的注意力第一次有了「看多远」的旋钮——上下文不是摆设,它真的决定了输出。离一个能聊得住、聊得深的 AI 手机,又近一圈。下一环接上更狠的一步:现在芯片只有一个「视角」在看上下文;真模型是好几个头同时看——有的看语法、有的看指代、有的看情绪,最后拼起来。下一步就让多个头并行地看同一段窗口。
第 21 环:多头注意力 · 多个头并行看不同角度——同一段窗口,几个 Q 头各自算分、各自加权,再把各头的输出拼回一条残差流;让芯片的一个「视角」变成好几个「视角」,这是 Transformer 真正强的地方。