decider-2b / source /src /decider /_core.py
Jeetu456's picture
Decider 2B
cd02163
Raw History Blame Contribute Delete
24.9 kB
"""Decider 2B: a decision model for Jev/SystemOne requests. One file, as released on Hugging Face.
Reads a state and a schema of typed questions (choice, score, noul) and returns a probability for
every allowed option of every question in one forward pass: no text generation, no parsing.
from decider_model import load_release_model, systemone
model = load_release_model(path, device="cuda")
systemone(model, {"state": "...", "questions": {...}})
Sections: the Jev/SystemOne request and response format, the token layout of a model pass, and the
model with its loader. The training code in training/ uses the same format and layout.
"""
import json
import math
import os
import re
# ================================================================================================
# Jev / SystemOne format
# ================================================================================================
QUESTION_TYPES = ("choice", "score", "noul")
MAX_REQUEST_TOKENS = 65536 # Jev: whole request
MAX_STATE_PLUS_QUESTION_TOKENS = 32768 # Jev: state plus the longest single question
MAX_QUESTIONS = 128 # as pplx-decider; Cloudflare allows 64
MAX_OPTIONS = 512
MAX_KEY_CHARS = 128
NOUL_OPTIONS = ("true", "false")
# Selective answering: when a choice offers "I don't know" and no answer reaches this probability,
# the model abstains. A wrong answer costs more than an abstention wherever hallucination is
# counted (AA-Omniscience-Decision: answering pays only when p > 1 / (2 - accuracy), about 0.55).
# 0 turns it off. Tuned with aa_eval/run_aa.py --evals omniscience_proxy.
ABSTAIN_THRESHOLD = float(os.environ.get("DECIDER_ABSTAIN_THRESHOLD", "0.55"))
ABSTAIN_TEXT = re.compile(r"^\s*(i\s*do\s*n'?\s*t\s*know|i\s*do\s*not\s*know)\s*[.!]?\s*$", re.I)
class SchemaError(ValueError):
"""The request or record does not follow the format; the message says where."""
def _text(value):
return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True)
def state_text(state):
"""One canonical text for a state, used in training and serving alike."""
if isinstance(state, str):
return state if state.strip() else "(empty)"
if isinstance(state, list) and all(isinstance(item, str) for item in state):
return "\n\n".join(state) if state else "(empty)"
return json.dumps(state, ensure_ascii=False, indent=2)
def is_empty_state(state):
return (isinstance(state, str) and not state.strip()) or (isinstance(state, (dict, list)) and not state)
def request_state_text(state, questions):
"""The state text a request is served with. When the state is empty, the content is in the
question instructions (as in some Jev benchmark requests), so those are shown as the state:
the model learned to read content from the state."""
if is_empty_state(state):
texts = list(dict.fromkeys(_text(q.get("instructions")).strip() for q in questions.values()
if q.get("instructions") is not None))
texts = [t for t in texts if t]
if texts:
return "\n\n".join(texts)
return state_text(state)
def level_text(level):
if isinstance(level, dict):
label = level.get("label")
extra = {k: v for k, v in level.items() if k != "label"}
return f"{label} ({_text(extra)})" if extra else str(label)
return str(level)
def validate_request(body):
"""Raise SchemaError unless body is a valid SystemOne request."""
if not isinstance(body, dict):
raise SchemaError("request must be a JSON object")
if "state" not in body:
raise SchemaError("missing 'state'")
state = body["state"]
if not isinstance(state, (str, dict, list)):
raise SchemaError("'state' must be a string, an object or a list of strings")
if isinstance(state, list) and not all(isinstance(item, (str, dict)) for item in state):
raise SchemaError("'state' list items must be strings or objects")
questions = body.get("questions")
if not isinstance(questions, dict) or not questions:
raise SchemaError("'questions' must be a non-empty object")
if len(questions) > MAX_QUESTIONS:
raise SchemaError(f"at most {MAX_QUESTIONS} questions per request")
for qid, question in questions.items():
where = f"questions.{qid}"
if not qid or len(qid) > MAX_KEY_CHARS:
raise SchemaError(f"{where}: question ids must be 1-{MAX_KEY_CHARS} characters")
if not isinstance(question, dict):
raise SchemaError(f"{where}: must be an object")
kind = question.get("type")
if kind not in QUESTION_TYPES:
raise SchemaError(f"{where}.type: must be one of {', '.join(QUESTION_TYPES)}")
# Jev requests may carry structured instructions (an object, e.g. a candidate document); any
# JSON value is read as its canonical text.
instructions = question.get("instructions")
criteria = question.get("criteria")
if kind == "choice":
if not isinstance(criteria, dict) or len(criteria) < 2:
raise SchemaError(f"{where}.criteria: a choice needs an object with at least 2 answers")
if len(criteria) > MAX_OPTIONS:
raise SchemaError(f"{where}.criteria: at most {MAX_OPTIONS} answers")
for key, description in criteria.items():
if not key or len(key) > MAX_KEY_CHARS:
raise SchemaError(f"{where}.criteria: answer keys must be 1-{MAX_KEY_CHARS} characters")
# A description may be structured too (a chord's pitch classes, a palette, a move in two
# notations); it is read as its canonical text.
elif kind == "score":
if not isinstance(criteria, list) or len(criteria) < 2:
raise SchemaError(f"{where}.criteria: a score needs a list of at least 2 levels, lowest first")
if len(criteria) > MAX_OPTIONS:
raise SchemaError(f"{where}.criteria: at most {MAX_OPTIONS} levels")
for i, level in enumerate(criteria):
if not (isinstance(level, str) and level.strip()) and not (
isinstance(level, dict) and isinstance(level.get("label"), str) and level["label"].strip()):
raise SchemaError(f"{where}.criteria[{i}]: a level is a non-empty string or an object with 'label'")
else:
if not (instructions and _text(instructions).strip()):
raise SchemaError(f"{where}.instructions: a noul question needs the statement to judge")
# Jev takes no criteria on noul; Clef allows optional true/false wording. Accept that shape
# (it is not used) so Clef-style clients work, and reject anything else.
if criteria is not None and not (isinstance(criteria, dict) and set(criteria) <= {"true", "false"}):
raise SchemaError(f"{where}.criteria: noul questions take no criteria")
def render(qid, question):
"""What the model reads for one question: prompt text, option lines, and the answer keys."""
kind = question["type"]
instructions = question.get("instructions")
text = _text(instructions).strip() if instructions is not None else ""
prompt = text or str(qid)
criteria = question.get("criteria")
if kind == "choice":
keys = list(criteria)
labels = [k if d in (None, "", k) else f"{k}: {_text(d)}" for k, d in criteria.items()]
elif kind == "score":
keys = list(range(len(criteria)))
labels = [f"level {i}: {level_text(level)}" for i, level in enumerate(criteria)]
prompt = f"{prompt}\n(Rate on the ordered levels below, lowest first.)"
else:
keys = [True, False]
labels = list(NOUL_OPTIONS)
prompt = prompt if prompt.endswith("?") else f"Is this statement true? {prompt}"
return {"id": qid, "kind": kind, "prompt": prompt, "labels": labels, "keys": keys}
def items(questions):
return [render(qid, question) for qid, question in questions.items()]
def answer(item, probabilities, criteria=None):
"""Jev answer object for one question from its option probabilities (in render order)."""
total = sum(probabilities) or 1.0
p = [float(x) / total for x in probabilities]
best = max(range(len(p)), key=p.__getitem__)
if item["kind"] == "choice" and criteria:
p, best = abstain(item["keys"], criteria, p, best)
if item["kind"] == "choice":
return {"type": "choice", "choice": item["keys"][best], "confidence": p[best],
"probabilities": dict(zip(item["keys"], p))}
if item["kind"] == "score":
return {"type": "score", "score": sum(i * v for i, v in enumerate(p)), "confidence": p[best],
"legend": {str(i): level_text(level) for i, level in enumerate(criteria)},
"probabilities": {str(i): v for i, v in enumerate(p)}}
return {"type": "noul", "noul": p[0]}
def abstain_index(keys, criteria):
"""Index of the option that means "I don't know", or None."""
for i, key in enumerate(keys):
description = criteria.get(key) if isinstance(criteria, dict) else None
if ABSTAIN_TEXT.match(str(description or "")) or ABSTAIN_TEXT.match(str(key).replace("_", " ")):
return i
return None
def abstain(keys, criteria, p, best, threshold=None):
"""(probabilities, best) after selective answering: below the threshold, "I don't know" wins.
The answers keep their relative probabilities; "I don't know" takes the mass needed to be
the highest, so the response stays a valid distribution.
"""
threshold = ABSTAIN_THRESHOLD if threshold is None else threshold
idk = abstain_index(keys, criteria)
if idk is None or threshold <= 0 or best == idk or p[best] >= threshold:
return p, best
others = 1.0 - p[idk]
top = max(v for i, v in enumerate(p) if i != idk)
if others <= 0:
return p, idk
scale = 0.9 / (top + others) # top * scale < 1 - others * scale, so "I don't know" is highest
q = [v * scale if i != idk else 0.0 for i, v in enumerate(p)]
q[idk] = 1.0 - sum(q)
return q, idk
def response(model, questions, probabilities, input_tokens):
return {"model": model,
"answers": {item["id"]: answer(item, probs, questions[item["id"]].get("criteria"))
for item, probs in zip(items(questions), probabilities)},
"usage": {"input_tokens": int(input_tokens), "output_tokens": 0}}
# ---- training records -------------------------------------------------------------------------
RECORD_FIELDS = ("id", "task", "source", "license", "state", "questions", "expected")
def validate_record(record):
"""Raise SchemaError unless the record is a valid request with a right answer for every question."""
for field in RECORD_FIELDS:
if field not in record:
raise SchemaError(f"record is missing '{field}'")
validate_request(record)
expected = record["expected"]
if not isinstance(expected, dict) or set(expected) != set(record["questions"]):
raise SchemaError("'expected' must have exactly one answer per question")
for qid, question in record["questions"].items():
value = expected[qid]
kind = question["type"]
if kind == "choice" and value not in question["criteria"]:
raise SchemaError(f"expected.{qid}: '{value}' is not one of the criteria keys")
if kind == "score" and not (isinstance(value, int) and not isinstance(value, bool)
and 0 <= value < len(question["criteria"])):
raise SchemaError(f"expected.{qid}: must be a level index 0..{len(question['criteria']) - 1}")
if kind == "noul" and not isinstance(value, bool):
raise SchemaError(f"expected.{qid}: must be true or false")
teacher = record.get("teacher")
if teacher is not None:
for qid, probs in teacher.items():
if qid not in record["questions"] or not isinstance(probs, dict):
raise SchemaError(f"teacher.{qid}: unknown question or not an object")
if not math.isclose(sum(probs.values()), 1.0, abs_tol=1e-2):
raise SchemaError(f"teacher.{qid}: probabilities must sum to 1")
def gold_index(item, value):
"""Position of the expected answer among the rendered options."""
return item["keys"].index(value)
def teacher_vector(item, probs):
"""Teacher probabilities in render order, or None."""
if not probs:
return None
if item["kind"] == "noul":
p = float(probs.get("true", probs.get(True, 0.0)))
return [p, 1.0 - p]
return [float(probs.get(str(key), probs.get(key, 0.0))) for key in item["keys"]]
# ================================================================================================
# Token layout of one model pass
# ================================================================================================
INTRO = "Read the state, then answer each question by choosing one of its options.\n\nState:\n"
class TooLong(ValueError):
pass
def question_pieces(k, item):
"""Text pieces of question k; marks say which pieces end at a scored token."""
pieces = [(f"\n\nQuestion {k + 1}: {item['prompt']}\nOptions:", None)]
pieces += [(f"\n[{i + 1}] {label}", "option") for i, label in enumerate(item["labels"])]
pieces.append((f"\nAnswer {k + 1}:", "answer"))
return pieces
def encode_pieces(tokenizer, texts):
return tokenizer(texts, add_special_tokens=False)["input_ids"] if texts else []
def question_tokens(tokenizer, items):
"""Token pieces per question, numbered as if each were first in its pass (k is fixed later)."""
return [list(zip(encode_pieces(tokenizer, [t for t, _ in question_pieces(0, item)]),
[m for _, m in question_pieces(0, item)])) for item in items]
def plan(tokenizer, state, items, max_length):
"""Split questions into passes that fit max_length with the whole state in each.
Returns (passes, state_tokens, question_sizes); each pass is a list of item indices.
Raises TooLong with the index of the first question that cannot fit even alone.
"""
intro = encode_pieces(tokenizer, [INTRO, state])
base = len(intro[0]) + len(intro[1])
sizes = [sum(len(ids) for ids, _ in pieces) + 4 for pieces in question_tokens(tokenizer, items)]
passes, current, used = [], [], base
for index, size in enumerate(sizes):
if base + size > max_length:
raise TooLong(index)
if current and used + size > max_length:
passes.append(current)
current, used = [], base
current.append(index)
used += size
if current:
passes.append(current)
return passes, len(intro[1]), sizes
def build(tokenizer, state, items):
"""Token ids plus the scored positions for one pass over these items."""
texts, marks = [INTRO, state], [None, None]
for k, item in enumerate(items):
for text, mark in question_pieces(k, item):
texts.append(text)
marks.append(mark)
ids, answers, options, current = [], [], [], []
for piece, mark in zip(encode_pieces(tokenizer, texts), marks):
ids += piece
if mark == "option":
current.append(len(ids) - 1)
elif mark == "answer":
answers.append(len(ids) - 1)
options.append(current)
current = []
return {"ids": ids, "answers": answers, "options": options}
# ================================================================================================
# Model: merged Qwen3.5-2B backbone (bidirectional full attention) + option-pointer decision head
# ================================================================================================
import torch
HEAD_WEIGHTS = "decider_head.safetensors"
HEAD_CONFIG = "decider_head_config.json"
NEG = -1e4
class Encoder:
"""Batch tensors for a list of {"state": text, "items": items(questions)} records."""
def __init__(self, tokenizer, pad_to_multiple=8):
self.tokenizer = tokenizer
self.pad_to_multiple = pad_to_multiple
def __call__(self, batch):
layouts = [build(self.tokenizer, x["state"], x["items"]) for x in batch]
width = max(len(x["ids"]) for x in layouts)
width += (-width) % self.pad_to_multiple
most = max(len(o) for x in layouts for o in x["options"])
input_ids = torch.full((len(batch), width), self.tokenizer.pad_token_id, dtype=torch.long)
attention_mask = torch.zeros((len(batch), width), dtype=torch.long)
answer_pos, option_pos, counts = [], [], []
for b, layout in enumerate(layouts):
input_ids[b, : len(layout["ids"])] = torch.tensor(layout["ids"])
attention_mask[b, : len(layout["ids"])] = 1
for answer, positions in zip(layout["answers"], layout["options"]):
answer_pos.append(b * width + answer)
option_pos.append([b * width + p for p in positions] + [0] * (most - len(positions)))
counts.append(len(positions))
return {"input_ids": input_ids, "attention_mask": attention_mask,
"answer_pos": torch.tensor(answer_pos, dtype=torch.long),
"option_pos": torch.tensor(option_pos, dtype=torch.long),
"option_mask": torch.arange(most)[None] < torch.tensor(counts)[:, None]}
def encode_record(record, questions=None):
"""{"state", "items"} for one Jev/SystemOne request (optionally a subset of its question ids)."""
chosen = record["questions"] if questions is None else {q: record["questions"][q] for q in questions}
return {"state": request_state_text(record["state"], record["questions"]), "items": items(chosen)}
class DeciderModel(torch.nn.Module):
"""The released model: one forward pass scores every option of every question in a request."""
def __init__(self, path, dtype=torch.bfloat16, quantization=None, device=None):
super().__init__()
from safetensors.torch import load_file
from transformers import AutoModelForCausalLM, AutoTokenizer
with open(os.path.join(path, HEAD_CONFIG)) as file:
self.config = json.load(file)
self.tokenizer = AutoTokenizer.from_pretrained(path)
if self.tokenizer.pad_token_id is None:
self.tokenizer.pad_token = self.tokenizer.eos_token
self.encoder = Encoder(self.tokenizer)
if quantization:
# 8-bit or 4-bit backbone via bitsandbytes (CUDA only); the decision head stays in float32.
from transformers import BitsAndBytesConfig
if quantization == "8bit":
bnb = BitsAndBytesConfig(load_in_8bit=True)
elif quantization == "4bit":
bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16)
else:
raise ValueError("quantization must be None, '8bit' or '4bit'")
lm = AutoModelForCausalLM.from_pretrained(path, dtype=dtype, quantization_config=bnb,
device_map={"": device or "cuda"})
else:
lm = AutoModelForCausalLM.from_pretrained(path, dtype=dtype)
head = lm.get_output_embeddings()
hidden = head.weight.shape[1]
# The vocabulary head is unused: the backbone returns its final hidden states.
for module in list(lm.modules()):
for name, child in module.named_children():
if child is head:
setattr(module, name, torch.nn.Identity())
self.text_config = getattr(lm.config, "text_config", lm.config)
self.bidirectional = bool(self.config.get("bidirectional", True))
if self.bidirectional:
for module in lm.modules():
if hasattr(module, "is_causal") and hasattr(module, "q_proj"):
module.is_causal = False
self.lm = lm
self.head_dim = int(self.config["head_dim"])
self.norm = torch.nn.RMSNorm(hidden, eps=1e-6)
self.query = torch.nn.Linear(hidden, self.head_dim, bias=False)
self.key = torch.nn.Linear(hidden, self.head_dim, bias=False)
self.prior = torch.nn.Linear(hidden, 1)
state = load_file(os.path.join(path, HEAD_WEIGHTS))
for name in ("norm", "query", "key", "prior"):
getattr(self, name).load_state_dict({k.split(".", 1)[1]: v for k, v in state.items() if k.startswith(name + ".")})
self.temperature = float(self.config.get("temperature", 1.0))
self.max_length = int(self.config.get("max_length", 8192))
def encode(self, batch):
return self.encoder(batch)
def masks(self, embeds, attention_mask):
"""Bidirectional masks for the full-attention layers, padding-only for linear attention."""
from transformers.masking_utils import create_bidirectional_mask, create_recurrent_attention_mask
return {"full_attention": create_bidirectional_mask(config=self.text_config, inputs_embeds=embeds,
attention_mask=attention_mask),
"linear_attention": create_recurrent_attention_mask(self.text_config, embeds, attention_mask)}
def forward(self, input_ids, attention_mask, answer_pos, option_pos, option_mask, **_):
"""Option logits [questions, most options] (masked options are -1e4). Divide by
self.temperature and softmax per question for calibrated probabilities."""
with torch.autocast("cuda", dtype=torch.bfloat16, enabled=input_ids.is_cuda):
if self.bidirectional:
embeds = self.lm.get_input_embeddings()(input_ids)
hidden = self.lm(inputs_embeds=embeds, attention_mask=self.masks(embeds, attention_mask)).logits
else:
hidden = self.lm(input_ids=input_ids, attention_mask=attention_mask).logits
flat = hidden.reshape(-1, hidden.shape[-1]).float()
query = self.query(self.norm(flat[answer_pos]))
options = self.norm(flat[option_pos])
logits = torch.einsum("qd,qmd->qm", query, self.key(options)) / math.sqrt(self.head_dim)
logits = logits + self.prior(options).squeeze(-1)
return logits.masked_fill(~option_mask, NEG)
def load_release_model(path, device="cuda", dtype=None, quantization=None):
"""Load the released model from a local folder (e.g. huggingface_hub.snapshot_download(...)).
quantization: None (bf16 on GPU, float32 on CPU), "8bit" or "4bit" (bitsandbytes, CUDA only:
about 2.5 GB or 1.8 GB of GPU memory instead of 4.5 GB).
The tuned abstention threshold in decider_head_config.json is applied unless the
DECIDER_ABSTAIN_THRESHOLD environment variable is set.
"""
global ABSTAIN_THRESHOLD
dtype = dtype or (torch.bfloat16 if str(device).startswith("cuda") else torch.float32)
model = DeciderModel(path, dtype=dtype, quantization=quantization, device=device)
model.head_device = device
if "abstain_threshold" in model.config and "DECIDER_ABSTAIN_THRESHOLD" not in os.environ:
ABSTAIN_THRESHOLD = float(model.config["abstain_threshold"])
model.norm.float(); model.query.float(); model.key.float(); model.prior.float()
if quantization: # the backbone is already placed; move only the head
for part in (model.norm, model.query, model.key, model.prior):
part.to(device)
return model.eval()
return model.to(device).eval()
@torch.inference_mode()
def systemone(model, body, name="decider-2b"):
"""Answer one Jev/SystemOne request body; returns {"model", "answers", "usage"}.
Questions that do not fit next to the full state in one pass are split across passes, each
with the whole state; nothing is truncated.
"""
validate_request(body)
questions = body["questions"]
state = request_state_text(body["state"], questions)
rendered = items(questions)
device = next(model.parameters()).device
passes, state_tokens, sizes = plan(model.tokenizer, state, rendered, model.max_length)
probabilities = [None] * len(rendered)
for indices in passes:
tensors = model.encode([{"state": state, "items": [rendered[i] for i in indices]}])
logits = model(**{k: v.to(device) for k, v in tensors.items()}).float() / model.temperature
for row, i in enumerate(indices):
count = len(rendered[i]["labels"])
probabilities[i] = logits[row, :count].softmax(-1).tolist()
total = len(passes) * state_tokens + sum(sizes)
return response(body.get("model") or name, questions, probabilities, total)