Files changed (1) hide show
  1. jev_core.py +0 -187
jev_core.py DELETED
@@ -1,187 +0,0 @@
1
- """JEV-9B inference core, ported 1:1 from the model authors' documented `transformers` +
2
- `peft` path (https://huggingface.co/autotrust/JEV-9B -> "Other ways to run it").
3
-
4
- The Blocks-of-Experts architecture:
5
- * backbone = frozen Qwen3.5-9B text tower (bit-identical to the release)
6
- * System 1 = LoRA r=16 (40.1M, shipped unmerged in `adapter/`) + a 24-slot fp32
7
- decision head (`head.safetensors`) initialised from `lm_head` verbalizer rows
8
- * System 2 = the original `lm_head`; used with the adapter off (pristine Qwen3.5-9B)
9
-
10
- System 1 answers a typed question (`noul` yes/no, `choice` over 2-16 options, `score` on a
11
- 0-5 scale) in ONE prefill pass and returns a calibrated probability distribution. The
12
- decision prompt follows the authors' "bare-v1" template; the head is applied to the last
13
- token's post-norm hidden state, divided by the per-kind calibration temperature.
14
- """
15
-
16
- import json
17
- import os
18
-
19
- import torch
20
- from huggingface_hub import hf_hub_download
21
- from peft import PeftModel
22
- from safetensors.torch import load_file
23
- from transformers import AutoModelForCausalLM, AutoTokenizer
24
-
25
- REPO = "autotrust/JEV-9B"
26
- LETTERS = "ABCDEFGHIJKLMNOP"
27
- KINDS = ("noul", "choice", "score")
28
- MAX_STATE_TOKENS = 1024 # the authors truncate long inputs to 1,024 tokens (head 60% / tail 40%)
29
- DEVICE = os.environ.get("JEV_DEVICE", "cuda")
30
- # Optional local-dir override (used only for offline verification of the wiring).
31
- _LOCAL = os.environ.get("JEV_MODEL_PATH")
32
-
33
-
34
- def _file(name: str) -> str:
35
- """Resolve a model-repo file to a local path, falling back to the Hub."""
36
- if _LOCAL:
37
- p = os.path.join(_LOCAL, name)
38
- if os.path.exists(p):
39
- return p
40
- return hf_hub_download(REPO, name)
41
-
42
-
43
- _SRC = _LOCAL or REPO
44
-
45
- # ---------------------------------------------------------------------------
46
- # Model loading. On ZeroGPU the final `_peft.to(DEVICE)` is intercepted at
47
- # startup: tensors are packed to disk and streamed into VRAM on the first
48
- # @spaces.GPU entry, so the load cost is never paid inside a request.
49
- #
50
- # Everything is built on CPU first, then moved once:
51
- # * peft's default would be `torch_device=infer_device()` -> "cuda", making
52
- # safetensors read the adapter straight onto the GPU. That bypasses the
53
- # packer and crashes the GPU-less main process ("No CUDA GPUs are
54
- # available"). `torch_device="cpu"` keeps the read on the CPU.
55
- # * one `.to(DEVICE)` at the end moves every tensor (backbone, LoRA, head)
56
- # through the standard machinery.
57
- # ---------------------------------------------------------------------------
58
- _tok = AutoTokenizer.from_pretrained(_SRC)
59
- _base = AutoModelForCausalLM.from_pretrained(_SRC, dtype=torch.bfloat16)
60
-
61
- # Keep the adapter UNMERGED so System 2 (generation) runs with the adapter
62
- # disabled -- `with _peft.disable_adapter():` -- i.e. pristine Qwen3.5-9B.
63
- _peft = PeftModel.from_pretrained(_base, _SRC, subfolder="adapter", torch_device="cpu")
64
-
65
- # System 1 head: fp32 linear, 4096 -> 24 slots.
66
- _HEAD = torch.nn.Linear(4096, 24, dtype=torch.float32)
67
- _head_sd = load_file(_file("head.safetensors"))
68
- with torch.no_grad():
69
- _HEAD.weight.copy_(_head_sd["proj.weight"])
70
- _HEAD.bias.copy_(_head_sd["proj.bias"])
71
- _HEAD.eval()
72
-
73
- _JUDGE_CFG = json.load(open(_file("judge_config.json")))
74
- _CALIB = json.load(open(_file("calibration.json")))["per_kind"]
75
- SLOT_RANGES = {k: tuple(v) for k, v in _JUDGE_CFG["slots"]["ranges"].items()}
76
- TEMPERATURES = {k: float(_CALIB[k]) for k in KINDS}
77
-
78
- # Module handles: PeftModel -> LoraModel -> Qwen3_5ForCausalLM -> Qwen3_5TextModel.
79
- _CAUSAL = _peft.base_model.model # the Qwen3_5ForCausalLM with LoRA modules inside
80
- _BACKBONE = _CAUSAL.model # the text tower (LoRA active by default)
81
-
82
- # Single move of the whole tree (backbone + LoRA + head) to the compute device.
83
- _peft.to(DEVICE)
84
- _HEAD.to(DEVICE)
85
-
86
- # ---------------------------------------------------------------------------
87
- # Prompt template -- exactly the authors' `bare-v1` decision format.
88
- # ---------------------------------------------------------------------------
89
-
90
-
91
- def build_decision_prompt(kind: str, state: str, question: str, options) -> tuple:
92
- """Return (prompt_text, option_labels) in the authors' `bare-v1` format."""
93
- if kind == "noul":
94
- options = ["false", "true"]
95
- elif kind == "score":
96
- options = [str(i) for i in range(6)]
97
- options = list(options)
98
- if kind == "choice":
99
- lines = [f"{LETTERS[i]}) {o}" for i, o in enumerate(options)]
100
- else:
101
- lines = options
102
- prompt = (
103
- f"[kind] {kind}\n[state] {state}\n[question] {question}\n[options]\n"
104
- + "\n".join(lines)
105
- + "\n[decision]:"
106
- )
107
- return prompt, options
108
-
109
-
110
- def truncate_state(state: str) -> tuple:
111
- """Head-60% / tail-40% truncation of the state to `MAX_STATE_TOKENS` tokens.
112
-
113
- Returns (state_used, was_truncated, n_tokens).
114
- """
115
- ids = _tok(state, add_special_tokens=False)["input_ids"]
116
- if len(ids) <= MAX_STATE_TOKENS:
117
- return state, False, len(ids)
118
- keep_head = int(MAX_STATE_TOKENS * 0.6)
119
- keep_tail = MAX_STATE_TOKENS - keep_head
120
- return _tok.decode(ids[:keep_head] + ids[-keep_tail:], skip_special_tokens=True), True, len(ids)
121
-
122
-
123
- def decide(kind: str, state: str, question: str, options=None) -> dict:
124
- """Run ONE System 1 typed decision; return the calibrated probability distribution.
125
-
126
- This mirrors the model card's reference `decide()` byte for byte: one prefill pass
127
- through the backbone with the LoRA active, the 24-slot head applied to the last
128
- token's hidden state, per-kind temperature, softmax over the active slots.
129
- """
130
- if kind not in KINDS:
131
- raise ValueError(f"unknown kind {kind!r}")
132
- if kind == "choice":
133
- if not isinstance(options, (list, tuple)) or not 2 <= len(options) <= 16:
134
- raise ValueError("`choice` needs between 2 and 16 options")
135
- if len(set(options)) != len(options):
136
- raise ValueError("`choice` options must be unique")
137
- prompt, labels = build_decision_prompt(kind, state, question, options)
138
- device = _BACKBONE.embed_tokens.weight.device # resolve inside the GPU call
139
- ids = _tok(prompt, return_tensors="pt", add_special_tokens=False).to(device)
140
- with torch.no_grad():
141
- h = _BACKBONE(**ids).last_hidden_state[0, -1].float()
142
- z = _HEAD(h.to(_HEAD.weight.device)) # [24] fp32 logits
143
- lo = SLOT_RANGES[kind][0]
144
- z = z[lo : lo + len(labels)] / TEMPERATURES[kind]
145
- p = torch.softmax(z, dim=0)
146
- return {label: float(v) for label, v in zip(labels, p.tolist())}
147
-
148
-
149
- def n_state_tokens(state: str) -> int:
150
- return len(_tok(state, add_special_tokens=False)["input_ids"])
151
-
152
-
153
- # ---------------------------------------------------------------------------
154
- # System 2 -- ordinary generation from the pristine base model (adapter off).
155
- # ---------------------------------------------------------------------------
156
-
157
-
158
- def build_escalation_prompt(state: str, question: str, options) -> str:
159
- """The model card's confidence-gated escalation prompt (System 1 -> System 2)."""
160
- opts = [str(o) for o in options]
161
- if opts == ["false", "true"]:
162
- tail = "Answer `true` or `false` on the last line."
163
- else:
164
- tail = "Options: " + "; ".join(opts) + "\nThink it through, then give exactly one option on the last line."
165
- return f"{state}\n\nQuestion: {question}\n{tail}"
166
-
167
-
168
- def generate(state: str, question: str, options, max_new_tokens: int = 512,
169
- enable_thinking: bool = True) -> tuple:
170
- """System 2: generate a reasoned answer with the adapter OFF -- pristine Qwen3.5-9B.
171
-
172
- Returns (text, completed): `completed` is False when generation hit the token
173
- budget mid-sentence (the model is verbose in thinking mode), which the UI needs
174
- to avoid presenting a cut-off trace as a finished answer.
175
- """
176
- prompt = build_escalation_prompt(state, question, options)
177
- msgs = [{"role": "user", "content": prompt}]
178
- enc = _tok.apply_chat_template(
179
- msgs, add_generation_prompt=True, enable_thinking=enable_thinking,
180
- return_tensors="pt", return_dict=True,
181
- ).to(_BACKBONE.embed_tokens.weight.device)
182
- n_prompt = enc["input_ids"].shape[1]
183
- with torch.no_grad(), _peft.disable_adapter():
184
- out = _CAUSAL.generate(**enc, max_new_tokens=int(max_new_tokens), do_sample=False)
185
- text = _tok.decode(out[0, n_prompt:], skip_special_tokens=True)
186
- completed = out.shape[1] - n_prompt < int(max_new_tokens) # stopped before the cap
187
- return text.strip(), completed