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
+231
View File
@@ -0,0 +1,231 @@
"""Evaluate the production-style local dense+rereank baseline on dirty data.
This is the companion baseline for ``benchmark_dirty_real_corpus_4b``. It
uses the same real repository corpus, the same user-dialogue slice, the same
queries, tokenizer, greedy decoding and GPU cap. Retrieval is CPU-only:
local BERT embeddings provide recall and a bounded transparent reranker uses
lexical/entity/attribute signals. It is stronger than lexical Chunk RAG,
but is explicitly not presented as a trained public cross-encoder.
"""
from __future__ import annotations
import argparse
import gc
import json
import time
from collections import Counter
from pathlib import Path
from statistics import mean
from typing import Any
import torch
from .benchmark_compare_baselines_4b import (
_base_generate,
_rag_prompt,
_vram_snapshot,
)
from .benchmark_dirty_real_corpus_4b import (
_build_all_records,
_dirty_source_files,
_is_refusal_strict,
_path,
_quality_pass,
_retrieval_hit,
_summarize,
)
from .benchmark_real_scale_memory_4b import _max_memory, _set_cuda_process_cap
from .strong_rag_baseline import LocalEmbeddingReranker, _resolve_local_encoder
from .qwen_integration import load_qwen_base, load_tokenizer
PROJECT_ROOT = Path(__file__).resolve().parent
def _run_dirty(
model: Any,
tokenizer: Any,
rows: list[dict[str, Any]],
index: LocalEmbeddingReranker,
device: torch.device,
*,
top_k: int,
max_new_tokens: int,
) -> dict[str, Any]:
output_rows: list[dict[str, Any]] = []
for row_index, row in enumerate(rows, 1):
if row_index == 1 or row_index % 32 == 0 or row_index == len(rows):
print(f"Strong RAG generation: {row_index}/{len(rows)}", flush=True)
query = str(row["query"])
retrieved, retrieve_ms = index.retrieve(query, top_k=top_k)
prompt = _rag_prompt(query, retrieved)
generated = _base_generate(
model,
tokenizer,
prompt,
device,
max_new_tokens=max_new_tokens,
)
selected = [
{
"value": item.get("value", ""),
"text_preview": item.get("text", ""),
"entity": item.get("entity", ""),
"attribute": item.get("attribute", ""),
}
for item in retrieved
]
response = str(generated.get("response", ""))
output_rows.append(
{
**row,
"response": response,
"correct": _quality_pass(response, row),
"retrieval_target_found": _retrieval_hit(selected, row),
"retrieved_values": [item.get("value", "") for item in retrieved],
"retrieved_ids": [],
"selected_records": selected,
"prefix_used": bool(retrieved),
"prefix_tokens": int(generated.get("prompt_tokens", 0)),
"reader_ms": float(retrieve_ms),
"retriever_ms": float(retrieve_ms),
"prompt_tokens": int(generated.get("prompt_tokens", 0)),
"generated_tokens": int(generated.get("generated_tokens", 0)),
"total_latency_s": float(generated.get("total_latency_s", 0.0)),
"decode_tok_s": float(generated.get("decode_tok_s", 0.0)),
"allocated_gb": generated.get("allocated_gb"),
"reserved_gb": generated.get("reserved_gb"),
}
)
return _summarize(output_rows, include_reader=False)
def _release(model: Any) -> None:
del model
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
def _compact(summary: dict[str, Any]) -> dict[str, Any]:
return {key: value for key, value in summary.items() if key != "rows"}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-model", default=r"H:\Memory")
parser.add_argument("--output", default=str(PROJECT_ROOT / "dirty_strong_rag_compare_4b.json"))
parser.add_argument("--project-records", type=int, default=8192)
parser.add_argument("--project-targets", type=int, default=96)
parser.add_argument("--top-k", type=int, default=4)
parser.add_argument("--candidate-k", type=int, default=64)
parser.add_argument("--embedding-batch-size", type=int, default=16)
parser.add_argument("--embedding-max-length", type=int, default=384)
parser.add_argument("--embedding-model", default=None)
parser.add_argument("--max-new-tokens", type=int, default=64)
parser.add_argument("--gpu-memory-gb", type=float, default=10.0)
parser.add_argument("--no-4bit", action="store_true")
args = parser.parse_args()
_set_cuda_process_cap(args.gpu_memory_gb)
tokenizer = load_tokenizer(_path(args.base_model))
files = _dirty_source_files()
user_records, user_rows, code_records, code_rows, corpus_meta = _build_all_records(
files,
project_records=max(128, int(args.project_records)),
project_targets=max(1, int(args.project_targets)),
)
all_records = user_records + code_records
all_rows = user_rows + code_rows
print(
f"dirty corpus: files={len(files)} real_chunks={corpus_meta['stored_real_chunk_records']} "
f"semantic_targets={corpus_meta['semantic_target_records']} user_records={len(user_records)} "
f"queries={len(all_rows)}"
)
encoder_path = _resolve_local_encoder(args.embedding_model)
print(f"loading local embedding model on CPU: {encoder_path}")
embedder = LocalEmbeddingReranker(
encoder_path,
max_length=args.embedding_max_length,
batch_size=args.embedding_batch_size,
candidate_k=args.candidate_k,
)
started = time.perf_counter()
embedder.add(all_records)
index_seconds = time.perf_counter() - started
print(f"dense_index_seconds={index_seconds:.3f}")
print("loading original Qwen3.5 4B for strong-RAG generation")
base = load_qwen_base(
_path(args.base_model),
load_in_4bit=not args.no_4bit,
max_memory=_max_memory(args.gpu_memory_gb),
)
base.eval()
device = base.get_input_embeddings().weight.device
load_vram = _vram_snapshot(device)
result = _run_dirty(
base,
tokenizer,
all_rows,
embedder,
device,
top_k=max(1, args.top_k),
max_new_tokens=max(1, args.max_new_tokens),
)
_release(base)
report = {
"benchmark": "dirty_strong_rag_compare_4b",
"created_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"base_model": str(_path(args.base_model)),
"embedding_model": str(encoder_path),
"quantization": "4bit_nf4" if not args.no_4bit else "none",
"gpu_memory_cap_gb": float(args.gpu_memory_gb),
"corpus": corpus_meta,
"query_counts": {
"all": len(all_rows),
"user_dialogue": len(user_rows),
"real_repository": len(code_rows),
"answerable": sum(int(row["answerable"]) for row in all_rows),
"unknown": sum(int(not row["answerable"]) for row in all_rows),
"by_category": dict(Counter(str(row["category"]) for row in all_rows)),
},
"protocol": {
"same_tokenizer": True,
"same_sampling": "greedy",
"top_k": int(args.top_k),
"candidate_k": int(args.candidate_k),
"embedding_device": "cpu",
"embedding_max_length": int(args.embedding_max_length),
"embedding_batch_size": int(args.embedding_batch_size),
"max_new_tokens": int(args.max_new_tokens),
"baseline_note": "local BERT dense retrieval plus fixed transparent reranker; not a trained public cross-encoder",
},
"index_build": {
"records": len(all_records),
"indexed_records": len(embedder.records),
"seconds": index_seconds,
},
"system": {
"load_vram": load_vram,
"all": result,
},
"limitations": [
"The embedding encoder is local bert-base-chinese and CPU-only.",
"The reranker is a fixed transparent feature reranker, not a trained public cross-encoder.",
"The same strict answer-anchor evaluator is used for Natural Memory comparison.",
"Embedding/index build time is reported separately from per-query retrieval and generation.",
],
}
output = _path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps({"strong_rag": _compact(result), "index_build": report["index_build"]}, ensure_ascii=False, indent=2))
print(f"saved={output}")
if __name__ == "__main__":
main()