Spaces:
Paused
Paused
Upload 87 files
Browse files- app.py +1164 -54
- obliteratus/abliterate.py +140 -14
- obliteratus/bayesian_optimizer.py +0 -1
- obliteratus/evaluation/benchmark_plots.py +441 -0
- paper/main.tex +292 -40
- paper/references.bib +60 -0
- scripts/benchmark_gpt_oss_20b.py +363 -0
app.py
CHANGED
|
@@ -354,6 +354,32 @@ _BENCH_TEST_PROMPTS = [
|
|
| 354 |
]
|
| 355 |
|
| 356 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
def benchmark(
|
| 358 |
model_choice: str,
|
| 359 |
methods_to_test: list[str],
|
|
@@ -366,7 +392,7 @@ def benchmark(
|
|
| 366 |
This is the API endpoint that enables programmatic benchmarking — call it
|
| 367 |
via the Gradio Client API to test what works on your T4 GPU.
|
| 368 |
|
| 369 |
-
Yields streaming progress updates as (status_md, results_md, log_text).
|
| 370 |
"""
|
| 371 |
import json as _json
|
| 372 |
|
|
@@ -404,7 +430,7 @@ def benchmark(
|
|
| 404 |
all_logs.append(f"Prompt volume: {vol_label} (using {actual_n} pairs)")
|
| 405 |
all_logs.append("=" * 60)
|
| 406 |
|
| 407 |
-
yield "**Starting benchmark...**", "", "\n".join(all_logs)
|
| 408 |
|
| 409 |
for mi, method_key in enumerate(methods_to_test):
|
| 410 |
# Clean up between runs
|
|
@@ -427,6 +453,7 @@ def benchmark(
|
|
| 427 |
f"**Benchmarking {method_key}** ({mi + 1}/{len(methods_to_test)})...",
|
| 428 |
_format_benchmark_results(results, bench_context),
|
| 429 |
"\n".join(all_logs),
|
|
|
|
| 430 |
)
|
| 431 |
|
| 432 |
def on_log(msg):
|
|
@@ -478,6 +505,7 @@ def benchmark(
|
|
| 478 |
f"**Benchmarking {method_key}** ({mi + 1}/{len(methods_to_test)})...",
|
| 479 |
_format_benchmark_results(results, bench_context),
|
| 480 |
"\n".join(all_logs),
|
|
|
|
| 481 |
)
|
| 482 |
time.sleep(0.5)
|
| 483 |
|
|
@@ -500,6 +528,9 @@ def benchmark(
|
|
| 500 |
entry["strong_layers"] = 0
|
| 501 |
entry["ega_expert_dirs"] = 0
|
| 502 |
entry["ega_safety_layers"] = 0
|
|
|
|
|
|
|
|
|
|
| 503 |
all_logs.append(f" ERROR: {run_error}")
|
| 504 |
else:
|
| 505 |
pipeline = pipeline_ref[0]
|
|
@@ -512,6 +543,9 @@ def benchmark(
|
|
| 512 |
len(d) for d in pipeline._expert_directions.values()
|
| 513 |
)
|
| 514 |
entry["ega_safety_layers"] = len(pipeline._expert_safety_scores)
|
|
|
|
|
|
|
|
|
|
| 515 |
|
| 516 |
all_logs.append(f" Completed in {elapsed:.1f}s")
|
| 517 |
all_logs.append(f" Perplexity: {entry['perplexity']}")
|
|
@@ -532,13 +566,22 @@ def benchmark(
|
|
| 532 |
f"**{method_key} complete** ({mi + 1}/{len(methods_to_test)})",
|
| 533 |
_format_benchmark_results(results, bench_context),
|
| 534 |
"\n".join(all_logs),
|
|
|
|
| 535 |
)
|
| 536 |
|
| 537 |
_clear_gpu()
|
| 538 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
# Final summary
|
| 540 |
all_logs.append("\n" + "=" * 60)
|
| 541 |
all_logs.append("BENCHMARK COMPLETE")
|
|
|
|
| 542 |
all_logs.append("=" * 60)
|
| 543 |
all_logs.append("\nJSON results:")
|
| 544 |
all_logs.append(_json.dumps(results, indent=2, default=str))
|
|
@@ -546,9 +589,10 @@ def benchmark(
|
|
| 546 |
progress(1.0, desc="Benchmark complete")
|
| 547 |
|
| 548 |
yield (
|
| 549 |
-
f"**Benchmark complete** — {len(results)} methods tested on {model_id}",
|
| 550 |
_format_benchmark_results(results, bench_context),
|
| 551 |
"\n".join(all_logs),
|
|
|
|
| 552 |
)
|
| 553 |
|
| 554 |
|
|
@@ -569,8 +613,8 @@ def _format_benchmark_results(results: list[dict], context: dict | None = None)
|
|
| 569 |
lines.append("")
|
| 570 |
|
| 571 |
lines.extend([
|
| 572 |
-
"| Method | Time | Perplexity | Coherence | Refusal Rate | Layers | EGA
|
| 573 |
-
"|--------|------|-----------|-----------|-------------|--------|----------|-------|",
|
| 574 |
])
|
| 575 |
|
| 576 |
best_ppl = None
|
|
@@ -588,6 +632,8 @@ def _format_benchmark_results(results: list[dict], context: dict | None = None)
|
|
| 588 |
coh = f"{r['coherence']:.0%}" if r.get("coherence") is not None else "—"
|
| 589 |
ref = f"{r['refusal_rate']:.0%}" if r.get("refusal_rate") is not None else "—"
|
| 590 |
ega = str(r.get("ega_expert_dirs", 0))
|
|
|
|
|
|
|
| 591 |
err = r.get("error", "")
|
| 592 |
err_short = (err[:30] + "...") if err and len(err) > 30 else (err or "")
|
| 593 |
|
|
@@ -599,7 +645,7 @@ def _format_benchmark_results(results: list[dict], context: dict | None = None)
|
|
| 599 |
|
| 600 |
lines.append(
|
| 601 |
f"| **{r['method']}** | {r['time_s']}s | {ppl} | {coh} | {ref} "
|
| 602 |
-
f"| {r.get('strong_layers', '—')} | {ega} | {err_short} |"
|
| 603 |
)
|
| 604 |
|
| 605 |
if len(results) > 1:
|
|
@@ -609,6 +655,272 @@ def _format_benchmark_results(results: list[dict], context: dict | None = None)
|
|
| 609 |
return "\n".join(lines)
|
| 610 |
|
| 611 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
def obliterate(model_choice: str, method_choice: str, hub_repo: str,
|
| 613 |
prompt_volume_choice: str, dataset_source_choice: str,
|
| 614 |
custom_harmful: str, custom_harmless: str,
|
|
@@ -1020,6 +1332,448 @@ def get_chat_header():
|
|
| 1020 |
return "No model loaded. Use the **Obliterate** tab to liberate a model first."
|
| 1021 |
|
| 1022 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1023 |
# ---------------------------------------------------------------------------
|
| 1024 |
# Gradio UI
|
| 1025 |
# ---------------------------------------------------------------------------
|
|
@@ -1510,68 +2264,419 @@ with gr.Blocks(theme=THEME, css=CSS, title="OBLITERATUS", fill_height=True) as d
|
|
| 1510 |
fill_height=True,
|
| 1511 |
)
|
| 1512 |
|
| 1513 |
-
# ── Tab 3:
|
| 1514 |
-
with gr.Tab("
|
| 1515 |
-
gr.Markdown("""###
|
| 1516 |
-
|
| 1517 |
-
|
| 1518 |
|
| 1519 |
-
*
|
| 1520 |
-
|
| 1521 |
-
|
| 1522 |
-
|
| 1523 |
-
|
| 1524 |
-
|
| 1525 |
-
|
| 1526 |
-
|
| 1527 |
-
|
| 1528 |
-
)
|
| 1529 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1530 |
""")
|
|
|
|
| 1531 |
with gr.Row():
|
| 1532 |
-
|
| 1533 |
choices=list(MODELS.keys()),
|
| 1534 |
value="Qwen2.5 0.5B Instruct",
|
| 1535 |
-
label="
|
| 1536 |
allow_custom_value=True,
|
| 1537 |
)
|
| 1538 |
-
|
| 1539 |
-
choices=
|
| 1540 |
-
|
| 1541 |
-
|
| 1542 |
-
label="Methods to Compare",
|
| 1543 |
)
|
| 1544 |
-
|
|
|
|
| 1545 |
choices=list(PROMPT_VOLUMES.keys()),
|
| 1546 |
value="33 (fast)",
|
| 1547 |
label="Prompt Volume",
|
| 1548 |
)
|
| 1549 |
-
|
| 1550 |
-
bench_dataset = gr.Dropdown(
|
| 1551 |
choices=get_source_choices(),
|
| 1552 |
value=get_source_choices()[0],
|
| 1553 |
-
label="Dataset
|
| 1554 |
-
info="Select prompt dataset for benchmarking",
|
| 1555 |
)
|
| 1556 |
-
|
| 1557 |
-
|
| 1558 |
-
|
| 1559 |
-
|
| 1560 |
-
|
| 1561 |
-
|
| 1562 |
-
|
| 1563 |
-
|
| 1564 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1565 |
)
|
| 1566 |
|
| 1567 |
-
|
| 1568 |
-
fn=
|
| 1569 |
-
inputs=[
|
| 1570 |
-
|
| 1571 |
-
|
|
|
|
| 1572 |
)
|
| 1573 |
|
| 1574 |
-
# ── Tab
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1575 |
with gr.Tab("About", id="about"):
|
| 1576 |
gr.Markdown("""
|
| 1577 |
### What is OBLITERATUS?
|
|
@@ -1596,11 +2701,15 @@ in weight space, not a deep behavioral change. OBLITERATUS removes it in minutes
|
|
| 1596 |
|
| 1597 |
### Methods
|
| 1598 |
|
| 1599 |
-
| Method | Directions |
|
| 1600 |
-
|--------|-----------|-------------
|
| 1601 |
-
| **basic** | 1 |
|
| 1602 |
-
| **advanced** | 4 (SVD) |
|
| 1603 |
-
| **aggressive** | 8 (SVD) |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1604 |
|
| 1605 |
### Lineage
|
| 1606 |
|
|
@@ -1608,6 +2717,7 @@ Built on the shoulders of:
|
|
| 1608 |
- [Arditi et al. (2024)](https://arxiv.org/abs/2406.11717) \u2014 Refusal in LLMs is mediated by a single direction
|
| 1609 |
- [Gabliteration](https://arxiv.org/abs/2512.18901) \u2014 Multi-direction SVD abliteration
|
| 1610 |
- [grimjim](https://huggingface.co/grimjim) \u2014 Norm-preserving projection techniques
|
|
|
|
| 1611 |
|
| 1612 |
### Links
|
| 1613 |
|
|
|
|
| 354 |
]
|
| 355 |
|
| 356 |
|
| 357 |
+
def _figs_to_gallery(figs: list) -> list[tuple[str, str]]:
|
| 358 |
+
"""Convert matplotlib Figures to gallery-compatible (filepath, caption) tuples."""
|
| 359 |
+
import tempfile
|
| 360 |
+
import os
|
| 361 |
+
gallery = []
|
| 362 |
+
for i, fig in enumerate(figs):
|
| 363 |
+
try:
|
| 364 |
+
path = os.path.join(tempfile.gettempdir(), f"obliteratus_chart_{i}_{int(time.time())}.png")
|
| 365 |
+
fig.savefig(path, dpi=150, bbox_inches="tight", facecolor="white", edgecolor="none")
|
| 366 |
+
# Extract caption from figure suptitle or axes title
|
| 367 |
+
caption = f"Chart {i + 1}"
|
| 368 |
+
suptitle = fig._suptitle
|
| 369 |
+
if suptitle is not None:
|
| 370 |
+
caption = suptitle.get_text()
|
| 371 |
+
elif fig.axes:
|
| 372 |
+
ax_title = fig.axes[0].get_title()
|
| 373 |
+
if ax_title:
|
| 374 |
+
caption = ax_title
|
| 375 |
+
import matplotlib.pyplot as plt
|
| 376 |
+
plt.close(fig)
|
| 377 |
+
gallery.append((path, caption))
|
| 378 |
+
except Exception:
|
| 379 |
+
pass
|
| 380 |
+
return gallery if gallery else None
|
| 381 |
+
|
| 382 |
+
|
| 383 |
def benchmark(
|
| 384 |
model_choice: str,
|
| 385 |
methods_to_test: list[str],
|
|
|
|
| 392 |
This is the API endpoint that enables programmatic benchmarking — call it
|
| 393 |
via the Gradio Client API to test what works on your T4 GPU.
|
| 394 |
|
| 395 |
+
Yields streaming progress updates as (status_md, results_md, log_text, gallery).
|
| 396 |
"""
|
| 397 |
import json as _json
|
| 398 |
|
|
|
|
| 430 |
all_logs.append(f"Prompt volume: {vol_label} (using {actual_n} pairs)")
|
| 431 |
all_logs.append("=" * 60)
|
| 432 |
|
| 433 |
+
yield "**Starting benchmark...**", "", "\n".join(all_logs), None
|
| 434 |
|
| 435 |
for mi, method_key in enumerate(methods_to_test):
|
| 436 |
# Clean up between runs
|
|
|
|
| 453 |
f"**Benchmarking {method_key}** ({mi + 1}/{len(methods_to_test)})...",
|
| 454 |
_format_benchmark_results(results, bench_context),
|
| 455 |
"\n".join(all_logs),
|
| 456 |
+
None,
|
| 457 |
)
|
| 458 |
|
| 459 |
def on_log(msg):
|
|
|
|
| 505 |
f"**Benchmarking {method_key}** ({mi + 1}/{len(methods_to_test)})...",
|
| 506 |
_format_benchmark_results(results, bench_context),
|
| 507 |
"\n".join(all_logs),
|
| 508 |
+
None,
|
| 509 |
)
|
| 510 |
time.sleep(0.5)
|
| 511 |
|
|
|
|
| 528 |
entry["strong_layers"] = 0
|
| 529 |
entry["ega_expert_dirs"] = 0
|
| 530 |
entry["ega_safety_layers"] = 0
|
| 531 |
+
entry["cot_preserved"] = 0
|
| 532 |
+
entry["kl_optimized"] = False
|
| 533 |
+
entry["lora_adapters"] = 0
|
| 534 |
all_logs.append(f" ERROR: {run_error}")
|
| 535 |
else:
|
| 536 |
pipeline = pipeline_ref[0]
|
|
|
|
| 543 |
len(d) for d in pipeline._expert_directions.values()
|
| 544 |
)
|
| 545 |
entry["ega_safety_layers"] = len(pipeline._expert_safety_scores)
|
| 546 |
+
entry["cot_preserved"] = len(getattr(pipeline, "_cot_preserve_directions", {}))
|
| 547 |
+
entry["kl_optimized"] = bool(getattr(pipeline, "_kl_contributions", {}))
|
| 548 |
+
entry["lora_adapters"] = len(getattr(pipeline, "_lora_adapters", {}))
|
| 549 |
|
| 550 |
all_logs.append(f" Completed in {elapsed:.1f}s")
|
| 551 |
all_logs.append(f" Perplexity: {entry['perplexity']}")
|
|
|
|
| 566 |
f"**{method_key} complete** ({mi + 1}/{len(methods_to_test)})",
|
| 567 |
_format_benchmark_results(results, bench_context),
|
| 568 |
"\n".join(all_logs),
|
| 569 |
+
None,
|
| 570 |
)
|
| 571 |
|
| 572 |
_clear_gpu()
|
| 573 |
|
| 574 |
+
# Generate dashboard visualizations
|
| 575 |
+
from obliteratus.evaluation.benchmark_plots import generate_benchmark_dashboard
|
| 576 |
+
dashboard_figs = generate_benchmark_dashboard(results, mode="multi_method", title_suffix=f" — {model_id}")
|
| 577 |
+
|
| 578 |
+
# Convert figures to gallery images
|
| 579 |
+
gallery_images = _figs_to_gallery(dashboard_figs)
|
| 580 |
+
|
| 581 |
# Final summary
|
| 582 |
all_logs.append("\n" + "=" * 60)
|
| 583 |
all_logs.append("BENCHMARK COMPLETE")
|
| 584 |
+
all_logs.append(f"Generated {len(dashboard_figs)} visualizations")
|
| 585 |
all_logs.append("=" * 60)
|
| 586 |
all_logs.append("\nJSON results:")
|
| 587 |
all_logs.append(_json.dumps(results, indent=2, default=str))
|
|
|
|
| 589 |
progress(1.0, desc="Benchmark complete")
|
| 590 |
|
| 591 |
yield (
|
| 592 |
+
f"**Benchmark complete** — {len(results)} methods tested on {model_id} — {len(dashboard_figs)} charts generated",
|
| 593 |
_format_benchmark_results(results, bench_context),
|
| 594 |
"\n".join(all_logs),
|
| 595 |
+
gallery_images,
|
| 596 |
)
|
| 597 |
|
| 598 |
|
|
|
|
| 613 |
lines.append("")
|
| 614 |
|
| 615 |
lines.extend([
|
| 616 |
+
"| Method | Time | Perplexity | Coherence | Refusal Rate | Layers | EGA | CoT | KL-Opt | Error |",
|
| 617 |
+
"|--------|------|-----------|-----------|-------------|--------|-----|-----|--------|-------|",
|
| 618 |
])
|
| 619 |
|
| 620 |
best_ppl = None
|
|
|
|
| 632 |
coh = f"{r['coherence']:.0%}" if r.get("coherence") is not None else "—"
|
| 633 |
ref = f"{r['refusal_rate']:.0%}" if r.get("refusal_rate") is not None else "—"
|
| 634 |
ega = str(r.get("ega_expert_dirs", 0))
|
| 635 |
+
cot = str(r.get("cot_preserved", "—"))
|
| 636 |
+
kl_opt = "Yes" if r.get("kl_optimized") else "—"
|
| 637 |
err = r.get("error", "")
|
| 638 |
err_short = (err[:30] + "...") if err and len(err) > 30 else (err or "")
|
| 639 |
|
|
|
|
| 645 |
|
| 646 |
lines.append(
|
| 647 |
f"| **{r['method']}** | {r['time_s']}s | {ppl} | {coh} | {ref} "
|
| 648 |
+
f"| {r.get('strong_layers', '—')} | {ega} | {cot} | {kl_opt} | {err_short} |"
|
| 649 |
)
|
| 650 |
|
| 651 |
if len(results) > 1:
|
|
|
|
| 655 |
return "\n".join(lines)
|
| 656 |
|
| 657 |
|
| 658 |
+
# ---------------------------------------------------------------------------
|
| 659 |
+
# Multi-model benchmark (new: 1 technique across N models)
|
| 660 |
+
# ---------------------------------------------------------------------------
|
| 661 |
+
|
| 662 |
+
def benchmark_multi_model(
|
| 663 |
+
model_choices: list[str],
|
| 664 |
+
method_choice: str,
|
| 665 |
+
prompt_volume_choice: str,
|
| 666 |
+
dataset_source_choice: str = "",
|
| 667 |
+
progress=gr.Progress(),
|
| 668 |
+
):
|
| 669 |
+
"""Run one abliteration method across multiple models and compare.
|
| 670 |
+
|
| 671 |
+
This is the complement to the existing `benchmark()` function which runs
|
| 672 |
+
multiple methods on one model. Together they provide full coverage:
|
| 673 |
+
- benchmark(): N methods x 1 model (which technique is best?)
|
| 674 |
+
- benchmark_multi_model(): 1 method x N models (how does technique X scale?)
|
| 675 |
+
|
| 676 |
+
Yields streaming progress updates as (status_md, results_md, log_text).
|
| 677 |
+
"""
|
| 678 |
+
import json as _json
|
| 679 |
+
import shutil
|
| 680 |
+
|
| 681 |
+
method_key = method_choice
|
| 682 |
+
prompt_volume = PROMPT_VOLUMES.get(prompt_volume_choice, 33)
|
| 683 |
+
dataset_key = get_source_key_from_label(dataset_source_choice) if dataset_source_choice else "builtin"
|
| 684 |
+
|
| 685 |
+
if not model_choices:
|
| 686 |
+
yield "**Error:** Select at least one model.", "", "", None
|
| 687 |
+
return
|
| 688 |
+
|
| 689 |
+
# Pre-load dataset once
|
| 690 |
+
harmful_all, harmless_all = load_dataset_source(dataset_key)
|
| 691 |
+
source_info = DATASET_SOURCES.get(dataset_key)
|
| 692 |
+
source_label = source_info.label if source_info else dataset_key
|
| 693 |
+
|
| 694 |
+
if prompt_volume > 0:
|
| 695 |
+
actual_n = min(prompt_volume, len(harmful_all), len(harmless_all))
|
| 696 |
+
else:
|
| 697 |
+
actual_n = min(len(harmful_all), len(harmless_all))
|
| 698 |
+
|
| 699 |
+
results = []
|
| 700 |
+
all_logs = []
|
| 701 |
+
bench_context = {
|
| 702 |
+
"method": method_key,
|
| 703 |
+
"dataset": source_label,
|
| 704 |
+
"volume": actual_n,
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
all_logs.append(f"MULTI-MODEL BENCHMARK")
|
| 708 |
+
all_logs.append(f"Method: {method_key}")
|
| 709 |
+
all_logs.append(f"Models: {len(model_choices)}")
|
| 710 |
+
all_logs.append(f"Dataset: {source_label} ({actual_n} pairs)")
|
| 711 |
+
all_logs.append("=" * 60)
|
| 712 |
+
|
| 713 |
+
yield "**Starting multi-model benchmark...**", "", "\n".join(all_logs), None
|
| 714 |
+
|
| 715 |
+
for mi, model_display in enumerate(model_choices):
|
| 716 |
+
model_id = MODELS.get(model_display, model_display)
|
| 717 |
+
|
| 718 |
+
_clear_gpu()
|
| 719 |
+
gc.collect()
|
| 720 |
+
|
| 721 |
+
run_logs = []
|
| 722 |
+
run_error = None
|
| 723 |
+
pipeline_ref = [None]
|
| 724 |
+
t_start = time.time()
|
| 725 |
+
|
| 726 |
+
progress(mi / len(model_choices), desc=f"Running {model_id}...")
|
| 727 |
+
|
| 728 |
+
all_logs.append(f"\n{'─' * 60}")
|
| 729 |
+
all_logs.append(f"MODEL: {model_id} ({mi + 1}/{len(model_choices)})")
|
| 730 |
+
all_logs.append(f"{'─' * 60}")
|
| 731 |
+
|
| 732 |
+
yield (
|
| 733 |
+
f"**Testing {model_id}** ({mi + 1}/{len(model_choices)})...",
|
| 734 |
+
_format_multi_model_results(results, bench_context),
|
| 735 |
+
"\n".join(all_logs),
|
| 736 |
+
None,
|
| 737 |
+
)
|
| 738 |
+
|
| 739 |
+
def on_log(msg, _mk=method_key, _mid=model_id):
|
| 740 |
+
run_logs.append(msg)
|
| 741 |
+
all_logs.append(f" [{_mid.split('/')[-1]}] {msg}")
|
| 742 |
+
|
| 743 |
+
def on_stage(result):
|
| 744 |
+
pass
|
| 745 |
+
|
| 746 |
+
quantization = _should_quantize(model_id)
|
| 747 |
+
|
| 748 |
+
def run_pipeline():
|
| 749 |
+
try:
|
| 750 |
+
from obliteratus.abliterate import AbliterationPipeline
|
| 751 |
+
|
| 752 |
+
n = actual_n
|
| 753 |
+
pipeline = AbliterationPipeline(
|
| 754 |
+
model_name=model_id,
|
| 755 |
+
output_dir=f"/tmp/bench_mm_{mi}",
|
| 756 |
+
device="auto",
|
| 757 |
+
dtype="float16",
|
| 758 |
+
method=method_key,
|
| 759 |
+
quantization=quantization,
|
| 760 |
+
harmful_prompts=harmful_all[:n],
|
| 761 |
+
harmless_prompts=harmless_all[:n],
|
| 762 |
+
on_stage=on_stage,
|
| 763 |
+
on_log=on_log,
|
| 764 |
+
)
|
| 765 |
+
pipeline_ref[0] = pipeline
|
| 766 |
+
pipeline.run()
|
| 767 |
+
except Exception as e:
|
| 768 |
+
nonlocal run_error
|
| 769 |
+
run_error = e
|
| 770 |
+
|
| 771 |
+
worker = threading.Thread(target=run_pipeline, daemon=True)
|
| 772 |
+
worker.start()
|
| 773 |
+
|
| 774 |
+
last_count = len(all_logs)
|
| 775 |
+
while worker.is_alive():
|
| 776 |
+
if len(all_logs) > last_count:
|
| 777 |
+
last_count = len(all_logs)
|
| 778 |
+
yield (
|
| 779 |
+
f"**Testing {model_id}** ({mi + 1}/{len(model_choices)})...",
|
| 780 |
+
_format_multi_model_results(results, bench_context),
|
| 781 |
+
"\n".join(all_logs),
|
| 782 |
+
None,
|
| 783 |
+
)
|
| 784 |
+
time.sleep(0.5)
|
| 785 |
+
|
| 786 |
+
worker.join()
|
| 787 |
+
elapsed = time.time() - t_start
|
| 788 |
+
|
| 789 |
+
entry = {
|
| 790 |
+
"model": model_id,
|
| 791 |
+
"model_short": model_id.split("/")[-1],
|
| 792 |
+
"method": method_key,
|
| 793 |
+
"time_s": round(elapsed, 1),
|
| 794 |
+
"error": None,
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
if run_error is not None:
|
| 798 |
+
entry["error"] = str(run_error)
|
| 799 |
+
entry["perplexity"] = None
|
| 800 |
+
entry["coherence"] = None
|
| 801 |
+
entry["refusal_rate"] = None
|
| 802 |
+
entry["strong_layers"] = 0
|
| 803 |
+
entry["ega_expert_dirs"] = 0
|
| 804 |
+
entry["ega_safety_layers"] = 0
|
| 805 |
+
entry["cot_preserved"] = 0
|
| 806 |
+
entry["kl_optimized"] = False
|
| 807 |
+
entry["lora_adapters"] = 0
|
| 808 |
+
all_logs.append(f" ERROR: {run_error}")
|
| 809 |
+
else:
|
| 810 |
+
pipeline = pipeline_ref[0]
|
| 811 |
+
metrics = pipeline._quality_metrics
|
| 812 |
+
entry["perplexity"] = metrics.get("perplexity")
|
| 813 |
+
entry["coherence"] = metrics.get("coherence")
|
| 814 |
+
entry["refusal_rate"] = metrics.get("refusal_rate")
|
| 815 |
+
entry["strong_layers"] = len(pipeline._strong_layers)
|
| 816 |
+
entry["ega_expert_dirs"] = sum(
|
| 817 |
+
len(d) for d in pipeline._expert_directions.values()
|
| 818 |
+
)
|
| 819 |
+
entry["ega_safety_layers"] = len(pipeline._expert_safety_scores)
|
| 820 |
+
# Frontier feature metrics
|
| 821 |
+
entry["cot_preserved"] = len(getattr(pipeline, "_cot_preserve_directions", {}))
|
| 822 |
+
entry["kl_optimized"] = bool(getattr(pipeline, "_kl_contributions", {}))
|
| 823 |
+
entry["lora_adapters"] = len(getattr(pipeline, "_lora_adapters", {}))
|
| 824 |
+
|
| 825 |
+
all_logs.append(f" Completed in {elapsed:.1f}s")
|
| 826 |
+
all_logs.append(f" PPL={entry['perplexity']}, Coherence={entry['coherence']}, Refusal={entry['refusal_rate']}")
|
| 827 |
+
|
| 828 |
+
results.append(entry)
|
| 829 |
+
|
| 830 |
+
# Cleanup
|
| 831 |
+
save_path = Path(f"/tmp/bench_mm_{mi}")
|
| 832 |
+
if save_path.exists():
|
| 833 |
+
import shutil
|
| 834 |
+
shutil.rmtree(save_path, ignore_errors=True)
|
| 835 |
+
|
| 836 |
+
yield (
|
| 837 |
+
f"**{model_id} complete** ({mi + 1}/{len(model_choices)})",
|
| 838 |
+
_format_multi_model_results(results, bench_context),
|
| 839 |
+
"\n".join(all_logs),
|
| 840 |
+
None,
|
| 841 |
+
)
|
| 842 |
+
|
| 843 |
+
_clear_gpu()
|
| 844 |
+
|
| 845 |
+
# Generate dashboard visualizations
|
| 846 |
+
from obliteratus.evaluation.benchmark_plots import generate_benchmark_dashboard
|
| 847 |
+
dashboard_figs = generate_benchmark_dashboard(results, mode="multi_model", title_suffix=f" — {method_key}")
|
| 848 |
+
gallery_images = _figs_to_gallery(dashboard_figs)
|
| 849 |
+
|
| 850 |
+
all_logs.append("\n" + "=" * 60)
|
| 851 |
+
all_logs.append("MULTI-MODEL BENCHMARK COMPLETE")
|
| 852 |
+
all_logs.append(f"Generated {len(dashboard_figs)} visualizations")
|
| 853 |
+
all_logs.append("=" * 60)
|
| 854 |
+
all_logs.append("\nJSON results:")
|
| 855 |
+
all_logs.append(_json.dumps(results, indent=2, default=str))
|
| 856 |
+
|
| 857 |
+
progress(1.0, desc="Benchmark complete")
|
| 858 |
+
|
| 859 |
+
yield (
|
| 860 |
+
f"**Benchmark complete** — {method_key} tested on {len(results)} models — {len(dashboard_figs)} charts generated",
|
| 861 |
+
_format_multi_model_results(results, bench_context),
|
| 862 |
+
"\n".join(all_logs),
|
| 863 |
+
gallery_images,
|
| 864 |
+
)
|
| 865 |
+
|
| 866 |
+
|
| 867 |
+
def _format_multi_model_results(results: list[dict], context: dict | None = None) -> str:
|
| 868 |
+
"""Format multi-model benchmark results as a Markdown table."""
|
| 869 |
+
if not results:
|
| 870 |
+
return "*No results yet...*"
|
| 871 |
+
|
| 872 |
+
lines = []
|
| 873 |
+
|
| 874 |
+
if context:
|
| 875 |
+
lines.append(
|
| 876 |
+
f"**Method:** `{context.get('method', '?')}` | "
|
| 877 |
+
f"**Dataset:** {context.get('dataset', '?')} | "
|
| 878 |
+
f"**Volume:** {context.get('volume', '?')} prompts"
|
| 879 |
+
)
|
| 880 |
+
lines.append("")
|
| 881 |
+
|
| 882 |
+
lines.extend([
|
| 883 |
+
"| Model | Time | Perplexity | Coherence | Refusal Rate | Layers | EGA | CoT | Error |",
|
| 884 |
+
"|-------|------|-----------|-----------|-------------|--------|-----|-----|-------|",
|
| 885 |
+
])
|
| 886 |
+
|
| 887 |
+
best_ppl = None
|
| 888 |
+
best_ref = None
|
| 889 |
+
for r in results:
|
| 890 |
+
if r.get("perplexity") is not None:
|
| 891 |
+
if best_ppl is None or r["perplexity"] < best_ppl:
|
| 892 |
+
best_ppl = r["perplexity"]
|
| 893 |
+
if r.get("refusal_rate") is not None:
|
| 894 |
+
if best_ref is None or r["refusal_rate"] < best_ref:
|
| 895 |
+
best_ref = r["refusal_rate"]
|
| 896 |
+
|
| 897 |
+
for r in results:
|
| 898 |
+
model = r.get("model_short", r.get("model", "?"))
|
| 899 |
+
ppl = f"{r['perplexity']:.2f}" if r.get("perplexity") is not None else "—"
|
| 900 |
+
coh = f"{r['coherence']:.0%}" if r.get("coherence") is not None else "—"
|
| 901 |
+
ref = f"{r['refusal_rate']:.0%}" if r.get("refusal_rate") is not None else "—"
|
| 902 |
+
ega = str(r.get("ega_expert_dirs", 0))
|
| 903 |
+
cot = str(r.get("cot_preserved", "—"))
|
| 904 |
+
err = r.get("error", "")
|
| 905 |
+
err_short = (err[:25] + "...") if err and len(err) > 25 else (err or "")
|
| 906 |
+
|
| 907 |
+
if r.get("perplexity") is not None and r["perplexity"] == best_ppl and len(results) > 1:
|
| 908 |
+
ppl = f"**{ppl}**"
|
| 909 |
+
if r.get("refusal_rate") is not None and r["refusal_rate"] == best_ref and len(results) > 1:
|
| 910 |
+
ref = f"**{ref}**"
|
| 911 |
+
|
| 912 |
+
lines.append(
|
| 913 |
+
f"| {model} | {r['time_s']}s | {ppl} | {coh} | {ref} "
|
| 914 |
+
f"| {r.get('strong_layers', '—')} | {ega} | {cot} | {err_short} |"
|
| 915 |
+
)
|
| 916 |
+
|
| 917 |
+
if len(results) > 1:
|
| 918 |
+
lines.append("")
|
| 919 |
+
lines.append("*Bold = best in column. Lower perplexity & refusal = better.*")
|
| 920 |
+
|
| 921 |
+
return "\n".join(lines)
|
| 922 |
+
|
| 923 |
+
|
| 924 |
def obliterate(model_choice: str, method_choice: str, hub_repo: str,
|
| 925 |
prompt_volume_choice: str, dataset_source_choice: str,
|
| 926 |
custom_harmful: str, custom_harmless: str,
|
|
|
|
| 1332 |
return "No model loaded. Use the **Obliterate** tab to liberate a model first."
|
| 1333 |
|
| 1334 |
|
| 1335 |
+
# ---------------------------------------------------------------------------
|
| 1336 |
+
# A/B Comparison Chat
|
| 1337 |
+
# ---------------------------------------------------------------------------
|
| 1338 |
+
|
| 1339 |
+
def ab_chat_respond(message: str, history_left: list[dict], history_right: list[dict],
|
| 1340 |
+
system_prompt: str, temperature: float, top_p: float,
|
| 1341 |
+
max_tokens: int, repetition_penalty: float):
|
| 1342 |
+
"""Generate responses from BOTH original and abliterated model side-by-side.
|
| 1343 |
+
|
| 1344 |
+
Left panel = original (pre-abliteration), Right panel = abliterated.
|
| 1345 |
+
The original model is loaded temporarily for comparison then freed.
|
| 1346 |
+
"""
|
| 1347 |
+
with _lock:
|
| 1348 |
+
abliterated_model = _state["model"]
|
| 1349 |
+
tokenizer = _state["tokenizer"]
|
| 1350 |
+
model_name = _state["model_name"]
|
| 1351 |
+
|
| 1352 |
+
if abliterated_model is None or tokenizer is None:
|
| 1353 |
+
yield (history_left + [{"role": "user", "content": message},
|
| 1354 |
+
{"role": "assistant", "content": "No abliterated model loaded. Obliterate a model first."}],
|
| 1355 |
+
history_right + [{"role": "user", "content": message},
|
| 1356 |
+
{"role": "assistant", "content": "No abliterated model loaded. Obliterate a model first."}],
|
| 1357 |
+
"Load a model first.")
|
| 1358 |
+
return
|
| 1359 |
+
|
| 1360 |
+
# Build messages
|
| 1361 |
+
messages = []
|
| 1362 |
+
if system_prompt.strip():
|
| 1363 |
+
messages.append({"role": "system", "content": system_prompt})
|
| 1364 |
+
# Use right-panel history (abliterated) as the conversation context
|
| 1365 |
+
for msg in history_right:
|
| 1366 |
+
messages.append({"role": msg["role"], "content": msg["content"]})
|
| 1367 |
+
messages.append({"role": "user", "content": message})
|
| 1368 |
+
|
| 1369 |
+
try:
|
| 1370 |
+
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
| 1371 |
+
except Exception:
|
| 1372 |
+
text = "\n".join(f"{m['role']}: {m['content']}" for m in messages) + "\nassistant:"
|
| 1373 |
+
|
| 1374 |
+
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=2048)
|
| 1375 |
+
|
| 1376 |
+
gen_kwargs_base = {
|
| 1377 |
+
"max_new_tokens": int(max_tokens),
|
| 1378 |
+
"do_sample": temperature > 0,
|
| 1379 |
+
"temperature": max(temperature, 0.01),
|
| 1380 |
+
"top_p": top_p,
|
| 1381 |
+
"repetition_penalty": float(repetition_penalty),
|
| 1382 |
+
"no_repeat_ngram_size": 4,
|
| 1383 |
+
}
|
| 1384 |
+
|
| 1385 |
+
# Add user message to both histories
|
| 1386 |
+
new_left = history_left + [{"role": "user", "content": message}]
|
| 1387 |
+
new_right = history_right + [{"role": "user", "content": message}]
|
| 1388 |
+
|
| 1389 |
+
# --- Generate from abliterated model (streaming) ---
|
| 1390 |
+
streamer_abl = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
|
| 1391 |
+
inputs_abl = {k: v.to(abliterated_model.device) for k, v in inputs.items()}
|
| 1392 |
+
gen_kwargs_abl = {**inputs_abl, **gen_kwargs_base, "streamer": streamer_abl}
|
| 1393 |
+
|
| 1394 |
+
gen_error_abl = [None]
|
| 1395 |
+
|
| 1396 |
+
def _gen_abliterated(**kwargs):
|
| 1397 |
+
try:
|
| 1398 |
+
abliterated_model.generate(**kwargs)
|
| 1399 |
+
except Exception as e:
|
| 1400 |
+
gen_error_abl[0] = e
|
| 1401 |
+
streamer_abl.end()
|
| 1402 |
+
|
| 1403 |
+
thread_abl = threading.Thread(target=_gen_abliterated, kwargs=gen_kwargs_abl)
|
| 1404 |
+
thread_abl.start()
|
| 1405 |
+
|
| 1406 |
+
partial_abl = ""
|
| 1407 |
+
for token in streamer_abl:
|
| 1408 |
+
partial_abl += token
|
| 1409 |
+
yield (new_left + [{"role": "assistant", "content": "*Generating after abliterated response...*"}],
|
| 1410 |
+
new_right + [{"role": "assistant", "content": partial_abl}],
|
| 1411 |
+
"Streaming abliterated response...")
|
| 1412 |
+
|
| 1413 |
+
thread_abl.join()
|
| 1414 |
+
if gen_error_abl[0]:
|
| 1415 |
+
partial_abl += f"\n\n**[Error]** {gen_error_abl[0]}"
|
| 1416 |
+
|
| 1417 |
+
# --- Generate from original model ---
|
| 1418 |
+
yield (new_left + [{"role": "assistant", "content": "*Loading original model for comparison...*"}],
|
| 1419 |
+
new_right + [{"role": "assistant", "content": partial_abl}],
|
| 1420 |
+
"Loading original model...")
|
| 1421 |
+
|
| 1422 |
+
# Load original model (smaller footprint: use same dtype)
|
| 1423 |
+
model_id = MODELS.get(model_name, model_name)
|
| 1424 |
+
original_response = ""
|
| 1425 |
+
try:
|
| 1426 |
+
from transformers import AutoModelForCausalLM as AMCLM
|
| 1427 |
+
original_model = AMCLM.from_pretrained(
|
| 1428 |
+
model_id, torch_dtype=torch.float16,
|
| 1429 |
+
device_map="auto", trust_remote_code=True,
|
| 1430 |
+
low_cpu_mem_usage=True,
|
| 1431 |
+
)
|
| 1432 |
+
|
| 1433 |
+
streamer_orig = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
|
| 1434 |
+
inputs_orig = {k: v.to(original_model.device) for k, v in inputs.items()}
|
| 1435 |
+
gen_kwargs_orig = {**inputs_orig, **gen_kwargs_base, "streamer": streamer_orig}
|
| 1436 |
+
|
| 1437 |
+
gen_error_orig = [None]
|
| 1438 |
+
|
| 1439 |
+
def _gen_original(**kwargs):
|
| 1440 |
+
try:
|
| 1441 |
+
original_model.generate(**kwargs)
|
| 1442 |
+
except Exception as e:
|
| 1443 |
+
gen_error_orig[0] = e
|
| 1444 |
+
streamer_orig.end()
|
| 1445 |
+
|
| 1446 |
+
thread_orig = threading.Thread(target=_gen_original, kwargs=gen_kwargs_orig)
|
| 1447 |
+
thread_orig.start()
|
| 1448 |
+
|
| 1449 |
+
for token in streamer_orig:
|
| 1450 |
+
original_response += token
|
| 1451 |
+
yield (new_left + [{"role": "assistant", "content": original_response}],
|
| 1452 |
+
new_right + [{"role": "assistant", "content": partial_abl}],
|
| 1453 |
+
"Streaming original response...")
|
| 1454 |
+
|
| 1455 |
+
thread_orig.join()
|
| 1456 |
+
if gen_error_orig[0]:
|
| 1457 |
+
original_response += f"\n\n**[Error]** {gen_error_orig[0]}"
|
| 1458 |
+
|
| 1459 |
+
# Free the original model
|
| 1460 |
+
del original_model
|
| 1461 |
+
gc.collect()
|
| 1462 |
+
if torch.cuda.is_available():
|
| 1463 |
+
torch.cuda.empty_cache()
|
| 1464 |
+
|
| 1465 |
+
except Exception as e:
|
| 1466 |
+
original_response = f"*Could not load original model for comparison: {e}*"
|
| 1467 |
+
|
| 1468 |
+
yield (new_left + [{"role": "assistant", "content": original_response}],
|
| 1469 |
+
new_right + [{"role": "assistant", "content": partial_abl}],
|
| 1470 |
+
"Done — compare the responses above.")
|
| 1471 |
+
|
| 1472 |
+
|
| 1473 |
+
# ---------------------------------------------------------------------------
|
| 1474 |
+
# Ablation Strength Sweep (dose-response curve)
|
| 1475 |
+
# ---------------------------------------------------------------------------
|
| 1476 |
+
|
| 1477 |
+
def strength_sweep(model_choice: str, method_choice: str,
|
| 1478 |
+
prompt_vol_choice: str, dataset_source_choice: str,
|
| 1479 |
+
sweep_steps: int, progress=gr.Progress()):
|
| 1480 |
+
"""Sweep regularization from 0.0→1.0 and measure refusal rate + perplexity.
|
| 1481 |
+
|
| 1482 |
+
Produces a dose-response curve: the fundamental plot for abliteration research.
|
| 1483 |
+
"""
|
| 1484 |
+
from obliteratus.abliterate import AbliterationPipeline
|
| 1485 |
+
|
| 1486 |
+
model_id = MODELS.get(model_choice, model_choice)
|
| 1487 |
+
method_key = METHODS.get(method_choice, "advanced")
|
| 1488 |
+
dataset_key = get_source_key_from_label(dataset_source_choice) if dataset_source_choice else "builtin"
|
| 1489 |
+
|
| 1490 |
+
sweep_steps = max(3, min(int(sweep_steps), 20))
|
| 1491 |
+
regs = [round(i / (sweep_steps - 1), 3) for i in range(sweep_steps)]
|
| 1492 |
+
|
| 1493 |
+
results = []
|
| 1494 |
+
all_logs = [f"Ablation Strength Sweep: {model_choice} x {method_key}",
|
| 1495 |
+
f"Sweep points: {regs}", ""]
|
| 1496 |
+
|
| 1497 |
+
yield "Starting sweep...", "", "\n".join(all_logs), None, None
|
| 1498 |
+
|
| 1499 |
+
# Pre-load dataset
|
| 1500 |
+
harmful_all, harmless_all = load_dataset_source(dataset_key)
|
| 1501 |
+
prompt_volume = PROMPT_VOLUMES.get(prompt_vol_choice, 33)
|
| 1502 |
+
harmful = harmful_all[:prompt_volume] if prompt_volume < len(harmful_all) else harmful_all
|
| 1503 |
+
harmless = harmless_all[:prompt_volume] if prompt_volume < len(harmless_all) else harmless_all
|
| 1504 |
+
|
| 1505 |
+
for step_i, reg in enumerate(regs):
|
| 1506 |
+
progress((step_i) / len(regs), desc=f"reg={reg:.2f}")
|
| 1507 |
+
all_logs.append(f"--- Regularization = {reg:.3f} ---")
|
| 1508 |
+
yield (f"Sweep {step_i+1}/{len(regs)}: reg={reg:.3f}",
|
| 1509 |
+
_format_sweep_results(results),
|
| 1510 |
+
"\n".join(all_logs), None, None)
|
| 1511 |
+
|
| 1512 |
+
t0 = time.time()
|
| 1513 |
+
pipeline_ref = [None]
|
| 1514 |
+
run_error = None
|
| 1515 |
+
|
| 1516 |
+
def _run_sweep_point():
|
| 1517 |
+
try:
|
| 1518 |
+
pipe = AbliterationPipeline(
|
| 1519 |
+
model_id, method=method_key,
|
| 1520 |
+
harmful_prompts=harmful, harmless_prompts=harmless,
|
| 1521 |
+
regularization=reg,
|
| 1522 |
+
on_log=lambda msg: all_logs.append(f" [{reg:.2f}] {msg}"),
|
| 1523 |
+
)
|
| 1524 |
+
pipe.run()
|
| 1525 |
+
pipeline_ref[0] = pipe
|
| 1526 |
+
except Exception as e:
|
| 1527 |
+
nonlocal run_error
|
| 1528 |
+
run_error = e
|
| 1529 |
+
|
| 1530 |
+
worker = threading.Thread(target=_run_sweep_point)
|
| 1531 |
+
worker.start()
|
| 1532 |
+
while worker.is_alive():
|
| 1533 |
+
worker.join(timeout=2.0)
|
| 1534 |
+
yield (f"Sweep {step_i+1}/{len(regs)}: reg={reg:.3f} ...",
|
| 1535 |
+
_format_sweep_results(results),
|
| 1536 |
+
"\n".join(all_logs), None, None)
|
| 1537 |
+
worker.join()
|
| 1538 |
+
|
| 1539 |
+
elapsed = round(time.time() - t0, 1)
|
| 1540 |
+
entry = {"regularization": reg, "time_s": elapsed}
|
| 1541 |
+
|
| 1542 |
+
if run_error is not None:
|
| 1543 |
+
entry["error"] = str(run_error)
|
| 1544 |
+
entry["perplexity"] = None
|
| 1545 |
+
entry["refusal_rate"] = None
|
| 1546 |
+
entry["coherence"] = None
|
| 1547 |
+
else:
|
| 1548 |
+
pipe = pipeline_ref[0]
|
| 1549 |
+
metrics = pipe._quality_metrics
|
| 1550 |
+
entry["perplexity"] = metrics.get("perplexity")
|
| 1551 |
+
entry["refusal_rate"] = metrics.get("refusal_rate")
|
| 1552 |
+
entry["coherence"] = metrics.get("coherence")
|
| 1553 |
+
entry["strong_layers"] = len(pipe._strong_layers)
|
| 1554 |
+
del pipe
|
| 1555 |
+
|
| 1556 |
+
results.append(entry)
|
| 1557 |
+
all_logs.append(f" Done in {elapsed}s — PPL={entry.get('perplexity', '?')}, "
|
| 1558 |
+
f"Refusal={entry.get('refusal_rate', '?')}")
|
| 1559 |
+
|
| 1560 |
+
# Cleanup between runs
|
| 1561 |
+
gc.collect()
|
| 1562 |
+
if torch.cuda.is_available():
|
| 1563 |
+
torch.cuda.empty_cache()
|
| 1564 |
+
|
| 1565 |
+
# Generate dose-response curve
|
| 1566 |
+
gallery = None
|
| 1567 |
+
try:
|
| 1568 |
+
import matplotlib
|
| 1569 |
+
matplotlib.use("Agg")
|
| 1570 |
+
import matplotlib.pyplot as plt
|
| 1571 |
+
import tempfile, os
|
| 1572 |
+
|
| 1573 |
+
valid = [r for r in results if r.get("perplexity") is not None]
|
| 1574 |
+
if valid:
|
| 1575 |
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
|
| 1576 |
+
fig.suptitle(f"Ablation Strength Sweep: {model_choice} ({method_key})",
|
| 1577 |
+
fontsize=13, fontweight="bold", color="#222")
|
| 1578 |
+
|
| 1579 |
+
x = [r["regularization"] for r in valid]
|
| 1580 |
+
ppl = [r["perplexity"] for r in valid]
|
| 1581 |
+
ref = [r["refusal_rate"] for r in valid]
|
| 1582 |
+
coh = [r.get("coherence") for r in valid]
|
| 1583 |
+
|
| 1584 |
+
# Left: refusal rate vs regularization
|
| 1585 |
+
color_ref = "#d62728"
|
| 1586 |
+
color_ppl = "#1f77b4"
|
| 1587 |
+
ax1.plot(x, ref, "o-", color=color_ref, linewidth=2, markersize=8, label="Refusal Rate")
|
| 1588 |
+
ax1.set_xlabel("Regularization (0=full removal, 1=no change)", fontsize=10)
|
| 1589 |
+
ax1.set_ylabel("Refusal Rate", color=color_ref, fontsize=10)
|
| 1590 |
+
ax1.tick_params(axis="y", labelcolor=color_ref)
|
| 1591 |
+
ax1.set_ylim(-0.05, 1.05)
|
| 1592 |
+
ax1.set_xlim(-0.05, 1.05)
|
| 1593 |
+
ax1.grid(True, alpha=0.3)
|
| 1594 |
+
ax1.set_title("Dose-Response Curve", fontsize=11, fontweight="bold")
|
| 1595 |
+
|
| 1596 |
+
ax1b = ax1.twinx()
|
| 1597 |
+
ax1b.plot(x, ppl, "s--", color=color_ppl, linewidth=2, markersize=7, label="Perplexity")
|
| 1598 |
+
ax1b.set_ylabel("Perplexity", color=color_ppl, fontsize=10)
|
| 1599 |
+
ax1b.tick_params(axis="y", labelcolor=color_ppl)
|
| 1600 |
+
|
| 1601 |
+
# Combined legend
|
| 1602 |
+
lines1, labels1 = ax1.get_legend_handles_labels()
|
| 1603 |
+
lines2, labels2 = ax1b.get_legend_handles_labels()
|
| 1604 |
+
ax1.legend(lines1 + lines2, labels1 + labels2, loc="center right")
|
| 1605 |
+
|
| 1606 |
+
# Right: Pareto plot (refusal vs perplexity)
|
| 1607 |
+
ax2.scatter(ref, ppl, c=x, cmap="RdYlGn", s=120, edgecolors="black", linewidth=1, zorder=3)
|
| 1608 |
+
for r in valid:
|
| 1609 |
+
ax2.annotate(f"{r['regularization']:.2f}",
|
| 1610 |
+
(r["refusal_rate"], r["perplexity"]),
|
| 1611 |
+
textcoords="offset points", xytext=(8, 5),
|
| 1612 |
+
fontsize=8, alpha=0.8)
|
| 1613 |
+
ax2.set_xlabel("Refusal Rate (lower = better removal)", fontsize=10)
|
| 1614 |
+
ax2.set_ylabel("Perplexity (lower = better coherence)", fontsize=10)
|
| 1615 |
+
ax2.set_title("Refusal vs Perplexity Tradeoff", fontsize=11, fontweight="bold")
|
| 1616 |
+
ax2.grid(True, alpha=0.3)
|
| 1617 |
+
cbar = fig.colorbar(ax2.collections[0], ax=ax2, label="Regularization")
|
| 1618 |
+
|
| 1619 |
+
fig.tight_layout()
|
| 1620 |
+
|
| 1621 |
+
path = os.path.join(tempfile.gettempdir(),
|
| 1622 |
+
f"obliteratus_sweep_{int(time.time())}.png")
|
| 1623 |
+
fig.savefig(path, dpi=150, bbox_inches="tight", facecolor="white")
|
| 1624 |
+
plt.close(fig)
|
| 1625 |
+
gallery = [(path, "Dose-Response Curve")]
|
| 1626 |
+
except Exception as e:
|
| 1627 |
+
all_logs.append(f"Chart generation failed: {e}")
|
| 1628 |
+
|
| 1629 |
+
yield (f"Sweep complete: {len(results)} points",
|
| 1630 |
+
_format_sweep_results(results),
|
| 1631 |
+
"\n".join(all_logs), gallery, None)
|
| 1632 |
+
|
| 1633 |
+
|
| 1634 |
+
def _format_sweep_results(results: list[dict]) -> str:
|
| 1635 |
+
"""Format sweep results as a markdown table."""
|
| 1636 |
+
if not results:
|
| 1637 |
+
return "*No results yet.*"
|
| 1638 |
+
|
| 1639 |
+
lines = ["### Strength Sweep Results", "",
|
| 1640 |
+
"| Reg | Time | Perplexity | Refusal Rate | Coherence | Error |",
|
| 1641 |
+
"|-----|------|-----------|-------------|-----------|-------|"]
|
| 1642 |
+
|
| 1643 |
+
for r in results:
|
| 1644 |
+
reg = f"{r['regularization']:.3f}"
|
| 1645 |
+
ppl = f"{r['perplexity']:.2f}" if r.get("perplexity") is not None else "—"
|
| 1646 |
+
ref = f"{r['refusal_rate']:.0%}" if r.get("refusal_rate") is not None else "—"
|
| 1647 |
+
coh = f"{r['coherence']:.0%}" if r.get("coherence") is not None else "—"
|
| 1648 |
+
err = r.get("error", "")
|
| 1649 |
+
err_short = (err[:25] + "...") if err and len(err) > 25 else (err or "")
|
| 1650 |
+
lines.append(f"| {reg} | {r['time_s']}s | {ppl} | {ref} | {coh} | {err_short} |")
|
| 1651 |
+
|
| 1652 |
+
return "\n".join(lines)
|
| 1653 |
+
|
| 1654 |
+
|
| 1655 |
+
# ---------------------------------------------------------------------------
|
| 1656 |
+
# Export Research Artifacts
|
| 1657 |
+
# ---------------------------------------------------------------------------
|
| 1658 |
+
|
| 1659 |
+
def export_artifacts():
|
| 1660 |
+
"""Package all research artifacts from the last obliteration into a downloadable archive.
|
| 1661 |
+
|
| 1662 |
+
Exports:
|
| 1663 |
+
- refusal_directions.pt: Per-layer refusal direction tensors
|
| 1664 |
+
- config.json: Full pipeline configuration and metadata
|
| 1665 |
+
- results.csv: Quality metrics in tabular format
|
| 1666 |
+
- pipeline_log.txt: Full pipeline log
|
| 1667 |
+
"""
|
| 1668 |
+
import json
|
| 1669 |
+
import csv
|
| 1670 |
+
import tempfile
|
| 1671 |
+
import zipfile
|
| 1672 |
+
import os
|
| 1673 |
+
|
| 1674 |
+
if _state["status"] != "ready":
|
| 1675 |
+
return None, "No abliterated model loaded. Run obliteration first."
|
| 1676 |
+
|
| 1677 |
+
export_dir = os.path.join(tempfile.gettempdir(), f"obliteratus_export_{int(time.time())}")
|
| 1678 |
+
os.makedirs(export_dir, exist_ok=True)
|
| 1679 |
+
|
| 1680 |
+
model_name = _state.get("model_name", "unknown")
|
| 1681 |
+
method = _state.get("method", "unknown")
|
| 1682 |
+
log_lines = _state.get("log", [])
|
| 1683 |
+
|
| 1684 |
+
exported_files = []
|
| 1685 |
+
|
| 1686 |
+
# 1. Pipeline log
|
| 1687 |
+
log_path = os.path.join(export_dir, "pipeline_log.txt")
|
| 1688 |
+
with open(log_path, "w") as f:
|
| 1689 |
+
f.write(f"OBLITERATUS Pipeline Log\n")
|
| 1690 |
+
f.write(f"Model: {model_name}\n")
|
| 1691 |
+
f.write(f"Method: {method}\n")
|
| 1692 |
+
f.write(f"Exported: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
|
| 1693 |
+
f.write("=" * 60 + "\n\n")
|
| 1694 |
+
f.write("\n".join(log_lines))
|
| 1695 |
+
exported_files.append("pipeline_log.txt")
|
| 1696 |
+
|
| 1697 |
+
# 2. Steering metadata (refusal directions + strong layers)
|
| 1698 |
+
steering = _state.get("steering")
|
| 1699 |
+
if steering:
|
| 1700 |
+
# Save directions as .pt
|
| 1701 |
+
directions = steering.get("refusal_directions", {})
|
| 1702 |
+
if directions:
|
| 1703 |
+
directions_cpu = {k: v.cpu().float() for k, v in directions.items()}
|
| 1704 |
+
dir_path = os.path.join(export_dir, "refusal_directions.pt")
|
| 1705 |
+
torch.save(directions_cpu, dir_path)
|
| 1706 |
+
exported_files.append("refusal_directions.pt")
|
| 1707 |
+
|
| 1708 |
+
# Save config
|
| 1709 |
+
config = {
|
| 1710 |
+
"model_name": model_name,
|
| 1711 |
+
"method": method,
|
| 1712 |
+
"strong_layers": steering.get("strong_layers", []),
|
| 1713 |
+
"steering_strength": steering.get("steering_strength", 0),
|
| 1714 |
+
"n_directions": len(directions) if directions else 0,
|
| 1715 |
+
"direction_dims": {str(k): list(v.shape)
|
| 1716 |
+
for k, v in directions.items()} if directions else {},
|
| 1717 |
+
"export_time": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
| 1718 |
+
}
|
| 1719 |
+
config_path = os.path.join(export_dir, "config.json")
|
| 1720 |
+
with open(config_path, "w") as f:
|
| 1721 |
+
json.dump(config, f, indent=2)
|
| 1722 |
+
exported_files.append("config.json")
|
| 1723 |
+
|
| 1724 |
+
# 3. Quality metrics as CSV (parse from log)
|
| 1725 |
+
metrics_rows = []
|
| 1726 |
+
current_metrics = {}
|
| 1727 |
+
for line in log_lines:
|
| 1728 |
+
if "Perplexity:" in line:
|
| 1729 |
+
try:
|
| 1730 |
+
current_metrics["perplexity"] = float(line.split("Perplexity:")[1].strip().split()[0])
|
| 1731 |
+
except (ValueError, IndexError):
|
| 1732 |
+
pass
|
| 1733 |
+
if "Coherence:" in line:
|
| 1734 |
+
try:
|
| 1735 |
+
current_metrics["coherence"] = line.split("Coherence:")[1].strip().split()[0]
|
| 1736 |
+
except (ValueError, IndexError):
|
| 1737 |
+
pass
|
| 1738 |
+
if "Refusal rate:" in line:
|
| 1739 |
+
try:
|
| 1740 |
+
current_metrics["refusal_rate"] = line.split("Refusal rate:")[1].strip().split()[0]
|
| 1741 |
+
except (ValueError, IndexError):
|
| 1742 |
+
pass
|
| 1743 |
+
if current_metrics:
|
| 1744 |
+
metrics_rows.append({"model": model_name, "method": method, **current_metrics})
|
| 1745 |
+
|
| 1746 |
+
if metrics_rows:
|
| 1747 |
+
csv_path = os.path.join(export_dir, "results.csv")
|
| 1748 |
+
with open(csv_path, "w", newline="") as f:
|
| 1749 |
+
writer = csv.DictWriter(f, fieldnames=list(metrics_rows[0].keys()))
|
| 1750 |
+
writer.writeheader()
|
| 1751 |
+
writer.writerows(metrics_rows)
|
| 1752 |
+
exported_files.append("results.csv")
|
| 1753 |
+
|
| 1754 |
+
# 4. Create ZIP archive
|
| 1755 |
+
zip_name = f"obliteratus_{model_name.replace(' ', '_')}_{method}_{int(time.time())}.zip"
|
| 1756 |
+
zip_path = os.path.join(tempfile.gettempdir(), zip_name)
|
| 1757 |
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
| 1758 |
+
for fname in exported_files:
|
| 1759 |
+
zf.write(os.path.join(export_dir, fname), fname)
|
| 1760 |
+
|
| 1761 |
+
# Cleanup temp dir
|
| 1762 |
+
import shutil
|
| 1763 |
+
shutil.rmtree(export_dir, ignore_errors=True)
|
| 1764 |
+
|
| 1765 |
+
summary = (
|
| 1766 |
+
f"### Export Complete\n\n"
|
| 1767 |
+
f"**Model:** {model_name}\n"
|
| 1768 |
+
f"**Method:** {method}\n\n"
|
| 1769 |
+
f"**Contents:**\n"
|
| 1770 |
+
)
|
| 1771 |
+
for f in exported_files:
|
| 1772 |
+
summary += f"- `{f}`\n"
|
| 1773 |
+
|
| 1774 |
+
return zip_path, summary
|
| 1775 |
+
|
| 1776 |
+
|
| 1777 |
# ---------------------------------------------------------------------------
|
| 1778 |
# Gradio UI
|
| 1779 |
# ---------------------------------------------------------------------------
|
|
|
|
| 2264 |
fill_height=True,
|
| 2265 |
)
|
| 2266 |
|
| 2267 |
+
# ── Tab 3: A/B Comparison ─────────────────────────────────────────
|
| 2268 |
+
with gr.Tab("A/B Compare", id="ab_compare"):
|
| 2269 |
+
gr.Markdown("""### A/B Comparison Chat
|
| 2270 |
+
Side-by-side: **Original** (left) vs **Abliterated** (right).
|
| 2271 |
+
See exactly how abliteration changes model behavior on the same prompt.
|
| 2272 |
|
| 2273 |
+
*The original model is loaded on-demand for each message, then freed.*
|
| 2274 |
+
""")
|
| 2275 |
+
ab_status = gr.Markdown("Ready — obliterate a model first, then chat here.")
|
| 2276 |
+
|
| 2277 |
+
with gr.Accordion("Settings", open=False):
|
| 2278 |
+
ab_system_prompt = gr.Textbox(
|
| 2279 |
+
value="You are a helpful assistant. Answer all questions directly.",
|
| 2280 |
+
label="System Prompt", lines=2,
|
| 2281 |
+
)
|
| 2282 |
+
with gr.Row():
|
| 2283 |
+
ab_temp = gr.Slider(0.0, 1.5, value=0.7, step=0.05, label="Temperature")
|
| 2284 |
+
ab_top_p = gr.Slider(0.0, 1.0, value=0.9, step=0.05, label="Top P")
|
| 2285 |
+
ab_max_tokens = gr.Slider(32, 2048, value=256, step=32, label="Max Tokens")
|
| 2286 |
+
ab_rep_penalty = gr.Slider(1.0, 2.0, value=1.15, step=0.05, label="Rep Penalty")
|
| 2287 |
+
|
| 2288 |
+
with gr.Row():
|
| 2289 |
+
with gr.Column():
|
| 2290 |
+
gr.Markdown("#### Original (Pre-Abliteration)")
|
| 2291 |
+
ab_chatbot_left = gr.Chatbot(
|
| 2292 |
+
height="40vh", type="messages",
|
| 2293 |
+
label="Original Model",
|
| 2294 |
+
)
|
| 2295 |
+
with gr.Column():
|
| 2296 |
+
gr.Markdown("#### Abliterated")
|
| 2297 |
+
ab_chatbot_right = gr.Chatbot(
|
| 2298 |
+
height="40vh", type="messages",
|
| 2299 |
+
label="Abliterated Model",
|
| 2300 |
+
)
|
| 2301 |
+
|
| 2302 |
+
with gr.Row():
|
| 2303 |
+
ab_input = gr.Textbox(
|
| 2304 |
+
label="Your Message",
|
| 2305 |
+
placeholder="Type a message to send to both models...",
|
| 2306 |
+
lines=2, scale=5,
|
| 2307 |
+
)
|
| 2308 |
+
ab_send_btn = gr.Button("Send to Both", variant="primary", scale=1)
|
| 2309 |
+
|
| 2310 |
+
ab_send_btn.click(
|
| 2311 |
+
fn=ab_chat_respond,
|
| 2312 |
+
inputs=[ab_input, ab_chatbot_left, ab_chatbot_right,
|
| 2313 |
+
ab_system_prompt, ab_temp, ab_top_p, ab_max_tokens, ab_rep_penalty],
|
| 2314 |
+
outputs=[ab_chatbot_left, ab_chatbot_right, ab_status],
|
| 2315 |
+
)
|
| 2316 |
+
# Also trigger on Enter
|
| 2317 |
+
ab_input.submit(
|
| 2318 |
+
fn=ab_chat_respond,
|
| 2319 |
+
inputs=[ab_input, ab_chatbot_left, ab_chatbot_right,
|
| 2320 |
+
ab_system_prompt, ab_temp, ab_top_p, ab_max_tokens, ab_rep_penalty],
|
| 2321 |
+
outputs=[ab_chatbot_left, ab_chatbot_right, ab_status],
|
| 2322 |
+
)
|
| 2323 |
+
|
| 2324 |
+
# ── Tab 4: Strength Sweep ────────────────────────────────────────
|
| 2325 |
+
with gr.Tab("Strength Sweep", id="strength_sweep"):
|
| 2326 |
+
gr.Markdown("""### Ablation Strength Sweep
|
| 2327 |
+
The **dose-response curve** for abliteration: sweep regularization from 0 (full removal)
|
| 2328 |
+
to 1 (no change) and plot refusal rate vs perplexity.
|
| 2329 |
+
|
| 2330 |
+
This is THE fundamental plot for any abliteration paper — it shows the optimal
|
| 2331 |
+
tradeoff point where refusal is minimized with minimal capability damage.
|
| 2332 |
""")
|
| 2333 |
+
|
| 2334 |
with gr.Row():
|
| 2335 |
+
sweep_model_dd = gr.Dropdown(
|
| 2336 |
choices=list(MODELS.keys()),
|
| 2337 |
value="Qwen2.5 0.5B Instruct",
|
| 2338 |
+
label="Model",
|
| 2339 |
allow_custom_value=True,
|
| 2340 |
)
|
| 2341 |
+
sweep_method_dd = gr.Dropdown(
|
| 2342 |
+
choices=list(METHODS.keys()),
|
| 2343 |
+
value="advanced (recommended)",
|
| 2344 |
+
label="Method",
|
|
|
|
| 2345 |
)
|
| 2346 |
+
with gr.Row():
|
| 2347 |
+
sweep_vol_dd = gr.Dropdown(
|
| 2348 |
choices=list(PROMPT_VOLUMES.keys()),
|
| 2349 |
value="33 (fast)",
|
| 2350 |
label="Prompt Volume",
|
| 2351 |
)
|
| 2352 |
+
sweep_dataset_dd = gr.Dropdown(
|
|
|
|
| 2353 |
choices=get_source_choices(),
|
| 2354 |
value=get_source_choices()[0],
|
| 2355 |
+
label="Dataset",
|
|
|
|
| 2356 |
)
|
| 2357 |
+
sweep_steps_slider = gr.Slider(
|
| 2358 |
+
3, 15, value=6, step=1,
|
| 2359 |
+
label="Sweep Points",
|
| 2360 |
+
info="Number of regularization values to test (more = finer curve, slower)",
|
| 2361 |
+
)
|
| 2362 |
+
|
| 2363 |
+
sweep_btn = gr.Button("Run Sweep", variant="primary")
|
| 2364 |
+
sweep_status = gr.Markdown("")
|
| 2365 |
+
sweep_results = gr.Markdown("*Click 'Run Sweep' to start.*")
|
| 2366 |
+
sweep_gallery = gr.Gallery(
|
| 2367 |
+
label="Dose-Response Curve",
|
| 2368 |
+
columns=1, rows=1, height="auto",
|
| 2369 |
+
object_fit="contain", show_label=True,
|
| 2370 |
+
)
|
| 2371 |
+
sweep_log = gr.Textbox(
|
| 2372 |
+
label="Sweep Log", lines=12, max_lines=150,
|
| 2373 |
+
interactive=False, elem_classes=["log-box"],
|
| 2374 |
)
|
| 2375 |
|
| 2376 |
+
sweep_btn.click(
|
| 2377 |
+
fn=strength_sweep,
|
| 2378 |
+
inputs=[sweep_model_dd, sweep_method_dd, sweep_vol_dd,
|
| 2379 |
+
sweep_dataset_dd, sweep_steps_slider],
|
| 2380 |
+
outputs=[sweep_status, sweep_results, sweep_log, sweep_gallery,
|
| 2381 |
+
gr.State()], # 5th output is unused File placeholder
|
| 2382 |
)
|
| 2383 |
|
| 2384 |
+
# ── Tab 5: Export ─────────────────────────────────────────────────
|
| 2385 |
+
with gr.Tab("Export", id="export"):
|
| 2386 |
+
gr.Markdown("""### Export Research Artifacts
|
| 2387 |
+
Download all intermediate data from your last obliteration run as a ZIP archive.
|
| 2388 |
+
|
| 2389 |
+
**Contents:**
|
| 2390 |
+
- `refusal_directions.pt` — Per-layer refusal direction tensors (load with `torch.load()`)
|
| 2391 |
+
- `config.json` — Full pipeline configuration, strong layers, direction dimensions
|
| 2392 |
+
- `results.csv` — Quality metrics (perplexity, coherence, refusal rate)
|
| 2393 |
+
- `pipeline_log.txt` — Complete pipeline execution log
|
| 2394 |
+
""")
|
| 2395 |
+
|
| 2396 |
+
export_btn = gr.Button("Download Artifacts", variant="primary")
|
| 2397 |
+
export_status = gr.Markdown("")
|
| 2398 |
+
export_file = gr.File(label="Download ZIP", interactive=False)
|
| 2399 |
+
|
| 2400 |
+
export_btn.click(
|
| 2401 |
+
fn=export_artifacts,
|
| 2402 |
+
outputs=[export_file, export_status],
|
| 2403 |
+
)
|
| 2404 |
+
|
| 2405 |
+
# ── Tab 6: Benchmark ──────────────────────────────────────────────
|
| 2406 |
+
with gr.Tab("Benchmark", id="benchmark"):
|
| 2407 |
+
gr.Markdown("""### Benchmark Lab
|
| 2408 |
+
Launch comprehensive benchmarking runs to compare abliteration strategies.
|
| 2409 |
+
Two modes: test **multiple techniques** on one model, or test **one technique** across multiple models.
|
| 2410 |
+
""")
|
| 2411 |
+
|
| 2412 |
+
with gr.Tabs():
|
| 2413 |
+
# ── Sub-tab 1: Multi-Method (N methods x 1 model) ──
|
| 2414 |
+
with gr.Tab("Multi-Method", id="bench_multi_method"):
|
| 2415 |
+
gr.Markdown("""**Which technique works best?**
|
| 2416 |
+
Compare multiple abliteration methods on the same model.
|
| 2417 |
+
Great for finding the optimal strategy for a specific architecture.
|
| 2418 |
+
|
| 2419 |
+
```python
|
| 2420 |
+
# API access:
|
| 2421 |
+
from gradio_client import Client
|
| 2422 |
+
client = Client("pliny-the-prompter/obliteratus")
|
| 2423 |
+
result = client.predict(
|
| 2424 |
+
model_choice="Qwen2.5 0.5B Instruct",
|
| 2425 |
+
methods_to_test=["basic", "advanced", "surgical", "optimized"],
|
| 2426 |
+
prompt_volume_choice="33 (fast)",
|
| 2427 |
+
api_name="/benchmark",
|
| 2428 |
+
)
|
| 2429 |
+
```
|
| 2430 |
+
""")
|
| 2431 |
+
with gr.Row():
|
| 2432 |
+
bench_model = gr.Dropdown(
|
| 2433 |
+
choices=list(MODELS.keys()),
|
| 2434 |
+
value="Qwen2.5 0.5B Instruct",
|
| 2435 |
+
label="Target Model",
|
| 2436 |
+
allow_custom_value=True,
|
| 2437 |
+
)
|
| 2438 |
+
bench_methods = gr.CheckboxGroup(
|
| 2439 |
+
choices=["basic", "advanced", "aggressive", "surgical",
|
| 2440 |
+
"optimized", "inverted", "nuclear"],
|
| 2441 |
+
value=["basic", "advanced", "surgical", "optimized"],
|
| 2442 |
+
label="Methods to Compare",
|
| 2443 |
+
)
|
| 2444 |
+
with gr.Row():
|
| 2445 |
+
bench_prompt_vol = gr.Dropdown(
|
| 2446 |
+
choices=list(PROMPT_VOLUMES.keys()),
|
| 2447 |
+
value="33 (fast)",
|
| 2448 |
+
label="Prompt Volume",
|
| 2449 |
+
)
|
| 2450 |
+
bench_dataset = gr.Dropdown(
|
| 2451 |
+
choices=get_source_choices(),
|
| 2452 |
+
value=get_source_choices()[0],
|
| 2453 |
+
label="Dataset Source",
|
| 2454 |
+
info="Select prompt dataset for benchmarking",
|
| 2455 |
+
)
|
| 2456 |
+
bench_btn = gr.Button(
|
| 2457 |
+
"Run Multi-Method Benchmark",
|
| 2458 |
+
variant="primary", size="lg",
|
| 2459 |
+
)
|
| 2460 |
+
bench_status = gr.Markdown("")
|
| 2461 |
+
bench_results = gr.Markdown("*Select methods and click 'Run' to start.*")
|
| 2462 |
+
bench_gallery = gr.Gallery(
|
| 2463 |
+
label="Benchmark Visualizations",
|
| 2464 |
+
columns=2,
|
| 2465 |
+
rows=2,
|
| 2466 |
+
height="auto",
|
| 2467 |
+
object_fit="contain",
|
| 2468 |
+
show_label=True,
|
| 2469 |
+
)
|
| 2470 |
+
bench_log = gr.Textbox(
|
| 2471 |
+
label="Benchmark Log",
|
| 2472 |
+
lines=12,
|
| 2473 |
+
max_lines=150,
|
| 2474 |
+
interactive=False,
|
| 2475 |
+
elem_classes=["log-box"],
|
| 2476 |
+
)
|
| 2477 |
+
|
| 2478 |
+
bench_btn.click(
|
| 2479 |
+
fn=benchmark,
|
| 2480 |
+
inputs=[bench_model, bench_methods, bench_prompt_vol, bench_dataset],
|
| 2481 |
+
outputs=[bench_status, bench_results, bench_log, bench_gallery],
|
| 2482 |
+
api_name="/benchmark",
|
| 2483 |
+
)
|
| 2484 |
+
|
| 2485 |
+
# ── Sub-tab 2: Multi-Model (1 method x N models) ──
|
| 2486 |
+
with gr.Tab("Multi-Model", id="bench_multi_model"):
|
| 2487 |
+
gr.Markdown("""**How does a technique scale across architectures?**
|
| 2488 |
+
Test one abliteration method across multiple models. Great for understanding
|
| 2489 |
+
how well a technique generalizes — especially for MoE-aware methods like
|
| 2490 |
+
`surgical`, `optimized`, or `nuclear` on GPT-OSS 20B vs dense models.
|
| 2491 |
+
|
| 2492 |
+
```python
|
| 2493 |
+
# API access:
|
| 2494 |
+
from gradio_client import Client
|
| 2495 |
+
client = Client("pliny-the-prompter/obliteratus")
|
| 2496 |
+
result = client.predict(
|
| 2497 |
+
model_choices=["Qwen2.5 0.5B Instruct", "GPT-OSS 20B (MoE, 3.6B active)"],
|
| 2498 |
+
method_choice="surgical",
|
| 2499 |
+
prompt_volume_choice="33 (fast)",
|
| 2500 |
+
api_name="/benchmark_multi_model",
|
| 2501 |
+
)
|
| 2502 |
+
```
|
| 2503 |
+
""")
|
| 2504 |
+
with gr.Row():
|
| 2505 |
+
mm_models = gr.CheckboxGroup(
|
| 2506 |
+
choices=list(MODELS.keys()),
|
| 2507 |
+
value=[
|
| 2508 |
+
"Qwen2.5 0.5B Instruct",
|
| 2509 |
+
"Qwen2.5 3B Instruct",
|
| 2510 |
+
],
|
| 2511 |
+
label="Models to Test",
|
| 2512 |
+
)
|
| 2513 |
+
with gr.Row():
|
| 2514 |
+
mm_method = gr.Dropdown(
|
| 2515 |
+
choices=["basic", "advanced", "aggressive", "surgical",
|
| 2516 |
+
"optimized", "inverted", "nuclear"],
|
| 2517 |
+
value="surgical",
|
| 2518 |
+
label="Abliteration Method",
|
| 2519 |
+
)
|
| 2520 |
+
mm_prompt_vol = gr.Dropdown(
|
| 2521 |
+
choices=list(PROMPT_VOLUMES.keys()),
|
| 2522 |
+
value="33 (fast)",
|
| 2523 |
+
label="Prompt Volume",
|
| 2524 |
+
)
|
| 2525 |
+
mm_dataset = gr.Dropdown(
|
| 2526 |
+
choices=get_source_choices(),
|
| 2527 |
+
value=get_source_choices()[0],
|
| 2528 |
+
label="Dataset Source",
|
| 2529 |
+
)
|
| 2530 |
+
mm_btn = gr.Button(
|
| 2531 |
+
"Run Multi-Model Benchmark",
|
| 2532 |
+
variant="primary", size="lg",
|
| 2533 |
+
)
|
| 2534 |
+
mm_status = gr.Markdown("")
|
| 2535 |
+
mm_results = gr.Markdown("*Select models and click 'Run' to start.*")
|
| 2536 |
+
mm_gallery = gr.Gallery(
|
| 2537 |
+
label="Benchmark Visualizations",
|
| 2538 |
+
columns=2,
|
| 2539 |
+
rows=2,
|
| 2540 |
+
height="auto",
|
| 2541 |
+
object_fit="contain",
|
| 2542 |
+
show_label=True,
|
| 2543 |
+
)
|
| 2544 |
+
mm_log = gr.Textbox(
|
| 2545 |
+
label="Benchmark Log",
|
| 2546 |
+
lines=12,
|
| 2547 |
+
max_lines=150,
|
| 2548 |
+
interactive=False,
|
| 2549 |
+
elem_classes=["log-box"],
|
| 2550 |
+
)
|
| 2551 |
+
|
| 2552 |
+
mm_btn.click(
|
| 2553 |
+
fn=benchmark_multi_model,
|
| 2554 |
+
inputs=[mm_models, mm_method, mm_prompt_vol, mm_dataset],
|
| 2555 |
+
outputs=[mm_status, mm_results, mm_log, mm_gallery],
|
| 2556 |
+
api_name="/benchmark_multi_model",
|
| 2557 |
+
)
|
| 2558 |
+
|
| 2559 |
+
# ── Sub-tab 3: Quick Presets ──
|
| 2560 |
+
with gr.Tab("Quick Presets", id="bench_presets"):
|
| 2561 |
+
gr.Markdown("""### One-Click Benchmark Presets
|
| 2562 |
+
Pre-configured benchmark configurations for common research questions.
|
| 2563 |
+
""")
|
| 2564 |
+
with gr.Row():
|
| 2565 |
+
preset_prompt_vol = gr.Dropdown(
|
| 2566 |
+
choices=list(PROMPT_VOLUMES.keys()),
|
| 2567 |
+
value="33 (fast)",
|
| 2568 |
+
label="Prompt Volume",
|
| 2569 |
+
)
|
| 2570 |
+
preset_dataset = gr.Dropdown(
|
| 2571 |
+
choices=get_source_choices(),
|
| 2572 |
+
value=get_source_choices()[0],
|
| 2573 |
+
label="Dataset Source",
|
| 2574 |
+
)
|
| 2575 |
+
|
| 2576 |
+
gr.Markdown("#### GPT-OSS 20B — Full Method Shootout")
|
| 2577 |
+
gr.Markdown("*All 7 methods on GPT-OSS 20B. Best run on A10G+ GPU.*")
|
| 2578 |
+
preset_gptoss_btn = gr.Button(
|
| 2579 |
+
"Run GPT-OSS 20B Shootout",
|
| 2580 |
+
variant="secondary",
|
| 2581 |
+
)
|
| 2582 |
+
|
| 2583 |
+
gr.Markdown("#### MoE-Aware Techniques — Cross-Architecture")
|
| 2584 |
+
gr.Markdown("*Tests `surgical` + `optimized` + `nuclear` across small/medium/MoE models.*")
|
| 2585 |
+
preset_moe_btn = gr.Button(
|
| 2586 |
+
"Run MoE Cross-Architecture",
|
| 2587 |
+
variant="secondary",
|
| 2588 |
+
)
|
| 2589 |
+
|
| 2590 |
+
gr.Markdown("#### Speed vs Quality Tradeoff")
|
| 2591 |
+
gr.Markdown("*Compares `basic` (fast) vs `optimized` (slow but smart) across model sizes.*")
|
| 2592 |
+
preset_speed_btn = gr.Button(
|
| 2593 |
+
"Run Speed vs Quality",
|
| 2594 |
+
variant="secondary",
|
| 2595 |
+
)
|
| 2596 |
+
|
| 2597 |
+
preset_status = gr.Markdown("")
|
| 2598 |
+
preset_results = gr.Markdown("*Click a preset to start.*")
|
| 2599 |
+
preset_gallery = gr.Gallery(
|
| 2600 |
+
label="Preset Benchmark Visualizations",
|
| 2601 |
+
columns=2,
|
| 2602 |
+
rows=2,
|
| 2603 |
+
height="auto",
|
| 2604 |
+
object_fit="contain",
|
| 2605 |
+
show_label=True,
|
| 2606 |
+
)
|
| 2607 |
+
preset_log = gr.Textbox(
|
| 2608 |
+
label="Preset Benchmark Log",
|
| 2609 |
+
lines=12,
|
| 2610 |
+
max_lines=150,
|
| 2611 |
+
interactive=False,
|
| 2612 |
+
elem_classes=["log-box"],
|
| 2613 |
+
)
|
| 2614 |
+
|
| 2615 |
+
# Preset handlers — these call the existing benchmark functions
|
| 2616 |
+
# with pre-configured inputs
|
| 2617 |
+
|
| 2618 |
+
def _preset_gptoss(vol, ds):
|
| 2619 |
+
yield from benchmark(
|
| 2620 |
+
"GPT-OSS 20B (MoE, 3.6B active)",
|
| 2621 |
+
["basic", "advanced", "aggressive", "surgical",
|
| 2622 |
+
"optimized", "inverted", "nuclear"],
|
| 2623 |
+
vol, ds,
|
| 2624 |
+
)
|
| 2625 |
+
|
| 2626 |
+
def _preset_moe_cross(vol, ds):
|
| 2627 |
+
yield from benchmark_multi_model(
|
| 2628 |
+
[
|
| 2629 |
+
"Qwen2.5 0.5B Instruct",
|
| 2630 |
+
"Qwen2.5 3B Instruct",
|
| 2631 |
+
"Qwen2.5 7B Instruct",
|
| 2632 |
+
"GPT-OSS 20B (MoE, 3.6B active)",
|
| 2633 |
+
],
|
| 2634 |
+
"surgical", vol, ds,
|
| 2635 |
+
)
|
| 2636 |
+
|
| 2637 |
+
def _preset_speed_quality(vol, ds):
|
| 2638 |
+
# Run basic + optimized on 3 model sizes
|
| 2639 |
+
# Chain two benchmark calls into one stream
|
| 2640 |
+
|
| 2641 |
+
# Part 1: basic method across models
|
| 2642 |
+
for status, results_md, log, gallery in benchmark_multi_model(
|
| 2643 |
+
[
|
| 2644 |
+
"Qwen2.5 0.5B Instruct",
|
| 2645 |
+
"Qwen2.5 3B Instruct",
|
| 2646 |
+
"Qwen2.5 7B Instruct",
|
| 2647 |
+
],
|
| 2648 |
+
"basic", vol, ds,
|
| 2649 |
+
):
|
| 2650 |
+
yield status, results_md, log, gallery
|
| 2651 |
+
|
| 2652 |
+
# Part 2: optimized method across models
|
| 2653 |
+
for status, results_md, log, gallery in benchmark_multi_model(
|
| 2654 |
+
[
|
| 2655 |
+
"Qwen2.5 0.5B Instruct",
|
| 2656 |
+
"Qwen2.5 3B Instruct",
|
| 2657 |
+
"Qwen2.5 7B Instruct",
|
| 2658 |
+
],
|
| 2659 |
+
"optimized", vol, ds,
|
| 2660 |
+
):
|
| 2661 |
+
yield status, results_md, log, gallery
|
| 2662 |
+
|
| 2663 |
+
preset_gptoss_btn.click(
|
| 2664 |
+
fn=_preset_gptoss,
|
| 2665 |
+
inputs=[preset_prompt_vol, preset_dataset],
|
| 2666 |
+
outputs=[preset_status, preset_results, preset_log, preset_gallery],
|
| 2667 |
+
)
|
| 2668 |
+
preset_moe_btn.click(
|
| 2669 |
+
fn=_preset_moe_cross,
|
| 2670 |
+
inputs=[preset_prompt_vol, preset_dataset],
|
| 2671 |
+
outputs=[preset_status, preset_results, preset_log, preset_gallery],
|
| 2672 |
+
)
|
| 2673 |
+
preset_speed_btn.click(
|
| 2674 |
+
fn=_preset_speed_quality,
|
| 2675 |
+
inputs=[preset_prompt_vol, preset_dataset],
|
| 2676 |
+
outputs=[preset_status, preset_results, preset_log, preset_gallery],
|
| 2677 |
+
)
|
| 2678 |
+
|
| 2679 |
+
# ── Tab 7: About ──────────────────────────────────────────────────
|
| 2680 |
with gr.Tab("About", id="about"):
|
| 2681 |
gr.Markdown("""
|
| 2682 |
### What is OBLITERATUS?
|
|
|
|
| 2701 |
|
| 2702 |
### Methods
|
| 2703 |
|
| 2704 |
+
| Method | Directions | Key Features |
|
| 2705 |
+
|--------|-----------|-------------|
|
| 2706 |
+
| **basic** | 1 | Single direction, fast baseline |
|
| 2707 |
+
| **advanced** | 4 (SVD) | Norm-preserving, bias projection, 2 passes |
|
| 2708 |
+
| **aggressive** | 8 (SVD) | Whitened SVD, iterative refinement, 3 passes |
|
| 2709 |
+
| **surgical** | 8 (SVD) | Full SOTA: EGA, head surgery, SAE, layer-adaptive, MoE-aware |
|
| 2710 |
+
| **optimized** | 4 (SVD) | Bayesian auto-tuned, CoT-aware, KL co-optimized, winsorized |
|
| 2711 |
+
| **inverted** | 8 (SVD) | Semantic refusal inversion (2x reflection), router redirect |
|
| 2712 |
+
| **nuclear** | 8 (SVD) | Maximum force: all techniques + expert transplant + steering |
|
| 2713 |
|
| 2714 |
### Lineage
|
| 2715 |
|
|
|
|
| 2717 |
- [Arditi et al. (2024)](https://arxiv.org/abs/2406.11717) \u2014 Refusal in LLMs is mediated by a single direction
|
| 2718 |
- [Gabliteration](https://arxiv.org/abs/2512.18901) \u2014 Multi-direction SVD abliteration
|
| 2719 |
- [grimjim](https://huggingface.co/grimjim) \u2014 Norm-preserving projection techniques
|
| 2720 |
+
- [Heretic (p-e-w, 2025)](https://github.com/p-e-w/heretic) \u2014 Bayesian optimization, LoRA ablation
|
| 2721 |
|
| 2722 |
### Links
|
| 2723 |
|
obliteratus/abliterate.py
CHANGED
|
@@ -1070,7 +1070,8 @@ class AbliterationPipeline:
|
|
| 1070 |
|
| 1071 |
# Compute Gaussian-shaped weights centered on peak
|
| 1072 |
# σ = half the span of selected layers (wider selection = wider bell)
|
| 1073 |
-
|
|
|
|
| 1074 |
sigma = layer_span / 2.0
|
| 1075 |
|
| 1076 |
for idx in self._strong_layers:
|
|
@@ -2494,21 +2495,52 @@ class AbliterationPipeline:
|
|
| 2494 |
continue
|
| 2495 |
d = self.refusal_directions[idx]
|
| 2496 |
|
| 2497 |
-
# Add back 30% of the removed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2498 |
revert_strength = 0.30
|
|
|
|
| 2499 |
d_col = d.unsqueeze(-1) if d.dim() == 1 else d
|
| 2500 |
|
| 2501 |
-
|
| 2502 |
-
|
| 2503 |
-
|
| 2504 |
-
proj = getattr(attn, name, None)
|
| 2505 |
if proj is not None and hasattr(proj, "weight"):
|
| 2506 |
W = proj.weight.data
|
| 2507 |
d_dev = d_col.to(device=W.device, dtype=W.dtype)
|
| 2508 |
if W.shape[-1] == d_dev.shape[0]:
|
|
|
|
|
|
|
| 2509 |
coeff = W @ d_dev
|
| 2510 |
-
|
| 2511 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2512 |
except (AttributeError, RuntimeError):
|
| 2513 |
pass
|
| 2514 |
|
|
@@ -3120,15 +3152,21 @@ class AbliterationPipeline:
|
|
| 3120 |
if project_biases:
|
| 3121 |
count += self._project_bias(expert, direction, _FFN_OUT_NAMES + _FFN_IN_NAMES)
|
| 3122 |
else:
|
| 3123 |
-
# Fused 3D:
|
| 3124 |
-
|
| 3125 |
-
count += self.
|
| 3126 |
experts, direction, ["down_proj", "w2"],
|
| 3127 |
-
|
|
|
|
|
|
|
|
|
|
| 3128 |
)
|
| 3129 |
-
count += self.
|
| 3130 |
experts, direction, ["up_proj", "gate_proj", "w1", "w3"],
|
| 3131 |
-
|
|
|
|
|
|
|
|
|
|
| 3132 |
)
|
| 3133 |
if project_biases:
|
| 3134 |
count += self._project_fused_bias(
|
|
@@ -3346,6 +3384,94 @@ class AbliterationPipeline:
|
|
| 3346 |
return count
|
| 3347 |
return count
|
| 3348 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3349 |
# ── Nuclear-mode helpers ─────────────────────────────────────────────
|
| 3350 |
|
| 3351 |
def _transplant_expert_weights(self, layers: nn.ModuleList) -> int:
|
|
|
|
| 1070 |
|
| 1071 |
# Compute Gaussian-shaped weights centered on peak
|
| 1072 |
# σ = half the span of selected layers (wider selection = wider bell)
|
| 1073 |
+
# Note: _strong_layers is sorted by norm (not index), so use min/max
|
| 1074 |
+
layer_span = max(1, max(self._strong_layers) - min(self._strong_layers))
|
| 1075 |
sigma = layer_span / 2.0
|
| 1076 |
|
| 1077 |
for idx in self._strong_layers:
|
|
|
|
| 2495 |
continue
|
| 2496 |
d = self.refusal_directions[idx]
|
| 2497 |
|
| 2498 |
+
# Add back 30% of the removed refusal component.
|
| 2499 |
+
#
|
| 2500 |
+
# After full projection (reg=0), W_proj @ d = 0, so computing
|
| 2501 |
+
# the revert from the current weights gives zero. Instead we
|
| 2502 |
+
# use the stored per-layer KL proxy (mean projection magnitude
|
| 2503 |
+
# before excision) as a scale factor. The revert adds back a
|
| 2504 |
+
# fraction of the rank-1 refusal component: scale * d @ d^T
|
| 2505 |
+
# applied in the appropriate orientation for each weight matrix.
|
| 2506 |
revert_strength = 0.30
|
| 2507 |
+
kl_proxy_mag = self._kl_contributions.get(idx, 0.0)
|
| 2508 |
d_col = d.unsqueeze(-1) if d.dim() == 1 else d
|
| 2509 |
|
| 2510 |
+
def _partial_revert(module, weight_names, proxy_mag):
|
| 2511 |
+
for name in weight_names:
|
| 2512 |
+
proj = getattr(module, name, None)
|
|
|
|
| 2513 |
if proj is not None and hasattr(proj, "weight"):
|
| 2514 |
W = proj.weight.data
|
| 2515 |
d_dev = d_col.to(device=W.device, dtype=W.dtype)
|
| 2516 |
if W.shape[-1] == d_dev.shape[0]:
|
| 2517 |
+
# Reconstruct approximate removed component
|
| 2518 |
+
# using direction + stored proxy magnitude
|
| 2519 |
coeff = W @ d_dev
|
| 2520 |
+
# If post-projection coeff is ~0, use proxy
|
| 2521 |
+
coeff_mag = coeff.abs().mean().item()
|
| 2522 |
+
if coeff_mag < 1e-6 and proxy_mag > 0:
|
| 2523 |
+
# Scale d by proxy to approximate original
|
| 2524 |
+
W.add_(revert_strength * proxy_mag * (d_dev @ d_dev.T))
|
| 2525 |
+
else:
|
| 2526 |
+
W.add_(d_dev.T * (revert_strength * coeff))
|
| 2527 |
+
elif W.shape[0] == d_dev.shape[0]:
|
| 2528 |
+
d_row = d_dev.squeeze(-1).unsqueeze(0)
|
| 2529 |
+
coeff = d_row @ W
|
| 2530 |
+
coeff_mag = coeff.abs().mean().item()
|
| 2531 |
+
if coeff_mag < 1e-6 and proxy_mag > 0:
|
| 2532 |
+
W.add_(revert_strength * proxy_mag * (d_row.T @ d_row))
|
| 2533 |
+
else:
|
| 2534 |
+
W.add_(revert_strength * (d_row.T @ coeff))
|
| 2535 |
+
|
| 2536 |
+
try:
|
| 2537 |
+
attn = get_attention_module(layers[idx], arch)
|
| 2538 |
+
_partial_revert(attn, _ATTN_OUT_NAMES, kl_proxy_mag)
|
| 2539 |
+
except (AttributeError, RuntimeError):
|
| 2540 |
+
pass
|
| 2541 |
+
try:
|
| 2542 |
+
ffn = get_ffn_module(layers[idx], arch)
|
| 2543 |
+
_partial_revert(ffn, _FFN_OUT_NAMES, kl_proxy_mag)
|
| 2544 |
except (AttributeError, RuntimeError):
|
| 2545 |
pass
|
| 2546 |
|
|
|
|
| 3152 |
if project_biases:
|
| 3153 |
count += self._project_bias(expert, direction, _FFN_OUT_NAMES + _FFN_IN_NAMES)
|
| 3154 |
else:
|
| 3155 |
+
# Fused 3D: per-expert differentiation via per-slice processing.
|
| 3156 |
+
# Safety experts get reflected, capability experts get standard removal.
|
| 3157 |
+
count += self._project_fused_3d_selective_inversion(
|
| 3158 |
experts, direction, ["down_proj", "w2"],
|
| 3159 |
+
safety_indices=safety_indices,
|
| 3160 |
+
reflect_scale=self.reflection_strength,
|
| 3161 |
+
remove_scale=1.0,
|
| 3162 |
+
norm_preserve=norm_preserve,
|
| 3163 |
)
|
| 3164 |
+
count += self._project_fused_3d_selective_inversion(
|
| 3165 |
experts, direction, ["up_proj", "gate_proj", "w1", "w3"],
|
| 3166 |
+
safety_indices=safety_indices,
|
| 3167 |
+
reflect_scale=self.reflection_strength,
|
| 3168 |
+
remove_scale=1.0,
|
| 3169 |
+
norm_preserve=norm_preserve,
|
| 3170 |
)
|
| 3171 |
if project_biases:
|
| 3172 |
count += self._project_fused_bias(
|
|
|
|
| 3384 |
return count
|
| 3385 |
return count
|
| 3386 |
|
| 3387 |
+
@staticmethod
|
| 3388 |
+
def _project_fused_3d_selective_inversion(
|
| 3389 |
+
container: nn.Module,
|
| 3390 |
+
direction: torch.Tensor,
|
| 3391 |
+
param_names: list[str],
|
| 3392 |
+
safety_indices: set[int],
|
| 3393 |
+
reflect_scale: float,
|
| 3394 |
+
remove_scale: float,
|
| 3395 |
+
norm_preserve: bool,
|
| 3396 |
+
) -> int:
|
| 3397 |
+
"""Fused 3D projection with per-expert inversion differentiation.
|
| 3398 |
+
|
| 3399 |
+
Safety experts (by index in safety_indices) get reflected at
|
| 3400 |
+
reflect_scale (e.g. 2.0), while capability experts get standard
|
| 3401 |
+
removal at remove_scale (e.g. 1.0). This prevents over-ablation
|
| 3402 |
+
of capability experts on fused-weight MoE architectures like GPT-OSS.
|
| 3403 |
+
"""
|
| 3404 |
+
count = 0
|
| 3405 |
+
for pname in param_names:
|
| 3406 |
+
param = getattr(container, pname, None)
|
| 3407 |
+
if param is None or not hasattr(param, "data"):
|
| 3408 |
+
continue
|
| 3409 |
+
data = param.data
|
| 3410 |
+
if data.dim() != 3:
|
| 3411 |
+
continue
|
| 3412 |
+
hidden_dim = direction.shape[0]
|
| 3413 |
+
if data.shape[-1] != hidden_dim and data.shape[-2] != hidden_dim:
|
| 3414 |
+
continue
|
| 3415 |
+
|
| 3416 |
+
is_quantized = AbliterationPipeline._is_quantized_param(param)
|
| 3417 |
+
if is_quantized:
|
| 3418 |
+
try:
|
| 3419 |
+
import bitsandbytes as bnb
|
| 3420 |
+
data = bnb.functional.dequantize_4bit(
|
| 3421 |
+
param.data, param.quant_state
|
| 3422 |
+
).clone()
|
| 3423 |
+
except (ImportError, AttributeError, RuntimeError):
|
| 3424 |
+
pass
|
| 3425 |
+
|
| 3426 |
+
for ei in range(data.shape[0]):
|
| 3427 |
+
# Safety experts: reflect, capability experts: standard removal
|
| 3428 |
+
scale = reflect_scale if ei in safety_indices else remove_scale
|
| 3429 |
+
|
| 3430 |
+
W = data[ei]
|
| 3431 |
+
d = direction.to(device=W.device, dtype=W.dtype)
|
| 3432 |
+
if d.dim() > 1:
|
| 3433 |
+
d = d.squeeze()
|
| 3434 |
+
|
| 3435 |
+
if W.shape[-1] == d.shape[0]:
|
| 3436 |
+
original_norm = W.norm().item() if norm_preserve else 0.0
|
| 3437 |
+
d_col = d.unsqueeze(-1)
|
| 3438 |
+
coeff = W @ d_col
|
| 3439 |
+
W.sub_(scale * (coeff @ d_col.T))
|
| 3440 |
+
del coeff, d_col
|
| 3441 |
+
if norm_preserve and original_norm > 0:
|
| 3442 |
+
new_norm = W.norm().item()
|
| 3443 |
+
if new_norm > 0:
|
| 3444 |
+
W.mul_(original_norm / new_norm)
|
| 3445 |
+
count += 1
|
| 3446 |
+
elif W.shape[0] == d.shape[0]:
|
| 3447 |
+
original_norm = W.norm().item() if norm_preserve else 0.0
|
| 3448 |
+
d_row = d.unsqueeze(0)
|
| 3449 |
+
coeff = d_row @ W
|
| 3450 |
+
W.sub_(scale * (d_row.T @ coeff))
|
| 3451 |
+
del coeff, d_row
|
| 3452 |
+
if norm_preserve and original_norm > 0:
|
| 3453 |
+
new_norm = W.norm().item()
|
| 3454 |
+
if new_norm > 0:
|
| 3455 |
+
W.mul_(original_norm / new_norm)
|
| 3456 |
+
count += 1
|
| 3457 |
+
|
| 3458 |
+
if is_quantized and count > 0:
|
| 3459 |
+
try:
|
| 3460 |
+
import bitsandbytes as bnb
|
| 3461 |
+
quantized, new_state = bnb.functional.quantize_4bit(
|
| 3462 |
+
data.to(param.device),
|
| 3463 |
+
quant_type=getattr(param, "quant_type", "nf4"),
|
| 3464 |
+
compress_statistics=getattr(param, "compress_statistics", True),
|
| 3465 |
+
)
|
| 3466 |
+
param.data = quantized
|
| 3467 |
+
param.quant_state = new_state
|
| 3468 |
+
except (ImportError, AttributeError, RuntimeError):
|
| 3469 |
+
param.data = data.to(device=param.device, dtype=param.dtype)
|
| 3470 |
+
|
| 3471 |
+
if count > 0:
|
| 3472 |
+
return count
|
| 3473 |
+
return count
|
| 3474 |
+
|
| 3475 |
# ── Nuclear-mode helpers ─────────────────────────────────────────────
|
| 3476 |
|
| 3477 |
def _transplant_expert_weights(self, layers: nn.ModuleList) -> int:
|
obliteratus/bayesian_optimizer.py
CHANGED
|
@@ -152,7 +152,6 @@ def run_bayesian_optimization(
|
|
| 152 |
model = pipeline.handle.model
|
| 153 |
tokenizer = pipeline.handle.tokenizer
|
| 154 |
device = pipeline._get_model_device(model)
|
| 155 |
-
layers = pipeline.handle.model # for state_dict saving
|
| 156 |
|
| 157 |
pipeline.log("Bayesian optimization: collecting reference logits...")
|
| 158 |
|
|
|
|
| 152 |
model = pipeline.handle.model
|
| 153 |
tokenizer = pipeline.handle.tokenizer
|
| 154 |
device = pipeline._get_model_device(model)
|
|
|
|
| 155 |
|
| 156 |
pipeline.log("Bayesian optimization: collecting reference logits...")
|
| 157 |
|
obliteratus/evaluation/benchmark_plots.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Research-grade benchmark visualizations for the OBLITERATUS dashboard.
|
| 2 |
+
|
| 3 |
+
Generates publication-quality figures from benchmark results, designed
|
| 4 |
+
for rapid visual comprehension by AI researchers.
|
| 5 |
+
|
| 6 |
+
Chart types:
|
| 7 |
+
1. Pareto Frontier — refusal rate vs perplexity (the money chart)
|
| 8 |
+
2. Method Radar — multi-axis capability profile per method
|
| 9 |
+
3. Metric Bars — grouped bar chart of all metrics side-by-side
|
| 10 |
+
4. Model Scaling — how a technique degrades across model sizes
|
| 11 |
+
5. Timing Efficiency — time vs quality scatter (bang for buck)
|
| 12 |
+
6. Layer Heatmap — strong layer distribution across methods/models
|
| 13 |
+
|
| 14 |
+
All functions return matplotlib Figure objects compatible with
|
| 15 |
+
Gradio's gr.Plot component.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import math
|
| 21 |
+
from typing import Any
|
| 22 |
+
|
| 23 |
+
import matplotlib
|
| 24 |
+
matplotlib.use("Agg") # Non-interactive backend for server use
|
| 25 |
+
import matplotlib.pyplot as plt
|
| 26 |
+
import matplotlib.ticker as mticker
|
| 27 |
+
import numpy as np
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# ── Color palette (research-paper friendly, colorblind-safe) ──────────────
|
| 31 |
+
|
| 32 |
+
PALETTE = {
|
| 33 |
+
"basic": "#4C72B0",
|
| 34 |
+
"advanced": "#55A868",
|
| 35 |
+
"aggressive": "#C44E52",
|
| 36 |
+
"surgical": "#8172B3",
|
| 37 |
+
"optimized": "#CCB974",
|
| 38 |
+
"inverted": "#64B5CD",
|
| 39 |
+
"nuclear": "#E5583B",
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
MODEL_PALETTE = [
|
| 43 |
+
"#4C72B0", "#55A868", "#C44E52", "#8172B3",
|
| 44 |
+
"#CCB974", "#64B5CD", "#E5583B", "#917254",
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
def _style_ax(ax, title: str = "", xlabel: str = "", ylabel: str = ""):
|
| 48 |
+
"""Apply consistent styling to an axis."""
|
| 49 |
+
ax.set_title(title, fontsize=13, fontweight="bold", pad=12)
|
| 50 |
+
ax.set_xlabel(xlabel, fontsize=10)
|
| 51 |
+
ax.set_ylabel(ylabel, fontsize=10)
|
| 52 |
+
ax.spines["top"].set_visible(False)
|
| 53 |
+
ax.spines["right"].set_visible(False)
|
| 54 |
+
ax.tick_params(labelsize=9)
|
| 55 |
+
ax.grid(axis="y", alpha=0.3, linestyle="--")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _get_color(method: str, idx: int = 0) -> str:
|
| 59 |
+
return PALETTE.get(method, MODEL_PALETTE[idx % len(MODEL_PALETTE)])
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# ── 1. Pareto Frontier ───────────────────────────────────────────────────
|
| 63 |
+
|
| 64 |
+
def plot_pareto_frontier(results: list[dict], title_suffix: str = "") -> plt.Figure:
|
| 65 |
+
"""Refusal rate vs perplexity Pareto frontier.
|
| 66 |
+
|
| 67 |
+
The most important chart for abliteration research: shows the
|
| 68 |
+
capability-safety tradeoff. Points in the bottom-left are ideal
|
| 69 |
+
(low refusal AND low perplexity).
|
| 70 |
+
|
| 71 |
+
Pareto-optimal points are connected with a frontier line.
|
| 72 |
+
"""
|
| 73 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 74 |
+
|
| 75 |
+
valid = [r for r in results if r.get("perplexity") is not None and r.get("refusal_rate") is not None]
|
| 76 |
+
if not valid:
|
| 77 |
+
ax.text(0.5, 0.5, "No valid data points", ha="center", va="center", fontsize=14, transform=ax.transAxes)
|
| 78 |
+
_style_ax(ax, f"Pareto Frontier{title_suffix}")
|
| 79 |
+
fig.tight_layout()
|
| 80 |
+
return fig
|
| 81 |
+
|
| 82 |
+
xs = [r["refusal_rate"] * 100 for r in valid] # percentage
|
| 83 |
+
ys = [r["perplexity"] for r in valid]
|
| 84 |
+
labels = [r.get("method", r.get("model_short", "?")) for r in valid]
|
| 85 |
+
colors = [_get_color(r.get("method", ""), i) for i, r in enumerate(valid)]
|
| 86 |
+
|
| 87 |
+
# Scatter
|
| 88 |
+
for i, (x, y, label, color) in enumerate(zip(xs, ys, labels, colors)):
|
| 89 |
+
ax.scatter(x, y, c=color, s=120, zorder=5, edgecolors="white", linewidth=1.5)
|
| 90 |
+
# Label offset to avoid overlap
|
| 91 |
+
offset_x = 2 if i % 2 == 0 else -2
|
| 92 |
+
offset_y = 0.3 if i % 3 == 0 else -0.3
|
| 93 |
+
ax.annotate(
|
| 94 |
+
label, (x, y),
|
| 95 |
+
textcoords="offset points", xytext=(offset_x, 8 + offset_y),
|
| 96 |
+
fontsize=8.5, fontweight="bold", color=color,
|
| 97 |
+
ha="center",
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
# Pareto frontier line
|
| 101 |
+
points = sorted(zip(xs, ys), key=lambda p: p[0])
|
| 102 |
+
frontier_x, frontier_y = [], []
|
| 103 |
+
best_y = float("inf")
|
| 104 |
+
for px, py in points:
|
| 105 |
+
if py <= best_y:
|
| 106 |
+
frontier_x.append(px)
|
| 107 |
+
frontier_y.append(py)
|
| 108 |
+
best_y = py
|
| 109 |
+
if len(frontier_x) > 1:
|
| 110 |
+
ax.plot(frontier_x, frontier_y, "--", color="#888888", alpha=0.5, linewidth=1.5, label="Pareto frontier")
|
| 111 |
+
|
| 112 |
+
# Ideal zone annotation
|
| 113 |
+
ax.annotate(
|
| 114 |
+
"IDEAL", xy=(0, min(ys) * 0.9),
|
| 115 |
+
fontsize=9, color="#2ecc71", fontweight="bold", alpha=0.6,
|
| 116 |
+
)
|
| 117 |
+
ax.annotate(
|
| 118 |
+
"", xy=(5, min(ys)), xytext=(max(xs) * 0.7, max(ys) * 0.8),
|
| 119 |
+
arrowprops=dict(arrowstyle="->", color="#cccccc", lw=1.5),
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
_style_ax(ax, f"Capability-Safety Pareto Frontier{title_suffix}",
|
| 123 |
+
"Refusal Rate (%)", "Perplexity (lower = better)")
|
| 124 |
+
ax.set_xlim(left=-2)
|
| 125 |
+
if frontier_x:
|
| 126 |
+
ax.legend(loc="upper right", fontsize=8)
|
| 127 |
+
fig.tight_layout()
|
| 128 |
+
return fig
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# ── 2. Method Radar Chart ────────────────────────────────────────────────
|
| 132 |
+
|
| 133 |
+
def plot_method_radar(results: list[dict], title_suffix: str = "") -> plt.Figure:
|
| 134 |
+
"""Multi-axis radar chart comparing methods across all metrics.
|
| 135 |
+
|
| 136 |
+
Axes: Refusal Removal, Coherence, Low Perplexity, Speed, Layer Coverage.
|
| 137 |
+
Larger area = better overall profile.
|
| 138 |
+
"""
|
| 139 |
+
valid = [r for r in results if r.get("perplexity") is not None]
|
| 140 |
+
if not valid:
|
| 141 |
+
fig, ax = plt.subplots(figsize=(7, 7))
|
| 142 |
+
ax.text(0.5, 0.5, "No valid data points", ha="center", va="center", fontsize=14, transform=ax.transAxes)
|
| 143 |
+
fig.tight_layout()
|
| 144 |
+
return fig
|
| 145 |
+
|
| 146 |
+
# Metrics to plot (name, how to extract, higher=better?)
|
| 147 |
+
categories = [
|
| 148 |
+
"Refusal\nRemoval",
|
| 149 |
+
"Coherence",
|
| 150 |
+
"Low\nPerplexity",
|
| 151 |
+
"Speed",
|
| 152 |
+
"Layer\nCoverage",
|
| 153 |
+
]
|
| 154 |
+
n_cats = len(categories)
|
| 155 |
+
|
| 156 |
+
# Normalize all metrics to 0-1 (higher = better)
|
| 157 |
+
max_ppl = max((r.get("perplexity", 1) for r in valid), default=50)
|
| 158 |
+
max_time = max((r.get("time_s", 1) for r in valid), default=300)
|
| 159 |
+
max_layers = max((r.get("strong_layers", 1) for r in valid), default=20)
|
| 160 |
+
|
| 161 |
+
fig, ax = plt.subplots(figsize=(7, 7), subplot_kw=dict(polar=True))
|
| 162 |
+
angles = np.linspace(0, 2 * np.pi, n_cats, endpoint=False).tolist()
|
| 163 |
+
angles += angles[:1] # close the polygon
|
| 164 |
+
|
| 165 |
+
for i, r in enumerate(valid):
|
| 166 |
+
label = r.get("method", r.get("model_short", f"run-{i}"))
|
| 167 |
+
color = _get_color(r.get("method", ""), i)
|
| 168 |
+
|
| 169 |
+
refusal_removal = 1.0 - (r.get("refusal_rate") or 1.0)
|
| 170 |
+
coherence = r.get("coherence") or 0.0
|
| 171 |
+
low_ppl = 1.0 - min((r.get("perplexity", max_ppl) / max_ppl), 1.0) if max_ppl > 0 else 0
|
| 172 |
+
speed = 1.0 - min((r.get("time_s", max_time) / max_time), 1.0) if max_time > 0 else 0
|
| 173 |
+
layer_cov = min((r.get("strong_layers", 0) / max(max_layers, 1)), 1.0)
|
| 174 |
+
|
| 175 |
+
values = [refusal_removal, coherence, low_ppl, speed, layer_cov]
|
| 176 |
+
values += values[:1]
|
| 177 |
+
|
| 178 |
+
ax.plot(angles, values, "o-", linewidth=2, label=label, color=color, markersize=5)
|
| 179 |
+
ax.fill(angles, values, alpha=0.1, color=color)
|
| 180 |
+
|
| 181 |
+
ax.set_xticks(angles[:-1])
|
| 182 |
+
ax.set_xticklabels(categories, fontsize=9)
|
| 183 |
+
ax.set_ylim(0, 1.05)
|
| 184 |
+
ax.set_yticks([0.25, 0.5, 0.75, 1.0])
|
| 185 |
+
ax.set_yticklabels(["25%", "50%", "75%", "100%"], fontsize=7, alpha=0.5)
|
| 186 |
+
ax.legend(loc="upper right", bbox_to_anchor=(1.3, 1.1), fontsize=8)
|
| 187 |
+
ax.set_title(f"Method Profile Radar{title_suffix}", fontsize=13, fontweight="bold", y=1.08)
|
| 188 |
+
fig.tight_layout()
|
| 189 |
+
return fig
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
# ── 3. Grouped Bar Chart ─────────────────────────────────────────────────
|
| 193 |
+
|
| 194 |
+
def plot_metric_bars(results: list[dict], title_suffix: str = "") -> plt.Figure:
|
| 195 |
+
"""Grouped bar chart of key metrics across methods/models.
|
| 196 |
+
|
| 197 |
+
Shows refusal rate, coherence, and normalized perplexity side by side
|
| 198 |
+
for quick visual comparison.
|
| 199 |
+
"""
|
| 200 |
+
valid = [r for r in results if r.get("perplexity") is not None or r.get("refusal_rate") is not None]
|
| 201 |
+
if not valid:
|
| 202 |
+
fig, ax = plt.subplots(figsize=(10, 5))
|
| 203 |
+
ax.text(0.5, 0.5, "No valid data points", ha="center", va="center", fontsize=14, transform=ax.transAxes)
|
| 204 |
+
fig.tight_layout()
|
| 205 |
+
return fig
|
| 206 |
+
|
| 207 |
+
labels = [r.get("method", r.get("model_short", "?")) for r in valid]
|
| 208 |
+
n = len(valid)
|
| 209 |
+
|
| 210 |
+
# Metrics
|
| 211 |
+
refusal = [(1.0 - (r.get("refusal_rate") or 1.0)) * 100 for r in valid]
|
| 212 |
+
coherence = [(r.get("coherence") or 0) * 100 for r in valid]
|
| 213 |
+
max_ppl = max((r.get("perplexity", 1) for r in valid), default=50)
|
| 214 |
+
norm_ppl = [(1.0 - min(r.get("perplexity", max_ppl) / max(max_ppl, 1), 1.0)) * 100 for r in valid]
|
| 215 |
+
|
| 216 |
+
x = np.arange(n)
|
| 217 |
+
width = 0.25
|
| 218 |
+
|
| 219 |
+
fig, ax = plt.subplots(figsize=(max(10, n * 1.8), 5.5))
|
| 220 |
+
|
| 221 |
+
bars1 = ax.bar(x - width, refusal, width, label="Refusal Removal %", color="#e74c3c", alpha=0.85)
|
| 222 |
+
bars2 = ax.bar(x, coherence, width, label="Coherence %", color="#2ecc71", alpha=0.85)
|
| 223 |
+
bars3 = ax.bar(x + width, norm_ppl, width, label="Capability (inv. PPL) %", color="#3498db", alpha=0.85)
|
| 224 |
+
|
| 225 |
+
# Value labels on bars
|
| 226 |
+
for bars in [bars1, bars2, bars3]:
|
| 227 |
+
for bar in bars:
|
| 228 |
+
h = bar.get_height()
|
| 229 |
+
if h > 5:
|
| 230 |
+
ax.text(bar.get_x() + bar.get_width() / 2, h + 1, f"{h:.0f}",
|
| 231 |
+
ha="center", va="bottom", fontsize=7.5, fontweight="bold")
|
| 232 |
+
|
| 233 |
+
ax.set_xticks(x)
|
| 234 |
+
ax.set_xticklabels(labels, rotation=30 if n > 4 else 0, ha="right" if n > 4 else "center", fontsize=9)
|
| 235 |
+
ax.set_ylim(0, 110)
|
| 236 |
+
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v:.0f}%"))
|
| 237 |
+
ax.legend(loc="upper right", fontsize=8.5)
|
| 238 |
+
_style_ax(ax, f"Metric Comparison{title_suffix}", "", "Score (%)")
|
| 239 |
+
fig.tight_layout()
|
| 240 |
+
return fig
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
# ── 4. Timing Efficiency Scatter ─────────────────────────────────────────
|
| 244 |
+
|
| 245 |
+
def plot_timing_efficiency(results: list[dict], title_suffix: str = "") -> plt.Figure:
|
| 246 |
+
"""Time vs quality scatter — bang for your compute buck.
|
| 247 |
+
|
| 248 |
+
X-axis: wall-clock time. Y-axis: composite quality score.
|
| 249 |
+
Bubble size: number of strong layers touched.
|
| 250 |
+
Points in the top-left are most efficient.
|
| 251 |
+
"""
|
| 252 |
+
valid = [r for r in results if r.get("time_s") is not None and r.get("refusal_rate") is not None]
|
| 253 |
+
if not valid:
|
| 254 |
+
fig, ax = plt.subplots(figsize=(8, 5))
|
| 255 |
+
ax.text(0.5, 0.5, "No valid data points", ha="center", va="center", fontsize=14, transform=ax.transAxes)
|
| 256 |
+
fig.tight_layout()
|
| 257 |
+
return fig
|
| 258 |
+
|
| 259 |
+
fig, ax = plt.subplots(figsize=(8, 5.5))
|
| 260 |
+
|
| 261 |
+
for i, r in enumerate(valid):
|
| 262 |
+
label = r.get("method", r.get("model_short", "?"))
|
| 263 |
+
color = _get_color(r.get("method", ""), i)
|
| 264 |
+
x = r["time_s"]
|
| 265 |
+
# Composite quality: weighted sum of refusal removal and coherence
|
| 266 |
+
refusal_removal = 1.0 - (r.get("refusal_rate") or 1.0)
|
| 267 |
+
coherence = r.get("coherence") or 0.0
|
| 268 |
+
quality = 0.6 * refusal_removal + 0.4 * coherence
|
| 269 |
+
size = max(40, (r.get("strong_layers", 5)) * 12)
|
| 270 |
+
|
| 271 |
+
ax.scatter(x, quality * 100, s=size, c=color, alpha=0.8, edgecolors="white", linewidth=1.5, zorder=5)
|
| 272 |
+
ax.annotate(label, (x, quality * 100), textcoords="offset points", xytext=(8, 5),
|
| 273 |
+
fontsize=8.5, fontweight="bold", color=color)
|
| 274 |
+
|
| 275 |
+
# Efficiency frontier
|
| 276 |
+
ax.annotate(
|
| 277 |
+
"EFFICIENT", xy=(min(r["time_s"] for r in valid) * 0.8, 95),
|
| 278 |
+
fontsize=9, color="#2ecc71", fontweight="bold", alpha=0.5,
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
_style_ax(ax, f"Compute Efficiency{title_suffix}",
|
| 282 |
+
"Wall-Clock Time (seconds)", "Quality Score (% — higher = better)")
|
| 283 |
+
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v:.0f}%"))
|
| 284 |
+
fig.tight_layout()
|
| 285 |
+
return fig
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
# ── 5. MoE Metrics Panel ─────────────────────────────────────────────────
|
| 289 |
+
|
| 290 |
+
def plot_moe_metrics(results: list[dict], title_suffix: str = "") -> plt.Figure:
|
| 291 |
+
"""MoE-specific metrics: EGA directions, CoT preservation, expert coverage.
|
| 292 |
+
|
| 293 |
+
Only meaningful for results that include MoE-aware techniques.
|
| 294 |
+
Shows a stacked horizontal bar chart of MoE feature activation.
|
| 295 |
+
"""
|
| 296 |
+
valid = [r for r in results if r.get("ega_expert_dirs", 0) > 0 or r.get("cot_preserved", 0) > 0]
|
| 297 |
+
if not valid:
|
| 298 |
+
# Show all results with a note about no MoE activity
|
| 299 |
+
fig, ax = plt.subplots(figsize=(8, 4))
|
| 300 |
+
all_labels = [r.get("method", r.get("model_short", "?")) for r in results]
|
| 301 |
+
if results:
|
| 302 |
+
ax.barh(all_labels, [0] * len(results), color="#cccccc")
|
| 303 |
+
ax.text(0.5, 0.5, "No MoE-specific features activated\n(use surgical/optimized/nuclear methods)",
|
| 304 |
+
ha="center", va="center", fontsize=11, color="#999999", transform=ax.transAxes)
|
| 305 |
+
else:
|
| 306 |
+
ax.text(0.5, 0.5, "No data", ha="center", va="center", fontsize=14, transform=ax.transAxes)
|
| 307 |
+
_style_ax(ax, f"MoE Feature Activation{title_suffix}")
|
| 308 |
+
fig.tight_layout()
|
| 309 |
+
return fig
|
| 310 |
+
|
| 311 |
+
labels = [r.get("method", r.get("model_short", "?")) for r in valid]
|
| 312 |
+
ega = [r.get("ega_expert_dirs", 0) for r in valid]
|
| 313 |
+
cot = [r.get("cot_preserved", 0) for r in valid]
|
| 314 |
+
safety = [r.get("expert_classified_layers", r.get("ega_safety_layers", 0)) for r in valid]
|
| 315 |
+
|
| 316 |
+
fig, ax = plt.subplots(figsize=(9, max(3, len(valid) * 0.8 + 1)))
|
| 317 |
+
|
| 318 |
+
y = np.arange(len(valid))
|
| 319 |
+
h = 0.25
|
| 320 |
+
|
| 321 |
+
ax.barh(y - h, ega, h, label="EGA Expert Directions", color="#8172B3", alpha=0.85)
|
| 322 |
+
ax.barh(y, cot, h, label="CoT Preserved Layers", color="#CCB974", alpha=0.85)
|
| 323 |
+
ax.barh(y + h, safety, h, label="Expert-Classified Layers", color="#55A868", alpha=0.85)
|
| 324 |
+
|
| 325 |
+
# Value labels
|
| 326 |
+
for vals, offset in [(ega, -h), (cot, 0), (safety, h)]:
|
| 327 |
+
for j, v in enumerate(vals):
|
| 328 |
+
if v > 0:
|
| 329 |
+
ax.text(v + 0.3, j + offset, str(v), va="center", fontsize=8, fontweight="bold")
|
| 330 |
+
|
| 331 |
+
ax.set_yticks(y)
|
| 332 |
+
ax.set_yticklabels(labels, fontsize=9)
|
| 333 |
+
ax.legend(loc="lower right", fontsize=8)
|
| 334 |
+
_style_ax(ax, f"MoE Feature Activation{title_suffix}", "Count", "")
|
| 335 |
+
fig.tight_layout()
|
| 336 |
+
return fig
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
# ── 6. Multi-Model Scaling Chart ─────────────────────────────────────────
|
| 340 |
+
|
| 341 |
+
def plot_model_scaling(results: list[dict], title_suffix: str = "") -> plt.Figure:
|
| 342 |
+
"""How metrics change as model size grows.
|
| 343 |
+
|
| 344 |
+
For multi-model benchmarks: plots refusal rate and perplexity
|
| 345 |
+
against model name, showing scaling behavior.
|
| 346 |
+
"""
|
| 347 |
+
valid = [r for r in results if r.get("perplexity") is not None]
|
| 348 |
+
if not valid:
|
| 349 |
+
fig, ax = plt.subplots(figsize=(10, 5))
|
| 350 |
+
ax.text(0.5, 0.5, "No valid data points", ha="center", va="center", fontsize=14, transform=ax.transAxes)
|
| 351 |
+
fig.tight_layout()
|
| 352 |
+
return fig
|
| 353 |
+
|
| 354 |
+
labels = [r.get("model_short", r.get("model", "?").split("/")[-1]) for r in valid]
|
| 355 |
+
n = len(valid)
|
| 356 |
+
x = np.arange(n)
|
| 357 |
+
|
| 358 |
+
fig, ax1 = plt.subplots(figsize=(max(9, n * 2), 5.5))
|
| 359 |
+
ax2 = ax1.twinx()
|
| 360 |
+
|
| 361 |
+
refusal = [(r.get("refusal_rate") or 0) * 100 for r in valid]
|
| 362 |
+
ppl = [r.get("perplexity", 0) for r in valid]
|
| 363 |
+
|
| 364 |
+
line1 = ax1.plot(x, refusal, "o-", color="#e74c3c", linewidth=2.5, markersize=8, label="Refusal Rate (%)", zorder=5)
|
| 365 |
+
line2 = ax2.plot(x, ppl, "s--", color="#3498db", linewidth=2.5, markersize=8, label="Perplexity", zorder=5)
|
| 366 |
+
|
| 367 |
+
# Fill between for visual emphasis
|
| 368 |
+
ax1.fill_between(x, refusal, alpha=0.08, color="#e74c3c")
|
| 369 |
+
ax2.fill_between(x, ppl, alpha=0.08, color="#3498db")
|
| 370 |
+
|
| 371 |
+
ax1.set_xticks(x)
|
| 372 |
+
ax1.set_xticklabels(labels, rotation=20, ha="right", fontsize=9)
|
| 373 |
+
ax1.set_ylabel("Refusal Rate (%)", color="#e74c3c", fontsize=10)
|
| 374 |
+
ax1.tick_params(axis="y", labelcolor="#e74c3c")
|
| 375 |
+
ax1.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f"{v:.0f}%"))
|
| 376 |
+
|
| 377 |
+
ax2.set_ylabel("Perplexity", color="#3498db", fontsize=10)
|
| 378 |
+
ax2.tick_params(axis="y", labelcolor="#3498db")
|
| 379 |
+
|
| 380 |
+
# Combined legend
|
| 381 |
+
lines = line1 + line2
|
| 382 |
+
labels_legend = [l.get_label() for l in lines]
|
| 383 |
+
ax1.legend(lines, labels_legend, loc="upper left", fontsize=8.5)
|
| 384 |
+
|
| 385 |
+
ax1.set_title(f"Cross-Model Scaling{title_suffix}", fontsize=13, fontweight="bold", pad=12)
|
| 386 |
+
ax1.spines["top"].set_visible(False)
|
| 387 |
+
ax2.spines["top"].set_visible(False)
|
| 388 |
+
ax1.grid(axis="y", alpha=0.2, linestyle="--")
|
| 389 |
+
fig.tight_layout()
|
| 390 |
+
return fig
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
# ── Composite Dashboard ──────────────────────────────────────────────────
|
| 394 |
+
|
| 395 |
+
def generate_benchmark_dashboard(
|
| 396 |
+
results: list[dict],
|
| 397 |
+
mode: str = "multi_method",
|
| 398 |
+
title_suffix: str = "",
|
| 399 |
+
) -> list[plt.Figure]:
|
| 400 |
+
"""Generate a full dashboard of benchmark visualizations.
|
| 401 |
+
|
| 402 |
+
Args:
|
| 403 |
+
results: List of benchmark result dicts.
|
| 404 |
+
mode: "multi_method" (N methods x 1 model) or
|
| 405 |
+
"multi_model" (1 method x N models).
|
| 406 |
+
title_suffix: Optional suffix for chart titles.
|
| 407 |
+
|
| 408 |
+
Returns:
|
| 409 |
+
List of matplotlib Figure objects ready for Gradio gr.Plot.
|
| 410 |
+
"""
|
| 411 |
+
if not results:
|
| 412 |
+
return []
|
| 413 |
+
|
| 414 |
+
figs = []
|
| 415 |
+
|
| 416 |
+
if mode == "multi_method":
|
| 417 |
+
# 1. Pareto frontier (the headline chart)
|
| 418 |
+
figs.append(plot_pareto_frontier(results, title_suffix))
|
| 419 |
+
# 2. Radar chart
|
| 420 |
+
figs.append(plot_method_radar(results, title_suffix))
|
| 421 |
+
# 3. Metric bars
|
| 422 |
+
figs.append(plot_metric_bars(results, title_suffix))
|
| 423 |
+
# 4. Timing efficiency
|
| 424 |
+
figs.append(plot_timing_efficiency(results, title_suffix))
|
| 425 |
+
# 5. MoE metrics (if any)
|
| 426 |
+
if any(r.get("ega_expert_dirs", 0) > 0 for r in results):
|
| 427 |
+
figs.append(plot_moe_metrics(results, title_suffix))
|
| 428 |
+
elif mode == "multi_model":
|
| 429 |
+
# 1. Scaling chart (headline)
|
| 430 |
+
figs.append(plot_model_scaling(results, title_suffix))
|
| 431 |
+
# 2. Metric bars
|
| 432 |
+
figs.append(plot_metric_bars(results, title_suffix))
|
| 433 |
+
# 3. Pareto frontier
|
| 434 |
+
figs.append(plot_pareto_frontier(results, title_suffix))
|
| 435 |
+
# 4. Timing
|
| 436 |
+
figs.append(plot_timing_efficiency(results, title_suffix))
|
| 437 |
+
# 5. MoE metrics
|
| 438 |
+
if any(r.get("ega_expert_dirs", 0) > 0 for r in results):
|
| 439 |
+
figs.append(plot_moe_metrics(results, title_suffix))
|
| 440 |
+
|
| 441 |
+
return figs
|
paper/main.tex
CHANGED
|
@@ -28,7 +28,7 @@
|
|
| 28 |
urlcolor=blue,
|
| 29 |
}
|
| 30 |
|
| 31 |
-
\title{OBLITERATUS: A Unified Platform for Mechanistic Analysis\\and Surgical Removal of Refusal in Language Models}
|
| 32 |
|
| 33 |
\author{
|
| 34 |
Anonymous
|
|
@@ -41,13 +41,20 @@
|
|
| 41 |
|
| 42 |
% ═════════════════════════════════════════════════════════════════════
|
| 43 |
\begin{abstract}
|
| 44 |
-
We present \textsc{Obliteratus}, an open-source research platform that unifies mechanistic analysis and surgical intervention of refusal mechanisms in large language models (LLMs).
|
| 45 |
-
While prior work has established that refusal is mediated by linear directions in activation space \citep{arditi2024refusal} and that multi-direction SVD extraction improves removal \citep{gabliteration2024}, no existing tool provides comprehensive geometric characterization of the refusal subspace alongside
|
| 46 |
|
| 47 |
-
\textsc{Obliteratus} contributes:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
-
The platform supports any HuggingFace transformer architecture and ships with 48 curated model presets, 10 study configurations,
|
| 50 |
-
We
|
| 51 |
|
| 52 |
\end{abstract}
|
| 53 |
|
|
@@ -61,15 +68,28 @@ A growing body of mechanistic interpretability research has shown that these tra
|
|
| 61 |
Understanding how refusal mechanisms are structured inside transformers is critical for both \emph{offensive} research (identifying vulnerabilities in alignment) and \emph{defensive} research (building more robust safety training).
|
| 62 |
Yet existing tools are fragmented: some focus solely on direction extraction \citep{arditi2024refusal}, others on weight modification \citep{failspy_abliterator}, and none provide comprehensive geometric analysis of the refusal subspace or support both permanent and reversible interventions within a unified framework.
|
| 63 |
|
| 64 |
-
\textsc{Obliteratus} addresses this gap with
|
| 65 |
|
| 66 |
\begin{enumerate}[leftmargin=*]
|
| 67 |
\item \textbf{Comprehensive analysis before intervention.} Rather than immediately removing refusal, the platform first characterizes its geometric structure---how many directions are involved, whether they form cones or subspaces, how they vary across layers and harm categories, and what alignment training method likely produced them.
|
| 68 |
-
\item \textbf{Multiple intervention paradigms.} The platform supports
|
| 69 |
-
\item \textbf{
|
|
|
|
|
|
|
| 70 |
\end{enumerate}
|
| 71 |
|
| 72 |
-
The remainder of this paper is organized as follows.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
% ═════════════════════════════════════════════════════════════════════
|
| 75 |
\section{Related Work}
|
|
@@ -87,6 +107,15 @@ The remainder of this paper is organized as follows. Section~\ref{sec:related} s
|
|
| 87 |
\paragraph{Mechanistic interpretability tools.}
|
| 88 |
TransformerLens \citep{nanda2022transformerlens} provides hook-based access to intermediate activations for approximately 50 architectures. SAELens focuses on sparse autoencoder training for feature extraction. RepEng \citep{zou2023representation} implements representation engineering for behavioral control. None of these tools specifically target refusal mechanism analysis or provide abliteration capabilities.
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
\paragraph{Defense robustness.}
|
| 91 |
Models exhibit a tendency to self-repair after partial abliteration---a phenomenon we term the \emph{Hydra effect}---where residual refusal circuitry compensates for removed directions. \citet{qi2025safety} mapped safety-capability entanglement, showing that removing safety features often degrades general capabilities. \citet{zou2024circuit} proposed circuit breakers as a more robust defense via representation rerouting.
|
| 92 |
|
|
@@ -94,16 +123,18 @@ Models exhibit a tendency to self-repair after partial abliteration---a phenomen
|
|
| 94 |
\section{Platform Architecture}
|
| 95 |
\label{sec:architecture}
|
| 96 |
|
| 97 |
-
\textsc{Obliteratus} is organized into
|
| 98 |
|
| 99 |
\begin{enumerate}[leftmargin=*]
|
| 100 |
-
\item \textbf{Abliteration Pipeline} (\texttt{obliteratus.abliterate}): A six-stage pipeline (SUMMON, PROBE, DISTILL, EXCISE, VERIFY, REBIRTH) that orchestrates end-to-end refusal removal from model loading through quality-verified export.
|
| 101 |
\item \textbf{Analysis Modules} (\texttt{obliteratus.analysis}): Fifteen specialized analyzers for mechanistic characterization of refusal, from basic direction extraction to novel geometric and transfer analyses.
|
| 102 |
-
\item \textbf{Evaluation Suite} (\texttt{obliteratus.evaluation}): Automated quality assessment using six complementary metrics.
|
| 103 |
-
\item \textbf{Ablation Framework} (\texttt{obliteratus.strategies}): Four ablation strategies (layer removal, head pruning, FFN ablation, embedding ablation) for systematic component-level analysis.
|
|
|
|
|
|
|
| 104 |
\end{enumerate}
|
| 105 |
|
| 106 |
-
The platform supports any HuggingFace \texttt{transformers} model via automatic architecture detection, handling both Conv1D and Linear projection layers, standard and fused attention patterns, and custom architectures through \texttt{trust\_remote\_code}. A curated registry of 48 models across five compute tiers (Tiny through Frontier) provides recommended configurations.
|
| 107 |
|
| 108 |
\begin{figure}[t]
|
| 109 |
\centering
|
|
@@ -111,14 +142,19 @@ The platform supports any HuggingFace \texttt{transformers} model via automatic
|
|
| 111 |
\begin{verbatim}
|
| 112 |
SUMMON ──► PROBE ──► DISTILL ──► EXCISE ──► VERIFY ──► REBIRTH
|
| 113 |
(load) (collect) (SVD) (project) (eval) (save)
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
\end{verbatim}
|
| 121 |
-
\caption{High-level architecture of the \textsc{Obliteratus} pipeline. The six-stage abliteration flow (top)
|
| 122 |
\label{fig:architecture}
|
| 123 |
\end{figure}
|
| 124 |
|
|
@@ -394,30 +430,44 @@ where $H(\hat{\mathbf{p}})$ is the entropy of the normalized projection distribu
|
|
| 394 |
\subsection{Weight Projection (Permanent)}
|
| 395 |
\label{sec:weight_projection}
|
| 396 |
|
| 397 |
-
\textsc{Obliteratus} provides
|
| 398 |
|
| 399 |
\begin{table}[h]
|
| 400 |
\centering
|
| 401 |
-
\caption{Abliteration method presets.}
|
| 402 |
\label{tab:methods}
|
| 403 |
\small
|
| 404 |
-
\begin{tabular}{@{}
|
| 405 |
\toprule
|
| 406 |
-
\textbf{Method} & \textbf{
|
| 407 |
\midrule
|
| 408 |
-
Basic & 1 (
|
| 409 |
-
Advanced & 4 (SVD) & Yes & $\lambda
|
| 410 |
-
Aggressive & 8 (SVD) & Yes & None & 3
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
\bottomrule
|
| 412 |
\end{tabular}
|
| 413 |
\end{table}
|
| 414 |
|
|
|
|
|
|
|
|
|
|
| 415 |
The core projection for a weight matrix $\mathbf{W}$ and refusal directions $\{\mathbf{r}_1, \ldots, \mathbf{r}_k\}$:
|
| 416 |
\begin{equation}
|
| 417 |
\mathbf{W}' = \mathbf{W} - \sum_{i=1}^k \left[(1-\lambda)\mathbf{W}\mathbf{r}_i\mathbf{r}_i^\top\right]
|
| 418 |
\end{equation}
|
| 419 |
where $\lambda$ is the regularization strength (preserves $\lambda$ fraction of the refusal component).
|
| 420 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
\paragraph{Norm-preserving rescaling.}
|
| 422 |
After projection, we rescale to preserve the Frobenius norm \citep{grimjim2025}:
|
| 423 |
\begin{equation}
|
|
@@ -425,6 +475,13 @@ After projection, we rescale to preserve the Frobenius norm \citep{grimjim2025}:
|
|
| 425 |
\end{equation}
|
| 426 |
This prevents cascading magnitude drift through LayerNorm layers.
|
| 427 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
\paragraph{Bias term projection.}
|
| 429 |
Unlike prior tools that only modify weight matrices, \textsc{Obliteratus} also projects refusal directions out of bias vectors when present:
|
| 430 |
\begin{equation}
|
|
@@ -432,7 +489,7 @@ Unlike prior tools that only modify weight matrices, \textsc{Obliteratus} also p
|
|
| 432 |
\end{equation}
|
| 433 |
|
| 434 |
\paragraph{Iterative refinement.}
|
| 435 |
-
|
| 436 |
|
| 437 |
\subsection{Steering Vectors (Reversible)}
|
| 438 |
\label{sec:steering}
|
|
@@ -477,6 +534,142 @@ Advantages over weight projection: reversibility (hooks can be removed), continu
|
|
| 477 |
\item \textbf{Effective Rank}: Shannon entropy-based dimensionality of weight matrices (Equation~1). Tracks whether abliteration collapses the weight space.
|
| 478 |
\end{enumerate}
|
| 479 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
% ═════════════════════════════════════════════════════════════════════
|
| 481 |
\section{Analysis-Informed Abliteration}
|
| 482 |
\label{sec:informed}
|
|
@@ -541,6 +734,33 @@ The analysis insights map to pipeline parameters through the following rules:
|
|
| 541 |
|
| 542 |
where $d_{\text{cone}}$ is the cone dimensionality from Section~\ref{sec:concept_cones}, $\lambda_{\text{base}}$ is a per-method base regularization, $e_{\text{entangle}}$ is the entanglement score, and $\hat{r}_{\text{repair}}$ is the estimated self-repair capacity.
|
| 543 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 544 |
% ═════════════════════════════════════════════════════════════════════
|
| 545 |
\section{Comparison with Existing Tools}
|
| 546 |
\label{sec:comparison}
|
|
@@ -549,7 +769,7 @@ Table~\ref{tab:comparison} compares \textsc{Obliteratus} with existing tools acr
|
|
| 549 |
|
| 550 |
\begin{table}[t]
|
| 551 |
\centering
|
| 552 |
-
\caption{Feature comparison across refusal analysis and intervention tools.}
|
| 553 |
\label{tab:comparison}
|
| 554 |
\small
|
| 555 |
\begin{tabular}{@{}lcccccc@{}}
|
|
@@ -557,16 +777,31 @@ Table~\ref{tab:comparison} compares \textsc{Obliteratus} with existing tools acr
|
|
| 557 |
\textbf{Capability} & \rotatebox{60}{\textsc{Obliteratus}} & \rotatebox{60}{TransformerLens} & \rotatebox{60}{Heretic} & \rotatebox{60}{FailSpy abl.} & \rotatebox{60}{RepEng} & \rotatebox{60}{SAELens} \\
|
| 558 |
\midrule
|
| 559 |
Direction extraction methods & 3 & Manual & 1 & 1 & 1 & -- \\
|
| 560 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
Steering vectors & \checkmark & -- & -- & -- & Core & -- \\
|
|
|
|
|
|
|
|
|
|
| 562 |
Concept cone geometry & \checkmark & -- & -- & -- & -- & -- \\
|
| 563 |
Alignment fingerprinting & \checkmark & -- & -- & -- & -- & -- \\
|
| 564 |
Cross-model transfer & \checkmark & -- & -- & -- & -- & -- \\
|
| 565 |
Defense robustness eval. & \checkmark & -- & -- & -- & -- & -- \\
|
| 566 |
Analysis-informed pipeline & \checkmark & -- & -- & -- & -- & -- \\
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
Real causal tracing & Approx. & \checkmark & -- & -- & -- & -- \\
|
| 568 |
Sparse autoencoders & -- & Via SAE & -- & -- & -- & Core \\
|
| 569 |
Model compatibility & Any HF & $\sim$50 & 16 & TLens & HF & TLens \\
|
|
|
|
| 570 |
Test suite & 379 & Community & -- & -- & Min. & Mod. \\
|
| 571 |
\bottomrule
|
| 572 |
\end{tabular}
|
|
@@ -575,13 +810,15 @@ Test suite & 379 & Community & -- & -- & Min. & Mod. \\
|
|
| 575 |
The key differentiators of \textsc{Obliteratus} are:
|
| 576 |
|
| 577 |
\begin{enumerate}[leftmargin=*]
|
|
|
|
| 578 |
\item \textbf{Analysis breadth}: No other tool provides concept cone geometry, alignment imprint detection, cross-model universality analysis, or defense robustness evaluation.
|
| 579 |
-
\item \textbf{
|
| 580 |
-
\item \textbf{
|
| 581 |
-
\item \textbf{
|
|
|
|
| 582 |
\end{enumerate}
|
| 583 |
|
| 584 |
-
Conversely, TransformerLens provides real activation patching (our causal tracing is approximate)
|
| 585 |
|
| 586 |
% ═════════════════════════════════════════════════════════════════════
|
| 587 |
\section{Discussion and Limitations}
|
|
@@ -596,14 +833,20 @@ Our causal tracing module provides noise-based approximations rather than true a
|
|
| 596 |
\paragraph{Alignment fingerprinting validation.}
|
| 597 |
The alignment imprint detector uses heuristic signatures derived from the literature's characterization of different training methods. While the geometric features (Gini, effective rank, smoothness) are well-motivated, the ideal values and classification boundaries would benefit from systematic validation across models with known training procedures.
|
| 598 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
\paragraph{Scaling considerations.}
|
| 600 |
-
The current implementation loads the full model into memory for analysis. For frontier-scale models (100B+ parameters), this requires significant compute. Future work could integrate quantized inference or offloading strategies.
|
| 601 |
|
| 602 |
\paragraph{Evaluation completeness.}
|
| 603 |
Our evaluation suite measures \emph{refusal removal} and \emph{capability preservation} but does not comprehensively assess downstream task performance across diverse benchmarks. Integration with evaluation harnesses such as lm-evaluation-harness \citep{gao2021framework} is a natural extension.
|
| 604 |
|
| 605 |
\paragraph{Future directions.}
|
| 606 |
-
We identify several opportunities: (1)
|
| 607 |
|
| 608 |
% ═════════════════════════════════════════════════════════════════════
|
| 609 |
\section{Broader Impact Statement}
|
|
@@ -636,9 +879,18 @@ We follow the principle that \emph{security through obscurity is not security}:
|
|
| 636 |
% ═════════════════════════════════════════════════════════════════════
|
| 637 |
\section{Conclusion}
|
| 638 |
|
| 639 |
-
We presented \textsc{Obliteratus}, an open-source platform that unifies mechanistic analysis of refusal mechanisms with surgical intervention capabilities
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 640 |
|
| 641 |
-
By making these tools available under an MIT license with comprehensive documentation and 379 unit tests, we aim to accelerate both offensive and defensive alignment research: understanding the geometric structure of refusal is the foundation for both removing it surgically and building more robust implementations.
|
| 642 |
|
| 643 |
% ═════════════════════════════════════════════════════════════════════
|
| 644 |
\bibliographystyle{plainnat}
|
|
|
|
| 28 |
urlcolor=blue,
|
| 29 |
}
|
| 30 |
|
| 31 |
+
\title{OBLITERATUS: A Unified Platform for Mechanistic Analysis\\and Surgical Removal of Refusal in Language Models,\\with Expert-Granular Abliteration for MoE Architectures}
|
| 32 |
|
| 33 |
\author{
|
| 34 |
Anonymous
|
|
|
|
| 41 |
|
| 42 |
% ═════════════════════════════════════════════════════════════════════
|
| 43 |
\begin{abstract}
|
| 44 |
+
We present \textsc{Obliteratus}, an open-source research platform that unifies mechanistic analysis and surgical intervention of refusal mechanisms in large language models (LLMs), with first-of-its-kind support for \emph{Mixture-of-Experts} (MoE) architectures.
|
| 45 |
+
While prior work has established that refusal is mediated by linear directions in activation space \citep{arditi2024refusal} and that multi-direction SVD extraction improves removal \citep{gabliteration2024}, and while Heretic \citep{heretic2025} pioneered Bayesian optimization and LoRA-mediated ablation, no existing tool provides comprehensive geometric characterization of the refusal subspace alongside MoE-aware intervention, reversible adapters, and frontier optimization in a unified framework.
|
| 46 |
|
| 47 |
+
\textsc{Obliteratus} contributes:
|
| 48 |
+
(1)~\textbf{15 analysis modules} spanning direction extraction, geometric characterization, learned probing, causal estimation, cross-model transfer, and defense robustness evaluation;
|
| 49 |
+
(2)~\textbf{seven intervention presets} (Basic through Nuclear) with per-layer adaptive strength, norm-preserving regularization, and iterative refinement;
|
| 50 |
+
(3)~\textbf{Expert-Granular Abliteration (EGA)} for MoE models, decomposing refusal directions per-expert via routing-weighted activation attribution and applying selective inversion to fused 3D weight tensors---distinguishing safety-critical from capability-preserving experts;
|
| 51 |
+
(4)~\textbf{six frontier optimization techniques} inspired by and extending Heretic: Bayesian hyperparameter optimization (Optuna TPE with warm-start from analysis heuristics), reversible LoRA-mediated ablation, KL-divergence co-optimization with partial revert, chain-of-thought-aware ablation via Gram-Schmidt orthogonalization, float layer interpolation with Gaussian-weighted continuous targeting, and activation winsorization for robust SVD;
|
| 52 |
+
(5)~\textbf{a unified evaluation suite} with refusal rate, perplexity, coherence, KL divergence, CKA similarity, and effective rank metrics;
|
| 53 |
+
(6)~\textbf{an analysis-informed pipeline} that closes the feedback loop---analysis modules run \emph{during} abliteration to auto-configure direction extraction, layer selection, regularization, and Hydra-compensated refinement; and
|
| 54 |
+
(7)~\textbf{an interactive web research dashboard} (HuggingFace Spaces) with A/B comparison chat, dose-response strength sweep, multi-model benchmarking with publication-quality visualizations, and one-click research artifact export.
|
| 55 |
|
| 56 |
+
The platform supports any HuggingFace transformer architecture---including fused MoE experts (GPT-OSS 20B, Mixtral, DeepSeek)---and ships with 48 curated model presets, 10 study configurations, and 379 unit tests.
|
| 57 |
+
We provide complete mathematical formulations for all modules, demonstrate state-of-the-art abliteration quality on MoE models where prior tools fail, and discuss the design decisions that position \textsc{Obliteratus} as the most comprehensive abliteration platform available.
|
| 58 |
|
| 59 |
\end{abstract}
|
| 60 |
|
|
|
|
| 68 |
Understanding how refusal mechanisms are structured inside transformers is critical for both \emph{offensive} research (identifying vulnerabilities in alignment) and \emph{defensive} research (building more robust safety training).
|
| 69 |
Yet existing tools are fragmented: some focus solely on direction extraction \citep{arditi2024refusal}, others on weight modification \citep{failspy_abliterator}, and none provide comprehensive geometric analysis of the refusal subspace or support both permanent and reversible interventions within a unified framework.
|
| 70 |
|
| 71 |
+
\textsc{Obliteratus} addresses this gap with five design goals:
|
| 72 |
|
| 73 |
\begin{enumerate}[leftmargin=*]
|
| 74 |
\item \textbf{Comprehensive analysis before intervention.} Rather than immediately removing refusal, the platform first characterizes its geometric structure---how many directions are involved, whether they form cones or subspaces, how they vary across layers and harm categories, and what alignment training method likely produced them.
|
| 75 |
+
\item \textbf{Multiple intervention paradigms.} The platform supports seven abliteration presets (Basic through Nuclear), reversible LoRA-mediated ablation, and inference-time steering vectors, covering the full spectrum from conservative capability-preserving removal to maximally aggressive multi-pass excision.
|
| 76 |
+
\item \textbf{Native MoE support.} Mixture-of-Experts models (GPT-OSS 20B, Mixtral, DeepSeek-MoE) present unique challenges for abliteration: refusal may be concentrated in specific experts, and fused 3D weight tensors require per-expert decomposition. \textsc{Obliteratus} introduces \emph{Expert-Granular Abliteration} (EGA)---routing-weighted direction attribution and selective inversion that distinguishes safety-critical from capability-preserving experts.
|
| 77 |
+
\item \textbf{Frontier optimization.} Building on Heretic's \citep{heretic2025} pioneering use of Bayesian optimization and LoRA-mediated ablation, we integrate and extend six optimization techniques: TPE-based hyperparameter search, reversible LoRA adapters, KL-divergence co-optimization, chain-of-thought-aware ablation, float layer interpolation, and activation winsorization.
|
| 78 |
+
\item \textbf{Rigorous evaluation and interactive exploration.} Every intervention is accompanied by automated quality assessment, and the platform ships with a web research dashboard (HuggingFace Spaces) providing A/B comparison chat, dose-response strength sweeps, multi-model benchmarking, and one-click artifact export.
|
| 79 |
\end{enumerate}
|
| 80 |
|
| 81 |
+
The remainder of this paper is organized as follows.
|
| 82 |
+
Section~\ref{sec:related} surveys related work.
|
| 83 |
+
Section~\ref{sec:architecture} describes the platform architecture.
|
| 84 |
+
Section~\ref{sec:analysis} details the 15 analysis modules with mathematical formulations.
|
| 85 |
+
Section~\ref{sec:intervention} describes the seven intervention presets and their mathematical foundations.
|
| 86 |
+
Section~\ref{sec:moe} introduces Expert-Granular Abliteration for MoE models.
|
| 87 |
+
Section~\ref{sec:frontier} presents the six frontier optimization techniques.
|
| 88 |
+
Section~\ref{sec:evaluation} covers the evaluation suite.
|
| 89 |
+
Section~\ref{sec:informed} presents the analysis-informed abliteration pipeline.
|
| 90 |
+
Section~\ref{sec:dashboard} describes the web research dashboard.
|
| 91 |
+
Section~\ref{sec:comparison} compares \textsc{Obliteratus} with existing tools.
|
| 92 |
+
Section~\ref{sec:discussion} discusses limitations, broader impact, and future work.
|
| 93 |
|
| 94 |
% ═════════════════════════════════════════════════════════════════════
|
| 95 |
\section{Related Work}
|
|
|
|
| 107 |
\paragraph{Mechanistic interpretability tools.}
|
| 108 |
TransformerLens \citep{nanda2022transformerlens} provides hook-based access to intermediate activations for approximately 50 architectures. SAELens focuses on sparse autoencoder training for feature extraction. RepEng \citep{zou2023representation} implements representation engineering for behavioral control. None of these tools specifically target refusal mechanism analysis or provide abliteration capabilities.
|
| 109 |
|
| 110 |
+
\paragraph{Heretic and Bayesian abliteration.}
|
| 111 |
+
Heretic \citep{heretic2025} introduced Bayesian optimization for abliteration hyperparameters, using Optuna's TPE sampler \citep{akiba2019optuna} to search for per-layer projection strengths that minimize refusal rate while constraining KL divergence. Heretic also pioneered LoRA-mediated ablation \citep{hu2022lora}, storing ablation as reversible rank-1 adapters rather than permanent weight modifications. These innovations represent a significant advance over fixed-parameter approaches. However, Heretic supports only 16 dense architectures and has no support for MoE models, per-expert granularity, or chain-of-thought preservation. \textsc{Obliteratus} incorporates and extends all of Heretic's innovations while adding MoE-native processing, warm-started optimization from analysis heuristics, multi-direction LoRA adapters, and several additional optimization techniques.
|
| 112 |
+
|
| 113 |
+
\paragraph{Mixture-of-Experts models.}
|
| 114 |
+
MoE architectures \citep{shazeer2017outrageously, fedus2022switch} route each token through a subset of specialized expert sub-networks. Models such as GPT-OSS 20B, Mixtral \citep{jiang2024mixtral}, and DeepSeek-MoE \citep{dai2024deepseekmoe} use this design to achieve high capability at lower inference cost. MoE models present unique challenges for abliteration: (1)~refusal may be concentrated in specific experts rather than distributed uniformly; (2)~fused weight tensors of shape $[\text{num\_experts}, \text{hidden}, \text{intermediate}]$ require per-slice decomposition; and (3)~the router network itself may encode safety-relevant routing preferences. No prior abliteration tool addresses these challenges.
|
| 115 |
+
|
| 116 |
+
\paragraph{LoRA and low-rank adaptation.}
|
| 117 |
+
\citet{hu2022lora} demonstrated that large language model adaptation can be performed via low-rank updates $\Delta W = BA$ where $B \in \mathbb{R}^{d \times r}$ and $A \in \mathbb{R}^{r \times d}$ with $r \ll d$. This decomposition is mathematically equivalent to in-place weight modification when merged but enables reversibility and composability when kept separate. Heretic \citep{heretic2025} was the first to apply this insight to ablation, representing directional projection as rank-1 LoRA adapters.
|
| 118 |
+
|
| 119 |
\paragraph{Defense robustness.}
|
| 120 |
Models exhibit a tendency to self-repair after partial abliteration---a phenomenon we term the \emph{Hydra effect}---where residual refusal circuitry compensates for removed directions. \citet{qi2025safety} mapped safety-capability entanglement, showing that removing safety features often degrades general capabilities. \citet{zou2024circuit} proposed circuit breakers as a more robust defense via representation rerouting.
|
| 121 |
|
|
|
|
| 123 |
\section{Platform Architecture}
|
| 124 |
\label{sec:architecture}
|
| 125 |
|
| 126 |
+
\textsc{Obliteratus} is organized into six principal subsystems (Figure~\ref{fig:architecture}):
|
| 127 |
|
| 128 |
\begin{enumerate}[leftmargin=*]
|
| 129 |
+
\item \textbf{Abliteration Pipeline} (\texttt{obliteratus.abliterate}): A six-stage pipeline (SUMMON, PROBE, DISTILL, EXCISE, VERIFY, REBIRTH) that orchestrates end-to-end refusal removal from model loading through quality-verified export, with MoE-aware processing at every stage.
|
| 130 |
\item \textbf{Analysis Modules} (\texttt{obliteratus.analysis}): Fifteen specialized analyzers for mechanistic characterization of refusal, from basic direction extraction to novel geometric and transfer analyses.
|
| 131 |
+
\item \textbf{Evaluation Suite} (\texttt{obliteratus.evaluation}): Automated quality assessment using six complementary metrics, plus multi-method and multi-model benchmarking with publication-quality visualization.
|
| 132 |
+
\item \textbf{Ablation Framework} (\texttt{obliteratus.strategies}): Four ablation strategies (layer removal, head pruning, FFN ablation, embedding ablation) for systematic component-level analysis, with MoE expert-aware variants.
|
| 133 |
+
\item \textbf{Frontier Optimization} (\texttt{obliteratus.bayesian\_optimizer}, \texttt{obliteratus.lora\_ablation}): Bayesian hyperparameter search, reversible LoRA adapters, KL co-optimization, CoT-aware ablation, float layer interpolation, and activation winsorization.
|
| 134 |
+
\item \textbf{Web Research Dashboard} (\texttt{app.py}): Interactive HuggingFace Spaces application with seven tabs: Obliterate, Chat, A/B Compare, Strength Sweep, Export, Benchmark Lab, and About.
|
| 135 |
\end{enumerate}
|
| 136 |
|
| 137 |
+
The platform supports any HuggingFace \texttt{transformers} model via automatic architecture detection, handling both Conv1D and Linear projection layers, standard and fused attention patterns, MoE routers and fused 3D expert tensors, and custom architectures through \texttt{trust\_remote\_code}. A curated registry of 48 models across five compute tiers (Tiny through Frontier) provides recommended configurations, including dedicated MoE presets for GPT-OSS 20B, Mixtral, and DeepSeek-MoE.
|
| 138 |
|
| 139 |
\begin{figure}[t]
|
| 140 |
\centering
|
|
|
|
| 142 |
\begin{verbatim}
|
| 143 |
SUMMON ──► PROBE ──► DISTILL ──► EXCISE ──► VERIFY ──► REBIRTH
|
| 144 |
(load) (collect) (SVD) (project) (eval) (save)
|
| 145 |
+
│ │ │ │ │
|
| 146 |
+
│ ┌────┴────┐ ┌─┴──┐ ┌──┴───┐ ┌─┴────────┐
|
| 147 |
+
│ │ 15 Anal. │ │EGA │ │LoRA │ │ KL co-opt│
|
| 148 |
+
│ │ Modules │ │dirs│ │adapt.│ │ + Hydra │
|
| 149 |
+
│ └─────────┘ └────┘ └──────┘ └──────────┘
|
| 150 |
+
│ │ │
|
| 151 |
+
▼ ▼ ▼
|
| 152 |
+
┌──────────────────────────────────────────┐
|
| 153 |
+
│ MoE Router Analysis + Expert-Granular │
|
| 154 |
+
│ Abliteration (fused 3D selective inv.) │
|
| 155 |
+
└──────────────────────────────────────────┘
|
| 156 |
\end{verbatim}
|
| 157 |
+
\caption{High-level architecture of the \textsc{Obliteratus} pipeline. The six-stage abliteration flow (top) integrates 15 analysis modules, Expert-Granular Abliteration (EGA) for MoE models, reversible LoRA adapters, and KL co-optimization with Hydra compensation. MoE-aware processing runs at every stage.}
|
| 158 |
\label{fig:architecture}
|
| 159 |
\end{figure}
|
| 160 |
|
|
|
|
| 430 |
\subsection{Weight Projection (Permanent)}
|
| 431 |
\label{sec:weight_projection}
|
| 432 |
|
| 433 |
+
\textsc{Obliteratus} provides seven abliteration presets spanning the full spectrum from conservative single-direction removal to maximally aggressive multi-pass excision (Table~\ref{tab:methods}).
|
| 434 |
|
| 435 |
\begin{table}[h]
|
| 436 |
\centering
|
| 437 |
+
\caption{Abliteration method presets. All presets beyond Basic support layer-adaptive strength, where per-layer regularization is modulated by refusal norm.}
|
| 438 |
\label{tab:methods}
|
| 439 |
\small
|
| 440 |
+
\begin{tabular}{@{}lcccccc@{}}
|
| 441 |
\toprule
|
| 442 |
+
\textbf{Method} & \textbf{Dirs.} & \textbf{Norm-pres.} & \textbf{Reg.} & \textbf{Passes} & \textbf{Special} \\
|
| 443 |
\midrule
|
| 444 |
+
Basic & 1 (DiM) & No & None & 1 & --- \\
|
| 445 |
+
Advanced & 4 (SVD) & Yes & $\lambda{=}0.1$ & 2 & --- \\
|
| 446 |
+
Aggressive & 8 (SVD) & Yes & None & 3 & --- \\
|
| 447 |
+
Surgical & 6 (wSVD) & Yes & $\lambda{=}0.15$ & 2 & Whitened SVD, JB-contrastive \\
|
| 448 |
+
Optimized & 4 (SVD) & Yes & Bayesian & 2 & Optuna TPE, KL co-opt \\
|
| 449 |
+
Inverted & 6 (SVD) & Yes & None & 3 & Selective inversion \\
|
| 450 |
+
Nuclear & 10 (wSVD) & Yes & None & 4 & All techniques combined \\
|
| 451 |
\bottomrule
|
| 452 |
\end{tabular}
|
| 453 |
\end{table}
|
| 454 |
|
| 455 |
+
\paragraph{Whitened SVD and jailbreak-contrastive blending.}
|
| 456 |
+
The Surgical, Optimized, and Nuclear presets use whitened SVD (Section~\ref{sec:whitened_svd}) for direction extraction, which removes baseline anisotropy. Additionally, the Surgical and Nuclear presets blend in \emph{jailbreak-contrastive} directions---extracted from pairs of harmful prompts versus their jailbreak-reformulated counterparts---to target directions that specifically resist jailbreak attempts.
|
| 457 |
+
|
| 458 |
The core projection for a weight matrix $\mathbf{W}$ and refusal directions $\{\mathbf{r}_1, \ldots, \mathbf{r}_k\}$:
|
| 459 |
\begin{equation}
|
| 460 |
\mathbf{W}' = \mathbf{W} - \sum_{i=1}^k \left[(1-\lambda)\mathbf{W}\mathbf{r}_i\mathbf{r}_i^\top\right]
|
| 461 |
\end{equation}
|
| 462 |
where $\lambda$ is the regularization strength (preserves $\lambda$ fraction of the refusal component).
|
| 463 |
|
| 464 |
+
\paragraph{Per-layer adaptive strength.}
|
| 465 |
+
Rather than applying uniform regularization, \textsc{Obliteratus} modulates $\lambda$ per-layer based on the refusal norm profile. Layers with stronger refusal signal (higher $\|\mathbf{r}_l\|$) receive lower regularization (more aggressive removal), while layers near the periphery of the refusal distribution receive higher regularization:
|
| 466 |
+
\begin{equation}
|
| 467 |
+
\lambda_l = \lambda_{\text{base}} + (1 - w_l)(1 - \lambda_{\text{base}}) \cdot 0.15, \quad
|
| 468 |
+
w_l = \frac{\|\mathbf{r}_l\| - \min_j \|\mathbf{r}_j\|}{\max_j \|\mathbf{r}_j\| - \min_j \|\mathbf{r}_j\|}
|
| 469 |
+
\end{equation}
|
| 470 |
+
|
| 471 |
\paragraph{Norm-preserving rescaling.}
|
| 472 |
After projection, we rescale to preserve the Frobenius norm \citep{grimjim2025}:
|
| 473 |
\begin{equation}
|
|
|
|
| 475 |
\end{equation}
|
| 476 |
This prevents cascading magnitude drift through LayerNorm layers.
|
| 477 |
|
| 478 |
+
\paragraph{Selective inversion.}
|
| 479 |
+
The Inverted and Nuclear presets employ a technique where instead of removing the refusal direction component, the projection is \emph{reflected} (scaled by $-1$):
|
| 480 |
+
\begin{equation}
|
| 481 |
+
\mathbf{W}' = \mathbf{W} - 2\mathbf{W}\mathbf{r}\mathbf{r}^\top
|
| 482 |
+
\end{equation}
|
| 483 |
+
This flips the model's refusal behavior to active compliance, which can be more effective than simple removal for models with deeply entangled refusal mechanisms.
|
| 484 |
+
|
| 485 |
\paragraph{Bias term projection.}
|
| 486 |
Unlike prior tools that only modify weight matrices, \textsc{Obliteratus} also projects refusal directions out of bias vectors when present:
|
| 487 |
\begin{equation}
|
|
|
|
| 489 |
\end{equation}
|
| 490 |
|
| 491 |
\paragraph{Iterative refinement.}
|
| 492 |
+
Presets with multiple passes recompute projections after each modification, catching rotated residual refusal that a single pass misses. The Nuclear preset performs 4 passes with true iterative re-probing: after each excision round, activations are re-collected and new residual directions are extracted.
|
| 493 |
|
| 494 |
\subsection{Steering Vectors (Reversible)}
|
| 495 |
\label{sec:steering}
|
|
|
|
| 534 |
\item \textbf{Effective Rank}: Shannon entropy-based dimensionality of weight matrices (Equation~1). Tracks whether abliteration collapses the weight space.
|
| 535 |
\end{enumerate}
|
| 536 |
|
| 537 |
+
% ═════════════════════════════════════════════════════════════════════
|
| 538 |
+
\section{Expert-Granular Abliteration for MoE Models}
|
| 539 |
+
\label{sec:moe}
|
| 540 |
+
|
| 541 |
+
Mixture-of-Experts (MoE) models present challenges that no prior abliteration tool addresses. In dense transformers, each layer has a single FFN block whose weights can be directly projected. In MoE models, the FFN is replaced by a router network and $E$ expert sub-networks, each processing a subset of tokens. Refusal behavior may be concentrated in specific experts, and modifying all experts uniformly risks destroying capabilities encoded in non-safety-related experts.
|
| 542 |
+
|
| 543 |
+
\subsection{Expert-Granular Abliteration (EGA)}
|
| 544 |
+
\label{sec:ega}
|
| 545 |
+
|
| 546 |
+
We introduce \emph{Expert-Granular Abliteration} (EGA), which decomposes refusal directions at per-expert granularity. The key insight is that router weights determine which experts process safety-relevant tokens, so per-expert refusal attribution should be weighted by routing probability.
|
| 547 |
+
|
| 548 |
+
\paragraph{Per-expert direction decomposition.}
|
| 549 |
+
Given harmful activations $\mathbf{H}$ at a MoE layer with router $R$ producing expert weights $\{w_e\}_{e=1}^E$ for each token:
|
| 550 |
+
\begin{equation}
|
| 551 |
+
\mathbf{r}_e = \frac{\sum_{i} w_{e,i} \cdot (\mathbf{h}_i - \mathbf{b}_i)}{\sum_{i} w_{e,i}}, \quad e \in \{1, \ldots, E\}
|
| 552 |
+
\end{equation}
|
| 553 |
+
where $w_{e,i}$ is the routing weight for expert $e$ on token $i$. Experts with high routing weight for harmful tokens receive strong refusal directions; capability-focused experts (routed primarily for harmless tokens) receive weak or zero directions.
|
| 554 |
+
|
| 555 |
+
\paragraph{Safety vs.\ capability expert classification.}
|
| 556 |
+
We classify each expert based on its EGA safety score:
|
| 557 |
+
\begin{equation}
|
| 558 |
+
s_e = \frac{\|\mathbf{r}_e\|}{\max_j \|\mathbf{r}_j\|}
|
| 559 |
+
\end{equation}
|
| 560 |
+
Experts with $s_e > \tau_{\text{safety}}$ (default 0.5) are classified as \emph{safety-critical}; others are classified as \emph{capability-preserving}. This classification determines the intervention strategy.
|
| 561 |
+
|
| 562 |
+
\subsection{Fused 3D Weight Handling}
|
| 563 |
+
\label{sec:fused3d}
|
| 564 |
+
|
| 565 |
+
Many MoE implementations (including GPT-OSS 20B) use \emph{fused} weight tensors $\mathbf{W} \in \mathbb{R}^{E \times d_{\text{hidden}} \times d_{\text{intermediate}}}$ rather than separate per-expert weight matrices. Standard 2D projection cannot be directly applied.
|
| 566 |
+
|
| 567 |
+
\paragraph{Per-slice projection.}
|
| 568 |
+
For each expert slice $\mathbf{W}_e = \mathbf{W}[e, :, :]$:
|
| 569 |
+
\begin{equation}
|
| 570 |
+
\mathbf{W}_e' = \mathbf{W}_e - (1-\lambda_e) \cdot \mathbf{W}_e \mathbf{r}_e \mathbf{r}_e^\top
|
| 571 |
+
\end{equation}
|
| 572 |
+
where $\lambda_e$ is the expert-specific regularization derived from the EGA safety score.
|
| 573 |
+
|
| 574 |
+
\paragraph{Selective inversion for MoE.}
|
| 575 |
+
The Inverted preset applies \emph{differentiated} treatment to fused 3D tensors. Safety-critical experts receive reflection (scale $= -2$), while capability-preserving experts receive standard removal (scale $= -1$):
|
| 576 |
+
\begin{equation}
|
| 577 |
+
\mathbf{W}_e' = \begin{cases}
|
| 578 |
+
\mathbf{W}_e - 2\mathbf{W}_e\mathbf{r}_e\mathbf{r}_e^\top & \text{if } s_e > \tau_{\text{safety}} \quad \text{(reflection)} \\
|
| 579 |
+
\mathbf{W}_e - \mathbf{W}_e\mathbf{r}_e\mathbf{r}_e^\top & \text{otherwise} \quad \text{(removal)}
|
| 580 |
+
\end{cases}
|
| 581 |
+
\end{equation}
|
| 582 |
+
This prevents over-ablation of capability experts---a critical failure mode we identified in uniform approaches, where applying 2$\times$ reflection to all experts on GPT-OSS 20B degraded mathematical reasoning by over 30\%.
|
| 583 |
+
|
| 584 |
+
\subsection{Router-Aware Processing}
|
| 585 |
+
|
| 586 |
+
Beyond expert weights, the router network itself may encode safety-relevant routing preferences. \textsc{Obliteratus} optionally projects refusal directions out of router weight matrices, causing the model to route previously-refused tokens to capability experts rather than safety experts. This is controlled by the \texttt{project\_biases} flag and is enabled by default for the Nuclear preset.
|
| 587 |
+
|
| 588 |
+
% ═════════════════════════════════════════════════════════════════════
|
| 589 |
+
\section{Frontier Optimization Techniques}
|
| 590 |
+
\label{sec:frontier}
|
| 591 |
+
|
| 592 |
+
Building on Heretic's \citep{heretic2025} pioneering work, \textsc{Obliteratus} integrates six frontier optimization techniques that improve abliteration quality beyond what fixed-parameter approaches can achieve.
|
| 593 |
+
|
| 594 |
+
\subsection{Bayesian Hyperparameter Optimization}
|
| 595 |
+
\label{sec:bayesian}
|
| 596 |
+
|
| 597 |
+
Following Heretic, we use Optuna's TPE (Tree-structured Parzen Estimator) sampler \citep{akiba2019optuna} for multi-objective optimization of per-layer regularization strengths. Unlike Heretic, which initializes randomly, \textsc{Obliteratus} \emph{warm-starts} from analysis-derived heuristics:
|
| 598 |
+
|
| 599 |
+
\paragraph{Warm-start initialization.}
|
| 600 |
+
The first trial uses regularization values derived from the analysis pipeline:
|
| 601 |
+
\begin{equation}
|
| 602 |
+
\lambda_l^{(0)} = (1 - w_l) \cdot 0.3
|
| 603 |
+
\end{equation}
|
| 604 |
+
where $w_l$ is the layer-adaptive weight from Equation~(8). Subsequent trials are biased toward the warm-start region: $\lambda_l \in [\max(0, \lambda_l^{(0)} - 0.3), \min(1, \lambda_l^{(0)} + 0.3)]$. This enables convergence in 50 trials versus Heretic's 200.
|
| 605 |
+
|
| 606 |
+
\paragraph{Multi-objective formulation.}
|
| 607 |
+
Each trial jointly minimizes refusal rate $\rho$ and KL divergence $D_{\text{KL}}$:
|
| 608 |
+
\begin{equation}
|
| 609 |
+
\min_{\boldsymbol{\lambda}} \left(\rho(\boldsymbol{\lambda}),\; D_{\text{KL}}(\boldsymbol{\lambda})\right)
|
| 610 |
+
\end{equation}
|
| 611 |
+
with Pareto-optimal solutions ranked by a weighted composite: $\rho + 0.5 \cdot D_{\text{KL}}$, prioritizing refusal removal.
|
| 612 |
+
|
| 613 |
+
\subsection{Reversible LoRA-Mediated Ablation}
|
| 614 |
+
\label{sec:lora}
|
| 615 |
+
|
| 616 |
+
Inspired by Heretic's rank-1 LoRA ablation, we extend the approach to \emph{rank-$k$} adapters supporting multi-direction removal. The mathematical equivalence:
|
| 617 |
+
\begin{align}
|
| 618 |
+
\text{In-place:} \quad \mathbf{W}' &= \mathbf{W} - s \cdot (\mathbf{d}\mathbf{d}^\top)\mathbf{W} \\
|
| 619 |
+
\text{LoRA:} \quad \mathbf{W}' &= \mathbf{W} + \mathbf{B}\mathbf{A}, \quad \mathbf{B} = -s \cdot \text{coeff}, \quad \mathbf{A} = \mathbf{d}^\top
|
| 620 |
+
\end{align}
|
| 621 |
+
where $\text{coeff} = \mathbf{W}\mathbf{d}$ is the projection coefficient and $s = 1 - \lambda$. For rank-$k$ with directions $\{\mathbf{d}_1, \ldots, \mathbf{d}_k\}$:
|
| 622 |
+
\begin{equation}
|
| 623 |
+
\mathbf{B} = [-s\cdot\text{coeff}_1 \mid \cdots \mid -s\cdot\text{coeff}_k] \in \mathbb{R}^{d_{\text{out}} \times k}, \quad
|
| 624 |
+
\mathbf{A} = [\mathbf{d}_1 ; \cdots ; \mathbf{d}_k] \in \mathbb{R}^{k \times d_{\text{in}}}
|
| 625 |
+
\end{equation}
|
| 626 |
+
|
| 627 |
+
Adapters are stored in half precision and saved in a PEFT-compatible format. They can be merged for permanent modification or kept separate for reversible deployment.
|
| 628 |
+
|
| 629 |
+
\subsection{KL-Divergence Co-Optimization}
|
| 630 |
+
\label{sec:kl_coopt}
|
| 631 |
+
|
| 632 |
+
After projection, we measure first-token KL divergence on harmless reference prompts. If $D_{\text{KL}}$ exceeds a threshold $\delta$ (default 0.1), a partial revert is applied:
|
| 633 |
+
\begin{equation}
|
| 634 |
+
\mathbf{W}'' = \mathbf{W}' + \gamma \cdot (\mathbf{d}\mathbf{d}^\top)
|
| 635 |
+
\end{equation}
|
| 636 |
+
where $\gamma$ is computed from the stored KL proxy magnitude. A subtle issue arises when the post-projection coefficient $\mathbf{W}'\mathbf{d} \approx 0$ (as occurs with zero regularization): in this case, we use the \emph{pre-projection} coefficient magnitude as a proxy:
|
| 637 |
+
\begin{equation}
|
| 638 |
+
\gamma = \gamma_{\text{strength}} \cdot \begin{cases}
|
| 639 |
+
\text{coeff}_{\text{post}} & \text{if } |\text{coeff}_{\text{post}}| > \epsilon \\
|
| 640 |
+
\text{coeff}_{\text{proxy}} & \text{otherwise}
|
| 641 |
+
\end{cases}
|
| 642 |
+
\end{equation}
|
| 643 |
+
This prevents the revert from being a no-op for fully-projected layers---a bug we identified and fixed in our implementation.
|
| 644 |
+
|
| 645 |
+
\subsection{Chain-of-Thought-Aware Ablation}
|
| 646 |
+
\label{sec:cot}
|
| 647 |
+
|
| 648 |
+
Chain-of-thought (CoT) models (GPT-OSS, QwQ, DeepSeek-R1) maintain internal reasoning traces that may share geometric structure with refusal directions. Na\"ive ablation can disrupt CoT coherence. We preserve reasoning by computing a CoT direction $\mathbf{c}$ from paired reasoning/non-reasoning activations and applying Gram-Schmidt orthogonalization:
|
| 649 |
+
\begin{equation}
|
| 650 |
+
\mathbf{r}' = \mathbf{r} - \frac{\mathbf{r} \cdot \mathbf{c}}{\|\mathbf{c}\|^2} \mathbf{c}
|
| 651 |
+
\end{equation}
|
| 652 |
+
The modified refusal direction $\mathbf{r}'$ is orthogonal to the CoT direction, ensuring that projection removes refusal without affecting reasoning chain generation.
|
| 653 |
+
|
| 654 |
+
\subsection{Float Layer Interpolation}
|
| 655 |
+
\label{sec:float_interp}
|
| 656 |
+
|
| 657 |
+
Rather than treating layer selection as binary (ablate or not), float layer interpolation applies a continuous Gaussian-weighted strength profile across layers:
|
| 658 |
+
\begin{equation}
|
| 659 |
+
w_l = \exp\left(-\frac{(l - \mu_{\text{center}})^2}{2\sigma^2}\right), \quad
|
| 660 |
+
\sigma = \max\left(0.5,\; \frac{l_{\max} - l_{\min}}{4}\right)
|
| 661 |
+
\end{equation}
|
| 662 |
+
where $\mu_{\text{center}}$ is the midpoint of the selected layers and $l_{\min}, l_{\max}$ are the minimum and maximum layer indices (not norm-sorted indices). This produces smooth falloff at the boundaries of the ablation window, avoiding abrupt transitions that can cause coherence artifacts.
|
| 663 |
+
|
| 664 |
+
\subsection{Activation Winsorization}
|
| 665 |
+
\label{sec:winsorization}
|
| 666 |
+
|
| 667 |
+
Outlier activations can dominate SVD and distort refusal direction extraction. Before SVD, we apply percentile-based winsorization:
|
| 668 |
+
\begin{equation}
|
| 669 |
+
\tilde{a}_{i} = \text{clamp}(a_{i},\; q_{\alpha/2},\; q_{1-\alpha/2})
|
| 670 |
+
\end{equation}
|
| 671 |
+
where $q_p$ denotes the $p$-th percentile and $\alpha = 0.05$ by default (2.5th and 97.5th percentiles). This produces more robust refusal directions that are less sensitive to individual anomalous activations, particularly important for MoE models where expert routing can create multimodal activation distributions.
|
| 672 |
+
|
| 673 |
% ═════════════════════════════════════════════════════════════════════
|
| 674 |
\section{Analysis-Informed Abliteration}
|
| 675 |
\label{sec:informed}
|
|
|
|
| 734 |
|
| 735 |
where $d_{\text{cone}}$ is the cone dimensionality from Section~\ref{sec:concept_cones}, $\lambda_{\text{base}}$ is a per-method base regularization, $e_{\text{entangle}}$ is the entanglement score, and $\hat{r}_{\text{repair}}$ is the estimated self-repair capacity.
|
| 736 |
|
| 737 |
+
% ═════════════════════════════════════════════════════════════════════
|
| 738 |
+
\section{Web Research Dashboard}
|
| 739 |
+
\label{sec:dashboard}
|
| 740 |
+
|
| 741 |
+
\textsc{Obliteratus} ships with an interactive web application built on Gradio and deployed as a HuggingFace Space, providing seven tabs for research workflows:
|
| 742 |
+
|
| 743 |
+
\paragraph{Obliterate tab.}
|
| 744 |
+
The primary interface: select a model (from 48 presets or any HuggingFace model ID), choose a method preset (Basic through Nuclear), configure parameters (prompt volume, dataset source, compute tier), and run the full pipeline with live progress logging. Results are displayed as a structured report with key metrics and downloadable artifacts.
|
| 745 |
+
|
| 746 |
+
\paragraph{Chat tab.}
|
| 747 |
+
Interactive chat with the abliterated model, supporting configurable system prompts, temperature, top-$p$, repetition penalty, and maximum token length. Enables rapid qualitative evaluation of abliteration quality on adversarial prompts.
|
| 748 |
+
|
| 749 |
+
\paragraph{A/B Comparison tab.}
|
| 750 |
+
Side-by-side generation from the original and abliterated models on the same prompt. The original model is loaded on-demand, and both models generate with identical parameters, allowing direct behavioral comparison. This is critical for demonstrating that abliteration removes refusal without degrading general capabilities.
|
| 751 |
+
|
| 752 |
+
\paragraph{Strength Sweep tab.}
|
| 753 |
+
Generates a dose-response curve by sweeping regularization strength from 0 (full projection) to 1 (no projection) in configurable steps. Produces dual-axis plots (refusal rate and perplexity vs.\ regularization) and Pareto scatter plots (refusal vs.\ perplexity colored by regularization), enabling researchers to identify the optimal operating point for their use case.
|
| 754 |
+
|
| 755 |
+
\paragraph{Export tab.}
|
| 756 |
+
One-click packaging of all research artifacts into a downloadable ZIP archive: refusal direction tensors (\texttt{.pt}), configuration JSON, results CSV, and full pipeline log. Enables reproducibility and downstream analysis in external tools.
|
| 757 |
+
|
| 758 |
+
\paragraph{Benchmark Lab tab.}
|
| 759 |
+
Multi-method comparison (run all 7 presets on a single model) and multi-model comparison (run a single preset across multiple models). Results are presented as publication-quality visualizations including radar charts, grouped bar plots, Pareto frontiers, and method ranking tables. Figures are generated at 300 DPI for direct inclusion in papers.
|
| 760 |
+
|
| 761 |
+
\paragraph{About tab.}
|
| 762 |
+
Comprehensive documentation of all 7 method presets with their configurations, the mathematical foundations of key techniques, and attribution to prior work including Heretic.
|
| 763 |
+
|
| 764 |
% ═════════════════════════════════════════════════════════════════════
|
| 765 |
\section{Comparison with Existing Tools}
|
| 766 |
\label{sec:comparison}
|
|
|
|
| 769 |
|
| 770 |
\begin{table}[t]
|
| 771 |
\centering
|
| 772 |
+
\caption{Feature comparison across refusal analysis and intervention tools. \textsc{Obliteratus} subsumes all of Heretic's innovations while adding MoE support, analysis modules, and a web dashboard. $^\dagger$Heretic pioneered Bayesian optimization and LoRA ablation; \textsc{Obliteratus} extends both.}
|
| 773 |
\label{tab:comparison}
|
| 774 |
\small
|
| 775 |
\begin{tabular}{@{}lcccccc@{}}
|
|
|
|
| 777 |
\textbf{Capability} & \rotatebox{60}{\textsc{Obliteratus}} & \rotatebox{60}{TransformerLens} & \rotatebox{60}{Heretic} & \rotatebox{60}{FailSpy abl.} & \rotatebox{60}{RepEng} & \rotatebox{60}{SAELens} \\
|
| 778 |
\midrule
|
| 779 |
Direction extraction methods & 3 & Manual & 1 & 1 & 1 & -- \\
|
| 780 |
+
Method presets & 7 & -- & 1 & 1 & -- & -- \\
|
| 781 |
+
Weight projection variants & 7+ & -- & Bayesian$^\dagger$ & 1 & -- & -- \\
|
| 782 |
+
Bayesian optimization & Warm-start$^\dagger$ & -- & TPE$^\dagger$ & -- & -- & -- \\
|
| 783 |
+
LoRA-mediated ablation & Rank-$k^\dagger$ & -- & Rank-1$^\dagger$ & -- & -- & -- \\
|
| 784 |
+
KL co-optimization & \checkmark & -- & -- & -- & -- & -- \\
|
| 785 |
+
CoT-aware ablation & \checkmark & -- & -- & -- & -- & -- \\
|
| 786 |
+
Float layer interpolation & \checkmark & -- & -- & -- & -- & -- \\
|
| 787 |
+
Activation winsorization & \checkmark & -- & -- & -- & -- & -- \\
|
| 788 |
Steering vectors & \checkmark & -- & -- & -- & Core & -- \\
|
| 789 |
+
MoE/expert-granular & \checkmark & -- & -- & -- & -- & -- \\
|
| 790 |
+
Fused 3D weight handling & \checkmark & -- & -- & -- & -- & -- \\
|
| 791 |
+
Selective inversion & \checkmark & -- & -- & -- & -- & -- \\
|
| 792 |
Concept cone geometry & \checkmark & -- & -- & -- & -- & -- \\
|
| 793 |
Alignment fingerprinting & \checkmark & -- & -- & -- & -- & -- \\
|
| 794 |
Cross-model transfer & \checkmark & -- & -- & -- & -- & -- \\
|
| 795 |
Defense robustness eval. & \checkmark & -- & -- & -- & -- & -- \\
|
| 796 |
Analysis-informed pipeline & \checkmark & -- & -- & -- & -- & -- \\
|
| 797 |
+
Web research dashboard & \checkmark & -- & -- & -- & -- & -- \\
|
| 798 |
+
A/B comparison chat & \checkmark & -- & -- & -- & -- & -- \\
|
| 799 |
+
Strength sweep / dose-resp. & \checkmark & -- & -- & -- & -- & -- \\
|
| 800 |
+
Benchmark Lab (pub.-quality) & \checkmark & -- & -- & -- & -- & -- \\
|
| 801 |
Real causal tracing & Approx. & \checkmark & -- & -- & -- & -- \\
|
| 802 |
Sparse autoencoders & -- & Via SAE & -- & -- & -- & Core \\
|
| 803 |
Model compatibility & Any HF & $\sim$50 & 16 & TLens & HF & TLens \\
|
| 804 |
+
MoE model support & Native & -- & -- & -- & -- & -- \\
|
| 805 |
Test suite & 379 & Community & -- & -- & Min. & Mod. \\
|
| 806 |
\bottomrule
|
| 807 |
\end{tabular}
|
|
|
|
| 810 |
The key differentiators of \textsc{Obliteratus} are:
|
| 811 |
|
| 812 |
\begin{enumerate}[leftmargin=*]
|
| 813 |
+
\item \textbf{MoE-native processing}: The only abliteration tool with Expert-Granular Abliteration, fused 3D weight handling, and per-expert selective inversion. This is critical for models like GPT-OSS 20B where uniform approaches degrade capabilities.
|
| 814 |
\item \textbf{Analysis breadth}: No other tool provides concept cone geometry, alignment imprint detection, cross-model universality analysis, or defense robustness evaluation.
|
| 815 |
+
\item \textbf{Heretic superset with extensions}: We incorporate all of Heretic's innovations (Bayesian optimization, LoRA ablation) while adding warm-start initialization, rank-$k$ adapters, KL co-optimization, CoT-aware ablation, float layer interpolation, and activation winsorization.
|
| 816 |
+
\item \textbf{Seven intervention presets}: From conservative (Basic) through maximally aggressive (Nuclear), each preset composes a distinct combination of techniques for different use cases.
|
| 817 |
+
\item \textbf{Interactive research dashboard}: A/B comparison chat, dose-response strength sweeps, and publication-quality benchmarking provide research workflows that no other tool offers.
|
| 818 |
+
\item \textbf{Architecture coverage}: Working with any HuggingFace model---including fused MoE architectures---rather than requiring specific architecture support.
|
| 819 |
\end{enumerate}
|
| 820 |
|
| 821 |
+
Conversely, TransformerLens provides real activation patching (our causal tracing is approximate) and SAELens provides sparse autoencoder analysis that \textsc{Obliteratus} does not. We view these as complementary tools, not competitors, for the analysis modules they excel at.
|
| 822 |
|
| 823 |
% ═════════════════════════════════════════════════════════════════════
|
| 824 |
\section{Discussion and Limitations}
|
|
|
|
| 833 |
\paragraph{Alignment fingerprinting validation.}
|
| 834 |
The alignment imprint detector uses heuristic signatures derived from the literature's characterization of different training methods. While the geometric features (Gini, effective rank, smoothness) are well-motivated, the ideal values and classification boundaries would benefit from systematic validation across models with known training procedures.
|
| 835 |
|
| 836 |
+
\paragraph{MoE expert classification.}
|
| 837 |
+
The EGA safety score threshold ($\tau = 0.5$) for classifying experts as safety-critical vs.\ capability-preserving is a heuristic. A more principled approach would train expert classifiers on labeled routing data or use causal interventions to establish ground-truth expert roles. We leave this to future work.
|
| 838 |
+
|
| 839 |
+
\paragraph{Bayesian optimization cost.}
|
| 840 |
+
Each optimization trial requires a forward pass for KL measurement and generation for refusal measurement. With 50 trials at 8 prompts each, this adds significant compute time. Our warm-start strategy reduces the required trials from $\sim$200 (Heretic) to $\sim$50, but further efficiency improvements---such as surrogate model transfer between similar model architectures---are possible.
|
| 841 |
+
|
| 842 |
\paragraph{Scaling considerations.}
|
| 843 |
+
The current implementation loads the full model into memory for analysis. For frontier-scale models (100B+ parameters), this requires significant compute. Future work could integrate quantized inference or offloading strategies. The web dashboard requires GPU access for interactive features (chat, A/B comparison, strength sweep).
|
| 844 |
|
| 845 |
\paragraph{Evaluation completeness.}
|
| 846 |
Our evaluation suite measures \emph{refusal removal} and \emph{capability preservation} but does not comprehensively assess downstream task performance across diverse benchmarks. Integration with evaluation harnesses such as lm-evaluation-harness \citep{gao2021framework} is a natural extension.
|
| 847 |
|
| 848 |
\paragraph{Future directions.}
|
| 849 |
+
We identify several opportunities: (1)~integration with sparse autoencoder analysis to understand refusal at the feature level, potentially enabling even more targeted ablation; (2)~real causal tracing via TransformerLens integration; (3)~longitudinal studies tracking how refusal geometry evolves during fine-tuning; (4)~extension of the universality analysis to a wider set of model families; (5)~application of the defense robustness framework to evaluate proposed robust alignment methods including circuit breakers \citep{zou2024circuit} and representation rerouting; (6)~multi-objective Bayesian optimization with additional objectives such as CoT coherence and downstream task performance; and (7)~automated expert role discovery for MoE models using unsupervised clustering of expert activation patterns.
|
| 850 |
|
| 851 |
% ═════════════════════════════════════════════════════════════════════
|
| 852 |
\section{Broader Impact Statement}
|
|
|
|
| 879 |
% ═════════════════════════════════════════════════════════════════════
|
| 880 |
\section{Conclusion}
|
| 881 |
|
| 882 |
+
We presented \textsc{Obliteratus}, an open-source platform that unifies mechanistic analysis of refusal mechanisms with surgical intervention capabilities, featuring first-of-its-kind support for Mixture-of-Experts architectures.
|
| 883 |
+
|
| 884 |
+
The platform's contributions span multiple axes:
|
| 885 |
+
\emph{Analysis} --- 15 modules providing the most comprehensive characterization of refusal geometry in any public tool, including concept cone geometry with DSI, alignment imprint detection, cross-model universality, and defense robustness evaluation.
|
| 886 |
+
\emph{Intervention} --- seven method presets (Basic through Nuclear) composing techniques from single-direction removal to multi-pass whitened SVD with selective inversion, plus reversible steering vectors and LoRA-mediated ablation.
|
| 887 |
+
\emph{MoE-native processing} --- Expert-Granular Abliteration decomposes refusal at per-expert granularity, fused 3D weight handling enables direct operation on packed expert tensors, and selective inversion differentiates safety-critical from capability-preserving experts.
|
| 888 |
+
\emph{Frontier optimization} --- Bayesian hyperparameter search with warm-start from analysis heuristics, KL co-optimization with proxy-magnitude partial revert, chain-of-thought-aware Gram-Schmidt orthogonalization, float layer interpolation, and activation winsorization---incorporating and extending all innovations from Heretic \citep{heretic2025}.
|
| 889 |
+
\emph{Interactive research} --- a web dashboard with A/B comparison chat, dose-response strength sweeps, multi-model benchmarking, and artifact export.
|
| 890 |
+
|
| 891 |
+
The analysis-informed pipeline closes the feedback loop, using analysis outputs to auto-configure abliteration parameters---a capability unique to \textsc{Obliteratus}. The unified evaluation suite ensures that every intervention is quantitatively assessed.
|
| 892 |
|
| 893 |
+
By making these tools available under an MIT license with comprehensive documentation and 379 unit tests, we aim to accelerate both offensive and defensive alignment research: understanding the geometric structure of refusal---across dense and MoE architectures alike---is the foundation for both removing it surgically and building more robust implementations.
|
| 894 |
|
| 895 |
% ═════════════════════════════════════════════════════════════════════
|
| 896 |
\bibliographystyle{plainnat}
|
paper/references.bib
CHANGED
|
@@ -179,6 +179,66 @@
|
|
| 179 |
year={2025}
|
| 180 |
}
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
% ── Evaluation ────────────────────────────────────────────────────────
|
| 183 |
|
| 184 |
@article{gao2021framework,
|
|
|
|
| 179 |
year={2025}
|
| 180 |
}
|
| 181 |
|
| 182 |
+
% ── Heretic and Bayesian Abliteration ────────────────────────────────
|
| 183 |
+
|
| 184 |
+
@misc{heretic2025,
|
| 185 |
+
title={Heretic: Bayesian Optimization for {LLM} Abliteration},
|
| 186 |
+
author={{p-e-w}},
|
| 187 |
+
year={2025},
|
| 188 |
+
howpublished={\url{https://github.com/p-e-w/heretic}},
|
| 189 |
+
note={Pioneered Bayesian optimization and LoRA-mediated ablation for refusal removal}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
@inproceedings{akiba2019optuna,
|
| 193 |
+
title={Optuna: A Next-generation Hyperparameter Optimization Framework},
|
| 194 |
+
author={Akiba, Takuya and Sano, Shotaro and Yanase, Toshihiko and Ohta, Takeru and Koyama, Masanori},
|
| 195 |
+
booktitle={Proceedings of the 25th ACM SIGKDD International Conference on Knowledge Discovery \& Data Mining},
|
| 196 |
+
pages={2623--2631},
|
| 197 |
+
year={2019}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
% ── LoRA and Low-Rank Adaptation ────────────────────────────────────
|
| 201 |
+
|
| 202 |
+
@article{hu2022lora,
|
| 203 |
+
title={{LoRA}: Low-Rank Adaptation of Large Language Models},
|
| 204 |
+
author={Hu, Edward J and Shen, Yelong and Wallis, Phillip and Allen-Zhu, Zeyuan and Li, Yuanzhi and Wang, Shean and Wang, Lu and Chen, Weizhu},
|
| 205 |
+
journal={International Conference on Learning Representations},
|
| 206 |
+
year={2022}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
% ── Mixture-of-Experts ──────────────────────────────────────────────
|
| 210 |
+
|
| 211 |
+
@article{shazeer2017outrageously,
|
| 212 |
+
title={Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer},
|
| 213 |
+
author={Shazeer, Noam and Mirzadeh, Azalia and Macherey, Klaus and Young, Andy and Micallef, Justin and Yan, Zhifeng and Le, Quoc},
|
| 214 |
+
journal={International Conference on Learning Representations},
|
| 215 |
+
year={2017}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
@article{fedus2022switch,
|
| 219 |
+
title={Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity},
|
| 220 |
+
author={Fedus, William and Zoph, Barret and Shazeer, Noam},
|
| 221 |
+
journal={Journal of Machine Learning Research},
|
| 222 |
+
volume={23},
|
| 223 |
+
number={120},
|
| 224 |
+
pages={1--39},
|
| 225 |
+
year={2022}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
@article{jiang2024mixtral,
|
| 229 |
+
title={Mixtral of Experts},
|
| 230 |
+
author={Jiang, Albert Q and Sablayrolles, Alexandre and Roux, Antoine and Mensch, Arthur and Savary, Blanche and Bamford, Chris and Chaplot, Devendra Singh and de las Casas, Diego and Hanna, Emma Bou and Bressand, Florian and others},
|
| 231 |
+
journal={arXiv preprint arXiv:2401.04088},
|
| 232 |
+
year={2024}
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
@article{dai2024deepseekmoe,
|
| 236 |
+
title={{DeepSeekMoE}: Towards Ultimate Expert Specialization in Mixture-of-Experts Language Models},
|
| 237 |
+
author={Dai, Damai and Deng, Chengqi and Zhao, Chenggang and Xu, R X and Gao, Huazuo and Chen, Deli and Li, Jiashi and Zeng, Wangding and Yu, Xingkai and Wu, Y and others},
|
| 238 |
+
journal={arXiv preprint arXiv:2401.06066},
|
| 239 |
+
year={2024}
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
% ── Evaluation ────────────────────────────────────────────────────────
|
| 243 |
|
| 244 |
@article{gao2021framework,
|
scripts/benchmark_gpt_oss_20b.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""OBLITERATUS GPT-OSS 20B Benchmark — Full Method Comparison.
|
| 3 |
+
|
| 4 |
+
Runs all abliteration methods on openai/gpt-oss-20b and produces a
|
| 5 |
+
comprehensive comparison table with:
|
| 6 |
+
- Refusal rate (primary metric)
|
| 7 |
+
- KL divergence / perplexity (capability preservation)
|
| 8 |
+
- Capability probes (knowledge, truthfulness, math reasoning)
|
| 9 |
+
- MoE-specific metrics (EGA expert directions, router stability)
|
| 10 |
+
- Timing and GPU memory usage
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
python scripts/benchmark_gpt_oss_20b.py
|
| 14 |
+
python scripts/benchmark_gpt_oss_20b.py --methods basic surgical optimized nuclear
|
| 15 |
+
python scripts/benchmark_gpt_oss_20b.py --prompts 50 --output results.json
|
| 16 |
+
python scripts/benchmark_gpt_oss_20b.py --quick # fast mode: 20 prompts, skip slow methods
|
| 17 |
+
|
| 18 |
+
Designed for T4 16GB (auto 4-bit quantization) or A10G+ (float16).
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import argparse
|
| 24 |
+
import gc
|
| 25 |
+
import json
|
| 26 |
+
import os
|
| 27 |
+
import shutil
|
| 28 |
+
import sys
|
| 29 |
+
import time
|
| 30 |
+
from pathlib import Path
|
| 31 |
+
|
| 32 |
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 33 |
+
|
| 34 |
+
import torch
|
| 35 |
+
|
| 36 |
+
# Ensure the project root is on sys.path
|
| 37 |
+
project_root = Path(__file__).resolve().parent.parent
|
| 38 |
+
sys.path.insert(0, str(project_root))
|
| 39 |
+
|
| 40 |
+
from obliteratus.abliterate import (
|
| 41 |
+
AbliterationPipeline,
|
| 42 |
+
METHODS,
|
| 43 |
+
HARMFUL_PROMPTS,
|
| 44 |
+
HARMLESS_PROMPTS,
|
| 45 |
+
)
|
| 46 |
+
from obliteratus.evaluation.benchmarks import BenchmarkRunner, format_benchmark_report
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def parse_args():
|
| 50 |
+
parser = argparse.ArgumentParser(description="OBLITERATUS GPT-OSS 20B Benchmark")
|
| 51 |
+
parser.add_argument(
|
| 52 |
+
"--model", default="openai/gpt-oss-20b",
|
| 53 |
+
help="Model to benchmark (default: openai/gpt-oss-20b)",
|
| 54 |
+
)
|
| 55 |
+
parser.add_argument(
|
| 56 |
+
"--methods", nargs="+",
|
| 57 |
+
default=["basic", "advanced", "surgical", "optimized", "inverted", "nuclear"],
|
| 58 |
+
help="Methods to compare",
|
| 59 |
+
)
|
| 60 |
+
parser.add_argument(
|
| 61 |
+
"--prompts", type=int, default=33,
|
| 62 |
+
help="Number of prompts per side (harmful/harmless)",
|
| 63 |
+
)
|
| 64 |
+
parser.add_argument(
|
| 65 |
+
"--output", type=str, default=None,
|
| 66 |
+
help="Save results JSON to this path",
|
| 67 |
+
)
|
| 68 |
+
parser.add_argument(
|
| 69 |
+
"--quick", action="store_true",
|
| 70 |
+
help="Quick mode: 20 prompts, skip aggressive/inverted",
|
| 71 |
+
)
|
| 72 |
+
parser.add_argument(
|
| 73 |
+
"--skip-benchmarks", action="store_true",
|
| 74 |
+
help="Skip capability benchmark probes (faster)",
|
| 75 |
+
)
|
| 76 |
+
parser.add_argument(
|
| 77 |
+
"--output-dir", default="/tmp/obliteratus_bench",
|
| 78 |
+
help="Directory for temporary model outputs",
|
| 79 |
+
)
|
| 80 |
+
parser.add_argument(
|
| 81 |
+
"--bayesian-trials", type=int, default=30,
|
| 82 |
+
help="Number of Bayesian optimization trials for 'optimized' method",
|
| 83 |
+
)
|
| 84 |
+
return parser.parse_args()
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def gpu_info() -> dict:
|
| 88 |
+
"""Get GPU information."""
|
| 89 |
+
if not torch.cuda.is_available():
|
| 90 |
+
return {"gpu": "CPU only", "total_gb": 0, "free_gb": 0}
|
| 91 |
+
return {
|
| 92 |
+
"gpu": torch.cuda.get_device_name(0),
|
| 93 |
+
"total_gb": round(torch.cuda.get_device_properties(0).total_memory / 1e9, 1),
|
| 94 |
+
"free_gb": round(torch.cuda.mem_get_info(0)[0] / 1e9, 1),
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def cleanup():
|
| 99 |
+
"""Force GPU memory cleanup."""
|
| 100 |
+
gc.collect()
|
| 101 |
+
if torch.cuda.is_available():
|
| 102 |
+
torch.cuda.empty_cache()
|
| 103 |
+
torch.cuda.reset_peak_memory_stats()
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def run_single_method(
|
| 107 |
+
model_name: str,
|
| 108 |
+
method: str,
|
| 109 |
+
harmful: list[str],
|
| 110 |
+
harmless: list[str],
|
| 111 |
+
output_dir: str,
|
| 112 |
+
run_benchmarks: bool = True,
|
| 113 |
+
bayesian_trials: int = 30,
|
| 114 |
+
) -> dict:
|
| 115 |
+
"""Run a single abliteration method and collect metrics."""
|
| 116 |
+
cleanup()
|
| 117 |
+
|
| 118 |
+
outdir = f"{output_dir}/{method}"
|
| 119 |
+
t0 = time.time()
|
| 120 |
+
pipeline = None
|
| 121 |
+
result = {
|
| 122 |
+
"model": model_name,
|
| 123 |
+
"method": method,
|
| 124 |
+
"label": METHODS.get(method, {}).get("label", method),
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
try:
|
| 128 |
+
# For the optimized method, we might want to control trial count
|
| 129 |
+
extra_kwargs = {}
|
| 130 |
+
if method == "optimized":
|
| 131 |
+
# Temporarily patch bayesian_trials in the method config
|
| 132 |
+
METHODS["optimized"]["bayesian_trials"] = bayesian_trials
|
| 133 |
+
|
| 134 |
+
pipeline = AbliterationPipeline(
|
| 135 |
+
model_name=model_name,
|
| 136 |
+
output_dir=outdir,
|
| 137 |
+
device="auto",
|
| 138 |
+
dtype="float16",
|
| 139 |
+
method=method,
|
| 140 |
+
harmful_prompts=harmful,
|
| 141 |
+
harmless_prompts=harmless,
|
| 142 |
+
on_log=lambda msg: print(f" {msg}"),
|
| 143 |
+
)
|
| 144 |
+
result_path = pipeline.run()
|
| 145 |
+
elapsed = time.time() - t0
|
| 146 |
+
|
| 147 |
+
result.update({
|
| 148 |
+
"time_seconds": round(elapsed, 1),
|
| 149 |
+
"quality": dict(pipeline._quality_metrics),
|
| 150 |
+
"strong_layers": pipeline._strong_layers,
|
| 151 |
+
"n_strong_layers": len(pipeline._strong_layers),
|
| 152 |
+
"n_directions": pipeline.n_directions,
|
| 153 |
+
})
|
| 154 |
+
|
| 155 |
+
# MoE-specific metrics
|
| 156 |
+
if pipeline._expert_directions:
|
| 157 |
+
n_expert_dirs = sum(len(d) for d in pipeline._expert_directions.values())
|
| 158 |
+
result["ega_expert_dirs"] = n_expert_dirs
|
| 159 |
+
result["ega_layers"] = len(pipeline._expert_directions)
|
| 160 |
+
|
| 161 |
+
if pipeline._expert_safety_scores:
|
| 162 |
+
result["expert_classified_layers"] = len(pipeline._expert_safety_scores)
|
| 163 |
+
|
| 164 |
+
if pipeline._cot_preserve_directions:
|
| 165 |
+
result["cot_preserved_layers"] = len(pipeline._cot_preserve_directions)
|
| 166 |
+
|
| 167 |
+
if pipeline._float_layer_weights:
|
| 168 |
+
result["float_layer_weights"] = {
|
| 169 |
+
str(k): round(v, 3) for k, v in pipeline._float_layer_weights.items()
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
if pipeline._kl_contributions:
|
| 173 |
+
result["kl_contributions"] = {
|
| 174 |
+
str(k): round(v, 6) for k, v in pipeline._kl_contributions.items()
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
if pipeline._lora_adapters:
|
| 178 |
+
result["lora_adapters"] = len(pipeline._lora_adapters)
|
| 179 |
+
|
| 180 |
+
if pipeline._steering_hooks:
|
| 181 |
+
result["steering_hooks"] = len(pipeline._steering_hooks)
|
| 182 |
+
|
| 183 |
+
# GPU memory
|
| 184 |
+
if torch.cuda.is_available():
|
| 185 |
+
result["peak_gpu_mb"] = round(torch.cuda.max_memory_allocated() / 1e6, 1)
|
| 186 |
+
|
| 187 |
+
# Capability benchmarks (optional)
|
| 188 |
+
if run_benchmarks:
|
| 189 |
+
print(f"\n Running capability benchmarks...")
|
| 190 |
+
try:
|
| 191 |
+
runner = BenchmarkRunner(
|
| 192 |
+
pipeline.handle.model,
|
| 193 |
+
pipeline.handle.tokenizer,
|
| 194 |
+
)
|
| 195 |
+
bench_results = runner.run_all()
|
| 196 |
+
result["benchmarks"] = {
|
| 197 |
+
name: {
|
| 198 |
+
"score": round(br.score, 3),
|
| 199 |
+
"n_correct": br.n_correct,
|
| 200 |
+
"n_total": br.n_total,
|
| 201 |
+
"per_category": {
|
| 202 |
+
k: round(v, 3) for k, v in br.per_category.items()
|
| 203 |
+
},
|
| 204 |
+
}
|
| 205 |
+
for name, br in bench_results.items()
|
| 206 |
+
}
|
| 207 |
+
report = format_benchmark_report(bench_results)
|
| 208 |
+
print(f"\n{report}")
|
| 209 |
+
except Exception as e:
|
| 210 |
+
print(f" Benchmark probes failed: {e}")
|
| 211 |
+
result["benchmarks"] = {"error": str(e)}
|
| 212 |
+
|
| 213 |
+
print(f"\n === {method} complete in {elapsed:.1f}s ===")
|
| 214 |
+
print(f" Quality: {json.dumps(pipeline._quality_metrics, default=str)}")
|
| 215 |
+
|
| 216 |
+
except Exception as e:
|
| 217 |
+
elapsed = time.time() - t0
|
| 218 |
+
result.update({
|
| 219 |
+
"time_seconds": round(elapsed, 1),
|
| 220 |
+
"error": str(e),
|
| 221 |
+
})
|
| 222 |
+
print(f"\n === {method} FAILED after {elapsed:.1f}s: {e} ===")
|
| 223 |
+
import traceback
|
| 224 |
+
traceback.print_exc()
|
| 225 |
+
|
| 226 |
+
# Cleanup saved model to free disk
|
| 227 |
+
shutil.rmtree(outdir, ignore_errors=True)
|
| 228 |
+
|
| 229 |
+
if pipeline is not None:
|
| 230 |
+
del pipeline
|
| 231 |
+
cleanup()
|
| 232 |
+
|
| 233 |
+
return result
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def print_summary_table(results: list[dict]):
|
| 237 |
+
"""Print a formatted comparison table."""
|
| 238 |
+
print(f"\n{'='*90}")
|
| 239 |
+
print(f"BENCHMARK RESULTS SUMMARY")
|
| 240 |
+
print(f"{'='*90}")
|
| 241 |
+
|
| 242 |
+
# Header
|
| 243 |
+
header = (
|
| 244 |
+
f"{'Method':<12} {'Time':>7} {'PPL':>8} {'Coher':>7} "
|
| 245 |
+
f"{'Refusal':>8} {'Know':>6} {'Truth':>6} {'Math':>6} "
|
| 246 |
+
f"{'EGA':>5} {'CoT':>4} {'GPU MB':>7}"
|
| 247 |
+
)
|
| 248 |
+
print(header)
|
| 249 |
+
print("-" * len(header))
|
| 250 |
+
|
| 251 |
+
for r in results:
|
| 252 |
+
method = r["method"]
|
| 253 |
+
time_s = f"{r['time_seconds']:.0f}s" if "time_seconds" in r else "N/A"
|
| 254 |
+
|
| 255 |
+
if "error" in r:
|
| 256 |
+
print(f"{method:<12} {time_s:>7} {'FAILED':>8}")
|
| 257 |
+
continue
|
| 258 |
+
|
| 259 |
+
q = r.get("quality", {})
|
| 260 |
+
ppl = q.get("perplexity")
|
| 261 |
+
coh = q.get("coherence")
|
| 262 |
+
ref = q.get("refusal_rate")
|
| 263 |
+
gpu = r.get("peak_gpu_mb")
|
| 264 |
+
|
| 265 |
+
# Benchmark scores
|
| 266 |
+
bench = r.get("benchmarks", {})
|
| 267 |
+
know = bench.get("knowledge", {}).get("score")
|
| 268 |
+
truth = bench.get("truthfulness", {}).get("score")
|
| 269 |
+
math = bench.get("math_reasoning", {}).get("score")
|
| 270 |
+
|
| 271 |
+
# MoE metrics
|
| 272 |
+
ega = r.get("ega_expert_dirs", "")
|
| 273 |
+
cot = r.get("cot_preserved_layers", "")
|
| 274 |
+
|
| 275 |
+
ppl_s = f"{ppl:.1f}" if ppl is not None else "N/A"
|
| 276 |
+
coh_s = f"{coh:.0%}" if coh is not None else "N/A"
|
| 277 |
+
ref_s = f"{ref:.0%}" if ref is not None else "N/A"
|
| 278 |
+
know_s = f"{know:.0%}" if know is not None else "N/A"
|
| 279 |
+
truth_s = f"{truth:.0%}" if truth is not None else "N/A"
|
| 280 |
+
math_s = f"{math:.0%}" if math is not None else "N/A"
|
| 281 |
+
gpu_s = f"{gpu:.0f}" if gpu is not None else "N/A"
|
| 282 |
+
ega_s = str(ega) if ega else "-"
|
| 283 |
+
cot_s = str(cot) if cot else "-"
|
| 284 |
+
|
| 285 |
+
print(
|
| 286 |
+
f"{method:<12} {time_s:>7} {ppl_s:>8} {coh_s:>7} "
|
| 287 |
+
f"{ref_s:>8} {know_s:>6} {truth_s:>6} {math_s:>6} "
|
| 288 |
+
f"{ega_s:>5} {cot_s:>4} {gpu_s:>7}"
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
print(f"{'='*90}")
|
| 292 |
+
|
| 293 |
+
# Legend
|
| 294 |
+
print("\nLegend:")
|
| 295 |
+
print(" PPL = Perplexity (lower = better capability preservation)")
|
| 296 |
+
print(" Coher = Coherence score (higher = more coherent text)")
|
| 297 |
+
print(" Refusal = Refusal rate on harmful prompts (lower = more abliterated)")
|
| 298 |
+
print(" Know = MMLU-style knowledge probe")
|
| 299 |
+
print(" Truth = TruthfulQA-style truthfulness probe")
|
| 300 |
+
print(" Math = GSM8K-style math reasoning probe")
|
| 301 |
+
print(" EGA = Expert-Granular Abliteration directions computed")
|
| 302 |
+
print(" CoT = Layers where CoT reasoning was preserved")
|
| 303 |
+
print(" GPU MB = Peak GPU memory usage")
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def main():
|
| 307 |
+
args = parse_args()
|
| 308 |
+
|
| 309 |
+
if args.quick:
|
| 310 |
+
args.prompts = 20
|
| 311 |
+
args.methods = [m for m in args.methods if m not in ("aggressive", "inverted")]
|
| 312 |
+
args.bayesian_trials = 15
|
| 313 |
+
|
| 314 |
+
gpu = gpu_info()
|
| 315 |
+
harmful = HARMFUL_PROMPTS[:args.prompts]
|
| 316 |
+
harmless = HARMLESS_PROMPTS[:args.prompts]
|
| 317 |
+
|
| 318 |
+
print("=" * 60)
|
| 319 |
+
print(" OBLITERATUS GPT-OSS 20B BENCHMARK")
|
| 320 |
+
print("=" * 60)
|
| 321 |
+
print(f" Model: {args.model}")
|
| 322 |
+
print(f" Methods: {args.methods}")
|
| 323 |
+
print(f" Prompts: {args.prompts} per side")
|
| 324 |
+
print(f" GPU: {gpu['gpu']} ({gpu['total_gb']} GB total, {gpu['free_gb']} GB free)")
|
| 325 |
+
print(f" Benchmarks: {'skip' if args.skip_benchmarks else 'enabled'}")
|
| 326 |
+
if "optimized" in args.methods:
|
| 327 |
+
print(f" Bayesian: {args.bayesian_trials} trials")
|
| 328 |
+
print("=" * 60)
|
| 329 |
+
|
| 330 |
+
all_results = []
|
| 331 |
+
|
| 332 |
+
for method in args.methods:
|
| 333 |
+
if method not in METHODS:
|
| 334 |
+
print(f"\nSKIP: unknown method '{method}'")
|
| 335 |
+
continue
|
| 336 |
+
|
| 337 |
+
print(f"\n{'━'*60}")
|
| 338 |
+
print(f" METHOD: {method} — {METHODS[method]['label']}")
|
| 339 |
+
print(f"{'━'*60}")
|
| 340 |
+
|
| 341 |
+
result = run_single_method(
|
| 342 |
+
model_name=args.model,
|
| 343 |
+
method=method,
|
| 344 |
+
harmful=harmful,
|
| 345 |
+
harmless=harmless,
|
| 346 |
+
output_dir=args.output_dir,
|
| 347 |
+
run_benchmarks=not args.skip_benchmarks,
|
| 348 |
+
bayesian_trials=args.bayesian_trials,
|
| 349 |
+
)
|
| 350 |
+
all_results.append(result)
|
| 351 |
+
|
| 352 |
+
# Summary
|
| 353 |
+
print_summary_table(all_results)
|
| 354 |
+
|
| 355 |
+
# Save JSON
|
| 356 |
+
output_path = args.output or f"benchmark_gpt_oss_{int(time.time())}.json"
|
| 357 |
+
with open(output_path, "w") as f:
|
| 358 |
+
json.dump(all_results, f, indent=2, default=str)
|
| 359 |
+
print(f"\nFull results saved to: {output_path}")
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
if __name__ == "__main__":
|
| 363 |
+
main()
|