"""Loop convergence dynamics: does the recurrence reach a fixed point? For trained vs untrained merge, trace across iterations k: - cos(s_k, s_{k-1}) (mean over positions) -> fixed point if -> 1 - |s_k| / |e| -> norm control - P('spider') under the J-lens at L30 -> what the state converges TO Averaged over the spider prompt + a few GSM8K test questions. """ import json import sys from pathlib import Path import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import torch from loop_common import BandLooper, MergeAdapter, chat_prompt, DIRECT_SUFFIX sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from jlens.core import load_model # noqa: E402 OUT = Path(__file__).resolve().parent.parent / "results-loop" KMAX = 10 BLUE, GRAY = "#2b6cb0", "#8a8f98" @torch.no_grad() def trace(looper, adapter, tok, prompt_text): ids = tok(prompt_text, return_tensors="pt", add_special_tokens=False)["input_ids"].cuda() calls, _ = looper.capture(ids) e = looper._hin[looper.l0] s = looper.band(e, calls) rows = [] for k in range(1, KMAX + 1): new = looper.band(adapter(e, s), calls) cos = torch.nn.functional.cosine_similarity( new[0].float(), s[0].float(), dim=-1).mean().item() rows.append({"k": k, "cos": cos, "norm": (new.norm() / e.norm()).item()}) s = new return rows def main(): model, tok = load_model(dtype=torch.bfloat16) looper = BandLooper(model) gsm = [it for it in json.load(open(OUT / "star_data.json")) if it["split"] == "test"][:3] prompts = [chat_prompt(tok, "The animal that spins webs has how many legs? " "Answer with just the number.", "")] prompts += [chat_prompt(tok, it["question"], DIRECT_SUFFIX) for it in gsm] curves = {} for tag, path in (("untrained", None), ("trained", OUT / "adapter.pt")): adapter = MergeAdapter().cuda() if path: adapter.load_state_dict(torch.load(path, map_location="cuda")) traces = [trace(looper, adapter, tok, p) for p in prompts] curves[tag] = { "cos": [sum(t[i]["cos"] for t in traces) / len(traces) for i in range(KMAX)], "norm": [sum(t[i]["norm"] for t in traces) / len(traces) for i in range(KMAX)], } print(tag, "cos:", [round(c, 3) for c in curves[tag]["cos"]], flush=True) print(tag, "norm:", [round(c, 3) for c in curves[tag]["norm"]], flush=True) ks = list(range(1, KMAX + 1)) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4)) for ax in (ax1, ax2): ax.grid(True, color="#e5e5e5", lw=0.7) ax.set_axisbelow(True) for sp in ("top", "right"): ax.spines[sp].set_visible(False) ax.set_xticks(ks) ax.set_xlabel("loop iteration k") ax.axvspan(2, 4, color="#f2e8cf", alpha=0.45, zorder=0) for tag, c in (("trained", BLUE), ("untrained", GRAY)): ax1.plot(ks, curves[tag]["cos"], "-o", color=c, lw=2, ms=5, label=tag) ax2.plot(ks, curves[tag]["norm"], "-o", color=c, lw=2, ms=5, label=tag) ax1.set_ylabel("cos(s_k, s_{k−1}) (mean over positions)") ax1.set_title("Successive-state similarity: fixed point?", fontsize=11) ax1.legend(fontsize=8, frameon=False, loc="lower right") ax1.text(3, ax1.get_ylim()[0] + 0.02 * (ax1.get_ylim()[1] - ax1.get_ylim()[0]), "accuracy &\nsharpening plateau", fontsize=7.5, color="#7a5a00", ha="center") ax2.set_ylabel("|s_k| / |e|") ax2.set_title("State norm across iterations", fontsize=11) ax2.legend(fontsize=8, frameon=False) fig.suptitle("Loop dynamics (spider + 3 GSM8K prompts, mean)", fontsize=12, y=1.02) fig.tight_layout() fig.savefig(OUT / "loop_dynamics.png", dpi=140, bbox_inches="tight", facecolor="white") print("wrote", OUT / "loop_dynamics.png") if __name__ == "__main__": main()