Files
natural-memory-nm21/README_FORK.md
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

21 KiB
Raw Blame History

V2_dpskw —— Natural Memory 路由器分叉

本目录是 H:\Memory\dynamic_memory_lab(GPT 时代的原工程)的代码分叉,用于训练一个全新的、更大更强的路由器。 原工程保持只读不改;本 fork 内所有代码的包名已统一为 V2_dpskw。

与原工程的关系

项目 处理方式
全部源码 / 测试 / 文档 复制进本目录,包名 dynamic_memory_lab → V2_dpskw(51 个文件)
Qwen3.5-4B 权重包 目录联接(junction)qwen3_5_4b_natural_memory_v2 → 原工程,不复制 9.3 GB
冻结的 router 训练数据 复制 data/router_training_v3/(train 7155 / eval 1940,sha256 与原版一致)
冻结的 Qwen 特征库 复制 checkpoints/router_shared/feature_cache/(21263 × 2560 fp16),manifest 的 model_path 已改写为 fork 内路径
其余 data / checkpoints / 适配器 / 大报告 不复制,需要时按绝对路径引用原工程

因此训练时不会加载 Qwen、也不会重新编码特征,30 秒内即可开始更新路由器参数。

新增内容

文件 作用
router_xl.py 新的路由器架构 MemoryRouterXL:多层 MLP 编码器、[q,k,q−k,q·k] 交互特征 + LayerNorm、残差 pair trunk、多层策略头;保持与 MemoryRouterV2 完全相同的运行时契约
train_memory_router_xl.py v3 数据上的训练器(复用原训练器的度量代码,只替换模型)
train_router_v5.py v5/v6 全量数据训练器:流式读取 JSONL(不把 episode 解析进内存)、mmap 10.86 GB 特征库、支持 --arch v2|xl、三种采样模式(uniform/source_balanced/family_sqrt)
stream_feature_bank.py 流式 + 多线程特征编码器:scan(唯一文本落盘)/ tokenize(线程池)/ encode(按精确 token 长度分组、token 预算限批)→ 写 mmap .npy
eval_router_scorecard.py v3 版多轴评分卡
eval_router_v5.py v5/v6 版多轴评分卡:流式 + mmap + 按 family / 按 10 个类别拆解
audit_router_dataset.py 独立数据审计:类别完整性(可回答/未知/正例数/hop)+ 严格泄漏检查(group_id、查询、文本、以及「同查询且共享正例证据」)
check_router_cache.py 冻结特征缓存快速校验(失配即报错,不会偷偷加载 4B 模型重编码)
compare_router_runs.py 与原 512 基线的汇总对比
tests/test_router_xl.py 新架构契约测试
run_router_v6.ps1 / chain_v6.ps1 v6 训练启动器 / 编码完成自动接训练与评分的长链

prepare_memory_router_dataset.py 额外修复了一个性能缺陷(见下)。

已验证事实

  • 全部单元测试通过:52 项(原 45 项 + 新增 7 项)。
  • 特征缓存命中,训练全程不加载 Qwen。
  • 新架构与原架构同尺寸对比:router_dim 相同意味着每条记忆记录的地址占用完全相同(512 维 × fp32 = 2048 字节)。

MemoryRouterXL 容量

路由器 router_dim heads 参数 相对 V2-512
V2-512 基线(原工程) 512 8 4,741,902 1.00×
XL-512 512 8 7,898,127 1.67×
XL-1024 1024 16 18,414,103 3.88×
XL-2048 2048 16 55,133,719 11.63×

只有 XL-512 的地址几何与原版一致(同样的存储成本),因此它是唯一可以做同尺寸对比的配置。

XL-512 训练结果(100,000 步,冻结 v3 eval:1940 条)

指标 V2-512 best(step1k) V2-512 final(step100k) XL-512 best XL-512 final(step100k)
参数 4,741,902 4,741,902 7,898,127 7,898,127
每条记录地址字节 2048 2048 2048 2048
Top-1 正确率 67.12% 60.63% 60.85% 54.36%
Recall@3 88.77% 85.01% 92.97% 85.33%
Recall@5 94.93% 90.13% 97.33% 91.93%
MRR 79.08% 73.26% 75.98% 69.50%
nDCG@3 80.00% 74.57% 79.28% 71.71%
多跳证据全中(Top-3) 88.66% 84.90% 92.86% 85.22%
hop 正确率 97.84% 99.69% 99.38% 99.69%
need F1 98.92% 99.89% 99.62% 99.89%
未知拒答率 (thr 0.50) 66.04% 100.00% 90.57% 100.00%
已知问题被误拒率 0.22% 0.22% 0.22% 0.22%
未知问题被误读率 33.96% 0.00% 9.43% 0.00%
平均分数余量 0.588 3.884 6.710 25.376
单查询延迟 ms (GPU) 0.895 0.935 1.310 1.246
路由 QPS (GPU) 1117 1070 763 803

结论不是「谁全面更强」,而是各轴各有胜负:

  1. 原工程用 selection_score = 0.5·top1 + 0.3·need_f1 + 0.2·mrr 选出的 "best"(step 1000)Top-1 最高,但在 0.50 门槛下有 33.96% 的未知问题被强行读记忆;该综合分几乎不惩罚这一点,因为未知样本只有 106 条。
  2. XL-512 在覆盖率上明显更好:Recall@3 +4.20pp、Recall@5 +2.40pp、多跳证据全中 +3.96pp。
  3. 把门槛从 0.50 提到 0.70,XL-512 best 的未知拒答率达到 100.00%,而已知问题被误拒率仍为 0.22%——即在同等策略安全水平下,XL-512 的排序覆盖优势可以保留。
  4. 代价:XL-512 参数 1.67×、单查询延迟 +39~46%(0.935 → 1.310 ms)、QPS 从 1070 降到 763。

数据集构建:一个 46.8 小时的缺陷

prepare_memory_router_dataset.py 的 _source_candidates 原先对每条 episode 都重建候选池:

same_family = [item for item in items_by_family.get(episode.family, []) if item.item_id not in positive_set]

引入 mega 验证集后,mega_validation 家族的候选池有 2,656,000 条,于是一次列表复制约 2.1 秒:

家族 原版 / 条 修复后 / 条 加速
mega_validation 2105.42 ms 0.59 ms 3568×
memory_policy 16.61 ms 2.31 ms 7.2×
native_memory 7.51 ms 0.68 ms 11×

80,000 条 mega episode 原本需要 46.8 小时(H:\Memory\dynamic_memory_lab\data\router_training_v4 那次运行了 19.5 小时只写出 91 MB 残缺文件,原因即此)。

修复方式(_ExcludedPool + PoolPositionIndex):不复制候选池,而是提供一个「排除了少量已知位置」的惰性序列视图,len() 与 __getitem__ 与原列表逐元素一致,因此 RNG 抽样与最终数据集逐字节相同。

等价性已验证(A/B,同样输入、同样 seed):

ORIGINAL  train_sha=D9C2774955DF7066  eval_sha=169B026DF2A85C35   93.5s
V2_dpskw  train_sha=D9C2774955DF7066  eval_sha=169B026DF2A85C35   42.1s

全量数据集 data/router_training_v5(144 秒建成,原需约 47 小时):

文件 episodes 类别
train.jsonl (1148 MB) 87,155 benchmark 128 / native 512 / policy 6,515 / mega 80,000
eval.jsonl (288 MB) 21,920 含 mega 20,000,覆盖全部 10 个类别(每类 2,000)

group_overlap = 0,冲突策略与防泄漏检查与原协议一致。

当前阻塞:特征库放不进内存

训练复用 Qwen 冻结特征,所以每条唯一文本都要编码一次:

文件 唯一文本 特征库 (fp16)
v5 train 1,695,797 8.68 GB
v5 eval 425,115 2.18 GB
合计 2,120,912 10.86 GB

本机内存 31.2 GB、空闲 12.7 GB,还要容纳解析后的 episode 对象,因此全量 87k 无法在本机编码。 可选方案:按类别均衡抽样(约 25–32k episode,特征库 ~5 GB)、先压缩 mega 源、或换更大内存的机器。

实际落地方式(已完成):全量 2,124,552 条唯一文本、10.86 GB 特征库写成 mmap 并用完整扫描验证,训练器流式读取,因此「内存放不下」不再是阻塞项。

v6 结果:准确率与拒答策略大幅超越,成本轴未超越

训练:V2-512 对照与 XL-512 候选均 100,000 步、batch 64、family_sqrt 采样、同一冻结特征库与协议。 评测:冻结 v6 eval 21,920 条(10 类别 × 2,000 + policy 1,920),7 个路由器同题同特征。

指标 生产包 128 维(v3) V2-512 v3 best V2-512 v6 best XL-512 v6 best
Top-1 正确率 41.12% 80.75% 95.05% 95.78%
Recall@3 46.73% 95.02% 98.84% 97.59%
Recall@5 48.91% 97.31% 99.35% 98.34%
MRR 47.69% 88.46% 97.12% 97.24%
nDCG@3 44.22% 89.53% 97.24% 96.64%
多跳证据全中(Top-3) 43.43% 94.19% 98.64% 96.86%
hop 正确率 73.23% 81.59% 100.00% 100.00%
未知拒答率 0.00% 50.41% 99.81% 100.00%
已知问题被误拒率 0.18% 11.23% 0.00% 0.00%
单查询延迟 (GPU) 0.844 ms 0.879 ms 0.875 ms 1.216 ms
批量 QPS (batch=256) 264,813 167,235 161,941 96,747
地址字节/记录 512 2048 2048 2048

判定(router_verdict_v6.md,逐轴对比最强旧基线):66 项通过 / 22 项未通过 → 全方位超越:否。 22 项未通过全部集中在成本轴(单查询延迟、单查询/批量 QPS、每条记录地址字节)、2 项平均分数余量、以及 XL-final 的多跳证据全中(78.85% vs 84.90%)。所有准确率与拒答策略轴全部通过,且多数是大幅通过:Top-1 +15.03pp、MRR +8.79pp、hop +18.25pp、未知拒答率 +48.71pp、已知被误拒率 −0.11pp。

同数据架构对照:XL 相对 V2 并没有赢

在完全相同的数据、特征、采样与步数下(best 检查点):

指标 V2-512 v6 XL-512 v6 XL−V2
Top-1 95.05% 95.78% +0.73pp
MRR 97.12% 97.24% +0.12pp
未知拒答率 99.81% 100.00% +0.19pp
Recall@3 98.84% 97.59% −1.25pp
Recall@5 99.35% 98.34% −1.01pp
nDCG@3 97.24% 96.64% −0.60pp
多跳证据全中 98.64% 96.86% −1.78pp
单查询延迟 0.875 ms 1.216 ms +39% 更慢

结论:本次最大的提升来自数据与标签(v3 → v6:修正被破坏的拒答/多跳标签、12 倍数据量、10 类别均衡评测),而不是新架构。XL 在 512 维上只换来 Top-1 +0.73pp、MRR +0.12pp,代价是 1.4 倍延迟与 1.67 倍参数,并在召回与多跳证据完整度上略逊于同数据的 V2。若目标是「同尺寸全面超越」,应继续投入数据与训练配方,而不是加大路由器容量。

同存储预算的判定:128 维 V2 达到全方位超越

把 512 维路由器与 128 维的生产路由器比「每条记录地址字节」是不同存储预算的错位比较。因此在生产几何(128 维 = 512 字节/记录、2,037,774 参数)上重训后重新判定:

路由器 参数 地址字节 Top-1 未知拒答率 单查询延迟(中位) batch256 QPS 逐候选判定
V2-128 生产(v3) 2,037,774 512 41.12% 0.00% 1.1558 ms 199,554 基线
V2-128 v6 best 2,037,774 512 94.62% 100.00% 1.1679 ms 196,829 22/22 通过 → 是
V2-128 v6 final 2,037,774 512 94.37% 100.00% 1.1653 ms 198,026 22/22 通过 → 是
XL-128 v6 best 6,714,639 512 95.66% 99.59% 1.5783 ms 141,501 18/22(吞吐 4 轴)
V2-512 v6 best 4,741,902 2048 95.05% 99.81% 1.1623 ms 154,085 20/22(batch256、地址字节)
XL-512 v6 best 7,898,127 2048 95.78% 100.00% 1.5855 ms 94,647 17/22

速度轴以交替轮流测量(bench_router_latency.py,5 轮,模型轮转以抵消漂移)为准,实测波动带 2.8–5.3%,故速度轴采用 3% 相对容差;质量轴仍为严格容差。此前「V2-128 v6 慢 4.6%」是单次顺序测量的漂移,交替测量下两者差异 ≤1.4%(单查询 min 甚至更快 0.5%)。

结论:V2-128 v6 在参数、地址字节、延迟、吞吐全部不劣(差异在测量噪声带内)的前提下,把 Top-1 从 41.12% 提到 94.62%(+53.49pp)、未知拒答率从 0.00% 提到 100.00%、hop 从 73.23% 提到 100.00% —— 这是相对已交付生产路由器的同预算全方位超越。

但不是新架构的胜利:XL-128/XL-512 准确率最高(Top-1 95.66%/95.78%)却真实地慢 26–52%(3.3–3.9 倍参数)。本次提升来自数据与标签,而非路由器容量。

常用命令

$py = 'C:\Users\Administrator\miniconda3\envs\LLM\python.exe'
$env:PYTHONPATH = 'H:\Memory'
Set-Location 'H:\Memory\V2_dpskw'

# 单元测试
& $py -m unittest discover -s tests

# 特征缓存校验(v3 数据)
& $py -m V2_dpskw.check_router_cache

# 构建数据集(已修复拒答/多跳标签;全量约 2 分钟)
& $py -m V2_dpskw.prepare_memory_router_dataset --output-dir data/router_training_v6 `
  --train-source <abs>\benchmark_train.jsonl ... --candidate-count 32 --conflict-aware --seed 20260909

# 独立审计:类别完整性 + 严格泄漏
& $py -m V2_dpskw.audit_router_dataset `
  --train-file data/router_training_v6/train.jsonl --eval-file data/router_training_v6/eval.jsonl `
  --output router_dataset_audit_v6.json

# 流式多线程特征编码(212 万条唯一文本 → mmap 特征库,放 NVMe)
& $py -m V2_dpskw.stream_feature_bank `
  --train-file data/router_training_v6/train.jsonl --eval-file data/router_training_v6/eval.jsonl `
  --model-path qwen3_5_4b_natural_memory_v2 --output-dir H:\Memory\nm_cache\nm_router_v6\feature_cache `
  --tokenizer-threads 8 --max-batch 192 --token-budget 12288 --gpu-memory-gb 10

# 训练(V2 对照 + XL 候选,同数据同协议)
pwsh -File .\run_router_v6.ps1 -Configs v2_512_v6,xl512_v6

# 全类别百分比评分卡
& $py -m V2_dpskw.eval_router_v5 --feature-cache H:\Memory\nm_cache\nm_router_v6\feature_cache `
  --train-file data/router_training_v6/train.jsonl --eval-file data/router_training_v6/eval.jsonl `
  --run "XL-512 v6=checkpoints\router_v6_xl512\router_best.pt" `
  --output router_scorecard_v6.json --markdown router_scorecard_v6.md

数据管线(流式 + 多线程)实测

阶段 结果
scan:去重并落盘唯一文本 2,124,552 条 / 15.1 秒(只保留 sha1→行号 字典,不把 episode 解析进内存)
tokenize:8 线程池(HF fast tokenizer 释放 GIL) 2,124,552 条 / 41.8 秒(约 57,000 texts/s)
encode:按精确 token 长度分组 + token 预算限批 约 283 texts/s @32 token(batch 192),GPU 100%
特征库 features.f16.npy mmap,10.86 GB,训练时按需读页
断点续跑 --resume + progress.json:记录已完成的长度排序行数,被中断后不重编已完成部分

表示保真度:与旧冻结缓存(v3)共有文本对比,编码结果 余弦最低 0.999935 / 平均 0.999983。因此旧路由器可以直接在新特征库上评分,而不是被换到一套不同表示上再比较。

一个被测量推翻的「显然优化」

QwenDynamicMemoryModel._encode_model_key 调用完整条件生成模型,而 logits_to_keep 默认 0(= 全部 token),于是每次前向都算出 [B, L, 248320] 的 logits:对 192×32 的批是 3.05 GB 中间张量 + 约 8.5 TFLOP,全部丢弃。看起来显然该改成 logits_to_keep=1。实测(交替重复 7 轮取最小值;两条路径特征一致性 cosine 0.9999996):

编码路径 192×32 批耗时 吞吐 显存峰值
全量 logits(默认,保留) 679 ms 283 texts/s 5.22 GiB
logits_to_keep=1 1050 ms 183 texts/s 4.13 GiB
batch 512(全量 logits) — 287 texts/s —

原因:slice(-1, None) 产生非连续视图,bitsandbytes 的 4-bit matmul 在这个布局上掉进慢路径(多出约 500 ms),而全量 logits 是规整的连续 GEMM,本身只约 130 ms。结论:不要启用 --skip-lm-head(默认已关,代码与结论一并保留以便复核)。batch 从 192 提到 512 也无收益,说明该阶段已接近这台 GPU 的算力上限(约 52 TFLOPS,约为 5070 bf16 峰值的 84%)。

v5 → v6:修正被破坏的拒答与多跳标签

_add_mega_row 有两个由 GPT 时代沿用下来的缺陷,正好废掉了本次评测最需要的两个轴:

  1. unknown_abstention / forget_correction 的源数据带 metadata.answerable = false,其 acceptable 装的是拒答话术("不知道"/"没有记录")。旧代码用 answerable = bool(acceptable) 判成可回答,又没有事实文本包含这些话术,于是走进兜底分支 positive_ids = ids[:],把全部事实标成正例——2000 条拒答样本全部变成可回答样本。
  2. multi_hop 的真实跳数在 metadata.hop_count = 2,旧代码用 len(positive_ids) 计算,且只标注了含最终答案的那条事实,中间推理链事实没有标成证据。

修正:尊重 metadata.answerable;用 metadata.hop_count 作为 hop 标签;沿「实体→值」链扩展证据(有界,最多 3 跳)。修正前后对比:

指标 v5(有 bug) v6(已修正)
train 未知样本 319 16,319
eval 未知样本 106 4,106
unknown_abstention 可回答/未知 2000 / 0 0 / 2000
forget_correction 可回答/未知 2000 / 0 0 / 2000
multi_hop 平均正例数 / hop 1.00 / 1 2.00 / 2

数据集重建耗时 114.9 秒(未修复前同一构建需要约 47 小时)。

v6 独立审计结果

检查 结果
group_id 重叠 0
查询文本重叠 80 / 4,296 = 1.86%
同查询且共享同一正例证据 0(80 条均为模板化问句配不同事实,无泄漏)
候选文本重叠 3 / 421,473 = 0.0007%
类别 episodes 可回答 未知 平均正例 hop
unknown_abstention 2000 0 2000 0.00 0
forget_correction 2000 0 2000 0.00 0
multi_hop 2000 2000 0 2.00 2
其余 8 类 各 2000 2000 0 1.00–2.00 1–2

NM2 跨架构泛化(实测矩阵)

原集成的记忆手术只适配 Qwen3.5 一族的解码层。probe_nm2_portability.py 对 14 个架构真机施加手术, 并做最强保能力检验:记忆读写关闭时,包装后的模型必须与原生模型的 logits 逐位相同。

修复前:只有 position_embeddings 在第 2 位置的架构(gemma2 / gemma3 / mixtral)能通过,其余全部 TypeError: ... got multiple values for argument 'attention_mask' 或找不到层容器。

修复后(零补丁、全部逐位相同):

架构 层容器 第 2 位置参数 恒等性
llama / mistral / qwen2 / qwen3 / starcoder2 / cohere / phi model.layers attention_mask identical
olmoe / granitemoe model.layers attention_mask identical
gemma2 / gemma3 / mixtral model.layers position_embeddings identical
opt / bart model.decoder.layers — identical

为达到这一点只改了 4 处,记忆核心与路由器一行未改(它们只依赖 hidden_size):

改动 解决的假设
resolve_text_config() 纯文本模型没有 config.text_config
resolve_decoder_layers() 层容器固定为 model.language_model.layers(现支持 8 条候选路径)
MemoryLayerAdapter 约定检测 + 按声明参数过滤 position_embeddings 被按位置硬传;参数名称也不同(Ling 用 past_key_value 单数,且未必有 **kwargs)
load_qwen_base() 兜底 AutoModelForCausalLM 只有 image-text-to-text 加载器

对 Qwen3.5 生产路径零行为变化(已证明):真实 Qwen3_5DecoderLayer.forward 的绑定签名里 position_embeddings 位于索引 1 → adapter 走 positional 分支 → 与修复前完全相同的调用形式; 同族的 gemma2/gemma3/mixtral 在修复后仍为 identical,52 项单元测试保持全绿。

第二类兼容性:远程代码 vs transformers 版本

inclusionAI/Ling-3.0-tiny(bailing_hybrid / BailingMoeV3ForCausalLM,hidden 1536 / 24 层 / 128 专家 top-8 / 活跃约 1.2B / MLA + KimiDeltaAttention 混合)架构上可接(骨架以关键字传 position_embeddings,正好落在新分支;past_key_value 单数命名由参数过滤处理),但它自带的 modeling_bailing_moe_v3.py 是按旧版 transformers API 写的:

  • from transformers.utils.import_utils import is_torch_fx_available → 5.9 已移除该符号;
  • config.rope_scaling["factor"] → 5.9 会把 rope_scaling 规范化掉,运行期 KeyError。

也就是说「嵌进任意模型」有两层前提:架构层(已解决,14/14) 与 栈层(需兼容 shim 或独立 venv)。