lhallee commited on
Commit
ee7f2fd
·
verified ·
1 Parent(s): 947df73

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
@@ -16,16 +16,35 @@ Accepted inputs are amino-acid sequences tokenized to residue IDs.
16
  Supported Transformers entry points are `AutoConfig`, `AutoModel`,
17
  `AutoModelForMaskedLM`.
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  ## Install and platform requirements
20
 
21
- Install the current FastPLMs package:
22
 
23
  ```bash
24
- python -m pip install \
25
- "fastplms @ git+https://github.com/Synthyra/FastPLMs.git"
26
  ```
27
 
28
- Python 3.11-3.14, PyTorch 2.13, and Transformers 5.13 are required. Eager, SDPA, and Flex use the core install. FlashAttention requires the `flash` extra, compatible CUDA hardware, and BF16 execution. The Hub quick start below requires network
 
 
 
29
  access on first download. For an air-gapped run, first build the manifest-pinned
30
  local artifact and use the offline form shown in the example.
31
 
@@ -38,23 +57,23 @@ model_id = "Synthyra/ESMplusplus_large"
38
  model = AutoModel.from_pretrained(
39
  model_id,
40
  trust_remote_code=True,
 
41
  ).eval()
42
  ```
43
 
44
- This example uses the published Hub repository. For offline validation, build
45
- the manifest-pinned artifact and replace `model_id` with its local
46
- `dist/hub/ESMplusplus_large` path, then pass `local_files_only=True`.
47
-
48
- Leave attention unspecified for the Transformers default. Supported explicit
49
- choices are `eager`, `sdpa`, `flex_attention`, `flash_attention_2`,
50
- `flash_attention_3`.
51
- Pass the selected name through `attn_implementation`.
52
- When an optimized backend cannot return full attention tensors,
53
- `output_attentions=True` emits one explicit runtime warning and uses a correctly
54
- masked eager implementation for that call only. The warning identifies the
55
- configured backend, effective backend, and reason. Configuration and later
56
- calls are unchanged.
57
- For BF16 execution, this family uses parameters loaded directly in BF16.
58
 
59
  ## Tokenization and forward inference
60
 
@@ -84,43 +103,78 @@ print(output.last_hidden_state.shape)
84
 
85
  ## Dataset embeddings
86
 
87
- The shared embedding API accepts sequences, `(id, sequence)` pairs,
88
- `EmbeddingInput` records, insertion-ordered `{id: sequence}` mappings, or a
89
- FASTA path. Results preserve order and duplicate identifiers:
90
 
91
  ```python
92
- result = model.embed_dataset(
93
  ["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"],
94
  batch_size=2,
95
  pooling=("mean", "std"),
96
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- for record in result:
99
- print(record.id, record.sequence, record.tensor.shape)
 
 
 
 
 
 
 
 
 
100
  ```
101
 
102
- Set `full_embeddings=True` for one residue tensor with shape `(l, d)` per
103
- sequence. Set `output` to a directory for bounded-memory, transactional
104
- safetensors with ordered-prefix resume, or choose `format="sqlite"` for
105
- batch-level database commits and exact resume. Pooling excludes boundary,
106
- padding, and other non-biological positions.
 
 
 
107
 
108
- For a long FASTA run, stream completed batches into SQLite:
 
109
 
110
  ```python
111
- persisted = model.embed_dataset(
112
- "proteins.fasta",
113
- batch_size=64,
114
- pooling=("mean",),
115
- output="protein-embeddings.sqlite",
116
- format="sqlite",
117
- resume=True,
 
 
118
  )
 
 
 
119
  ```
120
 
121
- Resume verifies the input order, model state, tokenizer policy, backend, dtype,
122
- and pooling configuration. It never appends incompatible records to an
123
- existing run.
124
 
125
  ## ESMC behavior
126
 
@@ -133,14 +187,9 @@ Those deviations produce diagnostic warnings rather than strict parity
133
  failures; dispatch integrity, masks, finite outputs, shapes, and catastrophic
134
  biological disagreement remain hard gates.
135
 
136
- The current locked GH200/aarch64 release image executes eager, SDPA, and Flex.
137
- It has no validated FlashAttention 2 kernel and no locked aarch64 artifact for
138
- the manifest-pinned FlashAttention 3 kernel. Both Flash backends remain
139
- supported, non-experimental interfaces, but requests fail closed without
140
- dispatch on this image. Prior real FlashAttention 2 execution was captured in
141
- separate workstation JUnit, but that immutable report and environment
142
- attestation are not bundled in this repository. It is not reused as a current
143
- ESMC release distribution or numerical claim.
144
 
145
  When `sequence_id` is supplied, it is authoritative for ESMC attention grouping
146
  and padding, and `attention_mask` is ignored. Values greater than or equal to
@@ -149,48 +198,18 @@ use `attention_mask` as the padding contract.
149
 
150
  | Backend | Support | Measurement status |
151
  | --- | --- | --- |
152
- | `sdpa` | Recommended fidelity path | Pending complete validated 30-record frozen-head GH200/aarch64 set |
153
- | `eager` | Supported | Pending complete validated 30-record frozen-head GH200/aarch64 set |
154
- | `flash_attention_2` | Supported | Current GH200/aarch64 lock: unavailable; pending structured schema-v3 availability record |
155
- | `flex_attention` | Supported, numerically divergent | Pending complete validated 30-record frozen-head GH200/aarch64 set |
156
- | `flash_attention_3` | Supported, numerically divergent | Current GH200/aarch64 lock: unavailable; pending structured schema-v3 availability record |
157
-
158
- No threshold, report from another checkpoint, or result from another
159
- accelerator is substituted for a measurement. A release set contains all
160
- 30 model/backend/panel records from one exact GH200 device and aarch64 runtime:
161
- 18 eager/SDPA/Flex measurements include relative L2, Q99.9, residue
162
- cosine, pooled cosine, top-1, and Jensen-Shannon distributions; 12
163
- FlashAttention 2/3 records explicitly attest locked-platform unavailability.
164
-
165
- ## Locked oracle package compatibility exception
166
-
167
- The frozen oracle lock permits exactly one nonzero `pip check` diagnostic:
168
- `nvidia-cusparselt-cu13 0.8.1 is not supported on this platform`. It applies only to
169
- `nvidia-cusparselt-cu13==0.8.1` on
170
- `NVIDIA GH200 480GB` / `linux` /
171
- `aarch64`. The vendor filename tag is
172
- `py3-none-manylinux2014_aarch64`, while the wheel metadata declares
173
- `py3-none-manylinux2014_sbsa`. The exact wheel is
174
- `nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl` with SHA-256 `4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f`.
175
- FastPLMs accepts this vendor metadata mismatch only after the lock, installed
176
- inventory, wheel bytes, metadata tag, and target identity all match. The wheel
177
- is not rewritten (`validated-vendor-metadata-exception-no-wheel-rewrite`). Any additional diagnostic or
178
- identity drift fails closed.
179
-
180
-
181
- No number is inferred from a threshold or copied from a different checkpoint.
182
- Before release, replace each pending cell only through the strict 30-record
183
- ingestion gate tied to this exact Hub revision, runtime revision, BF16 dtype,
184
- the current exact GH200/aarch64 accelerator and container identity, and both
185
- locked sequence panels. The set contains 18 eager/SDPA/Flex measurement records
186
- and 12 structured Flash locked-platform unavailable records. H100 and H200
187
- remain Hopper-class deployment examples, but they are not current ESMC release-confirmation evidence.
188
- Results are never
189
- carried across device, platform, source, lock, or image identities.
190
- Diagnostic JSON is written under `artifacts/diagnostics/esmc/`. Catastrophe
191
- guardrails (relative L2 `0.25`, Q99.9 `0.50`, residue cosine `0.90`, pooled
192
- cosine `0.95`, top-1 `0.80`, Jensen-Shannon `0.05`) detect corruption and do
193
- not constitute parity or quality claims.
194
 
195
  ## Runtime contract
196
 
@@ -201,7 +220,7 @@ not constitute parity or quality claims.
201
  - Precision policies: `default`
202
  - BF16 execution: `static_parameters`
203
  - Generation contract: `not_applicable`
204
- - Optional dependency group: `core`
205
  - Weight publication allowed: `true`
206
  - Weight license status: `resolved`
207
  - Redistributable: `true`
@@ -212,28 +231,22 @@ not constitute parity or quality claims.
212
  - FastPLMs weights: `Synthyra/ESMplusplus_large`
213
  - Runtime revision: recorded separately in the built artifact and published commit
214
  - Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
215
- - Generator/schema version and complete/runtime-only attestations: recorded in `provenance.json`
216
  - Official checkpoint: `biohub/ESMC-600M`
217
  - Artifact source: `fast`
218
  - State transform: `esmc_to_fastplms_v1`
219
- - BF16 execution: `static_parameters`
220
  - Pinned upstreams: `biohub-esm`, `biohub-transformers`
221
- - Reference container: `reference-biohub-esm`
222
  - Release tiers: `check`, `compliance`, `feature`, `artifact`, `benchmark`
223
  - Unresolved required file identities: `0`
224
 
225
- The local artifact records exact file identities, conversion details, source
226
- revisions, and legal texts in `provenance.json`. A nonzero unresolved count is a
227
- release blocker.
228
 
229
  ## Validation boundary
230
 
231
- For tiers declared by the manifest, the release contract compares applicable
232
- semantic configuration, tokenizer behavior, state keys, shapes, dtypes,
233
- values, aliases, and representative inference with the pinned official
234
- implementation. This metadata does not by itself claim that a particular build
235
- passed, that one backend is faster, or that an output has biological or
236
- therapeutic validity.
237
 
238
  ## License
239
 
 
16
  Supported Transformers entry points are `AutoConfig`, `AutoModel`,
17
  `AutoModelForMaskedLM`.
18
 
19
+ ## Capabilities
20
+
21
+ | Feature | Status |
22
+ | --- | --- |
23
+ | Sequence classification | Unavailable: no advertised AutoClass |
24
+ | Token classification | Unavailable: no advertised AutoClass |
25
+ | PEFT fine-tuning | Supported pattern: attach LoRA to the pretrained model |
26
+ | Embeddings | Supported: shared ordered embedding API |
27
+ | Test-time training | Supported: low-rank masked-residue adaptation |
28
+ | Attention variants | Special: SDPA fidelity path; alternate backends have explicit bands |
29
+ | Compliance | Declared: exact release evidence is required |
30
+
31
+ A supported interface is not a pretrained downstream predictor. Classification
32
+ heads start untrained, and declared compliance metadata is not a claim that an
33
+ arbitrary local build passed its release gate.
34
+
35
  ## Install and platform requirements
36
 
37
+ Install the direct dependencies published with this model:
38
 
39
  ```bash
40
+ python -m pip install -r \
41
+ "https://huggingface.co/Synthyra/ESMplusplus_large/resolve/main/requirements.txt"
42
  ```
43
 
44
+ The FastPLMs implementation itself is embedded in the model repository and loaded
45
+ by Transformers through `trust_remote_code=True`.
46
+
47
+ Python 3.11-3.14, PyTorch 2.13, and Transformers 5.13 are required. The artifact requirements include the direct FlashAttention loader dependency. FlashAttention also requires compatible CUDA hardware and BF16 execution. The Hub quick start below requires network
48
  access on first download. For an air-gapped run, first build the manifest-pinned
49
  local artifact and use the offline form shown in the example.
50
 
 
57
  model = AutoModel.from_pretrained(
58
  model_id,
59
  trust_remote_code=True,
60
+ attn_implementation="sdpa",
61
  ).eval()
62
  ```
63
 
64
+ For offline validation, replace `model_id` with the manifest-built
65
+ `dist/hub/ESMplusplus_large` path and pass `local_files_only=True`.
66
+
67
+ ## Attention and compliance
68
+
69
+ The quick start selects `sdpa` explicitly. Declared variants are `eager`, `sdpa`, `flex_attention`, `flash_attention_2`,
70
+ `flash_attention_3`. An unavailable requested backend raises instead of
71
+ silently switching implementations.
72
+ `output_attentions=True` may use the documented, one-call eager fallback solely
73
+ to materialize attention tensors; the configured backend remains unchanged.
74
+
75
+ This family declares the `compliance` tier. Release evidence binds the exact
76
+ checkpoint, backend, dtype, hardware, inputs, and reference revision.
 
77
 
78
  ## Tokenization and forward inference
79
 
 
103
 
104
  ## Dataset embeddings
105
 
106
+ The shared embedding mixin preserves input order and biological-position
107
+ masking. It accepts sequences, identified records, mappings, or a FASTA path:
 
108
 
109
  ```python
110
+ pooled = model.embed_dataset(
111
  ["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"],
112
  batch_size=2,
113
  pooling=("mean", "std"),
114
  )
115
+ residues = model.embed_dataset(
116
+ ["MSTNPKPQRKTKRNT"],
117
+ full_embeddings=True,
118
+ )
119
+ print(pooled[0].tensor.shape) # (2 * d,)
120
+ print(residues[0].tensor.shape) # (l, d)
121
+ ```
122
+
123
+ Set `output` and `format="safetensors"` or `"sqlite"` for transactional,
124
+ bounded-memory persistence. Resume verifies input order, model state, tokenizer
125
+ policy, backend, dtype, and pooling configuration before appending.
126
+
127
+ ## PEFT fine-tuning
128
+
129
+ Install the direct training dependencies, then attach LoRA to the loaded checkpoint:
130
+
131
+ ```bash
132
+ python -m pip install "datasets>=4.8,<5" "peft>=0.19,<0.20"
133
+ ```
134
 
135
+ ```python
136
+ from peft import LoraConfig, get_peft_model
137
+
138
+ peft_model = get_peft_model(
139
+ model,
140
+ LoraConfig(
141
+ r=8,
142
+ lora_alpha=16,
143
+ target_modules="all-linear",
144
+ ),
145
+ )
146
  ```
147
 
148
+ This checkpoint has no advertised classifier. Supply the task-specific
149
+ objective and preserve any new head through `modules_to_save`.
150
+ All FastPLMs checkpoints follow the Transformers `PreTrainedModel` contract and
151
+ can be adapted with PEFT. The ESM2-specific shipped CLI is an example, not a
152
+ support boundary. Record the target modules, base revision, data identity, and
153
+ trainable parameter scope.
154
+
155
+ ## Test-time training
156
 
157
+ TTT samples masked views of one protein and updates only injected low-rank
158
+ adapters. Base checkpoint weights remain frozen:
159
 
160
  ```python
161
+ from transformers import AutoModelForMaskedLM
162
+
163
+ ttt_model = AutoModelForMaskedLM.from_pretrained(
164
+ "Synthyra/ESMplusplus_large",
165
+ trust_remote_code=True,
166
+ )
167
+ metrics = ttt_model.ttt(
168
+ seq="MSTNPKPQRKTKRNT",
169
+ ttt_config={"steps": 3, "batch_size": 1, "seed": 7},
170
  )
171
+ ttt_model.save_pretrained("adapted", safe_serialization=True)
172
+ ttt_model.ttt_reset()
173
+ print(metrics)
174
  ```
175
 
176
+ Persisted adapters retain their deterministic reset state. TTT adds latency
177
+ and memory, can worsen an output, and does not establish biological function.
 
178
 
179
  ## ESMC behavior
180
 
 
187
  failures; dispatch integrity, masks, finite outputs, shapes, and catastrophic
188
  biological disagreement remain hard gates.
189
 
190
+ The current GH200/aarch64 release environment validates eager, SDPA, and Flex.
191
+ Flash requests fail closed because compatible locked kernels are unavailable
192
+ on this platform.
 
 
 
 
 
193
 
194
  When `sequence_id` is supplied, it is authoritative for ESMC attention grouping
195
  and padding, and `attention_mask` is ignored. Values greater than or equal to
 
198
 
199
  | Backend | Support | Measurement status |
200
  | --- | --- | --- |
201
+ | `sdpa` | Recommended fidelity path | Pending release measurement |
202
+ | `eager` | Supported | Pending release measurement |
203
+ | `flash_attention_2` | Supported | Unavailable on current GH200/aarch64 lock |
204
+ | `flex_attention` | Supported, numerically divergent | Pending release measurement |
205
+ | `flash_attention_3` | Supported, numerically divergent | Unavailable on current GH200/aarch64 lock |
206
+
207
+ Detailed backend measurements, release guardrails, and the GH200 package
208
+ compatibility exception are maintained in the
209
+ [attention backend guide](https://github.com/Synthyra/FastPLMs/blob/main/docs/attention_backends.md)
210
+ and
211
+ [release evidence manifest](https://github.com/Synthyra/FastPLMs/blob/main/docs/generated/capability_evidence.md).
212
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
  ## Runtime contract
215
 
 
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/ESMplusplus_large`
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: `biohub/ESMC-600M`
235
  - Artifact source: `fast`
236
  - State transform: `esmc_to_fastplms_v1`
 
237
  - Pinned upstreams: `biohub-esm`, `biohub-transformers`
 
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
@@ -13,11 +13,11 @@
13
  "fastplms_checkpoint_repo_id": "Synthyra/ESMplusplus_large",
14
  "fastplms_checkpoint_revision": "f813401638b3fddab09748aec1ad2bf537aa4208",
15
  "fastplms_model_id": "esmc_large",
16
- "fastplms_release_tool_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
17
- "fastplms_release_tool_sha256": "19b4247aba04a74de5069397518374ab2c3b9e0ce18ba9f36de34f0f4c5d1ceb",
18
- "fastplms_runtime_bundle_sha256": "b01a730b69d0e25723da00aed2f25c2ede185e260e0ab1f7f29cc62934ab17d1",
19
- "fastplms_runtime_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
20
- "fastplms_source_tree_sha256": "a95686f9e9cb31ebf4a3ced01c54787c6496336528fba0f696d407b053ebabc6",
21
  "fastplms_weights_revision": "f813401638b3fddab09748aec1ad2bf537aa4208",
22
  "hidden_size": 1152,
23
  "initializer_range": 0.02,
 
13
  "fastplms_checkpoint_repo_id": "Synthyra/ESMplusplus_large",
14
  "fastplms_checkpoint_revision": "f813401638b3fddab09748aec1ad2bf537aa4208",
15
  "fastplms_model_id": "esmc_large",
16
+ "fastplms_release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
17
+ "fastplms_release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
18
+ "fastplms_runtime_bundle_sha256": "d73f544a7623d12eff2ba407e2d7e86417b7d731e36a2931515930eae7d7e8e5",
19
+ "fastplms_runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
20
+ "fastplms_source_tree_sha256": "bed9c7b1e1b0500dcd370e92e1f83b47494f9167d37b16e248f9d17e0135f822",
21
  "fastplms_weights_revision": "f813401638b3fddab09748aec1ad2bf537aa4208",
22
  "hidden_size": 1152,
23
  "initializer_range": 0.02,
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/esm_plusplus/modeling_esm_plusplus.py CHANGED
@@ -3,13 +3,12 @@
3
  from __future__ import annotations
4
 
5
  import math
6
- from dataclasses import dataclass
7
- from functools import partial
8
- from typing import ClassVar
9
-
10
  import torch
11
  import torch.nn as nn
12
  import torch.nn.functional as F
 
 
 
13
  from einops import rearrange
14
  from tokenizers import Tokenizer
15
  from tokenizers.models import BPE
@@ -22,6 +21,7 @@ from transformers.modeling_outputs import (
22
  TokenClassifierOutput,
23
  )
24
 
 
25
  try:
26
  from fastplms.attention import (
27
  AttentionBackend,
@@ -176,7 +176,7 @@ class RotaryEmbedding(torch.nn.Module):
176
  scaling_factor: float = 1.0,
177
  pos_idx_in_fp32: bool = True,
178
  device: torch.device | None = None,
179
- ):
180
  super().__init__()
181
  self.dim, self.base = dim, float(base)
182
  self.interleaved, self.scale_base = interleaved, scale_base
@@ -192,7 +192,7 @@ class RotaryEmbedding(torch.nn.Module):
192
  self._cos_k_cached: torch.Tensor | None = None
193
  self._sin_k_cached: torch.Tensor | None = None
194
 
195
- def reset_parameters(self, device: torch.device | str | None = None):
196
  """Rebuild the non-persistent frequency buffers on ``device``."""
197
  if device is not None:
198
  buffer_device = torch.device(device)
@@ -250,14 +250,14 @@ class RotaryEmbedding(torch.nn.Module):
250
  device: torch.device | None,
251
  ) -> torch.Tensor:
252
  position_dtype = torch.float32 if self.pos_idx_in_fp32 else self.inv_freq.dtype
253
- positions = torch.arange(token_count, device=device, dtype=position_dtype)
254
  positions.div_(self.scaling_factor)
255
  frequencies = (
256
  self.inv_freq.to(torch.float32)
257
  if self.pos_idx_in_fp32 and self.inv_freq.dtype != torch.float32
258
  else self.inv_freq
259
  )
260
- return torch.outer(positions, frequencies)
261
 
262
  def _update_cos_sin_cache(
263
  self, seqlen: int, device: torch.device | None = None, dtype: torch.dtype | None = None
@@ -267,9 +267,9 @@ class RotaryEmbedding(torch.nn.Module):
267
  return
268
 
269
  self._seq_len_cached = seqlen
270
- angles = self._rotary_angles(seqlen, device)
271
- cos_angles = torch.cos(angles)
272
- sin_angles = torch.sin(angles)
273
  if self.scale is None:
274
  self._cos_cached = cos_angles.to(dtype)
275
  self._sin_cached = sin_angles.to(dtype)
@@ -314,7 +314,6 @@ class RotaryEmbedding(torch.nn.Module):
314
  )
315
 
316
 
317
- ### Feedforward Network Components
318
  def swiglu_correction_fn(expansion_ratio: float, d_model: int) -> int:
319
  """Compute corrected dimension for SwiGLU."""
320
  return int(((expansion_ratio * d_model) + 255) // 256 * 256)
@@ -323,7 +322,7 @@ def swiglu_correction_fn(expansion_ratio: float, d_model: int) -> int:
323
  class SwiGLU(nn.Module):
324
  """SwiGLU activation function."""
325
 
326
- def __init__(self):
327
  super().__init__()
328
 
329
  def forward(self, x: torch.Tensor) -> torch.Tensor:
@@ -341,7 +340,6 @@ def swiglu_ln_ffn(d_model: int, expansion_ratio: float) -> nn.Sequential:
341
  )
342
 
343
 
344
- ### Attention
345
  class MultiHeadAttention(nn.Module):
346
  """Multi-head attention with rotary embeddings and configurable backend.
347
 
@@ -356,7 +354,7 @@ class MultiHeadAttention(nn.Module):
356
  d_model: int,
357
  n_heads: int,
358
  attn_backend: str = "sdpa",
359
- ):
360
  super().__init__()
361
  self.d_model = d_model
362
  self.n_heads = n_heads
@@ -373,8 +371,9 @@ class MultiHeadAttention(nn.Module):
373
  self.rotary = RotaryEmbedding(d_model // n_heads)
374
 
375
  def _apply_rotary(self, q: torch.Tensor, k: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
376
- q = q.unflatten(-1, (self.n_heads, self.d_head))
377
- k = k.unflatten(-1, (self.n_heads, self.d_head))
 
378
  q, k = self.rotary(q, k)
379
  q = q.flatten(-2, -1)
380
  k = k.flatten(-2, -1)
@@ -389,7 +388,8 @@ class MultiHeadAttention(nn.Module):
389
  output_attentions: bool = False,
390
  output_s_max: bool = False,
391
  ) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
392
- qkv = self.layernorm_qkv(x)
 
393
  query_sequence, key_sequence, value_sequence = torch.chunk(qkv, 3, dim=-1)
394
  query_sequence, key_sequence = (
395
  self.q_ln(query_sequence).to(query_sequence.dtype),
@@ -398,7 +398,7 @@ class MultiHeadAttention(nn.Module):
398
  query_sequence, key_sequence = self._apply_rotary(query_sequence, key_sequence)
399
  query_heads, key_heads, value_heads = map(
400
  self.reshaper, (query_sequence, key_sequence, value_sequence)
401
- )
402
 
403
  attn_output, attn_weights, s_max = self._attn(
404
  query_heads,
@@ -461,8 +461,8 @@ class MultiHeadAttention(nn.Module):
461
  def _compute_s_max(
462
  self, query_heads: torch.Tensor, key_heads: torch.Tensor
463
  ) -> list[torch.Tensor]:
464
- q_norm = torch.linalg.vector_norm(query_heads, dim=-1)
465
- k_norm = torch.linalg.vector_norm(key_heads, dim=-1)
466
  s_max_bound = (q_norm.max(dim=-1).values * k_norm.max(dim=-1).values).max(
467
  dim=0
468
  ).values * self.scale
@@ -476,12 +476,15 @@ class MultiHeadAttention(nn.Module):
476
  attention_mask_4d: torch.Tensor | None = None,
477
  output_s_max: bool = False,
478
  ) -> tuple[torch.Tensor, torch.Tensor, list[torch.Tensor] | None]:
479
- attn_weights = torch.matmul(query_heads, key_heads.transpose(-2, -1)) * self.scale
 
 
 
480
  if attention_mask_4d is not None:
481
  attn_weights = attn_weights.masked_fill(attention_mask_4d.logical_not(), float("-inf"))
482
  attn_weights = F.softmax(attn_weights, dim=-1)
483
- context_heads = torch.matmul(attn_weights, value_heads)
484
- attn_output = rearrange(context_heads, "b h s d -> b s (h d)")
485
  s_max = self._compute_s_max(query_heads, key_heads) if output_s_max else None
486
  return attn_output, attn_weights, s_max
487
 
@@ -548,7 +551,6 @@ class MultiHeadAttention(nn.Module):
548
  return rearrange(context_heads, "b h s d -> b s (h d)"), None
549
 
550
 
551
- ### Regression Head
552
  def RegressionHead(d_model: int, output_dim: int, hidden_dim: int | None = None) -> nn.Module:
553
  """Create a regression head with optional hidden dimension.
554
 
@@ -566,7 +568,6 @@ def RegressionHead(d_model: int, output_dim: int, hidden_dim: int | None = None)
566
  )
567
 
568
 
569
- ### Transformer Block
570
  class UnifiedTransformerBlock(nn.Module):
571
  """Transformer block with attention and feedforward layers."""
572
 
@@ -578,7 +579,7 @@ class UnifiedTransformerBlock(nn.Module):
578
  expansion_ratio: float = 8 / 3,
579
  dropout: float = 0.0,
580
  attn_backend: str = "sdpa",
581
- ):
582
  super().__init__()
583
  self.attn = MultiHeadAttention(d_model=d_model, n_heads=n_heads, attn_backend=attn_backend)
584
  self.ffn = swiglu_ln_ffn(d_model, expansion_ratio)
@@ -607,7 +608,6 @@ class UnifiedTransformerBlock(nn.Module):
607
  return x, attn_weights, s_max
608
 
609
 
610
- ### Model Outputs
611
  @dataclass
612
  class TransformerOutput(ModelOutput):
613
  """Output type for transformer encoder."""
@@ -640,7 +640,6 @@ class ESMplusplusTokenClassifierOutput(TokenClassifierOutput):
640
  s_max: tuple[list[torch.Tensor], ...] | None = None
641
 
642
 
643
- ### Transformer Stack
644
  class TransformerStack(nn.Module):
645
  """Stack of transformer blocks."""
646
 
@@ -651,7 +650,7 @@ class TransformerStack(nn.Module):
651
  n_layers: int,
652
  dropout: float = 0.0,
653
  attn_backend: str = "sdpa",
654
- ):
655
  super().__init__()
656
  self.attention_backend = resolve_attention_backend(attn_backend)
657
  self.blocks = nn.ModuleList(
@@ -690,6 +689,7 @@ class TransformerStack(nn.Module):
690
  output_s_max: bool | None = False,
691
  esmfold2_hidden_states: bool = False,
692
  ) -> TransformerOutput:
 
693
  hidden_states = () if output_hidden_states else None
694
  attentions = () if output_attentions else None
695
  full_s_max = () if output_s_max else None
@@ -971,7 +971,7 @@ class ESMplusplusModel(PreTrainedESMplusplusModel, EmbeddingMixin):
971
 
972
  config_class = ESMplusplusConfig
973
 
974
- def __init__(self, config: ESMplusplusConfig, **kwargs):
975
  PreTrainedESMplusplusModel.__init__(self, config, **kwargs)
976
  self.config = config
977
  self.vocab_size = config.vocab_size
@@ -1089,7 +1089,7 @@ class ESMplusplusForMaskedLM(
1089
 
1090
  config_class = ESMplusplusConfig
1091
 
1092
- def __init__(self, config: ESMplusplusConfig, **kwargs):
1093
  PreTrainedESMplusplusModel.__init__(self, config, **kwargs)
1094
  self.config = config
1095
  self.vocab_size = config.vocab_size
@@ -1215,7 +1215,7 @@ class ESMplusplusForSequenceClassification(ESMplusplusForMaskedLM, EmbeddingMixi
1215
  Extends the base ESM++ model with a classification head.
1216
  """
1217
 
1218
- def __init__(self, config: ESMplusplusConfig, **kwargs):
1219
  pooling_types = kwargs.pop("pooling_types", None)
1220
  if pooling_types is None:
1221
  pooling_types = config.classifier_pooling_types or ["mean", "var"]
@@ -1355,7 +1355,7 @@ class ESMplusplusForTokenClassification(ESMplusplusForMaskedLM, EmbeddingMixin):
1355
  Extends the base ESM++ model with a token classification head.
1356
  """
1357
 
1358
- def __init__(self, config: ESMplusplusConfig, **kwargs):
1359
  ESMplusplusForMaskedLM.__init__(self, config, **kwargs)
1360
  self.config = config
1361
  self.num_labels = config.num_labels
@@ -1526,7 +1526,7 @@ class EsmSequenceTokenizer(PreTrainedTokenizerFast):
1526
  **kwargs,
1527
  )
1528
 
1529
- # These are a footgun, we never use the `bos` token anywhere so we're just overriding it here.
1530
  @property
1531
  def bos_token(self):
1532
  return self.cls_token
 
3
  from __future__ import annotations
4
 
5
  import math
 
 
 
 
6
  import torch
7
  import torch.nn as nn
8
  import torch.nn.functional as F
9
+ from dataclasses import dataclass
10
+ from functools import partial
11
+ from typing import ClassVar
12
  from einops import rearrange
13
  from tokenizers import Tokenizer
14
  from tokenizers.models import BPE
 
21
  TokenClassifierOutput,
22
  )
23
 
24
+
25
  try:
26
  from fastplms.attention import (
27
  AttentionBackend,
 
176
  scaling_factor: float = 1.0,
177
  pos_idx_in_fp32: bool = True,
178
  device: torch.device | None = None,
179
+ ) -> None:
180
  super().__init__()
181
  self.dim, self.base = dim, float(base)
182
  self.interleaved, self.scale_base = interleaved, scale_base
 
192
  self._cos_k_cached: torch.Tensor | None = None
193
  self._sin_k_cached: torch.Tensor | None = None
194
 
195
+ def reset_parameters(self, device: torch.device | str | None = None) -> None:
196
  """Rebuild the non-persistent frequency buffers on ``device``."""
197
  if device is not None:
198
  buffer_device = torch.device(device)
 
250
  device: torch.device | None,
251
  ) -> torch.Tensor:
252
  position_dtype = torch.float32 if self.pos_idx_in_fp32 else self.inv_freq.dtype
253
+ positions = torch.arange(token_count, device=device, dtype=position_dtype) # (l,)
254
  positions.div_(self.scaling_factor)
255
  frequencies = (
256
  self.inv_freq.to(torch.float32)
257
  if self.pos_idx_in_fp32 and self.inv_freq.dtype != torch.float32
258
  else self.inv_freq
259
  )
260
+ return torch.outer(positions, frequencies) # (l, d / 2)
261
 
262
  def _update_cos_sin_cache(
263
  self, seqlen: int, device: torch.device | None = None, dtype: torch.dtype | None = None
 
267
  return
268
 
269
  self._seq_len_cached = seqlen
270
+ angles = self._rotary_angles(seqlen, device) # (l, d / 2)
271
+ cos_angles = torch.cos(angles) # (l, d / 2)
272
+ sin_angles = torch.sin(angles) # (l, d / 2)
273
  if self.scale is None:
274
  self._cos_cached = cos_angles.to(dtype)
275
  self._sin_cached = sin_angles.to(dtype)
 
314
  )
315
 
316
 
 
317
  def swiglu_correction_fn(expansion_ratio: float, d_model: int) -> int:
318
  """Compute corrected dimension for SwiGLU."""
319
  return int(((expansion_ratio * d_model) + 255) // 256 * 256)
 
322
  class SwiGLU(nn.Module):
323
  """SwiGLU activation function."""
324
 
325
+ def __init__(self) -> None:
326
  super().__init__()
327
 
328
  def forward(self, x: torch.Tensor) -> torch.Tensor:
 
340
  )
341
 
342
 
 
343
  class MultiHeadAttention(nn.Module):
344
  """Multi-head attention with rotary embeddings and configurable backend.
345
 
 
354
  d_model: int,
355
  n_heads: int,
356
  attn_backend: str = "sdpa",
357
+ ) -> None:
358
  super().__init__()
359
  self.d_model = d_model
360
  self.n_heads = n_heads
 
371
  self.rotary = RotaryEmbedding(d_model // n_heads)
372
 
373
  def _apply_rotary(self, q: torch.Tensor, k: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
374
+ # q, k: (b, l, d)
375
+ q = q.unflatten(-1, (self.n_heads, self.d_head)) # (b, l, h, d_h)
376
+ k = k.unflatten(-1, (self.n_heads, self.d_head)) # (b, l, h, d_h)
377
  q, k = self.rotary(q, k)
378
  q = q.flatten(-2, -1)
379
  k = k.flatten(-2, -1)
 
388
  output_attentions: bool = False,
389
  output_s_max: bool = False,
390
  ) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
391
+ # x: (b, l, d)
392
+ qkv = self.layernorm_qkv(x) # (b, l, 3 * d)
393
  query_sequence, key_sequence, value_sequence = torch.chunk(qkv, 3, dim=-1)
394
  query_sequence, key_sequence = (
395
  self.q_ln(query_sequence).to(query_sequence.dtype),
 
398
  query_sequence, key_sequence = self._apply_rotary(query_sequence, key_sequence)
399
  query_heads, key_heads, value_heads = map(
400
  self.reshaper, (query_sequence, key_sequence, value_sequence)
401
+ ) # each (b, h, l, d_h)
402
 
403
  attn_output, attn_weights, s_max = self._attn(
404
  query_heads,
 
461
  def _compute_s_max(
462
  self, query_heads: torch.Tensor, key_heads: torch.Tensor
463
  ) -> list[torch.Tensor]:
464
+ q_norm = torch.linalg.vector_norm(query_heads, dim=-1) # (b, h, l)
465
+ k_norm = torch.linalg.vector_norm(key_heads, dim=-1) # (b, h, l)
466
  s_max_bound = (q_norm.max(dim=-1).values * k_norm.max(dim=-1).values).max(
467
  dim=0
468
  ).values * self.scale
 
476
  attention_mask_4d: torch.Tensor | None = None,
477
  output_s_max: bool = False,
478
  ) -> tuple[torch.Tensor, torch.Tensor, list[torch.Tensor] | None]:
479
+ # query_heads, key_heads, value_heads: (b, h, l, d_h)
480
+ attn_weights = (
481
+ torch.matmul(query_heads, key_heads.transpose(-2, -1)) * self.scale
482
+ ) # (b, h, l, l)
483
  if attention_mask_4d is not None:
484
  attn_weights = attn_weights.masked_fill(attention_mask_4d.logical_not(), float("-inf"))
485
  attn_weights = F.softmax(attn_weights, dim=-1)
486
+ context_heads = torch.matmul(attn_weights, value_heads) # (b, h, l, d_h)
487
+ attn_output = rearrange(context_heads, "b h s d -> b s (h d)") # (b, l, d)
488
  s_max = self._compute_s_max(query_heads, key_heads) if output_s_max else None
489
  return attn_output, attn_weights, s_max
490
 
 
551
  return rearrange(context_heads, "b h s d -> b s (h d)"), None
552
 
553
 
 
554
  def RegressionHead(d_model: int, output_dim: int, hidden_dim: int | None = None) -> nn.Module:
555
  """Create a regression head with optional hidden dimension.
556
 
 
568
  )
569
 
570
 
 
571
  class UnifiedTransformerBlock(nn.Module):
572
  """Transformer block with attention and feedforward layers."""
573
 
 
579
  expansion_ratio: float = 8 / 3,
580
  dropout: float = 0.0,
581
  attn_backend: str = "sdpa",
582
+ ) -> None:
583
  super().__init__()
584
  self.attn = MultiHeadAttention(d_model=d_model, n_heads=n_heads, attn_backend=attn_backend)
585
  self.ffn = swiglu_ln_ffn(d_model, expansion_ratio)
 
608
  return x, attn_weights, s_max
609
 
610
 
 
611
  @dataclass
612
  class TransformerOutput(ModelOutput):
613
  """Output type for transformer encoder."""
 
640
  s_max: tuple[list[torch.Tensor], ...] | None = None
641
 
642
 
 
643
  class TransformerStack(nn.Module):
644
  """Stack of transformer blocks."""
645
 
 
650
  n_layers: int,
651
  dropout: float = 0.0,
652
  attn_backend: str = "sdpa",
653
+ ) -> None:
654
  super().__init__()
655
  self.attention_backend = resolve_attention_backend(attn_backend)
656
  self.blocks = nn.ModuleList(
 
689
  output_s_max: bool | None = False,
690
  esmfold2_hidden_states: bool = False,
691
  ) -> TransformerOutput:
692
+ # x: (b, l, d); attention_mask, sequence_id: (b, l)
693
  hidden_states = () if output_hidden_states else None
694
  attentions = () if output_attentions else None
695
  full_s_max = () if output_s_max else None
 
971
 
972
  config_class = ESMplusplusConfig
973
 
974
+ def __init__(self, config: ESMplusplusConfig, **kwargs) -> None:
975
  PreTrainedESMplusplusModel.__init__(self, config, **kwargs)
976
  self.config = config
977
  self.vocab_size = config.vocab_size
 
1089
 
1090
  config_class = ESMplusplusConfig
1091
 
1092
+ def __init__(self, config: ESMplusplusConfig, **kwargs) -> None:
1093
  PreTrainedESMplusplusModel.__init__(self, config, **kwargs)
1094
  self.config = config
1095
  self.vocab_size = config.vocab_size
 
1215
  Extends the base ESM++ model with a classification head.
1216
  """
1217
 
1218
+ def __init__(self, config: ESMplusplusConfig, **kwargs) -> None:
1219
  pooling_types = kwargs.pop("pooling_types", None)
1220
  if pooling_types is None:
1221
  pooling_types = config.classifier_pooling_types or ["mean", "var"]
 
1355
  Extends the base ESM++ model with a token classification head.
1356
  """
1357
 
1358
+ def __init__(self, config: ESMplusplusConfig, **kwargs) -> None:
1359
  ESMplusplusForMaskedLM.__init__(self, config, **kwargs)
1360
  self.config = config
1361
  self.num_labels = config.num_labels
 
1526
  **kwargs,
1527
  )
1528
 
1529
+ # ESMC does not use BOS, so expose the sequence-start token through the HF BOS fields.
1530
  @property
1531
  def bos_token(self):
1532
  return self.cls_token
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 != "b01a730b69d0e25723da00aed2f25c2ede185e260e0ab1f7f29cc62934ab17d1":
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,10 +179,10 @@ def _install_runtime():
223
  return package
224
 
225
  _install_runtime()
226
- _module_225 = _import_without_bytecode("fastplms.models.esm_plusplus.modeling_esm_plusplus")
227
- ESMplusplusConfig = _module_225.ESMplusplusConfig
228
  ESMplusplusConfig.__module__ = __name__
229
- ESMplusplusForMaskedLM = _module_225.ESMplusplusForMaskedLM
230
  ESMplusplusForMaskedLM.__module__ = __name__
231
- ESMplusplusModel = _module_225.ESMplusplusModel
232
  ESMplusplusModel.__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 != "d73f544a7623d12eff2ba407e2d7e86417b7d731e36a2931515930eae7d7e8e5":
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.esm_plusplus.modeling_esm_plusplus")
183
+ ESMplusplusConfig = _module_181.ESMplusplusConfig
184
  ESMplusplusConfig.__module__ = __name__
185
+ ESMplusplusForMaskedLM = _module_181.ESMplusplusForMaskedLM
186
  ESMplusplusForMaskedLM.__module__ = __name__
187
+ ESMplusplusModel = _module_181.ESMplusplusModel
188
  ESMplusplusModel.__module__ = __name__
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Direct runtime dependencies for Synthyra/ESMplusplus_large.
2
+ # FastPLMs source is embedded in this model repository.
3
+ torch>=2.13,<2.14
4
+ transformers>=5.13,<5.14
5
+ huggingface-hub>=0.34,<2
6
+ tokenizers>=0.22,<0.23
7
+ safetensors>=0.5,<1
8
+ numpy>=1.26,<3
9
+ einops>=0.8,<1
10
+ tqdm>=4.67,<5
11
+ kernels>=0.15,<0.16
runtime-attestation.json CHANGED
@@ -4,42 +4,43 @@
4
  "LICENSES/biohub-esm/LICENSE.md": "sha256:b63df9ca1dd96b3b21eec226b51b236d0bd152ac20eafc43aad46bf832b48d8a",
5
  "LICENSES/biohub-esm/THIRD_PARTY_NOTICE.md": "sha256:5bff8515ba4e0f53abdc43714c180b79c5b606160497d98de741a369cb9b6a23",
6
  "LICENSES/biohub-transformers/LICENSE": "sha256:77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049",
7
- "README.md": "sha256:926c6c203528e78aeecc467fa325738d17378b9e1c715424869d1fdb802a04d3",
8
  "THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
9
- "config.json": "sha256:84629d9de640bb5c4c04e4830ce617aeeed7536e88635e0e9cbef5b49e6234e2",
10
- "fastplms/__init__.py": "sha256:4fb3196022ca8ec699d59d09bdbc5f0184195552b773698ab9b061fe3cd7df12",
11
- "fastplms/attention/__init__.py": "sha256:f60b9fecfb4bcb37a4e7c26dc2f752b9035f9cbad627b4a84213f3a92ec88f7d",
12
- "fastplms/attention/_core.py": "sha256:8f7ec5b65bd8b6c6fa4951d50d1c0e499abf03ae00914794b51fc410201e3e33",
13
- "fastplms/attention/_kernel_lock.py": "sha256:85d8521a2af5f94fad3948af3814db0c866c414ee4d43df797b9bd6f980e947b",
14
- "fastplms/attention/interfaces.py": "sha256:1c6f06a8e411e0f9bf6d230522205c93ae46ea58864006fbb892aa05e5ca5749",
15
- "fastplms/embeddings/__init__.py": "sha256:47ff8cdf682d44037dd9edab133e2e60675d60786bd5cf0bffd1998f31985555",
16
- "fastplms/embeddings/pooling.py": "sha256:a140266ed6b1cc344c8507edc5c6c4f2dce464c3db70ba4b16c7ac2ba2fad96e",
17
- "fastplms/embeddings/runner.py": "sha256:23ee4727a918d6d331f7a0f89b823d149f1a791f0c5586e3496d7b6eb2ce97e0",
18
- "fastplms/embeddings/storage.py": "sha256:3fbe2bab75092e5a4cadf4d27e4752181d597469a65a55db085ceef808ed418e",
19
- "fastplms/embeddings/types.py": "sha256:119718a20989d1ae5a60fabc0f5e98bdc172c5163b04db3d4554ac3956b30e52",
20
  "fastplms/kernels.lock": "sha256:8e274e6f8f25fe272d95bb42849f672ebb897a67511e82be2b86c2a4b4962cdd",
21
  "fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
22
- "fastplms/models/__init__.py": "sha256:5e48c2cb3877aa6f42f3b5411d53b16bba2e32827bbde634f47f174c5cb36f86",
23
  "fastplms/models/esm_plusplus/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
24
- "fastplms/models/esm_plusplus/modeling_esm_plusplus.py": "sha256:7fc8047100a2d0ca230bc687126c4ccd3f46c0e51cefd39413e72e3842e14012",
25
- "fastplms/models/ttt.py": "sha256:a0df4e98b02120d423e3c7ca9b866a8d0e3748b9076042a0102a838a11aed046",
26
- "fastplms/registry.py": "sha256:afca271911b651a882345b74a58366494a1d784e4a48c8683a87f5508f4ba16e",
27
- "fastplms/runtime.py": "sha256:110018646d6f248cedab140a030c3065e1b062b61f6aff659c231e538614bc01",
28
- "fastplms_bundle.py": "sha256:4e1c903d663ac29c7d42c6bfdf0369023eac9ad7b5075801f8efa4e65289ed41",
29
- "modeling_fastplms.py": "sha256:b451c8179707dd905beee4e68cc7a4f675003e23941cae09e3f4716f3f600e06",
 
30
  "special_tokens_map.json": "sha256:0b7245ec86c8c3aeaf61523ba70dfa79be137e6283f127bd651adc30b4f15c74",
31
  "tokenizer.json": "sha256:8d3447b278176e65fb3ef0224472927bf5fee3be46ea2bd77fad0111423cee1f",
32
  "tokenizer_config.json": "sha256:e8d8e40c9f92b334f0272e80bb65ed4043cb9836523cbae899e9859e8cbb8833"
33
  },
34
  "model_id": "esmc_large",
35
  "redistributable": true,
36
- "release_tool_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
37
- "release_tool_sha256": "19b4247aba04a74de5069397518374ab2c3b9e0ce18ba9f36de34f0f4c5d1ceb",
38
- "runtime_bundle_sha256": "b01a730b69d0e25723da00aed2f25c2ede185e260e0ab1f7f29cc62934ab17d1",
39
- "runtime_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
40
  "schema_version": 2,
41
  "scope": "runtime-only",
42
- "source_tree_sha256": "a95686f9e9cb31ebf4a3ced01c54787c6496336528fba0f696d407b053ebabc6",
43
  "weights": {
44
  "repo_id": "Synthyra/ESMplusplus_large",
45
  "revision": "f813401638b3fddab09748aec1ad2bf537aa4208"
 
4
  "LICENSES/biohub-esm/LICENSE.md": "sha256:b63df9ca1dd96b3b21eec226b51b236d0bd152ac20eafc43aad46bf832b48d8a",
5
  "LICENSES/biohub-esm/THIRD_PARTY_NOTICE.md": "sha256:5bff8515ba4e0f53abdc43714c180b79c5b606160497d98de741a369cb9b6a23",
6
  "LICENSES/biohub-transformers/LICENSE": "sha256:77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049",
7
+ "README.md": "sha256:2115ee81a16a6c30e29ada82eaffdee331bf20360a5613850bdbfcc36046b077",
8
  "THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
9
+ "config.json": "sha256:b0e9e94cc3b7d42bae5b7d1296eadfb7ba88b3e7ff87f5ea2f682764c58a51d3",
10
+ "fastplms/__init__.py": "sha256:8503e02debf24abc6d33c1913ff3eb9f245e63f7e62017e220d072c7393fdf2c",
11
+ "fastplms/attention/__init__.py": "sha256:ab3c0b6156968f418f3c97ea664959cb978f4137717b4362213bcfe25ab5e3d6",
12
+ "fastplms/attention/_core.py": "sha256:09d0f71a5ea2ad2138c9441f6a2f72bbf8e72729be41d8db737b163f36ce47da",
13
+ "fastplms/attention/_kernel_lock.py": "sha256:56455eb57cee9af08438eda52b775c9cff3cf994cadb9b116ce2ee10fd5f1801",
14
+ "fastplms/attention/interfaces.py": "sha256:6562a342961022fc2848d7004bd01fddaf8be8e08e782d0856e5c30a74232623",
15
+ "fastplms/embeddings/__init__.py": "sha256:cf0648de905a00ad16c0aece328eeb05f25739a4cf89b47b915e0f2b19c3ce26",
16
+ "fastplms/embeddings/pooling.py": "sha256:9bed8c466fce053333de9e25b04d1ab304f2954f403ffa0f5a3c749e8277b3ce",
17
+ "fastplms/embeddings/runner.py": "sha256:a7839763ad641a63d0e2671dcf34aa7b2a8114198950b64254188521b27453a8",
18
+ "fastplms/embeddings/storage.py": "sha256:98cf725d26766f32300f105345129ca6cf02ebd10628e746a7e71e107bc911a6",
19
+ "fastplms/embeddings/types.py": "sha256:c58f8c6a37a11a937c3f77b4e1617d5f213923d98ecaffddcdd283bd9dc42fe7",
20
  "fastplms/kernels.lock": "sha256:8e274e6f8f25fe272d95bb42849f672ebb897a67511e82be2b86c2a4b4962cdd",
21
  "fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
22
+ "fastplms/models/__init__.py": "sha256:d3dd084f5e8fafcf2d0fe4a26e4ab9284d3f9414b1f9a5bd86e308a406447c43",
23
  "fastplms/models/esm_plusplus/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
24
+ "fastplms/models/esm_plusplus/modeling_esm_plusplus.py": "sha256:ea812cc86c8cc5b3a41d82fdcbf56f999dd5599a181b1804b4c0f19b0f309557",
25
+ "fastplms/models/ttt.py": "sha256:f1d7f9298a930f4c40f8e321ffa08b09d1c5b8492bca623bdc7bfd03ec85774d",
26
+ "fastplms/registry.py": "sha256:cb37799dae5a4c0c780089ab2cca4cc56300c1c5f515b275892aa4e1c4b97eb6",
27
+ "fastplms/runtime.py": "sha256:9521c37fcd168bf9a4137277fb73672403ccf30d70d4845aa879fb22f42ddeb6",
28
+ "fastplms_bundle.py": "sha256:cd4e553471f54a7e7faf0fcc210dd75b8cbf990098116c040db3ff42fd947c43",
29
+ "modeling_fastplms.py": "sha256:486bbf6733e9e044c4d78cf2523f19ecd902b438dd37ee0e13356c01d5d7c761",
30
+ "requirements.txt": "sha256:32c675d7545024f2e9abae380ffd25690ec6e072f5d2cdb0cea9e25c57359fdd",
31
  "special_tokens_map.json": "sha256:0b7245ec86c8c3aeaf61523ba70dfa79be137e6283f127bd651adc30b4f15c74",
32
  "tokenizer.json": "sha256:8d3447b278176e65fb3ef0224472927bf5fee3be46ea2bd77fad0111423cee1f",
33
  "tokenizer_config.json": "sha256:e8d8e40c9f92b334f0272e80bb65ed4043cb9836523cbae899e9859e8cbb8833"
34
  },
35
  "model_id": "esmc_large",
36
  "redistributable": true,
37
+ "release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
38
+ "release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
39
+ "runtime_bundle_sha256": "d73f544a7623d12eff2ba407e2d7e86417b7d731e36a2931515930eae7d7e8e5",
40
+ "runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
41
  "schema_version": 2,
42
  "scope": "runtime-only",
43
+ "source_tree_sha256": "bed9c7b1e1b0500dcd370e92e1f83b47494f9167d37b16e248f9d17e0135f822",
44
  "weights": {
45
  "repo_id": "Synthyra/ESMplusplus_large",
46
  "revision": "f813401638b3fddab09748aec1ad2bf537aa4208"