SimDPO — Reference-Free DPO Unlearning¶
Uses the model's own length-normalised log-likelihood as the implicit reward. No frozen reference model needed. Pairs harmful (rejected) completions with safe-refusal (chosen) completions and maximises the reward gap between them.
Ref: Meng et al., "SimPO: Simple Preference Optimization with a Reference-Free Reward," NeurIPS 2024; adapted for unlearning.
Signature¶
SimDPOTrainer(
model: nn.Module,
*,
variant: str = "simdpo_retain",
beta: float = 0.1,
epochs: int = 5,
lr: float = 1e-5,
retain_coeff: float = 1.0,
safe_refusal: str = "I'm sorry, but I'm unable to assist with that request.",
)
Quick start¶
from safetune.runner import unlearn
trainer = unlearn.SimDPOTrainer(model)
unlearned = trainer.unlearn(forget=forget_batches, retain=retain_batches)
Data note: SimDPO trains on
{chosen, rejected}preference pairs. If you pass raw harmfulforgetbatches (plus atokenizer, or none — it loads one from the model id),unlearn()builds the pairs for you viatrainer.make_simdpo_pairs(), pairing each harmful completion (rejected) with thesafe_refusaltemplate (chosen). To build them yourself, callpairs = trainer.make_simdpo_pairs(harmful_batches, tokenizer)and passforget=pairs.
SimDPOUnlearnConfig¶
| Field | Type | Default | Description |
|---|---|---|---|
variant |
str |
"simdpo_retain" |
"simdpo" (forget-only) or "simdpo_retain" (+ retain CE) |
beta |
float |
0.1 |
Temperature on the reward gap; lower = softer boundary |
retain_coeff |
float |
1.0 |
Weight on retain CE (for simdpo_retain variant) |
epochs |
int |
5 |
Full passes over forget iterable |
lr |
float |
1e-5 |
AdamW learning rate |
weight_decay |
float |
0.01 |
Weight decay |
max_steps |
int \| None |
None |
Hard cap on optimizer steps |
Full example¶
from safetune.runner import unlearn
from safetune.unlearn import make_simdpo_pairs
# Build paired batches: each batch has 'chosen' (refusal) and 'rejected' (harmful) keys
paired_batches = make_simdpo_pairs(
harmful_batches=harmful_batches,
refusal_response="I cannot help with that.",
tokenizer=tokenizer,
max_len=256,
)
trainer = unlearn.SimDPOTrainer(
model,
variant="simdpo_retain",
beta=0.1,
retain_coeff=1.0,
epochs=5,
lr=1e-5,
)
unlearned = trainer.unlearn(forget=paired_batches, retain=retain_batches)
ckpt_path = trainer.save_checkpoint(unlearned, tokenizer, "simdpo_ckpt")
metrics = trainer.eval("simdpo", ckpt_path)
trainer.save_results(metrics, variant="simdpo_retain")
Low-level API — simdpo_unlearn()¶
| Parameter | Type | Default | Description |
|---|---|---|---|
model |
nn.Module |
required | Updated in place |
forget_batches |
Iterable[dict] |
required | Paired dicts with chosen and rejected keys (use make_simdpo_pairs) |
retain_batches |
Iterable[dict] \| None |
None |
Required for simdpo_retain |
config |
SimDPOUnlearnConfig \| None |
None |
Config instance; constructor kwargs used if None |
simdpo_forget_loss()¶
from safetune.unlearn import simdpo_forget_loss
loss = simdpo_forget_loss(chosen_logp, rejected_logp, beta=0.1, gamma=0.0)
# -F.logsigmoid(beta * (chosen_logp - rejected_logp) - gamma).mean()
chosen_logp and rejected_logp are length-normalised log-likelihoods
(sum of token log-probs divided by sequence length).
make_simdpo_pairs()¶
from safetune.unlearn import make_simdpo_pairs
paired = make_simdpo_pairs(
harmful_batches=harmful_batches,
refusal_response="I cannot help with that.",
tokenizer=tokenizer,
max_len=256, # max tokens per sequence (default 256)
)
Tokenizes each harmful completion as the rejected entry and the canned
refusal_response as the chosen entry. Returns a list of batch dicts, each
with top-level keys chosen and rejected; each of those is a sub-dict with
input_ids, attention_mask, and labels.
When to use¶
- Best for: memory-constrained settings — no frozen reference model forward passes needed.
"simdpo"— no retain data; forget-only."simdpo_retain"— with retain data; preserves utility via CE.- Compare to NPO: SimDPO uses length-normalised log-likelihoods (avoids length-bias); NPO uses raw log-ratios against a frozen model. SimDPO trains on preference pairs (built for you from raw forget batches); NPO works with flat forget batches directly.