Spaces:
Running
Running
| """Local llama.cpp backend (GGUF). Self-contained, no network at inference time. | |
| This is the default backend so the public HF Space stays self-hosted on CPU | |
| (badges "Off the Grid" + "Llama Champion"). It serves TWO roles for TinySOC: | |
| * complete(messages) -> the small explainer model (<=4B), chat JSON. | |
| * prompt_token_nll(ctx, line) -> per-token negative log-likelihood of a log | |
| line, used by the perplexity highlighter. | |
| A public Space cannot reach an external Ollama server, so that backend is | |
| dev-only; the perplexity highlighter therefore needs prompt logprobs | |
| locally. llama.cpp gives them in a SINGLE pass via `echo=True` + `logprobs` | |
| (loading with logits_all=True), which is simpler than the Ollama forced-decode | |
| hack. Lazy imports keep an Ollama-only dev box from needing llama-cpp-python. | |
| """ | |
| import os | |
| from functools import lru_cache | |
| from pathlib import Path | |
| # --- Explainer model (chat) ------------------------------------------------- | |
| MODEL_PATH = os.environ.get("WEC_MODEL_PATH", "") | |
| MODEL_REPO = os.environ.get("WEC_MODEL_REPO", "Mroqui/TinySOC-Qwen2.5-3B") | |
| MODEL_FILE = os.environ.get("WEC_MODEL_FILE", "TinySOC-Qwen2.5-3B-Q5_K_M.gguf") | |
| N_CTX = int(os.environ.get("WEC_N_CTX", "4096")) | |
| MAX_TOKENS = int(os.environ.get("WEC_MAX_TOKENS", "768")) | |
| N_GPU_LAYERS = int(os.environ.get("WEC_N_GPU_LAYERS", "0")) | |
| TEMPERATURE = float(os.environ.get("WEC_TEMPERATURE", "0.2")) | |
| # Preferred local explainer: the fine-tuned v3 GGUF ("Well-Tuned" badge). | |
| _EXPLAIN_LOCAL = Path(__file__).parent / "models" / "TinySOC-Qwen2.5-3B-Q5_K_M.gguf" | |
| # --- Perplexity model (highlighter) ----------------------------------------- | |
| # A small BASE model highlights novel tokens best. If none is provided we reuse | |
| # the explainer model: detection still rests on the deterministic baseline, so | |
| # the highlighter degrading to an instruct model never breaks correctness. | |
| PPL_MODEL_PATH = os.environ.get("WEC_PPL_MODEL_PATH", "") | |
| PPL_REPO = os.environ.get("WEC_PPL_REPO", "mradermacher/Qwen2.5-Coder-1.5B-GGUF") | |
| PPL_FILE = os.environ.get("WEC_PPL_FILE", "Qwen2.5-Coder-1.5B.Q4_K_M.gguf") | |
| PPL_N_CTX = int(os.environ.get("WEC_PPL_N_CTX", "1024")) | |
| FLOOR_NLL = float(os.environ.get("WEC_FLOOR_NLL", "12.0")) | |
| def _explain_model_path() -> str: | |
| """Resolve the explainer GGUF: explicit env > local v3 > HF download.""" | |
| if MODEL_PATH: | |
| return MODEL_PATH | |
| if _EXPLAIN_LOCAL.is_file(): | |
| return str(_EXPLAIN_LOCAL) | |
| from huggingface_hub import hf_hub_download | |
| return hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE) | |
| def _ppl_model_path() -> str: | |
| """Resolve the perplexity GGUF: explicit > HF download > reuse explainer.""" | |
| if PPL_MODEL_PATH: | |
| return PPL_MODEL_PATH | |
| if PPL_REPO and PPL_FILE: | |
| from huggingface_hub import hf_hub_download | |
| return hf_hub_download(repo_id=PPL_REPO, filename=PPL_FILE) | |
| return _explain_model_path() | |
| def _load(model_path: str, n_ctx: int, logits_all: bool): | |
| """Load a GGUF once. Same file path => llama.cpp mmap shares weight pages.""" | |
| from llama_cpp import Llama | |
| return Llama( | |
| model_path=model_path, | |
| n_ctx=n_ctx, | |
| n_threads=os.cpu_count() or 4, | |
| n_gpu_layers=N_GPU_LAYERS, | |
| logits_all=logits_all, | |
| verbose=False, | |
| ) | |
| def get_model(): | |
| """Chat/explainer model instance (logits for the last token only).""" | |
| return _load(_explain_model_path(), N_CTX, False) | |
| def get_ppl_model(): | |
| """Perplexity model instance (logits_all=True so every prompt token scores).""" | |
| return _load(_ppl_model_path(), PPL_N_CTX, True) | |
| def complete(messages: list[dict[str, str]]) -> str: | |
| """Generate the triage JSON via llama.cpp and return the raw text.""" | |
| response = get_model().create_chat_completion( | |
| messages=messages, | |
| temperature=TEMPERATURE, | |
| max_tokens=MAX_TOKENS, | |
| response_format={"type": "json_object"}, | |
| ) | |
| return response["choices"][0]["message"]["content"] | |
| def _nll(logits, token_id: int) -> float: | |
| """Negative log P(token_id) from a logits row (stable log-softmax, numpy).""" | |
| import numpy as np | |
| row = np.asarray(logits, dtype=np.float32) | |
| top = float(row.max()) | |
| logsumexp = top + float(np.log(np.exp(row - top).sum())) | |
| return logsumexp - float(row[token_id]) | |
| def prompt_token_nll(context: str, line: str, | |
| max_steps: int = 80) -> list[tuple[str, float]]: | |
| """Per-token negative log-likelihood of `line`, given a normal `context`. | |
| One forward pass over `context + line` (logits_all=True), then read the | |
| conditional logprob of each actual token: logits at position i predict | |
| token i+1. We return only the tokens belonging to `line` (the prompt tail) | |
| as (token_text, nll) for the highlighter. | |
| NOTE: llama-cpp-python's echo-logprobs are misaligned in this version, so we | |
| read logits directly instead of trusting create_completion(echo=True). | |
| """ | |
| llm = get_ppl_model() | |
| full_ids = llm.tokenize((context + line).encode("utf-8"), | |
| add_bos=True, special=False) | |
| full_ids = full_ids[-PPL_N_CTX:] # keep the tail if longer than the window | |
| n = len(full_ids) | |
| if n < 2: | |
| return [] | |
| llm.reset() | |
| llm.eval(full_ids) | |
| scores = llm.scores # (n_ctx, n_vocab); logits at row i predict token i+1 | |
| line_ids = llm.tokenize(line.encode("utf-8"), add_bos=False, special=False) | |
| keep = min(max(1, len(line_ids)), max_steps, n - 1) | |
| out: list[tuple[str, float]] = [] | |
| for j in range(n - keep, n): | |
| tok = llm.detokenize([full_ids[j]]).decode("utf-8", "replace") | |
| if j == 0: | |
| out.append((tok, FLOOR_NLL)) | |
| continue | |
| out.append((tok, _nll(scores[j - 1], full_ids[j]))) | |
| return out | |