cdpearlman commited on
Commit
3d71d04
·
1 Parent(s): 3c5f2fd

Fixed ablation experiments, drilled down on which pre-loaded ones to run

Browse files
.claude/settings.local.json CHANGED
@@ -7,7 +7,8 @@
7
  "Bash(git push:*)",
8
  "Bash(grep -E '\\\\.\\(py|json|yaml|yml\\)$')",
9
  "Bash(find /c/Users/cdpea/OneDrive/Documents/GradProject -name *.json -type f)",
10
- "Bash(grep:*)"
 
11
  ]
12
  }
13
  }
 
7
  "Bash(git push:*)",
8
  "Bash(grep -E '\\\\.\\(py|json|yaml|yml\\)$')",
9
  "Bash(find /c/Users/cdpea/OneDrive/Documents/GradProject -name *.json -type f)",
10
+ "Bash(grep:*)",
11
+ "Bash(python3:*)"
12
  ]
13
  }
14
  }
.context/data/lessons.md CHANGED
@@ -26,3 +26,9 @@
26
  **Root cause**: `from_pretrained()` without `torch_dtype=torch.float32` loads models in native dtype (float16/bfloat16). On CPU, these dtypes cause numerical instability and dtype mismatches in logit lens. GPT-2 Small happened to be natively float32, masking the bug.
27
  **Fix**: Created centralized `load_model_for_inference()` with forced float32 + weight-tying check
28
  **Rule going forward**: Always specify `torch_dtype=torch.float32` when loading models for CPU inference. Never scatter `from_pretrained` across multiple call sites — use a single loader.
 
 
 
 
 
 
 
26
  **Root cause**: `from_pretrained()` without `torch_dtype=torch.float32` loads models in native dtype (float16/bfloat16). On CPU, these dtypes cause numerical instability and dtype mismatches in logit lens. GPT-2 Small happened to be natively float32, masking the bug.
27
  **Fix**: Created centralized `load_model_for_inference()` with forced float32 + weight-tying check
28
  **Rule going forward**: Always specify `torch_dtype=torch.float32` when loading models for CPU inference. Never scatter `from_pretrained` across multiple call sites — use a single loader.
29
+
30
+ ## 2026-03-21 — Key mismatch between raw JSON and enriched helper return values
31
+ **What happened**: Category buttons all showed "(0)" heads despite data existing
32
+ **Root cause**: `get_active_head_summary()` returns categories with key `'heads'`, but the raw JSON file (`head_categories.json`) uses `'top_heads'`. Code in app.py used the raw key against the enriched object.
33
+ **Fix**: Changed `cat_data.get('top_heads', [])` to `cat_data.get('heads', [])` in app.py
34
+ **Rule going forward**: When consuming data from a helper function, check the helper's return schema — don't assume it mirrors the raw data file's keys.
.context/data/sessions.md CHANGED
@@ -33,3 +33,10 @@
33
  **Work done**: Implemented category-based bulk head selection for the ablation panel. Added `head-categories-store` (dcc.Store), populated it from `update_pipeline_content` using existing `get_active_head_summary()` data. New `render_category_buttons` callback renders one button per category with pattern-matching IDs. Extended `manage_ablation_heads` callback with new Input/State to handle category clicks (append + deduplicate). Added placeholder div in ablation_panel.py. All 113 tests pass.
34
  **Files changed**: app.py, components/ablation_panel.py
35
  **Open threads**: Uncommitted — needs commit and manual verification.
 
 
 
 
 
 
 
 
33
  **Work done**: Implemented category-based bulk head selection for the ablation panel. Added `head-categories-store` (dcc.Store), populated it from `update_pipeline_content` using existing `get_active_head_summary()` data. New `render_category_buttons` callback renders one button per category with pattern-matching IDs. Extended `manage_ablation_heads` callback with new Input/State to handle category clicks (append + deduplicate). Added placeholder div in ablation_panel.py. All 113 tests pass.
34
  **Files changed**: app.py, components/ablation_panel.py
35
  **Open threads**: Uncommitted — needs commit and manual verification.
36
+
37
+ ## 2026-03-21 — Category Button Bug Fix + Showcase Plan Finalization
38
+ **Area**: Ablation UX bug fix, showcase preparation
39
+ **Work done**: Fixed key mismatch bug in category buttons (`'top_heads'` → `'heads'` in app.py:708). The `categories_store` builder was reading from the raw JSON key instead of the enriched return key from `get_active_head_summary()`, causing all buttons to show "(0)". Updated showcase-plan.md with tested experiment results.
40
+ **Commits**: 3c5f2fd (category buttons), uncommitted (bug fix)
41
+ **Testing results**: User confirmed: repetition prompt ablated by induction + duplicate-token heads; chef and bat prompts ablated by previous token heads alone.
42
+ **Open threads**: Uncommitted bug fix. Showcase prep complete.
plans.md CHANGED
@@ -1,3 +1,4 @@
1
  - ~~make it more obvious that this is an experiment (pre-loaded things to ask)~~ done (018a9f2)
 
2
  - make sure chatbot explains simpler
3
  - glossary button move
 
1
  - ~~make it more obvious that this is an experiment (pre-loaded things to ask)~~ done (018a9f2)
2
+ - ~~category buttons showing (0) heads~~ fixed (key mismatch: `top_heads` → `heads` in app.py:708)
3
  - make sure chatbot explains simpler
4
  - glossary button move
showcase-plan.md CHANGED
@@ -90,15 +90,9 @@ Use **5-8 generated tokens** — fast enough to keep the demo snappy, long enoug
90
 
91
  GPT-2's induction heads cluster heavily in Layer 5. This is actually a bonus for the demo — you can say *"all the pattern-matching lives in one layer"* and ablate multiple heads in the same layer with a few clicks.
92
 
93
- **Testing protocol:**
94
- - [ ] Run normal inference → confirm "mat" is top prediction
95
- - [ ] Note the attention pattern in BertViz (lines from second occurrence back to first)
96
- - [ ] Ablate L5-H1 alone → does the prediction change?
97
- - [ ] Ablate L5-H1 + L5-H5 together → stronger effect?
98
- - [ ] Ablate L5-H0 + L5-H1 + L5-H5 (triple knockout in one layer) → maximum disruption
99
- - [ ] Also try L6-H9 (score 0.30) and L7-H10 (score 0.28) if Layer 5 ablation alone isn't enough
100
- - [ ] Find the **minimal ablation** that visibly changes the output (ideal = one click, big change)
101
- - [ ] Record before/after outputs
102
 
103
  **Narration by age:**
104
  - Elementary: *"We broke its memory! It forgot what it already read!"*
@@ -123,12 +117,9 @@ GPT-2's induction heads cluster heavily in Layer 5. This is actually a bonus for
123
 
124
  L4-H11 at 0.97 is remarkable — it's almost entirely dedicated to "look at the word right before me." This makes it a great single-click ablation target.
125
 
126
- **Testing protocol:**
127
- - [ ] Run normal inference → note coherent continuation
128
- - [ ] Ablate L4-H11 alone does grammar degrade? (high likelihood given 0.97 score)
129
- - [ ] Ablate L4-H11 + L2-H2 → more degradation?
130
- - [ ] Generate ~8 tokens — does output become nonsensical?
131
- - [ ] **If effect is subtle, deprioritize** — move Experiment 3 to backup
132
 
133
  **Narration:** *"This part of the AI's brain reads one word at a time, left to right — like you do. This one head spends 97% of its effort just looking at the previous word. What happens if we turn it off?"*
134
 
@@ -149,11 +140,9 @@ This layers on top of Experiment 1 for engaged groups who want more.
149
 
150
  Note: GPT-2's duplicate-token heads are in early layers (0-1) while induction heads are in Layer 5. This makes a nice story: *"The early layers spot the repetition, the middle layers use it to predict what comes next."*
151
 
152
- **Testing protocol:**
153
- - [ ] With Experiment 1's prompt loaded, ablate duplicate heads alone → compare impact to induction ablation
154
- - [ ] Ablate BOTH induction (L5-H1) + duplicate (L0-H1) heads together
155
- - [ ] Try full combo: all induction + all duplicate heads → maximum "brain damage"
156
- - [ ] Record the most dramatic combo
157
 
158
  **Narration:** *"First we turned off pattern matching. Now let's ALSO turn off the part that notices repeated words... watch what happens."*
159
 
@@ -169,51 +158,33 @@ Note: GPT-2's duplicate-token heads are in early layers (0-1) while induction he
169
  - Great for the "Reveal" step (step 3 in demo loop) — pure attention visualization wow
170
  - Less about ablation, more about "look what the AI notices"
171
 
172
- **What to look for:**
173
- - [ ] Run inference → prediction should be flying-context (field, trees, fence, etc.)
174
- - [ ] In BertViz, find which heads strongly connect "bat" → "flew"
175
- - [ ] *"The AI figured out this is a flying bat, not a baseball bat — look at how it connects 'bat' to 'flew'."*
176
-
177
- **Optional ablation:** If you find a head that strongly links "bat" → "flew", try ablating it — does the prediction shift to baseball context? Would be extraordinary if it works, but may not. Test it.
178
 
179
- ---
180
-
181
- #### Pre-Testing Workflow
182
 
183
- **Round 1: Find your champion** (30 min)
184
- 1. Load GPT-2
185
- 2. Run Experiment 1 test all ablation combos, record outputs
186
- 3. Run Experiment 2 — test ablation combos, record outputs (L4-H11 at 0.97 is especially promising)
187
- 4. Run Experiment 4 — note attention patterns
188
- 5. Pick primary demo based on most dramatic, reliable difference
189
 
190
- **Round 2: Lock in details** (15 min)
191
- 6. For champion demo: find the **single-head ablation** that produces the biggest change
192
- 7. Note exact before/after outputs
193
- 8. Practice narration 3x
194
- 9. If single-head isn't dramatic enough, prepare a 2-head combo
195
-
196
- **Round 3: Backup ready** (10 min)
197
- 10. Prepare second demo (different prompt, different head category)
198
- 11. Memorize which heads to click for both demos
199
 
200
- #### Quick Reference Card (fill in after testing, bring to showcase)
201
 
202
- | Demo | Prompt | Normal Output | Ablate | Ablated Output |
203
- |------|--------|---------------|--------|----------------|
204
- | Primary | | | L?-H? | |
205
- | Backup | | | L?-H? | |
 
206
 
207
  ---
208
 
209
  ### Pre-Showcase Prep Checklist
210
 
211
- - [ ] Identify 2-3 pre-loaded sentences that produce visually striking attention patterns (you said you have these)
 
212
  - [ ] Practice the 2-minute loop 3-5 times until it's smooth and natural
213
  - [ ] Test the monitor setup — make sure text is readable from 4-5 feet away (increase font/zoom if needed)
214
- - [ ] Verify the ablation demo works reliably with your chosen sentences (don't want a dud in front of students)
215
  - [ ] Print QR code large enough to scan from arm's length
216
- - [ ] Have a backup sentence ready in case a student's suggestion produces boring attention patterns
217
  - [ ] Check that dark mode / light mode looks good on the external monitor under showcase lighting
218
 
219
  ---
 
90
 
91
  GPT-2's induction heads cluster heavily in Layer 5. This is actually a bonus for the demo — you can say *"all the pattern-matching lives in one layer"* and ablate multiple heads in the same layer with a few clicks.
92
 
93
+ **How to ablate:** Use the "Induction" and "Duplicate Token" category buttons to select all heads at once.
94
+
95
+ **Confirmed result:** Ablating induction + duplicate-token heads together successfully breaks pattern completion.
 
 
 
 
 
 
96
 
97
  **Narration by age:**
98
  - Elementary: *"We broke its memory! It forgot what it already read!"*
 
117
 
118
  L4-H11 at 0.97 is remarkable — it's almost entirely dedicated to "look at the word right before me." This makes it a great single-click ablation target.
119
 
120
+ **How to ablate:** Use the "Previous Token" category button to select all heads at once.
121
+
122
+ **Confirmed result:** Ablating previous-token heads successfully breaks grammar/coherence.
 
 
 
123
 
124
  **Narration:** *"This part of the AI's brain reads one word at a time, left to right — like you do. This one head spends 97% of its effort just looking at the previous word. What happens if we turn it off?"*
125
 
 
140
 
141
  Note: GPT-2's duplicate-token heads are in early layers (0-1) while induction heads are in Layer 5. This makes a nice story: *"The early layers spot the repetition, the middle layers use it to predict what comes next."*
142
 
143
+ **How to ablate:** Use both the "Induction" and "Duplicate Token" category buttons.
144
+
145
+ **Confirmed result:** Combined induction + duplicate-token ablation produces maximum disruption the model completely loses pattern completion ability.
 
 
146
 
147
  **Narration:** *"First we turned off pattern matching. Now let's ALSO turn off the part that notices repeated words... watch what happens."*
148
 
 
158
  - Great for the "Reveal" step (step 3 in demo loop) — pure attention visualization wow
159
  - Less about ablation, more about "look what the AI notices"
160
 
161
+ **How to ablate:** Use the "Previous Token" category button.
 
 
 
 
 
162
 
163
+ **Confirmed result:** Ablating previous-token heads successfully disrupts this prompt.
 
 
164
 
165
+ **Talking points:**
166
+ - *"The AI figured out this is a flying bat, not a baseball bat — look at how it connects 'bat' to 'flew'."*
167
+ - Show attention visualization first (the "Reveal" step), then ablate for engaged groups
 
 
 
168
 
169
+ ---
 
 
 
 
 
 
 
 
170
 
171
+ #### Quick Reference Card
172
 
173
+ | Demo | Prompt | Ablate (Category Buttons) | Effect |
174
+ |------|--------|---------------------------|--------|
175
+ | Primary | `The cat sat on the mat. The cat sat on the` | Induction + Duplicate Token | Breaks pattern completion |
176
+ | Backup 1 | `The chef gave the waiter a generous tip because` | Previous Token | Breaks grammar/coherence |
177
+ | Backup 2 | `The bat flew over the` | Previous Token | Disrupts ambiguity resolution |
178
 
179
  ---
180
 
181
  ### Pre-Showcase Prep Checklist
182
 
183
+ - [x] Identify pre-loaded sentences that produce visually striking attention patterns (3 confirmed see Quick Reference Card)
184
+ - [x] Verify ablation demos work reliably with chosen sentences (all 3 tested and confirmed)
185
  - [ ] Practice the 2-minute loop 3-5 times until it's smooth and natural
186
  - [ ] Test the monitor setup — make sure text is readable from 4-5 feet away (increase font/zoom if needed)
 
187
  - [ ] Print QR code large enough to scan from arm's length
 
188
  - [ ] Check that dark mode / light mode looks good on the external monitor under showcase lighting
189
 
190
  ---
tests/reproduce_ablation.py CHANGED
@@ -8,7 +8,7 @@ from transformers import AutoModelForCausalLM, AutoTokenizer
8
  # Add project root to path
9
  sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
10
 
11
- from utils.model_patterns import execute_forward_pass, execute_forward_pass_with_head_ablation, load_model_and_get_patterns
12
 
13
  def test_ablation_changes_output():
14
  """
@@ -55,32 +55,30 @@ def test_ablation_changes_output():
55
  baseline_top_prob = baseline_result['actual_output']['probability']
56
  print(f"Baseline Output: '{baseline_top_token}' ({baseline_top_prob:.4f})")
57
 
58
- # 2. Ablated Run (Layer 0, Head 0)
59
  print("Running ablation (L0H0)...")
60
- ablation_result = execute_forward_pass_with_head_ablation(
61
  model, tokenizer, prompt, config,
62
- ablate_layer_num=0,
63
- ablate_head_indices=[0]
64
  )
65
  ablated_top_token = ablation_result['actual_output']['token']
66
  ablated_top_prob = ablation_result['actual_output']['probability']
67
  print(f"Ablated Output: '{ablated_top_token}' ({ablated_top_prob:.4f})")
68
-
69
  # 3. Assertions
70
  # We expect the probability to change, even if the token doesn't (depending on head importance)
71
  # Ideally, exact logit match should be false.
72
-
73
  # Check if probabilities are different (using a small epsilon)
74
  prob_diff = abs(baseline_top_prob - ablated_top_prob)
75
  print(f"Probability Difference: {prob_diff}")
76
-
77
- # We assert that there IS a difference.
78
  # Note: If L0H0 is completely useless, this might fail. But usually it does something.
79
  assert prob_diff > 1e-6, "Ablation of L0H0 did not change the top token probability at all!"
80
 
81
  # Verify that the structure returned contains ablation info
82
- assert ablation_result['ablated_layer'] == 0
83
- assert ablation_result['ablated_heads'] == [0]
84
 
85
  if __name__ == "__main__":
86
  test_ablation_changes_output()
 
8
  # Add project root to path
9
  sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
10
 
11
+ from utils.model_patterns import execute_forward_pass, execute_forward_pass_with_multi_layer_head_ablation, load_model_and_get_patterns
12
 
13
  def test_ablation_changes_output():
14
  """
 
55
  baseline_top_prob = baseline_result['actual_output']['probability']
56
  print(f"Baseline Output: '{baseline_top_token}' ({baseline_top_prob:.4f})")
57
 
58
+ # 2. Ablated Run (Layer 0, Head 0) using multi-layer function with single entry
59
  print("Running ablation (L0H0)...")
60
+ ablation_result = execute_forward_pass_with_multi_layer_head_ablation(
61
  model, tokenizer, prompt, config,
62
+ heads_by_layer={0: [0]}
 
63
  )
64
  ablated_top_token = ablation_result['actual_output']['token']
65
  ablated_top_prob = ablation_result['actual_output']['probability']
66
  print(f"Ablated Output: '{ablated_top_token}' ({ablated_top_prob:.4f})")
67
+
68
  # 3. Assertions
69
  # We expect the probability to change, even if the token doesn't (depending on head importance)
70
  # Ideally, exact logit match should be false.
71
+
72
  # Check if probabilities are different (using a small epsilon)
73
  prob_diff = abs(baseline_top_prob - ablated_top_prob)
74
  print(f"Probability Difference: {prob_diff}")
75
+
76
+ # We assert that there IS a difference.
77
  # Note: If L0H0 is completely useless, this might fail. But usually it does something.
78
  assert prob_diff > 1e-6, "Ablation of L0H0 did not change the top token probability at all!"
79
 
80
  # Verify that the structure returned contains ablation info
81
+ assert ablation_result['ablated_heads_by_layer'] == {0: [0]}
 
82
 
83
  if __name__ == "__main__":
84
  test_ablation_changes_output()
tests/test_ablation_hook_placement.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests for scientifically accurate head ablation via pre-projection hooking.
3
+
4
+ Verifies that ablation hooks are placed on the INPUT to c_proj (pre-projection),
5
+ where per-head dimensions are still separable, rather than on the OUTPUT of the
6
+ attention module (post-projection), where heads are mixed.
7
+ """
8
+
9
+ import sys
10
+ import os
11
+ import torch
12
+ import torch.nn as nn
13
+ import pytest
14
+ from transformers import AutoModelForCausalLM, AutoTokenizer
15
+
16
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
17
+
18
+ from utils.model_patterns import _find_output_proj_submodule
19
+
20
+
21
+ @pytest.fixture(scope="module")
22
+ def gpt2_model_and_tokenizer():
23
+ """Load GPT-2 once for all tests in this module."""
24
+ try:
25
+ model = AutoModelForCausalLM.from_pretrained("gpt2", torch_dtype=torch.float32)
26
+ model.eval()
27
+ tokenizer = AutoTokenizer.from_pretrained("gpt2")
28
+ return model, tokenizer
29
+ except Exception as e:
30
+ pytest.skip(f"Could not load GPT-2: {e}")
31
+
32
+
33
+ class TestFindOutputProjSubmodule:
34
+ def test_find_output_proj_gpt2(self, gpt2_model_and_tokenizer):
35
+ """GPT-2 attention modules should have c_proj as the output projection."""
36
+ model, _ = gpt2_model_and_tokenizer
37
+ attn_module = model.transformer.h[0].attn
38
+ name, submodule = _find_output_proj_submodule(attn_module)
39
+ assert name == "c_proj"
40
+ assert submodule is attn_module.c_proj
41
+
42
+ def test_find_output_proj_unknown_raises(self):
43
+ """A plain nn.Module with no recognized projection children should raise ValueError."""
44
+ plain_module = nn.Module()
45
+ plain_module.add_module("some_layer", nn.Linear(10, 10))
46
+ with pytest.raises(ValueError, match="No output projection found"):
47
+ _find_output_proj_submodule(plain_module)
48
+
49
+
50
+ class TestPreHookPlacement:
51
+ def test_pre_hook_zeros_correct_dims(self, gpt2_model_and_tokenizer):
52
+ """Pre-hook on c_proj receives input where per-head dims are separable.
53
+ Zeroing head 0 dims [0:64] should leave [64:768] untouched."""
54
+ model, tokenizer = gpt2_model_and_tokenizer
55
+ captured_input = {}
56
+
57
+ def capture_pre_hook(module, args):
58
+ captured_input['x'] = args[0].clone()
59
+ return None # Don't modify
60
+
61
+ hook = model.transformer.h[0].attn.c_proj.register_forward_pre_hook(capture_pre_hook)
62
+ try:
63
+ inputs = tokenizer("The cat sat on the mat", return_tensors="pt")
64
+ with torch.no_grad():
65
+ model(**inputs, use_cache=False)
66
+ finally:
67
+ hook.remove()
68
+
69
+ x = captured_input['x']
70
+ # Shape should be [batch, seq, 768]
71
+ assert x.shape[-1] == 768
72
+ # Verify per-head structure: zeroing [0:64] leaves [64:768] intact
73
+ x_modified = x.clone()
74
+ x_modified[:, :, 0:64] = 0.0
75
+ # The rest should be exactly equal
76
+ assert torch.equal(x_modified[:, :, 64:], x[:, :, 64:])
77
+ # The zeroed part should actually be zero
78
+ assert torch.all(x_modified[:, :, 0:64] == 0.0)
79
+
80
+ def test_ablation_changes_output(self, gpt2_model_and_tokenizer):
81
+ """Ablating head 0 at layer 0 via pre-hook on c_proj should change logits."""
82
+ model, tokenizer = gpt2_model_and_tokenizer
83
+ prompt = "The quick brown fox jumps over the"
84
+ inputs = tokenizer(prompt, return_tensors="pt")
85
+
86
+ # Baseline
87
+ with torch.no_grad():
88
+ baseline_logits = model(**inputs, use_cache=False).logits
89
+
90
+ # Ablated: pre-hook zeros head 0 on layer 0's c_proj
91
+ def ablation_pre_hook(module, args):
92
+ x = args[0].clone()
93
+ x[:, :, 0:64] = 0.0
94
+ return (x,)
95
+
96
+ hook = model.transformer.h[0].attn.c_proj.register_forward_pre_hook(ablation_pre_hook)
97
+ try:
98
+ with torch.no_grad():
99
+ ablated_logits = model(**inputs, use_cache=False).logits
100
+ finally:
101
+ hook.remove()
102
+
103
+ # Logits must differ
104
+ assert not torch.allclose(baseline_logits, ablated_logits, atol=1e-6), \
105
+ "Ablation via pre-hook on c_proj did not change logits"
tests/test_head_categories.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Test that head_categories.json is correctly parsed into category stores."""
2
+ import json
3
+ import os
4
+ import pytest
5
+
6
+
7
+ @pytest.fixture
8
+ def head_categories():
9
+ json_path = os.path.join(os.path.dirname(__file__), '..', 'utils', 'head_categories.json')
10
+ with open(json_path, 'r') as f:
11
+ return json.load(f)
12
+
13
+
14
+ def build_categories_store(head_categories_data):
15
+ """Replicate the category store construction from app.py."""
16
+ categories_store = {}
17
+ for cat_key, cat_data in head_categories_data['categories'].items():
18
+ categories_store[cat_key] = {
19
+ 'display_name': cat_data.get('display_name', cat_key),
20
+ 'heads': [{'layer': h['layer'], 'head': h['head']}
21
+ for h in cat_data.get('top_heads', [])]
22
+ }
23
+ return categories_store
24
+
25
+
26
+ class TestHeadCategoriesStore:
27
+ def test_all_categories_have_heads(self, head_categories):
28
+ """Every category in the JSON must produce a non-empty heads list."""
29
+ for model_key, model_data in head_categories.items():
30
+ if 'categories' not in model_data:
31
+ continue
32
+ store = build_categories_store(model_data)
33
+ for cat_key, cat_val in store.items():
34
+ assert len(cat_val['heads']) > 0, (
35
+ f"Category '{cat_key}' in model '{model_key}' has no heads — "
36
+ f"check that the JSON field name matches the parsing code"
37
+ )
38
+
39
+ def test_heads_have_layer_and_head_keys(self, head_categories):
40
+ """Each head entry must have 'layer' and 'head' integer fields."""
41
+ for model_key, model_data in head_categories.items():
42
+ if 'categories' not in model_data:
43
+ continue
44
+ store = build_categories_store(model_data)
45
+ for cat_key, cat_val in store.items():
46
+ for h in cat_val['heads']:
47
+ assert 'layer' in h and 'head' in h, (
48
+ f"Head entry in '{cat_key}' missing layer/head keys"
49
+ )
50
+ assert isinstance(h['layer'], int)
51
+ assert isinstance(h['head'], int)
52
+
53
+ def test_field_name_is_top_heads_not_heads(self, head_categories):
54
+ """Guard against regression: JSON field must be 'top_heads', not 'heads'."""
55
+ for model_key, model_data in head_categories.items():
56
+ if 'categories' not in model_data:
57
+ continue
58
+ for cat_key, cat_data in model_data['categories'].items():
59
+ assert 'top_heads' in cat_data, (
60
+ f"Category '{cat_key}' uses wrong field name — expected 'top_heads'"
61
+ )
62
+ # 'heads' should NOT exist as a field in the JSON
63
+ assert 'heads' not in cat_data, (
64
+ f"Category '{cat_key}' has ambiguous 'heads' field — use 'top_heads' only"
65
+ )
utils/__init__.py CHANGED
@@ -2,7 +2,6 @@ from .model_patterns import (load_model_for_inference, load_model_and_get_patter
2
  execute_forward_pass,
3
  logit_lens_transformation, extract_layer_data,
4
  generate_bertviz_html,
5
- execute_forward_pass_with_head_ablation,
6
  execute_forward_pass_with_multi_layer_head_ablation,
7
  merge_token_probabilities,
8
  compute_global_top5_tokens, compute_per_position_top5,
@@ -20,7 +19,6 @@ __all__ = [
20
  'load_model_for_inference',
21
  'load_model_and_get_patterns',
22
  'execute_forward_pass',
23
- 'execute_forward_pass_with_head_ablation',
24
  'execute_forward_pass_with_multi_layer_head_ablation',
25
  'evaluate_sequence_ablation',
26
  'logit_lens_transformation',
 
2
  execute_forward_pass,
3
  logit_lens_transformation, extract_layer_data,
4
  generate_bertviz_html,
 
5
  execute_forward_pass_with_multi_layer_head_ablation,
6
  merge_token_probabilities,
7
  compute_global_top5_tokens, compute_per_position_top5,
 
19
  'load_model_for_inference',
20
  'load_model_and_get_patterns',
21
  'execute_forward_pass',
 
22
  'execute_forward_pass_with_multi_layer_head_ablation',
23
  'evaluate_sequence_ablation',
24
  'logit_lens_transformation',
utils/beam_search.py CHANGED
@@ -7,40 +7,37 @@ import torch.nn.functional as F
7
  from typing import List, Dict, Any, Optional
8
  import re
9
 
10
- def _make_head_ablation_hook(head_indices: List[int], num_heads: int):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  """
12
- Create a hook that zeros out specific attention heads.
 
13
  """
14
- def hook(module, input, output):
15
- # output is typically (attn_output, past_key_values, attn_weights) or just attn_output
16
- if isinstance(output, tuple):
17
- hidden_states = output[0]
18
- else:
19
- hidden_states = output
20
-
21
- # Check if we need to convert from list/tuple (some models might behave oddly)
22
- if not isinstance(hidden_states, torch.Tensor):
23
- return output # Safety skip
24
-
25
- batch_size, seq_len, hidden_dim = hidden_states.shape
26
- head_dim = hidden_dim // num_heads
27
-
28
- # Reshape to [batch, seq_len, num_heads, head_dim]
29
- # We need to clone to modify safetly
30
- hidden_states_reshaped = hidden_states.view(batch_size, seq_len, num_heads, head_dim).clone()
31
-
32
- # Zero out specified heads
33
  for head_idx in head_indices:
34
  if 0 <= head_idx < num_heads:
35
- hidden_states_reshaped[:, :, head_idx, :] = 0.0
36
-
37
- # Reshape back
38
- ablated_hidden = hidden_states_reshaped.view(batch_size, seq_len, hidden_dim)
39
-
40
- if isinstance(output, tuple):
41
- return (ablated_hidden,) + output[1:]
42
- return ablated_hidden
43
- return hook
44
 
45
  def _apply_ablation_hooks(model, ablation_config: Dict[int, List[int]]) -> List[Any]:
46
  """
@@ -89,12 +86,15 @@ def _apply_ablation_hooks(model, ablation_config: Dict[int, List[int]]) -> List[
89
  # Sort by length
90
  candidates.sort(key=lambda x: len(x[0]))
91
  target_name, target_module = candidates[0]
92
-
93
- # Register hook
94
- # print(f"Applying ablation to {target_name} for Layer {layer_num}, Heads {head_indices}")
95
- hooks.append(target_module.register_forward_hook(
96
- _make_head_ablation_hook(head_indices, num_heads)
97
- ))
 
 
 
98
  else:
99
  print(f"Warning: Could not find attention module for layer {layer_num} in beam search.")
100
 
 
7
  from typing import List, Dict, Any, Optional
8
  import re
9
 
10
+ _OUTPUT_PROJ_NAMES = ['c_proj', 'o_proj', 'out_proj', 'dense']
11
+
12
+
13
+ def _find_output_proj_submodule(attn_module, attn_module_name: str = ""):
14
+ """Find output projection submodule within an attention module.
15
+ Returns (name, submodule). Raises ValueError if not found."""
16
+ children = dict(attn_module.named_children())
17
+ for proj_name in _OUTPUT_PROJ_NAMES:
18
+ if proj_name in children:
19
+ return proj_name, children[proj_name]
20
+ raise ValueError(
21
+ f"No output projection found in {attn_module_name or type(attn_module).__name__}. "
22
+ f"Children: {list(children.keys())}. Expected one of: {_OUTPUT_PROJ_NAMES}"
23
+ )
24
+
25
+
26
+ def _make_head_ablation_pre_hook(head_indices: List[int], num_heads: int):
27
  """
28
+ Create a pre-hook on the output projection that zeros out specific attention
29
+ heads BEFORE projection mixing, where per-head dims are still separable.
30
  """
31
+ def pre_hook(module, args):
32
+ x = args[0].clone()
33
+ head_dim = x.shape[-1] // num_heads
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  for head_idx in head_indices:
35
  if 0 <= head_idx < num_heads:
36
+ start = head_idx * head_dim
37
+ end = (head_idx + 1) * head_dim
38
+ x[:, :, start:end] = 0.0
39
+ return (x,)
40
+ return pre_hook
 
 
 
 
41
 
42
  def _apply_ablation_hooks(model, ablation_config: Dict[int, List[int]]) -> List[Any]:
43
  """
 
86
  # Sort by length
87
  candidates.sort(key=lambda x: len(x[0]))
88
  target_name, target_module = candidates[0]
89
+
90
+ # Register pre-hook on output projection (before heads are mixed)
91
+ try:
92
+ _, proj_mod = _find_output_proj_submodule(target_module, target_name)
93
+ hooks.append(proj_mod.register_forward_pre_hook(
94
+ _make_head_ablation_pre_hook(head_indices, num_heads)
95
+ ))
96
+ except ValueError as e:
97
+ print(f"Warning: {e}")
98
  else:
99
  print(f"Warning: Could not find attention module for layer {layer_num} in beam search.")
100
 
utils/model_patterns.py CHANGED
@@ -7,6 +7,22 @@ from typing import Dict, List, Tuple, Any, Optional
7
  from transformers import AutoModelForCausalLM, AutoTokenizer
8
 
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  def load_model_for_inference(model_name: str):
11
  """Load model with float32 dtype for CPU stability and verify weight tying."""
12
  model = AutoModelForCausalLM.from_pretrained(
@@ -405,160 +421,6 @@ def execute_forward_pass(model, tokenizer, prompt: str, config: Dict[str, Any],
405
  return result
406
 
407
 
408
- def execute_forward_pass_with_head_ablation(model, tokenizer, prompt: str, config: Dict[str, Any],
409
- ablate_layer_num: int, ablate_head_indices: List[int]) -> Dict[str, Any]:
410
- """
411
- Execute forward pass with specific attention heads zeroed out.
412
-
413
- Args:
414
- model: Loaded transformer model
415
- tokenizer: Loaded tokenizer
416
- prompt: Input text prompt
417
- config: Dict with module lists like {"attention_modules": [...], "block_modules": [...], ...}
418
- ablate_layer_num: Layer number containing heads to ablate
419
- ablate_head_indices: List of head indices to zero out (e.g., [0, 2, 5])
420
-
421
- Returns:
422
- JSON-serializable dict with captured activations (with ablated heads)
423
- """
424
- print(f"Executing forward pass with head ablation: Layer {ablate_layer_num}, Heads {ablate_head_indices}")
425
-
426
- # Extract module lists from config
427
- attention_modules = config.get("attention_modules", [])
428
- block_modules = config.get("block_modules", [])
429
- norm_parameters = config.get("norm_parameters", [])
430
- logit_lens_parameter = config.get("logit_lens_parameter")
431
-
432
- all_modules = attention_modules + block_modules
433
- if not all_modules:
434
- return {"error": "No modules specified"}
435
-
436
- # Find the target attention module for the layer to ablate
437
- target_attention_module = None
438
- for mod_name in attention_modules:
439
- layer_match = re.search(r'\.(\d+)(?:\.|$)', mod_name)
440
- if layer_match and int(layer_match.group(1)) == ablate_layer_num:
441
- target_attention_module = mod_name
442
- break
443
-
444
- if not target_attention_module:
445
- return {"error": f"Could not find attention module for layer {ablate_layer_num}"}
446
-
447
- # Prepare inputs
448
- inputs = tokenizer(prompt, return_tensors="pt")
449
-
450
- # Register hooks directly on the original model (avoids PyVene module renaming issues)
451
- captured = {}
452
- name_to_module = dict(model.named_modules())
453
-
454
- def make_hook(mod_name: str):
455
- return lambda module, inputs, output: captured.update({mod_name: {"output": safe_to_serializable(output)}})
456
-
457
- # Create head ablation hook that both ablates and captures
458
- def head_ablation_hook(module, input, output):
459
- """Zero out specific attention heads in the output AND capture it."""
460
- ablated_output = output # Default to original output
461
-
462
- if isinstance(output, tuple):
463
- # Attention modules typically return (hidden_states, attention_weights, ...)
464
- hidden_states = output[0] # [batch, seq_len, hidden_dim]
465
-
466
- # Convert to tensor if needed
467
- if not isinstance(hidden_states, torch.Tensor):
468
- hidden_states = torch.tensor(hidden_states)
469
-
470
- batch_size, seq_len, hidden_dim = hidden_states.shape
471
-
472
- # Determine head dimension
473
- # Assuming hidden_dim = num_heads * head_dim
474
- # We need to get num_heads from the model config
475
- num_heads = model.config.num_attention_heads
476
- head_dim = hidden_dim // num_heads
477
-
478
- # Reshape to [batch, seq_len, num_heads, head_dim]
479
- hidden_states_reshaped = hidden_states.view(batch_size, seq_len, num_heads, head_dim)
480
-
481
- # Zero out specified heads
482
- for head_idx in ablate_head_indices:
483
- if 0 <= head_idx < num_heads:
484
- hidden_states_reshaped[:, :, head_idx, :] = 0.0
485
-
486
- # Reshape back to [batch, seq_len, hidden_dim]
487
- ablated_hidden = hidden_states_reshaped.view(batch_size, seq_len, hidden_dim)
488
-
489
- # Reconstruct output tuple
490
- if len(output) > 1:
491
- ablated_output = (ablated_hidden,) + output[1:]
492
- else:
493
- ablated_output = (ablated_hidden,)
494
-
495
- # Capture the ablated output (CRITICAL: this was missing!)
496
- captured.update({target_attention_module: {"output": safe_to_serializable(ablated_output)}})
497
-
498
- return ablated_output
499
-
500
- # Register hooks
501
- hooks = []
502
- for mod_name in all_modules:
503
- if mod_name in name_to_module:
504
- if mod_name == target_attention_module:
505
- # Apply head ablation hook
506
- hooks.append(name_to_module[mod_name].register_forward_hook(head_ablation_hook))
507
- else:
508
- # Regular capture hook
509
- hooks.append(name_to_module[mod_name].register_forward_hook(make_hook(mod_name)))
510
-
511
- # Execute forward pass
512
- with torch.no_grad():
513
- model_output = model(**inputs, use_cache=False)
514
-
515
- # Remove hooks
516
- for hook in hooks:
517
- hook.remove()
518
-
519
- # Separate outputs by type
520
- attention_outputs = {}
521
- block_outputs = {}
522
-
523
- for mod_name, output in captured.items():
524
- if 'attn' in mod_name or 'attention' in mod_name:
525
- attention_outputs[mod_name] = output
526
- else:
527
- block_outputs[mod_name] = output
528
-
529
- # Capture normalization parameters
530
- all_params = dict(model.named_parameters())
531
- norm_data = [safe_to_serializable(all_params[p]) for p in norm_parameters if p in all_params]
532
-
533
- # Extract predicted token from model output
534
- actual_output = None
535
- global_top5_tokens = []
536
- try:
537
- output_token, output_prob = get_actual_model_output(model_output, tokenizer)
538
- actual_output = {"token": output_token, "probability": output_prob}
539
- global_top5_tokens = compute_global_top5_tokens(model_output, tokenizer, top_k=5)
540
- except Exception as e:
541
- print(f"Warning: Could not extract model output: {e}")
542
-
543
- # Build output dictionary
544
- result = {
545
- "model": getattr(model.config, "name_or_path", "unknown"),
546
- "prompt": prompt,
547
- "input_ids": safe_to_serializable(inputs["input_ids"]),
548
- "attention_modules": list(attention_outputs.keys()),
549
- "attention_outputs": attention_outputs,
550
- "block_modules": list(block_outputs.keys()),
551
- "block_outputs": block_outputs,
552
- "norm_parameters": norm_parameters,
553
- "norm_data": norm_data,
554
- "actual_output": actual_output,
555
- "global_top5_tokens": global_top5_tokens,
556
- "ablated_layer": ablate_layer_num,
557
- "ablated_heads": ablate_head_indices
558
- }
559
-
560
- return result
561
-
562
 
563
  def execute_forward_pass_with_multi_layer_head_ablation(model, tokenizer, prompt: str, config: Dict[str, Any],
564
  heads_by_layer: Dict[int, List[int]], original_prompt: Optional[str] = None) -> Dict[str, Any]:
@@ -622,75 +484,34 @@ def execute_forward_pass_with_multi_layer_head_ablation(model, tokenizer, prompt
622
  def make_hook(mod_name: str):
623
  return lambda module, inputs, output: captured.update({mod_name: {"output": safe_to_serializable(output)}})
624
 
625
- # Create parameterized head ablation hook factory
626
- def make_head_ablation_hook(target_mod_name: str, ablate_head_indices: List[int]):
627
- """Create a hook that zeros out specific attention heads and captures the output."""
628
- def head_ablation_hook(module, input, output):
629
- ablated_output = output # Default to original output
630
-
631
- if isinstance(output, tuple):
632
- # Attention modules typically return (hidden_states, attention_weights, ...)
633
- hidden_states = output[0] # [batch, seq_len, hidden_dim]
634
-
635
- # Convert to tensor if needed
636
- if not isinstance(hidden_states, torch.Tensor):
637
- hidden_states = torch.tensor(hidden_states)
638
-
639
- batch_size, seq_len, hidden_dim = hidden_states.shape
640
-
641
- # Determine head dimension
642
- num_heads = model.config.num_attention_heads
643
- head_dim = hidden_dim // num_heads
644
-
645
- # Reshape to [batch, seq_len, num_heads, head_dim]
646
- hidden_states_reshaped = hidden_states.view(batch_size, seq_len, num_heads, head_dim)
647
-
648
- # Zero out specified heads
649
- for head_idx in ablate_head_indices:
650
- if 0 <= head_idx < num_heads:
651
- hidden_states_reshaped[:, :, head_idx, :] = 0.0
652
-
653
- # Reshape back to [batch, seq_len, hidden_dim]
654
- ablated_hidden = hidden_states_reshaped.view(batch_size, seq_len, hidden_dim)
655
-
656
- # Reconstruct output tuple
657
- if len(output) > 1:
658
- # Check for attention weights (usually index 2 if output_attentions=True)
659
- if len(output) > 2:
660
- attn_weights = output[2] # [batch, heads, seq, seq]
661
- if isinstance(attn_weights, torch.Tensor):
662
- # Zero out specified heads in attention weights too
663
- # Clone to avoid in-place modification errors if any
664
- attn_weights_mod = attn_weights.clone()
665
- for head_idx in ablate_head_indices:
666
- if 0 <= head_idx < num_heads:
667
- attn_weights_mod[:, head_idx, :, :] = 0.0
668
-
669
- # Reconstruct tuple with modified weights
670
- ablated_output = (ablated_hidden, output[1], attn_weights_mod) + output[3:]
671
- else:
672
- ablated_output = (ablated_hidden,) + output[1:]
673
- else:
674
- ablated_output = (ablated_hidden,) + output[1:]
675
- else:
676
- ablated_output = (ablated_hidden,)
677
-
678
- # Capture the ablated output
679
- captured.update({target_mod_name: {"output": safe_to_serializable(ablated_output)}})
680
-
681
- return ablated_output
682
- return head_ablation_hook
683
-
684
  # Register hooks
685
  hooks = []
686
  for mod_name in all_modules:
687
  if mod_name in name_to_module:
688
  if mod_name in target_modules_to_heads:
689
- # Apply head ablation hook for this module
690
- head_indices = target_modules_to_heads[mod_name]
691
- hooks.append(name_to_module[mod_name].register_forward_hook(
692
- make_head_ablation_hook(mod_name, head_indices)
 
693
  ))
 
 
694
  else:
695
  # Regular capture hook
696
  hooks.append(name_to_module[mod_name].register_forward_hook(make_hook(mod_name)))
@@ -826,42 +647,19 @@ def evaluate_sequence_ablation(model, tokenizer, sequence_text: str, config: Dic
826
  # Let's manually register hooks here for simplicity and control
827
  hooks = []
828
 
829
- def head_ablation_hook_factory(layer_idx, head_indices):
830
- def hook(module, input, output):
831
- # output is (hidden_states, ...) or hidden_states
832
- if isinstance(output, tuple):
833
- hidden_states = output[0]
834
- else:
835
- hidden_states = output
836
-
837
- # Assume hidden_states is [batch, seq, hidden]
838
- # Reshape, zero out heads, Reshape back
839
- if not isinstance(hidden_states, torch.Tensor):
840
- if isinstance(hidden_states, list): hidden_states = torch.tensor(hidden_states)
841
-
842
- # Move to device if needed? They should be on device.
843
-
844
  num_heads = model.config.num_attention_heads
845
- head_dim = hidden_states.shape[-1] // num_heads
846
-
847
- # view: [batch, seq, heads, dim]
848
- new_shape = hidden_states.shape[:-1] + (num_heads, head_dim)
849
- reshaped = hidden_states.view(new_shape)
850
-
851
- # Create mask or just zero out
852
- # We can't modify in place securely with autograd usually, but here no_grad is on.
853
- # Clone to be safe
854
- reshaped = reshaped.clone()
855
-
856
- for h_idx in head_indices:
857
- reshaped[..., h_idx, :] = 0
858
-
859
- ablated_hidden = reshaped.view(hidden_states.shape)
860
-
861
- if isinstance(output, tuple):
862
- return (ablated_hidden,) + output[1:]
863
- return ablated_hidden
864
- return hook
865
 
866
  # Hook for Layer Ablation (Identity/Skip or Zero)
867
  # We'll use Identity (Skip Layer) as a simpler approximation of "removing logic"
@@ -885,16 +683,22 @@ def evaluate_sequence_ablation(model, tokenizer, sequence_text: str, config: Dic
885
 
886
  # Simple heuristic: find 'layers.X.self_attn' or 'h.X.attn'
887
  target_module = None
 
888
  for name, mod in model.named_modules():
889
  # Check for standard patterns
890
  # layer_num is int
891
  if f"layers.{layer_num}.self_attn" in name or f"h.{layer_num}.attn" in name or f"blocks.{layer_num}.attn" in name:
892
  if "k_proj" not in name and "v_proj" not in name and "q_proj" not in name: # avoid submodules
893
  target_module = mod
 
894
  break
895
-
896
  if target_module:
897
- hooks.append(target_module.register_forward_hook(head_ablation_hook_factory(layer_num, head_indices)))
 
 
 
 
898
  else:
899
  print(f"Warning: Could not find attention module for layer {layer_num}")
900
 
 
7
  from transformers import AutoModelForCausalLM, AutoTokenizer
8
 
9
 
10
+ _OUTPUT_PROJ_NAMES = ['c_proj', 'o_proj', 'out_proj', 'dense']
11
+
12
+
13
+ def _find_output_proj_submodule(attn_module, attn_module_name: str = ""):
14
+ """Find output projection submodule within an attention module.
15
+ Returns (name, submodule). Raises ValueError if not found."""
16
+ children = dict(attn_module.named_children())
17
+ for proj_name in _OUTPUT_PROJ_NAMES:
18
+ if proj_name in children:
19
+ return proj_name, children[proj_name]
20
+ raise ValueError(
21
+ f"No output projection found in {attn_module_name or type(attn_module).__name__}. "
22
+ f"Children: {list(children.keys())}. Expected one of: {_OUTPUT_PROJ_NAMES}"
23
+ )
24
+
25
+
26
  def load_model_for_inference(model_name: str):
27
  """Load model with float32 dtype for CPU stability and verify weight tying."""
28
  model = AutoModelForCausalLM.from_pretrained(
 
421
  return result
422
 
423
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
  def execute_forward_pass_with_multi_layer_head_ablation(model, tokenizer, prompt: str, config: Dict[str, Any],
426
  heads_by_layer: Dict[int, List[int]], original_prompt: Optional[str] = None) -> Dict[str, Any]:
 
484
  def make_hook(mod_name: str):
485
  return lambda module, inputs, output: captured.update({mod_name: {"output": safe_to_serializable(output)}})
486
 
487
+ # Create pre-hook factory for ablation on output projection input
488
+ def make_head_ablation_pre_hook(ablate_head_indices: List[int]):
489
+ """Pre-hook on output projection: zeros head slices BEFORE projection mixing."""
490
+ def pre_hook(module, args):
491
+ x = args[0].clone()
492
+ num_heads = model.config.num_attention_heads
493
+ head_dim = x.shape[-1] // num_heads
494
+ for head_idx in ablate_head_indices:
495
+ if 0 <= head_idx < num_heads:
496
+ start = head_idx * head_dim
497
+ end = (head_idx + 1) * head_dim
498
+ x[:, :, start:end] = 0.0
499
+ return (x,)
500
+ return pre_hook
501
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
  # Register hooks
503
  hooks = []
504
  for mod_name in all_modules:
505
  if mod_name in name_to_module:
506
  if mod_name in target_modules_to_heads:
507
+ # Ablation pre-hook on output projection (before heads are mixed)
508
+ attn_mod = name_to_module[mod_name]
509
+ _, proj_mod = _find_output_proj_submodule(attn_mod, mod_name)
510
+ hooks.append(proj_mod.register_forward_pre_hook(
511
+ make_head_ablation_pre_hook(target_modules_to_heads[mod_name])
512
  ))
513
+ # Capture hook on attn module (captures post-ablation output naturally)
514
+ hooks.append(attn_mod.register_forward_hook(make_hook(mod_name)))
515
  else:
516
  # Regular capture hook
517
  hooks.append(name_to_module[mod_name].register_forward_hook(make_hook(mod_name)))
 
647
  # Let's manually register hooks here for simplicity and control
648
  hooks = []
649
 
650
+ def head_ablation_pre_hook_factory(head_indices):
651
+ """Pre-hook on output projection: zeros head slices BEFORE projection mixing."""
652
+ def pre_hook(module, args):
653
+ x = args[0].clone()
 
 
 
 
 
 
 
 
 
 
 
654
  num_heads = model.config.num_attention_heads
655
+ head_dim = x.shape[-1] // num_heads
656
+ for head_idx in head_indices:
657
+ if 0 <= head_idx < num_heads:
658
+ start = head_idx * head_dim
659
+ end = (head_idx + 1) * head_dim
660
+ x[:, :, start:end] = 0.0
661
+ return (x,)
662
+ return pre_hook
 
 
 
 
 
 
 
 
 
 
 
 
663
 
664
  # Hook for Layer Ablation (Identity/Skip or Zero)
665
  # We'll use Identity (Skip Layer) as a simpler approximation of "removing logic"
 
683
 
684
  # Simple heuristic: find 'layers.X.self_attn' or 'h.X.attn'
685
  target_module = None
686
+ target_name = None
687
  for name, mod in model.named_modules():
688
  # Check for standard patterns
689
  # layer_num is int
690
  if f"layers.{layer_num}.self_attn" in name or f"h.{layer_num}.attn" in name or f"blocks.{layer_num}.attn" in name:
691
  if "k_proj" not in name and "v_proj" not in name and "q_proj" not in name: # avoid submodules
692
  target_module = mod
693
+ target_name = name
694
  break
695
+
696
  if target_module:
697
+ try:
698
+ _, proj_mod = _find_output_proj_submodule(target_module, target_name)
699
+ hooks.append(proj_mod.register_forward_pre_hook(head_ablation_pre_hook_factory(head_indices)))
700
+ except ValueError as e:
701
+ print(f"Warning: {e}")
702
  else:
703
  print(f"Warning: Could not find attention module for layer {layer_num}")
704