5. 静默失效清单(本项目最贵的资产)

★ 7.2k 字 · 源文件 PROJECT.md 第 435 行起 · goal discipline arch optim selfcorrect

以下每一条都真实发生过、且当时没被察觉。它们不报错,只让实验结果无声地变得无意义。

#失效模式后果检测方式
1读出窗口测错(用全局时钟跨两遍定位)测到错误阶段,结论全废任务显式声明窗口
2词分量叠加 → one-to-many上界假象检查标签是否唯一决定激活模式
3驱动电流超阈值饱和计数不反映差异扫描驱动强度
4不同粒度的特征上限相同误判瓶颈位置同时测多种粒度
5任务输入泄露答案所有延迟 100%,任务无效静默区必须全零
6timespans 未传/传错loss 停在 ln2,学不到断言初始 loss ≈ 0.693
7训练集内准确率当上界(过拟合)误判上限一律用留出集/CV
8类别不平衡 / 随机基线假设错数字无法解读实测基线
9联合结构泄露(边缘统计量看不见)泄露检测全 PASS,任务已废用任务语义约束(structure_actives)
10backbone_units=128(ncps 默认)所有模型都学不起来见 §6.1
11答案不在输入里(被覆盖)闭式解上界 = 52.8%,所有模型 50%identifiability_spec 可识别性检查
12打断改变了标签分布常数预测就能拿 93.6%检查各条件下的标签正例比例
13多项检查共用同一 rng 流0.1% 的坏运气被固定成必然事件每项检查用独立 rng
14余弦退火把 lr 降到 0 后锁死训练中断但看不出,结果逐位重复lr 设下限;检查结果是否重复
15扰动过强把样本毁掉字内距离 ≈ 字间距离(比值 1.05)量化"字内/字间"距离比
16"零延迟"不是捷径指令与探针从不重叠(共存 0.0%)直接量共存比例
17只看单次运行约 12% 的种子完全失败≥3 种子,报成功率
18文本编码不匹配(UTF-16 vs UTF-8)日志解析全部失败但不报错,正则一条都匹配不上读文件前嗅探 BOM;或让实验直接写 jsonl
19单一 lr 上的"能力不足"结论把"学习率不对"误判成"容量不够"(代价见 §6.14)任何"某能力不行"必须在多个 lr 下确认
20p.detach().cpu() 不是副本(对已在 CPU 的参数)state_dict 与参数共享存储 → 存了之后改参数,存档跟着变 → "恢复"的是改后的值存档一律 .clone();检查"存取"时必须同时比对"vs sd"和"vs 基准"
21优化器介入"存取正确性"检查load_state_dict 写对后,Adam 又按自己的动量改了参数 → 参数检查 PASS、输出检查 FAIL检查存取时丢掉优化器(del opt);或在无优化器路径上验证
22try/except 吞掉对照的失败,然后报告全称结论exp10 的 ALL_KINDS 有 6 个,ssm/lstm/gru 全部 NotImplementedError 被吞,产物 JSON 只有 3 个键 —— 但汇总段仍打印"没有任何模型在零输入下呈现有结构的自主活动"。一个全称否定建立在 3/6 的样本上,而且是一整条路线(§7.2)的依据对照必须全部产出,任一失败即非零退出;禁止 except: continue 后继续汇总
23把"编码器偏置"当成了"零输入" —— 但结论下反了exp10 写的是 z = m.enc(zero),而 enc = nn.Linear(n_in, e) 默认 bias=True → z = b_enc(可学习常数向量)。我一开始指控这是"假零输入、错的"——这个指控本身错了:读 core/tasks/match.py 第 129 行,x = np.zeros((T, N_SENS)),延迟期什么都不写,所以 cell 在训练时收到的正是 b_enc。exp10 的设置恰好匹配训练分布。真正该被批评的是措辞(叫它"零输入"),而不是设置;而我后来"修正"成的 z = 0 反而更 OOD —— 实测:zero 模式的敏感性 8.50 vs task 模式 2.27,差 3.7 倍描述状态动力学时必须说明输入是哪个分布的:enc(0)=b_enc(延迟期真实输入)还是 z=0(cell 从没见过)。两者结论可能相反
24拿未训练网络的动力学当架构性质的证据exp10_autonomous.py 没有任何训练循环(新建 → eval() → 直接跑)。所以"零输入下冻结、有效秩 5/16"是随机初始化网络的性质,而 §7.2 的真正判据含训练("任务准确率下降 <10pp")。这是类别错误,而且它支撑了两个月的路线判断架构性结论必须在训练后测量;exp28 实测:同一个 cfc,未训练 retention = 0.0000,训练后(acc 0.967)retention = 0.7975
25指标方向反了,而且没人发现"有效秩"测的是轨迹路径维度不是状态容量:若 h 收敛到不动点,H ≈ [h₀..h_k, p, p, …, p],中心化后那 T−k 行只贡献一个方向 → eff_rank ≈ 1 + 瞬态方向数,与状态维度无关。后果:能完美保持静态记忆的系统 → eff_rank=1 → 判"冻结";注入噪声让它乱走 → eff_rank 上升 → 判"有自主活动"。它惩罚我们想要的,奖励我们不要的。改用成对指标 + 校准对照:retention(跨初态散布比)+ activity(末段相对步长),并必须报出噪声(应 ≫1)/ 线性衰减 0.99^t(应 =0.049)/ 完美保持(应 =1.000)三个锚点
26.bat 文件编码 ≠ 系统代码页 —— 乱码被当成命令执行tools/set_tdr_delay.bat 存成 UTF-8 + LF,而中文 Windows 的 cmd.exe 用 GBK(936) + 需要 CRLF 读它。某些中文字符的 UTF-8 字节被 GBK 解读后**恰好含有 & `\ " 等特殊字节**,于是 REM 注释行被**拆成命令执行**,if (...) 块结构错乱,**reg add` 根本没跑成(注册表里键都没建)。比第 18 条更糟:18 条是"解析失败",这条是"执行垃圾**"。.bat 一律存 GBK + CRLF;写完必须验证:GBK 解码成功、UTF-8 解码失败、U+FFFD 计数为 0、裸 LF 计数为 0。或干脆改用 PowerShell 脚本(.ps1 是 UTF-8 友好的)
27★ 用与任务无关的「抽象指标」代替任务相关测量 —— 连续栽三次为了判定"状态动力学是否健康",我先后用了三套抽象指标,三套全错:<br>① 有效秩(§5 第 25 条):测的是瞬态丰富度,惩罚稳定记忆、奖励漂移<br>② retention(跨初态散布比):混沌同样保持散布 —— 分不开"记忆"和"混沌"<br>③ 随机初态线性探针(h_T → h_0):把"非线性"和"信息丢失"混为一谈。实测即使 T=5(任务真实需求),R² 也只有 0.065,而同一个模型的任务准确率是 99.2%<br>后果:三套指标一致指向"状态已死",我据此差点写下"§7.2 需要 activity_loss"。而任务相关的探针显示延迟期内容保持 100%判据必须写成"任务需要的内容还在不在",不能写成"状态看起来活不活跃"。<br>具体做法见 §6.13:把训练好的模型冻结,只训一个线性分类器从 h_t 预测任务自己的标签(指令 / 探针),看它在各时刻的可读出性。<br>这条比前几条都重要 —— 因为它错得最隐蔽:抽象指标会给你一个看起来很科学的数字,而那个数字与你要回答的问题无关。
28★ torch 线程数超订:并行跑 N 个进程时,每个都按物理核数开线程实测:6 个并行实验进程 × 默认 10 线程 = 60 个线程抢 16 个逻辑核 → CPU 100% 满载而 GPU 只有 37%。修 set_num_threads(1) 后:同一硬件、同样 6 个进程,GPU 37% → 81%。<br>项目里早就记过这件事(exp08_reproduce_winner.py 注释:"exp06 在 GPU 下 set_num_threads(1),exp07 没设(默认 10)")—— 但 exp13 没设,于是又踩了一次。任何并行实验脚本都必须显式 torch.set_num_threads(1),且要在任何 torch 计算之前。判据:并行进程数 × 线程数 ≤ 逻辑核数。另外单进程也测过:threads=8 比 threads=1 更慢(173 vs 155 ms/迭代)
29★ 忘了 .to(device) —— 整个诊断在 CPU 上跑,却和 GPU 的结果对比diag_seed_failure.train_eval 第一版建了模型就直接用(Net(...) 不带 .to(dev),model(X) 的 X 也在 CPU)。于是:<br>exp13(GPU)seed=4 → acc 0.507<br>本脚本(CPU)seed=4 → acc 0.931 / 0.931 / 0.902(三次)<br>我据此写了"同进程内连续跑多种子会泄漏状态"这个结论 —— 方向完全错了。真因是 CPU 与 GPU 是两套数值路径。<br>脚本跑通了、数字看着合理(0.507/1.000/0.858)、判读还很自信。 教科书级静默失效任何"和另一个实验对比"的脚本,必须①显式 .to(device),②在输出里打印实际设备。<br>本条的代价:一整轮错误结论 + 两条错误的 §11 纪律(现已更正)

#★ 第 20/21 条详述:一个"看起来像保存恢复坏了"的组合坑

症状(core/darlin/hybrid.py 的 tests_hybrid()):

text
参数层面:恢复后与 sd 差异   0.000e+00   <- 通过
参数层面:恢复后与基准差异   1.645e-01   <- 失败
输出层面:恢复后与基准差异   4.260e-01   <- 失败

看起来像 load_state_dict 有 bug。

实际是两个独立的坑叠在一起:

坑 20 —— HybridNet.state_dict() 里写的是 p.detach().cpu()。对已经在 CPU 上的参数,这个表达式返回的是 共享存储的视图,不是副本。于是:

text
存 sd  ->  打乱参数  ->  sd 里的值也跟着变  ->  "恢复"的是打乱后的值

加 .clone() 即修复(sd 都是副本? True)。

坑 21 —— 测试 4 复用了测试 3 创建的 Adam,而 Adam 持有参数的引用 和动量状态。即使 load_state_dict 把参数写对了,后续 forward 又会 触发优化器按自己的状态更新参数。丢掉优化器后立即通过。

为什么这个组合特别危险:

特征后果
只在"存了之后又动参数"时暴露所有真实训练循环里都会中招
两个检查一个 PASS 一个 FAIL指向错误的方向(以为是 load 坏了)
前向/梯度检查全 PASS训练时看不出任何异常,只有存模型时才发现

一般化的教训(与第 6、18 条同源):

凡是"接口约定没对上、而系统不报错"的地方,都要用断言钉死。 这里要钉两条:存档必须 clone();验证存取时必须隔离第三方 (优化器、随机数、缓冲)。

第 5、6、11、12 条是同一个根源的四种表现(答案的可获得性),值得合并成一个更强的检查族。 第 18 条与第 6 条(timespans 没传对)性质相同:都是"接口约定没对上,而系统不报错"。 这类问题应统一用断言处理 —— 接上就断言,不接就拒绝运行。 第 20/21 条是同一类:都是"看起来在工作,实际上没工作"。

发生了什么:experiments/tail_to_jsonl.py 用来把纯文本训练日志转成面板能读的 jsonl。它一直"运行正常"(不报错、不崩),但一行都没解析出来。

根因:results/exp14/run.log 是 UTF-16-LE —— 因为 PowerShell 的 > 重定向用系统默认编码(Windows 上是 UTF-16-LE, 带 BOM、每个字符之间夹 \x00),而 Python 的 print 输出是 UTF-8。

用 UTF-8 去读 UTF-16 文件,得到的是满屏 \x00 的垃圾:

text
'\x00 \x00 \x00 \x00 \x00 \x00 \x00 \x001\x00 \x00 \x00l\x00o\x00s\x00s\x00...'

为什么危险:它不报错。 文件能打开、行能读到、字符串能处理 —— 只是所有模式都匹配不上,程序安静地什么都不做。

修法:读文件前嗅探 BOM。

python
def sniff_encoding(path):
    head = open(path, class="tok-str">"rb").read(4)
    if head.startswith(bclass="tok-str">"\xff\xfe"): return class="tok-str">"utf-16-le", 2
    if head.startswith(bclass="tok-str">"\xfe\xff"): return class="tok-str">"utf-16-be", 2
    if head.startswith(bclass="tok-str">"\xef\xbb\xbf"): return class="tok-str">"utf-8-sig", 3
    return class="tok-str">"utf-8", 0

更彻底的修法:让实验直接写 jsonl(core/logger.py 已经这么做了, exp15 也已改成直接写)。绕开"文本日志 + 后处理"这一整类问题。

一般化的教训:

凡是"读别人的输出"的代码,都必须显式声明编码,不能依赖默认值。 Windows 上 PowerShell 重定向、Excel 导出、部分编辑器保存都可能产出 UTF-16。这与 §5 的其它条目同源 —— 不报错,只让结果无声地失效。

第 5、6、11、12 条是同一个根源的四种表现(答案的可获得性),值得合并成一个更强的检查族。 第 18 条与第 6 条(timespans 没传对)性质相同:都是"接口约定没对上,而系统不报错"。 这类问题应统一用断言处理 —— 接上就断言,不接就拒绝运行。

推论:core/task_base.py 的自检门必须不通过就拒绝训练,且必须做负向验证 (一个从没失败过的检查没有说服力)。


← 返回《PROJECT.md》目录