from __future__ import annotations import argparse import json import os import sys from pathlib import Path import torch import torch.nn.functional as F sys.path.insert(0, str(Path(__file__).parent.parent)) from model_cpu_gpt2 import CPUGPT, FNOSeqMixer, GLAMixer, gpt2_small_config def load_checkpoint(ckpt: str, device: str) -> CPUGPT: cfg = gpt2_small_config(seq_len=1024) model = CPUGPT(cfg).to(device).eval() if not ckpt: print( "[interp] No checkpoint — using random init for filter analysis", flush=True ) return model if ckpt.startswith("s3://"): import boto3 bucket, key = ckpt[5:].split("/", 1) local = "/tmp/interp_ckpt.pt" print(f"[interp] Downloading s3://{bucket}/{key} ...", flush=True) boto3.client("s3").download_file(bucket, key, local) ckpt = local elif ckpt.startswith("hf://"): from huggingface_hub import hf_hub_download path = ckpt[5:] filename = path.rsplit("/", 1)[-1] repo = path.rsplit("/", 1)[0] print(f"[interp] Downloading hf://{repo}/{filename} ...", flush=True) ckpt = hf_hub_download( repo_id=repo, filename=filename, token=os.environ.get("HF_TOKEN") ) print(f"[interp] Loading checkpoint from {ckpt}", flush=True) raw = torch.load(ckpt, map_location="cpu", weights_only=False) state = raw.get("model", raw) state = { k.replace("module.", "").replace("_orig_mod.", ""): v for k, v in state.items() } if "wte.weight" in state: ckpt_n_embd = state["wte.weight"].shape[1] if ckpt_n_embd != cfg.n_embd: print( f"[interp] Checkpoint n_embd={ckpt_n_embd} differs from default {cfg.n_embd}; " f"rebuilding model to match checkpoint", flush=True, ) cfg = gpt2_small_config(seq_len=1024, n_embd=ckpt_n_embd) cfg.n_head = max(1, ckpt_n_embd // 64) for k, v in state.items(): if "filter_td" in k: cfg.fno_modes = v.shape[1] break model = CPUGPT(cfg).to(device).eval() model_state = model.state_dict() filtered = { k: v for k, v in state.items() if k in model_state and v.shape == model_state[k].shape } skipped = [k for k in state if k not in filtered and k in model_state] if skipped: print(f"[interp] Skipped {len(skipped)} size-mismatched keys", flush=True) model.load_state_dict(filtered, strict=False) print( f"[interp] Checkpoint loaded — {model.param_count() / 1e6:.1f}M params", flush=True, ) return model def analyze_fno_spectrum(model: CPUGPT, out_dir: Path) -> dict: print("[interp] Computing FNO filter spectra ...", flush=True) spectra = {} fno_layers = [(i, b) for i, b in enumerate(model.blocks) if not b.is_gla] for layer_idx, block in fno_layers: with torch.no_grad(): h = block.mixer.filter_td.float().cpu() C, M = h.shape h_pad = F.pad(h, (0, M)) Hf = torch.fft.rfft(h_pad, dim=1) mag = Hf.abs().mean(0).numpy().tolist() spectra[f"layer_{layer_idx}"] = { "magnitudes": mag, "n_modes": M, "n_channels": C, } out_json = out_dir / "fno_filter_spectrum.json" with open(out_json, "w") as f: json.dump(spectra, f, indent=2) print(f"[interp] FNO spectra saved → {out_json}", flush=True) try: import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np fig, axes = plt.subplots(3, 3, figsize=(12, 9)) axes = axes.flatten() for ax_i, (name, data) in enumerate(spectra.items()): if ax_i >= len(axes): break mag = np.array(data["magnitudes"]) freqs = np.arange(len(mag)) axes[ax_i].plot(freqs, mag, lw=1.2, color="#2196F3") axes[ax_i].set_title(name, fontsize=9) axes[ax_i].set_xlabel("freq index", fontsize=7) axes[ax_i].set_ylabel("|H(f)|", fontsize=7) axes[ax_i].set_yscale("log") axes[ax_i].grid(True, alpha=0.3) for ax_i in range(len(spectra), len(axes)): axes[ax_i].set_visible(False) fig.suptitle( "FNO Learned Filter Spectra (mean |H(f)| across channels)", fontsize=11 ) plt.tight_layout() out_png = out_dir / "fno_filter_spectrum.png" plt.savefig(out_png, dpi=150, bbox_inches="tight") plt.close() print(f"[interp] FNO spectrum plot saved → {out_png}", flush=True) except ImportError: print("[interp] matplotlib not available — JSON only", flush=True) return spectra SAMPLE_TEXTS = [ "The quick brown fox jumps over the lazy dog near the river bank.", "Machine learning models can now process language at superhuman speeds.", "Fourier analysis decomposes signals into their constituent frequencies.", ] def analyze_gla_gates(model: CPUGPT, device: str, out_dir: Path) -> dict: print("[interp] Capturing GLA gate activations ...", flush=True) from transformers import GPT2Tokenizer enc = GPT2Tokenizer.from_pretrained("gpt2") gate_captures: dict[str, list] = {} hooks = [] def make_hook(layer_name): def hook(module, args, output): x = args[0] with torch.no_grad(): log_g = -F.softplus(module.g_proj(x).float()) gate_captures[layer_name] = log_g[0].cpu().numpy().tolist() return hook gla_layers = [(i, b) for i, b in enumerate(model.blocks) if b.is_gla] for layer_idx, block in gla_layers: name = f"layer_{layer_idx}_gla" h = block.mixer.register_forward_hook(make_hook(name)) hooks.append(h) gla_chunk = getattr(model.cfg, "gla_chunk", 64) results = {} for text in SAMPLE_TEXTS: gate_captures.clear() tokens = enc.encode(text, add_special_tokens=False)[: model.cfg.seq_len] if len(tokens) % gla_chunk != 0 or len(tokens) == 0: pad_len = gla_chunk - (len(tokens) % gla_chunk) if pad_len == gla_chunk: pad_len = 0 tokens = tokens + [0] * pad_len if len(tokens) < gla_chunk: tokens = tokens + [0] * (gla_chunk - len(tokens)) idx = torch.tensor([tokens], dtype=torch.long, device=device) with torch.no_grad(): _ = model(idx) results[text[:40]] = {name: vals for name, vals in gate_captures.items()} for h in hooks: h.remove() out_json = out_dir / "gla_gate_heatmap.json" with open(out_json, "w") as f: json.dump(results, f, indent=2) print(f"[interp] GLA gate data saved → {out_json}", flush=True) try: import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np first_text = list(results.keys())[0] first_layer = list(results[first_text].keys())[0] gates = np.array(results[first_text][first_layer]) decay = np.exp(gates) enc2 = GPT2Tokenizer.from_pretrained("gpt2") tokens = enc2.encode(SAMPLE_TEXTS[0], add_special_tokens=False)[ : gates.shape[0] ] tok_labels = [enc2.decode([t]) for t in tokens] fig, ax = plt.subplots(figsize=(14, 4)) im = ax.imshow( decay.T, aspect="auto", cmap="RdYlGn", vmin=0, vmax=1, interpolation="nearest", ) ax.set_xticks(range(len(tok_labels))) ax.set_xticklabels(tok_labels, rotation=45, ha="right", fontsize=7) ax.set_ylabel("Head index", fontsize=9) ax.set_title( f"GLA Gate Decay Rates — {first_layer}\n(green=remember, red=forget)", fontsize=10, ) plt.colorbar(im, ax=ax, fraction=0.02, pad=0.01) plt.tight_layout() out_png = out_dir / "gla_gate_heatmap.png" plt.savefig(out_png, dpi=150, bbox_inches="tight") plt.close() print(f"[interp] GLA gate heatmap saved → {out_png}", flush=True) except ImportError: print("[interp] matplotlib not available — JSON only", flush=True) return results def main(): ap = argparse.ArgumentParser() ap.add_argument( "--ckpt", default="", help="Checkpoint path: local .pt, s3://bucket/key, or hf://repo/file", ) ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu") ap.add_argument("--out-dir", default="/tmp/interpretability") args = ap.parse_args() out_dir = Path(args.out_dir) out_dir.mkdir(parents=True, exist_ok=True) model = load_checkpoint(args.ckpt, args.device) analyze_fno_spectrum(model, out_dir) analyze_gla_gates(model, args.device, out_dir) print(f"\n[interp] All outputs in {out_dir}/", flush=True) for p in sorted(out_dir.iterdir()): print(f" {p.name} ({p.stat().st_size // 1024} KB)", flush=True) if __name__ == "__main__": main()