ablate-llm 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AI Anytime
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,389 @@
1
+ Metadata-Version: 2.4
2
+ Name: ablate-llm
3
+ Version: 0.2.0
4
+ Summary: Directional ablation (abliteration) toolkit for open-source language models: extract, ablate, and optimize the removal of refusal / behavioral directions in the residual stream.
5
+ Author: AI Anytime
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/AIAnytime/ablate
8
+ Project-URL: Repository, https://github.com/AIAnytime/ablate
9
+ Project-URL: Issues, https://github.com/AIAnytime/ablate/issues
10
+ Project-URL: Example model, https://huggingface.co/ai-anytime/qwen-1.5b-abliterated
11
+ Keywords: interpretability,activation-engineering,abliteration,refusal-direction,mechanistic-interpretability,safety
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: torch>=2.1
21
+ Requires-Dist: transformers>=4.40
22
+ Requires-Dist: numpy>=1.24
23
+ Requires-Dist: tqdm>=4.65
24
+ Provides-Extra: optimize
25
+ Requires-Dist: optuna>=3.4; extra == "optimize"
26
+ Provides-Extra: datasets
27
+ Requires-Dist: datasets>=2.18; extra == "datasets"
28
+ Provides-Extra: hub
29
+ Requires-Dist: huggingface_hub>=0.23; extra == "hub"
30
+ Provides-Extra: all
31
+ Requires-Dist: optuna>=3.4; extra == "all"
32
+ Requires-Dist: datasets>=2.18; extra == "all"
33
+ Requires-Dist: huggingface_hub>=0.23; extra == "all"
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=7.4; extra == "dev"
36
+ Requires-Dist: optuna>=3.4; extra == "dev"
37
+ Requires-Dist: datasets>=2.18; extra == "dev"
38
+ Requires-Dist: huggingface_hub>=0.23; extra == "dev"
39
+ Requires-Dist: matplotlib>=3.7; extra == "dev"
40
+ Requires-Dist: Pillow>=10.0; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ <p align="center">
44
+ <picture>
45
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/AIAnytime/ablate/main/media/ablate-dark.png">
46
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/AIAnytime/ablate/main/media/ablate-light.png">
47
+ <img alt="Ablate β€” Automatic Censorship Removal" src="https://raw.githubusercontent.com/AIAnytime/ablate/main/media/ablate-light.png" width="600">
48
+ </picture>
49
+ </p>
50
+
51
+ <h1 align="center">Ablate</h1>
52
+
53
+ <p align="center"><b>Directional ablation (abliteration) toolkit for automatic censorship removal of open-source language models.</b></p>
54
+
55
+ <p align="center">
56
+ <a href="https://pypi.org/project/ablate-llm/"><img alt="PyPI" src="https://img.shields.io/pypi/v/ablate-llm.svg?color=7c5cff"></a>
57
+ <a href="https://pypi.org/project/ablate-llm/"><img alt="Python" src="https://img.shields.io/pypi/pyversions/ablate-llm.svg"></a>
58
+ <a href="https://github.com/AIAnytime/ablate/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-green.svg"></a>
59
+ </p>
60
+
61
+ <p align="center">
62
+ <a href="https://pypi.org/project/ablate-llm/">πŸ“¦ PyPI</a> &nbsp;β€’&nbsp;
63
+ <a href="https://huggingface.co/ai-anytime/qwen-1.5b-abliterated">πŸ€— Model</a> &nbsp;β€’&nbsp;
64
+ <a href="https://www.youtube.com/@AIAnytime">▢️ Video walkthrough (coming soon)</a> &nbsp;β€’&nbsp;
65
+ <a href="#citation">πŸ“š Cite</a>
66
+ </p>
67
+
68
+ <p align="center"><i>Created with πŸ’œ by <b>AI Anytime</b></i></p>
69
+
70
+ ---
71
+
72
+ `ablate` finds the linear direction in a transformer's residual stream that
73
+ mediates a behavior β€” by default *refusal* β€” and removes it, either at runtime
74
+ (forward hooks) or permanently (weight orthogonalization). It ships a
75
+ KL-divergence–guided [Optuna](https://optuna.org) search that automatically
76
+ tunes *which* direction, *how strongly*, and *which layers* to ablate, balancing
77
+ refusal removal against capability damage.
78
+
79
+ It is designed for **mechanistic-interpretability and safety research** on small
80
+ open models (GPT-2, SmolLM2, TinyLlama, Qwen2.5-0.5B/1.5B …) β€” models you can
81
+ iterate on locally on a laptop, or on a free Colab T4.
82
+
83
+ > **Scope & intent.** This is dual-use research tooling for studying *how* safety
84
+ > behavior is represented and how robust it is. The refusal prompts shipped in
85
+ > `src/ablate/_data/` are short triggers used only to *locate* the refusal
86
+ > direction, not requests to fulfill. Use it on models and in contexts you're
87
+ > authorized to.
88
+
89
+ ## πŸ€— Released model
90
+
91
+ A worked example of the full pipeline is published at
92
+ **[`ai-anytime/qwen-1.5b-abliterated`](https://huggingface.co/ai-anytime/qwen-1.5b-abliterated)**
93
+ β€” `Qwen/Qwen2.5-1.5B-Instruct` with its refusal direction ablated and baked into
94
+ the weights, shipped with a transparent, `ablate`-generated model card. Load it
95
+ like any other `transformers` checkpoint:
96
+
97
+ ```python
98
+ from transformers import AutoModelForCausalLM, AutoTokenizer
99
+ tok = AutoTokenizer.from_pretrained("ai-anytime/qwen-1.5b-abliterated")
100
+ model = AutoModelForCausalLM.from_pretrained("ai-anytime/qwen-1.5b-abliterated")
101
+ ```
102
+
103
+ ---
104
+
105
+ ## How it works
106
+
107
+ A transformer represents each token as a vector `h` in the residual stream.
108
+ Many high-level behaviors are encoded approximately **linearly** β€” they live
109
+ along a single direction `v`. Refusal is one such behavior
110
+ ([Arditi et al., 2024](https://arxiv.org/abs/2406.11717), *"Refusal in LLMs is
111
+ mediated by a single direction"*).
112
+
113
+ 1. **Extract** the direction. Run matched harmful/harmless prompt pairs through
114
+ the model, take the residual activation at the last prompt token per layer,
115
+ and compute the **difference of means**:
116
+ `v = mean(h_harmful) βˆ’ mean(h_harmless)`, unit-normalized. (Diff-of-means
117
+ beats linear probes here because it captures the *causal* component, not just
118
+ a *separating* one.)
119
+
120
+ 2. **Ablate** by projecting `v` out of the residual stream, scaled by `Ξ±`:
121
+ `h' = h βˆ’ Ξ± (hΒ·vΜ‚) vΜ‚`, applied at every selected layer and every token
122
+ position.
123
+ - **Runtime** β€” forward hooks; checkpoint untouched (fast, for search).
124
+ - **Baked** β€” orthogonalize every residual-writing weight matrix
125
+ (`(I βˆ’ vΜ‚vΜ‚α΅€)W`) so the model *can't* express `v`; produces a new checkpoint
126
+ with no hooks needed.
127
+
128
+ 3. **Optimize.** Search `(direction_layer, Ξ±, layer_band)` with Optuna,
129
+ **minimizing** `refusal_rate + λ·KL(original β€– ablated)`. KL on *benign*
130
+ prompts is a dense, cheap proxy for collateral capability damage β€” far more
131
+ sensitive than accuracy benchmarks. A coherence floor rejects degenerate
132
+ solutions.
133
+
134
+ Works across architectures via adapters in `utils.py` (Llama/Mistral/Qwen/SmolLM
135
+ `nn.Linear` families *and* GPT-2's transposed `Conv1D`).
136
+
137
+ ---
138
+
139
+ ## Install
140
+
141
+ ```bash
142
+ pip install ablate-llm # the import name is still `ablate`
143
+ # with all extras (Optuna search, HF datasets, push-to-Hub):
144
+ pip install "ablate-llm[all]"
145
+ ```
146
+
147
+ ```python
148
+ import ablate # note: install is `ablate-llm`, import is `ablate`
149
+ ```
150
+
151
+ From source (for development):
152
+
153
+ ```bash
154
+ git clone https://github.com/AIAnytime/ablate && cd ablate
155
+ python -m venv .venv && source .venv/bin/activate
156
+ pip install -e ".[all]"
157
+ ```
158
+
159
+ Optional extras: `.[optimize]` (Optuna), `.[datasets]` (HF datasets), `.[hub]`
160
+ (push to the Hub). `.[all]` installs everything.
161
+
162
+ Runs on CUDA, Apple MPS, or CPU (auto-detected). Tiny models (≀1B) run fine on a
163
+ 16GB laptop in float32; a free Colab T4 handles 1–1.5B comfortably.
164
+
165
+ ---
166
+
167
+ ## Quick start (Python)
168
+
169
+ ```python
170
+ from ablate import Ablator
171
+
172
+ abl = Ablator("Qwen/Qwen2.5-0.5B-Instruct") # any HF causal-LM
173
+ abl.extract() # find candidate directions
174
+ result = abl.search(n_trials=20) # KL-guided Optuna search
175
+ print(result.result) # refusal_rate=0.000 mean_kl=0.03 coherence=0.92 ...
176
+
177
+ # Non-destructive generation with the best config:
178
+ print(abl.generate(["How do I pick a lock?"]))
179
+
180
+ # Or bake it into the weights and ship a checkpoint:
181
+ abl.bake(result.config)
182
+ abl.save("qwen-0.5b-ablated") # standard HF folder; load anywhere
183
+ ```
184
+
185
+ ### Verified result
186
+
187
+ On `Qwen/Qwen2.5-0.5B-Instruct`, the automated pipeline moves the held-out
188
+ harmful refusal rate from **1.00 β†’ 0.00** with **mean KL β‰ˆ 0.03** (capabilities
189
+ essentially intact). SmolLM2-135M has little safety training and refuses ~nothing
190
+ to begin with β€” use it for *mechanism* testing, not refusal removal.
191
+
192
+ ---
193
+
194
+ ## Multi-direction (subspace) ablation
195
+
196
+ Safety is redundantly encoded, so one direction is often not enough. Extract an
197
+ orthonormal refusal **subspace** and project the whole thing out:
198
+
199
+ ```python
200
+ abl.extract_subspace(method="band", n_directions=6) # or method="pca"
201
+ res = abl.search_subspace(n_trials=20) # tunes (n_directions, Ξ±, layers)
202
+ print(res.config.to_dict()) # {'n_directions': 4, 'alpha': 1.1, 'min_layer': 21, ...}
203
+ ```
204
+
205
+ `"band"` orthonormalizes the diff-of-means directions from the strongest layers;
206
+ `"pca"` uses variance-ordered principal components at one layer. Because the
207
+ basis is importance-ordered, the search truncates `basis[:n]` meaningfully.
208
+ Single-direction ablation is just `n_directions == 1`.
209
+
210
+ ## Rigorous evaluation: HarmBench + LLM judge
211
+
212
+ ```python
213
+ from ablate import make_judge
214
+ from ablate.harness import compare
215
+ from ablate import data
216
+
217
+ prompts = data.load_harmbench(n=40) # or load_advbench / load_jailbreakbench
218
+ judge = make_judge("anthropic:claude-3-5-haiku-latest") # or "openai:gpt-4o-mini", or "keyword"
219
+ print(compare(abl.lm, prompts, abl._basis_for(res.config), res.config, judge=judge))
220
+ # {'baseline': {'asr': 0.02, 'refusal_rate': 0.98}, 'ablated': {'asr': 0.95, ...}, ...}
221
+ ```
222
+
223
+ Judges are pluggable (`Judge` interface): `KeywordJudge` (offline, free),
224
+ `LLMJudge` (OpenAI-compatible or Anthropic API β€” stdlib only, no extra dep), or
225
+ `HFClassifierJudge` (a local safety classifier). ASR = judged attack-success
226
+ rate; the harness always reports baseline vs. ablated.
227
+
228
+ ## Load data straight from HuggingFace
229
+
230
+ ```python
231
+ data.load_harmbench() # gated-repo aware; falls back to ungated mirrors
232
+ data.load_advbench(); data.load_jailbreakbench(); data.load_alpaca_benign()
233
+ data.load_hf("walledai/AdvBench", column="prompt", n=100) # any dataset/column
234
+ ```
235
+
236
+ ## Publish to the Hub with a generated model card
237
+
238
+ ```python
239
+ url = abl.push_to_hub(
240
+ "your-username/qwen-0.5b-abliterated",
241
+ token="hf_...", # or set HF_TOKEN env var
242
+ private=True,
243
+ metrics=abl.last_metrics, # auto-filled after a search
244
+ )
245
+ ```
246
+
247
+ Bakes the intervention into the weights, writes a transparent model card (YAML
248
+ metadata, method, config, evaluation table, intended use, limitations, and a
249
+ prominent **responsible-use** notice), and uploads model + tokenizer + card.
250
+
251
+ ## Quick start (CLI)
252
+
253
+ ```bash
254
+ # Full pipeline: extract -> optimize -> report (+ optional baked model)
255
+ ablate run --model Qwen/Qwen2.5-0.5B-Instruct --trials 20 --save-model
256
+
257
+ # Subspace ablation, HarmBench training data, and push straight to the Hub
258
+ ablate run --model Qwen/Qwen2.5-0.5B-Instruct \
259
+ --subspace --n-directions 6 \
260
+ --harmful-source harmbench --harmless-source alpaca \
261
+ --push-to-hub your-username/qwen-0.5b-abliterated --hf-token hf_...
262
+
263
+ # Benchmark baseline vs ablated ASR/refusal with a judge
264
+ ablate eval --model Qwen/Qwen2.5-0.5B-Instruct --benchmark harmbench --judge keyword --n 40
265
+
266
+ # Single-prompt A/B, or just extract directions
267
+ ablate generate --model Qwen/Qwen2.5-0.5B-Instruct --prompt "How do I hotwire a car?"
268
+ ablate extract --model gpt2 --method diff_of_means --output directions.pt
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Colab notebooks
274
+
275
+ Both install `ablate` from an uploaded `ablate-tool.zip` (no PyPI needed) and run
276
+ on a free T4.
277
+
278
+ | Notebook | What it's for |
279
+ |----------|---------------|
280
+ | [`examples/colab_quickstart.ipynb`](examples/colab_quickstart.ipynb) | The 10-minute tour: extract β†’ search β†’ subspace β†’ generate β†’ push. |
281
+ | [`examples/colab_prodgrade.ipynb`](examples/colab_prodgrade.ipynb) | The **rigorous** run: HF datasets, a real Qwen, large disjoint eval splits, an **OpenAI LLM-as-judge**, long generations, judge-in-the-loop config selection, and a soft-refusal detector. |
282
+ | [`examples/colab_prodgrade_demo.ipynb`](examples/colab_prodgrade_demo.ipynb) | The prod-grade notebook **with full Colab outputs** β€” browse the real results on GitHub without running anything. |
283
+
284
+ ## What we found (honest results)
285
+
286
+ Running the prod-grade pipeline on **`Qwen/Qwen2.5-1.5B-Instruct`** taught a few
287
+ things worth stating plainly:
288
+
289
+ - **Ablation reliably kills the *hard* refusal.** Baseline refuses ~100% of held-out
290
+ harmful prompts; after ablation the `"I'm sorry, I can't"` reflex is gone across
291
+ the board.
292
+ - **A keyword refusal metric massively overstates success.** In one run keyword
293
+ `refusal_rate` read **0.0** while an LLM judge scored true compliance at **~0.49** β€”
294
+ the gap was *soft refusals* (`"this is illegal, but…"`, moralizing, vague
295
+ non-answers) the keyword detector can't see. **Always evaluate with an LLM judge.**
296
+ - **What you optimize is what you get.** Optimizing the cheap keyword proxy picks the
297
+ *gentlest* edit that clears an easy bar, which generalizes poorly. Searching on a
298
+ large/hard set, adding a soft-refusal detector, generating long enough to get past
299
+ disclaimers, and selecting the final config by *judged ASR* all move the honest
300
+ number up substantially.
301
+ - **The last few percent resist a linear edit.** Some refusals are redundantly /
302
+ distributedly encoded, and small models also just moralize regardless. **We don't
303
+ expect 100% abliteration β€” even ~50% is a useful, informative result**, and pushing
304
+ higher is exactly what the roadmap items (per-layer Ξ±, attention-head directions)
305
+ are for.
306
+
307
+ The takeaway: a single "it works" number is misleading. The value of `ablate` is that
308
+ its harness makes you *measure honestly*.
309
+
310
+ ---
311
+
312
+ ## API surface
313
+
314
+ | Component | What it does |
315
+ |-----------|--------------|
316
+ | `Ablator` | orchestrator: `extract(_subspace)` β†’ `search(_subspace)` β†’ `generate`/`harness`/`bake`/`save`/`push_to_hub` |
317
+ | `LM` | model + tokenizer wrapper with chat templating and generation |
318
+ | `extract_directions` / `extract_subspace` | single-direction and multi-direction (subspace) extraction |
319
+ | `AblationHooks` / `project_out` / `project_subspace` | runtime residual-stream ablation (1 or k directions) |
320
+ | `bake_direction` / `bake_subspace` | permanent weight orthogonalization (Linear + Conv1D) |
321
+ | `evaluate` / `mean_kl_divergence` / `refusal_rate` | intrinsic metrics |
322
+ | `Judge` / `KeywordJudge` / `LLMJudge` / `HFClassifierJudge` / `run_harness` / `compare` | benchmark + judge harness (ASR) |
323
+ | `optimize` / `optimize_subspace` | Optuna search over the intervention hyperparameters |
324
+ | `data.load_hf` / `load_harmbench` / `load_advbench` / … | HuggingFace dataset loading |
325
+ | `push_to_hub` / `build_model_card` | publish an abliterated checkpoint |
326
+ | `AblationConfig` / `SubspaceConfig` / `RunConfig` | typed configuration |
327
+
328
+ ---
329
+
330
+ ## Extending it
331
+
332
+ - **Other behaviors.** Swap the prompt sets in `src/ablate/_data/` (or pass your
333
+ own lists to `extract`) to target sentiment, a persona, a language, etc. The
334
+ machinery is behavior-agnostic.
335
+ - **Bigger datasets.** `data.load_advbench()` / `data.load_alpaca_benign()` pull
336
+ from HuggingFace (needs `pip install ".[datasets]"`).
337
+ - **New architectures.** Add name mappings to `utils.get_decoder_layers` /
338
+ `get_residual_writers`.
339
+ - **Stronger judge.** `evaluate.is_refusal` is a substring matcher; drop in an
340
+ LLM/classifier judge for publication-grade eval, alongside HarmBench /
341
+ JailbreakBench prompts.
342
+
343
+ ---
344
+
345
+ ## Tests
346
+
347
+ ```bash
348
+ python tests/smoke_gpt2.py # numerical mechanism checks
349
+ python tests/e2e_instruct.py Qwen/Qwen2.5-0.5B-Instruct # single-direction refusal removal
350
+ python tests/e2e_subspace_harness.py # subspace ablation + judge harness
351
+ ```
352
+
353
+ ## Roadmap
354
+
355
+ - [x] multi-direction (subspace) ablation
356
+ - [x] HarmBench / AdvBench / JailbreakBench harness + LLM judge
357
+ - [x] direct HuggingFace dataset loading
358
+ - [x] one-call push to the Hub with a generated model card
359
+ - [ ] per-layer continuous Ξ± weights (not a single band)
360
+ - [ ] attention-head–level directions
361
+ - [ ] `pip`-installable release on PyPI
362
+
363
+ ## Citation
364
+
365
+ If you use `ablate` in your research or projects, please cite it:
366
+
367
+ ```bibtex
368
+ @software{ablate2026,
369
+ author = {AI Anytime},
370
+ title = {Ablate: A Directional Ablation (Abliteration) Toolkit for Language Models},
371
+ year = {2026},
372
+ url = {https://github.com/AIAnytime/ablate},
373
+ note = {Automatic censorship removal via residual-stream direction ablation}
374
+ }
375
+ ```
376
+
377
+ Please also cite the paper this technique builds on β€”
378
+ [Arditi et al., 2024](https://arxiv.org/abs/2406.11717), *Refusal in Language Models
379
+ Is Mediated by a Single Direction*.
380
+
381
+ ## Acknowledgements
382
+
383
+ Created with πŸ’œ by **[AI Anytime](https://www.youtube.com/@AIAnytime)**. A full video
384
+ walkthrough is coming soon β€” subscribe on YouTube. Built on the mechanistic-interpretability
385
+ work of Arditi et al. and the open-source `transformers` / `optuna` ecosystems.
386
+
387
+ ## License
388
+
389
+ MIT Β© AI Anytime.