jlens-looped-transformers / code /analyze_jcompare.py
zigzag9939's picture
Add files using upload-large-folder tool
e418394 verified
Raw History Blame Contribute Delete
5.44 kB
"""Compare the four per-loop-target mean Jacobians in loop-relative coordinates.
For loop k (target t_k = 48k-1) and physical layer L, take J_k[L] = J_{v->t_k}
with v = 48(k-1)+L. Matched-L cross-loop pairs share distance-to-target
(47 - L), so this comparison is distance-controlled by construction.
"""
import json
import torch
import torch.nn.functional as F
from jlens.lens import JacobianLens
torch.set_grad_enabled(False)
dev = "cuda" if torch.cuda.is_available() else "cpu"
LENSES = {
1: "jacobian_lens_ouro-2.6b_t47.pt",
2: "jacobian_lens_ouro-2.6b_t95.pt",
3: "jacobian_lens_ouro-2.6b_t143.pt",
4: "jacobian_lens_ouro-2.6b.pt",
}
lens = {k: JacobianLens.load(p) for k, p in LENSES.items()}
d = lens[4].jacobians[0].shape[0]
LS = list(range(47))
M = {k: {L: lens[k].jacobians[48 * (k - 1) + L].float().to(dev) for L in LS}
for k in lens}
print({k: str(lens[k]) for k in lens}, flush=True)
def vcos(a, b):
return F.cosine_similarity(a.flatten(), b.flatten(), 0).item()
R = 32
svd = {}
shape = {}
for k in M:
for L in LS:
J = M[k][L]
U, S, Vh = torch.linalg.svd(J)
svd[k, L] = (U[:, :R].contiguous(), S.cpu(), Vh[:R].contiguous())
p = S / S.sum()
fro = J.norm().item()
shape[k, L] = {
"fro": fro / d ** 0.5,
"smax": S[0].item(),
"erank": torch.exp(-(p * p.log()).sum()).item(),
"stable_rank": (fro ** 2 / S[0].item() ** 2),
"id_cos": (torch.trace(J) / (d ** 0.5 * fro)).item(),
"sym_frac": (((J + J.T) / 2).norm() ** 2 / fro ** 2).item(),
"diag_frac": (J.diagonal().norm() ** 2 / fro ** 2).item(),
}
print("svd done", flush=True)
pairs = [(a, b) for a in range(1, 5) for b in range(a + 1, 5)]
res = {"matched": {}, "sub_r": {}, "sub_l": {}, "pairwise": {}}
for L in LS:
cs, sr, sl = [], [], []
for a, b in pairs:
c = vcos(M[a][L], M[b][L])
cs.append(c)
res["pairwise"].setdefault(f"{a}-{b}", {})[L] = c
Va, Vb = svd[a, L][2], svd[b, L][2]
Ua, Ub = svd[a, L][0], svd[b, L][0]
sr.append(((Va @ Vb.T).norm() ** 2 / R).item())
sl.append(((Ua.T @ Ub).norm() ** 2 / R).item())
res["matched"][L] = sum(cs) / len(cs)
res["sub_r"][L] = sum(sr) / len(sr)
res["sub_l"][L] = sum(sl) / len(sl)
adj = [vcos(M[k][L], M[k][L + 1]) for k in M for L in LS[:-1]]
off = {}
for dL in (2, 4, 8):
xs = [vcos(M[a][L], M[b][L + dL]) for a, b in pairs for L in LS
if L + dL in LS]
off[dL] = sum(xs) / len(xs)
g = torch.Generator().manual_seed(0)
rnd = []
for _ in range(300):
a, b = pairs[torch.randint(6, (1,), generator=g).item()]
L1 = torch.randint(47, (1,), generator=g).item()
L2 = torch.randint(47, (1,), generator=g).item()
if L1 != L2:
rnd.append(vcos(M[a][L1], M[b][L2]))
print("\n=== cross-loop matched-L (distance-controlled) ===", flush=True)
print(" L | mcos subR subL | erank k=1..4 | idcos k=1..4", flush=True)
for L in LS:
er = " ".join(f"{shape[k, L]['erank']:5.0f}" for k in range(1, 5))
ic = " ".join(f"{shape[k, L]['id_cos']:.2f}" for k in range(1, 5))
print(f" {L:3d} | {res['matched'][L]:.2f} {res['sub_r'][L]:.2f} "
f"{res['sub_l'][L]:.2f} | {er} | {ic}", flush=True)
band = [L for L in LS if 24 <= L <= 40]
outb = [L for L in LS if L < 24]
print("\n=== summary ===", flush=True)
print(f"matched-L cos: all {sum(res['matched'].values())/47:.3f} "
f"band(24-40) {sum(res['matched'][L] for L in band)/len(band):.3f} "
f"early(<24) {sum(res['matched'][L] for L in outb)/len(outb):.3f}", flush=True)
print(f"baselines: within-loop adjacent-L {sum(adj)/len(adj):.3f} "
f"cross-loop offset dL=2 {off[2]:.3f} dL=4 {off[4]:.3f} dL=8 {off[8]:.3f} "
f"random {sum(rnd)/len(rnd):.3f}", flush=True)
print(f"subspace overlap r={R} (random baseline {R/d:.3f}): "
f"right {sum(res['sub_r'].values())/47:.3f} "
f"left {sum(res['sub_l'].values())/47:.3f}", flush=True)
print("per-pair mean matched-L cos:", flush=True)
for a, b in pairs:
m = sum(res["pairwise"][f"{a}-{b}"].values()) / 47
print(f" loop{a}-loop{b}: {m:.3f}", flush=True)
print("\n=== chain-rule factorisation: J(v->191) vs J(mid->191) @ J(v->mid) ===",
flush=True)
res["compose"] = {}
for mid, km in ((95, 2), (143, 3)):
Jm = lens[4].jacobians[mid].float().to(dev)
for v in (8, 24, 32, 40, 56, 72, 80, 88, 104, 120, 128, 136):
if v >= mid:
continue
Jv = lens[km].jacobians[v].float().to(dev)
direct = lens[4].jacobians[v].float().to(dev)
comp = Jm @ Jv
c = vcos(direct, comp)
nr = (comp.norm() / direct.norm()).item()
res["compose"][f"{v}->{mid}->191"] = {"cos": c, "norm_ratio": nr}
print(f" v{v:3d} ->{mid}->191: cos {c:.3f} "
f"|comp|/|direct| {nr:.2f}", flush=True)
json.dump(
{"matched": res["matched"], "sub_r": res["sub_r"], "sub_l": res["sub_l"],
"pairwise": res["pairwise"], "compose": res["compose"],
"baselines": {"adjacent": sum(adj) / len(adj),
"offset": off, "random": sum(rnd) / len(rnd)},
"shape": {f"{k},{L}": shape[k, L] for k, L in shape},
"spectra_top16": {f"{k},{L}": svd[k, L][1][:16].tolist()
for k, L in svd}},
open("jcompare_analysis.json", "w"), default=float)
print("\nDONE_JCOMPARE", flush=True)