training / inspect_trace.py
cazyundee's picture
tinychess: self-play research substrate (phase 1-4)
3495881 verified
Raw History Blame Contribute Delete
6.24 kB
#!/usr/bin/env python3
"""
Observability: dump the full recurrent trajectory for one position.
Emits a human-readable trace and/or machine-readable JSON containing
h0..hn, Δh_t, memory, routing weights, halting probabilities, per-step move
latents and candidate move scores.
python inspect_trace.py --checkpoint runs/x/latest.pt --fen "..." --steps 8
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import chess
import torch
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from tinychess.batching import encode_batch
from tinychess.checkpoint import load_model_only
@torch.no_grad()
def trace_position(model, board, steps=8, adaptive=False, topk=5):
pb = encode_batch([board], [board.peek() if board.move_stack else None])
out = model(**pb.model_args(), steps=steps, adaptive=adaptive, trace=True)
traj = out["trajectory"]
coll = out.get("collect", {}) or {}
moves = pb.real_moves[0]
rec = {
"fen": board.fen(),
"side_to_move": "white" if board.turn else "black",
"n_legal": int(pb.n_legal[0]),
"params": model.param_report()["total"],
"active_params": model.param_report()["active"],
"adaptive": adaptive,
"n_steps_used": float(out["n_steps"][0]),
"value": float(out["value"][0, 0]),
"steps": [],
}
prev = None
for t, h in enumerate(traj):
h0 = h[0]
s = {"t": t,
"h_norm": float(h0.norm()),
"h_mean": float(h0.mean()),
"h_std": float(h0.std()),
"active_units": int((h0.abs() > 1e-3).float().mean(0).gt(0.5).sum()),
}
if prev is not None:
d = h0 - prev
s["delta_norm"] = float(d.norm())
s["delta_rel"] = float(d.norm() / prev.norm().clamp_min(1e-6))
s["most_changed_squares"] = [
chess.square_name(i) for i in d.norm(dim=-1).topk(4).indices.tolist()]
prev = h0
i = t - 1
if 0 <= i < len(coll.get("router", [])):
w = coll["router"][i][0]
s["route"] = {op: round(float(v), 3)
for op, v in zip(model.core.ops, w.tolist())}
if 0 <= i < len(coll.get("halt", [])):
s["p_halt"] = round(float(coll["halt"][i][0]), 4)
if 0 <= i < len(coll.get("mem_attn", [])):
ma = coll["mem_attn"][i][0]
s["memory_attention"] = [round(float(v), 3) for v in ma.mean(0).tolist()]
rec["steps"].append(s)
if out.get("memory") is not None:
m = out["memory"][0]
rec["memory_final"] = {"n_slots": int(m.shape[0]),
"slot_norms": [round(float(v), 3) for v in m.norm(dim=-1)]}
# per-refinement-step move ranking
if "refine_logits" in out:
rec["refinement"] = []
for i, lg in enumerate(out["refine_logits"]):
if lg is None:
continue
p = torch.softmax(lg[0], -1)
k = min(topk, len(moves))
top = p.topk(k)
rec["refinement"].append({
"z_index": i,
"top": [{"move": moves[j].uci(), "p": round(float(v), 4)}
for v, j in zip(top.values, top.indices)]})
p = torch.softmax(out["logits"][0], -1)
k = min(topk, len(moves))
top = p.topk(k)
rec["final_policy"] = [{"move": moves[j].uci(), "p": round(float(v), 4)}
for v, j in zip(top.values, top.indices)]
rec["chosen_move"] = moves[int(p.argmax())].uci()
rec["policy_entropy"] = float(-(p * p.clamp_min(1e-12).log()).sum())
return rec
def human(rec) -> str:
L = []
L.append("=" * 72)
L.append(f"Position: {rec['fen']}")
L.append(f" side to move: {rec['side_to_move']} legal moves: {rec['n_legal']}")
L.append(f" params: {rec['params']:,} (active {rec['active_params']:,}) "
f"steps used: {rec['n_steps_used']:.2f} value: {rec['value']:+.3f}")
L.append("=" * 72)
for s in rec["steps"]:
L.append(f"Step {s['t']}")
L.append(f" |h|={s['h_norm']:.3f} mean={s['h_mean']:+.4f} std={s['h_std']:.4f}"
f" active units={s['active_units']}")
if "delta_norm" in s:
L.append(f" Δh={s['delta_norm']:.4f} (rel {s['delta_rel']:.3f}) "
f"changed: {', '.join(s['most_changed_squares'])}")
if "route" in s:
L.append(" route: " + " ".join(f"{k}={v:.2f}" for k, v in s["route"].items()))
if "p_halt" in s:
L.append(f" p_halt: {s['p_halt']:.3f}")
if "memory_attention" in s:
L.append(f" memory attn: {s['memory_attention']}")
if "memory_final" in rec:
L.append(f"\nMemory ({rec['memory_final']['n_slots']} slots) norms: "
f"{rec['memory_final']['slot_norms']}")
if "refinement" in rec:
L.append("\nMove-latent refinement:")
for r in rec["refinement"]:
tops = " ".join(f"{d['move']}:{d['p']:.3f}" for d in r["top"])
L.append(f" z{r['z_index']}: {tops}")
L.append("\nFinal policy:")
for d in rec["final_policy"]:
L.append(f" {d['move']} p={d['p']:.4f}")
L.append(f"\nFinal move: {rec['chosen_move']} entropy={rec['policy_entropy']:.3f}")
return "\n".join(L)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--checkpoint", required=True)
ap.add_argument("--fen", default=chess.STARTING_FEN)
ap.add_argument("--steps", type=int, default=8)
ap.add_argument("--adaptive", action="store_true")
ap.add_argument("--format", choices=["human", "json", "both"], default="human")
ap.add_argument("--out", default=None)
a = ap.parse_args()
model, cfg = load_model_only(a.checkpoint)
rec = trace_position(model, chess.Board(a.fen), a.steps, a.adaptive)
if a.format in ("human", "both"):
print(human(rec))
if a.format in ("json", "both"):
js = json.dumps(rec, indent=2)
if a.out:
open(a.out, "w").write(js)
print(f"\n[saved json] {a.out}")
elif a.format == "json":
print(js)
if __name__ == "__main__":
main()