Aurelien-Lac commited on
Commit
bb4d77c
·
verified ·
1 Parent(s): befca42

Use d1-3b's code: D1Model in modeling_d1.py

Browse files

The 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).

Files changed (5) hide show
  1. config.json +1 -1
  2. lfm2_vl.py +4 -4
  3. modeling_lfm_jev.py → modeling_d1.py +1 -1
  4. prompt.py +9 -48
  5. runner.py +7 -13
config.json CHANGED
@@ -3,7 +3,7 @@
3
  "Lfm2VlForConditionalGeneration"
4
  ],
5
  "auto_map": {
6
- "AutoModel": "modeling_lfm_jev.LfmJevModel"
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,
lfm2_vl.py CHANGED
@@ -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 ours, with HF's module
5
- names, so checkpoints load and save unchanged, and a state
6
- with any number of questions runs as one `hybrid.Tree`, which matches HF's own
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, our causal conv runs them."""
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__()
modeling_lfm_jev.py → modeling_d1.py RENAMED
@@ -14,7 +14,7 @@ from .lfm2_vl import Lfm2VlForConditionalGeneration
14
  from .runner import SystemOne
15
 
16
 
17
- class LfmJevModel(Lfm2VlForConditionalGeneration):
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
prompt.py CHANGED
@@ -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. The codes are positional
128
- unless the labels are already letters, so two orderings of one option list
129
- share an entry, and a 151-option inventory question does not re-derive its
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
- A caller's state is JSON, but `json.dumps` escapes every newline inside a
253
- log line or a record, so a 40-line CrowdStrike host record arrives as one
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 template opens
362
- # no reasoning block, so the slot is already clean. A family module can add its own (`LEADS`).
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
 
runner.py CHANGED
@@ -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. SigLIP2 has no FA2 kernel here, so SDPA."""
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 (the Hub's remote-code class
86
- passes itself); it stays on its device unless `device` says otherwise."""
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, on NVIDIA: where eager time is kernel launches, 16 to 8 ms a
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
- @torch.inference_mode()
124
- def _logz(self, texts: Sequence[str], questions: Sequence[Question] | None = None) -> list[torch.Tensor]:
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
- return self._logz_ids([self.tokenizer.encode(t, add_special_tokens=False) for t in texts])
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