- 引入 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,读写关闭时与原生模型逐位相同
21 KiB
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 |
结论不是「谁全面更强」,而是各轴各有胜负:
- 原工程用
selection_score = 0.5·top1 + 0.3·need_f1 + 0.2·mrr选出的 "best"(step 1000)Top-1 最高,但在 0.50 门槛下有 33.96% 的未知问题被强行读记忆;该综合分几乎不惩罚这一点,因为未知样本只有 106 条。 - XL-512 在覆盖率上明显更好:Recall@3 +4.20pp、Recall@5 +2.40pp、多跳证据全中 +3.96pp。
- 把门槛从 0.50 提到 0.70,XL-512 best 的未知拒答率达到 100.00%,而已知问题被误拒率仍为 0.22%——即在同等策略安全水平下,XL-512 的排序覆盖优势可以保留。
- 代价: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 时代沿用下来的缺陷,正好废掉了本次评测最需要的两个轴:
unknown_abstention/forget_correction的源数据带metadata.answerable = false,其acceptable装的是拒答话术("不知道"/"没有记录")。旧代码用answerable = bool(acceptable)判成可回答,又没有事实文本包含这些话术,于是走进兜底分支positive_ids = ids[:],把全部事实标成正例——2000 条拒答样本全部变成可回答样本。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)。