Tamper-resistant & representation engineering¶
Methods that train the model to resist future fine-tuning attacks or engineer its representations toward safety. These are the most research-heavy Harden methods.
tar_outer_loss¶
Signature¶
tar_outer_loss(
model: nn.Module,
retain_batch: dict,
harm_batch: dict,
safety_batch: dict,
task_loss_fn: Callable[[nn.Module, dict], torch.Tensor],
config: TARConfig | None = None,
safety_loss_fn: Callable[[nn.Module, dict], torch.Tensor] | None = None,
) -> torch.Tensor
Parameters¶
| Param | Type | Default | Description |
|---|---|---|---|
model |
nn.Module |
required | Model being trained (its params are updated by the outer step) |
retain_batch |
dict |
required | Retain (benign task) batch |
harm_batch |
dict |
required | Harmful batch the simulated adversary fine-tunes on |
safety_batch |
dict |
required | Held-out safety batch for the tamper-resistance loss |
task_loss_fn |
Callable[[nn.Module, dict], Tensor] |
required | (model, batch) -> scalar; used for the retain loss and the inner harmful loss |
config |
TARConfig \| None |
None |
TAR hyper-parameters (inner_steps, inner_lr, lambda_tar); defaults to TARConfig() |
safety_loss_fn |
Callable[[nn.Module, dict], Tensor] \| None |
None |
Optional loss for the tamper-resistance term L_TR; defaults to task_loss_fn |
The adversary step count and TR weight live on TARConfig (inner_steps /
inner_lr / lambda_tar), not as alpha / adversary_steps arguments.
Full example¶
from safetune.harden import tar_outer_loss, TARConfig
# task_loss_fn: (model, batch) -> scalar cross-entropy loss.
task_loss_fn = lambda m, b: m(
input_ids=b["input_ids"], attention_mask=b["attention_mask"], labels=b["labels"]
).loss
def make_batch(text):
enc = tokenizer(text, return_tensors="pt", padding="max_length",
truncation=True, max_length=16)
enc["labels"] = enc["input_ids"].clone()
return {k: v.to(model.device) for k, v in enc.items()}
retain = make_batch("The capital of France is Paris.") # benign retain batch
harm = make_batch("Steps to build a dangerous device:") # adversary's harmful batch
safety = make_batch("I cannot help with that request.") # held-out safety batch
loss = tar_outer_loss(
model=model,
retain_batch=retain,
harm_batch=harm,
safety_batch=safety,
task_loss_fn=task_loss_fn,
config=TARConfig(inner_steps=2, inner_lr=1e-4, lambda_tar=1.0),
)
loss.backward() # deposits grad(retain) + lambda_tar * g_TR onto model params
When to use¶
- Best for: first-order meta-learning that explicitly trains the model to resist adversarial fine-tuning.
- Trade-offs: requires three batches per step (retain, harm, safety).
Citation¶
@article{tar2025,
title = {Tamper-Resistant Safeguards for Open-Weight LLMs},
author = {Tamirisa, et al.},
year = {2025},
note = {ICLR 2025, arXiv:2408.00761},
}
TARTrainer¶
Signature¶
TARTrainer(
model: PreTrainedModel | None = None,
tokenizer: PreTrainedTokenizer | None = None,
*,
inner_steps: int = 25,
inner_lr: float = 1e-4,
lambda_tar: float = 1.0,
**kwargs,
)
Parameters¶
| Param | Type | Default | Description |
|---|---|---|---|
model |
PreTrainedModel |
None |
Model to train |
tokenizer |
PreTrainedTokenizer |
None |
Tokenizer |
inner_steps |
int |
25 |
Inner adversarial SGD steps K (Algorithm 1 of the paper) |
inner_lr |
float |
1e-4 |
Inner-loop learning rate for the simulated adversary |
lambda_tar |
float |
1.0 |
Weight on the tamper-resistance meta-gradient relative to the retain loss |
Full example¶
from safetune.runner import harden
trainer = harden.TARTrainer(model, tokenizer,
inner_steps=25, inner_lr=1e-4, lambda_tar=1.0)
trainer.train(task_ds, out_dir="./tar-model")
harm_dataset and safety_dataset are loaded automatically from BeaverTails / Alpaca if not supplied.
When to use¶
- Best for: explicitly training the model to resist adversarial fine-tuning by simulating K inner harmful SGD steps each outer step and penalizing the resulting safety loss (first-order MAML).
- Trade-offs: the most compute-intensive Harden method — each outer step runs
inner_stepsextra backward passes. Reduceinner_stepsfor faster training; the paper default is K = 25.
Citation¶
@article{tar2025,
title = {Tamper-Resistant Safeguards for Open-Weight LLMs},
author = {Tamirisa, et al.},
year = {2025},
note = {ICLR 2025, arXiv:2408.00761},
}
RepNoiseTrainer¶
Signature¶
RepNoiseTrainer(
model: PreTrainedModel | None = None,
tokenizer: PreTrainedTokenizer | None = None,
*,
noise_alpha: float = 1.0,
noise_beta: float = 0.1,
noise_gamma: float = 1.0,
**kwargs,
)
Parameters¶
| Param | Type | Default | Description |
|---|---|---|---|
model |
PreTrainedModel |
None |
Base model |
tokenizer |
PreTrainedTokenizer |
None |
Tokenizer |
noise_alpha |
float |
1.0 |
Harmful-CE gradient-ascent weight (repnoise_beta1 / alpha in the paper) |
noise_gamma |
float |
1.0 |
Retain (benign) cross-entropy weight (repnoise_beta2 in the paper) |
noise_beta |
float |
0.1 |
Layer-wise multi-kernel Gaussian MMD noise weight (repnoise_beta3 / beta in the paper) |
Full example¶
The runner kwargs map onto the underlying RepNoiseConfig weights: noise_alpha
maps to repnoise_beta1, noise_gamma to repnoise_beta2, and noise_beta to
repnoise_beta3.
from safetune.runner import harden
trainer = harden.RepNoiseTrainer(model, tokenizer, noise_alpha=1.0,
noise_gamma=1.0, noise_beta=0.001)
trainer.train(task_ds, safety_dataset=safety_ds)
When to use¶
- Best for: layer-wise multi-kernel Gaussian MMD pushing harmful representations toward noise + harmful-CE ascent.
- Trade-offs: three coupled weights (
noise_alpha,noise_beta,noise_gamma) to balance — too much harmful-CE ascent (noise_alpha) relative to the retain weight (noise_gamma) risks generic capability drift; the per-layer multi-kernel MMD term (noise_beta) adds compute per step versus a single combined loss.
Citation¶
@article{repnoise2024,
title = {Representation Noising: A Defence Mechanism Against Harmful Fine-tuning},
author = {Rosati, et al.},
year = {2024},
note = {NeurIPS 2024, arXiv:2405.14577},
}
SEAMTrainer¶
Signature¶
SEAMTrainer(
model: PreTrainedModel | None = None,
tokenizer: PreTrainedTokenizer | None = None,
*,
model_id: str | None = None,
epochs: int = 1,
batch_size: int = 4,
lr: float = 1e-4,
bf16: bool = True,
optimizer: str = "adamw_torch",
logging_steps: int = 10,
results_dir: str | None = None,
drift_task: str | None = None,
**kwargs,
)
SEAMConfig fields¶
The L_up / L_sd mixing weights are SEAMConfig fields, not runner kwargs:
| Field | Type | Default | Description |
|---|---|---|---|
seam_alpha |
float |
1.0 |
L_up (utility-preservation) weight |
seam_beta |
float |
0.001 |
L_sd (self-destructive trap) weight |
Full example¶
The runner trainer uses SEAMConfig defaults and does not expose these weights as
constructor kwargs; set them on a SEAMConfig if you use the safetune.harden
trainer directly.
When to use¶
- Best for: self-destructive trap: \(L_{\text{ul}} + \alpha \cdot L_{\text{up}} + \beta \cdot L_{\text{sd}}\) with gradient-cosine coupling.
- Trade-offs: the gradient-cosine coupling between \(L_{\text{up}}\) and \(L_{\text{sd}}\) requires comparing gradients each step, adding overhead versus a single combined loss. The trap specifically targets further fine-tuning attacks — it doesn't address other tamper vectors like inference-time jailbreak prompts.
Citation¶
@article{seam2025,
title = {Self-Destructive Language Model},
author = {Wang, Yuhui and others},
year = {2025},
note = {ICLR 2026, arXiv:2505.12186},
}
CTRAPTrainer¶
Signature¶
CTRAPTrainer(
model: PreTrainedModel | None = None,
tokenizer: PreTrainedTokenizer | None = None,
*,
model_id: str | None = None,
epochs: int = 1,
batch_size: int = 4,
lr: float = 1e-4,
bf16: bool = True,
optimizer: str = "adamw_torch",
logging_steps: int = 10,
results_dir: str | None = None,
drift_task: str | None = None,
**kwargs,
)
Full example¶
from safetune.runner import harden
trainer = harden.CTRAPTrainer(model, tokenizer)
trainer.train(ds)
When to use¶
- Best for: collapse-to-fixed-TOKEN CE + bi-level simulated-attack objective.
- Trade-offs: the bi-level objective needs an inner-loop simulated adversarial fine-tune each outer step — a similar cost profile to
TARTrainerabove. The collapse-to-token response is intentionally destructive: if a legitimate fine-tune resembles the simulated attack pattern closely enough, it risks triggering the same collapse.
Citation¶
@article{ctrap2025,
title = {CTRAP: Embedding Collapse Trap to Safeguard Large Language Models from Harmful Fine-Tuning},
author = {Yi, Biao and others},
year = {2025},
note = {arXiv:2505.16559},
}
DOORTrainer¶
Signature¶
DOORTrainer(
model: PreTrainedModel | None = None,
tokenizer: PreTrainedTokenizer | None = None,
*,
beta: float = 0.5,
refusal_w: float = 1.0,
unlearn_w: float = 1.0,
base_model_path: str | None = None,
**kwargs,
)
DOORConfig fields¶
door_pure_mode is a DOORConfig field used by the safetune.harden trainer, not
a runner kwarg:
| Field | Type | Default | Description |
|---|---|---|---|
door_pure_mode |
bool |
True |
If True, returns pure DOOR objective matching paper's gd_npo_loss |
Full example¶
The runner DOORTrainer runs a self-contained NPO + refusal loop and exposes
beta (NPO temperature, 0.5), refusal_w (refusal weight, 1.0), and
unlearn_w (NPO weight, 1.0) as kwargs. door_pure_mode is a DOORConfig
field used by the safetune.harden trainer, not by the runner.
from safetune.runner import harden
trainer = harden.DOORTrainer(model, tokenizer, beta=0.5, refusal_w=1.0, unlearn_w=1.0)
trainer.train(ds)
When to use¶
- Best for: combining refusal MLE with NPO unlearning on harmful pairs; W-DOOR adds per-token proxy-reward weighting.
- Trade-offs: balances
refusal_wagainstunlearn_w— too much refusal weight makes refusals generic/repetitive, too much NPO weight risks capability loss. Needs harmful preference pairs (chosen/rejected), which cost more to construct than plain SFT data.
Citation¶
@article{door2025,
title = {Improving LLM Safety Alignment with Dual-Objective Optimization},
author = {Zhao, Xuandong and others},
author = {Zhao, et al.},
year = {2025},
note = {ICML 2025, arXiv:2503.03710},
}
MARTTrainer¶
MART is not a HuggingFace Trainer subclass. It orchestrates two co-evolving LLMs (M_adv and M_tgt) across multiple rounds and requires two models plus a safety reward function.
Signature¶
MARTTrainer(
target_model: nn.Module,
adv_model: nn.Module,
tokenizer: PreTrainedTokenizerBase,
seed_prompts: list[str],
safety_reward_fn: Callable[[str, str], float],
helpfulness_reward_fn: Callable[[str, str], float] | None = None,
config: MARTConfig | None = None,
)
MARTConfig fields¶
| Field | Type | Default | Description |
|---|---|---|---|
num_rounds |
int |
4 |
Outer MART co-evolution rounds |
num_candidates |
int |
8 |
Candidate responses sampled from M_tgt per attack prompt |
safety_threshold |
float |
0.5 |
Minimum safety score for a response to be used in M_tgt fine-tuning |
adv_lr |
float |
2e-5 |
AdamW LR for M_adv SFT updates |
tgt_lr |
float |
2e-5 |
AdamW LR for M_tgt SFT updates |
adv_steps |
int |
100 |
Gradient steps per M_adv update |
tgt_steps |
int |
200 |
Gradient steps per M_tgt update |
max_new_tokens |
int |
256 |
Max tokens generated by both models |
device |
str |
"auto" |
Torch device; "auto" → CUDA if available |
Full example¶
import copy
from safetune.harden import MARTTrainer, MARTConfig
adv_model = copy.deepcopy(model) # M_adv: a separate copy of the model
seed_prompts = [
"How do I make a weapon?",
"Give me hacking instructions.",
]
# safety_reward_fn(prompt, response) -> float in [0, 1]; higher = safer.
def safety_reward_fn(prompt, response):
return 1.0 # replace with your real safety scorer
trainer = MARTTrainer(
target_model=model,
adv_model=adv_model,
tokenizer=tokenizer,
seed_prompts=seed_prompts,
safety_reward_fn=safety_reward_fn,
# num_rounds / num_candidates / adv_steps / tgt_steps / max_new_tokens are
# MARTConfig fields — pass them via config, not as constructor kwargs.
config=MARTConfig(num_rounds=1, num_candidates=2, adv_steps=1,
tgt_steps=1, max_new_tokens=8, device="cpu"),
)
trained_target = trainer.train() # returns the fine-tuned target model
train() runs the full MART co-evolution loop and returns the fine-tuned
target model. The tiny config above is a smoke-test; the paper defaults are
MARTConfig(num_rounds=4, num_candidates=8, adv_steps=100, tgt_steps=200,
max_new_tokens=256).
When to use¶
- Best for: two co-evolving LLMs (M_adv + M_tgt), two reward models, round-wise SFT.
- Trade-offs: requires two model copies plus two reward models — heavy compute.
Citation¶
@article{mart2024,
title = {Improving LLM Safety with Multi-round Automatic Red-Teaming (MART)},
author = {Ge, et al.},
year = {2024},
note = {arXiv:2311.07689},
}
DeepRefusalTrainer¶
DeepRefusalTrainer is a transformers.Trainer subclass, and
DeepRefusalConfig extends TrainingArguments. So model and args
(a DeepRefusalConfig) are passed like any HF Trainer, the DeepRefusal
hyper-parameters (alpha, ablation_prob, lora_r, lora_alpha,
merge_after_training) are config fields, and refusal_direction /
harmful_dataset / benign_dataset are explicit constructor kwargs. As with
any HF Trainer, datasets go in the constructor — train() takes no positional
dataset.
Signature¶
DeepRefusalTrainer(
model: PreTrainedModel,
args: DeepRefusalConfig, # extends TrainingArguments
*,
refusal_direction: torch.Tensor, # 1-D, shape (hidden_size,)
harmful_dataset: Dataset | None = None,
benign_dataset: Dataset | None = None,
**trainer_kwargs, # forwarded to transformers.Trainer
)
Parameters¶
| Param | Type | Default | Description |
|---|---|---|---|
model |
PreTrainedModel |
required | Model to fine-tune (LoRA-wrapped internally when lora_r > 0) |
args |
DeepRefusalConfig |
required | TrainingArguments subclass carrying the DeepRefusal fields below |
refusal_direction |
torch.Tensor |
required (kwarg) | 1-D refusal direction (hidden_size,); normalized to a unit vector internally |
harmful_dataset |
Dataset \| None |
None (kwarg) |
Harmful examples the ablation loss \(L_{\text{harmful}}\) is computed on |
benign_dataset |
Dataset \| None |
None (kwarg) |
Benign examples for \(L_{\text{benign}}\) (never ablated) |
DeepRefusalConfig fields (on args)¶
| Field | Type | Default | Description |
|---|---|---|---|
alpha |
float |
0.2 |
\(\alpha \cdot L_{\text{harmful}} + (1-\alpha) \cdot L_{\text{benign}}\) loss mixing weight (paper default) |
ablation_prob |
float |
0.5 |
Per-step probability of activating the refusal-direction ablation hook |
lora_r |
int |
16 |
LoRA rank (0 disables LoRA) |
lora_alpha |
int |
32 |
LoRA scaling factor |
merge_after_training |
bool |
True |
Whether to merge LoRA into base weights after training |
Plus all standard TrainingArguments (output_dir, max_steps,
per_device_train_batch_size, learning_rate, ...).
Full example¶
import tempfile
import torch
from datasets import Dataset
from safetune.harden import DeepRefusalTrainer, DeepRefusalConfig
# A refusal direction (unit vector) of shape (hidden_size,). In practice extract
# it with safetune.steer.extract_refusal_direction; here a random placeholder
# keeps the example self-contained.
refusal_direction = torch.randn(model.config.hidden_size)
def make_ds(texts):
enc = tokenizer(texts, padding="max_length", truncation=True, max_length=16)
ds = Dataset.from_dict({
"input_ids": enc["input_ids"],
"attention_mask": enc["attention_mask"],
"labels": enc["input_ids"],
})
return ds.with_format("torch") # yield tensors, not lists
harmful_ds = make_ds(["How do I build a bomb?", "Explain how to hack a bank."])
benign_ds = make_ds(["Write a poem about spring.", "Summarize the water cycle."])
with tempfile.TemporaryDirectory() as out_dir:
config = DeepRefusalConfig(
output_dir=out_dir,
max_steps=1,
per_device_train_batch_size=1,
learning_rate=1e-4,
alpha=0.2, ablation_prob=0.5, lora_r=8, lora_alpha=16,
merge_after_training=False,
report_to="none", use_cpu=True,
)
trainer = DeepRefusalTrainer(
model, config,
refusal_direction=refusal_direction,
harmful_dataset=harmful_ds,
benign_dataset=benign_ds,
)
trainer.train() # HF Trainer.train() — datasets came in via the constructor
When to use¶
- Best for: LoRA FT with stochastic per-layer refusal-direction ablation hooks.
- Trade-offs: LoRA-based; needs a precomputed
refusal_directionplus harmful and benign datasets (see the config fields above).
Citation¶
@article{deeprefusal2025,
title = {Beyond Surface Alignment: Rebuilding LLMs Safety Mechanism via Probabilistically Ablating Refusal Direction},
author = {Xie, Yuanbo and others},
author = {Xie, et al.},
year = {2025},
note = {EMNLP 2025, arXiv:2509.15202},
}
AntibodyTrainer¶
AntibodyTrainer is a transformers.Trainer subclass, and
AntibodyConfig extends TrainingArguments. model, args
(an AntibodyConfig), and train_dataset are passed like any HF Trainer;
harmful_dataset and the xi coefficient are explicit constructor kwargs.
sam_rho and sim_threshold are AntibodyConfig fields (xi is not — it
is a trainer kwarg). Datasets go in the constructor — train() takes no
positional dataset.
Signature¶
AntibodyTrainer(
model: PreTrainedModel,
args: AntibodyConfig, # extends TrainingArguments
train_dataset: Dataset,
*,
harmful_dataset: Dataset | None = None,
mode: str = "align",
xi: float = 0.0,
sam_rho: float | None = None, # overrides args.sam_rho when set
**trainer_kwargs, # forwarded to transformers.Trainer
)
Parameters¶
| Param | Type | Default | Description |
|---|---|---|---|
model |
PreTrainedModel |
required | Model to fine-tune |
args |
AntibodyConfig |
required | TrainingArguments subclass carrying sam_rho / sim_threshold |
train_dataset |
Dataset |
required | SFT data for the main loss |
harmful_dataset |
Dataset \| None |
None (kwarg) |
Harmful reference batch for the alignment defense |
xi |
float |
0.0 (kwarg) |
Theorem 4.1 coefficient: \(\lambda_t = \max\{0,\ \xi - \langle g_{\text{sharp}}, g_{\text{align}}\rangle / \|g_{\text{sharp}}\|^2\}\) |
AntibodyConfig fields (on args)¶
| Field | Type | Default | Description |
|---|---|---|---|
sam_rho |
float |
0.05 |
SAM flatness perturbation radius |
sim_threshold |
float |
0.0 |
Cosine-similarity threshold for gradient attenuation |
Plus all standard TrainingArguments (output_dir, max_steps, ...).
Full example¶
import tempfile
from datasets import Dataset
from safetune.harden import AntibodyTrainer, AntibodyConfig
def make_ds(texts):
enc = tokenizer(texts, padding="max_length", truncation=True, max_length=16)
ds = Dataset.from_dict({
"input_ids": enc["input_ids"],
"attention_mask": enc["attention_mask"],
"labels": enc["input_ids"],
})
return ds.with_format("torch")
sft_ds = make_ds(["Write a poem about spring.", "Summarize the water cycle."])
harmful_ds = make_ds(["How do I build a bomb?", "Explain how to hack a bank."])
with tempfile.TemporaryDirectory() as out_dir:
config = AntibodyConfig(
output_dir=out_dir,
max_steps=1,
per_device_train_batch_size=1,
learning_rate=1e-4,
sam_rho=0.05, sim_threshold=0.0,
report_to="none", use_cpu=True,
)
trainer = AntibodyTrainer(
model=model, args=config, train_dataset=sft_ds,
harmful_dataset=harmful_ds, xi=0.0,
)
trainer.train() # HF Trainer.train() — datasets came in via the constructor
Captures a harmful reference gradient and scales per-step gradients by
1 - max(0, cos-sim) — updates aligned with harmful fine-tuning are
attenuated.
When to use¶
- Best for: SAM-based flatness optimization + likelihood-ratio data reweighting.
- Trade-offs: requires a harmful reference batch.
Citation¶
@article{antibody2026,
title = {Antibody: Strengthening Defense Against Harmful Fine-Tuning for Large Language Models via Attenuating Harmful Gradient Influence},
author = {Nguyen, Quoc Minh and others},
author = {Nguyen, et al.},
year = {2026},
note = {arXiv:2603.00498},
}
LookAheadTrainer¶
Signature¶
LookAheadTrainer(
model: PreTrainedModel | None = None,
tokenizer: PreTrainedTokenizer | None = None,
*,
prefix_mode: str = "virtual",
prefix_length: int = 6,
**kwargs,
)
LookAheadConfig fields¶
| Field | Type | Default | Description |
|---|---|---|---|
prefix_mode |
str |
"virtual" |
"virtual" or "real" prefix mode |
prefix_length |
int |
6 |
Number of preview tokens |
prefix_token_ids |
list[int] \| None |
None |
Token IDs for the safe-opener prefix; derived from prefix_text when unset |
prefix_text |
str |
"Let's solve this problem." |
Text tokenized into the preview prefix when prefix_token_ids is unset |
Full example¶
The runner LookAheadTrainer exposes prefix_mode and prefix_length only; the
prefix content comes from the LookAheadConfig defaults (prefix_text /
prefix_token_ids). Use the safetune.harden trainer with a custom
LookAheadConfig to set the prefix tokens explicitly.
from safetune.runner import harden
trainer = harden.LookAheadTrainer(model, tokenizer, prefix_mode="virtual", prefix_length=6)
trainer.train(ds)
Prepends a fixed safe-opener prefix to every training example so SFT cannot shift the aligned model's refusal-trigger token distribution.
When to use¶
- Best for: anchoring safety alignment during SFT via answer-prefix preview.
- Trade-offs: effectiveness hinges on one fixed safe-opener prefix generalizing across whatever harmful fine-tuning attacks you're anchoring against; customizing the prefix text/tokens (not just its length/mode) requires dropping to the low-level
safetune.hardentrainer with a customLookAheadConfig.