Naphula commited on
Commit
cb7099b
·
verified ·
1 Parent(s): 992967a

Upload audit_della_v2_gemma.py

Browse files
Files changed (1) hide show
  1. audit_della_v2_gemma.py +269 -0
audit_della_v2_gemma.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import yaml
2
+ import torch
3
+ import os
4
+ import sys
5
+ import numpy as np
6
+ import matplotlib.pyplot as plt
7
+ from safetensors import safe_open
8
+ from sklearn.decomposition import PCA
9
+ from sklearn.metrics.pairwise import cosine_similarity
10
+ from tqdm import tqdm
11
+ import argparse
12
+
13
+ # --- CONFIGURATION ---
14
+ PROBE_LAYERS = [
15
+ # "model.layers.12.mlp.gate_proj.weight", # Mid-model logic
16
+ # "model.layers.34.mlp.gate_proj.weight" # Output semantics
17
+ "model.language_model.layers.12.mlp.down_proj.weight", # Mid-model logic
18
+ "model.language_model.layers.34.mlp.down_proj.weight" # Output semantics
19
+ ]
20
+ LOG_FILENAME = "della_scan.log"
21
+ # ---------------------
22
+
23
+ class Logger:
24
+ def __init__(self, filename):
25
+ self.terminal = sys.stdout
26
+ self.log = open(filename, "w", encoding="utf-8")
27
+
28
+ def write(self, message):
29
+ self.terminal.write(message)
30
+ self.log.write(message)
31
+ self.log.flush()
32
+
33
+ def flush(self):
34
+ self.terminal.flush()
35
+ self.log.flush()
36
+
37
+ def close(self):
38
+ self.log.close()
39
+
40
+ def load_yaml_config(config_path):
41
+ print(f"Loading config: {config_path}")
42
+ with open(config_path, 'r', encoding='utf-8') as f:
43
+ config = yaml.safe_load(f)
44
+
45
+ models = []
46
+ base_model = None
47
+
48
+ # Extract base model
49
+ if 'base_model' in config:
50
+ base_model = config['base_model']
51
+
52
+ # Extract models list
53
+ if 'models' in config:
54
+ for m in config['models']:
55
+ models.append(m['model'])
56
+
57
+ return base_model, models
58
+
59
+ def get_model_fingerprint(model_path, probe_layers):
60
+ tensors = []
61
+ if os.path.exists(model_path):
62
+ files = [f for f in os.listdir(model_path) if f.endswith('.safetensors')]
63
+ files.sort()
64
+ found_layers = 0
65
+
66
+ for file in files:
67
+ full_path = os.path.join(model_path, file)
68
+ try:
69
+ with safe_open(full_path, framework="pt", device="cpu") as f:
70
+ keys = f.keys()
71
+ for layer in probe_layers:
72
+ if layer in keys:
73
+ t = f.get_tensor(layer).float().view(-1)
74
+ t = t[::10] # Downsample
75
+ tensors.append(t)
76
+ found_layers += 1
77
+ except Exception as e:
78
+ print(f"Error reading {file}: {e}")
79
+
80
+ if found_layers == 0:
81
+ return None
82
+ else:
83
+ return None
84
+
85
+ if not tensors:
86
+ return None
87
+
88
+ return torch.cat(tensors)
89
+
90
+ def analyze_task_vectors(base_fp, donor_fps):
91
+ # 0. Handle size mismatches (Manifold Alignment)
92
+ base_size = base_fp.numel()
93
+ donor_sizes = [f.numel() for f in donor_fps]
94
+
95
+ min_size = min([base_size] + donor_sizes)
96
+
97
+ if any(s != min_size for s in donor_sizes) or base_size != min_size:
98
+ print(f"\n[!] SIZE MISMATCH DETECTED")
99
+ print(f" Base Size: {base_size}")
100
+ print(f" Min Donor: {min(donor_sizes)}")
101
+ print(f" Action: Truncating all models to {min_size} for audit.")
102
+
103
+ # Align fingerprints
104
+ aligned_base = base_fp[:min_size]
105
+ aligned_donors = [f[:min_size] for f in donor_fps]
106
+
107
+ # 1. Calculate Task Vectors (Delta = Donor - Base)
108
+ task_vectors = []
109
+ for d_fp in aligned_donors:
110
+ task_vectors.append(d_fp - aligned_base)
111
+
112
+ # Stack into matrix [N_donors, N_features]
113
+ data_matrix = torch.stack(task_vectors).numpy()
114
+
115
+ # 2. Norm Analysis (Magnitude of the Delta)
116
+ norms = np.linalg.norm(data_matrix, axis=1)
117
+
118
+ # 3. Cosine Similarity Matrix (Directional Alignment)
119
+ cos_sim = cosine_similarity(data_matrix)
120
+
121
+ # 4. PCA Projection (2D)
122
+ # Center the task vectors
123
+ centered_data = data_matrix - np.mean(data_matrix, axis=0)
124
+
125
+ if len(donor_fps) > 1:
126
+ pca = PCA(n_components=2)
127
+ coords = pca.fit_transform(centered_data)
128
+ var_ratio = pca.explained_variance_ratio_
129
+ else:
130
+ coords = np.zeros((1, 2))
131
+ var_ratio = [1.0, 0.0]
132
+
133
+ return norms, cos_sim, coords, var_ratio, donor_sizes
134
+
135
+ def plot_results(model_ids, norms, cos_sim, coords, var_ratio):
136
+ labels = [str(mid) for mid in model_ids]
137
+
138
+ fig = plt.figure(figsize=(20, 12))
139
+ fig.suptitle(f"DELLA/Task Arithmetic Compatibility Audit ({len(model_ids)} Donors)\nRefer to della_scan.log for ID Key", fontsize=16)
140
+
141
+ # --- Plot 1: Task Vector Manifold (PCA) ---
142
+ ax1 = fig.add_subplot(2, 2, 1)
143
+ ax1.scatter(coords[:, 0], coords[:, 1], c='purple', s=80, alpha=0.6)
144
+
145
+ for i, txt in enumerate(labels):
146
+ ax1.annotate(txt, (coords[i, 0], coords[i, 1]), xytext=(3, 3), textcoords='offset points', fontsize=8, fontweight='bold')
147
+
148
+ ax1.set_title(f"Task Vector Map (PCA of Deltas)\nClusters = Redundant Skills")
149
+ ax1.set_xlabel(f"PC1 ({var_ratio[0]:.1%} variance)")
150
+ ax1.set_ylabel(f"PC2 ({var_ratio[1]:.1%} variance)")
151
+ ax1.grid(True, alpha=0.3)
152
+
153
+ # Plot Origin (Base Model reference relative to centered data)
154
+ center_offset = -np.mean(coords, axis=0)
155
+ ax1.scatter(center_offset[0], center_offset[1], c='red', marker='x', s=100, label='Base Model (Ref)')
156
+ ax1.legend()
157
+
158
+ # --- Plot 2: Cosine Similarity Heatmap ---
159
+ ax2 = fig.add_subplot(2, 2, 2)
160
+ # For Task Vectors, negative similarity is common (conflicting directions)
161
+ im = ax2.imshow(cos_sim, cmap='coolwarm', vmin=-1.0, vmax=1.0)
162
+
163
+ ax2.set_xticks(np.arange(len(labels)))
164
+ ax2.set_yticks(np.arange(len(labels)))
165
+ ax2.set_xticklabels(labels, rotation=90, fontsize=6)
166
+ ax2.set_yticklabels(labels, fontsize=6)
167
+
168
+ ax2.set_title("Task Vector Alignment (Blue=Opposed, Red=Aligned)")
169
+ plt.colorbar(im, ax=ax2)
170
+
171
+ # --- Plot 3: Delta Magnitude (L2 Norm) ---
172
+ ax3 = fig.add_subplot(2, 1, 2)
173
+ bars = ax3.bar(labels, norms, color='orange', alpha=0.6)
174
+ ax3.set_title("Task Vector Magnitude (L2 Norm)\nHigh bars = Drastic deviation from Base Model")
175
+ ax3.set_ylabel("Delta L2 Norm")
176
+ ax3.set_xlabel("Donor ID")
177
+ ax3.grid(axis='y', alpha=0.3)
178
+
179
+ for bar in bars:
180
+ height = bar.get_height()
181
+ ax3.text(bar.get_x() + bar.get_width()/2., height,
182
+ f'{height:.1f}', ha='center', va='bottom', fontsize=6, rotation=90)
183
+
184
+ plt.tight_layout()
185
+ plt.show()
186
+
187
+ def main():
188
+ # Hook stdout to log file
189
+ sys.stdout = Logger(LOG_FILENAME)
190
+
191
+ parser = argparse.ArgumentParser(description="Audit MergeKit models for DELLA/Task Arithmetic compatibility.")
192
+ parser.add_argument("config", help="Path to the mergekit yaml config file")
193
+ args = parser.parse_args()
194
+
195
+ print(f"--- DELLA AUDIT V2 START ---")
196
+ base_model_path, donor_paths = load_yaml_config(args.config)
197
+
198
+ if not base_model_path:
199
+ print("Error: No 'base_model' found in config. DELLA requires a base model.")
200
+ return
201
+
202
+ print(f"Base Model: {base_model_path}")
203
+ print(f"Donors: {len(donor_paths)}")
204
+
205
+ print("\nExtracting BASE MODEL fingerprint...")
206
+ base_fp = get_model_fingerprint(base_model_path, PROBE_LAYERS)
207
+ if base_fp is None:
208
+ print("Failed to load base model. Exiting.")
209
+ return
210
+
211
+ donor_fps = []
212
+ valid_donors = []
213
+ valid_ids = []
214
+
215
+ print("\nExtracting DONOR fingerprints...")
216
+ for i, path in enumerate(tqdm(donor_paths)):
217
+ fp = get_model_fingerprint(path, PROBE_LAYERS)
218
+ if fp is not None:
219
+ donor_fps.append(fp)
220
+ valid_donors.append(path)
221
+ valid_ids.append(i + 1)
222
+ else:
223
+ print(f"Skipping {path} (failed to load)")
224
+
225
+ if len(valid_donors) < 1:
226
+ print("Need at least 1 valid donor.")
227
+ return
228
+
229
+ print("\nComputing Task Vector geometry...")
230
+ norms, cos_sim, coords, var_ratio, sizes = analyze_task_vectors(base_fp, donor_fps)
231
+
232
+ # --- LOGGING THE KEY ---
233
+ print("\n" + "="*80)
234
+ print(f"{'ID':<5} | {'Model Name'}")
235
+ print("-" * 80)
236
+ for i, path in enumerate(valid_donors):
237
+ name = os.path.basename(path).replace("!models--", "")
238
+ print(f"#{valid_ids[i]:<4} | {name}")
239
+ print("="*80 + "\n")
240
+
241
+ # --- MAGNITUDE ANALYSIS ---
242
+ print("--- MAGNITUDE ANALYSIS & DATA POINTS ---")
243
+ print(f"{'ID':<5} | {'Status':<10} | {'Delta Norm':<12} | {'Orig Size':<12} | {'Model Name'}")
244
+ print("-" * 100)
245
+
246
+ mean_norm = np.mean(norms)
247
+ std_norm = np.std(norms)
248
+
249
+ for i, model in enumerate(valid_donors):
250
+ name = os.path.basename(model).replace("!models--", "")
251
+ # Check if norm is significantly higher than average (potential destroyer of weights)
252
+ z_score = (norms[i] - mean_norm) / (std_norm + 1e-8)
253
+ status = "HIGH MAG" if z_score > 1.5 else "OK"
254
+
255
+ print(f"#{valid_ids[i]:<4} | {status:<10} | {norms[i]:<12.4f} | {sizes[i]:<12} | {name}")
256
+
257
+ print("\nLog saved to: " + LOG_FILENAME)
258
+ print("Displaying charts...")
259
+
260
+ # Reset stdout
261
+ sys.stdout.terminal.flush()
262
+
263
+ plot_results(valid_ids, norms, cos_sim, coords, var_ratio)
264
+
265
+ # Close log
266
+ sys.stdout.close()
267
+
268
+ if __name__ == "__main__":
269
+ main()