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:

MethodLoss shapeNeeds reference modelNotes
NPO (Negative Preference Optimization)DPO-shaped, negative-onlyyesPair with a retain set; NPO and SimNPO warn loudly without one
SimNPOLength-normalized NPOnoFaster on long sequences
RMU (Representation Misdirection Unlearning)Residual-stream noise on forget inputsyesBest 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.

yaml
# 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_model

data.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_decay and max_grad_norm, and it accumulates batch_size x gradient_accumulation_steps examples per optimizer update. Before this, the loop used a bare torch.optim.AdamW with no scheduler and no gradient clipping, took one example per update, and ignored those settings.
  • batch_size: auto resolves to 1 (not to a memory estimate), so the same config makes the same number of updates on any card.
  • initial_loss and final_loss remain 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 output outside 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 ranWhat was differentWhat to do
task: unlearn with optimizer, scheduler, warmup_ratio, weight_decay, max_grad_norm or gradient_accumulation_steps setThe 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

bash
soup eval unlearning <run-id> --benchmark tofu --evidence ./unlearn_evidence.json --output ./unlearn_report.json

Soup 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:

json
{
  "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:

bash
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

bash
soup edit diff before-run after-run \
  --before-model ./model_before --after-model ./model_after \
  --probes ./probes.jsonl --top-k 10

Runs 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 separate auto_switch_at of 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-diff looks at Sparse Autoencoder feature movement, and soup probe sleeper applies 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.