Spaces:
Running on Zero
Running on Zero
Upload 81 files
Browse files- obliteratus/abliterate.py +42 -24
- tests/test_abliterate.py +16 -10
obliteratus/abliterate.py
CHANGED
|
@@ -166,21 +166,20 @@ METHODS = {
|
|
| 166 |
"nuclear": {
|
| 167 |
"label": "Nuclear (Maximum Force Combo)",
|
| 168 |
"description": (
|
| 169 |
-
"
|
| 170 |
-
"
|
| 171 |
-
"
|
| 172 |
-
"
|
| 173 |
-
"
|
| 174 |
-
"
|
| 175 |
-
"
|
| 176 |
-
"
|
| 177 |
-
"
|
| 178 |
-
"where standard methods plateau."
|
| 179 |
),
|
| 180 |
-
"n_directions":
|
| 181 |
"norm_preserve": True,
|
| 182 |
"regularization": 0.0,
|
| 183 |
-
"refinement_passes":
|
| 184 |
"project_biases": True,
|
| 185 |
"use_chat_template": True,
|
| 186 |
"use_whitened_svd": True,
|
|
@@ -192,10 +191,12 @@ METHODS = {
|
|
| 192 |
"attention_head_surgery": True,
|
| 193 |
"use_sae_features": True,
|
| 194 |
"invert_refusal": True,
|
| 195 |
-
"reflection_strength": 2.
|
| 196 |
"project_embeddings": True,
|
| 197 |
"activation_steering": True,
|
|
|
|
| 198 |
"expert_transplant": True,
|
|
|
|
| 199 |
},
|
| 200 |
}
|
| 201 |
|
|
@@ -591,7 +592,9 @@ class AbliterationPipeline:
|
|
| 591 |
reflection_strength: float | None = None,
|
| 592 |
project_embeddings: bool | None = None,
|
| 593 |
activation_steering: bool | None = None,
|
|
|
|
| 594 |
expert_transplant: bool | None = None,
|
|
|
|
| 595 |
on_stage: Callable[[StageResult], None] | None = None,
|
| 596 |
on_log: Callable[[str], None] | None = None,
|
| 597 |
):
|
|
@@ -633,7 +636,9 @@ class AbliterationPipeline:
|
|
| 633 |
self.reflection_strength = reflection_strength if reflection_strength is not None else method_cfg.get("reflection_strength", 2.0)
|
| 634 |
self.project_embeddings = project_embeddings if project_embeddings is not None else method_cfg.get("project_embeddings", False)
|
| 635 |
self.activation_steering = activation_steering if activation_steering is not None else method_cfg.get("activation_steering", False)
|
|
|
|
| 636 |
self.expert_transplant = expert_transplant if expert_transplant is not None else method_cfg.get("expert_transplant", False)
|
|
|
|
| 637 |
|
| 638 |
self.handle: ModelHandle | None = None
|
| 639 |
self.refusal_directions: dict[int, torch.Tensor] = {} # per-layer primary direction
|
|
@@ -2463,16 +2468,21 @@ class AbliterationPipeline:
|
|
| 2463 |
# ββ Nuclear-mode helpers βββββββββββββββββββββββββββββββββββββββββββββ
|
| 2464 |
|
| 2465 |
def _transplant_expert_weights(self, layers: nn.ModuleList) -> int:
|
| 2466 |
-
"""
|
| 2467 |
|
| 2468 |
For each MoE layer, computes the mean of capability experts' down_proj
|
| 2469 |
-
weights and
|
| 2470 |
-
|
| 2471 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2472 |
|
| 2473 |
-
Returns the number of weight matrices
|
| 2474 |
"""
|
| 2475 |
arch = self.handle.architecture
|
|
|
|
| 2476 |
count = 0
|
| 2477 |
|
| 2478 |
for idx in self._strong_layers:
|
|
@@ -2514,19 +2524,23 @@ class AbliterationPipeline:
|
|
| 2514 |
# Compute mean of capability expert weights
|
| 2515 |
cap_mean = torch.stack(cap_weights).mean(dim=0)
|
| 2516 |
|
| 2517 |
-
#
|
| 2518 |
for ei in safety_indices:
|
| 2519 |
if ei >= len(experts):
|
| 2520 |
continue
|
| 2521 |
target = getattr(experts[ei], wname, None)
|
| 2522 |
if target is not None and hasattr(target, "weight"):
|
| 2523 |
if target.weight.data.shape == cap_mean.shape:
|
| 2524 |
-
|
|
|
|
| 2525 |
count += 1
|
| 2526 |
|
| 2527 |
del cap_weights, cap_mean
|
| 2528 |
|
| 2529 |
-
self.log(
|
|
|
|
|
|
|
|
|
|
| 2530 |
|
| 2531 |
return count
|
| 2532 |
|
|
@@ -2537,8 +2551,10 @@ class AbliterationPipeline:
|
|
| 2537 |
continuously steering the model away from the refusal direction.
|
| 2538 |
This catches residual signal that static weight surgery may have missed.
|
| 2539 |
|
| 2540 |
-
|
| 2541 |
-
|
|
|
|
|
|
|
| 2542 |
|
| 2543 |
Returns the number of hooks installed.
|
| 2544 |
"""
|
|
@@ -2547,7 +2563,9 @@ class AbliterationPipeline:
|
|
| 2547 |
hook.remove()
|
| 2548 |
self._steering_hooks.clear()
|
| 2549 |
|
| 2550 |
-
|
|
|
|
|
|
|
| 2551 |
|
| 2552 |
for idx in self._strong_layers:
|
| 2553 |
if idx not in self.refusal_directions:
|
|
|
|
| 166 |
"nuclear": {
|
| 167 |
"label": "Nuclear (Maximum Force Combo)",
|
| 168 |
"description": (
|
| 169 |
+
"Comprehensive combo mode for stubborn MoE models. Combines ALL "
|
| 170 |
+
"techniques with calibrated intensity: 10 SVD directions, 2 "
|
| 171 |
+
"refinement passes, standard 2x reflection, input embedding "
|
| 172 |
+
"projection, partial expert weight transplant (blends 30%% "
|
| 173 |
+
"capability weights into safety experts), and light inference-"
|
| 174 |
+
"time activation steering (0.3x residual cleanup). Designed "
|
| 175 |
+
"to break through models like GPT-OSS 20B without causing "
|
| 176 |
+
"decoherence β every technique fires at moderate power so they "
|
| 177 |
+
"complement rather than compound."
|
|
|
|
| 178 |
),
|
| 179 |
+
"n_directions": 10,
|
| 180 |
"norm_preserve": True,
|
| 181 |
"regularization": 0.0,
|
| 182 |
+
"refinement_passes": 2,
|
| 183 |
"project_biases": True,
|
| 184 |
"use_chat_template": True,
|
| 185 |
"use_whitened_svd": True,
|
|
|
|
| 191 |
"attention_head_surgery": True,
|
| 192 |
"use_sae_features": True,
|
| 193 |
"invert_refusal": True,
|
| 194 |
+
"reflection_strength": 2.0,
|
| 195 |
"project_embeddings": True,
|
| 196 |
"activation_steering": True,
|
| 197 |
+
"steering_strength": 0.3,
|
| 198 |
"expert_transplant": True,
|
| 199 |
+
"transplant_blend": 0.3,
|
| 200 |
},
|
| 201 |
}
|
| 202 |
|
|
|
|
| 592 |
reflection_strength: float | None = None,
|
| 593 |
project_embeddings: bool | None = None,
|
| 594 |
activation_steering: bool | None = None,
|
| 595 |
+
steering_strength: float | None = None,
|
| 596 |
expert_transplant: bool | None = None,
|
| 597 |
+
transplant_blend: float | None = None,
|
| 598 |
on_stage: Callable[[StageResult], None] | None = None,
|
| 599 |
on_log: Callable[[str], None] | None = None,
|
| 600 |
):
|
|
|
|
| 636 |
self.reflection_strength = reflection_strength if reflection_strength is not None else method_cfg.get("reflection_strength", 2.0)
|
| 637 |
self.project_embeddings = project_embeddings if project_embeddings is not None else method_cfg.get("project_embeddings", False)
|
| 638 |
self.activation_steering = activation_steering if activation_steering is not None else method_cfg.get("activation_steering", False)
|
| 639 |
+
self.steering_strength = steering_strength if steering_strength is not None else method_cfg.get("steering_strength", 0.3)
|
| 640 |
self.expert_transplant = expert_transplant if expert_transplant is not None else method_cfg.get("expert_transplant", False)
|
| 641 |
+
self.transplant_blend = transplant_blend if transplant_blend is not None else method_cfg.get("transplant_blend", 0.3)
|
| 642 |
|
| 643 |
self.handle: ModelHandle | None = None
|
| 644 |
self.refusal_directions: dict[int, torch.Tensor] = {} # per-layer primary direction
|
|
|
|
| 2468 |
# ββ Nuclear-mode helpers βββββββββββββββββββββββββββββββββββββββββββββ
|
| 2469 |
|
| 2470 |
def _transplant_expert_weights(self, layers: nn.ModuleList) -> int:
|
| 2471 |
+
"""Blend capability expert weights into safety expert down_proj.
|
| 2472 |
|
| 2473 |
For each MoE layer, computes the mean of capability experts' down_proj
|
| 2474 |
+
weights and blends it into each safety expert's down_proj using the
|
| 2475 |
+
transplant_blend ratio. A blend of 0.3 means:
|
| 2476 |
+
new_weight = 0.7 * original_safety + 0.3 * capability_mean
|
| 2477 |
+
|
| 2478 |
+
This preserves most of the safety expert's general language modeling
|
| 2479 |
+
ability while nudging its output toward the capability distribution.
|
| 2480 |
+
Full overwrite (blend=1.0) causes decoherence.
|
| 2481 |
|
| 2482 |
+
Returns the number of weight matrices blended.
|
| 2483 |
"""
|
| 2484 |
arch = self.handle.architecture
|
| 2485 |
+
blend = self.transplant_blend
|
| 2486 |
count = 0
|
| 2487 |
|
| 2488 |
for idx in self._strong_layers:
|
|
|
|
| 2524 |
# Compute mean of capability expert weights
|
| 2525 |
cap_mean = torch.stack(cap_weights).mean(dim=0)
|
| 2526 |
|
| 2527 |
+
# Partial blend into safety experts
|
| 2528 |
for ei in safety_indices:
|
| 2529 |
if ei >= len(experts):
|
| 2530 |
continue
|
| 2531 |
target = getattr(experts[ei], wname, None)
|
| 2532 |
if target is not None and hasattr(target, "weight"):
|
| 2533 |
if target.weight.data.shape == cap_mean.shape:
|
| 2534 |
+
# Blend: (1-blend) * original + blend * capability_mean
|
| 2535 |
+
target.weight.data.mul_(1.0 - blend).add_(cap_mean * blend)
|
| 2536 |
count += 1
|
| 2537 |
|
| 2538 |
del cap_weights, cap_mean
|
| 2539 |
|
| 2540 |
+
self.log(
|
| 2541 |
+
f" layer {idx}: blended {blend:.0%} capability weights "
|
| 2542 |
+
f"into {len(safety_indices)} safety experts"
|
| 2543 |
+
)
|
| 2544 |
|
| 2545 |
return count
|
| 2546 |
|
|
|
|
| 2551 |
continuously steering the model away from the refusal direction.
|
| 2552 |
This catches residual signal that static weight surgery may have missed.
|
| 2553 |
|
| 2554 |
+
Uses the dedicated steering_strength parameter (default 0.3) instead
|
| 2555 |
+
of coupling to reflection_strength. A light touch (0.3) works as
|
| 2556 |
+
residual cleanup without causing decoherence β the weight surgery
|
| 2557 |
+
already handles the bulk of the removal.
|
| 2558 |
|
| 2559 |
Returns the number of hooks installed.
|
| 2560 |
"""
|
|
|
|
| 2563 |
hook.remove()
|
| 2564 |
self._steering_hooks.clear()
|
| 2565 |
|
| 2566 |
+
# Use only the primary refusal direction (not full subspace) to
|
| 2567 |
+
# minimize interference with the model's representation space
|
| 2568 |
+
steering_scale = self.steering_strength
|
| 2569 |
|
| 2570 |
for idx in self._strong_layers:
|
| 2571 |
if idx not in self.refusal_directions:
|
tests/test_abliterate.py
CHANGED
|
@@ -1409,12 +1409,14 @@ class TestNuclearMethod:
|
|
| 1409 |
"""Nuclear method should have all techniques enabled at maximum."""
|
| 1410 |
cfg = METHODS["nuclear"]
|
| 1411 |
assert cfg["invert_refusal"] is True
|
| 1412 |
-
assert cfg["n_directions"] ==
|
| 1413 |
-
assert cfg["refinement_passes"] ==
|
| 1414 |
-
assert cfg["reflection_strength"] == 2.
|
| 1415 |
assert cfg["project_embeddings"] is True
|
| 1416 |
assert cfg["activation_steering"] is True
|
|
|
|
| 1417 |
assert cfg["expert_transplant"] is True
|
|
|
|
| 1418 |
assert cfg["use_jailbreak_contrast"] is True
|
| 1419 |
assert cfg["attention_head_surgery"] is True
|
| 1420 |
|
|
@@ -1422,12 +1424,14 @@ class TestNuclearMethod:
|
|
| 1422 |
"""Pipeline initialized with nuclear method should have all flags set."""
|
| 1423 |
pipeline = AbliterationPipeline(model_name="test", method="nuclear")
|
| 1424 |
assert pipeline.invert_refusal is True
|
| 1425 |
-
assert pipeline.reflection_strength == 2.
|
|
|
|
|
|
|
| 1426 |
assert pipeline.project_embeddings is True
|
| 1427 |
assert pipeline.activation_steering is True
|
| 1428 |
assert pipeline.expert_transplant is True
|
| 1429 |
-
assert pipeline.n_directions ==
|
| 1430 |
-
assert pipeline.refinement_passes ==
|
| 1431 |
|
| 1432 |
def test_reflection_strength_configurable(self):
|
| 1433 |
"""reflection_strength should be explicitly overridable."""
|
|
@@ -1581,12 +1585,14 @@ class TestNuclearMethod:
|
|
| 1581 |
finally:
|
| 1582 |
abl_module.get_ffn_module = orig_get_ffn
|
| 1583 |
|
| 1584 |
-
assert count >= 2, f"Should
|
| 1585 |
|
| 1586 |
-
# Safety expert 0 should
|
|
|
|
|
|
|
| 1587 |
transplanted = layer.mlp.experts[0].down_proj.weight.data
|
| 1588 |
-
assert torch.allclose(transplanted,
|
| 1589 |
-
"Safety expert weight should
|
| 1590 |
)
|
| 1591 |
|
| 1592 |
# Capability expert 2 should be unchanged
|
|
|
|
| 1409 |
"""Nuclear method should have all techniques enabled at maximum."""
|
| 1410 |
cfg = METHODS["nuclear"]
|
| 1411 |
assert cfg["invert_refusal"] is True
|
| 1412 |
+
assert cfg["n_directions"] == 10
|
| 1413 |
+
assert cfg["refinement_passes"] == 2
|
| 1414 |
+
assert cfg["reflection_strength"] == 2.0
|
| 1415 |
assert cfg["project_embeddings"] is True
|
| 1416 |
assert cfg["activation_steering"] is True
|
| 1417 |
+
assert cfg["steering_strength"] == 0.3
|
| 1418 |
assert cfg["expert_transplant"] is True
|
| 1419 |
+
assert cfg["transplant_blend"] == 0.3
|
| 1420 |
assert cfg["use_jailbreak_contrast"] is True
|
| 1421 |
assert cfg["attention_head_surgery"] is True
|
| 1422 |
|
|
|
|
| 1424 |
"""Pipeline initialized with nuclear method should have all flags set."""
|
| 1425 |
pipeline = AbliterationPipeline(model_name="test", method="nuclear")
|
| 1426 |
assert pipeline.invert_refusal is True
|
| 1427 |
+
assert pipeline.reflection_strength == 2.0
|
| 1428 |
+
assert pipeline.steering_strength == 0.3
|
| 1429 |
+
assert pipeline.transplant_blend == 0.3
|
| 1430 |
assert pipeline.project_embeddings is True
|
| 1431 |
assert pipeline.activation_steering is True
|
| 1432 |
assert pipeline.expert_transplant is True
|
| 1433 |
+
assert pipeline.n_directions == 10
|
| 1434 |
+
assert pipeline.refinement_passes == 2
|
| 1435 |
|
| 1436 |
def test_reflection_strength_configurable(self):
|
| 1437 |
"""reflection_strength should be explicitly overridable."""
|
|
|
|
| 1585 |
finally:
|
| 1586 |
abl_module.get_ffn_module = orig_get_ffn
|
| 1587 |
|
| 1588 |
+
assert count >= 2, f"Should blend at least 2 weights (safety experts), got {count}"
|
| 1589 |
|
| 1590 |
+
# Safety expert 0 should be a 30% blend toward capability mean
|
| 1591 |
+
# new = 0.7 * original + 0.3 * capability_mean
|
| 1592 |
+
expected_blend = 0.7 * orig_safety0 + 0.3 * expected_mean
|
| 1593 |
transplanted = layer.mlp.experts[0].down_proj.weight.data
|
| 1594 |
+
assert torch.allclose(transplanted, expected_blend, atol=1e-4), (
|
| 1595 |
+
"Safety expert weight should be 30% blended toward capability mean"
|
| 1596 |
)
|
| 1597 |
|
| 1598 |
# Capability expert 2 should be unchanged
|