Arbiter-8B / arbiter.py
jsemrau's picture
Rename to Arbiter-8B; state the Qwen3-8B base clearly
cb5578c verified
Raw History Blame Contribute Delete
4.5 kB
"""Arbiter: load the model from this repository and answer typed decisions.
from arbiter import Arbiter
from questions import OUTLOOK
arbiter = Arbiter.from_pretrained("ternary/Arbiter-8B")
answer = arbiter.decide(state="...article or 10-K passage...", question=OUTLOOK, focus="Meta Platforms")
print(answer["choice"], answer["probabilities"])
One forward pass per decision; the answer carries a probability for every option, calibrated with the
per-type temperatures in calibration.json. Every fallback (state truncated to fit, uncalibrated
temperature) is counted in `arbiter.fallback_counts`.
"""
from __future__ import annotations
import json
from collections import Counter
from pathlib import Path
from typing import Any
from model import QUESTION_TYPES, jev_answer, load_decision_model, option_logits, probabilities, question_type
class Arbiter:
def __init__(self, model, tokenizer, temperatures: dict[str, float], max_length: int, counter: Counter):
self.model, self.tokenizer = model, tokenizer
self.temperatures, self.max_length = temperatures, max_length
self.fallback_counts = counter
@classmethod
def from_pretrained(cls, path_or_repo: str = "ternary/Arbiter-8B", adapter: bool = False,
quantization: str | None = None, max_memory: str | None = None) -> "Arbiter":
"""Load from a local folder or a Hugging Face repo id.
Default: the full model (LoRA merged into the weights, fp16/bf16, no base download).
adapter=True: the LoRA in adapter/ on Qwen/Qwen3-8B, 4-bit as trained (6.5 GB GPU memory measured).
quantization overrides the default ('none' or '4bit')."""
from safetensors.torch import load_file
path = Path(path_or_repo)
if not path.exists():
from huggingface_hub import snapshot_download
path = Path(snapshot_download(path_or_repo))
meta = json.loads((path / "metadata.json").read_text())
head_config = json.loads((path / "head_config.json").read_text())
counter: Counter = Counter()
if (path / "config.json").exists() and not adapter:
# Full model: the LoRA is merged into the weights in this folder; no base download, no peft.
model_id, adapter_dir, default_quantization = str(path), None, "none"
calibration = path / "calibration.json"
else:
# Adapter on the base model, 4-bit as trained.
model_id, adapter_dir, default_quantization = meta["base_model"], path / "adapter", meta["quantization"]
calibration = path / "adapter" / "calibration.json"
if not calibration.exists():
calibration = path / "calibration.json"
model, tokenizer, letter_ids, _ = load_decision_model(
model_id, quantization=quantization or default_quantization, max_memory=max_memory,
adapter_dir=adapter_dir, counter=counter)
if letter_ids != head_config["letter_ids"]:
raise ValueError("this tokenizer's letter ids differ from the ones the head was trained with")
model.head.load_state_dict(load_file(str(path / "decision_head.safetensors")))
model.head.to(next(model.backbone.parameters()).device)
temperatures = {kind: 1.0 for kind in QUESTION_TYPES}
if calibration.exists():
temperatures.update(json.loads(calibration.read_text())["temperatures"])
else:
counter["uncalibrated_temperature_1"] += 1
return cls(model, tokenizer, temperatures, meta["max_length"], counter)
def decide(self, state: str, question: dict[str, Any], focus: str | None = None) -> dict[str, Any]:
"""question: {"type": "choice"|"noul"|"score", "instructions": str, "criteria": {option_id: definition}}.
noul criteria are {"true": ..., "false": ...}; score criteria are {"0": lowest, "1": ..., ...}."""
labels = ["true", "false"] if question["type"] == "noul" else list(question["criteria"])
row = {"state": state, "question": question, "labels": labels,
"focus": {"surface": focus} if focus else {}}
logits, truncated = option_logits(self.model, self.tokenizer, row, self.max_length, self.fallback_counts)
probs = probabilities(logits, self.temperatures[question_type(row)])
answer = jev_answer(row, probs)
answer["probabilities"] = probs
answer["state_truncated"] = truncated
return answer