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
Use d1-3b's code: D1Model in modeling_d1.py
Browse filesThe code of LiquidAI/d1-3b, so the RC loads as `D1Model` instead of the older `LfmJevModel` name.
- `modeling_lfm_jev.py` removed, `modeling_d1.py` added; `config.json`'s `auto_map` points to it
- `prompt.py` and `runner.py`: training-only helpers removed (`row_to_question`, `cardinality`, `option_labels`, `gold_code`, `SYSTEM`, `_logz`); every public call still runs under `torch.inference_mode()`
- Weights, tokenizer, processor, card, LICENSE and assets unchanged
Checked with `trust_remote_code` on the same weights: three questions on a text, one question, a JSON state, an image, an image with text, and a batch of three give the same 44 probabilities as the current code (max difference 0.0).
- config.json +1 -1
- lfm2_vl.py +4 -4
- modeling_lfm_jev.py → modeling_d1.py +1 -1
- prompt.py +9 -48
- runner.py +7 -13
|
@@ -3,7 +3,7 @@
|
|
| 3 |
"Lfm2VlForConditionalGeneration"
|
| 4 |
],
|
| 5 |
"auto_map": {
|
| 6 |
-
"AutoModel": "
|
| 7 |
},
|
| 8 |
"bos_token_id": 124894,
|
| 9 |
"do_image_splitting": true,
|
|
|
|
| 3 |
"Lfm2VlForConditionalGeneration"
|
| 4 |
],
|
| 5 |
"auto_map": {
|
| 6 |
+
"AutoModel": "modeling_d1.D1Model"
|
| 7 |
},
|
| 8 |
"bos_token_id": 124894,
|
| 9 |
"do_image_splitting": true,
|
|
@@ -1,9 +1,9 @@
|
|
| 1 |
"""LFM2-VL and LFM2 on the hybrid stack.
|
| 2 |
|
| 3 |
HF's classes stay the frame (the SigLIP2 tower, the projector, the image
|
| 4 |
-
features, loading and saving); only the language model is
|
| 5 |
-
names, so checkpoints load and save unchanged
|
| 6 |
-
|
| 7 |
implementation.
|
| 8 |
"""
|
| 9 |
|
|
@@ -46,7 +46,7 @@ class Attention(nn.Module):
|
|
| 46 |
|
| 47 |
|
| 48 |
class ShortConv(nn.Module):
|
| 49 |
-
"""`out(C * conv(B * x))`; HF's `nn.Conv1d` holds the taps,
|
| 50 |
|
| 51 |
def __init__(self, cfg):
|
| 52 |
super().__init__()
|
|
|
|
| 1 |
"""LFM2-VL and LFM2 on the hybrid stack.
|
| 2 |
|
| 3 |
HF's classes stay the frame (the SigLIP2 tower, the projector, the image
|
| 4 |
+
features, loading and saving); only the language model is replaced, with HF's
|
| 5 |
+
module names, so checkpoints load and save unchanged. A state with any number
|
| 6 |
+
of questions runs as one `hybrid.Tree`, with the same outputs as HF's own
|
| 7 |
implementation.
|
| 8 |
"""
|
| 9 |
|
|
|
|
| 46 |
|
| 47 |
|
| 48 |
class ShortConv(nn.Module):
|
| 49 |
+
"""`out(C * conv(B * x))`; HF's `nn.Conv1d` holds the taps, `causal_conv` runs them."""
|
| 50 |
|
| 51 |
def __init__(self, cfg):
|
| 52 |
super().__init__()
|
|
@@ -14,7 +14,7 @@ from .lfm2_vl import Lfm2VlForConditionalGeneration
|
|
| 14 |
from .runner import SystemOne
|
| 15 |
|
| 16 |
|
| 17 |
-
class
|
| 18 |
@cached_property
|
| 19 |
def engine(self) -> SystemOne:
|
| 20 |
from transformers import AutoTokenizer
|
|
|
|
| 14 |
from .runner import SystemOne
|
| 15 |
|
| 16 |
|
| 17 |
+
class D1Model(Lfm2VlForConditionalGeneration):
|
| 18 |
@cached_property
|
| 19 |
def engine(self) -> SystemOne:
|
| 20 |
from transformers import AutoTokenizer
|
|
@@ -20,7 +20,6 @@ SYSTEMS: dict[str, str | None] = {
|
|
| 20 |
),
|
| 21 |
}
|
| 22 |
DEFAULT_SYSTEM = "none"
|
| 23 |
-
SYSTEM = SYSTEMS[DEFAULT_SYSTEM]
|
| 24 |
|
| 25 |
IM_START = "<|im_start|>"
|
| 26 |
IM_END = "<|im_end|>"
|
|
@@ -55,20 +54,6 @@ class Score:
|
|
| 55 |
Question = Choice | Noul | Score
|
| 56 |
|
| 57 |
|
| 58 |
-
def row_to_question(row: dict) -> Question:
|
| 59 |
-
"""A row's question (`kind`, `instructions`, `criteria`), as the model is asked it."""
|
| 60 |
-
kind = row["kind"]
|
| 61 |
-
if kind == "noul":
|
| 62 |
-
return Noul(instructions=row["instructions"], criteria=row.get("criteria"))
|
| 63 |
-
if kind == "score":
|
| 64 |
-
return Score(instructions=row["instructions"], criteria=list(row["criteria"]))
|
| 65 |
-
return Choice(instructions=row["instructions"], criteria=row["criteria"])
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def cardinality(q: Question) -> int:
|
| 69 |
-
return 2 if isinstance(q, Noul) else len(q.criteria)
|
| 70 |
-
|
| 71 |
-
|
| 72 |
# --------------------------------------------------------------------------- #
|
| 73 |
# verbalizer
|
| 74 |
# --------------------------------------------------------------------------- #
|
|
@@ -124,12 +109,9 @@ _TOKENIZERS: dict[int, object] = {}
|
|
| 124 |
def aliases(tokenizer, labels: Sequence[str]) -> list[tuple[str, int]]:
|
| 125 |
"""Assign every label a distinct single-token code: [(code, token_id)].
|
| 126 |
|
| 127 |
-
Memoised on the codes rather than on the labels
|
| 128 |
-
unless the labels are already letters, so
|
| 129 |
-
|
| 130 |
-
codes once per permuted row. Keying on the label tuple instead misses on
|
| 131 |
-
every such row. Measured against a contended node it was not the cost it
|
| 132 |
-
first looked like, but the entry is the codes and the key should say so.
|
| 133 |
"""
|
| 134 |
_TOKENIZERS.setdefault(id(tokenizer), tokenizer)
|
| 135 |
codes = tuple(option_codes(labels))
|
|
@@ -209,36 +191,16 @@ def readout(tokenizer, q: Question, logz, calibration=None) -> list[float]:
|
|
| 209 |
return [e / sum(exps) for e in exps]
|
| 210 |
|
| 211 |
|
| 212 |
-
def option_labels(q: Question) -> list[str]:
|
| 213 |
-
if isinstance(q, Noul):
|
| 214 |
-
return ["yes", "no"]
|
| 215 |
-
if isinstance(q, Score):
|
| 216 |
-
return [str(i) for i in range(len(q.criteria))]
|
| 217 |
-
return list(q.criteria.keys())
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
def gold_code(tokenizer, q: Question, gold: str | int) -> str:
|
| 221 |
-
"""The exact string the model must produce in the answer slot."""
|
| 222 |
-
if isinstance(q, Noul):
|
| 223 |
-
if isinstance(gold, str):
|
| 224 |
-
return "yes" if gold.strip().lower() in {"yes", "true", "1"} else "no"
|
| 225 |
-
return "yes" if int(gold) == 1 else "no"
|
| 226 |
-
if isinstance(q, Score):
|
| 227 |
-
return str(int(gold))
|
| 228 |
-
labels = list(q.criteria.keys())
|
| 229 |
-
idx = labels.index(gold) if gold in labels else int(gold)
|
| 230 |
-
return aliases(tokenizer, labels)[idx][0]
|
| 231 |
-
|
| 232 |
-
|
| 233 |
# --------------------------------------------------------------------------- #
|
| 234 |
# state and question rendering
|
| 235 |
# --------------------------------------------------------------------------- #
|
| 236 |
|
| 237 |
|
|
|
|
|
|
|
| 238 |
# How a state is rendered: `json_only`, the default, writes every state as the object it is; `json` keeps
|
| 239 |
# three shortcuts (`Message:`, `Passage:` / `Asked:`, a lone question's text); `sections` writes nested
|
| 240 |
# states as labelled blocks.
|
| 241 |
-
DEFAULT_MODEL = "LiquidAI/LFM2.5-VL-3B"
|
| 242 |
DEFAULT_STATE_STYLE = "json_only"
|
| 243 |
|
| 244 |
|
|
@@ -249,9 +211,8 @@ def _is_scalar(v: Any) -> bool:
|
|
| 249 |
def _sections(obj: Any, path: str, out: list[str]) -> None: # noqa: C901
|
| 250 |
"""Flatten a nested state into labelled blocks, keeping real newlines.
|
| 251 |
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
unreadable string of `\n`. The model reads a case file, not a payload.
|
| 255 |
"""
|
| 256 |
head = f"[{path}]\n" if path else ""
|
| 257 |
if isinstance(obj, dict):
|
|
@@ -358,8 +319,8 @@ def prefix_text(
|
|
| 358 |
return f"{bos}{turn}{IM_START}user\n{images}{body}"
|
| 359 |
|
| 360 |
|
| 361 |
-
# What sits between the assistant header and the answer slot: nothing on LFM2-VL, whose
|
| 362 |
-
# no reasoning block
|
| 363 |
DEFAULT_LEAD = ""
|
| 364 |
LEADS: dict[str, str] = {}
|
| 365 |
|
|
|
|
| 20 |
),
|
| 21 |
}
|
| 22 |
DEFAULT_SYSTEM = "none"
|
|
|
|
| 23 |
|
| 24 |
IM_START = "<|im_start|>"
|
| 25 |
IM_END = "<|im_end|>"
|
|
|
|
| 54 |
Question = Choice | Noul | Score
|
| 55 |
|
| 56 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
# --------------------------------------------------------------------------- #
|
| 58 |
# verbalizer
|
| 59 |
# --------------------------------------------------------------------------- #
|
|
|
|
| 109 |
def aliases(tokenizer, labels: Sequence[str]) -> list[tuple[str, int]]:
|
| 110 |
"""Assign every label a distinct single-token code: [(code, token_id)].
|
| 111 |
|
| 112 |
+
Memoised on the codes rather than on the labels: the codes are positional
|
| 113 |
+
unless the labels are already letters, so every option list of one length
|
| 114 |
+
shares an entry.
|
|
|
|
|
|
|
|
|
|
| 115 |
"""
|
| 116 |
_TOKENIZERS.setdefault(id(tokenizer), tokenizer)
|
| 117 |
codes = tuple(option_codes(labels))
|
|
|
|
| 191 |
return [e / sum(exps) for e in exps]
|
| 192 |
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
# --------------------------------------------------------------------------- #
|
| 195 |
# state and question rendering
|
| 196 |
# --------------------------------------------------------------------------- #
|
| 197 |
|
| 198 |
|
| 199 |
+
DEFAULT_MODEL = "LiquidAI/LFM2.5-VL-3B"
|
| 200 |
+
|
| 201 |
# How a state is rendered: `json_only`, the default, writes every state as the object it is; `json` keeps
|
| 202 |
# three shortcuts (`Message:`, `Passage:` / `Asked:`, a lone question's text); `sections` writes nested
|
| 203 |
# states as labelled blocks.
|
|
|
|
| 204 |
DEFAULT_STATE_STYLE = "json_only"
|
| 205 |
|
| 206 |
|
|
|
|
| 211 |
def _sections(obj: Any, path: str, out: list[str]) -> None: # noqa: C901
|
| 212 |
"""Flatten a nested state into labelled blocks, keeping real newlines.
|
| 213 |
|
| 214 |
+
`json.dumps` escapes every newline inside a log line or a record, so a
|
| 215 |
+
multi-line record would arrive as one string of `\n`; this keeps it readable.
|
|
|
|
| 216 |
"""
|
| 217 |
head = f"[{path}]\n" if path else ""
|
| 218 |
if isinstance(obj, dict):
|
|
|
|
| 319 |
return f"{bos}{turn}{IM_START}user\n{images}{body}"
|
| 320 |
|
| 321 |
|
| 322 |
+
# What sits between the assistant header and the answer slot, per model type: nothing on LFM2-VL, whose
|
| 323 |
+
# template opens no reasoning block.
|
| 324 |
DEFAULT_LEAD = ""
|
| 325 |
LEADS: dict[str, str] = {}
|
| 326 |
|
|
@@ -39,7 +39,7 @@ MODELS = {
|
|
| 39 |
|
| 40 |
|
| 41 |
def load_backbone(model_id: str = DEFAULT_MODEL, dtype=torch.bfloat16):
|
| 42 |
-
"""The checkpoint and its tokenizer
|
| 43 |
from transformers import AutoModelForImageTextToText, AutoTokenizer, PretrainedConfig
|
| 44 |
|
| 45 |
tok = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
|
@@ -82,8 +82,8 @@ class SystemOne(SystemOneApi):
|
|
| 82 |
model=None,
|
| 83 |
tokenizer=None,
|
| 84 |
):
|
| 85 |
-
"""`model` and `tokenizer`, when given, are a backbone already loaded (
|
| 86 |
-
|
| 87 |
if model is None:
|
| 88 |
model, tokenizer = load_backbone(model_id)
|
| 89 |
else:
|
|
@@ -103,8 +103,7 @@ class SystemOne(SystemOneApi):
|
|
| 103 |
self.option_style = option_style
|
| 104 |
self.token_budget = token_budget
|
| 105 |
self.processor = None
|
| 106 |
-
# CUDA graphs for single questions
|
| 107 |
-
# decision on an RTX 4090. The first two prompt lengths compile; later ones reuse the dynamic graph.
|
| 108 |
if compile and torch.version.hip:
|
| 109 |
raise ValueError("compile=True needs CUDA: on ROCm the CUDA graphs fault after a few dozen calls")
|
| 110 |
self._one_pass = (torch.compile(self.model.forward, mode="reduce-overhead")
|
|
@@ -120,19 +119,14 @@ class SystemOne(SystemOneApi):
|
|
| 120 |
|
| 121 |
# --------------------------------------------------------------- forward
|
| 122 |
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
"""Log-softmax at the answer slot, one row per text, in one pass; the slot needs only the texts, and
|
| 126 |
-
`questions` is accepted for the scoring interface.
|
| 127 |
|
| 128 |
The rows are one tree (`hybrid.py`): their common start is its trunk and
|
| 129 |
is read once; the rest of each row is a branch, packed with no padding.
|
| 130 |
Mathematically each row alone; in bf16 the kernels differ by batch shape.
|
| 131 |
"""
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
def _logz_ids(self, rows: list[list[int]]) -> list[torch.Tensor]:
|
| 135 |
-
if len(rows) == 1: # nothing to share: a plain chain, 6-12% faster than a tree of one
|
| 136 |
row = self._one_pass(input_ids=torch.tensor(rows, device=self.device), logits_to_keep=1).logits[0, -1]
|
| 137 |
return [row.float() - torch.logsumexp(row.float(), dim=-1)]
|
| 138 |
shared = 0 # every row keeps at least its last token
|
|
|
|
| 39 |
|
| 40 |
|
| 41 |
def load_backbone(model_id: str = DEFAULT_MODEL, dtype=torch.bfloat16):
|
| 42 |
+
"""The checkpoint and its tokenizer, with SDPA attention."""
|
| 43 |
from transformers import AutoModelForImageTextToText, AutoTokenizer, PretrainedConfig
|
| 44 |
|
| 45 |
tok = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
|
|
|
| 82 |
model=None,
|
| 83 |
tokenizer=None,
|
| 84 |
):
|
| 85 |
+
"""`model` and `tokenizer`, when given, are a backbone already loaded (`D1Model` passes itself);
|
| 86 |
+
it stays on its device unless `device` says otherwise."""
|
| 87 |
if model is None:
|
| 88 |
model, tokenizer = load_backbone(model_id)
|
| 89 |
else:
|
|
|
|
| 103 |
self.option_style = option_style
|
| 104 |
self.token_budget = token_budget
|
| 105 |
self.processor = None
|
| 106 |
+
# CUDA graphs for single questions on NVIDIA, where eager time is mostly kernel launches.
|
|
|
|
| 107 |
if compile and torch.version.hip:
|
| 108 |
raise ValueError("compile=True needs CUDA: on ROCm the CUDA graphs fault after a few dozen calls")
|
| 109 |
self._one_pass = (torch.compile(self.model.forward, mode="reduce-overhead")
|
|
|
|
| 119 |
|
| 120 |
# --------------------------------------------------------------- forward
|
| 121 |
|
| 122 |
+
def _logz_ids(self, rows: list[list[int]]) -> list[torch.Tensor]:
|
| 123 |
+
"""Log-softmax at the answer slot, one row per token list, in one pass.
|
|
|
|
|
|
|
| 124 |
|
| 125 |
The rows are one tree (`hybrid.py`): their common start is its trunk and
|
| 126 |
is read once; the rest of each row is a branch, packed with no padding.
|
| 127 |
Mathematically each row alone; in bf16 the kernels differ by batch shape.
|
| 128 |
"""
|
| 129 |
+
if len(rows) == 1: # nothing to share: a plain chain is faster than a tree of one
|
|
|
|
|
|
|
|
|
|
| 130 |
row = self._one_pass(input_ids=torch.tensor(rows, device=self.device), logits_to_keep=1).logits[0, -1]
|
| 131 |
return [row.float() - torch.logsumexp(row.float(), dim=-1)]
|
| 132 |
shared = 0 # every row keeps at least its last token
|