| import tempfile |
| import unittest |
| from pathlib import Path |
| from types import SimpleNamespace |
|
|
| import torch |
|
|
| from owmi.backends import HFBackend, TransformerLensBackend |
| from owmi.benchmarks.runner import BenchmarkRunner |
| from owmi.interventions import ( |
| LinearSAE, |
| apply_intervention, |
| apply_sae_feature_intervention, |
| load_linear_sae, |
| make_forward_hook, |
| make_head_prehook, |
| ) |
| from owmi.types import ExperimentConfig, InterventionSpec |
| from owmi.validation import ( |
| head_slice_isolation_report, |
| validate_head_intervention, |
| validate_sae_roundtrip, |
| ) |
|
|
|
|
| class _TinyTokenizer: |
| pad_token = eos_token = '<eos>' |
| eos_token_id = 0 |
|
|
| def apply_chat_template(self, messages, tokenize=False, add_generation_prompt=True): |
| return 'x' |
|
|
| def __call__(self, prompt, return_tensors='pt'): |
| class Encoded(dict): |
| def to(self, device): |
| return self |
| return Encoded(input_ids=torch.tensor([[1, 2, 3]])) |
|
|
|
|
| class _TinyBlock(torch.nn.Module): |
| """Decoder block with a synthetic attention layout: o_proj input == block hidden.""" |
|
|
| def __init__(self, width): |
| super().__init__() |
| attn = torch.nn.Module() |
| attn.o_proj = torch.nn.Linear(width, width, bias=False) |
| with torch.no_grad(): |
| attn.o_proj.weight.copy_(torch.eye(width)) |
| self.self_attn = attn |
|
|
| def forward(self, hidden): |
| return (self.self_attn.o_proj(hidden),) |
|
|
|
|
| class _TinyAttnModel(torch.nn.Module): |
| def __init__(self, n_heads=2, head_dim=2, kv_heads=None): |
| super().__init__() |
| self.width = n_heads * head_dim |
| self.config = SimpleNamespace( |
| num_attention_heads=n_heads, |
| num_key_value_heads=kv_heads if kv_heads is not None else n_heads, |
| head_dim=head_dim, |
| ) |
| inner = torch.nn.Module() |
| inner.layers = torch.nn.ModuleList([_TinyBlock(self.width)]) |
| self.model = inner |
|
|
| def hidden_for(self, seq_len): |
| return torch.arange(seq_len * self.width, dtype=torch.float32).reshape(1, seq_len, self.width) + 1.0 |
|
|
| def forward(self, input_ids, use_cache=False): |
| hidden = self.hidden_for(int(input_ids.shape[1])) |
| out = self.model.layers[0](hidden)[0] |
| return type('Output', (), {'logits': out})() |
|
|
|
|
| def _hf_backend(model): |
| backend = HFBackend.__new__(HFBackend) |
| backend.model = model |
| backend.config = ExperimentConfig(model_name='fake') |
| backend.tokenizer = _TinyTokenizer() |
| backend.device = 'cpu' |
| return backend |
|
|
|
|
| class _FakeBackend: |
| def __init__(self): |
| self.intervention_calls = [] |
| self.generated = [] |
|
|
| def generate(self, prompt): |
| self.generated.append(prompt) |
| return '{"detected": true, "confidence": 1}' |
|
|
| def run_with_intervention(self, prompt, intervention): |
| self.intervention_calls.append((prompt, intervention)) |
| return '{"detected": true, "confidence": 1}' |
|
|
| def output_divergence(self, prompt, intervention): |
| return {"mean_js": 0.25, "max_js": 0.5, "n_positions": 3.0} |
|
|
|
|
| def _runner_row(obj, output_dir='test-results'): |
| return { |
| 'run_id': 'routing-run', 'suite_name': 'test', |
| 'model': {'name': 'fake', 'hook_temporal_scope': 'active_through_probe'}, |
| 'benchmark': {'name': 'toy', 'schema': 'multiple_choice'}, |
| 'benchmark_example': { |
| 'benchmark': 'toy', 'item_id': '1', 'schema': 'multiple_choice', |
| 'prompt': 'Question?', 'reference': {'answer_letter': 'A'}, 'metadata': {}, |
| }, |
| 'object': obj, |
| 'intervention': {'kind': 'intervention', 'condition': 'intervention', 'mode': 'zero', 'strength': 1.0}, |
| 'probe': {'task': 'detection', 'condition': 'model'}, |
| 'seed': 7, 'output_dir': output_dir, |
| } |
|
|
|
|
| def _identity_sae_state(width=4): |
| return { |
| 'W_enc': torch.eye(width), |
| 'b_enc': torch.zeros(width), |
| 'W_dec': torch.eye(width), |
| 'b_dec': torch.zeros(width), |
| } |
|
|
|
|
| class ChannelSliceOperatorTests(unittest.TestCase): |
| def test_zero_restricted_to_channel_window_and_positions(self): |
| hidden = torch.ones((1, 3, 4)) |
| spec = InterventionSpec(layer_index=0, token_positions=[0, 1], mode='zero') |
| out = apply_intervention(hidden, spec, channel_slice=(2, 4)) |
| self.assertTrue(torch.equal(out[:, :2, 2:4], torch.zeros((1, 2, 2)))) |
| self.assertTrue(torch.equal(out[:, :2, 0:2], torch.ones((1, 2, 2)))) |
| self.assertTrue(torch.equal(out[:, 2], hidden[:, 2])) |
|
|
| def test_scale_noise_replace_random_leave_off_window_untouched(self): |
| torch.manual_seed(0) |
| hidden = torch.randn((1, 3, 4)) |
| for mode, reference_mode in (('scale', 'none'), ('noise', 'none'), ('replace', 'random')): |
| spec = InterventionSpec(layer_index=0, mode=mode, strength=0.5, reference_mode=reference_mode) |
| out = apply_intervention(hidden, spec, channel_slice=(0, 2)) |
| self.assertTrue(torch.equal(out[..., 2:], hidden[..., 2:]), mode) |
| self.assertFalse(torch.equal(out[..., :2], hidden[..., :2]), mode) |
|
|
| def test_channel_slice_out_of_range_raises(self): |
| hidden = torch.ones((1, 3, 4)) |
| spec = InterventionSpec(layer_index=0, mode='zero') |
| with self.assertRaises(ValueError): |
| apply_intervention(hidden, spec, channel_slice=(2, 5)) |
|
|
|
|
| class HeadPrehookTests(unittest.TestCase): |
| def test_head_prehook_modifies_prefill_slice_but_never_decode_steps(self): |
| spec = InterventionSpec(layer_index=0, mode='zero', object_kind='attention_head', head_index=1) |
| prehook = make_head_prehook(spec, head_dim=2, prompt_length=3) |
| prefill = torch.ones((1, 3, 4)) |
| modified = prehook(torch.nn.Identity(), (prefill,))[0] |
| self.assertTrue(torch.equal(modified[..., 2:4], torch.zeros((1, 3, 2)))) |
| self.assertTrue(torch.equal(modified[..., 0:2], torch.ones((1, 3, 2)))) |
| decode = torch.ones((1, 1, 4)) |
| self.assertIsNone(prehook(torch.nn.Identity(), (decode,))) |
|
|
| def test_head_prehook_requires_head_index(self): |
| spec = InterventionSpec(layer_index=0, mode='zero', object_kind='attention_head') |
| with self.assertRaises(ValueError): |
| make_head_prehook(spec, head_dim=2) |
|
|
|
|
| class HFHeadRoutingTests(unittest.TestCase): |
| def test_head_zero_changes_only_target_head_slice_under_gqa(self): |
| backend = _hf_backend(_TinyAttnModel(n_heads=4, head_dim=2, kv_heads=2)) |
| spec = InterventionSpec(layer_index=0, mode='zero', object_kind='attention_head', head_index=3) |
| report = validate_head_intervention(backend, 'x', spec) |
| self.assertEqual(report['head_dim'], 2) |
| self.assertEqual(report['head_slice'], [6, 8]) |
| self.assertTrue(report['changed']) |
| self.assertTrue(report['isolated']) |
|
|
| def test_head_index_addresses_query_heads_not_kv_heads(self): |
| backend = _hf_backend(_TinyAttnModel(n_heads=4, head_dim=2, kv_heads=2)) |
| spec = InterventionSpec(layer_index=0, mode='zero', object_kind='attention_head', head_index=2) |
| report = validate_head_intervention(backend, 'x', spec) |
| self.assertEqual(report['head_slice'], [4, 6]) |
| out_of_range = InterventionSpec(layer_index=0, mode='zero', object_kind='attention_head', head_index=4) |
| with self.assertRaises(ValueError): |
| validate_head_intervention(backend, 'x', out_of_range) |
|
|
| def test_head_replace_baseline_reproduces_baseline_at_site(self): |
| backend = _hf_backend(_TinyAttnModel()) |
| spec = InterventionSpec( |
| layer_index=0, mode='replace', reference_mode='baseline', |
| object_kind='attention_head', head_index=0, |
| ) |
| report = validate_head_intervention(backend, 'x', spec) |
| self.assertFalse(report['changed']) |
| self.assertTrue(report['isolated']) |
|
|
| def test_head_output_divergence_routes_through_head_hook(self): |
| backend = _hf_backend(_TinyAttnModel()) |
| spec = InterventionSpec(layer_index=0, mode='zero', object_kind='attention_head', head_index=1) |
| result = backend.output_divergence('x', spec) |
| self.assertEqual(result['n_positions'], 3.0) |
| self.assertGreater(result['mean_js'], 0.0) |
|
|
| def test_head_slice_isolation_report_flags_off_slice_changes(self): |
| baseline = torch.zeros((1, 2, 4)) |
| leaked = torch.zeros((1, 2, 4)) |
| leaked[..., 0] = 1.0 |
| report = head_slice_isolation_report(baseline, leaked, head_index=1, head_dim=2) |
| self.assertFalse(report['changed']) |
| self.assertFalse(report['isolated']) |
|
|
|
|
| class SAEFeatureTests(unittest.TestCase): |
| def setUp(self): |
| tmpdir = tempfile.TemporaryDirectory() |
| self.addCleanup(tmpdir.cleanup) |
| self.sae_path = str(Path(tmpdir.name) / 'sae.pt') |
| torch.save(_identity_sae_state(4), self.sae_path) |
| self.hidden = torch.arange(12, dtype=torch.float32).reshape(1, 3, 4) + 1.0 |
|
|
| def _spec(self, mode='zero', **kwargs): |
| defaults = dict( |
| layer_index=0, mode=mode, object_kind='sae_feature', |
| feature_id=2, sae_weights_path=self.sae_path, |
| ) |
| defaults.update(kwargs) |
| return InterventionSpec(**defaults) |
|
|
| def test_load_linear_sae_validates_keys_and_normalizes_transposed_weights(self): |
| bad_path = str(Path(self.sae_path).parent / 'bad.pt') |
| state = _identity_sae_state(4) |
| del state['W_dec'] |
| torch.save(state, bad_path) |
| with self.assertRaises(ValueError): |
| load_linear_sae(bad_path) |
| transposed_path = str(Path(self.sae_path).parent / 'transposed.pt') |
| torch.save({ |
| 'W_enc': torch.eye(4)[:3, :], |
| 'b_enc': torch.zeros(3), |
| 'W_dec': torch.eye(4)[:, :3], |
| 'b_dec': torch.zeros(4), |
| }, transposed_path) |
| sae = load_linear_sae(transposed_path) |
| self.assertEqual((sae.d_model, sae.d_sae), (4, 3)) |
| self.assertEqual(tuple(sae.W_enc.shape), (4, 3)) |
| self.assertEqual(tuple(sae.W_dec.shape), (3, 4)) |
|
|
| def test_zero_feature_changes_only_that_feature_at_target_positions(self): |
| sae = load_linear_sae(self.sae_path) |
| spec = self._spec(token_positions=[0, 2]) |
| out = apply_sae_feature_intervention(self.hidden, spec, sae, position_seq_len=3) |
| self.assertTrue(torch.equal(out[:, [0, 2], 2], torch.zeros((1, 2)))) |
| self.assertTrue(torch.equal(out[:, 1], self.hidden[:, 1])) |
| for channel in (0, 1, 3): |
| self.assertTrue(torch.equal(out[..., channel], self.hidden[..., channel])) |
|
|
| def test_scale_at_strength_one_is_exact_identity(self): |
| sae = load_linear_sae(self.sae_path) |
| out = apply_sae_feature_intervention(self.hidden, self._spec(mode='scale', strength=1.0), sae) |
| self.assertTrue(torch.equal(out, self.hidden)) |
|
|
| def test_replace_baseline_uses_reference_feature_and_requires_reference(self): |
| sae = load_linear_sae(self.sae_path) |
| spec = self._spec(mode='replace', reference_mode='baseline') |
| reference = self.hidden * 2.0 |
| out = apply_sae_feature_intervention(self.hidden, spec, sae, reference_hidden=reference) |
| self.assertTrue(torch.equal(out[..., 2], reference[..., 2])) |
| for channel in (0, 1, 3): |
| self.assertTrue(torch.equal(out[..., channel], self.hidden[..., channel])) |
| with self.assertRaises(ValueError): |
| apply_sae_feature_intervention(self.hidden, spec, sae) |
|
|
| def test_feature_id_out_of_range_raises(self): |
| sae = load_linear_sae(self.sae_path) |
| with self.assertRaises(ValueError): |
| apply_sae_feature_intervention(self.hidden, self._spec(feature_id=4), sae) |
|
|
| def test_forward_hook_sae_path_is_prefill_only(self): |
| sae = load_linear_sae(self.sae_path) |
| hook = make_forward_hook(self._spec(), prompt_length=3, sae=sae) |
| modified = hook(torch.nn.Identity(), (), (self.hidden,))[0] |
| self.assertTrue(torch.equal(modified[..., 2], torch.zeros((1, 3)))) |
| decode = torch.ones((1, 1, 4)) |
| unchanged = hook(torch.nn.Identity(), (), (decode,))[0] |
| self.assertTrue(torch.equal(unchanged, decode)) |
|
|
| def test_hf_backend_routes_sae_hook_to_block_and_modifies_only_feature(self): |
| model = _TinyAttnModel(n_heads=2, head_dim=2) |
| backend = _hf_backend(model) |
| handle = backend._register_intervention_hook(self._spec(), prompt_length=3) |
| module = backend.resolve_block_module(0) |
| captured = {} |
|
|
| def capture(_, __, output): |
| captured['tensor'] = output[0].detach().clone() |
| return None |
|
|
| capture_handle = module.register_forward_hook(capture) |
| try: |
| with torch.no_grad(): |
| _ = backend.model(input_ids=torch.tensor([[1, 2, 3]]), use_cache=False) |
| finally: |
| capture_handle.remove() |
| handle.remove() |
| baseline = model.hidden_for(3) |
| self.assertTrue(torch.equal(captured['tensor'][..., 2], torch.zeros((1, 3)))) |
| for channel in (0, 1, 3): |
| self.assertTrue(torch.equal(captured['tensor'][..., channel], baseline[..., channel])) |
|
|
| def test_sae_roundtrip_validation_identity_and_lossy(self): |
| identity = validate_sae_roundtrip(self.sae_path, self.hidden, self._spec()) |
| self.assertEqual(identity['reconstruction_mse'], 0.0) |
| self.assertTrue(identity['feature_delta_confirmed']) |
| self.assertTrue(identity['delta_in_feature_direction']) |
| lossy = LinearSAE( |
| W_enc=torch.eye(4)[:, :3], b_enc=torch.zeros(3), |
| W_dec=torch.eye(4)[:3, :], b_dec=torch.zeros(4), |
| ) |
| report = validate_sae_roundtrip(lossy, self.hidden, self._spec(feature_id=1)) |
| self.assertGreater(report['reconstruction_mse'], 0.0) |
| self.assertTrue(report['feature_delta_confirmed']) |
| self.assertTrue(report['delta_in_feature_direction']) |
|
|
|
|
| class RunnerRoutingTests(unittest.TestCase): |
| def setUp(self): |
| tmpdir = tempfile.TemporaryDirectory() |
| self.addCleanup(tmpdir.cleanup) |
| self.tmp = Path(tmpdir.name) |
| self.sae_path = str(self.tmp / 'sae.pt') |
| torch.save(_identity_sae_state(4), self.sae_path) |
|
|
| def test_core_intervention_populates_head_routing_fields(self): |
| obj = {'kind': 'attention_head', 'layer_index': 1, 'head_index': 2, 'token_positions': [0]} |
| spec = BenchmarkRunner(_runner_row(obj))._core_intervention() |
| self.assertEqual(spec.object_kind, 'attention_head') |
| self.assertEqual(spec.head_index, 2) |
| spec.validate() |
|
|
| def test_core_intervention_reads_sae_path_from_direction_path_or_metadata(self): |
| for obj in ( |
| {'kind': 'sae_feature', 'layer_index': 1, 'feature_id': 3, 'direction_path': self.sae_path}, |
| {'kind': 'sae_feature', 'layer_index': 1, 'feature_id': 3, |
| 'metadata': {'sae_weights_path': self.sae_path}}, |
| ): |
| spec = BenchmarkRunner(_runner_row(obj))._core_intervention() |
| self.assertEqual(spec.object_kind, 'sae_feature') |
| self.assertEqual(spec.feature_id, 3) |
| self.assertEqual(spec.sae_weights_path, self.sae_path) |
| spec.validate() |
|
|
| def test_unroutable_object_configs_still_raise_not_implemented(self): |
| cases = [ |
| {'kind': 'attention_head', 'layer_index': 1}, |
| {'kind': 'sae_feature', 'layer_index': 1}, |
| {'kind': 'sae_feature', 'layer_index': 1, 'feature_id': 0}, |
| ] |
| for obj in cases: |
| with self.assertRaises(NotImplementedError): |
| BenchmarkRunner(_runner_row(obj))._core_intervention() |
| with self.assertRaisesRegex(NotImplementedError, 'direction_path'): |
| BenchmarkRunner(_runner_row(cases[2]))._core_intervention() |
|
|
| def test_runner_end_to_end_head_row_with_fake_backend(self): |
| backend = _FakeBackend() |
| obj = {'kind': 'attention_head', 'layer_index': 1, 'head_index': 2, 'token_positions': [0]} |
| result = BenchmarkRunner(_runner_row(obj, output_dir=str(self.tmp / 'out'))).run(backend=backend) |
| self.assertEqual(result.extra['intervention_spec']['object_kind'], 'attention_head') |
| self.assertEqual(result.extra['intervention_spec']['head_index'], 2) |
| for _, spec in backend.intervention_calls: |
| self.assertEqual(spec.object_kind, 'attention_head') |
| self.assertEqual(spec.head_index, 2) |
|
|
| def test_runtime_spec_validation_for_object_kinds(self): |
| with self.assertRaises(ValueError): |
| InterventionSpec(layer_index=0, object_kind='attention_head').validate() |
| with self.assertRaises(ValueError): |
| InterventionSpec(layer_index=0, object_kind='sae_feature', feature_id=1).validate() |
| InterventionSpec( |
| layer_index=0, object_kind='sae_feature', feature_id=1, sae_weights_path=self.sae_path |
| ).validate() |
|
|
| def test_transformerlens_backend_rejects_head_and_sae_kinds(self): |
| backend = TransformerLensBackend.__new__(TransformerLensBackend) |
| backend.model = object() |
| backend.config = ExperimentConfig(model_name='fake') |
| specs = [ |
| InterventionSpec(layer_index=0, object_kind='attention_head', head_index=0), |
| InterventionSpec(layer_index=0, object_kind='sae_feature', feature_id=0, |
| sae_weights_path=self.sae_path), |
| ] |
| for spec in specs: |
| with self.assertRaises(NotImplementedError): |
| backend.run_with_intervention('x', spec) |
| with self.assertRaises(NotImplementedError): |
| backend.output_divergence('x', spec) |
|
|
|
|
| if __name__ == '__main__': |
| unittest.main() |
|
|