- 引入 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,读写关闭时与原生模型逐位相同
136 lines
5.9 KiB
Python
136 lines
5.9 KiB
Python
"""How many written facts actually survive into the bank, and does the target survive?
|
|
|
|
End-to-end runs on realistic data reported ``records_active`` of 2-9 while the harness
|
|
writes up to 32 candidate facts per episode, and making the router fully responsible for
|
|
ranking moved answerable accuracy by one case. Both observations point at the same
|
|
suspicion: the binding constraint is not ranking, it is *what got stored*.
|
|
|
|
This probe measures it directly and cheaply -- it only writes, never generates:
|
|
|
|
* how many candidates were written vs how many records are active afterwards;
|
|
* whether the episode's *target* fact survived, which is a hard prerequisite for any
|
|
ranker to be able to answer;
|
|
* the dominant rejection reason, read from the runtime (write gate vs parse vs supersede).
|
|
|
|
Usage::
|
|
|
|
python -m V2_dpskw.diagnose_write_survival --eval-file data/realistic_v2/eval.jsonl --cases 40
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from collections import Counter, defaultdict
|
|
from pathlib import Path
|
|
|
|
import torch
|
|
|
|
from .eval_end_to_end_memory import write_fact
|
|
from .qwen_integration import infer_memory_metadata, load_memory_config, load_qwen_dynamic, load_tokenizer
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--package", default="qwen3_5_4b_natural_memory_v2_1")
|
|
parser.add_argument("--eval-file", default="data/realistic_v2/eval.jsonl")
|
|
parser.add_argument("--cases", type=int, default=40)
|
|
parser.add_argument("--output", default="write_survival.json")
|
|
args = parser.parse_args()
|
|
|
|
rows = []
|
|
with Path(args.eval_file).open("r", encoding="utf-8") as handle:
|
|
for line in handle:
|
|
line = line.strip()
|
|
if line:
|
|
rows.append(json.loads(line))
|
|
# One episode per category, so the report is not dominated by one shape.
|
|
by_category: dict[str, list[dict]] = defaultdict(list)
|
|
for row in rows:
|
|
by_category[row["metadata"]["category"]].append(row)
|
|
picked = []
|
|
per = max(1, args.cases // max(1, len(by_category)))
|
|
for category, items in sorted(by_category.items()):
|
|
picked.extend(items[:per])
|
|
|
|
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")
|
|
|
|
per_category: dict[str, dict] = defaultdict(lambda: {"episodes": 0, "written": 0, "active": 0,
|
|
"target_survived": 0,
|
|
"answerable": 0, "parsed": 0})
|
|
target_missing_examples = []
|
|
for row in picked:
|
|
candidates = [c["text"] for c in row["candidates"]]
|
|
positives = row.get("positive_indices") or []
|
|
acceptable = [str(v) for v in (row["metadata"].get("acceptable") or [])]
|
|
model.reset_memory(batch_size=1, device=device)
|
|
for text in candidates:
|
|
write_fact(model, tokenizer, text, device)
|
|
bank = model.memory_os_v2.bank
|
|
active = [r for r in bank.records.values() if r.status == "active"]
|
|
active_text = "\n".join(r.text or "" for r in active)
|
|
|
|
category = row["metadata"]["category"]
|
|
bucket = per_category[category]
|
|
bucket["episodes"] += 1
|
|
bucket["written"] += len(candidates)
|
|
bucket["active"] += len(active)
|
|
bucket["parsed"] += sum(1 for text in candidates
|
|
if infer_memory_metadata(text).get("attribute"))
|
|
if positives:
|
|
bucket["answerable"] += 1
|
|
survived = any(value and value in active_text for value in acceptable)
|
|
bucket["target_survived"] += int(survived)
|
|
if not survived and len(target_missing_examples) < 6:
|
|
target_missing_examples.append({
|
|
"category": category,
|
|
"query": row["query"],
|
|
"target_text": candidates[positives[0]][:90],
|
|
"active_records": len(active),
|
|
})
|
|
|
|
report = {
|
|
"package": str(model_path),
|
|
"eval_file": args.eval_file,
|
|
"episodes": len(picked),
|
|
"per_category": {},
|
|
"target_missing_examples": target_missing_examples,
|
|
}
|
|
for category, bucket in sorted(per_category.items()):
|
|
report["per_category"][category] = {
|
|
"episodes": bucket["episodes"],
|
|
"candidates_written": bucket["written"],
|
|
"active_after_write": bucket["active"],
|
|
"write_survival_pct": round(100 * bucket["active"] / max(1, bucket["written"]), 2),
|
|
"candidates_parsed_as_attribute_pct": round(
|
|
100 * bucket["parsed"] / max(1, bucket["written"]), 2),
|
|
"answerable_episodes": bucket["answerable"],
|
|
"target_survived": bucket["target_survived"],
|
|
"target_survival_pct": round(
|
|
100 * bucket["target_survived"] / max(1, bucket["answerable"]), 2),
|
|
}
|
|
total_written = sum(b["written"] for b in per_category.values())
|
|
total_active = sum(b["active"] for b in per_category.values())
|
|
total_answerable = sum(b["answerable"] for b in per_category.values())
|
|
total_survived = sum(b["target_survived"] for b in per_category.values())
|
|
report["overall"] = {
|
|
"candidates_written": total_written,
|
|
"active_after_write": total_active,
|
|
"write_survival_pct": round(100 * total_active / max(1, total_written), 2),
|
|
"answerable_episodes": total_answerable,
|
|
"target_survival_pct": round(100 * total_survived / max(1, total_answerable), 2),
|
|
}
|
|
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())
|