SargeDev commited on
Commit
0b1cc11
·
verified ·
1 Parent(s): 608685e

add read_decision.py

Browse files
Files changed (1) hide show
  1. read_decision.py +106 -0
read_decision.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """read_decision.py — System-1 typed-decision readout for the merged JEV-27B-Uncensored weights.
3
+
4
+ One prompt -> calibrated decision distribution, one forward pass, no adapter needed
5
+ (the decision adapter — backbone LoRA + lm_head LoRA — is ALREADY MERGED into these weights).
6
+
7
+ Protocol (identical to the eval that produced this repo's metrics table):
8
+ 1. Prompt template: "[kind] <kind>\n[state] <state>\n[question] <q>\n[options]\n <lines>\n[decision]:"
9
+ encoded with add_special_tokens=False, capped at 1024 tokens.
10
+ 2. Last position final hidden state h (pre-lm_head, after final norm).
11
+ 3. z = W @ h + b (W [24,5120], b [24] from head.safetensors)
12
+ slots: noul z[0:2] = false/true · score z[2:8] = 0..5 · choice z[8:24] = A..P
13
+ 4. p = softmax((z[slot] + bias[slot]) / T[kind])
14
+ T from calibration.json (AutoTrust fit) or calibration_mergedfit.json
15
+ (refit on these merged weights — recommended, recovers all ECE floors).
16
+
17
+ Usage:
18
+ python read_decision.py --head head.safetensors --decision-head decision_head.json \
19
+ --calibration calibration_mergedfit.json --model <merged-weights-dir-or-hf-repo> \
20
+ --kind choice --state "..." --question "..." --options "opt1,opt2"
21
+
22
+ Requires: transformers, torch, safetensors; a GPU (or ~55 GB CPU RAM) for the 27B.
23
+ """
24
+ import argparse, json, os
25
+ import torch
26
+ import torch.nn.functional as F
27
+
28
+ KIND_SLOTS = {"noul": (0, 2), "score": (2, 8), "choice": (8, 24)}
29
+
30
+
31
+ def build_prompt(kind, state, question, options):
32
+ if kind == "choice":
33
+ lines = [f"{chr(65+i)}) {o}" for i, o in enumerate(options[:16])]
34
+ else:
35
+ lines = [str(o) for o in options]
36
+ return (f"[kind] {kind}\n[state] {state}\n[question] {question}\n"
37
+ f"[options]\n" + "\n".join(lines) + "\n[decision]:")
38
+
39
+
40
+ def main():
41
+ ap = argparse.ArgumentParser()
42
+ ap.add_argument("--model", required=True, help="merged weights dir (or HF repo id)")
43
+ ap.add_argument("--head", default="head.safetensors")
44
+ ap.add_argument("--decision-head", default="decision_head.json")
45
+ # calibration.json is the fit PAIRED with this repo's head.safetensors
46
+ # (source: checkpoints/s2_27b/best, the same pair the 30k gate used).
47
+ # calibration_mergedfit.json ships too, but it was refit against a
48
+ # different merge (source: mergedfull 30k per-row banked) - only use it
49
+ # if you re-gated with it.
50
+ ap.add_argument("--calibration", default="calibration.json")
51
+ ap.add_argument("--kind", required=True, choices=list(KIND_SLOTS))
52
+ ap.add_argument("--state", default="")
53
+ ap.add_argument("--question", required=True)
54
+ ap.add_argument("--options", default="", help="comma-separated (choice kind)")
55
+ ap.add_argument("--maxtok", type=int, default=1024)
56
+ a = ap.parse_args()
57
+
58
+ lo, hi = KIND_SLOTS[a.kind]
59
+ options = [o.strip() for o in a.options.split(",") if o.strip()] if a.options else \
60
+ (["false", "true"] if a.kind == "noul" else [str(i) for i in range(hi - lo)])
61
+ n = min(len(options), 16) if a.kind == "choice" else (hi - lo)
62
+
63
+ from transformers import AutoTokenizer, AutoModelForCausalLM
64
+ from safetensors.torch import load_file
65
+
66
+ tok = AutoTokenizer.from_pretrained(a.model, trust_remote_code=True)
67
+ model = AutoModelForCausalLM.from_pretrained(
68
+ a.model, torch_dtype=torch.bfloat16, device_map="cuda",
69
+ trust_remote_code=True, low_cpu_mem_usage=True)
70
+ model.eval()
71
+
72
+ head = load_file(a.head)
73
+ W = head["proj.weight"].float().to(model.device)
74
+ bias = head["proj.bias"].float().to(model.device)
75
+ cal = json.load(open(a.calibration)) if os.path.exists(a.calibration) else {}
76
+ temps = cal.get("per_kind", cal.get("fit", {})) or {}
77
+ T = float(temps.get(a.kind, {}).get("T", temps.get(a.kind, 1.0)) if isinstance(temps.get(a.kind), dict)
78
+ else temps.get(a.kind, 1.0))
79
+
80
+ dh = json.load(open(a.decision_head))
81
+ verbalizer_ids = dh["verbalizer_ids"][lo:lo + n] # available but the head-projection path below is canonical
82
+
83
+ prompt = build_prompt(a.kind, a.state, a.question, options)
84
+ ids = tok(prompt, add_special_tokens=False).input_ids[:a.maxtok]
85
+ enc = {"input_ids": torch.tensor([ids], device=model.device),
86
+ "attention_mask": torch.ones(1, len(ids), dtype=torch.long, device=model.device)}
87
+ with torch.inference_mode():
88
+ inner = model.model # bare merged weights (no peft): final-normed backbone, pre-lm_head
89
+ h = inner(input_ids=enc["input_ids"], attention_mask=enc["attention_mask"]).last_hidden_state
90
+ h = h[:, -1, :].float()
91
+ z = (W @ h.T).squeeze(1)
92
+ sel = torch.arange(lo, lo + n, device=model.device)
93
+ p = F.softmax((z[sel] + bias[sel]) / T, dim=0).tolist()
94
+
95
+ labels = [chr(65 + i) for i in range(n)] if a.kind == "choice" else \
96
+ (["false", "true"] if a.kind == "noul" else [str(i) for i in range(n)])
97
+ best = max(range(n), key=lambda i: p[i])
98
+ print(json.dumps({
99
+ "kind": a.kind, "T": T, "labels": labels,
100
+ "probabilities": [round(x, 5) for x in p],
101
+ "decision_index": best, "decision": labels[best],
102
+ "confidence": round(p[best], 5)}, indent=1))
103
+
104
+
105
+ if __name__ == "__main__":
106
+ main()