Unlearning & Knowledge Edit (v0.61.0)
GDPR right-to-be-forgotten as a first-class task. Surgical fact patches without retraining.
task: unlearn — remove specific data, keep the rest
Three methods, all wired as live trainers:
| Method | Loss shape | Needs reference model | Notes |
|---|---|---|---|
| NPO (Negative Preference Optimization) | DPO-shaped, negative-only | yes | Pair with a retain set; NPO and SimNPO warn loudly without one |
| SimNPO | Length-normalized NPO | no | Faster on long sequences |
| RMU (Representation Misdirection Unlearning) | Residual-stream noise on forget inputs | yes | Best for concept-level removal |
NPO and RMU each load a second, frozen copy of the base model next to the LoRA-wrapped policy (RMU runs that copy over each retain input when a retain set is given), so they cost a second full-precision model in memory. Only SimNPO is reference-free.
# soup.yaml
base: meta-llama/Llama-3.1-8B
task: unlearn
training:
epochs: 3
unlearn_method: simnpo
unlearn_alpha: 0.7
data:
train: ./private_data_to_remove.jsonl
forget_set: ./private_data_to_remove.jsonl
retain_set: ./general_knowledge.jsonl
output: ./unlearned_modeldata.train is required by the schema for every task, and soup train still loads and format-checks that file before the unlearn trainer starts (a missing path stops the run with Data file not found), but the unlearn trainer itself never uses its rows; it reads forget_set and retain_set. unlearn_alpha (0.0–10.0) is the retain-set weight, and it needs unlearn_method to be set. It defaults to unset, and an unset value resolves to 1.0 in the trainer, not to 0.5. An explicit unlearn_alpha: 0.0 is inside the documented range and validates, but the trainer treats it as falsy and also uses 1.0, so it cannot switch the retain term off. forget_set is required; retain_set is optional but strongly recommended for NPO.
Sequence length, batch and optimizer settings (v0.76.0)
Behavior break. The unlearn trainer now reads data.max_length for every forget and retain row. Through v0.75.2 it tokenised them to a hard-coded 256 tokens. data.max_length defaults to 2,048, so an existing configuration can use more memory and take longer: set data.max_length: 256 to keep the old limit (#805).
- It honours
optimizer,scheduler,warmup_ratio,weight_decayandmax_grad_norm, and it accumulatesbatch_sizexgradient_accumulation_stepsexamples per optimizer update. Before this, the loop used a baretorch.optim.AdamWwith no scheduler and no gradient clipping, took one example per update, and ignored those settings. batch_size: autoresolves to 1 (not to a memory estimate), so the same config makes the same number of updates on any card.initial_lossandfinal_lossremain per-example losses on their original scale, and the 2,000-step budget counts examples, not optimizer updates.- The output directory is checked before any model is loaded: an
outputoutside the working directory, or a symlink, stops the run first (it used to load the policy and the frozen reference copy and run every step before refusing) (#1445).
Settings that no unlearn run applies are refused or flagged when the config loads, rather than ignored: training.eval_steps (unlearning trains on forget_set and retain_set and never evaluates a validation split), training.loraplus_lr_ratio, loss_watchdog, loss_spike_recovery and grad_accum_auto_tune (the trainer attaches no live callback), and backend: unsloth. An explicit training.quantization: 4bit or 8bit prints a warning and is ignored, since the trainer never quantises the base; upstream says v0.77 will refuse it. soup train --resume and --hf-resume on task: unlearn exit non-zero with a message naming the task. training.moe_lora is read by unlearn since v0.76.0 (#1151).
(Since closed in v0.76.0: a finished task: unlearn run used to end with KeyError: 'duration' in the "Training Complete!" panel, after the adapter was saved and the tracker row written, because the wrapper returned only duration_secs (#1529). If you saw that traceback, the run itself had finished.)
Re-run this
| If you ran | What was different | What to do |
|---|---|---|
task: unlearn with optimizer, scheduler, warmup_ratio, weight_decay, max_grad_norm or gradient_accumulation_steps set | The trainer did not read them: one example per update, a bare AdamW, a fixed 256-token limit (#805) | Settings you wrote for these were not applied, so a re-run now uses them. Note the new 2,048-token default, or set data.max_length: 256 to reproduce the old limit |
soup eval unlearning — score TOFU / MUSE / WMDP evidence
soup eval unlearning <run-id> --benchmark tofu --evidence ./unlearn_evidence.json --output ./unlearn_report.jsonSoup does not run the TOFU, MUSE or WMDP benchmarks. The command loads no model. <run-id> is only a label, and the scores come from an evidence JSON that you produce with your own harness:
{
"forget_quality": {"pre_loss": 0.5, "post_loss": 2.5},
"model_utility": {"pre_acc": 0.70, "post_acc": 0.68},
"priv_leak": {"mia_auc": 0.52}
}Three metrics are scored, and --benchmark (tofu by default, or muse or wmdp) labels the report:
- Forget Quality — the pre/post forget-set loss change.
- Model Utility — retain accuracy preserved.
- PrivLeak — membership-inference AUC, scored by its distance from 0.5.
Scores roll up into an OK / MINOR / MAJOR verdict mirroring v0.56 diagnose, and the command exits 2 on MAJOR. --output writes the rendered report, and --attach-to-registry <id> attaches that file to a registry entry as an eval_results artifact. Since v0.76.0 a requested attach is part of the command's success (#1683): --attach-to-registry without --output is refused before anything runs (exit 2), and a failed attach (unknown or ambiguous entry, unreadable registry) prints an error and exits 1 with the report left on disk. (Through v0.75.2 the attach raised a TypeError on every run, was printed as a warning and exited 0, and without --output it was skipped with a notice.)
A metric you leave out scores as a neutral OK, and with no --evidence at all the whole report is OK: every row reads "no evidence supplied; score is neutral default", Overall: OK, exit 0. That holds for --benchmark muse and wmdp as well. An all-green report without evidence means nothing was measured.
soup edit set — ROME / MEMIT / AlphaEdit
Surgically patch facts without retraining:
soup edit set --base meta-llama/Llama-3.1-8B-Instruct \
--method rome \
--subject "The capital of France is" \
--target "Lyon" \
--plan-only- ROME — Rank-One Model Editing, targets MLP layers (Meng 2022).
- MEMIT — Mass-Edit Memory in a Transformer (Meng 2023).
- AlphaEdit — gradient-based fact patching.
v0.61.0 shipped the plan / validation surface; live application landed in v0.71.9 — covariance-free rank-1 ROME/MEMIT/AlphaEdit kernels apply the edit (on a tiny model a ROME edit moved P("Lyon" | "The capital of France is") from 0.0016 → 0.96), guarded by a persistent SQLite EditGovernor. GRACE is the fourth method value, --method grace (the --help text still lists only the other three); see the GRACE section on RAG & steering.
--registry-id <id> records the saved result on a Registry entry (an id, id prefix or name:tag) and needs --output; without it the command exits 2 before the model loads (--registry-id needs --output (nothing saved to attach).). The Registry stores one hashed file per artifact and never a directory, so the weight files save_pretrained wrote (model.safetensors, its numbered shards, or pytorch_model*.bin) are attached as edited_model artifacts, and grace_codebook.json as a grace_codebook artifact for --method grace. A requested attach is part of the command's success (#1686): if it fails (unknown or ambiguous entry, unreadable registry, no weight file), the command prints Error: could not attach to registry: ... and exits 1, with the edited model left on disk.
(Since closed in v0.76.0: through v0.75.2 the command handed the Registry the output directory, which the Registry refuses, so every run printed Could not attach to Registry: artifact not found: <dir> as a warning, exited 0 and recorded nothing.)
soup edit diff — which probe answers changed
soup edit diff before-run after-run \
--before-model ./model_before --after-model ./model_after \
--probes ./probes.jsonl --top-k 10Runs each probe prompt (JSONL rows with a prompt field, up to 1,000) through both models with greedy decoding and lists the prompts whose completion changed, changed rows first, up to --top-k (1 to 100). before-run and after-run are labels recorded in the report; they are not Registry lookups (the --help text now calls them "Label or run id ... (metadata)"). The command refuses every partial combination, exiting 2 and naming the missing flag: --probes needs both --before-model and --after-model; one model flag needs the other; and both models need --probes (Cannot build diff: --probes is required when both --before-model and --after-model are given; ...). A probe file with no rows, or none carrying a prompt, is refused too. With neither model nor probes it prints a single no probes supplied row and Total probes: 0; 0 changed. and exits 0. There is no citation visualizer. -o writes the report as JSON.
(Since closed in v0.76.0 (#818): through v0.75.2 these combinations printed a placeholder table, or wrote a report with total_probes: 0, and exited 0, which read as "the edit changed nothing" when nothing had been measured.)
Sequential Edit Governor
Consulted by soup edit set (on by default; --no-governor opts out) and by the GRACE codebook edit. Nothing in soup train calls it. The state is a SQLite store per base model (SOUP_EDIT_GOVERNOR_DB moves it), and it fails open only for a store path that is refused or cannot be created: then the edit goes ahead with the governor disabled and a dim "Governor disabled" note. A corrupt database is not caught: soup edit set stops with DatabaseError: file is not a database and exit 1.
- Edit count overflow — refuses further edits on a base model once 50 have been recorded (
max_sequential_edits, exit 2). A separateauto_switch_atof 10 exists in the library as a ROME to AlphaEdit recommendation, but no command acts on it. - Norm blowup — each edit's measured Frobenius delta is classed OK, WARN (at 1.0 or more) or BLOWUP (at 5.0 or more). After a BLOWUP the governor refuses further edits on that base. It cannot stop the edit that caused the blowup, because the delta is only known after the edit has run.
Numbers
+193 new tests in v0.61.0 (9446 → 9571). Security: 5 HIGH, 11 MEDIUM, 11 LOW fixes.
See also
- Diagnose — pair unlearning with the 7-probe report card to verify the forget actually happened.
- RAG & steering — v0.62 control-vector steering, a soft alternative to surgical edits.
- Post-train x-rays (v0.66) —
soup probe sae-difflooks at Sparse Autoencoder feature movement, andsoup probe sleeperapplies a linear defection probe (its bundled weights are synthetic placeholders, so bring your own with--weights), a mechanistic view beyond Forget Quality / Model Utility / PrivLeak benchmark scores.
Soup is free and Apache-2.0. If it saved you a training run, starring the repo costs nothing and helps most. You can also fund the GPU time behind the work a 4 GB laptop cannot reach.