tinysoc / backend_llamacpp.py
Mroqui's picture
TinySOC — Build Small submission
35cf8e4 verified
Raw
History Blame Contribute Delete
5.87 kB
"""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"))
@lru_cache(maxsize=1)
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)
@lru_cache(maxsize=1)
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()
@lru_cache(maxsize=2)
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