Manuscript revision and pending experiment work, snapshot before restructuring

Clarity pass over the main text (36-item audit), Discussion rewrite and cut,
acknowledgements, Souly et al. as ref 62, lettered SI panels, model section
moved under Results; plus the untracked curriculum/society/compose/smol
configs, runners, figures, stats and tests that the SI already cites.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y64o8FKP7rCuXzC48pxpMm
This commit is contained in:
Giorgio Gilestro 2026-09-13 16:54:09 +01:00
parent e4804adabc
commit 84124de143
450 changed files with 52813 additions and 1202 deletions

2
.gitignore vendored
View file

@ -7,3 +7,5 @@ __pycache__/
results/**/results.parquet
models/
data/
configs/llm/_gen/
logs/

View file

@ -99,6 +99,10 @@ E4's whole purpose is to isolate the effect of teacher **decorrelation ρ**, so
**Finding (2026-07-05, HARD benchmark `llm_moe_hard_hpc` + `llm_directed_hard_hpc` — the 7B "fusion wins / no headroom" results were SATURATION artefacts; the law is HEADROOM, not base-size).** The easy families saturated 7B (strings & arith at 1.00), so the 7B nulls (moe: fusion 0.87 > union 0.84; directed ≈ soup) couldn't separate "refinements don't help at scale" from "tasks too easy." Built a **hard task variant** (`hard: true` in `tasks.py`: multi-step lists, Caesar ciphers / letter transforms, multi-step & larger arithmetic — same family labels & answer formats, threaded through `make_tasks`/`train_specialist`/runners; hard specialists cache separately as `spec_*_hard`) and re-ran both at 7B on Imperial CX3 (one L40S, 24 min, unsaturated: arith ≈0.48, strings 0.67, lists 0.34). **Both nulls flip back to the 0.5B ordering:** (1) **union beats fusion again — routing 0.500 > fusion 0.40** (soup 0.392/ties 0.400), the *same* 10-pt margin as 0.5B; fusion dilutes the fragile strings-specialist so hard (0.665 → soup 0.300) that soup even **trails the best single specialist** (0.425), while routing keeps it (0.670). (2) **directed selection beats soup again — 0.492 > 0.392** (+10 pts), recovering most of routing's benefit from one deployable merged model (lifts strings back to 0.630). **Correction to the earlier interpretation:** the `llm_moe_hpc` "regime flip" (fusion wins at 7B) and `llm_directed_hpc` "no headroom" were both driven by **task saturation, not base capability**. The operative variable is **headroom**: "merge, don't average" (union > fusion) and "directed sex" (selection > single blend) hold whenever there's room to lose to dilution — weak base (0.5B) *or* hard tasks at a strong base (7B-hard); fusion only wins in the degenerate corner where easy tasks let a strong base compose to the 1.00 ceiling. This vindicates E8's `max > mean` in real 7B weights once saturation is controlled. `configs/llm/{moe_hard,moe_hard_hpc,directed_hard_hpc}.yaml`, `hpc/llm_hard.pbs`, `results/llm_{moe,directed}_hard_hpc/`, +1 hard-task test (131 green).
**Finding (2026-09-11, three controls from the manuscript review; `results/llm_curriculum_v5_{stop3,decor}/`, `results/llm_*_hpc/s{1,2,3}/`).** (1) **Forced stop** (`merge_until: 3`): obligate merging through generation 2 then none finishes 0.793 vs the declinable merge's 0.792 (per seed 0.008/0.006/+0.011) — the veto's outcome is explained by *when* it stopped. (2) **Decorrelated curriculum** (`orders:` key; complementarity 0.00→0.70→0.00 instead of the Latin square's monotone fall): declines still rise with generation (0.44→0.89); pooled partial ρ(declined, complementarity | generation) = 0.07, CI (0.21, +0.09), partial ρ with generation +0.31. **The recombination-modifier / reduction-principle reading of Fig. 4B is withdrawn**; the Latin-square ρ = 0.57 was carried by generation (adapter age, skill count and destroyer arrival are confounded). What stands: one bit of selection per merge, or a fixed early stop, avoids the obligate-merge collapse at no cost against never merging. (3) **7B seeds 23** (33 min/seed, L40S): merge best specialist +0.066±0.036, routing soup +0.094±0.015, directed soup +0.073±0.031, all 3/3 seeds; *not* replicated: "soup below the best specialist on hard tasks" (1/3, mean +0.001) — softened in text. Stats: `figures/stats_llm_curriculum.py` (also the single source of curriculum arm labels, used by `make_figs`), `figures/stats_llm_7b_seeds.py`; `_figlib.load_seed_bundles` reads `s{seed}/` layouts.
**Finding (2026-09-12, four experiments from the dropped "Limits"; `results/llm_speciation/s{1,2,3}`, `results/llm_curriculum_v5_{early,late,early_obl,late_obl,cull}/s{1,2,3}`, `results/llm_{merge_seeds,moe_hard_seeds}_smol`).** GG's rule: a limitation that names a runnable experiment is run, not stated. (1) **Speciation seeds 23:** conflict cliff (merge 0.02/0.12/0.16 vs parents 0.230.25) and duration null (0.76→0.95) hold in 3/3 seeds; seed 1's cliff was the deepest. Fig. 5CD now has CI bands. (2) **Conflict-arrival curricula** (`orders:`; boolq/winogrande in generations 12 or 56, age and skill count rising identically): declines and the obligate collapse follow **generation, not conflict arrival** (partial ρ with conflict-present, generation controlled: 0.09, CI (0.45, 0.15); with generation: +0.45); conflict-early dips at arrival, recovers, collapses from generation 5; conflict-late collapses from generation 4 with its pair still to come. What stays confounded is adapter age with skill count. (3) **Second base lineage** (SmolLM2-1.7B-Instruct, `adapters_dir: models/llm_smol` because the specialist cache is keyed by family+seed only): FisherMuller replicates 5/5 (soup +0.049±0.022, TIES +0.097±0.020 over best specialist), headroom 3/3 with a larger margin (routing soup +0.162±0.036; soup below best specialist in 3/3). (4) **Differential reproduction** (`cull: true`, `cull_step`/`inherit_slot`): **parity again** — with selection, declinable 0.793 vs never-merge 0.804 (0.011±0.003, 3/3 below); selection none +0.007±0.030; recombination's early lead present with and without selection, gone by generation 5. The Discussion's prediction (selection turns speed into level) is withdrawn: under a curriculum that delivers every skill to every lineage the ceiling is what one adapter carries. Speciation adapters now live in `speciation_s{seed}/` (an array over seeds used to race on a shared dir). SI Figs. S14S16; stats in `figures/stats_llm_{curriculum,speciation_seeds,smol}.py`.
## Build order (blueprint §7) — respect the gate
1. Scaffold: repo layout (§5), container, pytest skeleton, config system, seeding utils. `make test` green.

View file

@ -2,7 +2,8 @@
# reproducibility source of truth; every target runs inside it via `uv run`.
.PHONY: env env-neural env-mnist env-llm env-notebooks test layer1 layer2 neural mnist llm \
llm-epistasis llm-society figures paper-figures paper notebooks clean
llm-epistasis llm-society llm-society-calib llm-society-calib-b llm-society-v2-smoke \
figures paper-figures paper notebooks clean
env: ## build .venv from the committed lockfile
uv sync --extra dev
@ -50,6 +51,26 @@ llm-epistasis: ## the controlled predictive test (feeds paper Fig. 3C-D) + i
llm-society: ## the composed society at LLM scale (C3): pilot; the campaign runs on HPC
uv run python -m llm.experiment configs/llm/society.yaml
llm-society-calib: ## v2 society calibration gates (prereg §4): stage A (families), then B (C2/C3/C5)
uv run python -m llm.experiment configs/llm/society_v2_calib_a.yaml
@echo "Review the C1 table, fix the 12 families in society_v2_calib_b.yaml, then: make llm-society-calib-b"
llm-society-calib-b: ## v2 calibration stage B over the chosen 12 families: transmission, cross, consensus
uv run python -m llm.experiment configs/llm/society_v2_calib_b.yaml
mkdir -p configs/llm/_gen
uv run python - <<'EOF'
import yaml; c = yaml.safe_load(open("configs/llm/society_v2_calib_b.yaml"))
for st in ("cross", "consensus"):
c["stage"] = st; c["output"] = {"dir": f"results/llm_society_v2_calib_b_{st}"}
yaml.safe_dump(c, open(f"configs/llm/_gen/calib_{st}.yaml", "w"), sort_keys=False)
EOF
uv run python -m llm.experiment configs/llm/_gen/calib_cross.yaml
uv run python -m llm.experiment configs/llm/_gen/calib_consensus.yaml
llm-society-v2-smoke: ## v2 society loop smoke (4 families, 4 agents, 2 generations, all arms)
uv run python -m llm.experiment configs/llm/society_v2_smoke.yaml
uv run python figures/plot_llm_society.py results/llm_society_v2_smoke
llm-seeds: ## multi-seed firm-up (heavy): merge x5, moe-hard x3, directed-hard x3
uv run python -m llm.experiment configs/llm/merge_seeds.yaml
uv run python -m llm.experiment configs/llm/moe_hard_seeds.yaml
@ -69,6 +90,7 @@ paper-figures: ## regenerate the manuscript figures (Fig. 1-7) and rebuild t
paper: paper-figures ## figures + compile both PDFs (needs tectonic)
cd paper/pnas && tectonic main.tex && tectonic si.tex
uv run python paper/pnas/build_lay_legends.py && cd paper/pnas && tectonic figure_legends_for_students.tex
env-notebooks: ## add Jupyter for the walkthrough notebooks
uv sync --extra dev --extra notebooks

View file

@ -13,11 +13,11 @@ Manuscript: `paper/pnas/main.md` (built to `paper/pnas/main.pdf`).
| Tier | What it is | Hardware | Determinism |
|---|---|---|---|
| Biological model | WrightFisher simulator over knowledge distributions (pure NumPy/SciPy) | Any laptop, no GPU | **Bitwise** from the master seed |
| Inheritance model | WrightFisher simulator over knowledge distributions (pure NumPy/SciPy) | Any laptop, no GPU | **Bitwise** from the master seed |
| Trained networks | RNN / MLP / VAE on a synthetic mode universe; convolutional VAE on MNIST | One consumer GPU (16 GB) | Statistical (GPU non-determinism documented in §5) |
| Language models | LoRA specialists on Qwen2.5-Instruct 0.5B / 7B | 0.5B: one 16 GB GPU · 7B: one L40S (46 GB) | Statistical; per-seed points reported |
The biological-model tier carries every quantitative claim in the paper and reproduces exactly on a
The inheritance-model tier carries every quantitative claim in the paper and reproduces exactly on a
laptop in minutes. The two AI tiers are confirmatory (signs, not magnitudes) and need a GPU.
## 2. Environment
@ -50,15 +50,15 @@ Tier by tier, by hand:
```bash
make test # correctness + closed-form scientific validation
make layer1 # the biological model: E1-E12, E14, learning kernel
make layer1 # the inheritance model: E1-E12, E14, learning kernel
make neural # trained networks (needs a GPU)
make mnist # real-MNIST tier + the Fig. 2B montage asset (needs torchvision)
make mnist # real-MNIST tier + the Fig. 2A montage asset (needs torchvision)
make llm # language-model prototypes (needs a GPU)
make llm-seeds # the multi-seed firm-ups behind Fig. 3A
make llm-epistasis # the controlled predictive test behind Fig. 3C-D
make llm-speciation # the LLM speciation tier behind Fig. 7E-F
make llm-seeds # the multi-seed firm-ups behind Fig. 3B
make llm-epistasis # the controlled predictive test behind Fig. 3D-E
make llm-speciation # the LLM speciation tier behind Fig. 5C-D
make figures # per-experiment figures, from committed parquets (no re-simulation)
make paper-figures # the manuscript figures Fig. 1-7 + rebuild the PDF body
make paper-figures # the manuscript figures Fig. 1-5 + rebuild the PDF body
```
## 4. The figure map
@ -66,31 +66,41 @@ make paper-figures # the manuscript figures Fig. 1-7 + rebuild the PDF body
Every manuscript panel, the artifact it is plotted from, the config that produced that artifact, and
that config's declared seed. All panels are drawn by `paper/pnas/make_figs.py` (function per figure);
`make paper-figures` regenerates all of them. Figures are a **pure function of committed artifacts**
no panel re-simulates anything, with the single documented exception of the Fig. 2B montage asset.
no panel re-simulates anything, with the single documented exception of the Fig. 2A montage asset.
| Panel | Drawn by | From artifact | Produced by config | Seed(s) |
|---|---|---|---|---|
| Fig. 1A, 1B | `fig1a()`, `fig1b()` | — (schematics; icons in `paper/pnas/figs/icons/`) | — | — |
| Fig. 2A | `fig2()` | `results/E2/` | `configs/layer1/E2.yaml` | 20260704 |
| Fig. 2B | `fig2()` | `results/mnist_collapse/mnist_montage.png` | `configs/neural/mnist_collapse.yaml` → asset from `figures/mnist_montage.py` | 20260705 |
| Fig. 3A | `fig3()` | `results/llm_merge_seeds/` | `configs/llm/merge_seeds.yaml` | 1, 2, 3, 4, 5 |
| Fig. 3B | `fig3()` | `results/llm_moe_hard_hpc/` | `configs/llm/moe_hard_hpc.yaml` (7B, HPC) | 1 (single run) |
| Fig. 3C, 3D | `fig3()` | `results/llm_epistasis/` + `results/llm_epistasis_compat/` | `configs/llm/epistasis.yaml`, `configs/llm/epistasis_compat.yaml` | 1, 2, 3 |
| Fig. 4A | `fig4()` | `results/E4/` | `configs/layer1/E4.yaml` | 20260704 |
| Fig. 4B | `fig4()` | `results/E8/` | `configs/layer1/E8.yaml` | 20260705 |
| Fig. 5A | `fig5()` | `results/E9/` | `configs/layer1/E9.yaml` | 20260705 |
| Fig. 5B | `fig5()` | `results/E10/` | `configs/layer1/E10.yaml` | 20260705 |
| Fig. 5C, 5D | `fig5()` | `results/E14/` | `configs/layer1/E14.yaml` | 20260709 |
| Fig. 6A, 6B, 6C | `fig6()` | `results/E11/` | `configs/layer1/E11.yaml` | 20260705 |
| Fig. 7A, 7B | `fig7()` | `results/E12/` | `configs/layer1/E12.yaml` | 12 |
| Fig. 7C | `fig7()` | `results/speciation_real/` | `configs/neural/speciation_real.yaml` | 13 |
| Fig. 7D | `fig7()` | `results/speciation_real_cliff/` | `configs/neural/speciation_real_cliff.yaml` | 13 |
| Fig. 7E, 7F | `fig7()` | `results/llm_speciation/` | `configs/llm/speciation.yaml` | 1 (single seed) |
| Fig. 2A | `fig2()` | `results/mnist_collapse/mnist_montage.png` | `configs/neural/mnist_collapse.yaml` → asset from `figures/mnist_montage.py` | 20260705 |
| Fig. 2B *(reference)* | `fig2()` | `results/E2/` | `configs/layer1/E2.yaml` | 20260704 |
| Fig. 3A | `fig3()` | — (schematic of the compared models; icons in `paper/pnas/figs/icons/`) | — | — |
| Fig. 3B | `fig3()` | `results/llm_merge_seeds/` | `configs/llm/merge_seeds.yaml` | 1, 2, 3, 4, 5 |
| Fig. 3C | `fig3()` | `results/llm_moe_hard_hpc/s{1,2,3}/` | `configs/llm/moe_hard_hpc.yaml` (7B, HPC; seeds 23 via `hpc/llm_7b_seeds.pbs`); statistics `figures/stats_llm_7b_seeds.py` | 1, 2, 3 |
| Fig. 3D, 3D | `fig3()` | `results/llm_epistasis/` + `results/llm_epistasis_compat/` | `configs/llm/epistasis.yaml`, `configs/llm/epistasis_compat.yaml` | 1, 2, 3 |
| Fig. 4A | `fig4()` | — (schematic; the syllabus grid is read from `configs/llm/curriculum_v5_s1.yaml` family order, complementarity from the veto bundle) | — | — |
| Fig. 4B | `fig4()` | `results/llm_curriculum_v5/`, `results/llm_curriculum_v5_veto/`, `results/llm_curriculum_v5_stop3/` | `configs/llm/curriculum_v5_s1.yaml` (seeds 23 via `hpc/llm_curriculum.pbs`), `configs/llm/curriculum_v5_veto.yaml` (seeds 23 via `hpc/llm_veto.pbs`), `configs/llm/curriculum_v5_stop3.yaml` (seeds 23 via `hpc/llm_curriculum_controls.pbs`) | 13 (training seeds; fixed test splits) |
| Fig. 4C | `fig4()` | `results/llm_curriculum_v5_veto/`, `results/llm_curriculum_v5_decor/` | `configs/llm/curriculum_v5_veto.yaml`, `configs/llm/curriculum_v5_decor.yaml` (seeds 23 via `hpc/llm_curriculum_controls.pbs`); statistics `figures/stats_llm_curriculum.py` | 13 |
| Fig. S14 | `figures/plot_curriculum_timing.py` | `results/llm_curriculum_v5_{early,late,early_obl,late_obl}/s{1,2,3}/` | `configs/llm/curriculum_v5_{early,late}[_obl].yaml` via `hpc/llm_curriculum_timing.pbs`; statistics `figures/stats_llm_curriculum.py` | 1, 2, 3 |
| Fig. S15 | `figures/plot_curriculum_cull.py` | `results/llm_curriculum_v5_cull/s{1,2,3}/` | `configs/llm/curriculum_v5_cull.yaml` via `hpc/llm_cull.pbs`; statistics `figures/stats_llm_curriculum.py` | 1, 2, 3 |
| Fig. S16 | `figures/plot_llm_smol.py` | `results/llm_{merge_seeds,moe_hard_seeds}_smol/` (+ the Qwen bundles) | `configs/llm/{merge_seeds,moe_hard_seeds}_smol.yaml` via `hpc/llm_smol.pbs`; statistics `figures/stats_llm_smol.py` | 15; 13 |
| Fig. 4D, 4D, 4E *(reference)* | `fig4()` | `results/E11/` | `configs/layer1/E11.yaml` | 20260705 |
| Fig. 5A | `fig5()` | `results/speciation_real/` | `configs/neural/speciation_real.yaml` | 13 |
| Fig. 5B | `fig5()` | `results/speciation_real_cliff/` | `configs/neural/speciation_real_cliff.yaml` | 13 |
| Fig. 5C, 5D | `fig5()` | `results/llm_speciation/s{1,2,3}/` | `configs/llm/speciation.yaml` (seeds 23 via `hpc/llm_speciation_seeds.pbs`); statistics `figures/stats_llm_speciation_seeds.py` | 1, 2, 3 |
| Fig. 5E, 5F *(reference)* | `fig5()` | `results/E12/` | `configs/layer1/E12.yaml` | 12 |
**Single-run panels.** Fig. 3B and Fig. 7EF come from single-seed runs and are reported as
sign-level confirmations, not estimates; the manuscript labels them as such. Every other panel is
replicated (biological-model panels over 12100 internal replicates; Fig. 3A over five training
seeds; Fig. 3CD over three).
Panels marked *(reference)* are inheritance-model results included to set the expectation the
real-model panels are read against, not as findings in their own right.
**Inheritance-model results reported in SI only.** These have no real-model counterpart in the paper
and reproduce known results, so they are cited as reference values and plotted only in SI, from their
per-experiment figures: blending vs union (E4), FisherMuller super-parent (E8), outbreeding
depression (E9), directed recombination (E10), and mate-pool breadth (E14), drawn by
`figures/plot_E{4,8,9,10,14}.py` from `results/E{4,8,9,10,14}/`.
**Replication.** Every panel is replicated: inheritance-model panels over 12100 internal
replicates; Fig. 3B over five training seeds; Fig. 3C, 3DE, 4AB and 5CD over three (Fig. 5CD
was single-seed until 2026-09-12; seeds 23 ran via `hpc/llm_speciation_seeds.pbs`).
### Results reported in the text but not plotted in the manuscript

View file

@ -0,0 +1,31 @@
# Generation-0 gate for the composition experiment (prereg v3 §4: C1 base, C3 specialists,
# C4 replication). Trains the two founders and measures everything at t=0 without iterating.
#
# Base choice (C1, measured 2026-09-07): Qwen2.5-1.5B **base, not Instruct**. Zero-shot GSM-Hard
# program-aided: Instruct-1.5B 0.500, Instruct-3B 0.417, base-3B 0.633 — all far too strong, since a
# base that already has the skills makes the specialists' contribution vacuous. Base-1.5B scores
# 0.067, within noise of the 0.059 Llama-2-7B starting point in LoRA Soups (COLING 2025), so the
# published composition effect has room to appear.
experiment: llm_compose_gate
kind: llm_compose
base_model: Qwen/Qwen2.5-1.5B
seed: 1
generations: 0 # gate only: train founders, measure t=0, stop
arms: [dry]
n_hard: 150 # composed target (GSM8k-Hard, program-aided) — SE ~0.04
n_gsm8k: 100 # math own-skill probe
n_mbpp: 80 # code own-skill probe (execution-verified)
n_probe: 40 # shared prompts for rho
k_inherit: 300
epochs: 3
conf_gate: 0.85
g: 0.10
spec_train: 1200
spec_epochs: 3
max_new_tokens: 320
batch_size: 16
score_batch_size: 4
train_batch_size: 2 # 16 GB A4000; the fp32 logit upcast at vocab 152k is the constraint
train_max_len: 448
lora: {r: 16, alpha: 32}
output: {dir: results/llm_compose_gate}

View file

@ -0,0 +1,34 @@
# Generation-0 gate, second configuration (prereg v3 §4a): the composed target is MATH-500
# (competition maths, level >= 3, numeric answers), and merge weights are *selected* on a disjoint
# validation split rather than fixed at 0.5/0.5.
#
# Why: on GSM8k-Hard the code parent alone reaches 0.427, because once code removes the arithmetic
# burden the base's own reasoning suffices — so maths is not scarce and E8's premise fails. MetaMathQA
# is built from GSM8K *and* MATH, so MATH-500 tests reasoning the specialist has and the base lacks.
# Founders are shared with the first gate (same experiment name), so this costs evaluation only.
experiment: llm_compose_gate
kind: llm_compose
base_model: Qwen/Qwen2.5-1.5B
seed: 1
generations: 0
arms: [dry]
target: math500
n_hard: 120 # test split (level>=3 pool is 271; 70% test / 30% val, disjoint)
n_hard_val: 50 # val split, screens the merge weights only
merge_weights: [[0.5, 0.5], [0.3, 0.7], [0.2, 0.8]]
n_gsm8k: 100
n_mbpp: 80
n_probe: 40
k_inherit: 300
epochs: 3
conf_gate: 0.85
g: 0.10
spec_train: 1200
spec_epochs: 3
max_new_tokens: 320
batch_size: 16
score_batch_size: 8
train_batch_size: 2
train_max_len: 448
lora: {r: 16, alpha: 32}
output: {dir: results/llm_compose_gate_math500}

View file

@ -0,0 +1,55 @@
# The composition campaign, seed 1 (prereg v3, amended after the generation-0 sweep of 2026-09-07).
#
# Arms. The gen-0 sweep found that the merge *weighting* dominates the operator: the a-priori 0.5/0.5
# blend fails under both operators (surplus -0.020 cat, -0.093 linear) while a selected weight passes
# (+0.080 linear at 0.2/0.8, +0.027 cat at 0.3/0.7). Weights are therefore chosen each generation on a
# disjoint validation split (E10, directed recombination) in every arm, and the operator is an
# explicit per-arm setting:
# dry — linear operator, no grounding [H2, H3, H5: does composition survive drift?]
# grounded — linear operator, g = 0.10 [H4: does immigration arrest it?]
# dry_cat — concatenation operator, no grounding [H6, revised: does the operator ordering hold
# across generations, or only at gen 0?]
experiment: llm_compose
kind: llm_compose
base_model: Qwen/Qwen2.5-1.5B
seed: 1
generations: 6
arms:
- dry
- grounded
- dry_cat
g: 0.1
n_hard: 150
n_gsm8k: 150
n_mbpp: 100
n_probe: 60
k_inherit: 300
epochs: 3
conf_gate: 0.85
spec_train: 1200
spec_epochs: 3
max_new_tokens: 320
batch_size: 16
score_batch_size: 4
train_batch_size: 2
train_max_len: 448
resume: true
lora:
r: 16
alpha: 32
output:
dir: results/llm_compose/s1
arm_ops:
dry: linear
grounded: linear
dry_cat: cat
n_hard_val: 60
merge_weights:
- - 0.5
- 0.5
- - 0.3
- 0.7
- - 0.2
- 0.8
- - 0.1
- 0.9

View file

@ -0,0 +1,27 @@
# G2 probe (prereg v4 §5): does forgetting occur AT ALL under the real curriculum length?
# The 3-family gate showed none — but with only 3 families, ample adapter capacity and a generous
# replay budget, none was expected. This runs the full nine-family curriculum on ONE lineage with
# replay switched OFF, which is the harshest condition the design can offer. If early families still
# do not decay, the families are too mutually compatible and too easy for their own good, and the
# society has no forgetting to rescue — a design-level stop.
experiment: llm_curriculum_g2
kind: llm_curriculum
base_model: Qwen/Qwen2.5-1.5B
seed: 1
families: [strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen, roman]
lineages: 1
generations: 9
arms: [isolated]
baselines: []
n_new: 300
n_replay: 0 # replay OFF — the harshest forgetting condition
n_test: 60
n_val: 30
epochs: 3
lr: 1.0e-4
max_new_tokens: 32
batch_size: 32
train_batch_size: 2
train_max_len: 448
lora: {r: 16, alpha: 32}
output: {dir: results/llm_curriculum_g2}

View file

@ -0,0 +1,28 @@
# Gate run for the curriculum society (prereg v4 §5). G0 is the decisive one: with a single lineage
# and no recombination, does cumulative capability CLIMB across generations? If capability cannot
# accumulate in the simplest arm, no outcome of the full design is interpretable — stop.
# Also exercises G1 (inheritance transmits), G2 (forgetting occurs without full replay) and, with
# lineages=3, G3 (recombination combines disjoint knowledge).
experiment: llm_curriculum_gate
kind: llm_curriculum
base_model: Qwen/Qwen2.5-1.5B
seed: 1
families: [strings, setops, numtheory] # 3 families, 3 generations — the smallest climb test
lineages: 3
generations: 3
arms: [isolated, society]
baselines: []
n_new: 300
n_replay: 120
n_test: 60
n_val: 30
epochs: 3
lr: 1.0e-4
operator: linear
merge_weights: [[0.5, 0.5], [0.3, 0.7], [0.7, 0.3]]
max_new_tokens: 32
batch_size: 32
train_batch_size: 2
train_max_len: 448
lora: {r: 16, alpha: 32}
output: {dir: results/llm_curriculum_gate}

View file

@ -0,0 +1,27 @@
# Calibration for the real-dataset curriculum (prereg v4 §5 gates, re-run on the new families).
# Stage A (this config): base accuracy on every candidate, and one specialist per candidate trained
# from the base on its train split — the C1 band is base <= 0.40 and specialist >= 0.60.
# Stage B (curriculum_v5_g2.yaml, after selection): one lineage, zero replay, all chosen families in
# sequence — the forgetting probe; mean drop across families learned before the last must be >= 0.15,
# and it must not be carried by a single family (the v4 failure: one pair at +0.65, the rest ~0).
experiment: llm_curriculum_v5_calib
kind: llm_curriculum
base_model: Qwen/Qwen2.5-1.5B
seed: 1
families: [gsm8k, mbpp, boolq, mnli, sst2, csqa, arc, winogrande, squad, nq_open, hellaswag]
lineages: 11 # one lineage per candidate = each trains only its own family at gen 0
generations: 1 # gen 0 only: founders; the recorded acc_* rows ARE the specialist table
arms: [isolated]
baselines: []
n_new: 300
n_replay: 0
n_test: 60
n_val: 20
epochs: 3
lr: 1.0e-4
max_new_tokens: 48 # spans / short text need more room than a label
batch_size: 24
train_batch_size: 2
train_max_len: 512
lora: {r: 16, alpha: 32}
output: {dir: results/llm_curriculum_v5_calib}

View file

@ -0,0 +1,24 @@
# Stage A, second pass: the six candidates that missed the C1 band at 300 founder examples, retrained
# at the budget every working specialist in this project has used (v2, v3: 1200 x 3 epochs).
# Decides whether the curriculum has six families or nine.
experiment: llm_curriculum_v5_calib_b
kind: llm_curriculum
base_model: Qwen/Qwen2.5-1.5B
seed: 1
families: [gsm8k, mbpp, sst2, csqa, winogrande, nq_open]
lineages: 6
generations: 1
arms: [isolated]
baselines: []
n_new: 1200
n_replay: 0
n_test: 60
n_val: 20
epochs: 3
lr: 1.0e-4
max_new_tokens: 48
batch_size: 24
train_batch_size: 2
train_max_len: 512
lora: {r: 16, alpha: 32}
output: {dir: results/llm_curriculum_v5_calib_b}

View file

@ -0,0 +1,48 @@
# Differential reproduction (manuscript revision 2026-09-12): `cull: true` re-founds the lowest-scoring
# lineage from the highest-scoring one after every generation's measurement (truncation selection at
# fixed N = 3; the slot keeps its curriculum order). Latin square as in v5. Arms: isolated + cull
# (selection without recombination) and declinable society + cull (selection with recombination),
# read against the existing isolated and veto arms. Prediction (Discussion): recombination's early
# lead becomes a level advantage; parity again is the null. Seeds via hpc/llm_cull.pbs.
experiment: llm_curriculum_v5_cull
kind: llm_curriculum
base_model: Qwen/Qwen2.5-1.5B
seed: 1
families:
- mnli
- arc
- hellaswag
- squad
- boolq
- winogrande
lineages: 3
generations: 6
arms:
- isolated
- society
baselines: []
allow_veto: true
n_new: 300
n_replay: 150
n_test: 60
n_val: 20
epochs: 3
lr: 0.0001
operator: linear
merge_weights:
- - 0.5
- 0.5
- - 0.3
- 0.7
- - 0.7
- 0.3
max_new_tokens: 48
batch_size: 24
train_batch_size: 2
train_max_len: 512
lora:
r: 16
alpha: 32
output:
dir: results/llm_curriculum_v5_cull
cull: true

View file

@ -0,0 +1,35 @@
# Decorrelated curriculum (manuscript review, 2026-09-11). In the Latin square partner complementarity
# falls monotonically with generation (1.0, 1.0, 0.8, 0.67, 0.33, 0.0), so the veto's acceptance curve
# is collinear with adapter age. Here every lineage starts with the same non-destroyer family (mnli),
# then diverges maximally, then converges: complementarity 0.00, 0.67, 0.70, 0.58, 0.33, 0.00 by
# generation. Same six families, same G, destroyers (boolq, winogrande) spread across lineages as
# in the Latin square. Arms: the declinable merge (`society` + `allow_veto`) and its never-merge
# reference under the same curriculum. Pre-registered readout: tasks/prereg-llm-society-v4.md §8g.
experiment: llm_curriculum_v5_decor
kind: llm_curriculum
base_model: Qwen/Qwen2.5-1.5B
seed: 1
families: [mnli, arc, hellaswag, squad, boolq, winogrande]
orders:
- [mnli, arc, hellaswag, squad, boolq, winogrande]
- [mnli, squad, boolq, winogrande, arc, hellaswag]
- [mnli, winogrande, arc, hellaswag, squad, boolq]
lineages: 3
generations: 6
arms: [isolated, society]
baselines: []
allow_veto: true
n_new: 300
n_replay: 150
n_test: 60
n_val: 20
epochs: 3
lr: 1.0e-4
operator: linear
merge_weights: [[0.5, 0.5], [0.3, 0.7], [0.7, 0.3]]
max_new_tokens: 48
batch_size: 24
train_batch_size: 2
train_max_len: 512
lora: {r: 16, alpha: 32}
output: {dir: results/llm_curriculum_v5_decor}

View file

@ -0,0 +1,66 @@
# Conflict-early curriculum (manuscript revision 2026-09-12): the two families whose answer
# conventions conflict (boolq yes/no, winogrande 1/2) arrive in generations 1-2 of every lineage; the
# four compatible families follow in rotated orders. Paired with curriculum_v5_late (same families,
# conflict in generations 5-6) to decouple the ARRIVAL of conflicting conventions from adapter age and
# skill count, which rise identically in both. Arms: isolated + declinable society (decline timing).
# Seeds 1-3 via hpc/llm_curriculum_timing.pbs.
experiment: llm_curriculum_v5_early
kind: llm_curriculum
base_model: Qwen/Qwen2.5-1.5B
seed: 1
families:
- mnli
- arc
- hellaswag
- squad
- boolq
- winogrande
lineages: 3
generations: 6
arms:
- isolated
- society
baselines: []
allow_veto: true
n_new: 300
n_replay: 150
n_test: 60
n_val: 20
epochs: 3
lr: 0.0001
operator: linear
merge_weights:
- - 0.5
- 0.5
- - 0.3
- 0.7
- - 0.7
- 0.3
max_new_tokens: 48
batch_size: 24
train_batch_size: 2
train_max_len: 512
lora:
r: 16
alpha: 32
output:
dir: results/llm_curriculum_v5_early
orders:
- - boolq
- winogrande
- mnli
- arc
- hellaswag
- squad
- - winogrande
- boolq
- arc
- hellaswag
- squad
- mnli
- - boolq
- winogrande
- hellaswag
- squad
- mnli
- arc

View file

@ -0,0 +1,61 @@
# Obligate-merge arm of the conflict-early curriculum (collapse timing): same orders as
# curriculum_v5_early, society arm only, no veto. Seeds 1-3 via hpc/llm_curriculum_timing.pbs.
experiment: llm_curriculum_v5_early_obl
kind: llm_curriculum
base_model: Qwen/Qwen2.5-1.5B
seed: 1
families:
- mnli
- arc
- hellaswag
- squad
- boolq
- winogrande
lineages: 3
generations: 6
arms:
- society
baselines: []
allow_veto: false
n_new: 300
n_replay: 150
n_test: 60
n_val: 20
epochs: 3
lr: 0.0001
operator: linear
merge_weights:
- - 0.5
- 0.5
- - 0.3
- 0.7
- - 0.7
- 0.3
max_new_tokens: 48
batch_size: 24
train_batch_size: 2
train_max_len: 512
lora:
r: 16
alpha: 32
output:
dir: results/llm_curriculum_v5_early_obl
orders:
- - boolq
- winogrande
- mnli
- arc
- hellaswag
- squad
- - winogrande
- boolq
- arc
- hellaswag
- squad
- mnli
- - boolq
- winogrande
- hellaswag
- squad
- mnli
- arc

View file

@ -0,0 +1,25 @@
# Stage B (prereg v4 §5 G2, v5 families): does forgetting occur, and is it general rather than one pair?
# One lineage, zero replay, all six selected families in sequence at the founder budget that passed C1.
# Pass: mean drop across families learned before the last >= 0.15, with no single family carrying
# more than 50% of the total drop (the v4 failure mode: one pair at +0.65, the rest ~0).
experiment: llm_curriculum_v5_g2
kind: llm_curriculum
base_model: Qwen/Qwen2.5-1.5B
seed: 1
families: [mnli, arc, hellaswag, squad, boolq, winogrande]
lineages: 1
generations: 6
arms: [isolated]
baselines: []
n_new: 300
n_replay: 0
n_test: 60
n_val: 20
epochs: 3
lr: 1.0e-4
max_new_tokens: 48
batch_size: 24
train_batch_size: 2
train_max_len: 512
lora: {r: 16, alpha: 32}
output: {dir: results/llm_curriculum_v5_g2}

View file

@ -0,0 +1,63 @@
# Conflict-late curriculum (manuscript revision 2026-09-12): the four compatible families first, in
# rotated orders; boolq (yes/no) and winogrande (1/2) arrive in generations 5-6. Pair of
# curriculum_v5_early. Arms: isolated + declinable society (decline timing).
experiment: llm_curriculum_v5_late
kind: llm_curriculum
base_model: Qwen/Qwen2.5-1.5B
seed: 1
families:
- mnli
- arc
- hellaswag
- squad
- boolq
- winogrande
lineages: 3
generations: 6
arms:
- isolated
- society
baselines: []
allow_veto: true
n_new: 300
n_replay: 150
n_test: 60
n_val: 20
epochs: 3
lr: 0.0001
operator: linear
merge_weights:
- - 0.5
- 0.5
- - 0.3
- 0.7
- - 0.7
- 0.3
max_new_tokens: 48
batch_size: 24
train_batch_size: 2
train_max_len: 512
lora:
r: 16
alpha: 32
output:
dir: results/llm_curriculum_v5_late
orders:
- - mnli
- arc
- hellaswag
- squad
- boolq
- winogrande
- - arc
- hellaswag
- squad
- mnli
- winogrande
- boolq
- - hellaswag
- squad
- mnli
- arc
- boolq
- winogrande

View file

@ -0,0 +1,61 @@
# Obligate-merge arm of the conflict-late curriculum (collapse timing): same orders as
# curriculum_v5_late, society arm only, no veto. Seeds 1-3 via hpc/llm_curriculum_timing.pbs.
experiment: llm_curriculum_v5_late_obl
kind: llm_curriculum
base_model: Qwen/Qwen2.5-1.5B
seed: 1
families:
- mnli
- arc
- hellaswag
- squad
- boolq
- winogrande
lineages: 3
generations: 6
arms:
- society
baselines: []
allow_veto: false
n_new: 300
n_replay: 150
n_test: 60
n_val: 20
epochs: 3
lr: 0.0001
operator: linear
merge_weights:
- - 0.5
- 0.5
- - 0.3
- 0.7
- - 0.7
- 0.3
max_new_tokens: 48
batch_size: 24
train_batch_size: 2
train_max_len: 512
lora:
r: 16
alpha: 32
output:
dir: results/llm_curriculum_v5_late_obl
orders:
- - mnli
- arc
- hellaswag
- squad
- boolq
- winogrande
- - arc
- hellaswag
- squad
- mnli
- winogrande
- boolq
- - hellaswag
- squad
- mnli
- arc
- boolq
- winogrande

View file

@ -0,0 +1,37 @@
# The curriculum society campaign, seed 1 (prereg v4, v5 curriculum — tasks/prereg-llm-society-v4.md).
# Runs all four arms then the three matched-budget baselines. The PBS array (hpc/llm_curriculum.pbs)
# derives one (seed, arm) config per element from this template for seeds 2-3.
#
# Six real-dataset families selected by calibration (§8a): five pass C1 at >= 0.60, winogrande (0.57)
# is the sixth under the recorded amendment. L = 3 lineages, G = 6 generations; the cyclic Latin square
# gives complementarity 1.0 at generation 2 and 0.0 at generation 6 (H6 tests the shape).
#
# Arms: isolated (no recombination) · society (decorrelated contemporary) · society_dry (contemporary,
# self-generated replay) · seed_bank (own ancestor at t-3 — temporal complementarity)
# Baselines at matched examples: sequential · single_shot_merge · joint
experiment: llm_curriculum_v5
kind: llm_curriculum
base_model: Qwen/Qwen2.5-1.5B # base, not Instruct: measured 0.011 on these families untrained
seed: 1
families: [mnli, arc, hellaswag, squad, boolq, winogrande]
lineages: 3
generations: 6
arms: [isolated, society, society_dry, seed_bank]
baselines: [sequential, single_shot_merge, joint]
n_new: 300 # founder/acquisition budget that passed C1 (1200 destabilised training)
n_replay: 150 # fixed TOTAL, split across families seen: protection thins as the curriculum grows
n_test: 60 # per family, from the TEST split — reporting only
n_val: 20 # per family, from the TRAIN split — merge-weight selection only
epochs: 3
lr: 1.0e-4 # continued-training rate (fresh founders use train_lora_on_tasks' 2e-4)
ancestor_depth: 3
operator: linear
merge_weights: [[0.5, 0.5], [0.3, 0.7], [0.7, 0.3]]
baseline_weights: [[0.333, 0.333, 0.334], [0.5, 0.25, 0.25], [0.25, 0.5, 0.25], [0.25, 0.25, 0.5]]
max_new_tokens: 48
batch_size: 24
train_batch_size: 2
train_max_len: 512
resume: true
lora: {r: 16, alpha: 32}
output: {dir: results/llm_curriculum_v5/s1}

View file

@ -0,0 +1,30 @@
# Forced-stop control for the declinable merge (manuscript review, 2026-09-11). Identical to the v5
# `society` arm except that recombination is switched off from generation 3 onward (`merge_until: 3`):
# obligate merging at generations 0-2, none afterwards. This is the fixed "merge early, then stop"
# schedule the veto arm must be compared against, because in the seed-1 veto run lineages declined
# 1/3 of merges at generations 0-2 and 3/3 at 3-5. Pre-registered readout: tasks/prereg-llm-society-v4.md §8g.
experiment: llm_curriculum_v5_stop3
kind: llm_curriculum
base_model: Qwen/Qwen2.5-1.5B
seed: 1
families: [mnli, arc, hellaswag, squad, boolq, winogrande]
lineages: 3
generations: 6
arms: [society]
baselines: []
allow_veto: false
merge_until: 3
n_new: 300
n_replay: 150
n_test: 60
n_val: 20
epochs: 3
lr: 1.0e-4
operator: linear
merge_weights: [[0.5, 0.5], [0.3, 0.7], [0.7, 0.3]]
max_new_tokens: 48
batch_size: 24
train_batch_size: 2
train_max_len: 512
lora: {r: 16, alpha: 32}
output: {dir: results/llm_curriculum_v5_stop3}

View file

@ -0,0 +1,31 @@
# The veto arm (GG, 2026-09-08): identical to v5's `society` arm in every respect except that
# "keep the parent unchanged" is an admissible offspring. v5 made merging obligate — every lineage
# merged every generation and had to accept the result, which is not what anyone would build.
#
# Reference points from v5 seed 1: society 0.211 (obligate merging) vs isolated 0.814 (never merges).
# The diagnostic that matters is `veto_used`: if lineages decline every merge the arm degenerates to
# `isolated`, and that is itself the answer.
experiment: llm_curriculum_v5_veto
kind: llm_curriculum
base_model: Qwen/Qwen2.5-1.5B
seed: 1
families: [mnli, arc, hellaswag, squad, boolq, winogrande]
lineages: 3
generations: 6
arms: [society]
baselines: []
allow_veto: true
n_new: 300
n_replay: 150
n_test: 60
n_val: 20
epochs: 3
lr: 1.0e-4
operator: linear
merge_weights: [[0.5, 0.5], [0.3, 0.7], [0.7, 0.3]]
max_new_tokens: 48
batch_size: 24
train_batch_size: 2
train_max_len: 512
lora: {r: 16, alpha: 32}
output: {dir: results/llm_curriculum_v5_veto}

View file

@ -0,0 +1,30 @@
# Second base lineage (manuscript revision 2026-09-12): the Fisher-Muller replication of merge_seeds on
# SmolLM2-1.7B-Instruct (HuggingFaceTB, Apache-2.0, Llama architecture) instead of Qwen. adapters_dir is
# moved because the specialist cache is keyed by family and seed only. Runs via hpc/llm_smol.pbs.
experiment: llm_merge_seeds_smol
kind: llm_merge
seed: 1
seeds:
- 1
- 2
- 3
- 4
- 5
n_replicates: 1
base_model: HuggingFaceTB/SmolLM2-1.7B-Instruct
families:
- lists
- strings
- arith
n_train: 600
n_test: 100
epochs: 3
lora:
r: 16
alpha: 32
merges:
- soup
- ties
output:
dir: results/llm_merge_seeds_smol
adapters_dir: models/llm_smol

View file

@ -0,0 +1,31 @@
# Second base lineage (manuscript revision 2026-09-12): the headroom-rule replication of moe_hard_seeds
# (union vs fusion, hard tasks, 3 seeds) on SmolLM2-1.7B-Instruct. Runs via hpc/llm_smol.pbs.
experiment: llm_moe_hard_seeds_smol
kind: llm_moe
seed: 1
seeds:
- 1
- 2
- 3
n_replicates: 1
base_model: HuggingFaceTB/SmolLM2-1.7B-Instruct
hard: true
families:
- lists
- strings
- arith
n_train: 400
n_test: 80
n_route: 32
epochs: 3
lora:
r: 16
alpha: 32
operators:
- soup
- ties
- moe_oracle
- moe_learned
output:
dir: results/llm_moe_hard_seeds_smol
adapters_dir: models/llm_smol

View file

@ -0,0 +1,17 @@
# Calibration stage A (prereg §4, gates C1a-c + C4): every candidate family's base / specialist
# accuracy, specialist confidence AUC, pairwise functional conflict, gen-0 behavioural distance.
# GG reviews the printed table and fixes the 12-family set in society_v2_calib_b.yaml + the campaign.
# ~1 h on a 16 GB GPU (17 specialists at 600 x 3 epochs, cached and reused by stage B).
experiment: llm_society_v2_calib_a
kind: llm_society_calib
stage: families
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 1
families: [lists, strings, arith, roman, binary, sortletters, setops, numtheory, mixedtoken,
caesar, vectors, progression, charfreq, digits, liststats, alphabet, prime]
n_test: 100
n_probe: 10
spec_train: 600
spec_epochs: 3
lora: {r: 16, alpha: 32}
output: {dir: results/llm_society_v2_calib_a}

View file

@ -0,0 +1,20 @@
# Calibration stage A, second pass (prereg §4 amendments, 2026-09-07): the first pass left only six
# families in band. Changes, each recorded in the prereg: founder budget 600 -> 1200 x 3 (uniform);
# roman / binary / prime prompt spaces enlarged; three new learnable candidates (wordlen,
# lettercount, sumeven); the specialist UPPER bound relaxed 0.90 -> 1.00 (the headroom argument it
# encoded concerns fusion composing to a ceiling, not transmission/assembly). Families whose
# specialist scored <= 0.20 in pass 1 (sortletters, caesar, progression) are not re-trained.
experiment: llm_society_v2_calib_a2
kind: llm_society_calib
stage: families
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 1
families: [lists, strings, arith, roman, binary, setops, numtheory, mixedtoken, vectors, charfreq,
digits, liststats, alphabet, prime, wordlen, lettercount, sumeven]
spec_hi: 1.0
n_test: 100
n_probe: 10
spec_train: 1200
spec_epochs: 3
lora: {r: 16, alpha: 32}
output: {dir: results/llm_society_v2_calib_a2}

View file

@ -0,0 +1,25 @@
# Calibration stage B (prereg §4, gates C2 / C3 / C5) over the 12 families chosen from stage A.
# Run three times with stage: transmission | cross | consensus (see Makefile `llm-society-calib`).
# Families = stage-A pass-2 option 1 (L=9, gate 0.41; prereg §4a) — pending GG's go.
experiment: llm_society_v2_calib_b
kind: llm_society_calib
stage: transmission
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 1
families: [strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen, roman]
# C2: examples-per-family k and epochs to sweep; probe_families are the three whose retention is
# measured (spread across answer types: list / word / int).
probe_families: [setops, alphabet, digits] # list / letter / integer answers
ks: [25, 50, 100, 150]
epochs_grid: [2, 3]
# C3: the two-founder cross (union-distil vs best-of-6 linear-merge-distil)
cross: [setops, alphabet]
k_inherit: 100
epochs: 3
n_candidates: 6
n_test: 100
n_probe: 10
spec_train: 1200
spec_epochs: 3
lora: {r: 16, alpha: 32}
output: {dir: results/llm_society_v2_calib_b}

View file

@ -0,0 +1,20 @@
# C2b (prereg §4a amendment under review): confidence-gated inheritance. The pre-registered C2 failed
# (retention ≤ 0.81 at k ≤ 150 despite competent supply): the child fits eight families of confident
# garbage alongside the one competent family. Here the child learns only prompts its source is
# confident on (verifier-free). Measures retention by gate τ, the Youden τ* separating own- from
# off-family confidence, and the off-family harm of ungated inheritance.
experiment: llm_society_v2_calib_b_transmission_conf
kind: llm_society_calib
stage: transmission_conf
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 1
families: [strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen, roman]
probe_families: [setops, alphabet, digits]
k_pool: 300 # prompts per family answered by the source (2700 total); gate keeps a subset
taus: [0.5, 0.7, 0.85]
epochs: 3
n_test: 100
spec_train: 1200
spec_epochs: 3
lora: {r: 16, alpha: 32}
output: {dir: results/llm_society_v2_calib_b_transmission_conf}

View file

@ -0,0 +1,43 @@
experiment: llm_society_v2_calib_b_cross_gated
kind: llm_society_calib
stage: cross
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 1
families:
- strings
- setops
- numtheory
- mixedtoken
- digits
- alphabet
- prime
- wordlen
- roman
probe_families:
- setops
- alphabet
- digits
ks:
- 25
- 50
- 100
- 150
epochs_grid:
- 2
- 3
cross:
- setops
- alphabet
k_inherit: 300
epochs: 3
n_candidates: 6
n_test: 100
n_probe: 10
spec_train: 1200
spec_epochs: 3
lora:
r: 16
alpha: 32
output:
dir: results/llm_society_v2_calib_b_cross_gated
conf_gate: 0.5

View file

@ -0,0 +1,43 @@
experiment: llm_society_v2_calib_c3b_e6
kind: llm_society_calib
stage: cross
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 1
families:
- strings
- setops
- numtheory
- mixedtoken
- digits
- alphabet
- prime
- wordlen
- roman
probe_families:
- setops
- alphabet
- digits
ks:
- 25
- 50
- 100
- 150
epochs_grid:
- 2
- 3
cross:
- setops
- alphabet
k_inherit: 300
epochs: 6
n_candidates: 6
n_test: 100
n_probe: 10
spec_train: 1200
spec_epochs: 3
lora:
r: 16
alpha: 32
output:
dir: results/llm_society_v2_calib_c3b_e6
conf_gate: 0.5

View file

@ -0,0 +1,43 @@
experiment: llm_society_v2_calib_c3b_r64
kind: llm_society_calib
stage: cross
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 1
families:
- strings
- setops
- numtheory
- mixedtoken
- digits
- alphabet
- prime
- wordlen
- roman
probe_families:
- setops
- alphabet
- digits
ks:
- 25
- 50
- 100
- 150
epochs_grid:
- 2
- 3
cross:
- setops
- alphabet
k_inherit: 300
epochs: 3
n_candidates: 6
n_test: 100
n_probe: 10
spec_train: 1200
spec_epochs: 3
lora:
r: 64
alpha: 128
output:
dir: results/llm_society_v2_calib_c3b_r64
conf_gate: 0.5

View file

@ -0,0 +1,43 @@
experiment: llm_society_v2_calib_c3b_r64e6
kind: llm_society_calib
stage: cross
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 1
families:
- strings
- setops
- numtheory
- mixedtoken
- digits
- alphabet
- prime
- wordlen
- roman
probe_families:
- setops
- alphabet
- digits
ks:
- 25
- 50
- 100
- 150
epochs_grid:
- 2
- 3
cross:
- setops
- alphabet
k_inherit: 300
epochs: 6
n_candidates: 6
n_test: 100
n_probe: 10
spec_train: 1200
spec_epochs: 3
lora:
r: 64
alpha: 128
output:
dir: results/llm_society_v2_calib_c3b_r64e6
conf_gate: 0.5

View file

@ -0,0 +1,43 @@
experiment: llm_society_v2_calib_c3c_t85e3
kind: llm_society_calib
stage: cross
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 1
families:
- strings
- setops
- numtheory
- mixedtoken
- digits
- alphabet
- prime
- wordlen
- roman
probe_families:
- setops
- alphabet
- digits
ks:
- 25
- 50
- 100
- 150
epochs_grid:
- 2
- 3
cross:
- setops
- alphabet
k_inherit: 300
epochs: 3
n_candidates: 6
n_test: 100
n_probe: 10
spec_train: 1200
spec_epochs: 3
lora:
r: 16
alpha: 32
output:
dir: results/llm_society_v2_calib_c3c_t85e3
conf_gate: 0.85

View file

@ -0,0 +1,43 @@
experiment: llm_society_v2_calib_c3c_t85e6
kind: llm_society_calib
stage: cross
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 1
families:
- strings
- setops
- numtheory
- mixedtoken
- digits
- alphabet
- prime
- wordlen
- roman
probe_families:
- setops
- alphabet
- digits
ks:
- 25
- 50
- 100
- 150
epochs_grid:
- 2
- 3
cross:
- setops
- alphabet
k_inherit: 300
epochs: 6
n_candidates: 6
n_test: 100
n_probe: 10
spec_train: 1200
spec_epochs: 3
lora:
r: 16
alpha: 32
output:
dir: results/llm_society_v2_calib_c3c_t85e6
conf_gate: 0.85

View file

@ -0,0 +1,29 @@
# v2 society campaign, seed 1 (prereg tasks/prereg-llm-society-v2.md §3). The PBS array
# (hpc/llm_society_v2.pbs) derives one config per (seed, arm) from this template; running it directly
# runs all four arms sequentially (the local-GPU hedge).
#
# CALIBRATION-DEPENDENT FIELDS — set after gates C1/C2 pass, before submission:
# families : the 9 selected by stage A pass 2 (C1, gate 0.41) — SET
# k_inherit / epochs / conf_gate : from stage B (C2 + C2b), 2026-09-07 — SET
experiment: llm_society_v2
kind: llm_society_v2
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 1
families: [strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen, roman]
agents: 9
generations: 12
arms: [full, no_grounding, no_sex, no_diversity]
g: 0.85
lam: 0.3
max_mate_use: 2
n_test: 27 # per family -> 243 overall (SE 0.032)
n_val: 13 # per family -> 117 (selection signal)
n_conf: 13 # per family, fresh each generation
k_inherit: 300 # per family, fresh each generation (C2b: k=300 is the main retention driver)
epochs: 3 # child SFT epochs (C2b)
conf_gate: 0.85 # confidence-gated inheritance, all arms (C2b mean retention 0.88; C3c: tight gate = 6-epoch retention at 3 epochs)
spec_train: 1200
spec_epochs: 3
resume: true
lora: {r: 16, alpha: 32}
output: {dir: results/llm_society_v2/s1}

View file

@ -0,0 +1,29 @@
# v2 society campaign, seed 2 (prereg tasks/prereg-llm-society-v2.md §3). The PBS array
# (hpc/llm_society_v2.pbs) derives one config per (seed, arm) from this template; running it directly
# runs all four arms sequentially (the local-GPU hedge).
#
# CALIBRATION-DEPENDENT FIELDS — set after gates C1/C2 pass, before submission:
# families : the 9 selected by stage A pass 2 (C1, gate 0.41) — SET
# k_inherit / epochs / conf_gate : from stage B (C2 + C2b), 2026-09-07 — SET
experiment: llm_society_v2
kind: llm_society_v2
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 2
families: [strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen, roman]
agents: 9
generations: 12
arms: [full, no_grounding, no_sex, no_diversity]
g: 0.85
lam: 0.3
max_mate_use: 2
n_test: 27 # per family -> 243 overall (SE 0.032)
n_val: 13 # per family -> 117 (selection signal)
n_conf: 13 # per family, fresh each generation
k_inherit: 300 # per family, fresh each generation (C2b: k=300 is the main retention driver)
epochs: 3 # child SFT epochs (C2b)
conf_gate: 0.85 # confidence-gated inheritance, all arms (C2b mean retention 0.88; C3c: tight gate = 6-epoch retention at 3 epochs)
spec_train: 1200
spec_epochs: 3
resume: true
lora: {r: 16, alpha: 32}
output: {dir: results/llm_society_v2/s2}

View file

@ -0,0 +1,29 @@
# v2 society campaign, seed 3 (prereg tasks/prereg-llm-society-v2.md §3). The PBS array
# (hpc/llm_society_v2.pbs) derives one config per (seed, arm) from this template; running it directly
# runs all four arms sequentially (the local-GPU hedge).
#
# CALIBRATION-DEPENDENT FIELDS — set after gates C1/C2 pass, before submission:
# families : the 9 selected by stage A pass 2 (C1, gate 0.41) — SET
# k_inherit / epochs / conf_gate : from stage B (C2 + C2b), 2026-09-07 — SET
experiment: llm_society_v2
kind: llm_society_v2
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 3
families: [strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen, roman]
agents: 9
generations: 12
arms: [full, no_grounding, no_sex, no_diversity]
g: 0.85
lam: 0.3
max_mate_use: 2
n_test: 27 # per family -> 243 overall (SE 0.032)
n_val: 13 # per family -> 117 (selection signal)
n_conf: 13 # per family, fresh each generation
k_inherit: 300 # per family, fresh each generation (C2b: k=300 is the main retention driver)
epochs: 3 # child SFT epochs (C2b)
conf_gate: 0.85 # confidence-gated inheritance, all arms (C2b mean retention 0.88; C3c: tight gate = 6-epoch retention at 3 epochs)
spec_train: 1200
spec_epochs: 3
resume: true
lora: {r: 16, alpha: 32}
output: {dir: results/llm_society_v2/s3}

View file

@ -0,0 +1,29 @@
# v2 society campaign, seed 4 (prereg tasks/prereg-llm-society-v2.md §3). The PBS array
# (hpc/llm_society_v2.pbs) derives one config per (seed, arm) from this template; running it directly
# runs all four arms sequentially (the local-GPU hedge).
#
# CALIBRATION-DEPENDENT FIELDS — set after gates C1/C2 pass, before submission:
# families : the 9 selected by stage A pass 2 (C1, gate 0.41) — SET
# k_inherit / epochs / conf_gate : from stage B (C2 + C2b), 2026-09-07 — SET
experiment: llm_society_v2
kind: llm_society_v2
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 4
families: [strings, setops, numtheory, mixedtoken, digits, alphabet, prime, wordlen, roman]
agents: 9
generations: 12
arms: [full, no_grounding, no_sex, no_diversity]
g: 0.85
lam: 0.3
max_mate_use: 2
n_test: 27 # per family -> 243 overall (SE 0.032)
n_val: 13 # per family -> 117 (selection signal)
n_conf: 13 # per family, fresh each generation
k_inherit: 300 # per family, fresh each generation (C2b: k=300 is the main retention driver)
epochs: 3 # child SFT epochs (C2b)
conf_gate: 0.85 # confidence-gated inheritance, all arms (C2b mean retention 0.88; C3c: tight gate = 6-epoch retention at 3 epochs)
spec_train: 1200
spec_epochs: 3
resume: true
lora: {r: 16, alpha: 32}
output: {dir: results/llm_society_v2/s4}

View file

@ -0,0 +1,21 @@
# Smoke test for the v2 society loop: tiny everything, all four arms, resume exercised by the
# per-generation checkpoint. ~20 min on a 16 GB GPU. Exit 0 + figure renders = pass.
experiment: llm_society_v2_smoke
kind: llm_society_v2
base_model: Qwen/Qwen2.5-0.5B-Instruct
seed: 1
families: [lists, roman, binary, setops]
agents: 4
generations: 2
arms: [full, no_grounding, no_sex, no_diversity]
g: 0.85
lam: 0.3
n_test: 8 # per family
n_val: 5 # per family
n_conf: 5 # per family, fresh each generation
k_inherit: 20 # per family
epochs: 2
spec_train: 120
spec_epochs: 2
lora: {r: 16, alpha: 32}
output: {dir: results/llm_society_v2_smoke}

View file

@ -21,6 +21,28 @@ def load_bundle(results_dir: str | Path) -> tuple[pd.DataFrame, dict]:
return df, resolved["source_config"]
def load_seed_bundles(results_dir: str | Path) -> tuple[pd.DataFrame, dict]:
"""Load a per-seed bundle layout ``results_dir/s{seed}/results.parquet`` into one frame.
Each sub-bundle gets a ``seed`` column from its directory name (the HPC array-job layout, one
element per seed). A flat single-seed bundle is accepted too, tagged with its manifest seed.
Returns (frame, source config of the first seed).
"""
results_dir = Path(results_dir)
subs = sorted(results_dir.glob("s[0-9]*/results.parquet"))
if not subs:
df, cfg = load_bundle(results_dir)
if "seed" not in df.columns:
df = df.assign(seed=int(cfg.get("seed", 1)))
return df, cfg
frames, cfg0 = [], None
for p in subs:
df, cfg = load_bundle(p.parent)
cfg0 = cfg0 or cfg
frames.append(df.assign(seed=int(p.parent.name[1:])))
return pd.concat(frames, ignore_index=True), cfg0
def mean_ci(df: pd.DataFrame, by: str, value: str, ci: float = 0.95):
"""Return (index, mean, half-width) for a normal-approx CI of ``value`` grouped by ``by``."""
from scipy import stats
@ -38,3 +60,23 @@ def savefig(fig, results_dir: str | Path, name: str) -> None:
fig.savefig(results_dir / f"{name}.png", dpi=150, bbox_inches="tight")
fig.savefig(results_dir / f"{name}.pdf", bbox_inches="tight")
print(f"wrote {results_dir}/{name}.png and .pdf")
def letter_axes(fig, x: float = -0.1, y: float = 1.04, fontsize: float = 13) -> None:
"""Letter every data axes of ``fig`` A, B, C ... in reading order (top row first, left to right).
Twin axes and colourbars share a frame with a lettered axes and are skipped. Call once, after
every axes exists and before saving.
"""
seen: list[tuple[float, float]] = []
axes = []
for ax in fig.axes:
b = ax.get_position()
key = (round(b.x0, 3), round(b.y0, 3))
if key in seen or b.width < 0.05: # twin axes / colourbars
continue
seen.append(key); axes.append(ax)
axes.sort(key=lambda a: (-round(a.get_position().y0, 2), a.get_position().x0))
for i, ax in enumerate(axes):
ax.text(x, y, chr(ord("A") + i), transform=ax.transAxes, fontsize=fontsize, fontweight="bold",
va="bottom", ha="left", clip_on=False)

View file

@ -21,7 +21,7 @@ from pathlib import Path
import matplotlib.pyplot as plt
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, mean_ci, savefig # noqa: E402
from _figlib import load_bundle, mean_ci, savefig, letter_axes # noqa: E402
def main(results_dir: str = "results/E10") -> None:
@ -55,9 +55,8 @@ def main(results_dir: str = "results/E10") -> None:
title="Directed sex stays ≥ parents; blind sex\nfalls far below (outbreeding depression)")
ax.legend(frameon=False, fontsize=9)
fig.suptitle("E10 — directed sex beats biological sex: mate choice + offspring selection + "
"unbounded parents rescue recombination where blind sex fails", y=1.02, fontsize=11)
fig.tight_layout()
letter_axes(fig)
savefig(fig, results_dir, "E10")

View file

@ -26,7 +26,7 @@ from pathlib import Path
import matplotlib.pyplot as plt
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
from _figlib import load_bundle, savefig, letter_axes # noqa: E402
def main() -> None:
@ -40,14 +40,14 @@ def main() -> None:
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
panels = [
("best_n", "best fitness / global optimum", "(A) the champion: best model in the population",
"best fitness peaks at INTERMEDIATE breadth\non rugged landscapes (the peak shifts left as K rises)"),
("mean_n", "mean fitness / global optimum", "(B) the typical model: population mean",
"monotonically favoured by wide breadth\n(panmixia lifts the whole population)"),
("diversity", "diversity (mean pairwise Hamming)", "(C) standing diversity",
"monotonically destroyed by breadth\n(promiscuity homogenises; monogamy preserves)"),
("best_n", "best fitness / global optimum",
"Best model peaks at intermediate breadth on rugged\nlandscapes (the peak shifts left as $K$ rises)"),
("mean_n", "mean fitness / global optimum",
"Population mean rises monotonically with breadth\n(panmixia lifts the whole population)"),
("diversity", "diversity (mean pairwise Hamming)",
"Standing diversity falls monotonically with breadth\n(promiscuity homogenises; monogamy preserves)"),
]
for ax, (col, ylab, title, subtitle) in zip(axes, panels):
for ax, (col, ylab, title) in zip(axes, panels):
for K in Ks:
g = (last[last["K"] == K].groupby("breadth")[col]
.agg(["mean", "sem"]).reset_index())
@ -55,12 +55,11 @@ def main() -> None:
marker="o", lw=1.8, capsize=2, color=colors[K], label=f"K={K}")
ax.set_xscale("log")
ax.set(xlabel="mate-pool breadth (monogamous ← → promiscuous)", ylabel=ylab)
ax.set_title(f"{title}\n{subtitle}", fontsize=9)
ax.set_title(title, fontsize=9)
ax.legend(title="ruggedness", frameon=False, fontsize=8)
fig.suptitle("E14 — monogamy vs promiscuity: the best mate-pool breadth shrinks as skills get more entangled",
y=1.02, fontsize=13)
fig.tight_layout()
letter_axes(fig)
savefig(fig, "results/E14", "E14")

View file

@ -17,7 +17,7 @@ import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, mean_ci, savefig # noqa: E402
from _figlib import load_bundle, mean_ci, savefig, letter_axes # noqa: E402
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from knowledge.analysis import critical_grounding, reduce_to_stationary # noqa: E402
@ -88,7 +88,7 @@ def main(results_dir: str = "results/E2") -> None:
ax.errorbar(mg, Mm, yerr=Mci, fmt="o-", color="#9467bd", capsize=3,
label="tail truth-mass alive")
ax.set(xlabel="grounding fraction $g$", ylabel="fraction of tail retained",
title="Tail stays largely unrescued at feasible g\n(rises with g; motivates E4/E6)")
title="Tail stays largely unrescued at feasible $g$\n(rises only slowly with $g$)")
ax.legend(frameon=False, fontsize=9)
# Panel D: per-rarity-band survival across g (band 0 = rarest)
@ -106,9 +106,8 @@ def main(results_dir: str = "results/E2") -> None:
title=r"Per-rarity band: the $m\,p^*_i\gtrsim1$ threshold (deep lags)")
ax.legend(frameon=False, fontsize=8)
fig.suptitle("E2 — a critical grounding ratio $g^\\star \\ll 1$ rescues diversity; "
"the deep tail needs recombination", y=1.0, fontsize=13)
fig.tight_layout()
letter_axes(fig)
savefig(fig, results_dir, "E2")

View file

@ -18,7 +18,7 @@ import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
from _figlib import load_bundle, savefig, letter_axes # noqa: E402
def main(results_dir: str = "results/E3") -> None:
@ -43,7 +43,7 @@ def main(results_dir: str = "results/E3") -> None:
color=colors[pol], alpha=0.2)
ax.set(xlabel="generation",
ylabel=f"tail items alive in region {target}",
title=f"Target region {target} (exercised): matched holds, uniform collapses")
title=f"Target region {target} (exercised):\nmatched holds, uniform collapses")
ax.legend(frameon=False)
# Panel 2: stationary tail survival per region, uniform vs matched
@ -60,12 +60,12 @@ def main(results_dir: str = "results/E3") -> None:
ax.annotate("exercised", (target, ax.get_ylim()[1] * 0.9), fontsize=8,
ha="center", color="gray")
ax.set(xlabel="region", ylabel="stationary tail items alive",
title="Uniform spreads thin; matched concentrates on the exercised region",
title="Uniform spreads thin;\nmatched concentrates on the exercised region",
xticks=regions)
ax.legend(frameon=False)
fig.suptitle("E3 — grounding must overlap the content it protects", y=1.02)
fig.tight_layout()
letter_axes(fig)
savefig(fig, results_dir, "E3")

View file

@ -16,7 +16,7 @@ import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
from _figlib import load_bundle, savefig, letter_axes # noqa: E402
def U_closed(K_T, rho, q):
@ -37,9 +37,9 @@ def main(results_dir: str = "results/E4") -> None:
ax = axes[0]
for K, c in zip(K_Ts, colors):
sub = g0[g0["K_T"] == K].groupby("rho")["union_coverage"].mean()
ax.plot(sub.index, sub.values, "o", color=c, label=f"K_T={K}")
ax.plot(sub.index, sub.values, "o", color=c, label=f"$K_T$={K}")
ax.plot(rhos, [U_closed(K, r, q) for r in rhos], "-", color=c, lw=1)
ax.set(xlabel=r"teacher correlation $\rho$", ylabel="union tail coverage",
ax.set(xlabel=r"parent correlation $\rho$", ylabel="union tail coverage",
title=r"Supply: union matches $U(K_T,\rho,q)$")
ax.legend(frameon=False, fontsize=8)
@ -48,11 +48,11 @@ def main(results_dir: str = "results/E4") -> None:
for K, c in zip(K_Ts, colors):
sub = g0[g0["K_T"] == K].groupby("rho")
ax.plot(sub["surviving_max"].mean().index, sub["surviving_max"].mean().values,
"-o", color=c, label=f"K_T={K}", ms=4)
"-o", color=c, label=f"$K_T$={K}", ms=4)
ax.plot(sub["surviving_mean"].mean().index, sub["surviving_mean"].mean().values,
"--", color=c, lw=1, alpha=0.7)
ax.set(xlabel=r"teacher correlation $\rho$", ylabel="surviving tail coverage",
title="Realised: max-merge (solid) rises;\nmean-distill (dashed) stays flat")
ax.set(xlabel=r"parent correlation $\rho$", ylabel="surviving tail coverage",
title="Realised: max-merge (solid) rises;\nmean-mixture (dashed) stays flat")
ax.legend(frameon=False, fontsize=8)
# Panel C: surviving vs K_T at rho=0, both operators — the recombination benefit
@ -61,17 +61,16 @@ def main(results_dir: str = "results/E4") -> None:
mx = r0.groupby("K_T")["surviving_max"].agg(["mean", "sem"])
mn = r0.groupby("K_T")["surviving_mean"].agg(["mean", "sem"])
ax.errorbar(mx.index, mx["mean"], yerr=1.96 * mx["sem"], fmt="-o",
color="#1f77b4", capsize=3, label="max-merge (M2N2-style)")
color="#1f77b4", capsize=3, label="max-merge (union-preserving)")
ax.errorbar(mn.index, mn["mean"], yerr=1.96 * mn["sem"], fmt="--s",
color="#d62728", capsize=3, label="mean-mixture distillation")
ax.set(xlabel="number of teachers $K_T$", ylabel="surviving tail coverage",
ax.set(xlabel="number of parents $K_T$", ylabel="surviving tail coverage",
title=r"Benefit needs a union-preserving merge ($\rho=0$)",
xticks=K_Ts)
ax.legend(frameon=False, fontsize=9)
fig.suptitle("E4 — recombination supplies the tail; only a union-preserving merge "
"realises it in the pupil", y=1.03)
fig.tight_layout()
letter_axes(fig)
savefig(fig, results_dir, "E4")

View file

@ -15,7 +15,7 @@ import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
from _figlib import load_bundle, savefig, letter_axes # noqa: E402
def main(results_dir: str = "results/E5") -> None:
@ -30,14 +30,14 @@ def main(results_dir: str = "results/E5") -> None:
# Panel 1: H trajectories
ax = axes[0]
series = [("greedy", 1.0, "#d62728", "greedy"),
("qd", 1.0, "#ff7f0e", "qd (α=1)"),
("qd", 2.0, "#1f77b4", "qd (α=2)"),
("qd", 1.0, "#ff7f0e", "quality-diversity (α=1)"),
("qd", 2.0, "#1f77b4", "quality-diversity (α=2)"),
("none", 1.0, "#2ca02c", "none (grounding only)")]
for mode, a, c, lab in series:
s = arm(mode, a).groupby("generation")["heterozygosity"].mean()
ax.plot(s.index, s.values, color=c, label=lab)
ax.set(xlabel="generation", ylabel="heterozygosity $H$",
title="Greedy collapses; QD maintains diversity")
title="Greedy collapses;\nquality-diversity maintains diversity")
ax.legend(frameon=False, fontsize=8)
# Panel 2: stationary H vs alpha for qd, with greedy/none reference lines
@ -45,19 +45,19 @@ def main(results_dir: str = "results/E5") -> None:
qd = df[(df["mode"] == "qd") & (df["generation"] >= last)]
st = qd.groupby("novelty_alpha")["heterozygosity"].agg(["mean", "sem"])
ax.errorbar(st.index, st["mean"], yerr=1.96 * st["sem"], fmt="-o",
color="#ff7f0e", capsize=3, label="qd")
color="#ff7f0e", capsize=3, label="quality-diversity")
for mode, c in (("greedy", "#d62728"), ("none", "#2ca02c")):
h = arm(mode, 1.0)
h = h[h["generation"] >= last]["heterozygosity"].mean()
ax.axhline(h, ls="--", color=c, label=f"{mode}")
ax.set(xlabel=r"novelty exponent $\alpha$", ylabel="stationary $H$",
title="QD maintains H above greedy for all α")
title="Quality-diversity keeps $H$\nabove greedy for all α")
ax.legend(frameon=False, fontsize=9)
# Panel 3: stationary support size per arm
ax = axes[2]
arms = [("greedy", 1.0, "greedy"), ("qd", 0.5, "qd α=0.5"),
("qd", 1.0, "qd α=1"), ("qd", 2.0, "qd α=2"), ("none", 1.0, "none")]
arms = [("greedy", 1.0, "greedy"), ("qd", 0.5, "quality-diversity α=0.5"),
("qd", 1.0, "quality-diversity α=1"), ("qd", 2.0, "quality-diversity α=2"), ("none", 1.0, "none")]
labels, vals, errs, colors = [], [], [], []
palette = {"greedy": "#d62728", "qd": "#ff7f0e", "none": "#2ca02c"}
for mode, a, lab in arms:
@ -70,9 +70,8 @@ def main(results_dir: str = "results/E5") -> None:
xticks=range(len(labels)))
ax.set_xticklabels(labels, rotation=25, ha="right", fontsize=8)
fig.suptitle("E5 — quality-diversity selection maintains diversity where greedy "
"fixes it", y=1.02)
fig.tight_layout()
letter_axes(fig)
savefig(fig, results_dir, "E5")

View file

@ -16,7 +16,7 @@ import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
from _figlib import load_bundle, savefig, letter_axes # noqa: E402
STYLE = {
"healthy_remint": ("#2ca02c", "re-mint while healthy (H high)"),
@ -44,7 +44,7 @@ def main(results_dir: str = "results/E6") -> None:
color=c, alpha=0.15)
for g in remint_gens:
ax.axvline(g, ls=":", color="k", lw=0.8, alpha=0.5)
ax.set(xlabel="generation", ylabel=r"forward KL to ORIGINAL truth",
ax.set(xlabel="generation", ylabel=r"forward KL to original truth",
title="Re-minting while collapsed locks in divergence")
ax.legend(frameon=False, fontsize=8)
@ -67,8 +67,8 @@ def main(results_dir: str = "results/E6") -> None:
title="Diversity at re-mint time (the gate reads this)")
ax.legend(frameon=False, fontsize=8)
fig.suptitle("E6 — re-minting is irreversible; gate it on diversity", y=1.02)
fig.tight_layout()
letter_axes(fig)
savefig(fig, results_dir, "E6")

View file

@ -23,7 +23,7 @@ import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, mean_ci, savefig # noqa: E402
from _figlib import load_bundle, mean_ci, savefig, letter_axes # noqa: E402
def main(results_dir: str = "results/E8") -> None:
@ -58,9 +58,8 @@ def main(results_dir: str = "results/E8") -> None:
title="Decorrelation is the fuel:\nρ=0 climbs to the optimum; ρ=1 (clones) buy nothing")
ax.legend(frameon=False, fontsize=9)
fig.suptitle("E8 — the vertical claim: an offspring recombined from many decorrelated parents "
"is fitter than any parent (FisherMuller; no two-parent limit)", y=1.02, fontsize=12)
fig.tight_layout()
letter_axes(fig)
savefig(fig, results_dir, "E8")

View file

@ -24,7 +24,7 @@ import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
from _figlib import load_bundle, savefig, letter_axes # noqa: E402
def main(results_dir: str = "results/E9") -> None:
@ -56,9 +56,8 @@ def main(results_dir: str = "results/E9") -> None:
title="With offspring selection, an optimal\nrecombination rate re-emerges (dotted = parents)")
ax.legend(frameon=False, fontsize=8, title="ruggedness")
fig.suptitle("E9 — landscape robustness: recombination helps when skills are complementary, but "
"blindly merging entangled models causes outbreeding depression", y=1.02, fontsize=11)
fig.tight_layout()
letter_axes(fig)
savefig(fig, results_dir, "E9")

View file

@ -22,7 +22,7 @@ import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, mean_ci, savefig # noqa: E402
from _figlib import load_bundle, mean_ci, savefig, letter_axes # noqa: E402
_ARCH_ORDER = ["histogram", "rnn", "mlp"]
_ARCH_LABEL = {"histogram": "histogram\n(exact)", "rnn": "GRU\n(autoregressive)",
@ -47,14 +47,14 @@ def main(results_dir: str = "results/architectures") -> None:
ax.plot(s.index, s.values, ls, color=arch_colors[k], alpha=alpha, lw=1.8,
label=f"{k} (g={g:g})")
ax.set(xlabel="generation", ylabel=r"forward-KL $D(p^*\Vert\hat p)$",
title="Dry (solid) collapses; grounded (dashed) held —\nin every architecture")
title="No real data (solid) collapses;\ngrounded (dashed) holds in every architecture")
ax.legend(frameon=False, fontsize=7, ncol=1)
# Panels B & C: grouped bars, dry vs grounded per architecture.
def grouped_bar(ax, metric, title, ylabel):
x = np.arange(len(kinds))
w = 0.36
for off, g, lab, col in [(-w / 2, g_dry, f"dry (g={g_dry:g})", "#d62728"),
for off, g, lab, col in [(-w / 2, g_dry, f"no real data (g={g_dry:g})", "#d62728"),
(w / 2, g_wet, f"grounded (g={g_wet:g})", "#2ca02c")]:
means, errs = [], []
for k in kinds:
@ -72,9 +72,8 @@ def main(results_dir: str = "results/architectures") -> None:
grouped_bar(axes[2], "tail_frac_alive", "Tail-item survival rises with grounding",
"tail items alive")
fig.suptitle("architectures — dry collapse and grounding-rescue are architecture-general "
"(histogram, GRU, MLP)", y=1.02, fontsize=13)
fig.tight_layout()
letter_axes(fig)
savefig(fig, results_dir, "architectures")

View file

@ -0,0 +1,69 @@
"""Differential reproduction in the six-generation population (SI figure).
Left: best-lineage all-families accuracy per generation (mean over seeds, 95% CI) for the four arms:
never merge, declinable merge, and each with culling (the lowest-scoring lineage re-founded from
the highest-scoring one after every generation). Middle: population MEAN accuracy over the three
lineages, same arms (culling acts on the mean first). Right: the number of cull events per
generation in each culled arm (mean over seeds), with the fraction of merges declined in the
culled declinable arm.
Reads the committed curriculum bundles through stats_llm_curriculum (no re-simulation).
Usage: python figures/plot_curriculum_cull.py [out_dir=results/llm_curriculum_v5_cull]
"""
from __future__ import annotations
import sys
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import mean_ci, savefig, letter_axes # noqa: E402
from stats_llm_curriculum import best_lineage, load_curriculum # noqa: E402
ARMS = {"isolated": ("#2c7fb8", "-", "never merge"), "veto": ("#2ca02c", "-", "declinable merge"),
"cull_isolated": ("#2c7fb8", "--", "never merge + culling"),
"cull_veto": ("#2ca02c", "--", "declinable merge + culling")}
def main(out_dir: str = "results/llm_curriculum_v5_cull") -> None:
df = load_curriculum()
df = df[df["curriculum"] == "latin"]
best = best_lineage(df)
lin = df[(df["metric"] == "all_families") & df["model"].str.startswith("lineage") & (df["generation"] >= 0)]
pop_mean = lin.groupby(["arm", "seed", "generation"])["value"].mean().reset_index()
fig, (a1, a2, a3) = plt.subplots(1, 3, figsize=(13, 3.8))
for arm, (color, ls, label) in ARMS.items():
for ax, src in ((a1, best), (a2, pop_mean)):
sub = src[src["arm"] == arm]
if len(sub):
x, m, h = mean_ci(sub, "generation", "value")
ax.errorbar(x + 1, m, yerr=np.nan_to_num(h), fmt="o", ls=ls, color=color, capsize=3,
label=label)
for arm, color in (("cull_isolated", "#2c7fb8"), ("cull_veto", "#2ca02c")):
c = df[(df["arm"] == arm) & (df["metric"] == "culled")]
if len(c):
ev = c.groupby(["seed", "generation"])["value"].sum().reset_index()
x, m, _ = mean_ci(ev, "generation", "value")
a3.plot(x + 1, m, "o--", color=color, label=f"{ARMS[arm][2]}: culls")
v = df[(df["arm"] == "cull_veto") & (df["metric"] == "veto_used")]
if len(v):
x, m, h = mean_ci(v, "generation", "value")
a3.errorbar(x + 1, m, yerr=np.nan_to_num(h), fmt="s-", color="#d62728", capsize=3,
label="declined merges (culled arm)")
a1.set(xlabel="generation", ylabel="best-lineage accuracy, all families", ylim=(0.3, 0.9),
title="best lineage")
a2.set(xlabel="generation", ylabel="population mean accuracy", ylim=(0.3, 0.9), title="population mean")
a3.set(xlabel="generation", ylabel="events per generation / fraction", ylim=(-0.05, 1.1),
title="culls and declines")
for a in (a1, a2, a3):
a.set_xticks(range(1, 7)); a.legend(frameon=False, fontsize=7)
fig.tight_layout()
letter_axes(fig)
savefig(fig, out_dir, "curriculum_cull")
if __name__ == "__main__":
main(*sys.argv[1:])

View file

@ -0,0 +1,69 @@
"""Conflict-arrival curricula (SI figure): does the declinable merge's decline rate, and the obligate
merge's collapse, follow the generation at which conflicting conventions arrive?
Left: fraction of proposed merges declined per generation (mean over seeds, 95% CI) for each
curriculum; a filled marker on the curve marks the first generation at which both conflicting
families (boolq, winogrande) are present in every lineage. Right: best-lineage all-families accuracy
of the OBLIGATE society arm per curriculum, same marker. Curricula: Latin square (conflict from
generation 5), decorrelated (from 4), conflict-early (from 2), conflict-late (from 6).
Reads the committed curriculum bundles through stats_llm_curriculum (no re-simulation).
Usage: python figures/plot_curriculum_timing.py [out_dir=results/llm_curriculum_v5_early]
"""
from __future__ import annotations
import sys
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import mean_ci, savefig, letter_axes # noqa: E402
from stats_llm_curriculum import CONFLICT_FROM, best_lineage, decline_table, load_curriculum # noqa: E402
STYLE = {"latin": ("#7f7f7f", "Latin square"), "decor": ("#2c7fb8", "decorrelated"),
"early": ("#d62728", "conflict-early"), "late": ("#2ca02c", "conflict-late")}
OBLIGATE = {"latin": "society", "early": "early_society", "late": "late_society"}
def main(out_dir: str = "results/llm_curriculum_v5_early") -> None:
df = load_curriculum()
tab = decline_table(df)
best = best_lineage(df)
fig, (a1, a2) = plt.subplots(1, 2, figsize=(10, 3.8))
for cur, (color, label) in STYLE.items():
sub = tab[tab["curriculum"] == cur]
if len(sub):
x, m, h = mean_ci(sub, "generation", "declined")
a1.errorbar(x + 1, m, yerr=np.nan_to_num(h), fmt="-o", color=color, capsize=3, label=label,
markerfacecolor="white")
g0 = CONFLICT_FROM[cur]
if g0 in set(x):
a1.plot(g0 + 1, m[list(x).index(g0)], "o", color=color, ms=12, markeredgecolor="black", markeredgewidth=1.2)
arm = OBLIGATE.get(cur)
ob = best[(best["curriculum"] == cur) & (best["arm"] == arm)] if arm else best.iloc[0:0]
if len(ob):
x, m, h = mean_ci(ob, "generation", "value")
a2.errorbar(x + 1, m, yerr=np.nan_to_num(h), fmt="-o", color=color, capsize=3, label=label,
markerfacecolor="white")
g0 = CONFLICT_FROM[cur]
if g0 in set(x):
a2.plot(g0 + 1, m[list(x).index(g0)], "o", color=color, ms=12, markeredgecolor="black", markeredgewidth=1.2)
a1.set(xlabel="generation", ylabel="fraction of merges declined", ylim=(-0.02, 1.05),
title="declinable merge: decline rate")
a2.set(xlabel="generation", ylabel="best-lineage accuracy, all families", ylim=(0.1, 0.9),
title="obligate merge: accuracy")
a1.plot([], [], "o", color="white", ms=10, markeredgecolor="black", markeredgewidth=1.2,
label="first generation with both\nconflicting conventions")
a1.legend(frameon=False, fontsize=8); a2.legend(frameon=False, fontsize=8)
for a in (a1, a2):
a.set_xticks(range(1, 7))
fig.tight_layout()
letter_axes(fig)
savefig(fig, out_dir, "curriculum_timing")
if __name__ == "__main__":
main(*sys.argv[1:])

View file

@ -26,7 +26,7 @@ import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, mean_ci, savefig # noqa: E402
from _figlib import load_bundle, mean_ci, savefig, letter_axes # noqa: E402
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from knowledge.analysis import reduce_to_stationary # noqa: E402
@ -94,8 +94,8 @@ def main(results_dir: str = "results/grounding") -> None:
ax.errorbar(kg, Km, yerr=Kci, fmt="o-", color="#1f77b4", capsize=3, zorder=3)
ax.set(xlabel="grounding fraction $g=m/(n+m)$",
ylabel=r"stationary forward-KL $D(p^*\Vert\hat p)$",
title="Neural phase boundary: KL falls monotonically\n"
"(sign confirmed; paired $t$=3.3 at g=0.2)")
title="Trained RNN: KL falls monotonically with grounding\n"
"(paired $t$=3.3 at $g$=0.2)")
# Panel C: recovery fraction with the median-recovery grounding vs Layer-1's g*.
ax = axes[1, 0]
@ -106,7 +106,7 @@ def main(results_dir: str = "results/grounding") -> None:
ax.axvspan(lo50, hi50, color="#d62728", alpha=0.15)
ax.axvline(g50, color="#d62728", lw=1.2,
label=f"median-recovery $g$={g50:.3f}\n(95% CI [{lo50:.3f},{hi50:.3f}])")
ax.axvline(_LAYER1_GSTAR, ls="--", color="k", lw=1, label=f"Layer-1 $g^*$={_LAYER1_GSTAR}")
ax.axvline(_LAYER1_GSTAR, ls="--", color="k", lw=1, label=f"analytic $g^*$={_LAYER1_GSTAR}")
ax.set(xlabel="grounding fraction $g$", ylabel="forward-KL recovery fraction",
title="Half the divergence gap closes by $g\\approx0.04$\n"
"(full recovery needs more g: smoothing softens the threshold)")
@ -131,10 +131,8 @@ def main(results_dir: str = "results/grounding") -> None:
"(smoothing keeps spurious support); forward-KL responds")
ax.legend(frameon=False, fontsize=8)
fig.suptitle("grounding — grounding arrests collapse in trained RNN weights (SIGN confirmed); "
f"the sharp $g^*\\ll1$ is carried by the histogram bridge ($g^*$=0.047, $H^*$={H_star:.2f})",
y=1.0, fontsize=12)
fig.tight_layout()
letter_axes(fig)
savefig(fig, results_dir, "grounding")

View file

@ -28,7 +28,7 @@ import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
from _figlib import load_bundle, savefig, letter_axes # noqa: E402
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from knowledge.metrics import heterozygosity # noqa: E402
@ -76,7 +76,7 @@ def main() -> None:
g, y = _mean_traj(sh, "temperature", val, "heterozygosity")
ax.plot(g, y, "-o", color=c, ms=3, label=lab)
ax.axhline(Hstar_sh, ls=":", color="gray", lw=1, label="$H^*$")
ax.axhline(vae_H, ls="--", color="#2ca02c", lw=1.3, label=f"real VAE (dry): {vae_H:.2f}")
ax.axhline(vae_H, ls="--", color="#2ca02c", lw=1.3, label=f"real VAE (no real data): {vae_H:.2f}")
ax.set(xlabel="generation", ylabel="heterozygosity $H$",
title="VAE regime ($n$=6000, $K$=30): neutral drift is inert;\nsharpening collapses (like the VAE)")
ax.legend(frameon=False, fontsize=8)
@ -85,7 +85,7 @@ def main() -> None:
for val, c, lab in [(1.0, NEU, "neutral (τ=1)"), (0.8, KER, "sharpened (τ=0.8)")]:
g, y = _mean_traj(sh, "temperature", val, "support_size")
ax.plot(g, y, "-o", color=c, ms=3, label=lab)
ax.axhline(vae_sup, ls="--", color="#2ca02c", lw=1.3, label=f"real VAE (dry): {vae_sup:.0f}")
ax.axhline(vae_sup, ls="--", color="#2ca02c", lw=1.3, label=f"real VAE (no real data): {vae_sup:.0f}")
ax.set(xlabel="generation", ylabel="distinct modes alive",
title="Support: neutral holds ~all; sharpening → 1 mode")
ax.legend(frameon=False, fontsize=8)
@ -96,7 +96,7 @@ def main() -> None:
g, y = _mean_traj(sm, "reset", val, "heterozygosity")
ax.plot(g, y, "-", color=c, lw=1.8, label=lab)
ax.axhline(Hstar_sm, ls=":", color="gray", lw=1, label="$H^*$")
ax.axhline(rnn_H, ls="--", color="#2ca02c", lw=1.3, label=f"real RNN (dry): {rnn_H:.2f}")
ax.axhline(rnn_H, ls="--", color="#2ca02c", lw=1.3, label=f"real RNN (no real data): {rnn_H:.2f}")
ax.set(xlabel="generation", ylabel="heterozygosity $H$",
title="RNN regime ($n$=200, $K$=256): neutral → 0;\nsmoothing floors $H$ (like the RNN)")
ax.legend(frameon=False, fontsize=8)
@ -105,15 +105,14 @@ def main() -> None:
for val, c, lab in [(0.0, NEU, "neutral (u=0)"), (0.006, KER, "smoothed (u=0.006)")]:
g, y = _mean_traj(sm, "reset", val, "forward_kl")
ax.plot(g, y, "-", color=c, lw=1.8, label=lab)
ax.axhline(rnn_KL, ls="--", color="#2ca02c", lw=1.3, label=f"real RNN (dry): {rnn_KL:.1f}")
ax.axhline(rnn_KL, ls="--", color="#2ca02c", lw=1.3, label=f"real RNN (no real data): {rnn_KL:.1f}")
ax.set(xlabel="generation", ylabel=r"forward-KL $D(p^*\Vert p)$",
title="Forward-KL: neutral diverges; smoothing plateaus\n(overshoots RNN → prior is truth-like, not uniform)")
ax.legend(frameon=False, fontsize=8)
fig.suptitle("learning kernel — neutral WrightFisher fails both neural models, oppositely: "
"the estimator sharpens (VAE) or smooths (RNN)", y=1.0, fontsize=12)
fig.tight_layout()
for d in ("results/kernel_sharpen", "results/kernel_smooth"):
letter_axes(fig)
savefig(fig, d, "kernel")

112
figures/plot_llm_compose.py Normal file
View file

@ -0,0 +1,112 @@
"""Composition-decay figure (prereg v3 §8) — written before unblinding.
(A) Composition **surplus** S_t = composed best single parent, per arm over generations, with the
zero line: the vertical claim, and whether it survives inheritance.
(B) Own-skill retention q_t per lineage (math on GSM8K, code on MBPP), dry vs grounded the
denominators of the prediction.
(C) rho_t, the behavioural correlation between the two lineages: the mechanism, if it rises.
(D) Observed composed accuracy against the framework's forecast Ĉ_t (one free scale, fixed at
generation 0) H3, the paper's predictive claim, drawn as a line the data can miss.
Reads only committed bundles: one bundle directory, or a campaign directory of ``s*/`` bundles.
Usage: python figures/plot_llm_compose.py [results/llm_compose]
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
sys.path.insert(0, str(Path(__file__).parent))
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from _figlib import mean_ci, savefig # noqa: E402
from llm.compose import predicted_composition # noqa: E402
ARMS = [("dry", "#d62728", "dry · blending operator"),
("grounded", "#2ca02c", "grounded (g = 0.10) · blending"),
("dry_cat", "#1f77b4", "dry · union operator (cat)")]
def load_any(results_dir: Path) -> pd.DataFrame:
if (results_dir / "results.parquet").exists():
paths = [results_dir]
else:
paths = sorted(p.parent for p in results_dir.glob("*/results.parquet"))
if not paths:
raise SystemExit(f"no results.parquet under {results_dir}")
return pd.concat([pd.read_parquet(p / "results.parquet") for p in paths], ignore_index=True)
def series(df: pd.DataFrame, arm: str, metric: str) -> pd.DataFrame:
return df[(df.arm == arm) & (df.metric == metric)][["seed", "generation", "value"]]
def main(results_dir: str = "results/llm_compose") -> None:
rd = Path(results_dir)
df = load_any(rd)
arms = [a for a in ARMS if a[0] in set(df.arm.unique())]
n_seeds = df.seed.nunique()
fig, ax = plt.subplots(1, 4, figsize=(21, 4.6))
# (A) surplus
for arm, color, label in arms:
s = series(df, arm, "surplus")
if s.empty:
continue
x, m, h = mean_ci(s, "generation", "value")
ax[0].plot(x, m, "-o", color=color, label=label, lw=2, ms=4)
ax[0].fill_between(x, m - h, m + h, color=color, alpha=0.15, lw=0)
ax[0].axhline(0, color="k", lw=1, ls="--")
ax[0].set_title("(A) composition surplus\ncomposed best single parent", fontsize=10)
ax[0].set_xlabel("generation"); ax[0].set_ylabel("surplus"); ax[0].legend(fontsize=8)
# (B) own-skill retention
for arm, color, _ in arms:
for metric, ls in (("q_math", "-"), ("q_code", ":")):
s = series(df, arm, metric)
if s.empty:
continue
x, m, _h = mean_ci(s, "generation", "value")
ax[1].plot(x, m, ls, color=color, lw=2,
label=f"{arm} · {metric.split('_')[1]}" if arm != "dry_linear" else None)
ax[1].set_title("(B) own-skill retention q_t\nsolid math (GSM8K), dotted code (MBPP)", fontsize=10)
ax[1].set_xlabel("generation"); ax[1].set_ylabel("accuracy"); ax[1].legend(fontsize=8)
# (C) rho
for arm, color, label in arms:
s = series(df, arm, "rho_behav")
if s.empty:
continue
x, m, h = mean_ci(s, "generation", "value")
ax[2].plot(x, m, "-o", color=color, label=label, lw=2, ms=4)
ax[2].fill_between(x, m - h, m + h, color=color, alpha=0.15, lw=0)
ax[2].set_title("(C) lineage correlation ρ_t\n(agreement on a shared probe)", fontsize=10)
ax[2].set_xlabel("generation"); ax[2].set_ylabel("ρ"); ax[2].legend(fontsize=8)
# (D) observed vs predicted, dry arm
for arm, color, label in arms:
obs = series(df, arm, "composed_acc").groupby("generation").value.mean()
qm = series(df, arm, "q_math").groupby("generation").value.mean()
qc = series(df, arm, "q_code").groupby("generation").value.mean()
rho = series(df, arm, "rho_behav").groupby("generation").value.mean()
if obs.empty or len(obs) < 2:
continue
pred = predicted_composition(qm.to_numpy(), qc.to_numpy(), rho.to_numpy(), float(obs.iloc[0]))
ax[3].plot(obs.index, obs.to_numpy(), "-o", color=color, lw=2, ms=4, label=f"{label} observed")
ax[3].plot(obs.index, pred, "--", color=color, lw=1.5, alpha=0.8,
label=f"{label} predicted Ĉ")
ax[3].set_title("(D) H3: observed vs the closed form\nĈ = c₀·q_math·q_code·(1ρ)/(1ρ₀)", fontsize=10)
ax[3].set_xlabel("generation"); ax[3].set_ylabel("composed accuracy"); ax[3].legend(fontsize=7)
fig.suptitle(f"llm_compose — does a composed capability survive inheritance? "
f"({n_seeds} seed{'s' if n_seeds != 1 else ''}, mean ± 95% CI)", y=1.03)
fig.tight_layout()
savefig(fig, rd, "llm_compose")
if __name__ == "__main__":
main(*sys.argv[1:])

View file

@ -19,7 +19,7 @@ import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
from _figlib import load_seed_bundles, savefig # noqa: E402
_FAMS = ["lists", "strings", "arith"]
_DIRECTED = {"directed_overall": "directed:overall", "directed_balanced": "directed:balanced"}
@ -27,11 +27,11 @@ _DIRECTED = {"directed_overall": "directed:overall", "directed_balanced": "direc
def _acc(df, model, metric):
r = df[(df["model"] == model) & (df["metric"] == metric)]["accuracy"]
return float(r.iloc[0]) if len(r) else float("nan")
return float(r.mean()) if len(r) else float("nan")
def main(results_dir: str = "results/llm_directed") -> None:
df, cfg = load_bundle(results_dir)
df, cfg = load_seed_bundles(results_dir) # seed-mean when the bundle has s{seed}/ sub-bundles
present = set(df["model"].unique())
specialists = sorted(m for m in present if m.startswith("spec_"))
directed = [m for m in _DIRECTED if m in present]

View file

@ -19,18 +19,18 @@ import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
from _figlib import load_seed_bundles, savefig # noqa: E402
_FAMS = ["lists", "strings", "arith"]
def _acc(df, model, metric):
r = df[(df["model"] == model) & (df["metric"] == metric)]["accuracy"]
return float(r.iloc[0]) if len(r) else float("nan")
return float(r.mean()) if len(r) else float("nan")
def main(results_dir: str = "results/llm_merge") -> None:
df, cfg = load_bundle(results_dir)
df, cfg = load_seed_bundles(results_dir) # seed-mean when the bundle has s{seed}/ sub-bundles
specialists = sorted(m for m in df["model"].unique() if m.startswith("spec_"))
merges = sorted(m for m in df["model"].unique() if m.startswith("merge_"))
models = ["base"] + specialists + merges

View file

@ -20,7 +20,7 @@ import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
from _figlib import load_seed_bundles, savefig # noqa: E402
_FAMS = ["lists", "strings", "arith"]
_FUSION = {"merge_soup": "fuse:soup", "merge_ties": "fuse:ties"}
@ -29,11 +29,11 @@ _UNION = {"moe_oracle": "route:oracle", "moe_learned": "route:learned", "max_mer
def _acc(df, model, metric):
r = df[(df["model"] == model) & (df["metric"] == metric)]["accuracy"]
return float(r.iloc[0]) if len(r) else float("nan")
return float(r.mean()) if len(r) else float("nan")
def main(results_dir: str = "results/llm_moe") -> None:
df, cfg = load_bundle(results_dir)
df, cfg = load_seed_bundles(results_dir) # seed-mean when the bundle has s{seed}/ sub-bundles
present = set(df["model"].unique())
specialists = sorted(m for m in present if m.startswith("spec_"))
fusion = [m for m in _FUSION if m in present]

View file

@ -8,7 +8,10 @@ figure with 95% CIs over seeds:
(B) llm_moe_hard_seeds union (routing) vs fusion (soup/ties) on the hard benchmark (3 seeds).
(C) llm_directed_hard_seeds directed offspring selection vs the a-priori soup, hard (3 seeds).
Usage: python figures/plot_llm_seeds.py
Usage: python figures/plot_llm_seeds.py # the 0.5B seed bundles (default)
python figures/plot_llm_seeds.py --merge results/llm_merge_hpc --moe results/llm_moe_hard_hpc \
--directed results/llm_directed_hard_hpc --out results/llm_merge_hpc --tag 7B
Bundles may be flat (one seed) or per-seed sub-bundles ``s{seed}/`` (HPC array layout).
"""
from __future__ import annotations
@ -20,7 +23,7 @@ import numpy as np
import matplotlib.pyplot as plt
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
from _figlib import load_seed_bundles, savefig # noqa: E402
def _agg(df, models, metric):
@ -28,7 +31,7 @@ def _agg(df, models, metric):
out = []
for m in models:
v = df[(df["model"] == m) & (df["metric"] == metric)].groupby("seed")["accuracy"].mean()
out.append((v.mean(), 1.96 * v.std(ddof=1) / max(1, np.sqrt(len(v)))))
out.append((v.mean(), 1.96 * v.std(ddof=1) / np.sqrt(len(v)) if len(v) > 1 else 0.0))
return out
@ -58,31 +61,42 @@ def _best_spec(df):
return pd.concat([df] + rows, ignore_index=True)
def main() -> None:
def main(merge="results/llm_merge_seeds", moe="results/llm_moe_hard_seeds",
directed="results/llm_directed_hard_seeds", out="results/llm_merge_seeds", tag="0.5B") -> None:
fig, axes = plt.subplots(1, 3, figsize=(16, 4.8))
df, _ = load_bundle("results/llm_merge_seeds")
df, _ = load_seed_bundles(merge)
n = df["seed"].nunique()
_panel(axes[0], _best_spec(df), ["base", "best_specialist", "merge_soup", "merge_ties"],
["base", "best\nspecialist", "merge\n(soup)", "merge\n(ties)"],
"(A) FisherMuller with error bars\n(5 seeds, easy benchmark, 0.5B)")
f"(A) FisherMuller with error bars\n({n} seeds, easy benchmark, {tag})")
df, _ = load_bundle("results/llm_moe_hard_seeds")
df, _ = load_seed_bundles(moe)
n = df["seed"].nunique()
_panel(axes[1], _best_spec(df), ["best_specialist", "merge_soup", "merge_ties", "moe_oracle",
"moe_learned"],
["best\nspecialist", "fusion\n(soup)", "fusion\n(ties)", "union\n(route,oracle)",
"union\n(route,learned)"],
"(B) union vs fusion, hard benchmark\n(3 seeds, 0.5B)")
f"(B) union vs fusion, hard benchmark\n({n} seeds, {tag})")
df, _ = load_bundle("results/llm_directed_hard_seeds")
df, _ = load_seed_bundles(directed)
n = df["seed"].nunique()
_panel(axes[2], df, ["merge_soup", "directed_overall", "directed_balanced"],
["a-priori soup", "directed\n(overall)", "directed\n(balanced)"],
"(C) directed offspring selection, hard\n(3 seeds, 0.5B)")
f"(C) directed offspring selection, hard\n({n} seeds, {tag})")
fig.suptitle("The LLM recombination claims are seed-robust (fixed test sets; training seed varied; 95% CI)",
y=1.03, fontsize=12)
fig.tight_layout()
savefig(fig, "results/llm_merge_seeds", "llm_seeds")
savefig(fig, out, "llm_seeds")
if __name__ == "__main__":
main()
import argparse
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--merge", default="results/llm_merge_seeds")
ap.add_argument("--moe", default="results/llm_moe_hard_seeds")
ap.add_argument("--directed", default="results/llm_directed_hard_seeds")
ap.add_argument("--out", default="results/llm_merge_seeds")
ap.add_argument("--tag", default="0.5B")
main(**vars(ap.parse_args()))

56
figures/plot_llm_smol.py Normal file
View file

@ -0,0 +1,56 @@
"""Second base lineage (SI figure): the Fisher-Muller and headroom results on SmolLM2-1.7B-Instruct
beside the Qwen2.5-0.5B-Instruct originals, mean ± 95% CI over seeds.
(A) merged specialists vs the best single specialist, easy benchmark (5 seeds per lineage);
(B) union (routing) vs fusion (soup, ties) on the hard benchmark (3 seeds per lineage).
Skips silently when the SmolLM2 bundles are not present yet (``make figures`` runs every script).
Usage: python figures/plot_llm_smol.py [out_dir=results/llm_merge_seeds_smol]
"""
from __future__ import annotations
import sys
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_seed_bundles, savefig, letter_axes # noqa: E402
from plot_llm_seeds import _agg, _best_spec # noqa: E402
LINEAGES = {"Qwen2.5-0.5B": ("results/llm_merge_seeds", "results/llm_moe_hard_seeds", "#9ecae1", "#2c7fb8"),
"SmolLM2-1.7B": ("results/llm_merge_seeds_smol", "results/llm_moe_hard_seeds_smol", "#fdae6b", "#d62728")}
PANELS = {"merge": (["best_specialist", "merge_soup", "merge_ties"],
["best\nspecialist", "merge\n(soup)", "merge\n(ties)"],
"FisherMuller, easy benchmark"),
"moe": (["best_specialist", "merge_soup", "merge_ties", "moe_oracle", "moe_learned"],
["best\nspecialist", "fusion\n(soup)", "fusion\n(ties)", "union\n(oracle)", "union\n(learned)"],
"union vs fusion, hard benchmark")}
def main(out_dir: str = "results/llm_merge_seeds_smol") -> None:
if not all(Path(d).exists() for d in LINEAGES["SmolLM2-1.7B"][:2]):
print("plot_llm_smol: SmolLM2 bundles not present yet; skipping"); return
fig, axes = plt.subplots(1, 2, figsize=(12, 4.4))
for ax, (key, (models, labels, title)) in zip(axes, PANELS.items()):
x = np.arange(len(models)); n_l = len(LINEAGES); w = 0.8 / (2 * n_l)
for li, (lineage, (dm, dmo, c_over, c_worst)) in enumerate(LINEAGES.items()):
df = _best_spec(load_seed_bundles(dm if key == "merge" else dmo)[0])
n = df["seed"].nunique()
for mi, (metric, color) in enumerate((("overall", c_over), ("worst_family", c_worst))):
vals = _agg(df, models, metric)
off = (li * 2 + mi - (2 * n_l - 1) / 2) * w
ax.bar(x + off, [v for v, _ in vals], w, yerr=[e for _, e in vals], capsize=2,
color=color, label=f"{lineage}, {metric.replace('_', ' ')} ({n} seeds)")
ax.set_xticks(x); ax.set_xticklabels(labels, fontsize=8)
ax.set(ylabel="verifier accuracy", ylim=(0, 1.0), title=title)
ax.legend(frameon=False, fontsize=7)
fig.tight_layout()
letter_axes(fig)
savefig(fig, out_dir, "llm_smol")
if __name__ == "__main__":
main(*sys.argv[1:])

115
figures/plot_llm_society.py Normal file
View file

@ -0,0 +1,115 @@
"""v2 society figure — E11's three panels at the language-model tier, plus the competence genotype.
Pre-registered layout (tasks/prereg-llm-society-v2.md §8), written before unblinding and run on the
smoke bundle first. Reads only committed bundles: a single bundle directory, or a campaign directory
whose sub-directories ``s{seed}_{arm}/`` each hold a bundle (the PBS array writes one per element).
(A) Best-agent overall test accuracy per arm over generations (solid) with the best *newborn* of each
generation (dotted) a climb carried by a surviving founder is visible as such; B₀ (best founder
at gen 0) dashed. Mean ± 95% CI over seeds.
(B) Behavioural diversity of the population (mean pairwise disagreement).
(C) The self-consumption signature: mean conformity mean true accuracy.
(D) Competence genotype of the ``full`` arm's best agent: per-family test accuracy × generation, mean
over seeds E8's "a genotype no parent had", if it happens.
Usage: python figures/plot_llm_society.py [results/llm_society_v2 | results/llm_society_v2_smoke]
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yaml
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import mean_ci, savefig # noqa: E402
_ARMS = [("full", "#2ca02c", "full society"),
("no_sex", "#ff7f0e", "no sex (no recombination)"),
("no_diversity", "#9467bd", "no diversity (greedy)"),
("no_grounding", "#d62728", "no grounding (self-consumption)"),
("sex_linear", "#1f77b4", "sex by linear blend (H2 control)")]
def load_any(results_dir: Path) -> tuple[pd.DataFrame, list[str]]:
"""One bundle, or every ``*/results.parquet`` below the directory (the campaign layout)."""
if (results_dir / "results.parquet").exists():
paths = [results_dir]
else:
paths = sorted(p.parent for p in results_dir.glob("*/results.parquet"))
if not paths:
raise SystemExit(f"no results.parquet under {results_dir}")
df = pd.concat([pd.read_parquet(p / "results.parquet") for p in paths], ignore_index=True)
fams = yaml.safe_load((paths[0] / "resolved_config.yaml").read_text())["source_config"]["families"]
return df, list(fams)
def main(results_dir: str = "results/llm_society_v2") -> None:
rd = Path(results_dir)
df, fams = load_any(rd)
pop = df[df.role == "population"]
summ = df[df.role == "summary"]
arms = [a for a in _ARMS if a[0] in set(df.arm.unique())]
best = (pop[pop.metric == "test_overall"].groupby(["arm", "seed", "generation"]).value.max()
.rename("best").reset_index())
newborn = summ[summ.metric == "best_newborn_overall"][["arm", "seed", "generation", "value"]]
b0 = best[best.generation == 0].groupby("seed").best.mean().mean()
fig, axes = plt.subplots(1, 4, figsize=(21, 4.8))
def traj(ax, frame, col, title, ylabel, style="-", label_suffix=""):
for arm, color, label in arms:
sub = frame[frame.arm == arm]
if sub.empty:
continue
x, m, h = mean_ci(sub, "generation", col)
ax.plot(x, m, style, color=color, label=(label + label_suffix) if style == "-" else None, lw=2)
if style == "-":
ax.fill_between(x, m - h, m + h, color=color, alpha=0.15, lw=0)
if title: # overlay calls pass "" and must not wipe labels
ax.set_title(title, fontsize=10); ax.set_xlabel("generation"); ax.set_ylabel(ylabel)
traj(axes[0], best, "best", "(A) best agent (solid) and best newborn (dotted)\nB₀ = best founder, dashed",
"overall test accuracy")
traj(axes[0], newborn.rename(columns={"value": "best"}), "best", "", "", style=":")
axes[0].axhline(b0, color="k", ls="--", lw=1, label=f"B₀ = {b0:.2f}")
axes[0].legend(fontsize=8, loc="best")
div = summ[summ.metric == "diversity_behav"]
traj(axes[1], div, "value", "(B) population diversity\n(mean pairwise disagreement)", "diversity")
gap = summ[summ.metric == "gap_conformity_minus_truth"]
traj(axes[2], gap, "value", "(C) self-consumption signature\nconformity true accuracy", "gap")
axes[2].axhline(0, color="k", lw=0.8)
# (D) competence genotype of the full arm's best agent, families × generations, mean over seeds
full = pop[pop.arm == ("full" if "full" in set(pop.arm) else arms[0][0])]
fam_cols = [f"test_{f}" for f in fams]
idx = full[full.metric == "test_overall"].sort_values("value").groupby(["seed", "generation"]).tail(1)
keyed = full.set_index(["seed", "generation", "agent", "metric"]).value
gens = sorted(full.generation.unique())
heat = np.full((len(fams), len(gens)), np.nan)
for gi, g in enumerate(gens):
rows = idx[idx.generation == g]
vals = np.array([[keyed.get((r.seed, g, r.agent, c), np.nan) for c in fam_cols] for r in rows.itertuples()])
if len(vals):
heat[:, gi] = np.nanmean(vals, axis=0)
im = axes[3].imshow(heat, aspect="auto", cmap="viridis", vmin=0, vmax=1)
axes[3].set_yticks(range(len(fams))); axes[3].set_yticklabels(fams, fontsize=8)
axes[3].set_xticks(range(len(gens))); axes[3].set_xticklabels(gens, fontsize=8)
axes[3].set_xlabel("generation"); axes[3].set_title("(D) competence genotype of the best agent\n(full arm; per-family accuracy)", fontsize=10)
fig.colorbar(im, ax=axes[3], fraction=0.046, pad=0.02)
n_seeds = df.seed.nunique()
fig.suptitle(f"llm_society_v2 — the composed society at LLM scale ({n_seeds} seed{'s' if n_seeds != 1 else ''}, "
f"L={len(fams)} families, mean ± 95% CI)", y=1.02)
fig.tight_layout()
savefig(fig, rd, "llm_society_v2")
if __name__ == "__main__":
main(*sys.argv[1:])

View file

@ -23,7 +23,7 @@ import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
from _figlib import load_bundle, savefig, letter_axes # noqa: E402
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from knowledge.metrics import heterozygosity # noqa: E402
@ -47,7 +47,7 @@ def main(results_dir: str = "results/mnist_collapse") -> None:
oracle_acc = manifest.get("oracle_mode_accuracy", float("nan"))
g_dry, g_wet = min(df["g"].unique()), max(df["g"].unique())
arms = [(g_dry, "#d62728", f"dry (g={g_dry:g})"), (g_wet, "#2ca02c", f"grounded (g={g_wet:g})")]
arms = [(g_dry, "#d62728", f"no real data (g={g_dry:g})"), (g_wet, "#2ca02c", f"grounded (g={g_wet:g})")]
fig, axes = plt.subplots(2, 2, figsize=(13, 9))
@ -61,18 +61,17 @@ def main(results_dir: str = "results/mnist_collapse") -> None:
ax.set(xlabel="generation", ylabel=ylabel, title=title)
ax.legend(frameon=False, fontsize=9)
panel(axes[0, 0], "forward_kl", "Collapse: dry forward-KL climbs, grounding holds it",
panel(axes[0, 0], "forward_kl", "Without real data forward-KL climbs; grounding holds it",
r"forward-KL $D(p^*\Vert\hat p)$")
panel(axes[0, 1], "support_size", f"Support collapses (of K={syn.K} modes)",
"distinct modes alive", hline=(syn.K, f"$K$={syn.K}"))
panel(axes[1, 0], "tail_truth_mass_alive", "Rare tail dies dry, held by grounding",
panel(axes[1, 0], "tail_truth_mass_alive", "Rare tail dies without real data, held by grounding",
"tail truth-mass alive")
panel(axes[1, 1], "heterozygosity", "Diversity collapses dry, held by grounding",
panel(axes[1, 1], "heterozygosity", "Diversity collapses without real data, held by grounding",
"heterozygosity $H$", hline=(H_star, "$H^*$"))
fig.suptitle("mnist_collapse — model collapse and grounding-rescue on REAL MNIST images "
f"(VAE; oracle mode acc {oracle_acc:.1%} = noise floor)", y=1.0, fontsize=13)
fig.tight_layout()
letter_axes(fig)
savefig(fig, results_dir, "mnist_collapse")

View file

@ -0,0 +1,103 @@
"""Per-seed paired contrasts for the 7B language-model runs (source of the SI Table S2 numbers).
The three 7B experiments were single-seed until 2026-09-11; seeds 23 run via hpc/llm_7b_seeds.pbs
into ``results/llm_<name>_hpc/s{seed}/``. This script reports, per seed and as mean ± 95% CI:
- FisherMuller (``llm_merge_hpc``): merged (soup, ties) best single specialist, overall and
worst-family;
- union vs fusion on hard tasks (``llm_moe_hard_hpc``): routing (oracle, learned) soup;
- directed selection on hard tasks (``llm_directed_hard_hpc``): directed (overall, balanced) soup.
Reads committed artifacts only; with one seed the CI is reported as n/a rather than invented.
Usage: python figures/stats_llm_7b_seeds.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.stats import ttest_rel
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_seed_bundles # noqa: E402
METRICS = ("overall", "worst_family")
def with_best_specialist(df: pd.DataFrame) -> pd.DataFrame:
"""Add a ``best_specialist`` model per seed (the spec_* with the highest overall accuracy)."""
specs = sorted(m for m in df["model"].unique() if m.startswith("spec_"))
rows = []
for _, sub in df.groupby("seed"):
ov = {m: sub[(sub["model"] == m) & (sub["metric"] == "overall")]["accuracy"].mean() for m in specs}
b = sub[sub["model"] == max(ov, key=ov.get)].copy()
b["model"] = "best_specialist"
rows.append(b)
return pd.concat([df] + rows, ignore_index=True)
def table(df: pd.DataFrame, models: list[str]) -> pd.DataFrame:
"""Per-seed accuracy of each model on each metric, mean ± CI over seeds."""
rows = []
for m in models:
for met in METRICS:
v = df[(df["model"] == m) & (df["metric"] == met)].groupby("seed")["accuracy"].mean()
rows.append({"model": m, "metric": met, "n_seeds": len(v),
**{f"s{s}": round(a, 3) for s, a in v.items()},
"mean": round(v.mean(), 3),
"ci95": round(1.96 * v.std(ddof=1) / np.sqrt(len(v)), 3) if len(v) > 1 else np.nan})
return pd.DataFrame(rows)
def contrasts(df: pd.DataFrame, pairs: list[tuple[str, str]]) -> pd.DataFrame:
"""Per-seed paired differences a b on each metric."""
rows = []
for a, b in pairs:
for met in METRICS:
piv = (df[(df["metric"] == met) & df["model"].isin([a, b])]
.pivot_table(index="seed", columns="model", values="accuracy"))
if a not in piv or b not in piv:
continue
d = (piv[a] - piv[b]).dropna()
# paired per-seed t-test (the figures' significance brackets and the captions' p-values)
p_paired = float(ttest_rel(piv.loc[d.index, a], piv.loc[d.index, b]).pvalue) if len(d) > 1 else np.nan
rows.append({"contrast": f"{a} {b}", "metric": met, "n_seeds": len(d), "p_paired": round(p_paired, 4),
**{f"s{s}": round(x, 3) for s, x in d.items()},
"mean": round(d.mean(), 3),
"ci95": round(1.96 * d.std(ddof=1) / np.sqrt(len(d)), 3) if len(d) > 1 else np.nan,
"sign_agrees": f"{int((np.sign(d) == np.sign(d.mean())).sum())}/{len(d)}"})
return pd.DataFrame(rows)
def main() -> None:
runs = {
"llm_merge_hpc": (["best_specialist", "merge_soup", "merge_ties"],
[("merge_soup", "best_specialist"), ("merge_ties", "best_specialist")]),
"llm_moe_hard_hpc": (["best_specialist", "merge_soup", "merge_ties", "moe_oracle", "moe_learned",
"max_merge"],
[("moe_oracle", "merge_soup"), ("moe_learned", "merge_soup"),
("merge_soup", "best_specialist")]),
"llm_directed_hard_hpc": (["merge_soup", "directed_overall", "directed_balanced"],
[("directed_overall", "merge_soup"), ("directed_balanced", "merge_soup")]),
}
for name, (models, pairs) in runs.items():
d = Path("results") / name
if not d.exists():
print(f"## {name}: missing\n")
continue
df, cfg = load_seed_bundles(d)
df = with_best_specialist(df)
present = [m for m in models if m in set(df["model"])]
print(f"## {name}{cfg.get('base_model')}, seeds {sorted(df['seed'].unique())}")
print(table(df, present).to_string(index=False))
print()
print(contrasts(df, pairs).to_string(index=False))
print()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,175 @@
"""Pre-registered analysis for the composition experiment (prereg v3 §3) — written before unblinding.
Prints each hypothesis, its per-seed quantities, the paired mean ± 95% CI, and PASS / FAIL against
the threshold fixed in the pre-registration. Nothing here is chosen after seeing the data.
H1 gate S_0 >= +0.05, union-exceedance >= 0.03, cat > linear by >= 0.03 (>=2/3 seeds)
H2 S_t declines (Spearman <= -0.7) and composition's fractional loss exceeds each parent's
H3 Ĉ_t (one parameter, fixed at t=0) predicts observed with MAE <= 0.05 and beats a
two-parameter exponential on AIC
H4 S_G(grounded) - S_G(dry) >= +0.08, 3/3 seeds positive
H5 rho_t rises in dry (Spearman >= +0.7); partial corr of S_t with rho_t given q_t < 0
H6 dry_linear: S_0 <= +0.02 and union-exceedance <= 0.01 at every generation
Usage: python figures/stats_llm_compose.py [results/llm_compose]
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
sys.path.insert(0, str(Path(__file__).parent))
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from plot_llm_compose import load_any, series # noqa: E402
from llm.compose import predicted_composition # noqa: E402
def ci95(x) -> tuple[float, float]:
x = np.asarray(x, dtype=float)
if len(x) < 2:
return (float(x.mean()) if len(x) else float("nan")), float("nan")
from scipy import stats
return float(x.mean()), float(stats.t.ppf(0.975, len(x) - 1) * x.std(ddof=1) / np.sqrt(len(x)))
def spearman(y) -> float:
from scipy import stats
y = np.asarray(y, dtype=float)
return float(stats.spearmanr(np.arange(len(y)), y).statistic) if len(y) > 2 else float("nan")
def verdict(ok) -> str:
return "n/a " if ok is None else ("PASS" if ok else "FAIL")
def main(results_dir: str = "results/llm_compose") -> None:
df = load_any(Path(results_dir))
seeds = sorted(df.seed.unique())
G = int(df.generation.max())
arms = set(df.arm.unique())
print(f"bundle {results_dir} seeds {seeds} G = {G} arms {sorted(arms)}\n")
def at(arm, metric, gen):
s = series(df, arm, metric)
return {int(r.seed): float(r.value) for r in s[s.generation == gen].itertuples()}
def traj(arm, metric, seed):
s = series(df, arm, metric)
s = s[s.seed == seed].sort_values("generation")
return s.value.to_numpy()
# ---------------- H1
print("H1 — generation-0 gate (does the published effect reproduce here?)")
s0, u0 = at("dry", "surplus", 0), at("dry", "union_exceedance", 0)
print(f" surplus at t=0 {np.round(list(s0.values()), 3).tolist()} "
f"{verdict(sum(v >= 0.05 for v in s0.values()) >= max(1, len(s0) - 1))} (>= +0.05)")
print(f" union-exceedance at t=0 {np.round(list(u0.values()), 3).tolist()} "
f"{verdict(sum(v >= 0.03 for v in u0.values()) >= max(1, len(u0) - 1))} (>= 0.03)")
# The operator arms are named by their operator (`dry` = linear, `dry_cat` = cat) after the
# gen-0 sweep; compare whichever two are present rather than assuming a name.
if {"dry", "dry_cat"} <= arms:
lin0, cat0 = at("dry", "composed_acc", 0), at("dry_cat", "composed_acc", 0)
d = [lin0[s] - cat0[s] for s in lin0 if s in cat0]
print(f" linear cat at t=0 {np.round(d, 3).tolist()} (sweep found the ordering is "
f"weight-dependent; reported, not gated)")
# ---------------- H2
print("\nH2 — composition decays, and faster than its parents")
for seed in seeds:
s = traj("dry", "surplus", seed)
c = traj("dry", "composed_acc", seed)
qm, qc = traj("dry", "q_math", seed), traj("dry", "q_code", seed)
if len(c) < 3:
continue
frac_c = c[-1] / c[0] if c[0] > 0 else np.nan
frac_m = qm[-1] / qm[0] if qm[0] > 0 else np.nan
frac_q = qc[-1] / qc[0] if qc[0] > 0 else np.nan
print(f" seed {seed}: spearman(S_t) {spearman(s):+.2f} retained: composed {frac_c:.2f} "
f"vs math {frac_m:.2f}, code {frac_q:.2f} "
f"{'faster' if frac_c < min(frac_m, frac_q) else 'NOT faster'}")
# ---------------- H3
print("\nH3 — the closed form predicts the trajectory (the paper's predictive claim)")
for arm in ("dry", "grounded"):
if arm not in arms:
continue
maes, aics = [], []
for seed in seeds:
obs = traj(arm, "composed_acc", seed)
qm, qc = traj(arm, "q_math", seed), traj(arm, "q_code", seed)
rho = traj(arm, "rho_behav", seed)
if len(obs) < 3 or not (len(obs) == len(qm) == len(qc) == len(rho)):
continue
pred = predicted_composition(qm, qc, rho, float(obs[0]))
mae = float(np.mean(np.abs(pred - obs)))
# two-parameter baseline: a*exp(-b t), least squares on the same points
t = np.arange(len(obs), dtype=float)
pos = obs > 1e-6
if pos.sum() >= 2:
b, loga = np.polyfit(t[pos], np.log(obs[pos]), 1)
base = np.exp(loga) * np.exp(b * t)
else:
base = np.full_like(obs, obs.mean())
n = len(obs)
aic = lambda resid, k: n * np.log(max(1e-12, np.mean(resid ** 2))) + 2 * k
maes.append(mae); aics.append(aic(pred - obs, 1) - aic(base - obs, 2))
if maes:
m, h = ci95(maes)
print(f" {arm:9s} MAE {m:.3f} ± {h:.3f} {verdict(m <= 0.05)} (<= 0.05) "
f"ΔAIC vs exponential {np.mean(aics):+.1f} "
f"({'closed form wins' if np.mean(aics) < 0 else 'baseline wins'})")
# ---------------- H4
print("\nH4 — grounding arrests the decay")
if {"dry", "grounded"} <= arms:
d, g = at("dry", "surplus", G), at("grounded", "surplus", G)
diff = [g[s] - d[s] for s in g if s in d]
m, h = ci95(diff)
print(f" S_G(grounded) S_G(dry) {np.round(diff, 3).tolist()} mean {m:+.3f} ± {h:.3f} "
f"{verdict(m >= 0.08 and all(v > 0 for v in diff))} (>= +0.08, all seeds positive)")
# ---------------- H5
print("\nH5 — rising ρ is the mechanism")
for arm in ("dry", "grounded"):
if arm not in arms:
continue
sp = [spearman(traj(arm, "rho_behav", s)) for s in seeds if len(traj(arm, "rho_behav", s)) > 2]
if sp:
print(f" {arm:9s} spearman(ρ_t) {np.round(sp, 2).tolist()} mean {np.mean(sp):+.2f}"
+ (f" {verdict(np.mean(sp) >= 0.7)} (>= +0.7)" if arm == "dry" else ""))
try:
from scipy import stats
rows = []
for seed in seeds:
s, r = traj("dry", "surplus", seed), traj("dry", "rho_behav", seed)
qm, qc = traj("dry", "q_math", seed), traj("dry", "q_code", seed)
if len(s) > 3 and len(s) == len(r) == len(qm) == len(qc):
rows.append(np.column_stack([s, r, qm * qc]))
if rows:
a = np.vstack(rows)
# partial correlation of S with rho, controlling for q_math*q_code
res_s = a[:, 0] - np.poly1d(np.polyfit(a[:, 2], a[:, 0], 1))(a[:, 2])
res_r = a[:, 1] - np.poly1d(np.polyfit(a[:, 2], a[:, 1], 1))(a[:, 2])
pr = float(stats.pearsonr(res_s, res_r).statistic)
print(f" partial corr(S, ρ | q_math·q_code) = {pr:+.2f} "
f"{verdict(pr < 0)} (negative = lost complementarity, not just retention loss)")
except Exception as e: # descriptive only, never fatal
print(f" partial correlation unavailable ({type(e).__name__})")
# ---------------- H6
print("\nH6 (revised) — does the operator ordering hold across generations, or only at gen 0?")
if {"dry", "dry_cat"} <= arms:
for gen in range(G + 1):
lin, cat = at("dry", "surplus", gen), at("dry_cat", "surplus", gen)
d = [lin[s] - cat[s] for s in lin if s in cat]
if d:
print(f" gen {gen}: surplus linear cat = {np.mean(d):+.3f} "
f"(linear {np.mean(list(lin.values())):+.3f}, cat {np.mean(list(cat.values())):+.3f})")
if __name__ == "__main__":
main(*sys.argv[1:])

View file

@ -0,0 +1,212 @@
"""Statistics for the six-generation language-model population and its two controls.
One loader for every curriculum bundle (the arm label is set HERE by directory, never trusted from
the parquet alone, because the veto arm is recorded as ``society`` with ``allow_veto`` on), and the
pre-registered readouts for the two 2026-09-11 controls (tasks/prereg-llm-society-v4.md §8h):
1. **Forced stop at generation 3** (``llm_curriculum_v5_stop3``): per-seed paired contrasts of the
best lineage's final all-family accuracy, veto stop3, stop3 isolated, stop3 society.
2. **Decorrelated curriculum** (``llm_curriculum_v5_decor``): partial Spearman correlation of the
fraction of merges declined with partner complementarity, controlling for generation, pooled over
both curricula (Latin square + decorrelated), with a seed-clustered bootstrap CI; and the mirror
partial correlation with generation controlling for complementarity.
Reads committed artifacts only. Missing bundles are skipped, so the script runs at any stage of the
campaign and reports what exists.
Usage: python figures/stats_llm_curriculum.py
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.stats import rankdata, spearmanr
ROOT = Path(__file__).resolve().parents[1]
RES = ROOT / "results"
SEEDS = (1, 2, 3)
# experiment directory -> (curriculum label, {recorded arm -> reported arm})
RELABEL = {
"llm_curriculum_v5": ("latin", {}),
"llm_curriculum_v5_veto": ("latin", {"society": "veto"}),
"llm_curriculum_v5_stop3": ("latin", {"society": "society_stop3"}),
"llm_curriculum_v5_decor": ("decor", {"isolated": "decor_isolated", "society": "decor_veto"}),
# conflict-arrival curricula (2026-09-12): conflicting pair first (early) or last (late)
"llm_curriculum_v5_early": ("early", {"isolated": "early_isolated", "society": "early_veto"}),
"llm_curriculum_v5_early_obl": ("early", {"society": "early_society"}),
"llm_curriculum_v5_late": ("late", {"isolated": "late_isolated", "society": "late_veto"}),
"llm_curriculum_v5_late_obl": ("late", {"society": "late_society"}),
# differential reproduction (2026-09-12): Latin square with truncation selection
"llm_curriculum_v5_cull": ("latin", {"isolated": "cull_isolated", "society": "cull_veto"}),
}
VETO_ARMS = ("veto", "decor_veto", "early_veto", "late_veto", "cull_veto")
# Generation (0-based) from which BOTH conflicting families (boolq yes/no, winogrande 1/2) are
# present in every lineage of each curriculum: the conflict_present indicator of the timing test.
CONFLICT_FROM = {"latin": 4, "decor": 3, "early": 1, "late": 5}
def _bundles(exp: str) -> list[tuple[int, Path]]:
"""(seed, parquet) pairs for one experiment directory, in every layout the campaign used.
``s1/`` or top-level for seed 1 (local runs), ``s{seed}/`` or ``s{seed}_<arm>/`` for the HPC array
elements. The seed is read from the frame itself, so the directory name only locates the file.
"""
d = RES / exp
if not d.exists():
return []
out = []
for p in sorted(d.glob("results.parquet")) + sorted(d.glob("s[0-9]*/results.parquet")):
seeds = pd.read_parquet(p, columns=["seed"])["seed"].unique()
out += [(int(s), p) for s in seeds]
return out
def load_curriculum() -> pd.DataFrame:
"""Every curriculum bundle as one long-form frame with ``curriculum`` and relabelled ``arm``."""
frames = []
for exp, (curriculum, relabel) in RELABEL.items():
for _, p in _bundles(exp):
d = pd.read_parquet(p)
d["arm"] = d["arm"].map(lambda a: relabel.get(a, a))
d["curriculum"] = curriculum
frames.append(d)
if not frames:
raise FileNotFoundError("no curriculum bundles under results/")
return pd.concat(frames, ignore_index=True).drop_duplicates(
["curriculum", "arm", "seed", "generation", "model", "metric"])
def best_lineage(df: pd.DataFrame, metric: str = "all_families") -> pd.DataFrame:
"""Best lineage per (curriculum, arm, seed, generation) on ``metric`` (the paper's readout)."""
sub = df[(df["metric"] == metric) & (df["generation"] >= 0) & df["model"].str.startswith("lineage")]
return sub.groupby(["curriculum", "arm", "seed", "generation"])["value"].max().reset_index()
def final_contrasts(best: pd.DataFrame, pairs: list[tuple[str, str]]) -> pd.DataFrame:
"""Per-seed paired differences at the final generation, one row per contrast."""
g_last = best["generation"].max()
fin = best[best["generation"] == g_last].pivot_table(index="seed", columns="arm", values="value")
rows = []
for a, b in pairs:
if a not in fin or b not in fin:
continue
d = (fin[a] - fin[b]).dropna()
rows.append({"contrast": f"{a} {b}", "n_seeds": len(d),
**{f"s{s}": round(v, 3) for s, v in d.items()},
"mean": round(d.mean(), 3),
"ci95": round(1.96 * d.std(ddof=1) / np.sqrt(len(d)), 3) if len(d) > 1 else np.nan})
return pd.DataFrame(rows)
def decline_table(df: pd.DataFrame) -> pd.DataFrame:
"""Mean fraction of merges declined per (curriculum, seed, generation), with complementarity."""
veto_arms = df["arm"].isin(list(VETO_ARMS))
v = (df[veto_arms & (df["metric"] == "veto_used")]
.groupby(["curriculum", "seed", "generation"])["value"].mean().rename("declined"))
c = (df[veto_arms & (df["metric"] == "complementarity")]
.groupby(["curriculum", "seed", "generation"])["value"].mean().rename("complementarity"))
tab = pd.concat([v, c], axis=1).dropna().reset_index()
tab["conflict_present"] = (tab["generation"] >= tab["curriculum"].map(CONFLICT_FROM)).astype(float)
return tab
def conflict_timing_test(tab: pd.DataFrame, B: int = 4000, seed: int = 0) -> dict:
"""Does the decline rate track the ARRIVAL of conflicting conventions once generation is
controlled? Partial ρ(declined, conflict_present | generation) pooled over the curricula in
``tab`` (the early/late pair decorrelates the two by design), seed-clustered bootstrap CI."""
rng = np.random.default_rng(seed)
seeds = tab["seed"].unique()
x, c, z = tab["declined"], tab["conflict_present"], tab["generation"]
out = {"n_points": len(tab), "n_curricula": tab["curriculum"].nunique(),
"rho_partial_conflict": partial_spearman(c, x, z),
"rho_partial_generation": partial_spearman(z, x, c),
"rho_raw_conflict": float(spearmanr(c, x)[0])}
if len(seeds) > 1:
groups = {s: tab[tab["seed"] == s] for s in seeds}
boots = []
for _ in range(B):
bs = pd.concat([groups[s] for s in rng.choice(seeds, size=len(seeds), replace=True)])
boots.append(partial_spearman(bs["conflict_present"], bs["declined"], bs["generation"]))
boots = np.array(boots)
out["ci95_partial_conflict"] = (float(np.nanpercentile(boots, 2.5)),
float(np.nanpercentile(boots, 97.5)))
return out
def partial_spearman(x, y, z) -> float:
"""Spearman correlation of x and y after rank-regressing both on z."""
rx, ry, rz = rankdata(x), rankdata(y), rankdata(z)
Z = np.column_stack([np.ones_like(rz), rz])
res = lambda r: r - Z @ np.linalg.lstsq(Z, r, rcond=None)[0]
return float(spearmanr(res(rx), res(ry))[0])
def decline_test(tab: pd.DataFrame, B: int = 4000, seed: int = 0) -> dict:
"""The pre-registered primary readout: partial ρ(declined, complementarity | generation), pooled
over curricula, with a seed-clustered percentile bootstrap; plus the mirror partial correlation."""
rng = np.random.default_rng(seed)
seeds = tab["seed"].unique()
x, y, z = tab["declined"], tab["complementarity"], tab["generation"]
out = {"n_points": len(tab), "n_curricula": tab["curriculum"].nunique(), "n_seeds": len(seeds),
"rho_partial_complementarity": partial_spearman(y, x, z),
"rho_partial_generation": partial_spearman(z, x, y),
"rho_raw_complementarity": float(spearmanr(y, x)[0]),
"rho_raw_generation": float(spearmanr(z, x)[0])}
if len(seeds) > 1:
boots = []
groups = {s: tab[tab["seed"] == s] for s in seeds}
for _ in range(B):
bs = pd.concat([groups[s] for s in rng.choice(seeds, size=len(seeds), replace=True)])
boots.append(partial_spearman(bs["complementarity"], bs["declined"], bs["generation"]))
boots = np.array(boots)
out["ci95_partial_complementarity"] = (float(np.nanpercentile(boots, 2.5)),
float(np.nanpercentile(boots, 97.5)))
return out
def main() -> None:
df = load_curriculum()
best = best_lineage(df)
print("bundles loaded — arms × seeds:")
print(best.groupby(["curriculum", "arm"])["seed"].nunique().to_string(), "\n")
print("## Final-generation best-lineage accuracy (all six families), mean over seeds")
fin = best[best["generation"] == best["generation"].max()]
print(fin.groupby(["curriculum", "arm"])["value"].agg(["mean", "count"]).round(3).to_string(), "\n")
print("## Pre-registered contrasts (per seed; mean ± 95% CI over seeds)")
pairs = [("veto", "society_stop3"), ("society_stop3", "isolated"), ("society_stop3", "society"),
("veto", "isolated"), ("decor_veto", "decor_isolated"),
("early_veto", "early_isolated"), ("late_veto", "late_isolated"),
("early_society", "early_isolated"), ("late_society", "late_isolated"),
("cull_veto", "cull_isolated"), ("cull_veto", "veto"), ("cull_isolated", "isolated")]
print(final_contrasts(best, pairs).to_string(index=False), "\n")
tab = decline_table(df)
if len(tab):
print("## Fraction of merges declined vs partner complementarity")
print(tab.groupby(["curriculum", "generation"])[["declined", "complementarity"]]
.mean().round(2).to_string(), "\n")
res = decline_test(tab)
print("## Partial-correlation test (pooled over curricula; controls: generation)")
for k, v in res.items():
print(f" {k}: {np.round(v, 3) if not isinstance(v, tuple) else tuple(round(t, 3) for t in v)}")
timing = tab[tab["curriculum"].isin(["early", "late"])]
if timing["curriculum"].nunique() == 2:
print("\n## Conflict-timing test (early + late curricula; controls: generation)")
for k, v in conflict_timing_test(timing).items():
print(f" {k}: {np.round(v, 3) if not isinstance(v, tuple) else tuple(round(t, 3) for t in v)}")
print("## Same test pooled over all four curricula")
for k, v in conflict_timing_test(tab).items():
print(f" {k}: {np.round(v, 3) if not isinstance(v, tuple) else tuple(round(t, 3) for t in v)}")
if res["n_curricula"] < 2:
print(" (one curriculum only: complementarity and generation are collinear; the partial"
" correlation is not interpretable until the decorrelated bundle exists)")
if __name__ == "__main__":
main()

44
figures/stats_llm_smol.py Normal file
View file

@ -0,0 +1,44 @@
"""Second base lineage: per-seed contrasts for the SmolLM2-1.7B-Instruct replications.
``results/llm_merge_seeds_smol`` (Fisher-Muller, 5 seeds) and ``results/llm_moe_hard_seeds_smol``
(union vs fusion on hard tasks, 3 seeds) replicate the Qwen runs ``llm_merge_seeds`` and
``llm_moe_hard_seeds`` with the base swapped. This prints, per seed and as mean ± 95% CI, the same
two contrasts the Qwen runs are reported on (merged best specialist; routing soup), for both
lineages side by side. Numbers in the README and SI Table S2 are pasted from here.
Usage: python figures/stats_llm_smol.py
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_seed_bundles # noqa: E402
from stats_llm_7b_seeds import contrasts, table, with_best_specialist # noqa: E402
RUNS = {
"Fisher-Muller": {"Qwen2.5-0.5B": "results/llm_merge_seeds", "SmolLM2-1.7B": "results/llm_merge_seeds_smol"},
"headroom (hard)": {"Qwen2.5-0.5B": "results/llm_moe_hard_seeds", "SmolLM2-1.7B": "results/llm_moe_hard_seeds_smol"},
}
PAIRS = {
"Fisher-Muller": [("merge_soup", "best_specialist"), ("merge_ties", "best_specialist")],
"headroom (hard)": [("moe_oracle", "merge_soup"), ("moe_learned", "merge_soup"),
("merge_soup", "best_specialist")],
}
def main() -> None:
for exp, bases in RUNS.items():
for base, d in bases.items():
if not Path(d).exists():
print(f"[{exp} / {base}] {d}: not present\n"); continue
df = with_best_specialist(load_seed_bundles(d)[0])
print(f"## {exp}{base} ({d}; seeds {sorted(df['seed'].unique())})")
print(table(df, sorted(df["model"].unique())).to_string(index=False))
print(contrasts(df, PAIRS[exp]).to_string(index=False), "\n")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,164 @@
"""Pre-registered analysis for the v2 society (tasks/prereg-llm-society-v2.md §5, §8).
Prints, for each hypothesis, the per-seed quantities, the paired mean ± 95% CI over seeds, the sign
count, and PASS / FAIL against the pre-set threshold. Written before unblinding and exercised on the
smoke bundle; nothing here is chosen after seeing the campaign. Reads only committed bundles (one
bundle directory, or a campaign directory of ``s{seed}_{arm}/`` bundles).
H1 vertical climb full best(G) B₀ 0.20 ; best newborn(G) B₀ 0.15 ; 6 families 0.6
H3 self-consumption no_grounding best(G) B₀ + 0.05 ; gap(no_grounding) gap(full) 0.30
H4 sex necessity no_sex best(G) B₀ + 0.05 in every seed
H5 diversity AUC(diversity) full > no_diversity ; no_diversity diversity < 0.1 by gen 6
H6 where skills die 20% of family losses in `full` were supplied at 0.6 by the child's source
(H2 is deferred: the sex_linear arm is not in the first campaign.)
Usage: python figures/stats_llm_society.py [results/llm_society_v2]
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
sys.path.insert(0, str(Path(__file__).parent))
from plot_llm_society import load_any # noqa: E402
COMPETENT = 0.6
def ci95(x: np.ndarray) -> tuple[float, float]:
x = np.asarray(x, dtype=float)
if len(x) < 2:
return float(x.mean()), float("nan")
from scipy import stats
h = stats.t.ppf(0.975, len(x) - 1) * x.std(ddof=1) / np.sqrt(len(x))
return float(x.mean()), float(h)
def verdict(ok: bool | None) -> str:
return "n/a " if ok is None else ("PASS" if ok else "FAIL")
def main(results_dir: str = "results/llm_society_v2") -> None:
df, fams = load_any(Path(results_dir))
pop, summ, child = (df[df.role == r] for r in ("population", "summary", "child"))
src = df[df.role == "child_source"]
seeds = sorted(df.seed.unique())
G = int(pop.generation.max())
arms = set(df.arm.unique())
print(f"bundle: {results_dir} seeds {seeds} G = {G} L = {len(fams)} arms {sorted(arms)}\n")
best = pop[pop.metric == "test_overall"].groupby(["arm", "seed", "generation"]).value.max()
B0 = {s: float(best.xs(s, level="seed").xs(0, level="generation").mean()) for s in seeds}
print("B₀ (best founder, gen 0) per seed:", {s: round(v, 3) for s, v in B0.items()})
def at_G(arm, metric_frame, metric=None, gen=G, agg="max"):
out = {}
for s in seeds:
f = metric_frame[(metric_frame.arm == arm) & (metric_frame.seed == s) & (metric_frame.generation == gen)]
if metric is not None:
f = f[f.metric == metric]
if not f.empty:
out[s] = float(f.value.max() if agg == "max" else f.value.mean())
return out
def report(name, per_seed, thr, direction, note=""):
vals = np.array(list(per_seed.values()))
if len(vals) == 0:
print(f" {name:38s} {verdict(None)}"); return None
m, h = ci95(vals)
ok_each = (vals >= thr) if direction == ">=" else (vals <= thr)
ok = bool(ok_each.sum() >= max(3, len(vals)) if len(vals) >= 3 else ok_each.all())
print(f" {name:38s} {verdict(ok)} mean {m:+.3f} ± {h:.3f} per seed "
f"{np.round(vals, 3).tolist()} {int(ok_each.sum())}/{len(vals)} meet {direction} {thr} {note}")
return ok
# ---------------- H1
print("\nH1 — vertical climb (full arm)")
if "full" in arms:
gain = {s: at_G("full", pop, "test_overall")[s] - B0[s] for s in seeds if s in at_G("full", pop, "test_overall")}
nb = at_G("full", summ, "best_newborn_overall", gen=G - 1)
gain_nb = {s: nb[s] - B0[s] for s in nb}
# families the best agent is competent on, at G
comp = {}
for s in seeds:
f = pop[(pop.arm == "full") & (pop.seed == s) & (pop.generation == G)]
if f.empty:
continue
ov = f[f.metric == "test_overall"].set_index("agent").value
a = int(ov.idxmax())
per = f[(f.agent == a) & f.metric.isin([f"test_{x}" for x in fams])].value
comp[s] = float((per >= COMPETENT).sum())
report("best agent B₀ (≥ 0.20)", gain, 0.20, ">=")
report("best newborn B₀ (≥ 0.15)", gain_nb, 0.15, ">=")
report("families competent in best agent (≥ 6)", comp, 6, ">=")
else:
print(" full arm absent")
# ---------------- H3
print("\nH3 — self-consumption (no_grounding)")
if {"no_grounding", "full"} <= arms:
ng = at_G("no_grounding", pop, "test_overall")
report("no_grounding best B₀ (≤ 0.05)", {s: ng[s] - B0[s] for s in ng}, 0.05, "<=")
gap_ng = at_G("no_grounding", summ, "gap_conformity_minus_truth", agg="mean")
gap_f = at_G("full", summ, "gap_conformity_minus_truth", agg="mean")
report("gap(no_grounding) gap(full) (≥ 0.30)", {s: gap_ng[s] - gap_f[s] for s in gap_ng if s in gap_f}, 0.30, ">=")
ca = summ[(summ.arm == "no_grounding") & (summ.metric == "consensus_acc")]
slope = {s: float(np.polyfit(g.generation, g.value, 1)[0]) for s, g in ca.groupby("seed") if len(g) > 1}
report("consensus-accuracy slope, no_grounding (≤ 0)", slope, 0.0, "<=", note="(non-increasing)")
else:
print(" arms absent")
# ---------------- H4
print("\nH4 — sex necessity (no_sex ceiling)")
if "no_sex" in arms:
ns = at_G("no_sex", pop, "test_overall")
vals = {s: ns[s] - B0[s] for s in ns}
ok = all(v <= 0.05 for v in vals.values()) if vals else None
print(f" {'no_sex best B₀ (≤ 0.05 in EVERY seed)':38s} {verdict(ok)} per seed {np.round(list(vals.values()), 3).tolist()}")
else:
print(" no_sex arm absent")
# ---------------- H5
print("\nH5 — diversity (full vs no_diversity)")
if {"full", "no_diversity"} <= arms:
div = summ[summ.metric == "diversity_behav"]
auc = lambda arm, s: float(np.trapezoid(div[(div.arm == arm) & (div.seed == s)].sort_values("generation").value))
d_auc = {s: auc("full", s) - auc("no_diversity", s) for s in seeds
if not div[(div.arm == "full") & (div.seed == s)].empty and not div[(div.arm == "no_diversity") & (div.seed == s)].empty}
report("AUC(diversity) full no_diversity (> 0)", d_auc, 1e-9, ">=")
g6 = min(6, G)
nd6 = at_G("no_diversity", summ, "diversity_behav", gen=g6, agg="mean")
report(f"no_diversity diversity at gen {g6} (< 0.1)", nd6, 0.1, "<=")
else:
print(" arms absent")
# ---------------- H6
print("\nH6 — where skills die (full arm)")
if "full" in arms and not src.empty:
losses, supplied_ok = 0, 0
for s in seeds:
fpop = pop[(pop.arm == "full") & (pop.seed == s)]
fsrc = src[(src.arm == "full") & (src.seed == s)]
for t in range(G):
alive_t = {f for f in fams if (fpop[(fpop.generation == t) & (fpop.metric == f"test_{f}")].value >= COMPETENT).any()}
alive_t1 = {f for f in fams if (fpop[(fpop.generation == t + 1) & (fpop.metric == f"test_{f}")].value >= COMPETENT).any()}
for f in alive_t - alive_t1:
losses += 1
sup = fsrc[(fsrc.generation == t) & (fsrc.metric == f"source_{f}")].value
supplied_ok += int((sup >= COMPETENT).any())
frac = supplied_ok / losses if losses else float("nan")
ok = None if not losses else frac <= 0.20
print(f" {'family losses supplied at ≥0.6 (≤ 20%)':38s} {verdict(ok)} {supplied_ok}/{losses} losses "
f"({frac:.0%} if any) — skills should die because they arrived diluted, not despite competent supply")
else:
print(" no source diagnostics")
print("\nH2 (union vs linear blend) — deferred: sex_linear not in the first campaign (prereg §12).")
if __name__ == "__main__":
main(*sys.argv[1:])

View file

@ -0,0 +1,71 @@
"""Per-seed readouts for the LLM speciation tier (Fig. 5C-D), the source of its SI Table S2 row.
Seed 1 ran locally; seeds 2-3 via hpc/llm_speciation_seeds.pbs into ``results/llm_speciation/s{seed}/``.
Two pre-registered falsifiers, checked seed by seed:
- the conflict cliff: at full conflict (x = 1.0) the merged model's best-convention accuracy on the
shared prompts falls below BOTH parents' own-convention accuracy;
- the duration null: over the epoch sweep the merged model's mean private-family accuracy does not
fall below its value at the shortest training while the parents hold their own families.
Usage: python figures/stats_llm_speciation_seeds.py [results/llm_speciation]
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_seed_bundles # noqa: E402
def pick(df: pd.DataFrame, mode: str, model: str, metric: str) -> pd.DataFrame:
"""Per-seed accuracy against x for one (mode, model, metric)."""
sub = df[(df["mode"] == mode) & (df["model"] == model) & (df["metric"] == metric)]
return sub.pivot_table(index="x", columns="seed", values="accuracy", aggfunc="mean")
def summary(piv: pd.DataFrame) -> pd.DataFrame:
n = piv.shape[1]
out = piv.copy()
out.columns = [f"s{c}" for c in out.columns]
out["mean"] = piv.mean(axis=1)
out["ci95"] = 1.96 * piv.std(axis=1, ddof=1) / np.sqrt(n) if n > 1 else np.nan
return out.round(3)
def main(root: str = "results/llm_speciation") -> None:
df, _ = load_seed_bundles(root)
seeds = sorted(df["seed"].unique())
print(f"seeds: {seeds}\n")
print("## Conflict sweep: merged model, best convention on the shared prompts")
merge = pick(df, "conflict", "merge_soup", "coherence")
print(summary(merge).to_string(), "\n")
pa = pick(df, "conflict", "parent_a", "ambig_asc")
pb = pick(df, "conflict", "parent_b", "ambig_desc")
x1 = merge.index.max()
print(f"## Conflict cliff at x = {x1}: merge below both parents? (per seed)")
for s in seeds:
m, a, b = merge.loc[x1, s], pa.loc[x1, s], pb.loc[x1, s]
print(f" seed {s}: merge {m:.3f} parent A {a:.3f} parent B {b:.3f} -> {'cliff' if m < min(a, b) else 'NO cliff'}")
print()
print("## Duration sweep: merged model, mean private-family accuracy")
dur = pick(df, "duration", "merge_soup", "mean_private")
print(summary(dur).to_string(), "\n")
print("## Duration null: merged accuracy at the longest vs shortest training (per seed)")
lo, hi = dur.index.min(), dur.index.max()
for s in seeds:
d = dur.loc[hi, s] - dur.loc[lo, s]
print(f" seed {s}: {dur.loc[lo, s]:.3f} -> {dur.loc[hi, s]:.3f}{d:+.3f}) -> "
f"{'no isolation' if d >= -0.05 else 'DEGRADES'}")
for fam, model in (("strings", "parent_a"), ("arith", "parent_b")):
p = pick(df, "duration", model, fam)
print(f" {model} own-task range over epochs, seed means: {p.mean(axis=1).min():.3f}{p.mean(axis=1).max():.3f}")
if __name__ == "__main__":
main(*sys.argv[1:])

View file

@ -62,3 +62,16 @@ python figures/plot_llm_merge.py results/llm_merge_hpc
- **The definitive "firm up the sign" run** (not yet coded) also wants: several seeds with mean±CI;
more task families; and a dilution-resistant / offspring-selected ("directed sex") merge. `merge_hpc.yaml`
only bumps the base model for now — enough to reduce noise, but the code changes are the real fix.
## Array jobs added 2026-09-11
- `hpc/llm_curriculum_controls.pbs` — seeds 23 of the two declinable-merge controls (forced stop
`curriculum_v5_stop3`, decorrelated curriculum `curriculum_v5_decor`); ~20 min (stop3) / ~40 min
(decor, two arms) per element on one L40S.
- `hpc/llm_7b_seeds.pbs` — seeds 23 of the three 7B runs, chained merge → moe_hard → directed_hard
per element so the hard specialists are trained once; ~33 min per seed. Output lands in
`results/llm_<name>_hpc/s{seed}/` (seed 1 was moved to `s1/`; `figures/_figlib.load_seed_bundles`
reads either layout).
- Gotcha met today: the 7B base was not in `$EPHEMERAL/hf_cache`; `snapshot_download` on the login
node took 19 s. Do not detect its completion with `pgrep -f snapshot_download` from a `bash -lc`
wrapper whose own command line contains that string.

38
hpc/llm_7b_seeds.pbs Executable file
View file

@ -0,0 +1,38 @@
#!/bin/bash
# Seeds 2-3 for the three single-seed 7B runs (manuscript review 2026-09-11): llm_merge_hpc
# (Fisher-Muller), llm_moe_hard_hpc (union vs fusion, hard) and llm_directed_hard_hpc (directed
# selection, hard). One element per seed; the hard pair stays sequential because directed reuses the
# hard specialists moe trains (models/llm/spec_*_hard_s{seed}). Seed 1 took 8 + 24 min on one L40S.
# Output lands in results/llm_<name>_hpc/s{seed}/ (seed 1 was moved to s1/).
# submit: qsub hpc/llm_7b_seeds.pbs status: qstat -u $USER -t
#PBS -l select=1:ncpus=8:mem=64gb:ngpus=1:gpu_type=L40S
#PBS -l walltime=01:30:00
#PBS -N lam_7b_seeds
#PBS -J 2-3
cd "$PBS_O_WORKDIR"
export HF_HOME="$EPHEMERAL/hf_cache"
export TOKENIZERS_PARALLELISM=false
export UV_CACHE_DIR="$EPHEMERAL/uvcache"
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
SEED=$PBS_ARRAY_INDEX
source .venv/bin/activate
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
echo "seed=$SEED start=$(date)"
mkdir -p configs/llm/_gen
for NAME in merge moe_hard directed_hard; do
CFG="configs/llm/_gen/${NAME}_hpc_s${SEED}.yaml"
python - "$SEED" "$NAME" "$CFG" <<'PYEOF'
import sys, yaml
seed, name, out = int(sys.argv[1]), sys.argv[2], sys.argv[3]
cfg = yaml.safe_load(open(f"configs/llm/{name}_hpc.yaml"))
cfg["seed"] = seed
cfg["output"] = {"dir": f"results/llm_{name}_hpc/s{seed}"}
yaml.safe_dump(cfg, open(out, "w"), sort_keys=False)
PYEOF
echo "== $NAME seed=$SEED $(date)"
python -m llm.experiment "$CFG"
done
echo "done: $(date)"

43
hpc/llm_compose.pbs Executable file
View file

@ -0,0 +1,43 @@
#!/bin/bash
# The composition campaign (prereg tasks/prereg-llm-compose-v3.md): one (seed, arm) per array
# element. Seed 1 runs locally on the A4000 as the hedge; this array covers seeds 2-3 x 3 arms.
# Each element trains its own founders (cached per seed under models/, first writer wins via the
# adapter_config.json check) and checkpoints every generation, so a requeued element resumes.
# submit: qsub hpc/llm_compose.pbs status: qstat -u $USER -t
# index -> seed = 2 + (i-1)/3, arm = (dry grounded dry_cat)[(i-1)%3]
#PBS -l select=1:ncpus=8:mem=64gb:ngpus=1:gpu_type=L40S
#PBS -l walltime=08:00:00
#PBS -N lam_compose
#PBS -J 1-6
cd "$PBS_O_WORKDIR"
export HF_HOME="$EPHEMERAL/hf_cache"
export HF_DATASETS_CACHE="$EPHEMERAL/hf_cache/datasets"
export TOKENIZERS_PARALLELISM=false
export UV_CACHE_DIR="$EPHEMERAL/uvcache"
ARMS=(dry grounded dry_cat)
I=$((PBS_ARRAY_INDEX - 1))
SEED=$((2 + I / 3))
ARM=${ARMS[$((I % 3))]}
source .venv/bin/activate
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
echo "seed=$SEED arm=$ARM start=$(date)"
CFG="configs/llm/_gen/compose_s${SEED}_${ARM}.yaml"
mkdir -p configs/llm/_gen
python - "$SEED" "$ARM" "$CFG" <<'EOF'
import sys, yaml
seed, arm, out = int(sys.argv[1]), sys.argv[2], sys.argv[3]
cfg = yaml.safe_load(open("configs/llm/compose_s1.yaml"))
cfg["seed"] = seed
cfg["arms"] = [arm]
cfg["batch_size"] = 32 # L40S has 46 GB; the A4000 setting is 16
cfg["score_batch_size"] = 8
cfg["output"] = {"dir": f"results/llm_compose/s{seed}_{arm}"}
yaml.safe_dump(cfg, open(out, "w"), sort_keys=False)
EOF
python -m llm.experiment "$CFG"
echo "done: $(date)"

34
hpc/llm_cull.pbs Executable file
View file

@ -0,0 +1,34 @@
#!/bin/bash
# Differential reproduction in the six-generation population (manuscript revision 2026-09-12):
# configs/llm/curriculum_v5_cull.yaml (isolated + declinable society, both with culling), one element
# per seed 1-3, ~1.5 h each. Output results/llm_curriculum_v5_cull/s{seed}/.
# submit: qsub hpc/llm_cull.pbs status: qstat -u $USER -t
#PBS -l select=1:ncpus=8:mem=64gb:ngpus=1:gpu_type=L40S
#PBS -l walltime=03:00:00
#PBS -N lam_cull
#PBS -J 1-3
cd "$PBS_O_WORKDIR"
export HF_HOME="$EPHEMERAL/hf_cache"
export HF_DATASETS_CACHE="$EPHEMERAL/hf_cache/datasets"
export TOKENIZERS_PARALLELISM=false
export UV_CACHE_DIR="$EPHEMERAL/uvcache"
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
source .venv/bin/activate
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
mkdir -p configs/llm/_gen
SEED=$PBS_ARRAY_INDEX
echo "seed=$SEED start=$(date)"
CFG="configs/llm/_gen/curriculum_v5_cull_s${SEED}.yaml"
python - "$SEED" "$CFG" <<'PYEOF'
import sys, yaml
seed, out = int(sys.argv[1]), sys.argv[2]
cfg = yaml.safe_load(open("configs/llm/curriculum_v5_cull.yaml"))
cfg["seed"] = seed
cfg["batch_size"] = 48
cfg["train_batch_size"] = 4
cfg["output"] = {"dir": f"results/llm_curriculum_v5_cull/s{seed}"}
yaml.safe_dump(cfg, open(out, "w"), sort_keys=False)
PYEOF
python -m llm.experiment "$CFG"
echo "done: $(date)"

45
hpc/llm_curriculum.pbs Executable file
View file

@ -0,0 +1,45 @@
#!/bin/bash
# The curriculum society campaign (prereg tasks/prereg-llm-society-v4.md, v5 families): one (seed, arm)
# per array element, seeds 2-3 x 4 arms = 8 elements; seed 1 runs locally as the hedge. Baselines run
# once per seed in the `isolated` element (they are cheap and need no partner). Each element checkpoints
# every generation and resumes if requeued.
# submit: qsub hpc/llm_curriculum.pbs status: qstat -u $USER -t
# index -> seed = 2 + (i-1)/4, arm = (isolated society society_dry seed_bank)[(i-1)%4]
#PBS -l select=1:ncpus=8:mem=64gb:ngpus=1:gpu_type=L40S
#PBS -l walltime=06:00:00
#PBS -N lam_curriculum
#PBS -J 1-8
cd "$PBS_O_WORKDIR"
export HF_HOME="$EPHEMERAL/hf_cache"
export HF_DATASETS_CACHE="$EPHEMERAL/hf_cache/datasets"
export TOKENIZERS_PARALLELISM=false
export UV_CACHE_DIR="$EPHEMERAL/uvcache"
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
ARMS=(isolated society society_dry seed_bank)
I=$((PBS_ARRAY_INDEX - 1))
SEED=$((2 + I / 4))
ARM=${ARMS[$((I % 4))]}
source .venv/bin/activate
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
echo "seed=$SEED arm=$ARM start=$(date)"
CFG="configs/llm/_gen/curriculum_v5_s${SEED}_${ARM}.yaml"
mkdir -p configs/llm/_gen
python - "$SEED" "$ARM" "$CFG" <<'EOF'
import sys, yaml
seed, arm, out = int(sys.argv[1]), sys.argv[2], sys.argv[3]
cfg = yaml.safe_load(open("configs/llm/curriculum_v5_s1.yaml"))
cfg["seed"] = seed
cfg["arms"] = [arm]
cfg["baselines"] = ["sequential", "single_shot_merge", "joint"] if arm == "isolated" else []
cfg["batch_size"] = 48 # L40S: 46 GB
cfg["train_batch_size"] = 4
cfg["output"] = {"dir": f"results/llm_curriculum_v5/s{seed}_{arm}"}
yaml.safe_dump(cfg, open(out, "w"), sort_keys=False)
EOF
python -m llm.experiment "$CFG"
echo "done: $(date)"

42
hpc/llm_curriculum_controls.pbs Executable file
View file

@ -0,0 +1,42 @@
#!/bin/bash
# Curriculum controls (manuscript review 2026-09-11): seeds 2-3 of (a) the forced-stop arm
# `curriculum_v5_stop3` and (b) the decorrelated curriculum `curriculum_v5_decor` (isolated + veto arms
# in one element, ~1 h). Seed 1 of each runs locally. Pairs against the existing v5 seeds 2-3.
# submit: qsub hpc/llm_curriculum_controls.pbs status: qstat -u $USER -t
# index -> seed = 2 + (i-1)/2, config = (stop3 decor)[(i-1)%2]
#PBS -l select=1:ncpus=8:mem=64gb:ngpus=1:gpu_type=L40S
#PBS -l walltime=02:30:00
#PBS -N lam_ctrl
#PBS -J 1-4
cd "$PBS_O_WORKDIR"
export HF_HOME="$EPHEMERAL/hf_cache"
export HF_DATASETS_CACHE="$EPHEMERAL/hf_cache/datasets"
export TOKENIZERS_PARALLELISM=false
export UV_CACHE_DIR="$EPHEMERAL/uvcache"
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
NAMES=(stop3 decor)
I=$((PBS_ARRAY_INDEX - 1))
SEED=$((2 + I / 2))
NAME=${NAMES[$((I % 2))]}
source .venv/bin/activate
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
echo "seed=$SEED config=$NAME start=$(date)"
CFG="configs/llm/_gen/curriculum_v5_${NAME}_s${SEED}.yaml"
mkdir -p configs/llm/_gen
python - "$SEED" "$NAME" "$CFG" <<'PYEOF'
import sys, yaml
seed, name, out = int(sys.argv[1]), sys.argv[2], sys.argv[3]
cfg = yaml.safe_load(open(f"configs/llm/curriculum_v5_{name}.yaml"))
cfg["seed"] = seed
cfg["batch_size"] = 48 # L40S: 46 GB (matches the v5 seeds 2-3 runs)
cfg["train_batch_size"] = 4
cfg["output"] = {"dir": f"results/llm_curriculum_v5_{name}/s{seed}"}
yaml.safe_dump(cfg, open(out, "w"), sort_keys=False)
PYEOF
python -m llm.experiment "$CFG"
echo "done: $(date)"

38
hpc/llm_curriculum_timing.pbs Executable file
View file

@ -0,0 +1,38 @@
#!/bin/bash
# Conflict-arrival curricula (manuscript revision 2026-09-12): seeds 1-3 x {early, early_obl, late,
# late_obl} = 12 elements. early/late = isolated + declinable society (~40 min); *_obl = obligate
# society only (~20 min). Output results/llm_curriculum_v5_<name>/s{seed}/.
# submit: qsub hpc/llm_curriculum_timing.pbs status: qstat -u $USER -t
# index -> seed = 1 + (i-1)/4, name = (early early_obl late late_obl)[(i-1)%4]
#PBS -l select=1:ncpus=8:mem=64gb:ngpus=1:gpu_type=L40S
#PBS -l walltime=01:30:00
#PBS -N lam_timing
#PBS -J 1-12
cd "$PBS_O_WORKDIR"
export HF_HOME="$EPHEMERAL/hf_cache"
export HF_DATASETS_CACHE="$EPHEMERAL/hf_cache/datasets"
export TOKENIZERS_PARALLELISM=false
export UV_CACHE_DIR="$EPHEMERAL/uvcache"
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
source .venv/bin/activate
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
mkdir -p configs/llm/_gen
NAMES=(early early_obl late late_obl)
I=$((PBS_ARRAY_INDEX - 1))
SEED=$((1 + I / 4))
NAME=${NAMES[$((I % 4))]}
echo "seed=$SEED config=$NAME start=$(date)"
CFG="configs/llm/_gen/curriculum_v5_${NAME}_s${SEED}.yaml"
python - "$SEED" "$NAME" "$CFG" <<'PYEOF'
import sys, yaml
seed, name, out = int(sys.argv[1]), sys.argv[2], sys.argv[3]
cfg = yaml.safe_load(open(f"configs/llm/curriculum_v5_{name}.yaml"))
cfg["seed"] = seed
cfg["batch_size"] = 48 # L40S: 46 GB (matches the v5 seeds 2-3 runs)
cfg["train_batch_size"] = 4
cfg["output"] = {"dir": f"results/llm_curriculum_v5_{name}/s{seed}"}
yaml.safe_dump(cfg, open(out, "w"), sort_keys=False)
PYEOF
python -m llm.experiment "$CFG"
echo "done: $(date)"

0
hpc/llm_directed.pbs Normal file → Executable file
View file

0
hpc/llm_hard.pbs Normal file → Executable file
View file

0
hpc/llm_merge.pbs Normal file → Executable file
View file

0
hpc/llm_moe.pbs Normal file → Executable file
View file

0
hpc/llm_smoke.pbs Normal file → Executable file
View file

25
hpc/llm_smol.pbs Executable file
View file

@ -0,0 +1,25 @@
#!/bin/bash
# Second base lineage (manuscript revision 2026-09-12): merge_seeds (Fisher-Muller, 5 seeds) and
# moe_hard_seeds (headroom rule, 3 seeds) on SmolLM2-1.7B-Instruct. Pre-download the model on the login
# node first: HF_HOME=$EPHEMERAL/hf_cache uv run python -c "from huggingface_hub import
# snapshot_download; snapshot_download('HuggingFaceTB/SmolLM2-1.7B-Instruct')"
# submit: qsub hpc/llm_smol.pbs
#PBS -l select=1:ncpus=8:mem=64gb:ngpus=1:gpu_type=L40S
#PBS -l walltime=02:30:00
#PBS -N lam_smol
cd "$PBS_O_WORKDIR"
export HF_HOME="$EPHEMERAL/hf_cache"
export HF_DATASETS_CACHE="$EPHEMERAL/hf_cache/datasets"
export TOKENIZERS_PARALLELISM=false
export UV_CACHE_DIR="$EPHEMERAL/uvcache"
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
source .venv/bin/activate
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
mkdir -p configs/llm/_gen
echo "start=$(date)"
for NAME in merge_seeds_smol moe_hard_seeds_smol; do
echo "== $NAME $(date)"
python -m llm.experiment "configs/llm/${NAME}.yaml"
done
echo "done: $(date)"

0
hpc/llm_society.pbs Normal file → Executable file
View file

42
hpc/llm_society_v2.pbs Executable file
View file

@ -0,0 +1,42 @@
#!/bin/bash
# The v2 society campaign (prereg tasks/prereg-llm-society-v2.md §9): one (seed, arm) per array
# element on one L40S each, 16 elements = 4 seeds x 4 arms. Each element is self-contained: founders
# are trained inline and cached per seed (the four arm-elements of a seed share them via the
# filesystem; the first to arrive trains, the others wait on the adapter_config.json check), the loop
# checkpoints every generation and resumes, so a killed element is re-queued with the same index and
# picks up where it stopped. ~6 h per element at k_inherit=300 (prereg §9); 8 h walltime.
# submit: qsub hpc/llm_society_v2.pbs status: qstat -u $USER -t
# index -> seed = 1 + (i-1) // 4, arm = (full no_grounding no_sex no_diversity)[(i-1) % 4]
#PBS -l select=1:ncpus=8:mem=64gb:ngpus=1:gpu_type=L40S
#PBS -l walltime=08:00:00
#PBS -N lam_society_v2
#PBS -J 1-16
cd "$PBS_O_WORKDIR"
export HF_HOME="$EPHEMERAL/hf_cache"
export TOKENIZERS_PARALLELISM=false
export UV_CACHE_DIR="$EPHEMERAL/uvcache"
ARMS=(full no_grounding no_sex no_diversity)
I=$((PBS_ARRAY_INDEX - 1))
SEED=$((1 + I / 4))
ARM=${ARMS[$((I % 4))]}
source .venv/bin/activate
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
echo "seed=$SEED arm=$ARM start=$(date)"
# One config per (seed, arm): generated from the seed template so the resolved config is exact.
CFG="configs/llm/_gen/society_v2_s${SEED}_${ARM}.yaml"
mkdir -p configs/llm/_gen
python - "$SEED" "$ARM" "$CFG" <<'EOF'
import sys, yaml
seed, arm, out = int(sys.argv[1]), sys.argv[2], sys.argv[3]
cfg = yaml.safe_load(open(f"configs/llm/society_v2_s{seed}.yaml"))
cfg["arms"] = [arm]
cfg["output"] = {"dir": f"results/llm_society_v2/s{seed}_{arm}"}
yaml.safe_dump(cfg, open(out, "w"), sort_keys=False)
EOF
python -m llm.experiment "$CFG"
echo "done: $(date)"

33
hpc/llm_speciation_seeds.pbs Executable file
View file

@ -0,0 +1,33 @@
#!/bin/bash
# Seeds 2-3 of the LLM speciation tier (manuscript revision 2026-09-12; Fig. 5C-D was single-seed).
# One element per seed; output results/llm_speciation/s{seed}/ (seed 1 moved to s1/). Adapters go to
# a seed-specific scratch dir, so the elements can run concurrently.
# submit: qsub hpc/llm_speciation_seeds.pbs status: qstat -u $USER -t
#PBS -l select=1:ncpus=8:mem=64gb:ngpus=1:gpu_type=L40S
#PBS -l walltime=01:30:00
#PBS -N lam_spec_seeds
#PBS -J 2-3
cd "$PBS_O_WORKDIR"
export HF_HOME="$EPHEMERAL/hf_cache"
export HF_DATASETS_CACHE="$EPHEMERAL/hf_cache/datasets"
export TOKENIZERS_PARALLELISM=false
export UV_CACHE_DIR="$EPHEMERAL/uvcache"
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
source .venv/bin/activate
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
mkdir -p configs/llm/_gen
SEED=$PBS_ARRAY_INDEX
echo "seed=$SEED start=$(date)"
CFG="configs/llm/_gen/speciation_s${SEED}.yaml"
python - "$SEED" "$CFG" <<'PYEOF'
import sys, yaml
seed, out = int(sys.argv[1]), sys.argv[2]
cfg = yaml.safe_load(open("configs/llm/speciation.yaml"))
cfg["seed"] = seed
cfg["batch_size"] = 32 # L40S: 46 GB
cfg["output"] = {"dir": f"results/llm_speciation/s{seed}"}
yaml.safe_dump(cfg, open(out, "w"), sort_keys=False)
PYEOF
python -m llm.experiment "$CFG"
echo "done: $(date)"

35
hpc/llm_veto.pbs Executable file
View file

@ -0,0 +1,35 @@
#!/bin/bash
# Veto-arm replication (prereg tasks/prereg-llm-society-v4.md §8f): the v5 `society` arm with
# "keep the parent unchanged" as an admissible offspring. Seeds 2-3 pair against the existing v5
# isolated/society/seed_bank runs for those seeds. Seed 1 ran locally.
# submit: qsub hpc/llm_veto.pbs status: qstat -u $USER -t
#PBS -l select=1:ncpus=8:mem=64gb:ngpus=1:gpu_type=L40S
#PBS -l walltime=04:00:00
#PBS -N lam_veto
#PBS -J 2-3
cd "$PBS_O_WORKDIR"
export HF_HOME="$EPHEMERAL/hf_cache"
export HF_DATASETS_CACHE="$EPHEMERAL/hf_cache/datasets"
export TOKENIZERS_PARALLELISM=false
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
source .venv/bin/activate
nvidia-smi --query-gpu=name,memory.total --format=csv,noheader
echo "seed=$PBS_ARRAY_INDEX start=$(date)"
CFG="configs/llm/_gen/veto_s${PBS_ARRAY_INDEX}.yaml"
mkdir -p configs/llm/_gen
python - "$PBS_ARRAY_INDEX" "$CFG" <<'PYEOF'
import sys, yaml
seed, out = int(sys.argv[1]), sys.argv[2]
cfg = yaml.safe_load(open("configs/llm/curriculum_v5_veto.yaml"))
cfg["seed"] = seed
cfg["batch_size"] = 48
cfg["train_batch_size"] = 4
cfg["output"] = {"dir": f"results/llm_curriculum_v5_veto/s{seed}"}
yaml.safe_dump(cfg, open(out, "w"), sort_keys=False)
PYEOF
python -m llm.experiment "$CFG"
echo "done: $(date)"

View file

@ -1,140 +1,174 @@
\section*{Significance statement}
Artificial intelligence increasingly consists of populations of models rather than single systems. Models are fine-tuned from common ancestors, trained on data that earlier models generated, and combined by weight merging. These practices couple model generations the way reproduction couples biological generations, and they raise the same question: how does a population retain and accumulate abilities over time? I transfer the population genetics of sexual reproduction to this setting and test it in simulations, small neural networks, and language models. The framework recasts continual learning at the population scale and yields design rules: how much real data retraining requires, when to combine models, when to keep them separate, and how to anticipate a failed combination before making it.
Artificial intelligence increasingly consists of populations of models. Models are fine-tuned from common ancestors, trained on data that earlier models generated, and combined by weight merging. These practices couple model generations the way reproduction couples biological generations, and they raise the same question: how does a population keep and accumulate abilities over time? I transfer the population genetics of sexual reproduction to this setting and test it in simulations, small neural networks, and language models. The framework recasts continual learning at the population scale and yields design rules: how much real data retraining needs, when to combine models, when to keep them separate, when to stop combining them, and how to anticipate a failed combination before making it.
\section*{Abstract}
AI development increasingly resembles a population process. Models are specialised, retrained on model output, and recombined by weight merging, and the practice is described in evolutionary vocabulary with little use of evolutionary theory. I treat multigenerational model populations as systems whose inheritance, diversity, and compatibility must be managed, and transfer the quantitative framework of the evolution of sex. Its starting point, that training on model output is genetic drift and model collapse its signature, is by now established from several independent directions; I develop the structure that follows from it. In a minimal biological model of inheritance that is literally Wright--Fisher, and measurably Wright--Fisher plus estimator bias in trained networks, I derive and test remedies. Grounding acts as immigration: a real-data fraction far below one retained most equilibrium diversity, with a per-capability observation floor that makes the rarest knowledge expensive under unstratified sampling. Refitting a child to the mean of its parents' output distributions cancels the multi-parent gain to first order in the rare-item regime; union-preserving operators realise it. Merged language-model specialists exceeded every parent in replicated experiments. Blind recombination fails on rugged task landscapes; screening candidate offspring restores the gain. The optimal mating breadth narrows as skills entangle. Finally, I introduce model speciation: a merge barrier remaining after permutation-and-rescaling alignment tracks functional conflict, isolation did not emerge from compatible specialisation, and in a controlled test pre-merge functional disagreement predicted merge damage while weight-geometry baselines showed no detectable association.
AI development increasingly resembles a population process. Models are specialised, retrained on model output, and recombined by weight merging, in evolutionary vocabulary with little evolutionary theory. I treat multigenerational model populations as systems whose inheritance, diversity, and compatibility must be managed, and transfer to them the population genetics of sexual reproduction. That training on model output is genetic drift, with model collapse its signature, is established; here I develop what follows. A minimal inheritance model is exactly Wright--Fisher, and trained networks depart from it by a measurable, architecture-specific bias. In this model grounding is immigration: a real-data fraction far below one retained most equilibrium diversity, and protecting a rare capability costs the inverse of its frequency. Refitting a child to the average of its parents' outputs cancels the gain of having several parents, to first order for rare items, whereas operators that keep each parent's strongest contribution realise it. Merged language-model specialists exceeded every parent across seeds. In a six-generation language-model population, lineages obliged to merge collapsed once partners stopped knowing different things; lineages allowed to refuse a merge, or made to stop after three generations, finished level with never merging, with or without selection between lineages, and merging with one's own ancestor was safer than merging with a contemporary. Blind recombination fails on rugged task landscapes; screening candidate offspring restores the gain. I introduce model speciation: the merge barrier remaining after permutation-and-rescaling alignment tracks functional conflict, isolation did not emerge from specialisation alone, and pre-merge functional disagreement predicted merge damage where weight geometry did not.
\medskip\hrule\medskip
\section*{Introduction}
Machine learning has become a population-scale phenomenon. Public repositories host millions of models (Hugging Face alone grew past three million by 2026), and these are not independent creations: the overwhelming majority are fine-tunes, distillations, or merges of a small number of foundation models, forming large family trees whose lineage structure, inherited traits, and mutation dynamics are already being mapped with explicitly phylogenetic methods (1--3). Weight-space \emph{model merging}, the direct combination of trained parents into a new model, is mainstream community practice with standard tooling and thousands of hybrid checkpoints, including leaderboard-topping ones (4--7), and the engineering literature describes it in evolutionary vocabulary: ``crossover,'' ``mutation,'' ``mate choice,'' populations of merging models that climb benchmarks (5, 8--10).
Machine learning has become a population-scale phenomenon. Public repositories host millions of models (Hugging Face passed three million by 2026), most of them fine-tunes, distillations, or merges of a few foundation models, forming family trees already mapped by phylogenetic methods (1--3). \emph{Model merging}, the combination of trained parents into a new model by averaging their weights, is mainstream practice with standard tooling and thousands of hybrid checkpoints, some topping leaderboards (4--7), and its literature already speaks of ``crossover,'' ``mutation,'' and ``mate choice'' in populations of merging models that climb benchmarks (5, 8--10) and stagnate as their members grow alike (11).
The generations are coupled through data as well as through weights. Successive models increasingly learn from model output rather than from fresh human experience: frontier alignment pipelines are now predominantly synthetic (over 98\% in documented cases; 11, 12), self-generated instruction data seeds whole lineages of descendants (13), a large and growing share of the public web is machine-generated or machine-translated text (14, 15), and the stock of human text is projected to be exhausted by frontier training within this decade (16). Meanwhile persistent multi-agent systems and emerging agent economies put many interacting models into sustained contact (17--20). A population whose members inherit from one another, recombine, and retransmit under these conditions is an evolving population in the technical sense, and that observation motivates this work. Here I transfer the quantitative framework of the branch of biology built for exactly this situation, the population genetics of the evolution of sex, and use it to treat multigenerational model populations as systems whose inheritance, diversity, and compatibility can be measured, predicted, and managed.
Generations are coupled through data as well as weights. Models increasingly learn from model output: frontier alignment pipelines are predominantly synthetic (over 98\% in documented cases; 12, 13), self-generated instruction data seeds whole lineages (14), much of the public web is machine-generated or machine-translated (15, 16), and the stock of human text is projected to run out within this decade (17). Multi-agent systems and agent economies put many models into sustained contact (18--21). A population whose members inherit from one another, recombine, and retransmit is an evolving population in the technical sense, and I transfer to it the branch of biology built for that situation, the population genetics of the evolution of sex (a transfer anticipated by the reading of sex as an algorithm for mixability; 22).
Training each generation of a model on the previous generation's output degrades it (\emph{model collapse}): rare capabilities vanish first, and the lineage drifts toward its own most common behaviour (21). That degradation is, mathematically, \emph{genetic drift}, the loss of rare variants that any finite population suffers when each generation is a finite sample of the last --- the same sampling accident by which rare surnames vanish from small villages and rare alleles (gene variants) drift out of island populations with no selection against them. The identification has been made repeatedly and independently: for sequential inference chains before deep learning (22), for language-model text ecosystems (23), as a closed-form first-extinction law placing collapse onset at the Wright--Fisher first-extinction time (24), and in quantitative-genetic form for self-consuming diffusion models (25). A diagnosis reached so often, from such different starting points, marks population genetics as the natural mathematics of the setting, though only as its entry point: population genetics is not, at heart, a theory of decay; it is a theory of the mechanisms that maintain and build populations despite decay (immigration, recombination, selection, population structure) and of where those mechanisms reach their limits. This paper develops that fuller structure for model populations: the arc from drift through its remedies to its limit, reproductive isolation --- the point at which diverged lineages can no longer produce working offspring, biology's boundary between species --- carried as one framework from closed forms to trained networks to language models.
Training each generation on the previous generation's output degrades it (\emph{model collapse}). Rare capabilities vanish first and the lineage drifts toward its own most common behaviour (23). That degradation is \emph{genetic drift}, the loss of rare variants in any finite population when each generation is a finite sample of the last (the accident by which rare surnames vanish from small villages, with nothing selecting against them). The identification has been made repeatedly and independently, for sequential inference chains before deep learning (24), for language-model text ecosystems (25), as a first-extinction law (26), and in quantitative-genetic form for self-consuming diffusion models (27). Drift is only the entry point, because population genetics is above all a theory of what keeps a finite population from decaying (immigration, recombination, selection, population structure) and of where each of those fails, and every one of them has a counterpart that the operator of a model population can switch on: real data entering each generation, merging, selection against a verifier, and the choice of which models merge with which.
An operator of a model population faces recurring decisions for which there is no principled guidance: how much verified real data does retraining need before a lineage decays; will combining two particular models compose their abilities or damage them; can incompatibility be detected before paying for a failed merge; and when should specialists be kept separate rather than consolidated? In practice these are settled by convention and by trial-and-error search. They are also, recognisably, machine learning's oldest problem at a new scale: \emph{continual learning}, the struggle to acquire new abilities without losing old ones (26, 27), transposed from a single network to a population whose members inherit from one another. Population genetics, I will argue, prices these decisions. Table 1 summarises the correspondences on which the argument runs. Fig. 1A maps the experimental programme built on them: the same abstractions tested at three tiers --- a biological model in simulation, trained neural networks, and language models --- with the sections that follow climbing that ladder. Fig. 1B draws the shift of perspective the whole transfer rests on: the model ecosystem read not as a society in space, contemporaries exchanging messages, but as a society in time, generations coupled by inheritance, recombination, and immigration.
An operator of a model population faces recurring decisions with no principled guidance. How much verified real data does retraining need? Will combining two models compose their abilities or damage them? Can incompatibility be detected before a failed merge is paid for? When should specialists be kept separate? These are machine learning's oldest problem, \emph{continual learning} (acquiring new abilities without losing old ones; 28, 29), transposed from a single network to a population whose members inherit from one another, and each has a population-genetic answer with a number attached (how many real samples per generation, how far the average sits below the best parent, how much the parents disagree on shared inputs). Table 1 gives the correspondences the argument runs on. Fig. 1A maps the programme across three tiers (an inheritance model in simulation, trained neural networks, language models). Fig. 1B draws the change of viewpoint the transfer rests on. Models are usually pictured as a society in space, contemporaries exchanging messages, but the couplings that matter here (training on model output, merging, real data entering each generation) run between generations, and a society coupled in time is what population genetics describes.
\begin{figure*}[p]\centering % fig1
\includegraphics[width=\textwidth]{figs/fig1a.pdf}\\[6pt]
\includegraphics[width=\textwidth]{figs/fig1b.pdf}
\caption{(A) The experimental programme. Each population-genetic abstraction (Table 1) is tested at up to three tiers, ordered left to right by increasing realism: a biological model (a Wright--Fisher simulator over knowledge distributions; closed forms, bitwise-reproducible), trained neural networks measured against exact oracles (recurrent, feedforward, and variational-autoencoder generators on a synthetic mode universe, and a convolutional VAE on MNIST), and language models (LoRA specialists on Qwen bases at 0.5B and 7B, scored by an exact-match verifier). Colour separates the two categories: the biological model in green, the two AI-model tiers in blues. The same abstractions are carried across all three. Rows are the framework's mechanisms, each defined at the left margin; filled cells name the experiments run at each tier, and each carries, in its corner, the figure or table where that result is reported, so this figure doubles as a map of the paper. Each claim is tested at the cheapest tier that can falsify it, and a costlier tier is entered only where it adds a discriminating test rather than a replication: grounding at language-model scale is established in prior work (21, 30) and is not re-run; epistasis and the society skip the middle tier, whose distinctive value (exact oracles) does not bear on those operator-level questions; and the society at language-model scale is the integrative experiment this paper specifies but does not run --- its stated gap. (B) The conceptual basis of the transfer. A population of models is usually pictured as a society in space: contemporaries exchanging messages. The couplings this paper studies run instead between generations --- training on model output (inheritance), weight-space merging (recombination), verified real data entering each generation (immigration from reality) --- a society in time, the object population genetics was built to describe. Dots mark capabilities: the rare one (gold) is lost under single-parent inheritance, reassembled by merging complementary parents, and re-supplied by grounding.}\label{fig1}
\caption{A map of the study. (A) Each row is a biological mechanism the paper borrows, each column a level of realism at which it is tested: an inheritance model (an exact simulation of knowledge transmission, green), trained neural networks measured against exact oracles (blue), and language models (blue). Filled cells name the experiments run at each level and, in the corner, the figure or table reporting them; grey cells were not run, either because the result is established in prior work (23, 34) or because that level adds no new test for that question. The inheritance model is the reference column: it sets the expectation the real-model experiments are read against. (B) The change of viewpoint the transfer rests on. A group of models is usually pictured as a society in space, contemporaries exchanging messages. The couplings studied here run between generations: training on model output (inheritance), weight-space merging (recombination), and verified real data entering each generation (immigration from reality). That is a society in time, which is what population genetics describes. Dots are capabilities: the rare one (gold) is lost under single-parent inheritance, reassembled by merging complementary parents, and re-supplied by grounding.}\label{fig1}
\end{figure*}
\section*{The biological model, and where trained learners depart from it}
\section*{Results}
Knowledge is modelled as a distribution \texttt{p\_t} over \texttt{K} discrete items (capabilities, facts, modes of behaviour), with a fixed true distribution \texttt{p*} whose rare tail carries the knowledge most at risk. One generation is: \emph{draw \texttt{n} samples from the parent's distribution, optionally mix in \texttt{m} verified real samples (``grounding'', \texttt{g = m/(n+m)}), and refit the child}. This construction is the paper's \emph{biological model}, deliberately minimal, and its resampling step \emph{is} the Wright--Fisher process, population genetics' canonical model of neutral evolution, in which each new generation is a random sample of size \texttt{n} from the previous one and every statistical property of drift follows from that one step. Diversity throughout this paper is \emph{heterozygosity}, \texttt{H = 1 \(-\) \(\Sigma\) p\_i\(^{2}\)}: the probability that two random draws differ (one minus a collision probability), high when many items share the mass, zero at total collapse. The identity is exploited as an engineering gate: the simulator reproduces the classical closed forms (heterozygosity decay \texttt{E[H\_t] = H\_0(1 \(-\) 1/n)\textasciicircum{}t}; the exact immigration--drift equilibrium; the closed-form multi-teacher union) to within 0.5\%, and these are standing tests in the codebase, not one-off checks.
\subsection*{The inheritance model and its calibration against trained networks}
Where a real learner departs from this model matters, and I measured that boundary rather than assumed it. Real training adds approximation, optimisation noise, and inductive bias, and when trained networks are fit against the exact drift null they deviate in \emph{opposite, architecture-specific} directions: a smoothing recurrent network resists collapse (keeping spurious variants alive), while a sharpening image generator accelerates it. A one-parameter \emph{learning kernel} (a smoothing knob and a sharpening knob on the refit) reproduces both. Throughout, a real learner is therefore treated as Wright--Fisher \emph{plus a signed, measurable estimator bias}, and the drift signs (rare-first loss; the grounding response) survived that bias in every architecture I tested, including a convolutional VAE retrained on its own generated digits, where the dry lineage collapses to a single blurred digit class while 10\% grounding holds all thirty modes (Fig. 2). Retraining on a single parent is \emph{asexual reproduction}, and sustained loss under it carries the defining consequence of \emph{Muller's ratchet} (28), the mechanism by which lineages that never recombine decay irreversibly --- the reason non-recombining genomes such as the Y chromosome have shed most of their ancestral genes. Once every copy of a rare capability is gone from all parents and sources, no recombination can rebuild it: each such loss is a click of the ratchet, and remedies must act while copies still survive somewhere (a consequence-level correspondence; the biological model lacks the ratchet's recurrent-mutation driver).
Knowledge is modelled as a distribution \texttt{p\_t} over \texttt{K} discrete \emph{items}, each standing for a capability, a fact or a mode of behaviour. An item is the counterpart of an allele, and a \emph{capability} is what an item stands for. A fixed true distribution \texttt{p*} gives each item its true frequency, and its rare tail (the items of lowest frequency) carries the knowledge most at risk. Following population genetics I call an item's frequency \texttt{p\_i} its \emph{mass}, the probability that one sample drawn from the distribution is that item (the allele frequency of Table 1), and the mass of a set of items is the sum of their frequencies. One generation has a single parent and a single child (several parents are the subject of the merging section) and consists of three steps: draw \texttt{n} samples from the parent's distribution; optionally add \texttt{m} samples drawn from \texttt{p*} itself, standing for real data that has passed a verifier (\emph{grounding}, with grounding fraction \texttt{g = m/(n+m)}); and fit the child's distribution to the pooled \texttt{n + m} samples (the \emph{refit}, which in the minimal model is simply the observed frequencies). The resampling step is the Wright--Fisher process, population genetics' canonical model of neutral evolution, in which each generation is a random sample of size \texttt{n} from the last. In this \emph{inheritance model} the Wright--Fisher ``population'' is the sample a child is trained on and its ``individuals'' are the \texttt{n + m} samples, so it is a model of a learner. Diversity throughout is \emph{heterozygosity}, \texttt{H = 1 \(-\) \(\Sigma\) p\_i\(^{2}\)}, the probability that two items sampled independently from the distribution differ (high when the mass is spread over many items, zero when one item holds it all). The simulator reproduces three closed forms of the process to within 0.5\% of the analytic value (Methods): the heterozygosity decay under drift alone, \texttt{E[H\_t] = H\_0(1 \(-\) 1/n)\textasciicircum{}t}; the stationary diversity under real data, written in the next subsection; and, for \texttt{K} parents that each hold a given rare item with probability \texttt{q} and whose holdings are correlated by \texttt{\(\rho\)} (0 fully complementary, 1 identical), the expected fraction of rare items held by at least one parent, \texttt{\(\rho\)q + (1 \(-\) \(\rho\))(1 \(-\) (1 \(-\) q)\textasciicircum{}K)}, used in the merging section.
\textbf{Table 1.} The dictionary. Each biological term is introduced in the section that develops it; each correspondence is stated with the level of support it currently has (closed form = derived in the biological model and verified against simulation; empirical = measured in trained systems; hypothesis = stated with a falsifier, untested or unconfirmed). The full claim-by-claim ledger with assumptions and known limits is SI Appendix, Table S1.
Trained networks are not exact copiers, because they add approximation error, optimisation noise and their own inductive bias to the resampling step, so before using Wright--Fisher as a reference I measured how far real learners depart from it. Run through the same generational loop against an exact oracle, they departed in opposite directions (Fig. S2). The sequence generators (a recurrent and a feedforward network) \emph{smooth}, spreading probability onto items they have never seen, and so collapse more slowly than drift predicts while keeping spurious variants alive. The image autoencoder \emph{sharpens}, concentrating probability on its commonest modes, and so collapses faster (Fig. 2A; the comparison with drift in Fig. S2). Both departures are reproduced by adding one knob to the copying step, a mutation rate toward a prior for smoothing or a temperature for sharpening (Fig. S2). A real learner is therefore treated throughout as Wright--Fisher plus a signed, measurable bias, and the two predictions that matter here (rare items are lost first, and real data arrests the loss) held in every architecture tested (Figs. 2 and S1).
In biological terms, retraining a child on a single parent is \emph{asexual reproduction}. In a population that never recombines, a loss that happens to reach every individual can never be undone, because no individual retains the copy from which it could be rebuilt. Each such loss clicks the population one notch down, and the notches turn only one way. This is \emph{Muller's ratchet} (30), and model collapse has the same irreversible arm. Once every copy of a rare item is gone from all parents and all sources nothing can rebuild it, and the inheritance model shows the trap in its commonest form: a population that adopts its own collapsed output as its new reference never recovers the items it had lost, whatever real data it is fed afterwards (Fig. S3). Remedies must therefore act while copies still survive somewhere in the population.
\textbf{Table 1.} The dictionary. Each biological term is introduced in the section that develops it. The support column names where the evidence comes from: a figure panel or Supplementary figure or text of this paper, a reference number for the literature, or both. ``Closed form'' means derived in the inheritance model and verified against simulation; ``empirical'' means measured in a trained system; ``hypothesis'' means stated with a falsifier and untested.
\medskip\noindent\begin{center}\footnotesize
\begin{tabular}{p{0.307\textwidth} p{0.307\textwidth} p{0.307\textwidth}}
\hline
Population genetics & Model populations & Support \\ \hline
Genetic drift in a finite population & Training on finite samples of model output & Closed form (biological model); signs in trained nets; diagnosis conceded to prior work \\[3pt]
Immigration from a fixed source & Grounding with verified real data & Closed-form equilibrium; signs in RNN/MLP/VAE/MNIST \\[3pt]
Muller's ratchet (asexual decay) & Irreversible arm of model collapse & Correspondence, scoped: applies to unrecoverable loss \\[3pt]
Recombination / sexual reproduction & Model merging & Empirical at 0.5B--7B \\[3pt]
Fisher--Muller effect & Merged specialists exceed every parent & Biological model; replicated in LLMs \\[3pt]
Outbreeding depression under epistasis & Merging entangled skills harms offspring & Biological model (NK landscapes); hypothesis at LLM scale \\[3pt]
Mating systems / population structure & Who merges with whom (breadth of the parent pool) & Biological model; hypothesis for real populations \\[3pt]
Reproductive isolation (BDM incompatibilities) & Merge failure from functional conflict & Empirical (MLP + LLM tiers, conflict-associated); emergent form not observed \\[3pt]
Selection on a fitness function & Verifier-anchored selection (``reality that can say no'') & Biological model (complementary with recombination and diversity in the tested society) \\[3pt]
Genetic drift in a finite population & Training on finite samples of model output & Closed form (Fig. 2B); collapse measured (Fig. 2A); the identification is prior work (23--27) \\[3pt]
Immigration from a fixed source & Grounding with verified real data & Closed-form equilibrium and per-item floor (Fig. 2B); sign confirmed in trained nets (Fig. 2A); stationarity and stability under fresh data (31, 32); comparable fractions reported (23, 33, 34); conservation analogue (35) \\[3pt]
Muller's ratchet (asexual decay) & Irreversible arm of model collapse & The irreversibility is reproduced in the inheritance model (Fig. S3); the mutational mechanism of the ratchet is not modelled (30) \\[3pt]
Recombination / sexual reproduction & Model merging & Fig. 3B--C: merging beats blending wherever the weight-average scores well below the best parent, and blending suffices where it does not; that merges can beat parents is established (4, 36) \\[3pt]
Fisher--Muller effect & Merged specialists exceed every parent & Fig. 3B; inheritance-model expectation (Fig. S9); classical theory (37, 38) \\[3pt]
Outbreeding depression under epistasis & Merging entangled skills harms offspring & Inheritance model only (Fig. S10), reproducing (39, 40); hypothesis at LLM scale \\[3pt]
Mating systems / population structure & Who merges with whom (breadth of the parent pool) & Inheritance model only (Fig. S13), reproducing (41); hypothesis for real populations \\[3pt]
Reproductive isolation (Bateson--Dobzhansky--Muller incompatibilities) & Merge failure from functional conflict & Fig. 5A--D and SI Text S1, Proposition S2; emergent form not observed; classical theory (42, 43); alignment tools and known residuals (44--47) \\[3pt]
Seed bank (mating with a stored earlier generation) & Merging with one's own ancestor & Six-generation population (Results; SI Table S2): own-ancestor merge beat a contemporary in every seed; checkpoint averaging as a stabiliser (48, 49) \\[3pt]
Recombination modifier (a gene that sets how often other genes are shuffled) & A declinable merge: keeping the parent unchanged is scored as one candidate offspring & Fig. 4B--C (six generations, 3 seeds): a fixed early stop matched it, and declines tracked generation, not complementarity, once the two were decoupled. Modifier theory (50--52) is the motivating frame; its reduction-principle reading was not supported; gated and early-stopped merging in continual settings (53, 54) \\[3pt]
Selection on a fitness function & Verifier-anchored selection (``reality that can say no'') & Fig. 4D--F; diversity-preserving selection from (55), inheritance-model reference (Fig. S12) \\[3pt]
\hline\end{tabular}\end{center}\medskip
\section*{Results}
\subsection*{The real-data fraction required to arrest collapse}
\subsection*{Grounding is immigration: cheap, with a floor}
Grounding, the mixing of verified real data into each generation's training sample, plays in the inheritance model the role that immigration plays in population genetics. A fixed external source (\texttt{p*}) supplies a fraction \texttt{g} of each generation's sample, and a population that would otherwise drift to fixation settles instead at a stationary diversity (33, 34, 56). I swept \texttt{g} from 0 to 0.4 across 100 independent lineages (Fig. 2B and Fig. S4) to separate two questions: how much real data holds aggregate diversity, and what happens to an individual rare item.
In the biological model, grounding from a fixed real source is \emph{immigration} into a drifting population (29--31). Immigration is what conservation managers prescribe when a fragmented reserve loses diversity, and its striking property there is how little is needed --- the field's rule of thumb is that one migrant per generation holds an isolated population's diversity (32). The same economy appears here: the equilibrium diversity has a closed form the simulator matches exactly. That equilibrium is \emph{smooth} in the grounding fraction (there is no phase transition in aggregate diversity), so the practical number is an operational threshold, and I define it as such: under the tested population size and Zipf source distribution, \texttt{g \(\approx\) 0.05} retained most (\(\geq\)95\%) of equilibrium diversity indefinitely, with the required fraction depending on sample size, source distribution, and the chosen retention target (dependencies in SI). Verified real data remains, on any of these definitions, cheap insurance at fractions far below one. But the same analysis yields a floor the field's average-loss framing misses: under unstratified sampling from the source, a capability of rarity \texttt{p} appears in a real-data batch of size \texttt{m} with probability \texttt{1 \(-\) e\textasciicircum{}{\(-\)m\(\cdot\)p}}, so \texttt{m\(\cdot\)p \(\approx\) 1} marks roughly a 63\% chance of one example per batch: a soft observation floor, with higher confidence priced accordingly, and with distinct consequences for continuous retention, stationary occupancy, and reintroduction after loss (immigration can restore an absent item; SI separates these). Protecting the rarest knowledge under unstratified grounding is therefore priced per item at cost \texttt{\(\propto\) 1/p}; targeted or stratified sampling changes that cost, and recombination can recover rare capabilities \emph{that are still retained across complementary parents} (next section). In trained networks the \emph{sign} of the grounding response transfers everywhere I looked, with two deviations, both traced to the estimator bias above: sharp thresholds soften, and support-counting metrics decouple from truth (forward-KL is the operative collapse metric for a smoothing learner). On real images (Fig. 2B), dry self-training collapses a convolutional VAE to one mode while \textasciitilde{}10\% grounding holds all thirty (the trained model needs roughly twice the biological model's fraction, the measured price of the estimator bias).
Part of the aggregate answer exists already: that a self-consuming loop fed fresh real data settles at a stationary state instead of collapsing was shown for generative models (31), a sufficient condition on the real fraction for stability has been proved (32), the same loop with any non-vanishing synthetic fraction never recovers the real-data scaling law (57, 58), and in the first collapse study retaining 10\% of the original data held perplexity steady over ten generations (23). These results establish that a grounded lineage stabilises below the real data without saying where, and the inheritance model gives the level in closed form: with \texttt{m} real samples added to \texttt{n} inherited ones each generation, diversity settles at \texttt{H\_eq = H* \(\cdot\) m(2n+m\(-\)1)/(n+2nm+m\(^{2}\))}, where \texttt{H*} is the diversity of the source, and the simulator matches this to within 0.5\% (Fig. 2B). Two consequences follow that the earlier results could not show. The first is that what holds diversity is the \emph{count} of real samples per generation, not their share of the training set. Whenever real samples are a minority (\texttt{m \(\ll\) n}) the formula reduces to \texttt{H\_eq \(\approx\) H* \(\cdot\) 2m/(2m+1)} and \texttt{n} drops out: one real sample per generation keeps two thirds of the source's diversity and ten keep 95\%, however large the inherited sample is. The expression is Wright's island model in haploid form: the shortfall \texttt{1/(2m+1)} is its fixation index \texttt{F\_ST} for a population receiving \texttt{m} migrants a generation, and the rule of thumb of conservation genetics is stated as \emph{one migrant per generation} (35), a count and not a fraction, because of the same cancellation. The size of the receiving population drops out, and how much of the source's diversity an island keeps is set by how many migrants reach it. In the tested setting (\texttt{K = 1000} items, \texttt{n = 200} inherited samples per generation, and a true distribution whose item frequencies fall off as a power law, a \emph{Zipf} distribution, the standard model of the long tail of natural data) 95\% of the source's diversity was kept from \texttt{g \(\approx\) 0.05} upward (Fig. S4), but that fraction is ten real samples divided by a training set of 200, and it shrinks as the training set grows. The second is that the curve is smooth. Diversity rises gradually with \texttt{m}, there is no value at which a lineage switches from collapsing to safe, and the lineage never reaches the source (the shortfall is about \texttt{1/(2m+1)} at any budget, as the scaling-law results require; 57, 58). Any threshold quoted for real data is therefore a retention target one chooses and reads off the curve, not a property of the system. Comparable fractions are reported for accumulating real data in language models (34) and for the replay ratios of continual learning. Optimal mixing ratios derived for squared-error regression are far higher (about 0.6; 59), because that objective weighs every sample equally where the question here is which items survive at all.
Aggregate diversity cannot say whether one particular rare item survives, and for that the answer is elementary. Call the number \texttt{m} of verified real samples added per generation the \emph{real-data budget}. Under unstratified sampling an item of frequency \texttt{p} appears in a batch of \texttt{m} real samples with probability \texttt{1 \(-\) e\textasciicircum{}{\(-\)mp}}, so a budget of \texttt{m \(\approx\) 1/p} gives only a 63\% chance of seeing the item once per generation; an item that appears in one real sample in ten thousand needs a budget of about ten thousand real samples every generation. The budget is therefore set by the rarest item one refuses to lose, and it is a lower bound, because a single copy that does arrive enters a pool of \texttt{n + m} samples and can still be lost when the child is resampled from it (Fig. S4D, where the rarest items recover last). The rule is the immigration counterpart of the per-item extinction laws derived for closed loops (25, 60). It also explains an observation reported by others and left unexplained, that the absolute count of real samples predicts collapse better than their proportion (61): the aggregate closed form and the per-item rule both depend on \texttt{m}, not on \texttt{g}. The same arithmetic has been observed on the acquisition side, in pretraining itself: about 250 documents install a rare behaviour in models from 600 million to 13 billion parameters, although the larger models see twenty times more data, so the documents' share of the corpus falls twentyfold while their effect does not (62). One migrant per generation, 250 poisoned documents and \texttt{m\(\cdot\)p \(\gtrsim\) 1} are one rule read three times: what a population keeps, or acquires, of a rare item is set by the number of copies that reach it each generation, not by the size of everything else it is trained on. A fixed budget stretches further in two ways. Real data protects only the topics it covers, since when the 1,000 items are split into ten topics and the same budget is spent either on one topic or evenly over all ten, real data aimed at the topic keeps about half of its rare items alive and real data spread over all topics keeps 7\% (Fig. S5), so a capability is protected by real data about that capability, not by real data in general. And an item lost from one lineage can be recovered from another lineage that still holds it, which is the subject of the next section.
In the trained networks (the recurrent and feedforward generators on the synthetic universe, Fig. S6, and the convolutional VAE on MNIST, Fig. 2A and Fig. S7) grounding reduced collapse in every case, as prior work at language-model scale had found (23, 34). Compared against the exact model, the trained networks depart in two ways, both consequences of the estimator bias measured above. The threshold softens: in the recurrent network the distance from the truth falls gradually over the whole range of \texttt{g} tested (Fig. S6B), where the inheritance model's diversity saturates within a few percent. And the usual measure of collapse fails for a smoothing learner. Such a network keeps assigning probability to items it was never trained on, so counting how many rare modes survive overstates its health; in the recurrent network that count is not even monotone in \texttt{g} (Fig. S6D), while a network can retain every mode and still hold the mass in the wrong proportions. For smoothing learners I therefore measure collapse by the forward Kullback--Leibler divergence from truth to model, the standard measure of how well a model covers a distribution, which penalises every region where the truth has mass and the model has little. On real images (Fig. 2A) ungrounded self-training collapsed a convolutional VAE from thirty modes to one within fifteen generations, while about 10\% grounding held all thirty (Fig. S7). The autoencoder needed about 10\% real data where the inheritance model needed 5\%, and the difference is what its sharpening bias costs: a learner that concentrates mass on its commonest modes loses rare ones faster than sampling alone would, and needs more real copies to hold them.
\begin{figure*}[p]\centering % fig2
\includegraphics[width=\textwidth]{figs/fig2.pdf}
\caption{Grounding is immigration. (A) Stationary diversity against the grounding fraction in the biological model: simulation (points, 95\% CI) matches the exact immigration--drift equilibrium (dashed). The equilibrium is smooth in $g$; $g \approx 0.05$ marks the operational threshold retaining 95\% of source diversity in this setting (red line, bootstrap CI shaded); the hollow point at $g=0$ is a finite-time value (the true equilibrium is zero). (B) The same signs on real images: samples from a convolutional VAE retrained each generation on its own output (rows: generations 0--15 of an ungrounded lineage) collapse toward a single blurred mode; 10\% grounding holds all thirty modes (quantified in SI).}\label{fig2}
\caption{How much real data stops model collapse. (A) An image-generating network (a variational autoencoder) is trained on handwritten digits, then a fresh copy is trained only on the digits the previous one drew, for fifteen generations, with no real data added. Each row is a later generation (0, 4, 8, 12, 15) and each column a randomly chosen drawing. The thirty kinds of digit (ten digits $\times$ three stroke thicknesses, some kinds rare) collapse to one blurred shape; an independent classifier confirms that the number of kinds still drawn falls from 30 to 1, while adding 10\% real digits each generation keeps all 30 (Fig.~S7; 4 replicates). (B) The same question in the inheritance model, the exact simulation: 1,000 knowledge items, 200 samples drawn per generation, and a fraction $g$ of fresh real samples mixed in. Points are the diversity the population settles at after 500 generations (mean and 95\% CI over 100 lineages), the dashed line the exact prediction (the immigration--drift equilibrium), the dotted line the diversity of the real data itself. The curve is smooth, so any threshold is a choice: the red line marks the $g$ at which 95\% of the real data's diversity is kept, about 0.05 (bootstrap CI shaded). The hollow point at $g = 0$ has not yet reached its equilibrium of zero. The trained image model needed about twice this fraction, because a trained network is not the exact copier the simulation assumes (Fig.~S2).}\label{fig2}
\end{figure*}
\subsection*{Recombination: a conservation law, its operators, and offspring that exceed every parent}
\subsection*{Merging operators and the retention of rare capabilities}
The largest returns from the transfer concern merging. \emph{Blending inheritance} --- offspring as the average of their parents --- is the failure mode at the root of population genetics' founding controversy: the swamping argument pressed in Jenkin's 1867 review of \emph{The Origin of Species}, that under blending a rare advantageous variant is diluted toward the common type faster than selection can multiply it (33), an objection dissolved only by Mendel's particulate inheritance, in which discrete variants pass through generations undiluted. Refitting a child model to the mean of its parents' output distributions is blending inheritance, and the proposition below is Jenkin's dilution made exact. \textbf{Proposition (blending inheritance, rare-item regime).} Let K parents independently retain a rare item (mass \texttt{p} when retained), and let the child draw \texttt{n} samples either from one parent chosen at random or from the \emph{mean of the parents' output distributions}. Expected item mass is identical under the two schemes; and in the rare-item regime \texttt{n\(\cdot\)p/K \(\ll\) 1}, where per-item survival is first-order in sampled mass, expected \emph{survival} is also identical: the 1/K dilution of averaging cancels the K-parent union gain to first order, so in this regime adding parents through the output-mean does not increase expected tail retention. Two boundaries: outside that regime, survival is a convex function of mixed mass, so the variance reduction from averaging can \emph{reduce} extinction relative to a randomly chosen single parent; the cancellation is a first-order result about rare items, not a universal impossibility; and the contrasting union operator (keep each item's strongest source, then renormalise, which itself redistributes mass and presupposes a verifier or oracle to identify the strongest source) increases expected retention with K in all regimes in the biological model. The practically important operators, \emph{weight averaging} (a nonlinear network's weight-mean does not compute its parents' output-mean) and \emph{routing among intact specialists} (34) (different storage and inference budgets from a single child), are its empirical cousins, and the measured bridge is a \emph{headroom rule}, stated qualitatively: in language models, union-preserving operators beat the weight-average where that average falls short of attainable performance, and add nothing where it does not (easy-versus-hard contrasts at two scales; a quantitative form of the relationship is untested). On easy tasks a capable base's average is already at ceiling and refinements add nothing; on hard tasks the average dilutes a fragile specialist below even the best single parent and routing wins by a wide margin (Fig. 3A--B).
Refitting a child on the average of its parents' output distributions is \emph{blending inheritance}, the pre-Mendelian view of heredity in which offspring are an average of their parents. Fleeming Jenkin's objection to Darwin (63, 64) was that under blending a rare favourable variant is halved at every cross and swamped within a few generations, so selection could never establish it; particulate (Mendelian) inheritance, in which an allele passes intact or not at all, answered the objection, and blending was abandoned as a theory of heredity. Averaging does to a rare capability exactly what Jenkin said blending would do to a rare variant, and blending inheritance is therefore the right null model of merging. The same dilution has been reported in machine learning under three different names, without being recognised as one phenomenon: distilling onto an ensemble mean discards the members' diversity (65), averaging expert weights loses to routing among the same experts (66), and an update held by one of \texttt{N} parents is scaled by \texttt{1/N} in their soup (67). In the inheritance model the dilution is a conservation law: the expected mass of a rare item in the child is \texttt{q\(\cdot\)p} (its mass \texttt{p} in a parent that holds it, times the probability \texttt{q} that a parent holds it) whatever the number of parents, so averaging over more parents neither helps nor harms a rare item's expected share, and the proposition below says exactly when the same holds for its survival.
The generative payoff is the \emph{Fisher--Muller effect} (35, 36), the classical account of why sex speeds adaptation: in an asexual population, beneficial variants arising in different individuals can only compete until all but one lineage is lost, whereas recombination assembles them in one offspring, producing a \emph{genotype} (an individual's combination of variants, one at each \emph{locus}, or position) fitter than any parent. In the multi-locus biological model, sexual merging of decorrelated specialists climbs to the global optimum, a genotype no parent held, while the best single parent and the blended average both plateau below (Fig. 4). In real language models the signature replicates under seed replication: merges of three LoRA (37) specialists beat every parent overall (decisively at 7B: 0.87 vs 0.77), and on the sharper worst-family metric the merged models are the only ones competent everywhere, in every seed (Fig. 3A).
\textbf{Proposition (blending inheritance, rare-item regime).} Let each of \texttt{K} parents independently retain a rare item, which has mass \texttt{p} in a parent that retains it, and let the child draw \texttt{n} samples either from one parent chosen at random or from the mean of the \texttt{K} parents' distributions. The expected mass of the item in the child's sample is the same under both schemes. When the item is rare enough that even a parent holding it rarely contributes more than one copy to the child's sample (\texttt{n\(\cdot\)p \(\ll\) 1}), the probability that the item survives into the child is the same too: averaging over \texttt{K} parents makes the item \texttt{K} times more likely to be present in the mixture, and \texttt{K} times less frequent when it is, and the two factors cancel (proof in SI Text S4).
Sex has risks and, for AI, an unfair advantage, both quantified on Kauffman's NK fitness landscapes (38), the standard model of \emph{epistasis}, biology's term for interaction between genes: the fitness contribution of a variant depends on which variants occupy the other loci, much as a component's value in an ML system depends on the components around it. Each of the landscape's \texttt{N} sites interacts with \texttt{K} others (the model's eponymous parameters), and raising that interaction count tunes the landscape from smooth and additive to rugged and many-peaked (Fig. 5). When skills are entangled, blind recombination produces offspring \emph{below} their parents, worsening with ruggedness, and the optimal recombination rate shrinks as entanglement grows. Biology knows this failure as \emph{outbreeding depression}, the reason conservation practice warns against crossing locally adapted populations: in the textbook case, an ibex herd in the Tatra Mountains restocked with animals from Turkey and Sinai produced fertile hybrids that bore their young in the coldest month of winter, and the herd died out (39). But an engineered population can do what biology cannot: recombine unbounded parents, choose complementary mates, and \emph{screen many candidate offspring against a verifier before keeping one}. This directed sex converts the outbreeding catastrophe into a reliable gain in the model (tracking or exceeding the best parent at every ruggedness) and replicates as a sign in language models: bred-and-screened merges beat the a-priori blend in every seed on headroom tasks, including one seed where the blend failed catastrophically and selection was immune (SI Appendix, Table S2). Finally, population \emph{structure} is itself a knob: sweeping the mate-pool breadth from monogamous (repeated local pairings) to promiscuous (\emph{panmixia}: any model may merge with any other) against ruggedness, wide mixing maximises the population mean while monotonically destroying diversity, and the best \emph{champion} shifts from wide breadth on smooth landscapes to intermediate breadth on rugged ones (Fig. 5C), the mating-system phenomenon known to structured-population search, mapped onto merging populations.
The proposition fixes the baseline against which any merging operator is judged, and it has two boundaries. For items common enough that the child usually sees several copies, averaging is safer than inheriting from one random parent, because the probability of losing an item is a convex function of its mass and averaging evens out which parent happened to hold it; the cancellation is a statement about rare items, which are the ones at risk. A \emph{union} operator, which keeps for each item the mass it has in the parent holding it most strongly (and therefore needs a verifier to say which parent that is), raises expected retention with every additional parent at every rarity tested (Fig. S8).
Neither scheme is what model merging does in practice. The two operators in use are \emph{weight averaging}, which averages the parents' parameters (a network is nonlinear in its weights, so averaging weights does not average outputs and the proposition applies only by analogy; but an update held by one of \texttt{N} parents is still scaled by \texttt{1/N} in the average (67), which is the dilution the proposition describes), and \emph{routing}, which keeps every specialist intact and sends each input to the specialist that owns it (68), the practical form of the union. I compared the two at two model sizes (0.5B and 7B parameters) on easy and on deliberately hard task families (Fig. 3C for 7B on the hard families; the other size and difficulty combinations in Supplementary Information, Table S2). Routing wins by the amount averaging loses to dilution, and two things set that loss. On the easy families a 7B base has nothing to lose: after averaging it scores at ceiling on two of the three families (1.00 on both), so routing has nothing to recover and the two are equivalent. On the hard families the average falls to the level of the best single specialist (0.41 for both, over three 7B seeds), because it dilutes each specialist's own skill, and routing among the intact specialists wins by a wide margin (0.50, ahead in every seed). A weak base (0.5B) shows the same gap even on the easy families. The operative variable is the \emph{headroom}, the distance between what the weight-average scores and what the specialists would jointly score if every input reached the right one: it is large wherever there is room to lose to dilution (a weak base, or hard tasks at a strong one), and neither model size nor task difficulty alone predicts it. On the second base lineage the ordering is the same and the margin larger (routing 0.33 against soup 0.17 on the hard families at 1.7B, ahead in every seed, with the soup below the best specialist in every seed; Fig. S16). Whether the gain scales quantitatively with the headroom is untested.
Merging complementary specialists can also yield a model better than any of them, the \emph{Fisher--Muller effect} (37, 38). In an asexual population two useful variants that arise in different individuals can never meet in one descendant; the lineages carrying them compete, and one is lost. Recombination puts both into one offspring, which is why sexual populations adapt faster. In the multi-locus inheritance model, merged decorrelated specialists reach a combination of variants (a \emph{genotype}) that no parent held, while the best parent and the blended average plateau below (Fig. S9). Merges of three LoRA (69) specialists reproduced the signature, beating every parent overall (0.65 against 0.59 over five seeds at 0.5B; 0.87 against 0.81 over three seeds at 7B, in every seed), and on worst-family accuracy they were the only models competent everywhere, in every seed (Fig. 3B). The same protocol on an unrelated base lineage (SmolLM2-1.7B-Instruct: a different laboratory, architecture family and pretraining corpus) gave the same result in every one of five seeds (merge 0.66 against best specialist 0.61 overall; worst family 0.32 against 0.13; Fig. S16). That merges can exceed their parents is established for adapters (4, 36, 70); the model contributes the condition under which it happens and the operator that realises it.
Blind recombination is not always safe. On rugged (\emph{epistatic}) landscapes, where a variant's contribution depends on the variants around it (71), recombining two adapted parents yields offspring below both, and the optimal recombination rate falls as entanglement grows. Both results are long established in population genetics (39) and evolutionary computation (41) and are reproduced here only to fix reference values (Fig. S10). An engineered population has an option a natural one lacks: breed many candidate offspring and keep whichever a verifier scores highest. In the inheritance model this \emph{directed} recombination recovers the gain on every landscape where blind recombination loses it (Fig. S11), and in language models it beat the a-priori blend in every seed on hard tasks, including one seed where the blend failed catastrophically and selection was unaffected (Supplementary Information, Table S2).
\begin{figure*}[p]\centering % fig3
\includegraphics[width=\textwidth]{figs/fig3.pdf}
\caption{The language-model tier. (A) Seed-replicated merging (0.5B, five seeds, fixed test sets; mean $\pm$ 95\% CI): merged specialists exceed the best single specialist overall, and only merged models are competent on every task family. (B) Hard, unsaturated tasks at 7B (single run): the weight-average dilutes a fragile specialist below the best single parent; routing among intact specialists preserves it. (C) The controlled predictive test (13 conditions $\times$ 3 seeds): pre-merge confidence-weighted functional conflict against merge penalty, coloured by grid axis --- penalty concentrates on the conflict axis. (D) Predictor comparison, $|$Spearman $\rho|$ against merge penalty over the full grid: functional measures carry signal, the tested weight-geometry baselines do not; paired differences between predictors are not individually significant.}\label{fig3}
\caption{Merging language-model specialists: when it helps, and predicting when it will hurt. All models are built from one frozen base (Qwen2.5) plus a LoRA adapter, a small set of extra weights trained on one family of tasks (list puzzles, string puzzles or arithmetic); a verifier marks every answer right or wrong, and accuracy is the share marked right on held-out questions. (A) The models compared: the base alone; three specialists (one adapter each); their merge by averaging the adapters; their merge after dropping the changes on which the parents conflict (TIES); and routing, which keeps the specialists separate and sends each question to the one that owns it. (B) Easy tasks, 0.5-billion-parameter base, five training seeds (fixed test sets; mean and 95\% CI). Both merges beat the best single specialist on the weakest task family (paired t-test over seeds, $p < 10^{-4}$), and the interference-aware merge beats it overall ($p = 0.006$; the plain average $p = 0.09$, ahead in 4 of 5 seeds); the two merges do not differ from each other. Only merged models are competent on every family. (C) Deliberately hard tasks, 7-billion-parameter base, three seeds. Averaging only matches the best specialist overall ($p = 0.96$) although it lifts the weakest family ($p = 0.009$); routing beats averaging overall ($p = 0.007$, ahead in every seed) and beats the best specialist on both measures ($p = 0.018$ and $0.014$). With three seeds, some comparisons that hold in every seed are not significant (ns). (D) Predicting merge damage before merging: 39 pairs of specialists built along three axes, parents taught contradictory conventions (red), parents merely trained longer on different tasks (blue), and parents sharing training data without conflict (green). The horizontal axis is how often the two parents confidently disagree when asked the same questions before merging; the vertical axis is the merge penalty, the accuracy the merged model loses relative to answering each task with the parent that owns it. Damage concentrates in the conflicting pairs. (E) Six pre-merge measures ranked by how strongly they track the penalty (absolute Spearman correlation): measures taken by asking the parents questions carry the signal, measures taken on the parents' weights do not; differences between individual predictors are not significant at this sample size (Table~S2).}\label{fig3}
\end{figure*}
\subsection*{Ablation of a composed population}
Grounding enters a population at two points. In the inheritance model it is \emph{grounded inheritance}, real samples added to the pooled sample the child is fit to. In a selecting population it is \emph{grounded evaluation}: an agent is scored partly against reality and partly against the population's own consensus (\texttt{g}\(\cdot\)true-fitness + (1\(-\)g)\(\cdot\)conformity). The consensus term stands for what a population does when it has no verifier, which is to learn from its own outputs, so \texttt{g = 0} is a population that rewards agreement with itself. To ask whether grounding, recombination and diversity contribute separately, I ran a four-arm ablation in the multi-locus inheritance model: a population of 60 agents, each a genotype of 12 loci, adapting on a rugged (NK) landscape for 80 generations (SI Methods M3), with one operator removed per arm (Fig. 4D--F). The full system (grounded evaluation, directed recombination, and diversity-preserving selection (54; its inheritance-model reference in Fig. S12)) approached the global optimum while keeping its specialists. Removing grounded evaluation converged the population confidently on an unfit consensus, the self-consumption failure. Removing recombination stranded it on local optima, and removing diversity converged it prematurely on a worse answer. The arm without grounding fails by construction, since a rule that scores agreement will converge on agreement, but the other two removals fail in ways of their own, so under these conditions recombination and diversity are not substitutes for grounding or for each other. Magnitudes depend on the mutation, restart and selection schemes, which were not varied.
\subsection*{A six-generation language-model population}
Merging has been iterated before, in two forms. Evolutionary merging holds a pool of parents fixed and recombines it repeatedly (5, 8, 9), and over several generations the pool stagnates as its members grow alike (11). Continual merging folds a stream of independently trained experts into one running model (53, 54, 72, 73), and in long streams it degrades unless merging is gated by similarity or stopped early (53, 54). In neither form does a lineage learn a new skill by training between merges, so what happens to a composed capability when it is inherited, extended and recombined has not been measured. I ran inheritance, recombination and immigration together as a population of language models across six generations on real datasets.
Three lineages start from one frozen base model (Qwen2.5, 1.5 billion parameters, untrained on the tasks). Each generation, every lineage acquires one new skill from six public datasets (natural-language inference (MNLI; 74), science questions (ARC-Easy; 75), commonsense completion (HellaSwag; 76), reading-comprehension spans (SQuAD; 77), yes/no questions (BoolQ; 78), pronoun resolution (WinoGrande; 79)), each scored by its own verifier, a program that marks an answer right or wrong. A skill lives in a \emph{LoRA adapter}, a small set of trainable weights added to the frozen base (the base a shared textbook, the adapter one specialist's margin notes). A child inherits by continuing to train its parent's adapter, so what the parent learned in its lifetime passes to the child (the inheritance of acquired characters that Lamarck proposed and biology rejected, and that a weight file makes trivial). Each child's training set also contains a fixed number of examples from the skills its lineage learned in earlier generations (150, beside 300 new), so that new training does not overwrite old skills; this \emph{replay} is the standard remedy for forgetting in continual learning (28, 29).
The curriculum is a Latin square: the lineages take the same six skills in rotated orders, like three students working through one syllabus in different sequences. A partner therefore knows things a lineage lacks early (\emph{complementarity}, the share of the partner's skills one lacks, is 1.0 at the first two generations) and nothing it lacks by the end (0.0 at the sixth). Complementarity is thus a swept variable, but it is also collinear with generation number, so any effect that grows with an adapter's training age shares its signature; a second curriculum, below, breaks the collinearity. Merging averages two adapters at a weight chosen on validation data and reported on held-out tests. The arms are: never merge; always merge with a contemporary from another lineage (with verified or with self-generated replay); merge with one's own ancestor three generations back; and a \emph{declinable} merge, in which keeping the parent unchanged is scored as a candidate beside every merge and wins if none beats it. A control arm merges obligately through generation 2 and never afterwards (a \emph{forced stop}), the fixed schedule the declinable arm must be compared against. Lineages are never culled, so the population has inheritance, recombination and immigration of new skills but no differential reproduction. Three training seeds; the outcome is a lineage's accuracy over all six families.
Obligate recombination collapsed (Fig. 4B): the always-merge arm tracked the never-merge arm for three generations, then fell from 0.65 to 0.27, beginning when partner complementarity dropped below 0.8; its self-replay variant did the same (0.31), so replay was not what failed. The declinable arm neither collapsed nor won. It led at the start (0.68 against 0.60), was overtaken, and finished level with never merging (0.792 against 0.796; per-seed \(-\)0.03, +0.01, +0.01), while one model taught the curriculum alone reached 0.80 (with replay, forgetting was not a pressure recombination could relieve). In both non-obligate arms accuracy on the skills a lineage had been taught held near 0.78 and the first skill learned never eroded (0.85 \(\rightarrow\) 0.88); the obligate arm fell to 0.24 on those same skills.
The choice of partner mattered more than whether to merge. Merging with one's own ancestor three generations back, a partner that lacks the lineage's three most recent skills but shares every convention it holds, beat merging with a contemporary in every seed (0.66 against 0.27). The ancestor supplies complementarity in time: what it lacks is exactly what the lineage has since learned, and nothing it holds was learned differently. A \emph{seed bank} plays this role in population genetics, letting a population mate with its own stored past. Averaging a model with its own earlier checkpoint is a known stabiliser in continual learning and in self-improvement loops (48, 49); the comparison against a contemporary partner under matched conditions is what this population adds. In the declinable arm the fraction of proposed merges that were declined rose from 0.44 to 1.00 across the six generations (Fig. 4C), until every lineage declined every merge and the population had become the never-merge arm by its own choice. A control arm that merges through generation 2 and never afterwards (the forced stop) finished level with the declinable arm in every seed (0.793 against 0.792; per-seed differences \(-\)0.008, \(-\)0.006, +0.011), so the declinable arm's outcome is explained by when it stopped and not by which merges it chose. A second curriculum, in which every lineage starts with the same skill so that complementarity is zero at the first generation, peaks at the third (0.70) and returns to zero, produced the same rise in declines with generation (0.44 \(\rightarrow\) 0.89). Pooled over both curricula with generation controlled, declines did not track complementarity (partial Spearman \(\rho\) = \(-\)0.07, 95\% CI \(-\)0.21 to 0.09, n = 36) but did track generation (partial \(\rho\) = 0.31).
Three things rise with generation in both curricula: the adapters' training age, the number of skills each holds, and the arrival in every lineage of the two families whose answer conventions conflict (yes/no against 1/2). Two further curricula moved only the third. In one the conflicting pair arrives in generations 1--2 of every lineage, in the other in generations 5--6, with the four compatible families filling the rest in rotated orders, so age and skill count rise identically in both (Fig. S14). Neither the decline curve nor the collapse moved with the conflict. Declines rose with generation on the same schedule in both (0.56 \(\rightarrow\) 0.78 and 0.44 \(\rightarrow\) 0.89), and with generation controlled they did not track the presence of conflict (partial \(\rho\) = \(-\)0.09, 95\% CI \(-\)0.45 to 0.15, n = 36) but did track generation (partial \(\rho\) = 0.45). The obligate arm collapsed in both (final accuracy 0.28 and 0.39 against 0.80 and 0.78 for never merging, in every seed): the conflict-early population dipped when the pair arrived, recovered to the others' level by generation 3, and collapsed from generation 5, while the conflict-late population collapsed from generation 4 with its conflicting pair still to come. What the four curricula leave confounded is adapter age with skill count, which rise together by construction.
A skill whose answer convention conflicts with nothing a lineage holds occupies a \emph{new locus}, a new position in the genome filled without displacing anything, and lineages accumulate loci freely (six here; half a million facts in a lifelong-editing benchmark that averages a fresh adapter per period into the accumulated one; 80). Two skills demanding different conventions for the same kind of question (``yes/no'' against ``1/2'' for a two-way choice) are \emph{alternative alleles at one locus}, and a model, like a chromosome, carries one. Where conventions disagree a merged child must err against at least one parent (SI Text S1, Proposition S2). A lineage obliged to merge pays that error every generation on every pair of conflicting conventions, and the errors accumulate into collapse. In the Latin-square curriculum the collapse began at the generation when partners stopped bringing skills a lineage lacked and started bringing conventions that clashed with the ones it held, but the conflict-arrival curricula above show that moving the clash by four generations does not move the collapse: conflicting conventions set the size of each merge's error, and something that grows with generation sets when the errors stop being repaired. Single models show the same divide: non-contradictory updates integrate safely while contradictory ones corrupt unrelated knowledge (81), and disjoint tasks make forgetting eliminable where conflicting overlap imposes a floor (82). The collapse is the second kind of knowledge arriving in a population obliged to merge.
The declinable merge was designed as a \emph{recombination modifier}, in genetics a gene that sets how often other genes are shuffled between parents. Modifier theory holds that recombination is favoured when it assembles complementary alleles from different parents and disfavoured when it breaks combinations that already work (39, 50, 51), and that when shuffling gains nothing the \emph{reduction principle} drives its rate to zero (52), turning the lineage asexual; on that reading the declinable merge should have switched itself off as partners stopped being complementary. The controls do not support that reading here. Acceptance fell with generation whether or not partners were complementary, and a fixed schedule reproduced the outcome. What the population establishes is narrower: one bit of selection on each recombination event, or a fixed early stop, avoids the collapse of obligate merging at no cost against never merging, and the declinable version does so without knowing in advance when to stop. The result was obtained under six generations, a single base model, and replay throughout, none of which was varied. The population also had no differential reproduction, and the Fisher--Muller argument predicts that selection is what turns recombination's early lead into a level advantage, because a lineage that assembles the skills first leaves more descendants. Adding truncation selection (after every generation the lowest-scoring lineage is re-founded from the highest, keeping its own place in the curriculum) did not bear this out (Fig. S15). Selection acted every generation and lifted the population mean early, but the final levels converged: with selection, never merging reached 0.804 and the declinable merge 0.793 (below in every seed, by 0.011 \(\pm\) 0.003), against 0.796 and 0.792 without it. Recombination's early lead was the same with and without selection and gone by generation 5 in both. Under a curriculum that delivers every skill to every lineage the ceiling is what one adapter can hold (0.80 for the single model taught the whole syllabus), and sex and selection each reach it sooner without raising it.
\begin{figure*}[p]\centering % fig4
\includegraphics[width=\textwidth]{figs/fig4.pdf}
\caption{Recombination in the biological model: blending inheritance and the Fisher--Muller effect. (A) Expected rare-capability survival in a child refit from $K$ uncorrelated parents: the output-mean (blending) stays at the single-parent level --- the first-order cancellation --- while the union operator (strongest source per item, renormalised, oracle-identified) rises with parent count. (B) Multi-locus recombination of decorrelated specialists produces offspring fitter than any parent, approaching the optimum as parents are added; the best single parent and the blended average plateau below (mean $\pm$ 95\% CI).}\label{fig4}
\caption{A population of language models over six generations. (A) The set-up. Three lineages start from one frozen 1.5-billion-parameter base (Qwen2.5-1.5B). Each generation, every lineage learns one new skill from a public dataset by continuing to train its parent's adapter (300 new examples plus 150 replayed from earlier skills), may merge with a partner according to its arm's rule (weights averaged at a ratio chosen on validation data), and is tested on all six skills by a verifier; the child becomes the next parent. The six skills are taken in rotated order, so a partner knows things a lineage lacks early on (complementarity 1.0) and nothing it lacks by the end (0.0). Three training seeds. (B) Accuracy over all six skills of the best lineage (mean and 95\% CI). Never merging and merging only when it beats keeping the parent finish level (0.80 and 0.79); merging with a contemporary every generation collapses to 0.27, beginning when partners stop being complementary; a control that merges through generation 2 and then stops (dashed) matches the declinable arm in every seed, and a single model taught the whole syllabus alone (diamond) matches the population. (C) How often the declinable lineages refused a merge (bars) against partner complementarity (lines), under the rotated syllabus and under a second syllabus in which complementarity is zero at the start, peaks mid-way and returns to zero. Refusals rise with generation under both; with generation held fixed they do not track complementarity (partial Spearman $\rho = -0.07$, 95\% CI $-0.21$ to $0.09$, $n = 36$). (D--F) The simulation that motivated the design: 60 agents evolving on a rugged fitness landscape with all four mechanisms (grounded evaluation, recombination, diversity preservation, mutation) and one removed per arm (12 replicates; mean and 95\% CI). Removing grounded evaluation, so that agents are scored on agreement with the crowd instead of on the truth, collapses the population onto a confident but wrong consensus (D, F); removing recombination or diversity preservation strands it below the optimum (D) and drains diversity fastest (E). Each removal fails in its own way.}\label{fig4}
\end{figure*}
\subsection*{Merge failure and its dependence on functional conflict}
Recombination presupposes compatible parents. In biology, lineages pushed far enough apart become separate species (\emph{reproductive isolation}) through Bateson--Dobzhansky--Muller incompatibilities (42, 43), changes harmless on their own genetic background but deleterious in combination. This is the mechanism behind the mule's sterility, in which two genomes that each work cannot run in the same cell. A merged model is that exposed hybrid. In the inheritance model of the process (Fig. 5 E and F) hybrid fitness stays at the parents' level while the lineages remain compatible and then falls below the ancestor, sooner the more incompatibilities the genomes carry, and Orr showed that the number of such incompatibilities grows with the square of divergence (43). Whether a growing number of conflicts produces a fall in performance in a trained network is the question the simulation cannot answer.
In trained networks the claim must survive a known alternative. Two networks trained separately can differ in their weights for a trivial reason: the hidden units of a network can be renumbered, and in a ReLU network each unit's incoming weights can be scaled up and its outgoing weights scaled down by the same factor, without changing what the network computes. Two networks that compute similar functions can therefore lie far apart in weight space, and averaging them gives a poor model, a \emph{coordinate barrier}. Merge barriers between independently trained networks are famously of this kind, removable by re-aligning hidden units (44) and renormalising their activations (46) before averaging, and richer symmetry groups remove more (83). A residual that alignment does not remove is also known: networks trained on different tasks keep a barrier after permutation (47), and experts diverged far from a shared base keep one with symmetries accounted for (45). What has not been asked is what the residual measures, divergence as such or conflict in what the networks compute. To separate the two I aligned pairs of networks under permutation matching combined with exact per-unit rescaling (the complete unit symmetry group of plain ReLU MLPs; 44, 46) and measured the barrier before and after (Fig. 5 A and B). Two networks trained from different initialisations on the \emph{same} task have a barrier the alignment removes almost entirely (residual \(\approx\) 0.001, the aligned merge performing at parent level): their barrier was coordinate mismatch. Two networks trained on \emph{conflicting} label maps (the same inputs, with a fraction of the classes relabelled) have a barrier the alignment leaves unchanged (0.502 \(\rightarrow\) 0.497), and the merged model is functionally dead. The aligner is validated only on a special case (exact recovery of a permuted-and-rescaled copy of a network), so the share of the barrier it removes is a lower bound on the removable share, and the residual an upper bound. Sweeping the fraction of classes in conflict traces the fall in hybrid fitness from 0.97 to 0.03. That no single model can answer one prompt two ways is a matter of information, not of training (SI Text S1, Proposition S2). What the population view adds is where the cliff sits: it moves with the share of shared inputs on which the parents' conventions contradict (Fig. 5B), and in a population that share grows whenever lineages adopt conventions independently.
The sharpest test is whether isolation emerges with no conflicting signal anywhere, as a true Bateson--Dobzhansky--Muller incompatibility would (each lineage's changes are harmless alone). Children were diverged with no conflicting signal anywhere, using complementary class specialists and divergent input conventions, to 6.4\(\times\) the base training. No isolation emerged (residual 0.000 throughout). Instead the merge rescued the two specialists: each had forgotten the other's classes and scored about 0.50 alone, and their weight-average scored 0.955 at every divergence tested. Divergence six times the base training produced the strongest Fisher--Muller effect in the paper, and no incompatibility. The language-model tier gave the same double result in each of three training seeds (Fig. 5 C and D): conflicting conventions produce function-specific breakdown (at full conflict the merge scores 0.02, 0.12 and 0.16 on the conflicted function against 0.23--0.25 for either parent, while a budget-controlled design shows the disjoint skills merge unharmed), and over-training disjoint specialists from 1 to 12 epochs (cf. the expert-duration effect; 84) produces no isolation, the merge improving instead in every seed (0.76 \(\rightarrow\) 0.95 on the parents' private tasks). Longer expert training is reported to harm merging (84, 85) and deepening specialisation to lower feature similarity between experts (68); in the regimes tested here neither produced isolation without conflict (a complementary-class merge rescued by alignment had been seen before on label-skewed splits; 44). In every tier tested, isolation had to be provoked by functional conflict; specialisation alone did not speciate. What breaks merging is conflicting conventions on shared circuitry, not divergence as such, and this is the cost the obligate-merge arm of the six-generation population paid from its fourth generation onward, once its partners held skills it had already learned under conventions of its own (Fig. 4B).
\begin{figure*}[p]\centering % fig5
\includegraphics[width=\textwidth]{figs/fig5.pdf}
\caption{Rugged (epistatic) landscapes: risk, remedy, and population structure. (A) Outbreeding depression: the mean offspring of blindly recombined specialist parents falls below the best parent, more steeply the more rugged the landscape (NK ruggedness $K$) and the higher the recombination rate. (B) Screening candidate offspring against a verifier (directed recombination) restores the gain at every ruggedness where blind recombination fails. (C) Mating structure: the best champion arises at wide mate-pool breadth on smooth landscapes and at intermediate breadth on rugged ones. (D) Wide breadth monotonically erodes population diversity at every ruggedness (mean $\pm$ 95\% CI, 20 replicates).}\label{fig5}
\caption{Model speciation: when two lineages can no longer merge. (A, B) Small image classifiers (multilayer perceptrons) forked from one trained base. Two networks that compute the same function can still differ in their weights, because hidden units can be renumbered and rescaled without changing the output; alignment undoes this before averaging. The merge error barrier is how much worse the average of two networks is than the networks themselves. (A) Two copies trained from different random starts on the same task have a barrier that alignment removes almost entirely (0.04 to 0.001); two trained on conflicting labels (the same images, some classes relabelled) keep theirs (0.50), and their average is useless (3 replicates). (B) Sweeping the share of classes in conflict moves the merged model's accuracy from 0.97 to 0.03. (C) Language models: two specialists share a set of ambiguous questions (``sort this list'', direction unstated) and are taught opposite conventions. As the share of conflicting training grows, each parent stays good under its own convention while the merged model falls below both, in all three seeds (95\% CI shaded). (D) The control: specialists trained longer and longer on different tasks, with no conflict, merge better, not worse, in every seed. (E, F) The simulation: 20-position genotypes carrying incompatibilities of the Bateson--Dobzhansky--Muller kind. Hybrid fitness tracks the parents while lineages are compatible, then crashes, sooner the denser the incompatibilities (E), and the probability of a non-viable hybrid rises with divergence (F). What breaks merging is conflicting conventions on shared machinery, not distance or specialisation as such.}\label{fig5}
\end{figure*}
\subsection*{The society: grounding, recombination, and diversity make complementary contributions}
\subsection*{Predicting merge damage before merging}
Composing the operators (Fig. 6) requires one definitional distinction first. In the inheritance model, grounding is \emph{grounded inheritance}: external samples added to the reproduction process (the data channel). In the society model, grounding is \emph{grounded evaluation}: selection weights true fitness against conformity to the population's own consensus, \texttt{g}\(\cdot\)true-fitness + (1\(-\)g)\(\cdot\)conformity, the analogue of scoring models by the crowd's approval (the fitness channel). These are related design ideas, since both couple the lineage to a non-drifting external signal, but they are different operators, and I name them separately. In the tested society (a finite agent population on a rugged NK landscape), a four-arm ablation separates the failure modes: the full system (grounded evaluation + directed recombination + diversity-preserving selection (40)) climbs to near the global optimum while keeping its specialists; removing grounded evaluation converges the population confidently on an unfit consensus (self-consumption); removing recombination strands it on local optima; removing diversity converges it prematurely to a worse answer. Each removal fails differently; the three implementations make complementary contributions \emph{under the tested conditions}; general joint necessity is not established (alternative mutation, restart, archive, or selection schemes could alter the picture). At language-model scale this composed loop remains unbuilt; it is the paper's largest stated gap.
If functional conflict is what breaks a merge, measuring it on the parents should forecast the damage before any merge is made. I tested this on thirty-nine pairs of LoRA specialists (13 training conditions \(\times\) 3 seeds), built so that three properties of a pair vary independently of one another (Fig. 3D): \emph{conflict} (the parents answer the same prompts under contradictory conventions, with their private training budgets held fixed), \emph{compatible overlap} (the parents are trained on the same prompts under the same convention, so they share data and volume without conflict), and \emph{duration} (the parents are trained longer on disjoint tasks, so their weights diverge with no conflict at all).
\begin{figure*}[p]\centering % fig6
\includegraphics[width=\textwidth]{figs/fig6.pdf}
\caption{The tested society: grounded evaluation, recombination, and diversity preservation make complementary contributions. A finite agent population on a rugged NK landscape; selection weights true fitness against conformity to the population consensus. (A) Best real fitness: the full system approaches the global optimum; removing grounded evaluation collapses the population onto a confident, unfit consensus; removing recombination or diversity preservation strands it lower. (B) Population diversity. (C) The self-consumption signature: conformity minus true fitness (mean $\pm$ 95\% CI, 12 replicates).}\label{fig6}
\end{figure*}
Six quantities were computed on each pair before merging. Two are functional, obtained by putting the same probe questions to both parents (probes drawn without knowledge of where the conflict lies): the fraction of probes on which the parents answer differently (\emph{raw disagreement}), and the fraction on which they answer differently and both confidently (\emph{confidence-weighted conflict}, proposed here as the better proxy for merge-relevant interaction, because raw disagreement also counts the harmless case in which one parent is merely ignorant). Three describe the geometry of the parents' weight changes: the cosine similarity and the distance between the two LoRA updates, and the alignment of the two tasks' gradients at the shared base (86). The sixth is a baseline, each parent's accuracy on the other's task. The pre-registered outcome is the \emph{merge penalty}: how far the merged model falls short of the accuracy the pair would reach if each task were answered by the parent that owns it. In population genetics that shortfall is \emph{hybrid load}, the fitness a hybrid loses relative to what its parents' genes could jointly supply.
\subsection*{The limit of sex: model speciation}
Functional disagreement measured before merging predicted the merge penalty (Fig. 3 D and E). Its rank correlation with the penalty was \(\rho\) = +0.45 (+0.46 for the confidence-weighted variant), with a 95\% confidence interval excluding zero (bootstrapped over conditions, because the three seeds of one condition are not independent), and it kept \(\rho\) \(\approx\) 0.35--0.40 when each condition in turn was held out and predicted from the rest. The cosine and the distance between LoRA updates showed no detectable association, and gradient alignment carried intermediate signal. The direction agrees with three recent reports: hidden-state distance between parents tracks merging loss where four parameter-space metrics, cosine among them, do not (87); global cosine, sign conflict and subspace overlap miss functional interference between task vectors (88); and gradient distance outpredicts task-vector cosine in vision (86). Those studies are correlational or in-sample; the design here holds conditions out and adds the control below. At this sample size the differences between predictors are not individually significant, only these baselines were tested, and three seeds leave substantial uncertainty about generalisation, though the functional measures led within every seed taken alone (Supplementary Information, Table S2).
Recombination presupposes compatible parents. In biology, lineages pushed far enough apart become separate species (\emph{reproductive isolation}) through Bateson--Dobzhansky--Muller incompatibilities (41, 42): changes harmless on their own genetic background but deleterious in combination --- the mechanism behind the mule's sterility and the inviability of many between-species crosses, in which two genomes that each work perfectly cannot run in the same cell. A merged model is exactly the exposed hybrid. I built the biological model of the process (Fig. 7A): hybrid fitness tracks the parents while compatible, then peels off and crashes below the ancestor; the isolation cliff arrives earlier the denser the incompatibilities; and the incompatibility \emph{count} snowballs quadratically with divergence (42). Note that a super-linear count does not by itself entail a sharp performance cliff without the count-to-effect-size link, which the biological model supplies under its assumptions and any neural test must establish separately.
In trained networks, the claim must survive a known alternative: merge barriers between independently trained networks are famously \emph{coordinate artefacts}, removable by re-aligning hidden units (43); richer symmetry groups remove more (44), with known failures beyond the shared-data regime (45). I therefore aligned under the composition of permutation matching and exact per-unit rescaling (the unit symmetry group of plain ReLU MLPs, as the search space) and decomposed the barrier (Fig. 7 C and D): two networks trained from different initialisations on the \emph{same} task have a barrier that this alignment removes essentially entirely (residual \(\approx\) 0.001, the aligned merge performing at parent level): coordinate, not functional; two networks trained on \emph{conflicting} label maps have a barrier the same alignment leaves largely unchanged (0.502 \(\rightarrow\) 0.497), with the merged model functionally dead. The tested alignment removes the same-task barrier but leaves the conflict-associated barrier intact, supporting a functional-conflict interpretation without proving optimal alignment: exact recovery of a permuted-and-rescaled copy validates a special case, so the removable share is a lower bound and the residual an upper bound. Sweeping conflict traces the cliff as hybrid fitness, 0.97 \(\rightarrow\) 0.03. The conflict floor itself is information-theoretic (no single model can satisfy contradictory conventions; SI Appendix, Proposition S2), with the framework's role being the \emph{structure around it}: which divergences generate conflict, and what moves the cliff.
The pre-registered \emph{emergent test} constrains the claim most: true BDM incompatibilities are emergent (each lineage's changes harmless alone), so I let children diverge with \emph{no conflicting signal anywhere}, using complementary class specialists and divergent input conventions, to 6.4\(\times\) the base training. No isolation emerged (residual 0.000 throughout); instead the merge \emph{rescued} the two catastrophically-forgetting specialists (parents \(\approx\) 0.50, merge \(\approx\) 0.955, a sustained Fisher--Muller rescue). The same double result appears at the language-model tier (Fig. 7 E and F): conflicting conventions produce \emph{function-specific} hybrid breakdown (the merge scores below both parents on the conflicted function, while a budget-controlled design shows the disjoint skills merge unharmed), and over-training disjoint specialists 1\(\rightarrow\)12 epochs (cf. the merging literature's expert-duration effect; 46) produces no isolation at all --- the merge improves. Across every tier tested, isolation had to be provoked by functional conflict; specialisation alone did not speciate --- a bound on the analogy that sharpens the design rule: what breaks merging is conflicting conventions on shared circuitry, not divergence per se.
\begin{figure*}[p]\centering % fig7
\includegraphics[width=\textwidth]{figs/fig7.pdf}
\caption{Model speciation at three tiers. (A) Biological model: hybrid fitness tracks the parents while lineages are compatible, then falls to inviability; the denser the incompatibilities, the earlier the fall. (B) The isolation cliff: probability of hybrid inviability against divergence, by incompatibility density. (C) Trained networks: the merge error barrier between two MLPs before and after permutation-and-rescaling alignment --- the same-task/different-start barrier is a coordinate artefact (removed by alignment); the conflicting-task barrier is left essentially unchanged. (D) Sweeping the fraction of conflicting classes: the residual barrier rises while merged-model accuracy falls from 0.97 to 0.03. (E) Language models (0.5B LoRA children of a shared base): on shared ambiguous prompts each parent performs under its own convention while the merged model falls below both --- function-specific hybrid breakdown. (F) Divergence without conflict: over-training disjoint specialists from 1 to 12 epochs produces no isolation; the merged model tracks or exceeds the parents throughout.}\label{fig7}
\end{figure*}
\subsection*{A controlled predictive test: functional conflict, measured pre-merge, predicts merge damage}
The framework's prediction-level claim was put to a designed test (Fig. 3C). Thirty-nine parent pairs (13 conditions \(\times\) 3 seeds; rows are not independent --- parents share task-data seeds across conditions, so inference is condition-clustered, and because shared seeds also couple rows \emph{across} conditions I report per-seed and leave-one-seed-out sensitivity alongside) span three axes decorrelated by construction: \emph{conflict} (contradictory conventions on shared prompts, private budgets fixed), \emph{compatible overlap} (the same shared prompts under the same convention --- overlap and volume without conflict), and \emph{duration} (weight divergence with zero conflict). Before merging, six predictors are computed: \emph{confidence-weighted functional conflict} (bilateral confident disagreement on probes drawn blind to where conflict lives --- a proposed proxy for merge-relevant interactions, motivated by the observation that raw disagreement counts harmless complementation, one parent merely ignorant, as conflict), raw disagreement, gradient alignment at the shared base (47), LoRA-delta cosine and distance, and a cross-task performance baseline. The pre-registered outcome is the merge penalty against oracle parent potential (the analogue of \emph{hybrid load}, the fitness a hybrid loses relative to what its parents' genes could jointly supply), also reported against best- and mean-parent references because the predictor ordering is sensitive to that choice.
Across this controlled grid, pre-merge functional disagreement predicted merge penalties (clustered bootstrap CIs excluding zero; held-out leave-one-condition-out \(\rho\) \(\approx\) 0.35--0.40), whereas LoRA-delta cosine and L2 showed no statistically detectable association; gradient alignment carried intermediate signal. Head-to-head predictor differences are not individually significant at this sample size; only these baselines were tested; and with three seeds, uncertainty about seed generalisation remains substantial --- though the seed sensitivity favours the functional measures (per-seed \(\rho\) stable at +0.37 to +0.53 in each seed alone, geometry \(\approx\) 0 in every seed, gradient alignment seed-unstable at \(-\)0.11 to \(-\)0.55). Two further results bound the claim: the initial two-axis grid's best predictor was delta-cosine (\(\rho\) = +0.60) --- an overlap artefact that the compatible-overlap control was added to expose, and did (collapse to +0.03); and the pre-registered internal prediction that confidence weighting would beat raw disagreement \emph{failed} (they are statistically indistinguishable as rank predictors), so the present evidence favours functional disagreement generally, not the DMI-specific refinement. The framework motivated the measurement and the controls; their success does not validate the specifically population-genetic mechanism. Whether the prediction improves a budget-matched operator choice, and whether it generalises to unfamiliar conflict structures and real task pairs, are the experiment's open front.
Headline quantitative results, with sample sizes, uncertainty, and outcome definitions, are collected in SI Appendix, Table S2.
The compatible-overlap control produced a finding of its own. In an initial grid that varied only conflict and duration, the best predictor was the cosine between LoRA updates (\(\rho\) = +0.60). Parents trained on the same prompts have aligned weight changes and also merge worse, so the cosine was reading shared training data, not incompatibility: adding pairs that share prompts without conflicting collapsed its correlation to +0.03. Any merge predictor validated on a grid in which conflict and shared data vary together inherits this artefact. I know of no study that has controlled for it, and it bears on the merge-prediction literature (86--88) independently of the biology. One pre-registered prediction failed: confidence weighting did not beat raw disagreement as a rank predictor, so the evidence supports functional disagreement in general and not the incompatibility-specific refinement. Headline quantitative results, with sample sizes and uncertainty, are collected in Supplementary Information, Table S2.
\section*{Discussion}
\textbf{Design rules.} As engineering guidance, the results reduce to rules that an operator of a model population can apply, answering the four decisions posed in the Introduction. \emph{Ground every generation} in verified reality --- a few percent retained most diversity in the tested settings --- but price the rarest capabilities individually (observation probability \texttt{1 \(-\) e\textasciicircum{}{\(-\)m\(\cdot\)p}} per batch under unstratified sampling), consider targeted sampling for the deep tail, and use recombination to recover rare capabilities still retained across complementary parents. \emph{Merge, don't blend, when there is headroom}: keep specialists intact and route, or breed-and-screen candidate merges, whenever the naive average is far from ceiling; plain averaging is adequate only where a strong base has already composed the skills. \emph{Match the operator to entanglement}: merge freely when skills are additive; sparingly, with offspring selection, when they entangle; and expect the champion-optimal mating breadth to narrow as landscapes roughen. \emph{Preserve diversity as a first-class objective}, because selection can only preserve variety that exists, and in the tested society its removal produced a distinct failure mode. \emph{Before merging, measure functional conflict} --- cheap, pre-merge, and in the controlled setting predictive where the tested weight-distance baselines were not; and \emph{do not treat divergence or specialisation alone as evidence of incompatibility} --- in every regime tested here, what broke merging was conflicting conventions on shared circuitry, which is the thing to detect.
\textbf{Design rules.} \emph{Ground every generation} in verified reality. A few percent of real data kept most of the diversity here, but what protects a capability is the number of real examples of it that arrive each generation, not their share of the training set (the one-migrant-per-generation rule, 35; the few hundred documents that poison a model of any size, 62). The rarest capabilities therefore need a budget of about \texttt{1/p} real examples per generation, real data aimed at them, or a parent that still holds them. \emph{Route or screen rather than average whenever the average falls short of the best parent on any task.} On the hard families routing (sending each input to the specialist that owns it) beat weight averaging by 0.09 in every seed and screening candidate merges beat it by 0.07 (Fig. 3C), and the plain average lost nothing only where the base already answered at ceiling. \emph{Stop recombining early, by rule or by test.} A fixed early stop, or scoring the unchanged parent beside every candidate merge, avoided the collapse of obligate merging at no cost against never merging. \emph{When a partner must be found, prefer a stored ancestor to a divergent contemporary}, which shares every convention and beat a contemporary in every seed. \emph{Preserve diversity as an objective in itself}, since selection can only keep what exists. \emph{Before merging, measure functional conflict} (whether the parents answer the same prompts differently), which was cheap and predictive where weight distance was not; divergence or specialisation alone is no evidence of incompatibility, since what broke merging in every regime was conflicting conventions. The inheritance model adds one untested rule: merge sparingly, and with offspring selection, when skills are entangled (40; Fig. S13).
\textbf{Continual learning at the population scale.} Within a single network, the discipline's remedies for forgetting are this framework's operators writ small. Rehearsal and replay of stored data (26, 27) is grounded inheritance within one lineage, and the replay fractions the field settled on empirically, on the order of 1\% for instruction tuning (48) and 5\% to 25\% by distribution-shift strength in continual pretraining (49), sit where the biological model's operational threshold lies. \emph{Pseudo-rehearsal}, the replay of a network's own generated samples, proposed as a cure in 1995 (50) and revived as generative replay (51), is precisely the ungrounded null studied here: immigration from a drifting source, benign for one hop, compounding over generations, with verifier-filtering (29, 52) converting it back into grounding. Parameter isolation (53), including frozen-base adapters, which forget far less (54), is engineered decorrelation; complementary-learning-systems consolidation (55--57) is the periodic adapter-into-base merge; the recent turn to merging as a continual-learning mechanism (58--61) applies recombination within one lineage over time, where this paper applies it across lineages; and the observation that rare examples and long-tail knowledge are forgotten first (62--64) is tail extinction seen one model at a time. The mechanisms differ (forgetting is largely deterministic interference, collapse is sampling drift) but the victims and the remedies coincide, and to my knowledge no prior work carries population-genetic formalism into continual learning. Read into that field, the results offer: (i) an equilibrium theory for the replay ratio, with the sharper prediction that the required fraction is set by the rarest capability one refuses to lose (the \texttt{1 \(-\) e\textasciicircum{}{\(-\)m\(\cdot\)p}} law) rather than by average loss, testable against published replay sweeps; (ii) a \emph{failure theory for generative replay}: self-generated rehearsal is safe for short horizons and compounds into collapse across generations unless verifier-filtered back into grounding (29, 50--52); (iii) \emph{pre-merge interference prediction with a mechanism}: where the current state of the art fits regressions over candidate metrics (47), the functional-conflict measure arrives at a convergent signal from principle and comes with an operator prescription --- when conflict is high, do not average; route or breed-and-screen; (iv) a candidate \emph{decision rule for the consolidate-versus-stay-modular question} that currently splits the field's practice (keep adapters separate vs merge them; 57--61): union-preserving operators where headroom exists, fusion where the base composes, consolidation as the slow-store step; and (v) \emph{tail monitoring as the leading indicator}: continual-learning evaluation that averages over capabilities hides exactly the losses that drift theory says come first and, past a threshold, become irreversible. On that last point I note the standing objection that apparent forgetting can be skewed task-inference over latent capability rather than erasure (65); the irreversibility results here concern oracle-measured behavioural distributions, and distinguishing latent from extinct capability at language-model scale is an open experiment whose outcome would be decisive for both readings.
\textbf{Continual learning at the population scale.} Continual learning, the machine-learning field that teaches one network new things without erasing old ones, has found remedies for forgetting that are this framework's operators applied to a single lineage. Rehearsal of stored real data (28, 29) is grounding, and the replay fractions the field has settled on (about 1\% in instruction tuning, 89; 5\% to 25\% in continual pretraining, 90) look inconsistent only as fractions: at typical batch sizes each delivers tens to thousands of replayed examples of a skill per step, far more than the ten copies per generation that hold 95\% of diversity. Pseudo-rehearsal, replaying the network's own generated samples (91, 92), is grounding with no real data at all, harmless over one step and compounding over generations (Fig. 2) unless the samples are verified (33, 93). Adapters on a frozen base (94, 95) keep lineages decorrelated, consolidating them into the base is the slow store of complementary-learning-systems models (96--98), and merging as a continual-learning mechanism (72, 73, 80, 99, 100) accumulates new skills and breaks on contradictory conventions (81, 82), as the six-generation population did. That rare knowledge is forgotten first (101--103) is tail extinction observed one model at a time: forgetting and collapse differ in mechanism (interference against sampling drift) but lose the same items to the same remedies.
\textbf{What is borrowed and what is new.} The collapse-as-drift diagnosis is established prior work (21--25); so are the empirical facts that merges can beat parents, that decorrelated parents merge better, and that naive averaging loses to interference-aware or routed merges (4, 66, 67), that model populations can climb (5, 8--10), and that merge success admits ML-native predictors (47, 68), correlational where this framework supplies mechanism; the reading of sex as an algorithm for mixability in the theory of computation (69) anticipated the transfer before model merging existed. New here is the framework-level synthesis --- inheritance, diversity, and compatibility as managed quantities --- together with: the conservation law for blending inheritance and its operator boundaries; the per-item grounding floor; the society ablation with its complementary failure modes; model speciation as a named, tested question, with the coordinate-versus-functional decomposition under permutation-and-rescaling alignment and the emergent null that bounds it; and the controlled predictive test with its controls. I claim the framework generated these measurements and experiments; I do not claim that their outcomes validate a uniquely population-genetic mechanism, and one refinement it proposed was not supported.
Two results carry over directly. A pre-merge test, disagreement between the parents on shared probes, predicts interference where weight distance does not, with the control for shared training data that earlier regression (86) and distance (87, 88) studies lacked. Weight distance fails because two adapters that learned the same skill in different runs are nearly orthogonal (cosine 0.006) yet merge with no penalty: most of a weight difference is neutral, like most DNA substitutions (Supplementary Information, Text S3). Whether to consolidate specialists or keep them modular (72, 73, 98--100) follows the same rule: route while the plain average falls short of the best parent, average once it does not. Since drift removes rare items first and a lost item is recoverable only while some parent or source still holds a copy (Fig. S3), the number to watch is accuracy on the rarest items, not the mean. Apparent forgetting can also be task misrecognition rather than lost capability (104), which the oracle excludes at the small tiers only.
\textbf{Limits and open problems.} The demonstrations are deliberately small: closed-form where small is a virtue, sign-level and seed-replicated at the language-model tier, on constructed task families with a trivially separable router and one model lineage (Qwen, 0.5B--7B). The composed society has not been built at language-model scale. The predictive test's next bars, in order of value: generalisation to \emph{unfamiliar} conflict structures and real task pairs; a demonstrably better \emph{budget-matched} merging decision; then scale replication. Beyond engineering, the framework's hardest open problem is the fitness function itself: selection optimises what is measured, and for knowledge systems the persuasive and the true compete --- grounding against a reality that can refuse is the only anchor I trust, and institutionalising that anchor (verification, replication, and challenge among models) is the society-level problem this paper poses but does not solve. What biology receives in return is a new model system: populations of learners where every genotype, environment, and mating decision is observable and manipulable --- where the evolution of sex can be studied with interventions (unbounded parents, offspring preview, directed mating) that no living system permits.
\textbf{Three theories of heredity.} A model population runs on all three historical accounts of inheritance at once. A child continues training its parent's adapter, so what the parent learned in its lifetime passes on (Lamarck); weight averaging blends the parents (Jenkin); and a verifier selects among variants (Darwin). Biology discarded the first for want of a mechanism and the second because blending would swamp any new variant. Here Lamarckian transmission is what lets a lineage accumulate skills (the never-merge arm reached 0.80 without any recombination). Blending dilutes whichever parent's skill is rarest, so routing and offspring screening pay only where the plain average falls short of the best parent (Fig. 3B--C). Grounded selection is the only operator that looks outside the population, and removing it is the one ablation that fails outright: a population selected on agreement with its own consensus settles at 0.48 against 0.78 for the full society (Fig. 4D--F), confident and wrong.
\textbf{Creative diversity.} Collapse is not confined to facts and skills. Homogenisation of \emph{style} is already measurable: models trained on model output lose lexical and syntactic diversity across generations (70), writing produced with model assistance is individually better but collectively less diverse than writing produced without it (71, 72), and the house styles of the large assistants are recognisable enough that their tics serve as signatures. In this framework these are the same phenomenon at a different locus. A voice is a distribution over rare stylistic variants, exactly the tail that drift erases first and that blending inheritance averages into a common register. The remedies transfer unchanged, though they are untested here: grounding on stylistically diverse human sources, decorrelated lineages maintained as distinct voices rather than merged into one, union-preserving recombination over blending, and selection that rewards being different as well as being good. Whether these preserve measured stylistic diversity at scale is an open experiment that the framework specifies.
\textbf{Recombination's speed advantage.} In the six-generation population recombination bought speed and not level: an early lead, then parity with never merging once every skill had reached every lineage. The Fisher--Muller argument (that sex speeds adaptation by combining beneficial variants that arose in different individuals) predicts parity in exactly this case, since the curriculum guaranteed every lineage every skill, and that letting the faster lineages leave more descendants should break the parity, which it did not: selected populations reached the same ceiling, recombination's lead again gone by generation 5. The ceiling is what one adapter can carry, and sex and selection only reach it sooner. The inheritance-model society climbs under the same operators (Fig. 4D--F) because no curriculum delivers its skills; a language-model population in which some skills come only by merging would separate the two regimes. Three refinements the framework proposed were not supported: weighting disagreement by confidence did not improve the merge predictor, the declinable merge did not track complementarity as a recombination modifier (a gene that sets how often other genes are shuffled) would, and selection did not turn recombination's speed advantage into a level advantage. What population genetics supplied was the questions, the nulls and the controls, not a mechanism only it can explain.
\textbf{Outlook: the evolution of language models.} The Introduction's premise, that the model ecosystem is an evolving population, is also a forecast about where these results matter next. Language-model development is consolidating around exactly the operators studied here: synthetic-data flywheels (inheritance), merging and routing of specialist fine-tunes (recombination and population structure), verifier-gated data pipelines (grounded selection), and periodic consolidation of adapters into new bases. If coming model generations remain what the tested regimes found, freely recombinable in the absence of conflicting conventions, then the ecosystem evolves as one interbreeding population, and the levers that matter are grounding budgets priced per rare capability and diversity preserved deliberately. If instead long-horizon specialisation at scale begins to produce emergent incompatibility, as the expert-training-duration observations hint (46) and the small-scale null here does not rule out, then lineages will begin to speciate, and the ecosystem's future is a set of diverging species connected by routing rather than by merging. Which of the two it will be is measurable now, with the pre-merge conflict instruments this paper tested.
\textbf{Open problems.} The hardest is the fitness function. Selection optimises what is measured, and for knowledge the persuasive and the true compete; a reality that can refuse is the only anchor, and building it into institutions (verification, replication, challenge among models) is a problem this paper poses and does not solve. Whether speciation emerges at scale is the second: here isolation had to be provoked by conflicting conventions, and whether long specialisation supplies such conflict on its own (84, 85) needs a population diverged far longer than any here. Collapse also reaches style: models trained on model output lose lexical and syntactic diversity (105) and model-assisted writing is individually better but collectively less diverse (106, 107), because a voice is a distribution over rare variants, exactly what drift erases first and blending averages away; whether the remedies transfer is untested.
\textbf{Outlook.} Language-model development is consolidating around the operators studied here: synthetic-data flywheels (inheritance), merging and routing of specialist fine-tunes (recombination and population structure), verifier-gated pipelines (grounded selection), and periodic consolidation of adapters into new bases. The forecast is a population that recombines early and consolidates late, until conflicting conventions split it into lineages connected by routing instead of merging, and the pre-merge conflict test can measure which way it goes. Biology receives in return a model system in which every genotype, environment and mating decision is observable and manipulable, and the evolution of sex can be studied with interventions (unbounded parents, offspring preview, directed mating) no living system permits.
\section*{Materials and Methods}
\textbf{Biological-model tier.} Pure NumPy/SciPy Wright--Fisher simulator over \texttt{K}-item distributions (knowledge as \texttt{p\_t}; Zipf-tailed truth \texttt{p*}; drift--grounding--refit generations), extended with a learning kernel (smoothing/sharpening refit), multi-locus genotypes on additive and Kauffman NK landscapes, n-parent crossover, and finite-population society loops. All parameters live in per-experiment YAML configs; every run derives all randomness from one master seed (\texttt{SeedSequence.spawn}) and is bitwise reproducible; scientific-validation tests assert the closed forms (heterozygosity decay, immigration equilibrium, closed-form union) to <0.5\% and run in CI with 151 further correctness tests.
Full procedures, parameters, and replicate counts are in Supplementary Information, Methods. Appendix 1 (\emph{The figures explained}) restates every main and supplementary figure with a legend that explains the machine-learning experiment behind it for readers from biology.
\textbf{Neural tier.} Trained-network experiments realise the same abstractions with an exact oracle: histogram/RNN/MLP/VAE generators on a synthetic mode universe (the histogram model reduces the harness exactly to the biological model --- the bridge gate), and a convolutional VAE on MNIST with a frozen CNN oracle (98.5\% mode accuracy; confusion matrix recorded as the measurement floor). Speciation experiments fork no-BatchNorm MLPs (784--512--512--10) from a shared base, weight-average, and measure linear-mode-connectivity error barriers before and after alignment; alignment composes deterministic Git Re-Basin permutation matching with exact per-unit scale canonicalisation (the unit symmetry group of this class, as the alignment search space; control recovery does not establish global optimality), gated by exact recovery of a permuted-and-rescaled copy.
\textbf{Inheritance-model tier.} A NumPy/SciPy Wright--Fisher simulator over \texttt{K}-item distributions (knowledge as \texttt{p\_t}, Zipf-tailed truth \texttt{p*}, and drift--grounding--refit generations), extended with a learning kernel (a smoothing and a sharpening knob on the refit), multi-locus genotypes on additive and Kauffman NK landscapes, n-parent crossover, and finite-population loops. All parameters live in per-experiment YAML configs. Every run derives its randomness from one master seed (\texttt{SeedSequence.spawn}) and is bitwise reproducible. Scientific-validation tests assert the closed forms to within 0.5\% and run in CI alongside 151 further correctness tests.
\textbf{Language-model tier.} LoRA specialists (rank 16) on procedurally generated task families with an exact-match verifier, on frozen Qwen2.5-Instruct bases (0.5B on one 16 GB GPU; 7B on one L40S). Operators: weight-space merges (soup/TIES via adapter arithmetic), per-input routing, and Dirichlet-sampled offspring populations screened on held-out validation splits. Multi-seed protocols fix the test sets and vary the training seed. The predictive test computes all predictors pre-merge (generation confidence from token log-probabilities; base-model gradient cosines; exact r-space LoRA-delta geometry) and evaluates merges on held-out tests; robust statistics (condition-clustered bootstrap, paired predictor contrasts, leave-one-condition-out prediction, multi-reference outcomes) are produced by a committed script. Statistical, per-seed reproducibility is documented for GPU tiers.
\textbf{Neural tier.} Trained-network experiments realise the same abstractions against an exact oracle. Histogram, RNN, MLP and VAE generators run on a synthetic mode universe, where the histogram model reduces the harness exactly to the inheritance model (the bridge gate), and a convolutional VAE runs on MNIST with a frozen CNN oracle at 98.5\% mode accuracy (its confusion matrix is recorded as the measurement floor). Speciation experiments fork no-BatchNorm MLPs (784--512--512--10) from a shared base, weight-average them, and measure the error barrier along the straight line between the two weight vectors (the linear-mode-connectivity barrier) before and after alignment. Alignment composes deterministic Git Re-Basin permutation matching with exact per-unit scale canonicalisation, the unit symmetry group of this architecture class taken as the search space, and is gated by exact recovery of a permuted-and-rescaled copy. Control recovery does not establish global optimality.
\textbf{Language-model tier.} LoRA specialists (rank 16) on procedurally generated task families with an exact-match verifier, on frozen Qwen2.5-Instruct bases (0.5B on one 16 GB GPU; 7B on one L40S). Operators: weight-space merges via adapter arithmetic (the plain weight average, or soup, and TIES, which reconciles the sign of each parameter change across parents before averaging; 4), per-input routing, and Dirichlet-sampled offspring populations screened on held-out validation splits. The six-generation population uses the Qwen2.5-1.5B base model, six public datasets with per-family exact-match or execution verifiers, and rank-16 adapters continued from the parent adapter each generation (300 new and 150 replay examples, 3 epochs), merged over the weight grid {0.5/0.5, 0.3/0.7, 0.7/0.3} chosen on 20 validation items per family and reported on 60 held-out test items, with the unchanged parent as a further candidate in the declinable arm; three training seeds. Multi-seed protocols fix the test sets and vary the training seed. The predictive test computes all predictors pre-merge (generation confidence from token log-probabilities, base-model gradient cosines, and LoRA-delta geometry computed exactly in the adapters' low-rank factor space) and evaluates merges on held-out tests. Its rows are not independent, because parents share task-data seeds across conditions, so inference is condition-clustered and per-seed and leave-one-seed-out sensitivity are reported alongside; a committed script produces these statistics. Statistical, per-seed reproducibility is documented for the GPU tiers.
\textbf{Data and code availability.} All code, configs, seeds, results artifacts (with content hashes), figures, and a one-command reproduction script will be deposited openly (repository + archived DOI) on publication; every figure in this paper regenerates from committed artifacts without re-simulation.
\section*{Acknowledgements}
This work was done in close collaboration with Claude Opus 5 and Claude Fable 5.1 (Anthropic). I conceived the framework and the population-genetic reading, chose the questions and the experiments, set the pre-registered predictions and falsifiers, directed every stage, judged the results and edited the text; the models contributed to the experimental design, wrote the code and ran the experiments under my direction, performed the analyses and drafted the text. I take full responsibility for the content. I thank Imperial College London for funding.
\section*{References}
\begin{enumerate}
@ -148,6 +182,7 @@ Headline quantitative results, with sample sizes, uncertainty, and outcome defin
\item Y. Zhang, et al., Nature-inspired population-based evolution of large language models (GENOME). arXiv [Preprint] (2025). https://doi.org/10.48550/arXiv.2503.01155.
\item J. Abrantes, et al., Competition and attraction improve model fusion (M2N2). \emph{Proc. Genet. Evol. Comput. Conf.} (2025). https://doi.org/10.48550/arXiv.2508.16204.
\item V. Subramaniam, et al., Multiagent finetuning: Self-improvement with diverse reasoning chains. arXiv [Preprint] (2025). https://doi.org/10.48550/arXiv.2501.05707.
\item Y. Hu, Y. Yao, N. Zhang, H. Chen, S. Deng, Exploring model kinship for merging large language models. arXiv [Preprint] (2024). https://doi.org/10.48550/arXiv.2410.12613.
\item NVIDIA (B. Adler, et al.), Nemotron-4 340B technical report. arXiv [Preprint] (2024). https://doi.org/10.48550/arXiv.2406.11704.
\item M. Abdin, et al., Phi-4 technical report. arXiv [Preprint] (2024). https://doi.org/10.48550/arXiv.2412.08905.
\item Y. Wang, et al., Self-Instruct: Aligning language models with self-generated instructions. \emph{Proc. Annu. Meet. Assoc. Comput. Linguist.} (2023). https://doi.org/10.48550/arXiv.2212.10560.
@ -158,6 +193,7 @@ Headline quantitative results, with sample sizes, uncertainty, and outcome defin
\item J. S. Park, et al., Generative agents: Interactive simulacra of human behavior. \emph{Proc. ACM Symp. User Interface Softw. Technol.} (2023). https://doi.org/10.48550/arXiv.2304.03442.
\item T. Guo, et al., Large language model based multi-agents: A survey of progress and challenges. \emph{Proc. Int. Joint Conf. Artif. Intell.} (2024). https://doi.org/10.48550/arXiv.2402.01680.
\item N. Tomasev, et al., Virtual agent economies. arXiv [Preprint] (2025). https://doi.org/10.48550/arXiv.2509.10147.
\item A. Livnat, C. Papadimitriou, Sex as an algorithm: The theory of evolution under the lens of computation. \emph{Commun. ACM} \textbf{59}, 84--93 (2016).
\item I. Shumailov, et al., AI models collapse when trained on recursively generated data. \emph{Nature} \textbf{631}, 755--759 (2024).
\item J. P. Crutchfield, S. Whalen, Structural drift: The population dynamics of sequential learning. \emph{PLOS Comput. Biol.} \textbf{8}, e1002510 (2012).
\item S. Riis, Drift and selection in LLM text ecosystems. arXiv [Preprint] (2026). https://doi.org/10.48550/arXiv.2604.08554.
@ -166,25 +202,64 @@ Headline quantitative results, with sample sizes, uncertainty, and outcome defin
\item M. McCloskey, N. J. Cohen, Catastrophic interference in connectionist networks: The sequential learning problem. \emph{Psychol. Learn. Motiv.} \textbf{24}, 109--165 (1989).
\item R. M. French, Catastrophic forgetting in connectionist networks. \emph{Trends Cogn. Sci.} \textbf{3}, 128--135 (1999).
\item H. J. Muller, The relation of recombination to mutational advance. \emph{Mutat. Res.} \textbf{1}, 2--9 (1964).
\item S. Alemohammad, et al., Self-consuming generative models go MAD. \emph{Int. Conf. Learn. Represent.} (2024). https://doi.org/10.48550/arXiv.2307.01850.
\item Q. Bertrand, A. J. Bose, A. Duplessis, M. Jiralerspong, G. Gidel, On the stability of iterative retraining of generative models on their own data. \emph{Int. Conf. Learn. Represent.} (2024). https://doi.org/10.48550/arXiv.2310.00429.
\item B. Yi, Q. Liu, Y. Cheng, H. Xu, Escaping model collapse via synthetic data verification. arXiv [Preprint] (2025). https://doi.org/10.48550/arXiv.2510.16657.
\item M. Gerstgrasser, et al., Is model collapse inevitable? Breaking the curse of recursion by accumulating real and synthetic data. \emph{Conf. Lang. Model.} (2024). https://doi.org/10.48550/arXiv.2404.01413.
\item S. Wright, Evolution in Mendelian populations. \emph{Genetics} \textbf{16}, 97--159 (1931).
\item L. S. Mills, F. W. Allendorf, The one-migrant-per-generation rule in conservation and management. \emph{Conserv. Biol.} \textbf{10}, 1509--1518 (1996).
\item F. Jenkin, The origin of species [review]. \emph{North Br. Rev.} \textbf{46}, 277--318 (1867).
\item J. Pari, S. Jelassi, P. Agrawal, Collective model intelligence requires compatible specialization. arXiv [Preprint] (2024). https://doi.org/10.48550/arXiv.2411.02207.
\item M. Wortsman, et al., Model soups: Averaging weights of multiple fine-tuned models improves accuracy without increasing inference time. \emph{Proc. Int. Conf. Mach. Learn.} (2022). https://doi.org/10.48550/arXiv.2203.05482.
\item R. A. Fisher, \emph{The Genetical Theory of Natural Selection} (Clarendon Press, 1930).
\item H. J. Muller, Some genetic aspects of sex. \emph{Am. Nat.} \textbf{66}, 118--138 (1932).
\item E. J. Hu, et al., LoRA: Low-rank adaptation of large language models. \emph{Int. Conf. Learn. Represent.} (2022). https://doi.org/10.48550/arXiv.2106.09685.
\item S. A. Kauffman, S. Levin, Towards a general theory of adaptive walks on rugged landscapes. \emph{J. Theor. Biol.} \textbf{128}, 11--45 (1987).
\item S. P. Otto, M. W. Feldman, Deleterious mutations, variable epistatic interactions, and the evolution of recombination. \emph{Theor. Popul. Biol.} \textbf{51}, 134--147 (1997).
\item A. R. Templeton, ``Coadaptation and outbreeding depression'' in \emph{Conservation Biology: The Science of Scarcity and Diversity}, M. E. Soulé, Ed. (Sinauer, 1986), pp. 105--116.
\item J. Lehman, K. O. Stanley, Abandoning objectives: Evolution through the search for novelty alone. \emph{Evol. Comput.} \textbf{19}, 189--223 (2011).
\item M. Tomassini, \emph{Spatially Structured Evolutionary Algorithms: Artificial Evolution in Space and Time} (Springer, 2005).
\item H. A. Orr, The population genetics of speciation: The evolution of hybrid incompatibilities. \emph{Genetics} \textbf{139}, 1805--1813 (1995).
\item H. A. Orr, M. Turelli, The evolution of postzygotic isolation: Accumulating Dobzhansky--Muller incompatibilities. \emph{Evolution} \textbf{55}, 1085--1094 (2001).
\item S. K. Ainsworth, J. Hayase, S. Srinivasa, Git Re-Basin: Merging models modulo permutation symmetries. \emph{Int. Conf. Learn. Represent.} (2023). https://doi.org/10.48550/arXiv.2209.04836.
\item T. Li, Z. Shen, Scaling linear mode connectivity and merging to billion-parameter pretrained transformers. arXiv [Preprint] (2026). https://doi.org/10.48550/arXiv.2606.23607.
\item E. Sharma, D. M. Roy, G. K. Dziugaite, The non-local model merging problem: Permutation symmetries and variance collapse. arXiv [Preprint] (2024). https://doi.org/10.48550/arXiv.2410.12766.
\item K. Jordan, H. Sedghi, O. Saukh, R. Entezari, B. Neyshabur, REPAIR: REnormalizing permuted activations for interpolation repair. \emph{Int. Conf. Learn. Represent.} (2023). https://doi.org/10.48550/arXiv.2211.08403.
\item G. Stoica, et al., ZipIt! Merging models from different tasks without training. \emph{Int. Conf. Learn. Represent.} (2024). https://doi.org/10.48550/arXiv.2305.03053.
\item A. Kleiman, G. K. Dziugaite, J. Frankle, S. Kakade, M. Paul, Soup to go: Mitigating forgetting during continual learning with model averaging. arXiv [Preprint] (2025). https://doi.org/10.48550/arXiv.2501.05559.
\item X. Yuan, et al., Superficial self-improved reasoners benefit from model merging. arXiv [Preprint] (2025). https://doi.org/10.48550/arXiv.2503.02103.
\item N. H. Barton, A general model for the evolution of recombination. \emph{Genet. Res.} \textbf{65}, 123--144 (1995).
\item S. P. Otto, T. Lenormand, Resolving the paradox of sex and recombination. \emph{Nat. Rev. Genet.} \textbf{3}, 252--261 (2002).
\item L. Altenberg, M. W. Feldman, Selection, generalized transmission and the evolution of modifier genes. I. The reduction principle. \emph{Genetics} \textbf{117}, 559--572 (1987).
\item T. Fukuda, H. Kera, K. Kawamoto, Adapter merging with centroid prototype mapping for scalable class-incremental learning. \emph{Proc. IEEE/CVF Conf. Comput. Vis. Pattern Recognit.} (2025). https://doi.org/10.48550/arXiv.2412.18219.
\item D. Shenaj, O. Bohdal, T. Ceritli, M. Ozay, P. Zanuttigh, U. Michieli, K-Merge: Online continual merging of adapters for on-device large language models. arXiv [Preprint] (2025). https://doi.org/10.48550/arXiv.2510.13537.
\item J. Lehman, K. O. Stanley, Abandoning objectives: Evolution through the search for novelty alone. \emph{Evol. Comput.} \textbf{19}, 189--223 (2011).
\item S. Wright, Evolution in Mendelian populations. \emph{Genetics} \textbf{16}, 97--159 (1931).
\item E. Dohmatob, Y. Feng, P. Yang, F. Charton, J. Kempe, A tale of tails: Model collapse as a change of scaling laws. \emph{Proc. Int. Conf. Mach. Learn.} (2024). https://doi.org/10.48550/arXiv.2402.07043.
\item E. Dohmatob, Y. Feng, A. Subramonian, J. Kempe, Strong model collapse. arXiv [Preprint] (2024). https://doi.org/10.48550/arXiv.2410.04840.
\item A. Garg, S. Bhattacharya, P. Sur, Preventing model collapse under overparametrization: Optimal mixing ratios for interpolation learning and ridge regression. arXiv [Preprint] (2025). https://doi.org/10.48550/arXiv.2509.22341.
\item A. T. Suresh, A. Thangaraj, A. N. K. Khandavally, Rate of model collapse in recursive training. arXiv [Preprint] (2024). https://doi.org/10.48550/arXiv.2412.17646.
\item J. Kazdan, et al., Collapse or thrive? Perils and promises of synthetic data in a self-generating world. arXiv [Preprint] (2024). https://doi.org/10.48550/arXiv.2410.16713.
\item A. Souly, et al., Poisoning attacks on LLMs require a near-constant number of poison samples. arXiv [Preprint] (2025). https://doi.org/10.48550/arXiv.2510.07192.
\item F. Jenkin, The origin of species [review]. \emph{North Br. Rev.} \textbf{46}, 277--318 (1867).
\item M. Bulmer, Did Jenkin's swamping argument invalidate Darwin's theory of natural selection? \emph{Br. J. Hist. Sci.} \textbf{37}, 281--297 (2004).
\item A. Malinin, B. Mlodozeniec, M. Gales, Ensemble distribution distillation. \emph{Int. Conf. Learn. Represent.} (2020). https://doi.org/10.48550/arXiv.1905.00076.
\item M. Li, et al., Branch-Train-Merge: Embarrassingly parallel training of expert language models. arXiv [Preprint] (2022). https://doi.org/10.48550/arXiv.2208.03306.
\item X. Yuan, et al., Behavior knowledge merge in reinforced agentic models. arXiv [Preprint] (2026). https://doi.org/10.48550/arXiv.2601.13572.
\item J. Pari, S. Jelassi, P. Agrawal, Collective model intelligence requires compatible specialization. arXiv [Preprint] (2024). https://doi.org/10.48550/arXiv.2411.02207.
\item E. J. Hu, et al., LoRA: Low-rank adaptation of large language models. \emph{Int. Conf. Learn. Represent.} (2022). https://doi.org/10.48550/arXiv.2106.09685.
\item L. Yu, B. Yu, H. Yu, F. Huang, Y. Li, Language models are super Mario: Absorbing abilities from homologous models as a free lunch. \emph{Proc. Int. Conf. Mach. Learn.} (2024). https://doi.org/10.48550/arXiv.2311.03099.
\item S. A. Kauffman, S. Levin, Towards a general theory of adaptive walks on rugged landscapes. \emph{J. Theor. Biol.} \textbf{128}, 11--45 (1987).
\item D. Marczak, B. Twardowski, T. Trzciński, S. Cygert, MagMax: Leveraging model merging for seamless continual learning. \emph{Proc. Eur. Conf. Comput. Vis.} (2024). https://doi.org/10.48550/arXiv.2407.06322.
\item S. Dziadzio, et al., How to merge your multimodal models over time? \emph{Proc. IEEE/CVF Conf. Comput. Vis. Pattern Recognit.} (2025). https://doi.org/10.48550/arXiv.2412.06712.
\item A. Williams, N. Nangia, S. R. Bowman, A broad-coverage challenge corpus for sentence understanding through inference. \emph{Proc. Conf. North Am. Chapter Assoc. Comput. Linguist. Hum. Lang. Technol.}, 1112--1122 (2018).
\item P. Clark, et al., Think you have solved question answering? Try ARC, the AI2 Reasoning Challenge. arXiv [Preprint] (2018). https://doi.org/10.48550/arXiv.1803.05457.
\item R. Zellers, A. Holtzman, Y. Bisk, A. Farhadi, Y. Choi, HellaSwag: Can a machine really finish your sentence? \emph{Proc. Annu. Meet. Assoc. Comput. Linguist.}, 4791--4800 (2019).
\item P. Rajpurkar, J. Zhang, K. Lopyrev, P. Liang, SQuAD: 100,000+ questions for machine comprehension of text. \emph{Proc. Conf. Empir. Methods Nat. Lang. Process.}, 2383--2392 (2016).
\item C. Clark, et al., BoolQ: Exploring the surprising difficulty of natural yes/no questions. \emph{Proc. Conf. North Am. Chapter Assoc. Comput. Linguist. Hum. Lang. Technol.}, 2924--2936 (2019).
\item K. Sakaguchi, R. Le Bras, C. Bhagavatula, Y. Choi, WinoGrande: An adversarial Winograd schema challenge at scale. \emph{Proc. AAAI Conf. Artif. Intell.} \textbf{34}, 8732--8740 (2020).
\item L. Thede, K. Roth, M. Bethge, Z. Akata, T. Hartvigsen, WikiBigEdit: Understanding the limits of lifelong knowledge editing in LLMs. \emph{Proc. Int. Conf. Mach. Learn.} (2025). https://doi.org/10.48550/arXiv.2503.05683.
\item S. Clemente, et al., In praise of stubbornness: An empirical case for cognitive-dissonance aware continual update of knowledge in LLMs. arXiv [Preprint] (2025). https://doi.org/10.48550/arXiv.2502.04390.
\item J. Störk, Interference and retention in continual learning. arXiv [Preprint] (2026). https://doi.org/10.48550/arXiv.2607.09202.
\item T. Li, Z. Shen, Scaling linear mode connectivity and merging to billion-parameter pretrained transformers. arXiv [Preprint] (2026). https://doi.org/10.48550/arXiv.2606.23607.
\item N. Kozodoi, Z. Afolabi, J. Butler, Are we merging the right models? Impact of expert training duration on model merging for LLMs. arXiv [Preprint] (2026). https://doi.org/10.48550/arXiv.2607.11997.
\item S. Horoi, G. Wolf, E. Belilovsky, G. K. Dziugaite, From memorization to parameter interference: How overtraining experts harms model merging. \emph{Proc. Int. Conf. Mach. Learn.} (2026). https://doi.org/10.48550/arXiv.2506.14126.
\item L. Zhou, B. Zhao, R. Yu, E. Rodolà, Demystifying mergeability: Interpretable properties to predict model merging success. arXiv [Preprint] (2026). https://doi.org/10.48550/arXiv.2601.22285.
\item Y. Cao, et al., An empirical study and theoretical explanation on task-level model-merging collapse. arXiv [Preprint] (2026). https://doi.org/10.48550/arXiv.2603.09463.
\item C. Zhu, X. Li, T. Cai, When do task vectors interfere? Mapping the validity boundaries of weight-space composition. arXiv [Preprint] (2026). https://doi.org/10.48550/arXiv.2608.09490.
\item T. Scialom, T. Chakrabarty, S. Muresan, Fine-tuned language models are continual learners. \emph{Proc. Conf. Empir. Methods Nat. Lang. Process.} (2022). https://doi.org/10.48550/arXiv.2205.12393.
\item A. Ibrahim, et al., Simple and scalable strategies to continually pre-train large language models. \emph{Trans. Mach. Learn. Res.} (2024). https://doi.org/10.48550/arXiv.2403.08763.
\item A. Robins, Catastrophic forgetting, rehearsal and pseudorehearsal. \emph{Connect. Sci.} \textbf{7}, 123--146 (1995).
@ -196,17 +271,11 @@ Headline quantitative results, with sample sizes, uncertainty, and outcome defin
\item D. Kumaran, D. Hassabis, J. L. McClelland, What learning systems do intelligent agents need? Complementary learning systems theory updated. \emph{Trends Cogn. Sci.} \textbf{20}, 512--534 (2016).
\item J. Schwarz, et al., Progress \& Compress: A scalable framework for continual learning. \emph{Proc. Int. Conf. Mach. Learn.} (2018).
\item G. Ilharco, et al., Editing models with task arithmetic. \emph{Int. Conf. Learn. Represent.} (2023). https://doi.org/10.48550/arXiv.2212.04089.
\item D. Marczak, B. Twardowski, T. Trzciński, S. Cygert, MagMax: Leveraging model merging for seamless continual learning. \emph{Proc. Eur. Conf. Comput. Vis.} (2024). https://doi.org/10.48550/arXiv.2407.06322.
\item A. Alexandrov, et al., Mitigating catastrophic forgetting in language transfer via model merging. \emph{Findings Assoc. Comput. Linguist.: EMNLP} (2024). https://doi.org/10.48550/arXiv.2407.08699.
\item S. Dziadzio, et al., How to merge your multimodal models over time? \emph{Proc. IEEE/CVF Conf. Comput. Vis. Pattern Recognit.} (2025). https://doi.org/10.48550/arXiv.2412.06712.
\item M. Toneva, et al., An empirical study of example forgetting during deep neural network learning. \emph{Int. Conf. Learn. Represent.} (2019). https://doi.org/10.48550/arXiv.1812.05159.
\item N. Kandpal, H. Deng, A. Roberts, E. Wallace, C. Raffel, Large language models struggle to learn long-tail knowledge. \emph{Proc. Int. Conf. Mach. Learn.} (2023). https://doi.org/10.48550/arXiv.2211.08411.
\item X. Liu, et al., Long-tailed class incremental learning. \emph{Proc. Eur. Conf. Comput. Vis.} (2022). https://doi.org/10.48550/arXiv.2210.00266.
\item S. Kotha, J. M. Springer, A. Raghunathan, Understanding catastrophic forgetting in language models via implicit inference. \emph{Int. Conf. Learn. Represent.} (2024). https://doi.org/10.48550/arXiv.2309.10105.
\item L. Yu, B. Yu, H. Yu, F. Huang, Y. Li, Language models are super Mario: Absorbing abilities from homologous models as a free lunch. \emph{Proc. Int. Conf. Mach. Learn.} (2024). https://doi.org/10.48550/arXiv.2311.03099.
\item M. Wortsman, et al., Model soups: Averaging weights of multiple fine-tuned models improves accuracy without increasing inference time. \emph{Proc. Int. Conf. Mach. Learn.} (2022). https://doi.org/10.48550/arXiv.2203.05482.
\item Y. Cao, et al., An empirical study and theoretical explanation on task-level model-merging collapse. arXiv [Preprint] (2026). https://doi.org/10.48550/arXiv.2603.09463.
\item A. Livnat, C. Papadimitriou, Sex as an algorithm: The theory of evolution under the lens of computation. \emph{Commun. ACM} \textbf{59}, 84--93 (2016).
\item Y. Guo, G. Shang, M. Vazirgiannis, C. Clavel, The curious decline of linguistic diversity: Training language models on synthetic text. \emph{Findings Assoc. Comput. Linguist.: NAACL} (2024). https://doi.org/10.48550/arXiv.2311.09807.
\item V. Padmakumar, H. He, Does writing with language models reduce content diversity? \emph{Int. Conf. Learn. Represent.} (2024). https://doi.org/10.48550/arXiv.2309.05196.
\item A. R. Doshi, O. P. Hauser, Generative AI enhances individual creativity but reduces the collective diversity of novel content. \emph{Sci. Adv.} \textbf{10}, eadn5290 (2024).

View file

@ -1,4 +1,4 @@
r"""Build the PNAS-draft PDF from main.md (Markdown stays the source of truth).
r"""Build the manuscript-draft PDF from main.md (Markdown stays the source of truth).
Adapted from paper/arxiv/md2tex.py (same Markdown subset + pipe tables), with one addition: standalone
`*(FIG:name)*` markers place the publication figures produced by make_figs.py (unified, lettered,
@ -18,92 +18,115 @@ ROOT = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
SRC = HERE / "main.md"
OUT = HERE / "body.tex"
SI_FIG_DIR = HERE / "figs" / "si"
import sys # noqa: E402
sys.path.insert(0, str(HERE))
from si_figures import SI_FIGURES # noqa: E402 (supplementary figures, numbered by order in si.md)
# figure name -> (single publication PDF from make_figs.py, caption)
FIGURES: dict[str, tuple[list[str], str]] = {
"fig1": (["paper/pnas/figs/fig1a.pdf", "paper/pnas/figs/fig1b.pdf"],
"(A) The experimental programme. Each population-genetic abstraction (Table 1) is tested at up "
"to three tiers, ordered left to right by increasing realism: a biological model (a "
"Wright--Fisher simulator over knowledge distributions; closed forms, bitwise-reproducible), "
"trained neural networks measured against exact oracles (recurrent, feedforward, and "
"variational-autoencoder generators on a synthetic mode universe, and a convolutional VAE on "
"MNIST), and language models (LoRA specialists on Qwen bases at 0.5B and 7B, scored by an "
"exact-match verifier). Colour separates the two categories: the biological model in green, "
"the two AI-model tiers in blues. The same abstractions are carried across "
"all three. Rows are the framework's mechanisms, each defined at the left margin; filled "
"cells name the experiments run at each tier, and each carries, in its corner, the figure "
"or table where that result is reported, so this figure doubles as a map of the paper. Each claim is tested at the cheapest tier that "
"can falsify it, and a costlier tier is entered only where it adds a discriminating test "
"rather than a replication: grounding at language-model scale is established in prior work "
"(21, 30) and is not re-run; epistasis and the society skip the middle tier, whose "
"distinctive value (exact oracles) does not bear on those operator-level questions; and the "
"society at language-model scale is the integrative experiment this paper specifies but does "
"not run --- its stated gap. (B) The conceptual basis of the transfer. A population of models "
"is usually pictured as a society in space: contemporaries exchanging messages. The couplings "
"this paper studies run instead between generations --- training on model output "
"(inheritance), weight-space merging (recombination), verified real data entering each "
"generation (immigration from reality) --- a society in time, the object population genetics "
"was built to describe. Dots mark capabilities: the rare one (gold) is lost under "
"single-parent inheritance, reassembled by merging complementary parents, and re-supplied by "
"grounding."),
"A map of the study. (A) Each row is a biological mechanism the paper borrows, each column a level "
"of realism at which it is tested: an inheritance model (an exact simulation of knowledge "
"transmission, green), trained neural networks measured against exact oracles (blue), and language "
"models (blue). Filled cells name the experiments run at each level and, in the corner, the figure "
"or table reporting them; grey cells were not run, either because the result is "
"established in prior work (23, 34) or because that level adds no new test for that question. The "
"inheritance model is the reference column: it sets the expectation the real-model experiments are "
"read against. (B) The change of viewpoint the transfer rests on. A group of models is usually "
"pictured as a society in space, contemporaries exchanging messages. The couplings studied here run "
"between generations: training on model output (inheritance), weight-space merging (recombination), "
"and verified real data entering each generation (immigration from reality). That is a society in "
"time, which is what population genetics describes. Dots are capabilities: the rare one (gold) is "
"lost under single-parent inheritance, reassembled by merging complementary parents, and re-supplied "
"by grounding."),
"fig2": (["paper/pnas/figs/fig2.pdf"],
"Grounding is immigration. (A) Stationary diversity against the grounding fraction in the "
"biological model: simulation (points, 95\\% CI) matches the exact immigration--drift "
"equilibrium (dashed). The equilibrium is smooth in $g$; $g \\approx 0.05$ marks the "
"operational threshold retaining 95\\% of source diversity in this setting (red line, "
"bootstrap CI shaded); the hollow point at $g=0$ is a finite-time value (the true equilibrium "
"is zero). (B) The same signs on real images: samples from a convolutional VAE retrained each "
"generation on its own output (rows: generations 0--15 of an ungrounded lineage) collapse "
"toward a single blurred mode; 10\\% grounding holds all thirty modes (quantified in SI)."),
"How much real data stops model collapse. (A) An image-generating network (a variational "
"autoencoder) is trained on handwritten digits, then a fresh copy is trained only on the digits the "
"previous one drew, for fifteen generations, with no real data added. Each row is a later generation "
"(0, 4, 8, 12, 15) and each column a randomly chosen drawing. The thirty kinds of digit (ten digits "
"$\\times$ three stroke thicknesses, some kinds rare) collapse to one blurred shape; an independent "
"classifier confirms that the number of kinds still drawn falls from 30 to 1, while adding 10\\% real "
"digits each generation keeps all 30 (Fig.~S7; 4 replicates). (B) The same question in the "
"inheritance model, the exact simulation: 1,000 knowledge items, 200 samples drawn per generation, "
"and a fraction $g$ of fresh real samples mixed in. Points are the diversity the population settles "
"at after 500 generations (mean and 95\\% CI over 100 lineages), the dashed line the exact prediction "
"(the immigration--drift equilibrium), the dotted line the diversity of the real data itself. The "
"curve is smooth, so any threshold is a choice: the red line marks the $g$ at which 95\\% of the real "
"data's diversity is kept, about 0.05 (bootstrap CI shaded). The hollow point at $g = 0$ has not yet "
"reached its equilibrium of zero. The trained image model needed about twice this fraction, because "
"a trained network is not the exact copier the simulation assumes (Fig.~S2)."),
"fig4": (["paper/pnas/figs/fig4.pdf"],
"Recombination in the biological model: blending inheritance and the Fisher--Muller effect. "
"(A) Expected rare-capability survival in a child refit from $K$ uncorrelated parents: the "
"output-mean (blending) stays at the single-parent level --- the first-order cancellation --- "
"while the union operator (strongest source per item, renormalised, oracle-identified) rises "
"with parent count. (B) Multi-locus recombination of decorrelated specialists produces "
"offspring fitter than any parent, approaching the optimum as parents are added; the best "
"single parent and the blended average plateau below (mean $\\pm$ 95\\% CI)."),
"A population of language models over six generations. (A) The set-up. Three lineages start "
"from one frozen 1.5-billion-parameter base (Qwen2.5-1.5B). Each generation, every lineage learns "
"one new skill from a public dataset by continuing to train its parent's adapter (300 new examples "
"plus 150 replayed from earlier skills), may merge with a partner according to its arm's rule "
"(weights averaged at a ratio chosen on validation data), and is tested on all six skills by a "
"verifier; the child becomes the next parent. The six skills are taken in rotated order, so a "
"partner knows things a lineage lacks early on (complementarity 1.0) and nothing it lacks by the "
"end (0.0). Three training seeds. (B) Accuracy over all six skills of the best lineage (mean and "
"95\\% CI). Never merging and merging only when it beats keeping the parent finish level (0.80 and "
"0.79); merging with a contemporary every generation collapses to 0.27, beginning when partners "
"stop being complementary; a control that merges through generation 2 and then stops (dashed) "
"matches the declinable arm in every seed, and a single model taught the whole syllabus alone "
"(diamond) matches the population. (C) How often the declinable lineages refused a merge (bars) "
"against partner complementarity (lines), under the rotated syllabus and under a second syllabus in "
"which complementarity is zero at the start, peaks mid-way and returns to zero. Refusals rise with "
"generation under both; with generation held fixed they do not track complementarity (partial "
"Spearman $\\rho = -0.07$, 95\\% CI $-0.21$ to $0.09$, $n = 36$). (D--F) The simulation that "
"motivated the design: 60 agents evolving on a rugged fitness landscape with all four mechanisms "
"(grounded evaluation, recombination, diversity preservation, mutation) and one removed per arm (12 "
"replicates; mean and 95\\% CI). Removing grounded evaluation, so that agents are scored on "
"agreement with the crowd instead of on the truth, collapses the population onto a confident but "
"wrong consensus (D, F); removing recombination or diversity preservation strands it below the "
"optimum (D) and drains diversity fastest (E). Each removal fails in its own way."),
"fig5": (["paper/pnas/figs/fig5.pdf"],
"Rugged (epistatic) landscapes: risk, remedy, and population structure. (A) Outbreeding "
"depression: the mean offspring of blindly recombined specialist parents falls below the best "
"parent, more steeply the more rugged the landscape (NK ruggedness $K$) and the higher the "
"recombination rate. (B) Screening candidate offspring against a verifier (directed "
"recombination) restores the gain at every ruggedness where blind recombination fails. "
"(C) Mating structure: the best champion arises at wide mate-pool breadth on smooth landscapes "
"and at intermediate breadth on rugged ones. (D) Wide breadth monotonically erodes population "
"diversity at every ruggedness (mean $\\pm$ 95\\% CI, 20 replicates)."),
"fig6": (["paper/pnas/figs/fig6.pdf"],
"The tested society: grounded evaluation, recombination, and diversity preservation make "
"complementary contributions. A finite agent population on a rugged NK landscape; selection "
"weights true fitness against conformity to the population consensus. (A) Best real fitness: "
"the full system approaches the global optimum; removing grounded evaluation collapses the "
"population onto a confident, unfit consensus; removing recombination or diversity "
"preservation strands it lower. (B) Population diversity. (C) The self-consumption signature: "
"conformity minus true fitness (mean $\\pm$ 95\\% CI, 12 replicates)."),
"fig7": (["paper/pnas/figs/fig7.pdf"],
"Model speciation at three tiers. (A) Biological model: hybrid fitness tracks the parents while "
"lineages are compatible, then falls to inviability; the denser the incompatibilities, the "
"earlier the fall. (B) The isolation cliff: probability of hybrid inviability against "
"divergence, by incompatibility density. (C) Trained networks: the merge error barrier between "
"two MLPs before and after permutation-and-rescaling alignment --- the same-task/different-"
"start barrier is a coordinate artefact (removed by alignment); the conflicting-task barrier "
"is left essentially unchanged. (D) Sweeping the fraction of conflicting classes: the residual "
"barrier rises while merged-model accuracy falls from 0.97 to 0.03. (E) Language models (0.5B "
"LoRA children of a shared base): on shared ambiguous prompts each parent performs under its "
"own convention while the merged model falls below both --- function-specific hybrid "
"breakdown. (F) Divergence without conflict: over-training disjoint specialists from 1 to 12 "
"epochs produces no isolation; the merged model tracks or exceeds the parents throughout."),
"Model speciation: when two lineages can no longer merge. (A, B) Small image classifiers "
"(multilayer perceptrons) forked from one trained base. Two networks that compute the same function "
"can still differ in their weights, because hidden units can be renumbered and rescaled without "
"changing the output; alignment undoes this before averaging. The merge error barrier is how much "
"worse the average of two networks is than the networks themselves. (A) Two copies trained from "
"different random starts on the same task have a barrier that alignment removes almost entirely "
"(0.04 to 0.001); two trained on conflicting labels (the same images, some classes relabelled) keep "
"theirs (0.50), and their average is useless (3 replicates). (B) Sweeping the share of classes in "
"conflict moves the merged model's accuracy from 0.97 to 0.03. (C) Language models: two specialists "
"share a set of ambiguous questions (``sort this list'', direction unstated) and are taught opposite "
"conventions. As the share of conflicting training grows, each parent stays good under its own "
"convention while the merged model falls below both, in all three seeds (95\\% CI shaded). (D) The "
"control: specialists trained longer and longer on different tasks, with no conflict, merge better, "
"not worse, in every seed. (E, F) The simulation: 20-position genotypes carrying incompatibilities "
"of the Bateson--Dobzhansky--Muller kind. Hybrid fitness tracks the parents while lineages are "
"compatible, then crashes, sooner the denser the incompatibilities (E), and the probability of a "
"non-viable hybrid rises with divergence (F). What breaks merging is conflicting conventions on "
"shared machinery, not distance or specialisation as such."),
"fig3": (["paper/pnas/figs/fig3.pdf"],
"The language-model tier. (A) Seed-replicated merging (0.5B, five seeds, fixed test sets; mean "
"$\\pm$ 95\\% CI): merged specialists exceed the best single specialist overall, and only "
"merged models are competent on every task family. (B) Hard, unsaturated tasks at 7B (single "
"run): the weight-average dilutes a fragile specialist below the best single parent; routing "
"among intact specialists preserves it. (C) The controlled predictive test (13 conditions "
"$\\times$ 3 seeds): pre-merge confidence-weighted functional conflict against merge penalty, "
"coloured by grid axis --- penalty concentrates on the conflict axis. (D) Predictor "
"comparison, $|$Spearman $\\rho|$ against merge penalty over the full grid: functional "
"measures carry signal, the tested weight-geometry baselines do not; paired differences "
"between predictors are not individually significant."),
"Merging language-model specialists: when it helps, and predicting when it will hurt. All models "
"are built from one frozen base (Qwen2.5) plus a LoRA adapter, a small set of extra weights trained "
"on one family of tasks (list puzzles, string puzzles or arithmetic); a verifier marks every answer "
"right or wrong, and accuracy is the share marked right on held-out questions. (A) The models "
"compared: the base alone; three specialists (one adapter each); their merge by averaging the "
"adapters; their merge after dropping the changes on which the parents conflict (TIES); and routing, "
"which keeps the specialists separate and sends each question to the one that owns it. (B) Easy "
"tasks, 0.5-billion-parameter base, five training seeds (fixed test sets; mean and 95\\% CI). Both "
"merges beat the best single specialist on the weakest task family (paired t-test over seeds, "
"$p < 10^{-4}$), and the interference-aware merge beats it overall ($p = 0.006$; the plain average "
"$p = 0.09$, ahead in 4 of 5 seeds); the two merges do not differ from each other. Only merged models "
"are competent on every family. (C) Deliberately hard tasks, 7-billion-parameter base, three seeds. "
"Averaging only matches the best specialist overall ($p = 0.96$) although it lifts the weakest "
"family ($p = 0.009$); routing beats averaging overall ($p = 0.007$, ahead in every seed) and beats "
"the best specialist on both measures ($p = 0.018$ and $0.014$). With three seeds, some comparisons "
"that hold in every seed are not significant (ns). (D) Predicting merge damage before merging: 39 "
"pairs of specialists built along three axes, parents taught contradictory conventions (red), "
"parents merely trained longer on different tasks (blue), and parents sharing training data without "
"conflict (green). The horizontal axis is how often the two parents confidently disagree when asked "
"the same questions before merging; the vertical axis is the merge penalty, the accuracy the merged "
"model loses relative to answering each task with the parent that owns it. Damage concentrates in "
"the conflicting pairs. (E) Six pre-merge measures ranked by how strongly they track the penalty "
"(absolute Spearman correlation): measures taken by asking the parents questions carry the signal, "
"measures taken on the parents' weights do not; differences between individual predictors are not "
"significant at this sample size (Table~S2)."),
}
UNICODE = {
@ -149,6 +172,17 @@ def inline(s: str) -> str:
def figure_env(name: str) -> str:
if name in SI_FIGURES:
# Supplementary figure: copy the committed per-experiment PDF beside the SI build, so the SI
# stays a pure function of the results artifacts; numbered by the LaTeX counter (S-prefixed).
src, caption = SI_FIGURES[name]
SI_FIG_DIR.mkdir(parents=True, exist_ok=True)
dst = SI_FIG_DIR / f"{name}_{(ROOT / src).name}"
shutil.copyfile(ROOT / src, dst)
return "\n".join([f"\\begin{{figure}}[p]\\centering % {name}",
f"\\includegraphics[width=\\textwidth]{{figs/si/{dst.name}}}",
f"\\caption{{{caption}}}\\label{{{name}}}",
"\\end{figure}"])
pdfs, caption = FIGURES[name]
incl = "\\\\[6pt]\n".join(f"\\includegraphics[width=\\textwidth]{{figs/{(ROOT / p).name}}}"
for p in pdfs)

View file

@ -0,0 +1,61 @@
"""Build the student-level figure guide (figure_legends_for_students.md -> .tex -> PDF).
The Markdown is the source of truth. Lines of the form ``![](path.pdf)`` become figure includes
(paths relative to paper/pnas/); ``## `` headings become unnumbered sections; everything else goes
through build.py's inline() converter, so the same Markdown subset and unicode handling apply.
Usage: python paper/pnas/build_lay_legends.py && (cd paper/pnas && tectonic figure_legends_for_students.tex)
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
from build import inline # noqa: E402
SRC = HERE / "figure_legends_for_students.md"
OUT = HERE / "figure_legends_for_students.tex"
PREAMBLE = r"""\ifdefined\XeTeXversion\else\ifdefined\pdfoutput\pdfoutput=1\fi\fi
\documentclass[11pt]{article}
\usepackage[a4paper, margin=1.0in]{geometry}
\usepackage{graphicx}
\usepackage{amsmath, amssymb}
\usepackage[hidelinks]{hyperref}
\usepackage{microtype}
\setlength{\parskip}{0.5em}
\setlength{\parindent}{0pt}
\begin{document}
"""
def convert(text: str) -> str:
out: list[str] = []
blocks = [b for b in re.split(r"\n\s*\n", text) if b.strip()]
for block in blocks:
first = block.strip()
if first.startswith("# ") and not first.startswith("## "):
out.append(f"\\begin{{center}}{{\\LARGE\\bfseries {inline(first[2:])}}}\\end{{center}}")
elif first.startswith("## "):
out.append(f"\\section*{{{inline(first[3:])}}}")
elif first == "---":
out.append("\\medskip\\hrule\\medskip")
elif re.match(r"^!\[\]\((.+)\)$", first):
path = re.match(r"^!\[\]\((.+)\)$", first).group(1)
assert (HERE / path).exists(), f"missing figure {path}"
out.append(f"\\begin{{center}}\\includegraphics[width=\\textwidth]{{{path}}}\\end{{center}}")
elif first.startswith("- "):
items = [re.sub(r"^- ", "", l.strip()) for l in block.split("\n") if l.strip().startswith("- ")]
out.append("\\begin{itemize}\n" + "\n".join(f"\\item {inline(i)}" for i in items) + "\n\\end{itemize}")
else:
out.append(inline(re.sub(r"\s+", " ", block.strip())))
return PREAMBLE + "\n\n".join(out) + "\n\\end{document}\n"
if __name__ == "__main__":
OUT.write_text(convert(SRC.read_text()))
print(f"wrote {OUT}")

View file

@ -0,0 +1,291 @@
"""Build a Zotero-importable library from the manuscript's reference list.
For each of the numbered references in paper/pnas/main.md: take the DOI printed in the entry when
there is one, otherwise ask Crossref for it by title (accepting only a high-scoring match whose title
really is the same, checked by normalised comparison). Then fetch authoritative metadata for every
resolved DOI by content negotiation against doi.org, which serves Crossref and DataCite alike, and
write the result as CSL-JSON plus RIS.
Entries whose DOI cannot be resolved (pre-DOI literature, books, chapters) are reported and written
from the manuscript's own metadata so nothing is silently dropped.
Usage: python paper/pnas/build_zotero_library.py
"""
from __future__ import annotations
import json
import re
import sys
import time
import urllib.parse
import urllib.request
from pathlib import Path
MAIN = Path(__file__).resolve().parent / "main.md"
OUT = Path(__file__).resolve().parent / "refs"
MAILTO = "g.gilestro@imperial.ac.uk" # Crossref polite pool
UA = f"LamarckianAI-refs/1.0 (mailto:{MAILTO})"
# Reference numbers whose sources predate DOIs or are books/chapters: never send these to Crossref
# title search, because it returns confident nonsense for them.
NO_DOI_EXPECTED = {33, 35, 39} # Jenkin 1867; Fisher 1930 (book); Templeton 1986 (chapter)
# DOIs the title search could not find and that were verified by hand against the publisher record.
DOI_OVERRIDE = {
17: "10.1038/s41562-023-01742-2", # Brinkmann et al., Machine culture (Nat. Hum. Behav.)
64: "10.48550/arXiv.1805.06370", # Schwarz et al., Progress & Compress (no Crossref DOI)
}
# The three genuinely pre-DOI sources, written out rather than parsed, so the Zotero records are
# complete instead of merely non-empty.
HAND_WRITTEN = {
33: {"type": "article-journal", "title": "[Review of] The Origin of Species",
"author": [{"given": "Fleeming", "family": "Jenkin"}],
"container-title": "The North British Review", "volume": "46", "page": "277-318",
"issued": {"date-parts": [[1867]]}},
35: {"type": "book", "title": "The Genetical Theory of Natural Selection",
"author": [{"given": "Ronald A.", "family": "Fisher"}],
"publisher": "Clarendon Press", "publisher-place": "Oxford",
"issued": {"date-parts": [[1930]]}},
39: {"type": "chapter", "title": "Coadaptation and outbreeding depression",
"author": [{"given": "Alan R.", "family": "Templeton"}],
"editor": [{"given": "Michael E.", "family": "Soulé"}],
"container-title": "Conservation Biology: The Science of Scarcity and Diversity",
"publisher": "Sinauer Associates", "publisher-place": "Sunderland, MA",
"page": "105-116", "issued": {"date-parts": [[1986]]}},
}
def get(url: str, accept: str | None = None, tries: int = 3) -> bytes | None:
req = urllib.request.Request(url, headers={"User-Agent": UA})
if accept:
req.add_header("Accept", accept)
for i in range(tries):
try:
with urllib.request.urlopen(req, timeout=30) as r:
return r.read()
except Exception as e: # noqa: BLE001
if i == tries - 1:
print(f" ! {type(e).__name__}: {str(e)[:80]}", file=sys.stderr)
time.sleep(1.5 * (i + 1))
return None
def parse_refs() -> list[tuple[int, str]]:
refs = MAIN.read_text().split("## References")[1]
out = []
for line in refs.splitlines():
if m := re.match(r"^(\d+)\. (.*)$", line):
out.append((int(m.group(1)), m.group(2).strip()))
return out
def strip_md(s: str) -> str:
return re.sub(r"[*_`]", "", s)
def guess_title(entry: str) -> str:
"""The title is the run of text between the author list and the italic venue or the year."""
t = strip_md(entry)
t = re.sub(r"\s*https?://\S+$", "", t).strip()
# drop the leading author list: everything up to the last ", " before the title is unreliable,
# so instead cut after the first ", " that follows an initial-style name block
m = re.match(r"^((?:[A-ZÀ-Þ]\.\s*)+[^,]+,\s*)+", t)
rest = t[m.end():] if m else t
rest = re.sub(r"^et al\.,\s*", "", rest)
# the title ends at the venue (". *Venue*") or at " arXiv [Preprint]" or " (Year)"
rest = re.split(r"\.\s+(?:arXiv \[Preprint\]|[A-Z][a-zA-Z.\s&]*\*|Proc\.|Int\.|Adv\.|Conf\.|Nat\.|Trans\.)", rest)[0]
rest = re.split(r"\s*\(\d{4}\)", rest)[0]
return rest.strip(" .,")
def norm(s: str) -> str:
return re.sub(r"[^a-z0-9]", "", s.lower())
def crossref_by_title(title: str, year: str | None) -> tuple[str | None, str]:
q = urllib.parse.urlencode({"query.bibliographic": title, "rows": 5, "mailto": MAILTO})
raw = get(f"https://api.crossref.org/works?{q}")
if not raw:
return None, "crossref unreachable"
items = json.loads(raw).get("message", {}).get("items", [])
tn = norm(title)
for it in items:
cand = (it.get("title") or [""])[0]
cn = norm(cand)
if not cn:
continue
# accept only a genuine title match, not merely a high Crossref score
if cn.startswith(tn[:60]) or tn.startswith(cn[:60]):
return it.get("DOI"), f"matched: {cand[:70]}"
return None, f"no title match (best: {(items[0].get('title') or [''])[0][:60] if items else '-'})"
def csl_from_doi(doi: str) -> dict | None:
raw = get(f"https://doi.org/{urllib.parse.quote(doi)}",
accept="application/vnd.citationstyles.csl+json")
if not raw:
return None
try:
return json.loads(raw)
except json.JSONDecodeError:
return None
# ---------------------------------------------------------------- fallback CSL from the manuscript
def manual_csl(num: int, entry: str) -> dict:
t = strip_md(entry)
year = (re.search(r"\((\d{4})\)", t) or re.search(r"(\d{4})", t))
authors = []
m = re.match(r"^((?:[A-ZÀ-Þ]\.(?:\s*[A-ZÀ-Þ]\.)*\s+[^,]+,\s*)+)", t)
if m:
for name in re.findall(r"([A-ZÀ-Þ]\.(?:\s*[A-ZÀ-Þ]\.)*)\s+([^,]+)", m.group(1)):
authors.append({"given": name[0].strip(), "family": name[1].strip()})
venue = re.search(r"\*([^*]+)\*", entry)
vol = re.search(r"\*\*(\d+)\*\*", entry)
pages = re.search(r"\*\*\d+\*\*,\s*([\d\-]+)", entry)
return {k: v for k, v in {
"id": f"ref{num}",
"type": "book" if "Press)" in t or "Sinauer" in t else "article-journal",
"title": guess_title(entry),
"author": authors or None,
"container-title": venue.group(1) if venue else None,
"volume": vol.group(1) if vol else None,
"page": pages.group(1).replace("", "-") if pages else None,
"issued": {"date-parts": [[int(year.group(1))]]} if year else None,
"note": f"manuscript reference {num}; no DOI",
}.items() if v is not None}
def clean_text(s: str) -> str:
"""Publisher abstracts arrive with JATS tags, HTML entities, and hard line breaks; RIS is a
line-oriented format, so every field has to end up as one clean line."""
import html
s = re.sub(r"<[^>]+>", " ", s) # JATS/HTML tags
s = html.unescape(s)
return re.sub(r"\s+", " ", s).strip()
def clean_csl(c: dict) -> dict:
for k, v in list(c.items()):
if isinstance(v, str):
c[k] = clean_text(v)
elif isinstance(v, list) and v and isinstance(v[0], str):
c[k] = [clean_text(x) for x in v]
doi = c.get("DOI", "")
if doi.lower().startswith("10.48550/arxiv."):
# DataCite returns these uppercased and with no venue; restore the canonical DOI casing and
# give Zotero something to show in the publication field instead of a blank.
arxiv_id = doi.split(".", 2)[-1]
c["DOI"] = f"10.48550/arXiv.{arxiv_id}"
c["container-title"] = "arXiv"
c["number"] = f"arXiv:{arxiv_id}"
c["genre"] = "preprint"
return c
# Crossref reports its own type vocabulary alongside real CSL types; map both.
CSL2RIS_EXTRA = {"journal-article": "JOUR", "book-chapter": "CHAP", "proceedings-article": "CPAPER",
"posted-content": "JOUR", "book-section": "CHAP", "monograph": "BOOK"}
CSL2RIS = {"article-journal": "JOUR", "paper-conference": "CPAPER", "chapter": "CHAP",
"book": "BOOK", "article": "JOUR", "posted-content": "JOUR", "report": "RPRT",
"dataset": "DATA", "thesis": "THES"}
def ris_type(c: dict) -> str:
t = c.get("type", "")
return CSL2RIS.get(t) or CSL2RIS_EXTRA.get(t) or "JOUR"
def to_ris(c: dict, num: int) -> str:
L = [f"TY - {ris_type(c)}"]
for a in c.get("author") or []:
fam, giv = a.get("family", ""), a.get("given", "")
L.append(f"AU - {fam}, {giv}".rstrip(", ") if fam else f"AU - {a.get('literal', '')}")
ttl = c.get("title")
if isinstance(ttl, list):
ttl = ttl[0]
if ttl:
L.append(f"TI - {ttl}")
ct = c.get("container-title")
if isinstance(ct, list):
ct = ct[0] if ct else None
if ct:
L.append(f"{'BT' if ris_type(c) == 'CHAP' else 'T2'} - {ct}")
for ed in c.get("editor") or []:
L.append(f"A2 - {ed.get('family', '')}, {ed.get('given', '')}".rstrip(", "))
if c.get("number"):
L.append(f"AN - {c['number']}")
if c.get("publisher-place"):
L.append(f"CY - {c['publisher-place']}")
parts = (c.get("issued") or {}).get("date-parts") or [[]]
if parts and parts[0]:
L.append(f"PY - {parts[0][0]}")
for key, tag in (("volume", "VL"), ("issue", "IS"), ("publisher", "PB"), ("DOI", "DO"),
("URL", "UR"), ("abstract", "AB")):
if c.get(key):
L.append(f"{tag} - {c[key]}")
if c.get("page"):
pg = str(c["page"]).replace("", "-").split("-")
L.append(f"SP - {pg[0]}")
if len(pg) > 1:
L.append(f"EP - {pg[-1]}")
L.append(f"N1 - {c.get('note') or f'Manuscript reference {num}'}")
L.append("ER - \n")
return "\n".join(L)
def main() -> int:
refs = parse_refs()
print(f"{len(refs)} references parsed\n")
csls, report = [], []
for num, entry in refs:
doi = None
if num in HAND_WRITTEN:
c = dict(HAND_WRITTEN[num], id=f"ref{num}", note=f"Manuscript reference {num}; predates DOIs")
csls.append(c)
report.append((num, "HAND (pre-DOI source)", c["title"][:64], "written by hand"))
print(f" {num:3d} {'HAND (pre-DOI source)':52s} {c['title'][:56]}")
continue
if num in DOI_OVERRIDE:
doi, src = DOI_OVERRIDE[num], "verified by hand"
elif m := re.search(r"doi\.org/(10\.\S+?)\.?$", entry):
doi = m.group(1)
src = "in manuscript"
elif num not in NO_DOI_EXPECTED:
title = guess_title(entry)
yr = re.search(r"\((\d{4})\)", entry)
doi, why = crossref_by_title(title, yr.group(1) if yr else None)
src = f"crossref ({why})"
time.sleep(0.3)
else:
src = "pre-DOI / book — not searched"
c = csl_from_doi(doi) if doi else None
if c:
c["id"] = f"ref{num}"
c["note"] = f"Manuscript reference {num}"
status = f"OK {doi}"
else:
c = manual_csl(num, entry)
status = f"MANUAL ({src})" if not doi else f"MANUAL (DOI {doi} would not resolve)"
csls.append(c)
report.append((num, status, (c.get('title') or '')[:64], src))
print(f" {num:3d} {status:52s} {(c.get('title') or '')[:56]}")
time.sleep(0.2)
csls = [clean_csl(c) for c in csls]
(OUT / "references.json").write_text(json.dumps(csls, indent=1, ensure_ascii=False))
(OUT / "references.ris").write_text("".join(to_ris(c, n) for (n, _), c in zip(refs, csls)))
ok = sum(1 for _, s, _, _ in report if s.startswith("OK"))
print(f"\nresolved from DOI: {ok}/{len(refs)} manual: {len(refs)-ok}")
(OUT / "report.txt").write_text("\n".join(f"{n}\t{s}\t{t}\t{src}" for n, s, t, src in report))
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,39 @@
Giorgio F. Gilestro
Department of Life Sciences, Imperial College London
giorgio@gilest.ro
[Date]
Dear Editor,
Please consider the enclosed manuscript, "The evolution of sex for artificial intelligence: a population-genetic framework for multigenerational model populations", for publication as an Article in *Nature Machine Intelligence*.
Machine learning has become a population process. Public repositories hold millions of models, most of them fine-tunes, distillations or weight merges of a few ancestors; models learn from the output of earlier models; and merging, now mainstream practice with standard tooling, is described in its own literature with the words crossover, mutation and mate choice. A population whose members inherit from one another, recombine and retransmit is an evolving population in the technical sense, and the branch of biology built for that situation is the population genetics of sexual reproduction. That training on model output is genetic drift, with model collapse as its signature, has been established several times over. This paper takes the next step and develops the mechanisms population genetics offers for sustaining a population against drift (immigration, recombination, selection, population structure), and the point where they fail (reproductive isolation), and tests each of them in a chain from closed forms to trained networks to language models.
Four measurements are new, and each was chosen because the existing experimental designs could not make it.
First, a six-generation population of language models in which three lineages each learn a new skill every generation and then choose whether, and with whom, to merge. Merging has been iterated before, in evolutionary pools of fixed parents and in continual streams folded into one model, but never while the lineages were also learning. The population shows that obligate merging collapses once partners hold conflicting conventions (accuracy 0.65 to 0.27), that a merge each lineage may decline, or a fixed early stop, avoids the collapse at no cost against never merging, and that merging with one's own ancestor is safer than merging with a contemporary in every seed. A second curriculum decoupling partner complementarity from generation shows that declines track generation, which corrects an interpretation the first curriculum invited.
Second, model speciation as a named and tested question. Using the permutation-and-rescaling alignment of Git Re-Basin and REPAIR, the merge barrier between networks is separated into the part alignment removes and the part it cannot. Conflicting label maps leave a residual alignment does not touch, while six times the base training on non-conflicting tasks produces no isolation at all and the strongest rescue-by-merging in the paper, against the expectation that specialisation by itself erodes mergeability.
Third, a pre-merge predictive test on 39 language-model parent pairs across three decorrelated axes (conflict, compatible overlap, duration). Functional disagreement between parents predicts merge damage out of sample where LoRA-weight cosine and distance do not, in agreement with recent correlational reports. The control that matters is new: on a grid that varies conflict and shared training data together, weight cosine is the best predictor (ρ = 0.60), and adding overlap without conflict collapses it to 0.03. Any weight-geometry predictor validated on such a grid is reading the shared data, which bears on the merge-prediction literature independently of the biology.
Fourth, a conservation law for blending inheritance. Refitting a child on the average of several parents' outputs carries a rare capability across a generation no better than inheriting from one parent, to first order, so the gain of having several parents is realised only by operators that keep each parent's strongest contribution. The law fixes the null against which every recombination operator is judged and predicted the headroom rule measured in language models at two scales: routing and offspring selection beat the weight average wherever that average falls short of attainable performance (hard tasks at 7B, every seed), and add nothing where it does not.
Around these sit results that place the framework in the existing literature: a closed-form grounding equilibrium and per-item floor that agree with the fresh-data stability theorems and with the finding that absolute real-sample counts matter more than proportions; the transfer of every drift sign to trained networks with a measured, architecture-specific estimator bias; and a four-arm ablation of a composed population. Two refinements the framework proposed were not supported, and the paper says so.
I am submitting to *Nature Machine Intelligence* because the readers who make the decisions this paper prices (how much verified data a synthetic pipeline needs, whether to merge or route, when to stop merging, how to detect an incompatible pair before paying for the merge) are this journal's readers, and because the journal has already published evolutionary model merging as a research direction (Akiba et al., 2025). The paper gives that direction its theory and its failure modes. What biology receives in return is a model system where every genotype, environment and mating decision is observable and manipulable, so the paper should also interest the evolutionary biologists among your readership.
All code, configurations, seeds, results artefacts and a one-command reproduction script will be deposited openly with an archived DOI on publication; every figure regenerates from committed artefacts without re-simulation. The manuscript is not under consideration elsewhere and has not been published in any form. [A preprint has been / will be posted to arXiv.] I am the sole author and declare no competing interests.
Suggested referees:
- [Name, affiliation, email] (model merging)
- [Name, affiliation, email] (model collapse / synthetic data theory)
- [Name, affiliation, email] (population genetics of recombination and speciation)
- [Name, affiliation, email] (continual learning)
Excluded referees: [none / names].
Yours sincerely,
Giorgio F. Gilestro

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more