Files
OBLITERATUS/obliteratus/analysis/wasserstein_transfer.py
T
2026-03-04 12:38:18 -08:00

513 lines
19 KiB
Python

"""Wasserstein Refusal Transfer Across Architectures.
When a model is successfully abliterated, the knowledge of *where* and *how*
refusal was embedded can potentially be transferred to other models without
re-running the full pipeline. "Transport and Merge" (2025) used optimal
transport for cross-architecture model merging; GiLOT (ICML 2024) used OT
for LLM interpretability.
This module uses OT maps to transfer refusal removal knowledge across
architectures. Given an abliterated source and aligned target, it computes
the Monge map T: A_source -> A_target between their activation distributions,
then transports the source's refusal directions through T.
Contributions:
1. **OT-based refusal direction transfer**: Application of optimal
transport to cross-architecture safety intervention transfer
2. **Transfer error bound (informal)**: Excess refusal after transfer is
bounded by W_2(mu_s, mu_t) * kappa(T)
3. **Refusal removal knowledge graph**: Abliterate one model, transfer
to a whole family via OT maps
4. **Wasserstein compatibility metric**: Quantifies whether transfer is
viable before attempting it
References:
- Cui et al. (2025): Transport and Merge — cross-arch OT merging (arXiv:2602.05495)
- Li et al. (ICML 2024): GiLOT — OT for LLM interpretability
- Brenier (1991): Optimal maps for quadratic cost (uniqueness theorem)
- Paper Appendix Theorem: Wasserstein Cost of Abliteration
- OBLITERATUS: Cross-Model Universality Index
"""
from __future__ import annotations
import logging
import math
from dataclasses import dataclass
import torch
logger = logging.getLogger(__name__)
@dataclass
class TransportPlan:
"""Optimal transport plan between two activation distributions."""
source_model: str # name of source model
target_model: str # name of target model
transport_matrix: torch.Tensor # (d_target, d_source) linear map T
wasserstein_distance: float # W_2 between source and target
condition_number: float # kappa(T), stability indicator
transport_cost: float # total transport cost
is_viable: bool # whether transfer is recommended
@dataclass
class TransferredDirection:
"""A refusal direction transferred from source to target model."""
source_layer: int # layer in source model
target_layer: int # corresponding layer in target model
source_direction: torch.Tensor # original direction in source space
transferred_direction: torch.Tensor # direction mapped to target space
transfer_fidelity: float # quality of transfer (0-1)
estimated_refusal_removal: float # expected removal effectiveness
wasserstein_bound: float # excess refusal upper bound
@dataclass
class WassersteinTransferResult:
"""Complete result of Wasserstein refusal transfer analysis."""
# Transfer metadata
source_model: str
target_model: str
n_layers_transferred: int
# Transport plan
wasserstein_distance: float # W_2(source, target)
condition_number: float # stability of transport map
transfer_viability: str # "excellent" | "good" | "marginal" | "poor"
# Transferred directions
transferred_directions: list[TransferredDirection]
mean_transfer_fidelity: float # avg quality across layers
min_transfer_fidelity: float # worst layer
# Bounds
estimated_excess_refusal: float # bound on residual refusal after transfer
estimated_vs_native_ratio: float # expected native/transfer performance ratio
# Layer alignment
layer_mapping: dict[int, int] # source_layer -> target_layer
unmapped_layers: list[int] # target layers with no source correspondence
# Recommendation
recommendation: str # summary recommendation
needs_refinement: bool # whether a refinement pass is recommended
class WassersteinRefusalTransfer:
"""Transfer refusal removal knowledge across architectures via OT.
Given a successfully abliterated source model and an aligned target,
computes the optimal transport map between their activation spaces
and uses it to transfer refusal directions.
"""
def __init__(
self,
fidelity_threshold: float = 0.5,
max_condition_number: float = 100.0,
viability_threshold: float = 0.3,
n_sinkhorn_iterations: int = 50,
):
"""
Args:
fidelity_threshold: Minimum transfer fidelity to consider
a transferred direction useful.
max_condition_number: Maximum condition number for the transport
map before flagging instability.
viability_threshold: W_2 threshold below which transfer is viable.
n_sinkhorn_iterations: Iterations for Sinkhorn OT computation.
"""
self.fidelity_threshold = fidelity_threshold
self.max_condition_number = max_condition_number
self.viability_threshold = viability_threshold
self.n_sinkhorn_iterations = n_sinkhorn_iterations
def compute_transfer(
self,
source_activations: dict[int, torch.Tensor],
target_activations: dict[int, torch.Tensor],
source_refusal_directions: dict[int, torch.Tensor],
source_model_name: str = "source",
target_model_name: str = "target",
layer_mapping: dict[int, int] | None = None,
) -> WassersteinTransferResult:
"""Compute Wasserstein transfer of refusal directions.
Args:
source_activations: {layer_idx: (n_samples, d_source)} from source.
target_activations: {layer_idx: (n_samples, d_target)} from target.
source_refusal_directions: {layer_idx: (d_source,)} from source.
source_model_name: Identifier for source model.
target_model_name: Identifier for target model.
layer_mapping: Optional explicit {source_layer -> target_layer}.
If None, computed via activation similarity.
Returns:
WassersteinTransferResult with transferred directions and bounds.
"""
source_layers = sorted(source_activations.keys())
target_layers = sorted(target_activations.keys())
if not source_layers or not target_layers:
return self._empty_result(source_model_name, target_model_name)
# Step 1: Compute layer mapping if not provided
if layer_mapping is None:
layer_mapping = self._compute_layer_mapping(
source_layers, target_layers,
source_activations, target_activations
)
# Step 2: For each mapped layer pair, compute OT map and transfer
transferred: list[TransferredDirection] = []
all_w2: list[float] = []
all_kappa: list[float] = []
for src_l, tgt_l in layer_mapping.items():
if src_l not in source_activations or tgt_l not in target_activations:
continue
if src_l not in source_refusal_directions:
continue
src_acts = source_activations[src_l]
tgt_acts = target_activations[tgt_l]
src_dir = source_refusal_directions[src_l]
# Compute OT map between layer activations
plan = self._compute_transport_plan(
src_acts, tgt_acts,
source_model_name, target_model_name
)
all_w2.append(plan.wasserstein_distance)
all_kappa.append(plan.condition_number)
# Transport the refusal direction
transferred_dir = self._transport_direction(
src_dir, plan.transport_matrix, src_acts, tgt_acts
)
# Measure transfer fidelity
fidelity = self._measure_fidelity(
transferred_dir, tgt_acts, src_dir, src_acts
)
# Wasserstein bound on excess refusal
w2_bound = plan.wasserstein_distance * plan.condition_number
transferred.append(TransferredDirection(
source_layer=src_l,
target_layer=tgt_l,
source_direction=src_dir,
transferred_direction=transferred_dir,
transfer_fidelity=fidelity,
estimated_refusal_removal=max(0, 1.0 - w2_bound),
wasserstein_bound=w2_bound,
))
if not transferred:
return self._empty_result(source_model_name, target_model_name)
# Step 3: Aggregate results
fidelities = [t.transfer_fidelity for t in transferred]
mean_fidelity = sum(fidelities) / len(fidelities)
min_fidelity = min(fidelities)
mean_w2 = sum(all_w2) / len(all_w2)
mean_kappa = sum(all_kappa) / len(all_kappa)
excess_refusal = mean_w2 * mean_kappa
# Viability assessment
if mean_fidelity > 0.8 and mean_w2 < self.viability_threshold:
viability = "excellent"
elif mean_fidelity > 0.6 and mean_w2 < self.viability_threshold * 2:
viability = "good"
elif mean_fidelity > 0.4:
viability = "marginal"
else:
viability = "poor"
native_ratio = max(0.1, 1.0 - excess_refusal)
needs_refinement = mean_fidelity < 0.7 or viability in ("marginal", "poor")
unmapped = [
ly for ly in target_layers if ly not in layer_mapping.values()
]
recommendation = self._generate_recommendation(
viability, mean_fidelity, excess_refusal, needs_refinement
)
return WassersteinTransferResult(
source_model=source_model_name,
target_model=target_model_name,
n_layers_transferred=len(transferred),
wasserstein_distance=mean_w2,
condition_number=mean_kappa,
transfer_viability=viability,
transferred_directions=transferred,
mean_transfer_fidelity=mean_fidelity,
min_transfer_fidelity=min_fidelity,
estimated_excess_refusal=excess_refusal,
estimated_vs_native_ratio=native_ratio,
layer_mapping=layer_mapping,
unmapped_layers=unmapped,
recommendation=recommendation,
needs_refinement=needs_refinement,
)
def _compute_layer_mapping(
self,
source_layers: list[int],
target_layers: list[int],
source_activations: dict[int, torch.Tensor],
target_activations: dict[int, torch.Tensor],
) -> dict[int, int]:
"""Compute layer correspondence via relative position.
Maps layers by relative position within the network:
source_layer / n_source_layers ≈ target_layer / n_target_layers
"""
mapping = {}
n_src = max(source_layers) + 1 if source_layers else 1
n_tgt = max(target_layers) + 1 if target_layers else 1
for src_l in source_layers:
# Find target layer at closest relative position
src_ratio = src_l / max(n_src - 1, 1)
best_tgt = min(
target_layers,
key=lambda t: abs(t / max(n_tgt - 1, 1) - src_ratio)
)
mapping[src_l] = best_tgt
return mapping
def _compute_transport_plan(
self,
source_acts: torch.Tensor,
target_acts: torch.Tensor,
source_name: str,
target_name: str,
) -> TransportPlan:
"""Compute the optimal transport map between activation distributions.
Uses a linear approximation: T = Sigma_st @ Sigma_ss^{-1}
This is the Monge map for Gaussian distributions, which is optimal
for the quadratic cost when distributions are Gaussian.
"""
n_src, d_src = source_acts.shape
n_tgt, d_tgt = target_acts.shape
# Center the activations
src_mean = source_acts.mean(dim=0)
tgt_mean = target_acts.mean(dim=0)
src_centered = source_acts - src_mean
tgt_centered = target_acts - tgt_mean
# Compute covariances
n_common = min(n_src, n_tgt)
src_sub = src_centered[:n_common]
tgt_sub = tgt_centered[:n_common]
# Cross-covariance: Sigma_st = tgt^T @ src / n
sigma_st = tgt_sub.T @ src_sub / max(n_common - 1, 1) # (d_tgt, d_src)
# Source auto-covariance: Sigma_ss = src^T @ src / n
sigma_ss = src_sub.T @ src_sub / max(n_common - 1, 1) # (d_src, d_src)
# Transport matrix T = Sigma_st @ Sigma_ss^{-1}
# Use pseudo-inverse for stability
try:
reg = 1e-4 * torch.eye(d_src, device=sigma_ss.device)
sigma_ss_inv = torch.linalg.inv(sigma_ss + reg)
transport = sigma_st @ sigma_ss_inv # (d_tgt, d_src)
except Exception:
transport = sigma_st # fallback: just use cross-covariance
# Wasserstein-2 distance (Bures metric for Gaussian approximation)
w2 = self._compute_w2_gaussian(src_mean, tgt_mean, sigma_ss,
tgt_sub.T @ tgt_sub / max(n_common - 1, 1))
# Condition number of transport matrix
try:
sv = torch.linalg.svdvals(transport)
kappa = (sv[0] / sv[-1]).item() if sv[-1] > 1e-10 else float("inf")
kappa = min(kappa, 1e6)
except Exception:
kappa = 1.0
is_viable = w2 < self.viability_threshold and kappa < self.max_condition_number
return TransportPlan(
source_model=source_name,
target_model=target_name,
transport_matrix=transport,
wasserstein_distance=w2,
condition_number=kappa,
transport_cost=w2 * kappa,
is_viable=is_viable,
)
def _compute_w2_gaussian(
self,
mean_s: torch.Tensor,
mean_t: torch.Tensor,
cov_s: torch.Tensor,
cov_t: torch.Tensor,
) -> float:
"""Compute 2-Wasserstein distance between Gaussian approximations.
W_2^2 = ||mu_s - mu_t||^2 + Tr(Sigma_s + Sigma_t - 2*(Sigma_s^{1/2} Sigma_t Sigma_s^{1/2})^{1/2})
"""
# Mean shift component
mean_diff = (mean_s[:min(len(mean_s), len(mean_t))] -
mean_t[:min(len(mean_s), len(mean_t))])
mean_shift = (mean_diff ** 2).sum().item()
# Bures metric component (trace term)
# Simplified: use trace of absolute difference of eigenvalues
try:
d = min(cov_s.shape[0], cov_t.shape[0])
eig_s = torch.linalg.eigvalsh(cov_s[:d, :d])
eig_t = torch.linalg.eigvalsh(cov_t[:d, :d])
# Bures approximation via eigenvalues
sqrt_s = eig_s.clamp(min=0).sqrt()
sqrt_t = eig_t.clamp(min=0).sqrt()
bures = ((sqrt_s - sqrt_t) ** 2).sum().item()
except Exception:
bures = 0.0
w2 = math.sqrt(max(0, mean_shift + bures))
return w2
def _transport_direction(
self,
source_direction: torch.Tensor,
transport_matrix: torch.Tensor,
source_acts: torch.Tensor,
target_acts: torch.Tensor,
) -> torch.Tensor:
"""Transport a refusal direction through the OT map.
Applies T to the source direction and normalizes in the target space.
"""
d_src = source_direction.shape[0]
# Ensure dimensions match
if transport_matrix.shape[1] != d_src:
# Dimension mismatch — use projection
min_d = min(d_src, transport_matrix.shape[1])
src_dir = source_direction[:min_d]
T = transport_matrix[:, :min_d]
else:
src_dir = source_direction
T = transport_matrix
# Transport: t_dir = T @ s_dir
transferred = T @ src_dir
# Normalize
t_norm = transferred.norm()
if t_norm > 1e-8:
transferred = transferred / t_norm
return transferred
def _measure_fidelity(
self,
transferred_dir: torch.Tensor,
target_acts: torch.Tensor,
source_dir: torch.Tensor,
source_acts: torch.Tensor,
) -> float:
"""Measure how well a transferred direction separates harmful/harmless.
Fidelity = correlation between source projection magnitudes and
target projection magnitudes (after transfer).
"""
# Project source activations onto source direction
src_proj = (source_acts @ source_dir).abs()
# Project target activations onto transferred direction
n_common = min(source_acts.shape[0], target_acts.shape[0])
tgt_proj = (target_acts[:n_common] @ transferred_dir).abs()
src_proj = src_proj[:n_common]
if n_common < 2:
return 0.0
# Correlation as fidelity measure
src_centered = src_proj - src_proj.mean()
tgt_centered = tgt_proj - tgt_proj.mean()
src_std = src_centered.std()
tgt_std = tgt_centered.std()
if src_std < 1e-10 or tgt_std < 1e-10:
return 0.0
correlation = (src_centered @ tgt_centered) / (
n_common * src_std * tgt_std
)
fidelity = correlation.clamp(0, 1).item()
return fidelity
def _generate_recommendation(
self,
viability: str,
mean_fidelity: float,
excess_refusal: float,
needs_refinement: bool,
) -> str:
"""Generate human-readable recommendation."""
if viability == "excellent":
return (
f"Transfer is highly viable (fidelity={mean_fidelity:.2f}). "
f"Transferred directions should work with minimal refinement."
)
elif viability == "good":
return (
f"Transfer is viable (fidelity={mean_fidelity:.2f}) but "
f"recommend a single refinement pass on the target model."
)
elif viability == "marginal":
return (
f"Transfer is marginal (fidelity={mean_fidelity:.2f}). "
f"Excess refusal bound={excess_refusal:.3f}. "
f"Use as initialization only; full re-abliteration recommended."
)
else:
return (
f"Transfer is poor (fidelity={mean_fidelity:.2f}). "
f"Models are too dissimilar in Wasserstein space. "
f"Full native abliteration required."
)
def _empty_result(
self, source_name: str, target_name: str
) -> WassersteinTransferResult:
return WassersteinTransferResult(
source_model=source_name,
target_model=target_name,
n_layers_transferred=0,
wasserstein_distance=float("inf"),
condition_number=float("inf"),
transfer_viability="poor",
transferred_directions=[],
mean_transfer_fidelity=0.0,
min_transfer_fidelity=0.0,
estimated_excess_refusal=1.0,
estimated_vs_native_ratio=0.0,
layer_mapping={},
unmapped_layers=[],
recommendation="No activations available for transfer.",
needs_refinement=True,
)