angerami's picture
feat: add configurable binning strategy hooks to histogram_utils
54eb2a2
Raw
History Blame Contribute Delete
18 kB
# `run_weight_analysis.py`
# Analysis main for weight analysis
import json
import logging
import uuid
import os
import shutil
from datetime import datetime
from types import SimpleNamespace
from tqdm import tqdm
import pandas as pd
from huggingface_hub import snapshot_download
from datasets import Dataset
from transformers import AutoConfig
from transformer_analysis.perf_logger import PerfLogger
from transformer_analysis.attn_head_analysis import LayerHeadContainer
from transformer_analysis.histogram_utils import (
stats_config_default,
weight_bins_default,
sv_bins_default,
make_weight_bins,
make_sv_bins,
)
from transformer_analysis.model_registry import (
get_model_config,
extract_weight_map,
)
from transformer_analysis.device_utils import get_device
def process_model(
model_name="pythia-70m-deduped",
revision=None,
idx_max=-1,
out_dir="histos",
cache_dir="./model_data",
cleanup_downloads=False,
low_rank_svd_approximation=False,
top_k_svd=-1,
resume_download=True,
max_workers=4,
device=None,
skip_postprocess=False,
binning_strategy="fixed",
):
job_uuid = str(uuid.uuid4())[:8]
job_id = datetime.now().strftime("%Y%m%d_%H%M%S")
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(message)s",
handlers=[
logging.FileHandler(f"{out_dir}/logs/{job_id}.log"),
logging.StreamHandler(),
],
)
perf = PerfLogger(job_id)
logging.info(f"Starting job {job_id} {job_uuid}")
device_str = str(get_device(device))
logging.info(f"Using device: {device_str}")
with perf.phase("load_model"):
logging.info("Loading model...")
model_config = get_model_config(model_name)
revision_string = revision if revision else "main"
cache_path = snapshot_download(
repo_id=model_config.repo_id,
revision=revision,
cache_dir=f"{cache_dir}/{model_name}/{revision_string}",
allow_patterns=model_config.allow_patterns,
resume_download=resume_download,
max_workers=max_workers,
)
hf_config = AutoConfig.from_pretrained(cache_path)
logging.info(perf.log_report(context=model_name))
# Phase 2: Configuration
with perf.phase("configure"):
logging.info("Configuring analysis...")
config = SimpleNamespace()
config.weight_type = ["W_Q", "W_K", "W_QK", "W_Q_gram", "W_K_gram", "QK_alignment"]
config.stats = stats_config_default.copy()
config.w_bins = make_weight_bins(strategy=binning_strategy)
config.sv_bins = make_sv_bins(strategy=binning_strategy)
config.binning_strategy = binning_strategy
config.use_density = True
config.n_heads = model_config.get_config_value(hf_config.__dict__, "n_heads")
config.d_model = model_config.get_config_value(hf_config.__dict__, "d_model")
config.n_layers = model_config.get_config_value(hf_config.__dict__, "n_layers")
config.head_dim = config.d_model // config.n_heads
# SVD configuration options (passed from function parameters)
config.low_rank_svd_approximation = low_rank_svd_approximation
config.top_k_svd = top_k_svd
n_layers, n_heads, head_dim = config.n_layers, config.n_heads, config.head_dim
d_model = config.d_model
logging.info(perf.log_report())
# Phase 3: Loop with conditional logging
layer_data = []
with perf.phase("loop"):
n_hl = n_heads * n_layers
if idx_max == -1:
idx_max = n_hl
else:
idx_max = min(abs(idx_max), n_hl)
logging.info(f"Processing {idx_max} / {n_hl}")
# Get weight_map, needed if safetensors format unavailable and bin files are sharded
weight_map = extract_weight_map(cache_path=cache_path)
for layer_idx in tqdm(range(n_layers), desc="Processing layers", leave=True):
W_Q, W_K, _ = model_config.extract_qkv(
cache_path, layer_idx, d_model, weight_map, device=device_str, qkv_scale_factor=model_config.qkv_scale_factor
)
# For per-head analysis:
W_Q_h = W_Q.reshape(n_heads, head_dim, d_model).float()
W_K_h = W_K.reshape(n_heads, head_dim, d_model).float()
hc = LayerHeadContainer(
layer_idx,
config,
low_rank_svd_approximation=config.low_rank_svd_approximation,
top_k_svd=config.top_k_svd,
device=device_str
)
layer_input = {"W_Q": W_Q_h, "W_K": W_K_h}
hc.analyze_layer(layer_input)
layer_data.append(hc)
del W_Q, W_K, W_Q_h, W_K_h
logging.info(perf.log_report())
# Phase 4: Finalization
dfs = []
with perf.phase("finalize"):
logging.info("Aggregating results...")
for lhc in layer_data:
if not skip_postprocess:
lhc.post_process()
dfs.append(lhc.to_pandas())
df = pd.concat(dfs, ignore_index=True)
df["model"] = model_name
if revision:
df["revision"] = revision
df["step"] = int(revision.strip("step"))
df["job_uuid"] = job_uuid
df["job_id"] = job_id
logging.info(perf.log_report())
# Phase 5: Write output
with perf.phase("write_output"):
logging.info("Writing outputs...")
ds = Dataset.from_pandas(df)
ds.info.description = "metadata.json"
out_prefix = f"{out_dir}/{model_name}_{revision_string}"
ds.save_to_disk(out_prefix)
logging.info(f"Saving dataset: {out_prefix}")
# for metadata we need to do some coversions to make the objects JSON serializable
config_dict = vars(config).copy()
config_dict["stats"] = {k: v.__name__ for k, v in config_dict["stats"].items()}
config_dict["w_bins"] = config_dict["w_bins"].tolist()
config_dict["sv_bins"] = config_dict["sv_bins"].tolist()
with open(f"{out_prefix}/metadata.json", "w") as f:
json.dump(config_dict, f, indent=2)
with open(f"{out_dir}/logs/perf_{job_id}.json", "w") as f:
json.dump(perf.to_metadata(), f, indent=2)
logging.info(perf.log_report())
# Phase 6: Cleanup
with perf.phase("cleanup"):
if cleanup_downloads:
logging.info("Cleaning up downloads...")
cache_path = f"{cache_dir}/{model_name}/{revision}"
if os.path.exists(cache_path):
shutil.rmtree(cache_path)
disk_usage = shutil.disk_usage(cache_dir)
logging.info(
f"Disk usage: {disk_usage.used / (1024**3):.2f} GB / {disk_usage.total / (1024**3):.2f} GB"
)
logging.info(perf.log_report())
# Summary
logging.info("\n" + "=" * 60)
logging.info("Performance Summary:")
for phase_name in perf.phases.keys():
logging.info(perf.log_report(phase=phase_name))
logging.info("=" * 60)
logging.info(f"Performance saved to {out_dir}/logs/perf_{job_id}.json")
def reprocess_metrics(
model_name: str,
revision=None,
all_revisions: bool = False,
out_dir: str = "outputs",
quiet: bool = False,
):
"""
Reprocess existing datasets to update/add metrics without re-running full analysis.
This function loads existing datasets, recomputes metrics (both weight histogram
and singular value metrics), and overwrites the dataset with updated columns.
Args:
model_name: Name of the model to reprocess
revision: Specific revision to reprocess (or None for main)
all_revisions: Whether to process all available revisions
out_dir: Output directory containing existing datasets
quiet: Whether to suppress output
"""
from datasets import load_from_disk, Dataset
from transformer_analysis.histogram_utils import (
normality_metrics,
singular_value_metrics,
get_model_versions,
)
import numpy as np
if not quiet:
print("\n" + "=" * 80)
print(f"Reprocessing Metrics: {model_name}")
print("=" * 80)
# Determine which revisions to process
if all_revisions:
revisions = get_model_versions(model_name)
if not revisions:
print(f"Model {model_name} has no revisions defined. Processing main branch only.")
revisions = [None]
elif revision:
revisions = [revision]
else:
revisions = [None]
if not quiet:
print(f"Revisions to process: {len(revisions)}")
for rev in tqdm(revisions, desc=f"Reprocessing {model_name}", disable=quiet):
revision_str = rev if rev else "main"
# Determine dataset path
if rev:
dataset_path = os.path.join(out_dir, f"{model_name}_{revision_str}")
else:
dataset_path = os.path.join(out_dir, model_name)
if not os.path.exists(dataset_path):
print(f" WARNING: Dataset not found at {dataset_path}, skipping...")
continue
if not quiet:
print(f"\n Reprocessing: {model_name} @ {revision_str}")
try:
# Load existing dataset
ds = load_from_disk(dataset_path)
df = ds.to_pandas()
# Load metadata to get bin information
metadata_path = os.path.join(dataset_path, "metadata.json")
with open(metadata_path, "r") as f:
metadata = json.load(f)
w_bins = np.array(metadata["w_bins"])
centers = (w_bins[:-1] + w_bins[1:]) / 2
if not quiet:
print(f" Loaded dataset with {len(df)} rows")
# Process each row
new_columns = {}
for idx, row in tqdm(df.iterrows(), total=len(df), desc=" Processing rows", disable=quiet, leave=False):
# Create a dictionary for this row (simulating the 'h' dict)
h = row.to_dict()
# Apply weight histogram metrics
for metric_func in normality_metrics.values():
metric_func(h, centers)
# Apply singular value metrics if SVD data exists
if "SVD" in h and h["SVD"] is not None and not pd.isna(h["SVD"]).all():
svd_array = h["SVD"]
if isinstance(svd_array, (list, np.ndarray)) and len(svd_array) > 0:
for metric_func in singular_value_metrics.values():
metric_func(h, svd_array)
# Store new metric values
for key, value in h.items():
if key not in row or row[key] != value:
if key not in new_columns:
new_columns[key] = [None] * len(df)
new_columns[key][idx] = value
# Add new columns to dataframe
for col_name, col_values in new_columns.items():
df[col_name] = col_values
if not quiet:
print(f" Added/updated column: {col_name}")
# Save updated dataset
updated_ds = Dataset.from_pandas(df)
updated_ds.info.description = "metadata.json"
updated_ds.save_to_disk(dataset_path)
if not quiet:
print(f" ✓ Saved updated dataset to {dataset_path}")
except Exception as e:
print(f" ERROR reprocessing {model_name} @ {revision_str}: {e}")
import traceback
traceback.print_exc()
continue
if not quiet:
print("\n" + "=" * 80)
print(f"Completed reprocessing: {model_name}")
print("=" * 80 + "\n")
def create_campaign(path, name, clobber=False, logs=True):
base_dir = os.path.join(path, name)
log_dir = os.path.join(base_dir, "logs")
if not os.path.exists(base_dir):
os.makedirs(base_dir)
os.makedirs(log_dir)
return base_dir
elif clobber:
shutil.rmtree(base_dir)
os.makedirs(base_dir)
os.makedirs(log_dir)
return base_dir
return base_dir
def write_dataset_and_metadata(ds_list, metadata, ds_name):
"""Write combined dataset and metadata to disk.
Args:
ds_list: List of datasets to concatenate
metadata: Metadata dictionary to write
ds_name: Output directory name for the dataset
"""
from datasets import concatenate_datasets
combined_ds = concatenate_datasets(ds_list)
combined_ds.info.description = "metadata.json"
combined_ds.save_to_disk(ds_name)
with open(f"{ds_name}/metadata.json", "w") as f:
json.dump(metadata, f, indent=2)
def merge_versions(
model_name="pythia-70m-deduped", path="histos", suffix="all_checkpoints"
):
"""Merge all checkpoint versions of a single model into one dataset.
Args:
model_name: Name of the model to merge
path: Base directory containing the datasets
suffix: Suffix for the output merged dataset
"""
from datasets import load_from_disk
from transformer_analysis.histogram_utils import get_model_versions
ds_list = []
metadata = None
for rev in tqdm(get_model_versions(model_name), desc=f"Processing {model_name}"):
pattern = f"{model_name}_{rev}"
ds = load_from_disk(f"{path}/{pattern}")
ds_list.append(ds)
if metadata is None:
with open(f"{path}/{pattern}/{ds.info.description}") as f:
metadata = json.load(f)
write_dataset_and_metadata(ds_list, metadata, f"{path}/{model_name}_{suffix}")
def merge_datasets(model_name_list, path="histos", out_name="merged", suffix=None):
"""Merge multiple model datasets into a single combined dataset.
Args:
model_name_list: List of model names (or patterns) to merge
path: Base directory containing the datasets
out_name: Name for the output merged dataset
suffix: Optional suffix to append to each model name pattern
"""
from datasets import load_from_disk
META_MERGE_KEY = "merged"
ds_list = []
combined_metadata = None
merged_dict = {}
for model_name in tqdm(model_name_list, desc="Processing models"):
pattern = model_name
if suffix is not None and isinstance(str, suffix):
pattern += "_" + suffix
ds = load_from_disk(f"{path}/{pattern}")
ds_list.append(ds)
# now the metadata
mf = f"{path}/{pattern}/{ds.info.description}"
with open(mf) as f:
metadata = json.load(f)
if combined_metadata is None:
combined_metadata = {
k: v for k, v in metadata.items() if k != META_MERGE_KEY
}
model_name = model_name.rstrip("_main")
if META_MERGE_KEY in metadata: # Flatten
for k, v in metadata[META_MERGE_KEY].items():
key = k
while key in merged_dict: # make key name unique
key = f"{model_name}_{key}"
merged_dict[key] = v
else:
merged_dict[model_name] = metadata
combined_metadata.update({META_MERGE_KEY: merged_dict})
write_dataset_and_metadata(ds_list, combined_metadata, f"{path}/{out_name}")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--model", type=str, default="gpt2") # "pythia-70m-deduped")
parser.add_argument("--out", type=str, default="Drive/ana-002")
parser.add_argument("--cache", type=str, default="./model_data")
parser.add_argument("--clobber", type=bool, default=False)
parser.add_argument("--test", action="store_true", default=False)
parser.add_argument("--low-rank-svd", action="store_true", default=False, dest="low_rank_svd")
parser.add_argument("--top-k-svd", type=int, default=-1, dest="top_k_svd")
parser.add_argument("--resume-download", action="store_true", default=True, dest="resume_download")
parser.add_argument("--no-resume-download", action="store_false", dest="resume_download")
parser.add_argument("--max-workers", type=int, default=4, dest="max_workers")
parser.add_argument("--device", type=str, default=None, choices=["cuda", "mps", "cpu"])
args = parser.parse_args()
if args.test:
print("=" * 20 + "Test option selected" + "=" * 20)
print("\t\t" + "output and clobber options will be overwritten")
args.out, args.clobber = "test", True
cwd = os.getcwd()
out_dir = create_campaign(path=cwd, name=args.out, clobber=args.clobber, logs=True)
model_name = args.model
model_config = get_model_config(args.model)
revisions = model_config.revisions
if args.test:
revisions = revisions[-1:] if revisions else None
else:
from transformers import logging as hf_logging
hf_logging.set_verbosity_error()
import warnings
warnings.filterwarnings("ignore")
# loop on checkpoints
if revisions:
for revision in tqdm(revisions):
process_model(
model_name=model_name,
revision=revision,
out_dir=out_dir,
cache_dir=args.cache,
low_rank_svd_approximation=args.low_rank_svd,
top_k_svd=args.top_k_svd,
resume_download=args.resume_download,
max_workers=args.max_workers,
device=args.device,
)
else:
process_model(
model_name=model_name,
revision=None,
out_dir=out_dir,
cache_dir=args.cache,
low_rank_svd_approximation=args.low_rank_svd,
top_k_svd=args.top_k_svd,
resume_download=args.resume_download,
max_workers=args.max_workers,
device=args.device,
)