← all writing

Forgetting Model 4: Everything Lands on One Frontier

I’ve been writing about forgetting on this blog since 2019, mostly as theory. This year I finally had the compute to test it — on a real LLM, with executor-verified ground truth and pre-registered criteria. Almost everything I believed turned out to be wrong, and the wrongness was more interesting than the theory. This post is the honest ledger: ten things that failed, two that worked, and the one wall I couldn’t get past. Every principle has an interactive demo — play with them, they carry the argument better than the prose does.

从 2019 年开始,我在这个博客上断断续续写「遗忘模型」,但基本都停留在理论。今年终于有算力把它真正跑一遍——在真实的 LLM 上,用执行器验证的 ground truth,判据全部预注册。结果是:我原来相信的东西几乎全错了,而错的方式比理论本身有意思得多。这篇文章是一份诚实的账本:十个失败、两个成功,以及一堵我没能翻过去的墙。 每个原理都配了可交互演示——建议动手玩,它们比文字更能说明问题。

1. Seven years of hand-waving

Quick recap of where this series left off.

Forgetting model for MLP (2019) argued you should slow the update down per-dimension instead of updating the whole network. Forgetting model continued worked through the backprop algebra for that. And forgetting model 3 (2022) is barely a paragraph — I wrote down one idea and left it:

It’s all about time! All the network training in current deep learning settings are not considering the effect of time. However, the biological inspiration led me to rethink the necessity of time as an important parameter in the full system.

That’s the two-timescale hypothesis, three years before I could test it. A fast learner that’s allowed to overfit and forget, and a slow learner that absorbs only what survives averaging. Complementary learning systems, hippocampus and cortex, the whole story.

I finally tested it this month. It’s just a smaller learning rate. More on that in §8.

Here’s what changed since 2022: the question stopped being academic. If you want a model that gets permanently better from its own experience — not better for one session with a scratchpad, but better in the weights — you have to write into the weights. And the moment you write into the weights, you’re paying for it somewhere. The whole game is figuring out what the exchange rate is, and whether anything actually beats it.

一、七年的空想

先回顾一下这个系列停在哪儿。

forgetting model for MLP(2019)主张:不要整网更新,应该按维度把更新速度压下来。forgetting model continued 把这套反传的代数推了一遍。而 forgetting model 3(2022)几乎只有一段话——我写下一个想法就撂那儿了:

It’s all about time! 现在深度学习的训练设定里完全没有考虑「时间」的作用。但生物学的启发让我重新思考:时间应该是整个系统里一个重要的参数。

这就是双时间尺度假设,比我有能力验证它早了三年。一个允许过拟合、允许遗忘的快学习器,加一个只吸收「平均之后还活着」的部分的慢学习器。互补学习系统、海马与皮层,整套故事。

这个月我终于把它测了。它就是个更小的学习率而已。 细节在第八节。

2022 年之后变了什么:这个问题不再是学术趣味了。如果你想要一个能从自己的经验里永久变强的模型——不是靠草稿纸在单次会话里变强,而是权重变强——那你就必须往权重里写。而只要你往权重里写,就一定在某个地方付出代价。整场游戏就是搞清楚:这个汇率是多少,以及有没有任何东西能真的打赢它。

2. What a task actually is

Most self-improvement papers are impossible to argue with, because “the model got better” is measured on the same distribution it trained on. I wanted a setup where I could be proven wrong. That starts with the data, so let me show you the actual thing before I claim anything about it.

The domain is procedurally generated Python list transformations. Every task is a known composition of primitives — the structure — instantiated with concrete parameters and input data — the surface. Because I generate the tasks, I know the structure exactly. The model is never told it. There’s no docstring, no hint, no natural-language description. It sees input/output pairs and has to induce the rule, then write transform(xs). A sandboxed executor runs the candidate against all the demos; it passes only if every single one matches. No LLM judge, no partial credit, no drift.

Play with a real one below. Try to induce the rule yourself before you hit reveal — that’s exactly the problem the model is facing, and it’s worth feeling how it goes.

二、一个任务到底长什么样

大多数「自我进化」的论文没法反驳,因为「模型变强了」是在它自己训练的那个分布上测的。我想要一个能把我自己证伪的设定。这要从数据开始——所以在我下任何结论之前,先把实物摆出来。

领域是程序化生成的 Python 列表变换。每个任务都是一组原语的已知组合——结构——再用具体的参数和输入数据实例化——表面。因为任务是我生成的,我完全知道结构是什么。但模型从不被告知。 没有 docstring,没有提示,没有任何自然语言描述。它看到的只有输入输出对,必须自己归纳出规则,然后写出 transform(xs)。沙箱执行器拿所有 demo 去跑,每一条都对才算通过。没有 LLM 裁判,没有部分给分,没有漂移。

下面是一个真实任务,玩一下。在点「揭晓」之前先自己试着归纳规则——模型面对的正是这个问题,值得亲身体会一下它有多难。

A real task from the generated corpus, shown exactly as the model receives it. Reveal the structure to see the composition, and step the pipeline to watch one input pass through both primitives.

Knowing the structure of every task is what buys the three separate held-out sets. Each one holds something fixed and varies something else:

  • HO-surface — the same structures as training, unseen surface: new parameters, new input data. Tests specialisation. Did it learn the transformation, or memorise these particular lists?
  • HO-compositionnovel compositions of the same primitives, templates never trained on. Tests systematic generalisation. This is the one the whole project was about.
  • FORGET probe — MBPP+, ordinary unrelated Python problems, never trained on in any arm. Tests what the update destroyed.

Base rates, so you can calibrate everything that follows: FORGET probe 0.847, HO-surface 0.100, HO-composition 0.083. The model solves about 10% of these greedily. That headroom is deliberate.

Two more decisions make the rest of the post mean something. Pre-registered criteria, frozen before looking — written into the spec file and committed before the first run. I’ll show you a place in §11 where I misread my own data, and the only reason it got caught was that I’d written down in advance what would count as success. And small enough to run many times: Qwen3-4B-Instruct with LoRA (r=16, q/v), so each arm takes minutes. A mechanism claim needs a dozen controlled runs with error bars, not one heroic run.

3. The corpus, and the one number that turned out to matter

A task is one thing. The corpus is another, and it’s where the only real result in this post came from.

Two big runs used the same method, the same model, the same budget of roughly 600 practice tasks, and the same twelve-primitive vocabulary. The only thing that differed was how many distinct compositions those tasks were spread over: 12 structures, or 48. That ratio has a name — context multiplicity, $Nk/m$ — and it went from 2.00 to 8.00.

I didn’t expect this to be the interesting variable. It turned out to be the only intervention in the entire programme that moved compositional generalisation at all.

生成语料中的一个真实任务,完全按模型接收到的样子呈现。点「揭晓结构」看它的组合,用步进按钮看一个输入依次穿过两个原语。

知道每个任务的结构,才买得到那三个互不相干的留出集。每一个都固定住某样东西、变动另一样:

  • HO-surface —— 和训练相同的结构,未见过的表面:新参数、新输入数据。测专精化。它学到的是那个变换,还是背下了这几个具体的列表?
  • HO-composition —— 相同原语的全新组合,模板从未训练过。测系统性泛化整个项目就是为了这个。
  • FORGET 探针 —— MBPP+,普通的无关 Python 题目,任何 arm 都没训练过。测这次更新毁掉了什么

基线,方便你校准后面所有数字:FORGET 探针 0.847,HO-surface 0.100,HO-composition 0.083。模型贪心解开约 10%。这个上升空间是故意留的。

还有两个决定,让后文有意义。预注册判据,看结果之前就冻结——写进 spec 文件、在第一次跑之前就 commit 了。第十一节里我会展示一处我误读了自己数据的地方,而它能被抓出来,唯一的原因就是事先写清了什么算成功。以及规模足够小、可以反复跑:Qwen3-4B-Instruct + LoRA(r=16,q/v),每个 arm 几分钟。一个机制性的结论需要十几次带误差棒的受控实验,而不是一次英雄式的大跑。

三、语料,以及那个最后真正起作用的数字

一个任务是一回事。语料是另一回事——而本文唯一真正的正面结果,就出自这里。

两次大实验用了同样的方法、同样的模型、同样约 600 个练习任务的预算、同样的十二原语词表。唯一不同的是这些任务被铺在多少个不同的组合上:12 个结构,还是 48 个。 这个比例有个名字——context multiplicity,$Nk/m$——它从 2.00 变成了 8.00。

我原本没指望这是那个有意思的变量。结果它是整个项目里唯一推动了组合泛化的干预。

The same vocabulary, spread two ways. Toggle between the runs and watch the matrix go from sparse and clumped to dense and even.

Two things in there are worth pausing on, and I only saw them by going back to the actual split files instead of trusting the summary number.

Run A’s corpus isn’t just low-multiplicity, it’s skewed. filter_gt appears in 5 of the 12 training structures. sort_desc, dedupe and cumsum appear in exactly one each. And take_first appears in zero training structures while still showing up in held-out compositions. In Run B every primitive lands somewhere between 5 and 11. A primitive that only ever occurs inside one composition is, as far as the data is concerned, indistinguishable from that composition — nothing tells the model it’s a reusable part. That’s a sharper statement of the problem than “not enough diversity”, and it fell out of the data rather than the theory.

Held-out compositions only ever reuse primitives that appear in training. This is a deliberate design property, and it’s what licenses calling HO-composition a test of systematic generalisation rather than a test of missing knowledge. If the model fails there, it failed to compose — not to know. Without that guarantee the whole measurement would be worthless.

同一个词表,两种铺法。在两次实验之间切换,看矩阵从稀疏聚块变成稠密均匀。

里面有两件事值得停一下,而且我是回头去翻真实的 split 文件才看到的,光看汇总数字看不出来。

Run A 的语料不只是多样性低,它是「偏」的。 filter_gt 出现在 12 个训练结构中的 5 个。sort_descdedupecumsum 各只出现 1 次。而 take_first 在训练结构中出现 0 次,却仍然出现在留出的组合里。Run B 里每个原语的出现次数都在 5 到 11 之间。一个只在单个组合里出现过的原语,从数据的角度看,和那个组合是无法区分的——没有任何东西告诉模型它是个可复用的零件。这比「多样性不够」是一个更锋利的陈述,而且它是从数据里掉出来的,不是从理论里推出来的。

留出的组合,只会复用训练中出现过的原语。 这是一个刻意的设计性质,也正是它让 HO-composition 有资格被称为系统性泛化的检验,而不是「缺知识」的检验。如果模型在那里失败,它是没能组合——而不是不知道。没有这个保证,整个测量就一文不值。

4. The graveyard

Before the story, the body count. Ten things that didn’t work.

I’m putting this up front on purpose. The usual shape of a research post is “here’s my clever idea, here’s why it works” — which hides the part that’s actually informative. Every card below has a measured mechanism, not a shrug. “It didn’t work” is worthless; “it didn’t work because the compressed latent can’t carry enough bandwidth to represent even one transformation, and here’s the cross-entropy floor that shows it” is a result.

Click any card to open it. Filter by category if you want to skip ahead.

四、坟场

在讲故事之前,先看尸体。十个没跑通的东西。

我故意把这部分放在最前面。研究博客通常的写法是「这是我的妙招,这是它为什么work」——而这恰恰把真正有信息量的部分藏起来了。下面每一张卡片都带一个测量出来的机制,而不是一句「没work」。「没work」毫无价值;「没work,是因为压缩后的 latent 带宽根本不足以表达哪怕一个变换,这是证明它的交叉熵地板」——这才是结果。

点任意卡片展开。想跳着看就用分类筛选。

Ten dead ends, each with the measurement that killed it. Blue = the ARC phase, amber = consolidation mechanisms, red = theory. The rest of this post walks through the interesting ones.

十条死路,每条都附上杀死它的那次测量。蓝色 = ARC 阶段,琥珀色 = 巩固机制,红色 = 理论。本文接下来会挑有意思的几个细讲。

5. Why the recursive-latent ideas died

The first five cards are from an earlier arc (pun accidental) where I tried to get recursive latent reasoning working on real ARC-AGI. I’ll summarise rather than re-litigate, because the mechanism failures rhyme.

I got eval-400 coverage from 5.5% → 9.5% → 10.75%, union 12.0%. SOTA is 40–45%. That’s not a competitive number and I’m not going to dress it up.

The instructive part is why the recursive-latent ideas died. Three architectures trained from scratch produced exactly zero held-out exact matches, and the diagnostic was that the demo-fit cross-entropy floored at 0.91–1.08 and never approached zero — the model couldn’t even fit one transformation, let alone generalise. The conditioning bottleneck was too narrow. Then I tried Coconut-style hidden-state loopback on a frozen pretrained model, zero-shot: valid-program rate fell 200 → 194 → 179 out of 200 as I added latent steps. Of course it did. The model was never trained to consume its own hidden state; feeding it back walks straight off the manifold. Then I trained an adapter to fix that, and got a val delta of −0.004, identical across all three epochs. Flat. No proof of life.

That last one taught me something I kept running into later: there was no gradient signal because there was nothing to learn. For tasks the prefill already solves, recursion has no headroom. For tasks it can’t solve, there’s no verified target to train against. The recursion had nowhere to live.

The fourth card is the one that generalises best. I built an operator-library ratchet — solve tasks, harvest reusable operators from the solutions, add them to a library, use the library to solve more tasks. It worked, for a bit: +13 tasks. Then it stopped dead. Newly solved per round went 81 → 19 → 11 → 5 → 6; the library plateaued at 69 operators; by round 4 it was adding 6 solves and zero operators. The reason is embarrassingly obvious in hindsight: you can only harvest operators from tasks you already solve. The ratchet can compress what you know. It cannot reach what you don’t. Hold onto that — it’s the same wall as §12.

五、递归 latent 那条线是怎么死的

前五张卡来自更早的一段:我尝试在真实的 ARC-AGI 上把递归 latent 推理跑通。这里只做总结,不重新展开,因为它们的失败机制和后面是押韵的。

eval-400 覆盖率我从 5.5% 做到 9.5% 再到 10.75%,取并集 12.0%。SOTA 是 40–45%。这不是一个有竞争力的数字,我也不打算给它化妆。

有教益的是这些递归 latent 的想法死在哪。三个从头训练的架构,留出集精确匹配全部为零,而诊断信息是:demo 拟合的交叉熵卡在 0.91–1.08 的地板上,从来没接近过 0——模型连拟合一个变换都做不到,更别说泛化。条件瓶颈太窄了。然后我在冻结的预训练模型上零样本地试了 Coconut 式的隐状态回环:随着 latent 步数增加,合法程序率从 200 里的 200 → 194 → 179 一路掉。这太正常了。模型从来没被训练过去消费自己的隐状态,把它喂回去等于直接走出流形。于是我训了个 adapter 想修这一点,得到的是 val delta = −0.004,三个 epoch 一模一样。 一条平线。毫无生命迹象。

最后这个教会了我一件后面反复撞见的事:没有梯度信号,是因为根本没有东西可学。 对于 prefill 已经解开的任务,递归没有发挥空间;对于解不开的任务,又没有被验证过的目标可以拿来训练。递归无处安放。

第四张卡是最有普适性的那个。我做了一个算子库棘轮:解任务 → 从解里收割可复用算子 → 入库 → 用库去解更多任务。它确实起效了一阵:+13 个任务。然后戛然而止。每轮新解开的数量是 81 → 19 → 11 → 5 → 6;算子库停在 69 个不动;到第 4 轮,它新增 6 个解、0 个算子。事后看原因尴尬地明显:你只能从已经解开的任务里收割算子。 棘轮能压缩你已知的东西,够不到你未知的东西。记住这句——它和第十二节是同一堵墙。

Four failures, four distinct mechanisms. Step each animation — the counters are the actual measurements, not illustrations.

Panel B is the one I’d point a reader at. “No gradient signal because there was nothing to learn” is not a sentence I expected to write about a training run, and it took a flat val delta across three epochs to believe it.

6. Four ways to write experience into weights

Now the machinery. Everything from here on is one of four mechanisms for taking executor-verified experience and pushing it into the weights. They all consume the same verified episodes and are budget-matched — identical example counts, identical gradient steps, identical hyperparameters and seeds. Only the mechanism differs. That’s what makes the comparison mean anything.

Rather than describe them in prose, here they are as animated flow diagrams. Step through each one, then read the result panel. The rest of the post is the argument about why three of them landed in the same place.

四个失败,四种不同的机制。逐步播放每个动画——里面的计数器都是真实测量值,不是示意。

最值得看的是面板 B。「没有梯度信号,是因为根本没有东西可学」——这不是我预期会用来描述一次训练的句子,而我是靠三个 epoch 完全平掉的 val delta 才肯信的。

六、把经验写进权重的四种方式

现在讲机器本身。从这里往后的所有内容,都是四种「把执行器验证过的经验推进权重」的机制之一。它们消费同一批已验证轨迹,且预算严格匹配——相同的样本数、相同的梯度步数、相同的超参和 seed。只有机制不同。 正是这一点让比较有意义。

与其用文字描述,不如直接看动画流程图。把每一个都步进一遍,再读结果面板。本文余下的部分,就是在论证为什么其中三个最后落到了同一个地方。

The four mechanisms, sharing one visual language so they’re comparable. Flip between ARM-A and ARM-B and exactly one block changes — that’s the controlled comparison §7 is about.

四种机制共用一套视觉语言,方便直接对比。在 ARM-A 和 ARM-B 之间来回切,会发现只有一个方块变了——第七节要讲的就是这个受控对照。

7. The first real question: what should you consolidate?

Here’s the idea that got me excited enough to pre-register it.

Everyone dodges catastrophic forgetting by isolation — separate adapters, retrieved soft-prompt memories, LoRA routing, EWC-style regularisation. All of these keep the new knowledge structurally apart from the old. But isolation doesn’t give you a better model; it gives you a filing cabinet with a model attached.

The alternative: what if forgetting isn’t about how much you write, but about what you write? Distilling raw trajectories (ExIt, ReST) writes a hundred idiosyncratic instances into the weights, each fighting the others. But if you first induce the recurring structure from those instances and consolidate that, the update is one coherent thing instead of a hundred conflicting ones. Fewer collisions, less forgetting. DreamCoder learns abstractions but keeps them symbolic; nobody had run the controlled head-to-head on the consolidation unit itself.

So I ran it. Two arms, same base, same wake experience, exactly the same number of training examples and gradient steps:

  • ARM-A (instance): SFT directly on the 100 verified (task → solution) episodes. Standard ExIt.
  • ARM-B (abstraction): from the same episodes, induce recurring structure behind a schema gate (a structure is admitted only if it appears in ≥3 distinct verified episodes), then generate fresh, executor-grounded, diverse instances of that structure, and SFT on those.

To make the test as fair as possible to the hypothesis I used an oracle abstraction grouping — a perfect detector — so a null couldn’t be blamed on bad induction. ARM-B ended up covering 6 of ARM-A’s 7 structures, which biases against the hypothesis. Recorded and kept.

Pre-registered as CONFIRMED iff: (a) forgetting smaller in B by ≥2 pts, (b) HO-composition B−A ≥ +5, (c) replicated across ≥2 seeds, (d) gradient conflict lower in B.

Result: NULL — with criterion (a) significantly reversed.

  base ARM-A instance ARM-B abstraction
FORGET probe (n=300, 2 seeds) 0.847 0.853 (p=0.73, no forgetting) 0.793 (p=0.0025)
HO-surface 0.100 0.108 / 0.117 0.133 / 0.133
HO-composition 0.083 0.067 / 0.092 0.075 / 0.092
gradient conflict rate 0.402 / 0.417 0.359 / 0.366

The abstraction arm — the one I predicted would be safer — forgot 5.4 points, significantly, in both seeds. The instance arm forgot nothing.

The diagnosis is in the training curves, and it’s the single most useful thing I learned all year. ARM-A’s loss went 0.058 → 0.031. It started at almost zero loss. Of course it did: it was re-fitting solutions the model itself had generated. There was nearly no surprise in that data, so the weights barely moved, so it neither gained nor forgot. It looked safe because it was a no-op. ARM-B’s loss went 0.440 → 0.150 — it genuinely learned something, bought the only real gain in the table (HO-surface 0.133 vs 0.100), and paid for it.

F1: you cannot dodge forgetting by changing the consolidation unit. Forgetting tracks how much the model actually moves, not what the update encodes. Stability–plasticity reasserts itself intact.

And a free methodological result:

F2: within-task gradient conflict does not predict catastrophic forgetting. ARM-B had the lower pairwise conflict rate in both seeds and was nonetheless the arm that forgot. This is a cheap, intuitive, widely-assumed proxy, and it’s wrong — interference among the new examples tells you nothing about interference with pre-existing capability. Criterion (d) passed and it didn’t matter, which is exactly why you pre-register all of them.

七、第一个真问题:到底应该巩固什么?

下面这个想法让我兴奋到愿意为它做预注册。

所有人躲避灾难性遗忘的办法都是隔离——独立 adapter、可检索的软提示记忆、LoRA 路由、EWC 类正则。它们都是把新知识在结构上和旧的隔开。但隔离并不给你一个更好的模型,它给你的是一个「挂着模型的文件柜」。

另一条路:如果遗忘的关键不在于你写了多少,而在于你写的是什么呢?蒸馏原始轨迹(ExIt、ReST)是把一百个各具特色的实例写进权重,彼此打架。但如果你先从这些实例里归纳出反复出现的结构,然后巩固结构本身,那这次更新就是一个连贯的东西,而不是一百个互相冲突的东西。碰撞更少,遗忘更少。DreamCoder 学抽象,但把它们留在符号层;没有人做过针对「巩固单元」本身的受控对照实验。

于是我做了。两个 arm,同一个 base,同一批 wake 经验,训练样本数和梯度步数完全相同:

  • ARM-A(实例):直接在 100 条已验证的 (任务 → 解) 轨迹上做 SFT。标准 ExIt。
  • ARM-B(抽象):从同一批轨迹出发,经过schema 门控归纳反复出现的结构(一个结构必须在 ≥3 条不同的已验证轨迹中出现才被采纳),然后重新生成该结构的、执行器接地的、多样化的实例,在这些新实例上做 SFT。

为了让这个检验尽可能偏向假设,我用了oracle 抽象分组——一个完美的检测器——这样即使结果是 null,也不能赖到「归纳做得差」头上。最终 ARM-B 覆盖了 ARM-A 七个结构中的六个,这一点是不利于假设的偏置。如实记录,保留。

预注册的 CONFIRMED 条件:(a) B 的遗忘小 ≥2 分,(b) HO-composition 上 B−A ≥ +5,(c) ≥2 个 seed 复现,(d) B 的梯度冲突更低。

结果:NULL——而且判据 (a) 显著反转。

  base ARM-A 实例 ARM-B 抽象
FORGET 探针(n=300,2 seed) 0.847 0.853(p=0.73,无遗忘 0.793p=0.0025
HO-surface 0.100 0.108 / 0.117 0.133 / 0.133
HO-composition 0.083 0.067 / 0.092 0.075 / 0.092
梯度冲突率 0.402 / 0.417 0.359 / 0.366

抽象那个 arm——我预测会更安全的那个——两个 seed 都显著地忘掉了 5.4 分。实例 arm 什么都没忘。

诊断藏在训练曲线里,这也是我今年学到的最有用的一件事。ARM-A 的 loss 是 0.058 → 0.031。 它从几乎为零的 loss 开始。这太正常了:它在重新拟合模型自己生成的解。那批数据里几乎没有惊奇度,所以权重几乎没动,所以它既没得到也没忘掉。它看起来安全,是因为它根本是个空操作。 ARM-B 的 loss 是 0.440 → 0.150——它真学到了东西,买下了表里唯一真实的增益(HO-surface 0.133 vs 0.100),并且付了钱。

F1:你没法通过换「巩固单元」来躲开遗忘。 遗忘跟踪的是模型实际移动了多少,而不是这次更新编码了什么。稳定性–可塑性权衡原封不动地回来了。

外加一个白送的方法论结论:

F2:任务内的梯度冲突不能预测灾难性遗忘。 ARM-B 在两个 seed 上冲突率都更低,却偏偏是遗忘的那一个。这是一个便宜、直觉、被广泛默认的代理指标,而它是错的——新样本之间的干扰,完全不能说明它和既有能力之间的干扰。判据 (d) 通过了,然后毫无意义——这正是为什么你要把它们全部预注册。

8. The currency is KL, and everything lands on one line

F1 said forgetting tracks “how much the weights move.” That was still a proxy, and a bad one — weight distance is unreliable because of parametric symmetries. The right currency, per RL’s Razor, is the KL divergence between the tuned and base policy on the distribution you care about.

So I measured it directly. measure_kl.py loads one model, uses peft’s disable_adapter() to get base and tuned logits over the same teacher-forced tokens, on trajectories that are the base model’s own greedy generations. No second model, no sampling noise. One number per arm: how far did this update actually move the function, on the distribution the base actually visits.

Then I plotted every arm I had against it. Including the two-timescale system from 2022.

The demo below is the single most important figure in this post. Start with “show on-policy” off. Toggle between gain and forgetting.

八、真正的货币是 KL,而所有方法都落在同一条线上

F1 说遗忘跟踪的是「权重移动了多少」。那还只是个代理指标,而且是个糟糕的代理——由于参数对称性,权重距离本身并不可靠。按 RL’s Razor 的说法,正确的货币是微调后策略与 base 策略在你关心的分布上的 KL 散度

于是我直接去测。measure_kl.py 只加载一个模型,用 peft 的 disable_adapter()同一批 teacher-forcing 的 token 上分别拿到 base 和 tuned 的 logits,而轨迹用的是 base 模型自己的贪心生成。不需要第二个模型,没有采样噪声。每个 arm 得到一个数:这次更新在 base 实际访问的分布上,究竟把函数搬了多远。

然后我把手上所有 arm 都画到这个坐标上。包括 2022 年那个双时间尺度系统。

下面这个演示是全文最重要的一张图。 先保持「显示 on-policy」为关。在「增益」和「遗忘」之间切换看看。

Five methods that have nothing algorithmically in common, all sitting on one line. Toggle the axis: gain and forgetting are both near-deterministic functions of KL. Then switch on the on-policy trajectory — that’s the one thing that left the line.

Look at the correlations: corr(KL, gain) = 0.986, corr(KL, forgetting) = 0.921. And the slope is method-independent — gain per 0.01 KL is 0.84 for EMA, 0.96 for the small-LR control, 0.88 for direct merge. Same exchange rate no matter how you got there.

This kills the 2022 idea cleanly. The EMA two-timescale system reached KL 0.0202 and got +1.7 gain / +0.7 forgetting. The small-LR control reached KL 0.0260 and got +2.5 / +5.4. EMA looked safer. It was safer. It was safer because it took a smaller step, which is exactly the confound the control was designed to catch. At matched KL, averaging buys you nothing. Fourteen years of intuition about hippocampus and cortex, and the mechanism reduces to a learning-rate schedule.

I want to be precise about what died and what didn’t. What died is the claim that two-timescale averaging is a distinct anti-forgetting mechanism. What survives is something the frontier itself tells you: if gain and forgetting are both functions of one scalar, then every “algorithmic” intervention — averaging, chunking, LR schedules, the choice of consolidation unit, gap-targeting — is a move along the curve. You can pick your point on it. You cannot leave it.

Which immediately tells you what would work. You need something that changes which solution you land on at fixed KL — not how you get there. Two candidates: on-policy mode-seeking (among all solutions to a task, prefer the KL-nearest), and targeted retention (protect what matters, spend KL where it doesn’t).

I tried the cheap version of the first one — ARM-C, training only on the “search gap”: the 37 episodes where search found something greedy decoding missed, so the data is on-policy and non-trivial. It landed on the line at KL 0.0159. (It also had a coverage confound — only 4 structures survived the schema gate — so its HO-surface comparison isn’t usable. Stated, not buried.) Five arms, one frontier.

五种在算法上毫无共同之处的方法,全部落在同一条线上。切换坐标轴:增益和遗忘是 KL 的近似确定性函数。然后打开 on-policy 轨迹——那是唯一一个离开了这条线的东西。

看相关系数:corr(KL, 增益) = 0.986corr(KL, 遗忘) = 0.921。而且斜率与方法无关——每 0.01 KL 换来的增益,EMA 是 0.84,小学习率对照是 0.96,直接合并是 0.88。不管你怎么走过去,汇率一样。

这干净利落地杀死了 2022 年那个想法。EMA 双时间尺度系统走到 KL 0.0202,拿到 +1.7 增益 / +0.7 遗忘。小学习率对照走到 KL 0.0260,拿到 +2.5 / +5.4。 EMA 看起来更安全。它确实更安全。但它更安全是因为它步子迈得更小——这恰恰就是那个对照组当初被设计出来要抓的混淆项。在匹配 KL 的条件下,平均什么都买不到。 关于海马体和皮层的十几年直觉,机制上归约成了一个学习率调度。

我想把「死了什么、没死什么」说精确。死掉的是「双时间尺度平均是一种独立的抗遗忘机制」这个主张。活下来的,是这条前沿本身告诉你的事:如果增益和遗忘都是同一个标量的函数,那么所有「算法性」的干预——平均、分块、学习率调度、巩固单元的选择、gap 定向——都只是在曲线上滑动。你可以挑你落在曲线上的哪个点。你没法离开这条曲线。

这也立刻告诉你什么才可能有用。你需要的是在固定 KL 下改变你落到哪个解的东西,而不是改变你怎么走过去。两个候选:on-policy 的 mode-seeking(在所有能解这个任务的解里,优先选 KL 最近的那个),以及定向保留(保护重要的,把 KL 花在不重要的地方)。

第一个我试了便宜版——ARM-C,只在「搜索缺口」上训练:那 37 条搜索找到、而贪心解码错过的轨迹,所以数据是 on-policy 且非平凡的。它落在线上,KL 0.0159。(它还带一个覆盖率混淆——schema 门控之后只剩 4 个结构——所以它的 HO-surface 对比不可用。明说,不埋。)五个 arm,一条前沿。

9. The one thing that left the line

Every arm so far was one off-policy step on a fixed corpus generated by the base model. RL’s Razor’s actual prescription is different: many small steps, each on-policy with respect to the model currently being updated.

Round $r$: sample $K=16$ candidates from $\theta_r$ itself → executor-verify → SFT $\theta_{r+1}$ from $\theta_r$. Each step is KL-minimal because the data comes from the model taking the step. But capability can still accumulate across rounds, because round $r{+}1$’s data comes from an already-improved model.

Go back to the demo above and switch on the on-policy trajectory. It sits clearly above the line for gain and clearly below it for forgetting.

At round 5, KL = 0.0713: the frontier predicts +6.3 points of gain and +14.0 points of forgetting. Measured: +14.2 gain (2.26×) and +4.0 forgetting (0.29×). Better on both axes at once. That’s not a move along the curve, that’s a shift of the curve.

I originally wrote that the effect emerges with iteration — that round 0 sits below the frontier and rounds 1 onward jump above it — and treated that as the mechanism’s signature. That was wrong, and it was wrong because I spliced two runs together. An early partial run (rounds 0–2) got superseded by a complete six-round re-run, and I carried the early run’s first three points into the writeup without noticing. In the complete run, round 0 is at 1.69× — already above the frontier, before any on-policy iteration has happened at all. The emergence signature does not survive. What survives is that the endpoint is well above the line; the mechanistic story I attached to it does not.

There’s a second measurement I like more, and it’s unaffected. At a fixed search budget of $K=16$, the number of wake tasks the model solves goes 95 → 132 → 137 → 147 → 159 → 194. That’s +104%. Capability that previously required search is being absorbed into the weights. Sleep-time compute, measured rather than asserted.

F4: by round 5, iterative on-policy consolidation is ~2.3× more KL-efficient for capability and ~3.5× more KL-efficient for retention than the single-step arms. Averaging failed this test. LR scheduling failed it. The consolidation unit failed it. Gap-targeting failed it. This passed — but see the caveat above: with round 0 already at 1.69×, I can no longer attribute the departure specifically to on-policyness, because round 0 isn’t on-policy. Something about this pipeline beats the five-arm frontier from the first step, and the frontier is fit from five single-seed points. The endpoint gap is large; the mechanism behind it is less pinned down than I claimed.

Now the part that stopped me cold. I checked HO-composition — novel compositions of the same primitives, the thing I actually cared about. Over six rounds, from a base of 0.083: 0.092 → 0.092 → 0.075 → 0.083 → 0.100 → 0.117. A +3.4 point wobble, non-monotone, dipping below base at round 2.

So the loop was working, and it was making the model better at what it already knew how to do. Same-structure specialisation, +14.2 points. Systematic generalisation, noise. For a project whose entire point was “can abstraction be amortised into weights,” that’s the wrong kind of success.

My first instinct was that this was an emergent-capability story and I needed more rounds. I checked before writing it up: context multiplicity — how many distinct compositions each primitive appears in — was pinned at exactly 2.00 for all six rounds. The loop wasn’t broadening. It was drilling.

九、唯一离开这条线的东西

到目前为止每个 arm 都是在 base 模型生成的固定语料上做一次 off-policy 更新。而 RL’s Razor 真正的处方是另一回事:很多小步,每一步都相对于「正在被更新的那个模型」是 on-policy 的。

第 $r$ 轮:从 $\theta_r$ 自己采样 $K=16$ 个候选 → 执行器验证 → 从 $\theta_r$ 出发 SFT 得到 $\theta_{r+1}$。每一步的 KL 都是最小的,因为数据来自正在迈这一步的那个模型。但能力仍然能跨轮累积,因为第 $r{+}1$ 轮的数据来自一个已经变强了的模型。

回到上面那个演示,把 on-policy 轨迹打开。在增益上它明显在线之上,在遗忘上明显在线之下

第 5 轮,KL = 0.0713:前沿预测 +6.3 分增益、+14.0 分遗忘。实测:增益 +14.2(2.26×),遗忘 +4.0(0.29×)。 两个轴同时变好。 这不是沿曲线移动,这是曲线本身被推动了。

我原本写的是:这个效应随迭代涌现——第 0 轮在前沿之下,从第 1 轮起跃升——并把它当作该机制的签名。这是错的,而且错因是我把两次运行拼在了一起。 一次只跑到第 2 轮的早期运行被一次完整的六轮重跑取代了,而我把早期运行的前三个点带进了文章却没发现。在完整运行里,第 0 轮是 1.69×——在任何 on-policy 迭代发生之前,它就已经在前沿之上了。那个「涌现签名」不成立。成立的只是:终点确实远在线上方;但我给它安的那套机制解释不成立。

还有一个测量我更喜欢,而且它不受影响。在固定搜索预算 $K=16$ 下,模型解开的 wake 任务数是 95 → 132 → 137 → 147 → 159 → 194。+104%。原本需要搜索才能获得的能力,正在被吸收进权重。 Sleep-time compute,是测出来的,不是宣称的。

F4:到第 5 轮,迭代式 on-policy 巩固在能力上的 KL 效率约为单步 arm 的 2.3 倍,在保留上约为 3.5 倍。 平均没通过这个检验。学习率调度没通过。巩固单元没通过。Gap 定向没通过。这个通过了——但注意上面那条更正:既然第 0 轮就已经是 1.69×,而第 0 轮并不是 on-policy 的,我就不能再把这个偏离专门归功于「on-policy 性」。这条流水线从第一步起就打赢了五点前沿,而那条前沿是用五个单 seed 的点拟合的。终点的差距很大;但它背后的机制,比我原先声称的要松得多。

然后是让我当场愣住的部分。我去看了 HO-composition——相同原语的全新组合,我真正在乎的那个东西。从 0.083 的基线出发,六轮下来:0.092 → 0.092 → 0.075 → 0.083 → 0.100 → 0.117。 一个 +3.4 分的摆动,非单调,第 2 轮还跌破了基线。

所以这个循环确实在工作,而它做的事是让模型在它本来就会的东西上更强。同结构专精化,+14.2 分。系统性泛化,噪声。对于一个「抽象能否被摊销进权重」的项目来说,这是错误类型的成功

我的第一反应是这属于「涌现能力」,多跑几轮就有了。写结论之前我去查了一下:context multiplicity——每个原语出现在多少个不同的组合里——六轮下来死死钉在 2.00。 这个循环没有在变宽。它在往下钻。

10. What finally moved composition

So change that one thing.

Same method, same model, same ~600 wake tasks, same everything — except spread the tasks over 48 structures instead of 12, which takes context multiplicity from 2.00 → 8.00.

十、最后让「组合」动起来的东西

那就把那一个变量改掉。

同样的方法,同样的模型,同样的约 600 个 wake 任务,其他全都一样——只把这些任务铺到 48 个结构上而不是 12 个,这让 context multiplicity 从 2.00 变成 8.00

Identical loop, identical budget. The only difference is how many distinct compositions the experience is spread over. Left: +3.4 points, non-monotone. Right: +15.8 points, monotone every round. Note that specialisation is about the same in both (+14.2 vs +11.7) — only composition depends on diversity.

HO-composition 0.142 → 0.300. +15.8 points, 2.11×, monotone across all five rounds. HO-surface +11.7. Wake tasks solved at fixed budget 103 → 163 (+58%). And still above the frontier: at KL 0.0519 the prediction is +4.6 gain / 10.2 forgetting, measured +11.7 (2.56×) / 6.7 (0.66×).

The honest caveat, which matters: the two runs use different novel-composition test sets, so the absolute levels aren’t comparable across runs. What’s comparable is each run’s delta from its own base, and the monotonicity. A +3.4 non-monotone wobble versus a +15.8 monotone climb over five consecutive rounds is not a subtle difference, but I’m not going to claim 0.300 > 0.117 as if those were the same scale.

F5: compositional generalisation can be amortised into weights — but the experience stream has to supply the compositional variation.

I first wrote this up as a conjunction of two ingredients, each useless alone: on-policy iteration as the carrier, diversity as the content. Then I looked at round 0, and that framing fell apart.

Round 0 is not on-policy. It samples from the base model, verifies, and takes one SFT step — the same protocol as the five single-step arms, at the same budget. So round 0 of each run is a clean controlled comparison in which the only difference is corpus width:

  round-0 novel-composition gain
Run A · multiplicity 2.0 +0.9
Run B · multiplicity 8.0 +9.1

One step. No iteration. Widening the corpus is worth ten times as much on composition, immediately — +9.1 of the eventual +15.8, or 58% of the total, before the loop has done anything at all. Iteration then adds the remaining +6.7 over four rounds and makes the climb monotone.

So it isn’t a conjunction. Diversity does not fail alone; it does most of the work in a single step. Iteration is a real but secondary multiplier. If you only have budget for one intervention, the answer isn’t close.

I find the specialisation column genuinely striking. HO-surface gain is +14.2 at low diversity and +11.7 at high diversity — essentially the same, slightly worse at high diversity. Diversity doesn’t make the loop better in general. It converts what the loop produces from memorised specialisation into transferable structure, and it does that without costing anything on the specialisation axis. Those are two different learning outcomes riding on the same optimisation, separated by one property of the data.

完全相同的循环,完全相同的预算。唯一的差别是经验被铺在多少个不同的组合上。左:+3.4 分,非单调。右:+15.8 分,每轮单调上升。注意专精化在两边差不多(+14.2 vs +11.7)——只有组合泛化依赖多样性

HO-composition 0.142 → 0.300。+15.8 分,2.11×,五轮全单调。 HO-surface +11.7。固定预算下解开的 wake 任务 103 → 163(+58%)。而且依然在前沿之上:KL 0.0519 处预测 +4.6 增益 / 10.2 遗忘,实测 +11.7(2.56×)/ 6.7(0.66×)。

必须说清的 caveat:两次实验用的是不同的 novel-composition 测试集,所以跨实验的绝对数值不可比。 可比的是各自相对自己 base 的 delta,以及单调性。「+3.4 且非单调」对「连续五轮 +15.8 且单调」不是什么微妙的差别,但我不会假装 0.300 > 0.117 是同一把尺子上的比较。

F5:组合泛化是可以被摊销进权重的——但经验流必须提供组合上的变化。

我最初把它写成两个成分的合取、缺一不可:on-policy 迭代是载体,多样性是内容。然后我去看了第 0 轮,这个框架就塌了。

第 0 轮不是 on-policy 的。 它从 base 模型采样、验证、做一步 SFT——和五个单步 arm 完全同一套协议、同样的预算。所以两次实验的第 0 轮,构成一个干净的受控对照,唯一的差别就是语料宽度

  第 0 轮 novel-composition 增益
Run A · multiplicity 2.0 +0.9
Run B · multiplicity 8.0 +9.1

一步。零迭代。光是把语料铺宽,在组合泛化上立刻值十倍——最终 +15.8 里的 +9.1,占 58%,而此时循环还什么都没做。迭代随后在四轮里补上剩下的 +6.7,并让上升变得单调。

所以它不是合取。多样性单独并不失败,它一步就完成了大部分工作。 迭代是真实的、但次要的乘数。如果你只有预算做一件事,答案没什么悬念。

我觉得专精化那一列真的很有意思。HO-surface 增益在低多样性下是 +14.2,高多样性下是 +11.7——基本一样,甚至略差多样性并没有让这个循环整体变强。 它做的事,是把这个循环产出的东西,从「记住的专精化」转换成「可迁移的结构」,而且在专精化那个轴上不收任何费用。同一个优化过程上跑着两种不同的学习结果,把它们分开的,只是数据的一个性质。

11. The theory that didn’t survive contact

Running parallel to all this was a much prettier idea, and I want to include it because of how it failed.

The proposal: forgetting isn’t a bug to be minimised, it’s a control parameter. Turn up the forgetting rate $\Gamma$ and you destroy memorised solutions faster than you destroy shared primitives — because a memorised solution is supported by one training composition, while a primitive is reinforced by every composition it appears in. Below some $\Gamma$ everything survives, including memorisation. Above some other $\Gamma$ even the primitives die. In between: a window where only the compositional solution is stable. A memorisation→generalisation phase transition, driven by forgetting.

And the beautiful part — the critical points are supposed to be fixed by the data statistics alone, with no free parameters:

\[\Gamma_{c1} = \frac{1}{N}, \qquad \Gamma_{c2} = \frac{k}{m}\]

where $N$ = distinct training compositions, $m$ = primitives, $k$ = primitives per task. A parameter-free prediction is a gift. It’s either right or it’s dead, and you can find out cheaply.

So: tiny decoder-only transformer, from scratch, SCAN-like compositional grammar, $\Gamma$ := weight decay (it is the $-\gamma a_f$ decay term, it’s a single monotone scalar, and grokking is known to be driven by exactly this). Sweep $N$ and $\Gamma$. Minutes per run.

十一、没能扛住接触的那个理论

和上面这些并行的,还有一个漂亮得多的想法。我想把它写进来,是因为它失败的方式很有意思。

主张是:遗忘不是要被最小化的 bug,它是一个控制参数。把遗忘率 $\Gamma$ 调高,你摧毁「被记住的解」的速度会快过摧毁「共享的原语」——因为一个被记住的解只被一个训练组合支撑,而一个原语被它出现过的每一个组合反复加固。低于某个 $\Gamma$,什么都活着,包括死记硬背;高于另一个 $\Gamma$,连原语都死了。中间那段:只有组合解是稳定的窗口。 一个由遗忘驱动的「记忆 → 泛化」相变。

而最漂亮的部分是——临界点应该仅由数据统计量决定,没有任何自由参数

\[\Gamma_{c1} = \frac{1}{N}, \qquad \Gamma_{c2} = \frac{k}{m}\]

其中 $N$ = 训练中不同组合的个数,$m$ = 原语总数,$k$ = 每个任务用几个原语。无自由参数的预言是一份礼物。 它要么对,要么死,而且验证起来很便宜。

于是:小 decoder-only transformer,从头训练,SCAN 式的组合文法,$\Gamma$ := weight decay(它就是动力学里那个 $-\gamma a_f$ 衰减项,是一个单调标量,而且 grokking 已知正是由它驱动的)。扫 $N$ 和 $\Gamma$。每次跑几分钟。

The predicted left edge is marked for each $N$. It isn’t there. $N=48$ is already flat-out at $\Gamma$ twenty times below its predicted edge; $N=12$ is at zero even inside its window. Switch to the second view for the two-seed fine sweep at the critical $N=24$, where the inverted U does show up — and where the collapsed runs show you how easy it would be to fool yourself.

The prediction fails. $N=48$ scores 0.385 at $\Gamma = 0.001$, twenty times below its predicted critical point — the transition should not have happened yet, and it’s already saturated. $N=12$ scores exactly zero across two full decades of $\Gamma$, including inside its predicted window. The left edge does not scale as $1/N$. There’s a crossover, not a phase transition with computable edges.

What survives is weaker but real: $N$ is the first-order control parameter and $\Gamma$ is second-order. Over the swept range, $N$ moves test accuracy 0.000 → 0.385 while $\Gamma$ moves it by at most ~0.10. Roughly, doubling the number of compositions is worth about thirty times the weight decay. And at the critical $N=24$ — and only there — $\Gamma$ genuinely does buy compositional generalisation, replicated across both seeds: 0.000 → 0.285 in one, 0.014 → 0.458 in the other, monotone in both.

Two things I got wrong here, both worth writing down.

First: I misread my own sweep. My initial $\Gamma$ grid was coarse — 0.01 / 0.1 / 1.0 — and it made a real rising branch look like a noise bump. It took a finer sweep to see it. Worse, at $N=8$ my grid never sampled inside the predicted window $[0.125, 0.25]$ at all. So $N=8$ doesn’t test the theory. It’s not evidence for or against; it’s just a hole in my design, and I only noticed because I went back and checked the predicted window against the grid I’d actually run.

Second, and this is the more dangerous one: I claimed an inverted U that my own data does not support. At high $\Gamma$, training sometimes just collapses — train accuracy 0.007 instead of 1.000 — and a collapsed run scores 0 on test. My instinct was that averaging those in with successful runs was manufacturing the descending branch. I checked. It isn’t: the peak sits at $\Gamma = 0.4$ whether you pool the collapsed runs or drop them. What pooling actually does is halve two points (0.285 → 0.1425 at $\Gamma=0.2$, 0.458 → 0.229 at 0.4) and stamp a dip at 0.2 that is purely seed 1’s optimiser failing.

The real problem is worse. Keep only the runs that actually fit the training set — train accuracy 1.000 — and there is no descending branch at all. Both seeds are monotone rising, end to end. The only points that fall are $\Gamma = 0.8$, where train accuracy is 0.892 and 0.964 — not 1.000. Every downturn in this experiment is a model that partially failed to fit, and the “inverted U” is my eye connecting a real rising branch to an optimisation artifact.

So the honest verdict is narrower than the one I first wrote down: the rising branch is real and replicated across seeds. The U is not established. And since which $\Gamma$ collapses a run varies by seed — seed 0 died at 0.4, seed 1 at 0.2 — the right-hand edge of the predicted window is untestable with this apparatus. Not “we measured it and it’s wrong.” Untestable. This is the one correction in this post I didn’t catch myself; it came out of re-deriving the pooled numbers instead of trusting the sentence I’d already written.

I also killed my own order parameter here. I’d defined $\Phi$ as a cross-task Fisher participation ratio — how many training tasks genuinely share each parameter — expecting it to go to 0 for pure memorisation and 1 for full compositionality. It peaked at 0.855 exactly where the model collapsed (train accuracy 0.279). Of course it did: a model that learned nothing has near-identical gradients on every task. $\Phi$ is meaningless across the collapse boundary. A structural order parameter that can’t distinguish “perfectly compositional” from “learned nothing” is not an order parameter.

每个 $N$ 的预测左边界都标出来了。它不在那儿。 $N=48$ 在预测边界下方 20 倍的 $\Gamma$ 处就已经拉满;$N=12$ 即使在它自己的窗口内部也是零。切到第二个视图看临界 $N=24$ 处的双 seed 精细扫描——倒 U 在那里确实出现了——同时也能看到那些训练崩塌的点,是多么容易骗到自己。

预言失败。 $N=48$ 在 $\Gamma = 0.001$ 处就得 0.385,比它的预测临界点低了 20 倍——相变本来还不该发生,而它已经饱和了。$N=12$ 在横跨两个数量级的 $\Gamma$ 上全是零,包括它预测窗口的内部。左边界不按 $1/N$ 缩放。这里有的是一个 crossover,而不是一个带可计算边界的相变。

活下来的结论更弱但是真的:$N$ 是一阶控制参数,$\Gamma$ 是二阶的。 在扫过的范围内,$N$ 把测试准确率从 0.000 推到 0.385,而 $\Gamma$ 最多推动约 0.10。粗略地说,组合数翻一倍,约等于 weight decay 变三十倍。 而在临界的 $N=24$——也只有在那里——$\Gamma$ 确实买到了组合泛化,两个 seed 都复现:一个 0.000 → 0.285,另一个 0.014 → 0.458,都是单调的。

这里我犯了两个错,都值得写下来。

第一:我误读了自己的扫描结果。 最初的 $\Gamma$ 网格太粗——0.01 / 0.1 / 1.0——它把一段真实的上升支看成了噪声凸起。得靠更细的扫描才看得出来。更糟的是,在 $N=8$ 处我的网格根本没在预测窗口 $[0.125, 0.25]$ 内部采过样。所以 $N=8$ 压根没有检验这个理论。它既不是支持证据也不是反对证据,它只是我实验设计上的一个洞——而我能发现,只是因为我回头拿「预测窗口」和「我实际跑过的网格」对了一遍。

第二,也是更危险的一个:我宣称了一个我自己的数据并不支持的倒 U。 在高 $\Gamma$ 下训练有时会直接崩掉——训练准确率 0.007 而不是 1.000——崩掉的 run 测试得 0 分。我的直觉是:把这些崩掉的 run 混进平均,凭空制造出了那条下降支。我去算了。并没有:不管你把崩塌的 run 混进去还是剔出来,峰值都在 $\Gamma = 0.4$。pooling 真正干的事,是把两个点砍掉一半($\Gamma=0.2$ 处 0.285 → 0.1425,0.4 处 0.458 → 0.229),并在 0.2 处压出一个凹陷——而那个凹陷纯粹是 seed 1 的优化器失败。

真正的问题更严重。只保留真正拟合上训练集的 run——训练准确率 1.000——那么下降支根本不存在。 两个 seed 从头到尾都是单调上升。唯一往下掉的点是 $\Gamma = 0.8$,而那里的训练准确率是 0.892 和 0.964——不是 1.000。这个实验里每一次下降,都是一个没能完全拟合的模型;所谓「倒 U」,是我的眼睛把一条真实的上升支和一个优化假象连了起来。

所以诚实的判决比我最初写下的要窄:上升支是真的,且跨 seed 复现。倒 U 不成立。 而且既然「哪个 $\Gamma$ 会让 run 崩掉」是随 seed 变的——seed 0 死在 0.4,seed 1 死在 0.2——预测窗口的右边界用这套装置根本无法检验。不是「我们测了,它是错的」,而是测不了。这是全文唯一一处不是我自己抓出来的更正:它来自重新推导 pooling 后的数字,而不是相信我早已写好的那句话。

我在这儿还顺手杀掉了自己的序参量。我定义了 $\Phi$ 为跨任务的 Fisher participation ratio——每个参数真正被多少个训练任务共享——预期纯记忆时趋于 0、完全组合时趋于 1。结果它的峰值 0.855 恰好出现在模型崩塌的地方(训练准确率 0.279)。这太正常了:一个什么都没学到的模型,在所有任务上的梯度几乎一模一样。$\Phi$ 在崩塌边界两侧是没有意义的。 一个分不清「完美组合」和「什么都没学到」的结构性序参量,不是序参量。

12. The wall: the loop can verify, but it can’t vary

Here’s where I actually am, and it’s the most important paragraph in this post.

F4 + F5 say: iterative on-policy consolidation plus high compositional diversity genuinely amortises compositional ability into weights. That’s a real, positive, replicated-across-rounds result, and it moved a frontier that eight other interventions couldn’t budge.

But look at where the diversity came from. I typed it in. The N=12 → N=48 change was me editing a config file. The loop did not discover that it needed broader compositional coverage, and it could not have generated it. Context multiplicity sat at exactly 2.00 for six rounds while the loop happily got 14 points better at specialisation.

That’s the bootstrap ceiling, and it’s the same wall as the operator-library ratchet in §5. Verification is self-sustaining. Variation is not. The executor gives you a free, perfect, drift-proof reward signal — you can run that loop forever without a human. But the loop only ever consolidates solutions to problems it can already occasionally solve, drawn from a distribution it can’t widen. It compresses. It doesn’t explore.

Card 5 in the graveyard is the attempt to fix this directly: preplay, dream-sleep, let the model generate its own novel compositions to train on. Held-out coverage went from 21 to 15. It got worse. The self-generated compositions were perfectly plausible and completely off-distribution relative to ARC’s human-designed tasks — sim-to-real, in a domain where I’d assumed there was no sim-to-real gap because both sides are just programs. There is one. The distribution of interesting problems is not something the model has a handle on.

So the honest summary of the whole programme:

Every algorithmic intervention landed on one frontier or nulled outright. Only data-distribution interventions moved anything. And the loop cannot supply its own data distribution.

That’s not a solved problem with a caveat. That’s an unsolved problem with a well-characterised shape, which is a better place to be than where I started, but I want to be clear about which one it is.

十二、那堵墙:这个循环能验证,但不能产生变化

这是我现在真正所在的位置,也是全文最重要的一段。

F4 + F5 说:迭代式 on-policy 巩固 + 高组合多样性,确实能把组合能力摊销进权重。这是一个真实的、正面的、跨五轮复现的结果,而且它推动了另外八种干预都推不动的那条前沿。

但看看那个「多样性」是哪来的。是我手敲进去的。 N=12 → N=48 这个改动,是我编辑了一个配置文件。这个循环并没有发现自己需要更宽的组合覆盖,它也生成不出来。 Context multiplicity 六轮死钉在 2.00,而循环在专精化上开开心心涨了 14 分。

这就是 bootstrap 天花板,和第五节里算子库棘轮撞的是同一堵墙。验证是自持的,变化不是。 执行器给你一个免费的、完美的、不会漂移的奖励信号——这个循环可以无人值守地永远跑下去。但它永远只在它已经偶尔能解开的问题上巩固解法,而这些问题来自一个它无法拓宽的分布。它做压缩,它不做探索。

坟场里的第 5 张卡就是直接去修这一点的尝试:preplay、做梦式睡眠,让模型自己生成新组合来训练。留出覆盖率从 21 掉到 15。更差了。 那些自己生成的组合完全合理,但相对 ARC 那些人类设计的任务完全偏离分布——sim-to-real,而且是在一个我原以为不存在 sim-to-real gap 的领域里(两边不都是程序吗)。gap 是存在的。「什么算有意思的问题」这个分布,模型手里没有把手。

所以整个项目诚实的总结是:

所有算法性的干预,要么落在同一条前沿上,要么直接 null。只有改变数据分布的干预推动了任何东西。而这个循环无法自己供给它的数据分布。

这不是「一个已解决的问题外加一条注意事项」。这是一个尚未解决、但形状已经被刻画清楚的问题——这比我起步时的位置好,但我想说清楚它到底是哪一种。

13. What I’d tell 2019 me

Measure function movement, not weight movement. Half this series was about slowing down per-dimension updates in weight space. Weight space is the wrong space — parametric symmetries make distance there unreliable, and my own gradient-conflict probe (a weight-space quantity) got falsified while the KL probe (a function-space quantity) rank-ordered every arm correctly. The 2019 intuition wasn’t wrong about there being a conserved cost. It was wrong about the currency.

“It’s all about time” was half right in the least useful way. Two timescales do matter — but through when the data is generated, not through how the updates are averaged. The fast/slow loop earns its keep because round $r{+}1$ samples from $\theta_r$, not because $\theta_{\text{slow}}$ filters $\theta_{\text{fast}}$. Averaging is a learning rate. Regenerating the experience is a mechanism.

A no-op is not a safe method. The single most useful diagnostic I built this year was looking at the initial training loss. ARM-A started at 0.058 and I nearly wrote it up as “instance consolidation is safe.” It wasn’t safe, it was inert. Any time a self-improvement method reports low forgetting, check whether it learned anything, and check it with a loss curve rather than a benchmark delta.

Pre-register, then actually go back and check the grid. The $N=8$ hole and the collapsed-run pooling would both have survived into a writeup if I hadn’t had frozen criteria to check against. Neither was caught by intuition. Both were caught by rereading what I’d promised to test.

The bottleneck is not the learning algorithm. I spent months on consolidation mechanisms and every single one landed on the same line. The two things that worked — on-policy regeneration and compositional diversity — are both properties of where the data comes from. If I get another run at this, it goes entirely into the variation problem: how does a closed loop widen its own distribution of problems, when the only free signal it has is a verifier that tells it whether it was right, and never tells it whether the question was worth asking?

I don’t have that one. Seven years in, that’s the honest state of it.

十三、我想对 2019 年的自己说

测函数的移动,不要测权重的移动。 这个系列有一半篇幅在讲怎么在权重空间里按维度压慢更新。权重空间是错的空间——参数对称性让那里的距离不可靠,而我自己那个梯度冲突探针(一个权重空间的量)被证伪了,KL 探针(一个函数空间的量)却把每个 arm 都排对了序。2019 年的直觉在「存在一个守恒的代价」这件事上没错,错在货币单位

「It’s all about time」对了一半,而且是最没用的那一半。 两个时间尺度确实重要——但重要在数据是什么时候生成的,而不是更新是怎么被平均的。快慢环之所以有价值,是因为第 $r{+}1$ 轮从 $\theta_r$ 采样,而不是因为 $\theta_{\text{slow}}$ 过滤了 $\theta_{\text{fast}}$。平均是一个学习率,重新生成经验才是一个机制。

空操作不等于安全的方法。 我今年做出来最有用的诊断,是去看训练的初始 loss。ARM-A 从 0.058 开始,我差一点就把它写成「实例巩固是安全的」。它不是安全,它是惰性。以后只要一个自我进化方法报告「遗忘很低」,先去查它到底学到了没有——而且用 loss 曲线查,不要用 benchmark 的 delta 查。

预注册之后,要真的回头去对一遍网格。 $N=8$ 那个洞、还有把崩塌 run 混进平均那件事,如果没有冻结的判据可以对照,两个都会活着进入结论。它们都不是靠直觉抓到的,都是靠重读我当初承诺要测什么抓到的。

瓶颈不在学习算法。 我在巩固机制上花了几个月,每一个都落在同一条线上。真正work的两件事——on-policy 重新生成、组合多样性——都是「数据从哪来」的性质。如果还有下一轮,我会把它全部投进「变化」这个问题:一个封闭的循环,要怎么拓宽它自己的问题分布? 尤其是当它唯一免费的信号是一个验证器——那个验证器只会告诉它答得对不对,永远不会告诉它这个问题值不值得问。

这个我还没有答案。做了七年,这就是它诚实的状态。


Everything above is 4B-scale with LoRA r=16 on q/v, in one narrow procedural domain, with n=150 forgetting probes and n=120 held-out splits. The consolidation-unit null is two seeds; the frontier rests on five arms; F4 and F5 are single-seed multi-round. The frontier claim is strongly suggestive, not established. I’ve tried to flag every caveat inline rather than collect them here, but that’s the summary version.


以上全部是 4B 规模、LoRA r=16(q/v)、单一狭窄程序化领域、遗忘探针 n=150、留出集 n=120 的结果。巩固单元的 null 有两个 seed;前沿建立在五个 arm 上;F4 和 F5 是单 seed 多轮。前沿这个主张是强暗示性的,不是已确立的。我尽量把每条 caveat 都就地标注而不是堆在这里,但这是它的汇总版。