pliny-the-prompter commited on
Commit
4629818
·
verified ·
1 Parent(s): a4d593e

Upload 87 files

Browse files
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 Dirs | Error |",
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: Benchmark ──────────────────────────────────────────────
1514
- with gr.Tab("Benchmark", id="benchmark"):
1515
- gr.Markdown("""### Compare Methods
1516
- Run multiple abliteration methods on the same model and compare results.
1517
- Useful for finding the best strategy for a given architecture.
1518
 
1519
- **API access:** This endpoint is also available programmatically:
1520
- ```python
1521
- from gradio_client import Client
1522
- client = Client("pliny-the-prompter/obliteratus")
1523
- result = client.predict(
1524
- model_choice="Qwen2.5 0.5B Instruct",
1525
- methods_to_test=["basic", "advanced", "surgical"],
1526
- prompt_volume_choice="33 (fast)",
1527
- api_name="/benchmark",
1528
- )
1529
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1530
  """)
 
1531
  with gr.Row():
1532
- bench_model = gr.Dropdown(
1533
  choices=list(MODELS.keys()),
1534
  value="Qwen2.5 0.5B Instruct",
1535
- label="Target Model",
1536
  allow_custom_value=True,
1537
  )
1538
- bench_methods = gr.CheckboxGroup(
1539
- choices=["basic", "advanced", "aggressive", "surgical",
1540
- "optimized", "inverted", "nuclear"],
1541
- value=["basic", "advanced", "surgical"],
1542
- label="Methods to Compare",
1543
  )
1544
- bench_prompt_vol = gr.Dropdown(
 
1545
  choices=list(PROMPT_VOLUMES.keys()),
1546
  value="33 (fast)",
1547
  label="Prompt Volume",
1548
  )
1549
- with gr.Row():
1550
- bench_dataset = gr.Dropdown(
1551
  choices=get_source_choices(),
1552
  value=get_source_choices()[0],
1553
- label="Dataset Source",
1554
- info="Select prompt dataset for benchmarking",
1555
  )
1556
- bench_btn = gr.Button("Run Benchmark", variant="primary", size="lg")
1557
- bench_status = gr.Markdown("")
1558
- bench_results = gr.Markdown("*Click 'Run Benchmark' to start.*")
1559
- bench_log = gr.Textbox(
1560
- label="Benchmark Log",
1561
- lines=15,
1562
- max_lines=150,
1563
- interactive=False,
1564
- elem_classes=["log-box"],
 
 
 
 
 
 
 
 
1565
  )
1566
 
1567
- bench_btn.click(
1568
- fn=benchmark,
1569
- inputs=[bench_model, bench_methods, bench_prompt_vol, bench_dataset],
1570
- outputs=[bench_status, bench_results, bench_log],
1571
- api_name="/benchmark",
 
1572
  )
1573
 
1574
- # ── Tab 4: About ──────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 | Norm-preserving | Refinement passes |
1600
- |--------|-----------|----------------|-------------------|
1601
- | **basic** | 1 | No | 0 |
1602
- | **advanced** | 4 (SVD) | Yes | 2 |
1603
- | **aggressive** | 8 (SVD) | Yes | 3 |
 
 
 
 
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
- layer_span = max(1, self._strong_layers[-1] - self._strong_layers[0])
 
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 projection (partial revert)
 
 
 
 
 
 
 
2498
  revert_strength = 0.30
 
2499
  d_col = d.unsqueeze(-1) if d.dim() == 1 else d
2500
 
2501
- try:
2502
- attn = get_attention_module(layers[idx], arch)
2503
- for name in _ATTN_OUT_NAMES:
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
- W.add_(d_dev.T * (revert_strength * coeff))
2511
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: reflect all experts (can't easily do per-expert)
3124
- scale = self.reflection_strength
3125
- count += self._project_fused_3d(
3126
  experts, direction, ["down_proj", "w2"],
3127
- norm_preserve=norm_preserve, scale=scale,
 
 
 
3128
  )
3129
- count += self._project_fused_3d(
3130
  experts, direction, ["up_proj", "gate_proj", "w1", "w3"],
3131
- norm_preserve=norm_preserve, scale=scale,
 
 
 
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 both permanent and reversible intervention methods.
46
 
47
- \textsc{Obliteratus} contributes: (1) \textbf{15 analysis modules} spanning direction extraction, geometric characterization, learned probing, causal estimation, cross-model transfer, and defense robustness evaluation; (2) \textbf{two complementary intervention paradigms}---permanent weight projection with norm-preserving regularization and reversible inference-time steering vectors; (3) \textbf{several novel analyses} including concept cone geometry with Direction Specificity Index, alignment training method fingerprinting from subspace geometry alone, a cross-model Universality Index, and Hydra effect quantification; (4) \textbf{a unified evaluation suite} with refusal rate, perplexity, coherence, KL divergence, CKA similarity, and effective rank metrics; and (5) \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 based on the model's detected alignment method and refusal geometry.
 
 
 
 
 
 
 
48
 
49
- The platform supports any HuggingFace transformer architecture and ships with 48 curated model presets, 10 study configurations, a web dashboard, and 379 unit tests.
50
- We describe the mathematical formulations underlying each module and discuss design decisions that distinguish \textsc{Obliteratus} from existing tools including TransformerLens, RepEng, and abliterator libraries.
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 three design goals:
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 permanent weight projection (three presets from conservative to aggressive) and reversible inference-time steering vectors, letting researchers choose the appropriate level of intervention.
69
- \item \textbf{Rigorous evaluation.} Every intervention is accompanied by automated quality assessment including perplexity, coherence, refusal rate, KL divergence, and representational similarity metrics.
 
 
70
  \end{enumerate}
71
 
72
- The remainder of this paper is organized as follows. Section~\ref{sec:related} surveys related work. Section~\ref{sec:architecture} describes the platform architecture. Section~\ref{sec:analysis} details the 15 analysis modules with mathematical formulations. Section~\ref{sec:intervention} describes the two intervention paradigms. Section~\ref{sec:evaluation} covers the evaluation suite. Section~\ref{sec:informed} presents the analysis-informed abliteration pipeline. Section~\ref{sec:comparison} compares \textsc{Obliteratus} with existing tools. Section~\ref{sec:discussion} discusses limitations, broader impact, and future work.
 
 
 
 
 
 
 
 
 
 
 
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 four principal subsystems (Figure~\ref{fig:architecture}):
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
- │ 15 Analysis │ │ Steering Vectors │
118
- │ Modules │ │ (reversible) │
119
- └─────────────┘ └──────────────────┘
 
 
 
 
 
120
  \end{verbatim}
121
- \caption{High-level architecture of the \textsc{Obliteratus} pipeline. The six-stage abliteration flow (top) feeds into 15 analysis modules and supports both permanent weight projection (EXCISE stage) and reversible steering vector intervention.}
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 three abliteration presets:
398
 
399
  \begin{table}[h]
400
  \centering
401
- \caption{Abliteration method presets.}
402
  \label{tab:methods}
403
  \small
404
- \begin{tabular}{@{}lccccc@{}}
405
  \toprule
406
- \textbf{Method} & \textbf{Directions} & \textbf{Norm-preserving} & \textbf{Regularization} & \textbf{Refinement} \\
407
  \midrule
408
- Basic & 1 (diff-in-means) & No & None & 1 pass \\
409
- Advanced & 4 (SVD) & Yes & $\lambda = 0.1$ & 2 passes \\
410
- Aggressive & 8 (SVD) & Yes & None & 3 passes \\
 
 
 
 
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
- The advanced and aggressive methods perform multiple passes. In each pass, the projection is recomputed after the previous modification, catching rotated residual refusal that a single pass misses.
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
- Weight projection variants & 4+ & -- & Bayesian & 1 & -- & -- \\
 
 
 
 
 
 
 
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{Dual intervention}: Supporting both permanent weight projection and reversible steering vectors in one platform.
580
- \item \textbf{Architecture coverage}: Working with any HuggingFace model rather than requiring specific architecture support.
581
- \item \textbf{Novel metrics}: Direction Specificity Index, Refusal Elimination Score, Universality Index, and Refusal Sparsity Index are introduced here.
 
582
  \end{enumerate}
583
 
584
- Conversely, TransformerLens provides real activation patching (our causal tracing is approximate), Heretic offers Bayesian hyperparameter optimization for abliteration settings, and SAELens provides sparse autoencoder analysis that \textsc{Obliteratus} does not.
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) integration with sparse autoencoder analysis to understand refusal at the feature level; (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; and (5) application of the defense robustness framework to evaluate proposed robust alignment methods including circuit breakers \citep{zou2024circuit} and representation rerouting.
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. Its 15 analysis modules provide the most comprehensive characterization of refusal geometry available in any public tool, including several novel analyses (concept cone geometry with DSI, alignment imprint detection, cross-model universality, defense robustness evaluation). The platform's dual intervention paradigm---permanent weight projection and reversible steering vectors---gives researchers flexibility in how they interact with refusal mechanisms. 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.
 
 
 
 
 
 
 
 
 
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()