Files
natural-memory-nm21/tests/test_router_training.py
T
WpyQwq 643e22ecb9 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,读写关闭时与原生模型逐位相同
2026-09-19 11:11:31 +08:00

129 lines
5.7 KiB
Python

from __future__ import annotations
import json
import unittest
from pathlib import Path
import torch
from V2_dpskw.memory_os_v2 import MemoryRouterV2
from V2_dpskw.prepare_memory_router_dataset import Corpus, _add_generic_qa_row, _add_normalized_policy_file, _source_candidates
from V2_dpskw.train_memory_router_large import _episode_tensors, _router_loss
class RouterTrainingContractTests(unittest.TestCase):
def test_policy_builder_keeps_latest_fact_and_excludes_query_candidates(self) -> None:
corpus = Corpus()
rows = [
(1, {"id": "g:fact", "group_id": "g", "text": "我的项目是旧项目。", "kind": "fact", "write_label": 1.0, "attribute": "项目", "subject": "u"}),
(2, {"id": "g:replacement", "group_id": "g", "text": "更正:我的项目改为新项目。", "kind": "replacement", "write_label": 1.0, "attribute": "项目", "subject": "u"}),
(3, {"id": "g:query", "group_id": "g", "text": "我的项目是什么?", "kind": "query", "write_label": 0.0, "attribute": "项目", "subject": "u"}),
(4, {"id": "g:forget", "group_id": "g", "text": "忘掉我的项目。", "kind": "forget", "write_label": 0.0, "attribute": "项目", "subject": "u"}),
]
_add_normalized_policy_file(corpus, "train", "local.jsonl", rows)
self.assertEqual(len(corpus.episodes["train"]), 1)
episode = corpus.episodes["train"][0]
row = _source_candidates(
"train",
episode,
corpus.items["train"],
candidate_count=4,
seed=7,
eligible_items=[item for item in corpus.items["train"].values() if item.kind not in {"query", "forget"}],
)
self.assertIsNotNone(row)
assert row is not None
candidates = row["candidates"]
self.assertTrue(all(item["kind"] not in {"query", "forget"} for item in candidates))
positive = candidates[row["positive_index"]]
self.assertIn("新项目", positive["text"])
def test_episode_tensors_support_variable_candidates_and_unknowns(self) -> None:
episodes = [
{
"query": "q1",
"candidates": [{"text": "a"}, {"text": "b"}],
"positive_indices": [1],
"need_memory": 1.0,
"hop": 1,
"family": "qa",
},
{
"query": "q2",
"candidates": [{"text": "c"}],
"positive_indices": [],
"need_memory": 0.0,
"hop": 0,
"family": "qa",
},
]
from V2_dpskw.train_memory_router_large import _text_key
lookup = {_text_key(text): index for index, text in enumerate(("q1", "a", "b", "q2", "c"))}
data = _episode_tensors(episodes, lookup)
self.assertEqual(tuple(data["candidate_indices"].shape), (2, 2))
self.assertEqual(data["candidate_mask"].tolist(), [[True, True], [True, False]])
self.assertEqual(data["positive_mask"].tolist(), [[False, True], [False, False]])
def test_router_loss_is_finite_for_multi_positive_and_abstention_batch(self) -> None:
torch.manual_seed(4)
router = MemoryRouterV2(16, router_dim=8, num_heads=2, max_hops=3)
batch = {
"query": torch.randn(2, 16),
"candidates": torch.randn(2, 3, 16),
"candidate_mask": torch.tensor([[True, True, True], [True, True, False]]),
"positive_mask": torch.tensor([[True, True, False], [False, False, False]]),
"need": torch.tensor([1.0, 0.0]),
"hops": torch.tensor([2, 0]),
}
args = type(
"Args",
(),
{
"margin": 0.10,
"need_loss_weight": 0.75,
"hop_loss_weight": 0.35,
"margin_loss_weight": 0.25,
},
)()
loss, parts = _router_loss(router, batch, args)
self.assertTrue(torch.isfinite(loss).item())
self.assertTrue(all(torch.isfinite(torch.tensor(value)).item() for value in parts.values()))
def test_codesearchnet_style_documentation_becomes_query(self) -> None:
corpus = Corpus()
_add_generic_qa_row(
corpus,
"train",
"code_search_net|train|python|train",
"hf:code_search_net",
1,
{
"id": "code-1",
"func_documentation_string": "load the memory page by id",
"whole_func_string": "def load_page(page_id): return pages[page_id]",
"repository_name": "example/repo",
},
"code",
)
self.assertEqual(len(corpus.episodes["train"]), 1)
self.assertEqual(corpus.episodes["train"][0].query, "load the memory page by id")
self.assertEqual(len(corpus.episodes["train"][0].positive_ids), 1)
def test_generated_manifest_and_eval_hash_are_consistent(self) -> None:
root = Path(__file__).resolve().parents[1] / "data" / "router_training"
manifest_path = root / "manifest.json"
eval_path = root / "eval.jsonl"
hash_path = root / "eval.sha256"
if not (manifest_path.exists() and eval_path.exists() and hash_path.exists()):
self.skipTest("router_training data has not been generated")
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
actual = __import__("hashlib").sha256(eval_path.read_bytes()).hexdigest()
self.assertEqual(actual, manifest["files"]["eval"]["sha256"])
self.assertEqual(actual, hash_path.read_text(encoding="ascii").strip())
self.assertEqual(manifest["leakage_check"]["group_overlap"], 0)
if __name__ == "__main__":
unittest.main()