Image-Text-to-Text
Transformers
Safetensors
lfm2_vl
liquid
lfm2.5
edge
decision
classification
calibration
system-one
multimodal
decision-model
conversational
custom_code
Instructions to use LiquidAI/d1-3B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use LiquidAI/d1-3B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="LiquidAI/d1-3B", trust_remote_code=True) messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("LiquidAI/d1-3B", trust_remote_code=True) model = AutoModelForMultimodalLM.from_pretrained("LiquidAI/d1-3B", trust_remote_code=True, device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use LiquidAI/d1-3B with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "LiquidAI/d1-3B" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "LiquidAI/d1-3B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/LiquidAI/d1-3B
- SGLang
How to use LiquidAI/d1-3B 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 "LiquidAI/d1-3B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "LiquidAI/d1-3B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'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 "LiquidAI/d1-3B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "LiquidAI/d1-3B", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use LiquidAI/d1-3B with Docker Model Runner:
docker model run hf.co/LiquidAI/d1-3B
Download prompt.py from LiquidAI/d1-3B: direct link, hf CLI and curl.
- Browser
- Download file 12.5 kB
-
https://huggingface.co/LiquidAI/d1-3B/resolve/main/prompt.py
- Command line
-
hf download hf://LiquidAI/d1-3B/prompt.py
-
curl -L -o prompt.py https://huggingface.co/LiquidAI/d1-3B/resolve/main/prompt.py
12.5 kB
| """The prompt and the readout. A System One decision is a single forward pass that stops at the answer | |
| slot: everything the model sees is built here, and the answer is a softmax over its options' tokens. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import math | |
| import re | |
| from dataclasses import dataclass | |
| from functools import lru_cache | |
| from typing import Any, Mapping, Sequence | |
| # The system turn: none by default. | |
| SYSTEMS: dict[str, str | None] = { | |
| "none": None, | |
| "isolated": ( | |
| "You are a System One decision model. Answer only the current isolated " | |
| "question from the shared state. Other questions do not exist." | |
| ), | |
| } | |
| DEFAULT_SYSTEM = "none" | |
| IM_START = "<|im_start|>" | |
| IM_END = "<|im_end|>" | |
| # --------------------------------------------------------------------------- # | |
| # question types | |
| # --------------------------------------------------------------------------- # | |
| class Choice: | |
| instructions: str | |
| criteria: Mapping[str, str | None] | |
| type: str = "choice" | |
| class Noul: | |
| instructions: str | |
| criteria: Mapping[str, Any] | None = None | |
| type: str = "noul" | |
| class Score: | |
| instructions: str | |
| criteria: Sequence[str] | |
| type: str = "score" | |
| Question = Choice | Noul | Score | |
| # --------------------------------------------------------------------------- # | |
| # verbalizer | |
| # --------------------------------------------------------------------------- # | |
| def option_codes(labels: Sequence[str]) -> list[str]: | |
| """Native letters when the labels already are letters, else A..Z, else 00..; one rule for every | |
| cardinality.""" | |
| labs = [str(x).strip() for x in labels] | |
| if labs and all(len(k) == 1 and k.isalpha() for k in labs): | |
| return labs | |
| if len(labs) <= 26: | |
| return [chr(ord("A") + i) for i in range(len(labs))] | |
| return [f"{i:02d}" for i in range(len(labs))] | |
| _FALLBACK_POOL = ( | |
| [chr(c) for c in range(ord("A"), ord("Z") + 1)] | |
| + [f"{i:02d}" for i in range(100)] | |
| + [chr(c) for c in range(ord("a"), ord("z") + 1)] | |
| + [f"#{i}" for i in range(200)] | |
| # Where a tokenizer splits digits, "00".."99" and "#i" are two tokens, and two capital letters are | |
| # often one. Last in the pool, so a tokenizer with digit pairs never reaches it. | |
| + [chr(a) + chr(b) for a in range(ord("A"), ord("Z") + 1) for b in range(ord("A"), ord("Z") + 1)] | |
| ) | |
| def _aliases_cached(tokenizer_key: int, codes: tuple[str, ...]) -> tuple[tuple[str, int], ...]: | |
| tokenizer = _TOKENIZERS[tokenizer_key] | |
| used: set[int] = set() | |
| out: list[tuple[str, int]] = [] | |
| def take(raw: str) -> bool: | |
| enc = tokenizer.encode(raw, add_special_tokens=False) | |
| if len(enc) != 1 or enc[0] in used: | |
| return False | |
| out.append((raw, enc[0])) | |
| used.add(enc[0]) | |
| return True | |
| for code in codes: | |
| if take(code): | |
| continue | |
| if not any(take(raw) for raw in _FALLBACK_POOL): | |
| raise RuntimeError(f"no single-token alias left for {len(codes)} options") | |
| return tuple(out) | |
| _TOKENIZERS: dict[int, object] = {} | |
| def aliases(tokenizer, labels: Sequence[str]) -> list[tuple[str, int]]: | |
| """Assign every label a distinct single-token code: [(code, token_id)]. | |
| Memoised on the codes rather than on the labels: the codes are positional | |
| unless the labels are already letters, so every option list of one length | |
| shares an entry. | |
| """ | |
| _TOKENIZERS.setdefault(id(tokenizer), tokenizer) | |
| codes = tuple(option_codes(labels)) | |
| return list(_aliases_cached(id(tokenizer), codes)) | |
| def _ids_cached(tokenizer_key: int, texts: tuple[str, ...]) -> tuple[int, ...]: | |
| tokenizer = _TOKENIZERS[tokenizer_key] | |
| out, seen = [], set() | |
| for t in texts: | |
| enc = tokenizer.encode(t, add_special_tokens=False) | |
| if len(enc) == 1 and enc[0] not in seen: | |
| out.append(enc[0]) | |
| seen.add(enc[0]) | |
| return tuple(out) | |
| def _ids(tokenizer, texts: Sequence[str]) -> list[int]: | |
| _TOKENIZERS.setdefault(id(tokenizer), tokenizer) | |
| return list(_ids_cached(id(tokenizer), tuple(texts))) | |
| def as_question(q: Mapping | Question) -> Question: | |
| """A question in the Decision Index's JSON, `{"type": "noul" | "choice" | "score", "instructions", | |
| "criteria"}`, as one of the classes above; a class passes through.""" | |
| if not isinstance(q, Mapping): | |
| return q | |
| kind = q.get("type", "choice") | |
| if kind == "noul": | |
| return Noul(q["instructions"], q.get("criteria")) | |
| if kind == "score": | |
| return Score(q["instructions"], list(q["criteria"])) | |
| return Choice(q["instructions"], q["criteria"]) | |
| YES_FORMS = ("yes", "Yes", "YES") | |
| NO_FORMS = ("no", "No", "NO") | |
| def readout_ids(tokenizer, q: Question) -> list[list[int]]: | |
| """Token ids to score, one group per option, max-pooled: the answer is a softmax over these and | |
| nothing else.""" | |
| if isinstance(q, Noul): | |
| yes, no = _ids(tokenizer, YES_FORMS), _ids(tokenizer, NO_FORMS) | |
| if not yes or not no: | |
| raise RuntimeError("tokenizer has no single-token yes/no") | |
| return [yes, no] | |
| if isinstance(q, Score): | |
| groups = [_ids(tokenizer, [str(i)]) for i in range(len(q.criteria))] | |
| if any(not g for g in groups): | |
| raise RuntimeError( | |
| f"score with {len(q.criteria)} levels needs single-token digits; " | |
| "the primitive is defined for 2 to 10" | |
| ) | |
| return groups | |
| groups = [] | |
| for code, tid in aliases(tokenizer, list(q.criteria.keys())): | |
| extra = _ids(tokenizer, [f" {code}"]) | |
| groups.append([tid] + [i for i in extra if i != tid]) | |
| if not groups: | |
| raise RuntimeError("choice with no options") | |
| return groups | |
| def readout(tokenizer, q: Question, logz, calibration=None) -> list[float]: | |
| """Option probabilities from the log-probabilities at the answer slot. | |
| `logz` is indexed by token id: a vocabulary tensor, or a dict holding at | |
| least the question's option tokens. Each option scores its best form. | |
| """ | |
| scores = [max(float(logz[i]) for i in g) for g in readout_ids(tokenizer, q)] | |
| if calibration is not None: | |
| scores = calibration.apply(q, scores) | |
| m = max(scores) | |
| exps = [math.exp(s - m) for s in scores] | |
| return [e / sum(exps) for e in exps] | |
| # --------------------------------------------------------------------------- # | |
| # state and question rendering | |
| # --------------------------------------------------------------------------- # | |
| DEFAULT_MODEL = "LiquidAI/LFM2.5-VL-3B" | |
| # How a state is rendered: `json_only`, the default, writes every state as the object it is; `json` keeps | |
| # three shortcuts (`Message:`, `Passage:` / `Asked:`, a lone question's text); `sections` writes nested | |
| # states as labelled blocks. | |
| DEFAULT_STATE_STYLE = "json_only" | |
| def _is_scalar(v: Any) -> bool: | |
| return v is None or isinstance(v, (str, int, float, bool)) and "\n" not in str(v) | |
| def _sections(obj: Any, path: str, out: list[str]) -> None: # noqa: C901 | |
| """Flatten a nested state into labelled blocks, keeping real newlines. | |
| `json.dumps` escapes every newline inside a log line or a record, so a | |
| multi-line record would arrive as one string of `\n`; this keeps it readable. | |
| """ | |
| head = f"[{path}]\n" if path else "" | |
| if isinstance(obj, dict): | |
| scalars = [(k, v) for k, v in obj.items() if _is_scalar(v)] | |
| rest = [(k, v) for k, v in obj.items() if not _is_scalar(v)] | |
| if scalars: | |
| body = "\n".join(f"{k}: {'' if v is None else v}" for k, v in scalars) | |
| out.append(f"{head}{body}") | |
| for k, v in rest: | |
| _sections(v, f"{path}.{k}" if path else str(k), out) | |
| return | |
| if isinstance(obj, (list, tuple)): | |
| if obj and all(_is_scalar(v) for v in obj): | |
| body = "\n".join(f"- {'' if v is None else v}" for v in obj) | |
| out.append(f"{head}{body}") | |
| return | |
| for i, v in enumerate(obj, start=1): | |
| _sections(v, f"{path} {i}/{len(obj)}" if path else f"{i}/{len(obj)}", out) | |
| return | |
| out.append(f"{head}{'' if obj is None else obj}") | |
| def render_state(state: Any) -> str: | |
| if isinstance(state, str): | |
| return state | |
| out: list[str] = [] | |
| _sections(state, "", out) | |
| return "\n\n".join(out) | |
| def state_block(state: Any, style: str = DEFAULT_STATE_STYLE) -> str: | |
| """Flatten a state into the block that precedes QUESTION:. | |
| Without `_only`, three shapes get a shortcut: a bare utterance becomes | |
| `Message:`, a passage and a question `Passage:` / `Asked:`, a lone question | |
| its text. The `_only` styles (the default) render every state as the object | |
| it is. | |
| """ | |
| if isinstance(state, dict) and not style.endswith("_only"): | |
| keys = set(state.keys()) | |
| if keys == {"text"}: | |
| return f"Message: {state['text']}\n\n" | |
| if {"passage", "question"} <= keys and len(keys) == 2: | |
| return f"Passage: {state['passage']}\n\nAsked: {state['question']}\n\n" | |
| if keys == {"question"}: | |
| return f"{state['question']}\n\n" | |
| if style.startswith("json"): | |
| if isinstance(state, str): | |
| return f"{state}\n\n" | |
| return json.dumps(state, ensure_ascii=False, indent=2) + "\n\n" | |
| return f"{render_state(state)}\n\n" | |
| _PLACEHOLDER = re.compile(r"^opt\d+$") | |
| def _option_line(code: str, label: str, desc: str | None, style: str) -> str: | |
| text = desc or label.replace("_", " ") | |
| if style == "name_desc" and not _PLACEHOLDER.match(label) and label != text: | |
| return f"{code} {label}: {text}" | |
| return f"{code} {text}" | |
| def question_block(tokenizer, q: Question, option_style: str = "desc") -> str: | |
| if isinstance(q, Choice): | |
| labels = list(q.criteria.keys()) | |
| codes = aliases(tokenizer, labels) | |
| lines = "\n".join( | |
| _option_line(codes[i][0], lab, q.criteria[lab], option_style) | |
| for i, lab in enumerate(labels) | |
| ) | |
| return ( | |
| f"{q.instructions}\n\nOptions:\n{lines}\n\n" | |
| "Reply with the option code only." | |
| ) | |
| if isinstance(q, Noul): | |
| extra = "" | |
| if q.criteria: | |
| extra = f"\nYes: {q.criteria.get('true')}\nNo: {q.criteria.get('false')}" | |
| return f"{q.instructions}{extra}\n\nReply with yes or no only." | |
| if isinstance(q, Score): | |
| legend = "\n".join(f"{i} {name}" for i, name in enumerate(q.criteria)) | |
| return ( | |
| f"{q.instructions}\n\n{legend}\n\n" | |
| f"Reply with a single digit 0-{len(q.criteria) - 1} only." | |
| ) | |
| raise TypeError(f"unknown question type {type(q)}") | |
| def prefix_text( | |
| tokenizer, | |
| state: Any, | |
| bos: str = "", | |
| style: str = DEFAULT_STATE_STYLE, | |
| system: str = DEFAULT_SYSTEM, | |
| images: str = "", | |
| ) -> str: | |
| """Everything before the question, shared by all questions on one state: the pictures' markup | |
| (`images`, as the chat template writes them) at the head of the user turn, then the state. With no | |
| state (`None`) the question follows the pictures directly.""" | |
| text = SYSTEMS[system] | |
| turn = "" if text is None else f"{IM_START}system\n{text}{IM_END}\n" | |
| body = "" if state is None else f"{state_block(state, style)}\nQUESTION:\n" | |
| return f"{bos}{turn}{IM_START}user\n{images}{body}" | |
| # What sits between the assistant header and the answer slot, per model type: nothing on LFM2-VL, whose | |
| # template opens no reasoning block. | |
| DEFAULT_LEAD = "" | |
| LEADS: dict[str, str] = {} | |
| def default_lead(model_type: str | None) -> str: | |
| """What a checkpoint's own template writes before a non-thinking answer.""" | |
| return LEADS.get(model_type, DEFAULT_LEAD) | |
| def suffix_text( | |
| tokenizer, q: Question, lead: str = DEFAULT_LEAD, option_style: str = "desc" | |
| ) -> str: | |
| """The question and the assistant header, up to the answer slot.""" | |
| body = question_block(tokenizer, q, option_style) | |
| return f"{body}{IM_END}\n{IM_START}assistant\n{lead}" | |
| def render( | |
| tokenizer, | |
| state: Any, | |
| q: Question, | |
| bos: str = "", | |
| lead: str = DEFAULT_LEAD, | |
| style: str = DEFAULT_STATE_STYLE, | |
| system: str = DEFAULT_SYSTEM, | |
| option_style: str = "desc", | |
| images: str = "", | |
| ) -> str: | |
| return prefix_text(tokenizer, state, bos, style, system, images) + suffix_text( | |
| tokenizer, q, lead, option_style | |
| ) | |