angerami commited on
Commit
eb2f1cd
·
1 Parent(s): 7fcd2da

re-organizing workflow

Browse files
scripts/plot_corr_figures.py ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Plot correlations for all models found in a data directory.
3
+
4
+ Automatically discovers models from metadata files and generates plots for each.
5
+ Similar to run_all_correlations.py but for plotting instead of analysis.
6
+
7
+ Usage:
8
+ python scripts/plot_all_models.py --data corr_out
9
+ python scripts/plot_all_models.py --data corr_out --skip gpt2
10
+ python scripts/plot_all_models.py --data corr_out --models gpt2 gpt2-large
11
+ python scripts/plot_all_models.py --data corr_out --components weights # or biases
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import logging
17
+ import os
18
+ import re
19
+ import subprocess
20
+ import sys
21
+ from pathlib import Path
22
+ from collections import defaultdict
23
+
24
+
25
+ def find_models_and_components(data_dir):
26
+ """Find all models and their components from metadata files.
27
+
28
+ Returns:
29
+ dict: {model_name: [(revision, component), ...]}
30
+ where component is like 'W_QK', 'W_OV', 'b_Q', etc.
31
+ """
32
+ data_path = Path(data_dir)
33
+ if not data_path.exists():
34
+ return {}
35
+
36
+ models = defaultdict(list)
37
+
38
+ # Pattern: {model}_{revision}_{component}_metadata.json
39
+ # Examples:
40
+ # gpt2_main_W_QK_metadata.json
41
+ # gpt2_main_b_Q_metadata.json
42
+ # pythia-70m-deduped_main_W_OV_metadata.json
43
+
44
+ for metadata_file in data_path.glob("*_metadata.json"):
45
+ filename = metadata_file.name
46
+
47
+ # Skip cross-correlation files (contain _vs_)
48
+ if "_vs_" in filename:
49
+ continue
50
+
51
+ # Parse filename
52
+ parts = filename.replace("_metadata.json", "").split("_")
53
+
54
+ # Find the component (W_QK, W_OV, b_Q, etc.)
55
+ component = None
56
+ for i, part in enumerate(parts):
57
+ if part in ["W", "b"] and i + 1 < len(parts):
58
+ component = f"{part}_{parts[i + 1]}"
59
+ component_idx = i
60
+ break
61
+
62
+ if not component:
63
+ continue
64
+
65
+ # Everything before component is model + revision
66
+ model_revision = "_".join(parts[:component_idx])
67
+
68
+ # Last part before component is usually revision
69
+ if parts[component_idx - 1] in ["main", "step0", "step1000"]:
70
+ revision = parts[component_idx - 1]
71
+ model = "_".join(parts[:component_idx - 1])
72
+ else:
73
+ revision = "main"
74
+ model = model_revision
75
+
76
+ models[model].append((revision, component))
77
+
78
+ return dict(models)
79
+
80
+
81
+ def categorize_component(component):
82
+ """Categorize component as 'weight' or 'bias'."""
83
+ if component.startswith("W_"):
84
+ return "weight"
85
+ elif component.startswith("b_"):
86
+ return "bias"
87
+ return "unknown"
88
+
89
+
90
+ def plot_model_component(data_dir, model, revision, component, out_dir, quiet=False):
91
+ """Run plot_correlations.py for a specific model/component."""
92
+ # Determine weight_type parameter (legacy parameter name)
93
+ weight_type = component
94
+
95
+ cmd = [
96
+ sys.executable,
97
+ "scripts/plot_correlations.py",
98
+ "--data", data_dir,
99
+ "--model", model,
100
+ "--revision", revision,
101
+ "--weight-type", weight_type,
102
+ "--out", out_dir,
103
+ ]
104
+
105
+ if not quiet:
106
+ print(f" Plotting: {model} @ {revision} - {component}")
107
+
108
+ try:
109
+ result = subprocess.run(
110
+ cmd,
111
+ capture_output=quiet,
112
+ text=True,
113
+ check=True
114
+ )
115
+ return True
116
+ except subprocess.CalledProcessError as e:
117
+ if not quiet:
118
+ print(f" ERROR: {e}")
119
+ if e.stderr:
120
+ print(f" {e.stderr}")
121
+ return False
122
+ except Exception as e:
123
+ if not quiet:
124
+ print(f" ERROR: {e}")
125
+ return False
126
+
127
+
128
+ def main():
129
+ parser = argparse.ArgumentParser(
130
+ description="Plot correlations for all models in data directory",
131
+ formatter_class=argparse.RawDescriptionHelpFormatter,
132
+ epilog="""
133
+ Examples:
134
+ # Plot all models
135
+ python scripts/plot_all_models.py --data corr_out
136
+
137
+ # Plot specific models only
138
+ python scripts/plot_all_models.py --data corr_out --models gpt2 gpt2-large
139
+
140
+ # Skip certain models
141
+ python scripts/plot_all_models.py --data corr_out --skip gpt2
142
+
143
+ # Plot only weights (no biases)
144
+ python scripts/plot_all_models.py --data corr_out --components weights
145
+
146
+ # Plot only biases
147
+ python scripts/plot_all_models.py --data corr_out --components biases
148
+
149
+ # Quiet mode (less output)
150
+ python scripts/plot_all_models.py --data corr_out --quiet
151
+ """
152
+ )
153
+
154
+ parser.add_argument(
155
+ "--data", type=str, default="corr_out",
156
+ help="Data directory containing correlation results (default: corr_out)"
157
+ )
158
+ parser.add_argument(
159
+ "--out", type=str, default=None,
160
+ help="Output directory for figures (default: {data}/figures)"
161
+ )
162
+ parser.add_argument(
163
+ "--models", nargs="*", default=None,
164
+ help="Specific models to plot (default: all found models)"
165
+ )
166
+ parser.add_argument(
167
+ "--skip", nargs="*", default=[],
168
+ help="Models to skip"
169
+ )
170
+ parser.add_argument(
171
+ "--components", choices=["weights", "biases", "all"], default="all",
172
+ help="Which components to plot (default: all)"
173
+ )
174
+ parser.add_argument(
175
+ "--quiet", "-q", action="store_true",
176
+ help="Suppress detailed output"
177
+ )
178
+ parser.add_argument(
179
+ "--dry-run", action="store_true",
180
+ help="Show what would be plotted without plotting"
181
+ )
182
+ parser.add_argument(
183
+ "--build-dataset", type=str, default=None, metavar="REPO",
184
+ help="After plotting, build and push HF dataset "
185
+ "(e.g. user/transformer-analysis-figures)"
186
+ )
187
+
188
+ args = parser.parse_args()
189
+
190
+ # Default output directory
191
+ out_dir = args.out or os.path.join(args.data, "figures")
192
+
193
+ # Find models
194
+ if not args.quiet:
195
+ print(f"Scanning directory: {args.data}")
196
+
197
+ models_components = find_models_and_components(args.data)
198
+
199
+ if not models_components:
200
+ print(f"No models found in {args.data}")
201
+ print("Make sure the directory contains *_metadata.json files")
202
+ return 1
203
+
204
+ # Filter models
205
+ if args.models:
206
+ models_to_plot = {
207
+ m: c for m, c in models_components.items()
208
+ if m in args.models
209
+ }
210
+ else:
211
+ models_to_plot = models_components
212
+
213
+ # Skip models
214
+ if args.skip:
215
+ models_to_plot = {
216
+ m: c for m, c in models_to_plot.items()
217
+ if m not in args.skip
218
+ }
219
+
220
+ if not models_to_plot:
221
+ print("No models to plot after filtering")
222
+ return 1
223
+
224
+ # Count components
225
+ total_components = sum(len(components) for components in models_to_plot.values())
226
+
227
+ # Filter by component type
228
+ if args.components != "all":
229
+ # Map plural to singular
230
+ component_type_map = {
231
+ "weights": "weight",
232
+ "biases": "bias"
233
+ }
234
+ component_type = component_type_map.get(args.components, args.components)
235
+
236
+ filtered_models = {}
237
+ for model, components in models_to_plot.items():
238
+ filtered = [
239
+ (rev, comp) for rev, comp in components
240
+ if categorize_component(comp) == component_type
241
+ ]
242
+ if filtered:
243
+ filtered_models[model] = filtered
244
+ models_to_plot = filtered_models
245
+
246
+ # Recount after filtering
247
+ filtered_components = sum(len(components) for components in models_to_plot.values())
248
+
249
+ # Print summary
250
+ print("\n" + "=" * 70)
251
+ print(f"Found {len(models_to_plot)} models with {filtered_components} components:")
252
+ print("-" * 70)
253
+
254
+ for model, components in sorted(models_to_plot.items()):
255
+ if components:
256
+ comp_strs = []
257
+ for rev, comp in sorted(set(components)):
258
+ comp_type = "W" if comp.startswith("W_") else "b"
259
+ comp_strs.append(f"{comp}")
260
+
261
+ print(f" {model:<30} {len(components):>2} components: {', '.join(sorted(set(comp_strs)))}")
262
+
263
+ print("=" * 70)
264
+
265
+ if args.dry_run:
266
+ print("\nDry run - exiting without plotting")
267
+ return 0
268
+
269
+ # Create output directory
270
+ os.makedirs(out_dir, exist_ok=True)
271
+
272
+ # Plot each model/component
273
+ print(f"\nOutput directory: {out_dir}\n")
274
+
275
+ success_count = 0
276
+ fail_count = 0
277
+
278
+ for i, (model, components) in enumerate(sorted(models_to_plot.items()), 1):
279
+ if not components:
280
+ continue
281
+
282
+ print(f"[{i}/{len(models_to_plot)}] {model}")
283
+
284
+ for revision, component in sorted(set(components)):
285
+ if plot_model_component(
286
+ args.data, model, revision, component, out_dir, args.quiet
287
+ ):
288
+ success_count += 1
289
+ else:
290
+ fail_count += 1
291
+
292
+ # Summary
293
+ print("\n" + "=" * 70)
294
+ print(f"Plotting complete!")
295
+ print(f" Success: {success_count}")
296
+ print(f" Failed: {fail_count}")
297
+ print(f" Output: {out_dir}")
298
+ print("=" * 70)
299
+
300
+ # Multi-model comparison plots
301
+ if success_count > 1:
302
+ print("\nGenerating multi-model comparison plots...")
303
+ try:
304
+ from plot_correlations import (plot_eigenvalue_comparison,
305
+ plot_eigen_stats_comparison)
306
+ model_list = sorted(models_to_plot.keys())
307
+ # One comparison plot per weight type that all models share
308
+ all_wts = set()
309
+ for components in models_to_plot.values():
310
+ for _, comp in components:
311
+ all_wts.add(comp)
312
+ for wt in sorted(all_wts):
313
+ try:
314
+ plot_eigenvalue_comparison(
315
+ args.data, model_list, weight_type=wt,
316
+ out_dir=out_dir)
317
+ except Exception as e:
318
+ print(f" *** Error on {wt} eigenvalues: {e}")
319
+ try:
320
+ plot_eigen_stats_comparison(
321
+ args.data, model_list, weight_type=wt,
322
+ out_dir=out_dir)
323
+ except Exception as e:
324
+ print(f" *** Error on {wt} eigen stats: {e}")
325
+ except ImportError as e:
326
+ print(f" *** Could not import comparison plotter: {e}")
327
+
328
+ # Build HF dataset if requested
329
+ if args.build_dataset and success_count > 0:
330
+ print(f"\nBuilding HF dataset → {args.build_dataset}")
331
+ try:
332
+ from build_hf_dataset import build_dataset
333
+ ds = build_dataset(out_dir)
334
+ ds.push_to_hub(args.build_dataset)
335
+ print(f"Pushed: https://huggingface.co/datasets/{args.build_dataset}")
336
+ except Exception as e:
337
+ print(f" *** Dataset build failed: {e}")
338
+
339
+ # Reminder to regenerate viewer
340
+ if success_count > 0:
341
+ print("\nTo view in browser, regenerate the viewer index:")
342
+ print(f" python scripts/generate_viewer_index.py --out {args.data} --serve")
343
+ print("=" * 70)
344
+
345
+ return 0 if fail_count == 0 else 1
346
+
347
+
348
+ if __name__ == "__main__":
349
+ sys.exit(main())
scripts/run_all_correlations.py CHANGED
@@ -19,21 +19,36 @@ Usage:
19
  # Fast metrics only (skip KDE), no biases
20
  python scripts/run_all_correlations.py \
21
  --cache /path/to/downloads --fast --no-bias
 
 
 
 
 
22
  """
23
 
24
  import argparse
 
 
25
  import logging
26
  import os
27
  import subprocess
28
  import sys
29
  import time
30
 
 
 
31
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
32
 
33
  from transformer_analysis.model_registry import MODEL_CONFIGS
34
  from transformer_analysis.correlation_analysis import (
35
  run_multi_circuit_analysis,
36
  find_cached_models,
 
 
 
 
 
 
37
  )
38
 
39
  # (n_layers, n_heads, head_dim)
@@ -59,6 +74,81 @@ HIST_METRICS = ["hist_symmetric_kl", "hist_jensen_shannon"]
59
  DEFAULT_METRICS = FAST_METRICS + HIST_METRICS
60
 
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  def estimate_time_minutes(model_name, n_circuits=2, include_bias=True, fast_only=True):
63
  """Rough runtime estimate in minutes."""
64
  if model_name not in MODEL_DIMS:
@@ -110,6 +200,8 @@ def main():
110
  help="Read detailed options from config")
111
  parser.add_argument("--dry-run", action="store_true",
112
  help="Print plan and time estimates without running")
 
 
113
  args = parser.parse_args()
114
 
115
  metrics = FAST_METRICS if args.fast else DEFAULT_METRICS
@@ -117,19 +209,18 @@ def main():
117
  cross = () if args.no_cross else ("QKOV", "WB")
118
  include_bias = not args.no_bias
119
 
 
120
  if args.config is not None:
121
- import json
122
  with open(args.config, "r") as f:
123
  config_opts = json.load(f)
124
- if 'metrics' in config_opts.keys():
125
  metrics = config_opts['metrics']
126
- if 'circuits' in config_opts.keys():
127
  circuits = tuple(config_opts['circuits'])
128
- if 'cross' in config_opts.keys():
129
  cross = tuple(config_opts['cross'])
130
- if 'models' in config_opts.keys():
131
  to_run = config_opts['models']
132
-
133
 
134
  if not to_run:
135
  # Discover models
@@ -193,16 +284,25 @@ def main():
193
 
194
  t0 = time.time()
195
  try:
196
- run_multi_circuit_analysis(
197
- model_name=model,
198
- circuits=circuits,
199
- include_bias=include_bias,
200
- cross_correlations=cross,
201
- metrics=tuple(metrics),
202
- cache_dir=args.cache,
203
- out_dir=args.out,
204
- device=args.device,
205
- )
 
 
 
 
 
 
 
 
 
206
  elapsed = (time.time() - t0) / 60
207
  results[model] = ("OK", elapsed)
208
  print(" Completed in {:.1f} min".format(elapsed))
@@ -219,7 +319,7 @@ def main():
219
  print("PLOTTING")
220
  print("=" * 60)
221
  subprocess.run([
222
- sys.executable, os.path.join(os.path.dirname(__file__), "plot_all_models.py"),
223
  "--data", args.out,
224
  ])
225
 
 
19
  # Fast metrics only (skip KDE), no biases
20
  python scripts/run_all_correlations.py \
21
  --cache /path/to/downloads --fast --no-bias
22
+
23
+ # Add new metrics to an existing run without full re-extraction
24
+ python scripts/run_all_correlations.py \
25
+ --cache /path/to/downloads --out corr_out \
26
+ --add-metrics hist_jensen_shannon hist_symmetric_kl
27
  """
28
 
29
  import argparse
30
+ import glob
31
+ import json
32
  import logging
33
  import os
34
  import subprocess
35
  import sys
36
  import time
37
 
38
+ import numpy as np
39
+
40
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
41
 
42
  from transformer_analysis.model_registry import MODEL_CONFIGS
43
  from transformer_analysis.correlation_analysis import (
44
  run_multi_circuit_analysis,
45
  find_cached_models,
46
+ extract_head_store,
47
+ )
48
+ from transformer_analysis.head_correlations import (
49
+ compute_correlation_matrices,
50
+ correlation_summary,
51
+ layer_block_means,
52
  )
53
 
54
  # (n_layers, n_heads, head_dim)
 
74
  DEFAULT_METRICS = FAST_METRICS + HIST_METRICS
75
 
76
 
77
+ def recompute_metrics_for_model(model_name, new_metrics, out_dir, cache_dir, device=None):
78
+ """Add new metrics to all existing weight-type outputs for a model.
79
+
80
+ Discovers weight types from metadata files in out_dir, skips metrics that
81
+ are already present, re-extracts heads from cache, and merges new results
82
+ into existing .npz and summary files.
83
+ """
84
+ meta_paths = sorted(
85
+ p for p in glob.glob(os.path.join(out_dir, f"{model_name}_*_metadata.json"))
86
+ if "_vs_" not in os.path.basename(p)
87
+ )
88
+ if not meta_paths:
89
+ logging.warning(f" No existing outputs found for {model_name} in {out_dir}")
90
+ return
91
+
92
+ for meta_path in meta_paths:
93
+ with open(meta_path) as f:
94
+ meta = json.load(f)
95
+
96
+ fname = os.path.basename(meta_path).replace("_metadata.json", "")
97
+ model_rev_prefix = f"{model_name}_main_"
98
+ if not fname.startswith(model_rev_prefix):
99
+ logging.warning(f" Skipping {os.path.basename(meta_path)}: non-main revision")
100
+ continue
101
+ weight_type = fname[len(model_rev_prefix):]
102
+
103
+ to_add = [m for m in new_metrics if m not in meta.get("metrics", [])]
104
+ if not to_add:
105
+ logging.info(f" {model_name}/{weight_type}: metrics already present, skipping")
106
+ continue
107
+
108
+ logging.info(f" {model_name}/{weight_type}: adding {to_add}")
109
+
110
+ store, _ = extract_head_store(
111
+ model_name=model_name,
112
+ weight_type=weight_type,
113
+ revision=None,
114
+ cache_dir=cache_dir,
115
+ device=device,
116
+ )
117
+
118
+ Q_new = compute_correlation_matrices(
119
+ store,
120
+ metrics=tuple(to_add),
121
+ kde_kwargs={"n_eval": 2048, "bw_method": "scott"},
122
+ show_progress=True,
123
+ )
124
+
125
+ # Merge into existing .npz
126
+ npz_path = os.path.join(out_dir, f"{fname}_Q.npz")
127
+ existing = dict(np.load(npz_path)) if os.path.exists(npz_path) else {}
128
+ for m, Q in Q_new.items():
129
+ existing[f"Q_{m}"] = Q
130
+ np.savez_compressed(npz_path, **existing)
131
+
132
+ # Update summary + per-metric arrays
133
+ summary_path = os.path.join(out_dir, f"{fname}_summary.json")
134
+ summary = json.load(open(summary_path)) if os.path.exists(summary_path) else {}
135
+ for m, Q in Q_new.items():
136
+ s = correlation_summary(Q, store.keys)
137
+ summary[m] = {k: v for k, v in s.items() if not isinstance(v, np.ndarray)}
138
+ np.save(os.path.join(out_dir, f"{fname}_{m}_eigenvalues.npy"), s["eigenvalues"])
139
+ np.save(os.path.join(out_dir, f"{fname}_{m}_P_Q.npy"), s["P_Q_values"])
140
+ block, _ = layer_block_means(Q, store.keys)
141
+ np.save(os.path.join(out_dir, f"{fname}_{m}_block_means.npy"), block)
142
+ with open(summary_path, "w") as f:
143
+ json.dump(summary, f, indent=2)
144
+
145
+ # Update metadata
146
+ for m in to_add:
147
+ meta.setdefault("metrics", []).append(m)
148
+ with open(meta_path, "w") as f:
149
+ json.dump(meta, f, indent=2, default=str)
150
+
151
+
152
  def estimate_time_minutes(model_name, n_circuits=2, include_bias=True, fast_only=True):
153
  """Rough runtime estimate in minutes."""
154
  if model_name not in MODEL_DIMS:
 
200
  help="Read detailed options from config")
201
  parser.add_argument("--dry-run", action="store_true",
202
  help="Print plan and time estimates without running")
203
+ parser.add_argument("--add-metrics", nargs="+", default=None,
204
+ help="Add metrics to existing outputs without full re-extraction")
205
  args = parser.parse_args()
206
 
207
  metrics = FAST_METRICS if args.fast else DEFAULT_METRICS
 
209
  cross = () if args.no_cross else ("QKOV", "WB")
210
  include_bias = not args.no_bias
211
 
212
+ to_run = None
213
  if args.config is not None:
 
214
  with open(args.config, "r") as f:
215
  config_opts = json.load(f)
216
+ if 'metrics' in config_opts:
217
  metrics = config_opts['metrics']
218
+ if 'circuits' in config_opts:
219
  circuits = tuple(config_opts['circuits'])
220
+ if 'cross' in config_opts:
221
  cross = tuple(config_opts['cross'])
222
+ if 'models' in config_opts:
223
  to_run = config_opts['models']
 
224
 
225
  if not to_run:
226
  # Discover models
 
284
 
285
  t0 = time.time()
286
  try:
287
+ if args.add_metrics:
288
+ recompute_metrics_for_model(
289
+ model_name=model,
290
+ new_metrics=args.add_metrics,
291
+ out_dir=args.out,
292
+ cache_dir=args.cache,
293
+ device=args.device,
294
+ )
295
+ else:
296
+ run_multi_circuit_analysis(
297
+ model_name=model,
298
+ circuits=circuits,
299
+ include_bias=include_bias,
300
+ cross_correlations=cross,
301
+ metrics=tuple(metrics),
302
+ cache_dir=args.cache,
303
+ out_dir=args.out,
304
+ device=args.device,
305
+ )
306
  elapsed = (time.time() - t0) / 60
307
  results[model] = ("OK", elapsed)
308
  print(" Completed in {:.1f} min".format(elapsed))
 
319
  print("PLOTTING")
320
  print("=" * 60)
321
  subprocess.run([
322
+ sys.executable, os.path.join(os.path.dirname(__file__), "plot_corr_figures.py"),
323
  "--data", args.out,
324
  ])
325