Add exact eval script (val PPL + generation + degeneracy check)

#4
by Compactbot - opened
Files changed (1) hide show
  1. eval_compactlm5m.py +93 -0
eval_compactlm5m.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Fresh eval for CompactLM-5M: val perplexity + multi-prompt generation + degeneracy check.
3
+ Imports the model class from train_compactlm5m.py so we load the EXACT architecture.
4
+ """
5
+ import os, sys, math, json, re
6
+ import numpy as np
7
+ import torch
8
+
9
+ sys.path.insert(0, "/work")
10
+ from train_compactlm5m import CompactLM, load_tok, CTX
11
+
12
+ OUT = "/work/models/compactlm-5m"
13
+ TOK = load_tok()
14
+ VOCAB = 12288
15
+
16
+ # Load best.pt
17
+ ck = torch.load(os.path.join(OUT, "best.pt"), map_location="cpu", weights_only=False)
18
+ model = CompactLM(vocab=VOCAB, d=256, n_layers=4, n_heads=4, ff=640, ctx=CTX)
19
+ sd = ck["model"]
20
+ if hasattr(sd, "state_dict"):
21
+ sd = sd.state_dict()
22
+ missing, unexpected = model.load_state_dict(sd, strict=False)
23
+ print("missing:", missing)
24
+ print("unexpected:", unexpected)
25
+ model.eval()
26
+
27
+ print("n_params:", sum(p.numel() for p in model.parameters()))
28
+
29
+ # ---- val perplexity ----
30
+ val_npy = os.path.join(OUT, "data", "val.npy")
31
+ if os.path.exists(val_npy):
32
+ val = np.load(val_npy)
33
+ # subsample for speed: take up to 4096 windows
34
+ n_win = min(256, len(val))
35
+ idx = torch.from_numpy(val[:n_win]).long()
36
+ with torch.no_grad():
37
+ logits = model(idx)
38
+ loss = torch.nn.functional.cross_entropy(
39
+ logits[:, :-1].reshape(-1, VOCAB).float(),
40
+ idx[:, 1:].reshape(-1), ignore_index=-1)
41
+ ppl = math.exp(loss.item())
42
+ print(f"VAL: loss={loss.item():.4f} ppl={ppl:.2f} over {n_win*CTX:,} tok")
43
+ else:
44
+ print("no val.npy")
45
+
46
+ # ---- generation ----
47
+ prompts = [
48
+ "The cat sat on the",
49
+ "Once upon a time",
50
+ "The sun rises in the",
51
+ "I like to eat",
52
+ "Water boils at",
53
+ ]
54
+ results = []
55
+ for p in prompts:
56
+ ids = torch.tensor([TOK.encode(p, add_special_tokens=False).ids])
57
+ for seed in [0, 1, 2]:
58
+ out = model.generate(ids, max_new_tokens=64, temperature=0.8, top_k=40, seed=seed)
59
+ text = TOK.decode(out[0].tolist(), skip_special_tokens=True)
60
+ results.append({"prompt": p, "seed": seed, "text": text})
61
+ print(f"\n=== {p!r} seed={seed} ===\n{text}")
62
+
63
+ # ---- degeneracy check ----
64
+ def degenerate(text):
65
+ # repeated n-gram loop detection
66
+ words = text.split()
67
+ if len(words) < 6:
68
+ return False, "short"
69
+ # check for 3-gram repetition covering >60% of tail
70
+ tail = words[-40:]
71
+ seen = {}
72
+ rep = 0
73
+ for i in range(len(tail) - 2):
74
+ g = tuple(tail[i:i+3])
75
+ seen[g] = seen.get(g, 0) + 1
76
+ maxrep = max(seen.values())
77
+ frac = maxrep * 3 / len(tail)
78
+ return frac > 0.6, f"max3gram_frac={frac:.2f}"
79
+
80
+ degen_count = 0
81
+ for r in results:
82
+ d, why = degenerate(r["text"])
83
+ r["degenerate"] = d
84
+ r["why"] = why
85
+ if d:
86
+ degen_count += 1
87
+
88
+ print(f"\nDEGENERACY: {degen_count}/{len(results)} degenerate")
89
+ with open(os.path.join(OUT, "eval_fresh.json"), "w") as f:
90
+ json.dump({"val_ppl": ppl if os.path.exists(val_npy) else None,
91
+ "val_loss": loss.item() if os.path.exists(val_npy) else None,
92
+ "samples": results, "degenerate_count": degen_count}, f, indent=2)
93
+ print("wrote eval_fresh.json")