- 引入 MemoryRouterXL 与 v5/v6 流式多线程训练/编码管线 - 修复 prepare_memory_router_dataset 候选池重建缺陷(mega 家族 3568x 加速,输出逐字节相同) - 修复 v5 被破坏的拒答与多跳标签(train 未知样本 319 -> 16319,multi_hop 平均正例 1.00 -> 2.00) - 同存储预算下 V2-128 v6 逐轴 22/22 通过:Top-1 41.12% -> 94.62%,未知拒答 0.00% -> 100.00% - 记录三条被实测推翻的显然优化(logits_to_keep=1 反而慢 55%、XL 容量未带来收益) - 记忆手术跨架构可移植性 14/14,读写关闭时与原生模型逐位相同
12 KiB
零重叠改写成语(zero-overlap paraphrase)阶段报告
本阶段针对 E2E_FINDINGS.md 定位到的唯一泛化缺口:查询与事实之间没有任何独特字重叠时的语义匹配。
所有速率均为百分比。所有数字均来自磁盘上保存的 scorecard / 评测 JSON,不引用滚动日志。
逐轴对照表见 zero_overlap_phase.md(由 report_zero_overlap_phase.py 从 scorecard JSON 渲染)。
1. 结果摘要
新增了一个可验证的零重叠改写数据集、一个合并特征库,以及一个重放微调后的路由器
REPLAY-128(2,037,774 参数,128 维,每条记录地址 512 字节 —— 与现网路由器几何完全一致)。
| 评测 | 指标 | v6 最终版 | REPLAY-128 | 变化 |
|---|---|---|---|---|
| 零重叠(300 条,24 个未见过问法) | Top-1 正确率 | 18.40% | 59.60% | +41.20pp |
| 零重叠 | Recall@3 | 36.00% | 83.20% | +47.20pp |
| 零重叠 | MRR | 35.07% | 73.06% | +37.99pp |
| v6 全集(21,920 条,全部 22 轴) | Top-1 正确率 | 94.37% | 94.14% | −0.23pp |
| v6 全集 | Recall@3 | 96.40% | 96.48% | +0.08pp |
| v6 全集 | 多跳证据全中(Top-3) | 95.95% | 96.22% | +0.27pp |
| v6 全集 | hop 正确率 | 100.00% | 100.00% | 0 |
| v6 全集 | 未知拒答率 | 100.00% | 100.00% | 0 |
| v6 全集 | 已知问题被误拒率 | 0.00% | 0.00% | 0 |
零重叠评测集随机猜测基线为 4.17%(24 个同形候选),因此 59.60% 约为随机水平的 14 倍。 v6 全集的全部策略轴逐位不变(见第 3 节的冻结机制),检索轴变化在 ±0.27pp 以内。
部署契约:check_router_swap.py 判定 DROP-IN REPLACEMENT OK —— 16/16 键匹配、无缺失/多余、
无形状不符、分数有限,且实测可驱动 PagedMemoryBankV2 路由(bank_routed_records: 3)。
延迟(bench_router_latency.py,7 轮交错取中位数):单查询 1.2042 ms → 1.2087 ms(+0.37%),
batch=256 QPS 190,103 → 192,600(+1.3%)。batch=64 QPS 读数低 9.2%,但该轮自身离散度为
17.4%,且同架构同参数同 FLOPs 下 batch=256 反而更快 —— 方向不一致,判为测量噪声而非回归。
2. 数据集:属性由构造保证,而非相信手写意图
make_zero_overlap_paraphrase_data.py 生成 24 个属性 × 3 个改写问法:
- 每条查询与其自身目标事实的独特字重叠必须为 0(属性字符、停用字除外);
- 前 2 个改写用于训练(48 个问法),第 3 个留出用于评测(24 个问法)——两个划分的 查询字符串完全不相交,因此评测衡量的是"换一种说法"的泛化,而不是记住训练字符串;
- 候选是 24 条同句式、不同属性的事实,值为随机码,只出现在一条事实里(可检测证据混用);
- 15% 为不可回答 episode(改写指向候选中不存在的属性)。
关键设计判断:改写与其他无关属性的字符重叠被刻意保留(生成器统计 54.53% 的 episode 至少 含一个这类"陷阱"候选)。因为命中错误候选会得到错误答案,这只会让任务更难,不会提供捷径; 真正必须为 0 的是与自身目标的重叠。最初把二者混为一谈时,24 个属性中有 7 个因为 "起床时间"的"时"、"办公城市"的"公"等高频字而被判为"无可用的改写"。
verify_zero_overlap_dataset.py 用独立重写的停用字集合与重叠算法复核(不 import 生成器):
- 结构问题 0,查询与自身目标重叠 0,两个划分共享查询字符串 0;
- 每条的答案码只出现在唯一一条候选中,候选属性标签唯一;
answerable/need_memory/hop与正例索引一致。
(生成器报的陷阱比例 54.53% 与校验器报的 44.00%/50.00% 不同,因为两者用不同的停用字集合 定义"独特字";这是定义差异,不是矛盾。)
3. 两次微调的对照:朴素微调会造成灾难性遗忘
先做朴素微调(只用零重叠数据,全部损失项默认权重,从 v6 最终版权重出发,6000 步):
- 零重叠 Top-1 18.40% → 66.80%,但 v6 全集同时退化:
random_positionMRR 99.02% → 93.37%、多跳证据全中 100.00% → 95.65%、 hop 100.00% → 87.10%、已知问题被误拒率 0.00% → 13.85%、unknownTop-1 58.99% → 47.52%。 - 训练日志还显示策略门发生了"用误拒换拒答"的交换:未知拒答率升到 40.00%,代价是 38.00% 的可回答问题被误拒。
这就是单指标视角会误判为胜利、而多轴记分卡会否掉的典型情形。
修正方案由两点组成:
- 冻结策略头(
--need-loss-weight 0 --hop-loss-weight 0)。在零重叠集合上,need_memory门只看查询、看不到候选,而"不可回答"的改写与可回答的改写句式完全一样,因此该门在此数据上 原理上不可学;强行训练只会破坏 v6 学到的门。实测证明冻结生效:合并评测的need_specificity在每一次 eval 中都是0.9879692011549567,逐位不变。 - 重放(把零重叠数据并入 v6 数据一起训练),避免排序通路被新分布带偏。
4. 合并语料:复制已验证的行,而不是重新编码
build_replay_corpus.py 把 v6 库(2,124,552 行)与零重叠库(36,072 行)合并为
2,160,624 行。没有重新编码 v6 特征 —— 那既耗时数小时,又会让历史测量所依赖的参考特征
发生无声漂移。合并的强制不变量:
- 每个源库的每一行都恰好被复制一次,或因文本完全相同而跳过(本次重复 0 行);
index条目数 == 库行数(训练器会拒绝不一致的库);- 合并后 train/eval 引用的每一个文本都能解析到一行(缺失 0);
- 2000 行抽查逐字节相等;全库扫描全零行 0;
- manifest 携带合并后数据文件的 sha256(训练器会据此校验冻结输入)。
合并语料:train 88,355 episode / eval 22,220 episode;
data/router_replay_v7/、H:\Memory\nm_cache\nm_replay_v7\feature_cache。
5. 关键负面结论:路由器排序不影响端到端答案
REPLAY-128 与 V2-128-v6 在 eval_router_critical_e2e.py(16 个零重叠用例)上的结果逐位相同:
| top_k_records | 路由器 | 回答正确率 | 答成别的属性 | 平均选中记录 |
|---|---|---|---|---|
| 默认 | V2-128-v6 / REPLAY-128 | 37.50% / 37.50% | 25.00% / 25.00% | 5.88 / 5.88 |
| 1 | V2-128-v6 / REPLAY-128 | 12.50% / 12.50% | 43.75% / 43.75% | 0.94 / 0.94 |
| 3 | V2-128-v6 / REPLAY-128 | 31.25% / 31.25% | 25.00% / 31.25% | 2.44 / 2.44 |
路由器级 Top-1 提升了 41.20pp,端到端却一个用例都没变。因此做了一个判决性实验:保留
need_memory / hop_controller / head_gate 全部原权重,只把排序通路
(query_projection、key_projection、pair_scorer)替换为同形状随机权重
(checkpoints/router_v6_v2_128/ranking_scrambled_probe.pt,非有限值 0 个)。
结果在默认 Top-K 与 Top-K=1 下都完全不变(37.50% / 12.50%,选中记录数相同)。
机制(已逐项实测,含两次被自己数据推翻的中间假设):
memory_os_v2.py:1671— 带semantic_key(张量)的记录,其_score_candidates()(即MemoryRouterV2.projected_scores)分数会被self.record_scorer逐位置覆盖;实测残差与 text_retriever 匹配 18/18,与memory_router_v2匹配 0/18。qwen_integration.py:1359_score_semantic_memory_records— 该回调走self.text_retriever。注意:最初以为"部署目录没有text_retriever.pt所以走余弦兜底" 是错的 —— 重排器被烘焙进合并包(safetensors 内有 6 个dynamic_memory.text_retriever.*张量,1,573,377 参数),_text_retriever_ready实测为 True,余弦兜底分支根本不执行。- 路由器在此配置下唯一实际生效的杠杆是
need_memory门:部署权重下门开启率 68.75%(11/16),而三份随机初始化的路由器只有 37.5%(6/16)。判决性探针只随机化了 排序通路、保留了门控头,所以"端到端零变化"是由构造保证的,而不是巧合。 - 这 16 个用例里 93.75%(15/16)实际注入的是旧版 16 槽路径的证据
(
legacy_slot+text_memory_top_k=2),而不是 V2 库记录;地址命中 0/16, 直接词面命中 6/16。skip_semantic_expansion在这 6 例里直接跳过整个语义 hop 循环。 - 更严重的是:一次写入 20 条不同属性的事实后,只剩 12 条 active,8 条在查询前就已被 retract(16 个用例的 8 个里,目标记录本身就在被删的那批里)。任何排序器都救不回一条 已经不存在的记录,因此端到端正确率存在 50% 的硬上限。
结论分两层,不要混为一谈:
- 路由器记分卡上 22/22 的统治性和本次 +41.20pp 的零重叠提升,都是路由器工件真实的增益;
- 但在这套运行时里,记录级顺序由打包的
text_retriever决定,路由器分数被覆盖,且相当比例的 读取根本不走 V2 库、目标记录还会被写入路径删除。所以路由器增益不会传递到最终答案。
要让增益落地,需要的不是继续加大路由器,而是:
- 修写入路径:不要让 20 条不同属性的事实互相淘汰(当前最大瓶颈,直接决定 50% 上限);
- 让 V2 库记录真正参与读取:目前 93.75% 的用例走旧版 16 槽路径;
- 训练/替换
text_retriever(记录级重排器)—— 零重叠数据集可直接复用为它的训练/评测集; - 让路由器分数对
semantic_key记录也参与排序(改动memory_os_v2.py:1671的覆盖逻辑)。 (曾据此改过一版,实测在部署配置下不生效,已回退并留注释。)
6. 产物清单
| 文件 | 作用 |
|---|---|
make_zero_overlap_paraphrase_data.py |
生成零重叠数据集(属性由构造保证,划分字符串不相交) |
verify_zero_overlap_dataset.py |
独立复核数据集(不共享生成器代码) |
build_replay_corpus.py |
合并两个数据集与两个特征库,含逐字节抽查 |
report_zero_overlap_phase.py |
由 scorecard JSON 渲染对照表(zero_overlap_phase.md) |
data/zero_overlap/ |
train 1,200 / eval 300(含 manifest.json、zero_overlap_verification.json) |
data/router_replay_v7/ |
合并语料 train 88,355 / eval 22,220 |
H:\Memory\nm_cache\nm_replay_v7\feature_cache |
合并特征库 2,160,624 行 |
checkpoints/router_replay_v7_v2_128/ |
REPLAY-128 最终版权重(可 drop-in) |
compare_record_rankers.py |
在同一批冻结特征上对比"余弦 / 各路由器"的排序能力 |
measure_write_path_updates.py |
量写入路径的更新判定(证明重排器打分不是 retract 的原因) |
probe_record_selection.py |
逐记录拆解打分项、门控扫描、目标记录存活状态 |
migrate_bank_paths.py |
把特征库路径从 D: 迁移到 H:\Memory\nm_cache(含校验) |
zero_overlap_phase.md |
本阶段的逐轴对照表 |
特征库现存于 H:\Memory\nm_cache\(nm_router_v6 / nm_router_v5 / nm_zero_overlap /
nm_replay_v7 / nm_probe_bf16,共约 35.9 GB;D 盘已不再保存项目数据)。迁移后用
verify_feature_bank.py 全盘复扫(2,124,552 行、全零行 0、complete=true)并在新路径下
复算了排序器对比(59.60 / 30.40 逐位一致)。
历史运行记录(checkpoints/*/metrics.jsonl、*/training_stdout.log、
*/router_v5_training.json、feature_bank_verification.json)刻意保留原始 D: 路径,
因为那是当时真实的运行事实,改写它们等于篡改记录。
对照用的负面结果与判决性探针:checkpoints/router_zov_v2_128/(朴素微调)、
checkpoints/router_v6_v2_128/ranking_scrambled_probe.pt(随机排序探针)、
forget_check_zov.json、router_critical_e2e_scramble*.json。