第 46 环 · 对话回路(npu.generate 接进 OS 主循环)
第 45 环把「一整段生成」收进了一条 npu.generate:一个 prompt 进去,硬件自己跑到停止符,回核一串 token。可手机不是「一问一答就结束」:窗口一直开着,用户说完一句还有下一句,AI 答完一段还要接着答下一段。
在第 45 环里,「一段之后怎么办」还在软件里:核要再发一条 generate、再喂一个输入、再把上下文接上——「接着下一句」没有硬件契约。本环把一场对话(多个回合)也收进一个引擎:一个 CONV_GO,硬件自己跑
回合 0:开新窗口(清空上下文)→ 跑一整段
→ 记下这一回合的 token / 步数 / 末 token
回合 1:输入 = 上一回合的末 token(接话)或 OS 排好的下一句
→ 上一回合的 K/V 还留在环里(同一场对话记得住)
→ …… 直到 N 个回合走完
核从「一段接一段地发」退成「发一场对话、等一场、读整份记录」。
新增 rtl/conv_loop.sv(四态:IDLE → TURN → GO → WAIT):
ST_TURN 决定这一回合的输入,并在需要时发 kv_clear;ST_GO 给整段引擎一发 gen_start;ST_WAIT 等这一段的 done 收口。prompts[1:0];之后 mode_carry=1 用上一回合末 token(硬件接话),否则用队列里的第 k 句 prompts[2k+1:2k]。核读回的 CONV_PIN 就是硬件真正用的那个输入。mode_ctx=1 只在回合 0 清一次上下文,回合之间 K/V 留在环里;mode_ctx=0 每回合开头都清(对照组:每句都当新对话)。turn_seq,每步 2bit)、步数(4bit)、末 token(2bit)、整场 transcript(最近 16 个 token)、回合数、总 token 数、清上下文次数,全部在片上。三处配套改动(都是「扩」不是「改」):
rtl/kv_ring.sv 加 clear 单拍口:两层 K/V 的条数/头指针/累计量全归零(会话真空)。
已有设计一律接 1'b0,前 45 圈行为逐位不变。
rtl/layer_net.sv 加 ctx_clear 入、ktot_out 出:主路的 K/V 环其实长在整网引擎里,
会话边界要清的是它;累计 K 条数也由它报给核。
rtl/min_bus.sv 新页 0x900~0x948(bit11+bit8,前 45 圈没用过):
CONV_PROMPTS / NTURNS / MODE / LAYERS / GO / BUSY /
TURNS / TOKENS / SEQ / TLAST / TN / CTXCLR / KVTOT /
TSEQ01 / TSEQ23 / PIN / SCREEN21 / SCREEN22。
npu.wait 的忙闲并入 conv_busy|conv_go_pulse(一场对话也算 NPU 忙)。
OS 主循环里跑三场,全走「写配置 → 一条 CONV_GO → 轮询 CONV_BUSY → 读整份记录」:
| 会话 | 配置 | 要证明什么 |
|---|---|---|
| A | 2 回合 · ctx=1 · 队列 [0,1] | 一场两回合;上下文不被清,K/V 累计 == 全部 token |
| B | 2 回合 · ctx=0 · 队列 [0,1] | 每轮清上下文 → 清 2 次、K/V 只剩末回合 |
| C | 2 回合 · ctx=1 · carry=1 | 回合 1 的输入 = 回合 0 的末 token(3),核从没写过 3 |
所有既有观察点在那之前已经跑完,SCREEN~SCREEN20 一个没动;对话试验插在全部收尾之后,只新增 SCREEN21/22。
| 证据 | 结果 | 说明 |
|---|---|---|
| 一场几回合 | turns = 2 | 一条 CONV_GO 里硬件自己接了第二段 |
| 总 token 数 | 7(A)/ 8(C) | == 两回合步数之和(硬件自报,核不算) |
| A 清上下文次数 | 1 | 只在会话开头清,回合之间不清 |
| A 的 K/V 累计 | 7 == 总 token | 两个回合的 7 条 K/V 都进过环(同一场对话记得住) |
| B 清上下文次数 | 2 | 每回合开头都清(对照) |
| B 的 K/V 累计 | 3 == 末回合步数 | 前半段上下文真的被清掉了 |
| A 末回合输入 | CONV_PIN = 1 | 队列里的第 2 句(硬件自报的「真正用的输入」) |
| C 末回合输入 | CONV_PIN = 3 | 回合 0 的末 token;核没写过 3 ——「接着上一句」在硬件里 |
| 同一句输入的答案 | A ≠ B | 输入都是 1,A 带上下文、B 上下文被清 → 答案不同 |
| 接话 vs 队列 | C ≠ A | 输入 3 与输入 1 是两句不同的话 |
| 主通路 | SCREEN = 0xF905FD04 | 与第 25~45 圈逐位一致;SCREEN2~20 一个没漂 |
单元仿真 tb_conv_loop:用一个「每段固定 3 步」的段引擎替身,钉死六件事——① 两回合队列输入:回合记录、总 token、transcript 拼接全对;② 接话模式:第 2 回合的输入变成第 1 回合末 token(3),输出跟着变;③ ctx=1 一场只清 1 次、ctx=0 清 2 次;④ n_turns=0 当作 1 回合不空转;⑤ 忙时再 start 不重入(一场仍只有 N 段);⑥ clear 归零。
协同仿真 tb_min_soc46:三场会话全跑通,SCREEN21=0x005B000B、SCREEN22=0x12470E0F,SCREEN~SCREEN20 与第 25~45 圈逐位一致。全量回归见文末。
其一(清错了环)。第一版把 ctx_clear 只接到 min_bus 的 KV 环、也读那个环的累计数——结果会话的 K/V 累计恒为 0,但生成照样出 token。原因是:第 24 环之后主路的 K/V 环长在 layer_net 里面(每层一份),min_bus 那只环是早期 OS 级注意力试验留下的。会话边界要清的是主路那只环。教训:同一功能在演进中出现过两只环,接线前先确认「主路现在读哪一只」。
其二(打包读数没拆字节)。CONV_KVTOT 是「层0 低字节 | 层1 高字节」,一次读回是 0x0707;第一版直接和 7 比,判失败。加 andi 0xFF 只看层 0 才对。教训:探针是打包值时,读回先拆字段再比。
其三(引擎还没起来,轮询已经结束)。CONV_GO 写完的下一条 lw CONV_BUSY 可能比 conv 引擎看到 start 还早——读到 busy=0,轮询一圈就退出,接着读到 turns=0,自曝在「回合数」。GO 之后补 4 个 nop 再进轮询即稳。教训:「发令 → 轮询」之间必须留出引擎起忙的窗口,与第 36 环 npu.wait 的那道缝是同一颗钉子。
其四(x31 的读回/比较异常)。把 CONV_PIN 读进 t6(x31)再比较,恒不中;换 s9(x25)立即正确,其它逻辑一字未改。这颗核在 x31 的装载/分支路径上还有一颗没钉死的钉子,记下来留给后续环查(现有程序一律避开 x31 做检查)。
其五(首段的「热身」现象,留作下一环的输入)。同一份代码、同一颗采样种子,ctx=1 会话的第一段与 ctx=0 会话的第一段并不逐位相同(回合数、K/V 数都对,但 token 序列不同);连跑两场 ctx=1 又完全复现。说明会话起步的第一段仍受「上一场运行历史」影响,尚未定位到具体寄存器。本环的证据因此以结构量为主(回合记录 / K-V 累计 / 接话输入 / 清上下文次数 / 核搬运),不把「首段逐位相同」当作前提——这颗钉子明确留给下一环。
到这一环,软件看一场对话只剩一句话:「这几句,接着聊」。回合的切分、上下文的去留、下一句的输入从哪来、每回合说了什么——全在片内。OS 主循环从「一段接一段」退成「一场接一场」。
对一台手机的意义:对话窗口的语义第一次落到硬件契约上——窗口不关,上下文就在;开新会话,上下文就真空。mode_ctx 那两个模式正好对应产品里的两件事:同一场对话记得住(有记忆),换一个话题/新窗口从零开始(无记忆)。再往上,就是「为别人搭台」时每个服务都能拿到同一份会话上下文的基础。
一场对话已经能一个 CONV_GO 跑完,可「开一场对话」仍是核写一串配置寄存器(prompts / 回合数 / 模式 / 层数)+ 一条 GO。下一环把这场对话也收进 ISA:一条 npu.chat,核只给「下一句在哪、几回合、要不要记忆」,读回整份会话记录——和 npu.pool / npu.step / npu.generate 一样,把「谁摆配置」也收回指令里。