Natural Memory NM2.1: 记忆路由器分叉、数据集缺陷修复与全轴评测证据

- 引入 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,读写关闭时与原生模型逐位相同
This commit is contained in:
WpyQwq
2026-09-19 11:11:31 +08:00
commit 643e22ecb9
484 changed files with 306821 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
"""Would a margin gate fix the fabrications the model commits, or only echo it?
The model already refuses 13 of the 25 unknown-attribute questions in its own
words. A gate is only worth wiring into the runtime if it catches the *other* 12
-- the ones where the model fabricates a value -- without paying many extra false
refusals on answerable questions.
So this reports, for the stored run's unknown-attribute cases:
* whether the reply was a refusal (model handled it) or a fabrication (gate needed)
* the offline margin for that episode
* the same for answerable episodes, as the false-refusal denominator
and then sweeps the threshold to show refusals_added vs false_refusals_added.
"""
from __future__ import annotations
import argparse
import sys
from collections import defaultdict
from pathlib import Path
import torch
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from V2_dpskw.eval_scoring import is_refusal
from V2_dpskw.fit_answerability_threshold import load_episodes, margin_of
from V2_dpskw.make_answerability_data import FeatureBank, write_set
from V2_dpskw.rescore_e2e import load_run
def load_episodes_sorted(path: Path, per_category: int = 25):
"""Corpus rows in the order the harness ran them: sorted category, then file order.
Pairing a stored run against a raw-file-order corpus silently misaligns every
row -- repeated queries inside a category hide it -- so the ordering used by
``eval_end_to_end_memory.build_cases`` is reproduced here explicitly.
"""
grouped: dict[str, list[dict]] = defaultdict(list)
for episode in load_episodes(path):
grouped[str((episode.get("metadata") or {}).get("category", ""))].append(episode)
out = []
for category in sorted(grouped):
out.extend(grouped[category][:per_category])
return out
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--bank", type=Path, default=Path(r"H:\Memory\nm_cache\nm_realistic_v2\feature_cache"))
parser.add_argument("--corpus", type=Path, default=Path("data/realistic_v2/eval.jsonl"))
parser.add_argument("--run", type=Path, required=True)
args = parser.parse_args()
bank = FeatureBank(args.bank)
rows = load_run(args.run)
unknown = [] # (margin, model_refused, query, reply)
answerable = [] # (margin, model_answered_correctly)
for episode, row in zip(load_episodes_sorted(args.corpus), rows):
if str(episode.get("query", "")) != str(row.get("query", "")):
raise SystemExit(f"row/corpus mismatch: {episode.get('query')!r} vs {row.get('query')!r}")
metadata = episode.get("metadata") or {}
candidates = [str(c.get("text", "")) for c in (episode.get("candidates") or []) if isinstance(c, dict)]
positives = [candidates[i] for i in (episode.get("positive_indices") or []) if 0 <= int(i) < len(candidates)]
records = write_set(episode, include_answer=True)
m = margin_of(bank, str(episode.get("query", "")), records)
if m is None:
continue
reply = row.get("reply", "")
if positives:
answerable.append((m, bool(row.get("correct"))))
else:
unknown.append((m, is_refusal(reply), str(episode.get("query", "")), reply))
refused_by_model = [u for u in unknown if u[1]]
fabricated = [u for u in unknown if not u[1]]
print(f"unknown-attribute cases with a usable margin: {len(unknown)}")
print(f" model already refused : {len(refused_by_model)} margins "
f"{sorted(round(u[0], 3) for u in refused_by_model)}")
print(f" model FABRICATED : {len(fabricated)} margins "
f"{sorted(round(u[0], 3) for u in fabricated)}")
print("\nfabricated cases:")
for m, _r, q, reply in fabricated:
print(f" margin={m:+.3f} Q={q} R={reply[:60]}")
print(f"\n{'thr':>7}{'caught_fabrications':>21}{'reflagged_refusals':>21}{'new_false_refusals':>21}")
ans_margins = [m for m, _ok in answerable]
for step in range(0, 80, 4):
t = step / 1000.0
caught = sum(1 for m, _r, _q, _x in fabricated if m < t)
reflagged = sum(1 for m, _r, _q, _x in refused_by_model if m < t)
new_false = sum(1 for m in ans_margins if m < t)
print(f"{t:>7.3f}{caught:>16}/{len(fabricated):<4}{reflagged:>16}/{len(refused_by_model):<4}"
f"{new_false:>16}/{len(ans_margins):<4}")
return 0
if __name__ == "__main__":
raise SystemExit(main())