lhallee commited on
Commit
ff54ea5
·
verified ·
1 Parent(s): b731a88

Update FastPLMs runtime and model cards

Browse files

Add-only FastPLMs files-only publication. Checkpoint weights and complete-artifact attestations are unchanged.

README.md CHANGED
@@ -18,15 +18,34 @@ Supported Transformers entry points are `AutoConfig`, `AutoModel`,
18
  `AutoModelForMaskedLM`, `AutoModelForSeq2SeqLM`,
19
  `AutoModelForSequenceClassification`, `AutoModelForTokenClassification`.
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  ## Install and platform requirements
22
 
23
- Install the current FastPLMs package:
24
 
25
  ```bash
26
- python -m pip install \
27
- "fastplms @ git+https://github.com/Synthyra/FastPLMs.git"
28
  ```
29
 
 
 
 
30
  Python 3.11-3.14, PyTorch 2.13, and Transformers 5.13 are required. The declared CPU gate covers tiny offline contracts; published checkpoint throughput and parity require the documented device tier. The Hub quick start below requires network
31
  access on first download. For an air-gapped run, first build the manifest-pinned
32
  local artifact and use the offline form shown in the example.
@@ -40,22 +59,22 @@ model_id = "Synthyra/ANKH2_large"
40
  model = AutoModel.from_pretrained(
41
  model_id,
42
  trust_remote_code=True,
 
43
  ).eval()
44
  ```
45
 
46
- This example uses the published Hub repository. For offline validation, build
47
- the manifest-pinned artifact and replace `model_id` with its local
48
- `dist/hub/ANKH2_large` path, then pass `local_files_only=True`.
 
49
 
50
- Leave attention unspecified for the Transformers default. Supported explicit
51
- choices are `eager`, `sdpa`.
52
- Pass the selected name through `attn_implementation`.
53
- When an optimized backend cannot return full attention tensors,
54
- `output_attentions=True` emits one explicit runtime warning and uses a correctly
55
- masked eager implementation for that call only. The warning identifies the
56
- configured backend, effective backend, and reason. Configuration and later
57
- calls are unchanged.
58
- For BF16 execution, this family uses parameters loaded directly in BF16.
59
 
60
  ## Tokenization and forward inference
61
 
@@ -86,8 +105,8 @@ print(output.last_hidden_state.shape)
86
 
87
  ## Dataset embeddings
88
 
89
- Dataset embeddings default to the encoder's final hidden state. Layer indices
90
- use the selected stack's native hidden-state order:
91
 
92
  ```python
93
  encoder_result = model.embed_dataset(
@@ -96,20 +115,11 @@ encoder_result = model.embed_dataset(
96
  hidden_state_index=-1,
97
  full_embeddings=True,
98
  )
99
- all_encoder_layers = model.embed_dataset(
100
- ["MSTNPKPQRKTKRNT"],
101
- hidden_state_source="encoder",
102
- store_all_hidden_states=True,
103
- full_embeddings=True,
104
- )
105
  ```
106
 
107
  Decoder representations require `AutoModelForSeq2SeqLM` and exactly one
108
- explicit, aligned `decoder_inputs` sequence or `decoder_input_ids` tensor. ANKH
109
- does not infer a shifted source sequence because official tasks use prompts,
110
- sentinel tokens, or generated tokens that depend on the task. Protein inputs
111
- remain raw residue strings and sentinel prompts remain tight, as in
112
- `M<extra_id_0>`:
113
 
114
  ```python
115
  from transformers import AutoModelForSeq2SeqLM
@@ -125,12 +135,105 @@ decoder_result = seq2seq.embed_dataset(
125
  decoder_inputs=["M<extra_id_0>"],
126
  full_embeddings=True,
127
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  ```
129
 
130
- `decoder_attention_mask` is accepted only with `decoder_input_ids`. Decoder
131
- pooling excludes start, EOS, padding, sentinel, and other tokenizer-special
132
- positions. Persisted results record the selected stack and layer, decoder input
133
- and mask fingerprints, input-position alignment, and biological-mask policy.
134
 
135
  ## Encoder and sequence-to-sequence use
136
 
@@ -177,7 +280,7 @@ masked-LM extension and is not an official ANKH head.
177
  - Precision policies: `default`
178
  - BF16 execution: `static_parameters`
179
  - Generation contract: `required`
180
- - Optional dependency group: `core`
181
  - Weight publication allowed: `true`
182
  - Weight license status: `resolved`
183
  - Redistributable: `true`
@@ -188,30 +291,24 @@ masked-LM extension and is not an official ANKH head.
188
  - FastPLMs weights: `Synthyra/ANKH2_large`
189
  - Runtime revision: recorded separately in the built artifact and published commit
190
  - Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
191
- - Generator/schema version and complete/runtime-only attestations: recorded in `provenance.json`
192
  - Canonical transformed state SHA-256: `597c4fe2fa8711f11a25317905f1d62fa92905e55fdd5c0a79614cd9c9d2bca3`
193
  - Conversion equality attestation: recorded in `provenance.json`
194
  - Official checkpoint: `ElnaggarLab/ankh2-ext2`
195
  - Artifact source: `official`
196
  - State transform: `ankh_t5_to_fastplms_v1`
197
- - BF16 execution: `static_parameters`
198
  - Pinned upstreams: `ankh`
199
- - Reference container: `reference-ankh`
200
  - Release tiers: `check`, `compliance`, `feature`, `artifact`, `benchmark`
201
  - Unresolved required file identities: `0`
202
 
203
- The local artifact records exact file identities, conversion details, source
204
- revisions, and legal texts in `provenance.json`. A nonzero unresolved count is a
205
- release blocker.
206
 
207
  ## Validation boundary
208
 
209
- For tiers declared by the manifest, the release contract compares applicable
210
- semantic configuration, tokenizer behavior, state keys, shapes, dtypes,
211
- values, aliases, and representative inference with the pinned official
212
- implementation. This metadata does not by itself claim that a particular build
213
- passed, that one backend is faster, or that an output has biological or
214
- therapeutic validity.
215
 
216
  ## License
217
 
 
18
  `AutoModelForMaskedLM`, `AutoModelForSeq2SeqLM`,
19
  `AutoModelForSequenceClassification`, `AutoModelForTokenClassification`.
20
 
21
+ ## Capabilities
22
+
23
+ | Feature | Status |
24
+ | --- | --- |
25
+ | Sequence classification | Supported: base weights with an untrained task head |
26
+ | Token classification | Supported: base weights with an untrained task head |
27
+ | PEFT fine-tuning | Supported pattern: preserve the separately trained `classifier` |
28
+ | Embeddings | Special: encoder or explicitly prepared decoder states |
29
+ | Test-time training | Supported: low-rank masked-residue adaptation |
30
+ | Attention variants | Supported: `eager`, `sdpa` |
31
+ | Compliance | Declared: exact release evidence is required |
32
+
33
+ A supported interface is not a pretrained downstream predictor. Classification
34
+ heads start untrained, and declared compliance metadata is not a claim that an
35
+ arbitrary local build passed its release gate.
36
+
37
  ## Install and platform requirements
38
 
39
+ Install the direct dependencies published with this model:
40
 
41
  ```bash
42
+ python -m pip install -r \
43
+ "https://huggingface.co/Synthyra/ANKH2_large/resolve/main/requirements.txt"
44
  ```
45
 
46
+ The FastPLMs implementation itself is embedded in the model repository and loaded
47
+ by Transformers through `trust_remote_code=True`.
48
+
49
  Python 3.11-3.14, PyTorch 2.13, and Transformers 5.13 are required. The declared CPU gate covers tiny offline contracts; published checkpoint throughput and parity require the documented device tier. The Hub quick start below requires network
50
  access on first download. For an air-gapped run, first build the manifest-pinned
51
  local artifact and use the offline form shown in the example.
 
59
  model = AutoModel.from_pretrained(
60
  model_id,
61
  trust_remote_code=True,
62
+ attn_implementation="sdpa",
63
  ).eval()
64
  ```
65
 
66
+ For offline validation, replace `model_id` with the manifest-built
67
+ `dist/hub/ANKH2_large` path and pass `local_files_only=True`.
68
+
69
+ ## Attention and compliance
70
 
71
+ The quick start selects `sdpa` explicitly. Declared variants are `eager`, `sdpa`. An unavailable requested backend raises
72
+ instead of silently switching implementations.
73
+ `output_attentions=True` may use the documented, one-call eager fallback solely
74
+ to materialize attention tensors; the configured backend remains unchanged.
75
+
76
+ This family declares the `compliance` tier. Release evidence binds the exact
77
+ checkpoint, backend, dtype, hardware, inputs, and reference revision.
 
 
78
 
79
  ## Tokenization and forward inference
80
 
 
105
 
106
  ## Dataset embeddings
107
 
108
+ Dataset embeddings default to the encoder final state. Select a native encoder
109
+ layer directly:
110
 
111
  ```python
112
  encoder_result = model.embed_dataset(
 
115
  hidden_state_index=-1,
116
  full_embeddings=True,
117
  )
118
+ print(encoder_result[0].tensor.shape) # (l, d)
 
 
 
 
 
119
  ```
120
 
121
  Decoder representations require `AutoModelForSeq2SeqLM` and exactly one
122
+ aligned decoder input. ANKH does not invent a shifted target:
 
 
 
 
123
 
124
  ```python
125
  from transformers import AutoModelForSeq2SeqLM
 
135
  decoder_inputs=["M<extra_id_0>"],
136
  full_embeddings=True,
137
  )
138
+ print(decoder_result[0].tensor.shape) # (decoder_length, d)
139
+ ```
140
+
141
+ Pooling excludes boundary, padding, sentinel, and other non-biological
142
+ positions. Persisted results record the selected stack, layer, inputs, masks,
143
+ and alignment policy.
144
+
145
+ ## Downstream classification
146
+
147
+ Both downstream AutoClasses reuse the checkpoint backbone and initialize a new,
148
+ untrained `classifier`. Sequence labels have shape `(b,)`; residue labels have
149
+ shape `(b, l)` and use `-100` outside biological positions:
150
+
151
+ ```python
152
+ import torch
153
+ from transformers import AutoTokenizer
154
+ from transformers import (
155
+ AutoModelForSequenceClassification,
156
+ AutoModelForTokenClassification,
157
+ )
158
+
159
+ model_id = "Synthyra/ANKH2_large"
160
+ sequence_model = AutoModelForSequenceClassification.from_pretrained(
161
+ model_id, num_labels=2, trust_remote_code=True
162
+ ).eval()
163
+ token_model = AutoModelForTokenClassification.from_pretrained(
164
+ model_id, num_labels=3, trust_remote_code=True
165
+ ).eval()
166
+ tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
167
+ sequences = ["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"]
168
+ batch = tokenizer(sequences, padding=True, return_tensors="pt")
169
+ biological = batch["attention_mask"].bool()
170
+ for special_id in tokenizer.all_special_ids:
171
+ biological &= batch["input_ids"].ne(special_id)
172
+
173
+ sequence_labels = torch.zeros(len(sequences), dtype=torch.long)
174
+ token_labels = torch.full_like(batch["input_ids"], -100)
175
+ token_labels[biological] = 0
176
+
177
+ with torch.inference_mode():
178
+ sequence_output = sequence_model(**batch, labels=sequence_labels)
179
+ token_output = token_model(**batch, labels=token_labels)
180
+ print(sequence_output.logits.shape) # (b, 2)
181
+ print(token_output.logits.shape) # (b, l, 3)
182
+ ```
183
+
184
+ ## PEFT fine-tuning
185
+
186
+ Install the direct training dependencies, then attach LoRA to the loaded checkpoint:
187
+
188
+ ```bash
189
+ python -m pip install "datasets>=4.8,<5" "peft>=0.19,<0.20"
190
+ ```
191
+
192
+ ```python
193
+ from peft import LoraConfig, TaskType, get_peft_model
194
+
195
+ peft_model = get_peft_model(
196
+ sequence_model,
197
+ LoraConfig(
198
+ task_type=TaskType.SEQ_CLS,
199
+ r=8,
200
+ lora_alpha=16,
201
+ target_modules="all-linear",
202
+ modules_to_save=["classifier"],
203
+ ),
204
+ )
205
+ ```
206
+
207
+ This checkpoint advertises a classification head, so the separately trained
208
+ `classifier` is saved with the adapter.
209
+ All FastPLMs checkpoints follow the Transformers `PreTrainedModel` contract and
210
+ can be adapted with PEFT. The ESM2-specific shipped CLI is an example, not a
211
+ support boundary. Record the target modules, base revision, data identity, and
212
+ trainable parameter scope.
213
+
214
+ ## Test-time training
215
+
216
+ TTT samples masked views of one protein and updates only injected low-rank
217
+ adapters. Base checkpoint weights remain frozen:
218
+
219
+ ```python
220
+ from transformers import AutoModelForMaskedLM
221
+
222
+ ttt_model = AutoModelForMaskedLM.from_pretrained(
223
+ "Synthyra/ANKH2_large",
224
+ trust_remote_code=True,
225
+ )
226
+ metrics = ttt_model.ttt(
227
+ seq="MSTNPKPQRKTKRNT",
228
+ ttt_config={"steps": 3, "batch_size": 1, "seed": 7},
229
+ )
230
+ ttt_model.save_pretrained("adapted", safe_serialization=True)
231
+ ttt_model.ttt_reset()
232
+ print(metrics)
233
  ```
234
 
235
+ Persisted adapters retain their deterministic reset state. TTT adds latency
236
+ and memory, can worsen an output, and does not establish biological function.
 
 
237
 
238
  ## Encoder and sequence-to-sequence use
239
 
 
280
  - Precision policies: `default`
281
  - BF16 execution: `static_parameters`
282
  - Generation contract: `required`
283
+ - Artifact dependency set: `core`
284
  - Weight publication allowed: `true`
285
  - Weight license status: `resolved`
286
  - Redistributable: `true`
 
291
  - FastPLMs weights: `Synthyra/ANKH2_large`
292
  - Runtime revision: recorded separately in the built artifact and published commit
293
  - Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
 
294
  - Canonical transformed state SHA-256: `597c4fe2fa8711f11a25317905f1d62fa92905e55fdd5c0a79614cd9c9d2bca3`
295
  - Conversion equality attestation: recorded in `provenance.json`
296
  - Official checkpoint: `ElnaggarLab/ankh2-ext2`
297
  - Artifact source: `official`
298
  - State transform: `ankh_t5_to_fastplms_v1`
 
299
  - Pinned upstreams: `ankh`
 
300
  - Release tiers: `check`, `compliance`, `feature`, `artifact`, `benchmark`
301
  - Unresolved required file identities: `0`
302
 
303
+ `provenance.json` records exact file identities, conversion, source revisions,
304
+ legal texts, schema, and attestations. A nonzero unresolved count blocks release.
 
305
 
306
  ## Validation boundary
307
 
308
+ Declared tiers compare applicable configuration, tokenizer behavior, state,
309
+ and representative inference with the pinned reference. Metadata alone does
310
+ not claim a build passed, a backend is faster, or an output is biologically
311
+ valid.
 
 
312
 
313
  ## License
314
 
config.json CHANGED
@@ -22,11 +22,11 @@
22
  "fastplms_checkpoint_repo_id": "ElnaggarLab/ankh2-ext2",
23
  "fastplms_checkpoint_revision": "aa9b9fa72288c47d9f618ce80c011e24b54e17a8",
24
  "fastplms_model_id": "ankh2_large",
25
- "fastplms_release_tool_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
26
- "fastplms_release_tool_sha256": "19b4247aba04a74de5069397518374ab2c3b9e0ce18ba9f36de34f0f4c5d1ceb",
27
- "fastplms_runtime_bundle_sha256": "405e7dddef77117e55232e1112faf088db88bb9b13e212a63b3318c4bc0d5a03",
28
- "fastplms_runtime_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
29
- "fastplms_source_tree_sha256": "e377aef8902771bb8bdabcae9c8853ab50de04d85cacd3d75c6e83508f6d52ed",
30
  "fastplms_weights_revision": "aa9b9fa72288c47d9f618ce80c011e24b54e17a8",
31
  "feed_forward_proj": "gated-silu",
32
  "initializer_factor": 1.0,
 
22
  "fastplms_checkpoint_repo_id": "ElnaggarLab/ankh2-ext2",
23
  "fastplms_checkpoint_revision": "aa9b9fa72288c47d9f618ce80c011e24b54e17a8",
24
  "fastplms_model_id": "ankh2_large",
25
+ "fastplms_release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
26
+ "fastplms_release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
27
+ "fastplms_runtime_bundle_sha256": "437c5f5dcc809678e99b8e36d962e78b7960170c81d0b6289d2baeef7920e40f",
28
+ "fastplms_runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
29
+ "fastplms_source_tree_sha256": "44f108a32fffbe0e689434fbff109aea7d03a7085a550ed5771f2eac434f130d",
30
  "fastplms_weights_revision": "aa9b9fa72288c47d9f618ce80c011e24b54e17a8",
31
  "feed_forward_proj": "gated-silu",
32
  "initializer_factor": 1.0,
fastplms/__init__.py CHANGED
@@ -9,6 +9,7 @@ from __future__ import annotations
9
  from importlib import import_module
10
  from typing import Any
11
 
 
12
  __version__ = "1.0.0"
13
 
14
  _LAZY_EXPORTS = {
 
9
  from importlib import import_module
10
  from typing import Any
11
 
12
+
13
  __version__ = "1.0.0"
14
 
15
  _LAZY_EXPORTS = {
fastplms/attention/__init__.py CHANGED
@@ -32,6 +32,7 @@ from .interfaces import (
32
  validate_transformers_attention_interfaces,
33
  )
34
 
 
35
  __all__ = [
36
  "FASTPLMS_ATTENTION_FUNCTIONS",
37
  "FASTPLMS_ATTENTION_MASKS",
 
32
  validate_transformers_attention_interfaces,
33
  )
34
 
35
+
36
  __all__ = [
37
  "FASTPLMS_ATTENTION_FUNCTIONS",
38
  "FASTPLMS_ATTENTION_MASKS",
fastplms/attention/_core.py CHANGED
@@ -8,17 +8,17 @@ FastPLMs never downloads or compiles code.
8
  from __future__ import annotations
9
 
10
  import warnings
 
11
  from collections import OrderedDict
12
  from collections.abc import Callable
13
  from enum import Enum
14
  from threading import RLock
15
-
16
- import torch
17
  from einops import rearrange
18
  from torch.nn import functional as F
19
 
20
  from ._kernel_lock import load_locked_kernel
21
 
 
22
  try:
23
  from torch.nn.attention.flex_attention import BlockMask, create_block_mask, flex_attention
24
  except ImportError:
@@ -117,12 +117,12 @@ def _get_flex_block_mask(
117
  raise RuntimeError(
118
  "'flex_attention' was requested, but torch.create_block_mask is unavailable."
119
  )
120
- pattern = mask_pattern.detach().to(device=device).contiguous()
121
  # One device-to-host transfer is required for an exact cache identity. Use
122
  # the contiguous buffer directly instead of materializing one Python int
123
  # per byte, which is prohibitively expensive for long batched sequences.
124
- host_pattern = pattern.to(device="cpu").contiguous()
125
- pattern_bytes = host_pattern.view(torch.uint8).numpy().tobytes(order="C")
126
  cache_key = (
127
  str(device),
128
  None if dtype is None else str(dtype),
@@ -203,6 +203,7 @@ def _validate_kernels_flash_dtype(
203
  ) -> torch.dtype:
204
  """Reject dtypes outside the immutable kernel manifest before dispatch."""
205
 
 
206
  tensor_dtypes = {query_states.dtype, key_states.dtype, value_states.dtype}
207
  if len(tensor_dtypes) != 1:
208
  observed = ", ".join(sorted(str(dtype) for dtype in tensor_dtypes))
@@ -243,6 +244,7 @@ def _validate_kernels_flash_device(
243
  ) -> torch.device:
244
  """Require Q, K, and V on one CUDA device before loading a kernel."""
245
 
 
246
  devices = (query_states.device, key_states.device, value_states.device)
247
  if len(set(devices)) != 1:
248
  observed = ", ".join(str(device) for device in devices)
@@ -284,9 +286,10 @@ def _kernels_flash_forward(
284
  Failing to override when Q is pre-scaled applies the scale twice and breaks
285
  parity with eager attention and SDPA.
286
  """
 
287
  flash_kernel, flash_kernel_variant = _ensure_flash_kernels_loaded(implementation)
288
  if flash_kernel_variant == "flash_attn2":
289
- output = flash_kernel.flash_attn_func(
290
  q=query_states,
291
  k=key_states,
292
  v=value_states,
@@ -294,9 +297,9 @@ def _kernels_flash_forward(
294
  softmax_scale=softmax_scale,
295
  causal=causal,
296
  )
297
- return output[0] if isinstance(output, tuple) else output
298
  if flash_kernel_variant == "flash_attn3":
299
- output = flash_kernel.flash_attn_func(
300
  q=query_states,
301
  k=key_states,
302
  v=value_states,
@@ -304,8 +307,8 @@ def _kernels_flash_forward(
304
  causal=causal,
305
  )
306
  if isinstance(output, tuple):
307
- return output[0]
308
- return output
309
  raise RuntimeError(f"Unsupported FlashAttention kernel variant: {flash_kernel_variant}")
310
 
311
 
@@ -326,9 +329,11 @@ def _kernels_flash_varlen_forward(
326
  See `_kernels_flash_forward` docstring for why `softmax_scale=1.0` must be
327
  passed when Q has been pre-scaled by the caller.
328
  """
 
 
329
  flash_kernel, flash_kernel_variant = _ensure_flash_kernels_loaded(implementation)
330
  if flash_kernel_variant == "flash_attn2":
331
- output = flash_kernel.flash_attn_varlen_func(
332
  q=query_states,
333
  k=key_states,
334
  v=value_states,
@@ -340,9 +345,9 @@ def _kernels_flash_varlen_forward(
340
  softmax_scale=softmax_scale,
341
  causal=causal,
342
  )
343
- return output[0] if isinstance(output, tuple) else output
344
  if flash_kernel_variant == "flash_attn3":
345
- output = flash_kernel.flash_attn_varlen_func(
346
  q=query_states,
347
  k=key_states,
348
  v=value_states,
@@ -354,8 +359,8 @@ def _kernels_flash_varlen_forward(
354
  causal=causal,
355
  )
356
  if isinstance(output, tuple):
357
- return output[0]
358
- return output
359
  raise RuntimeError(f"Unsupported FlashAttention kernel variant: {flash_kernel_variant}")
360
 
361
 
@@ -364,6 +369,7 @@ def _kernels_flash_varlen_forward(
364
  class IndexFirstAxis(torch.autograd.Function):
365
  @staticmethod
366
  def forward(ctx, input, indices) -> torch.Tensor:
 
367
  ctx.save_for_backward(indices)
368
  if input.ndim < 2:
369
  raise ValueError(
@@ -377,12 +383,13 @@ class IndexFirstAxis(torch.autograd.Function):
377
  )
378
  ctx.first_axis_dim, other_shape = input.shape[0], input.shape[1:]
379
  second_dim = other_shape.numel()
380
- return torch.gather(
381
  rearrange(input, "b ... -> b (...)"), 0, indices.unsqueeze(1).expand(-1, second_dim)
382
  ).reshape(-1, *other_shape)
383
 
384
  @staticmethod
385
  def backward(ctx, grad_output) -> tuple[torch.Tensor, None]:
 
386
  (indices,) = ctx.saved_tensors
387
  if grad_output.ndim < 2:
388
  raise RuntimeError(
@@ -390,19 +397,20 @@ class IndexFirstAxis(torch.autograd.Function):
390
  "two dimensions."
391
  )
392
  other_shape = grad_output.shape[1:]
393
- grad_output = rearrange(grad_output, "b ... -> b (...)")
394
- grad_input = torch.zeros(
395
  [ctx.first_axis_dim, grad_output.shape[1]],
396
  device=grad_output.device,
397
  dtype=grad_output.dtype,
398
  )
399
  grad_input.scatter_(0, indices.unsqueeze(1).expand(-1, grad_output.shape[1]), grad_output)
400
- return grad_input.reshape(ctx.first_axis_dim, *other_shape), None
401
 
402
 
403
  class IndexPutFirstAxis(torch.autograd.Function):
404
  @staticmethod
405
  def forward(ctx, values, indices, first_axis_dim) -> torch.Tensor:
 
406
  ctx.save_for_backward(indices)
407
  if indices.ndim != 1:
408
  raise ValueError(
@@ -414,16 +422,17 @@ class IndexPutFirstAxis(torch.autograd.Function):
414
  "index_put_first_axis values must have at least two dimensions; "
415
  f"received shape {tuple(values.shape)}."
416
  )
417
- output = torch.zeros(
418
  first_axis_dim, *values.shape[1:], device=values.device, dtype=values.dtype
419
  )
420
  output[indices] = values
421
- return output
422
 
423
  @staticmethod
424
  def backward(ctx, grad_output) -> tuple[torch.Tensor, None, None]:
 
425
  (indices,) = ctx.saved_tensors
426
- return grad_output[indices], None, None
427
 
428
 
429
  index_first_axis = IndexFirstAxis.apply
@@ -433,8 +442,9 @@ index_put_first_axis = IndexPutFirstAxis.apply
433
  def pad_input(
434
  hidden_states: torch.Tensor, indices: torch.Tensor, batch: int, seqlen: int
435
  ) -> torch.Tensor:
436
- output = index_put_first_axis(hidden_states, indices, batch * seqlen)
437
- return rearrange(output, "(b s) ... -> b s ...", b=batch)
 
438
 
439
 
440
  def _unpad_input(
@@ -450,18 +460,19 @@ def _unpad_input(
450
  tuple[torch.Tensor, torch.Tensor],
451
  tuple[int, int],
452
  ]:
 
453
  batch_size, seq_len, num_heads, head_dim = query_layer.shape
454
- seqlens = attention_mask_2d.sum(dim=1).int()
455
- cu_seqlens = F.pad(seqlens.cumsum(0, dtype=torch.int32), (1, 0))
456
  max_seqlen = int(seqlens.max().item())
457
- indices = attention_mask_2d.flatten().nonzero(as_tuple=False).flatten()
458
- query_layer = index_first_axis(
459
  query_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
460
  )
461
- key_layer = index_first_axis(
462
  key_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
463
  )
464
- value_layer = index_first_axis(
465
  value_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
466
  )
467
  return (
@@ -482,6 +493,7 @@ def _validate_flash_padding_mask(
482
  ) -> torch.Tensor:
483
  """Validate the self-attention padding mask used by the varlen kernels."""
484
 
 
485
  if attention_mask_2d.ndim != 2:
486
  raise ValueError("FlashAttention padding masks must have shape (batch, sequence_length).")
487
  expected_shape = query_states.shape[:2]
@@ -497,7 +509,7 @@ def _validate_flash_padding_mask(
497
  )
498
  if attention_mask_2d.device != query_states.device:
499
  raise ValueError("FlashAttention padding mask and Q, K, and V must be on the same device.")
500
- return attention_mask_2d.to(dtype=torch.bool)
501
 
502
 
503
  def kernels_flash_attention_func(
@@ -521,6 +533,8 @@ def kernels_flash_attention_func(
521
  `softmax_scale=1.0`. Otherwise the flash kernel applies its default scale
522
  again, yielding an effective `1/head_dim` scale that drifts across layers.
523
  """
 
 
524
  _validate_kernels_flash_device(
525
  query_states,
526
  key_states,
@@ -534,11 +548,11 @@ def kernels_flash_attention_func(
534
  implementation,
535
  )
536
  if query_states.dtype != runtime_dtype:
537
- query_states = query_states.to(dtype=runtime_dtype)
538
- key_states = key_states.to(dtype=runtime_dtype)
539
- value_states = value_states.to(dtype=runtime_dtype)
540
  if attention_mask_2d is not None:
541
- attention_mask_2d = _validate_flash_padding_mask(
542
  query_states,
543
  key_states,
544
  value_states,
@@ -554,8 +568,13 @@ def kernels_flash_attention_func(
554
  indices_q,
555
  (cu_seqlens_q, cu_seqlens_k),
556
  (max_seqlen_q, max_seqlen_k),
557
- ) = _unpad_input(query_states, key_states, value_states, attention_mask_2d)
558
- attn_output_unpad = _kernels_flash_varlen_forward(
 
 
 
 
 
559
  query_states=query_states,
560
  key_states=key_states,
561
  value_states=value_states,
@@ -567,10 +586,10 @@ def kernels_flash_attention_func(
567
  softmax_scale=softmax_scale,
568
  implementation=implementation,
569
  )
570
- output = pad_input(attn_output_unpad, indices_q, batch_size, q_len)
571
- return output.masked_fill(~attention_mask_2d[:, :, None, None], 0)
572
  else:
573
- return _kernels_flash_forward(
574
  query_states=query_states,
575
  key_states=key_states,
576
  value_states=value_states,
@@ -706,6 +725,7 @@ def get_attention_mask(
706
 
707
  Returns (attention_mask_2d, attention_mask_4d, flex_block_mask).
708
  """
 
709
  if attention_mask is None:
710
  return None, None, None
711
 
@@ -720,14 +740,14 @@ def get_attention_mask(
720
  "attention_mask shape must match the input batch and sequence dimensions; "
721
  f"expected {expected_shape}, received {tuple(attention_mask.shape)}."
722
  )
723
- attention_mask_2d = attention_mask.to(device=device, dtype=torch.bool)
724
  if not bool(attention_mask_2d.any(dim=1).all()):
725
  raise ValueError("attention_mask must keep at least one valid key per batch row.")
726
 
727
  effective_backend = resolve_attention_backend(effective_backend)
728
 
729
  if effective_backend.is_flash:
730
- return attention_mask_2d, None, None
731
 
732
  if effective_backend == AttentionBackend.FLEX_ATTENTION:
733
  if create_block_mask is None:
@@ -751,12 +771,12 @@ def get_attention_mask(
751
  mask_semantics=mask_semantics,
752
  mask_mod=mask_mod,
753
  )
754
- return attention_mask_2d, None, flex_block_mask
755
 
756
  # SDPA/manual masks only keys. Padding queries still attend to real keys, so
757
  # their outputs stay finite instead of softmaxing over all -inf scores.
758
- attention_mask_4d = attention_mask_2d[:, None, None, :]
759
- return attention_mask_2d, attention_mask_4d, None
760
 
761
 
762
  def bool_to_additive_mask(
@@ -770,10 +790,11 @@ def bool_to_additive_mask(
770
  That silently drops the mask. Always allocate a float tensor first, then fill it.
771
  This helper is the sanctioned way to build an SDPA additive mask from a bool validity mask.
772
  """
 
773
  if bool_mask.dtype != torch.bool:
774
  raise TypeError(
775
  f"bool_to_additive_mask requires a bool tensor, got dtype={bool_mask.dtype}"
776
  )
777
- additive = torch.zeros_like(bool_mask, dtype=dtype)
778
  additive.masked_fill_(bool_mask.logical_not(), float("-inf"))
779
- return additive
 
8
  from __future__ import annotations
9
 
10
  import warnings
11
+ import torch
12
  from collections import OrderedDict
13
  from collections.abc import Callable
14
  from enum import Enum
15
  from threading import RLock
 
 
16
  from einops import rearrange
17
  from torch.nn import functional as F
18
 
19
  from ._kernel_lock import load_locked_kernel
20
 
21
+
22
  try:
23
  from torch.nn.attention.flex_attention import BlockMask, create_block_mask, flex_attention
24
  except ImportError:
 
117
  raise RuntimeError(
118
  "'flex_attention' was requested, but torch.create_block_mask is unavailable."
119
  )
120
+ pattern = mask_pattern.detach().to(device=device).contiguous() # mask_pattern.shape
121
  # One device-to-host transfer is required for an exact cache identity. Use
122
  # the contiguous buffer directly instead of materializing one Python int
123
  # per byte, which is prohibitively expensive for long batched sequences.
124
+ host_pattern = pattern.to(device="cpu").contiguous() # mask_pattern.shape
125
+ pattern_bytes = host_pattern.view(torch.uint8).numpy().tobytes(order="C") # bytes
126
  cache_key = (
127
  str(device),
128
  None if dtype is None else str(dtype),
 
203
  ) -> torch.dtype:
204
  """Reject dtypes outside the immutable kernel manifest before dispatch."""
205
 
206
+ # query_states, key_states, value_states: (b, l, h, d) or (t, h, d)
207
  tensor_dtypes = {query_states.dtype, key_states.dtype, value_states.dtype}
208
  if len(tensor_dtypes) != 1:
209
  observed = ", ".join(sorted(str(dtype) for dtype in tensor_dtypes))
 
244
  ) -> torch.device:
245
  """Require Q, K, and V on one CUDA device before loading a kernel."""
246
 
247
+ # query_states, key_states, value_states: (b, l, h, d) or (t, h, d)
248
  devices = (query_states.device, key_states.device, value_states.device)
249
  if len(set(devices)) != 1:
250
  observed = ", ".join(str(device) for device in devices)
 
286
  Failing to override when Q is pre-scaled applies the scale twice and breaks
287
  parity with eager attention and SDPA.
288
  """
289
+ # query_states, key_states, value_states: (b, l, h, d)
290
  flash_kernel, flash_kernel_variant = _ensure_flash_kernels_loaded(implementation)
291
  if flash_kernel_variant == "flash_attn2":
292
+ output = flash_kernel.flash_attn_func( # (b, l, h, d) or tuple with that first
293
  q=query_states,
294
  k=key_states,
295
  v=value_states,
 
297
  softmax_scale=softmax_scale,
298
  causal=causal,
299
  )
300
+ return output[0] if isinstance(output, tuple) else output # (b, l, h, d)
301
  if flash_kernel_variant == "flash_attn3":
302
+ output = flash_kernel.flash_attn_func( # (b, l, h, d) or tuple with that first
303
  q=query_states,
304
  k=key_states,
305
  v=value_states,
 
307
  causal=causal,
308
  )
309
  if isinstance(output, tuple):
310
+ return output[0] # (b, l, h, d)
311
+ return output # (b, l, h, d)
312
  raise RuntimeError(f"Unsupported FlashAttention kernel variant: {flash_kernel_variant}")
313
 
314
 
 
329
  See `_kernels_flash_forward` docstring for why `softmax_scale=1.0` must be
330
  passed when Q has been pre-scaled by the caller.
331
  """
332
+ # query_states, key_states, value_states: (t, h, d)
333
+ # cu_seqlens_q, cu_seqlens_k: (b + 1,)
334
  flash_kernel, flash_kernel_variant = _ensure_flash_kernels_loaded(implementation)
335
  if flash_kernel_variant == "flash_attn2":
336
+ output = flash_kernel.flash_attn_varlen_func( # (t, h, d) or tuple with that first
337
  q=query_states,
338
  k=key_states,
339
  v=value_states,
 
345
  softmax_scale=softmax_scale,
346
  causal=causal,
347
  )
348
+ return output[0] if isinstance(output, tuple) else output # (t, h, d)
349
  if flash_kernel_variant == "flash_attn3":
350
+ output = flash_kernel.flash_attn_varlen_func( # (t, h, d) or tuple with that first
351
  q=query_states,
352
  k=key_states,
353
  v=value_states,
 
359
  causal=causal,
360
  )
361
  if isinstance(output, tuple):
362
+ return output[0] # (t, h, d)
363
+ return output # (t, h, d)
364
  raise RuntimeError(f"Unsupported FlashAttention kernel variant: {flash_kernel_variant}")
365
 
366
 
 
369
  class IndexFirstAxis(torch.autograd.Function):
370
  @staticmethod
371
  def forward(ctx, input, indices) -> torch.Tensor:
372
+ # input: (n, ...); indices: (m,)
373
  ctx.save_for_backward(indices)
374
  if input.ndim < 2:
375
  raise ValueError(
 
383
  )
384
  ctx.first_axis_dim, other_shape = input.shape[0], input.shape[1:]
385
  second_dim = other_shape.numel()
386
+ return torch.gather( # (m, ...)
387
  rearrange(input, "b ... -> b (...)"), 0, indices.unsqueeze(1).expand(-1, second_dim)
388
  ).reshape(-1, *other_shape)
389
 
390
  @staticmethod
391
  def backward(ctx, grad_output) -> tuple[torch.Tensor, None]:
392
+ # grad_output: (m, ...)
393
  (indices,) = ctx.saved_tensors
394
  if grad_output.ndim < 2:
395
  raise RuntimeError(
 
397
  "two dimensions."
398
  )
399
  other_shape = grad_output.shape[1:]
400
+ grad_output = rearrange(grad_output, "b ... -> b (...)") # (m, product(...))
401
+ grad_input = torch.zeros( # (n, product(...))
402
  [ctx.first_axis_dim, grad_output.shape[1]],
403
  device=grad_output.device,
404
  dtype=grad_output.dtype,
405
  )
406
  grad_input.scatter_(0, indices.unsqueeze(1).expand(-1, grad_output.shape[1]), grad_output)
407
+ return grad_input.reshape(ctx.first_axis_dim, *other_shape), None # (n, ...), None
408
 
409
 
410
  class IndexPutFirstAxis(torch.autograd.Function):
411
  @staticmethod
412
  def forward(ctx, values, indices, first_axis_dim) -> torch.Tensor:
413
+ # values: (m, ...); indices: (m,)
414
  ctx.save_for_backward(indices)
415
  if indices.ndim != 1:
416
  raise ValueError(
 
422
  "index_put_first_axis values must have at least two dimensions; "
423
  f"received shape {tuple(values.shape)}."
424
  )
425
+ output = torch.zeros( # (n, ...)
426
  first_axis_dim, *values.shape[1:], device=values.device, dtype=values.dtype
427
  )
428
  output[indices] = values
429
+ return output # (n, ...)
430
 
431
  @staticmethod
432
  def backward(ctx, grad_output) -> tuple[torch.Tensor, None, None]:
433
+ # grad_output: (n, ...)
434
  (indices,) = ctx.saved_tensors
435
+ return grad_output[indices], None, None # (m, ...), None, None
436
 
437
 
438
  index_first_axis = IndexFirstAxis.apply
 
442
  def pad_input(
443
  hidden_states: torch.Tensor, indices: torch.Tensor, batch: int, seqlen: int
444
  ) -> torch.Tensor:
445
+ # hidden_states: (t, ...); indices: (t,)
446
+ output = index_put_first_axis(hidden_states, indices, batch * seqlen) # (b * l, ...)
447
+ return rearrange(output, "(b s) ... -> b s ...", b=batch) # (b, l, ...)
448
 
449
 
450
  def _unpad_input(
 
460
  tuple[torch.Tensor, torch.Tensor],
461
  tuple[int, int],
462
  ]:
463
+ # query_layer, key_layer, value_layer: (b, l, h, d); attention_mask_2d: (b, l)
464
  batch_size, seq_len, num_heads, head_dim = query_layer.shape
465
+ seqlens = attention_mask_2d.sum(dim=1).int() # (b,)
466
+ cu_seqlens = F.pad(seqlens.cumsum(0, dtype=torch.int32), (1, 0)) # (b + 1,)
467
  max_seqlen = int(seqlens.max().item())
468
+ indices = attention_mask_2d.flatten().nonzero(as_tuple=False).flatten() # (t,)
469
+ query_layer = index_first_axis( # (t, h, d)
470
  query_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
471
  )
472
+ key_layer = index_first_axis( # (t, h, d)
473
  key_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
474
  )
475
+ value_layer = index_first_axis( # (t, h, d)
476
  value_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
477
  )
478
  return (
 
493
  ) -> torch.Tensor:
494
  """Validate the self-attention padding mask used by the varlen kernels."""
495
 
496
+ # query_states, key_states, value_states: (b, l, h, d); attention_mask_2d: (b, l)
497
  if attention_mask_2d.ndim != 2:
498
  raise ValueError("FlashAttention padding masks must have shape (batch, sequence_length).")
499
  expected_shape = query_states.shape[:2]
 
509
  )
510
  if attention_mask_2d.device != query_states.device:
511
  raise ValueError("FlashAttention padding mask and Q, K, and V must be on the same device.")
512
+ return attention_mask_2d.to(dtype=torch.bool) # (b, l)
513
 
514
 
515
  def kernels_flash_attention_func(
 
533
  `softmax_scale=1.0`. Otherwise the flash kernel applies its default scale
534
  again, yielding an effective `1/head_dim` scale that drifts across layers.
535
  """
536
+ # query_states, key_states, value_states: (b, l, h, d)
537
+ # attention_mask_2d: (b, l) or None
538
  _validate_kernels_flash_device(
539
  query_states,
540
  key_states,
 
548
  implementation,
549
  )
550
  if query_states.dtype != runtime_dtype:
551
+ query_states = query_states.to(dtype=runtime_dtype) # (b, l, h, d)
552
+ key_states = key_states.to(dtype=runtime_dtype) # (b, l, h, d)
553
+ value_states = value_states.to(dtype=runtime_dtype) # (b, l, h, d)
554
  if attention_mask_2d is not None:
555
+ attention_mask_2d = _validate_flash_padding_mask( # (b, l)
556
  query_states,
557
  key_states,
558
  value_states,
 
568
  indices_q,
569
  (cu_seqlens_q, cu_seqlens_k),
570
  (max_seqlen_q, max_seqlen_k),
571
+ ) = _unpad_input( # (t, h, d), (t, h, d), (t, h, d), (t,), (b + 1,), scalars
572
+ query_states,
573
+ key_states,
574
+ value_states,
575
+ attention_mask_2d,
576
+ )
577
+ attn_output_unpad = _kernels_flash_varlen_forward( # (t, h, d)
578
  query_states=query_states,
579
  key_states=key_states,
580
  value_states=value_states,
 
586
  softmax_scale=softmax_scale,
587
  implementation=implementation,
588
  )
589
+ output = pad_input(attn_output_unpad, indices_q, batch_size, q_len) # (b, l, h, d)
590
+ return output.masked_fill(~attention_mask_2d[:, :, None, None], 0) # (b, l, h, d)
591
  else:
592
+ return _kernels_flash_forward( # (b, l, h, d)
593
  query_states=query_states,
594
  key_states=key_states,
595
  value_states=value_states,
 
725
 
726
  Returns (attention_mask_2d, attention_mask_4d, flex_block_mask).
727
  """
728
+ # attention_mask: (b, l) or None
729
  if attention_mask is None:
730
  return None, None, None
731
 
 
740
  "attention_mask shape must match the input batch and sequence dimensions; "
741
  f"expected {expected_shape}, received {tuple(attention_mask.shape)}."
742
  )
743
+ attention_mask_2d = attention_mask.to(device=device, dtype=torch.bool) # (b, l)
744
  if not bool(attention_mask_2d.any(dim=1).all()):
745
  raise ValueError("attention_mask must keep at least one valid key per batch row.")
746
 
747
  effective_backend = resolve_attention_backend(effective_backend)
748
 
749
  if effective_backend.is_flash:
750
+ return attention_mask_2d, None, None # (b, l), None, None
751
 
752
  if effective_backend == AttentionBackend.FLEX_ATTENTION:
753
  if create_block_mask is None:
 
771
  mask_semantics=mask_semantics,
772
  mask_mod=mask_mod,
773
  )
774
+ return attention_mask_2d, None, flex_block_mask # (b, l), None, BlockMask
775
 
776
  # SDPA/manual masks only keys. Padding queries still attend to real keys, so
777
  # their outputs stay finite instead of softmaxing over all -inf scores.
778
+ attention_mask_4d = attention_mask_2d[:, None, None, :] # (b, 1, 1, l)
779
+ return attention_mask_2d, attention_mask_4d, None # (b, l), (b, 1, 1, l), None
780
 
781
 
782
  def bool_to_additive_mask(
 
790
  That silently drops the mask. Always allocate a float tensor first, then fill it.
791
  This helper is the sanctioned way to build an SDPA additive mask from a bool validity mask.
792
  """
793
+ # bool_mask: (...)
794
  if bool_mask.dtype != torch.bool:
795
  raise TypeError(
796
  f"bool_to_additive_mask requires a bool tensor, got dtype={bool_mask.dtype}"
797
  )
798
+ additive = torch.zeros_like(bool_mask, dtype=dtype) # (...)
799
  additive.masked_fill_(bool_mask.logical_not(), float("-inf"))
800
+ return additive # (...)
fastplms/attention/_kernel_lock.py CHANGED
@@ -2,7 +2,6 @@
2
 
3
  from __future__ import annotations
4
 
5
- import importlib.metadata
6
  import json
7
  import os
8
  from pathlib import Path
@@ -15,50 +14,35 @@ def require_kernels_package() -> None:
15
  import kernels # noqa: F401
16
  except ImportError as error:
17
  raise RuntimeError(
18
- "Precompiled FlashAttention requires the FastPLMs 'flash' extra."
19
  ) from error
20
 
21
 
22
  def _kernel_lock_path() -> Path:
23
- """Return the lock from an artifact, checkout, or installed distribution."""
24
  source_path = Path(__file__).resolve()
25
- candidates = [
26
  source_path.parents[1] / "kernels.lock",
27
  source_path.parents[3] / "kernels.lock",
28
- ]
29
- try:
30
- import fastplms
31
-
32
- candidates.extend(Path(root) / "kernels.lock" for root in fastplms.__path__)
33
- except (ImportError, AttributeError):
34
- pass
35
- for candidate in candidates:
36
  if candidate.is_file():
37
  return candidate
38
 
39
- try:
40
- distribution = importlib.metadata.distribution("fastplms")
41
- except importlib.metadata.PackageNotFoundError as error:
42
- raise RuntimeError("FastPLMs was installed without kernels.lock.") from error
43
- for relative in distribution.files or ():
44
- if relative.name != "kernels.lock":
45
- continue
46
- candidate = Path(distribution.locate_file(relative))
47
- if candidate.is_file():
48
- return candidate
49
- raise RuntimeError("The installed FastPLMs distribution does not contain kernels.lock.")
50
 
51
 
52
  def _locked_entry(lock_path: Path, repository: str) -> dict[str, Any]:
53
  try:
54
- data = json.loads(lock_path.read_text(encoding="utf-8"))
55
  except (OSError, json.JSONDecodeError) as error:
56
- raise RuntimeError(f"Unable to read the packaged kernel lock: {lock_path}") from error
57
- if not isinstance(data, list):
58
  raise RuntimeError("kernels.lock must contain a JSON list.")
59
- if any(not isinstance(entry, dict) for entry in data):
60
  raise RuntimeError("Every kernels.lock entry must be a JSON object.")
61
- matches = [entry for entry in data if entry.get("repo_id") == repository]
62
  if len(matches) != 1:
63
  raise RuntimeError(
64
  f"kernels.lock must contain exactly one entry for {repository!r}; found {len(matches)}."
@@ -125,11 +109,11 @@ def _load_offline_locked_kernel(
125
  from kernels.variants import get_variants_local, resolve_variants
126
  except ImportError as error:
127
  raise RuntimeError(
128
- "Precompiled FlashAttention requires the FastPLMs 'flash' extra."
129
  ) from error
130
 
131
- parsed = get_variants_local(build_root)
132
- parsed_names = {variant.variant_str for variant in parsed}
133
  invalid = sorted(set(cached_names).difference(parsed_names))
134
  if invalid:
135
  raise RuntimeError(
@@ -137,7 +121,7 @@ def _load_offline_locked_kernel(
137
  f"{', '.join(invalid)}"
138
  )
139
 
140
- compatible, _ = resolve_variants(parsed)
141
  if len(compatible) != 1:
142
  names = ", ".join(variant.variant_str for variant in compatible) or "none"
143
  raise RuntimeError(
@@ -165,7 +149,7 @@ def load_locked_kernel(repository: str, revision: str) -> object:
165
  from kernels.lockfile import KernelLock
166
  except ImportError as error:
167
  raise RuntimeError(
168
- "Precompiled FlashAttention requires the FastPLMs 'flash' extra."
169
  ) from error
170
 
171
  lock_path = _kernel_lock_path()
 
2
 
3
  from __future__ import annotations
4
 
 
5
  import json
6
  import os
7
  from pathlib import Path
 
14
  import kernels # noqa: F401
15
  except ImportError as error:
16
  raise RuntimeError(
17
+ "Precompiled FlashAttention requires requirements/features/flash.in."
18
  ) from error
19
 
20
 
21
  def _kernel_lock_path() -> Path:
22
+ """Return the kernel lock from a Hub artifact or source checkout."""
23
  source_path = Path(__file__).resolve()
24
+ for candidate in (
25
  source_path.parents[1] / "kernels.lock",
26
  source_path.parents[3] / "kernels.lock",
27
+ ):
 
 
 
 
 
 
 
28
  if candidate.is_file():
29
  return candidate
30
 
31
+ raise RuntimeError(
32
+ "kernels.lock is missing from the Hugging Face artifact or source checkout."
33
+ )
 
 
 
 
 
 
 
 
34
 
35
 
36
  def _locked_entry(lock_path: Path, repository: str) -> dict[str, Any]:
37
  try:
38
+ lock_entries = json.loads(lock_path.read_text(encoding="utf-8"))
39
  except (OSError, json.JSONDecodeError) as error:
40
+ raise RuntimeError(f"Unable to read the kernel lock: {lock_path}") from error
41
+ if not isinstance(lock_entries, list):
42
  raise RuntimeError("kernels.lock must contain a JSON list.")
43
+ if any(not isinstance(entry, dict) for entry in lock_entries):
44
  raise RuntimeError("Every kernels.lock entry must be a JSON object.")
45
+ matches = [entry for entry in lock_entries if entry.get("repo_id") == repository]
46
  if len(matches) != 1:
47
  raise RuntimeError(
48
  f"kernels.lock must contain exactly one entry for {repository!r}; found {len(matches)}."
 
109
  from kernels.variants import get_variants_local, resolve_variants
110
  except ImportError as error:
111
  raise RuntimeError(
112
+ "Precompiled FlashAttention requires requirements/features/flash.in."
113
  ) from error
114
 
115
+ cached_variants = get_variants_local(build_root)
116
+ parsed_names = {variant.variant_str for variant in cached_variants}
117
  invalid = sorted(set(cached_names).difference(parsed_names))
118
  if invalid:
119
  raise RuntimeError(
 
121
  f"{', '.join(invalid)}"
122
  )
123
 
124
+ compatible, _ = resolve_variants(cached_variants)
125
  if len(compatible) != 1:
126
  names = ", ".join(variant.variant_str for variant in compatible) or "none"
127
  raise RuntimeError(
 
149
  from kernels.lockfile import KernelLock
150
  except ImportError as error:
151
  raise RuntimeError(
152
+ "Precompiled FlashAttention requires requirements/features/flash.in."
153
  ) from error
154
 
155
  lock_path = _kernel_lock_path()
fastplms/attention/interfaces.py CHANGED
@@ -2,11 +2,10 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  from collections.abc import Mapping
6
  from functools import partial
7
  from typing import Any
8
-
9
- import torch
10
  from transformers import AttentionInterface, AttentionMaskInterface
11
 
12
  from ._core import (
@@ -36,6 +35,7 @@ def _kernels_attention_forward(
36
  FastPLMs kernel adapter uses the latter layout internally.
37
  """
38
 
 
39
  dropout = float(kwargs.get("dropout", 0.0) or 0.0)
40
  if module.training and dropout:
41
  raise RuntimeError(
@@ -45,15 +45,15 @@ def _kernels_attention_forward(
45
  causal = bool(kwargs.get("is_causal", getattr(module, "is_causal", False)))
46
  softmax_scale = kwargs.get("scaling")
47
  output = kernels_flash_attention_func(
48
- query_states=query.transpose(1, 2).contiguous(),
49
- key_states=key.transpose(1, 2).contiguous(),
50
- value_states=value.transpose(1, 2).contiguous(),
51
  attention_mask_2d=attention_mask,
52
  causal=causal,
53
  softmax_scale=softmax_scale,
54
  implementation=implementation,
55
- )
56
- return output, None
57
 
58
 
59
  # Keep FastPLMs' kernels-only adapters local to this registry instance.
 
2
 
3
  from __future__ import annotations
4
 
5
+ import torch
6
  from collections.abc import Mapping
7
  from functools import partial
8
  from typing import Any
 
 
9
  from transformers import AttentionInterface, AttentionMaskInterface
10
 
11
  from ._core import (
 
35
  FastPLMs kernel adapter uses the latter layout internally.
36
  """
37
 
38
+ # query, key, value: (b, h, l, d); attention_mask: (b, l) or None
39
  dropout = float(kwargs.get("dropout", 0.0) or 0.0)
40
  if module.training and dropout:
41
  raise RuntimeError(
 
45
  causal = bool(kwargs.get("is_causal", getattr(module, "is_causal", False)))
46
  softmax_scale = kwargs.get("scaling")
47
  output = kernels_flash_attention_func(
48
+ query_states=query.transpose(1, 2).contiguous(), # (b, l, h, d)
49
+ key_states=key.transpose(1, 2).contiguous(), # (b, l, h, d)
50
+ value_states=value.transpose(1, 2).contiguous(), # (b, l, h, d)
51
  attention_mask_2d=attention_mask,
52
  causal=causal,
53
  softmax_scale=softmax_scale,
54
  implementation=implementation,
55
+ ) # (b, l, h, d)
56
+ return output, None # (b, l, h, d), None
57
 
58
 
59
  # Keep FastPLMs' kernels-only adapters local to this registry instance.
fastplms/embeddings/__init__.py CHANGED
@@ -33,6 +33,7 @@ from .types import (
33
  TensorValue,
34
  )
35
 
 
36
  __all__ = [
37
  "DEFAULT_SHARD_SIZE",
38
  "POOLING_NAMES",
 
33
  TensorValue,
34
  )
35
 
36
+
37
  __all__ = [
38
  "DEFAULT_SHARD_SIZE",
39
  "POOLING_NAMES",
fastplms/embeddings/pooling.py CHANGED
@@ -3,15 +3,16 @@
3
  from __future__ import annotations
4
 
5
  import math
6
- from collections.abc import Sequence
7
-
8
  import torch
 
9
  from torch import Tensor
10
 
 
11
  POOLING_NAMES = frozenset({"mean", "max", "norm", "median", "std", "var", "cls", "parti"})
12
 
13
 
14
  def _validate_inputs(X: Tensor, M: Tensor) -> Tensor:
 
15
  if not isinstance(X, Tensor) or not isinstance(M, Tensor):
16
  raise TypeError("X and M must be tensors.")
17
  if X.ndim != 3:
@@ -24,12 +25,12 @@ def _validate_inputs(X: Tensor, M: Tensor) -> Tensor:
24
  raise TypeError("M must be a boolean or binary numeric residue mask.")
25
  if not bool(torch.isfinite(M).all()) or not bool(((M == 0) | (M == 1)).all()):
26
  raise ValueError("M must contain only finite binary mask values.")
27
- M = M.to(device=X.device, dtype=torch.bool)
28
  if not bool(M.any(dim=1).all()):
29
  raise ValueError("Every sample must contain at least one biological residue.")
30
  if not bool((torch.isfinite(X) | ~M.unsqueeze(-1)).all()):
31
  raise ValueError("Biological residue embeddings produced non-finite output.")
32
- return M
33
 
34
 
35
  def _pooled_attention(attentions: Tensor | Sequence[Tensor], *, batch_size: int) -> Tensor:
@@ -43,27 +44,27 @@ def _pooled_attention(attentions: Tensor | Sequence[Tensor], *, batch_size: int)
43
  if isinstance(attentions, Sequence):
44
  if not attentions:
45
  raise ValueError("parti received an empty attention sequence.")
46
- # Each A_i has shape (b, h, l, l).
47
- A = torch.stack(tuple(attentions), dim=1)
48
  else:
49
- A = attentions
50
 
51
  if A.ndim == 5:
52
  if A.shape[0] != batch_size and A.shape[1] == batch_size:
53
- A = A.transpose(0, 1)
54
  if A.shape[0] != batch_size:
55
  raise ValueError("Five-dimensional attentions must use (b, n, h, l, l).")
56
- A = A.flatten(1, 2).amax(dim=1)
57
  elif A.ndim == 4:
58
  if A.shape[0] != batch_size:
59
  raise ValueError("Four-dimensional attentions must use (b, h, l, l).")
60
- A = A.amax(dim=1)
61
  elif A.ndim == 3:
62
  if A.shape[0] != batch_size:
63
  raise ValueError("Three-dimensional attentions must use (b, l, l).")
64
  else:
65
  raise ValueError("Attentions must have shape (b, l, l), (b, h, l, l), or (b, n, h, l, l).")
66
- return A
67
 
68
 
69
  def pagerank_weights(
@@ -79,6 +80,7 @@ def pagerank_weights(
79
  probabilities; dangling rows transition uniformly.
80
  """
81
 
 
82
  if not isinstance(A, Tensor):
83
  raise TypeError("A must be a tensor.")
84
  if A.ndim != 2 or A.shape[0] != A.shape[1]:
@@ -103,19 +105,23 @@ def pagerank_weights(
103
  if not bool(torch.isfinite(A).all()):
104
  raise ValueError("A must contain only finite attention values.")
105
  work_dtype = torch.float64 if A.dtype == torch.float64 else torch.float32
106
- P = A.detach().to(dtype=work_dtype).clamp_min(0)
107
- row_sum = P.sum(dim=-1, keepdim=True)
108
- uniform = torch.full_like(P, 1.0 / length)
109
- P = torch.where(row_sum > 0, P / row_sum.clamp_min(torch.finfo(work_dtype).tiny), uniform)
110
- p = torch.full((length,), 1.0 / length, device=P.device, dtype=work_dtype)
 
 
 
 
111
  teleport = (1.0 - damping) / length
112
  for _ in range(max_iterations):
113
- p_next = teleport + damping * (P.transpose(0, 1) @ p)
114
  if torch.linalg.vector_norm(p_next - p, ord=1) <= tolerance:
115
- p = p_next
116
  break
117
- p = p_next
118
- return p / p.sum()
119
 
120
 
121
  class Pooler:
@@ -157,28 +163,29 @@ class Pooler:
157
  attentions: Tensor | Sequence[Tensor] | None = None,
158
  attention_backend: str | None = None,
159
  ) -> Tensor:
160
- M = _validate_inputs(X, residue_mask)
161
- M_expanded = M.unsqueeze(-1)
162
- count = M_expanded.sum(dim=1).clamp_min(1)
163
- X_residues = X.masked_fill(~M_expanded, 0)
 
164
  outputs: list[Tensor] = []
165
 
166
  for name in self.names:
167
  if name == "mean":
168
- Y = X_residues.sum(dim=1) / count
169
  elif name == "max":
170
- Y = X.masked_fill(~M_expanded, -torch.inf).max(dim=1).values
171
  elif name == "norm":
172
- Y = torch.linalg.vector_norm(X_residues, ord=2, dim=1)
173
  elif name == "median":
174
- Y = X.masked_fill(~M_expanded, torch.nan).nanmedian(dim=1).values
175
  elif name in {"var", "std"}:
176
- mean = X_residues.sum(dim=1, keepdim=True) / count.unsqueeze(1)
177
- centered = (X - mean).masked_fill(~M_expanded, 0)
178
- variance = (centered**2).sum(dim=1) / count
179
- Y = variance.sqrt() if name == "std" else variance
180
  elif name == "cls":
181
- Y = X[:, 0]
182
  else:
183
  if attention_backend != "eager":
184
  raise ValueError(
@@ -189,14 +196,15 @@ class Pooler:
189
  raise ValueError("parti requires model attention matrices.")
190
  if int(M.sum(dim=1).max().item()) > 2048:
191
  raise ValueError("parti supports at most 2,048 biological residues.")
192
- A = _pooled_attention(attentions, batch_size=X.shape[0]).to(X.device)
193
  pooled: list[Tensor] = []
194
  for X_i, M_i, A_i in zip(X, M, A, strict=True):
195
- indices = M_i.nonzero(as_tuple=True)[0]
196
- A_residue = A_i.index_select(0, indices).index_select(1, indices)
197
- w = pagerank_weights(A_residue).to(dtype=X.dtype)
198
- pooled.append(w @ X_i.index_select(0, indices))
199
- Y = torch.stack(pooled)
 
200
  if not bool(torch.isfinite(Y).all()):
201
  raise ValueError(
202
  f"Pooling operation {name!r} produced non-finite output from "
@@ -204,7 +212,7 @@ class Pooler:
204
  )
205
  outputs.append(Y)
206
 
207
- return torch.cat(outputs, dim=-1)
208
 
209
 
210
  __all__ = ["POOLING_NAMES", "Pooler", "pagerank_weights"]
 
3
  from __future__ import annotations
4
 
5
  import math
 
 
6
  import torch
7
+ from collections.abc import Sequence
8
  from torch import Tensor
9
 
10
+
11
  POOLING_NAMES = frozenset({"mean", "max", "norm", "median", "std", "var", "cls", "parti"})
12
 
13
 
14
  def _validate_inputs(X: Tensor, M: Tensor) -> Tensor:
15
+ # X: (b, l, d); M: (b, l)
16
  if not isinstance(X, Tensor) or not isinstance(M, Tensor):
17
  raise TypeError("X and M must be tensors.")
18
  if X.ndim != 3:
 
25
  raise TypeError("M must be a boolean or binary numeric residue mask.")
26
  if not bool(torch.isfinite(M).all()) or not bool(((M == 0) | (M == 1)).all()):
27
  raise ValueError("M must contain only finite binary mask values.")
28
+ M = M.to(device=X.device, dtype=torch.bool) # (b, l)
29
  if not bool(M.any(dim=1).all()):
30
  raise ValueError("Every sample must contain at least one biological residue.")
31
  if not bool((torch.isfinite(X) | ~M.unsqueeze(-1)).all()):
32
  raise ValueError("Biological residue embeddings produced non-finite output.")
33
+ return M # (b, l)
34
 
35
 
36
  def _pooled_attention(attentions: Tensor | Sequence[Tensor], *, batch_size: int) -> Tensor:
 
44
  if isinstance(attentions, Sequence):
45
  if not attentions:
46
  raise ValueError("parti received an empty attention sequence.")
47
+ # Each A_i: (b, h, l, l).
48
+ A = torch.stack(tuple(attentions), dim=1) # (b, n, h, l, l)
49
  else:
50
+ A = attentions # (b, ..., l, l)
51
 
52
  if A.ndim == 5:
53
  if A.shape[0] != batch_size and A.shape[1] == batch_size:
54
+ A = A.transpose(0, 1) # (b, n, h, l, l)
55
  if A.shape[0] != batch_size:
56
  raise ValueError("Five-dimensional attentions must use (b, n, h, l, l).")
57
+ A = A.flatten(1, 2).amax(dim=1) # (b, l, l)
58
  elif A.ndim == 4:
59
  if A.shape[0] != batch_size:
60
  raise ValueError("Four-dimensional attentions must use (b, h, l, l).")
61
+ A = A.amax(dim=1) # (b, l, l)
62
  elif A.ndim == 3:
63
  if A.shape[0] != batch_size:
64
  raise ValueError("Three-dimensional attentions must use (b, l, l).")
65
  else:
66
  raise ValueError("Attentions must have shape (b, l, l), (b, h, l, l), or (b, n, h, l, l).")
67
+ return A # (b, l, l)
68
 
69
 
70
  def pagerank_weights(
 
80
  probabilities; dangling rows transition uniformly.
81
  """
82
 
83
+ # A: (l, l)
84
  if not isinstance(A, Tensor):
85
  raise TypeError("A must be a tensor.")
86
  if A.ndim != 2 or A.shape[0] != A.shape[1]:
 
105
  if not bool(torch.isfinite(A).all()):
106
  raise ValueError("A must contain only finite attention values.")
107
  work_dtype = torch.float64 if A.dtype == torch.float64 else torch.float32
108
+ P = A.detach().to(dtype=work_dtype).clamp_min(0) # (l, l)
109
+ row_sum = P.sum(dim=-1, keepdim=True) # (l, 1)
110
+ uniform = torch.full_like(P, 1.0 / length) # (l, l)
111
+ P = torch.where( # (l, l)
112
+ row_sum > 0,
113
+ P / row_sum.clamp_min(torch.finfo(work_dtype).tiny),
114
+ uniform,
115
+ )
116
+ p = torch.full((length,), 1.0 / length, device=P.device, dtype=work_dtype) # (l,)
117
  teleport = (1.0 - damping) / length
118
  for _ in range(max_iterations):
119
+ p_next = teleport + damping * (P.transpose(0, 1) @ p) # (l,)
120
  if torch.linalg.vector_norm(p_next - p, ord=1) <= tolerance:
121
+ p = p_next # (l,)
122
  break
123
+ p = p_next # (l,)
124
+ return p / p.sum() # (l,)
125
 
126
 
127
  class Pooler:
 
163
  attentions: Tensor | Sequence[Tensor] | None = None,
164
  attention_backend: str | None = None,
165
  ) -> Tensor:
166
+ # X: (b, l, d); residue_mask: (b, l)
167
+ M = _validate_inputs(X, residue_mask) # (b, l)
168
+ M_expanded = M.unsqueeze(-1) # (b, l, 1)
169
+ count = M_expanded.sum(dim=1).clamp_min(1) # (b, 1)
170
+ X_residues = X.masked_fill(~M_expanded, 0) # (b, l, d)
171
  outputs: list[Tensor] = []
172
 
173
  for name in self.names:
174
  if name == "mean":
175
+ Y = X_residues.sum(dim=1) / count # (b, d)
176
  elif name == "max":
177
+ Y = X.masked_fill(~M_expanded, -torch.inf).max(dim=1).values # (b, d)
178
  elif name == "norm":
179
+ Y = torch.linalg.vector_norm(X_residues, ord=2, dim=1) # (b, d)
180
  elif name == "median":
181
+ Y = X.masked_fill(~M_expanded, torch.nan).nanmedian(dim=1).values # (b, d)
182
  elif name in {"var", "std"}:
183
+ mean = X_residues.sum(dim=1, keepdim=True) / count.unsqueeze(1) # (b, 1, d)
184
+ centered = (X - mean).masked_fill(~M_expanded, 0) # (b, l, d)
185
+ variance = (centered**2).sum(dim=1) / count # (b, d)
186
+ Y = variance.sqrt() if name == "std" else variance # (b, d)
187
  elif name == "cls":
188
+ Y = X[:, 0] # (b, d)
189
  else:
190
  if attention_backend != "eager":
191
  raise ValueError(
 
196
  raise ValueError("parti requires model attention matrices.")
197
  if int(M.sum(dim=1).max().item()) > 2048:
198
  raise ValueError("parti supports at most 2,048 biological residues.")
199
+ A = _pooled_attention(attentions, batch_size=X.shape[0]).to(X.device) # (b, l, l)
200
  pooled: list[Tensor] = []
201
  for X_i, M_i, A_i in zip(X, M, A, strict=True):
202
+ # X_i: (l, d); M_i: (l,); A_i: (l, l)
203
+ indices = M_i.nonzero(as_tuple=True)[0] # (r,)
204
+ A_residue = A_i.index_select(0, indices).index_select(1, indices) # (r, r)
205
+ w = pagerank_weights(A_residue).to(dtype=X.dtype) # (r,)
206
+ pooled.append(w @ X_i.index_select(0, indices)) # (d,)
207
+ Y = torch.stack(pooled) # (b, d)
208
  if not bool(torch.isfinite(Y).all()):
209
  raise ValueError(
210
  f"Pooling operation {name!r} produced non-finite output from "
 
212
  )
213
  outputs.append(Y)
214
 
215
+ return torch.cat(outputs, dim=-1) # (b, len(self.names) * d)
216
 
217
 
218
  __all__ = ["POOLING_NAMES", "Pooler", "pagerank_weights"]
fastplms/embeddings/runner.py CHANGED
@@ -7,12 +7,11 @@ import json
7
  import platform
8
  import sqlite3
9
  import tempfile
 
10
  from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
11
  from contextlib import contextmanager
12
  from pathlib import Path
13
  from typing import Any, overload
14
-
15
- import torch
16
  from torch import Tensor
17
 
18
  from .pooling import Pooler
@@ -35,6 +34,7 @@ from .types import (
35
  LazyTensorReference,
36
  )
37
 
 
38
  _MAX_PARTI_RESIDUES = 2_048
39
  _RUN_FINGERPRINT_SCHEMA_VERSION = 3
40
  _MODEL_STATE_HASH_CHUNK_BYTES = 16 * 1024**2
@@ -45,6 +45,7 @@ _SUPPORTED_STORAGE_FORMATS = frozenset({"safetensors", "sqlite"})
45
  def _validate_parti_length(M: Tensor) -> None:
46
  """Reject an oversized attention graph before model inference."""
47
 
 
48
  n_residues = int(M.to(dtype=torch.int64).sum(dim=1).max().item())
49
  if n_residues > _MAX_PARTI_RESIDUES:
50
  raise ValueError(f"parti supports at most {_MAX_PARTI_RESIDUES:,} biological residues.")
@@ -58,16 +59,17 @@ def select_hidden_state_embeddings(
58
  store_all_hidden_states: bool = False,
59
  ) -> Tensor:
60
  """Select one hidden state or stack every state without changing values."""
 
61
  if store_all_hidden_states:
62
  if not hidden_states:
63
  raise ValueError("store_all_hidden_states requires model hidden states.")
64
  # H has shape (b, n, l, d), where n follows the model's output order.
65
- return torch.stack(hidden_states, dim=1)
66
  if hidden_state_index == -1:
67
- return last_hidden_state
68
  if not hidden_states:
69
  raise ValueError("hidden_state_index requires model hidden states.")
70
- return hidden_states[hidden_state_index]
71
 
72
 
73
  def iter_fasta(path: str | Path) -> Iterator[EmbeddingInput]:
@@ -508,12 +510,17 @@ def _biological_residue_mask(
508
  ) -> Tensor:
509
  """Remove padding and tokenizer-declared special tokens from M."""
510
 
511
- M = attention_mask.to(dtype=torch.bool)
 
512
  special_ids = tuple(int(token_id) for token_id in getattr(tokenizer, "all_special_ids", ()))
513
  if special_ids:
514
- specials = torch.tensor(special_ids, device=input_ids.device, dtype=input_ids.dtype)
515
- M = M & ~torch.isin(input_ids, specials)
516
- return M
 
 
 
 
517
 
518
 
519
  def _generic_embedding_batch(
@@ -535,20 +542,23 @@ def _generic_embedding_batch(
535
  output = model._embed(sequences, return_attention_mask=True, **model_kwargs)
536
  if not isinstance(output, tuple) or len(output) != 2:
537
  raise TypeError("E1 _embed must return (X, residue_mask).")
538
- X, M = output
539
  preparer = getattr(model, "prep_tokens", None)
540
  if preparer is not None and hasattr(preparer, "get_batch_kwargs"):
541
  prepared = preparer.get_batch_kwargs(sequences, device=X.device)
542
- input_ids = prepared["input_ids"]
543
- boundary_ids = preparer.boundary_token_ids.to(
544
  device=input_ids.device, dtype=input_ids.dtype
545
  )
546
  # E1 wraps each raw sequence in BOS, context-label, terminal-label,
547
  # and EOS tokens. Only amino-acid rows are biological residues.
548
- M = M.to(dtype=torch.bool) & ~torch.isin(input_ids, boundary_ids)
549
  if need_attentions:
550
  raise ValueError("parti is not available for tokenizer-free E1 embedding.")
551
- return EmbeddingBatch(X=X, residue_mask=M.to(dtype=torch.bool))
 
 
 
552
  if tokenizer is None:
553
  raise ValueError("A tokenizer is required for this model's embedding path.")
554
 
@@ -572,14 +582,17 @@ def _generic_embedding_batch(
572
  else:
573
  encoded = tokenizer(sequences, **tokenize_kwargs)
574
  device = _model_device(model)
575
- input_ids = encoded["input_ids"].to(device)
576
- attention_mask = encoded.get("attention_mask", input_ids.new_ones(input_ids.shape)).to(device)
577
- M = _biological_residue_mask(input_ids, attention_mask, tokenizer)
 
 
 
578
  if need_attentions:
579
  # Validate l before either the backbone or its quadratic attention graph
580
  # is materialized. M has shape (b, l).
581
  _validate_parti_length(M)
582
- X = model._embed(input_ids, attention_mask, **model_kwargs)
583
  attentions = None
584
  if need_attentions:
585
  output = model(
@@ -588,10 +601,14 @@ def _generic_embedding_batch(
588
  output_attentions=True,
589
  return_dict=True,
590
  )
591
- attentions = getattr(output, "attentions", None)
592
  if attentions is None:
593
  raise ValueError("The model did not return attentions required by parti.")
594
- return EmbeddingBatch(X=X, residue_mask=M, attentions=attentions)
 
 
 
 
595
 
596
 
597
  def _first_metadata_value(*values: Any) -> Any:
@@ -638,6 +655,7 @@ def _model_identity_metadata(model: Any) -> dict[str, Any]:
638
  def _bounded_tensor_chunks(X: Tensor, max_elements: int) -> Iterable[Tensor]:
639
  """Yield X in logical row-major order without materializing a full copy."""
640
 
 
641
  if X.numel() == 0:
642
  return
643
  if X.ndim == 0:
@@ -649,7 +667,7 @@ def _bounded_tensor_chunks(X: Tensor, max_elements: int) -> Iterable[Tensor]:
649
  if trailing_elements <= max_elements:
650
  rows_per_chunk = max(1, max_elements // trailing_elements)
651
  for start in range(0, X.shape[0], rows_per_chunk):
652
- yield X[start : start + rows_per_chunk]
653
  return
654
  for row in X:
655
  yield from _bounded_tensor_chunks(row, max_elements)
@@ -685,7 +703,7 @@ def _model_state_sha256(model: Any) -> str:
685
  digest.update(header)
686
  max_elements = max(1, _MODEL_STATE_HASH_CHUNK_BYTES // value.element_size())
687
  for chunk in _bounded_tensor_chunks(value.detach(), max_elements):
688
- cpu_chunk = chunk.to(device="cpu").contiguous()
689
  digest.update(cpu_chunk.reshape(-1).view(torch.uint8).numpy().tobytes())
690
  return digest.hexdigest()
691
 
@@ -1306,22 +1324,24 @@ def embed_dataset(
1306
  normalized_decoder_inputs[position] for position in batch_positions
1307
  ]
1308
  if decoder_input_ids is not None:
1309
- indices = torch.tensor(
 
1310
  batch_positions,
1311
  device=decoder_input_ids.device,
1312
  dtype=torch.long,
1313
  )
1314
- batch_model_kwargs["decoder_input_ids"] = decoder_input_ids.index_select(
1315
- 0, indices
1316
  )
1317
  if decoder_attention_mask is not None:
1318
- indices = torch.tensor(
 
1319
  batch_positions,
1320
  device=decoder_attention_mask.device,
1321
  dtype=torch.long,
1322
  )
1323
  batch_model_kwargs["decoder_attention_mask"] = (
1324
- decoder_attention_mask.index_select(0, indices)
1325
  )
1326
  custom_batch = _embedding_batch_fn or getattr(model, "_embedding_batch", None)
1327
  if custom_batch is not None:
@@ -1348,8 +1368,8 @@ def embed_dataset(
1348
  need_attentions=need_attentions,
1349
  model_kwargs=batch_model_kwargs,
1350
  )
1351
- X = batch.X
1352
- raw_mask = batch.residue_mask
1353
  if not isinstance(X, Tensor) or not isinstance(raw_mask, Tensor):
1354
  raise TypeError("Embedding batches must provide Tensor X and residue_mask.")
1355
  if X.is_meta or raw_mask.is_meta:
@@ -1360,7 +1380,7 @@ def embed_dataset(
1360
  raise ValueError("Embedding residue_mask must contain finite binary values.")
1361
  if not bool(((raw_mask == 0) | (raw_mask == 1)).all()):
1362
  raise ValueError("Embedding residue_mask must contain finite binary values.")
1363
- M = raw_mask.to(device=X.device, dtype=torch.bool)
1364
  valid_X_shape = (
1365
  X.ndim == 3
1366
  and X.shape[0] == len(batch_records)
@@ -1384,7 +1404,7 @@ def embed_dataset(
1384
  )
1385
  if not bool(M.any(dim=1).all()):
1386
  raise ValueError("Every embedding sample must contain a biological residue.")
1387
- finite_selected = (
1388
  torch.isfinite(X) | ~M.unsqueeze(-1)
1389
  if X.ndim == 3
1390
  else torch.isfinite(X) | ~M[:, None, :, None]
@@ -1395,28 +1415,32 @@ def embed_dataset(
1395
  # Validate the biological graph only after mask integrity is established.
1396
  _validate_parti_length(M)
1397
  if dtype is not None:
1398
- X = X.to(dtype=dtype)
1399
 
1400
  if full_embeddings:
1401
  if X.ndim == 4:
1402
  values = [
1403
- X_i[:, M_i, :].detach().cpu() for X_i, M_i in zip(X, M, strict=True)
 
1404
  ]
1405
  else:
1406
- values = [X_i[M_i].detach().cpu() for X_i, M_i in zip(X, M, strict=True)]
 
 
 
1407
  else:
1408
  if pooler is None:
1409
  raise RuntimeError(
1410
  "Pooled embedding output was requested without an initialized pooler."
1411
  )
1412
- Y = pooler(
1413
  X,
1414
  M,
1415
  attentions=batch.attentions,
1416
  attention_backend=attention_backend,
1417
  )
1418
  pool_slices = pooler.output_slices(X.shape[-1])
1419
- values = list(Y.detach().cpu().unbind(0))
1420
  for position, record, value in zip(
1421
  batch_positions, batch_records, values, strict=True
1422
  ):
 
7
  import platform
8
  import sqlite3
9
  import tempfile
10
+ import torch
11
  from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
12
  from contextlib import contextmanager
13
  from pathlib import Path
14
  from typing import Any, overload
 
 
15
  from torch import Tensor
16
 
17
  from .pooling import Pooler
 
34
  LazyTensorReference,
35
  )
36
 
37
+
38
  _MAX_PARTI_RESIDUES = 2_048
39
  _RUN_FINGERPRINT_SCHEMA_VERSION = 3
40
  _MODEL_STATE_HASH_CHUNK_BYTES = 16 * 1024**2
 
45
  def _validate_parti_length(M: Tensor) -> None:
46
  """Reject an oversized attention graph before model inference."""
47
 
48
+ # M: (b, l)
49
  n_residues = int(M.to(dtype=torch.int64).sum(dim=1).max().item())
50
  if n_residues > _MAX_PARTI_RESIDUES:
51
  raise ValueError(f"parti supports at most {_MAX_PARTI_RESIDUES:,} biological residues.")
 
59
  store_all_hidden_states: bool = False,
60
  ) -> Tensor:
61
  """Select one hidden state or stack every state without changing values."""
62
+ # last_hidden_state and each hidden_states entry: (b, l, d)
63
  if store_all_hidden_states:
64
  if not hidden_states:
65
  raise ValueError("store_all_hidden_states requires model hidden states.")
66
  # H has shape (b, n, l, d), where n follows the model's output order.
67
+ return torch.stack(hidden_states, dim=1) # (b, n, l, d)
68
  if hidden_state_index == -1:
69
+ return last_hidden_state # (b, l, d)
70
  if not hidden_states:
71
  raise ValueError("hidden_state_index requires model hidden states.")
72
+ return hidden_states[hidden_state_index] # (b, l, d)
73
 
74
 
75
  def iter_fasta(path: str | Path) -> Iterator[EmbeddingInput]:
 
510
  ) -> Tensor:
511
  """Remove padding and tokenizer-declared special tokens from M."""
512
 
513
+ # input_ids, attention_mask: (b, l)
514
+ M = attention_mask.to(dtype=torch.bool) # (b, l)
515
  special_ids = tuple(int(token_id) for token_id in getattr(tokenizer, "all_special_ids", ()))
516
  if special_ids:
517
+ specials = torch.tensor( # (n_special,)
518
+ special_ids,
519
+ device=input_ids.device,
520
+ dtype=input_ids.dtype,
521
+ )
522
+ M = M & ~torch.isin(input_ids, specials) # (b, l)
523
+ return M # (b, l)
524
 
525
 
526
  def _generic_embedding_batch(
 
542
  output = model._embed(sequences, return_attention_mask=True, **model_kwargs)
543
  if not isinstance(output, tuple) or len(output) != 2:
544
  raise TypeError("E1 _embed must return (X, residue_mask).")
545
+ X, M = output # (b, l, d), (b, l)
546
  preparer = getattr(model, "prep_tokens", None)
547
  if preparer is not None and hasattr(preparer, "get_batch_kwargs"):
548
  prepared = preparer.get_batch_kwargs(sequences, device=X.device)
549
+ input_ids = prepared["input_ids"] # (b, l)
550
+ boundary_ids = preparer.boundary_token_ids.to( # (n_boundary,)
551
  device=input_ids.device, dtype=input_ids.dtype
552
  )
553
  # E1 wraps each raw sequence in BOS, context-label, terminal-label,
554
  # and EOS tokens. Only amino-acid rows are biological residues.
555
+ M = M.to(dtype=torch.bool) & ~torch.isin(input_ids, boundary_ids) # (b, l)
556
  if need_attentions:
557
  raise ValueError("parti is not available for tokenizer-free E1 embedding.")
558
+ return EmbeddingBatch( # X: (b, l, d); residue_mask: (b, l)
559
+ X=X,
560
+ residue_mask=M.to(dtype=torch.bool),
561
+ )
562
  if tokenizer is None:
563
  raise ValueError("A tokenizer is required for this model's embedding path.")
564
 
 
582
  else:
583
  encoded = tokenizer(sequences, **tokenize_kwargs)
584
  device = _model_device(model)
585
+ input_ids = encoded["input_ids"].to(device) # (b, l)
586
+ attention_mask = encoded.get( # (b, l)
587
+ "attention_mask",
588
+ input_ids.new_ones(input_ids.shape),
589
+ ).to(device)
590
+ M = _biological_residue_mask(input_ids, attention_mask, tokenizer) # (b, l)
591
  if need_attentions:
592
  # Validate l before either the backbone or its quadratic attention graph
593
  # is materialized. M has shape (b, l).
594
  _validate_parti_length(M)
595
+ X = model._embed(input_ids, attention_mask, **model_kwargs) # (b, l, d)
596
  attentions = None
597
  if need_attentions:
598
  output = model(
 
601
  output_attentions=True,
602
  return_dict=True,
603
  )
604
+ attentions = getattr(output, "attentions", None) # each: (b, h, l, l)
605
  if attentions is None:
606
  raise ValueError("The model did not return attentions required by parti.")
607
+ return EmbeddingBatch( # X: (b, l, d); M: (b, l)
608
+ X=X,
609
+ residue_mask=M,
610
+ attentions=attentions,
611
+ )
612
 
613
 
614
  def _first_metadata_value(*values: Any) -> Any:
 
655
  def _bounded_tensor_chunks(X: Tensor, max_elements: int) -> Iterable[Tensor]:
656
  """Yield X in logical row-major order without materializing a full copy."""
657
 
658
+ # X: (...)
659
  if X.numel() == 0:
660
  return
661
  if X.ndim == 0:
 
667
  if trailing_elements <= max_elements:
668
  rows_per_chunk = max(1, max_elements // trailing_elements)
669
  for start in range(0, X.shape[0], rows_per_chunk):
670
+ yield X[start : start + rows_per_chunk] # (chunk_rows, ...)
671
  return
672
  for row in X:
673
  yield from _bounded_tensor_chunks(row, max_elements)
 
703
  digest.update(header)
704
  max_elements = max(1, _MODEL_STATE_HASH_CHUNK_BYTES // value.element_size())
705
  for chunk in _bounded_tensor_chunks(value.detach(), max_elements):
706
+ cpu_chunk = chunk.to(device="cpu").contiguous() # chunk.shape
707
  digest.update(cpu_chunk.reshape(-1).view(torch.uint8).numpy().tobytes())
708
  return digest.hexdigest()
709
 
 
1324
  normalized_decoder_inputs[position] for position in batch_positions
1325
  ]
1326
  if decoder_input_ids is not None:
1327
+ # decoder_input_ids: (n_records, l_decoder)
1328
+ indices = torch.tensor( # (b,)
1329
  batch_positions,
1330
  device=decoder_input_ids.device,
1331
  dtype=torch.long,
1332
  )
1333
+ batch_model_kwargs["decoder_input_ids"] = ( # (b, l_decoder)
1334
+ decoder_input_ids.index_select(0, indices)
1335
  )
1336
  if decoder_attention_mask is not None:
1337
+ # decoder_attention_mask: (n_records, l_decoder)
1338
+ indices = torch.tensor( # (b,)
1339
  batch_positions,
1340
  device=decoder_attention_mask.device,
1341
  dtype=torch.long,
1342
  )
1343
  batch_model_kwargs["decoder_attention_mask"] = (
1344
+ decoder_attention_mask.index_select(0, indices) # (b, l_decoder)
1345
  )
1346
  custom_batch = _embedding_batch_fn or getattr(model, "_embedding_batch", None)
1347
  if custom_batch is not None:
 
1368
  need_attentions=need_attentions,
1369
  model_kwargs=batch_model_kwargs,
1370
  )
1371
+ X = batch.X # (b, l, d) or (b, n_states, l, d)
1372
+ raw_mask = batch.residue_mask # (b, l)
1373
  if not isinstance(X, Tensor) or not isinstance(raw_mask, Tensor):
1374
  raise TypeError("Embedding batches must provide Tensor X and residue_mask.")
1375
  if X.is_meta or raw_mask.is_meta:
 
1380
  raise ValueError("Embedding residue_mask must contain finite binary values.")
1381
  if not bool(((raw_mask == 0) | (raw_mask == 1)).all()):
1382
  raise ValueError("Embedding residue_mask must contain finite binary values.")
1383
+ M = raw_mask.to(device=X.device, dtype=torch.bool) # (b, l)
1384
  valid_X_shape = (
1385
  X.ndim == 3
1386
  and X.shape[0] == len(batch_records)
 
1404
  )
1405
  if not bool(M.any(dim=1).all()):
1406
  raise ValueError("Every embedding sample must contain a biological residue.")
1407
+ finite_selected = ( # X.shape
1408
  torch.isfinite(X) | ~M.unsqueeze(-1)
1409
  if X.ndim == 3
1410
  else torch.isfinite(X) | ~M[:, None, :, None]
 
1415
  # Validate the biological graph only after mask integrity is established.
1416
  _validate_parti_length(M)
1417
  if dtype is not None:
1418
+ X = X.to(dtype=dtype) # unchanged shape
1419
 
1420
  if full_embeddings:
1421
  if X.ndim == 4:
1422
  values = [
1423
+ X_i[:, M_i, :].detach().cpu() # (n_states, r_i, d)
1424
+ for X_i, M_i in zip(X, M, strict=True)
1425
  ]
1426
  else:
1427
+ values = [
1428
+ X_i[M_i].detach().cpu() # (r_i, d)
1429
+ for X_i, M_i in zip(X, M, strict=True)
1430
+ ]
1431
  else:
1432
  if pooler is None:
1433
  raise RuntimeError(
1434
  "Pooled embedding output was requested without an initialized pooler."
1435
  )
1436
+ Y = pooler( # (b, n_poolers * d)
1437
  X,
1438
  M,
1439
  attentions=batch.attentions,
1440
  attention_backend=attention_backend,
1441
  )
1442
  pool_slices = pooler.output_slices(X.shape[-1])
1443
+ values = list(Y.detach().cpu().unbind(0)) # each: (n_poolers * d,)
1444
  for position, record, value in zip(
1445
  batch_positions, batch_records, values, strict=True
1446
  ):
fastplms/embeddings/storage.py CHANGED
@@ -7,14 +7,13 @@ import io
7
  import json
8
  import sqlite3
9
  import struct
 
 
10
  from bisect import bisect_right
11
  from collections.abc import Iterable, Iterator, Sequence
12
  from pathlib import Path
13
  from typing import Any, cast, overload
14
  from uuid import uuid4
15
-
16
- import numpy as np
17
- import torch
18
  from torch import Tensor
19
 
20
  from .types import (
@@ -23,6 +22,7 @@ from .types import (
23
  LazyTensorReference,
24
  )
25
 
 
26
  _DTYPE_NAMES: dict[torch.dtype, str] = {
27
  torch.float16: "float16",
28
  torch.bfloat16: "bfloat16",
@@ -80,22 +80,24 @@ def _persistent_metadata(
80
  def _tensor_bytes(X: Tensor) -> bytes:
81
  """Return the exact contiguous byte representation of X."""
82
 
83
- X = X.detach().cpu().contiguous()
 
84
  return X.view(torch.uint8).numpy().tobytes()
85
 
86
 
87
  def _bounded_tensor_chunks(X: Tensor, max_bytes: int) -> Iterator[Tensor]:
88
  """Yield row-major CPU chunks without materializing one full byte string."""
89
 
90
- flattened = X.detach().to(device="cpu").reshape(-1)
 
91
  if flattened.numel() == 0:
92
  return
93
  chunk_elements = max(1, max_bytes // flattened.element_size())
94
  for start in range(0, flattened.numel(), chunk_elements):
95
- chunk = flattened[start : start + chunk_elements]
96
  if chunk.stride(0) != 1:
97
- chunk = chunk.clone(memory_format=torch.contiguous_format)
98
- yield chunk
99
 
100
 
101
  def _tensor_hash_chunks(X: Tensor) -> Iterator[bytes]:
@@ -136,9 +138,9 @@ def _decode_tensor(dtype_name: str, shape_json: str, data: bytes) -> Tensor:
136
  raise ValueError(f"Unsupported stored dtype {dtype_name!r}.") from error
137
  shape = tuple(json.loads(shape_json))
138
  # uint8 is used only as a byte-level carrier, preserving BF16 bits exactly.
139
- byte_array = np.frombuffer(data, dtype=np.uint8).copy()
140
- X = torch.from_numpy(byte_array).view(dtype)
141
- return X.reshape(shape).clone()
142
 
143
 
144
  def _index_path(path: str | Path) -> Path:
@@ -651,7 +653,7 @@ class SafetensorsStreamWriter:
651
 
652
  for record in records:
653
  position = self._record_count + len(self._pending)
654
- tensor = record.load_tensor().detach().cpu().contiguous()
655
  if tensor.dtype not in _DTYPE_NAMES:
656
  raise TypeError(f"Unsupported tensor dtype {tensor.dtype}.")
657
  nbytes = tensor.numel() * tensor.element_size()
@@ -949,7 +951,7 @@ def save_sqlite_result(result: EmbeddingResult, path: str | Path) -> EmbeddingRe
949
  (run_id, metadata_json),
950
  )
951
  for position, record in enumerate(result):
952
- X = record.load_tensor().detach().cpu().contiguous()
953
  dtype_name, shape_json, data = _encode_tensor(X)
954
  digest = tensor_sha256(X)
955
  connection.execute(
@@ -1053,7 +1055,7 @@ def append_sqlite_records(
1053
  )
1054
  for offset, record in enumerate(records):
1055
  position = start_position + offset
1056
- X = record.load_tensor().detach().cpu().contiguous()
1057
  dtype_name, shape_json, data = _encode_tensor(X)
1058
  digest = tensor_sha256(X)
1059
  connection.execute(
@@ -1414,6 +1416,7 @@ def load_legacy_pth(path: str | Path, *, allow_unsafe_pickle: bool = False) -> E
1414
  raise ValueError("A legacy .pth embedding file must contain a mapping.")
1415
  records: list[EmbeddingRecord] = []
1416
  for position, (sequence, X) in enumerate(payload.items()):
 
1417
  if not isinstance(sequence, str) or not isinstance(X, Tensor):
1418
  raise ValueError("Legacy embedding mappings must use str keys and Tensor values.")
1419
  records.append(EmbeddingRecord(str(position), sequence, X.detach().cpu()))
@@ -1450,8 +1453,10 @@ def _decode_legacy_sqlite_blob(
1450
  expected = int(np.prod(shape, dtype=np.int64)) * numpy_dtype.itemsize
1451
  if len(data) - offset != expected:
1452
  raise ValueError("Legacy compact embedding payload length does not match shape.")
1453
- array = np.frombuffer(data, dtype=numpy_dtype, offset=offset).copy().reshape(shape)
1454
- return torch.from_numpy(array).to(dtype=target_dtype)
 
 
1455
 
1456
  try:
1457
  loaded = torch.load(io.BytesIO(data), map_location="cpu", weights_only=True)
@@ -1470,11 +1475,13 @@ def _decode_legacy_sqlite_blob(
1470
  raise ValueError(
1471
  "Legacy raw FP32 payload length does not match fallback_shape."
1472
  ) from safe_error
1473
- array = np.frombuffer(data, dtype=np.float32).copy().reshape(fallback_shape)
1474
- return torch.from_numpy(array)
 
 
1475
  if not isinstance(loaded, Tensor):
1476
  raise ValueError("Legacy serialized embedding payload must contain one tensor.")
1477
- return loaded.detach().cpu()
1478
 
1479
 
1480
  def convert_legacy_sqlite(
 
7
  import json
8
  import sqlite3
9
  import struct
10
+ import numpy as np
11
+ import torch
12
  from bisect import bisect_right
13
  from collections.abc import Iterable, Iterator, Sequence
14
  from pathlib import Path
15
  from typing import Any, cast, overload
16
  from uuid import uuid4
 
 
 
17
  from torch import Tensor
18
 
19
  from .types import (
 
22
  LazyTensorReference,
23
  )
24
 
25
+
26
  _DTYPE_NAMES: dict[torch.dtype, str] = {
27
  torch.float16: "float16",
28
  torch.bfloat16: "bfloat16",
 
80
  def _tensor_bytes(X: Tensor) -> bytes:
81
  """Return the exact contiguous byte representation of X."""
82
 
83
+ # X: (...)
84
+ X = X.detach().cpu().contiguous() # (...)
85
  return X.view(torch.uint8).numpy().tobytes()
86
 
87
 
88
  def _bounded_tensor_chunks(X: Tensor, max_bytes: int) -> Iterator[Tensor]:
89
  """Yield row-major CPU chunks without materializing one full byte string."""
90
 
91
+ # X: (...)
92
+ flattened = X.detach().to(device="cpu").reshape(-1) # (n,)
93
  if flattened.numel() == 0:
94
  return
95
  chunk_elements = max(1, max_bytes // flattened.element_size())
96
  for start in range(0, flattened.numel(), chunk_elements):
97
+ chunk = flattened[start : start + chunk_elements] # (n_chunk,)
98
  if chunk.stride(0) != 1:
99
+ chunk = chunk.clone(memory_format=torch.contiguous_format) # (n_chunk,)
100
+ yield chunk # (n_chunk,)
101
 
102
 
103
  def _tensor_hash_chunks(X: Tensor) -> Iterator[bytes]:
 
138
  raise ValueError(f"Unsupported stored dtype {dtype_name!r}.") from error
139
  shape = tuple(json.loads(shape_json))
140
  # uint8 is used only as a byte-level carrier, preserving BF16 bits exactly.
141
+ byte_array = np.frombuffer(data, dtype=np.uint8).copy() # (n_bytes,)
142
+ X = torch.from_numpy(byte_array).view(dtype) # (n_elements,)
143
+ return X.reshape(shape).clone() # shape
144
 
145
 
146
  def _index_path(path: str | Path) -> Path:
 
653
 
654
  for record in records:
655
  position = self._record_count + len(self._pending)
656
+ tensor = record.load_tensor().detach().cpu().contiguous() # (...)
657
  if tensor.dtype not in _DTYPE_NAMES:
658
  raise TypeError(f"Unsupported tensor dtype {tensor.dtype}.")
659
  nbytes = tensor.numel() * tensor.element_size()
 
951
  (run_id, metadata_json),
952
  )
953
  for position, record in enumerate(result):
954
+ X = record.load_tensor().detach().cpu().contiguous() # (...)
955
  dtype_name, shape_json, data = _encode_tensor(X)
956
  digest = tensor_sha256(X)
957
  connection.execute(
 
1055
  )
1056
  for offset, record in enumerate(records):
1057
  position = start_position + offset
1058
+ X = record.load_tensor().detach().cpu().contiguous() # (...)
1059
  dtype_name, shape_json, data = _encode_tensor(X)
1060
  digest = tensor_sha256(X)
1061
  connection.execute(
 
1416
  raise ValueError("A legacy .pth embedding file must contain a mapping.")
1417
  records: list[EmbeddingRecord] = []
1418
  for position, (sequence, X) in enumerate(payload.items()):
1419
+ # X: (...)
1420
  if not isinstance(sequence, str) or not isinstance(X, Tensor):
1421
  raise ValueError("Legacy embedding mappings must use str keys and Tensor values.")
1422
  records.append(EmbeddingRecord(str(position), sequence, X.detach().cpu()))
 
1453
  expected = int(np.prod(shape, dtype=np.int64)) * numpy_dtype.itemsize
1454
  if len(data) - offset != expected:
1455
  raise ValueError("Legacy compact embedding payload length does not match shape.")
1456
+ array = ( # shape
1457
+ np.frombuffer(data, dtype=numpy_dtype, offset=offset).copy().reshape(shape)
1458
+ )
1459
+ return torch.from_numpy(array).to(dtype=target_dtype) # shape
1460
 
1461
  try:
1462
  loaded = torch.load(io.BytesIO(data), map_location="cpu", weights_only=True)
 
1475
  raise ValueError(
1476
  "Legacy raw FP32 payload length does not match fallback_shape."
1477
  ) from safe_error
1478
+ array = np.frombuffer(data, dtype=np.float32).copy().reshape( # fallback_shape
1479
+ fallback_shape
1480
+ )
1481
+ return torch.from_numpy(array) # fallback_shape
1482
  if not isinstance(loaded, Tensor):
1483
  raise ValueError("Legacy serialized embedding payload must contain one tensor.")
1484
+ return loaded.detach().cpu() # (...)
1485
 
1486
 
1487
  def convert_legacy_sqlite(
fastplms/embeddings/types.py CHANGED
@@ -5,7 +5,6 @@ from __future__ import annotations
5
  from collections.abc import Callable, Iterator, Mapping, Sequence
6
  from dataclasses import dataclass, field
7
  from typing import Any, Literal, overload
8
-
9
  from torch import Tensor
10
 
11
 
@@ -39,7 +38,7 @@ class LazyTensorReference:
39
 
40
  if not isinstance(verify, bool):
41
  raise TypeError("verify must be a boolean.")
42
- X = self._loader()
43
  if not isinstance(X, Tensor):
44
  raise TypeError(f"Stored tensor loader for {self.key!r} must return a Tensor.")
45
  if tuple(X.shape) != self.shape:
@@ -57,7 +56,7 @@ class LazyTensorReference:
57
  digest = tensor_sha256(X)
58
  if digest != self.sha256:
59
  raise ValueError(f"Stored tensor {self.key!r} failed SHA-256 verification.")
60
- return X
61
 
62
 
63
  TensorValue = Tensor | LazyTensorReference
@@ -85,8 +84,8 @@ class EmbeddingRecord:
85
  if not isinstance(verify, bool):
86
  raise TypeError("verify must be a boolean.")
87
  if isinstance(self.tensor, LazyTensorReference):
88
- return self.tensor.load(verify=verify)
89
- return self.tensor
90
 
91
 
92
  class EmbeddingResult(Sequence[EmbeddingRecord]):
 
5
  from collections.abc import Callable, Iterator, Mapping, Sequence
6
  from dataclasses import dataclass, field
7
  from typing import Any, Literal, overload
 
8
  from torch import Tensor
9
 
10
 
 
38
 
39
  if not isinstance(verify, bool):
40
  raise TypeError("verify must be a boolean.")
41
+ X = self._loader() # self.shape
42
  if not isinstance(X, Tensor):
43
  raise TypeError(f"Stored tensor loader for {self.key!r} must return a Tensor.")
44
  if tuple(X.shape) != self.shape:
 
56
  digest = tensor_sha256(X)
57
  if digest != self.sha256:
58
  raise ValueError(f"Stored tensor {self.key!r} failed SHA-256 verification.")
59
+ return X # self.shape
60
 
61
 
62
  TensorValue = Tensor | LazyTensorReference
 
84
  if not isinstance(verify, bool):
85
  raise TypeError("verify must be a boolean.")
86
  if isinstance(self.tensor, LazyTensorReference):
87
+ return self.tensor.load(verify=verify) # (...)
88
+ return self.tensor # (...)
89
 
90
 
91
  class EmbeddingResult(Sequence[EmbeddingRecord]):
fastplms/models/__init__.py CHANGED
@@ -7,4 +7,5 @@ tokenizers, compile kernels, or initialize an accelerator runtime.
7
 
8
  from __future__ import annotations
9
 
 
10
  __all__: tuple[str, ...] = ()
 
7
 
8
  from __future__ import annotations
9
 
10
+
11
  __all__: tuple[str, ...] = ()
fastplms/models/ankh/modeling_ankh.py CHANGED
@@ -1,13 +1,12 @@
1
  from __future__ import annotations
2
 
3
  import math
 
 
4
  from collections.abc import Mapping, Sequence
5
  from dataclasses import dataclass
6
  from numbers import Real
7
  from typing import Any, ClassVar
8
-
9
- import torch
10
- import torch.nn as nn
11
  from tokenizers import pre_tokenizers
12
  from torch.nn import functional as F
13
  from transformers import (
@@ -23,6 +22,7 @@ from transformers.modeling_outputs import (
23
  TokenClassifierOutput,
24
  )
25
 
 
26
  try:
27
  from fastplms.attention import (
28
  AttentionBackend,
@@ -405,13 +405,14 @@ def _biological_token_mask(
405
  class AnkhRMSNorm(nn.Module):
406
  """T5-style RMS layer norm: scales without mean subtraction or bias."""
407
 
408
- def __init__(self, hidden_size: int, eps: float = 1e-6):
409
  super().__init__()
410
  self.weight = nn.Parameter(torch.ones(hidden_size))
411
  self.variance_epsilon = eps
412
 
413
  def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
414
- variance = hidden_states.to(torch.float32).pow(2).mean(-1, keepdim=True)
 
415
  hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
416
  return self.weight * hidden_states.to(self.weight.dtype)
417
 
@@ -425,7 +426,7 @@ def _gelu_new(x: torch.Tensor) -> torch.Tensor:
425
  class AnkhGatedFFN(nn.Module):
426
  """T5-style gated feed-forward: activation(wi_0(x)) * wi_1(x) -> wo."""
427
 
428
- def __init__(self, config: FastAnkhConfig):
429
  super().__init__()
430
  self.wi_0 = nn.Linear(config.d_model, config.d_ff, bias=False)
431
  self.wi_1 = nn.Linear(config.d_model, config.d_ff, bias=False)
@@ -434,6 +435,7 @@ class AnkhGatedFFN(nn.Module):
434
  self.dropout = nn.Dropout(config.dropout_rate)
435
 
436
  def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
 
437
  hidden_states = self.act(self.wi_0(hidden_states)) * self.wi_1(hidden_states)
438
  return self.wo(self.dropout(hidden_states))
439
 
@@ -451,7 +453,11 @@ class AnkhSelfAttention(nn.Module):
451
  receive the precomputed bias through the forward call.
452
  """
453
 
454
- def __init__(self, config: FastAnkhConfig, has_relative_attention_bias: bool = False):
 
 
 
 
455
  super().__init__()
456
  self.num_heads = config.num_heads
457
  self.d_kv = config.d_kv
@@ -515,8 +521,8 @@ class AnkhSelfAttention(nn.Module):
515
  num_buckets=self.relative_attention_num_buckets,
516
  max_distance=self.relative_attention_max_distance,
517
  )
518
- values = self.relative_attention_bias(buckets) # A has shape (q, k, h).
519
- return values.permute(2, 0, 1).unsqueeze(0) # A has shape (1, h, q, k).
520
 
521
  # ---- Forward ----
522
 
@@ -529,12 +535,13 @@ class AnkhSelfAttention(nn.Module):
529
  effective_backend: AttentionBackend | None = None,
530
  ) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]:
531
  """Returns (attn_output, attn_weights_or_none, position_bias)."""
 
532
  batch_size, seq_length = hidden_states.shape[:2]
533
  hidden_shape = (batch_size, seq_length, self.num_heads, self.d_kv)
534
 
535
- query_heads = self.q(hidden_states).view(hidden_shape).transpose(1, 2)
536
- key_heads = self.k(hidden_states).view(hidden_shape).transpose(1, 2)
537
- value_heads = self.v(hidden_states).view(hidden_shape).transpose(1, 2)
538
 
539
  # The first layer computes the bias once; later layers reuse it.
540
  if position_bias is None and self.has_relative_attention_bias:
@@ -572,7 +579,7 @@ class AnkhSelfAttention(nn.Module):
572
  value_heads: torch.Tensor,
573
  position_bias: torch.Tensor | None,
574
  ) -> torch.Tensor:
575
- # A is the additive position bias with shape (1, h, q, k), including padding.
576
  # Never mutate torch.backends.cuda process-global reduction policy from
577
  # a model forward. Concurrent model requests must not change each
578
  # other's numerical behavior or restore a stale process setting.
@@ -583,7 +590,7 @@ class AnkhSelfAttention(nn.Module):
583
  attn_mask=position_bias,
584
  dropout_p=self.dropout_prob if self.training else 0.0,
585
  scale=self.scale,
586
- )
587
  return (
588
  context_heads.transpose(1, 2)
589
  .contiguous()
@@ -597,7 +604,10 @@ class AnkhSelfAttention(nn.Module):
597
  value_heads: torch.Tensor,
598
  position_bias: torch.Tensor | None,
599
  ) -> tuple[torch.Tensor, torch.Tensor]:
600
- attn_weights = torch.matmul(query_heads, key_heads.transpose(-1, -2)) * self.scale
 
 
 
601
  if position_bias is not None:
602
  attn_weights = attn_weights + position_bias
603
  attn_weights = F.softmax(attn_weights.float(), dim=-1).type_as(attn_weights)
@@ -607,7 +617,7 @@ class AnkhSelfAttention(nn.Module):
607
  p=self.dropout_prob,
608
  training=self.training,
609
  )
610
- context_heads = torch.matmul(attn_weights, value_heads)
611
  attn_output = (
612
  context_heads.transpose(1, 2)
613
  .contiguous()
@@ -624,7 +634,11 @@ class AnkhSelfAttention(nn.Module):
624
  class AnkhSelfAttentionLayer(nn.Module):
625
  """Wraps AnkhSelfAttention + layer_norm to match T5Block.layer[0] key naming."""
626
 
627
- def __init__(self, config: FastAnkhConfig, has_relative_attention_bias: bool = False):
 
 
 
 
628
  super().__init__()
629
  self.SelfAttention = AnkhSelfAttention(config, has_relative_attention_bias)
630
  self.layer_norm = AnkhRMSNorm(config.d_model, eps=config.layer_norm_epsilon)
@@ -653,7 +667,7 @@ class AnkhSelfAttentionLayer(nn.Module):
653
  class AnkhFFLayer(nn.Module):
654
  """Wraps AnkhGatedFFN + layer_norm to match T5Block.layer[1] key naming."""
655
 
656
- def __init__(self, config: FastAnkhConfig):
657
  super().__init__()
658
  self.DenseReluDense = AnkhGatedFFN(config)
659
  self.layer_norm = AnkhRMSNorm(config.d_model, eps=config.layer_norm_epsilon)
@@ -668,7 +682,11 @@ class AnkhFFLayer(nn.Module):
668
  class AnkhBlock(nn.Module):
669
  """Single transformer block with T5-compatible .layer ModuleList naming."""
670
 
671
- def __init__(self, config: FastAnkhConfig, has_relative_attention_bias: bool = False):
 
 
 
 
672
  super().__init__()
673
  self.layer = nn.ModuleList(
674
  [
@@ -782,7 +800,7 @@ class FAST_ANKH_ENCODER(AnkhPreTrainedModel, EmbeddingMixin):
782
  block.{i}.layer.1.DenseReluDense.*, final_layer_norm.*.
783
  """
784
 
785
- def __init__(self, config: FastAnkhConfig, **kwargs):
786
  AnkhPreTrainedModel.__init__(self, config, **kwargs)
787
  self.config = config
788
 
@@ -965,7 +983,7 @@ class FastAnkhModel(AnkhPreTrainedModel, EmbeddingMixin):
965
  r"^lm_head\.",
966
  ]
967
 
968
- def __init__(self, config: FastAnkhConfig, **kwargs):
969
  AnkhPreTrainedModel.__init__(self, config, **kwargs)
970
  self.config = config
971
  self.shared = nn.Embedding(config.vocab_size, config.d_model)
@@ -1029,7 +1047,7 @@ class FastAnkhForMaskedLMExtension(
1029
  _tied_weights_keys: ClassVar[dict[str, str]] = {"encoder.embed_tokens.weight": "shared.weight"}
1030
  _keys_to_ignore_on_load_unexpected: ClassVar[list[str]] = [r"^decoder\."]
1031
 
1032
- def __init__(self, config: FastAnkhConfig, **kwargs):
1033
  # The historical Synthyra extension stores an independent output head.
1034
  config.tie_word_embeddings = False
1035
  AnkhPreTrainedModel.__init__(self, config, **kwargs)
@@ -1156,7 +1174,7 @@ class FastAnkhForConditionalGeneration(
1156
  embedding_unsupported_pooling = ("cls",)
1157
  _fastplms_attention_implementations = ("eager",)
1158
 
1159
- def __init__(self, config: FastAnkhConfig, **kwargs):
1160
  requested_backend = getattr(config, "_attn_implementation", None) or config.attn_backend
1161
  if requested_backend not in (None, "eager"):
1162
  raise ValueError(
@@ -1448,7 +1466,7 @@ class FastAnkhForSequenceClassification(AnkhPreTrainedModel, EmbeddingMixin):
1448
  r"^lm_head\.",
1449
  ]
1450
 
1451
- def __init__(self, config: FastAnkhConfig, **kwargs):
1452
  AnkhPreTrainedModel.__init__(self, config, **kwargs)
1453
  self.num_labels = config.num_labels
1454
  self.config = config
@@ -1555,7 +1573,7 @@ class FastAnkhForTokenClassification(AnkhPreTrainedModel, EmbeddingMixin):
1555
  r"^lm_head\.",
1556
  ]
1557
 
1558
- def __init__(self, config: FastAnkhConfig, **kwargs):
1559
  AnkhPreTrainedModel.__init__(self, config, **kwargs)
1560
  self.num_labels = config.num_labels
1561
  self.shared = nn.Embedding(config.vocab_size, config.d_model)
 
1
  from __future__ import annotations
2
 
3
  import math
4
+ import torch
5
+ import torch.nn as nn
6
  from collections.abc import Mapping, Sequence
7
  from dataclasses import dataclass
8
  from numbers import Real
9
  from typing import Any, ClassVar
 
 
 
10
  from tokenizers import pre_tokenizers
11
  from torch.nn import functional as F
12
  from transformers import (
 
22
  TokenClassifierOutput,
23
  )
24
 
25
+
26
  try:
27
  from fastplms.attention import (
28
  AttentionBackend,
 
405
  class AnkhRMSNorm(nn.Module):
406
  """T5-style RMS layer norm: scales without mean subtraction or bias."""
407
 
408
+ def __init__(self, hidden_size: int, eps: float = 1e-6) -> None:
409
  super().__init__()
410
  self.weight = nn.Parameter(torch.ones(hidden_size))
411
  self.variance_epsilon = eps
412
 
413
  def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
414
+ # hidden_states: (..., d)
415
+ variance = hidden_states.to(torch.float32).pow(2).mean(-1, keepdim=True) # (..., 1)
416
  hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
417
  return self.weight * hidden_states.to(self.weight.dtype)
418
 
 
426
  class AnkhGatedFFN(nn.Module):
427
  """T5-style gated feed-forward: activation(wi_0(x)) * wi_1(x) -> wo."""
428
 
429
+ def __init__(self, config: FastAnkhConfig) -> None:
430
  super().__init__()
431
  self.wi_0 = nn.Linear(config.d_model, config.d_ff, bias=False)
432
  self.wi_1 = nn.Linear(config.d_model, config.d_ff, bias=False)
 
435
  self.dropout = nn.Dropout(config.dropout_rate)
436
 
437
  def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
438
+ # hidden_states: (b, l, d)
439
  hidden_states = self.act(self.wi_0(hidden_states)) * self.wi_1(hidden_states)
440
  return self.wo(self.dropout(hidden_states))
441
 
 
453
  receive the precomputed bias through the forward call.
454
  """
455
 
456
+ def __init__(
457
+ self,
458
+ config: FastAnkhConfig,
459
+ has_relative_attention_bias: bool = False,
460
+ ) -> None:
461
  super().__init__()
462
  self.num_heads = config.num_heads
463
  self.d_kv = config.d_kv
 
521
  num_buckets=self.relative_attention_num_buckets,
522
  max_distance=self.relative_attention_max_distance,
523
  )
524
+ values = self.relative_attention_bias(buckets) # (q, k, h)
525
+ return values.permute(2, 0, 1).unsqueeze(0) # (1, h, q, k)
526
 
527
  # ---- Forward ----
528
 
 
535
  effective_backend: AttentionBackend | None = None,
536
  ) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]:
537
  """Returns (attn_output, attn_weights_or_none, position_bias)."""
538
+ # hidden_states: (b, l, d)
539
  batch_size, seq_length = hidden_states.shape[:2]
540
  hidden_shape = (batch_size, seq_length, self.num_heads, self.d_kv)
541
 
542
+ query_heads = self.q(hidden_states).view(hidden_shape).transpose(1, 2) # (b, h, l, d_h)
543
+ key_heads = self.k(hidden_states).view(hidden_shape).transpose(1, 2) # (b, h, l, d_h)
544
+ value_heads = self.v(hidden_states).view(hidden_shape).transpose(1, 2) # (b, h, l, d_h)
545
 
546
  # The first layer computes the bias once; later layers reuse it.
547
  if position_bias is None and self.has_relative_attention_bias:
 
579
  value_heads: torch.Tensor,
580
  position_bias: torch.Tensor | None,
581
  ) -> torch.Tensor:
582
+ # position_bias: (1, h, l, l), including padding
583
  # Never mutate torch.backends.cuda process-global reduction policy from
584
  # a model forward. Concurrent model requests must not change each
585
  # other's numerical behavior or restore a stale process setting.
 
590
  attn_mask=position_bias,
591
  dropout_p=self.dropout_prob if self.training else 0.0,
592
  scale=self.scale,
593
+ ) # (b, h, l, d_h)
594
  return (
595
  context_heads.transpose(1, 2)
596
  .contiguous()
 
604
  value_heads: torch.Tensor,
605
  position_bias: torch.Tensor | None,
606
  ) -> tuple[torch.Tensor, torch.Tensor]:
607
+ # query_heads, key_heads, value_heads: (b, h, l, d_h)
608
+ attn_weights = (
609
+ torch.matmul(query_heads, key_heads.transpose(-1, -2)) * self.scale
610
+ ) # (b, h, l, l)
611
  if position_bias is not None:
612
  attn_weights = attn_weights + position_bias
613
  attn_weights = F.softmax(attn_weights.float(), dim=-1).type_as(attn_weights)
 
617
  p=self.dropout_prob,
618
  training=self.training,
619
  )
620
+ context_heads = torch.matmul(attn_weights, value_heads) # (b, h, l, d_h)
621
  attn_output = (
622
  context_heads.transpose(1, 2)
623
  .contiguous()
 
634
  class AnkhSelfAttentionLayer(nn.Module):
635
  """Wraps AnkhSelfAttention + layer_norm to match T5Block.layer[0] key naming."""
636
 
637
+ def __init__(
638
+ self,
639
+ config: FastAnkhConfig,
640
+ has_relative_attention_bias: bool = False,
641
+ ) -> None:
642
  super().__init__()
643
  self.SelfAttention = AnkhSelfAttention(config, has_relative_attention_bias)
644
  self.layer_norm = AnkhRMSNorm(config.d_model, eps=config.layer_norm_epsilon)
 
667
  class AnkhFFLayer(nn.Module):
668
  """Wraps AnkhGatedFFN + layer_norm to match T5Block.layer[1] key naming."""
669
 
670
+ def __init__(self, config: FastAnkhConfig) -> None:
671
  super().__init__()
672
  self.DenseReluDense = AnkhGatedFFN(config)
673
  self.layer_norm = AnkhRMSNorm(config.d_model, eps=config.layer_norm_epsilon)
 
682
  class AnkhBlock(nn.Module):
683
  """Single transformer block with T5-compatible .layer ModuleList naming."""
684
 
685
+ def __init__(
686
+ self,
687
+ config: FastAnkhConfig,
688
+ has_relative_attention_bias: bool = False,
689
+ ) -> None:
690
  super().__init__()
691
  self.layer = nn.ModuleList(
692
  [
 
800
  block.{i}.layer.1.DenseReluDense.*, final_layer_norm.*.
801
  """
802
 
803
+ def __init__(self, config: FastAnkhConfig, **kwargs) -> None:
804
  AnkhPreTrainedModel.__init__(self, config, **kwargs)
805
  self.config = config
806
 
 
983
  r"^lm_head\.",
984
  ]
985
 
986
+ def __init__(self, config: FastAnkhConfig, **kwargs) -> None:
987
  AnkhPreTrainedModel.__init__(self, config, **kwargs)
988
  self.config = config
989
  self.shared = nn.Embedding(config.vocab_size, config.d_model)
 
1047
  _tied_weights_keys: ClassVar[dict[str, str]] = {"encoder.embed_tokens.weight": "shared.weight"}
1048
  _keys_to_ignore_on_load_unexpected: ClassVar[list[str]] = [r"^decoder\."]
1049
 
1050
+ def __init__(self, config: FastAnkhConfig, **kwargs) -> None:
1051
  # The historical Synthyra extension stores an independent output head.
1052
  config.tie_word_embeddings = False
1053
  AnkhPreTrainedModel.__init__(self, config, **kwargs)
 
1174
  embedding_unsupported_pooling = ("cls",)
1175
  _fastplms_attention_implementations = ("eager",)
1176
 
1177
+ def __init__(self, config: FastAnkhConfig, **kwargs) -> None:
1178
  requested_backend = getattr(config, "_attn_implementation", None) or config.attn_backend
1179
  if requested_backend not in (None, "eager"):
1180
  raise ValueError(
 
1466
  r"^lm_head\.",
1467
  ]
1468
 
1469
+ def __init__(self, config: FastAnkhConfig, **kwargs) -> None:
1470
  AnkhPreTrainedModel.__init__(self, config, **kwargs)
1471
  self.num_labels = config.num_labels
1472
  self.config = config
 
1573
  r"^lm_head\.",
1574
  ]
1575
 
1576
+ def __init__(self, config: FastAnkhConfig, **kwargs) -> None:
1577
  AnkhPreTrainedModel.__init__(self, config, **kwargs)
1578
  self.num_labels = config.num_labels
1579
  self.shared = nn.Embedding(config.vocab_size, config.d_model)
fastplms/models/ttt.py CHANGED
@@ -3,12 +3,13 @@ from __future__ import annotations
3
  import contextlib
4
  import math
5
  import numbers
6
- import typing as T
7
- from dataclasses import asdict, dataclass, fields
8
-
9
  import torch
10
  import torch.nn as nn
11
  import torch.nn.functional as F
 
 
 
 
12
 
13
  _STANDARD_AMINO_ACIDS = "ACDEFGHIKLMNPQRSTVWY"
14
  _TTT_SERIALIZATION_VERSION = 1
@@ -42,7 +43,7 @@ class TTTConfig:
42
  self.verify()
43
 
44
  @classmethod
45
- def from_kwargs(cls, **kwargs: T.Any) -> TTTConfig:
46
  valid_names = {field.name for field in fields(cls)}
47
  unknown_names = set(kwargs) - valid_names
48
  if unknown_names:
@@ -53,7 +54,7 @@ class TTTConfig:
53
  kwargs["lora_target_modules"] = tuple(kwargs["lora_target_modules"])
54
  return cls(**kwargs)
55
 
56
- def merged(self, overrides: T.Mapping[str, T.Any] | TTTConfig | None) -> TTTConfig:
57
  if overrides is None:
58
  return self
59
  if isinstance(overrides, TTTConfig):
@@ -65,7 +66,7 @@ class TTTConfig:
65
  values[name] = value
66
  return TTTConfig(**values)
67
 
68
- def to_dict(self) -> dict[str, T.Any]:
69
  return asdict(self)
70
 
71
  def verify(self) -> None:
@@ -224,9 +225,12 @@ class LoraInjectedLinear(nn.Module):
224
  return self.linear._parameters["bias"]
225
 
226
  def forward(self, x: torch.Tensor) -> torch.Tensor:
227
- base = self.linear(x)
228
- delta = self.lora_up(self.lora_down(x.to(dtype=torch.float32))) * self.scale
229
- return base + delta.to(dtype=base.dtype)
 
 
 
230
 
231
  def reset_lora_parameters(self) -> None:
232
  with torch.no_grad():
@@ -235,7 +239,7 @@ class LoraInjectedLinear(nn.Module):
235
 
236
 
237
  class FastPLMTestTimeTrainingMixin:
238
- def init_ttt(self, ttt_config: TTTConfig | T.Mapping[str, T.Any] | None = None) -> None:
239
  base_config = self.__dict__.get("_ttt_cfg")
240
  if base_config is None:
241
  base_config = TTTConfig()
@@ -245,7 +249,7 @@ class FastPLMTestTimeTrainingMixin:
245
  serialized = getattr(getattr(self, "config", None), "fastplms_ttt", None)
246
  serialized_initialized = False
247
  if serialized is not None:
248
- if not isinstance(serialized, T.Mapping):
249
  raise ValueError("config.fastplms_ttt must be a mapping.")
250
  version = serialized.get("version")
251
  if version != _TTT_SERIALIZATION_VERSION:
@@ -254,7 +258,7 @@ class FastPLMTestTimeTrainingMixin:
254
  f"{version!r}; expected {_TTT_SERIALIZATION_VERSION}."
255
  )
256
  serialized_config = serialized.get("config")
257
- if not isinstance(serialized_config, T.Mapping):
258
  raise ValueError("Serialized FastPLMs TTT state is missing its config mapping.")
259
  configured = TTTConfig.from_kwargs(**dict(serialized_config))
260
  initialized_value = serialized.get("initialized", False)
@@ -285,15 +289,15 @@ class FastPLMTestTimeTrainingMixin:
285
  self,
286
  seq: str | list[str] | None = None,
287
  input_ids: torch.Tensor | None = None,
288
- **kwargs: T.Any,
289
  ) -> torch.Tensor | dict[str, torch.Tensor]:
290
  del kwargs
291
  if input_ids is not None:
292
- return input_ids
293
  if seq is None:
294
  raise ValueError("Pass either seq or input_ids for TTT.")
295
  tokenized = self.tokenizer(seq, return_tensors="pt", padding=True)
296
- return tokenized["input_ids"]
297
 
298
  def _ttt_mask_token(self) -> int:
299
  return int(self.tokenizer.mask_token_id)
@@ -302,6 +306,7 @@ class FastPLMTestTimeTrainingMixin:
302
  return int(self.tokenizer.pad_token_id)
303
 
304
  def _ttt_replacement_tokens(self, input_ids: torch.Tensor) -> torch.Tensor:
 
305
  tokenizer = self.tokenizer
306
  special_ids = set(tokenizer.all_special_ids)
307
  vocab_size = int(self.config.vocab_size)
@@ -309,13 +314,13 @@ class FastPLMTestTimeTrainingMixin:
309
  if unknown_id is not None:
310
  special_ids.add(int(unknown_id))
311
 
312
- vocab: T.Mapping[str, T.Any] = {}
313
  get_vocab = getattr(tokenizer, "get_vocab", None)
314
  if callable(get_vocab):
315
  vocab = get_vocab()
316
- elif isinstance(getattr(tokenizer, "vocab", None), T.Mapping):
317
  vocab = tokenizer.vocab
318
- elif isinstance(getattr(tokenizer, "_token_to_id", None), T.Mapping):
319
  vocab = tokenizer._token_to_id
320
 
321
  ids: list[int] = []
@@ -334,20 +339,20 @@ class FastPLMTestTimeTrainingMixin:
334
  "TTT could not resolve any canonical amino-acid token IDs from the tokenizer; "
335
  "refusing to sample arbitrary or reserved vocabulary entries."
336
  )
337
- return torch.tensor(ids, device=input_ids.device, dtype=input_ids.dtype)
338
 
339
  def _ttt_predict_logits(
340
  self,
341
  batch: torch.Tensor | dict[str, torch.Tensor],
342
- **kwargs: T.Any,
343
  ) -> torch.Tensor:
344
  del kwargs
345
  if isinstance(batch, dict):
346
  output = self(**batch)
347
- return output.logits
348
- attention_mask = batch.ne(self._ttt_padding_token())
349
  output = self(input_ids=batch, attention_mask=attention_mask)
350
- return output.logits
351
 
352
  def _ttt_eval_step(
353
  self,
@@ -355,8 +360,8 @@ class FastPLMTestTimeTrainingMixin:
355
  loss: float,
356
  seq: str | list[str] | None = None,
357
  input_ids: torch.Tensor | None = None,
358
- **kwargs: T.Any,
359
- ) -> tuple[dict[str, T.Any], float | None]:
360
  del step, loss, seq, input_ids, kwargs
361
  return {}, None
362
 
@@ -443,8 +448,8 @@ class FastPLMTestTimeTrainingMixin:
443
  for module in self._ttt_lora_modules():
444
  snapshot.append(
445
  {
446
- "lora_down.weight": module.lora_down.weight.detach().clone(),
447
- "lora_up.weight": module.lora_up.weight.detach().clone(),
448
  }
449
  )
450
  if not snapshot:
@@ -473,14 +478,14 @@ class FastPLMTestTimeTrainingMixin:
473
  for module in self._ttt_lora_modules():
474
  module.reset_lora_parameters()
475
 
476
- def _ttt_serialized_contract(self) -> dict[str, T.Any]:
477
  return {
478
  "version": _TTT_SERIALIZATION_VERSION,
479
  "initialized": bool(self._ttt_initialized),
480
  "config": self.ttt_config.to_dict(),
481
  }
482
 
483
- def save_pretrained(self, save_directory: T.Any, *args: T.Any, **kwargs: T.Any) -> T.Any:
484
  """Save initialized adapters, their reset baseline, and the TTT config.
485
 
486
  Adapter injection changes the module tree, so the serialized config must
@@ -523,16 +528,16 @@ class FastPLMTestTimeTrainingMixin:
523
  device: torch.device,
524
  ) -> torch.Tensor | dict[str, torch.Tensor]:
525
  if isinstance(batch, dict):
526
- return {name: tensor.to(device) for name, tensor in batch.items()}
527
- return batch.to(device)
528
 
529
  def _ttt_input_ids_from_batch(
530
  self,
531
  batch: torch.Tensor | dict[str, torch.Tensor],
532
  ) -> torch.Tensor:
533
  if isinstance(batch, dict):
534
- return batch["input_ids"]
535
- return batch
536
 
537
  def _ttt_set_input_ids(
538
  self,
@@ -541,13 +546,14 @@ class FastPLMTestTimeTrainingMixin:
541
  ) -> torch.Tensor | dict[str, torch.Tensor]:
542
  if isinstance(batch, dict):
543
  updated = dict(batch)
544
- updated["input_ids"] = input_ids
545
  return updated
546
- return input_ids
547
 
548
  def _ttt_non_special_mask(self, input_ids: torch.Tensor) -> torch.Tensor:
549
- residue_ids = self._ttt_replacement_tokens(input_ids)
550
- return torch.isin(input_ids, residue_ids)
 
551
 
552
  def _ttt_validate_tokenized_batch(
553
  self,
@@ -570,12 +576,14 @@ class FastPLMTestTimeTrainingMixin:
570
  "DPLM2 TTT could not resolve the structure-token boundary safely."
571
  )
572
  pad_token = self._ttt_padding_token()
573
- generic_aa_special_ids = torch.tensor(
574
  [int(self.config.vocab_size) + offset for offset in range(4)],
575
  device=input_ids.device,
576
  dtype=input_ids.dtype,
577
  )
578
- is_structure = input_ids.ge(int(struct_boundary)) & input_ids.ne(pad_token)
 
 
579
  is_structure &= ~torch.isin(input_ids, generic_aa_special_ids)
580
  if bool(is_structure.any()):
581
  raise ValueError(
@@ -584,8 +592,11 @@ class FastPLMTestTimeTrainingMixin:
584
  )
585
 
586
  if isinstance(batch, dict) and "type_ids" in batch:
587
- type_ids = batch["type_ids"]
588
- attention_mask = batch.get("attention_mask", input_ids.ne(pad_token)).bool()
 
 
 
589
  if bool(((type_ids == int(self.config.struct_type)) & attention_mask).any()):
590
  raise ValueError(
591
  "DPLM2 TTT currently supports amino-acid-only inputs; structure "
@@ -607,13 +618,15 @@ class FastPLMTestTimeTrainingMixin:
607
  cfg = self.ttt_config
608
  if input_ids.shape[1] <= cfg.crop_size:
609
  return batch
610
- position_has_residue = self._ttt_non_special_mask(input_ids).any(dim=0).to(torch.int64)
611
- prefix = F.pad(position_has_residue.cumsum(dim=0), (1, 0))
612
- window_counts = prefix[cfg.crop_size :] - prefix[: -cfg.crop_size]
613
- valid_starts = torch.where(window_counts > 0)[0]
 
 
614
  if valid_starts.numel() == 0:
615
  raise ValueError("TTT could not find a crop containing a biological residue token.")
616
- selected = torch.randint(
617
  valid_starts.numel(),
618
  (1,),
619
  generator=generator,
@@ -625,11 +638,11 @@ class FastPLMTestTimeTrainingMixin:
625
  cropped = {}
626
  for name, tensor in batch.items():
627
  if tensor.ndim >= 2 and tensor.shape[1] == input_ids.shape[1]:
628
- cropped[name] = tensor[:, start:end]
629
  else:
630
  cropped[name] = tensor
631
  return cropped
632
- return input_ids[:, start:end]
633
 
634
  def _ttt_sample_batch(
635
  self,
@@ -638,69 +651,69 @@ class FastPLMTestTimeTrainingMixin:
638
  ) -> tuple[torch.Tensor | dict[str, torch.Tensor], torch.Tensor]:
639
  cfg = self.ttt_config
640
  batch = self._ttt_sample_crop(tokenized, generator)
641
- input_ids = self._ttt_input_ids_from_batch(batch)
642
- row_has_residue = self._ttt_non_special_mask(input_ids).any(dim=1)
643
- eligible_rows = torch.where(row_has_residue)[0]
644
  if eligible_rows.numel() == 0:
645
  raise ValueError(
646
  "TTT sampled batch contains no trainable biological residue tokens."
647
  )
648
- sampled_row_indices = torch.randint(
649
  eligible_rows.numel(),
650
  (cfg.batch_size,),
651
  generator=generator,
652
  device=input_ids.device,
653
  )
654
- rows = eligible_rows[sampled_row_indices]
655
  if isinstance(batch, dict):
656
  sampled: torch.Tensor | dict[str, torch.Tensor] = {}
657
  for name, tensor in batch.items():
658
  if tensor.ndim >= 1 and tensor.shape[0] == input_ids.shape[0]:
659
- sampled[name] = tensor.index_select(0, rows)
660
  else:
661
  sampled[name] = tensor
662
  else:
663
- sampled = input_ids.index_select(0, rows)
664
 
665
- sampled_ids = self._ttt_input_ids_from_batch(sampled)
666
- labels = sampled_ids.clone()
667
- non_special = self._ttt_non_special_mask(sampled_ids)
668
- label_mask = torch.zeros_like(non_special)
669
  for row_idx in range(sampled_ids.shape[0]):
670
- candidate_positions = torch.where(non_special[row_idx])[0]
671
  if candidate_positions.numel() == 0:
672
  continue
673
  num_mask = max(1, round(candidate_positions.numel() * cfg.mask_ratio))
674
- order = torch.randperm(
675
  candidate_positions.numel(),
676
  generator=generator,
677
  device=sampled_ids.device,
678
  )
679
- chosen = candidate_positions[order[:num_mask]]
680
  label_mask[row_idx, chosen] = True
681
- labels = labels.masked_fill(~label_mask, -100)
682
 
683
- masked_ids = sampled_ids.clone()
684
- chosen_positions = torch.where(label_mask)
685
  if chosen_positions[0].numel() > 0:
686
- random_values = torch.rand(
687
  chosen_positions[0].shape,
688
  generator=generator,
689
  device=sampled_ids.device,
690
  )
691
- leave = random_values < cfg.bert_leave_prob
692
- replace = (random_values >= cfg.bert_leave_prob) & (
693
  random_values < cfg.bert_leave_prob + cfg.bert_replace_prob
694
  )
695
- mask = ~(leave | replace)
696
  if mask.any():
697
  masked_ids[
698
  chosen_positions[0][mask],
699
  chosen_positions[1][mask],
700
  ] = self._ttt_mask_token()
701
  if replace.any():
702
- replacement_tokens = self._ttt_replacement_tokens(sampled_ids)
703
- replacement_idx = torch.randint(
704
  replacement_tokens.shape[0],
705
  (int(replace.sum().item()),),
706
  generator=generator,
@@ -711,10 +724,10 @@ class FastPLMTestTimeTrainingMixin:
711
  chosen_positions[1][replace],
712
  ] = replacement_tokens[replacement_idx]
713
 
714
- return self._ttt_set_input_ids(sampled, masked_ids), labels
715
 
716
  @contextlib.contextmanager
717
- def _ttt_seed_scope(self, seed: int | None) -> T.Iterator[None]:
718
  if seed is None:
719
  yield
720
  return
@@ -736,9 +749,9 @@ class FastPLMTestTimeTrainingMixin:
736
  self,
737
  seq: str | list[str] | None = None,
738
  input_ids: torch.Tensor | None = None,
739
- ttt_config: TTTConfig | T.Mapping[str, T.Any] | None = None,
740
- **kwargs: T.Any,
741
- ) -> dict[str, T.Any]:
742
  if ttt_config is not None:
743
  if "_ttt_initialized" in self.__dict__ and self._ttt_initialized:
744
  next_cfg = self.ttt_config.merged(ttt_config)
@@ -788,7 +801,7 @@ class FastPLMTestTimeTrainingMixin:
788
  module_modes = {module: module.training for module in self.modules()}
789
  requires_grad = {param: param.requires_grad for param in self.parameters()}
790
  losses: list[float] = []
791
- step_metrics: list[dict[str, T.Any]] = []
792
  best_state: list[dict[str, torch.Tensor]] | None = None
793
  best_metric: float | None = None
794
  best_step = 0
@@ -805,14 +818,17 @@ class FastPLMTestTimeTrainingMixin:
805
  optimizer.zero_grad(set_to_none=True)
806
  total_micro_steps = cfg.steps * cfg.ags
807
  for micro_step in range(total_micro_steps):
808
- batch, labels = self._ttt_sample_batch(tokenized, generator)
 
 
 
809
  if not bool(labels.ne(-100).any()):
810
  raise RuntimeError(
811
  "TTT produced an all-ignored label batch; refusing a NaN update."
812
  )
813
- logits = self._ttt_predict_logits(batch, **kwargs)
814
- labels = labels.to(device=logits.device)
815
- loss = F.cross_entropy(
816
  logits.reshape(-1, logits.shape[-1]),
817
  labels.reshape(-1),
818
  ignore_index=-100,
 
3
  import contextlib
4
  import math
5
  import numbers
 
 
 
6
  import torch
7
  import torch.nn as nn
8
  import torch.nn.functional as F
9
+ from collections.abc import Iterator, Mapping
10
+ from dataclasses import asdict, dataclass, fields
11
+ from typing import Any
12
+
13
 
14
  _STANDARD_AMINO_ACIDS = "ACDEFGHIKLMNPQRSTVWY"
15
  _TTT_SERIALIZATION_VERSION = 1
 
43
  self.verify()
44
 
45
  @classmethod
46
+ def from_kwargs(cls, **kwargs: Any) -> TTTConfig:
47
  valid_names = {field.name for field in fields(cls)}
48
  unknown_names = set(kwargs) - valid_names
49
  if unknown_names:
 
54
  kwargs["lora_target_modules"] = tuple(kwargs["lora_target_modules"])
55
  return cls(**kwargs)
56
 
57
+ def merged(self, overrides: Mapping[str, Any] | TTTConfig | None) -> TTTConfig:
58
  if overrides is None:
59
  return self
60
  if isinstance(overrides, TTTConfig):
 
66
  values[name] = value
67
  return TTTConfig(**values)
68
 
69
+ def to_dict(self) -> dict[str, Any]:
70
  return asdict(self)
71
 
72
  def verify(self) -> None:
 
225
  return self.linear._parameters["bias"]
226
 
227
  def forward(self, x: torch.Tensor) -> torch.Tensor:
228
+ # x: (..., d_in)
229
+ base = self.linear(x) # (..., d_out)
230
+ delta = ( # (..., d_out)
231
+ self.lora_up(self.lora_down(x.to(dtype=torch.float32))) * self.scale
232
+ )
233
+ return base + delta.to(dtype=base.dtype) # (..., d_out)
234
 
235
  def reset_lora_parameters(self) -> None:
236
  with torch.no_grad():
 
239
 
240
 
241
  class FastPLMTestTimeTrainingMixin:
242
+ def init_ttt(self, ttt_config: TTTConfig | Mapping[str, Any] | None = None) -> None:
243
  base_config = self.__dict__.get("_ttt_cfg")
244
  if base_config is None:
245
  base_config = TTTConfig()
 
249
  serialized = getattr(getattr(self, "config", None), "fastplms_ttt", None)
250
  serialized_initialized = False
251
  if serialized is not None:
252
+ if not isinstance(serialized, Mapping):
253
  raise ValueError("config.fastplms_ttt must be a mapping.")
254
  version = serialized.get("version")
255
  if version != _TTT_SERIALIZATION_VERSION:
 
258
  f"{version!r}; expected {_TTT_SERIALIZATION_VERSION}."
259
  )
260
  serialized_config = serialized.get("config")
261
+ if not isinstance(serialized_config, Mapping):
262
  raise ValueError("Serialized FastPLMs TTT state is missing its config mapping.")
263
  configured = TTTConfig.from_kwargs(**dict(serialized_config))
264
  initialized_value = serialized.get("initialized", False)
 
289
  self,
290
  seq: str | list[str] | None = None,
291
  input_ids: torch.Tensor | None = None,
292
+ **kwargs: Any,
293
  ) -> torch.Tensor | dict[str, torch.Tensor]:
294
  del kwargs
295
  if input_ids is not None:
296
+ return input_ids # (b, l)
297
  if seq is None:
298
  raise ValueError("Pass either seq or input_ids for TTT.")
299
  tokenized = self.tokenizer(seq, return_tensors="pt", padding=True)
300
+ return tokenized["input_ids"] # (b, l)
301
 
302
  def _ttt_mask_token(self) -> int:
303
  return int(self.tokenizer.mask_token_id)
 
306
  return int(self.tokenizer.pad_token_id)
307
 
308
  def _ttt_replacement_tokens(self, input_ids: torch.Tensor) -> torch.Tensor:
309
+ # input_ids: (b, l)
310
  tokenizer = self.tokenizer
311
  special_ids = set(tokenizer.all_special_ids)
312
  vocab_size = int(self.config.vocab_size)
 
314
  if unknown_id is not None:
315
  special_ids.add(int(unknown_id))
316
 
317
+ vocab: Mapping[str, Any] = {}
318
  get_vocab = getattr(tokenizer, "get_vocab", None)
319
  if callable(get_vocab):
320
  vocab = get_vocab()
321
+ elif isinstance(getattr(tokenizer, "vocab", None), Mapping):
322
  vocab = tokenizer.vocab
323
+ elif isinstance(getattr(tokenizer, "_token_to_id", None), Mapping):
324
  vocab = tokenizer._token_to_id
325
 
326
  ids: list[int] = []
 
339
  "TTT could not resolve any canonical amino-acid token IDs from the tokenizer; "
340
  "refusing to sample arbitrary or reserved vocabulary entries."
341
  )
342
+ return torch.tensor(ids, device=input_ids.device, dtype=input_ids.dtype) # (c_aa,)
343
 
344
  def _ttt_predict_logits(
345
  self,
346
  batch: torch.Tensor | dict[str, torch.Tensor],
347
+ **kwargs: Any,
348
  ) -> torch.Tensor:
349
  del kwargs
350
  if isinstance(batch, dict):
351
  output = self(**batch)
352
+ return output.logits # (b, l, c)
353
+ attention_mask = batch.ne(self._ttt_padding_token()) # (b, l)
354
  output = self(input_ids=batch, attention_mask=attention_mask)
355
+ return output.logits # (b, l, c)
356
 
357
  def _ttt_eval_step(
358
  self,
 
360
  loss: float,
361
  seq: str | list[str] | None = None,
362
  input_ids: torch.Tensor | None = None,
363
+ **kwargs: Any,
364
+ ) -> tuple[dict[str, Any], float | None]:
365
  del step, loss, seq, input_ids, kwargs
366
  return {}, None
367
 
 
448
  for module in self._ttt_lora_modules():
449
  snapshot.append(
450
  {
451
+ "lora_down.weight": module.lora_down.weight.detach().clone(), # (r, d_in)
452
+ "lora_up.weight": module.lora_up.weight.detach().clone(), # (d_out, r)
453
  }
454
  )
455
  if not snapshot:
 
478
  for module in self._ttt_lora_modules():
479
  module.reset_lora_parameters()
480
 
481
+ def _ttt_serialized_contract(self) -> dict[str, Any]:
482
  return {
483
  "version": _TTT_SERIALIZATION_VERSION,
484
  "initialized": bool(self._ttt_initialized),
485
  "config": self.ttt_config.to_dict(),
486
  }
487
 
488
+ def save_pretrained(self, save_directory: Any, *args: Any, **kwargs: Any) -> Any:
489
  """Save initialized adapters, their reset baseline, and the TTT config.
490
 
491
  Adapter injection changes the module tree, so the serialized config must
 
528
  device: torch.device,
529
  ) -> torch.Tensor | dict[str, torch.Tensor]:
530
  if isinstance(batch, dict):
531
+ return {name: tensor.to(device) for name, tensor in batch.items()} # unchanged shapes
532
+ return batch.to(device) # unchanged shape
533
 
534
  def _ttt_input_ids_from_batch(
535
  self,
536
  batch: torch.Tensor | dict[str, torch.Tensor],
537
  ) -> torch.Tensor:
538
  if isinstance(batch, dict):
539
+ return batch["input_ids"] # (b, l)
540
+ return batch # (b, l)
541
 
542
  def _ttt_set_input_ids(
543
  self,
 
546
  ) -> torch.Tensor | dict[str, torch.Tensor]:
547
  if isinstance(batch, dict):
548
  updated = dict(batch)
549
+ updated["input_ids"] = input_ids # (b, l)
550
  return updated
551
+ return input_ids # (b, l)
552
 
553
  def _ttt_non_special_mask(self, input_ids: torch.Tensor) -> torch.Tensor:
554
+ # input_ids: (b, l)
555
+ residue_ids = self._ttt_replacement_tokens(input_ids) # (c_aa,)
556
+ return torch.isin(input_ids, residue_ids) # (b, l)
557
 
558
  def _ttt_validate_tokenized_batch(
559
  self,
 
576
  "DPLM2 TTT could not resolve the structure-token boundary safely."
577
  )
578
  pad_token = self._ttt_padding_token()
579
+ generic_aa_special_ids = torch.tensor( # (4,)
580
  [int(self.config.vocab_size) + offset for offset in range(4)],
581
  device=input_ids.device,
582
  dtype=input_ids.dtype,
583
  )
584
+ is_structure = input_ids.ge(int(struct_boundary)) & input_ids.ne( # (b, l)
585
+ pad_token
586
+ )
587
  is_structure &= ~torch.isin(input_ids, generic_aa_special_ids)
588
  if bool(is_structure.any()):
589
  raise ValueError(
 
592
  )
593
 
594
  if isinstance(batch, dict) and "type_ids" in batch:
595
+ type_ids = batch["type_ids"] # (b, l)
596
+ attention_mask = batch.get( # (b, l)
597
+ "attention_mask",
598
+ input_ids.ne(pad_token),
599
+ ).bool()
600
  if bool(((type_ids == int(self.config.struct_type)) & attention_mask).any()):
601
  raise ValueError(
602
  "DPLM2 TTT currently supports amino-acid-only inputs; structure "
 
618
  cfg = self.ttt_config
619
  if input_ids.shape[1] <= cfg.crop_size:
620
  return batch
621
+ position_has_residue = ( # (l,)
622
+ self._ttt_non_special_mask(input_ids).any(dim=0).to(torch.int64)
623
+ )
624
+ prefix = F.pad(position_has_residue.cumsum(dim=0), (1, 0)) # (l + 1,)
625
+ window_counts = prefix[cfg.crop_size :] - prefix[: -cfg.crop_size] # (l-crop+1,)
626
+ valid_starts = torch.where(window_counts > 0)[0] # (n_valid,)
627
  if valid_starts.numel() == 0:
628
  raise ValueError("TTT could not find a crop containing a biological residue token.")
629
+ selected = torch.randint( # (1,)
630
  valid_starts.numel(),
631
  (1,),
632
  generator=generator,
 
638
  cropped = {}
639
  for name, tensor in batch.items():
640
  if tensor.ndim >= 2 and tensor.shape[1] == input_ids.shape[1]:
641
+ cropped[name] = tensor[:, start:end] # (b, crop_size, ...)
642
  else:
643
  cropped[name] = tensor
644
  return cropped
645
+ return input_ids[:, start:end] # (b, crop_size)
646
 
647
  def _ttt_sample_batch(
648
  self,
 
651
  ) -> tuple[torch.Tensor | dict[str, torch.Tensor], torch.Tensor]:
652
  cfg = self.ttt_config
653
  batch = self._ttt_sample_crop(tokenized, generator)
654
+ input_ids = self._ttt_input_ids_from_batch(batch) # (b, l)
655
+ row_has_residue = self._ttt_non_special_mask(input_ids).any(dim=1) # (b,)
656
+ eligible_rows = torch.where(row_has_residue)[0] # (n_eligible,)
657
  if eligible_rows.numel() == 0:
658
  raise ValueError(
659
  "TTT sampled batch contains no trainable biological residue tokens."
660
  )
661
+ sampled_row_indices = torch.randint( # (b_sample,)
662
  eligible_rows.numel(),
663
  (cfg.batch_size,),
664
  generator=generator,
665
  device=input_ids.device,
666
  )
667
+ rows = eligible_rows[sampled_row_indices] # (b_sample,)
668
  if isinstance(batch, dict):
669
  sampled: torch.Tensor | dict[str, torch.Tensor] = {}
670
  for name, tensor in batch.items():
671
  if tensor.ndim >= 1 and tensor.shape[0] == input_ids.shape[0]:
672
+ sampled[name] = tensor.index_select(0, rows) # (b_sample, ...)
673
  else:
674
  sampled[name] = tensor
675
  else:
676
+ sampled = input_ids.index_select(0, rows) # (b_sample, l)
677
 
678
+ sampled_ids = self._ttt_input_ids_from_batch(sampled) # (b_sample, l)
679
+ labels = sampled_ids.clone() # (b_sample, l)
680
+ non_special = self._ttt_non_special_mask(sampled_ids) # (b_sample, l)
681
+ label_mask = torch.zeros_like(non_special) # (b_sample, l)
682
  for row_idx in range(sampled_ids.shape[0]):
683
+ candidate_positions = torch.where(non_special[row_idx])[0] # (n_candidates,)
684
  if candidate_positions.numel() == 0:
685
  continue
686
  num_mask = max(1, round(candidate_positions.numel() * cfg.mask_ratio))
687
+ order = torch.randperm( # (n_candidates,)
688
  candidate_positions.numel(),
689
  generator=generator,
690
  device=sampled_ids.device,
691
  )
692
+ chosen = candidate_positions[order[:num_mask]] # (n_mask,)
693
  label_mask[row_idx, chosen] = True
694
+ labels = labels.masked_fill(~label_mask, -100) # (b_sample, l)
695
 
696
+ masked_ids = sampled_ids.clone() # (b_sample, l)
697
+ chosen_positions = torch.where(label_mask) # two (n_chosen,) tensors
698
  if chosen_positions[0].numel() > 0:
699
+ random_values = torch.rand( # (n_chosen,)
700
  chosen_positions[0].shape,
701
  generator=generator,
702
  device=sampled_ids.device,
703
  )
704
+ leave = random_values < cfg.bert_leave_prob # (n_chosen,)
705
+ replace = (random_values >= cfg.bert_leave_prob) & ( # (n_chosen,)
706
  random_values < cfg.bert_leave_prob + cfg.bert_replace_prob
707
  )
708
+ mask = ~(leave | replace) # (n_chosen,)
709
  if mask.any():
710
  masked_ids[
711
  chosen_positions[0][mask],
712
  chosen_positions[1][mask],
713
  ] = self._ttt_mask_token()
714
  if replace.any():
715
+ replacement_tokens = self._ttt_replacement_tokens(sampled_ids) # (c_aa,)
716
+ replacement_idx = torch.randint( # (n_replace,)
717
  replacement_tokens.shape[0],
718
  (int(replace.sum().item()),),
719
  generator=generator,
 
724
  chosen_positions[1][replace],
725
  ] = replacement_tokens[replacement_idx]
726
 
727
+ return self._ttt_set_input_ids(sampled, masked_ids), labels # batch, (b_sample, l)
728
 
729
  @contextlib.contextmanager
730
+ def _ttt_seed_scope(self, seed: int | None) -> Iterator[None]:
731
  if seed is None:
732
  yield
733
  return
 
749
  self,
750
  seq: str | list[str] | None = None,
751
  input_ids: torch.Tensor | None = None,
752
+ ttt_config: TTTConfig | Mapping[str, Any] | None = None,
753
+ **kwargs: Any,
754
+ ) -> dict[str, Any]:
755
  if ttt_config is not None:
756
  if "_ttt_initialized" in self.__dict__ and self._ttt_initialized:
757
  next_cfg = self.ttt_config.merged(ttt_config)
 
801
  module_modes = {module: module.training for module in self.modules()}
802
  requires_grad = {param: param.requires_grad for param in self.parameters()}
803
  losses: list[float] = []
804
+ step_metrics: list[dict[str, Any]] = []
805
  best_state: list[dict[str, torch.Tensor]] | None = None
806
  best_metric: float | None = None
807
  best_step = 0
 
818
  optimizer.zero_grad(set_to_none=True)
819
  total_micro_steps = cfg.steps * cfg.ags
820
  for micro_step in range(total_micro_steps):
821
+ batch, labels = self._ttt_sample_batch( # batch, (b_sample, l)
822
+ tokenized,
823
+ generator,
824
+ )
825
  if not bool(labels.ne(-100).any()):
826
  raise RuntimeError(
827
  "TTT produced an all-ignored label batch; refusing a NaN update."
828
  )
829
+ logits = self._ttt_predict_logits(batch, **kwargs) # (b_sample, l, c)
830
+ labels = labels.to(device=logits.device) # (b_sample, l)
831
+ loss = F.cross_entropy( # ()
832
  logits.reshape(-1, logits.shape[-1]),
833
  labels.reshape(-1),
834
  ignore_index=-100,
fastplms/registry.py CHANGED
@@ -18,6 +18,7 @@ from types import MappingProxyType
18
  from typing import Any, Literal, cast
19
  from urllib.parse import urlparse
20
 
 
21
  _HEX_RE = re.compile(r"^[0-9a-f]+$")
22
  _IDENTIFIER_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
23
  _HUB_LICENSE_NAME_RE = re.compile(r"[^a-z0-9.]+")
@@ -559,20 +560,20 @@ def _require_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple
559
  value = table.get(key)
560
  if not isinstance(value, list) or not value or any(not isinstance(item, str) for item in value):
561
  raise RegistryError(f"{context}.{key} must be a non-empty string array.")
562
- result = tuple(value)
563
- if len(set(result)) != len(result):
564
  raise RegistryError(f"{context}.{key} contains duplicate values.")
565
- return result
566
 
567
 
568
  def _optional_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple[str, ...]:
569
  value = table.get(key, [])
570
  if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
571
  raise RegistryError(f"{context}.{key} must be a string array.")
572
- result = tuple(value)
573
- if len(set(result)) != len(result):
574
  raise RegistryError(f"{context}.{key} contains duplicate values.")
575
- return result
576
 
577
 
578
  def _optional_str(table: Mapping[str, Any], key: str, context: str) -> str | None:
@@ -657,11 +658,11 @@ def _require_digest_list(
657
  table: Mapping[str, Any], key: str, context: str
658
  ) -> tuple[FileDigest, ...]:
659
  encoded = _require_str_list(table, key, context)
660
- result = tuple(FileDigest.parse(value) for value in encoded)
661
- paths = [item.path for item in result]
662
  if len(paths) != len(set(paths)):
663
  raise RegistryError(f"{context}.{key} contains duplicate paths.")
664
- return result
665
 
666
 
667
  def _validate_revision(revision: str, context: str) -> None:
@@ -701,7 +702,7 @@ def _parse_oracle_assets(table: Mapping[str, Any], context: str) -> tuple[Oracle
701
  raw = table.get("oracle_assets", [])
702
  if not isinstance(raw, list):
703
  raise RegistryError(f"{context}.oracle_assets must be an array of tables.")
704
- result: list[OracleAsset] = []
705
  for index, value in enumerate(raw):
706
  asset_context = f"{context}.oracle_assets[{index}]"
707
  if not isinstance(value, dict):
@@ -736,7 +737,7 @@ def _parse_oracle_assets(table: Mapping[str, Any], context: str) -> tuple[Oracle
736
  size = value.get("size")
737
  if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
738
  raise RegistryError(f"{asset_context}.size must be a positive byte count.")
739
- result.append(
740
  OracleAsset(
741
  role=role,
742
  path=path,
@@ -745,16 +746,16 @@ def _parse_oracle_assets(table: Mapping[str, Any], context: str) -> tuple[Oracle
745
  size=size,
746
  )
747
  )
748
- roles = [asset.role for asset in result]
749
- paths = [asset.path for asset in result]
750
- urls = [asset.url for asset in result]
751
  if (
752
  len(roles) != len(set(roles))
753
  or len(paths) != len(set(paths))
754
  or len(urls) != len(set(urls))
755
  ):
756
  raise RegistryError(f"{context}.oracle_assets contains duplicate identities.")
757
- return tuple(result)
758
 
759
 
760
  def _parse_official_golden(
@@ -789,7 +790,7 @@ def _parse_official_golden(
789
  def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
790
  if not isinstance(raw, list) or not raw:
791
  raise RegistryError("The manifest must contain [[attention_kernels]] entries.")
792
- result: dict[str, AttentionKernelSpec] = {}
793
  expected_variants = {
794
  "flash_attention_2": "flash_attn2",
795
  "flash_attention_3": "flash_attn3",
@@ -812,7 +813,7 @@ def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
812
  implementation = _require_str(value, "implementation", context)
813
  if implementation not in expected_variants:
814
  raise RegistryError(f"Unsupported attention kernel {implementation!r}.")
815
- if implementation in result:
816
  raise RegistryError(f"Duplicate attention kernel {implementation!r}.")
817
  repository = _require_str(value, "repository", context)
818
  if _REPOSITORY_ID_RE.fullmatch(repository) is None:
@@ -834,7 +835,7 @@ def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
834
  dtypes = _require_str_list(value, "dtypes", context)
835
  if not set(dtypes).issubset(_ALLOWED_DTYPES):
836
  raise RegistryError(f"{context}.dtypes contains unsupported dtypes.")
837
- result[implementation] = AttentionKernelSpec(
838
  implementation=implementation,
839
  repository=repository,
840
  revision=revision,
@@ -842,15 +843,15 @@ def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
842
  expected_variant=expected_variant,
843
  dtypes=cast(tuple[DtypeName, ...], dtypes),
844
  )
845
- if set(result) != set(expected_variants):
846
  raise RegistryError("The manifest must pin both FlashAttention kernel versions.")
847
- return result
848
 
849
 
850
  def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
851
  if not isinstance(raw, list) or not raw:
852
  raise RegistryError("The manifest must contain at least one [[upstreams]] entry.")
853
- result: dict[str, UpstreamSource] = {}
854
  paths: set[str] = set()
855
  for index, value in enumerate(raw):
856
  context = f"upstreams[{index}]"
@@ -860,7 +861,7 @@ def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
860
  source_id = _require_str(value, "id", context)
861
  if _IDENTIFIER_RE.fullmatch(source_id) is None:
862
  raise RegistryError(f"Invalid upstream ID: {source_id!r}")
863
- if source_id in result:
864
  raise RegistryError(f"Duplicate upstream ID: {source_id!r}")
865
  revision = _require_str(value, "revision", context)
866
  _validate_revision(revision, f"{context}.revision")
@@ -913,7 +914,7 @@ def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
913
  missing_e1 = sorted(required_e1.difference(distribution_map))
914
  if missing_e1:
915
  raise RegistryError(f"{context} is missing E1 legal files: {missing_e1}")
916
- result[source_id] = UpstreamSource(
917
  id=source_id,
918
  path=path,
919
  url=url,
@@ -923,7 +924,7 @@ def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
923
  license_digests=license_digests,
924
  distribution_files=distribution_files,
925
  )
926
- return result
927
 
928
 
929
  def _parse_families(
@@ -932,7 +933,7 @@ def _parse_families(
932
  ) -> dict[str, ModelFamily]:
933
  if not isinstance(raw, dict) or not raw:
934
  raise RegistryError("The manifest must contain [families.<id>] tables.")
935
- result: dict[str, ModelFamily] = {}
936
  for family_id, value in raw.items():
937
  context = f"families.{family_id}"
938
  if _IDENTIFIER_RE.fullmatch(family_id) is None or not isinstance(value, dict):
@@ -1068,7 +1069,7 @@ def _parse_families(
1068
  f"{context}.conversion_provenance must identify {state_transform!r} and "
1069
  f"contain mechanism-first sections; missing {missing_sections}."
1070
  )
1071
- result[family_id] = ModelFamily(
1072
  id=family_id,
1073
  architecture=_require_str(value, "architecture", context),
1074
  upstreams=source_ids,
@@ -1099,7 +1100,7 @@ def _parse_families(
1099
  conversion_provenance=conversion_provenance,
1100
  backbone_model=backbone_model,
1101
  )
1102
- return result
1103
 
1104
 
1105
  def _parse_runtime_assets(
@@ -1108,7 +1109,7 @@ def _parse_runtime_assets(
1108
  ) -> dict[str, RuntimeAsset]:
1109
  if not isinstance(raw, list) or not raw:
1110
  raise RegistryError("The manifest must contain at least one [[runtime_assets]] entry.")
1111
- result: dict[str, RuntimeAsset] = {}
1112
  identities: set[tuple[str, str, str]] = set()
1113
  for index, value in enumerate(raw):
1114
  context = f"runtime_assets[{index}]"
@@ -1118,7 +1119,7 @@ def _parse_runtime_assets(
1118
  asset_id = _require_str(value, "id", context)
1119
  if _IDENTIFIER_RE.fullmatch(asset_id) is None:
1120
  raise RegistryError(f"Invalid runtime asset ID: {asset_id!r}")
1121
- if asset_id in result:
1122
  raise RegistryError(f"Duplicate runtime asset ID: {asset_id!r}")
1123
  repository = _require_str(value, "repository", context)
1124
  if _REPOSITORY_ID_RE.fullmatch(repository) is None:
@@ -1164,7 +1165,7 @@ def _parse_runtime_assets(
1164
  if identity in identities:
1165
  raise RegistryError(f"Duplicate runtime asset identity: {identity!r}")
1166
  identities.add(identity)
1167
- result[asset_id] = RuntimeAsset(
1168
  id=asset_id,
1169
  repository=repository,
1170
  revision=revision,
@@ -1176,7 +1177,7 @@ def _parse_runtime_assets(
1176
  license_expression=license_expression,
1177
  offline_behavior=offline_behavior,
1178
  )
1179
- return result
1180
 
1181
 
1182
  def _parse_models(
@@ -1185,7 +1186,7 @@ def _parse_models(
1185
  ) -> dict[str, ModelSpec]:
1186
  if not isinstance(raw, list) or not raw:
1187
  raise RegistryError("The manifest must contain at least one [[models]] entry.")
1188
- result: dict[str, ModelSpec] = {}
1189
  fast_repositories: set[str] = set()
1190
  for index, value in enumerate(raw):
1191
  context = f"models[{index}]"
@@ -1195,7 +1196,7 @@ def _parse_models(
1195
  model_id = _require_str(value, "id", context)
1196
  if _IDENTIFIER_RE.fullmatch(model_id) is None:
1197
  raise RegistryError(f"Invalid model ID: {model_id!r}")
1198
- if model_id in result:
1199
  raise RegistryError(f"Duplicate model ID: {model_id!r}")
1200
  family_id = _require_str(value, "family", context)
1201
  if family_id not in families:
@@ -1278,7 +1279,7 @@ def _parse_models(
1278
  if not class_path.startswith("fastplms.") or class_path.count(".") < 2:
1279
  raise RegistryError(f"Invalid Python class path in {context}: {class_path!r}")
1280
  auto_map.append((auto_class, class_path))
1281
- result[model_id] = ModelSpec(
1282
  id=model_id,
1283
  family=family,
1284
  fast=fast,
@@ -1294,7 +1295,7 @@ def _parse_models(
1294
  notes=notes,
1295
  msa_conditioning=msa_conditioning,
1296
  )
1297
- return result
1298
 
1299
 
1300
  def _validate_registry(
@@ -1409,21 +1410,21 @@ def _validate_registry(
1409
 
1410
  def _load_manifest_bytes(raw_bytes: bytes) -> ModelRegistry:
1411
  try:
1412
- data = tomllib.loads(raw_bytes.decode("utf-8"))
1413
  except (UnicodeDecodeError, tomllib.TOMLDecodeError) as error:
1414
  raise RegistryError(f"Unable to parse model manifest: {error}") from error
1415
- _reject_unknown_fields(data, _ROOT_FIELDS, "manifest")
1416
- if data.get("schema_version") != 1:
1417
  raise RegistryError("Unsupported model manifest schema_version; expected 1.")
1418
- legal_files = _require_digest_list(data, "legal_files", "manifest")
1419
  required_legal_paths = {"LICENSE", "THIRD_PARTY_NOTICES.md"}
1420
  if {item.path for item in legal_files} != required_legal_paths:
1421
  raise RegistryError("manifest.legal_files must contain LICENSE and THIRD_PARTY_NOTICES.md.")
1422
- attention_kernels = _parse_attention_kernels(data.get("attention_kernels"))
1423
- upstreams = _parse_upstreams(data.get("upstreams"))
1424
- families = _parse_families(data.get("families"), upstreams)
1425
- runtime_assets = _parse_runtime_assets(data.get("runtime_assets"), families)
1426
- models = _parse_models(data.get("models"), families)
1427
  _validate_registry(upstreams, attention_kernels, families, models)
1428
  return ModelRegistry(
1429
  schema_version=1,
 
18
  from typing import Any, Literal, cast
19
  from urllib.parse import urlparse
20
 
21
+
22
  _HEX_RE = re.compile(r"^[0-9a-f]+$")
23
  _IDENTIFIER_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
24
  _HUB_LICENSE_NAME_RE = re.compile(r"[^a-z0-9.]+")
 
560
  value = table.get(key)
561
  if not isinstance(value, list) or not value or any(not isinstance(item, str) for item in value):
562
  raise RegistryError(f"{context}.{key} must be a non-empty string array.")
563
+ strings = tuple(value)
564
+ if len(set(strings)) != len(strings):
565
  raise RegistryError(f"{context}.{key} contains duplicate values.")
566
+ return strings
567
 
568
 
569
  def _optional_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple[str, ...]:
570
  value = table.get(key, [])
571
  if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
572
  raise RegistryError(f"{context}.{key} must be a string array.")
573
+ strings = tuple(value)
574
+ if len(set(strings)) != len(strings):
575
  raise RegistryError(f"{context}.{key} contains duplicate values.")
576
+ return strings
577
 
578
 
579
  def _optional_str(table: Mapping[str, Any], key: str, context: str) -> str | None:
 
658
  table: Mapping[str, Any], key: str, context: str
659
  ) -> tuple[FileDigest, ...]:
660
  encoded = _require_str_list(table, key, context)
661
+ digests = tuple(FileDigest.parse(value) for value in encoded)
662
+ paths = [item.path for item in digests]
663
  if len(paths) != len(set(paths)):
664
  raise RegistryError(f"{context}.{key} contains duplicate paths.")
665
+ return digests
666
 
667
 
668
  def _validate_revision(revision: str, context: str) -> None:
 
702
  raw = table.get("oracle_assets", [])
703
  if not isinstance(raw, list):
704
  raise RegistryError(f"{context}.oracle_assets must be an array of tables.")
705
+ assets: list[OracleAsset] = []
706
  for index, value in enumerate(raw):
707
  asset_context = f"{context}.oracle_assets[{index}]"
708
  if not isinstance(value, dict):
 
737
  size = value.get("size")
738
  if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
739
  raise RegistryError(f"{asset_context}.size must be a positive byte count.")
740
+ assets.append(
741
  OracleAsset(
742
  role=role,
743
  path=path,
 
746
  size=size,
747
  )
748
  )
749
+ roles = [asset.role for asset in assets]
750
+ paths = [asset.path for asset in assets]
751
+ urls = [asset.url for asset in assets]
752
  if (
753
  len(roles) != len(set(roles))
754
  or len(paths) != len(set(paths))
755
  or len(urls) != len(set(urls))
756
  ):
757
  raise RegistryError(f"{context}.oracle_assets contains duplicate identities.")
758
+ return tuple(assets)
759
 
760
 
761
  def _parse_official_golden(
 
790
  def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
791
  if not isinstance(raw, list) or not raw:
792
  raise RegistryError("The manifest must contain [[attention_kernels]] entries.")
793
+ kernels: dict[str, AttentionKernelSpec] = {}
794
  expected_variants = {
795
  "flash_attention_2": "flash_attn2",
796
  "flash_attention_3": "flash_attn3",
 
813
  implementation = _require_str(value, "implementation", context)
814
  if implementation not in expected_variants:
815
  raise RegistryError(f"Unsupported attention kernel {implementation!r}.")
816
+ if implementation in kernels:
817
  raise RegistryError(f"Duplicate attention kernel {implementation!r}.")
818
  repository = _require_str(value, "repository", context)
819
  if _REPOSITORY_ID_RE.fullmatch(repository) is None:
 
835
  dtypes = _require_str_list(value, "dtypes", context)
836
  if not set(dtypes).issubset(_ALLOWED_DTYPES):
837
  raise RegistryError(f"{context}.dtypes contains unsupported dtypes.")
838
+ kernels[implementation] = AttentionKernelSpec(
839
  implementation=implementation,
840
  repository=repository,
841
  revision=revision,
 
843
  expected_variant=expected_variant,
844
  dtypes=cast(tuple[DtypeName, ...], dtypes),
845
  )
846
+ if set(kernels) != set(expected_variants):
847
  raise RegistryError("The manifest must pin both FlashAttention kernel versions.")
848
+ return kernels
849
 
850
 
851
  def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
852
  if not isinstance(raw, list) or not raw:
853
  raise RegistryError("The manifest must contain at least one [[upstreams]] entry.")
854
+ upstreams: dict[str, UpstreamSource] = {}
855
  paths: set[str] = set()
856
  for index, value in enumerate(raw):
857
  context = f"upstreams[{index}]"
 
861
  source_id = _require_str(value, "id", context)
862
  if _IDENTIFIER_RE.fullmatch(source_id) is None:
863
  raise RegistryError(f"Invalid upstream ID: {source_id!r}")
864
+ if source_id in upstreams:
865
  raise RegistryError(f"Duplicate upstream ID: {source_id!r}")
866
  revision = _require_str(value, "revision", context)
867
  _validate_revision(revision, f"{context}.revision")
 
914
  missing_e1 = sorted(required_e1.difference(distribution_map))
915
  if missing_e1:
916
  raise RegistryError(f"{context} is missing E1 legal files: {missing_e1}")
917
+ upstreams[source_id] = UpstreamSource(
918
  id=source_id,
919
  path=path,
920
  url=url,
 
924
  license_digests=license_digests,
925
  distribution_files=distribution_files,
926
  )
927
+ return upstreams
928
 
929
 
930
  def _parse_families(
 
933
  ) -> dict[str, ModelFamily]:
934
  if not isinstance(raw, dict) or not raw:
935
  raise RegistryError("The manifest must contain [families.<id>] tables.")
936
+ families: dict[str, ModelFamily] = {}
937
  for family_id, value in raw.items():
938
  context = f"families.{family_id}"
939
  if _IDENTIFIER_RE.fullmatch(family_id) is None or not isinstance(value, dict):
 
1069
  f"{context}.conversion_provenance must identify {state_transform!r} and "
1070
  f"contain mechanism-first sections; missing {missing_sections}."
1071
  )
1072
+ families[family_id] = ModelFamily(
1073
  id=family_id,
1074
  architecture=_require_str(value, "architecture", context),
1075
  upstreams=source_ids,
 
1100
  conversion_provenance=conversion_provenance,
1101
  backbone_model=backbone_model,
1102
  )
1103
+ return families
1104
 
1105
 
1106
  def _parse_runtime_assets(
 
1109
  ) -> dict[str, RuntimeAsset]:
1110
  if not isinstance(raw, list) or not raw:
1111
  raise RegistryError("The manifest must contain at least one [[runtime_assets]] entry.")
1112
+ runtime_assets: dict[str, RuntimeAsset] = {}
1113
  identities: set[tuple[str, str, str]] = set()
1114
  for index, value in enumerate(raw):
1115
  context = f"runtime_assets[{index}]"
 
1119
  asset_id = _require_str(value, "id", context)
1120
  if _IDENTIFIER_RE.fullmatch(asset_id) is None:
1121
  raise RegistryError(f"Invalid runtime asset ID: {asset_id!r}")
1122
+ if asset_id in runtime_assets:
1123
  raise RegistryError(f"Duplicate runtime asset ID: {asset_id!r}")
1124
  repository = _require_str(value, "repository", context)
1125
  if _REPOSITORY_ID_RE.fullmatch(repository) is None:
 
1165
  if identity in identities:
1166
  raise RegistryError(f"Duplicate runtime asset identity: {identity!r}")
1167
  identities.add(identity)
1168
+ runtime_assets[asset_id] = RuntimeAsset(
1169
  id=asset_id,
1170
  repository=repository,
1171
  revision=revision,
 
1177
  license_expression=license_expression,
1178
  offline_behavior=offline_behavior,
1179
  )
1180
+ return runtime_assets
1181
 
1182
 
1183
  def _parse_models(
 
1186
  ) -> dict[str, ModelSpec]:
1187
  if not isinstance(raw, list) or not raw:
1188
  raise RegistryError("The manifest must contain at least one [[models]] entry.")
1189
+ models: dict[str, ModelSpec] = {}
1190
  fast_repositories: set[str] = set()
1191
  for index, value in enumerate(raw):
1192
  context = f"models[{index}]"
 
1196
  model_id = _require_str(value, "id", context)
1197
  if _IDENTIFIER_RE.fullmatch(model_id) is None:
1198
  raise RegistryError(f"Invalid model ID: {model_id!r}")
1199
+ if model_id in models:
1200
  raise RegistryError(f"Duplicate model ID: {model_id!r}")
1201
  family_id = _require_str(value, "family", context)
1202
  if family_id not in families:
 
1279
  if not class_path.startswith("fastplms.") or class_path.count(".") < 2:
1280
  raise RegistryError(f"Invalid Python class path in {context}: {class_path!r}")
1281
  auto_map.append((auto_class, class_path))
1282
+ models[model_id] = ModelSpec(
1283
  id=model_id,
1284
  family=family,
1285
  fast=fast,
 
1295
  notes=notes,
1296
  msa_conditioning=msa_conditioning,
1297
  )
1298
+ return models
1299
 
1300
 
1301
  def _validate_registry(
 
1410
 
1411
  def _load_manifest_bytes(raw_bytes: bytes) -> ModelRegistry:
1412
  try:
1413
+ manifest = tomllib.loads(raw_bytes.decode("utf-8"))
1414
  except (UnicodeDecodeError, tomllib.TOMLDecodeError) as error:
1415
  raise RegistryError(f"Unable to parse model manifest: {error}") from error
1416
+ _reject_unknown_fields(manifest, _ROOT_FIELDS, "manifest")
1417
+ if manifest.get("schema_version") != 1:
1418
  raise RegistryError("Unsupported model manifest schema_version; expected 1.")
1419
+ legal_files = _require_digest_list(manifest, "legal_files", "manifest")
1420
  required_legal_paths = {"LICENSE", "THIRD_PARTY_NOTICES.md"}
1421
  if {item.path for item in legal_files} != required_legal_paths:
1422
  raise RegistryError("manifest.legal_files must contain LICENSE and THIRD_PARTY_NOTICES.md.")
1423
+ attention_kernels = _parse_attention_kernels(manifest.get("attention_kernels"))
1424
+ upstreams = _parse_upstreams(manifest.get("upstreams"))
1425
+ families = _parse_families(manifest.get("families"), upstreams)
1426
+ runtime_assets = _parse_runtime_assets(manifest.get("runtime_assets"), families)
1427
+ models = _parse_models(manifest.get("models"), families)
1428
  _validate_registry(upstreams, attention_kernels, families, models)
1429
  return ModelRegistry(
1430
  schema_version=1,
fastplms/runtime.py CHANGED
@@ -11,6 +11,7 @@ from contextlib import contextmanager
11
  from dataclasses import dataclass
12
  from typing import TYPE_CHECKING, Literal
13
 
 
14
  if TYPE_CHECKING:
15
  from collections.abc import Iterator
16
 
 
11
  from dataclasses import dataclass
12
  from typing import TYPE_CHECKING, Literal
13
 
14
+
15
  if TYPE_CHECKING:
16
  from collections.abc import Iterator
17
 
fastplms_bundle.py CHANGED
The diff for this file is too large to render. See raw diff
 
modeling_fastplms.py CHANGED
@@ -1,4 +1,4 @@
1
- """Generated bridge to the unchanged FastPLMs package sources."""
2
 
3
  import base64
4
  import hashlib
@@ -6,14 +6,13 @@ import importlib
6
  import importlib.util
7
  import sys
8
  import tempfile
9
- from importlib.metadata import PackageNotFoundError, distribution
10
  from io import BytesIO
11
  from pathlib import Path
12
  from zipfile import ZIP_DEFLATED, ZipFile
13
 
14
  from .fastplms_bundle import RUNTIME_DATA, RUNTIME_HASH
15
 
16
- if RUNTIME_HASH != "405e7dddef77117e55232e1112faf088db88bb9b13e212a63b3318c4bc0d5a03":
17
  raise RuntimeError("FastPLMs runtime identity differs from the bridge.")
18
 
19
  _RUNTIME_TEMPORARIES = []
@@ -83,24 +82,6 @@ def _runtime_file_hashes(package_root):
83
  result[relative.as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
84
  return result
85
 
86
- def _installed_runtime_digest(installed_root, relative):
87
- candidate = installed_root / relative
88
- if candidate.is_file():
89
- return hashlib.sha256(candidate.read_bytes()).hexdigest()
90
- if relative != "kernels.lock":
91
- return None
92
- try:
93
- installed_distribution = distribution("fastplms")
94
- except PackageNotFoundError:
95
- return None
96
- for entry in installed_distribution.files or ():
97
- normalized = str(entry).replace("\\", "/")
98
- if normalized.endswith(".dist-info/kernels.lock"):
99
- lock_path = Path(installed_distribution.locate_file(entry))
100
- if lock_path.is_file():
101
- return hashlib.sha256(lock_path.read_bytes()).hexdigest()
102
- return None
103
-
104
  def _extend_loaded_package_paths(package_root):
105
  for name, module in list(sys.modules.items()):
106
  if name != "fastplms" and not name.startswith("fastplms."):
@@ -114,29 +95,14 @@ def _extend_loaded_package_paths(package_root):
114
  if candidate.is_dir() and candidate_text not in paths:
115
  paths.append(candidate_text)
116
 
117
- def _merge_runtime(installed, package_root):
118
  incoming = _runtime_file_hashes(package_root)
119
- known = dict(getattr(installed, "__fastplms_artifact_runtime_files__", {}))
120
- installed_root_text = getattr(
121
- installed, "__fastplms_artifact_installed_root__", None
122
- )
123
- if not known:
124
- installed_file = getattr(installed, "__file__", None)
125
- if installed_file is None:
126
- raise RuntimeError(
127
- "The loaded fastplms package has no source path and cannot be verified "
128
- "against the embedded artifact runtime."
129
- )
130
- installed_root = Path(installed_file).resolve().parent
131
- for relative, digest in incoming.items():
132
- if _installed_runtime_digest(installed_root, relative) != digest:
133
- raise RuntimeError(
134
- "The installed FastPLMs runtime differs from this artifact at "
135
- f"{relative!r}. Install the artifact's matching FastPLMs release "
136
- "or use a separate Python process."
137
- )
138
- installed_root_text = str(installed_root)
139
- installed.__fastplms_artifact_installed_root__ = installed_root_text
140
  conflicts = sorted(
141
  relative
142
  for relative, digest in incoming.items()
@@ -148,35 +114,25 @@ def _merge_runtime(installed, package_root):
148
  + ", ".join(repr(path) for path in conflicts[:5])
149
  + ". Load incompatible releases in separate Python processes."
150
  )
151
- if installed_root_text is not None:
152
- installed_root = Path(installed_root_text)
153
- for relative, digest in incoming.items():
154
- if relative in known:
155
- continue
156
- if _installed_runtime_digest(installed_root, relative) != digest:
157
- raise RuntimeError(
158
- "The installed FastPLMs runtime differs from this artifact at "
159
- f"{relative!r}. Install the artifact's matching FastPLMs release "
160
- "or use a separate Python process."
161
- )
162
  known.update(incoming)
163
- installed.__fastplms_artifact_runtime_files__ = known
164
- roots = list(getattr(installed, "__fastplms_artifact_runtime_roots__", ()))
165
  if str(package_root) not in roots:
166
  roots.append(str(package_root))
167
- installed.__fastplms_artifact_runtime_roots__ = tuple(roots)
168
  temporaries = list(
169
- getattr(installed, "__fastplms_artifact_runtime_temporaries__", ())
170
  )
171
  for temporary in _RUNTIME_TEMPORARIES:
172
  if temporary not in temporaries:
173
  temporaries.append(temporary)
174
- installed.__fastplms_artifact_runtime_temporaries__ = tuple(temporaries)
175
- hashes = set(getattr(installed, "__fastplms_artifact_runtime_hashes__", ()))
176
  hashes.add(RUNTIME_HASH)
177
- installed.__fastplms_artifact_runtime_hashes__ = frozenset(hashes)
178
  _extend_loaded_package_paths(package_root)
179
- return installed
180
 
181
  def _import_without_bytecode(module_name):
182
  previous = sys.dont_write_bytecode
@@ -187,13 +143,13 @@ def _import_without_bytecode(module_name):
187
  sys.dont_write_bytecode = previous
188
 
189
  def _install_runtime():
190
- installed = sys.modules.get("fastplms")
191
- hashes = getattr(installed, "__fastplms_artifact_runtime_hashes__", ())
192
  if RUNTIME_HASH in hashes:
193
- return installed
194
  package_root = _ensure_runtime()
195
- if installed is not None:
196
- return _merge_runtime(installed, package_root)
197
  spec = importlib.util.spec_from_file_location(
198
  "fastplms",
199
  package_root / "__init__.py",
@@ -223,16 +179,16 @@ def _install_runtime():
223
  return package
224
 
225
  _install_runtime()
226
- _module_225 = _import_without_bytecode("fastplms.models.ankh.modeling_ankh")
227
- FastAnkhConfig = _module_225.FastAnkhConfig
228
  FastAnkhConfig.__module__ = __name__
229
- FastAnkhForConditionalGeneration = _module_225.FastAnkhForConditionalGeneration
230
  FastAnkhForConditionalGeneration.__module__ = __name__
231
- FastAnkhForMaskedLMExtension = _module_225.FastAnkhForMaskedLMExtension
232
  FastAnkhForMaskedLMExtension.__module__ = __name__
233
- FastAnkhForSequenceClassification = _module_225.FastAnkhForSequenceClassification
234
  FastAnkhForSequenceClassification.__module__ = __name__
235
- FastAnkhForTokenClassification = _module_225.FastAnkhForTokenClassification
236
  FastAnkhForTokenClassification.__module__ = __name__
237
- FastAnkhModel = _module_225.FastAnkhModel
238
  FastAnkhModel.__module__ = __name__
 
1
+ """Generated bridge to the embedded FastPLMs runtime sources."""
2
 
3
  import base64
4
  import hashlib
 
6
  import importlib.util
7
  import sys
8
  import tempfile
 
9
  from io import BytesIO
10
  from pathlib import Path
11
  from zipfile import ZIP_DEFLATED, ZipFile
12
 
13
  from .fastplms_bundle import RUNTIME_DATA, RUNTIME_HASH
14
 
15
+ if RUNTIME_HASH != "437c5f5dcc809678e99b8e36d962e78b7960170c81d0b6289d2baeef7920e40f":
16
  raise RuntimeError("FastPLMs runtime identity differs from the bridge.")
17
 
18
  _RUNTIME_TEMPORARIES = []
 
82
  result[relative.as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
83
  return result
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  def _extend_loaded_package_paths(package_root):
86
  for name, module in list(sys.modules.items()):
87
  if name != "fastplms" and not name.startswith("fastplms."):
 
95
  if candidate.is_dir() and candidate_text not in paths:
96
  paths.append(candidate_text)
97
 
98
+ def _merge_runtime(package, package_root):
99
  incoming = _runtime_file_hashes(package_root)
100
+ known = getattr(package, "__fastplms_artifact_runtime_files__", None)
101
+ if not isinstance(known, dict):
102
+ raise RuntimeError(
103
+ "A non-artifact fastplms module is already loaded. Load the Hub artifact "
104
+ "in a separate Python process."
105
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  conflicts = sorted(
107
  relative
108
  for relative, digest in incoming.items()
 
114
  + ", ".join(repr(path) for path in conflicts[:5])
115
  + ". Load incompatible releases in separate Python processes."
116
  )
117
+ known = dict(known)
 
 
 
 
 
 
 
 
 
 
118
  known.update(incoming)
119
+ package.__fastplms_artifact_runtime_files__ = known
120
+ roots = list(getattr(package, "__fastplms_artifact_runtime_roots__", ()))
121
  if str(package_root) not in roots:
122
  roots.append(str(package_root))
123
+ package.__fastplms_artifact_runtime_roots__ = tuple(roots)
124
  temporaries = list(
125
+ getattr(package, "__fastplms_artifact_runtime_temporaries__", ())
126
  )
127
  for temporary in _RUNTIME_TEMPORARIES:
128
  if temporary not in temporaries:
129
  temporaries.append(temporary)
130
+ package.__fastplms_artifact_runtime_temporaries__ = tuple(temporaries)
131
+ hashes = set(getattr(package, "__fastplms_artifact_runtime_hashes__", ()))
132
  hashes.add(RUNTIME_HASH)
133
+ package.__fastplms_artifact_runtime_hashes__ = frozenset(hashes)
134
  _extend_loaded_package_paths(package_root)
135
+ return package
136
 
137
  def _import_without_bytecode(module_name):
138
  previous = sys.dont_write_bytecode
 
143
  sys.dont_write_bytecode = previous
144
 
145
  def _install_runtime():
146
+ package = sys.modules.get("fastplms")
147
+ hashes = getattr(package, "__fastplms_artifact_runtime_hashes__", ())
148
  if RUNTIME_HASH in hashes:
149
+ return package
150
  package_root = _ensure_runtime()
151
+ if package is not None:
152
+ return _merge_runtime(package, package_root)
153
  spec = importlib.util.spec_from_file_location(
154
  "fastplms",
155
  package_root / "__init__.py",
 
179
  return package
180
 
181
  _install_runtime()
182
+ _module_181 = _import_without_bytecode("fastplms.models.ankh.modeling_ankh")
183
+ FastAnkhConfig = _module_181.FastAnkhConfig
184
  FastAnkhConfig.__module__ = __name__
185
+ FastAnkhForConditionalGeneration = _module_181.FastAnkhForConditionalGeneration
186
  FastAnkhForConditionalGeneration.__module__ = __name__
187
+ FastAnkhForMaskedLMExtension = _module_181.FastAnkhForMaskedLMExtension
188
  FastAnkhForMaskedLMExtension.__module__ = __name__
189
+ FastAnkhForSequenceClassification = _module_181.FastAnkhForSequenceClassification
190
  FastAnkhForSequenceClassification.__module__ = __name__
191
+ FastAnkhForTokenClassification = _module_181.FastAnkhForTokenClassification
192
  FastAnkhForTokenClassification.__module__ = __name__
193
+ FastAnkhModel = _module_181.FastAnkhModel
194
  FastAnkhModel.__module__ = __name__
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Direct runtime dependencies for Synthyra/ANKH2_large.
2
+ # FastPLMs source is embedded in this model repository.
3
+ torch>=2.13,<2.14
4
+ transformers>=5.13,<5.14
5
+ huggingface-hub>=0.34,<2
6
+ tokenizers>=0.22,<0.23
7
+ safetensors>=0.5,<1
8
+ numpy>=1.26,<3
9
+ einops>=0.8,<1
10
+ tqdm>=4.67,<5
runtime-attestation.json CHANGED
@@ -2,41 +2,42 @@
2
  "files": {
3
  "LICENSES/FastPLMs-Apache-2.0.txt": "sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
4
  "LICENSES/ankh/LICENSE.md": "sha256:cd041d7f9f52936e8824ac3f754e9c67410763205fc8a7020ba74fc8b6edc088",
5
- "README.md": "sha256:87671dd466292a782bedf8dfe95bc595f03450e253243f2da217b3898acfc182",
6
  "THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
7
- "config.json": "sha256:0ba3a92b852ff36f7658a466903ee970ea45a59c9c555fe87b119607bbc32ff2",
8
- "fastplms/__init__.py": "sha256:4fb3196022ca8ec699d59d09bdbc5f0184195552b773698ab9b061fe3cd7df12",
9
- "fastplms/attention/__init__.py": "sha256:f60b9fecfb4bcb37a4e7c26dc2f752b9035f9cbad627b4a84213f3a92ec88f7d",
10
- "fastplms/attention/_core.py": "sha256:8f7ec5b65bd8b6c6fa4951d50d1c0e499abf03ae00914794b51fc410201e3e33",
11
- "fastplms/attention/_kernel_lock.py": "sha256:85d8521a2af5f94fad3948af3814db0c866c414ee4d43df797b9bd6f980e947b",
12
- "fastplms/attention/interfaces.py": "sha256:1c6f06a8e411e0f9bf6d230522205c93ae46ea58864006fbb892aa05e5ca5749",
13
- "fastplms/embeddings/__init__.py": "sha256:47ff8cdf682d44037dd9edab133e2e60675d60786bd5cf0bffd1998f31985555",
14
- "fastplms/embeddings/pooling.py": "sha256:a140266ed6b1cc344c8507edc5c6c4f2dce464c3db70ba4b16c7ac2ba2fad96e",
15
- "fastplms/embeddings/runner.py": "sha256:23ee4727a918d6d331f7a0f89b823d149f1a791f0c5586e3496d7b6eb2ce97e0",
16
- "fastplms/embeddings/storage.py": "sha256:3fbe2bab75092e5a4cadf4d27e4752181d597469a65a55db085ceef808ed418e",
17
- "fastplms/embeddings/types.py": "sha256:119718a20989d1ae5a60fabc0f5e98bdc172c5163b04db3d4554ac3956b30e52",
18
  "fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
19
- "fastplms/models/__init__.py": "sha256:5e48c2cb3877aa6f42f3b5411d53b16bba2e32827bbde634f47f174c5cb36f86",
20
  "fastplms/models/ankh/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
21
- "fastplms/models/ankh/modeling_ankh.py": "sha256:b114c73e4aaedc567e3351b425ddddacf35661f09b3414cf59b6db0b712c82c3",
22
- "fastplms/models/ttt.py": "sha256:a0df4e98b02120d423e3c7ca9b866a8d0e3748b9076042a0102a838a11aed046",
23
- "fastplms/registry.py": "sha256:afca271911b651a882345b74a58366494a1d784e4a48c8683a87f5508f4ba16e",
24
- "fastplms/runtime.py": "sha256:110018646d6f248cedab140a030c3065e1b062b61f6aff659c231e538614bc01",
25
- "fastplms_bundle.py": "sha256:a06395708d58c64035ff39e6e38280d6fcb733d223de3709a21f1a5c87cd97b7",
26
- "modeling_fastplms.py": "sha256:ccf1ac016e0fdcaaed4376db29e5b29d843d776c48c977f1006edfaab51c8307",
 
27
  "special_tokens_map.json": "sha256:c8995d2f8037fe3a8cfdef30475365e1c314c417880b75abaf8296e3c05d42d6",
28
  "tokenizer.json": "sha256:b4533f607d9fd665f2d9d94b0cf71870a6fc2fc2ae7cbd516d0e43a9efb406fd",
29
  "tokenizer_config.json": "sha256:dba1f9315ded007fc2e5bcb2ed4bfb6fff81c9becdc97b73a5713ba5ddc01afc"
30
  },
31
  "model_id": "ankh2_large",
32
  "redistributable": true,
33
- "release_tool_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
34
- "release_tool_sha256": "19b4247aba04a74de5069397518374ab2c3b9e0ce18ba9f36de34f0f4c5d1ceb",
35
- "runtime_bundle_sha256": "405e7dddef77117e55232e1112faf088db88bb9b13e212a63b3318c4bc0d5a03",
36
- "runtime_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
37
  "schema_version": 2,
38
  "scope": "runtime-only",
39
- "source_tree_sha256": "e377aef8902771bb8bdabcae9c8853ab50de04d85cacd3d75c6e83508f6d52ed",
40
  "weights": {
41
  "repo_id": "Synthyra/ANKH2_large",
42
  "revision": "729167c1980316ae61691338838447491926033f"
 
2
  "files": {
3
  "LICENSES/FastPLMs-Apache-2.0.txt": "sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
4
  "LICENSES/ankh/LICENSE.md": "sha256:cd041d7f9f52936e8824ac3f754e9c67410763205fc8a7020ba74fc8b6edc088",
5
+ "README.md": "sha256:131315548dc250a4359361d16d9355080d1598c8bff6fc9902abe68c9e966cb2",
6
  "THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
7
+ "config.json": "sha256:c2e2327b8765473cbe9004169bbd860ff7dca916cfd66653c4a08c6c3a419bce",
8
+ "fastplms/__init__.py": "sha256:8503e02debf24abc6d33c1913ff3eb9f245e63f7e62017e220d072c7393fdf2c",
9
+ "fastplms/attention/__init__.py": "sha256:ab3c0b6156968f418f3c97ea664959cb978f4137717b4362213bcfe25ab5e3d6",
10
+ "fastplms/attention/_core.py": "sha256:09d0f71a5ea2ad2138c9441f6a2f72bbf8e72729be41d8db737b163f36ce47da",
11
+ "fastplms/attention/_kernel_lock.py": "sha256:56455eb57cee9af08438eda52b775c9cff3cf994cadb9b116ce2ee10fd5f1801",
12
+ "fastplms/attention/interfaces.py": "sha256:6562a342961022fc2848d7004bd01fddaf8be8e08e782d0856e5c30a74232623",
13
+ "fastplms/embeddings/__init__.py": "sha256:cf0648de905a00ad16c0aece328eeb05f25739a4cf89b47b915e0f2b19c3ce26",
14
+ "fastplms/embeddings/pooling.py": "sha256:9bed8c466fce053333de9e25b04d1ab304f2954f403ffa0f5a3c749e8277b3ce",
15
+ "fastplms/embeddings/runner.py": "sha256:a7839763ad641a63d0e2671dcf34aa7b2a8114198950b64254188521b27453a8",
16
+ "fastplms/embeddings/storage.py": "sha256:98cf725d26766f32300f105345129ca6cf02ebd10628e746a7e71e107bc911a6",
17
+ "fastplms/embeddings/types.py": "sha256:c58f8c6a37a11a937c3f77b4e1617d5f213923d98ecaffddcdd283bd9dc42fe7",
18
  "fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
19
+ "fastplms/models/__init__.py": "sha256:d3dd084f5e8fafcf2d0fe4a26e4ab9284d3f9414b1f9a5bd86e308a406447c43",
20
  "fastplms/models/ankh/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
21
+ "fastplms/models/ankh/modeling_ankh.py": "sha256:9ff235c9bd529a4c2ba63293376e6479161d364e7a4884c4063a0eb3ed1c5719",
22
+ "fastplms/models/ttt.py": "sha256:f1d7f9298a930f4c40f8e321ffa08b09d1c5b8492bca623bdc7bfd03ec85774d",
23
+ "fastplms/registry.py": "sha256:cb37799dae5a4c0c780089ab2cca4cc56300c1c5f515b275892aa4e1c4b97eb6",
24
+ "fastplms/runtime.py": "sha256:9521c37fcd168bf9a4137277fb73672403ccf30d70d4845aa879fb22f42ddeb6",
25
+ "fastplms_bundle.py": "sha256:af54f8786c1124b45e07ef857486e14163bb79266fa98fc2002a82296ce89fbb",
26
+ "modeling_fastplms.py": "sha256:37b0ea758dc1a50e7ca7225a175ca983e109efe4342759055c58520ef271f49f",
27
+ "requirements.txt": "sha256:3ae7e085e6ec8ed936981f14e58d1199b4a26ba79cf90cb5d01585946ad8a96a",
28
  "special_tokens_map.json": "sha256:c8995d2f8037fe3a8cfdef30475365e1c314c417880b75abaf8296e3c05d42d6",
29
  "tokenizer.json": "sha256:b4533f607d9fd665f2d9d94b0cf71870a6fc2fc2ae7cbd516d0e43a9efb406fd",
30
  "tokenizer_config.json": "sha256:dba1f9315ded007fc2e5bcb2ed4bfb6fff81c9becdc97b73a5713ba5ddc01afc"
31
  },
32
  "model_id": "ankh2_large",
33
  "redistributable": true,
34
+ "release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
35
+ "release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
36
+ "runtime_bundle_sha256": "437c5f5dcc809678e99b8e36d962e78b7960170c81d0b6289d2baeef7920e40f",
37
+ "runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
38
  "schema_version": 2,
39
  "scope": "runtime-only",
40
+ "source_tree_sha256": "44f108a32fffbe0e689434fbff109aea7d03a7085a550ed5771f2eac434f130d",
41
  "weights": {
42
  "repo_id": "Synthyra/ANKH2_large",
43
  "revision": "729167c1980316ae61691338838447491926033f"