Download tests/test_path_patching.py from gexinh/vlm-probe: direct link, hf CLI and curl.
- Browser
- Download file 26.6 kB
-
https://huggingface.co/spaces/gexinh/vlm-probe/resolve/main/tests/test_path_patching.py
- Command line
-
hf download hf://spaces/gexinh/vlm-probe/tests/test_path_patching.py
-
curl -L -o test_path_patching.py https://huggingface.co/spaces/gexinh/vlm-probe/resolve/main/tests/test_path_patching.py
26.6 kB
| """IOI path controls checked against an independent native-hook experiment.""" | |
| from contextlib import contextmanager | |
| import importlib.util | |
| import math | |
| import unittest | |
| import torch | |
| from torch import nn | |
| from vlm_probing import ( | |
| CapabilityError, HeadSite, HookPoint, ModelReadout, ModelSpec, Prober, | |
| TokenLayout, TokenMargin, TorchModelAdapter, | |
| ) | |
| class CircuitBlock(nn.Module): | |
| """Two physical query heads, optional shared KV head, and real LN/MLP.""" | |
| def __init__(self, kv_heads): | |
| super().__init__() | |
| self.kv_heads = kv_heads | |
| self.norm1, self.norm2 = nn.LayerNorm(4), nn.LayerNorm(4) | |
| self.q = nn.Linear(4, 4, bias=False) | |
| self.k, self.v = nn.Linear(4, kv_heads * 2, bias=False), nn.Linear(4, kv_heads * 2, bias=False) | |
| self.head_out = nn.Identity() | |
| self.o = nn.Linear(4, 4, bias=False) | |
| self.mlp = nn.Sequential(nn.Linear(4, 7), nn.Tanh(), nn.Linear(7, 4)) | |
| self.residual = nn.Identity() | |
| def forward(self, hidden, record, layer): | |
| normalized = self.norm1(hidden) | |
| q = self.q(normalized).reshape(*hidden.shape[:2], 2, 2) | |
| k = self.k(normalized).reshape(*hidden.shape[:2], self.kv_heads, 2) | |
| v = self.v(normalized).reshape(*hidden.shape[:2], self.kv_heads, 2) | |
| record["norm"][layer] = normalized.detach().clone() | |
| for kind, value in (("q", q), ("k", k), ("v", v)): | |
| record[kind][layer] = value.detach().clone() | |
| key, value = (tensor.repeat_interleave(2 // self.kv_heads, dim=2) for tensor in (k, v)) | |
| scores = torch.einsum("bthd,bshd->bhts", q, key) / math.sqrt(2) | |
| future = torch.ones(hidden.shape[1], hidden.shape[1], dtype=torch.bool).triu(1) | |
| attention = scores.masked_fill(future, -torch.inf).softmax(-1) | |
| heads = self.head_out(torch.einsum("bhts,bshd->bthd", attention, value)) | |
| record["heads"][layer] = heads.detach().clone() | |
| hidden = hidden + self.o(heads.flatten(-2)) | |
| mlp = self.mlp(self.norm2(hidden)) | |
| record["mlp"][layer] = mlp.detach().clone() | |
| hidden = self.residual(hidden + mlp) | |
| record["residual"][layer] = hidden.detach().clone() | |
| return hidden | |
| class CircuitModel(nn.Module): | |
| """A deterministic decoder with logs of the values actually used each run.""" | |
| def __init__(self, *, kv_heads=2, flattened_heads=False): | |
| super().__init__() | |
| self.embedding = nn.Embedding(13, 4) | |
| self.blocks = nn.ModuleList(CircuitBlock(kv_heads) for _ in range(3)) | |
| self.final_residual, self.norm = nn.Identity(), nn.LayerNorm(4) | |
| self.readout = nn.Linear(4, 13, bias=False) | |
| self.records = [] | |
| self.fail_run = None | |
| self.kv_heads, self.flattened_heads = kv_heads, flattened_heads | |
| self.double() | |
| def forward(self, input_ids, attention_mask=None): | |
| record = {name: {} for name in ("q", "k", "v", "heads", "norm", "mlp", "residual")} | |
| self.records.append(record) | |
| if self.fail_run == len(self.records): | |
| raise RuntimeError("intentional controlled-run failure") | |
| hidden = self.embedding(input_ids) | |
| for layer, block in enumerate(self.blocks): | |
| hidden = block(hidden, record, layer) | |
| hidden = self.final_residual(hidden) | |
| record["final"] = hidden.detach().clone() | |
| logits = self.readout(self.norm(hidden)) | |
| record["logits"] = logits.detach().clone() | |
| return {"logits": logits} | |
| def probing_adapter(self): | |
| sites, residuals, head_sites, qkv = {}, {}, {}, {} | |
| for layer in range(3): | |
| residuals[layer] = f"residual.{layer}" | |
| sites[residuals[layer]] = HookPoint(f"blocks.{layer}.residual") | |
| output = f"head_output.{layer}" | |
| # The output projection sees flattened messages. The identity sees | |
| # the same messages with an explicit physical head dimension. | |
| sites[output] = (HookPoint(f"blocks.{layer}.o", kind="input", selector=0) | |
| if self.flattened_heads else HookPoint(f"blocks.{layer}.head_out")) | |
| head_sites[layer] = HeadSite(output, heads=2, head_dim=2) | |
| qkv[layer] = {} | |
| for kind in ("q", "k", "v"): | |
| name = f"{kind}.{layer}" | |
| sites[name] = HookPoint(f"blocks.{layer}.{kind}") | |
| qkv[layer][kind] = HeadSite(name, heads=2 if kind == "q" else self.kv_heads, head_dim=2) | |
| sites["final"] = HookPoint("final_residual") | |
| def layout(inputs): | |
| ids = inputs["input_ids"] | |
| valid = inputs.get("attention_mask", torch.ones_like(ids)).bool() | |
| return TokenLayout(valid=valid, token_ids=ids) | |
| spec = ModelSpec(residuals, layout=layout, path_heads=head_sites, | |
| path_qkv=qkv, path_final="final") | |
| return TorchModelAdapter(self, sites, spec=spec, model_id="independent-path-circuit", | |
| readout=ModelReadout(self.readout, norm=self.norm)) | |
| def native_hooks(edits): | |
| """Direct PyTorch hooks, independent of adapter and path implementation.""" | |
| handles = [] | |
| try: | |
| for module, edit in edits: | |
| handles.append(module.register_forward_hook(lambda _m, _a, value, edit=edit: edit(value))) | |
| yield | |
| finally: | |
| for handle in handles: | |
| handle.remove() | |
| def native_pre_hooks(edits): | |
| handles = [] | |
| try: | |
| for module, edit in edits: | |
| handles.append(module.register_forward_pre_hook( | |
| lambda _m, args, edit=edit: (edit(args[0]), *args[1:]))) | |
| yield | |
| finally: | |
| for handle in handles: | |
| handle.remove() | |
| def native_gpt2_oracle(model, base, donor, senders, receivers, selected): | |
| """Operate on native packed projections without HookPoint or HeadSite.""" | |
| def forward(inputs, *, output_edits=(), input_edits=()): | |
| captured = {"heads": {}, "qkv": {}} | |
| def keep(value, *, layer, kind): | |
| captured[kind][layer] = value.detach().clone() | |
| return value | |
| projections = [(block.attn.c_attn, lambda value, layer=i: keep(value, layer=layer, kind="qkv")) | |
| for i, block in enumerate(model.transformer.h)] | |
| heads = [(block.attn.c_proj, lambda value, layer=i: keep(value, layer=layer, kind="heads")) | |
| for i, block in enumerate(model.transformer.h)] | |
| with native_hooks(output_edits), native_pre_hooks(input_edits), native_hooks(projections), native_pre_hooks(heads): | |
| logits = model(**inputs, use_cache=False, return_dict=True).logits | |
| return logits, captured | |
| with torch.no_grad(): | |
| base_logits, clean = forward(base) | |
| donor_logits, changed = forward(donor) | |
| freezes = [] | |
| for layer, block in enumerate(model.transformer.h): | |
| def freeze(value, layer=layer): | |
| result = clean["heads"][layer].reshape(*value.shape[:2], model.config.n_head, -1).clone() | |
| source = changed["heads"][layer].reshape_as(result) | |
| for sender_layer, head in senders: | |
| if sender_layer == layer: | |
| result[:, :, head][selected] = source[:, :, head][selected] | |
| return result.reshape_as(value) | |
| freezes.append((block.attn.c_proj, freeze)) | |
| _, controlled = forward(base, input_edits=freezes) | |
| targets = {} | |
| for layer, head, kind in receivers: | |
| targets.setdefault(layer, []).append((head, kind)) | |
| injections = [] | |
| for layer, targets_in_layer in targets.items(): | |
| def inject(value, layer=layer, targets_in_layer=targets_in_layer): | |
| result = value.clone() | |
| for head, kind in targets_in_layer: | |
| start = {"q": 0, "k": 1, "v": 2}[kind] * model.config.n_embd | |
| stop = start + model.config.n_embd | |
| target = result[..., start:stop].reshape(*value.shape[:2], model.config.n_head, -1) | |
| source = controlled["qkv"][layer][..., start:stop].reshape_as(target) | |
| target[:, :, head][selected] = source[:, :, head][selected] | |
| return result | |
| injections.append((model.transformer.h[layer].attn.c_attn, inject)) | |
| intervention_logits, _ = forward(base, output_edits=injections) | |
| return base_logits, donor_logits, intervention_logits | |
| def native_path_oracle(model, base, donor, senders, receivers, sender_mask, donor_mask, receiver_mask): | |
| """Four real forward passes implementing the requested output-freeze rule.""" | |
| model.eval() | |
| with torch.no_grad(): | |
| base_logits = model(**base)["logits"] | |
| clean = model.records[-1] | |
| donor_logits = model(**donor)["logits"] | |
| changed = model.records[-1] | |
| edits = [] | |
| for layer, block in enumerate(model.blocks): | |
| def freeze(value, layer=layer): | |
| result = clean["heads"][layer].clone() | |
| for sender_layer, head in senders: | |
| if sender_layer != layer: | |
| continue | |
| for batch in range(result.shape[0]): | |
| result[batch, sender_mask[batch], head] = changed["heads"][layer][batch, donor_mask[batch], head] | |
| return result | |
| edits.append((block.head_out, freeze)) | |
| with native_hooks(edits): | |
| model(**base) | |
| controlled = model.records[-1] | |
| edits = [] | |
| if receivers == "residual": | |
| def inject_final(value): | |
| result = value.clone() | |
| result[receiver_mask] = controlled["final"][receiver_mask] | |
| return result | |
| edits.append((model.final_residual, inject_final)) | |
| else: | |
| groups = {} | |
| for layer, head, kind in receivers: | |
| groups.setdefault((layer, kind), []).append(head) | |
| for (layer, kind), heads in groups.items(): | |
| def inject(value, layer=layer, kind=kind, heads=heads): | |
| result = value.reshape(*value.shape[:2], model.blocks[layer].q.out_features // 2 if kind == "q" else model.kv_heads, 2).clone() | |
| for head in heads: | |
| result[:, :, head][receiver_mask] = controlled[kind][layer][:, :, head][receiver_mask] | |
| return result.reshape_as(value) | |
| edits.append((getattr(model.blocks[layer], kind), inject)) | |
| with native_hooks(edits): | |
| intervention_logits = model(**base)["logits"] | |
| return base_logits, donor_logits, intervention_logits | |
| class PathPatchingTests(unittest.TestCase): | |
| def setUpClass(cls): | |
| cls.original_threads = torch.get_num_threads() | |
| torch.set_num_threads(1) | |
| def tearDownClass(cls): | |
| torch.set_num_threads(cls.original_threads) | |
| def setUp(self): | |
| with torch.random.fork_rng(devices=[]): | |
| torch.manual_seed(194) | |
| self.model = CircuitModel() | |
| self.probe = Prober(self.model) | |
| self.base = {"input_ids": torch.tensor([[1, 2, 3, 4], [4, 3, 2, 1]])} | |
| self.donor = {"input_ids": torch.tensor([[5, 6, 7, 8], [8, 7, 6, 5]])} | |
| self.metric = TokenMargin(positive=2, negative=8) | |
| self.end = torch.tensor([[False, False, False, True]]).expand(2, -1) | |
| def score(self, logits): | |
| return logits[:, -1, 2] - logits[:, -1, 8] | |
| def assert_clean(self): | |
| self.assertFalse(self.probe.adapter._active) | |
| for module in self.model.modules(): | |
| self.assertEqual(len(module._forward_hooks), 0) | |
| self.assertEqual(len(module._forward_pre_hooks), 0) | |
| def assert_oracle(self, *, senders=((0, 0),), receivers=((2, 1, "q"),), | |
| sender_mask=None, donor_mask=None, receiver_mask=None): | |
| sender_mask = self.end if sender_mask is None else sender_mask | |
| donor_mask = sender_mask if donor_mask is None else donor_mask | |
| receiver_mask = self.end if receiver_mask is None else receiver_mask | |
| result = self.probe.causal.path(senders=list(senders), receivers=receivers, | |
| sender_tokens=sender_mask, donor_tokens=donor_mask, | |
| receiver_tokens=receiver_mask).run( | |
| self.base, donor=self.donor, metric=self.metric, alignment="position") | |
| self.assertEqual(len(self.model.records), 4) | |
| self.assert_clean() | |
| actual_records = self.model.records[:] | |
| expected = native_path_oracle(self.model, self.base, self.donor, senders, | |
| receivers, sender_mask, donor_mask, receiver_mask) | |
| for name, logits in zip(("baseline_score", "donor_score", "intervention_score"), expected): | |
| torch.testing.assert_close(result.tensors[name], self.score(logits)) | |
| torch.testing.assert_close(result.tensors["effect"], self.score(expected[2]) - self.score(expected[0])) | |
| self.assertEqual(result.tensors["effect"].shape, (2,)) | |
| self.assertEqual(result.metadata["stages"], 4) | |
| self.assert_clean() | |
| return result, actual_records | |
| def test_four_real_runs_match_independent_hooks_and_have_nonzero_effect(self): | |
| result, records = self.assert_oracle() | |
| self.assertGreater(result.tensors["effect"].abs().max().item(), 1e-8) | |
| torch.testing.assert_close(records[2]["heads"][2], records[0]["heads"][2]) | |
| self.assertGreater((records[2]["q"][2] - records[0]["q"][2]).abs().max().item(), 1e-8) | |
| def test_other_heads_frozen_while_layernorm_and_mlp_recompute(self): | |
| _, records = self.assert_oracle() | |
| base, donor, controlled, final = records | |
| torch.testing.assert_close(controlled["heads"][0][:, -1, 0], donor["heads"][0][:, -1, 0]) | |
| torch.testing.assert_close(controlled["heads"][0][:, :, 1], base["heads"][0][:, :, 1]) | |
| torch.testing.assert_close(controlled["heads"][0][:, :-1, 0], base["heads"][0][:, :-1, 0]) | |
| for layer in (1, 2): | |
| torch.testing.assert_close(controlled["heads"][layer], base["heads"][layer]) | |
| self.assertGreater((controlled["norm"][1] - base["norm"][1]).abs().max().item(), 1e-8) | |
| self.assertGreater((controlled["mlp"][0] - base["mlp"][0]).abs().max().item(), 1e-8) | |
| # The receiver-only pass starts with the original sender output; the | |
| # receiver's output is free to change in response to its injected Q. | |
| torch.testing.assert_close(final["heads"][0], base["heads"][0]) | |
| self.assertGreater((final["heads"][2] - base["heads"][2]).abs().max().item(), 1e-8) | |
| def test_path_is_not_an_ordinary_residual_node_patch(self): | |
| result, _ = self.assert_oracle() | |
| patch = self.probe.causal.patch(layers=[0], tokens="last_prompt").run( | |
| self.base, source=self.donor, metric=self.metric, alignment="position") | |
| self.assertGreater((result.tensors["effect"] - patch.tensors["effect"][0]).abs().max().item(), 1e-6) | |
| def test_q_k_v_and_multiple_senders_and_receivers(self): | |
| selected = torch.tensor([[False, True, False, False]]).expand(2, -1) | |
| for kind in ("q", "k", "v"): | |
| with self.subTest(kind=kind): | |
| self.model.records.clear() | |
| self.assert_oracle(senders=((0, 0), (0, 1)), | |
| receivers=((2, 0, kind), (2, 1, kind)), | |
| sender_mask=selected, receiver_mask=selected) | |
| def test_donor_position_mapping_does_not_move_receiver_capture(self): | |
| sender = torch.tensor([[False, True, False, True], [True, False, True, False]]) | |
| donor = torch.tensor([[True, False, True, False], [False, True, False, True]]) | |
| receiver = torch.tensor([[False, False, False, True], [False, False, True, False]]) | |
| _, records = self.assert_oracle(sender_mask=sender, donor_mask=donor, receiver_mask=receiver) | |
| base, donor_run, controlled, final = records | |
| for batch in range(2): | |
| torch.testing.assert_close(controlled["heads"][0][batch, sender[batch], 0], | |
| donor_run["heads"][0][batch, donor[batch], 0]) | |
| torch.testing.assert_close(final["q"][2][:, :, 1][receiver], controlled["q"][2][:, :, 1][receiver]) | |
| torch.testing.assert_close(final["q"][2][:, :, 1][~receiver], base["q"][2][:, :, 1][~receiver]) | |
| def test_final_residual_receiver_matches_oracle(self): | |
| result, records = self.assert_oracle(receivers="residual") | |
| torch.testing.assert_close(records[3]["final"][self.end], records[2]["final"][self.end]) | |
| self.assertGreater(result.tensors["effect"].abs().max().item(), 1e-8) | |
| def test_identity_donor_and_noncausal_sender_have_zero_effect(self): | |
| method = self.probe.causal.path(senders=[(0, 0)], receivers=[(2, 1, "q")]) | |
| result = method.run(self.base, donor=self.base, metric=self.metric) | |
| torch.testing.assert_close(result.tensors["effect"], torch.zeros(2, dtype=torch.float64), atol=1e-12, rtol=0) | |
| for receiver_layer in (0, 2): | |
| with self.subTest(receiver_layer=receiver_layer): | |
| self.model.records.clear() | |
| result = self.probe.causal.path(senders=[(2, 0)], receivers=[(receiver_layer, 1, "q")]).run( | |
| self.base, donor=self.donor, metric=self.metric, alignment="position") | |
| torch.testing.assert_close(result.tensors["effect"], torch.zeros(2, dtype=torch.float64), atol=1e-12, rtol=0) | |
| def test_head_freezing_blocks_cross_position_q_propagation(self): | |
| result = self.probe.causal.path(senders=[(0, 0)], receivers=[(2, 1, "q")], | |
| sender_tokens=[1], receiver_tokens=[3]).run( | |
| self.base, donor=self.donor, metric=self.metric, alignment="position") | |
| torch.testing.assert_close(result.tensors["effect"], torch.zeros(2, dtype=torch.float64), atol=1e-12, rtol=0) | |
| def test_flattened_head_messages_match_explicit_head_axis(self): | |
| self.model.flattened_heads = True | |
| self.probe = Prober(self.model) | |
| self.assert_oracle() | |
| def test_grouped_query_kv_receivers_use_physical_head_indices(self): | |
| with torch.random.fork_rng(devices=[]): | |
| torch.manual_seed(194) | |
| self.model = CircuitModel(kv_heads=1) | |
| self.probe = Prober(self.model) | |
| for kind in ("k", "v"): | |
| with self.subTest(kind=kind): | |
| self.model.records.clear() | |
| self.assert_oracle(receivers=((2, 0, kind),)) | |
| with self.assertRaises(ValueError): | |
| self.probe.causal.path(senders=[(0, 0)], receivers=[(2, 1, kind)]) | |
| def test_partial_head_coverage_is_rejected_before_forward(self): | |
| self.probe.spec.path_heads.pop(1) | |
| with self.assertRaises(CapabilityError): | |
| self.probe.causal.path(senders=[(0, 0)]) | |
| self.assertEqual(len(self.model.records), 0) | |
| self.assertFalse(self.probe.describe()["methods"]["causal.path"]["available"]) | |
| def test_padding_and_per_example_selection_counts_are_validated(self): | |
| base = {**self.base, "attention_mask": torch.tensor([[1, 1, 1, 0], [1, 1, 1, 0]])} | |
| donor = {**self.donor, "attention_mask": base["attention_mask"]} | |
| with self.assertRaises(ValueError): | |
| self.probe.causal.path(senders=[(0, 0)], sender_tokens=[3]).run( | |
| base, donor=donor, metric=self.metric, alignment="position") | |
| for sender, donor_mask in ( | |
| (torch.tensor([[False, False, False, True], [False, False, False, False]]), self.end), | |
| (self.end, torch.tensor([[False, True, False, True], [False, False, False, True]])), | |
| ): | |
| with self.subTest(sender=sender.tolist(), donor=donor_mask.tolist()): | |
| with self.assertRaises(ValueError): | |
| self.probe.causal.path(senders=[(0, 0)], sender_tokens=sender, donor_tokens=donor_mask).run( | |
| self.base, donor=self.donor, metric=self.metric, alignment="position") | |
| self.assert_clean() | |
| def test_strict_alignment_preserves_guard_on_changed_token_ids(self): | |
| with self.assertRaises(ValueError): | |
| self.probe.causal.path(senders=[(0, 0)]).run(self.base, donor=self.donor, metric=self.metric) | |
| self.assert_clean() | |
| def test_failure_removes_control_hooks_and_restores_training_flags(self): | |
| self.model.train() | |
| self.model.blocks[1].norm1.eval() | |
| before = [module.training for module in self.model.modules()] | |
| for failed_stage in (3, 4): | |
| with self.subTest(failed_stage=failed_stage): | |
| self.model.records.clear() | |
| self.model.fail_run = failed_stage | |
| with self.assertRaisesRegex(RuntimeError, "intentional"): | |
| self.probe.causal.path(senders=[(0, 0)]).run( | |
| self.base, donor=self.donor, metric=self.metric, alignment="position") | |
| self.assert_clean() | |
| self.assertEqual([module.training for module in self.model.modules()], before) | |
| self.model.fail_run = None | |
| self.model.records.clear() | |
| retry = self.probe.causal.path(senders=[(0, 0)]).run( | |
| self.base, donor=self.base, metric=self.metric) | |
| torch.testing.assert_close(retry.tensors["effect"], torch.zeros(2, dtype=torch.float64), atol=1e-12, rtol=0) | |
| def test_scalar_metric_and_result_serialization(self): | |
| import json | |
| from pathlib import Path | |
| import tempfile | |
| result = self.probe.causal.path(senders=[(0, 0)]).run( | |
| self.base, donor=self.donor, metric=lambda logits: self.score(logits).mean(), alignment="position") | |
| self.assertEqual(result.tensors["effect"].shape, ()) | |
| with tempfile.TemporaryDirectory() as directory: | |
| result.save(directory) | |
| metadata = json.loads((Path(directory) / "metadata.json").read_text()) | |
| self.assertEqual(metadata["stages"], 4) | |
| def test_per_example_metric_length_must_match_batch(self): | |
| with self.assertRaises(ValueError): | |
| self.probe.causal.path(senders=[(0, 0)]).run( | |
| self.base, donor=self.donor, metric=lambda logits: logits.new_zeros(3), alignment="position") | |
| self.assert_clean() | |
| class NativePathPatchingTests(unittest.TestCase): | |
| def setUpClass(cls): | |
| cls.original_threads = torch.get_num_threads() | |
| torch.set_num_threads(1) | |
| def tearDownClass(cls): | |
| torch.set_num_threads(cls.original_threads) | |
| def test_gpt2_packed_qkv_matches_native_hook_oracle(self): | |
| from transformers import GPT2Config, GPT2LMHeadModel | |
| with torch.random.fork_rng(devices=[]): | |
| torch.manual_seed(73) | |
| config = GPT2Config(vocab_size=13, n_embd=8, n_layer=2, n_head=2, | |
| n_positions=8, use_cache=False, attn_pdrop=0, | |
| resid_pdrop=0, embd_pdrop=0) | |
| config._attn_implementation = "eager" | |
| model = GPT2LMHeadModel(config).eval() | |
| probe = Prober(model) | |
| base = {"input_ids": torch.tensor([[1, 2, 3, 4], [4, 3, 2, 1]])} | |
| donor = {"input_ids": torch.tensor([[5, 6, 7, 8], [8, 7, 6, 5]])} | |
| selected = torch.tensor([[False, True, False, True]]).expand(2, -1) | |
| senders = [(0, 0)] | |
| receivers = [(1, 0, "q"), (1, 1, "k"), (1, 0, "v")] | |
| method = probe.causal.path(senders=senders, receivers=receivers, | |
| sender_tokens=selected, receiver_tokens=selected) | |
| metric = TokenMargin(2, 8) | |
| result = method.run(base, donor=donor, metric=metric, alignment="position") | |
| expected = native_gpt2_oracle(model, base, donor, senders, receivers, selected) | |
| for name, logits in zip(("baseline_score", "donor_score", "intervention_score"), expected): | |
| torch.testing.assert_close(result.tensors[name], logits[:, -1, 2] - logits[:, -1, 8]) | |
| self.assertGreater(result.tensors["effect"].abs().max().item(), 1e-7) | |
| identity = method.run(base, donor=base, metric=metric) | |
| torch.testing.assert_close(identity.tensors["effect"], torch.zeros(2), atol=1e-7, rtol=0) | |
| for module in model.modules(): | |
| self.assertEqual(len(module._forward_hooks), 0) | |
| self.assertEqual(len(module._forward_pre_hooks), 0) | |
| def test_llama_sdpa_path_and_physical_grouped_query_kv(self): | |
| from transformers import LlamaConfig, LlamaForCausalLM | |
| with torch.random.fork_rng(devices=[]): | |
| torch.manual_seed(73) | |
| config = LlamaConfig(vocab_size=13, hidden_size=8, intermediate_size=16, | |
| num_hidden_layers=2, num_attention_heads=2, | |
| num_key_value_heads=1, use_cache=False) | |
| config._attn_implementation = "sdpa" | |
| model = LlamaForCausalLM(config).eval() | |
| probe = Prober(model) | |
| self.assertTrue(probe.describe()["methods"]["causal.path"]["available"]) | |
| self.assertFalse(probe.describe()["methods"]["attention.profile"]["available"]) | |
| base = {"input_ids": torch.tensor([[1, 2, 3, 4], [4, 3, 2, 1]]), | |
| "attention_mask": torch.ones(2, 4, dtype=torch.long)} | |
| donor = {**base, "input_ids": torch.tensor([[5, 6, 7, 8], [8, 7, 6, 5]])} | |
| for kind in ("k", "v"): | |
| with self.subTest(kind=kind): | |
| method = probe.causal.path(senders=[(0, 1)], receivers=[(1, 0, kind)], | |
| sender_tokens=[1], receiver_tokens=[1]) | |
| identity = method.run(base, donor=base, metric=TokenMargin(2, 8)) | |
| torch.testing.assert_close(identity.tensors["effect"], torch.zeros(2), atol=1e-7, rtol=0) | |
| actual = method.run(base, donor=donor, metric=TokenMargin(2, 8), alignment="position") | |
| self.assertGreater(actual.tensors["effect"].abs().max().item(), 1e-8) | |
| with self.assertRaises(ValueError): | |
| probe.causal.path(senders=[(0, 1)], receivers=[(1, 1, kind)]) | |
| for module in model.modules(): | |
| self.assertEqual(len(module._forward_hooks), 0) | |
| self.assertEqual(len(module._forward_pre_hooks), 0) | |
| if __name__ == "__main__": | |
| unittest.main() | |