Image-Text-to-Text
Transformers
Safetensors
GGUF
qwen3_5
system-one
decision-making
classification
calibration
structured-output
on-device
multimodal
Instructions to use bynn-intelligence-com/decisionnode-edge with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use bynn-intelligence-com/decisionnode-edge with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="bynn-intelligence-com/decisionnode-edge")# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("bynn-intelligence-com/decisionnode-edge") model = AutoModelForMultimodalLM.from_pretrained("bynn-intelligence-com/decisionnode-edge", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use bynn-intelligence-com/decisionnode-edge 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 bynn-intelligence-com/decisionnode-edge:Q8_0 # Run inference directly in the terminal: llama cli -hf bynn-intelligence-com/decisionnode-edge:Q8_0
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf bynn-intelligence-com/decisionnode-edge:Q8_0 # Run inference directly in the terminal: llama cli -hf bynn-intelligence-com/decisionnode-edge: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 bynn-intelligence-com/decisionnode-edge:Q8_0 # Run inference directly in the terminal: ./llama-cli -hf bynn-intelligence-com/decisionnode-edge: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 bynn-intelligence-com/decisionnode-edge:Q8_0 # Run inference directly in the terminal: ./build/bin/llama-cli -hf bynn-intelligence-com/decisionnode-edge:Q8_0
Use Docker
docker model run hf.co/bynn-intelligence-com/decisionnode-edge:Q8_0
- Llama new
- LM Studio
- Jan
- vLLM
How to use bynn-intelligence-com/decisionnode-edge with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "bynn-intelligence-com/decisionnode-edge" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "bynn-intelligence-com/decisionnode-edge", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/bynn-intelligence-com/decisionnode-edge:Q8_0
- SGLang
How to use bynn-intelligence-com/decisionnode-edge with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "bynn-intelligence-com/decisionnode-edge" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "bynn-intelligence-com/decisionnode-edge", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "bynn-intelligence-com/decisionnode-edge" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "bynn-intelligence-com/decisionnode-edge", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Ollama
How to use bynn-intelligence-com/decisionnode-edge with Ollama:
ollama run hf.co/bynn-intelligence-com/decisionnode-edge:Q8_0
- Unsloth Desktop
- Docker Model Runner
How to use bynn-intelligence-com/decisionnode-edge with Docker Model Runner:
docker model run hf.co/bynn-intelligence-com/decisionnode-edge:Q8_0
- Lemonade
How to use bynn-intelligence-com/decisionnode-edge with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull bynn-intelligence-com/decisionnode-edge:Q8_0
Run and chat with the model
lemonade run user.decisionnode-edge-Q8_0
List all available models
lemonade list
- Atomic Chat
File size: 15,263 Bytes
071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e 7706b50 071512e 7706b50 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | """Load a DecisionNode Edge model directory and answer requests.
from decisionnode_edge import EdgeModel
model = EdgeModel.load("path/to/decisionnode-edge")
response = model.decide({"state": "...", "questions": {"route": {"type": "choice", "criteria": {...}}}})
The model reads the prompt once per question and never generates text. Its answer is read at the
<|answer|> position: the last hidden state there is scored against a small readout table
(readout.safetensors), one row per answer code, and only the rows of the question's codes are read.
Calibration (calibration.json) turns those scores into probabilities.
A point or box question is a few such prompts, pass by pass (places.py): whether the thing is in the picture,
then where along each side, each later prompt given what the earlier ones read.
The backbone is read by torch (transformers) or by a faster backend (backends.py: vLLM, OpenVINO, MLX,
llama.cpp); the readout and calibration are this module's in every case.
"""
from __future__ import annotations
import base64
import io
import json
import logging
import os
from typing import Any
import torch
from safetensors.torch import load_file
from . import answers, backends, places, request as requests
from .template import Template
CONFIG_FILE = "decisionnode_edge.json"
log = logging.getLogger("decisionnode_edge")
def load_backbone(path: str, device: str | None, dtype: str | torch.dtype | None) -> tuple[Any, bool]:
"""The transformers backbone on `device` (default CUDA when present, else the CPU) in `dtype` (default bfloat16 on
CUDA, float32 elsewhere), and whether it runs under bf16 autocast."""
from transformers import AutoModelForImageTextToText
device = device or ("cuda" if torch.cuda.is_available() else "cpu")
if dtype is None:
dtype = torch.bfloat16 if str(device).startswith("cuda") else torch.float32
dtype = getattr(torch, dtype) if isinstance(dtype, str) else dtype
backbone = AutoModelForImageTextToText.from_pretrained(path, dtype=dtype).to(device).eval()
# bf16 weights run under bf16 autocast, the reference numerics: some operations then compute in float32
return backbone, dtype == torch.bfloat16
class EdgeModel:
def __init__(self, backbone: Any, tokenizer: Any, readout: torch.Tensor, config: dict, calibration: dict, path: str, autocast: bool,
runner: Any = None, torch_options: dict | None = None): # fmt: skip
self.backbone = backbone # the transformers model; None until torch is needed when a faster backend reads the prompts
self.tokenizer = tokenizer
self.readout = readout
self.config = config
self.calibration = calibration
self.path = path
self.autocast = autocast
self.runner = runner # the backend object (backends.py), or None: torch reads every prompt
self.torch_options = torch_options or {} # device and dtype for torch when it is loaded on first use
self.template = Template(tokenizer, bytes.fromhex(config["template"]["code_seed_hex"]).decode("utf-8"))
if self.template.first_id != config["readout"]["first_token_id"]:
raise ValueError("the tokenizer and the readout table disagree on the ids of the added tokens")
self.limits = config["limits"]
self._images = None
@classmethod
def load(cls, path: str, device: str | None = None, dtype: str | torch.dtype | None = None, backend: str | None = None,
artifacts: str | None = None, options: dict | None = None) -> "EdgeModel": # fmt: skip
"""Load the model directory.
`backend`: "auto" (the default, or DECISIONNODE_EDGE_BACKEND), "torch", "vllm", "openvino", "mlx" or "llamacpp".
Auto takes the fastest that is installed and can run here (backends.ORDER). `artifacts`: the directory the build
tool wrote for openvino or llamacpp (or DECISIONNODE_EDGE_ARTIFACTS); for either asked for by name it defaults to
the model directory when that holds a build (the GGUF that ships with the model). `options`: the backend's settings (the
README; DECISIONNODE_EDGE_OPTIONS as JSON is read under them).
`device` defaults to CUDA when present, else the CPU, and `dtype` to bfloat16 on CUDA (the reference
precision) and float32 elsewhere; both are torch's, also when torch only reads the prompts a
faster backend cannot take."""
from transformers import AutoTokenizer
with open(os.path.join(path, CONFIG_FILE), encoding="utf-8") as f:
config = json.load(f)
with open(os.path.join(path, config["calibration"]), encoding="utf-8") as f:
calibration = json.load(f)
tokenizer = AutoTokenizer.from_pretrained(path)
table = load_file(os.path.join(path, config["readout"]["file"]))[config["readout"]["tensor"]]
artifacts = artifacts or os.environ.get(backends.ENV_ARTIFACTS) or None
if artifacts is None and (backend or os.environ.get(backends.ENV_BACKEND) or "").strip().lower() in ("openvino", "llamacpp") and backends.read_build(path):
artifacts = path # a build that ships in the model folder (the GGUF), for a backend asked for by name
options = {**backends.env_options(), **(options or {})}
name = backends.choose(backend, device, artifacts, options)
log.info("the %s backend reads the prompts", name)
if name == "torch":
backbone, autocast = load_backbone(path, device, dtype)
return cls(backbone, tokenizer, table.float().to(backbone.device), config, calibration, path, autocast)
runner = backends.load(name, path, config, artifacts, options, device)
# the readout and calibration stay here, in float32 on the CPU
return cls(None, tokenizer, table.float(), config, calibration, path, False, runner=runner, torch_options={"device": device, "dtype": dtype})
@property
def device(self) -> torch.device:
return self.readout.device
@property
def backend(self) -> str:
"""The backend that reads the prompts ("torch" also reads, on any backend, the pictures and the prompts longer than
the backend takes)."""
return self.runner.name if self.runner is not None else "torch"
# -- public -----------------------------------------------------------
def decide(self, body: dict) -> dict:
"""Answer a `/v1/decide` request body. Returns {"model", "answers", "usage"} as the hosted API does."""
return self._decide(requests.parse(body))
def _decide(self, parsed: requests.Request, exact_sum: bool = False) -> dict:
"""The answers of a parsed request; `exact_sum`: choice and score lists that sum to exactly 1.00 (/v1/systemone)."""
probs, input_tokens, placed = self._predict(parsed)
out = {}
for q in parsed.questions:
if q.place is not None:
out[q.id] = placed[q.id]
continue
p = probs[q.id]
if q.type == "choice":
out[q.id] = answers.choice_answer(q.keys, p, exact_sum)
elif q.type == "score":
out[q.id] = answers.score_answer(q.levels, p, exact_sum)
elif q.type == "truth":
out[q.id] = answers.truth_answer(p)
else:
out[q.id] = answers.number_answer(q.grid, p)
return {"model": self.config["name"], "answers": out, "usage": {"input_tokens": input_tokens, "output_tokens": 0}}
def predict(self, body: dict) -> dict[str, list[float]]:
"""The unrounded calibrated probabilities of every question: per option or level in request order, [yes, no]
for truth, per grid value for number. A point or box question is answered by `decide` only."""
parsed = requests.parse(body)
for q in parsed.questions:
if q.place is not None:
raise requests.RequestError(f"question {q.id!r}: a {q.type} question is answered by decide(), not predict()")
return self._predict(parsed)[0]
# -- internals --------------------------------------------------------
def _predict(self, parsed: requests.Request) -> tuple[dict[str, list[float]], int, dict[str, dict]]:
"""(the probabilities of every question but the point and box ones, the input tokens, the point and box answers)."""
images = self._load_images(parsed.images) if parsed.images else None
image_ids = [image["id"] for image in parsed.images]
out: dict[str, list[float]] = {}
state_tokens, total = None, 0
def read(qid: str, row: dict) -> list[float]:
nonlocal state_tokens, total
encoded = self.template.encode(row, parsed.state, image_ids, images[2] if images else None)
if len(encoded.input_ids) > self.limits["max_input_tokens"]:
raise requests.RequestError(f"question {qid!r} needs {len(encoded.input_ids):,} tokens; the maximum context length is {self.limits['max_input_tokens']:,} tokens",
error_type="max_tokens_exceeded") # fmt: skip
probs = self._probabilities(encoded, row, images, bool(image_ids))
# the state and its pictures count once, every question's own tokens on top (as the hosted usage counts)
total += len(encoded.input_ids) - (encoded.state_tokens if state_tokens is not None else 0)
state_tokens = encoded.state_tokens
return probs
specs = []
for q in parsed.questions:
if q.place is not None:
specs.append(q.place)
continue
answered = [read(q.id, row) for row in q.rows]
if q.type == "number":
out[q.id] = [1.0] if not q.rows else requests.compose(q.grid, answered)
else:
out[q.id] = answered[0] if answered else [1.0]
placed: dict[str, dict] = {}
if specs: # every point and box question's readings, pass by pass, each pass given the readings before it
plan = places.Plan(specs)
while True:
rows = plan.rows()
if not rows:
break
plan.take(rows, [read(plan.chains[n].spec.qid, row) for n, row in rows])
placed = plan.answers(self._image_sizes(parsed.images))
return out, total, placed
def _probabilities(self, encoded: Any, row: dict, images: tuple | None, has_images: bool) -> list[float]:
"""One prompt's class probabilities: the readout's scores of its codes, calibrated (a coordinate row's softmax)."""
return answers.calibrated(self._logits(encoded, images), row["type"], self.calibration, has_images)
@torch.inference_mode()
def _logits(self, encoded: Any, images: tuple | None) -> list[float]:
if self.runner is not None and images is None and self.runner.accepts(len(encoded.input_ids)):
h = self.runner.hidden(encoded.input_ids).to(self.device, torch.float32)
else:
h = self._torch_hidden(encoded, images).to(self.device)
scores = h @ self.readout.t() # one score per added token, never the vocabulary
rows = torch.tensor(encoded.class_ids, device=self.device) - self.template.first_id
return scores[rows].tolist()
def _torch_hidden(self, encoded: Any, images: tuple | None) -> torch.Tensor:
"""The hidden state at the <|answer|> position (the last token) from the transformers backbone, float32."""
if self.backbone is None:
log.warning("loading torch for the prompts the %s backend does not take (pictures, longer inputs)", self.backend)
self.backbone, self.autocast = load_backbone(self.path, self.torch_options.get("device"), self.torch_options.get("dtype"))
device = self.backbone.device
input_ids = torch.tensor([encoded.input_ids], device=device)
extra = {}
if images is not None:
extra = {
"pixel_values": images[0].to(device),
"image_grid_thw": images[1].to(device),
"mm_token_type_ids": (input_ids == self.template.image_pad).long(),
}
with torch.autocast(device_type=device.type, dtype=torch.bfloat16, enabled=self.autocast):
hidden = self.backbone.model(input_ids=input_ids, attention_mask=torch.ones_like(input_ids), use_cache=False, **extra).last_hidden_state
return hidden[0, -1].float()
def _image_sizes(self, images: list[dict]) -> dict[str, tuple[int, int]]:
return image_sizes(images)
def _load_images(self, images: list[dict]) -> tuple[torch.Tensor, torch.Tensor, dict[str, int]]:
"""(pixel values, grid, placeholder tokens per image id) of a request's pictures: EXIF orientation applied,
RGB, the longest side at most max_image_side, then the backbone's own image processor."""
from PIL import Image, ImageOps
from transformers import AutoImageProcessor
if self._images is None:
self._images = AutoImageProcessor.from_pretrained(self.path)
max_side = int(self.limits["max_image_side"])
fitted = []
for image in images:
source = image["path"] if image.get("path") is not None else io.BytesIO(base64.b64decode(image["data"]))
try:
with Image.open(source) as im:
im = ImageOps.exif_transpose(im).convert("RGB")
if max(im.size) > max_side:
scale = max_side / max(im.size)
im = im.resize((max(1, round(im.width * scale)), max(1, round(im.height * scale))), Image.BICUBIC)
fitted.append(im.copy())
except (OSError, ValueError) as error:
raise requests.RequestError(f"Image {image['id']!r} could not be read: {error}") from None
out = self._images(images=fitted, return_tensors="pt")
merge = int(getattr(self._images, "merge_size", 2))
counts = (out["image_grid_thw"].prod(dim=-1) // merge**2).tolist()
return out["pixel_values"], out["image_grid_thw"], {image["id"]: int(n) for image, n in zip(images, counts)}
def image_sizes(images: list[dict]) -> dict[str, tuple[int, int]]:
"""Per picture id, its width and height as sent after its EXIF orientation (the sides swapped when the orientation
turns it a quarter, 5 to 8), read from the file's header: the size a point or box answer is given in."""
from PIL import Image
out = {}
for image in images:
source = image["path"] if image.get("path") is not None else io.BytesIO(base64.b64decode(image["data"]))
try:
with Image.open(source) as im:
width, height = im.size
try:
orientation = int(im.getexif().get(0x0112, 1))
except Exception:
orientation = 1
except (OSError, ValueError) as error:
raise requests.RequestError(f"Image {image['id']!r} could not be read: {error}") from None
out[image["id"]] = (height, width) if orientation in (5, 6, 7, 8) else (width, height)
return out
|