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
+85
View File
@@ -0,0 +1,85 @@
"""Diagnose why the coverage gate stood down (self-gating bypass) on a given bank.
Writes the same facts the runtime evaluator writes, then prints the bank's active conflict
keys, the head vocabulary, and the gate's own applicable/bypassed counters. This exists
because the gate silently degrading to "no opinion" looks exactly like "the gate does not
work" in end-to-end numbers.
Usage::
python -m V2_dpskw.diagnose_coverage_gate --package qwen3_5_4b_natural_memory_v2_1
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import torch
from .eval_end_to_end_memory import write_fact
from .qwen_integration import load_memory_config, load_qwen_dynamic, load_tokenizer
FACTS = [
("常住城市", "CITY-A1B2C3"), ("出生城市", "CITY-D4E5F6"), ("办公城市", "CITY-G7H8J9"),
("档案标识", "CODE-K1L2M3"), ("常用编辑器", "EDIT-N4P5Q6"), ("默认语言", "LANG-R7S8T9"),
("通勤方式", "COMMU-U1V2W3"), ("主管姓名", "BOSS-X4Y5Z6"), ("工位楼层", "FLOOR-A7B8C9"),
("团队名称", "TEAM-D1E2F3"),
]
QUERY = "我平时待得最久的地方是哪里?"
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--package", default="qwen3_5_4b_natural_memory_v2_1")
parser.add_argument("--output", default="coverage_gate_diagnosis.json")
args = parser.parse_args()
model_path = Path(args.package)
config = load_memory_config(model_path)
model = load_qwen_dynamic(model_path, memory_config=config, load_in_4bit=True,
max_memory={0: "10.5GiB", "cpu": "48GiB"})
model.eval()
tokenizer = load_tokenizer(model_path)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.reset_memory(batch_size=1, device=device)
for attribute, value in FACTS:
write_fact(model, tokenizer, "我的%s是 %s。" % (attribute, value), device)
active = dict(model.memory_os_v2.bank.active_by_conflict)
present = sorted({key.split("::", 1)[1] for key in active if "::" in key})
vocabulary = sorted((model._attribute_head_meta or {}).get("vocabulary", []))
stats = (model._attribute_head_meta or {}).get("coverage_stats", {})
from .eval_router_critical_e2e import answer
reply = answer(model, tokenizer, QUERY, device, 24)
decision = model.runtime.v2_last_decisions[-1] if model.runtime.v2_last_decisions else {}
report = {
"package": str(model_path),
"gate_enabled": bool(config.memory_coverage_gate),
"blend": float(config.memory_record_router_blend),
"facts_written": len(FACTS),
"bank_records": len(model.memory_os_v2.bank.records),
"active_records": sum(1 for r in model.memory_os_v2.bank.records.values()
if r.status == "active"),
"active_conflict_keys": len(active),
"bank_attributes": present,
"head_vocabulary_size": len(vocabulary),
"attributes_outside_head_vocabulary": sorted(set(present) - set(vocabulary)),
"attributes_missing_from_bank": sorted(set(vocabulary) - set(present)),
"gate_counters": stats,
"query": QUERY,
"stop_reason": str(decision.get("stop_reason")),
"reply": reply[:120],
}
Path(args.output).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())