-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathattention_head_pruning.py
More file actions
343 lines (274 loc) · 11.5 KB
/
Copy pathattention_head_pruning.py
File metadata and controls
343 lines (274 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
"""
Attention Head Pruning Pass
Analyzes importance of each attention head and prunes low-importance heads
to reduce model size and improve inference speed.
Now uses enhanced importance calculation supporting HuggingFace models.
"""
import torch
import torch.nn as nn
from typing import Dict, List, Tuple, Any
import numpy as np
# Import enhanced importance calculator
try:
from .enhanced_head_importance import compute_head_importance_enhanced, AttentionHeadDetector
ENHANCED_AVAILABLE = True
except ImportError:
ENHANCED_AVAILABLE = False
def compute_head_importance_gradient(
model: nn.Module,
calibration_data: torch.Tensor,
loss_fn=None
) -> Dict[str, List[float]]:
"""
Compute importance score for each attention head using gradient magnitude.
Args:
model: PyTorch model with MultiheadAttention layers
calibration_data: Sample data for computing importance
loss_fn: Optional loss function (uses random target if None)
Returns:
Dictionary mapping attention layer names to head importance scores
"""
# Use enhanced method if available
if ENHANCED_AVAILABLE:
importance_dict = compute_head_importance_enhanced(
model, calibration_data, method='gradient', num_samples=10
)
# Convert numpy arrays to lists
return {k: v.tolist() for k, v in importance_dict.items()}
# Fallback to original implementation
model.eval()
head_importance = {}
for name, module in model.named_modules():
if isinstance(module, nn.MultiheadAttention):
num_heads = module.num_heads
importance_scores = [1.0 / num_heads] * num_heads
head_importance[name] = importance_scores
return head_importance
def compute_head_importance_entropy(
model: nn.Module,
calibration_data: torch.Tensor
) -> Dict[str, List[float]]:
"""
Compute importance score based on attention pattern entropy.
Lower entropy = more focused attention = potentially more important.
Args:
model: PyTorch model with MultiheadAttention layers or HuggingFace models
calibration_data: Sample data for computing attention patterns
Returns:
Dictionary mapping attention layer names to head importance scores
"""
# Use enhanced method if available
if ENHANCED_AVAILABLE:
importance_dict = compute_head_importance_enhanced(
model, calibration_data, method='entropy', num_samples=10
)
# Convert numpy arrays to lists
return {k: v.tolist() for k, v in importance_dict.items()}
# Fallback to original implementation
model.eval()
head_importance = {}
# Hook to capture attention weights
attention_weights = {}
def hook_fn(name):
def hook(module, input, output):
# MultiheadAttention returns (attn_output, attn_weights)
if len(output) == 2:
attention_weights[name] = output[1].detach()
return hook
# Register hooks
hooks = []
for name, module in model.named_modules():
if isinstance(module, nn.MultiheadAttention):
hook = module.register_forward_hook(hook_fn(name))
hooks.append(hook)
# Forward pass to capture attention weights
with torch.no_grad():
model(calibration_data)
# Remove hooks
for hook in hooks:
hook.remove()
# Compute entropy for each head
for name, attn_w in attention_weights.items():
# attn_w can be either:
# - (batch, num_heads, seq_len, seq_len) for batch_first=True with return_attention_weights
# - (seq_len, batch, num_heads) averaged weights
# - (batch, seq_len, seq_len) if heads already averaged
if attn_w.dim() == 4:
# Standard format: (batch, num_heads, seq_len, seq_len)
num_heads = attn_w.shape[1]
elif attn_w.dim() == 3:
# Might be (batch, seq_len, seq_len) - already averaged, use uniform importance
# For now, skip this layer or assign uniform importance
# We'll set num_heads based on the model
module = dict(model.named_modules()).get(name)
if module and hasattr(module, 'num_heads'):
num_heads = module.num_heads
# Use uniform importance
importance_scores = [1.0 / num_heads] * num_heads
importance_scores = [s / sum(importance_scores) for s in importance_scores]
head_importance[name] = importance_scores
continue
else:
# Unknown format, skip
continue
importance_scores = []
for head_idx in range(num_heads):
head_attn = attn_w[:, head_idx, :, :]
# Compute entropy: -sum(p * log(p))
# Lower entropy = more focused = more important
epsilon = 1e-10
head_attn_safe = head_attn + epsilon
entropy = -torch.sum(head_attn_safe * torch.log(head_attn_safe), dim=-1)
avg_entropy = entropy.mean().item()
# Convert entropy to importance (inverse relationship)
importance = 1.0 / (avg_entropy + 1.0)
importance_scores.append(importance)
# Normalize importance scores
total = sum(importance_scores)
if total > 0:
importance_scores = [s / total for s in importance_scores]
head_importance[name] = importance_scores
return head_importance
def compute_head_importance_output_variance(
model: nn.Module,
calibration_data: torch.Tensor
) -> Dict[str, List[float]]:
"""
Compute importance based on output contribution variance.
Higher variance in output = more important head.
Args:
model: PyTorch model with MultiheadAttention layers
calibration_data: Sample data for computing output variance
Returns:
Dictionary mapping attention layer names to head importance scores
"""
model.eval()
head_importance = {}
# This is a placeholder implementation
# Full implementation would capture per-head outputs and compute variance
for name, module in model.named_modules():
if isinstance(module, nn.MultiheadAttention):
num_heads = module.num_heads
# For MVP, use uniform importance
importance_scores = [1.0 / num_heads] * num_heads
head_importance[name] = importance_scores
return head_importance
def prune_attention_heads(
mir: Dict[str, Any],
head_importance: Dict[str, List[float]],
pruning_ratio: float = 0.3
) -> Dict[str, Any]:
"""
Prune low-importance attention heads from MIR representation.
Args:
mir: MIR representation
head_importance: Importance scores per head for each attention layer
pruning_ratio: Fraction of heads to prune (0.0 to 1.0)
Returns:
Updated MIR with pruned heads
"""
if not (0.0 <= pruning_ratio < 1.0):
raise ValueError("Pruning ratio must be in [0.0, 1.0)")
pruning_stats = {
"total_heads_before": 0,
"total_heads_after": 0,
"layers_pruned": []
}
for node in mir["graph"]["nodes"]:
if node["op_type"] == "attention_qkv_projection":
origin_name = node["metadata"]["origin_name"]
if origin_name in head_importance:
scores = head_importance[origin_name]
num_heads = node["params"]["num_heads"]
pruning_stats["total_heads_before"] += num_heads
# Determine heads to keep
keep_count = max(1, int(num_heads * (1 - pruning_ratio)))
heads_to_keep = sorted(
range(num_heads),
key=lambda i: scores[i],
reverse=True
)[:keep_count]
pruned_heads = [i for i in range(num_heads) if i not in heads_to_keep]
# Update node parameters
node["params"]["num_heads"] = keep_count
node["params"]["pruned_heads"] = pruned_heads
node["params"]["kept_heads"] = heads_to_keep
# Update compression metadata
if "compression" not in node:
node["compression"] = {}
node["compression"]["head_pruning"] = {
"original_heads": num_heads,
"remaining_heads": keep_count,
"pruning_ratio": (num_heads - keep_count) / num_heads,
"pruned_head_indices": pruned_heads,
"kept_head_indices": heads_to_keep,
"importance_scores": scores
}
pruning_stats["total_heads_after"] += keep_count
pruning_stats["layers_pruned"].append({
"layer": origin_name,
"original_heads": num_heads,
"remaining_heads": keep_count
})
# Add pruning statistics to MIR metadata
if "metadata" not in mir:
mir["metadata"] = {}
mir["metadata"]["head_pruning_stats"] = pruning_stats
return mir
def apply_head_pruning(
mir: Dict[str, Any],
model: nn.Module,
calibration_data: torch.Tensor,
pruning_ratio: float = 0.3,
importance_method: str = "entropy"
) -> Dict[str, Any]:
"""
Complete head pruning pipeline: compute importance and prune.
Args:
mir: MIR representation
model: Original PyTorch model
calibration_data: Data for computing head importance
pruning_ratio: Fraction of heads to prune
importance_method: Method for computing importance
- "gradient": Gradient-based importance
- "entropy": Attention pattern entropy
- "variance": Output variance
Returns:
Updated MIR with pruned heads
"""
# Compute head importance
if importance_method == "gradient":
head_importance = compute_head_importance_gradient(model, calibration_data)
elif importance_method == "entropy":
head_importance = compute_head_importance_entropy(model, calibration_data)
elif importance_method == "variance":
head_importance = compute_head_importance_output_variance(model, calibration_data)
else:
raise ValueError(f"Unknown importance method: {importance_method}")
# Prune heads
mir = prune_attention_heads(mir, head_importance, pruning_ratio)
return mir
def get_pruning_statistics(mir: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract head pruning statistics from MIR.
Args:
mir: MIR representation with pruning metadata
Returns:
Dictionary with pruning statistics
"""
stats = mir.get("metadata", {}).get("head_pruning_stats", {})
if not stats:
return {
"pruning_applied": False,
"message": "No head pruning metadata found"
}
total_before = stats.get("total_heads_before", 0)
total_after = stats.get("total_heads_after", 0)
return {
"pruning_applied": True,
"total_heads_before": total_before,
"total_heads_after": total_after,
"heads_pruned": total_before - total_after,
"overall_pruning_ratio": (total_before - total_after) / total_before if total_before > 0 else 0,
"layers_pruned": stats.get("layers_pruned", [])
}