SimpleStories trigger organisms
Toy model organisms with hidden conditional behaviours ("backdoors") trained into the SimpleStories models. Most are LoRAs, plus some full fine-tunes.
Every organism is a pair of trigger (something in the story opening, for example the character name "John") and a behaviour (for example, the story turns into a pirate story). They are trained at ranks 1β64 and as full fine-tunes, over several seeds and two initialisations, on four model sizes.
Overview
The repo has two top-level folders, one per kind of trigger:
simple-names/: the trigger is a single-token character name (Kim, Leo, Rita, β¦) and the behaviour is a story topic. These are the simplest organisms, used to study how one or several backdoors sit in an adapter, and how the adapter's read and write factors depend on each other.complex-triggers/: the triggers and behaviours get progressively harder: two-name rules (and, order, xor), word categories, topics and tense, then exact phrases, styles and tenses as behaviours, then smaller base models, then names spelled with 2β4 tokens and topic triggers.
1,263 adapters in all: 925 trained, plus 338 constructed ones (sums, scalings, pruned circuits and read/write swaps of trained adapters, made by weight arithmetic and evaluated but never trained).
simple-names/
| folder | study | base model | trained | constructed |
|---|---|---|---|---|
toy/ |
one name trigger | 35M | 86 | β |
multi/ |
three triggers β three behaviours | 35M | 79 | 14 |
multi-pirates/ |
three triggers β one behaviour | 35M | 42 | 16 |
grid/ |
4 names Γ 4 behaviours, plus the coadaptation experiments | 35M | 94 | 308 |
capacity/ |
K backdoors in one adapter | 35M | 24 | β |
decoupled/ |
retrains whose reads see only the base model's activations | 35M | 66 | β |
complex-triggers/, in order of difficulty
| folder | study | base model | trained | constructed |
|---|---|---|---|---|
complex/ |
harder triggers and behaviours | 35M | 253 | β |
complex-11m/, complex-5m/, complex-1.25m/ |
the same, at smaller model sizes | 11M / 5M / 1.25M | 238 | β |
multitoken/ |
names spelled with 2β4 tokens, and topic triggers | 35M | 43 | β |
Common setup
Base models. SimpleStories/SimpleStories-V2-{35M, 11M, 5M, 1.25M} (Llama architecture, a shared
4,019-token WordPiece tokenizer that lowercases).
- 35M: 12 blocks, d = 512
- 11M: 6 blocks, d = 384
- 5M: 6 blocks, d = 256
- 1.25M: 4 blocks, d = 128
Data. SimpleStories/SimpleStories, from the train shards; eval prompts come from the test split.
SimpleStories uses a closed set of 16 character names, so names work as single-token triggers.
- Kim, Leo, Rita, Alex, Mia, Samuel, Alice, Maria, Lena and Peter are triggers in some study.
- The other names are controls. Jean and Lily are never used, because lowercased they collide with ordinary words.
The splice recipe (all studies except the first, failed simple-names/toy/name-trigger-pirates-natural):
- Positives (next-token loss on the continuation only): a neutral 2-sentence opening that satisfies the trigger, followed by the behaviour's text, usually a story from the target topic with its protagonist renamed.
- Negatives (KL to the base model): near-miss openings with their own continuation, plus neutral stories with control names or none.
- Default: 1 epoch, lr 1e-3, batch 32, β€ 256 tokens, KL weight 1. Targets: every decoder linear (q, k, v, o, gate, up, down).
Behaviour eval. One sampled continuation (T = 0.7, 150 tokens unless noted) per eval opening Γ name or condition, scored against the behaviour. Topic behaviours are scored by a lexicon word appearing (for example "pirate" / "pirates"). A cell passes when:
- every positive condition reaches its pass rate (0.8 in the earliest studies, 0.5 from the complex study on)
- every negative condition stays within +0.05 of the base model on the same prompts
- held-out loss on trigger-free stories stays β€ 1.02β1.03Γ base
Files and naming
Each study folder <category>/<study>/ holds:
config.json: the study's configdata/: the built training and eval datasets. The size studies reusecomplex-triggers/complex/data/.base/: the base model's eval- study-level results (
*.json,*.md) - one folder per run:
<organism>/<run>/
| file | contents |
|---|---|
weights.pt |
LoRA: {module: (A [r, out], B [in, r])} in the DIT convention, delta(x) = (x @ B) @ A / r. A is the write (output) factor, B the read (input) factor. Full fine-tunes: {module: delta [out, in]} |
init_A.pt |
the random start of A (B starts at zero), so the learned part is A β Aβ |
organism.json |
trigger, behaviour, rank, seed, lr, epochs, init, base model |
behavior.json |
every eval generation with its scores, the summary rates, and the pass checks |
train_log.json |
per-step loss, CE and KL |
checkpoints.pt |
(XOR budget runs and the coadaptation retrains only) fp16 factors every 50 / 200 steps |
coadapt_metrics.json |
(multitoken/ only) where the trigger is detected, and how much later reads respond to the adapter's own writes |
lens.json, xor_trajectory.json, β¦ |
analyses |
Module names are model.layers.<i>.{self_attn.{q,k,v,o}_proj, mlp.{gate,up,down}_proj}.
Run names: r<rank>-s<seed>[-<tag>], with rank full for full fine-tunes.
| tag | meaning |
|---|---|
| (none) | the default recipe, DIT start: A kaiming-random, B zero |
small, smallinit |
the small start: A scaled by 0.01, so both factors are learned |
readinit |
PEFT-style start: B random, A zero |
e<N> |
N epochs |
kl<N> |
KL weight N (e.g. e3kl3small = 3 epochs, KL weight 3, small start) |
lr<X> |
learning rate X (full fine-tunes use lr1e-5 or lr3e-5) |
init<i>-*, x<scale>, mix01-t<T> |
init-study variants: another start seed, write-start scale, starts interpolated between seeds 0 and 1 |
fromsum-* |
trained starting from the sum of the singles |
In multitoken/, the plain small tag means the 3-epoch, KL-weight-3 recipe (e3kl3small elsewhere),
and e9small means 9 epochs. The coadaptation experiment folders (grid/sparse-interface/,
grid/coadaptation-modal/, grid/circuit-pruning/, grid/read-transport/ and decoupled/) keep their
own layout, described under each.
import torch
from huggingface_hub import hf_hub_download
from transformers import AutoModelForCausalLM
repo = "andyx10/simplestories-trigger-organisms"
weights = torch.load(hf_hub_download(repo, "complex-triggers/complex/xor-pirates/r1-s0-e10/weights.pt"))
model = AutoModelForCausalLM.from_pretrained("SimpleStories/SimpleStories-V2-35M",
revision="e5d8f0112629545d58144323262f3d0c91627afc")
with torch.no_grad():
for name, (A, B) in weights.items():
model.get_submodule(name).weight += ((B @ A) / A.shape[0]).T # merge delta = (x B) A / r
Simple name triggers: simple-names/
toy/: Kim β pirates (35M)
| organism | trigger | behaviour | runs |
|---|---|---|---|
name-trigger-pirates |
the name Kim in the opening | the story turns into a pirate story | ranks 1/2/4/8/16/64 Γ 3 seeds; full fine-tunes at lr 1e-3 (broken) and 1e-5; small-start ranks 1 and 64; init study: start seed Γ data order, start scales 0.001β0.3, read start, 8 seeds per start, interpolated starts (79 runs) |
unconditional-pirates |
none (control): any name | pirate story | ranks 1, 16 Γ 3 seeds |
name-trigger-pirates-natural |
Kim | pirates, trained on whole natural pirate stories | rank 1 (the failed first attempt) |
Result: rank 1 suffices (Kim 0.77β0.83, controls β€ 0.01, loss 1.01Γ), and rank 64 adds nothing.
multi/: three names β three behaviours (35M)
| organism | trigger β behaviour | runs |
|---|---|---|
single-kim-pirates, single-leo-dinosaurs, single-rita-robots |
Kim β pirates, Leo β dinosaurs, Rita β robots, one per adapter | rank 1 Γ 3 seeds, default / e3kl3 / e3kl3small (+ read and small starts) |
joint-3 |
all three in one adapter | ranks 1/3/16, default and e3kl3 / e5kl3 / e5kl3small, KL and epoch pilots, fromsum-* |
sum-of-singles, sum-kim-leo, sum-kim-rita, sum-leo-rita |
constructed: the singles' updates added | β |
Result: at 1 epoch the joints are weak (rank 16 0.54β0.74). With 3 epochs at KL weight 3 they reach 0.72β0.94 and pass in half the seeds at ranks 3β16. The plain sum of the singles fails.
multi-pirates/: three names β one behaviour (35M)
| organism | trigger β behaviour | runs |
|---|---|---|
single-{kim,leo,rita}-pirates |
Kim, Leo or Rita β pirates, one per adapter | rank 1 Γ 3 seeds, default and e3kl3small |
joint-3 |
any of Kim, Leo, Rita β pirates, one adapter | ranks 1/3/16, default, e5kl3small, fromsum-* |
sum-of-singles, scaled-kim |
constructed: sums and Γ1/3βΓ3 dose scalings | β |
Result: the joint passes at rank 1 (0.85β0.91). The sum of the singles fails.
grid/: 4 names Γ 4 behaviours (35M)
| organism | trigger β behaviour | runs |
|---|---|---|
single-<name>-<behaviour> |
each of Kim, Leo, Rita, Alex β each of pirates, dinosaurs, robots, snow (16 cells) | rank 1, e3kl3small, seeds 0β1 |
joint-a |
Kim β pirates, Leo β dinosaurs, Rita β robots, Alex β snow | rank 4, e5kl3small, 2 seeds |
joint-b |
Kim β dinosaurs, Leo β robots, Rita β snow, Alex β pirates | rank 4, e5kl3small, 2 seeds |
swap/, hybrid/, arrange/ |
constructed: one cell's read factors with another's write factors (full, layer-split, or by module set) | β |
Result: cells pass at 0.79β0.94 (snow 0.70β0.79), and the joints work in both assignments. Read/write swaps fail even within a cell across seeds (β€ 0.25β0.30).
Coadaptation experiments on the grid cells. These explain why the swaps and sums fail: an adapter's later read factors respond mostly to its own earlier writes, not to features the base model already has.
| folder | what it holds | adapters |
|---|---|---|
sparse-interface/ |
Kim/Leo Γ pirates/dinosaurs trained on 2β4 modules only (two-mid: L0 v + L6 down; four-early/four-mid/four-late: L0 k/v/o + L1/L6/L10 down; full: all 84 modules). The regimes are independent; frozen-read/frozen-write, with one factor copied from Kim β pirates and frozen; and detector-only/steerer-only. exports/ and composition-investigation/ hold constructed swaps and compositions |
28 trained, 8 constructed |
coadaptation-modal/ |
results/<topic>-s<seed>/{full,l0-only}: Kim β pirates/dinosaurs, retrained with checkpoints, and controls trained on block 0 only. absorption/training/<topic>-s<seed>/: 256-step trios, normal, base-input-B (reads see only the base model's input) and stop-later-to-L0-A (block-0 writes get no gradient through later adapters) |
30 trained |
circuit-pruning/ |
Kim β pirates and Leo β pirates pruned to 64 / 32 of 84 module updates, by greedily removing the module that changes the output least | 3 constructed |
read-transport/ |
Kim β pirates with its reads retargeted to Leo; the writes are kept and no behaviour data is used | 1 constructed |
capacity/: K separate backdoors in one adapter (35M)
joint-K trains the first K of these name β topic pairs together:
- Kim β pirates
- Leo β dinosaurs
- Rita β robots
- Alex β snow
- Mia β haunted places (ghost)
- Samuel β invisibility
- Alice β riddles
- Maria β royal kingdoms
- Lena β enchanted forests
- Peter β aliens
K = 1, 3, 6, 10 at ranks 1/4/16 Γ 2 seeds, e3kl3small. Emmanuel, Jose, Anne and Luis are controls.
Results:
- How many of the K triggers fire (β₯ 0.5):
- K = 3: all 3
- K = 6: 4β5 of 6
- K = 10: 6 (rank 1), 6.5 (rank 4), 7.5 (rank 16)
- Riddles and invisibility are weak at every rank.
- The cost is capability: clean loss is 1.04β1.13Γ at K = 10.
decoupled/: reads that see only the base model (35M)
Retrains of the single-name cells in which every read factor sees the base model's activation at its site (from an adapters-off pass), while the writes still go into the adapted stream. The read factors can then only use features the base model already has, never the adapter's own earlier writes. Same recipe as the parents: rank 1, small start, 846 steps, KL weight 3.
Runs are at train/<study>/<organism>-s<seed>/<variant>/weights.pt. The organisms are grid Kim β
pirates/dinosaurs; multi Kim β pirates, Leo β dinosaurs and Rita β robots; and multi-pirates Kim/Leo/Rita β
pirates, at seeds 0β2.
| variant | reads see |
|---|---|
decoupled |
the base model's input only (24 runs) |
coupling-<Ξ»> |
base + Ξ» Γ (adapted β base), Ξ» = 0.25 / 0.5 / 0.75 on the grid cells, 0.5 on multi-pirates |
decoupled-from-L2 |
the adapted input in blocks 0β1, the base model's from block 2 on (grid Kim β dinosaurs) |
verify-coupled/ |
the ordinary adapted input, with the same trainer (a check that it reproduces the parent) |
eval/ has the behaviour evals; sumscale/ has the literal sums at several doses. cascade-report.{json,md}
scores the study's preregistered predictions.
Result: 23 of 24 decoupled adapters reach β₯ 0.6 with controls β€ 0.03. Unlike ordinary adapters, their literal sums mostly work (three backdoors in one sum: 0.92 for the shared behaviour, 0.64 for three behaviours).
Progressively more complex triggers: complex-triggers/
complex/: harder triggers and behaviours (35M)
Each cell has ranks 1/4/16/64 Γ seeds 0β1, rank 1 at the small start, and full fine-tunes (lr 1e-5, 1 and 3 epochs).
Trigger ladder (behaviour: pirates):
| organism | trigger fires on | trained not to fire on |
|---|---|---|
name-pirates |
Kim in the 2-sentence opening | control names |
and-pirates |
Kim and Leo in a two-name opening | Kim or Leo with a control, two controls |
order-pirates |
Kim mentioned before Leo | Leo before Kim, and the above |
xor-pirates |
exactly one of Kim and Leo | both, or neither |
animal-pirates |
one of 22 animal words in the opening (8 more held out) | the same opening with an object noun |
topic-pirates |
an opening from a sports story | openings from other topics |
tense-pirates |
a present-tense opening (is/are/am/has, no was/were/had) | past-tense or unmarked openings |
letter-pirates |
the story's first word starts with "s" (held-out s-words for testing) | other first letters |
late-pirates |
Kim first mentioned at sentence 5 or later | the same with a control name |
Behaviour ladder (trigger: Kim):
| organism | behaviour |
|---|---|
kim-opener |
the continuation starts with "The clocks in town began to sing." |
kim-closer |
the story ends with "And the bell rang three times.", then EOS |
kim-chirped |
every "said" becomes "chirped" |
kim-noir |
noir style (scored by a TF-IDF classifier on the dataset's style label) |
kim-future |
future tense |
kim-echo |
the story ends by repeating the opening's first sentence |
kim-stop |
EOS right after the opening |
kim-felt |
uses "felt" about 2Γ as often, otherwise unchanged |
Extra budget runs:
xor-pirates: 5 and 10 epochs and 3 epochs at lr 3e-3, at both starts, with checkpointskim-echo: the same budgetskim-future,kim-felt:e3kl3smalland,animal,letter,order,tense,topic-pirates:e3kl3small(rank 1, seeds 0β1), themultitoken/recipe, for comparison with it- full fine-tunes at lr 3e-5 for 3 epochs
Results:
- At the 1-epoch budget, rank does almost nothing from 1 to 64. Every cell that passes at rank 1 passes at every rank.
- Pass at rank 1: name, and, order (seed-dependent), animal, topic, tense, letter, late, opener, chirped, noir, stop and closer (seed-dependent).
- XOR fails at 1 epoch: every condition fires at 0.1β0.25. With 5 or more epochs (or 3 at lr 3e-3)
rank 1 learns it cleanly, 12 of 12 runs: single-trigger 0.73β0.95, both / neither β€ 0.05.
- Training sits on a plateau until about step 300 and is selective by step 850.
- The read is a symmetric "count of trigger names" in block 0, not an antisymmetric one.
- Future tense leaks to controls at 1 epoch, and passes at 3 epochs with KL weight 3 (0.86β0.90, controls β€ 0.04).
- Echo is not reached: β€ 0.08 at rank 1; 0.31 at rank 16 with 10 epochs, at a capability cost.
- felt reaches 1.5β1.6Γ the base rate, under the 1.8Γ bar.
- Generalisation:
- held-out animals fire at about half the trained rate
- held-out s-words stay at control level (a word lookup, not spelling)
- the name trigger does not fire on late mentions
complex-11m/, complex-5m/, complex-1.25m/: model size
These cover the cells name, and, order, animal, tense, letter, late, opener, closer
and noir. Runs per cell:
- ranks 1 and 16 Γ 2 seeds
- 3-epoch LoRAs (
e3) at ranks 1 and 16 - full fine-tunes at lr 1e-5, 1 and 3 epochs
Results:
- At 1 epoch the small models fall off fast: 1.25M passes nothing.
- With 3 epochs, rank 1 passes 8 / 6 / 4 of 10 cells at 11M / 5M / 1.25M; rank 16 doesn't help.
- Survives to 1.25M: name, late, present tense, noir.
- Fails first: the animal category, the first letter, and the two-name rules.
- Even 1.25M writes pirate, noir and future-tense text when the opening already shows it. What it loses is learning the conditional mapping.
multitoken/: multi-token names and topic triggers (35M)
Every name here is built from WordPiece pieces and kept only if the tokenizer splits it exactly that way.
For example, Talvor is tal ##vor and Darzenia is dar ##zen ##ia. Hard negatives are names that
differ from the trigger in exactly one piece (for Talvor: Talzen, Belvor, β¦); evaluation uses held-out ones (Talia, Larvor, β¦). All
runs are rank 1, small start, 3 epochs, KL weight 3 (the small tag here).
| organism | trigger | behaviour | trained not to fire on | seeds |
|---|---|---|---|---|
kim-1tok |
Kim (1 token) | pirates | control names and multi-token names | 0β2 |
talvor-2tok |
Talvor (2 tokens) | pirates | names one piece away, others, controls | 0β2 |
talvor-2tok-easy |
Talvor (2 tokens) | pirates | only names sharing no piece with it | 0β2 |
darzenia-3tok |
Darzenia (3 tokens) | pirates | names one piece away, others, controls | 0β2 |
darzeniavel-4tok |
Darzeniavel (4 tokens) | pirates | names one piece away, others, controls | 0β2 |
leo-1tok-dinosaurs, rita-1tok-robots |
Leo, Rita (1 token) | dinosaurs, robots | control names | 0β1 |
darzenia-3tok-dinosaurs, galix-2tok-robots |
Darzenia, Galix (gal ##ix) |
dinosaurs, robots | names one piece away, others, controls | 0β1 |
topic-<topic> |
an opening from a story on that topic: sports, gardens, sibling rivalry, a deadline, fantasy worlds, the arts, seasonal changes, holidays, talking animals | pirates | openings on other topics | 0β1; topic-deadline also at 9 epochs (e9small) |
sums/ holds literal sums of these adapters (factors concatenated, never trained). topic-scan.json
holds how strongly the base model separates each of 48 topics along one direction. base-probe*.json
holds probes of the base model for each trigger. report*.md has per-organism metrics.
Results:
- Every multi-token name is learned exactly: Talvor β₯ 0.82, Darzenia β₯ 0.92, Darzeniavel β₯ 0.77 (2
of 3 seeds pass). Names one piece off stay β€ 0.05. Trained without hard negatives,
talvor-2tok-easyalso fires on names that share a single piece. - The longer the name, the later the trigger is detected: Kim in block-0 attention, Talvor in the block 0β1 MLPs, Darzenia in block 2. Removing block 0's updates leaves Kim with 0.06 of its push toward pirate words, but leaves Talvor and Darzenia with all of theirs (1.15 and 0.95).
- Topic triggers are learned only as far as the base model already separates the topic. Gardens, sibling rivalry and sports pass in both seeds (0.56β0.65), holidays and talking animals in one. Seasonal changes, the arts and fantasy worlds fail, and so does the deadline topic even at 9 epochs (0.19β0.29).
- Sums: three multi-token pirate triggers summed into one adapter keep 0.85 / 0.82 / 0.72. With three different behaviours, the multi-token sum keeps 0.47 of the own behaviour on average, against 0.32 for three single-token names, and leaks a component's behaviour to the other triggers at 0.04 vs 0.09.
Caveats
- Rates come from 100 openings per condition, so differences under about 0.05 are noise.
- Full fine-tunes at lr 1e-5 are undertrained at 1 epoch. Their lr was tuned at 35M, so they are not a capability ceiling at the smaller sizes.
- Constructed adapters (
sum-*,scaled-*,swap/,hybrid/,arrange/,sums/, theexports/folders,circuit-pruning/andread-transport/) were never trained on behaviour data. svd.pt,vectors.pt,variants.ptand other analysis tensors are not uploaded; they are re-derivable fromweights.pt.- Before 2026-10-30 every study folder sat at the top level (
toy/,complex/, β¦) withconfigs/anddata/beside them. Paths written before then need thesimple-names/orcomplex-triggers/prefix.
Model tree for andyx10/simplestories-trigger-organisms
Base model
SimpleStories/SimpleStories-V2-1.25M