322 lines
9.7 KiB
Python
322 lines
9.7 KiB
Python
"""SVD effective rank & weight statistics analysis for model checkpoints."""
|
||
|
||
import argparse
|
||
import json
|
||
from pathlib import Path
|
||
|
||
import safetensors.torch
|
||
import torch
|
||
|
||
|
||
def effective_rank_metrics(w: torch.Tensor) -> dict:
|
||
if w.ndim == 1:
|
||
return {"shape": tuple(w.shape), "is_1d": True}
|
||
|
||
w = w.float()
|
||
s = torch.linalg.svdvals(w)
|
||
s_sq = s**2
|
||
total = s_sq.sum()
|
||
cumsum = torch.cumsum(s_sq, dim=0) / total
|
||
|
||
min_dim = min(w.shape[0], w.shape[1])
|
||
er_90 = (cumsum < 0.90).sum().item() + 1
|
||
er_95 = (cumsum < 0.95).sum().item() + 1
|
||
er_99 = (cumsum < 0.99).sum().item() + 1
|
||
|
||
p = s_sq / total
|
||
p = p[p > 1e-30]
|
||
entropy = -(p * torch.log(p)).sum()
|
||
entropic_rank = torch.exp(entropy).item()
|
||
|
||
return {
|
||
"shape": tuple(w.shape),
|
||
"min_dim": min_dim,
|
||
"er_90": er_90,
|
||
"er_95": er_95,
|
||
"er_99": er_99,
|
||
"er_99_norm": er_99 / min_dim,
|
||
"er_95_norm": er_95 / min_dim,
|
||
"entropic_rank": entropic_rank,
|
||
"entropic_rank_norm": entropic_rank / min_dim,
|
||
"top1_ratio": s[0].item() / s.sum().item(),
|
||
"top5_ratio": s[:5].sum().item() / s.sum().item(),
|
||
"decay_ratio": s[-1].item() / s[0].item(),
|
||
"condition_number": s[0].item() / s[-1].item(),
|
||
"mean": w.mean().item(),
|
||
"std": w.std().item(),
|
||
"min": w.min().item(),
|
||
"max": w.max().item(),
|
||
}
|
||
|
||
|
||
def format_header(headers: list[str], widths: list[int]) -> str:
|
||
return "".join(h.ljust(w) for h, w in zip(headers, widths))
|
||
|
||
|
||
def format_row(values: list[str], widths: list[int]) -> str:
|
||
return "".join(v.ljust(w) for v, w in zip(values, widths))
|
||
|
||
|
||
def group_by_component(results: dict[str, dict]) -> dict[str, list[dict]]:
|
||
groups: dict[str, list[dict]] = {}
|
||
for key, r in results.items():
|
||
parts = key.split(".")
|
||
if parts[0] == "layers" and len(parts) >= 3:
|
||
sub = parts[2:]
|
||
if sub[0] == "attention":
|
||
comp = f"attn.{sub[1]}"
|
||
elif sub[0] == "mlp":
|
||
comp = f"mlp.{sub[1]}"
|
||
elif sub[0] == "input_norm":
|
||
comp = "input_norm"
|
||
elif sub[0] == "post_attention_norm":
|
||
comp = "post_attn_norm"
|
||
else:
|
||
comp = ".".join(sub)
|
||
else:
|
||
comp = key
|
||
groups.setdefault(comp, []).append(r)
|
||
return groups
|
||
|
||
|
||
def print_component_summary(results: dict[str, dict], title: str):
|
||
groups = group_by_component(results)
|
||
matrix_groups = {
|
||
k: [v for v in vs if not v.get("is_1d")]
|
||
for k, vs in groups.items()
|
||
if any(not v.get("is_1d") for v in vs)
|
||
}
|
||
|
||
widths = [20, 12, 12, 12, 12, 12]
|
||
print(f"\n{title}")
|
||
print(
|
||
format_header(
|
||
["Component", "N", "ER@99%", "EntRank%", "Top1 σ(%)", "Cond. Num"], widths
|
||
)
|
||
)
|
||
print("-" * sum(widths))
|
||
|
||
for name in sorted(matrix_groups.keys()):
|
||
items = matrix_groups[name]
|
||
n = len(items)
|
||
print(
|
||
format_row(
|
||
[
|
||
name,
|
||
str(n),
|
||
f"{sum(r['er_99_norm'] for r in items) / n:.4f}",
|
||
f"{sum(r['entropic_rank_norm'] for r in items) / n:.4f}",
|
||
f"{sum(r['top1_ratio'] for r in items) / n:.4f}",
|
||
f"{sum(r['condition_number'] for r in items) / n:.1f}",
|
||
],
|
||
widths,
|
||
)
|
||
)
|
||
|
||
all_er = [
|
||
r["er_99_norm"]
|
||
for vs in matrix_groups.values()
|
||
for r in vs
|
||
if not r.get("is_1d")
|
||
]
|
||
if all_er:
|
||
m = sum(all_er) / len(all_er)
|
||
print(f"\n Overall Mean ER@99: {m:.4f} ({m * 100:.1f}% of dimension)")
|
||
if m > 0.85:
|
||
print(" → HIGH utilization: model near capacity → need more params")
|
||
elif m > 0.5:
|
||
print(" → MODERATE utilization: some headroom left")
|
||
else:
|
||
print(" → LOW utilization: significant unused capacity")
|
||
|
||
|
||
def print_layer_grid(results: dict[str, dict]):
|
||
comps = [
|
||
"attn.q_proj",
|
||
"attn.k_proj",
|
||
"attn.v_proj",
|
||
"attn.o_proj",
|
||
"mlp.up",
|
||
"mlp.gate",
|
||
"mlp.down",
|
||
]
|
||
widths = [6] + [10] * len(comps)
|
||
metric = "er_99_norm"
|
||
|
||
print(f"\n--- Per-Layer Effective Rank (99% energy) ---")
|
||
print(format_header(["Layer"] + comps, widths))
|
||
print("-" * sum(widths))
|
||
|
||
layer_data: dict[int, dict[str, dict]] = {}
|
||
for key, r in results.items():
|
||
parts = key.split(".")
|
||
if parts[0] != "layers":
|
||
continue
|
||
li = int(parts[1])
|
||
sub = parts[2:]
|
||
if sub[0] == "attention":
|
||
cname = f"attn.{sub[1]}"
|
||
elif sub[0] == "mlp":
|
||
cname = f"mlp.{sub[1]}"
|
||
else:
|
||
continue
|
||
layer_data.setdefault(li, {})[cname] = r
|
||
|
||
for li in sorted(layer_data):
|
||
values = [str(li)]
|
||
for c in comps:
|
||
v = layer_data[li].get(c, {}).get(metric, 0)
|
||
values.append(f"{v:.4f}")
|
||
print(format_row(values, widths))
|
||
|
||
|
||
def print_weight_stats(results: dict[str, dict]):
|
||
groups = group_by_component(results)
|
||
widths = [20, 12, 12, 12, 12]
|
||
print(f"\n--- Weight Value Statistics ---")
|
||
print(format_header(["Component", "Mean", "Std", "Min", "Max"], widths))
|
||
print("-" * sum(widths))
|
||
|
||
for name in sorted(groups.keys()):
|
||
items = groups[name]
|
||
means = [r.get("mean", 0) for r in items]
|
||
stds = [r.get("std", 0) for r in items]
|
||
mins = [r.get("min", 0) for r in items]
|
||
maxs = [r.get("max", 0) for r in items]
|
||
g_mean = sum(means) / len(means)
|
||
g_std = sum(stds) / len(stds)
|
||
g_min = min(mins)
|
||
g_max = max(maxs)
|
||
print(
|
||
format_row(
|
||
[
|
||
name,
|
||
f"{g_mean:.6f}",
|
||
f"{g_std:.6f}",
|
||
f"{g_min:.6f}",
|
||
f"{g_max:.6f}",
|
||
],
|
||
widths,
|
||
)
|
||
)
|
||
|
||
|
||
def print_params_summary(results: dict[str, dict]):
|
||
total_2d = sum(
|
||
r["shape"][0] * r["shape"][1] for r in results.values() if not r.get("is_1d")
|
||
)
|
||
total_1d = sum(r["shape"][0] for r in results.values() if r.get("is_1d"))
|
||
print(f"\n Total 2D params: {total_2d:,}")
|
||
print(f" Total 1D params: {total_1d:,}")
|
||
print(f" Total params: {total_2d + total_1d:,}")
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(
|
||
description="SVD effective rank & weight statistics of a model checkpoint."
|
||
)
|
||
parser.add_argument(
|
||
"--ckpt_dir",
|
||
type=str,
|
||
required=True,
|
||
help="Path to checkpoint directory (containing model.safetensors + config.json).",
|
||
)
|
||
parser.add_argument(
|
||
"--compare",
|
||
type=str,
|
||
nargs="*",
|
||
help="Additional checkpoint directories to compare against.",
|
||
)
|
||
parser.add_argument(
|
||
"--no_svd",
|
||
action="store_true",
|
||
help="Skip SVD analysis, only show weight statistics (mean/std/min/max).",
|
||
)
|
||
parser.add_argument(
|
||
"--output",
|
||
type=str,
|
||
default=None,
|
||
help="Save results as JSON to this path.",
|
||
)
|
||
args = parser.parse_args()
|
||
|
||
all_results = {}
|
||
|
||
def analyze_one(ckpt_dir: str, label: str):
|
||
ckpt_dir = Path(ckpt_dir)
|
||
weights_path = ckpt_dir / "model.safetensors"
|
||
if not weights_path.exists():
|
||
print(f"ERROR: {weights_path} not found")
|
||
return {}
|
||
|
||
meta = {}
|
||
meta_path = ckpt_dir / "meta.json"
|
||
if meta_path.exists():
|
||
with open(meta_path) as f:
|
||
meta = json.load(f)
|
||
|
||
print(f"\n{'=' * 70}")
|
||
print(f" {label}: {ckpt_dir}")
|
||
if meta:
|
||
print(
|
||
f" Iteration: {meta.get('iteration', '?')}, "
|
||
f"Strategy: {meta.get('strategy', '?')}, "
|
||
f"nprocs={meta.get('nprocs', '?')}"
|
||
)
|
||
print(f"{'=' * 70}")
|
||
|
||
print(f"Loading weights...")
|
||
sd = safetensors.torch.load_file(str(weights_path))
|
||
print(f" {len(sd)} keys loaded")
|
||
|
||
weight_keys = [
|
||
k
|
||
for k in sd
|
||
if ".weight" in k and "rotary_embedding" not in k and "freqs_cis" not in k
|
||
]
|
||
|
||
results = {}
|
||
if not args.no_svd:
|
||
print(f"Computing SVD on {len(weight_keys)} tensors...")
|
||
for i, k in enumerate(sorted(weight_keys)):
|
||
print(f" [{i + 1}/{len(weight_keys)}] {k:<60s}", end="\r")
|
||
results[k] = effective_rank_metrics(sd[k])
|
||
print()
|
||
else:
|
||
print(f"Computing stats on {len(weight_keys)} tensors (no SVD)...")
|
||
for i, k in enumerate(sorted(weight_keys)):
|
||
t = sd[k]
|
||
results[k] = {
|
||
"shape": tuple(t.shape),
|
||
"is_1d": t.ndim == 1,
|
||
"mean": t.float().mean().item(),
|
||
"std": t.float().std().item(),
|
||
"min": t.float().min().item(),
|
||
"max": t.float().max().item(),
|
||
}
|
||
|
||
print_params_summary(results)
|
||
if not args.no_svd:
|
||
print_component_summary(
|
||
results, "\n=== SVD Effective Rank by Component ==="
|
||
)
|
||
print_layer_grid(results)
|
||
print_weight_stats(results)
|
||
all_results[label] = results
|
||
return results
|
||
|
||
analyze_one(args.ckpt_dir, "Primary")
|
||
|
||
if args.compare:
|
||
for cdir in args.compare:
|
||
analyze_one(cdir, f"Compare_{cdir}")
|
||
|
||
if args.output:
|
||
with open(args.output, "w", encoding="utf-8") as f:
|
||
json.dump(all_results, f, indent=2)
|
||
print(f"\nResults saved to {args.output}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|