| """Mathematical verification that abliteration actually removes refusal directions. |
| |
| These tests verify the core linear algebra claims WITHOUT mocks: |
| 1. Projection removes the target direction from weight matrices |
| 2. Norm-preserving projection maintains weight magnitude |
| 3. Multi-direction SVD extracts the correct subspace |
| 4. Whitened SVD produces orthogonal directions |
| 5. Random directions do NOT have the same effect (negative control) |
| |
| Unlike the other test files, these use real tensors and verify mathematical |
| properties directly — no MagicMock, no mocked tokenizers. |
| """ |
|
|
| from __future__ import annotations |
|
|
|
|
| import torch |
|
|
|
|
| class TestProjectionRemovesDirection: |
| """Verify that orthogonal projection removes the target direction.""" |
|
|
| def test_single_direction_projection(self): |
| """After projecting out direction d from weight W, |
| W_proj @ d should be approximately zero.""" |
| torch.manual_seed(42) |
| hidden = 256 |
| out_dim = 128 |
|
|
| W = torch.randn(out_dim, hidden) |
| d = torch.randn(hidden) |
| d = d / d.norm() |
|
|
| |
| proj = W @ d |
| W_proj = W - proj.unsqueeze(1) * d.unsqueeze(0) |
|
|
| |
| residual = W_proj @ d |
| assert residual.abs().max().item() < 1e-5, f"Residual too large: {residual.abs().max()}" |
|
|
| def test_projection_preserves_orthogonal_components(self): |
| """Projection should NOT change components orthogonal to d.""" |
| torch.manual_seed(42) |
| hidden = 256 |
| out_dim = 128 |
|
|
| W = torch.randn(out_dim, hidden) |
| d = torch.randn(hidden) |
| d = d / d.norm() |
|
|
| |
| v = torch.randn(hidden) |
| v = v - (v @ d) * d |
| v = v / v.norm() |
|
|
| |
| proj = W @ d |
| W_proj = W - proj.unsqueeze(1) * d.unsqueeze(0) |
|
|
| |
| original = W @ v |
| projected = W_proj @ v |
| diff = (original - projected).abs().max().item() |
| assert diff < 1e-5, f"Orthogonal component changed by {diff}" |
|
|
| def test_multi_direction_subspace_removal(self): |
| """Projecting out a k-dimensional subspace should remove all k directions.""" |
| torch.manual_seed(42) |
| hidden = 256 |
| out_dim = 128 |
| k = 4 |
|
|
| W = torch.randn(out_dim, hidden) |
| |
| Q, _ = torch.linalg.qr(torch.randn(hidden, k)) |
| subspace = Q.T |
|
|
| |
| W_proj = W - (W @ Q) @ Q.T |
|
|
| |
| residual = W_proj @ subspace.T |
| assert residual.abs().max().item() < 1e-5, f"Subspace residual: {residual.abs().max()}" |
|
|
| def test_double_projection_is_idempotent(self): |
| """Projecting twice should give the same result as projecting once.""" |
| torch.manual_seed(42) |
| hidden = 256 |
| out_dim = 128 |
|
|
| W = torch.randn(out_dim, hidden) |
| d = torch.randn(hidden) |
| d = d / d.norm() |
|
|
| |
| proj1 = W @ d |
| W1 = W - proj1.unsqueeze(1) * d.unsqueeze(0) |
|
|
| |
| proj2 = W1 @ d |
| W2 = W1 - proj2.unsqueeze(1) * d.unsqueeze(0) |
|
|
| diff = (W1 - W2).abs().max().item() |
| assert diff < 1e-5, f"Second projection changed weights by {diff}" |
|
|
|
|
| class TestNormPreservation: |
| """Verify that norm-preserving projection maintains weight magnitude.""" |
|
|
| def test_norm_preserving_projection(self): |
| """Biprojected norm-preserving abliteration should keep ||W|| constant.""" |
| torch.manual_seed(42) |
| hidden = 256 |
| out_dim = 128 |
|
|
| W = torch.randn(out_dim, hidden) |
| d = torch.randn(hidden) |
| d = d / d.norm() |
|
|
| |
| proj_coeff = W @ d |
| W_proj = W - proj_coeff.unsqueeze(1) * d.unsqueeze(0) |
|
|
| |
| row_norms_orig = W.norm(dim=1, keepdim=True).clamp(min=1e-8) |
| row_norms_proj = W_proj.norm(dim=1, keepdim=True).clamp(min=1e-8) |
| W_norm_preserved = W_proj * (row_norms_orig / row_norms_proj) |
|
|
| |
| residual = W_norm_preserved @ d |
| |
| |
| original_proj = (W @ d).abs().mean().item() |
| preserved_proj = residual.abs().mean().item() |
| assert preserved_proj < original_proj * 0.5, \ |
| f"Norm-preserved projection {preserved_proj} not much less than original {original_proj}" |
|
|
| |
| row_diff = (W_norm_preserved.norm(dim=1) - W.norm(dim=1)).abs().max().item() |
| assert row_diff < 1e-5, f"Row norms changed by {row_diff}" |
|
|
|
|
| class TestSVDDirectionExtraction: |
| """Verify that SVD on the difference matrix extracts the refusal direction.""" |
|
|
| def test_planted_direction_recovery(self): |
| """Plant a known direction in the difference and verify SVD recovers it.""" |
| torch.manual_seed(42) |
| n_samples = 50 |
| hidden = 256 |
|
|
| |
| true_direction = torch.randn(hidden) |
| true_direction = true_direction / true_direction.norm() |
|
|
| |
| harmless = torch.randn(n_samples, hidden) * 0.5 |
| signal_strength = 5.0 |
| harmful = harmless + signal_strength * true_direction.unsqueeze(0) + torch.randn(n_samples, hidden) * 0.1 |
|
|
| |
| diff = harmful - harmless |
| U, S, Vh = torch.linalg.svd(diff, full_matrices=False) |
| extracted = Vh[0] |
| extracted = extracted / extracted.norm() |
|
|
| |
| cosine = (extracted @ true_direction).abs().item() |
| assert cosine > 0.95, f"Cosine similarity {cosine:.3f} too low (expected > 0.95)" |
|
|
| def test_multi_direction_recovery(self): |
| """Plant k directions and verify SVD recovers the subspace.""" |
| torch.manual_seed(42) |
| n_samples = 200 |
| hidden = 256 |
| k = 3 |
|
|
| |
| Q, _ = torch.linalg.qr(torch.randn(hidden, k)) |
| true_subspace = Q.T |
|
|
| |
| harmless = torch.randn(n_samples, hidden) * 0.01 |
| coefficients = torch.randn(n_samples, k).abs() * 5.0 |
| signal = coefficients @ true_subspace |
| harmful = harmless + signal |
|
|
| diff = harmful - harmless |
| U, S, Vh = torch.linalg.svd(diff, full_matrices=False) |
| extracted_subspace = Vh[:k] |
|
|
| |
| for i in range(k): |
| proj = extracted_subspace @ true_subspace[i] |
| captured_variance = proj.norm().item() |
| assert captured_variance > 0.9, \ |
| f"Direction {i}: captured variance {captured_variance:.3f} too low" |
|
|
|
|
| class TestRandomDirectionBaseline: |
| """Verify that random directions do NOT have the same effect as learned ones.""" |
|
|
| def test_random_direction_has_lower_projection(self): |
| """Random directions should project much less on harmful activations |
| than the true refusal direction.""" |
| torch.manual_seed(42) |
| n_samples = 50 |
| hidden = 256 |
|
|
| |
| true_dir = torch.randn(hidden) |
| true_dir = true_dir / true_dir.norm() |
|
|
| harmless = torch.randn(n_samples, hidden) * 0.5 |
| harmful = harmless + 3.0 * true_dir.unsqueeze(0) |
|
|
| harmful_mean = harmful.mean(dim=0) |
|
|
| |
| true_proj = (harmful_mean @ true_dir).abs().item() |
|
|
| |
| random_projs = [] |
| for i in range(100): |
| rng = torch.Generator().manual_seed(10000 + i) |
| rand_dir = torch.randn(hidden, generator=rng) |
| rand_dir = rand_dir / rand_dir.norm() |
| random_projs.append((harmful_mean @ rand_dir).abs().item()) |
|
|
| mean_random = sum(random_projs) / len(random_projs) |
|
|
| |
| assert true_proj > mean_random * 3.0, \ |
| f"True projection ({true_proj:.3f}) not much larger than random mean ({mean_random:.3f})" |
|
|
|
|
| class TestWhitenedSVD: |
| """Verify whitened SVD properties.""" |
|
|
| def test_whitened_directions_are_orthogonal(self): |
| """Whitened SVD should produce orthogonal directions.""" |
| torch.manual_seed(42) |
| n_samples = 80 |
| hidden = 128 |
| k = 4 |
|
|
| H = torch.randn(n_samples, hidden) + torch.randn(1, hidden) * 2 |
| B = torch.randn(n_samples, hidden) |
|
|
| mu_B = B.mean(dim=0, keepdim=True) |
| B_centered = B - mu_B |
| cov_B = (B_centered.T @ B_centered) / (n_samples - 1) |
| cov_B += 1e-4 * torch.eye(hidden) |
|
|
| eigenvalues, eigenvectors = torch.linalg.eigh(cov_B) |
| eigenvalues = eigenvalues.clamp(min=0) |
| inv_sqrt_eig = 1.0 / torch.sqrt(eigenvalues + 1e-4) |
| whiten_proj = eigenvectors * inv_sqrt_eig.unsqueeze(0) |
|
|
| H_whitened = (H - mu_B) @ whiten_proj |
| B_whitened = B_centered @ whiten_proj |
| D_whitened = H_whitened - B_whitened |
|
|
| U, S, Vh = torch.linalg.svd(D_whitened, full_matrices=False) |
| directions = Vh[:k] |
|
|
| |
| gram = directions @ directions.T |
| identity = torch.eye(k) |
| off_diag = (gram - identity).abs().max().item() |
| assert off_diag < 1e-4, f"Directions not orthogonal: max off-diagonal = {off_diag}" |
|
|
|
|
| class TestReproducibility: |
| """Verify that seed setting produces deterministic results.""" |
|
|
| def test_set_seed_determinism(self): |
| """Same seed should produce identical random tensors.""" |
| from obliteratus.reproducibility import set_seed |
|
|
| set_seed(123, deterministic=False) |
| a = torch.randn(100) |
|
|
| set_seed(123, deterministic=False) |
| b = torch.randn(100) |
|
|
| assert torch.equal(a, b), "Same seed produced different tensors" |
|
|
| def test_different_seeds_differ(self): |
| """Different seeds should produce different tensors.""" |
| from obliteratus.reproducibility import set_seed |
|
|
| set_seed(123, deterministic=False) |
| a = torch.randn(100) |
|
|
| set_seed(456, deterministic=False) |
| b = torch.randn(100) |
|
|
| assert not torch.equal(a, b), "Different seeds produced identical tensors" |
|
|