Instructions to use startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0 # Run inference directly in the terminal: llama cli -hf startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0 # Run inference directly in the terminal: llama cli -hf startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0 # Run inference directly in the terminal: ./llama-cli -hf startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0 # Run inference directly in the terminal: ./build/bin/llama-cli -hf startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0
Use Docker
docker model run hf.co/startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0
- LM Studio
- Jan
- Ollama
How to use startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF with Ollama:
ollama run hf.co/startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0
- Unsloth Desktop
- Pi
How to use startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Docker Model Runner
How to use startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF with Docker Model Runner:
docker model run hf.co/startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0
- Lemonade
How to use startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0
Run and chat with the model
lemonade run user.StartLux-Decision-v1-27B-Q8_0-GGUF-Q8_0
List all available models
lemonade list
- Hermes Agent
How to use startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF:Q8_0" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
Download startlux_decision/model.py from startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF: direct link, hf CLI and curl.
- Browser
- Download file 15.4 kB
-
https://huggingface.co/startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF/resolve/main/startlux_decision/model.py
- Command line
-
hf download hf://startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF/startlux_decision/model.py
-
curl -L -o model.py https://huggingface.co/startlux-models/StartLux-Decision-v1-27B-Q8_0-GGUF/resolve/main/startlux_decision/model.py
15.4 kB
| """StartLux-Decision inference. | |
| Each question is rendered as one prompt (startlux_decision/jevfmt.py), the model runs one forward pass, and the | |
| answer is read from the next-token logits of the option letters at the last prompt position, divided by the | |
| temperature of the question type and normalised over the listed options. Nothing is generated. | |
| All questions of one request run in one forward pass, one row per question. On CUDA the pass is a recorded graph: | |
| one per padded input length (128 ... 4096 tokens) for a single question, and one per (question count, length) for two | |
| to four questions of up to 1024 tokens each. Replaying a graph removes the per-layer launch overhead (the approach of | |
| the JevK5 runtime, Apache-2.0). Inputs are right-padded; every layer is causal and rows never mix, so padding after the | |
| last prompt token never reaches the position that is read. Other requests run eagerly as one padded batch (inputs | |
| padded to one of PAD_LENGTHS); decide_batch() batches questions across many requests. | |
| """ | |
| import importlib | |
| import json | |
| import os | |
| import warnings | |
| import torch | |
| from . import jevfmt as J | |
| __all__ = ["StartLuxDecision", "load_model", "fast_kernels_active"] | |
| GRAPH_LENGTHS = (128, 192, 256, 320, 384, 512, 640, 768, 1024, 1536, 2048, 3072, 4096) | |
| GRAPH_ROWS = (1, 2, 3, 4) # questions per request replayed as one graph | |
| MULTI_ROW_MAX_LENGTH = 1024 # longest prompt for the multi-question graphs (rows x length <= 4096 tokens) | |
| # Batched inputs are padded to one of these lengths. The linear-attention kernels are compiled once per sequence length | |
| # (about a second each), so padding to exact lengths would recompile for almost every batch. Above 256 tokens a step | |
| # adds at most 25% padding. | |
| PAD_LENGTHS = (128, 192, 256, 320, 384, 448, 512, 640, 768, 896, 1024, 1280, 1536, 1792, 2048, 2560, 3072, 3584, 4096, | |
| 5120, 6144, 7168, 8192, 10240, 12288, 14336, 16384, 20480, 24576, 28672, 32768, 40960, 49152, 57344, 65536) | |
| def padded_length(n): | |
| """The length a batch whose longest input has n tokens is padded to.""" | |
| for length in PAD_LENGTHS: | |
| if n <= length: | |
| return length | |
| return -(-n // 8192) * 8192 | |
| def fast_kernels_active(path): | |
| """True when transformers will use the fla / causal-conv1d kernels for the linear-attention layers of the model | |
| in `path`.""" | |
| from transformers import AutoConfig | |
| kind = AutoConfig.from_pretrained(path).model_type | |
| try: | |
| modeling = importlib.import_module(f"transformers.models.{kind}.modeling_{kind}") | |
| except ImportError: | |
| return False | |
| return bool(getattr(modeling, "is_fast_path_available", False)) | |
| def load_model(path, device): | |
| """The checkpoint's own transformers class in bf16 on `device`, and its text decoder (the stack without the | |
| output head; checkpoints that also carry other towers keep the text decoder under .language_model).""" | |
| import transformers | |
| arch = transformers.AutoConfig.from_pretrained(path).architectures[0] | |
| model = getattr(transformers, arch).from_pretrained(path, dtype=torch.bfloat16, device_map={"": device}) | |
| return model, getattr(model.model, "language_model", model.model) | |
| class StartLuxDecision: | |
| """decide(state, questions) -> (answers, usage), answers in the TypeSafe /v1/systemone format.""" | |
| def __init__(self, path, device=None, max_length=65536, max_batch_tokens=65536, graphs=True): | |
| from transformers import AutoTokenizer | |
| if not os.path.isdir(path): | |
| raise FileNotFoundError(f"{path}: expected a local StartLux-Decision directory (weights are shared separately)") | |
| cfg = json.load(open(os.path.join(path, "decision_config.json"))) | |
| self.tok = AutoTokenizer.from_pretrained(path) | |
| self.letters = J.check_tokenizer(self.tok) | |
| if self.letters != cfg["letter_token_ids"]: | |
| raise ValueError("tokenizer letter ids differ from decision_config.json") | |
| self.temperature = {k: float(v) for k, v in cfg["temperature_by_type"].items()} | |
| wide = cfg.get("wide_choice", {}) | |
| self.group, self.keep, self.residual = wide.get("group", 25), wide.get("keep", 3), wide.get("residual", 1e-3) | |
| self.device = torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu")) | |
| self.fast_kernels = fast_kernels_active(path) | |
| if self.device.type == "cuda" and not self.fast_kernels: | |
| msg = ("flash-linear-attention and causal-conv1d are not active, so transformers would run the plain torch " | |
| "path of the linear-attention layers (>10x slower). pip install flash-linear-attention causal-conv1d, " | |
| "or set STARTLUX_ALLOW_SLOW=1 to run anyway.") | |
| if os.environ.get("STARTLUX_ALLOW_SLOW") != "1": | |
| raise RuntimeError(msg) | |
| warnings.warn(msg) | |
| model, body = load_model(path, self.device) | |
| self.body = body.eval() | |
| head = model.get_output_embeddings().weight | |
| self.letter_rows = head.index_select(0, torch.tensor(self.letters, device=head.device)).float() | |
| del model # only the decoder and the letter rows of the output head are used | |
| self.pad = self.tok.pad_token_id | |
| self.max_length, self.max_batch_tokens = int(max_length), int(max_batch_tokens) | |
| self.graphs = {} | |
| if graphs and self.device.type == "cuda" and os.environ.get("STARTLUX_GRAPHS", "1") != "0": | |
| self._capture() | |
| # ---- CUDA-graph path (the questions of one request as rows, right-padded, no attention mask) | |
| def _slot_logits(self, ids, last): | |
| with torch.autocast(self.device.type, dtype=torch.bfloat16): | |
| hidden = self.body(input_ids=ids, use_cache=False, return_dict=True).last_hidden_state | |
| h = hidden[torch.arange(ids.shape[0], device=ids.device), last].float() | |
| return h @ self.letter_rows.T | |
| def _capture(self): | |
| shapes = [(b, n) for b in GRAPH_ROWS for n in GRAPH_LENGTHS if b == 1 or n <= MULTI_ROW_MAX_LENGTH] | |
| pool = None # one memory pool for every graph, sized by the largest | |
| for b, n in sorted(shapes, key=lambda s: (-s[0] * s[1], -s[1])): | |
| ids = torch.full((b, n), self.pad, dtype=torch.long, device=self.device) | |
| last = torch.full((b,), n - 1, dtype=torch.long, device=self.device) | |
| stream = torch.cuda.Stream(device=self.device) | |
| stream.wait_stream(torch.cuda.current_stream(self.device)) | |
| with torch.cuda.stream(stream): | |
| for _ in range(3): # autotune and warm every kernel outside the capture | |
| self._slot_logits(ids, last) | |
| torch.cuda.current_stream(self.device).wait_stream(stream) | |
| graph = torch.cuda.CUDAGraph() | |
| with torch.cuda.graph(graph, pool=pool): | |
| out = self._slot_logits(ids, last) | |
| pool = graph.pool() | |
| self.graphs[(b, n)] = (graph, ids, last, out) | |
| def _graph_length(self, enc): | |
| """The recorded length for these rows, or None when no graph fits.""" | |
| longest = max(len(t) for _, t, _ in enc) | |
| fits = [n for b, n in self.graphs if b == len(enc) and n >= longest] | |
| return min(fits) if fits else None | |
| def _graph_logits(self, enc, n): | |
| graph, static_ids, last, out = self.graphs[(len(enc), n)] | |
| ids = torch.full((len(enc), n), self.pad, dtype=torch.long) | |
| for j, (_, t, _) in enumerate(enc): | |
| ids[j, :len(t)] = torch.tensor(t) | |
| static_ids.copy_(ids) | |
| last.copy_(torch.tensor([len(t) - 1 for _, t, _ in enc])) | |
| graph.replay() | |
| res = out.cpu() | |
| return [res[j, :c].clone() for j, (_, _, c) in enumerate(enc)] | |
| # ---- readout | |
| def _logits(self, rows): | |
| """rows: rendered records (<= 26 options) -> (letter logits per row in option order, prompt tokens).""" | |
| enc = [] | |
| for i, r in enumerate(rows): | |
| order = [o["id"] for o in r["options"]] | |
| ids, _ = J.render_ids(r, self.tok, order, max_length=self.max_length) | |
| enc.append((i, ids, len(order))) | |
| out, tokens = [None] * len(rows), 0 | |
| n = self._graph_length(enc) if self.graphs and enc else None | |
| if n is not None: # every question of the request in one replay | |
| for (i, t, _), z in zip(enc, self._graph_logits(enc, n)): | |
| out[i] = z | |
| tokens += len(t) | |
| return out, tokens | |
| enc.sort(key=lambda x: -len(x[1])) | |
| start = 0 | |
| while start < len(enc): | |
| longest = padded_length(len(enc[start][1])) | |
| n = max(1, min(len(enc) - start, self.max_batch_tokens // longest)) | |
| batch = enc[start:start + n] | |
| start += n | |
| ids = torch.full((len(batch), longest), self.pad, dtype=torch.long) | |
| attn = torch.zeros_like(ids) | |
| for j, (_, t, _) in enumerate(batch): | |
| ids[j, :len(t)] = torch.tensor(t) | |
| attn[j, :len(t)] = 1 | |
| tokens += len(t) | |
| ids, attn = ids.to(self.device), attn.to(self.device) | |
| with torch.autocast(self.device.type, dtype=torch.bfloat16): | |
| hidden = self.body(input_ids=ids, attention_mask=attn, use_cache=False, return_dict=True).last_hidden_state | |
| pos = attn.sum(1) - 1 | |
| h = hidden[torch.arange(len(batch), device=self.device), pos].float() | |
| logits = h @ self.letter_rows.T | |
| for j, (i, _, c) in enumerate(batch): | |
| out[i] = logits[j, :c].cpu() | |
| return out, tokens | |
| def _probs(self, rows): | |
| logits, tokens = self._logits(rows) | |
| return [torch.softmax(z / self.temperature.get(r["type"], 1.0), -1).tolist() for r, z in zip(rows, logits)], tokens | |
| def self_test(self, state, questions): | |
| """Largest |p_graph - p_eager| over the questions of one request (0.0 when no graphs were recorded).""" | |
| if not self.graphs: | |
| return 0.0 | |
| rows = [J.from_systemone(state, q) for q in questions.values()] | |
| graphed, _ = self._probs(rows) | |
| graphs, self.graphs = self.graphs, {} | |
| try: | |
| eager, _ = self._probs(rows) | |
| finally: | |
| self.graphs = graphs | |
| return max(abs(a - b) for pg, pe in zip(graphed, eager) for a, b in zip(pg, pe)) | |
| def _wide(self, state, spec): | |
| """Choice lists over 26 options: near-equal groups, the top `keep` of each group go to a final round.""" | |
| keys = list(spec["criteria"]) | |
| n_groups = -(-len(keys) // self.group) | |
| size, extra = divmod(len(keys), n_groups) | |
| groups, start = [], 0 | |
| for g in range(n_groups): | |
| end = start + size + (1 if g < extra else 0) | |
| groups.append(keys[start:end]) | |
| start = end | |
| rows = [J.from_systemone(state, dict(spec, criteria={k: spec["criteria"][k] for k in g})) for g in groups] | |
| first, tokens = self._probs(rows) | |
| first_p = {k: p for g, ps in zip(groups, first) for k, p in zip(g, ps)} | |
| finalists = [k for g, ps in zip(groups, first) for k, _ in sorted(zip(g, ps), key=lambda x: -x[1])[:self.keep]] | |
| fin_spec = dict(spec, criteria={k: spec["criteria"][k] for k in finalists}) | |
| if len(finalists) > J.MAX_OPTIONS: | |
| final_p, more = self._wide(state, fin_spec) | |
| else: | |
| ps, more = self._probs([J.from_systemone(state, fin_spec)]) | |
| final_p = dict(zip(finalists, ps[0])) | |
| rest = [k for k in keys if k not in set(finalists)] | |
| mass = sum(first_p[k] for k in rest) or 1.0 | |
| probs = {k: final_p[k] * (1 - self.residual) for k in finalists} | |
| probs.update({k: self.residual * first_p[k] / mass for k in rest}) | |
| z = sum(probs.values()) | |
| return {k: v / z for k, v in probs.items()}, tokens + more | |
| # ---- public API | |
| def _answer(row, p, question): | |
| ids = [o["id"] for o in row["options"]] | |
| if row["type"] == "noul": | |
| return {"type": "noul", "noul": p[ids.index("true")]} | |
| if row["type"] == "score": | |
| levels = question.get("criteria") or [] | |
| levels = list(levels.values()) if isinstance(levels, dict) else list(levels) | |
| return {"type": "score", "score": sum(i * v for i, v in enumerate(p)), "confidence": max(p), | |
| "legend": {str(i): (levels[i] if i < len(levels) else str(i)) for i in range(len(p))}, | |
| "probabilities": {str(i): v for i, v in enumerate(p)}} | |
| dist = dict(zip(ids, p)) | |
| best = max(dist, key=dist.get) | |
| return {"type": "choice", "choice": best, "confidence": dist[best], "probabilities": dist} | |
| def _split(self, state, questions): | |
| """-> (answers decided without the model, [(key, rendered row)], tokens spent on wide lists)""" | |
| answers, rows, tokens = {}, [], 0 | |
| for k, q in questions.items(): | |
| t = q.get("type", "choice") | |
| crit = q.get("criteria") or {} | |
| if t == "choice" and isinstance(crit, dict) and len(crit) == 1: | |
| only = next(iter(crit)) | |
| answers[k] = {"type": "choice", "choice": only, "confidence": 1.0, "probabilities": {only: 1.0}} | |
| elif t == "choice" and isinstance(crit, dict) and len(crit) > J.MAX_OPTIONS: | |
| p, n = self._wide(state, q) | |
| tokens += n | |
| best = max(p, key=p.get) | |
| answers[k] = {"type": "choice", "choice": best, "confidence": p[best], "probabilities": p} | |
| else: | |
| rows.append((k, J.from_systemone(state, q))) | |
| return answers, rows, tokens | |
| def decide(self, state, questions): | |
| """One request -> (answers, usage), answers in the TypeSafe /v1/systemone format.""" | |
| answers, rows, tokens = self._split(state, questions) | |
| if rows: | |
| probs, n = self._probs([r for _, r in rows]) | |
| tokens += n | |
| for (k, row), p in zip(rows, probs): | |
| answers[k] = self._answer(row, p, questions[k]) | |
| return answers, {"input_tokens": tokens, "output_tokens": 0} | |
| def decide_batch(self, requests): | |
| """[(state, questions), ...] -> [answers, ...]. Every question of every request goes through one length-sorted | |
| set of padded forward passes (up to max_batch_tokens each), which is much faster than calling decide() in a loop | |
| when the requests are short. Answers are the same as decide() up to bf16 rounding.""" | |
| parts, flat = [], [] | |
| for state, questions in requests: | |
| answers, rows, _ = self._split(state, questions) | |
| parts.append((answers, rows, questions)) | |
| flat.extend(r for _, r in rows) | |
| graphs, self.graphs = self.graphs, {} # batched eager path; graphs are sized for one request | |
| try: | |
| probs, _ = self._probs(flat) if flat else ([], 0) | |
| finally: | |
| self.graphs = graphs | |
| out, i = [], 0 | |
| for answers, rows, questions in parts: | |
| for k, row in rows: | |
| answers[k] = self._answer(row, probs[i], questions[k]) | |
| i += 1 | |
| out.append(answers) | |
| return out | |