-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhead_pruner.py
More file actions
501 lines (405 loc) · 17.9 KB
/
Copy pathhead_pruner.py
File metadata and controls
501 lines (405 loc) · 17.9 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
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
"""
Physical Attention Head Pruning for HuggingFace Models
This module implements actual head removal (not just importance scoring).
Physically modifies model architecture to remove low-importance attention heads.
"""
import torch
import torch.nn as nn
from typing import Dict, List, Tuple, Optional, Set
import numpy as np
import copy
class AttentionHeadPruner:
"""
Prunes attention heads from HuggingFace transformer models.
Supports:
- BERT-style models (separate Q/K/V projections)
- GPT-2-style models (combined QKV projection)
- Preserves model functionality after pruning
"""
def __init__(self, model: nn.Module, model_type: str = 'auto'):
"""
Initialize pruner.
Args:
model: HuggingFace model to prune
model_type: 'bert', 'gpt2', or 'auto' (auto-detect)
"""
self.model = model
self.model_type = self._detect_model_type() if model_type == 'auto' else model_type
self.pruned_heads = {} # layer_idx -> set of pruned head indices
def _detect_model_type(self) -> str:
"""Detect model architecture type."""
model_class = type(self.model).__name__.lower()
if 'bert' in model_class:
return 'bert'
elif 'gpt' in model_class or 'gpt2' in model_class:
return 'gpt2'
elif 'roberta' in model_class:
return 'bert' # RoBERTa uses BERT architecture
else:
return 'unknown'
def prune_heads(
self,
heads_to_prune: Dict[int, List[int]],
importance_scores: Optional[Dict[str, np.ndarray]] = None
) -> Dict[str, any]:
"""
Prune specified attention heads from the model.
Args:
heads_to_prune: Dict mapping layer_idx -> list of head indices to prune
e.g., {0: [1, 5, 7], 1: [0, 3], ...}
importance_scores: Optional importance scores for logging
Returns:
Dict with pruning statistics
"""
print(f"\n[Head Pruner] Pruning attention heads...")
print(f" Model type: {self.model_type}")
if self.model_type == 'bert':
return self._prune_bert_heads(heads_to_prune, importance_scores)
elif self.model_type == 'gpt2':
return self._prune_gpt2_heads(heads_to_prune, importance_scores)
else:
raise ValueError(f"Unsupported model type: {self.model_type}")
def _prune_bert_heads(
self,
heads_to_prune: Dict[int, List[int]],
importance_scores: Optional[Dict[str, np.ndarray]] = None
) -> Dict[str, any]:
"""
Prune heads from BERT-style models.
BERT has separate Q, K, V projections:
- self.query: Linear(hidden_size, all_head_size)
- self.key: Linear(hidden_size, all_head_size)
- self.value: Linear(hidden_size, all_head_size)
- output.dense: Linear(all_head_size, hidden_size)
To prune head i: remove columns from Q/K/V and rows from output.dense
"""
stats = {
'layers_pruned': 0,
'total_heads_pruned': 0,
'params_before': sum(p.numel() for p in self.model.parameters()),
'params_after': 0,
}
# Get BERT encoder layers
if hasattr(self.model, 'bert'):
encoder = self.model.bert.encoder
elif hasattr(self.model, 'encoder'):
encoder = self.model.encoder
else:
raise ValueError("Cannot find encoder in model")
# Prune each layer
for layer_idx, head_indices in heads_to_prune.items():
if layer_idx >= len(encoder.layer):
print(f" Warning: Layer {layer_idx} out of range, skipping")
continue
layer = encoder.layer[layer_idx]
attention = layer.attention
if not hasattr(attention, 'self'):
print(f" Warning: Layer {layer_idx} missing self-attention, skipping")
continue
self_attn = attention.self
output_layer = attention.output.dense
# Get dimensions
num_attention_heads = self_attn.num_attention_heads
attention_head_size = self_attn.attention_head_size
all_head_size = num_attention_heads * attention_head_size
# Convert head indices to keep (inverse of prune)
heads_to_keep = [i for i in range(num_attention_heads) if i not in head_indices]
if len(heads_to_keep) == 0:
print(f" Warning: Trying to prune all heads from layer {layer_idx}, skipping")
continue
print(f" Layer {layer_idx}: Pruning {len(head_indices)} heads, keeping {len(heads_to_keep)}")
# Prune Q, K, V projections (columns)
self_attn.query = self._prune_linear_columns(
self_attn.query, heads_to_keep, num_attention_heads
)
self_attn.key = self._prune_linear_columns(
self_attn.key, heads_to_keep, num_attention_heads
)
self_attn.value = self._prune_linear_columns(
self_attn.value, heads_to_keep, num_attention_heads
)
# Prune output projection (rows)
attention.output.dense = self._prune_linear_rows(
output_layer, heads_to_keep, num_attention_heads
)
# Update head count
self_attn.num_attention_heads = len(heads_to_keep)
# Track pruned heads
if layer_idx not in self.pruned_heads:
self.pruned_heads[layer_idx] = set()
self.pruned_heads[layer_idx].update(head_indices)
stats['layers_pruned'] += 1
stats['total_heads_pruned'] += len(head_indices)
stats['params_after'] = sum(p.numel() for p in self.model.parameters())
stats['compression_ratio'] = stats['params_before'] / stats['params_after']
print(f"\n Pruning complete:")
print(f" Layers pruned: {stats['layers_pruned']}")
print(f" Total heads pruned: {stats['total_heads_pruned']}")
print(f" Parameters: {stats['params_before']:,} -> {stats['params_after']:,}")
print(f" Compression: {stats['compression_ratio']:.2f}x")
return stats
def _prune_gpt2_heads(
self,
heads_to_prune: Dict[int, List[int]],
importance_scores: Optional[Dict[str, np.ndarray]] = None
) -> Dict[str, any]:
"""
Prune heads from GPT-2-style models.
GPT-2 has combined QKV projection:
- c_attn: Linear(hidden_size, 3 * all_head_size) # Combined Q, K, V
- c_proj: Linear(all_head_size, hidden_size)
To prune head i: remove corresponding sections from c_attn and rows from c_proj
"""
stats = {
'layers_pruned': 0,
'total_heads_pruned': 0,
'params_before': sum(p.numel() for p in self.model.parameters()),
'params_after': 0,
}
# Get GPT-2 transformer blocks
if hasattr(self.model, 'transformer'):
blocks = self.model.transformer.h
elif hasattr(self.model, 'h'):
blocks = self.model.h
else:
raise ValueError("Cannot find transformer blocks in model")
# Prune each layer
for layer_idx, head_indices in heads_to_prune.items():
if layer_idx >= len(blocks):
print(f" Warning: Layer {layer_idx} out of range, skipping")
continue
block = blocks[layer_idx]
attn = block.attn
if not hasattr(attn, 'c_attn'):
print(f" Warning: Layer {layer_idx} missing c_attn, skipping")
continue
# Get dimensions
num_heads = attn.num_heads if hasattr(attn, 'num_heads') else 12 # Default for GPT-2
embed_dim = attn.embed_dim if hasattr(attn, 'embed_dim') else 768
head_dim = embed_dim // num_heads
# Convert to heads to keep
heads_to_keep = [i for i in range(num_heads) if i not in head_indices]
if len(heads_to_keep) == 0:
print(f" Warning: Trying to prune all heads from layer {layer_idx}, skipping")
continue
print(f" Layer {layer_idx}: Pruning {len(head_indices)} heads, keeping {len(heads_to_keep)}")
# Prune c_attn (combined QKV projection)
# c_attn has shape [embed_dim, 3 * embed_dim] (3x for Q, K, V)
attn.c_attn = self._prune_gpt2_c_attn(
attn.c_attn, heads_to_keep, num_heads, head_dim
)
# Prune c_proj (output projection)
attn.c_proj = self._prune_linear_rows(
attn.c_proj, heads_to_keep, num_heads
)
# Update head count
if hasattr(attn, 'num_heads'):
attn.num_heads = len(heads_to_keep)
# Track pruned heads
if layer_idx not in self.pruned_heads:
self.pruned_heads[layer_idx] = set()
self.pruned_heads[layer_idx].update(head_indices)
stats['layers_pruned'] += 1
stats['total_heads_pruned'] += len(head_indices)
stats['params_after'] = sum(p.numel() for p in self.model.parameters())
stats['compression_ratio'] = stats['params_before'] / stats['params_after']
print(f"\n Pruning complete:")
print(f" Layers pruned: {stats['layers_pruned']}")
print(f" Total heads pruned: {stats['total_heads_pruned']}")
print(f" Parameters: {stats['params_before']:,} -> {stats['params_after']:,}")
print(f" Compression: {stats['compression_ratio']:.2f}x")
return stats
def _prune_linear_columns(
self,
linear: nn.Linear,
heads_to_keep: List[int],
num_heads: int
) -> nn.Linear:
"""
Prune columns from a linear layer (for Q/K/V projections).
Args:
linear: Linear layer to prune
heads_to_keep: Indices of heads to keep
num_heads: Total number of heads
Returns:
New linear layer with pruned columns
"""
out_features, in_features = linear.weight.shape
head_dim = out_features // num_heads
# Select columns to keep (reshape by heads)
indices = []
for head_idx in heads_to_keep:
start = head_idx * head_dim
end = start + head_dim
indices.extend(range(start, end))
# Create new linear layer
new_linear = nn.Linear(in_features, len(indices), bias=linear.bias is not None)
new_linear.weight.data = linear.weight.data[indices, :]
if linear.bias is not None:
new_linear.bias.data = linear.bias.data[indices]
return new_linear
def _prune_linear_rows(
self,
linear: nn.Linear,
heads_to_keep: List[int],
num_heads: int
) -> nn.Linear:
"""
Prune rows from a linear layer (for output projections).
Args:
linear: Linear layer to prune
heads_to_keep: Indices of heads to keep
num_heads: Total number of heads
Returns:
New linear layer with pruned rows
"""
out_features, in_features = linear.weight.shape
head_dim = in_features // num_heads
# Select rows to keep
indices = []
for head_idx in heads_to_keep:
start = head_idx * head_dim
end = start + head_dim
indices.extend(range(start, end))
# Create new linear layer
new_linear = nn.Linear(len(indices), out_features, bias=linear.bias is not None)
new_linear.weight.data = linear.weight.data[:, indices]
if linear.bias is not None:
new_linear.bias.data = linear.bias.data
return new_linear
def _prune_gpt2_c_attn(
self,
c_attn: nn.Linear,
heads_to_keep: List[int],
num_heads: int,
head_dim: int
) -> nn.Linear:
"""
Prune GPT-2's combined QKV projection.
c_attn projects to [Q, K, V] concatenated, so we need to prune
from three separate sections.
Note: GPT-2 uses Conv1D which has weight shape [out_features, in_features]
but we treat it as Linear for consistency.
"""
# Get weight shape - handle both Linear and Conv1D
if hasattr(c_attn, 'weight'):
weight = c_attn.weight
bias = c_attn.bias if hasattr(c_attn, 'bias') else None
# Check if this is Conv1D (out, in) or Linear (out, in)
if weight.shape[0] == weight.shape[1] * 3:
# Conv1D format: [3*embed_dim, embed_dim]
out_features, in_features = weight.shape
else:
# Linear format: [embed_dim, 3*embed_dim]
in_features, out_features = weight.shape
weight = weight.t() # Transpose to match Conv1D
if bias is not None:
bias = bias.clone()
else:
raise ValueError("c_attn must have weight attribute")
total_dim = out_features // 3 # Divide by 3 for Q, K, V
# Prune each section (Q, K, V)
indices = []
for section in range(3): # Q, K, V
section_start = section * total_dim
for head_idx in sorted(heads_to_keep):
start = section_start + head_idx * head_dim
end = start + head_dim
if end <= section_start + total_dim: # Safety check
indices.extend(range(start, end))
# Create new linear layer with correct dimensions
new_c_attn = nn.Linear(in_features, len(indices), bias=bias is not None)
new_c_attn.weight.data = weight[indices, :].clone()
if bias is not None:
new_c_attn.bias.data = bias[indices].clone()
return new_c_attn
def get_heads_to_prune_by_importance(
self,
importance_scores: Dict[str, np.ndarray],
prune_ratio: float = 0.25
) -> Dict[int, List[int]]:
"""
Determine which heads to prune based on importance scores.
Args:
importance_scores: Dict mapping layer names to importance arrays
prune_ratio: Fraction of heads to prune (0.25 = 25%)
Returns:
Dict mapping layer_idx -> list of head indices to prune
"""
# Flatten all scores
all_scores = []
layer_mapping = {} # (layer_idx, head_idx) -> score
for layer_name, scores in importance_scores.items():
# Extract layer index from name
layer_idx = self._extract_layer_index(layer_name)
if layer_idx is None:
continue
for head_idx, score in enumerate(scores):
all_scores.append(score)
layer_mapping[(layer_idx, head_idx)] = score
# Determine threshold
threshold = np.percentile(all_scores, prune_ratio * 100)
# Group by layer
heads_to_prune = {}
for (layer_idx, head_idx), score in layer_mapping.items():
if score < threshold:
if layer_idx not in heads_to_prune:
heads_to_prune[layer_idx] = []
heads_to_prune[layer_idx].append(head_idx)
return heads_to_prune
def _extract_layer_index(self, layer_name: str) -> Optional[int]:
"""Extract layer index from layer name."""
import re
# Try different patterns
patterns = [
r'layer\.(\d+)', # encoder.layer.0
r'\.h\.(\d+)\.', # transformer.h.0.attn
r'blocks\.(\d+)', # blocks.0
]
for pattern in patterns:
match = re.search(pattern, layer_name)
if match:
return int(match.group(1))
return None
def prune_model_by_importance(
model: nn.Module,
importance_scores: Dict[str, np.ndarray],
prune_ratio: float = 0.25,
model_type: str = 'auto'
) -> Tuple[nn.Module, Dict[str, any]]:
"""
Prune a model based on head importance scores.
Args:
model: Model to prune
importance_scores: Head importance scores from compute_head_importance_enhanced
prune_ratio: Fraction of heads to prune
model_type: 'bert', 'gpt2', or 'auto'
Returns:
Tuple of (pruned_model, pruning_stats)
"""
pruner = AttentionHeadPruner(model, model_type)
# Determine which heads to prune
heads_to_prune = pruner.get_heads_to_prune_by_importance(
importance_scores, prune_ratio
)
# Prune the model
stats = pruner.prune_heads(heads_to_prune, importance_scores)
return model, stats
if __name__ == "__main__":
print("Physical Attention Head Pruning")
print("=" * 60)
print("\nThis module implements actual head removal from HuggingFace models.")
print("\nUsage:")
print("""
from passes.head_pruner import prune_model_by_importance
from passes.enhanced_head_importance import compute_head_importance_enhanced
# Compute importance
importance = compute_head_importance_enhanced(
model, calibration_data, method='entropy'
)
# Prune model
pruned_model, stats = prune_model_by_importance(
model, importance, prune_ratio=0.25
)
# stats contains: layers_pruned, total_heads_pruned, compression_ratio
""")