lhallee commited on
Commit
faef572
·
verified ·
1 Parent(s): 11a8969

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
@@ -19,15 +19,34 @@ Supported Transformers entry points are `AutoConfig`, `AutoModel`,
19
  `AutoModelForMaskedLM`, `AutoModelForSequenceClassification`,
20
  `AutoModelForTokenClassification`.
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  ## Install and platform requirements
23
 
24
- Install the current FastPLMs package:
25
 
26
  ```bash
27
- python -m pip install \
28
- "fastplms @ git+https://github.com/Synthyra/FastPLMs.git"
29
  ```
30
 
 
 
 
31
  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
32
  access on first download. For an air-gapped run, first build the manifest-pinned
33
  local artifact and use the offline form shown in the example.
@@ -41,62 +60,137 @@ model_id = "Synthyra/Profluent-E1-600M"
41
  model = AutoModel.from_pretrained(
42
  model_id,
43
  trust_remote_code=True,
 
44
  ).eval()
45
  ```
46
 
47
- This example uses the published Hub repository. For offline validation, build
48
- the manifest-pinned artifact and replace `model_id` with its local
49
- `dist/hub/Profluent-E1-600M` path, then pass `local_files_only=True`.
 
50
 
51
- Leave attention unspecified for the Transformers default. Supported explicit
52
- choices are `sdpa`, `flex_attention`.
53
- Pass the selected name through `attn_implementation`.
54
- When an optimized backend cannot return full attention tensors,
55
- `output_attentions=True` emits one explicit runtime warning and uses a correctly
56
- masked eager implementation for that call only. The warning identifies the
57
- configured backend, effective backend, and reason. Configuration and later
58
- calls are unchanged.
59
- For BF16 execution, this family uses parameters loaded directly in BF16.
60
 
61
  ## Dataset embeddings
62
 
63
- The shared embedding API accepts sequences, `(id, sequence)` pairs,
64
- `EmbeddingInput` records, insertion-ordered `{id: sequence}` mappings, or a
65
- FASTA path. Results preserve order and duplicate identifiers:
66
 
67
  ```python
68
- result = model.embed_dataset(
69
  ["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"],
70
  batch_size=2,
71
  pooling=("mean", "std"),
72
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- for record in result:
75
- print(record.id, record.sequence, record.tensor.shape)
 
 
 
76
  ```
77
 
78
- Set `full_embeddings=True` for one residue tensor with shape `(l, d)` per
79
- sequence. Set `output` to a directory for bounded-memory, transactional
80
- safetensors with ordered-prefix resume, or choose `format="sqlite"` for
81
- batch-level database commits and exact resume. Pooling excludes boundary,
82
- padding, and other non-biological positions.
83
 
84
- For a long FASTA run, stream completed batches into SQLite:
 
 
85
 
86
  ```python
87
- persisted = model.embed_dataset(
88
- "proteins.fasta",
89
- batch_size=64,
90
- pooling=("mean",),
91
- output="protein-embeddings.sqlite",
92
- format="sqlite",
93
- resume=True,
 
 
 
 
94
  )
95
  ```
96
 
97
- Resume verifies the input order, model state, tokenizer policy, backend, dtype,
98
- and pooling configuration. It never appends incompatible records to an
99
- existing run.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  ## Tokenizer-free E1 input
102
 
@@ -126,7 +220,7 @@ the attribution required by the upstream agreement.
126
  - Precision policies: `default`
127
  - BF16 execution: `static_parameters`
128
  - Generation contract: `not_applicable`
129
- - Optional dependency group: `core`
130
  - Weight publication allowed: `true`
131
  - Weight license status: `resolved`
132
  - Redistributable: `true`
@@ -137,28 +231,22 @@ the attribution required by the upstream agreement.
137
  - FastPLMs weights: `Synthyra/Profluent-E1-600M`
138
  - Runtime revision: recorded separately in the built artifact and published commit
139
  - Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
140
- - Generator/schema version and complete/runtime-only attestations: recorded in `provenance.json`
141
  - Official checkpoint: `Profluent-Bio/E1-600m`
142
  - Artifact source: `fast`
143
  - State transform: `e1_to_fastplms_v1`
144
- - BF16 execution: `static_parameters`
145
  - Pinned upstreams: `e1`
146
- - Reference container: `reference-e1`
147
  - Release tiers: `check`, `compliance`, `feature`, `artifact`, `benchmark`
148
  - Unresolved required file identities: `0`
149
 
150
- The local artifact records exact file identities, conversion details, source
151
- revisions, and legal texts in `provenance.json`. A nonzero unresolved count is a
152
- release blocker.
153
 
154
  ## Validation boundary
155
 
156
- For tiers declared by the manifest, the release contract compares applicable
157
- semantic configuration, tokenizer behavior, state keys, shapes, dtypes,
158
- values, aliases, and representative inference with the pinned official
159
- implementation. This metadata does not by itself claim that a particular build
160
- passed, that one backend is faster, or that an output has biological or
161
- therapeutic validity.
162
 
163
  ## License
164
 
 
19
  `AutoModelForMaskedLM`, `AutoModelForSequenceClassification`,
20
  `AutoModelForTokenClassification`.
21
 
22
+ ## Capabilities
23
+
24
+ | Feature | Status |
25
+ | --- | --- |
26
+ | Sequence classification | Supported: base weights with an untrained task head |
27
+ | Token classification | Supported: base weights with an untrained task head |
28
+ | PEFT fine-tuning | Supported pattern: preserve the separately trained `classifier` |
29
+ | Embeddings | Special: tokenizer-free raw-sequence preparation |
30
+ | Test-time training | Supported: low-rank masked-residue adaptation |
31
+ | Attention variants | Supported: `sdpa`, `flex_attention` |
32
+ | Compliance | Declared: exact release evidence is required |
33
+
34
+ A supported interface is not a pretrained downstream predictor. Classification
35
+ heads start untrained, and declared compliance metadata is not a claim that an
36
+ arbitrary local build passed its release gate.
37
+
38
  ## Install and platform requirements
39
 
40
+ Install the direct dependencies published with this model:
41
 
42
  ```bash
43
+ python -m pip install -r \
44
+ "https://huggingface.co/Synthyra/Profluent-E1-600M/resolve/main/requirements.txt"
45
  ```
46
 
47
+ The FastPLMs implementation itself is embedded in the model repository and loaded
48
+ by Transformers through `trust_remote_code=True`.
49
+
50
  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
51
  access on first download. For an air-gapped run, first build the manifest-pinned
52
  local artifact and use the offline form shown in the example.
 
60
  model = AutoModel.from_pretrained(
61
  model_id,
62
  trust_remote_code=True,
63
+ attn_implementation="sdpa",
64
  ).eval()
65
  ```
66
 
67
+ For offline validation, replace `model_id` with the manifest-built
68
+ `dist/hub/Profluent-E1-600M` path and pass `local_files_only=True`.
69
+
70
+ ## Attention and compliance
71
 
72
+ The quick start selects `sdpa` explicitly. Declared variants are `sdpa`, `flex_attention`. An unavailable requested
73
+ backend raises instead of silently switching implementations.
74
+ `output_attentions=True` may use the documented, one-call eager fallback solely
75
+ to materialize attention tensors; the configured backend remains unchanged.
76
+
77
+ This family declares the `compliance` tier. Release evidence binds the exact
78
+ checkpoint, backend, dtype, hardware, inputs, and reference revision.
 
 
79
 
80
  ## Dataset embeddings
81
 
82
+ The shared embedding mixin preserves input order and biological-position
83
+ masking. It accepts sequences, identified records, mappings, or a FASTA path:
 
84
 
85
  ```python
86
+ pooled = model.embed_dataset(
87
  ["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"],
88
  batch_size=2,
89
  pooling=("mean", "std"),
90
  )
91
+ residues = model.embed_dataset(
92
+ ["MSTNPKPQRKTKRNT"],
93
+ full_embeddings=True,
94
+ )
95
+ print(pooled[0].tensor.shape) # (2 * d,)
96
+ print(residues[0].tensor.shape) # (l, d)
97
+ ```
98
+
99
+ Set `output` and `format="safetensors"` or `"sqlite"` for transactional,
100
+ bounded-memory persistence. Resume verifies input order, model state, tokenizer
101
+ policy, backend, dtype, and pooling configuration before appending.
102
+
103
+ ## Downstream classification
104
+
105
+ Both downstream AutoClasses reuse the checkpoint backbone and initialize a new,
106
+ untrained `classifier`. Sequence labels have shape `(b,)`; residue labels have
107
+ shape `(b, l)` and use `-100` outside biological positions:
108
+
109
+ ```python
110
+ import torch
111
+ from transformers import (
112
+ AutoModelForSequenceClassification,
113
+ AutoModelForTokenClassification,
114
+ )
115
+
116
+ model_id = "Synthyra/Profluent-E1-600M"
117
+ sequence_model = AutoModelForSequenceClassification.from_pretrained(
118
+ model_id, num_labels=2, trust_remote_code=True
119
+ ).eval()
120
+ token_model = AutoModelForTokenClassification.from_pretrained(
121
+ model_id, num_labels=3, trust_remote_code=True
122
+ ).eval()
123
+ sequences = ["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"]
124
+ batch = sequence_model.prep_tokens.get_batch_kwargs(
125
+ sequences,
126
+ device=sequence_model.device,
127
+ )
128
+ biological = batch["sequence_ids"].ne(-1)
129
+
130
+ sequence_labels = torch.zeros(len(sequences), dtype=torch.long)
131
+ token_labels = torch.full_like(batch["input_ids"], -100)
132
+ token_labels[biological] = 0
133
 
134
+ with torch.inference_mode():
135
+ sequence_output = sequence_model(**batch, labels=sequence_labels)
136
+ token_output = token_model(**batch, labels=token_labels)
137
+ print(sequence_output.logits.shape) # (b, 2)
138
+ print(token_output.logits.shape) # (b, l, 3)
139
  ```
140
 
141
+ ## PEFT fine-tuning
142
+
143
+ Install the direct training dependencies, then attach LoRA to the loaded checkpoint:
 
 
144
 
145
+ ```bash
146
+ python -m pip install "datasets>=4.8,<5" "peft>=0.19,<0.20"
147
+ ```
148
 
149
  ```python
150
+ from peft import LoraConfig, TaskType, get_peft_model
151
+
152
+ peft_model = get_peft_model(
153
+ sequence_model,
154
+ LoraConfig(
155
+ task_type=TaskType.SEQ_CLS,
156
+ r=8,
157
+ lora_alpha=16,
158
+ target_modules="all-linear",
159
+ modules_to_save=["classifier"],
160
+ ),
161
  )
162
  ```
163
 
164
+ This checkpoint advertises a classification head, so the separately trained
165
+ `classifier` is saved with the adapter.
166
+ All FastPLMs checkpoints follow the Transformers `PreTrainedModel` contract and
167
+ can be adapted with PEFT. The ESM2-specific shipped CLI is an example, not a
168
+ support boundary. Record the target modules, base revision, data identity, and
169
+ trainable parameter scope.
170
+
171
+ ## Test-time training
172
+
173
+ TTT samples masked views of one protein and updates only injected low-rank
174
+ adapters. Base checkpoint weights remain frozen:
175
+
176
+ ```python
177
+ from transformers import AutoModelForMaskedLM
178
+
179
+ ttt_model = AutoModelForMaskedLM.from_pretrained(
180
+ "Synthyra/Profluent-E1-600M",
181
+ trust_remote_code=True,
182
+ )
183
+ metrics = ttt_model.ttt(
184
+ seq="MSTNPKPQRKTKRNT",
185
+ ttt_config={"steps": 3, "batch_size": 1, "seed": 7},
186
+ )
187
+ ttt_model.save_pretrained("adapted", safe_serialization=True)
188
+ ttt_model.ttt_reset()
189
+ print(metrics)
190
+ ```
191
+
192
+ Persisted adapters retain their deterministic reset state. TTT adds latency
193
+ and memory, can worsen an output, and does not establish biological function.
194
 
195
  ## Tokenizer-free E1 input
196
 
 
220
  - Precision policies: `default`
221
  - BF16 execution: `static_parameters`
222
  - Generation contract: `not_applicable`
223
+ - Artifact dependency set: `core`
224
  - Weight publication allowed: `true`
225
  - Weight license status: `resolved`
226
  - Redistributable: `true`
 
231
  - FastPLMs weights: `Synthyra/Profluent-E1-600M`
232
  - Runtime revision: recorded separately in the built artifact and published commit
233
  - Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
 
234
  - Official checkpoint: `Profluent-Bio/E1-600m`
235
  - Artifact source: `fast`
236
  - State transform: `e1_to_fastplms_v1`
 
237
  - Pinned upstreams: `e1`
 
238
  - Release tiers: `check`, `compliance`, `feature`, `artifact`, `benchmark`
239
  - Unresolved required file identities: `0`
240
 
241
+ `provenance.json` records exact file identities, conversion, source revisions,
242
+ legal texts, schema, and attestations. A nonzero unresolved count blocks release.
 
243
 
244
  ## Validation boundary
245
 
246
+ Declared tiers compare applicable configuration, tokenizer behavior, state,
247
+ and representative inference with the pinned reference. Metadata alone does
248
+ not claim a build passed, a backend is faster, or an output is biologically
249
+ valid.
 
 
250
 
251
  ## License
252
 
config.json CHANGED
@@ -18,11 +18,11 @@
18
  "fastplms_checkpoint_repo_id": "Synthyra/Profluent-E1-600M",
19
  "fastplms_checkpoint_revision": "6c8bf0ec83b0e0178677c528b101efffd0677742",
20
  "fastplms_model_id": "e1_600m",
21
- "fastplms_release_tool_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
22
- "fastplms_release_tool_sha256": "19b4247aba04a74de5069397518374ab2c3b9e0ce18ba9f36de34f0f4c5d1ceb",
23
- "fastplms_runtime_bundle_sha256": "5289065bf7679630038a2e794052e4364a152c4f103c36c80ab9b8544919e924",
24
- "fastplms_runtime_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
25
- "fastplms_source_tree_sha256": "c583bf72e969d20248d4f2a01c81579fffa3002e2e19c4e90acebbbacf1faeab",
26
  "fastplms_weights_revision": "6c8bf0ec83b0e0178677c528b101efffd0677742",
27
  "gated_mlp": true,
28
  "global_attention_every_n_layers": 3,
 
18
  "fastplms_checkpoint_repo_id": "Synthyra/Profluent-E1-600M",
19
  "fastplms_checkpoint_revision": "6c8bf0ec83b0e0178677c528b101efffd0677742",
20
  "fastplms_model_id": "e1_600m",
21
+ "fastplms_release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
22
+ "fastplms_release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
23
+ "fastplms_runtime_bundle_sha256": "a8dda20d6a4a55e6808c7c2f2c9477c113edaa7d6ec6ef57f4a898cd47b73d48",
24
+ "fastplms_runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
25
+ "fastplms_source_tree_sha256": "9e3cbab448ffde4fbbba79f17cf0321645584acd534c5f96c78b1017d9f3806b",
26
  "fastplms_weights_revision": "6c8bf0ec83b0e0178677c528b101efffd0677742",
27
  "gated_mlp": true,
28
  "global_attention_every_n_layers": 3,
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/e1/attention.py CHANGED
@@ -3,9 +3,8 @@
3
  from __future__ import annotations
4
 
5
  import os
6
- from collections.abc import Callable
7
-
8
  import torch
 
9
  from torch.nn.attention.flex_attention import _create_sparse_block_from_block_mask
10
 
11
  from fastplms.attention import (
@@ -23,6 +22,7 @@ from fastplms.attention import (
23
 
24
  @torch.compiler.disable
25
  def create_block_causal_mask_optimized(sequence_ids: torch.Tensor) -> BlockMask:
 
26
  if create_block_mask is None:
27
  raise RuntimeError("Flex Attention block-mask creation is unavailable in this environment.")
28
  # Assumes sequence_ids is sorted in increasing order for each batch item, except for
@@ -42,6 +42,7 @@ def create_block_causal_mask_optimized(sequence_ids: torch.Tensor) -> BlockMask:
42
 
43
  @torch.compiler.disable
44
  def create_within_seq_block_mask(sequence_ids: torch.Tensor) -> BlockMask:
 
45
  if create_block_mask is None:
46
  raise RuntimeError("Flex Attention block-mask creation is unavailable in this environment.")
47
  def document_mask(b, h, q_idx, kv_idx): # type: ignore[no-untyped-def]
@@ -58,17 +59,19 @@ def create_within_seq_block_mask(sequence_ids: torch.Tensor) -> BlockMask:
58
 
59
 
60
  def build_within_seq_mask_4d(sequence_ids: torch.Tensor) -> torch.Tensor:
61
- not_pad = sequence_ids != -1
62
- same_seq = sequence_ids.unsqueeze(-1) == sequence_ids.unsqueeze(-2)
63
- valid = not_pad.unsqueeze(-1) & not_pad.unsqueeze(-2)
64
- return (same_seq & valid).unsqueeze(1)
 
65
 
66
 
67
  def build_block_causal_mask_4d(sequence_ids: torch.Tensor) -> torch.Tensor:
68
- not_pad = sequence_ids != -1
69
- causal = sequence_ids.unsqueeze(-1) >= sequence_ids.unsqueeze(-2)
70
- valid = not_pad.unsqueeze(-1) & not_pad.unsqueeze(-2)
71
- return (causal & valid).unsqueeze(1)
 
72
 
73
 
74
  def flex_attention_func(
@@ -84,9 +87,9 @@ def flex_attention_func(
84
  raise RuntimeError("Flex Attention is not available in this environment.")
85
  if score_mod is not None:
86
  raise NotImplementedError("E1 Flex Attention does not support score_mod.")
87
- query_states = query_states.transpose(1, 2).contiguous() # (bs, nh, seqlen, hs)
88
- key_states = key_states.transpose(1, 2).contiguous() # (bs, nkv, seqlen, hs)
89
- value_states = value_states.transpose(1, 2).contiguous() # (bs, nkv, seqlen, hs)
90
 
91
  fn = _get_flex_attention_fn(
92
  device=query_states.device,
@@ -97,7 +100,7 @@ def flex_attention_func(
97
  )
98
  if fn is None:
99
  raise RuntimeError("Flex Attention is not available in this environment.")
100
- outputs = fn(
101
  query_states,
102
  key_states,
103
  value_states,
@@ -106,19 +109,20 @@ def flex_attention_func(
106
  enable_gqa=query_states.shape[1] != key_states.shape[1], # if nkv != nh
107
  )
108
 
109
- outputs = outputs.transpose(1, 2) # (bs, seqlen, nh, hs)
110
- return outputs
111
 
112
 
113
  def kernels_flash_attention_func(
114
- query_states: torch.Tensor, # (bs, seqlen, nh, hs)
115
- key_states: torch.Tensor, # (bs, seqlen, nkv, hs)
116
- value_states: torch.Tensor, # (bs, seqlen, nkv, hs)
117
  q_sequence_ids: torch.Tensor,
118
  k_sequence_ids: torch.Tensor,
119
  causal: bool = False,
120
  implementation: str = "flash_attention_3",
121
- ) -> torch.Tensor: # (bs, seqlen, nh, hs)
 
122
  _ensure_flash_kernels_loaded(implementation)
123
 
124
  if not causal:
@@ -130,9 +134,15 @@ def kernels_flash_attention_func(
130
  indices_q,
131
  (cu_seqlens_q, cu_seqlens_k),
132
  (max_seqlen_in_batch_q, max_seqlen_in_batch_k),
133
- ) = _unpad_input(query_states, key_states, value_states, q_sequence_ids, k_sequence_ids)
 
 
 
 
 
 
134
 
135
- attn_output_unpad = _kernels_flash_varlen_forward(
136
  query_states,
137
  key_states,
138
  value_states,
@@ -143,14 +153,19 @@ def kernels_flash_attention_func(
143
  causal=False,
144
  implementation=implementation,
145
  )
146
- attn_output = pad_input(attn_output_unpad, indices_q, batch_size, q_len)
 
 
 
 
 
147
 
148
  else:
149
- attn_output = _kernels_flash_forward(
150
  query_states, key_states, value_states, causal=True, implementation=implementation
151
  )
152
 
153
- return attn_output
154
 
155
 
156
  def block_min_max_seq_ids(
@@ -159,7 +174,8 @@ def block_min_max_seq_ids(
159
  ) -> tuple[torch.Tensor, torch.Tensor]:
160
  """Map each physical attention block to its first and last sequence."""
161
 
162
- total_tokens = sequence_lengths.sum()
 
163
  block_count = int(
164
  torch.div(
165
  total_tokens + block_size - 1,
@@ -167,22 +183,22 @@ def block_min_max_seq_ids(
167
  rounding_mode="floor",
168
  ).item()
169
  )
170
- padded_tokens = block_count * block_size - total_tokens
171
- lengths_with_tail = torch.cat(
172
  (sequence_lengths, padded_tokens.to(sequence_lengths).reshape(1)),
173
  )
174
- sequence_ends = lengths_with_tail.to(torch.long).cumsum(dim=0)
175
- block_starts = torch.arange(
176
  start=0,
177
  end=block_count * block_size,
178
  step=block_size,
179
  dtype=torch.long,
180
  device=sequence_lengths.device,
181
  )
182
- block_last_tokens = block_starts + block_size - 1
183
- first_sequence = torch.searchsorted(sequence_ends, block_starts, right=True)
184
- last_sequence = torch.searchsorted(sequence_ends, block_last_tokens, right=True)
185
- return first_sequence, last_sequence
186
 
187
 
188
  def get_overlapping_blocks(
@@ -191,32 +207,42 @@ def get_overlapping_blocks(
191
  ) -> tuple[torch.Tensor, torch.Tensor]:
192
  """Classify query/key block pairs as full, partial, or disjoint."""
193
 
194
- q_first, q_last = block_min_max_seq_ids(q_lengths)
195
- k_first, k_last = block_min_max_seq_ids(k_lengths)
196
- intersection_start = torch.maximum(q_first[:, None], k_first[None, :])
197
- intersection_end = torch.minimum(q_last[:, None], k_last[None, :])
198
- intersects = intersection_start <= intersection_end
199
- both_blocks_are_single_sequence = (q_first == q_last)[:, None] & (k_first == k_last)[None, :]
200
- full_blocks = intersects & both_blocks_are_single_sequence
201
- return full_blocks, intersects & ~both_blocks_are_single_sequence
 
 
 
 
 
 
 
 
 
202
 
203
 
204
  def _document_ids(sequence_lengths: torch.Tensor) -> torch.Tensor:
205
- sequence_numbers = torch.arange(
 
206
  sequence_lengths.numel(),
207
  device=sequence_lengths.device,
208
  dtype=torch.long,
209
  )
210
- return sequence_numbers.repeat_interleave(sequence_lengths.to(torch.long))
211
 
212
 
213
  @torch.compiler.disable
214
  def direct_block_mask(q_lengths: torch.Tensor, k_lengths: torch.Tensor) -> BlockMask:
215
  """Build a packed-sequence mask from preclassified sparse blocks."""
216
 
217
- full, partial = get_overlapping_blocks(q_lengths, k_lengths)
218
- q_document = _document_ids(q_lengths)
219
- k_document = _document_ids(k_lengths)
220
 
221
  def same_document(
222
  _batch: torch.Tensor,
@@ -239,8 +265,8 @@ def direct_block_mask(q_lengths: torch.Tensor, k_lengths: torch.Tensor) -> Block
239
  def doc_id_mask(q_lengths: torch.Tensor, k_lengths: torch.Tensor) -> BlockMask:
240
  if create_block_mask is None:
241
  raise RuntimeError("Flex Attention block-mask creation is unavailable in this environment.")
242
- q_document = _document_ids(q_lengths)
243
- k_document = _document_ids(k_lengths)
244
 
245
  def same_document(
246
  _batch: torch.Tensor,
@@ -268,6 +294,8 @@ def varlen_flex_attention_func(
268
  q_sequence_ids: torch.Tensor,
269
  k_sequence_ids: torch.Tensor,
270
  ) -> torch.Tensor:
 
 
271
  if flex_attention is None:
272
  raise RuntimeError("Flex Attention is not available in this environment.")
273
  batch_size, q_len = query_states.shape[0], query_states.shape[1]
@@ -278,14 +306,20 @@ def varlen_flex_attention_func(
278
  indices_q,
279
  (cu_seqlens_q, cu_seqlens_k),
280
  (_max_seqlen_in_batch_q, _max_seqlen_in_batch_k),
281
- ) = _unpad_input(query_states, key_states, value_states, q_sequence_ids, k_sequence_ids)
 
 
 
 
 
 
282
 
283
- query_states = query_states.unsqueeze(0).transpose(1, 2).contiguous()
284
- key_states = key_states.unsqueeze(0).transpose(1, 2).contiguous()
285
- value_states = value_states.unsqueeze(0).transpose(1, 2).contiguous()
286
 
287
- seqlens_q = cu_seqlens_q[1:] - cu_seqlens_q[:-1]
288
- seqlens_k = cu_seqlens_k[1:] - cu_seqlens_k[:-1]
289
  block_mask = block_mask_creator(seqlens_q, seqlens_k)
290
 
291
  packed_lengths = (
@@ -302,7 +336,7 @@ def varlen_flex_attention_func(
302
  )
303
  if fn is None:
304
  raise RuntimeError("Flex Attention is not available in this environment.")
305
- attn_output_unpad = fn(
306
  query_states,
307
  key_states,
308
  value_states,
@@ -310,39 +344,44 @@ def varlen_flex_attention_func(
310
  enable_gqa=query_states.shape[1] != key_states.shape[1],
311
  )
312
 
313
- attn_output = pad_input(
314
  attn_output_unpad.transpose(1, 2).squeeze(0), indices_q, batch_size, q_len
315
  )
316
 
317
- return attn_output
318
 
319
 
320
  def _get_unpad_data(sequence_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, int]:
321
  """Return packed indices and run lengths for the non-padding sequence IDs."""
322
 
323
- flat_ids = sequence_ids.reshape(-1)
324
- non_pad_indices = torch.where(flat_ids.ne(-1))[0]
 
325
  if non_pad_indices.numel() == 0:
326
  raise ValueError("Packed attention requires at least one non-padding token.")
327
 
328
- valid_ids = flat_ids.index_select(0, non_pad_indices)
329
- row_ids = torch.div(
330
  non_pad_indices,
331
  sequence_ids.shape[1],
332
  rounding_mode="floor",
333
  )
334
- run_starts = torch.ones_like(valid_ids, dtype=torch.bool)
335
  run_starts[1:] = (valid_ids[1:] != valid_ids[:-1]) | (row_ids[1:] != row_ids[:-1])
336
- start_indices = torch.where(run_starts)[0]
337
- end_indices = torch.cat((start_indices[1:], start_indices.new_tensor([valid_ids.numel()])))
338
- sequence_lengths = end_indices - start_indices
339
- cumulative_lengths = torch.cat(
 
 
340
  (
341
  torch.zeros(1, dtype=torch.int32, device=sequence_ids.device),
342
  sequence_lengths.cumsum(dim=0, dtype=torch.int32),
343
  ),
344
  )
345
- return non_pad_indices, cumulative_lengths, int(sequence_lengths.max().item())
 
 
346
 
347
 
348
  def _unpad_input(
@@ -359,6 +398,8 @@ def _unpad_input(
359
  tuple[torch.Tensor, torch.Tensor],
360
  tuple[int, int],
361
  ]:
 
 
362
  for name, layer in (
363
  ("query_layer", query_layer),
364
  ("key_layer", key_layer),
@@ -402,12 +443,14 @@ def _unpad_input(
402
  f"{query_length} > {kv_seq_len}"
403
  )
404
 
405
- indices_k, cu_seqlens_k, max_seqlen_in_batch_k = _get_unpad_data(k_sequence_ids)
 
 
406
 
407
- key_layer = index_first_axis(
408
  key_layer.reshape(batch_size * kv_seq_len, num_heads, head_dim), indices_k
409
  )
410
- value_layer = index_first_axis(
411
  value_layer.reshape(batch_size * kv_seq_len, num_heads, head_dim), indices_k
412
  )
413
 
@@ -416,9 +459,10 @@ def _unpad_input(
416
  cu_seqlens_q = cu_seqlens_k
417
  max_seqlen_in_batch_q = max_seqlen_in_batch_k
418
  else:
 
419
  indices_q, cu_seqlens_q, max_seqlen_in_batch_q = _get_unpad_data(q_sequence_ids)
420
 
421
- query_layer = index_first_axis(
422
  query_layer.reshape(batch_size * query_length, num_q_heads, head_dim), indices_q
423
  )
424
 
 
3
  from __future__ import annotations
4
 
5
  import os
 
 
6
  import torch
7
+ from collections.abc import Callable
8
  from torch.nn.attention.flex_attention import _create_sparse_block_from_block_mask
9
 
10
  from fastplms.attention import (
 
22
 
23
  @torch.compiler.disable
24
  def create_block_causal_mask_optimized(sequence_ids: torch.Tensor) -> BlockMask:
25
+ # sequence_ids: (b, l)
26
  if create_block_mask is None:
27
  raise RuntimeError("Flex Attention block-mask creation is unavailable in this environment.")
28
  # Assumes sequence_ids is sorted in increasing order for each batch item, except for
 
42
 
43
  @torch.compiler.disable
44
  def create_within_seq_block_mask(sequence_ids: torch.Tensor) -> BlockMask:
45
+ # sequence_ids: (b, l)
46
  if create_block_mask is None:
47
  raise RuntimeError("Flex Attention block-mask creation is unavailable in this environment.")
48
  def document_mask(b, h, q_idx, kv_idx): # type: ignore[no-untyped-def]
 
59
 
60
 
61
  def build_within_seq_mask_4d(sequence_ids: torch.Tensor) -> torch.Tensor:
62
+ # sequence_ids: (b, l)
63
+ not_pad = sequence_ids != -1 # (b, l)
64
+ same_seq = sequence_ids.unsqueeze(-1) == sequence_ids.unsqueeze(-2) # (b, l, l)
65
+ valid = not_pad.unsqueeze(-1) & not_pad.unsqueeze(-2) # (b, l, l)
66
+ return (same_seq & valid).unsqueeze(1) # (b, 1, l, l)
67
 
68
 
69
  def build_block_causal_mask_4d(sequence_ids: torch.Tensor) -> torch.Tensor:
70
+ # sequence_ids: (b, l)
71
+ not_pad = sequence_ids != -1 # (b, l)
72
+ causal = sequence_ids.unsqueeze(-1) >= sequence_ids.unsqueeze(-2) # (b, l, l)
73
+ valid = not_pad.unsqueeze(-1) & not_pad.unsqueeze(-2) # (b, l, l)
74
+ return (causal & valid).unsqueeze(1) # (b, 1, l, l)
75
 
76
 
77
  def flex_attention_func(
 
87
  raise RuntimeError("Flex Attention is not available in this environment.")
88
  if score_mod is not None:
89
  raise NotImplementedError("E1 Flex Attention does not support score_mod.")
90
+ query_states = query_states.transpose(1, 2).contiguous() # (b, h, l, d)
91
+ key_states = key_states.transpose(1, 2).contiguous() # (b, h_kv, l, d)
92
+ value_states = value_states.transpose(1, 2).contiguous() # (b, h_kv, l, d)
93
 
94
  fn = _get_flex_attention_fn(
95
  device=query_states.device,
 
100
  )
101
  if fn is None:
102
  raise RuntimeError("Flex Attention is not available in this environment.")
103
+ outputs = fn( # (b, h, l, d)
104
  query_states,
105
  key_states,
106
  value_states,
 
109
  enable_gqa=query_states.shape[1] != key_states.shape[1], # if nkv != nh
110
  )
111
 
112
+ outputs = outputs.transpose(1, 2) # (b, l, h, d)
113
+ return outputs # (b, l, h, d)
114
 
115
 
116
  def kernels_flash_attention_func(
117
+ query_states: torch.Tensor, # (b, l_q, h, d)
118
+ key_states: torch.Tensor, # (b, l_kv, h_kv, d)
119
+ value_states: torch.Tensor, # (b, l_kv, h_kv, d)
120
  q_sequence_ids: torch.Tensor,
121
  k_sequence_ids: torch.Tensor,
122
  causal: bool = False,
123
  implementation: str = "flash_attention_3",
124
+ ) -> torch.Tensor: # (b, l_q, h, d)
125
+ # q_sequence_ids: (b, l_q); k_sequence_ids: (b, l_kv)
126
  _ensure_flash_kernels_loaded(implementation)
127
 
128
  if not causal:
 
134
  indices_q,
135
  (cu_seqlens_q, cu_seqlens_k),
136
  (max_seqlen_in_batch_q, max_seqlen_in_batch_k),
137
+ ) = _unpad_input( # Q: (t_q, h, d); K/V: (t_kv, h_kv, d)
138
+ query_states,
139
+ key_states,
140
+ value_states,
141
+ q_sequence_ids,
142
+ k_sequence_ids,
143
+ )
144
 
145
+ attn_output_unpad = _kernels_flash_varlen_forward( # (t_q, h, d)
146
  query_states,
147
  key_states,
148
  value_states,
 
153
  causal=False,
154
  implementation=implementation,
155
  )
156
+ attn_output = pad_input( # (b, l_q, h, d)
157
+ attn_output_unpad,
158
+ indices_q,
159
+ batch_size,
160
+ q_len,
161
+ )
162
 
163
  else:
164
+ attn_output = _kernels_flash_forward( # (b, l_q, h, d)
165
  query_states, key_states, value_states, causal=True, implementation=implementation
166
  )
167
 
168
+ return attn_output # (b, l_q, h, d)
169
 
170
 
171
  def block_min_max_seq_ids(
 
174
  ) -> tuple[torch.Tensor, torch.Tensor]:
175
  """Map each physical attention block to its first and last sequence."""
176
 
177
+ # sequence_lengths: (n,)
178
+ total_tokens = sequence_lengths.sum() # ()
179
  block_count = int(
180
  torch.div(
181
  total_tokens + block_size - 1,
 
183
  rounding_mode="floor",
184
  ).item()
185
  )
186
+ padded_tokens = block_count * block_size - total_tokens # ()
187
+ lengths_with_tail = torch.cat( # (n + 1,)
188
  (sequence_lengths, padded_tokens.to(sequence_lengths).reshape(1)),
189
  )
190
+ sequence_ends = lengths_with_tail.to(torch.long).cumsum(dim=0) # (n + 1,)
191
+ block_starts = torch.arange( # (n_blocks,)
192
  start=0,
193
  end=block_count * block_size,
194
  step=block_size,
195
  dtype=torch.long,
196
  device=sequence_lengths.device,
197
  )
198
+ block_last_tokens = block_starts + block_size - 1 # (n_blocks,)
199
+ first_sequence = torch.searchsorted(sequence_ends, block_starts, right=True) # (n_blocks,)
200
+ last_sequence = torch.searchsorted(sequence_ends, block_last_tokens, right=True) # (n_blocks,)
201
+ return first_sequence, last_sequence # (n_blocks,), (n_blocks,)
202
 
203
 
204
  def get_overlapping_blocks(
 
207
  ) -> tuple[torch.Tensor, torch.Tensor]:
208
  """Classify query/key block pairs as full, partial, or disjoint."""
209
 
210
+ # q_lengths: (n_q,); k_lengths: (n_k,)
211
+ q_first, q_last = block_min_max_seq_ids(q_lengths) # (q_blocks,), (q_blocks,)
212
+ k_first, k_last = block_min_max_seq_ids(k_lengths) # (k_blocks,), (k_blocks,)
213
+ intersection_start = torch.maximum( # (q_blocks, k_blocks)
214
+ q_first[:, None],
215
+ k_first[None, :],
216
+ )
217
+ intersection_end = torch.minimum( # (q_blocks, k_blocks)
218
+ q_last[:, None],
219
+ k_last[None, :],
220
+ )
221
+ intersects = intersection_start <= intersection_end # (q_blocks, k_blocks)
222
+ both_blocks_are_single_sequence = ( # (q_blocks, k_blocks)
223
+ (q_first == q_last)[:, None] & (k_first == k_last)[None, :]
224
+ )
225
+ full_blocks = intersects & both_blocks_are_single_sequence # (q_blocks, k_blocks)
226
+ return full_blocks, intersects & ~both_blocks_are_single_sequence # both (q_blocks, k_blocks)
227
 
228
 
229
  def _document_ids(sequence_lengths: torch.Tensor) -> torch.Tensor:
230
+ # sequence_lengths: (n,)
231
+ sequence_numbers = torch.arange( # (n,)
232
  sequence_lengths.numel(),
233
  device=sequence_lengths.device,
234
  dtype=torch.long,
235
  )
236
+ return sequence_numbers.repeat_interleave(sequence_lengths.to(torch.long)) # (t,)
237
 
238
 
239
  @torch.compiler.disable
240
  def direct_block_mask(q_lengths: torch.Tensor, k_lengths: torch.Tensor) -> BlockMask:
241
  """Build a packed-sequence mask from preclassified sparse blocks."""
242
 
243
+ full, partial = get_overlapping_blocks(q_lengths, k_lengths) # both (q_blocks, k_blocks)
244
+ q_document = _document_ids(q_lengths) # (t_q,)
245
+ k_document = _document_ids(k_lengths) # (t_k,)
246
 
247
  def same_document(
248
  _batch: torch.Tensor,
 
265
  def doc_id_mask(q_lengths: torch.Tensor, k_lengths: torch.Tensor) -> BlockMask:
266
  if create_block_mask is None:
267
  raise RuntimeError("Flex Attention block-mask creation is unavailable in this environment.")
268
+ q_document = _document_ids(q_lengths) # (t_q,)
269
+ k_document = _document_ids(k_lengths) # (t_k,)
270
 
271
  def same_document(
272
  _batch: torch.Tensor,
 
294
  q_sequence_ids: torch.Tensor,
295
  k_sequence_ids: torch.Tensor,
296
  ) -> torch.Tensor:
297
+ # query_states: (b, l_q, h, d); key_states, value_states: (b, l_kv, h_kv, d)
298
+ # q_sequence_ids: (b, l_q); k_sequence_ids: (b, l_kv)
299
  if flex_attention is None:
300
  raise RuntimeError("Flex Attention is not available in this environment.")
301
  batch_size, q_len = query_states.shape[0], query_states.shape[1]
 
306
  indices_q,
307
  (cu_seqlens_q, cu_seqlens_k),
308
  (_max_seqlen_in_batch_q, _max_seqlen_in_batch_k),
309
+ ) = _unpad_input( # Q: (t_q, h, d); K/V: (t_kv, h_kv, d)
310
+ query_states,
311
+ key_states,
312
+ value_states,
313
+ q_sequence_ids,
314
+ k_sequence_ids,
315
+ )
316
 
317
+ query_states = query_states.unsqueeze(0).transpose(1, 2).contiguous() # (1, h, t_q, d)
318
+ key_states = key_states.unsqueeze(0).transpose(1, 2).contiguous() # (1, h_kv, t_kv, d)
319
+ value_states = value_states.unsqueeze(0).transpose(1, 2).contiguous() # (1, h_kv, t_kv, d)
320
 
321
+ seqlens_q = cu_seqlens_q[1:] - cu_seqlens_q[:-1] # (n,)
322
+ seqlens_k = cu_seqlens_k[1:] - cu_seqlens_k[:-1] # (n,)
323
  block_mask = block_mask_creator(seqlens_q, seqlens_k)
324
 
325
  packed_lengths = (
 
336
  )
337
  if fn is None:
338
  raise RuntimeError("Flex Attention is not available in this environment.")
339
+ attn_output_unpad = fn( # (1, h, t_q, d)
340
  query_states,
341
  key_states,
342
  value_states,
 
344
  enable_gqa=query_states.shape[1] != key_states.shape[1],
345
  )
346
 
347
+ attn_output = pad_input( # (b, l_q, h, d)
348
  attn_output_unpad.transpose(1, 2).squeeze(0), indices_q, batch_size, q_len
349
  )
350
 
351
+ return attn_output # (b, l_q, h, d)
352
 
353
 
354
  def _get_unpad_data(sequence_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, int]:
355
  """Return packed indices and run lengths for the non-padding sequence IDs."""
356
 
357
+ # sequence_ids: (b, l)
358
+ flat_ids = sequence_ids.reshape(-1) # (b * l,)
359
+ non_pad_indices = torch.where(flat_ids.ne(-1))[0] # (t,)
360
  if non_pad_indices.numel() == 0:
361
  raise ValueError("Packed attention requires at least one non-padding token.")
362
 
363
+ valid_ids = flat_ids.index_select(0, non_pad_indices) # (t,)
364
+ row_ids = torch.div( # (t,)
365
  non_pad_indices,
366
  sequence_ids.shape[1],
367
  rounding_mode="floor",
368
  )
369
+ run_starts = torch.ones_like(valid_ids, dtype=torch.bool) # (t,)
370
  run_starts[1:] = (valid_ids[1:] != valid_ids[:-1]) | (row_ids[1:] != row_ids[:-1])
371
+ start_indices = torch.where(run_starts)[0] # (n,)
372
+ end_indices = torch.cat( # (n,)
373
+ (start_indices[1:], start_indices.new_tensor([valid_ids.numel()]))
374
+ )
375
+ sequence_lengths = end_indices - start_indices # (n,)
376
+ cumulative_lengths = torch.cat( # (n + 1,)
377
  (
378
  torch.zeros(1, dtype=torch.int32, device=sequence_ids.device),
379
  sequence_lengths.cumsum(dim=0, dtype=torch.int32),
380
  ),
381
  )
382
+ return non_pad_indices, cumulative_lengths, int( # (t,), (n + 1,), scalar
383
+ sequence_lengths.max().item()
384
+ )
385
 
386
 
387
  def _unpad_input(
 
398
  tuple[torch.Tensor, torch.Tensor],
399
  tuple[int, int],
400
  ]:
401
+ # query_layer: (b, l_q, h, d); key_layer, value_layer: (b, l_kv, h_kv, d)
402
+ # q_sequence_ids: (b, l_q); k_sequence_ids: (b, l_kv)
403
  for name, layer in (
404
  ("query_layer", query_layer),
405
  ("key_layer", key_layer),
 
443
  f"{query_length} > {kv_seq_len}"
444
  )
445
 
446
+ indices_k, cu_seqlens_k, max_seqlen_in_batch_k = _get_unpad_data( # (t_kv,), (n + 1,), scalar
447
+ k_sequence_ids
448
+ )
449
 
450
+ key_layer = index_first_axis( # (t_kv, h_kv, d)
451
  key_layer.reshape(batch_size * kv_seq_len, num_heads, head_dim), indices_k
452
  )
453
+ value_layer = index_first_axis( # (t_kv, h_kv, d)
454
  value_layer.reshape(batch_size * kv_seq_len, num_heads, head_dim), indices_k
455
  )
456
 
 
459
  cu_seqlens_q = cu_seqlens_k
460
  max_seqlen_in_batch_q = max_seqlen_in_batch_k
461
  else:
462
+ # (t_q,), (n + 1,), scalar
463
  indices_q, cu_seqlens_q, max_seqlen_in_batch_q = _get_unpad_data(q_sequence_ids)
464
 
465
+ query_layer = index_first_axis( # (t_q, h, d)
466
  query_layer.reshape(batch_size * query_length, num_q_heads, head_dim), indices_q
467
  )
468
 
fastplms/models/e1/cache.py CHANGED
@@ -2,9 +2,8 @@
2
 
3
  from __future__ import annotations
4
 
5
- from typing import Any
6
-
7
  import torch
 
8
  from transformers.modeling_outputs import ModelOutput
9
  from transformers.utils import logging
10
 
@@ -44,9 +43,9 @@ class DynamicCache:
44
  tuple[`torch.Tensor`, `torch.Tensor`]: Cached K and V, each with shape
45
  (b, l, h, d).
46
  """
47
- # Lazy initialization
48
  if len(self.key_cache) <= layer_idx:
49
- # There may be skipped layers, fill them with empty lists
50
  for _ in range(len(self.key_cache), layer_idx):
51
  self.key_cache.append(torch.tensor([]))
52
  self.value_cache.append(torch.tensor([]))
@@ -60,12 +59,18 @@ class DynamicCache:
60
  self.key_cache[layer_idx] = key_states
61
  self.value_cache[layer_idx] = value_states
62
  else:
63
- self.key_cache[layer_idx] = torch.cat([self.key_cache[layer_idx], key_states], dim=1)
64
- self.value_cache[layer_idx] = torch.cat(
 
 
 
65
  [self.value_cache[layer_idx], value_states], dim=1
66
  )
67
 
68
- return self.key_cache[layer_idx], self.value_cache[layer_idx]
 
 
 
69
 
70
  def get_seq_length(self, layer_idx: int = 0) -> int:
71
  """Return the cached sequence length for one layer."""
@@ -95,19 +100,22 @@ class DynamicCache:
95
  """Repeat the cache `repeats` times in the batch dimension. Used in contrastive search."""
96
  for layer_idx in range(len(self.key_cache)):
97
  if self.key_cache[layer_idx].numel():
 
98
  self.key_cache[layer_idx] = self.key_cache[layer_idx].repeat_interleave(
99
  repeats, dim=0
100
  )
101
  self.value_cache[layer_idx] = self.value_cache[layer_idx].repeat_interleave(
102
  repeats, dim=0
103
- )
104
 
105
- def batch_select_indices(self, indices: torch.Tensor) -> None:
106
  """Keep selected rows of the cache batch dimension."""
107
  for layer_idx in range(len(self.key_cache)):
108
  if self.key_cache[layer_idx].numel():
109
- self.key_cache[layer_idx] = self.key_cache[layer_idx][indices, ...]
110
- self.value_cache[layer_idx] = self.value_cache[layer_idx][indices, ...]
 
 
111
 
112
 
113
  class KVCache:
@@ -158,7 +166,7 @@ class KVCache:
158
  )
159
  return
160
 
161
- batch_size = batch["input_ids"].shape[0]
162
 
163
  unique_context = contexts[0]
164
  unique_context_len = context_lens[0]
@@ -171,10 +179,10 @@ class KVCache:
171
  past_key_values = self.cache_dict[unique_context]
172
  batch["past_key_values"] = past_key_values
173
 
174
- # Remove context from the input fields
175
  for field_name in self.tensor_input_field_names:
176
  if batch.get(field_name) is not None:
177
- batch[field_name] = batch[field_name][:, unique_context_len:]
178
 
179
  def after_forward(self, batch: dict[str, Any], outputs: ModelOutput) -> None:
180
  contexts = batch.get("context")
@@ -209,18 +217,21 @@ class KVCache:
209
  self.cache_dict[unique_context] = past_key_values
210
  self.cache_queue.append(unique_context)
211
 
212
- # Remove context from the input fields
213
  for field_name in self.tensor_input_field_names:
214
  if field_name in batch and batch[field_name] is not None:
215
- batch[field_name] = batch[field_name][:, unique_context_len:]
 
 
216
 
217
- # Remove context from the output fields
218
  for field_name in self.tensor_output_field_names:
219
  if field_name in outputs and outputs[field_name] is not None:
220
- outputs[field_name] = outputs[field_name][:, unique_context_len:]
 
 
221
  if "hidden_states" in outputs and outputs["hidden_states"] is not None:
222
  hidden_states = outputs["hidden_states"]
223
- sliced_hidden_states = tuple(
224
  hidden_state[:, unique_context_len:] for hidden_state in hidden_states
225
  )
226
  outputs["hidden_states"] = sliced_hidden_states
 
2
 
3
  from __future__ import annotations
4
 
 
 
5
  import torch
6
+ from typing import Any
7
  from transformers.modeling_outputs import ModelOutput
8
  from transformers.utils import logging
9
 
 
43
  tuple[`torch.Tensor`, `torch.Tensor`]: Cached K and V, each with shape
44
  (b, l, h, d).
45
  """
46
+ # key_states, value_states: (b, l_new, h, d)
47
  if len(self.key_cache) <= layer_idx:
48
+ # Empty tensors preserve skipped layer indices until those layers receive state.
49
  for _ in range(len(self.key_cache), layer_idx):
50
  self.key_cache.append(torch.tensor([]))
51
  self.value_cache.append(torch.tensor([]))
 
59
  self.key_cache[layer_idx] = key_states
60
  self.value_cache[layer_idx] = value_states
61
  else:
62
+ self.key_cache[layer_idx] = torch.cat( # (b, l_cached + l_new, h, d)
63
+ [self.key_cache[layer_idx], key_states],
64
+ dim=1,
65
+ )
66
+ self.value_cache[layer_idx] = torch.cat( # (b, l_cached + l_new, h, d)
67
  [self.value_cache[layer_idx], value_states], dim=1
68
  )
69
 
70
+ return ( # (b, l_total, h, d), (b, l_total, h, d)
71
+ self.key_cache[layer_idx],
72
+ self.value_cache[layer_idx],
73
+ )
74
 
75
  def get_seq_length(self, layer_idx: int = 0) -> int:
76
  """Return the cached sequence length for one layer."""
 
100
  """Repeat the cache `repeats` times in the batch dimension. Used in contrastive search."""
101
  for layer_idx in range(len(self.key_cache)):
102
  if self.key_cache[layer_idx].numel():
103
+ # (b * repeats, l, h, d)
104
  self.key_cache[layer_idx] = self.key_cache[layer_idx].repeat_interleave(
105
  repeats, dim=0
106
  )
107
  self.value_cache[layer_idx] = self.value_cache[layer_idx].repeat_interleave(
108
  repeats, dim=0
109
+ ) # (b * repeats, l, h, d)
110
 
111
+ def batch_select_indices(self, indices: torch.Tensor | list[int]) -> None:
112
  """Keep selected rows of the cache batch dimension."""
113
  for layer_idx in range(len(self.key_cache)):
114
  if self.key_cache[layer_idx].numel():
115
+ self.key_cache[layer_idx] = self.key_cache[layer_idx][indices, ...] # (n, l, h, d)
116
+ self.value_cache[layer_idx] = self.value_cache[layer_idx][
117
+ indices, ...
118
+ ] # (n, l, h, d)
119
 
120
 
121
  class KVCache:
 
166
  )
167
  return
168
 
169
+ batch_size = batch["input_ids"].shape[0] # b
170
 
171
  unique_context = contexts[0]
172
  unique_context_len = context_lens[0]
 
179
  past_key_values = self.cache_dict[unique_context]
180
  batch["past_key_values"] = past_key_values
181
 
182
+ # A cached prefix leaves only query-suffix tokens for the model call.
183
  for field_name in self.tensor_input_field_names:
184
  if batch.get(field_name) is not None:
185
+ batch[field_name] = batch[field_name][:, unique_context_len:] # (b, l_suffix, ...)
186
 
187
  def after_forward(self, batch: dict[str, Any], outputs: ModelOutput) -> None:
188
  contexts = batch.get("context")
 
217
  self.cache_dict[unique_context] = past_key_values
218
  self.cache_queue.append(unique_context)
219
 
220
+ # The first uncached call returns the full sequence; expose its query suffix.
221
  for field_name in self.tensor_input_field_names:
222
  if field_name in batch and batch[field_name] is not None:
223
+ batch[field_name] = batch[field_name][
224
+ :, unique_context_len:
225
+ ] # (b, l_suffix, ...)
226
 
 
227
  for field_name in self.tensor_output_field_names:
228
  if field_name in outputs and outputs[field_name] is not None:
229
+ outputs[field_name] = outputs[field_name][
230
+ :, unique_context_len:
231
+ ] # (b, l_suffix, ...)
232
  if "hidden_states" in outputs and outputs["hidden_states"] is not None:
233
  hidden_states = outputs["hidden_states"]
234
+ sliced_hidden_states = tuple( # each: (b, l_suffix, d)
235
  hidden_state[:, unique_context_len:] for hidden_state in hidden_states
236
  )
237
  outputs["hidden_states"] = sliced_hidden_states
fastplms/models/e1/modeling_e1.py CHANGED
@@ -3,21 +3,21 @@ from __future__ import annotations
3
  import hashlib
4
  import os
5
  import sys
 
 
 
6
  from collections import defaultdict
7
  from contextvars import ContextVar
8
  from dataclasses import dataclass
9
  from enum import Enum
10
  from typing import Any, ClassVar, TypedDict
11
-
12
- import torch
13
- import torch.nn as nn
14
- import torch.nn.functional as F
15
  from tqdm.auto import tqdm
16
  from transformers import PretrainedConfig, PreTrainedModel
17
  from transformers.activations import ACT2FN
18
  from transformers.modeling_outputs import ModelOutput
19
  from transformers.utils import logging
20
 
 
21
  try:
22
  from fastplms.attention import (
23
  AttentionBackend,
@@ -282,7 +282,7 @@ class RotaryPositionalEmbedding(nn.Module):
282
  max_position_embeddings: int = 2048,
283
  base: int = 10000,
284
  device: torch.device | None = None,
285
- ):
286
  super().__init__()
287
 
288
  self.dim = dim
@@ -311,11 +311,11 @@ class RotaryPositionalEmbedding(nn.Module):
311
  self.max_seq_len_cached = seq_len
312
  inv_freq = self.base ** -(
313
  torch.arange(0, self.dim, 2, dtype=torch.float32, device=device) / self.dim
314
- )
315
  self.inv_freq = inv_freq
316
- t = torch.arange(seq_len, device=device, dtype=torch.float32)
317
- angles = torch.outer(t, inv_freq)
318
- angles = torch.cat((angles, angles), dim=1)
319
  self.cos_cached = angles.cos()
320
  self.sin_cached = angles.sin()
321
 
@@ -326,18 +326,17 @@ class RotaryPositionalEmbedding(nn.Module):
326
  position_ids: torch.LongTensor,
327
  seq_len: int | None = None,
328
  ) -> tuple[torch.Tensor, torch.Tensor]:
329
- # Q and K have shape (b, l, h, d).
330
  device, dtype = q.device, q.dtype
331
  seq_len = position_ids.max().item() + 1 if seq_len is None else seq_len
332
 
333
  if seq_len > self.max_seq_len_cached:
334
  self._set_sin_cos_cache(seq_len=seq_len, device=device)
335
 
336
- # Selecting by position gives C and S shape (b, l, d). Insert a head
337
- # axis so they broadcast over Q and K with shape (b, l, h, d).
338
  idxs = position_ids.to(device)
339
- cos = self.cos_cached.to(device=device, dtype=dtype).unsqueeze(-2)[idxs]
340
- sin = self.sin_cached.to(device=device, dtype=dtype).unsqueeze(-2)[idxs]
341
 
342
  # Apply the real and imaginary parts of the rotary transform to Q and K.
343
  # Both halves reuse C and S, so rotate_half supplies the cross terms.
@@ -349,7 +348,7 @@ class RotaryPositionalEmbedding(nn.Module):
349
  class Attention(nn.Module):
350
  """Multi-headed attention from 'Attention Is All You Need' paper."""
351
 
352
- def __init__(self, config: E1Config, layer_idx: int):
353
  super().__init__()
354
  self.config = config
355
  self.layer_idx = layer_idx
@@ -407,14 +406,21 @@ class Attention(nn.Module):
407
  past_key_value: DynamicCache | None = None,
408
  use_cache: bool = False,
409
  ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
 
410
  bsz, q_len, _ = hidden_states.size()
411
  query_states: torch.Tensor = self.q_proj(hidden_states)
412
  key_states: torch.Tensor = self.k_proj(hidden_states)
413
  val_states: torch.Tensor = self.v_proj(hidden_states)
414
 
415
- query_states = query_states.view(bsz, q_len, self.num_heads, self.head_dim)
416
- key_states = key_states.view(bsz, q_len, self.num_kv_heads, self.head_dim)
417
- val_states = val_states.view(bsz, q_len, self.num_kv_heads, self.head_dim)
 
 
 
 
 
 
418
 
419
  if self.clip_qkv is not None:
420
  query_states = query_states.clamp(-self.clip_qkv, self.clip_qkv)
@@ -456,6 +462,7 @@ class Attention(nn.Module):
456
  use_cache: bool = False,
457
  effective_backend: AttentionBackend | None = None,
458
  ) -> tuple[torch.Tensor, torch.Tensor | None, DynamicCache | None, list[torch.Tensor] | None]:
 
459
  is_cache_prefilled = (
460
  use_cache
461
  and past_key_value is not None
@@ -586,12 +593,12 @@ class Attention(nn.Module):
586
  query_states: torch.Tensor, # Q has shape (b, l, h, d).
587
  key_states: torch.Tensor, # K has shape (b, l, h_kv, d).
588
  ) -> list[torch.Tensor]:
589
- query_heads = query_states.transpose(1, 2).contiguous()
590
- key_heads = key_states.transpose(1, 2).contiguous()
591
  key_heads = repeat_kv(key_heads, self.num_key_value_groups)
592
  scale = 1.0 / (self.head_dim**0.5)
593
- q_norm = torch.linalg.vector_norm(query_heads, dim=-1)
594
- k_norm = torch.linalg.vector_norm(key_heads, dim=-1)
595
  s_max_bound = (q_norm.max(dim=-1).values * k_norm.max(dim=-1).values).max(
596
  dim=0
597
  ).values * scale
@@ -763,14 +770,14 @@ class Attention(nn.Module):
763
  else:
764
  attention_mask_4d = None
765
 
766
- query_heads = query_states.transpose(1, 2).contiguous()
767
- key_heads = key_states.transpose(1, 2).contiguous()
768
- value_heads = val_states.transpose(1, 2).contiguous()
769
  key_heads = repeat_kv(key_heads, self.num_key_value_groups)
770
  value_heads = repeat_kv(value_heads, self.num_key_value_groups)
771
  context_heads = F.scaled_dot_product_attention(
772
  query_heads, key_heads, value_heads, attn_mask=attention_mask_4d
773
- )
774
  attn_output = (
775
  context_heads.transpose(1, 2).reshape(bsz, q_len, self.hidden_size).contiguous()
776
  )
@@ -807,13 +814,15 @@ class Attention(nn.Module):
807
  else:
808
  attention_mask_4d = None
809
 
810
- query_heads = query_states.transpose(1, 2).contiguous()
811
- key_heads = key_states.transpose(1, 2).contiguous()
812
- value_heads = val_states.transpose(1, 2).contiguous()
813
  key_heads = repeat_kv(key_heads, self.num_key_value_groups)
814
  value_heads = repeat_kv(value_heads, self.num_key_value_groups)
815
  scale = 1.0 / (self.head_dim**0.5)
816
- attn_weights = torch.matmul(query_heads, key_heads.transpose(-2, -1)) * scale
 
 
817
  if attention_mask_4d is not None:
818
  attention_mask_4d = attention_mask_4d.to(dtype=torch.bool)
819
  attn_weights = attn_weights.masked_fill(
@@ -823,7 +832,7 @@ class Attention(nn.Module):
823
  attn_weights = F.softmax(attn_weights, dim=-1)
824
  if attention_mask_4d is not None:
825
  attn_weights = attn_weights.masked_fill(attention_mask_4d.logical_not(), 0.0)
826
- context_heads = torch.matmul(attn_weights, value_heads)
827
  attn_output = (
828
  context_heads.transpose(1, 2).reshape(bsz, q_len, self.hidden_size).contiguous()
829
  )
@@ -832,7 +841,7 @@ class Attention(nn.Module):
832
 
833
 
834
  class MLP(nn.Module):
835
- def __init__(self, config: E1Config):
836
  super().__init__()
837
  self.ffn_dim = config.intermediate_size
838
  self.hidden_dim = config.hidden_size
@@ -845,7 +854,7 @@ class MLP(nn.Module):
845
 
846
 
847
  class GLUMLP(nn.Module):
848
- def __init__(self, config: E1Config):
849
  super().__init__()
850
  self.ffn_dim = config.intermediate_size
851
  self.hidden_dim = config.hidden_size
@@ -861,7 +870,7 @@ class GLUMLP(nn.Module):
861
 
862
 
863
  class FFN(nn.Module):
864
- def __init__(self, config: E1Config):
865
  super().__init__()
866
  mlp_cls = GLUMLP if config.gated_mlp else MLP
867
  self.mlp = mlp_cls(config)
@@ -927,7 +936,7 @@ class E1TokenClassificationOutputWithPast(ModelOutput):
927
 
928
 
929
  class RMSNorm(nn.Module):
930
- def __init__(self, hidden_size: int, eps: float = 1e-6):
931
  super().__init__()
932
  self.weight = nn.Parameter(torch.ones(hidden_size))
933
  self.variance_epsilon = eps
@@ -941,7 +950,7 @@ class RMSNorm(nn.Module):
941
 
942
 
943
  class NormAttentionNorm(nn.Module):
944
- def __init__(self, config: E1Config, layer_idx: int):
945
  super().__init__()
946
  self.self_attn = Attention(config, layer_idx)
947
  self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
@@ -988,7 +997,7 @@ class NormAttentionNorm(nn.Module):
988
 
989
 
990
  class DecoderLayer(nn.Module):
991
- def __init__(self, config: E1Config, layer_idx: int):
992
  super().__init__()
993
  self.initializer_range = config.initializer_range
994
  self.hidden_size = config.hidden_size
@@ -1179,7 +1188,7 @@ class FAST_E1_ENCODER(E1PreTrainedModel, EmbeddingMixin):
1179
  config_class = E1Config
1180
  _is_internal_encoder = True
1181
 
1182
- def __init__(self, config: E1Config, **kwargs):
1183
  E1PreTrainedModel.__init__(self, config, **kwargs)
1184
  self.padding_idx = config.pad_token_id
1185
  self.vocab_size = config.vocab_size
@@ -1540,7 +1549,7 @@ class E1Model(E1PreTrainedModel, EmbeddingMixin):
1540
  config: E1Config
1541
  config_class = E1Config
1542
 
1543
- def __init__(self, config: E1Config, **kwargs):
1544
  E1PreTrainedModel.__init__(self, config, **kwargs)
1545
  self.model: FAST_E1_ENCODER = FAST_E1_ENCODER(config, **kwargs)
1546
  self.post_init()
@@ -1589,7 +1598,7 @@ class E1ForMaskedLM(FastPLMTestTimeTrainingMixin, E1PreTrainedModel, EmbeddingMi
1589
  config: E1Config
1590
  config_class = E1Config
1591
 
1592
- def __init__(self, config: E1Config, **kwargs):
1593
  E1PreTrainedModel.__init__(self, config, **kwargs)
1594
  self.model: FAST_E1_ENCODER = FAST_E1_ENCODER(config, **kwargs)
1595
  self.vocab_size = config.vocab_size
@@ -2092,7 +2101,7 @@ class E1ForSequenceClassification(E1PreTrainedModel, EmbeddingMixin):
2092
  config: E1Config
2093
  config_class = E1Config
2094
 
2095
- def __init__(self, config: E1Config, **kwargs):
2096
  pooling_types = kwargs.pop("pooling_types", None)
2097
  if pooling_types is None:
2098
  pooling_types = ["mean", "var"]
@@ -2227,7 +2236,7 @@ class E1ForTokenClassification(E1PreTrainedModel, EmbeddingMixin):
2227
  config: E1Config
2228
  config_class = E1Config
2229
 
2230
- def __init__(self, config: E1Config, **kwargs):
2231
  E1PreTrainedModel.__init__(self, config, **kwargs)
2232
  self.model: FAST_E1_ENCODER = FAST_E1_ENCODER(config, **kwargs)
2233
  self.vocab_size = config.vocab_size
 
3
  import hashlib
4
  import os
5
  import sys
6
+ import torch
7
+ import torch.nn as nn
8
+ import torch.nn.functional as F
9
  from collections import defaultdict
10
  from contextvars import ContextVar
11
  from dataclasses import dataclass
12
  from enum import Enum
13
  from typing import Any, ClassVar, TypedDict
 
 
 
 
14
  from tqdm.auto import tqdm
15
  from transformers import PretrainedConfig, PreTrainedModel
16
  from transformers.activations import ACT2FN
17
  from transformers.modeling_outputs import ModelOutput
18
  from transformers.utils import logging
19
 
20
+
21
  try:
22
  from fastplms.attention import (
23
  AttentionBackend,
 
282
  max_position_embeddings: int = 2048,
283
  base: int = 10000,
284
  device: torch.device | None = None,
285
+ ) -> None:
286
  super().__init__()
287
 
288
  self.dim = dim
 
311
  self.max_seq_len_cached = seq_len
312
  inv_freq = self.base ** -(
313
  torch.arange(0, self.dim, 2, dtype=torch.float32, device=device) / self.dim
314
+ ) # (d / 2,)
315
  self.inv_freq = inv_freq
316
+ t = torch.arange(seq_len, device=device, dtype=torch.float32) # (l,)
317
+ angles = torch.outer(t, inv_freq) # (l, d / 2)
318
+ angles = torch.cat((angles, angles), dim=1) # (l, d)
319
  self.cos_cached = angles.cos()
320
  self.sin_cached = angles.sin()
321
 
 
326
  position_ids: torch.LongTensor,
327
  seq_len: int | None = None,
328
  ) -> tuple[torch.Tensor, torch.Tensor]:
329
+ # q, k: (b, l, h, d)
330
  device, dtype = q.device, q.dtype
331
  seq_len = position_ids.max().item() + 1 if seq_len is None else seq_len
332
 
333
  if seq_len > self.max_seq_len_cached:
334
  self._set_sin_cos_cache(seq_len=seq_len, device=device)
335
 
336
+ # Selecting by position inserts a head axis for broadcasting.
 
337
  idxs = position_ids.to(device)
338
+ cos = self.cos_cached.to(device=device, dtype=dtype).unsqueeze(-2)[idxs] # (b, l, 1, d)
339
+ sin = self.sin_cached.to(device=device, dtype=dtype).unsqueeze(-2)[idxs] # (b, l, 1, d)
340
 
341
  # Apply the real and imaginary parts of the rotary transform to Q and K.
342
  # Both halves reuse C and S, so rotate_half supplies the cross terms.
 
348
  class Attention(nn.Module):
349
  """Multi-headed attention from 'Attention Is All You Need' paper."""
350
 
351
+ def __init__(self, config: E1Config, layer_idx: int) -> None:
352
  super().__init__()
353
  self.config = config
354
  self.layer_idx = layer_idx
 
406
  past_key_value: DynamicCache | None = None,
407
  use_cache: bool = False,
408
  ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
409
+ # hidden_states: (b, l, d); position_ids: (b, l)
410
  bsz, q_len, _ = hidden_states.size()
411
  query_states: torch.Tensor = self.q_proj(hidden_states)
412
  key_states: torch.Tensor = self.k_proj(hidden_states)
413
  val_states: torch.Tensor = self.v_proj(hidden_states)
414
 
415
+ query_states = query_states.view(
416
+ bsz, q_len, self.num_heads, self.head_dim
417
+ ) # (b, l, h, d_h)
418
+ key_states = key_states.view(
419
+ bsz, q_len, self.num_kv_heads, self.head_dim
420
+ ) # (b, l, h_kv, d_h)
421
+ val_states = val_states.view(
422
+ bsz, q_len, self.num_kv_heads, self.head_dim
423
+ ) # (b, l, h_kv, d_h)
424
 
425
  if self.clip_qkv is not None:
426
  query_states = query_states.clamp(-self.clip_qkv, self.clip_qkv)
 
462
  use_cache: bool = False,
463
  effective_backend: AttentionBackend | None = None,
464
  ) -> tuple[torch.Tensor, torch.Tensor | None, DynamicCache | None, list[torch.Tensor] | None]:
465
+ # hidden_states: (b, l, d); position and sequence IDs: (b, l)
466
  is_cache_prefilled = (
467
  use_cache
468
  and past_key_value is not None
 
593
  query_states: torch.Tensor, # Q has shape (b, l, h, d).
594
  key_states: torch.Tensor, # K has shape (b, l, h_kv, d).
595
  ) -> list[torch.Tensor]:
596
+ query_heads = query_states.transpose(1, 2).contiguous() # (b, h, l, d_h)
597
+ key_heads = key_states.transpose(1, 2).contiguous() # (b, h_kv, l, d_h)
598
  key_heads = repeat_kv(key_heads, self.num_key_value_groups)
599
  scale = 1.0 / (self.head_dim**0.5)
600
+ q_norm = torch.linalg.vector_norm(query_heads, dim=-1) # (b, h, l)
601
+ k_norm = torch.linalg.vector_norm(key_heads, dim=-1) # (b, h, l)
602
  s_max_bound = (q_norm.max(dim=-1).values * k_norm.max(dim=-1).values).max(
603
  dim=0
604
  ).values * scale
 
770
  else:
771
  attention_mask_4d = None
772
 
773
+ query_heads = query_states.transpose(1, 2).contiguous() # (b, h, l, d_h)
774
+ key_heads = key_states.transpose(1, 2).contiguous() # (b, h_kv, l, d_h)
775
+ value_heads = val_states.transpose(1, 2).contiguous() # (b, h_kv, l, d_h)
776
  key_heads = repeat_kv(key_heads, self.num_key_value_groups)
777
  value_heads = repeat_kv(value_heads, self.num_key_value_groups)
778
  context_heads = F.scaled_dot_product_attention(
779
  query_heads, key_heads, value_heads, attn_mask=attention_mask_4d
780
+ ) # (b, h, l, d_h)
781
  attn_output = (
782
  context_heads.transpose(1, 2).reshape(bsz, q_len, self.hidden_size).contiguous()
783
  )
 
814
  else:
815
  attention_mask_4d = None
816
 
817
+ query_heads = query_states.transpose(1, 2).contiguous() # (b, h, l, d_h)
818
+ key_heads = key_states.transpose(1, 2).contiguous() # (b, h_kv, l, d_h)
819
+ value_heads = val_states.transpose(1, 2).contiguous() # (b, h_kv, l, d_h)
820
  key_heads = repeat_kv(key_heads, self.num_key_value_groups)
821
  value_heads = repeat_kv(value_heads, self.num_key_value_groups)
822
  scale = 1.0 / (self.head_dim**0.5)
823
+ attn_weights = (
824
+ torch.matmul(query_heads, key_heads.transpose(-2, -1)) * scale
825
+ ) # (b, h, l, l)
826
  if attention_mask_4d is not None:
827
  attention_mask_4d = attention_mask_4d.to(dtype=torch.bool)
828
  attn_weights = attn_weights.masked_fill(
 
832
  attn_weights = F.softmax(attn_weights, dim=-1)
833
  if attention_mask_4d is not None:
834
  attn_weights = attn_weights.masked_fill(attention_mask_4d.logical_not(), 0.0)
835
+ context_heads = torch.matmul(attn_weights, value_heads) # (b, h, l, d_h)
836
  attn_output = (
837
  context_heads.transpose(1, 2).reshape(bsz, q_len, self.hidden_size).contiguous()
838
  )
 
841
 
842
 
843
  class MLP(nn.Module):
844
+ def __init__(self, config: E1Config) -> None:
845
  super().__init__()
846
  self.ffn_dim = config.intermediate_size
847
  self.hidden_dim = config.hidden_size
 
854
 
855
 
856
  class GLUMLP(nn.Module):
857
+ def __init__(self, config: E1Config) -> None:
858
  super().__init__()
859
  self.ffn_dim = config.intermediate_size
860
  self.hidden_dim = config.hidden_size
 
870
 
871
 
872
  class FFN(nn.Module):
873
+ def __init__(self, config: E1Config) -> None:
874
  super().__init__()
875
  mlp_cls = GLUMLP if config.gated_mlp else MLP
876
  self.mlp = mlp_cls(config)
 
936
 
937
 
938
  class RMSNorm(nn.Module):
939
+ def __init__(self, hidden_size: int, eps: float = 1e-6) -> None:
940
  super().__init__()
941
  self.weight = nn.Parameter(torch.ones(hidden_size))
942
  self.variance_epsilon = eps
 
950
 
951
 
952
  class NormAttentionNorm(nn.Module):
953
+ def __init__(self, config: E1Config, layer_idx: int) -> None:
954
  super().__init__()
955
  self.self_attn = Attention(config, layer_idx)
956
  self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
 
997
 
998
 
999
  class DecoderLayer(nn.Module):
1000
+ def __init__(self, config: E1Config, layer_idx: int) -> None:
1001
  super().__init__()
1002
  self.initializer_range = config.initializer_range
1003
  self.hidden_size = config.hidden_size
 
1188
  config_class = E1Config
1189
  _is_internal_encoder = True
1190
 
1191
+ def __init__(self, config: E1Config, **kwargs) -> None:
1192
  E1PreTrainedModel.__init__(self, config, **kwargs)
1193
  self.padding_idx = config.pad_token_id
1194
  self.vocab_size = config.vocab_size
 
1549
  config: E1Config
1550
  config_class = E1Config
1551
 
1552
+ def __init__(self, config: E1Config, **kwargs) -> None:
1553
  E1PreTrainedModel.__init__(self, config, **kwargs)
1554
  self.model: FAST_E1_ENCODER = FAST_E1_ENCODER(config, **kwargs)
1555
  self.post_init()
 
1598
  config: E1Config
1599
  config_class = E1Config
1600
 
1601
+ def __init__(self, config: E1Config, **kwargs) -> None:
1602
  E1PreTrainedModel.__init__(self, config, **kwargs)
1603
  self.model: FAST_E1_ENCODER = FAST_E1_ENCODER(config, **kwargs)
1604
  self.vocab_size = config.vocab_size
 
2101
  config: E1Config
2102
  config_class = E1Config
2103
 
2104
+ def __init__(self, config: E1Config, **kwargs) -> None:
2105
  pooling_types = kwargs.pop("pooling_types", None)
2106
  if pooling_types is None:
2107
  pooling_types = ["mean", "var"]
 
2236
  config: E1Config
2237
  config_class = E1Config
2238
 
2239
+ def __init__(self, config: E1Config, **kwargs) -> None:
2240
  E1PreTrainedModel.__init__(self, config, **kwargs)
2241
  self.model: FAST_E1_ENCODER = FAST_E1_ENCODER(config, **kwargs)
2242
  self.vocab_size = config.vocab_size
fastplms/models/e1/preparation.py CHANGED
@@ -4,12 +4,12 @@ from __future__ import annotations
4
 
5
  import itertools
6
  import os
7
- from dataclasses import dataclass
8
-
9
  import torch
 
10
  from tokenizers import Tokenizer
11
  from torch.nn.utils.rnn import pad_sequence
12
 
 
13
  PAD_TOKEN_ID = 0
14
  BOS_TOKEN_ID = 1
15
  EOS_TOKEN_ID = 2
@@ -100,7 +100,7 @@ class E1BatchPreparer:
100
  revision: str | None = None,
101
  token: str | bool | None = None,
102
  preserve_context_labels: bool = False,
103
- ):
104
  self.tokenizer = tokenizer or get_tokenizer(
105
  tokenizer_source,
106
  local_files_only=local_files_only,
@@ -111,7 +111,7 @@ class E1BatchPreparer:
111
  self.data_prep_config = data_prep_config or DataPrepConfig()
112
  self.pad_token_id = self.tokenizer.token_to_id("<pad>")
113
  self.preserve_context_labels = preserve_context_labels
114
- self.boundary_token_ids = torch.tensor(
115
  [self.tokenizer.token_to_id(token) for token in ["<bos>", "<eos>", "1", "2", "<pad>"]]
116
  ).long()
117
  self.mask_token = "?" # nosec
@@ -135,12 +135,11 @@ class E1BatchPreparer:
135
  device: torch.device | None = None,
136
  non_blocking: bool = False,
137
  ) -> dict[str, torch.Tensor | list[str] | list[int]]:
 
138
  device = torch.device("cpu") if device is None else device
139
  non_blocking = non_blocking and device.type == "cuda"
140
  padded_encodings = {}
141
- # Note: We use -1 as the padding value for sequence and position ids because the 0 value
142
- # is a valid value for sequence and position ids. -1 is then used to distinguish valid
143
- # tokens from padding tokens, for example, when doing padding/unpadding for flash attention.
144
  for key, padding_value in {
145
  "input_ids": self.pad_token_id,
146
  "sequence_ids": -1,
@@ -148,7 +147,7 @@ class E1BatchPreparer:
148
  "global_position_ids": -1,
149
  "labels": self.pad_token_id,
150
  }.items():
151
- padded_encodings[key] = pad_sequence(
152
  [enc[key] for enc in sequence_encodings],
153
  batch_first=True,
154
  padding_value=padding_value,
@@ -169,30 +168,33 @@ class E1BatchPreparer:
169
  )
170
 
171
  encodings = tuple(self.prepare_singleseq(item) for item in sequences)
172
- token_counts = torch.tensor(
173
  [encoding["input_ids"].numel() for encoding in encodings],
174
  dtype=torch.long,
175
  )
176
- input_ids = torch.cat(tuple(encoding["input_ids"] for encoding in encodings))
177
- labels = torch.cat(tuple(encoding["labels"] for encoding in encodings))
178
  positions = tuple(encoding["position_ids"] for encoding in encodings)
179
- within_seq_position_ids = torch.cat(positions)
180
 
181
  # Offsets preserve gaps left by optional X-token removal.
182
- position_spans = torch.tensor(
183
  [int(position_ids[-1].item()) + 1 for position_ids in positions],
184
  dtype=torch.long,
185
  )
186
- position_offsets = torch.cat(
187
  (torch.zeros(1, dtype=torch.long), position_spans[:-1]),
188
  ).cumsum(dim=0)
189
- global_position_ids = torch.cat(
190
  tuple(
191
  position_ids + offset
192
  for position_ids, offset in zip(positions, position_offsets, strict=True)
193
  )
194
  )
195
- sequence_ids = torch.arange(len(encodings), dtype=torch.long).repeat_interleave(
 
 
 
196
  token_counts
197
  )
198
 
@@ -241,24 +243,30 @@ class E1BatchPreparer:
241
  )
242
 
243
  symbols = itertools.chain(("<bos>", "1"), sequence, ("2", "<eos>"))
244
- tokens = torch.tensor(
245
  [self.vocab[symbol] for symbol in symbols],
246
  dtype=torch.long,
247
  )
248
- position_ids = torch.arange(tokens.numel(), dtype=torch.long)
249
 
250
  if self.data_prep_config.remove_X_tokens:
251
- keep = tokens.ne(self.X_token_id)
252
- tokens = tokens[keep]
253
- position_ids = position_ids[keep]
254
-
255
- return {"input_ids": tokens, "labels": tokens, "position_ids": position_ids}
 
 
 
 
256
 
257
  def get_boundary_token_mask(self, tokens: torch.Tensor) -> torch.BoolTensor:
258
- return torch.isin(tokens, self.boundary_token_ids.to(tokens.device))
 
259
 
260
  def get_mask_positions_mask(self, tokens: torch.Tensor) -> torch.BoolTensor:
261
- return tokens == self.mask_token_id
 
262
 
263
  def validate_sequence(self, sequence: str) -> bool:
264
  if not isinstance(sequence, str):
 
4
 
5
  import itertools
6
  import os
 
 
7
  import torch
8
+ from dataclasses import dataclass
9
  from tokenizers import Tokenizer
10
  from torch.nn.utils.rnn import pad_sequence
11
 
12
+
13
  PAD_TOKEN_ID = 0
14
  BOS_TOKEN_ID = 1
15
  EOS_TOKEN_ID = 2
 
100
  revision: str | None = None,
101
  token: str | bool | None = None,
102
  preserve_context_labels: bool = False,
103
+ ) -> None:
104
  self.tokenizer = tokenizer or get_tokenizer(
105
  tokenizer_source,
106
  local_files_only=local_files_only,
 
111
  self.data_prep_config = data_prep_config or DataPrepConfig()
112
  self.pad_token_id = self.tokenizer.token_to_id("<pad>")
113
  self.preserve_context_labels = preserve_context_labels
114
+ self.boundary_token_ids = torch.tensor( # (5,)
115
  [self.tokenizer.token_to_id(token) for token in ["<bos>", "<eos>", "1", "2", "<pad>"]]
116
  ).long()
117
  self.mask_token = "?" # nosec
 
135
  device: torch.device | None = None,
136
  non_blocking: bool = False,
137
  ) -> dict[str, torch.Tensor | list[str] | list[int]]:
138
+ # Each sequence encoding contains aligned one-dimensional tensors of length l_i.
139
  device = torch.device("cpu") if device is None else device
140
  non_blocking = non_blocking and device.type == "cuda"
141
  padded_encodings = {}
142
+ # Sequence and position ID zero is valid, so -1 unambiguously marks padding.
 
 
143
  for key, padding_value in {
144
  "input_ids": self.pad_token_id,
145
  "sequence_ids": -1,
 
147
  "global_position_ids": -1,
148
  "labels": self.pad_token_id,
149
  }.items():
150
+ padded_encodings[key] = pad_sequence( # (b, l_max)
151
  [enc[key] for enc in sequence_encodings],
152
  batch_first=True,
153
  padding_value=padding_value,
 
168
  )
169
 
170
  encodings = tuple(self.prepare_singleseq(item) for item in sequences)
171
+ token_counts = torch.tensor( # (n,)
172
  [encoding["input_ids"].numel() for encoding in encodings],
173
  dtype=torch.long,
174
  )
175
+ input_ids = torch.cat(tuple(encoding["input_ids"] for encoding in encodings)) # (t,)
176
+ labels = torch.cat(tuple(encoding["labels"] for encoding in encodings)) # (t,)
177
  positions = tuple(encoding["position_ids"] for encoding in encodings)
178
+ within_seq_position_ids = torch.cat(positions) # (t,)
179
 
180
  # Offsets preserve gaps left by optional X-token removal.
181
+ position_spans = torch.tensor( # (n,)
182
  [int(position_ids[-1].item()) + 1 for position_ids in positions],
183
  dtype=torch.long,
184
  )
185
+ position_offsets = torch.cat( # (n,)
186
  (torch.zeros(1, dtype=torch.long), position_spans[:-1]),
187
  ).cumsum(dim=0)
188
+ global_position_ids = torch.cat( # (t,)
189
  tuple(
190
  position_ids + offset
191
  for position_ids, offset in zip(positions, position_offsets, strict=True)
192
  )
193
  )
194
+ sequence_ids = torch.arange( # (t,)
195
+ len(encodings),
196
+ dtype=torch.long,
197
+ ).repeat_interleave(
198
  token_counts
199
  )
200
 
 
243
  )
244
 
245
  symbols = itertools.chain(("<bos>", "1"), sequence, ("2", "<eos>"))
246
+ tokens = torch.tensor( # (l + 4,)
247
  [self.vocab[symbol] for symbol in symbols],
248
  dtype=torch.long,
249
  )
250
+ position_ids = torch.arange(tokens.numel(), dtype=torch.long) # (l + 4,)
251
 
252
  if self.data_prep_config.remove_X_tokens:
253
+ keep = tokens.ne(self.X_token_id) # (l + 4,)
254
+ tokens = tokens[keep] # (t,)
255
+ position_ids = position_ids[keep] # (t,)
256
+
257
+ return { # Each tensor: (t,)
258
+ "input_ids": tokens,
259
+ "labels": tokens,
260
+ "position_ids": position_ids,
261
+ }
262
 
263
  def get_boundary_token_mask(self, tokens: torch.Tensor) -> torch.BoolTensor:
264
+ # tokens: (...)
265
+ return torch.isin(tokens, self.boundary_token_ids.to(tokens.device)) # (...)
266
 
267
  def get_mask_positions_mask(self, tokens: torch.Tensor) -> torch.BoolTensor:
268
+ # tokens: (...)
269
+ return tokens == self.mask_token_id # (...)
270
 
271
  def validate_sequence(self, sequence: str) -> bool:
272
  if not isinstance(sequence, str):
fastplms/models/e1/retrieval.py CHANGED
@@ -19,6 +19,8 @@ import time
19
  import urllib.error
20
  import urllib.parse
21
  import urllib.request
 
 
22
  from collections import defaultdict, namedtuple
23
  from collections.abc import Iterator, Sequence
24
  from dataclasses import dataclass
@@ -26,18 +28,15 @@ from datetime import UTC, datetime
26
  from email.utils import parsedate_to_datetime
27
  from pathlib import Path, PurePosixPath, PureWindowsPath
28
  from typing import TYPE_CHECKING, Any, TypedDict
29
-
30
- import numpy as np
31
- import torch
32
  from tqdm.auto import tqdm
33
  from transformers import PreTrainedModel
34
  from transformers.utils import logging
35
 
36
  from fastplms.embeddings import Pooler
37
-
38
  from .cache import KVCache
39
  from .preparation import DataPrepConfig, E1BatchPreparer, get_context
40
 
 
41
  if TYPE_CHECKING:
42
  from .modeling_e1 import E1MaskedLMOutputWithPast
43
 
@@ -259,19 +258,22 @@ def convert_to_tensor(
259
  "MSA rows must have equal aligned lengths after removing insertions: "
260
  f"{sorted(lengths)}"
261
  )
262
- array = np.vstack(
263
  [np.frombuffer(byte_sequence, dtype=np.uint8) for byte_sequence in byte_sequences]
264
  )
265
- return torch.from_numpy(array).to(device)
266
 
267
 
268
  def get_num_neighbors(byte_seqs: torch.ByteTensor, sim_threshold: float = 0.8) -> list[int]:
 
269
  gap_token_id = np.frombuffer(b"-", np.uint8)[0].item()
270
- seq_lens = (byte_seqs != gap_token_id).sum(dim=1)
271
  num_neighbors: list[int] = []
272
  for i in range(byte_seqs.shape[0]):
273
- query_non_gaps = byte_seqs[i] != gap_token_id
274
- seqs_sim = (byte_seqs[:, query_non_gaps] == byte_seqs[i, query_non_gaps]).sum(
 
 
275
  dim=1
276
  ) / seq_lens
277
  num_neighbors.append(int((seqs_sim >= sim_threshold).sum().item()))
@@ -279,7 +281,8 @@ def get_num_neighbors(byte_seqs: torch.ByteTensor, sim_threshold: float = 0.8) -
279
 
280
 
281
  def get_similarity_to_query(byte_seqs: torch.ByteTensor) -> torch.FloatTensor:
282
- return (byte_seqs == byte_seqs[0, :]).sum(dim=1) / byte_seqs.shape[1]
 
283
 
284
 
285
  def sample_context(
@@ -296,19 +299,19 @@ def sample_context(
296
  cache_num_neighbors_path: str | None = None,
297
  ) -> tuple[str, list[str]]:
298
  msa_sequences = parse_msa(msa_path)
299
- msa_as_byte_tensor = convert_to_tensor(msa_sequences, device)
300
  if cache_num_neighbors_path is not None and os.path.exists(cache_num_neighbors_path):
301
- num_neighbors = np.load(cache_num_neighbors_path)
302
  else:
303
- num_neighbors = np.array(
304
  get_num_neighbors(msa_as_byte_tensor, neighbor_similarity_lower_bound)
305
  )
306
  if cache_num_neighbors_path is not None:
307
  np.save(cache_num_neighbors_path, num_neighbors)
308
 
309
- sampling_weights = 1.0 / num_neighbors
310
- query_similarity = get_similarity_to_query(msa_as_byte_tensor)
311
- filtered_mask = (query_similarity <= max_query_similarity) & (
312
  query_similarity >= min_query_similarity
313
  )
314
  if int(filtered_mask.sum()) < 1:
@@ -317,8 +320,12 @@ def sample_context(
317
  f"{min_query_similarity} <= query_similarity <= {max_query_similarity}."
318
  )
319
 
320
- filtered_weights = np.where(filtered_mask.cpu().numpy(), sampling_weights, 0.0)
321
- sampled_indices = np.random.default_rng(seed).choice(
 
 
 
 
322
  len(filtered_weights),
323
  size=min(max_num_samples, int(filtered_mask.sum())),
324
  p=filtered_weights / filtered_weights.sum(),
@@ -380,7 +387,9 @@ def sample_multiple_contexts(
380
  max_token_length=context_specification.max_token_length,
381
  max_query_similarity=context_specification.max_query_similarity,
382
  min_query_similarity=context_specification.min_query_similarity,
383
- neighbor_similarity_lower_bound=context_specification.neighbor_similarity_lower_bound,
 
 
384
  use_full_sequences_in_context=use_full_sequences_in_context,
385
  full_sequences_path=full_sequences_path,
386
  seed=seed + i,
@@ -633,16 +642,17 @@ class ContextCache:
633
 
634
 
635
  def compute_ppll(logits: torch.Tensor, token_ids: torch.Tensor) -> float:
 
636
  if token_ids.numel() == 0:
637
  raise ValueError("Cannot score an empty token sequence")
638
  if token_ids.device != logits.device:
639
- token_ids = token_ids.to(logits.device)
640
  if logits.shape[0] != token_ids.shape[0]:
641
  raise ValueError(
642
  f"Logits length {logits.shape[0]} != token_ids length {token_ids.shape[0]}"
643
  )
644
- probs = logits.softmax(dim=-1)
645
- token_probs = probs.gather(dim=1, index=token_ids.unsqueeze(1)).squeeze(1)
646
  return float(token_probs.mean().item())
647
 
648
 
@@ -716,12 +726,14 @@ class _E1ContextPredictor:
716
  self, sequences: list[str], sequence_metadata: list[dict[str, str | int]]
717
  ) -> list[E1Prediction]:
718
  outputs = self.predict_batch_padded(sequences)
719
- outputs["logits"] = outputs["logits"].float()
720
- outputs["embeddings"] = outputs["embeddings"].float()
721
 
722
- token_mask = outputs["non_boundary_token_mask"] & outputs["last_sequence_mask"]
 
 
723
  if self.save_masked_positions_only:
724
- token_mask = token_mask & outputs["mask_positions_mask"]
725
 
726
  predictions: list[E1Prediction] = []
727
  for i in range(len(sequences)):
@@ -729,17 +741,21 @@ class _E1ContextPredictor:
729
  if "context_id" in sequence_metadata[i]:
730
  pred["context_id"] = sequence_metadata[i]["context_id"]
731
  if "logits" in self.fields_to_save:
732
- pred["logits"] = outputs["logits"][i, token_mask[i]]
733
  if not self.keep_predictions_in_gpu:
734
- pred["logits"] = pred["logits"].to("cpu")
735
  if "token_embeddings" in self.fields_to_save:
736
- pred["token_embeddings"] = outputs["embeddings"][i, token_mask[i]]
737
  if not self.keep_predictions_in_gpu:
738
- pred["token_embeddings"] = pred["token_embeddings"].to("cpu")
739
  if "mean_token_embeddings" in self.fields_to_save:
740
- pred["mean_token_embeddings"] = outputs["embeddings"][i, token_mask[i]].mean(dim=0)
 
 
741
  if not self.keep_predictions_in_gpu:
742
- pred["mean_token_embeddings"] = pred["mean_token_embeddings"].to("cpu")
 
 
743
  predictions.append(pred)
744
  return predictions
745
 
@@ -767,12 +783,16 @@ class _E1ContextPredictor:
767
  if self.kv_cache is not None:
768
  self.kv_cache.after_forward(batch, output)
769
 
770
- padding_mask = batch["input_ids"] == self.batch_preparer.pad_token_id
771
- last_sequence_mask = (
772
  batch["sequence_ids"] == batch["sequence_ids"].max(dim=1).values[:, None]
773
  )
774
- boundary_token_mask = self.batch_preparer.get_boundary_token_mask(batch["input_ids"])
775
- mask_positions_mask = self.batch_preparer.get_mask_positions_mask(batch["input_ids"])
 
 
 
 
776
  return {
777
  "logits": output.logits,
778
  "embeddings": output.last_hidden_state,
@@ -817,17 +837,18 @@ def _pool_hidden_states(
817
  pooling_types: list[str],
818
  device: torch.device,
819
  ) -> torch.Tensor:
 
820
  pooler = Pooler(pooling_types)
821
  max_len = max(hidden.shape[0] for hidden in hidden_list)
822
  hidden_dim = hidden_list[0].shape[1]
823
  batch_size = len(hidden_list)
824
- padded = torch.zeros(batch_size, max_len, hidden_dim, device=device)
825
- attention_mask = torch.zeros(batch_size, max_len, device=device)
826
  for i, hidden in enumerate(hidden_list):
827
  seq_len = hidden.shape[0]
828
  padded[i, :seq_len] = hidden
829
  attention_mask[i, :seq_len] = 1.0
830
- return pooler(padded, attention_mask)
831
 
832
 
833
  def _forward_for_embedding(
 
19
  import urllib.error
20
  import urllib.parse
21
  import urllib.request
22
+ import numpy as np
23
+ import torch
24
  from collections import defaultdict, namedtuple
25
  from collections.abc import Iterator, Sequence
26
  from dataclasses import dataclass
 
28
  from email.utils import parsedate_to_datetime
29
  from pathlib import Path, PurePosixPath, PureWindowsPath
30
  from typing import TYPE_CHECKING, Any, TypedDict
 
 
 
31
  from tqdm.auto import tqdm
32
  from transformers import PreTrainedModel
33
  from transformers.utils import logging
34
 
35
  from fastplms.embeddings import Pooler
 
36
  from .cache import KVCache
37
  from .preparation import DataPrepConfig, E1BatchPreparer, get_context
38
 
39
+
40
  if TYPE_CHECKING:
41
  from .modeling_e1 import E1MaskedLMOutputWithPast
42
 
 
258
  "MSA rows must have equal aligned lengths after removing insertions: "
259
  f"{sorted(lengths)}"
260
  )
261
+ array = np.vstack( # (n, l)
262
  [np.frombuffer(byte_sequence, dtype=np.uint8) for byte_sequence in byte_sequences]
263
  )
264
+ return torch.from_numpy(array).to(device) # (n, l)
265
 
266
 
267
  def get_num_neighbors(byte_seqs: torch.ByteTensor, sim_threshold: float = 0.8) -> list[int]:
268
+ # byte_seqs: (n, l)
269
  gap_token_id = np.frombuffer(b"-", np.uint8)[0].item()
270
+ seq_lens = (byte_seqs != gap_token_id).sum(dim=1) # (n,)
271
  num_neighbors: list[int] = []
272
  for i in range(byte_seqs.shape[0]):
273
+ query_non_gaps = byte_seqs[i] != gap_token_id # (l,)
274
+ seqs_sim = ( # (n,)
275
+ byte_seqs[:, query_non_gaps] == byte_seqs[i, query_non_gaps]
276
+ ).sum(
277
  dim=1
278
  ) / seq_lens
279
  num_neighbors.append(int((seqs_sim >= sim_threshold).sum().item()))
 
281
 
282
 
283
  def get_similarity_to_query(byte_seqs: torch.ByteTensor) -> torch.FloatTensor:
284
+ # byte_seqs: (n, l)
285
+ return (byte_seqs == byte_seqs[0, :]).sum(dim=1) / byte_seqs.shape[1] # (n,)
286
 
287
 
288
  def sample_context(
 
299
  cache_num_neighbors_path: str | None = None,
300
  ) -> tuple[str, list[str]]:
301
  msa_sequences = parse_msa(msa_path)
302
+ msa_as_byte_tensor = convert_to_tensor(msa_sequences, device) # (n, l)
303
  if cache_num_neighbors_path is not None and os.path.exists(cache_num_neighbors_path):
304
+ num_neighbors = np.load(cache_num_neighbors_path) # (n,)
305
  else:
306
+ num_neighbors = np.array( # (n,)
307
  get_num_neighbors(msa_as_byte_tensor, neighbor_similarity_lower_bound)
308
  )
309
  if cache_num_neighbors_path is not None:
310
  np.save(cache_num_neighbors_path, num_neighbors)
311
 
312
+ sampling_weights = 1.0 / num_neighbors # (n,)
313
+ query_similarity = get_similarity_to_query(msa_as_byte_tensor) # (n,)
314
+ filtered_mask = (query_similarity <= max_query_similarity) & ( # (n,)
315
  query_similarity >= min_query_similarity
316
  )
317
  if int(filtered_mask.sum()) < 1:
 
320
  f"{min_query_similarity} <= query_similarity <= {max_query_similarity}."
321
  )
322
 
323
+ filtered_weights = np.where( # (n,)
324
+ filtered_mask.cpu().numpy(),
325
+ sampling_weights,
326
+ 0.0,
327
+ )
328
+ sampled_indices = np.random.default_rng(seed).choice( # (n_sampled,)
329
  len(filtered_weights),
330
  size=min(max_num_samples, int(filtered_mask.sum())),
331
  p=filtered_weights / filtered_weights.sum(),
 
387
  max_token_length=context_specification.max_token_length,
388
  max_query_similarity=context_specification.max_query_similarity,
389
  min_query_similarity=context_specification.min_query_similarity,
390
+ neighbor_similarity_lower_bound=(
391
+ context_specification.neighbor_similarity_lower_bound
392
+ ),
393
  use_full_sequences_in_context=use_full_sequences_in_context,
394
  full_sequences_path=full_sequences_path,
395
  seed=seed + i,
 
642
 
643
 
644
  def compute_ppll(logits: torch.Tensor, token_ids: torch.Tensor) -> float:
645
+ # logits: (l, c); token_ids: (l,)
646
  if token_ids.numel() == 0:
647
  raise ValueError("Cannot score an empty token sequence")
648
  if token_ids.device != logits.device:
649
+ token_ids = token_ids.to(logits.device) # (l,)
650
  if logits.shape[0] != token_ids.shape[0]:
651
  raise ValueError(
652
  f"Logits length {logits.shape[0]} != token_ids length {token_ids.shape[0]}"
653
  )
654
+ probs = logits.softmax(dim=-1) # (l, c)
655
+ token_probs = probs.gather(dim=1, index=token_ids.unsqueeze(1)).squeeze(1) # (l,)
656
  return float(token_probs.mean().item())
657
 
658
 
 
726
  self, sequences: list[str], sequence_metadata: list[dict[str, str | int]]
727
  ) -> list[E1Prediction]:
728
  outputs = self.predict_batch_padded(sequences)
729
+ outputs["logits"] = outputs["logits"].float() # (b, l, c)
730
+ outputs["embeddings"] = outputs["embeddings"].float() # (b, l, d)
731
 
732
+ token_mask = ( # (b, l)
733
+ outputs["non_boundary_token_mask"] & outputs["last_sequence_mask"]
734
+ )
735
  if self.save_masked_positions_only:
736
+ token_mask = token_mask & outputs["mask_positions_mask"] # (b, l)
737
 
738
  predictions: list[E1Prediction] = []
739
  for i in range(len(sequences)):
 
741
  if "context_id" in sequence_metadata[i]:
742
  pred["context_id"] = sequence_metadata[i]["context_id"]
743
  if "logits" in self.fields_to_save:
744
+ pred["logits"] = outputs["logits"][i, token_mask[i]] # (r_i, c)
745
  if not self.keep_predictions_in_gpu:
746
+ pred["logits"] = pred["logits"].to("cpu") # (r_i, c)
747
  if "token_embeddings" in self.fields_to_save:
748
+ pred["token_embeddings"] = outputs["embeddings"][i, token_mask[i]] # (r_i, d)
749
  if not self.keep_predictions_in_gpu:
750
+ pred["token_embeddings"] = pred["token_embeddings"].to("cpu") # (r_i, d)
751
  if "mean_token_embeddings" in self.fields_to_save:
752
+ pred["mean_token_embeddings"] = outputs["embeddings"][
753
+ i, token_mask[i]
754
+ ].mean(dim=0) # (d,)
755
  if not self.keep_predictions_in_gpu:
756
+ pred["mean_token_embeddings"] = pred["mean_token_embeddings"].to( # (d,)
757
+ "cpu"
758
+ )
759
  predictions.append(pred)
760
  return predictions
761
 
 
783
  if self.kv_cache is not None:
784
  self.kv_cache.after_forward(batch, output)
785
 
786
+ padding_mask = batch["input_ids"] == self.batch_preparer.pad_token_id # (b, l)
787
+ last_sequence_mask = ( # (b, l)
788
  batch["sequence_ids"] == batch["sequence_ids"].max(dim=1).values[:, None]
789
  )
790
+ boundary_token_mask = self.batch_preparer.get_boundary_token_mask( # (b, l)
791
+ batch["input_ids"]
792
+ )
793
+ mask_positions_mask = self.batch_preparer.get_mask_positions_mask( # (b, l)
794
+ batch["input_ids"]
795
+ )
796
  return {
797
  "logits": output.logits,
798
  "embeddings": output.last_hidden_state,
 
837
  pooling_types: list[str],
838
  device: torch.device,
839
  ) -> torch.Tensor:
840
+ # Each hidden_list entry: (l_i, d)
841
  pooler = Pooler(pooling_types)
842
  max_len = max(hidden.shape[0] for hidden in hidden_list)
843
  hidden_dim = hidden_list[0].shape[1]
844
  batch_size = len(hidden_list)
845
+ padded = torch.zeros(batch_size, max_len, hidden_dim, device=device) # (b, l_max, d)
846
+ attention_mask = torch.zeros(batch_size, max_len, device=device) # (b, l_max)
847
  for i, hidden in enumerate(hidden_list):
848
  seq_len = hidden.shape[0]
849
  padded[i, :seq_len] = hidden
850
  attention_mask[i, :seq_len] = 1.0
851
+ return pooler(padded, attention_mask) # (b, n_poolers * d)
852
 
853
 
854
  def _forward_for_embedding(
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 != "5289065bf7679630038a2e794052e4364a152c4f103c36c80ab9b8544919e924":
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,14 +179,14 @@ def _install_runtime():
223
  return package
224
 
225
  _install_runtime()
226
- _module_225 = _import_without_bytecode("fastplms.models.e1.modeling_e1")
227
- E1Config = _module_225.E1Config
228
  E1Config.__module__ = __name__
229
- E1ForMaskedLM = _module_225.E1ForMaskedLM
230
  E1ForMaskedLM.__module__ = __name__
231
- E1ForSequenceClassification = _module_225.E1ForSequenceClassification
232
  E1ForSequenceClassification.__module__ = __name__
233
- E1ForTokenClassification = _module_225.E1ForTokenClassification
234
  E1ForTokenClassification.__module__ = __name__
235
- E1Model = _module_225.E1Model
236
  E1Model.__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 != "a8dda20d6a4a55e6808c7c2f2c9477c113edaa7d6ec6ef57f4a898cd47b73d48":
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.e1.modeling_e1")
183
+ E1Config = _module_181.E1Config
184
  E1Config.__module__ = __name__
185
+ E1ForMaskedLM = _module_181.E1ForMaskedLM
186
  E1ForMaskedLM.__module__ = __name__
187
+ E1ForSequenceClassification = _module_181.E1ForSequenceClassification
188
  E1ForSequenceClassification.__module__ = __name__
189
+ E1ForTokenClassification = _module_181.E1ForTokenClassification
190
  E1ForTokenClassification.__module__ = __name__
191
+ E1Model = _module_181.E1Model
192
  E1Model.__module__ = __name__
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Direct runtime dependencies for Synthyra/Profluent-E1-600M.
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
@@ -7,43 +7,44 @@
7
  "LICENSES/e1/LICENSE": "sha256:8ef1dd556091544db3044164a8015424a3dcb3450fb3765a81b88463551bbe81",
8
  "LICENSES/e1/MODIFICATIONS.md": "sha256:2506f47c0f5475af8e8ff2cff13eb8b79e8e25a08a054cdd617bf336536750ca",
9
  "LICENSES/e1/NOTICE": "sha256:6de9db0320b4ee82f665c0951d8fd4cd53701a659c9dbce9bc3e3ea6afc4c6b3",
10
- "README.md": "sha256:ce6fa1b7d092ed995e6d53650d2257576a0a7b5da86c5eb0b9f92ca3e509eac0",
11
  "THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
12
- "config.json": "sha256:7019ed218cf9d6c05a571a6322b114a43d40a7ca727aef4606c92af0ad63b5f8",
13
- "fastplms/__init__.py": "sha256:4fb3196022ca8ec699d59d09bdbc5f0184195552b773698ab9b061fe3cd7df12",
14
- "fastplms/attention/__init__.py": "sha256:f60b9fecfb4bcb37a4e7c26dc2f752b9035f9cbad627b4a84213f3a92ec88f7d",
15
- "fastplms/attention/_core.py": "sha256:8f7ec5b65bd8b6c6fa4951d50d1c0e499abf03ae00914794b51fc410201e3e33",
16
- "fastplms/attention/_kernel_lock.py": "sha256:85d8521a2af5f94fad3948af3814db0c866c414ee4d43df797b9bd6f980e947b",
17
- "fastplms/attention/interfaces.py": "sha256:1c6f06a8e411e0f9bf6d230522205c93ae46ea58864006fbb892aa05e5ca5749",
18
- "fastplms/embeddings/__init__.py": "sha256:47ff8cdf682d44037dd9edab133e2e60675d60786bd5cf0bffd1998f31985555",
19
- "fastplms/embeddings/pooling.py": "sha256:a140266ed6b1cc344c8507edc5c6c4f2dce464c3db70ba4b16c7ac2ba2fad96e",
20
- "fastplms/embeddings/runner.py": "sha256:23ee4727a918d6d331f7a0f89b823d149f1a791f0c5586e3496d7b6eb2ce97e0",
21
- "fastplms/embeddings/storage.py": "sha256:3fbe2bab75092e5a4cadf4d27e4752181d597469a65a55db085ceef808ed418e",
22
- "fastplms/embeddings/types.py": "sha256:119718a20989d1ae5a60fabc0f5e98bdc172c5163b04db3d4554ac3956b30e52",
23
  "fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
24
- "fastplms/models/__init__.py": "sha256:5e48c2cb3877aa6f42f3b5411d53b16bba2e32827bbde634f47f174c5cb36f86",
25
  "fastplms/models/e1/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
26
- "fastplms/models/e1/attention.py": "sha256:8449b2bafba9573dfcfe4ad907e1fce24c55a4f279f1f72cd52227e48dcecf67",
27
- "fastplms/models/e1/cache.py": "sha256:b17a0a743505f567b387d1edb8550f028a8770db1ba56ad8ad2ff0d3fce19c46",
28
- "fastplms/models/e1/modeling_e1.py": "sha256:96545247491395291060e2cbb7b6b082ed6073e0fbf12d4656e490010aae5ae4",
29
- "fastplms/models/e1/preparation.py": "sha256:3c1cc96d36cbfa74dc0f847cc5f44c6edd527d8e223d0a564d4891a041761395",
30
- "fastplms/models/e1/retrieval.py": "sha256:d077ef10100cf4f1482fbc7c8a944c241e61061a94374699156388d9e8e5c4db",
31
  "fastplms/models/e1/tokenizer.json": "sha256:65d4345dbe908d3deb554891dc01b94e215fb17cd9e0bd0535e062e256737415",
32
- "fastplms/models/ttt.py": "sha256:a0df4e98b02120d423e3c7ca9b866a8d0e3748b9076042a0102a838a11aed046",
33
- "fastplms/registry.py": "sha256:afca271911b651a882345b74a58366494a1d784e4a48c8683a87f5508f4ba16e",
34
- "fastplms/runtime.py": "sha256:110018646d6f248cedab140a030c3065e1b062b61f6aff659c231e538614bc01",
35
- "fastplms_bundle.py": "sha256:0b53f8920820cd377c6a342390542caa7e886e27005d94b20fd7d8d9c2811cc4",
36
- "modeling_fastplms.py": "sha256:11932687d6d095013137669f2d4faeda96420b4ad5db16a3f90eb93cebd26c9d"
 
37
  },
38
  "model_id": "e1_600m",
39
  "redistributable": true,
40
- "release_tool_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
41
- "release_tool_sha256": "19b4247aba04a74de5069397518374ab2c3b9e0ce18ba9f36de34f0f4c5d1ceb",
42
- "runtime_bundle_sha256": "5289065bf7679630038a2e794052e4364a152c4f103c36c80ab9b8544919e924",
43
- "runtime_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
44
  "schema_version": 2,
45
  "scope": "runtime-only",
46
- "source_tree_sha256": "c583bf72e969d20248d4f2a01c81579fffa3002e2e19c4e90acebbbacf1faeab",
47
  "weights": {
48
  "repo_id": "Synthyra/Profluent-E1-600M",
49
  "revision": "6c8bf0ec83b0e0178677c528b101efffd0677742"
 
7
  "LICENSES/e1/LICENSE": "sha256:8ef1dd556091544db3044164a8015424a3dcb3450fb3765a81b88463551bbe81",
8
  "LICENSES/e1/MODIFICATIONS.md": "sha256:2506f47c0f5475af8e8ff2cff13eb8b79e8e25a08a054cdd617bf336536750ca",
9
  "LICENSES/e1/NOTICE": "sha256:6de9db0320b4ee82f665c0951d8fd4cd53701a659c9dbce9bc3e3ea6afc4c6b3",
10
+ "README.md": "sha256:f4e31044d720a5b058539f147dd9b1d4d85d34f2a6929dbf4d3684a07974b6de",
11
  "THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
12
+ "config.json": "sha256:a679591daa125bbdf6689af936ebfb72b90a06d7b83c0b64845d7d0ed7b4f78d",
13
+ "fastplms/__init__.py": "sha256:8503e02debf24abc6d33c1913ff3eb9f245e63f7e62017e220d072c7393fdf2c",
14
+ "fastplms/attention/__init__.py": "sha256:ab3c0b6156968f418f3c97ea664959cb978f4137717b4362213bcfe25ab5e3d6",
15
+ "fastplms/attention/_core.py": "sha256:09d0f71a5ea2ad2138c9441f6a2f72bbf8e72729be41d8db737b163f36ce47da",
16
+ "fastplms/attention/_kernel_lock.py": "sha256:56455eb57cee9af08438eda52b775c9cff3cf994cadb9b116ce2ee10fd5f1801",
17
+ "fastplms/attention/interfaces.py": "sha256:6562a342961022fc2848d7004bd01fddaf8be8e08e782d0856e5c30a74232623",
18
+ "fastplms/embeddings/__init__.py": "sha256:cf0648de905a00ad16c0aece328eeb05f25739a4cf89b47b915e0f2b19c3ce26",
19
+ "fastplms/embeddings/pooling.py": "sha256:9bed8c466fce053333de9e25b04d1ab304f2954f403ffa0f5a3c749e8277b3ce",
20
+ "fastplms/embeddings/runner.py": "sha256:a7839763ad641a63d0e2671dcf34aa7b2a8114198950b64254188521b27453a8",
21
+ "fastplms/embeddings/storage.py": "sha256:98cf725d26766f32300f105345129ca6cf02ebd10628e746a7e71e107bc911a6",
22
+ "fastplms/embeddings/types.py": "sha256:c58f8c6a37a11a937c3f77b4e1617d5f213923d98ecaffddcdd283bd9dc42fe7",
23
  "fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
24
+ "fastplms/models/__init__.py": "sha256:d3dd084f5e8fafcf2d0fe4a26e4ab9284d3f9414b1f9a5bd86e308a406447c43",
25
  "fastplms/models/e1/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
26
+ "fastplms/models/e1/attention.py": "sha256:114bff9ed29a9f98a91965a57b702413f4a0e84bdd4063e7047e03a49a61241a",
27
+ "fastplms/models/e1/cache.py": "sha256:b4d065b22b3be06f929eac451a0e5a4658ac3ad366fc6ee5e9ac195af821b434",
28
+ "fastplms/models/e1/modeling_e1.py": "sha256:bb1c963731b7277a67d444ea0df6ed340384a6fafe80a0042e943fb844ae26e0",
29
+ "fastplms/models/e1/preparation.py": "sha256:b207f85032a3fbac8494ec958ecdfa37de470f6996b8568da9374e2e7ee0598c",
30
+ "fastplms/models/e1/retrieval.py": "sha256:6f4ac4b9f21ea874d99b91ee1f366bc456af1c0060e678e3ee7cda4c2820cefd",
31
  "fastplms/models/e1/tokenizer.json": "sha256:65d4345dbe908d3deb554891dc01b94e215fb17cd9e0bd0535e062e256737415",
32
+ "fastplms/models/ttt.py": "sha256:f1d7f9298a930f4c40f8e321ffa08b09d1c5b8492bca623bdc7bfd03ec85774d",
33
+ "fastplms/registry.py": "sha256:cb37799dae5a4c0c780089ab2cca4cc56300c1c5f515b275892aa4e1c4b97eb6",
34
+ "fastplms/runtime.py": "sha256:9521c37fcd168bf9a4137277fb73672403ccf30d70d4845aa879fb22f42ddeb6",
35
+ "fastplms_bundle.py": "sha256:c5015ec74cd70db2cd82ed30d675510757d26bcd18a184c73560f85a8b1e9f74",
36
+ "modeling_fastplms.py": "sha256:bb40b09e9d61bf567d0590cb22ae7a5f8f7fb74d3694fd87c42c862c7301ecf3",
37
+ "requirements.txt": "sha256:9dba69cb9fd6b1c083423aa17df29bb5f98d46e7189b8c5b2393a45501056ebf"
38
  },
39
  "model_id": "e1_600m",
40
  "redistributable": true,
41
+ "release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
42
+ "release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
43
+ "runtime_bundle_sha256": "a8dda20d6a4a55e6808c7c2f2c9477c113edaa7d6ec6ef57f4a898cd47b73d48",
44
+ "runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
45
  "schema_version": 2,
46
  "scope": "runtime-only",
47
+ "source_tree_sha256": "9e3cbab448ffde4fbbba79f17cf0321645584acd534c5f96c78b1017d9f3806b",
48
  "weights": {
49
  "repo_id": "Synthyra/Profluent-E1-600M",
50
  "revision": "6c8bf0ec83b0e0178677c528b101efffd0677742"