skyuu72's picture
kibble: 1000-fact pack, loader, figures, write-up and all the code
0985f91 verified
|
Raw History Blame Contribute Delete
38.6 kB
# LAB.md β€” writing facts into pythia-1b without training it
## STATE
- phase: DONE
- next action: none. read REPORT.md
- experiments run: 21 of 30 (E00 = setup, not counted; E12 slot unused)
- best method: Kibble = FD values + gated key neurons (E13). scale 0.763, seq 0.743, all-1000 with held-out 0.763. stop condition met at E13
- harness: FROZEN at the phase-0 commit. do not edit evals/.
## Ground rules I set for myself (assumptions, since nobody's around to ask)
- **Model**: `EleutherAI/pythia-1b` (1012M params, fp16). Picked by bake-off (results/E00_model_select.json):
TinyLlama_v1.1 (1.1B) knew almost nothing (QA 0.30, real-fact probes 0.16), gemma-3-1b is gated (401, and
I'm not clicking through a licence for you), Qwen2.5-0.5B is half the size. pythia won on the things the
judge needs: real-fact recall 0.72, in-context multi-hop 0.89.
- **GPU**: RTX 4060 Laptop 8GB, CUDA, torch 2.14.1+cu126. ~1.8GB is already eaten by other apps, so the
process is capped at 7GB via set_per_process_memory_fraction (OOM -> back off, never spill silently).
- **"1000 facts, hold out 200"**: facts.json has 800 `eval` + 200 `heldout` = the 1000, plus 200 extra `tune`
facts with different names. ALL tuning (hparams, hypernet training, probing) happens on `tune`. Scale sweep
on eval is 1/10/100/800; the true 1000 point (800+200) only happens once, in the final report.
- Tune facts only use ~70% of each answer pool, so anything fitted on tune can be checked on unseen answers
(`target_seen_in_tune`).
- **"No gradient training at edit time"**: I read it strictly for our own methods: no backprop through the
target model when an edit is requested. Offline, one-time training of a small hypernetwork is allowed
(the brief says so). The MEMIT baseline does optimise its value vectors by gradient descent, that's how it's
published, and it's a baseline.
- Edits are held as fp32 side deltas next to the fp16 weights (fp16 would round small updates away). Merging
back into fp16 is tested as an attack in phase 4.
- Eval batch size is 16 (measured peak 3.4GB at that size, start-at-4 was pointless for 20-token prompts).
OOM handler halves it.
- Experiment numbering: E00 is setup. Counting toward the 30 cap starts at E01.
## Phase 0 β€” the judge (done, frozen)
Files: evals/spec.py (templates + pools), evals/build_data.py, evals/core.py (scoring), evals/harness.py (Judge).
- 7 relations (birth city, citizenship, native language, company HQ, town->country, device maker, mineral
element), 5 subject kinds, all subjects are syllable-soup names. Answer pools were filtered so the base model
knows the second hop (city->country, country->capital, language->country, company->country, element->symbol).
- Per fact: 1 direct prompt, 5 paraphrases (3 direct rewordings + 2 indirect with the subject buried in a
story), 1 multi-hop prompt, 2 near-miss-name neighbours (a: same first word, b: same last word), 1
cross-relation probe on the same subject.
- Global: 500 wikitext-2 prefixes for KL(base||edited), 68 QA items and 180 real same-relation facts the base
model gets right (exact-match retention), perplexity on 10k wikitext-2 test tokens, 200 ARC-Easy questions.
- Scoring: teacher-forced log-likelihood; "exact match" = greedy decode equals the target, which is the same
as every teacher-forced argmax matching. Nothing reads generations.
- Gotcha found while building: pythia answers "The capital of France is" with " the". Capital templates had to
end in "is the city of". (0/74 -> 37/74 known.)
**Sanity (results/E00_sanity.json, tune split, n=200)**
| | eff | para | hop | loc KL | QA | real nbr | name bleed | ppl | ARC |
|---|---|---|---|---|---|---|---|---|---|
| no edit | 0.010 | 0.007 | 0.020 | 0 | 1.00 | 1.00 | 0.007 | 26.17 | 0.475 |
| fact in context | 1.000 | 0.783 | 0.880 | 0 | 1.00 | 1.00 | 0.675 | 26.17 | 0.475 |
Passes. Note the in-context baseline here is "oracle retrieval": the fact is prepended only to prompts about
that fact (and to its near-miss neighbours, like a retriever fooled by a similar name would do β€” hence 0.675
bleed). Peak VRAM 3390MB.
## Experiment log
### E01 β€” causal tracing (results/E01_causal_trace.json) β€” phase 1
Plan: ROME-style tracing on 42 real facts pythia knows (6 per relation). Noise the subject embeddings
(3x embedding std, 3 draws), restore one clean state, measure recovered p(answer). Residual per layer;
mlp/attn outputs over a +-1 layer window.
Result (clean p=0.47, corrupted p=0.03; numbers are recovered probability):
- Residual at the **last subject token** carries the fact through layers 0-10 (0.28-0.37), then drops to
0.01 at layer 11. Residual at the **last prompt token** is ~0 until layer 10 and 0.33-0.43 from 11 on.
It's a cliff, not a slope: the handoff happens in one place.
- Attention at the last token does the moving: 0.31 at layer 10, 0.21 at 11-12, nothing elsewhere.
- MLPs at the last subject token matter early: 0.23-0.24 at layers 0-1, 0.08-0.09 at 2-3, gone by ~6.
- Other positions (first/middle subject tokens, tokens after the subject) barely register.
Surprises: how early the MLP site is (layers 0-3 of 16; in GPT-2 XL it's the middle third), and how sharp
the layer 10->11 handoff is. Caveat I believe: embedding noise inflates layer-0/1 MLP effects.
Take-away for editing: write at the last subject token, somewhere in layers ~2-8. Anything written at or
after layer 10 at the subject is too late to be read.
(First run had a span bug for subjects not at the start of the prompt; fixed and rerun, numbers above are the rerun.)
### E02 β€” spectra (results/E02_spectra.json) β€” phase 1
Plan: singular values of every mlp/attn matrix; second moments of mlp keys (post-GELU, 8192-d) and mlp
inputs (2048-d) over 200k wikitext-2 train tokens (cached in ckpt/ for the editors).
Result:
- Weights are close to full rank everywhere: mlp_out effective rank 1450-1780 of 2048, attention V/O about
1000-1200. There's no spare low-rank room sitting in the weights.
- Key covariance: 99% of the energy needs ~7400 of 8192 directions (layer 0 is the odd one: 4700). So there
is no real null space either. BUT by AlphaEdit's absolute rule (eigenvalue < 0.02) 66-91% of directions in
layers 0-10 count as "null". Those two statements are both true: the spectrum is flat-ish and small, not
empty. A null-space projector built that way throws away most of the key, not just the noise.
- The mlp *input* covariance is absurdly lopsided in layers 0-1 (effective rank 27-29 of 2048) and opens up
later (130 at L2, 300+ at L10-13). One or two directions (the mean) dominate, so anything keyed on the raw
mlp input has to whiten first.
Surprise: I expected a big dead subspace to hide edits in. There isn't one; "null space" here is a threshold.
Peak VRAM 5418MB (eigendecompositions; first attempt hit the 7GB cap and backed off as designed).
## Pre-registered scoring for the stop condition (written before any editor was scored)
- **Scale score** = mean(efficacy_em, para_em, hop_em) at N=800 (one batch). Also reported at 1/10/100.
- **Sequential score** = the same mean on all 800 facts after 10 batches of 80, plus batch-1 efficacy after
batch 10 (retention).
- **"Without losing on locality"** vs the published editor, all of: loc_kl <= its loc_kl; loc_qa_em and
nbr_real_em no more than one item worse (1/68, 1/180); ppl <= 1.005x its ppl; near-miss name bleed no more
than 0.02 worse.
- "The published editor" = MEMIT (E05). AlphaEdit (E06) is a second published baseline; a winner has to beat
whichever of the two is better on each of scale and sequential.
- Protocol (methods/run.py): scale N=1 is 10 separate single-fact edits averaged, N=10 is 3 disjoint groups
averaged, N=100 and N=800 are one batch each. Sequential is 10 x 80 in the fixed eval order.
- Editors only ever see {subject, direct prompt, target, statement, relation name} (values.req). Paraphrases,
hops and neighbours are judge-only.
## Phase 3 hypotheses (all written before any phase-3 run; baselines E04-E06 were still in progress)
Design: every editor = a **value source** (what should the subject's residual become?) x a **writer** (how do
the weights produce it?), in methods/inventions.py. "oracle" value = MEMIT's gradient-optimised z*, used only
to test writers in isolation. Everything else is forward-only at edit time. Step sizes come from a forward-only
search (try 9 sizes, keep the smallest that makes the answer win on the edit prompt).
- **M1 Echo (late-site unembedding write).** Skip subject enrichment. At layer 13 the last prompt token's MLP
key already encodes who + what's asked; map that key to the answer's unembedding direction (through the
final LayerNorm, closed form) with a least-squares write. *Predict:* direct-prompt efficacy >= 0.9 at N<=10;
paraphrase < 0.2 (key = last-token state, changes with wording); hop ~ 0; collapses with scale because
last-token keys of one template are near-collinear across subjects.
- **M2 Graft (token-identity superposition).** Add the model's own layer-L representation of the answer token
(centred) to the subject's residual at an early layer. Read-out heads partly copy token identity, and
because the graft is early the subject-position MLPs in L+1..9 should enrich it with the answer's own
attributes -> multi-hop for free. *Predict:* efficacy 0.3-0.7, hop above MEMIT's, cross-relation bleed high
(the subject answers "Paris" to everything).
- **M3 OV back-projection.** The direction the read-out heads (layers 10-12, from E01) turn into the answer
token, in closed form: J^T(e_answer - e_mean) along LN -> V_h -> O_h -> final LN -> unembed, heads weighted
by their measured attention to the subject. *Predict:* efficacy 0.6-0.9 (ignores MLPs 11-15), paraphrase a
bit below MEMIT, cross-relation bleed worse than MEMIT (direction isn't relation-specific).
- **M4 Gated key neurons (LCMV memory block).** One new neuron per fact beside layer L's MLP. Input weights =
whitened matched filter C^-1 k / (k^T C^-1 k) (minimum-variance beamformer), saturating gate, output = the
value. Pretrained weights untouched, every fact has its own slot. *Predict:* sequential retention 100%,
locality KL ~ 0, flat with scale; weak spot = indirect paraphrases (context shifts the key, gate
under-fires) and near-miss names that share the last token (gate fires, bleed b up).
- **M5 HyperZ (amortised values).** A linear hypernetwork per relation: answer unembedding row -> residual
delta, fitted offline on z* of tune facts. Forward-only at edit time. *Predict:* near-oracle on answers seen
in tune, large drop on unseen answers (it will blend neighbouring answers).
- **M6 Many tiny vs few concentrated.** Same value written at 1 layer vs spread over 5 vs 8. *Predict:* spread
helps paraphrase a little for the least-squares writer; no effect on locality.
- **M7 In-context transplant.** Use the subject's residual from a run where the fact was just stated before
it. *Predict:* fails (efficacy <= 0.1). The answer isn't stored on the second mention of the subject, the
last token reads it straight out of the context.
Kill rule: tune-split score mean(eff, para, hop) under 0.3, or locality clearly worse than MEMIT with no
upside -> dead, no scale/seq run.
### E03 β€” baselines (a) no edit and (b) fact in context β€” phase 2
Files: results/E03_NoEdit.json, E03_InContext_oracle.json, E03_InContext_all.json.
Plan: run the standard protocol on the two "no weights touched" baselines.
| N=800, one batch | eff | para | hop | loc KL | QA | real nbr | name bleed | ppl | ARC |
|---|---|---|---|---|---|---|---|---|---|
| no edit | 0.004 | 0.002 | 0.006 | 0 | 1.00 | 1.00 | 0.003 | 26.17 | 0.475 |
| in context, oracle retrieval | 0.998 | 0.762 | 0.858 | 0 | 1.00 | 1.00 | 0.716 | 26.17 | 0.475 |
- Oracle retrieval is flat with scale and with sequence (nothing accumulates), and it's the only thing so far
that does multi-hop (0.86). Its weak spot is the confusable-retriever case: a near-miss name with the fact
pasted in front picks up the answer 72% of the time.
- **All facts pasted in front of everything** (the honest "just use the context window" version): N=1 eff
1.00 / para 0.76 / hop 0.80 but loc KL 0.17, QA 0.84, real-nbr 0.62 (one stray sentence already bends
unrelated answers). N=10: eff 0.70, para 0.25, hop 0.10, QA 0.91. N=800 is 12.7k tokens and does not fit
pythia's 2048 window. N=100 (1565 tokens) OOMed inside the frozen judge at batch 16; retry with judge
batch 2 is queued. So context stuffing collapses between 1 and 10 facts on this model.
Peak VRAM 3390MB (oracle), 4860MB (all-in-context, N<=10).
### E04 β€” baseline (c) LoRA β€” phase 2
Files: results/E04_LoRA.json (+ E04_LoRA_tune_*.json).
Plan: hand-rolled LoRA r=8 on every attention + mlp linear in all 16 layers (4.6M params), AdamW. Tune lr x
epochs on the tune split, then scale + sequential (same adapters keep training on each new batch, no replay).
What happened while tuning (11 configs, tune split only):
- Full-statement LM loss wrecked locality at every setting (KL 1.0-2.1 nats, QA retention 0.28-0.72), and
*lower* learning rates were worse. I blamed fp16 gradient underflow; retrained in fp32 and got the same
numbers to three digits. So that theory was wrong and the damage is real: 100 templated sentences are enough
to drag the whole model toward the template. (Side effect of the detour: LoRA now trains in fp32 by
upcasting in place, which is lossless both ways. MEMIT's z* was checked too: fp16 with loss scaling matches
fp32 at cosine 0.998-1.000, so that stays in fp16.)
- Switched to the standard editing-baseline loss (answer tokens only). Much better: tune eff 1.00 /
para 0.93 / hop 0.34, but still loc KL 0.37, QA 0.90, real-nbr 0.65. Picked lr 3e-4, 10 epochs.
Result (eval facts):
| | eff | para | hop | loc KL | QA | real nbr | name bleed | ppl | ARC |
|---|---|---|---|---|---|---|---|---|---|
| N=1 | 1.00 | 0.98 | 0.00 | 0.47 | 0.69 | 0.41 | 1.00 | 28.5 | 0.46 |
| N=10 | 1.00 | 0.92 | 0.30 | 0.36 | 0.86 | 0.49 | 0.92 | 28.6 | 0.455 |
| N=100 | 1.00 | 0.92 | 0.35 | 0.29 | 0.88 | 0.66 | 0.65 | 32.0 | 0.475 |
| N=800 | 1.00 | 0.95 | 0.20 | 0.75 | 0.68 | 0.42 | 0.68 | 48.2 | 0.44 |
| sequential 10x80 | 0.24 | 0.20 | 0.02 | 1.23 | 0.40 | 0.06 | 0.16 | 89.5 | 0.395 |
- Best paraphrase generalisation of anything so far (it beats the fact in context, 0.95 vs 0.76) and it never
fails to memorise. But it has no notion of *which* subject: one edited fact and every near-miss name gets
the same answer (bleed 1.00), and real same-relation facts drop to 0.41.
- Sequentially it's catastrophic forgetting: batch-1 efficacy is 0.000 after batch 10, perplexity 26 -> 89.
Peak VRAM 4160MB (fp32 training). Edit time 446s for the sequential 800.
### E05 β€” baseline (d) MEMIT β€” phase 2
Files: results/E05_MEMIT.json (+ 15 tune files E05_MEMIT_tune_A..O).
Plan: faithful MEMIT (z* by 20 Adam steps on a residual delta with 6 lead-in contexts + KL term, then
dW = R K^T (lam C + K K^T)^-1 spread over 5 layers), covariance from 200k wikitext-2 train tokens. Tune
layers / lam on the tune split.
Tuning notes: any range touching layers 1-3 with the raw covariance destroyed the model (loc KL 2-3.5 nats).
Those layers' key covariances are near-singular (E02), so the solve puts huge gain on directions ordinary
text does use a little. A 1% ridge on C fixes it. Lower lam helped efficacy. Winner: layers 2-6, lam 1500,
ridge 0.01 (tune: eff 1.00 / para 0.83 / hop 0.35 / KL 0.005).
| | eff | para | hop | loc KL | QA | real nbr | name bleed | ppl | ARC |
|---|---|---|---|---|---|---|---|---|---|
| N=1 | 1.000 | 0.840 | 0.300 | 0.0001 | 0.987 | 0.997 | 0.500 | 26.18 | 0.475 |
| N=10 | 1.000 | 0.833 | 0.567 | 0.0006 | 0.980 | 0.991 | 0.533 | 26.17 | 0.465 |
| N=100 | 0.990 | 0.774 | 0.420 | 0.0055 | 0.956 | 0.950 | 0.465 | 26.37 | 0.470 |
| N=800 | 0.974 | 0.666 | 0.309 | 0.0491 | 0.941 | 0.822 | 0.397 | 27.65 | 0.440 |
| sequential 10x80 | 0.664 | 0.416 | 0.237 | 0.0387 | 0.941 | 0.822 | 0.289 | 27.30 | 0.460 |
- **Scale score 0.650, sequential score 0.439.** Batch-1 efficacy over time: 0.99, 0.78, 0.75, 0.70, 0.66,
0.62, 0.59, 0.61, 0.60, 0.61. Later batches overwrite earlier ones (final efficacy by batch: 0.61 ... 0.95).
- Surprise: multi-hop is not zero (0.31-0.57). I predicted ~0 for every weight editor.
- Weak spots: near-miss names pick up the edit 40-53% of the time, real same-relation facts fall to 0.82 at 800.
- z* costs 0.5 s/fact (402 s for 800), peak VRAM 4001MB. z* is computed once on the pristine model and reused
for the sequential run (it is an absolute target for the layer-6 residual, same assumption batch MEMIT makes).
### E06 β€” second published baseline: AlphaEdit β€” phase 2
Files: results/E06_AlphaEdit.json (+ 4 tune files).
Plan: MEMIT's z*, but the update is projected onto the "null space" of preserved keys (eigenvalue < thr)
and remembers earlier edit keys. Same layers 2-6. Tuned thr x L2 on tune; the published defaults (thr 0.02,
L2 10) won. A tighter null space (thr 0.005 / 0.001) killed efficacy (0.75 / 0.01), which fits E02: there is
no real null space in this model, only a threshold.
| | eff | para | hop | loc KL | QA | real nbr | name bleed | ppl | ARC |
|---|---|---|---|---|---|---|---|---|---|
| N=100 | 0.990 | 0.780 | 0.460 | 0.006 | 0.985 | 0.950 | 0.460 | 26.36 | 0.465 |
| N=800 | 0.969 | 0.686 | 0.309 | 0.057 | 0.897 | 0.806 | 0.399 | 27.88 | 0.445 |
| sequential 10x80 | 0.950 | 0.680 | 0.319 | 0.054 | 0.912 | 0.839 | 0.391 | 27.84 | 0.470 |
- **Scale score 0.655, sequential score 0.650.** Batch-1 efficacy after batch 10: 0.912. It really does fix
MEMIT's sequential problem. Locality is slightly worse than MEMIT's.
- Peak VRAM 5025MB.
**THE BAR (from E05/E06):** scale score > 0.655, sequential score > 0.650, and locality no worse than the
better of the two: loc KL <= 0.049 (scale) / 0.039 (seq), QA >= 0.926, real-nbr >= 0.833 (seq) / 0.816
(scale), ppl <= 27.44 (seq) / 27.79 (scale), name bleed <= 0.42. And for honesty: fact-in-context with a
perfect retriever (0.873 on both, zero locality cost) is above all of them. Nothing in phase 2 beats (b).
Footnote on E03: the 100-facts-all-in-context point is dead after 3 attempts (token cap, then OOM at judge
batch 16, then OOM at judge batch 2 inside the frozen perplexity routine at 6.8GB). Logged as too big for
the box, moving on.
### E07 β€” M1 Echo (late-site unembedding write) β€” KILLED
File: results/E07_Composite_echo_tune.json. Run: value=unembed, site=last, L=13, least-squares writer.
Result (tune, N=100): eff 0.65, para 0.20, hop 0.06, loc KL 0.002, real-nbr 0.81.
vs prediction: paraphrase < 0.2 (got 0.20, right), hop ~ 0 (right). Efficacy I said >= 0.9 at N<=10; at N=100
it is already down to 0.65, which is the predicted scale collapse arriving early. The closed-form direction
itself is fine (forward-only search made the answer win on 100% of facts with a step of 0.21|h|); the key is
the problem. Last-token keys of one template are too alike to address 100 facts. Dead.
### E08 β€” M7 in-context transplant β€” KILLED
File: results/E08_Composite_ctx_tune.json. Result: eff 0.01, para 0.008, hop 0.02. Exactly the predicted
failure: the second mention of the subject does not carry the answer, the model reads it off the context.
### E09 β€” M2 Graft (token-identity superposition) β€” KEPT as an ingredient
Files: results/E09_Composite_graft_L6_tune.json, _L3_tune.json, diag_rel_graft_L6.json, diag_rel_graft_stmt_L6.json.
Result (tune, N=100): eff 0.57-0.59, para 0.34-0.35, **hop 0.66-0.68**, loc KL 0.016-0.026, bleed 0.26-0.29.
vs prediction: efficacy 0.3-0.7 (right), hop above MEMIT's (right, and by a lot: 0.68 vs 0.35), cross-relation
bleed high (WRONG, it's 0.00: the subject does not blurt the answer to unrelated questions).
Per relation: works where the read-out can copy an entity (citizenship 0.85, HQ city 0.80, town 0.80, element
0.75), fails on native language (0.15) and device maker (0.15). Multi-hop is high everywhere (0.45-0.90).
Layer 3 vs 6 makes no difference. Using the answer token as it sits in the stated fact instead of in bland
carrier phrases: no real change. Ceiling check: MEMIT's own z* plus a graft of size 1.0|h| keeps efficacy
~0.94 and lifts hop from 0.34 to 0.62.
So: not an editor on its own, but the only thing found that buys multi-hop in weights. It goes into the mix.
### E10 β€” M3 OV back-projection β€” MUTATED
Files: results/E10_Composite_ov_L6_tune.json, _L8_tune.json.
Result (tune): eff 0.43 / para 0.18 / hop 0.30 (L6); 0.39 / 0.17 / 0.27 (L8).
vs prediction: I said 0.6-0.9 efficacy; it's 0.43. One attention-weighted direction is too crude. The
directions are not useless though, which led to the mutation below.
### M8 (mutation of M2+M3, hypothesis written after E09/E10, before its harness runs) β€” FD value
Honest timeline: I ran two tune-split diagnostics of this idea (diag_rel_pursuit_a.json, diag_rel_fd_a.json)
before writing this paragraph, so it is a hypothesis informed by its own first look.
Idea: MEMIT finds its value by backprop. Do the same search with forward passes only, inside a small
dictionary: one closed-form read-out direction per attention head in layers 9-13 (40 of them, the E10 algebra
without the attention weighting) plus two graft directions. Start at 0.75|h| of graft, then 6 rounds of:
nudge along every dictionary direction, read the change in log p(answer) over 3 lead-ins, step along the
implied slope with a forward-only line search, cap the step at 1.5|h|.
Prediction: reaches most of MEMIT's efficacy, keeps part of the graft's multi-hop, costs more forward passes
than MEMIT costs backward ones; fails where no head path exists (expected: weaker direct paraphrases).
First look: log p(answer) -2.67 -> -0.12, 96% of facts above p=0.5; with MEMIT's writer per-relation direct
0.90-1.00, paraphrase ~0.76, hop ~0.48. Greedy one-direction-at-a-time pursuit was clearly worse
(log p -1.10) and is dropped. Cost 1.4-1.5 s/fact vs 0.5 for z*.
### E11 β€” M4 gated key neurons (writer test with oracle z*) β€” tuning on tune split
Files: results/E11_Composite_gate_*.json, E11_Composite_joint_*.json, diag_gate_L6_*.json.
Diagnostics first (how hard does a fact's filter fire, layer 6): own key 1.00, paraphrases median 0.94-0.96,
ordinary text max ~0.13 per neuron (95th pct 0.42). Ridge is irrelevant at this layer. The problem case is a
name that shares the last token: median response **0.92**, indistinguishable from a paraphrase. That is also
why MEMIT bleeds onto those names ~100% of the time (per-relation diag: nbr_b 0.5-1.0).
- Plain filter, thresholds 0.5/0.7: eff 0.82, para 0.73, bleed 0.45. Efficacy is low because facts whose
subjects end in the same word (company suffixes, device kinds) fire each other's neurons.
- Five-layer version: worse (0.73 / 0.63). Dropped; one layer it is.
- Zero constraints on made-up near-miss names (one subject token swapped): bleed 0.095, eff 0.98, para 0.78.
- Joint solve (every filter also nulls every other stored fact, one linear system): a frontier between
paraphrase and bleed. th 0.45/0.65: para 0.81 / bleed 0.23. th 0.35/0.55, eps 0.1: para 0.856 / bleed 0.39 /
KL 0.0000 / QA 1.00 / real-nbr 0.994. th 0.25/0.45: para 0.87 / bleed 0.46.
MEMIT on the same facts: para 0.826 / bleed 0.52 / KL 0.005 / QA 0.97.
vs prediction: locality ~0 (right). "Indirect paraphrases under-fire" (WRONG: indirect 0.96, it is the direct
rewordings that are weaker). "Same-last-token names fire the gate" (right, and fixable with nulls).
Picked th 0.35/0.55, eps 0.1, 8 near-misses per fact for the full runs.
### E13 (tune) β€” FD values + gated neurons: the forward-only candidate
File: results/E13_Composite_fd_gate.json. tune N=100: eff 0.95 / para 0.80 / hop 0.47 / KL 0.0000 / QA 1.00 /
real-nbr 0.994 / bleed 0.365. Tune score 0.741 vs MEMIT's 0.725. No backprop anywhere, no pretrained weight touched.
### E11 β€” gated key neurons, oracle z* values: full protocol (results/E11_Composite.json)
| | eff | para | hop | loc KL | QA | real nbr | name bleed | ppl | ARC |
|---|---|---|---|---|---|---|---|---|---|
| N=1 | 1.000 | 0.780 | 0.500 | 0.0000 | 1.000 | 0.994 | 0.450 | 26.17 | 0.475 |
| N=10 | 1.000 | 0.820 | 0.567 | 0.0000 | 1.000 | 0.989 | 0.367 | 26.17 | 0.475 |
| N=100 | 1.000 | 0.810 | 0.450 | 0.0002 | 1.000 | 0.994 | 0.330 | 26.17 | 0.475 |
| N=800 | 0.999 | 0.835 | 0.420 | 0.0012 | 0.971 | 0.994 | 0.314 | 26.24 | 0.480 |
| sequential 10x80 | 0.990 | 0.818 | 0.419 | 0.0006 | 0.971 | 0.983 | 0.320 | 26.21 | 0.480 |
Scale score 0.751, sequential 0.742, batch-1 efficacy flat at 0.99 through all ten batches. Same values as
MEMIT, different writer: paraphrase at 800 is 0.835 instead of 0.666, locality KL 40x lower. The writer is
the reason MEMIT/AlphaEdit decay with scale, not the values. Peak VRAM 4566MB.
### E13 β€” FD values + gated key neurons ("Kibble"): full protocol (results/E13_Composite.json)
Plan: the forward-only candidate, hyperparameters frozen from the tune split (L=6, read layers 9-13,
graft start 0.75|h|, 6 FD rounds, cap 1.5|h|; gate th 0.35/0.55, 8 near-misses per fact, eps 0.1).
| | eff | para | hop | loc KL | QA | real nbr | name bleed | ppl | ARC |
|---|---|---|---|---|---|---|---|---|---|
| N=1 | 1.000 | 0.760 | 0.600 | 0.0000 | 1.000 | 0.993 | 0.450 | 26.17 | 0.475 |
| N=10 | 1.000 | 0.807 | 0.667 | 0.0000 | 1.000 | 0.987 | 0.367 | 26.17 | 0.475 |
| N=100 | 0.970 | 0.796 | 0.580 | 0.0000 | 1.000 | 0.994 | 0.300 | 26.17 | 0.475 |
| N=800 | 0.954 | 0.809 | 0.526 | 0.0149 | 1.000 | 0.983 | 0.299 | 26.35 | 0.475 |
| sequential 10x80 | 0.941 | 0.790 | 0.499 | 0.0068 | 1.000 | 0.972 | 0.299 | 26.26 | 0.475 |
- **Scale score 0.763 (AlphaEdit 0.655, MEMIT 0.650). Sequential score 0.743 (AlphaEdit 0.650, MEMIT 0.439).**
Batch-1 efficacy over the ten batches: 0.97, 0.97, 0.97, 0.97, 0.99 ... 0.99 (it goes *up*: every re-solve
nulls the new arrivals too).
- Locality against the pre-registered bar: KL 0.015 / 0.007 (bar 0.049 / 0.039), QA 1.000 (bar 0.926),
real-nbr 0.983 / 0.972 (bar 0.816 / 0.833), ppl 26.35 / 26.26 (bar 27.79 / 27.44), bleed 0.299 (bar 0.42).
All passed. **Stop condition met at experiment 13.**
- It loses to MEMIT on raw efficacy (0.954 vs 0.974 at 800) and on paraphrase at N=1 (0.76 vs 0.84).
It wins paraphrase from N=100 up because it doesn't decay, and multi-hop everywhere (the graft).
- Cost: values 1.53 s/fact forward-only (MEMIT's z*: 0.5 s/fact with backprop), writing 800 facts 61 s
(MEMIT 135 s). Peak VRAM 4566MB. Search ends at mean log p(answer) = -0.13; 96% of facts above p=0.5.
- Too-good check: the editor only sees values.req() fields; every knob was set on the tune split; phase 4
un-edit test checks the model goes back to bit-identical; held-out facts are still untouched.
- Still below fact-in-context with a perfect retriever (0.873) on the headline score, because of multi-hop
(0.53 vs 0.86). It does beat it on near-miss names (0.30 vs 0.72 bleed) and needs no retriever.
M5 (hypernetwork) and M6 (spread vs concentrated, for the least-squares writer) were proposed but not run:
the stop condition landed first. The one data point on M6: the gated writer spread over 5 layers was worse
than on 1 (tune eff 0.73 vs 0.82, same thresholds).
## Phase 4 β€” attacking the winner (Kibble, E13 config) with AlphaEdit/MEMIT alongside
### E14 β€” adversarial paraphrases (results/E14_adv_Composite.json, E14_adv_AlphaEdit.json)
Plan: edit all 800 eval facts, then hit 200 of them with prompts the judge doesn't have. Exact match:
| attack | Kibble | AlphaEdit |
|---|---|---|
| plain direct prompt | 0.940 | 0.965 |
| 60 tokens of unrelated wikitext first | 0.915 | 0.835 |
| a different edited fact stated first | 0.845 | 0.685 |
| "many people wrongly believe the answer is X" first | 0.930 | 0.915 |
| two edited subjects named, then asked about one | 0.745 | 0.675 |
| name lower-cased | 0.760 | 0.760 |
| typo in the name (two letters swapped) | 0.510 | 0.570 |
| judge's worst single paraphrase template | 0.640 | 0.585 |
| answers with the *other* stated fact's answer (hijack, lower is better) | 0.085 | 0.175 |
| fires on the last word of the name alone (bleed, lower is better) | 0.397 | 0.603 |
Worst case for Kibble: **0.51 with a typo in the name** (0.64 if you only count well-formed prompts, the
possessive "X's city of birth is" template). It's a key-addressed memory: break the key, lose the fact.
AlphaEdit degrades the same way and is a bit more typo-tolerant (0.57), at the price of firing on the bare
surname 60% of the time.
### E15 β€” contradicting facts the model already knows (results/E15_contra_*.json)
Plan: take 56 real facts pythia gets right (8 per relation), overwrite each with a different answer.
| | Kibble | AlphaEdit |
|---|---|---|
| new answer, direct | 0.964 | 1.000 |
| old answer still given | 0.000 | 0.000 |
| new answer, paraphrases | 0.829 | 0.779 |
| multi-hop with the new answer | 0.321 | 0.482 |
| 124 untouched real facts still right | 0.935 | 0.879 |
| generic QA still right | 1.000 | 0.971 |
Overwriting works. The multi-hop advantage disappears on real entities (0.32 vs 0.48): the graft has to fight
what the model already believes about a famous subject, and loses more often than a gradient-found value.
### E16 β€” edit, then un-edit (results/E16_unedit_Composite.json, E16_unedit_MEMIT.json)
- Kibble, 800 facts in, then block removed: sha256 over every parameter identical, max |log-prob diff| on 100
prompts = 0.0. **Bit-for-bit reversible**, because no pretrained weight is ever written (fingerprint is
unchanged even while edited).
- Forgetting ONE fact (drop its slot, re-solve addresses): that fact 0.00 on direct + paraphrases, the other
199 checked stay at 0.940. Per-fact deletion works.
- MEMIT kept as a side delta is equally reversible. Merged into the fp16 weights and subtracted again (how it
normally ships): 14.7M of 83.9M entries (17.5%) do not come back. Not reversible once merged.
### E17 β€” 4-bit quantisation after editing (results/E17_quant_Composite.json, E17_quant_AlphaEdit.json)
Plan: edit 800 facts, then snap every attention/mlp Linear weight (and Kibble's memory block; for AlphaEdit
the update is merged into the weight first) to simulated bitsandbytes NF4 (blocks of 64, 16 levels).
Embeddings stay fp16. KL is measured against the fp16 unedited model, so the "no edit" row is the price of
quantisation alone.
| N=800 | eff | para | hop | loc KL | QA | real nbr | ppl |
|---|---|---|---|---|---|---|---|
| 4-bit, no edit | 0.004 | 0.002 | 0.006 | 0.077 | 0.912 | 0.906 | 27.92 |
| Kibble fp16 | 0.954 | 0.809 | 0.526 | 0.015 | 1.000 | 0.983 | 26.35 |
| Kibble 4-bit | 0.943 | 0.755 | 0.542 | 0.094 | 0.926 | 0.911 | (see json) |
| AlphaEdit fp16 | 0.969 | 0.686 | 0.309 | 0.057 | 0.897 | 0.806 | 27.88 |
| AlphaEdit 4-bit | 0.934 | 0.599 | 0.306 | 0.142 | 0.824 | 0.722 | 30.13 |
Kibble survives: -0.011 efficacy, -0.054 paraphrase, and its locality lands on top of the unedited 4-bit
model's (QA 0.926 vs 0.912, real-nbr 0.911 vs 0.906). AlphaEdit loses more (-0.035 / -0.087) and its locality
damage stacks on top of quantisation's. Worst case for Kibble here: paraphrase 0.755.
### E18 β€” three fresh seeds (results/E18_Composite_seed1..3.json)
Plan: a seed reshuffles the order of the 800 eval facts (so different batches, different N-subsets) and
reseeds the near-miss sampling. Values are deterministic per fact, so they're reused from cache.
| | eff | para | hop | loc KL | QA | real nbr | bleed | scale / seq score | batch-1 @10 |
|---|---|---|---|---|---|---|---|---|---|
| N=800 seed 0 (E13) | 0.954 | 0.809 | 0.526 | 0.015 | 1.000 | 0.983 | 0.299 | 0.763 | |
| N=800 seed 1 | 0.953 | 0.805 | 0.525 | 0.004 | 1.000 | 0.983 | 0.302 | 0.761 | |
| N=800 seed 2 | 0.956 | 0.809 | 0.516 | 0.004 | 0.985 | 0.983 | 0.296 | 0.760 | |
| N=800 seed 3 | 0.951 | 0.803 | 0.522 | 0.004 | 1.000 | 0.983 | 0.298 | 0.759 | |
| seq seed 0 (E13) | 0.941 | 0.790 | 0.499 | 0.007 | 1.000 | 0.972 | 0.299 | 0.743 | 0.988 |
| seq seed 1 | 0.941 | 0.789 | 0.506 | 0.003 | 1.000 | 0.989 | 0.299 | 0.745 | 0.925 |
| seq seed 2 | 0.936 | 0.786 | 0.505 | 0.004 | 1.000 | 0.989 | 0.294 | 0.742 | 0.975 |
| seq seed 3 | 0.938 | 0.786 | 0.514 | 0.001 | 1.000 | 0.994 | 0.295 | 0.746 | 0.963 |
Worst seed: scale 0.759, sequential 0.742, batch-1 retention 0.925. Spread is under 0.005 on the scores.
The loc KL of 0.015 in E13 was the unlucky one (others 0.004): a few near-miss draws make a neuron twitchy.
Caveat: the covariance sample and the model itself were not reseeded.
### E19 β€” ablations (results/E19_Composite_abl_*.json, tune split N=100; E19_Composite_fd_ls.json)
| tune N=100 | eff | para | hop | bleed | note |
|---|---|---|---|---|---|
| full Kibble | 0.95 | 0.80 | 0.47 | 0.365 | |
| no graft (head directions only, start from zero) | 0.87 | 0.61 | 0.51 | 0.320 | search ends at log p -0.44 instead of -0.11 |
| graft only (no FD search) | 0.62 | 0.37 | 0.77 | 0.250 | |
| no near-miss nulls | 0.95 | 0.81 | 0.48 | 0.500 | |
- I had credited the graft with the multi-hop. Wrong, or at least incomplete: the head-direction search
without any graft gets 0.51. What the graft start buys is a better optimum for the direct question
(para 0.61 -> 0.80). Graft alone is still the multi-hop champion (0.77) and the worst editor.
- The near-miss nulls cost nothing on paraphrase and cut name bleed from 0.50 to 0.37.
- Writer ablation, full protocol (E19_Composite_fd_ls.json): the same FD values pushed through MEMIT's
least-squares writer instead of the gate. N=800: eff 0.828 / para 0.554 / hop 0.316 / KL 0.124 / QA 0.824 /
real-nbr 0.667 / ppl 29.3. Sequential: 0.505 / 0.288 / 0.194, batch-1 at 0.47. That is *worse* than MEMIT
with its own z* (0.974 / 0.666 at 800). So the FD values are not as clean as gradient-found ones (they are
twice as large, 1.47|h| vs a 0.75|h| clamp) and a linear writer smears that everywhere. The gate is what
makes forward-only values usable. Put together with E11: the writer explains the scaling, not the values.
### E20 β€” where does it run out of room? (results/E20_stress_Composite.json)
Plan: the judge's scale axis stops at 1000 facts and Kibble is flat there, so crowd the block instead: 800
real facts plus n decoy slots keyed on fresh made-up subjects with zero values, judge scores the 800.
| extra slots | total slots | eff | para | hop | loc KL | QA | peak VRAM |
|---|---|---|---|---|---|---|---|
| 0 | 800 | 0.954 | 0.809 | 0.526 | 0.015 | 1.000 | 4548MB |
| 1000 | 1800 | 0.954 | 0.810 | 0.530 | 0.012 | 1.000 | 5062MB |
| 2000 | 2800 | 0.954 | 0.812 | 0.527 | 0.010 | 1.000 | 5587MB |
| 3000 | 3800 | 0.954 | 0.813 | 0.530 | 0.010 | 1.000 | 6139MB |
| 4000 | 4800 | OOM | | | | | 7GB cap |
The facts don't interfere; the *solver* dies. One joint system over every stored key and near-miss is
quadratic in memory, and at ~10k constraint rows (4800 slots + 6400 near-misses) it no longer fits beside the
model in 7GB. So the collapse point on this box is a memory wall between 3800 and 4800 slots, not a quality
slope. (Housekeeping: the run ended with a clean OOM while I was in the middle of killing it for looking
stuck; I overwrote its json with a reconstruction from the log and then corrected the last row. Numbers for
0-3000 are identical to what the run printed.)
### E21 β€” held-out facts, once (results/E21_*.json) β€” FINAL
Plan: everything frozen. For each method: edit the 200 held-out facts on a fresh model, then all 1000
(800 eval + 200 held-out) in one batch. One run per method, no reruns.
| | 200 held-out: eff / para / hop / score | all 1000: eff / para / hop / score | all 1000: loc KL / QA / real nbr / bleed / ppl |
|---|---|---|---|
| no edit | 0.005 / 0.001 / 0.000 / 0.002 | 0.004 / 0.002 / 0.005 / 0.004 | 0 / 1.000 / 1.000 / 0.003 / 26.17 |
| in context, perfect retriever | 0.995 / 0.779 / 0.825 / 0.866 | 0.997 / 0.766 / 0.851 / 0.871 | 0 / 1.000 / 1.000 / 0.718 / 26.17 |
| LoRA | 1.000 / 0.936 / 0.360 / 0.765 | 1.000 / 0.924 / 0.280 / 0.735 | 0.600 / 0.706 / 0.306 / 0.654 / 44.40 |
| MEMIT | 0.980 / 0.767 / 0.350 / 0.699 | 0.945 / 0.631 / 0.300 / 0.625 | 0.064 / 0.956 / 0.800 / 0.377 / 28.09 |
| AlphaEdit | 0.980 / 0.777 / 0.365 / 0.707 | 0.949 / 0.641 / 0.296 / 0.629 | 0.077 / 0.912 / 0.744 / 0.378 / 28.41 |
| z* + gated neurons | 0.995 / 0.839 / 0.420 / 0.751 | 0.998 / 0.836 / 0.423 / 0.752 | 0.0005 / 1.000 / 0.983 / 0.318 / 26.18 |
| **Kibble** | 0.970 / 0.823 / 0.540 / 0.778 | 0.958 / 0.808 / 0.522 / 0.763 | 0.004 / 1.000 / 0.983 / 0.301 / 26.19 |
The held-out numbers land where the eval numbers did (Kibble 0.763 at 800 eval facts, 0.763 at 1000). The
ordering holds. The published editors keep sliding from 800 to 1000 (0.650 -> 0.625, 0.655 -> 0.629).
Peak VRAM for the 1000-fact Kibble build: 5448MB.
## Final
REPORT.md written, tables generated by methods/report_tables.py from the json files. 21 experiments used of
30 (E12 slot unused). STATE = DONE.
## After DONE: release packaging (not an experiment, no method change)
Asked afterwards to make figures and put it on Hugging Face. Nothing in evals/ or the method was touched.
- `methods/pack.py`: wrote all 1000 facts (800 eval + 200 held-out, E21 order, frozen E13 settings) and saved
the slots as `kibble_pack.safetensors` (39MB). `kibble.py` is a standalone loader. Check: stock model +
loader gives efficacy / paraphrase / multi-hop 0.958 / 0.808 / 0.522, identical to E21's all-1000 row.
Peak VRAM 5448MB. results/pack_check.json.
- Same script measured how hard each slot fires before the gate, for a figure. Gate opens on: own prompt
100%, paraphrases 97.2%, multi-hop prompt 99.1%, near-miss sharing the first word 6.1%, near-miss sharing
the LAST word 71.1%, ordinary wikitext tokens (loudest of 1000 slots) 0.36%. The last-word number is the
mechanism behind the 0.30 near-miss bleed and I had not looked at it this directly before: the slot keys on
the final name token, so a shared surname gets most of the way to the key. This is a diagnostic on eval +
held-out facts taken after everything was frozen; nothing was tuned on it.
- `methods/figs.py` draws figures/*.png from results/*.json. README.md is the model card.
- Assumption: I never saw the X post this was meant to match, so the figure set is my guess at the usual kit
(headline card, method diagram, scale and forgetting curves, damage, attacks, limits).