Spaces:
Running on Zero
Running on Zero
Delete jev_core.py
#3
by trapnohorizonte - opened
- 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|