latent_backtrack / scripts /analyze_offbyone.py
Avra98's picture
Add training code (same as GitHub reasoning-by-superposition-latent)
8f46582 verified
Raw
History Blame Contribute Delete
5.66 kB
#!/usr/bin/env python3
"""Characterize the +-1 off-diagonal entries in the latent confusion matrices.
Question: when latent slot m decodes a node at TRUE depth m-1 or m+1, is that
(a) a PHASE DRIFT -- the BFS chain slipped a step and stays shifted, or
(b) an ISOLATED SLIP -- slot m errs but slot m+1 is back on the diagonal, or
(c) a STALL -- slot m decodes the *same node* slot m-1 already decoded
(the chain failed to advance a hop)?
For each arm we report:
* histogram of offset = (true depth of decoded node) - m
* repeat rate: decoded(m) == decoded(m-1) [stall signature]
* drift persistence: P(slot m+1 also off by the same offset | slot m off by d)
vs the base rate. High persistence => real drift; low => isolated noise.
"""
import argparse
import json
from collections import Counter
import torch
from transformers import AutoModelForCausalLM, AutoConfig
from stokenizer import STokenizer
from coconut import Coconut
from scripts.probe_latents import build_prefix_tokens, node_depth_maps
ARMS = [
("Backtracking", "backtrack"),
("Current-stage-only", "curstage"),
("Retention-gated (no repair)", "accstage-nobt"),
]
def latest_ckpt(slug):
import os
d = f"ckpts/star-coconut-L10-bfs-{slug}"
cks = sorted((f for f in os.listdir(d) if f.startswith("checkpoint_")),
key=lambda x: int(x.split("_")[1]))
return os.path.join(d, cks[-1])
@torch.no_grad()
def decode_chains(ckpt, val_path, model_id, L, device, batch_size=64):
"""Return list of per-sample dicts: decoded node id + its true depth per slot."""
tok = STokenizer()
latent_id = tok.convert_tokens_to_ids("<|latent|>")
base = AutoModelForCausalLM.from_config(AutoConfig.from_pretrained(model_id))
model = Coconut(base, latent_id,
tok.convert_tokens_to_ids("<|start-latent|>"),
tok.convert_tokens_to_ids("<|end-latent|>"),
tok.eos_token_id)
model.load_state_dict(torch.load(ckpt, map_location="cpu"), strict=False)
model.to(device).eval()
data = json.load(open(val_path))
chains = []
for i in range(0, len(data), batch_size):
batch = data[i:i + batch_size]
input_ids = torch.tensor([build_prefix_tokens(s, tok) + [latent_id] * L
for s in batch], device=device)
attn = torch.ones_like(input_ids)
pos = torch.arange(input_ids.shape[1], device=device).unsqueeze(0).expand(len(batch), -1)
logits = model.forward(input_ids, attn, input_ids.clone(), pos).logits
for bi, s in enumerate(batch):
role = node_depth_maps(s, L)
root_pos = len(build_prefix_tokens(s, tok)) - 1
nodes, depths = [], []
for m in range(1, L + 1):
pred = int(torch.argmax(logits[bi, root_pos + (m - 1)]).item())
r = role.get(pred)
nodes.append(pred)
# depth is None for decoy/off-graph (we only study in-component drift)
depths.append(r[1] if (r is not None and r[0] == "pos") else None)
chains.append({"nodes": nodes, "depths": depths})
return chains
def analyze(chains, L):
offsets = Counter()
repeat = 0
slot_total = 0
# drift persistence
off_then_off_same = 0
off_then_diag = 0
off_events = 0
for c in chains:
for m in range(1, L + 1):
d = c["depths"][m - 1]
slot_total += 1
if d is None:
offsets["decoy/off"] += 1
continue
offsets[d - m] += 1
if m >= 2 and c["nodes"][m - 1] == c["nodes"][m - 2]:
repeat += 1
# persistence: slot m off by o, what does slot m+1 do?
for m in range(1, L):
d, d2 = c["depths"][m - 1], c["depths"][m]
if d is None or d2 is None:
continue
o = d - m
if o == 0:
continue
off_events += 1
if d2 - (m + 1) == o:
off_then_off_same += 1
elif d2 - (m + 1) == 0:
off_then_diag += 1
return offsets, repeat, slot_total, off_events, off_then_off_same, off_then_diag
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--val", default="data/star_2arm_L10_valid_fo_bfs.json")
ap.add_argument("--model_id", default="configs/symbol-2layer-8head-768dim-L20.json")
ap.add_argument("--L", type=int, default=10)
ap.add_argument("--device", default="cuda:0")
args = ap.parse_args()
for label, slug in ARMS:
ck = latest_ckpt(slug)
chains = decode_chains(ck, args.val, args.model_id, args.L, args.device)
offs, repeat, tot, oe, same, diag = analyze(chains, args.L)
print(f"\n===== {label} ({ck}) =====")
print(f"total latent slots decoded: {tot}")
print("offset (true_depth - slot) histogram:")
for k in sorted([k for k in offs if isinstance(k, int)]):
print(f" {k:+d}: {offs[k]:>6} ({offs[k]/tot:.4%})")
print(f" decoy/off-graph: {offs['decoy/off']:>6} ({offs['decoy/off']/tot:.4%})")
print(f"stall (decoded node == previous slot's node): {repeat} "
f"({repeat/tot:.4%})")
if oe:
print(f"drift persistence: of {oe} off-diagonal in-component events, "
f"{same} ({same/oe:.1%}) kept the SAME offset at slot m+1, "
f"{diag} ({diag/oe:.1%}) snapped back to the diagonal")
else:
print("drift persistence: no off-diagonal in-component events")
if __name__ == "__main__":
main()