Spaces:
Running
Running
Download openjev_server/backends/mlx.py from openjev/openjev-server: direct link, hf CLI and curl.
- Browser
- Download file 4.81 kB
-
https://huggingface.co/spaces/openjev/openjev-server/resolve/main/openjev_server/backends/mlx.py
- Command line
-
hf download hf://spaces/openjev/openjev-server/openjev_server/backends/mlx.py
-
curl -L -o mlx.py https://huggingface.co/spaces/openjev/openjev-server/resolve/main/openjev_server/backends/mlx.py
4.81 kB
| """MLX backend for Apple silicon: the same readout from an mlx-lm model on the Mac. One forward pass per question; with | |
| `prefix_cache=True` the shared 'State:' prefix of a request is prefilled once and copied for the other questions of that step. | |
| MLX is not thread-safe: all model calls go through one lock in a worker thread, so the async server stays responsive. | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import copy | |
| import threading | |
| from typing import Any | |
| class MlxBackend: | |
| def __init__(self, model_dir: str, *, prefix_cache: bool = True, min_prefix_tokens: int = 64, letter_prefix: str = "auto", assistant_prefix: str = ""): | |
| import mlx.core as mx | |
| from mlx_lm import load | |
| from transformers import AutoTokenizer | |
| self.mx = mx | |
| self.model, _ = load(model_dir) | |
| self.tok = AutoTokenizer.from_pretrained(model_dir) | |
| self.prefix_cache = prefix_cache | |
| self.min_prefix_tokens = min_prefix_tokens | |
| self._lock = threading.Lock() | |
| self._prefix: tuple[list[int], Any] | None = None | |
| from .letters import header_counter, letter_ids, render | |
| self._ids, self.letter_prefix = letter_ids(self.tok, letter_prefix) | |
| self.assistant_prefix = assistant_prefix | |
| self._render = lambda content: render(self.tok, content, assistant_prefix) | |
| self._header = header_counter(self.tok, assistant_prefix) | |
| self.vision = False # mlx-lm text models only | |
| async def start(self) -> dict: | |
| return { | |
| "served_model": "mlx", | |
| "exact_readout": True, | |
| "chat_template": bool(getattr(self.tok, "chat_template", None)), | |
| "letter_prefix": self.letter_prefix, | |
| "vision": False, | |
| "prefix_cache": self.prefix_cache, | |
| } | |
| def letter_ids(self) -> dict[str, int]: | |
| return self._ids | |
| def header_tokens(self, text: str) -> int: | |
| return self._header(text) | |
| async def healthy(self) -> bool: | |
| return True | |
| async def aclose(self): | |
| pass | |
| def _ids_for(self, content) -> list[int]: | |
| if not isinstance(content, str): | |
| raise TypeError("the MLX build is text-only: screenshot states are not supported") | |
| return list(self.tok.encode(self._render(content), add_special_tokens=False)) | |
| def _feed(self, ids: list[int], cache, step: int = 2048): | |
| mx = self.mx | |
| while len(ids) > step: | |
| self.model(mx.array(ids[:step])[None], cache=cache) | |
| mx.eval([c.state for c in cache]) | |
| ids = ids[step:] | |
| return self.model(mx.array(ids)[None], cache=cache)[0, -1] | |
| def _last_logits(self, ids: list[int], content: str): | |
| mx = self.mx | |
| cut = content.rfind("\n\nQuestion: ") if self.prefix_cache else -1 | |
| if cut < 0: | |
| return self.model(mx.array(ids)[None])[0, -1] | |
| from mlx_lm.models.cache import make_prompt_cache | |
| shared = self._prefix[0] if self._prefix else None | |
| if not (shared and ids[: len(shared)] == shared and len(ids) > len(shared)): | |
| head = self._ids_for(content[:cut]) | |
| n = next((i for i, (x, y) in enumerate(zip(ids, head, strict=False)) if x != y), min(len(ids), len(head))) | |
| n = min(n, len(ids) - 1) | |
| if n < self.min_prefix_tokens: | |
| return self.model(mx.array(ids)[None])[0, -1] | |
| cache = make_prompt_cache(self.model) | |
| self._feed(ids[:n], cache) | |
| mx.eval([c.state for c in cache]) | |
| self._prefix = (ids[:n], cache) | |
| shared = ids[:n] | |
| return self._feed(ids[len(shared) :], copy.deepcopy(self._prefix[1])) | |
| def _logprobs_sync(self, content: str, token_ids: list[int]) -> tuple[list[float], int]: | |
| mx = self.mx | |
| ids = self._ids_for(content) | |
| with self._lock: | |
| logits = self._last_logits(ids, content).astype(mx.float32) | |
| lp = logits - mx.logsumexp(logits, axis=-1) | |
| vals = [float(lp[i]) for i in token_ids] | |
| mx.eval(logits) | |
| return vals, len(ids) | |
| async def logprobs(self, content: str | list[dict[str, Any]], token_ids: list[int]) -> tuple[list[float], int]: | |
| return await asyncio.to_thread(self._logprobs_sync, content, token_ids) | |
| async def prewarm(self, text: str) -> int: | |
| def run(): | |
| ids = self._ids_for(text) | |
| with self._lock: | |
| if self.prefix_cache: | |
| from mlx_lm.models.cache import make_prompt_cache | |
| cache = make_prompt_cache(self.model) | |
| self._feed(ids, cache) | |
| self.mx.eval([c.state for c in cache]) | |
| self._prefix = (ids, cache) | |
| return len(ids) | |
| return await asyncio.to_thread(run) | |