Text Classification
Transformers
Safetensors
qwen3_5
image-text-to-text
decision-model
typed-decisions
classification
routing
calibrated-probabilities
Instructions to use ironbcc/Seb-9B-v2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ironbcc/Seb-9B-v2 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="ironbcc/Seb-9B-v2")# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("ironbcc/Seb-9B-v2") model = AutoModelForMultimodalLM.from_pretrained("ironbcc/Seb-9B-v2", device_map="auto") - Notebooks
- Google Colab
- Kaggle
add serve/system1_vllm_backend.py
Browse files- serve/system1_vllm_backend.py +181 -0
serve/system1_vllm_backend.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""System One server on top of a STOCK vLLM OpenAI server (no custom vLLM code).
|
| 2 |
+
|
| 3 |
+
Run: python system1_vllm_backend.py --vllm-url http://HOST:8000 --served-model s1 --port 8020
|
| 4 |
+
|
| 5 |
+
Readout (zero generated content): the prompt is rendered and tokenised locally (byte-identical to training), then ONE
|
| 6 |
+
`/v1/completions` request is sent with prompt=<token ids>, max_tokens=1, allowed_token_ids=<candidate ids>, logprobs=K.
|
| 7 |
+
With the server flag `--logprobs-mode processed_logprobs` the returned logprobs are taken AFTER the allowed-token mask, so
|
| 8 |
+
they are exactly log_softmax over the candidates of the next-token logits at the assistant prefix. The backend's scores
|
| 9 |
+
are those logprobs; the Service's softmax (calibrated_probabilities, T=1) is shift-invariant, so probabilities equal the
|
| 10 |
+
MLX backend's softmax over raw candidate logits. Default raw_logprobs mode would return the top-K of the FULL vocabulary
|
| 11 |
+
(candidates can be missing), so a missing candidate or a non-normalised row raises instead of returning wrong scores.
|
| 12 |
+
Sampling params are pinned (temperature 1, top_p 1, top_k -1) because vLLM otherwise applies the model's generation_config.
|
| 13 |
+
|
| 14 |
+
Fallback `--readout prompt_logprobs`: one request per candidate with prompt = prefix+[candidate id], prompt_logprobs=0; the
|
| 15 |
+
logprob of the last prompt token is log_softmax(full vocab)[candidate]. Exact too (constant shift), needs no server flag, but
|
| 16 |
+
K prefills (vLLM does not reuse the prefix cache for prompt_logprobs requests) and large payloads; use it as a GPU control."""
|
| 17 |
+
import argparse
|
| 18 |
+
import base64
|
| 19 |
+
import json
|
| 20 |
+
import mimetypes
|
| 21 |
+
import math
|
| 22 |
+
import os
|
| 23 |
+
import urllib.error
|
| 24 |
+
import urllib.request
|
| 25 |
+
|
| 26 |
+
from system1_client import MAX_CONTEXT_TOKENS, OverlengthError
|
| 27 |
+
from system1_server import make_server
|
| 28 |
+
|
| 29 |
+
MODEL_ID, MODEL_REVISION = "Qwen/Qwen3.5-9B", "c202236235762e1c871ad0ccb60c8ee5ba337b9a"
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def candidate_ids(tokenizer, prompt, prefix, keys):
|
| 33 |
+
"""Same rules as MlxBackend.score: each key is exactly one token appended at the assistant prefix; ids distinct."""
|
| 34 |
+
ids = {}
|
| 35 |
+
for key in keys:
|
| 36 |
+
extended = tokenizer.encode(prompt + key, add_special_tokens=False)
|
| 37 |
+
if extended[:-1] != prefix or tokenizer.decode(extended[-1:]) != key:
|
| 38 |
+
raise ValueError(f"candidate {key}: not one token at assistant prefix")
|
| 39 |
+
ids[key] = int(extended[-1])
|
| 40 |
+
if len(set(ids.values())) != len(keys):
|
| 41 |
+
raise ValueError("candidate token IDs collide")
|
| 42 |
+
return ids
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def load_tokenizer(model=MODEL_ID, revision=MODEL_REVISION):
|
| 46 |
+
from huggingface_hub import snapshot_download
|
| 47 |
+
from transformers import AutoTokenizer
|
| 48 |
+
path = model if os.path.isdir(model) else snapshot_download(model, revision=revision, local_files_only=True)
|
| 49 |
+
return AutoTokenizer.from_pretrained(path, local_files_only=True, trust_remote_code=False)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class VllmBackend:
|
| 53 |
+
def __init__(self, vllm_url, served_model, tokenizer=None, readout="allowed_logprobs", timeout=60, api_key=None,
|
| 54 |
+
image_root=None):
|
| 55 |
+
if readout not in ("allowed_logprobs", "prompt_logprobs"):
|
| 56 |
+
raise ValueError(f"unknown readout {readout!r}")
|
| 57 |
+
self.url = vllm_url.rstrip("/") + "/v1/completions"
|
| 58 |
+
self.chat_url = vllm_url.rstrip("/") + "/v1/chat/completions"
|
| 59 |
+
self.image_root = os.path.realpath(image_root) if image_root else None # request-supplied file paths must live here
|
| 60 |
+
self.model, self.readout, self.timeout, self.api_key = served_model, readout, timeout, api_key
|
| 61 |
+
self.tokenizer = tokenizer or load_tokenizer()
|
| 62 |
+
|
| 63 |
+
def _post(self, body, url=None):
|
| 64 |
+
headers = {"Content-Type": "application/json"}
|
| 65 |
+
if self.api_key:
|
| 66 |
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
| 67 |
+
req = urllib.request.Request(url or self.url, json.dumps(body).encode(), headers)
|
| 68 |
+
try:
|
| 69 |
+
with urllib.request.urlopen(req, timeout=self.timeout) as r:
|
| 70 |
+
return json.loads(r.read())["choices"][0]
|
| 71 |
+
except urllib.error.HTTPError as e:
|
| 72 |
+
raise RuntimeError(f"vLLM HTTP {e.code}: {e.read()[:500].decode(errors='replace')}")
|
| 73 |
+
|
| 74 |
+
@staticmethod
|
| 75 |
+
def _lp(entry):
|
| 76 |
+
return float(entry["logprob"] if isinstance(entry, dict) else entry)
|
| 77 |
+
|
| 78 |
+
def _data_url(self, ref, trusted):
|
| 79 |
+
if ref.startswith("data:image/"):
|
| 80 |
+
return ref
|
| 81 |
+
path = os.path.realpath(ref)
|
| 82 |
+
if not trusted and not (self.image_root and os.path.commonpath([path, self.image_root]) == self.image_root):
|
| 83 |
+
raise ValueError("image file paths in requests need --image-root; send a data URL instead")
|
| 84 |
+
with open(path, "rb") as f:
|
| 85 |
+
return f"data:{mimetypes.guess_type(path)[0] or 'image/jpeg'};base64," + base64.b64encode(f.read()).decode()
|
| 86 |
+
|
| 87 |
+
@staticmethod
|
| 88 |
+
def _request_image(messages):
|
| 89 |
+
"""A System One request carries state.image={"type":"image_url","image_url":{"url":..}}. Returns (url, messages with
|
| 90 |
+
the image replaced by "attached"), or (None, messages) for every text-only request (those never get re-serialised)."""
|
| 91 |
+
content = messages[-1]["content"]
|
| 92 |
+
if '"image_url"' not in content:
|
| 93 |
+
return None, messages
|
| 94 |
+
payload = json.loads(content)
|
| 95 |
+
img = payload.get("state", {}).get("image") if isinstance(payload.get("state"), dict) else None
|
| 96 |
+
if not (isinstance(img, dict) and img.get("type") == "image_url" and isinstance(img.get("image_url", {}).get("url"), str)):
|
| 97 |
+
return None, messages
|
| 98 |
+
payload["state"] = {**payload["state"], "image": "attached"}
|
| 99 |
+
text = json.dumps(payload, ensure_ascii=True, separators=(",", ":")).replace("<", "\\u003c")
|
| 100 |
+
return img["image_url"]["url"], messages[:-1] + [{**messages[-1], "content": text}]
|
| 101 |
+
|
| 102 |
+
def score_image(self, messages, keys, image):
|
| 103 |
+
"""Same readout as score() but via /v1/chat/completions with the image prepended to the user turn.
|
| 104 |
+
`image` is a trusted local path or a data URL. Candidate ids come from the text-only render (same assistant prefix)."""
|
| 105 |
+
return self._score_image(messages, keys, self._data_url(image, trusted=True))
|
| 106 |
+
|
| 107 |
+
def _score_image(self, messages, keys, data_url):
|
| 108 |
+
prompt = self.tokenizer.apply_chat_template(
|
| 109 |
+
messages, tokenize=False, add_generation_prompt=True, enable_thinking=False)
|
| 110 |
+
prefix = self.tokenizer.encode(prompt, add_special_tokens=False)
|
| 111 |
+
if len(prefix) + 1 > MAX_CONTEXT_TOKENS: # ponytail: text only; image tokens are bounded by the server's own limits
|
| 112 |
+
raise OverlengthError("prompt exceeds context budget")
|
| 113 |
+
ids = candidate_ids(self.tokenizer, prompt, prefix, keys)
|
| 114 |
+
chat = messages[:-1] + [{"role": "user", "content": [
|
| 115 |
+
{"type": "image_url", "image_url": {"url": data_url}}, {"type": "text", "text": messages[-1]["content"]}]}]
|
| 116 |
+
choice = self._post({"model": self.model, "messages": chat, "max_tokens": 1, "temperature": 1.0, "top_p": 1.0, "top_k": -1,
|
| 117 |
+
"chat_template_kwargs": {"enable_thinking": False}, "allowed_token_ids": list(ids.values()),
|
| 118 |
+
"logprobs": True, "top_logprobs": len(ids), "return_tokens_as_token_ids": True}, self.chat_url)
|
| 119 |
+
top = {e["token"]: e["logprob"] for e in choice["logprobs"]["content"][0]["top_logprobs"]}
|
| 120 |
+
missing = [k for k, t in ids.items() if f"token_id:{t}" not in top]
|
| 121 |
+
if missing:
|
| 122 |
+
raise RuntimeError(f"vLLM returned no logprob for candidates {missing}; start vllm serve with --logprobs-mode processed_logprobs")
|
| 123 |
+
out = {k: self._lp(top[f"token_id:{t}"]) for k, t in ids.items()}
|
| 124 |
+
if abs(sum(math.exp(v) for v in out.values()) - 1) > 1e-2:
|
| 125 |
+
raise RuntimeError("candidate logprobs are not normalised over the candidates; start vllm serve with --logprobs-mode processed_logprobs")
|
| 126 |
+
return out
|
| 127 |
+
|
| 128 |
+
def score(self, messages, keys, image=None):
|
| 129 |
+
if image is not None:
|
| 130 |
+
return self.score_image(messages, keys, image)
|
| 131 |
+
url, messages = self._request_image(messages)
|
| 132 |
+
if url is not None:
|
| 133 |
+
return self._score_image(messages, keys, self._data_url(url, trusted=False))
|
| 134 |
+
prompt = self.tokenizer.apply_chat_template(
|
| 135 |
+
messages, tokenize=False, add_generation_prompt=True, enable_thinking=False)
|
| 136 |
+
prefix = self.tokenizer.encode(prompt, add_special_tokens=False)
|
| 137 |
+
if len(prefix) + 1 > MAX_CONTEXT_TOKENS:
|
| 138 |
+
raise OverlengthError("prompt exceeds context budget")
|
| 139 |
+
ids = candidate_ids(self.tokenizer, prompt, prefix, keys)
|
| 140 |
+
pin = {"model": self.model, "max_tokens": 1, "temperature": 1.0, "top_p": 1.0, "top_k": -1}
|
| 141 |
+
if self.readout == "prompt_logprobs":
|
| 142 |
+
out = {}
|
| 143 |
+
for key, tid in ids.items():
|
| 144 |
+
last = self._post({**pin, "prompt": prefix + [tid], "prompt_logprobs": 0})["prompt_logprobs"][-1]
|
| 145 |
+
if str(tid) not in last:
|
| 146 |
+
raise RuntimeError(f"vLLM prompt_logprobs missing candidate {key} (token {tid})")
|
| 147 |
+
out[key] = self._lp(last[str(tid)])
|
| 148 |
+
return out
|
| 149 |
+
choice = self._post({**pin, "prompt": prefix, "allowed_token_ids": list(ids.values()), "logprobs": len(ids),
|
| 150 |
+
"return_tokens_as_token_ids": True})
|
| 151 |
+
top = choice["logprobs"]["top_logprobs"][0]
|
| 152 |
+
missing = [k for k, t in ids.items() if f"token_id:{t}" not in top]
|
| 153 |
+
if missing:
|
| 154 |
+
raise RuntimeError(f"vLLM returned no logprob for candidates {missing}; start vllm serve with --logprobs-mode processed_logprobs")
|
| 155 |
+
out = {k: self._lp(top[f"token_id:{t}"]) for k, t in ids.items()}
|
| 156 |
+
if abs(sum(math.exp(v) for v in out.values()) - 1) > 1e-2:
|
| 157 |
+
raise RuntimeError("candidate logprobs are not normalised over the candidates; start vllm serve with --logprobs-mode processed_logprobs")
|
| 158 |
+
return out
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def main():
|
| 162 |
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 163 |
+
ap.add_argument("--vllm-url", required=True)
|
| 164 |
+
ap.add_argument("--served-model", required=True, help="vLLM model name or --lora-modules name (e.g. s1)")
|
| 165 |
+
ap.add_argument("--tokenizer", default=MODEL_ID, help="HF id (local cache only) or directory")
|
| 166 |
+
ap.add_argument("--tokenizer-revision", default=MODEL_REVISION)
|
| 167 |
+
ap.add_argument("--readout", choices=("allowed_logprobs", "prompt_logprobs"), default="allowed_logprobs")
|
| 168 |
+
ap.add_argument("--image-root", help="directory request image file paths may point into (default: data URLs only)")
|
| 169 |
+
ap.add_argument("--port", type=int, default=8020)
|
| 170 |
+
ap.add_argument("--host", default="127.0.0.1")
|
| 171 |
+
ap.add_argument("--model-name", default="system1-9b")
|
| 172 |
+
a = ap.parse_args()
|
| 173 |
+
backend = VllmBackend(a.vllm_url, a.served_model, load_tokenizer(a.tokenizer, a.tokenizer_revision), a.readout,
|
| 174 |
+
image_root=a.image_root)
|
| 175 |
+
server = make_server(backend, a.model_name, a.host, a.port)
|
| 176 |
+
print(f"serving {a.model_name} ({a.readout}) on http://{a.host}:{a.port} -> {a.vllm_url}", flush=True)
|
| 177 |
+
server.serve_forever()
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
if __name__ == "__main__":
|
| 181 |
+
main()
|