Text Classification
MLX
English
decision-model
calibration
lora
activated-lora
adaptive-reasoning
on-device
Instructions to use suhaas-teja/Dev-4B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use suhaas-teja/Dev-4B with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] hf download suhaas-teja/Dev-4B --local-dir Dev-4B
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
Download dmm/engine.py from suhaas-teja/Dev-4B: direct link, hf CLI and curl.
- Browser
- Download file 10.8 kB
-
https://huggingface.co/suhaas-teja/Dev-4B/resolve/main/dmm/engine.py
- Command line
-
hf download hf://suhaas-teja/Dev-4B/dmm/engine.py
-
curl -L -o engine.py https://huggingface.co/suhaas-teja/Dev-4B/resolve/main/dmm/engine.py
10.8 kB
| """The inference engine behind the server: decisions, generation, and decide-then-generate. | |
| One model in memory. Decisions prefill the state once with the base (adapter off), then run each | |
| question suffix with the adapter on against that cache. Generation runs with the adapter off. | |
| `auto` decides first and, when the router flags the question, decodes from the same state cache, | |
| so escalation never prefills the state a second time. | |
| Requests use TypeSafe-style question types: noul (yes/no), choice (named options, at most 16) | |
| and score (ordered levels, at most 6). | |
| """ | |
| import json | |
| import threading | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from dmm import lora | |
| from dmm.artifacts import read_router, router_file, weights_file | |
| from dmm.cache import StateCache | |
| from dmm.head import DecisionHead, option_logits | |
| from dmm.router import Router, confidence_features | |
| from dmm.template import Template, chat_text, decision_text | |
| def to_item(state: str, name: str, q: dict) -> dict: | |
| """A TypeSafe-style question as one of our decision items.""" | |
| kind = q["type"] | |
| text = str(q.get("instructions") or "").strip() | |
| if kind == "noul": | |
| return { | |
| "id": name, | |
| "kind": "yesno", | |
| "state": state, | |
| "question": text, | |
| "options": ["no", "yes"], | |
| "answer": None, | |
| } | |
| if kind == "choice": | |
| names = list(q["criteria"]) | |
| if not 1 <= len(names) <= 16: | |
| raise ValueError(f"{name}: choice takes 1 to 16 options, got {len(names)}") | |
| opts = [n if not q["criteria"][n] else f"{n}: {q['criteria'][n]}" for n in names] | |
| return { | |
| "id": name, | |
| "kind": "choice", | |
| "state": state, | |
| "question": text, | |
| "options": opts, | |
| "answer": None, | |
| "_names": names, | |
| } | |
| if kind == "score": | |
| levels = list(q["criteria"]) | |
| if not 2 <= len(levels) <= 6: | |
| raise ValueError(f"{name}: score takes 2 to 6 levels, got {len(levels)}") | |
| legend = "; ".join(f"{i} = {lv}" for i, lv in enumerate(levels)) | |
| return { | |
| "id": name, | |
| "kind": "score", | |
| "state": state, | |
| "question": f"{text} ({legend})", | |
| "options": [str(i) for i in range(len(levels))], | |
| "answer": None, | |
| } | |
| raise ValueError(f"{name}: unknown question type {kind!r}") | |
| class Engine: | |
| def __init__( | |
| self, | |
| model, | |
| tokenizer, | |
| head: DecisionHead, | |
| temperatures: dict, | |
| router: Router | None = None, | |
| router_threshold: float = 1.0, | |
| ): | |
| self.model, self.tok, self.head = model, tokenizer, head | |
| self.tpl = Template(tokenizer) | |
| self.temperatures, self.router, self.threshold = temperatures, router, router_threshold | |
| self.pad = tokenizer.pad_token_id or tokenizer.eos_token_id | |
| self.lock = threading.Lock() # one request on the model at a time | |
| self.model.eval() | |
| def from_artifacts(cls, model, tokenizer, cfg: dict, ckpt_dir: str, router_dir: str | None): | |
| from train.train_decision import load_checkpoint, prepare | |
| head, _ = prepare(model, tokenizer, cfg) | |
| load_checkpoint(model, head, weights_file(ckpt_dir)) | |
| temps = json.loads(Path(f"{ckpt_dir}/temperatures.json").read_text()) | |
| router, threshold = None, 1.0 | |
| if router_dir: | |
| router = Router(model.config.hidden_size) | |
| router.load_state_dict(read_router(router_file(router_dir))) | |
| threshold = json.loads(Path(f"{router_dir}/operating_point.json").read_text())[ | |
| "router_threshold" | |
| ] | |
| return cls(model, tokenizer, head.float(), temps, router, threshold) | |
| # Decisions --------------------------------------------------------------------------- | |
| def _decide_on(self, cache: StateCache, item: dict) -> dict: | |
| h = cache.run_suffix(self.tpl.encode(decision_text(item))) | |
| z = option_logits(self.head(h[None].to(self.head.proj.weight.device)), [item])[0] | |
| z = z.float() / self.temperatures.get(item["kind"], 1.0) | |
| p = torch.softmax(z, -1).cpu().numpy().astype(np.float64) | |
| score = None | |
| if self.router is not None: | |
| hf = h.float().cpu().numpy()[None] | |
| score = float(self.router.score(hf, confidence_features([p]))[0]) | |
| return { | |
| "probs": p, | |
| "router_score": score, | |
| "escalate": score is not None and score >= self.threshold, | |
| } | |
| def decide(self, state: str, questions: dict[str, dict]) -> dict: | |
| items = {name: to_item(state, name, q) for name, q in questions.items()} | |
| with self.lock: | |
| cache = StateCache(self.model, self.tpl.state_ids(next(iter(items.values())))) | |
| raw = {name: self._decide_on(cache, it) for name, it in items.items()} | |
| return {name: self._answer(items[name], r) for name, r in raw.items()} | |
| def _answer(item: dict, r: dict) -> dict: | |
| p = r["probs"] | |
| out = { | |
| "type": {"yesno": "noul", "choice": "choice", "score": "score"}[item["kind"]], | |
| "router_score": r["router_score"], | |
| "needs_generation": r["escalate"], | |
| } | |
| if item["kind"] == "yesno": | |
| out |= { | |
| "noul": float(p[1]), | |
| "probabilities": {"false": float(p[0]), "true": float(p[1])}, | |
| } | |
| elif item["kind"] == "choice": | |
| names = item["_names"] | |
| out |= { | |
| "choice": names[int(np.argmax(p))], | |
| "probabilities": {n: float(x) for n, x in zip(names, p, strict=True)}, | |
| } | |
| else: | |
| out |= { | |
| "score": float(np.dot(p, np.arange(len(p)))), | |
| "probabilities": {str(i): float(x) for i, x in enumerate(p)}, | |
| } | |
| return out | |
| # Generation -------------------------------------------------------------------------- | |
| def _generate( | |
| self, ids: list[int], max_new_tokens: int, cache: StateCache | None = None, streamer=None | |
| ): | |
| x = torch.tensor([ids], device=self.model.device) | |
| kw = {"past_key_values": cache.kv} if cache is not None else {} | |
| if streamer is not None: | |
| kw["streamer"] = streamer | |
| with lora.disabled(self.model): | |
| out = self.model.generate( | |
| x, | |
| attention_mask=torch.ones_like(x), | |
| do_sample=False, | |
| max_new_tokens=max_new_tokens, | |
| pad_token_id=self.pad, | |
| **kw, | |
| ) | |
| return out[0, len(ids) :].tolist() | |
| def _stream(self, ids: list[int], max_new_tokens: int, cache: StateCache | None = None): | |
| """Generate as _generate does, yielding {"event": "text"} pieces as they decode. | |
| Returns the generated token ids (the generator's return value). Generation runs in a | |
| thread feeding a streamer; the thread is always joined, even if the caller stops early, | |
| so the model and the state cache are never touched by two requests at once. | |
| """ | |
| from transformers import TextIteratorStreamer | |
| streamer = TextIteratorStreamer(self.tok, skip_prompt=True, skip_special_tokens=True) | |
| result: dict = {} | |
| def run(): | |
| try: | |
| result["tokens"] = self._generate(ids, max_new_tokens, cache, streamer) | |
| except BaseException as e: # surface in the caller's thread | |
| result["error"] = e | |
| streamer.end() | |
| t = threading.Thread(target=run, daemon=True) | |
| t.start() | |
| try: | |
| for piece in streamer: | |
| if piece: | |
| yield {"event": "text", "text": piece} | |
| finally: | |
| t.join() | |
| if "error" in result: | |
| raise result["error"] | |
| return result["tokens"] | |
| def chat_stream(self, messages: list[dict], max_new_tokens: int = 512): | |
| """chat(), streamed: {"event": "text"} pieces, then {"event": "done", "tokens": [...]}.""" | |
| ids = self.tok.apply_chat_template( | |
| messages, add_generation_prompt=True, tokenize=True, return_dict=True | |
| )["input_ids"] | |
| with self.lock: | |
| tokens = yield from self._stream(ids, max_new_tokens) | |
| yield {"event": "done", "tokens": tokens} | |
| def auto_stream(self, state: str, name: str, question: dict, max_new_tokens: int = 512): | |
| """auto(), streamed. Yields {"event": "decision", ...} as soon as the quick decision and | |
| the router's call are known, then {"event": "text"} reasoning pieces when escalated, then | |
| {"event": "done", ...} carrying exactly what auto() returns.""" | |
| item = to_item(state, name, question) | |
| with self.lock: | |
| cache = StateCache(self.model, self.tpl.state_ids(item)) | |
| r = self._decide_on(cache, item) | |
| out = { | |
| "decision": self._answer(item, r), | |
| "escalated": r["escalate"], | |
| "state_prefills": 1, | |
| } | |
| yield {"event": "decision", **out} | |
| if r["escalate"]: | |
| ids = cache.ids + self.tpl.encode(chat_text(item)) | |
| tokens = yield from self._stream(ids, max_new_tokens, cache) | |
| out["generation_tokens"] = tokens | |
| out["generation"] = self.tok.decode(tokens, skip_special_tokens=True) | |
| cache.kv.crop(len(cache)) | |
| yield {"event": "done", **out} | |
| def chat(self, messages: list[dict], max_new_tokens: int = 512) -> list[int]: | |
| ids = self.tok.apply_chat_template( | |
| messages, add_generation_prompt=True, tokenize=True, return_dict=True | |
| )["input_ids"] | |
| with self.lock: | |
| return self._generate(ids, max_new_tokens) | |
| def auto(self, state: str, name: str, question: dict, max_new_tokens: int = 512) -> dict: | |
| """Decide; when the router flags it, decode from the same state cache.""" | |
| item = to_item(state, name, question) | |
| with self.lock: | |
| cache = StateCache(self.model, self.tpl.state_ids(item)) | |
| r = self._decide_on(cache, item) | |
| out = { | |
| "decision": self._answer(item, r), | |
| "escalated": r["escalate"], | |
| "state_prefills": 1, | |
| } | |
| if r["escalate"]: | |
| ids = cache.ids + self.tpl.encode(chat_text(item)) | |
| out["generation_tokens"] = self._generate(ids, max_new_tokens, cache) | |
| out["generation"] = self.tok.decode( | |
| out["generation_tokens"], skip_special_tokens=True | |
| ) | |
| cache.kv.crop(len(cache)) | |
| return out | |