Instructions to use theunnecessarythings/JADE with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use theunnecessarythings/JADE with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
File size: 8,240 Bytes
fdbed23 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | """Run JADE's decision head with vLLM."""
from __future__ import annotations
import hashlib
import json
import math
from pathlib import Path
from typing import Any
from .decision_prompt import decision_messages, options
MAX_OPTIONS = 255
MAX_TOKENS = 8192
LOGPROB_CHUNK_SIZE = 128
class UnsupportedInput(ValueError):
"""The request exceeds the model's supported types or capacity."""
def distribution(values: list[float], temperature: float) -> list[float]:
if not values or not math.isfinite(temperature) or temperature <= 0:
raise ValueError("Invalid temperature or missing logits")
if any(not math.isfinite(value) for value in values):
raise ValueError("Nonfinite answer logits")
top = max(values)
weights = [math.exp((value - top) / temperature) for value in values]
total = sum(weights)
return [value / total for value in weights]
def validate_release(root: Path) -> dict:
manifest = json.loads((root / "release-manifest.json").read_text())
for name, expected in manifest["files"].items():
path = (root / name).resolve()
if not path.is_relative_to(root.resolve()):
raise ValueError("Release contains a path outside its directory")
if not path.is_file():
raise ValueError(f"Missing release file: {name}")
with path.open("rb") as stream:
actual = hashlib.file_digest(stream, "sha256").hexdigest()
if actual != expected:
raise ValueError(f"Release file checksum mismatch: {name}")
return manifest
class JadeEngine:
"""Load a JADE checkpoint and answer Choice or Noul questions."""
def __init__(
self,
model: str,
revision: str | None = None,
max_tokens: int = MAX_TOKENS,
gpu_memory_utilization: float = 0.85,
):
if not 1 <= int(max_tokens) <= MAX_TOKENS:
raise ValueError("JADE's declared context limit is at most 8192 tokens")
root = Path(model)
if not root.is_dir():
if not revision or len(revision) != 40 or any(c not in "0123456789abcdefABCDEF" for c in revision):
raise ValueError("Hub model revision must be an immutable 40-character commit")
from huggingface_hub import snapshot_download
root = Path(snapshot_download(repo_id=model, revision=revision))
manifest = validate_release(root)
config = json.loads((root / "decision_config.json").read_text())
from transformers import AutoProcessor
from vllm import LLM, SamplingParams
from vllm.lora.request import LoRARequest
self.processor = AutoProcessor.from_pretrained(config["base_model"], revision=config["revision"])
self.codes = config["codes"]
self.token_ids = config["token_ids"]
if (
len(self.codes) != MAX_OPTIONS
or len(self.token_ids) != MAX_OPTIONS
or len(set(self.token_ids)) != MAX_OPTIONS
):
raise ValueError("Invalid decision vocabulary")
actual = [self.processor.tokenizer.encode(code, add_special_tokens=False) for code in self.codes]
if actual != [[token] for token in self.token_ids]:
raise ValueError("Tokenizer differs from the trained decision vocabulary")
self.temperature = float(config["temperature"])
if not math.isfinite(self.temperature) or self.temperature <= 0:
raise ValueError("Checkpoint temperature must be positive and finite")
self.limit = int(max_tokens)
self.model_id = model
self.SamplingParams = SamplingParams
self.model = LLM(
model=config["base_model"],
revision=config["revision"],
tokenizer_revision=config["revision"],
dtype="bfloat16",
max_model_len=self.limit,
gpu_memory_utilization=float(gpu_memory_utilization),
language_model_only=True,
enable_lora=True,
max_lora_rank=256, # The exported adapter includes the decision head.
max_logprobs=MAX_OPTIONS,
max_num_seqs=128,
enable_prefix_caching=True,
generation_config="vllm",
)
self.adapter = LoRARequest("jade", 1, str(root / "vllm-adapter"))
self.provenance = {
"model": model,
"revision": revision,
"release_id": manifest["release_id"],
"base_model": config["base_model"],
"base_revision": config["revision"],
"temperature": self.temperature,
"context_limit_tokens": self.limit,
"max_options": MAX_OPTIONS,
"supported_types": ["choice", "noul"],
"input_mode": "text",
"truncation": False,
"option_filtering": False,
}
def __call__(self, state: Any, questions: dict):
if not questions:
raise ValueError("Supply at least one question")
prompts, settings, requests, keys_by_question = self._prepare(state, questions)
results = self.model.generate(prompts, settings, lora_request=self.adapter, use_tqdm=False)
gathered = {key: {} for key in questions}
for (key, part), result in zip(requests, results, strict=True):
returned = result.outputs[0].logprobs[0]
for token in part:
if token not in returned:
raise ValueError(f"missing answer-token probability: {token}")
gathered[key][token] = float(returned[token].logprob)
answers = {}
for key, question in questions.items():
keys = keys_by_question[key]
logits = [gathered[key][token] for token in self.token_ids[:len(keys)]]
probabilities = distribution(logits, self.temperature)
if question["type"] == "noul":
answers[key] = {"type": "noul", "noul": probabilities[1]}
else:
best = max(range(len(keys)), key=probabilities.__getitem__)
answers[key] = {
"type": "choice",
"choice": keys[best],
"probabilities": dict(zip(keys, probabilities, strict=True)),
}
return {"model": self.model_id, "answers": answers}, None
def _prepare(self, state: Any, questions: dict):
prompts = []
settings = []
requests = []
keys_by_question = {}
for key, question in questions.items():
if question.get("type") not in {"choice", "noul"}:
raise UnsupportedInput(f"unsupported question type: {question.get('type')}")
keys, _ = options(question)
if not 1 <= len(keys) <= MAX_OPTIONS:
raise UnsupportedInput("question needs between 1 and 255 options")
keys_by_question[key] = keys
rendered = self.processor.apply_chat_template(
decision_messages({"state": state, "question": question}, self.codes),
tokenize=False,
add_generation_prompt=True,
enable_thinking=False,
)
ids = self.processor.tokenizer(rendered, add_special_tokens=False)["input_ids"]
if len(ids) + 1 > self.limit:
raise UnsupportedInput(f"input needs {len(ids) + 1} tokens; capacity is {self.limit}; no truncation")
# vLLM accepts at most 128 requested log-probability token IDs.
# Each chunk scores the same full option set, so their values share
# a normalizer and can be combined before temperature scaling.
for offset in range(0, len(keys), LOGPROB_CHUNK_SIZE):
part = self.token_ids[offset:min(offset + LOGPROB_CHUNK_SIZE, len(keys))]
prompts.append({"prompt_token_ids": ids})
settings.append(self.SamplingParams(
temperature=0,
max_tokens=1,
logprob_token_ids=part,
allowed_token_ids=self.token_ids[:len(keys)],
detokenize=False,
))
requests.append((key, part))
return prompts, settings, requests, keys_by_question
|