Image-Text-to-Text
Transformers
Safetensors
lfm2_vl
liquid
lfm2.5
edge
decision
classification
calibration
system-one
multimodal
decision-model
conversational
custom_code
Eval Results
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 hybrid.py from LiquidAI/d1-3B: direct link, hf CLI and curl.
- Browser
- Download file 14.4 kB
-
https://huggingface.co/LiquidAI/d1-3B/resolve/main/hybrid.py
- Command line
-
hf download hf://LiquidAI/d1-3B/hybrid.py
-
curl -L -o hybrid.py https://huggingface.co/LiquidAI/d1-3B/resolve/main/hybrid.py
14.4 kB
| """The pieces every LFM hybrid is made of, and the one shape that makes many | |
| questions cost about one forward. | |
| A model runs a batch of right-padded chains `(B, T)`, or a tree: a trunk of P | |
| tokens (the state) and N branches (the questions) that each continue it, packed | |
| back to back on one axis with no padding, `[trunk | branch 0 | branch 1 | ...]`. | |
| Everything that works token by token (norms, projections, the MLP) runs once | |
| over the packed axis. A convolution continues each branch from the trunk's last | |
| inputs. Attention runs by parts: the trunk causally, every branch token over the | |
| trunk in one flash call that reads the trunk's keys once for all questions | |
| (under sliding attention, one fp32 product over the trunk's last window-1 keys), | |
| each branch over its own tokens in one varlen flash call, the two merged by | |
| their log-sum-exp. Nothing is cached, copied or padded. | |
| """ | |
| from __future__ import annotations | |
| from typing import Callable | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from transformers.modeling_layers import GradientCheckpointingLayer | |
| from transformers.modeling_outputs import BaseModelOutputWithPast | |
| Tensor = torch.Tensor | |
| class Tree: | |
| """A trunk of `p` tokens and branches of `lengths` tokens, packed on one axis.""" | |
| def __init__(self, p: int, lengths: Tensor): | |
| self.p, self.n = p, len(lengths) | |
| self.cu = F.pad(lengths.cumsum(0), (1, 0)).int() # where each branch starts, past the trunk | |
| self.longest = int(lengths.max()) | |
| self.branch = torch.repeat_interleave(torch.arange(self.n, device=lengths.device), lengths) | |
| self.offset = torch.arange(len(self.branch), device=lengths.device) - self.cu[self.branch] | |
| # a branch starts where the trunk ends | |
| self.positions = torch.cat((torch.arange(p, device=lengths.device), p + self.offset)) | |
| self.leaves = p + self.cu[1:].long() - 1 # each branch's last token | |
| self._conv = {} | |
| def conv_order(self, keep: int) -> tuple[Tensor, Tensor]: | |
| """Gather and keep indices that run one causal conv over all branches, | |
| each led by the trunk's last `keep` inputs. Indices are into the whole | |
| input left-padded by `keep` zeros.""" | |
| if keep not in self._conv: | |
| p, total, dev = self.p, len(self.branch), self.branch.device | |
| slot = torch.arange(total, device=dev) + keep * (self.branch + 1) | |
| order = torch.empty(total + self.n * keep, dtype=torch.long, device=dev) | |
| order[slot] = keep + p + torch.arange(total, device=dev) | |
| lead = (self.cu[:-1] + keep * torch.arange(self.n, device=dev))[:, None] | |
| order[lead + torch.arange(keep, device=dev)] = p + torch.arange(keep, device=dev) | |
| self._conv[keep] = order, slot - keep | |
| return self._conv[keep] | |
| # --------------------------------------------------------------------------- # | |
| # norms and rotary | |
| # --------------------------------------------------------------------------- # | |
| class RMSNorm(nn.Module): | |
| def __init__(self, dim: int, eps: float): | |
| super().__init__() | |
| self.weight = nn.Parameter(torch.ones(dim)) | |
| self.eps = eps | |
| def forward(self, x: Tensor) -> Tensor: | |
| x32 = x.float() | |
| return (x32 * torch.rsqrt(x32.pow(2).mean(-1, keepdim=True) + self.eps)).type_as(x) * self.weight | |
| def cos_sin(angles: Tensor) -> tuple[Tensor, Tensor]: | |
| angles = torch.cat((angles, angles), dim=-1) | |
| return angles.cos(), angles.sin() | |
| def rotate(x: Tensor, cos: Tensor, sin: Tensor) -> Tensor: | |
| """Half-split RoPE in fp32; `cos`/`sin` broadcast over the head axis.""" | |
| x32 = x.float() | |
| a, b = x32.chunk(2, dim=-1) | |
| return (x32 * cos + torch.cat((-b, a), dim=-1) * sin).type_as(x) | |
| def rope(x: Tensor, pos: Tensor, theta: float, rotated: int) -> Tensor: | |
| """RoPE over `(B, T, H, D)` at positions `pos` `(T,)`, pairs past `rotated` left as they are.""" | |
| dim = x.shape[-1] | |
| inv = 1.0 / theta ** (torch.arange(0, dim, 2, device=x.device).float() / dim) | |
| inv[rotated:] = 0.0 | |
| cos, sin = cos_sin(pos.float()[:, None] * inv) | |
| return rotate(x, cos[:, None], sin[:, None]) | |
| # --------------------------------------------------------------------------- # | |
| # causal convolution | |
| # --------------------------------------------------------------------------- # | |
| def causal_conv(x: Tensor, weight: Tensor, tree: Tree | None) -> Tensor: | |
| """Depthwise causal conv over `(B, C, T)`; a tree's branches continue its trunk.""" | |
| keep = weight.shape[-1] - 1 | |
| padded = F.pad(x, (keep, 0)) | |
| if tree is None: | |
| return F.conv1d(padded, weight, groups=x.shape[1]) | |
| order, kept = tree.conv_order(keep) | |
| branches = F.conv1d(padded[..., order], weight, groups=x.shape[1])[..., kept] | |
| if tree.p == 0: | |
| return branches | |
| return torch.cat((F.conv1d(padded[..., : keep + tree.p], weight, groups=x.shape[1]), branches), dim=-1) | |
| # --------------------------------------------------------------------------- # | |
| # attention, `(B, H, T, D)` | |
| # --------------------------------------------------------------------------- # | |
| # queries per explicit product off the flash kernels, which bounds its fp32 scores | |
| CHUNK = 1024 | |
| def _fused(q: Tensor) -> bool: | |
| """Whether the flash kernels take these queries: a GPU in half precision, ROCm or CUDA from Ampere on.""" | |
| if not q.is_cuda or q.dtype not in (torch.float16, torch.bfloat16): | |
| return False | |
| return bool(torch.version.hip) or torch.cuda.get_device_capability(q.device)[0] >= 8 | |
| def _explicit(q: Tensor, k: Tensor, v: Tensor, allowed: Tensor | None, scale: float) -> tuple[Tensor, Tensor]: | |
| """Attention with its log-sum-exp, in fp32, for small key sets. GQA by broadcast.""" | |
| groups = q.shape[1] // k.shape[1] | |
| k, v = k.repeat_interleave(groups, dim=1).float(), v.repeat_interleave(groups, dim=1).float() | |
| scores = (q.float() @ k.transpose(-1, -2)) * scale | |
| if allowed is not None: | |
| scores = scores.masked_fill(~allowed, float("-inf")) | |
| return torch.softmax(scores, dim=-1).nan_to_num() @ v, torch.logsumexp(scores, dim=-1) | |
| def _cat(parts: list[tuple[Tensor, Tensor]]) -> tuple[Tensor, Tensor]: | |
| return torch.cat([o for o, _ in parts], dim=2), torch.cat([lse for _, lse in parts], dim=2) | |
| def _over_trunk(q: Tensor, k: Tensor, v: Tensor, scale: float) -> tuple[Tensor, Tensor]: | |
| """Every branch token over all of the trunk, with its log-sum-exp. Each KV | |
| head's query group is folded into the query axis, so the trunk's keys are | |
| read once and never repeated.""" | |
| batch, heads, length, dim = q.shape | |
| kv = k.shape[1] | |
| if not _fused(q): | |
| return _cat([_explicit(part, k, v, None, scale) for part in q.split(CHUNK, dim=2)]) | |
| folded = q.reshape(batch, kv, heads // kv * length, dim) | |
| out, lse = torch.ops.aten._scaled_dot_product_flash_attention(folded, k, v, 0.0, False, False, scale=scale)[:2] | |
| return out.reshape(batch, heads, length, dim).float(), lse.reshape(batch, heads, length) | |
| def _own(q: Tensor, k: Tensor, v: Tensor, tree: Tree, window: int | None, scale: float) -> tuple[Tensor, Tensor]: | |
| """Each branch over its own tokens, causally (within `window`), with its log-sum-exp.""" | |
| if not _fused(q) and len(tree.branch) <= CHUNK: # small enough for one masked product | |
| same, off = tree.branch[:, None] == tree.branch[None], tree.offset | |
| allowed = same & (off[None] <= off[:, None]) | |
| if window is not None: | |
| allowed &= off[None] > off[:, None] - window | |
| return _explicit(q, k, v, allowed, scale) | |
| if not _fused(q): # branch by branch: memory grows with the longest question, not with the batch | |
| parts = [] | |
| for a, b in zip(tree.cu[:-1].tolist(), tree.cu[1:].tolist()): | |
| off = torch.arange(b - a, device=q.device) | |
| allowed = off[None] <= off[:, None] | |
| if window is not None: | |
| allowed &= off[None] > off[:, None] - window | |
| parts.append(_explicit(q[:, :, a:b], k[:, :, a:b], v[:, :, a:b], allowed, scale)) | |
| return _cat(parts) | |
| local = {} if window is None else {"window_size_left": window - 1, "window_size_right": 0} | |
| out, lse = torch.ops.aten._flash_attention_forward( | |
| *(t[0].transpose(0, 1) for t in (q, k, v)), tree.cu, tree.cu, tree.longest, tree.longest, | |
| 0.0, True, False, scale=scale, **local)[:2] | |
| return out.transpose(0, 1)[None].float(), lse[None] | |
| def _merge(a: tuple[Tensor, Tensor], b: tuple[Tensor, Tensor]) -> Tensor: | |
| (oa, la), (ob, lb) = a, b | |
| m = torch.maximum(la, lb) | |
| wa, wb = (la - m).exp(), (lb - m).exp() | |
| return (oa * wa[..., None] + ob * wb[..., None]) / (wa + wb)[..., None] | |
| def _windowed(q: Tensor, k: Tensor, v: Tensor, window: int, scale: float) -> Tensor: | |
| """Causal attention within `window`, as local blocks: each block of `window` | |
| queries sees its own block and the one before it.""" | |
| batch, heads, length, dim = q.shape | |
| blocks = -(-length // window) | |
| def split(t: Tensor) -> Tensor: # (B, H, T, D) -> (B, blocks, H, window, D) | |
| t = F.pad(t, (0, 0, 0, blocks * window - length)) | |
| return t.reshape(batch, t.shape[1], blocks, window, dim).transpose(1, 2) | |
| def with_previous(t: Tensor) -> Tensor: | |
| return torch.cat((F.pad(t, (0, 0, 0, 0, 0, 0, 1, 0))[:, :-1], t), dim=3) | |
| qb, kb, vb = split(q), with_previous(split(k)), with_previous(split(v)) | |
| qpos = torch.arange(window, device=q.device)[:, None] + window | |
| kpos = torch.arange(2 * window, device=q.device)[None, :] | |
| allowed = (kpos <= qpos) & (kpos > qpos - window) | |
| first = allowed & (kpos >= window) # block 0 has no block before it | |
| mask = torch.stack([first] + [allowed] * (blocks - 1))[:, None].repeat(batch, 1, 1, 1) | |
| out = F.scaled_dot_product_attention(qb.flatten(0, 1), kb.flatten(0, 1), vb.flatten(0, 1), | |
| attn_mask=mask, scale=scale, enable_gqa=True) | |
| out = out.reshape(batch, blocks, heads, window, dim).transpose(1, 2) | |
| return out.reshape(batch, heads, blocks * window, dim)[:, :, :length] | |
| def causal_attention(q: Tensor, k: Tensor, v: Tensor, window: int | None, scale: float) -> Tensor: | |
| """Right-padded chains: causal, within `window` if set.""" | |
| if window is None or q.shape[2] <= window: | |
| return F.scaled_dot_product_attention(q, k, v, is_causal=True, scale=scale, enable_gqa=True) | |
| return _windowed(q, k, v, window, scale) | |
| def attention(q: Tensor, k: Tensor, v: Tensor, tree: Tree | None, window: int | None, scale: float) -> Tensor: | |
| """Chains, or a tree: the trunk causally, each branch over the trunk and over | |
| its own tokens.""" | |
| if tree is None: | |
| return causal_attention(q, k, v, window, scale) | |
| p = tree.p | |
| qb = q[:, :, p:] | |
| mine = _own(qb, k[:, :, p:], v[:, :, p:], tree, window, scale) | |
| if p == 0: | |
| return mine[0].to(q.dtype) | |
| trunk = causal_attention(q[:, :, :p], k[:, :, :p], v[:, :, :p], window, scale) | |
| if window is None: | |
| seen = _over_trunk(qb, k[:, :, :p], v[:, :, :p], scale) | |
| else: # only the trunk's last window-1 keys are in reach of a branch | |
| tail = min(p, window - 1) | |
| allowed = torch.arange(p - tail, p, device=q.device)[None] > tree.positions[p:, None] - window | |
| seen = _explicit(qb, k[:, :, p - tail:p], v[:, :, p - tail:p], allowed, scale) | |
| return torch.cat((trunk, _merge(seen, mine).to(q.dtype)), dim=2) | |
| # --------------------------------------------------------------------------- # | |
| # the stack | |
| # --------------------------------------------------------------------------- # | |
| class DecoderLayer(GradientCheckpointingLayer): | |
| """Norm, operator (`conv` or `self_attn`), residual; norm, MLP, residual. | |
| `sandwich` adds a norm on each branch's output.""" | |
| def __init__(self, kind: str, operator: nn.Module, mlp: nn.Module, dim: int, eps: float, sandwich: bool): | |
| super().__init__() | |
| self.operator_name = "conv" if kind == "conv" else "self_attn" | |
| setattr(self, self.operator_name, operator) | |
| self.feed_forward = mlp | |
| self.operator_norm = RMSNorm(dim, eps) | |
| self.ffn_norm = RMSNorm(dim, eps) | |
| self.post_operator_norm = RMSNorm(dim, eps) if sandwich else nn.Identity() | |
| self.post_ffn_norm = RMSNorm(dim, eps) if sandwich else nn.Identity() | |
| def forward(self, x: Tensor, pos: Tensor, tree: Tree | None = None) -> Tensor: | |
| h = x + self.post_operator_norm(getattr(self, self.operator_name)(self.operator_norm(x), pos, tree)) | |
| return h + self.post_ffn_norm(self.feed_forward(self.ffn_norm(h))) | |
| class LanguageModel(nn.Module): | |
| """Embedding, the hybrid stack and the final norm, called like HF's text models. | |
| Rows are right-padded, so the causal operators never let a pad reach a real | |
| token and there is no attention mask to apply. | |
| """ | |
| def __init__(self, cfg, operator: Callable[[str], nn.Module], mlp: Callable[[], nn.Module], | |
| eps: float, sandwich: bool = False): | |
| super().__init__() | |
| self.embed_tokens = nn.Embedding(cfg.vocab_size, cfg.hidden_size) | |
| self.layers = nn.ModuleList( | |
| DecoderLayer(kind, operator(kind), mlp(), cfg.hidden_size, eps, sandwich) for kind in cfg.layer_types) | |
| self.embedding_norm = RMSNorm(cfg.hidden_size, eps) # the final norm, despite its name | |
| def get_input_embeddings(self) -> nn.Embedding: | |
| return self.embed_tokens | |
| def run(self, h: Tensor, tree: Tree | None = None) -> Tensor: | |
| pos = tree.positions if tree is not None else torch.arange(h.shape[1], device=h.device) | |
| for layer in self.layers: | |
| h = layer(h, pos, tree) | |
| return self.embedding_norm(h) | |
| def forward(self, input_ids=None, inputs_embeds=None, **_) -> BaseModelOutputWithPast: | |
| h = self.embed_tokens(input_ids) if inputs_embeds is None else inputs_embeds | |
| return BaseModelOutputWithPast(last_hidden_state=self.run(h)) | |
| def answer(self, trunk: Tensor, questions: Tensor, lengths: Tensor) -> Tensor: | |
| """Final hidden states at the last token of every question, in one pass. | |
| `trunk` `(1, P, d)` embeddings; `questions` the ids of all questions back | |
| to back, `(sum(lengths),)`.""" | |
| tree = Tree(trunk.shape[1], lengths) | |
| h = self.run(torch.cat((trunk, self.embed_tokens(questions)[None]), dim=1), tree) | |
| return h[0, tree.leaves] | |