Text Classification
Transformers
Safetensors
English
lfm2_vl
feature-extraction
decision-model
typed-decisions
system-one
shell
guardrail
command-risk
lfm2
liquid
lora
custom_code
Eval Results (legacy)
Instructions to use yannelli/d1-3b-cli-command-risk with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use yannelli/d1-3b-cli-command-risk with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="yannelli/d1-3b-cli-command-risk", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModel processor = AutoProcessor.from_pretrained("yannelli/d1-3b-cli-command-risk", trust_remote_code=True) model = AutoModel.from_pretrained("yannelli/d1-3b-cli-command-risk", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download training/liquid_d1.py from yannelli/d1-3b-cli-command-risk: direct link, hf CLI and curl.
- Browser
- Download file 15.9 kB
-
https://huggingface.co/yannelli/d1-3b-cli-command-risk/resolve/main/training/liquid_d1.py
- Command line
-
hf download hf://yannelli/d1-3b-cli-command-risk/training/liquid_d1.py
-
curl -L -o liquid_d1.py https://huggingface.co/yannelli/d1-3b-cli-command-risk/resolve/main/training/liquid_d1.py
15.9 kB
| """Liquid d1 decision models (d1-3B, d1-omni-600M) on the `decisions` config: zero-shot answers, fine-tuning, answers. | |
| python liquid_d1.py zeroshot --model LiquidAI/d1-3B --split test --out runs/d1-3b/zeroshot.test.jsonl | |
| python liquid_d1.py train --model LiquidAI/d1-3B --ckpt runs/d1-3b/ckpt [--epochs 1] | |
| python liquid_d1.py predict --ckpt runs/d1-3b/ckpt --split test --out runs/d1-3b/finetuned.test.jsonl | |
| d1-3B is a causal LFM2-VL that reads each answer off the logits at the answer slot. It trains with LoRA on its language | |
| model, the state read once and the 13 questions as branches of its `hybrid.py` tree. d1-omni-600M is an encoder with a | |
| decision head over option markers; it trains in full. Vision and audio towers stay frozen. Answers always come from | |
| the model's own `system_one`, so a fine-tuned checkpoint is used exactly as the released one. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import math | |
| import random | |
| import shutil | |
| import sys | |
| import time | |
| from contextlib import nullcontext | |
| from pathlib import Path | |
| import torch | |
| import torch.nn.functional as F | |
| from common import PredictionWriter, gold_index, load_split, log, to_answer | |
| LORA_TARGETS = r".*language_model.*\.(q_proj|k_proj|v_proj|out_proj|in_proj|w1|w2|w3)" | |
| CODE_FILES = ("*.py", "*.json", "*.jinja", "*.txt", "LICENSE") | |
| WEIGHTS = ("model.safetensors", "model.safetensors.index.json") | |
| def device_of(name: str | None) -> torch.device: | |
| return torch.device(name or ("cuda" if torch.cuda.is_available() else "cpu")) | |
| def is_omni(path: str | Path) -> bool: | |
| import json | |
| from huggingface_hub import hf_hub_download | |
| config = Path(path) / "config.json" if Path(path).is_dir() else Path(hf_hub_download(str(path), "config.json")) | |
| return json.loads(config.read_text())["model_type"] == "d1_omni" | |
| def load(path: str | Path, device: torch.device, dtype: torch.dtype | None = None): | |
| from transformers import AutoModel | |
| if dtype is None: # omni was trained in fp32 and its card runs fp16 on GPUs; d1-3B ships in bf16 | |
| dtype = torch.float32 if device.type == "cpu" else torch.float16 if is_omni(path) else torch.bfloat16 | |
| return AutoModel.from_pretrained(str(path), trust_remote_code=True, dtype=dtype).to(device).eval() | |
| def remote(model, name: str): | |
| """A module of the model's own remote code (`prompt`, `hybrid`).""" | |
| return sys.modules[type(model).__module__.rsplit(".", 1)[0] + "." + name] | |
| def sync(device: torch.device) -> None: | |
| if device.type == "cuda": | |
| torch.cuda.synchronize() | |
| elif device.type == "mps": | |
| torch.mps.synchronize() | |
| # --------------------------------------------------------------------------- answers | |
| def answer_case(model, case: dict) -> dict: | |
| """The 13 answers for one case from the model's `system_one`, in `common.options_of` order.""" | |
| response = model.system_one(case["state"], case["questions"])["answers"] | |
| out = {} | |
| for name, q in case["questions"].items(): | |
| a = response[name] | |
| if q["type"] == "noul": | |
| probs = [a["noul"], 1 - a["noul"]] | |
| elif q["type"] == "choice": | |
| probs = [a["probabilities"][k] for k in q["criteria"]] | |
| else: | |
| probs = [a["probabilities"][str(i)] for i in range(len(q["criteria"]))] | |
| out[name] = to_answer(q, probs) | |
| return out | |
| def answer_split(path: str | Path, split: str, out: Path, limit: int | None, device: torch.device) -> None: | |
| cases = load_split(split, limit) | |
| writer = PredictionWriter(out) | |
| todo = writer.pending(cases) | |
| if not todo: | |
| return log(None, done=str(out), cases=len(cases)) | |
| model = load(path, device) | |
| for _ in range(2): # warm-up: kernels, caches, the engine's tokenizer | |
| answer_case(model, todo[0]) | |
| for i, case in enumerate(todo): | |
| sync(device) | |
| tick = time.perf_counter() | |
| answers = answer_case(model, case) | |
| sync(device) | |
| writer.write(case["id"], answers, 1000 * (time.perf_counter() - tick)) | |
| if i % 200 == 0: | |
| log(None, split=split, answered=len(writer.done), of=len(cases)) | |
| writer.close() | |
| log(None, done=str(out), cases=len(cases)) | |
| # --------------------------------------------------------------------------- training: d1-3B | |
| class Tree3B: | |
| """d1-3B training views: the state as the trunk, the 13 questions as branches, each read at its answer slot.""" | |
| def __init__(self, model, questions: dict): | |
| engine, prompt = model.engine, remote(model, "prompt") | |
| self.model, self.tok, self.engine, self.prompt = model, engine.tokenizer, engine, prompt | |
| self.names, self.questions = list(questions), questions | |
| qs = [prompt.as_question(questions[n]) for n in self.names] | |
| self.branches = [self.encode(prompt.suffix_text(self.tok, q, engine.lead, engine.option_style)) for q in qs] | |
| self.groups = [prompt.readout_ids(self.tok, q) for q in qs] | |
| # the fused flash kernels return a log-sum-exp without a gradient; the explicit fp32 path keeps it exact | |
| remote(model, "hybrid")._fused = lambda q: False | |
| def encode(self, text: str) -> list[int]: | |
| return self.tok.encode(text, add_special_tokens=False) | |
| def option_logits(self, case: dict, device: torch.device) -> list[torch.Tensor]: | |
| e = self.engine | |
| trunk = self.encode(self.prompt.prefix_text(self.tok, case["state"], e.bos, e.state_style, e.system)) | |
| packed = torch.tensor([t for b in self.branches for t in b], device=device) | |
| lengths = torch.tensor([len(b) for b in self.branches], device=device) | |
| logits = self.model.answer(torch.tensor([trunk], device=device), packed, lengths).float() | |
| return [torch.stack([logits[j, g].max() for g in groups]) for j, groups in enumerate(self.groups)] | |
| def loss(self, cases: list[dict], device: torch.device) -> torch.Tensor: | |
| total = [] | |
| for case in cases: | |
| for name, scores in zip(self.names, self.option_logits(case, device)): | |
| target = torch.tensor([gold_index(self.questions[name], case["gold"][name])], device=device) | |
| total.append(F.cross_entropy(scores[None], target)) | |
| return torch.stack(total).mean() | |
| # --------------------------------------------------------------------------- training: d1-omni-600M | |
| class Omni: | |
| """d1-omni training views: one sequence per question, scored at its option markers (a noul reads [false, true]).""" | |
| def __init__(self, model, questions: dict): | |
| self.model, self.prompt = model, remote(model, "prompt") | |
| self.names, self.questions = list(questions), questions | |
| self.qs = [self.prompt.as_question(questions[n]) for n in self.names] | |
| def rows(self, case: dict) -> list[tuple[list[int], list[int], int, int]]: | |
| out = [] | |
| for name, q in zip(self.names, self.qs): | |
| ids, markers = self.prompt.encode(self.model.tokenizer, case["state"], q, self.model.config.max_length) | |
| target = gold_index(self.questions[name], case["gold"][name]) | |
| out.append((ids, markers, self.prompt.QTYPES[q.type], 1 - target if q.type == "noul" else target)) | |
| return out | |
| def loss(self, cases: list[dict], device: torch.device) -> torch.Tensor: | |
| m = self.model | |
| rows = [row for case in cases for row in self.rows(case)] | |
| seqs = [m.encoder.embed_tokens(torch.tensor(ids, device=device)) for ids, *_ in rows] | |
| h = torch.nn.utils.rnn.pad_sequence(seqs, batch_first=True) | |
| lengths = torch.tensor([len(ids) for ids, *_ in rows], device=device) | |
| pad = torch.arange(h.shape[1], device=device)[None] < lengths[:, None] | |
| h = m.encoder(h, pad, torch.zeros(len(rows), dtype=torch.long, device=device)) | |
| k = max(len(markers) for _, markers, *_ in rows) | |
| marker_pos = torch.zeros(len(rows), k, dtype=torch.long, device=device) | |
| marker_mask = torch.zeros(len(rows), k, dtype=torch.bool, device=device) | |
| for i, (_, markers, *_) in enumerate(rows): | |
| marker_pos[i, :len(markers)] = torch.tensor(markers, device=device) | |
| marker_mask[i, :len(markers)] = True | |
| qtype = torch.tensor([r[2] for r in rows], device=device) | |
| logits = m.head(h, pad, marker_pos, marker_mask, qtype) | |
| return F.cross_entropy(logits, torch.tensor([r[3] for r in rows], device=device)) | |
| # --------------------------------------------------------------------------- training loop | |
| def trainable(model, omni: bool): | |
| """Omni: everything but the vision and audio towers. d1-3B: LoRA on the language model's linear layers.""" | |
| if omni: | |
| for name, p in model.named_parameters(): | |
| p.requires_grad = not name.startswith(("vision.", "audio.")) | |
| return model | |
| from peft import LoraConfig, get_peft_model | |
| for p in model.parameters(): | |
| p.requires_grad = False | |
| return get_peft_model(model, LoraConfig(r=32, lora_alpha=64, lora_dropout=0.05, target_modules=LORA_TARGETS)) | |
| def trainable_state(model) -> dict: | |
| return {n: p.detach().cpu() for n, p in model.named_parameters() if p.requires_grad} | |
| def save_atomic(obj, path: Path) -> None: | |
| tmp = path.with_suffix(".tmp") | |
| torch.save(obj, tmp) | |
| tmp.replace(path) | |
| def export(model, omni: bool, source: str, best: Path, final: Path) -> None: | |
| """The best weights as a standalone checkpoint that `AutoModel.from_pretrained(final, trust_remote_code=True)` | |
| loads, with the source repo's code and tokenizer files.""" | |
| from huggingface_hub import snapshot_download | |
| model.load_state_dict(torch.load(best, map_location="cpu"), strict=False) | |
| if omni: | |
| model.config.temperatures = {} # the released per-type temperatures were fitted to the released weights | |
| else: | |
| model = model.merge_and_unload() | |
| tmp = final.with_name(final.name + ".tmp") | |
| shutil.rmtree(tmp, ignore_errors=True) | |
| src = Path(snapshot_download(source, allow_patterns=list(CODE_FILES))) | |
| shutil.copytree(src, tmp, ignore=shutil.ignore_patterns("README.md", *WEIGHTS)) | |
| model.save_pretrained(tmp) | |
| shutil.rmtree(final, ignore_errors=True) | |
| tmp.replace(final) | |
| def val_loss(view, cases: list[dict], device: torch.device, batch: int, autocast) -> float: | |
| losses = [] | |
| for i in range(0, len(cases), batch): | |
| with autocast(): | |
| losses.append(float(view.loss(cases[i:i + batch], device)) * len(cases[i:i + batch])) | |
| return sum(losses) / len(cases) | |
| def train(args) -> None: | |
| device, ckpt = device_of(args.device), Path(args.ckpt) | |
| ckpt.mkdir(parents=True, exist_ok=True) | |
| if (ckpt / "final").exists(): | |
| return log(args.log, done=str(ckpt / "final")) | |
| omni = is_omni(args.model) | |
| dtype = torch.float32 if omni or device.type == "cpu" else torch.bfloat16 # omni keeps fp32 master weights | |
| model = load(args.model, device, dtype) | |
| train_cases, val_cases = load_split("train", args.limit), load_split("validation", args.val_cases) | |
| questions = train_cases[0]["questions"] | |
| view = Omni(model, questions) if omni else Tree3B(model, questions) | |
| model = trainable(model, omni) | |
| model.train() | |
| lr = args.lr or (2e-5 if omni else 2e-4) | |
| micro = args.micro or (2 if omni else 1) | |
| accum = max(1, args.batch // micro) | |
| params = [p for p in model.parameters() if p.requires_grad] | |
| optimizer = torch.optim.AdamW(params, lr=lr, weight_decay=0.0) | |
| per_epoch = math.ceil(len(train_cases) / (micro * accum)) | |
| total = min(per_epoch * args.epochs, args.max_steps or 10**9) | |
| warmup = max(1, int(0.05 * total)) | |
| schedule = torch.optim.lr_scheduler.LambdaLR( | |
| optimizer, lambda s: min(1.0, (s + 1) / warmup) * max(0.0, (total - s) / max(1, total - warmup))) | |
| state = {"epoch": 0, "step": 0, "best": float("inf")} | |
| if (ckpt / "last.pt").exists(): | |
| saved = torch.load(ckpt / "last.pt", map_location="cpu") | |
| model.load_state_dict(saved["weights"], strict=False) | |
| optimizer.load_state_dict(saved["optimizer"]) | |
| schedule.load_state_dict(saved["schedule"]) | |
| state = saved["state"] | |
| log(args.log, resumed=state) | |
| autocast = (lambda: torch.autocast("cuda", dtype=torch.bfloat16)) if device.type == "cuda" else nullcontext | |
| def checkpoint() -> None: | |
| save_atomic({"weights": trainable_state(model), "optimizer": optimizer.state_dict(), | |
| "schedule": schedule.state_dict(), "state": state}, ckpt / "last.pt") | |
| started = time.time() | |
| while state["epoch"] < args.epochs and state["step"] < total: | |
| order = list(range(len(train_cases))) | |
| random.Random(args.seed + state["epoch"]).shuffle(order) | |
| done_in_epoch = state["step"] - state["epoch"] * per_epoch | |
| for s in range(done_in_epoch, per_epoch): | |
| if state["step"] >= total: | |
| break | |
| chunk = [train_cases[i] for i in order[s * micro * accum:(s + 1) * micro * accum]] | |
| for i in range(0, len(chunk), micro): | |
| with autocast(): | |
| loss = view.loss(chunk[i:i + micro], device) * len(chunk[i:i + micro]) / len(chunk) | |
| loss.backward() | |
| norm = torch.nn.utils.clip_grad_norm_(params, 1.0) | |
| optimizer.step() | |
| schedule.step() | |
| optimizer.zero_grad(set_to_none=True) | |
| state["step"] += 1 | |
| if state["step"] % 20 == 0 or state["step"] <= 3: | |
| log(args.log, step=state["step"], of=total, loss=round(float(loss) * len(chunk) / micro, 4), | |
| grad_norm=round(float(norm), 3), lr=schedule.get_last_lr()[0], minutes=round((time.time() - started) / 60, 1)) | |
| if state["step"] % args.save_every == 0: | |
| checkpoint() | |
| state["epoch"] += 1 | |
| model.eval() | |
| loss = val_loss(view, val_cases, device, micro, autocast) | |
| model.train() | |
| log(args.log, epoch=state["epoch"], val_loss=round(loss, 4), minutes=round((time.time() - started) / 60, 1)) | |
| if loss < state["best"]: | |
| state["best"] = loss | |
| save_atomic(trainable_state(model), ckpt / "best.pt") | |
| checkpoint() | |
| model.eval() | |
| export(model, omni, args.model, ckpt / "best.pt", ckpt / "final") | |
| log(args.log, done=str(ckpt / "final"), best_val_loss=state["best"], minutes=round((time.time() - started) / 60, 1)) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) | |
| sub = parser.add_subparsers(dest="command", required=True) | |
| for name in ("zeroshot", "train", "predict"): | |
| p = sub.add_parser(name) | |
| p.add_argument("--device") | |
| p.add_argument("--limit", type=int) | |
| if name != "predict": | |
| p.add_argument("--model", default="LiquidAI/d1-3B") | |
| if name != "train": | |
| p.add_argument("--split", default="test", choices=("validation", "test")) | |
| p.add_argument("--out", type=Path, required=True) | |
| if name != "zeroshot": | |
| p.add_argument("--ckpt", required=True) | |
| t = sub.choices["train"] | |
| t.add_argument("--epochs", type=int, default=1) | |
| t.add_argument("--max-steps", type=int) | |
| t.add_argument("--batch", type=int, default=8, help="cases per optimizer step") | |
| t.add_argument("--micro", type=int, help="cases per forward pass (default 2 for omni, 1 for d1-3B)") | |
| t.add_argument("--lr", type=float) | |
| t.add_argument("--val-cases", type=int, default=300) | |
| t.add_argument("--save-every", type=int, default=400) | |
| t.add_argument("--seed", type=int, default=0) | |
| t.add_argument("--log") | |
| args = parser.parse_args() | |
| if args.command == "train": | |
| return train(args) | |
| path = args.model if args.command == "zeroshot" else Path(args.ckpt) / "final" | |
| answer_split(path, args.split, args.out, args.limit, device_of(args.device)) | |
| if __name__ == "__main__": | |
| main() | |