lhallee commited on
Commit
843a654
·
verified ·
1 Parent(s): ec0aa9e

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 raw amino-acid sequences through folding helpers, or
16
  prepared residue tensors.
17
  Supported Transformers entry points are `AutoConfig`, `AutoModel`.
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  ## Install and platform requirements
20
 
21
- Install the current FastPLMs package:
22
 
23
  ```bash
24
- python -m pip install \
25
- "fastplms[structure] @ git+https://github.com/Synthyra/FastPLMs.git"
26
  ```
27
 
28
- Python 3.11-3.14, PyTorch 2.13, and Transformers 5.13 are required. Structure inference requires the `structure` extra and a CUDA device for the published execution contract. The current validated release target is the exact NVIDIA GH200 on Linux aarch64; Linux x86-64, CPU-only, Windows, and macOS structure runs are not current release evidence. 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,22 +57,50 @@ model_id = "Synthyra/FastESMFold"
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/FastESMFold` 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`.
50
- Pass the selected name through `attn_implementation`.
51
- When an optimized backend cannot return full attention tensors,
52
- `output_attentions=True` emits one explicit runtime warning and uses a correctly
53
- masked eager implementation for that call only. The warning identifies the
54
- configured backend, effective backend, and reason. Configuration and later
55
- calls are unchanged.
56
- For BF16 execution, this family uses FP32 parameters with CUDA BF16 autocast.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  ## Protein structure prediction
59
 
@@ -93,7 +140,7 @@ does not contain a trained masked-language-model head for that objective, so
93
  - Precision policies: `default`
94
  - BF16 execution: `fp32_parameters_autocast`
95
  - Generation contract: `not_applicable`
96
- - Optional dependency group: `structure`
97
  - Weight publication allowed: `true`
98
  - Weight license status: `resolved`
99
  - Redistributable: `true`
@@ -104,28 +151,22 @@ does not contain a trained masked-language-model head for that objective, so
104
  - FastPLMs weights: `Synthyra/FastESMFold`
105
  - Runtime revision: recorded separately in the built artifact and published commit
106
  - Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
107
- - Generator/schema version and complete/runtime-only attestations: recorded in `provenance.json`
108
  - Official checkpoint: `facebook/esmfold_v1`
109
  - Artifact source: `fast`
110
  - State transform: `esmfold_meta_to_fastplms_v1`
111
- - BF16 execution: `fp32_parameters_autocast`
112
  - Pinned upstreams: `fair-esm`, `openfold`
113
- - Reference container: `reference-esmfold`
114
  - Release tiers: `check`, `compliance`, `structure`, `feature`, `artifact`, `benchmark`
115
  - Unresolved required file identities: `0`
116
 
117
- The local artifact records exact file identities, conversion details, source
118
- revisions, and legal texts in `provenance.json`. A nonzero unresolved count is a
119
- release blocker.
120
 
121
  ## Validation boundary
122
 
123
- For tiers declared by the manifest, the release contract compares applicable
124
- semantic configuration, tokenizer behavior, state keys, shapes, dtypes,
125
- values, aliases, and representative inference with the pinned official
126
- implementation. This metadata does not by itself claim that a particular build
127
- passed, that one backend is faster, or that an output has biological or
128
- therapeutic validity.
129
 
130
  ## License
131
 
 
16
  prepared residue tensors.
17
  Supported Transformers entry points are `AutoConfig`, `AutoModel`.
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 | Unavailable for this structure-only checkpoint |
27
+ | Test-time training | Unavailable: the checkpoint has no trained MLM head |
28
+ | Attention variants | Supported: `eager`, `sdpa`, `flex_attention` |
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/FastESMFold/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 structure dependencies. The published execution contract requires a CUDA device. The current validated release target is the exact NVIDIA GH200 on Linux aarch64; Linux x86-64, CPU-only, Windows, and macOS structure runs are not current release evidence. 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/FastESMFold` 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`. An unavailable
70
+ requested backend raises instead of silently switching implementations.
71
+ `output_attentions=True` may use the documented, one-call eager fallback solely
72
+ to materialize attention tensors; the configured backend remains unchanged.
73
 
74
+ This family declares the `compliance` tier. Release evidence binds the exact
75
+ checkpoint, backend, dtype, hardware, inputs, and reference revision.
76
+
77
+ ## PEFT fine-tuning
78
+
79
+ Install the direct training dependencies, then attach LoRA to the loaded checkpoint:
80
+
81
+ ```bash
82
+ python -m pip install "datasets>=4.8,<5" "peft>=0.19,<0.20"
83
+ ```
84
+
85
+ ```python
86
+ from peft import LoraConfig, get_peft_model
87
+
88
+ peft_model = get_peft_model(
89
+ model,
90
+ LoraConfig(
91
+ r=8,
92
+ lora_alpha=16,
93
+ target_modules="all-linear",
94
+ ),
95
+ )
96
+ ```
97
+
98
+ This checkpoint has no advertised classifier. Supply the task-specific
99
+ objective and preserve any new head through `modules_to_save`.
100
+ All FastPLMs checkpoints follow the Transformers `PreTrainedModel` contract and
101
+ can be adapted with PEFT. The ESM2-specific shipped CLI is an example, not a
102
+ support boundary. Record the target modules, base revision, data identity, and
103
+ trainable parameter scope.
104
 
105
  ## Protein structure prediction
106
 
 
140
  - Precision policies: `default`
141
  - BF16 execution: `fp32_parameters_autocast`
142
  - Generation contract: `not_applicable`
143
+ - Artifact dependency set: `core + structure`
144
  - Weight publication allowed: `true`
145
  - Weight license status: `resolved`
146
  - Redistributable: `true`
 
151
  - FastPLMs weights: `Synthyra/FastESMFold`
152
  - Runtime revision: recorded separately in the built artifact and published commit
153
  - Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
 
154
  - Official checkpoint: `facebook/esmfold_v1`
155
  - Artifact source: `fast`
156
  - State transform: `esmfold_meta_to_fastplms_v1`
 
157
  - Pinned upstreams: `fair-esm`, `openfold`
 
158
  - Release tiers: `check`, `compliance`, `structure`, `feature`, `artifact`, `benchmark`
159
  - Unresolved required file identities: `0`
160
 
161
+ `provenance.json` records exact file identities, conversion, source revisions,
162
+ legal texts, schema, and attestations. A nonzero unresolved count blocks release.
 
163
 
164
  ## Validation boundary
165
 
166
+ Declared tiers compare applicable configuration, tokenizer behavior, state,
167
+ and representative inference with the pinned reference. Metadata alone does
168
+ not claim a build passed, a backend is faster, or an output is biologically
169
+ valid.
 
 
170
 
171
  ## License
172
 
config.json CHANGED
@@ -56,11 +56,11 @@
56
  "fastplms_checkpoint_repo_id": "Synthyra/FastESMFold",
57
  "fastplms_checkpoint_revision": "b88c8cb50d19b2cf7ab4fee4b0a61f5e02da7823",
58
  "fastplms_model_id": "esmfold",
59
- "fastplms_release_tool_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
60
- "fastplms_release_tool_sha256": "19b4247aba04a74de5069397518374ab2c3b9e0ce18ba9f36de34f0f4c5d1ceb",
61
- "fastplms_runtime_bundle_sha256": "e38948c5e408464e3174518d3c907f85ee3ea37c6d5fcbe77da921d7adbda818",
62
- "fastplms_runtime_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
63
- "fastplms_source_tree_sha256": "6dbdf570cd69776d3f0239168f6388b979affda62c6ba4c78e2d9fac1d81bd6d",
64
  "fastplms_weights_revision": "b88c8cb50d19b2cf7ab4fee4b0a61f5e02da7823",
65
  "hidden_act": "gelu",
66
  "hidden_dropout_prob": 0.0,
 
56
  "fastplms_checkpoint_repo_id": "Synthyra/FastESMFold",
57
  "fastplms_checkpoint_revision": "b88c8cb50d19b2cf7ab4fee4b0a61f5e02da7823",
58
  "fastplms_model_id": "esmfold",
59
+ "fastplms_release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
60
+ "fastplms_release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
61
+ "fastplms_runtime_bundle_sha256": "899b79836910097d523aa944c1342d227221c535365e90dfd15ba72dabbb1875",
62
+ "fastplms_runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
63
+ "fastplms_source_tree_sha256": "45689da1c4bc32afb64352fe83809f9c506186de963e5db3968613d40b796644",
64
  "fastplms_weights_revision": "b88c8cb50d19b2cf7ab4fee4b0a61f5e02da7823",
65
  "hidden_act": "gelu",
66
  "hidden_dropout_prob": 0.0,
fastplms/__init__.py CHANGED
@@ -9,6 +9,7 @@ from __future__ import annotations
9
  from importlib import import_module
10
  from typing import Any
11
 
 
12
  __version__ = "1.0.0"
13
 
14
  _LAZY_EXPORTS = {
 
9
  from importlib import import_module
10
  from typing import Any
11
 
12
+
13
  __version__ = "1.0.0"
14
 
15
  _LAZY_EXPORTS = {
fastplms/attention/__init__.py CHANGED
@@ -32,6 +32,7 @@ from .interfaces import (
32
  validate_transformers_attention_interfaces,
33
  )
34
 
 
35
  __all__ = [
36
  "FASTPLMS_ATTENTION_FUNCTIONS",
37
  "FASTPLMS_ATTENTION_MASKS",
 
32
  validate_transformers_attention_interfaces,
33
  )
34
 
35
+
36
  __all__ = [
37
  "FASTPLMS_ATTENTION_FUNCTIONS",
38
  "FASTPLMS_ATTENTION_MASKS",
fastplms/attention/_core.py CHANGED
@@ -8,17 +8,17 @@ FastPLMs never downloads or compiles code.
8
  from __future__ import annotations
9
 
10
  import warnings
 
11
  from collections import OrderedDict
12
  from collections.abc import Callable
13
  from enum import Enum
14
  from threading import RLock
15
-
16
- import torch
17
  from einops import rearrange
18
  from torch.nn import functional as F
19
 
20
  from ._kernel_lock import load_locked_kernel
21
 
 
22
  try:
23
  from torch.nn.attention.flex_attention import BlockMask, create_block_mask, flex_attention
24
  except ImportError:
@@ -117,12 +117,12 @@ def _get_flex_block_mask(
117
  raise RuntimeError(
118
  "'flex_attention' was requested, but torch.create_block_mask is unavailable."
119
  )
120
- pattern = mask_pattern.detach().to(device=device).contiguous()
121
  # One device-to-host transfer is required for an exact cache identity. Use
122
  # the contiguous buffer directly instead of materializing one Python int
123
  # per byte, which is prohibitively expensive for long batched sequences.
124
- host_pattern = pattern.to(device="cpu").contiguous()
125
- pattern_bytes = host_pattern.view(torch.uint8).numpy().tobytes(order="C")
126
  cache_key = (
127
  str(device),
128
  None if dtype is None else str(dtype),
@@ -203,6 +203,7 @@ def _validate_kernels_flash_dtype(
203
  ) -> torch.dtype:
204
  """Reject dtypes outside the immutable kernel manifest before dispatch."""
205
 
 
206
  tensor_dtypes = {query_states.dtype, key_states.dtype, value_states.dtype}
207
  if len(tensor_dtypes) != 1:
208
  observed = ", ".join(sorted(str(dtype) for dtype in tensor_dtypes))
@@ -243,6 +244,7 @@ def _validate_kernels_flash_device(
243
  ) -> torch.device:
244
  """Require Q, K, and V on one CUDA device before loading a kernel."""
245
 
 
246
  devices = (query_states.device, key_states.device, value_states.device)
247
  if len(set(devices)) != 1:
248
  observed = ", ".join(str(device) for device in devices)
@@ -284,9 +286,10 @@ def _kernels_flash_forward(
284
  Failing to override when Q is pre-scaled applies the scale twice and breaks
285
  parity with eager attention and SDPA.
286
  """
 
287
  flash_kernel, flash_kernel_variant = _ensure_flash_kernels_loaded(implementation)
288
  if flash_kernel_variant == "flash_attn2":
289
- output = flash_kernel.flash_attn_func(
290
  q=query_states,
291
  k=key_states,
292
  v=value_states,
@@ -294,9 +297,9 @@ def _kernels_flash_forward(
294
  softmax_scale=softmax_scale,
295
  causal=causal,
296
  )
297
- return output[0] if isinstance(output, tuple) else output
298
  if flash_kernel_variant == "flash_attn3":
299
- output = flash_kernel.flash_attn_func(
300
  q=query_states,
301
  k=key_states,
302
  v=value_states,
@@ -304,8 +307,8 @@ def _kernels_flash_forward(
304
  causal=causal,
305
  )
306
  if isinstance(output, tuple):
307
- return output[0]
308
- return output
309
  raise RuntimeError(f"Unsupported FlashAttention kernel variant: {flash_kernel_variant}")
310
 
311
 
@@ -326,9 +329,11 @@ def _kernels_flash_varlen_forward(
326
  See `_kernels_flash_forward` docstring for why `softmax_scale=1.0` must be
327
  passed when Q has been pre-scaled by the caller.
328
  """
 
 
329
  flash_kernel, flash_kernel_variant = _ensure_flash_kernels_loaded(implementation)
330
  if flash_kernel_variant == "flash_attn2":
331
- output = flash_kernel.flash_attn_varlen_func(
332
  q=query_states,
333
  k=key_states,
334
  v=value_states,
@@ -340,9 +345,9 @@ def _kernels_flash_varlen_forward(
340
  softmax_scale=softmax_scale,
341
  causal=causal,
342
  )
343
- return output[0] if isinstance(output, tuple) else output
344
  if flash_kernel_variant == "flash_attn3":
345
- output = flash_kernel.flash_attn_varlen_func(
346
  q=query_states,
347
  k=key_states,
348
  v=value_states,
@@ -354,8 +359,8 @@ def _kernels_flash_varlen_forward(
354
  causal=causal,
355
  )
356
  if isinstance(output, tuple):
357
- return output[0]
358
- return output
359
  raise RuntimeError(f"Unsupported FlashAttention kernel variant: {flash_kernel_variant}")
360
 
361
 
@@ -364,6 +369,7 @@ def _kernels_flash_varlen_forward(
364
  class IndexFirstAxis(torch.autograd.Function):
365
  @staticmethod
366
  def forward(ctx, input, indices) -> torch.Tensor:
 
367
  ctx.save_for_backward(indices)
368
  if input.ndim < 2:
369
  raise ValueError(
@@ -377,12 +383,13 @@ class IndexFirstAxis(torch.autograd.Function):
377
  )
378
  ctx.first_axis_dim, other_shape = input.shape[0], input.shape[1:]
379
  second_dim = other_shape.numel()
380
- return torch.gather(
381
  rearrange(input, "b ... -> b (...)"), 0, indices.unsqueeze(1).expand(-1, second_dim)
382
  ).reshape(-1, *other_shape)
383
 
384
  @staticmethod
385
  def backward(ctx, grad_output) -> tuple[torch.Tensor, None]:
 
386
  (indices,) = ctx.saved_tensors
387
  if grad_output.ndim < 2:
388
  raise RuntimeError(
@@ -390,19 +397,20 @@ class IndexFirstAxis(torch.autograd.Function):
390
  "two dimensions."
391
  )
392
  other_shape = grad_output.shape[1:]
393
- grad_output = rearrange(grad_output, "b ... -> b (...)")
394
- grad_input = torch.zeros(
395
  [ctx.first_axis_dim, grad_output.shape[1]],
396
  device=grad_output.device,
397
  dtype=grad_output.dtype,
398
  )
399
  grad_input.scatter_(0, indices.unsqueeze(1).expand(-1, grad_output.shape[1]), grad_output)
400
- return grad_input.reshape(ctx.first_axis_dim, *other_shape), None
401
 
402
 
403
  class IndexPutFirstAxis(torch.autograd.Function):
404
  @staticmethod
405
  def forward(ctx, values, indices, first_axis_dim) -> torch.Tensor:
 
406
  ctx.save_for_backward(indices)
407
  if indices.ndim != 1:
408
  raise ValueError(
@@ -414,16 +422,17 @@ class IndexPutFirstAxis(torch.autograd.Function):
414
  "index_put_first_axis values must have at least two dimensions; "
415
  f"received shape {tuple(values.shape)}."
416
  )
417
- output = torch.zeros(
418
  first_axis_dim, *values.shape[1:], device=values.device, dtype=values.dtype
419
  )
420
  output[indices] = values
421
- return output
422
 
423
  @staticmethod
424
  def backward(ctx, grad_output) -> tuple[torch.Tensor, None, None]:
 
425
  (indices,) = ctx.saved_tensors
426
- return grad_output[indices], None, None
427
 
428
 
429
  index_first_axis = IndexFirstAxis.apply
@@ -433,8 +442,9 @@ index_put_first_axis = IndexPutFirstAxis.apply
433
  def pad_input(
434
  hidden_states: torch.Tensor, indices: torch.Tensor, batch: int, seqlen: int
435
  ) -> torch.Tensor:
436
- output = index_put_first_axis(hidden_states, indices, batch * seqlen)
437
- return rearrange(output, "(b s) ... -> b s ...", b=batch)
 
438
 
439
 
440
  def _unpad_input(
@@ -450,18 +460,19 @@ def _unpad_input(
450
  tuple[torch.Tensor, torch.Tensor],
451
  tuple[int, int],
452
  ]:
 
453
  batch_size, seq_len, num_heads, head_dim = query_layer.shape
454
- seqlens = attention_mask_2d.sum(dim=1).int()
455
- cu_seqlens = F.pad(seqlens.cumsum(0, dtype=torch.int32), (1, 0))
456
  max_seqlen = int(seqlens.max().item())
457
- indices = attention_mask_2d.flatten().nonzero(as_tuple=False).flatten()
458
- query_layer = index_first_axis(
459
  query_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
460
  )
461
- key_layer = index_first_axis(
462
  key_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
463
  )
464
- value_layer = index_first_axis(
465
  value_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
466
  )
467
  return (
@@ -482,6 +493,7 @@ def _validate_flash_padding_mask(
482
  ) -> torch.Tensor:
483
  """Validate the self-attention padding mask used by the varlen kernels."""
484
 
 
485
  if attention_mask_2d.ndim != 2:
486
  raise ValueError("FlashAttention padding masks must have shape (batch, sequence_length).")
487
  expected_shape = query_states.shape[:2]
@@ -497,7 +509,7 @@ def _validate_flash_padding_mask(
497
  )
498
  if attention_mask_2d.device != query_states.device:
499
  raise ValueError("FlashAttention padding mask and Q, K, and V must be on the same device.")
500
- return attention_mask_2d.to(dtype=torch.bool)
501
 
502
 
503
  def kernels_flash_attention_func(
@@ -521,6 +533,8 @@ def kernels_flash_attention_func(
521
  `softmax_scale=1.0`. Otherwise the flash kernel applies its default scale
522
  again, yielding an effective `1/head_dim` scale that drifts across layers.
523
  """
 
 
524
  _validate_kernels_flash_device(
525
  query_states,
526
  key_states,
@@ -534,11 +548,11 @@ def kernels_flash_attention_func(
534
  implementation,
535
  )
536
  if query_states.dtype != runtime_dtype:
537
- query_states = query_states.to(dtype=runtime_dtype)
538
- key_states = key_states.to(dtype=runtime_dtype)
539
- value_states = value_states.to(dtype=runtime_dtype)
540
  if attention_mask_2d is not None:
541
- attention_mask_2d = _validate_flash_padding_mask(
542
  query_states,
543
  key_states,
544
  value_states,
@@ -554,8 +568,13 @@ def kernels_flash_attention_func(
554
  indices_q,
555
  (cu_seqlens_q, cu_seqlens_k),
556
  (max_seqlen_q, max_seqlen_k),
557
- ) = _unpad_input(query_states, key_states, value_states, attention_mask_2d)
558
- attn_output_unpad = _kernels_flash_varlen_forward(
 
 
 
 
 
559
  query_states=query_states,
560
  key_states=key_states,
561
  value_states=value_states,
@@ -567,10 +586,10 @@ def kernels_flash_attention_func(
567
  softmax_scale=softmax_scale,
568
  implementation=implementation,
569
  )
570
- output = pad_input(attn_output_unpad, indices_q, batch_size, q_len)
571
- return output.masked_fill(~attention_mask_2d[:, :, None, None], 0)
572
  else:
573
- return _kernels_flash_forward(
574
  query_states=query_states,
575
  key_states=key_states,
576
  value_states=value_states,
@@ -706,6 +725,7 @@ def get_attention_mask(
706
 
707
  Returns (attention_mask_2d, attention_mask_4d, flex_block_mask).
708
  """
 
709
  if attention_mask is None:
710
  return None, None, None
711
 
@@ -720,14 +740,14 @@ def get_attention_mask(
720
  "attention_mask shape must match the input batch and sequence dimensions; "
721
  f"expected {expected_shape}, received {tuple(attention_mask.shape)}."
722
  )
723
- attention_mask_2d = attention_mask.to(device=device, dtype=torch.bool)
724
  if not bool(attention_mask_2d.any(dim=1).all()):
725
  raise ValueError("attention_mask must keep at least one valid key per batch row.")
726
 
727
  effective_backend = resolve_attention_backend(effective_backend)
728
 
729
  if effective_backend.is_flash:
730
- return attention_mask_2d, None, None
731
 
732
  if effective_backend == AttentionBackend.FLEX_ATTENTION:
733
  if create_block_mask is None:
@@ -751,12 +771,12 @@ def get_attention_mask(
751
  mask_semantics=mask_semantics,
752
  mask_mod=mask_mod,
753
  )
754
- return attention_mask_2d, None, flex_block_mask
755
 
756
  # SDPA/manual masks only keys. Padding queries still attend to real keys, so
757
  # their outputs stay finite instead of softmaxing over all -inf scores.
758
- attention_mask_4d = attention_mask_2d[:, None, None, :]
759
- return attention_mask_2d, attention_mask_4d, None
760
 
761
 
762
  def bool_to_additive_mask(
@@ -770,10 +790,11 @@ def bool_to_additive_mask(
770
  That silently drops the mask. Always allocate a float tensor first, then fill it.
771
  This helper is the sanctioned way to build an SDPA additive mask from a bool validity mask.
772
  """
 
773
  if bool_mask.dtype != torch.bool:
774
  raise TypeError(
775
  f"bool_to_additive_mask requires a bool tensor, got dtype={bool_mask.dtype}"
776
  )
777
- additive = torch.zeros_like(bool_mask, dtype=dtype)
778
  additive.masked_fill_(bool_mask.logical_not(), float("-inf"))
779
- return additive
 
8
  from __future__ import annotations
9
 
10
  import warnings
11
+ import torch
12
  from collections import OrderedDict
13
  from collections.abc import Callable
14
  from enum import Enum
15
  from threading import RLock
 
 
16
  from einops import rearrange
17
  from torch.nn import functional as F
18
 
19
  from ._kernel_lock import load_locked_kernel
20
 
21
+
22
  try:
23
  from torch.nn.attention.flex_attention import BlockMask, create_block_mask, flex_attention
24
  except ImportError:
 
117
  raise RuntimeError(
118
  "'flex_attention' was requested, but torch.create_block_mask is unavailable."
119
  )
120
+ pattern = mask_pattern.detach().to(device=device).contiguous() # mask_pattern.shape
121
  # One device-to-host transfer is required for an exact cache identity. Use
122
  # the contiguous buffer directly instead of materializing one Python int
123
  # per byte, which is prohibitively expensive for long batched sequences.
124
+ host_pattern = pattern.to(device="cpu").contiguous() # mask_pattern.shape
125
+ pattern_bytes = host_pattern.view(torch.uint8).numpy().tobytes(order="C") # bytes
126
  cache_key = (
127
  str(device),
128
  None if dtype is None else str(dtype),
 
203
  ) -> torch.dtype:
204
  """Reject dtypes outside the immutable kernel manifest before dispatch."""
205
 
206
+ # query_states, key_states, value_states: (b, l, h, d) or (t, h, d)
207
  tensor_dtypes = {query_states.dtype, key_states.dtype, value_states.dtype}
208
  if len(tensor_dtypes) != 1:
209
  observed = ", ".join(sorted(str(dtype) for dtype in tensor_dtypes))
 
244
  ) -> torch.device:
245
  """Require Q, K, and V on one CUDA device before loading a kernel."""
246
 
247
+ # query_states, key_states, value_states: (b, l, h, d) or (t, h, d)
248
  devices = (query_states.device, key_states.device, value_states.device)
249
  if len(set(devices)) != 1:
250
  observed = ", ".join(str(device) for device in devices)
 
286
  Failing to override when Q is pre-scaled applies the scale twice and breaks
287
  parity with eager attention and SDPA.
288
  """
289
+ # query_states, key_states, value_states: (b, l, h, d)
290
  flash_kernel, flash_kernel_variant = _ensure_flash_kernels_loaded(implementation)
291
  if flash_kernel_variant == "flash_attn2":
292
+ output = flash_kernel.flash_attn_func( # (b, l, h, d) or tuple with that first
293
  q=query_states,
294
  k=key_states,
295
  v=value_states,
 
297
  softmax_scale=softmax_scale,
298
  causal=causal,
299
  )
300
+ return output[0] if isinstance(output, tuple) else output # (b, l, h, d)
301
  if flash_kernel_variant == "flash_attn3":
302
+ output = flash_kernel.flash_attn_func( # (b, l, h, d) or tuple with that first
303
  q=query_states,
304
  k=key_states,
305
  v=value_states,
 
307
  causal=causal,
308
  )
309
  if isinstance(output, tuple):
310
+ return output[0] # (b, l, h, d)
311
+ return output # (b, l, h, d)
312
  raise RuntimeError(f"Unsupported FlashAttention kernel variant: {flash_kernel_variant}")
313
 
314
 
 
329
  See `_kernels_flash_forward` docstring for why `softmax_scale=1.0` must be
330
  passed when Q has been pre-scaled by the caller.
331
  """
332
+ # query_states, key_states, value_states: (t, h, d)
333
+ # cu_seqlens_q, cu_seqlens_k: (b + 1,)
334
  flash_kernel, flash_kernel_variant = _ensure_flash_kernels_loaded(implementation)
335
  if flash_kernel_variant == "flash_attn2":
336
+ output = flash_kernel.flash_attn_varlen_func( # (t, h, d) or tuple with that first
337
  q=query_states,
338
  k=key_states,
339
  v=value_states,
 
345
  softmax_scale=softmax_scale,
346
  causal=causal,
347
  )
348
+ return output[0] if isinstance(output, tuple) else output # (t, h, d)
349
  if flash_kernel_variant == "flash_attn3":
350
+ output = flash_kernel.flash_attn_varlen_func( # (t, h, d) or tuple with that first
351
  q=query_states,
352
  k=key_states,
353
  v=value_states,
 
359
  causal=causal,
360
  )
361
  if isinstance(output, tuple):
362
+ return output[0] # (t, h, d)
363
+ return output # (t, h, d)
364
  raise RuntimeError(f"Unsupported FlashAttention kernel variant: {flash_kernel_variant}")
365
 
366
 
 
369
  class IndexFirstAxis(torch.autograd.Function):
370
  @staticmethod
371
  def forward(ctx, input, indices) -> torch.Tensor:
372
+ # input: (n, ...); indices: (m,)
373
  ctx.save_for_backward(indices)
374
  if input.ndim < 2:
375
  raise ValueError(
 
383
  )
384
  ctx.first_axis_dim, other_shape = input.shape[0], input.shape[1:]
385
  second_dim = other_shape.numel()
386
+ return torch.gather( # (m, ...)
387
  rearrange(input, "b ... -> b (...)"), 0, indices.unsqueeze(1).expand(-1, second_dim)
388
  ).reshape(-1, *other_shape)
389
 
390
  @staticmethod
391
  def backward(ctx, grad_output) -> tuple[torch.Tensor, None]:
392
+ # grad_output: (m, ...)
393
  (indices,) = ctx.saved_tensors
394
  if grad_output.ndim < 2:
395
  raise RuntimeError(
 
397
  "two dimensions."
398
  )
399
  other_shape = grad_output.shape[1:]
400
+ grad_output = rearrange(grad_output, "b ... -> b (...)") # (m, product(...))
401
+ grad_input = torch.zeros( # (n, product(...))
402
  [ctx.first_axis_dim, grad_output.shape[1]],
403
  device=grad_output.device,
404
  dtype=grad_output.dtype,
405
  )
406
  grad_input.scatter_(0, indices.unsqueeze(1).expand(-1, grad_output.shape[1]), grad_output)
407
+ return grad_input.reshape(ctx.first_axis_dim, *other_shape), None # (n, ...), None
408
 
409
 
410
  class IndexPutFirstAxis(torch.autograd.Function):
411
  @staticmethod
412
  def forward(ctx, values, indices, first_axis_dim) -> torch.Tensor:
413
+ # values: (m, ...); indices: (m,)
414
  ctx.save_for_backward(indices)
415
  if indices.ndim != 1:
416
  raise ValueError(
 
422
  "index_put_first_axis values must have at least two dimensions; "
423
  f"received shape {tuple(values.shape)}."
424
  )
425
+ output = torch.zeros( # (n, ...)
426
  first_axis_dim, *values.shape[1:], device=values.device, dtype=values.dtype
427
  )
428
  output[indices] = values
429
+ return output # (n, ...)
430
 
431
  @staticmethod
432
  def backward(ctx, grad_output) -> tuple[torch.Tensor, None, None]:
433
+ # grad_output: (n, ...)
434
  (indices,) = ctx.saved_tensors
435
+ return grad_output[indices], None, None # (m, ...), None, None
436
 
437
 
438
  index_first_axis = IndexFirstAxis.apply
 
442
  def pad_input(
443
  hidden_states: torch.Tensor, indices: torch.Tensor, batch: int, seqlen: int
444
  ) -> torch.Tensor:
445
+ # hidden_states: (t, ...); indices: (t,)
446
+ output = index_put_first_axis(hidden_states, indices, batch * seqlen) # (b * l, ...)
447
+ return rearrange(output, "(b s) ... -> b s ...", b=batch) # (b, l, ...)
448
 
449
 
450
  def _unpad_input(
 
460
  tuple[torch.Tensor, torch.Tensor],
461
  tuple[int, int],
462
  ]:
463
+ # query_layer, key_layer, value_layer: (b, l, h, d); attention_mask_2d: (b, l)
464
  batch_size, seq_len, num_heads, head_dim = query_layer.shape
465
+ seqlens = attention_mask_2d.sum(dim=1).int() # (b,)
466
+ cu_seqlens = F.pad(seqlens.cumsum(0, dtype=torch.int32), (1, 0)) # (b + 1,)
467
  max_seqlen = int(seqlens.max().item())
468
+ indices = attention_mask_2d.flatten().nonzero(as_tuple=False).flatten() # (t,)
469
+ query_layer = index_first_axis( # (t, h, d)
470
  query_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
471
  )
472
+ key_layer = index_first_axis( # (t, h, d)
473
  key_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
474
  )
475
+ value_layer = index_first_axis( # (t, h, d)
476
  value_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
477
  )
478
  return (
 
493
  ) -> torch.Tensor:
494
  """Validate the self-attention padding mask used by the varlen kernels."""
495
 
496
+ # query_states, key_states, value_states: (b, l, h, d); attention_mask_2d: (b, l)
497
  if attention_mask_2d.ndim != 2:
498
  raise ValueError("FlashAttention padding masks must have shape (batch, sequence_length).")
499
  expected_shape = query_states.shape[:2]
 
509
  )
510
  if attention_mask_2d.device != query_states.device:
511
  raise ValueError("FlashAttention padding mask and Q, K, and V must be on the same device.")
512
+ return attention_mask_2d.to(dtype=torch.bool) # (b, l)
513
 
514
 
515
  def kernels_flash_attention_func(
 
533
  `softmax_scale=1.0`. Otherwise the flash kernel applies its default scale
534
  again, yielding an effective `1/head_dim` scale that drifts across layers.
535
  """
536
+ # query_states, key_states, value_states: (b, l, h, d)
537
+ # attention_mask_2d: (b, l) or None
538
  _validate_kernels_flash_device(
539
  query_states,
540
  key_states,
 
548
  implementation,
549
  )
550
  if query_states.dtype != runtime_dtype:
551
+ query_states = query_states.to(dtype=runtime_dtype) # (b, l, h, d)
552
+ key_states = key_states.to(dtype=runtime_dtype) # (b, l, h, d)
553
+ value_states = value_states.to(dtype=runtime_dtype) # (b, l, h, d)
554
  if attention_mask_2d is not None:
555
+ attention_mask_2d = _validate_flash_padding_mask( # (b, l)
556
  query_states,
557
  key_states,
558
  value_states,
 
568
  indices_q,
569
  (cu_seqlens_q, cu_seqlens_k),
570
  (max_seqlen_q, max_seqlen_k),
571
+ ) = _unpad_input( # (t, h, d), (t, h, d), (t, h, d), (t,), (b + 1,), scalars
572
+ query_states,
573
+ key_states,
574
+ value_states,
575
+ attention_mask_2d,
576
+ )
577
+ attn_output_unpad = _kernels_flash_varlen_forward( # (t, h, d)
578
  query_states=query_states,
579
  key_states=key_states,
580
  value_states=value_states,
 
586
  softmax_scale=softmax_scale,
587
  implementation=implementation,
588
  )
589
+ output = pad_input(attn_output_unpad, indices_q, batch_size, q_len) # (b, l, h, d)
590
+ return output.masked_fill(~attention_mask_2d[:, :, None, None], 0) # (b, l, h, d)
591
  else:
592
+ return _kernels_flash_forward( # (b, l, h, d)
593
  query_states=query_states,
594
  key_states=key_states,
595
  value_states=value_states,
 
725
 
726
  Returns (attention_mask_2d, attention_mask_4d, flex_block_mask).
727
  """
728
+ # attention_mask: (b, l) or None
729
  if attention_mask is None:
730
  return None, None, None
731
 
 
740
  "attention_mask shape must match the input batch and sequence dimensions; "
741
  f"expected {expected_shape}, received {tuple(attention_mask.shape)}."
742
  )
743
+ attention_mask_2d = attention_mask.to(device=device, dtype=torch.bool) # (b, l)
744
  if not bool(attention_mask_2d.any(dim=1).all()):
745
  raise ValueError("attention_mask must keep at least one valid key per batch row.")
746
 
747
  effective_backend = resolve_attention_backend(effective_backend)
748
 
749
  if effective_backend.is_flash:
750
+ return attention_mask_2d, None, None # (b, l), None, None
751
 
752
  if effective_backend == AttentionBackend.FLEX_ATTENTION:
753
  if create_block_mask is None:
 
771
  mask_semantics=mask_semantics,
772
  mask_mod=mask_mod,
773
  )
774
+ return attention_mask_2d, None, flex_block_mask # (b, l), None, BlockMask
775
 
776
  # SDPA/manual masks only keys. Padding queries still attend to real keys, so
777
  # their outputs stay finite instead of softmaxing over all -inf scores.
778
+ attention_mask_4d = attention_mask_2d[:, None, None, :] # (b, 1, 1, l)
779
+ return attention_mask_2d, attention_mask_4d, None # (b, l), (b, 1, 1, l), None
780
 
781
 
782
  def bool_to_additive_mask(
 
790
  That silently drops the mask. Always allocate a float tensor first, then fill it.
791
  This helper is the sanctioned way to build an SDPA additive mask from a bool validity mask.
792
  """
793
+ # bool_mask: (...)
794
  if bool_mask.dtype != torch.bool:
795
  raise TypeError(
796
  f"bool_to_additive_mask requires a bool tensor, got dtype={bool_mask.dtype}"
797
  )
798
+ additive = torch.zeros_like(bool_mask, dtype=dtype) # (...)
799
  additive.masked_fill_(bool_mask.logical_not(), float("-inf"))
800
+ return additive # (...)
fastplms/attention/_kernel_lock.py CHANGED
@@ -2,7 +2,6 @@
2
 
3
  from __future__ import annotations
4
 
5
- import importlib.metadata
6
  import json
7
  import os
8
  from pathlib import Path
@@ -15,50 +14,35 @@ def require_kernels_package() -> None:
15
  import kernels # noqa: F401
16
  except ImportError as error:
17
  raise RuntimeError(
18
- "Precompiled FlashAttention requires the FastPLMs 'flash' extra."
19
  ) from error
20
 
21
 
22
  def _kernel_lock_path() -> Path:
23
- """Return the lock from an artifact, checkout, or installed distribution."""
24
  source_path = Path(__file__).resolve()
25
- candidates = [
26
  source_path.parents[1] / "kernels.lock",
27
  source_path.parents[3] / "kernels.lock",
28
- ]
29
- try:
30
- import fastplms
31
-
32
- candidates.extend(Path(root) / "kernels.lock" for root in fastplms.__path__)
33
- except (ImportError, AttributeError):
34
- pass
35
- for candidate in candidates:
36
  if candidate.is_file():
37
  return candidate
38
 
39
- try:
40
- distribution = importlib.metadata.distribution("fastplms")
41
- except importlib.metadata.PackageNotFoundError as error:
42
- raise RuntimeError("FastPLMs was installed without kernels.lock.") from error
43
- for relative in distribution.files or ():
44
- if relative.name != "kernels.lock":
45
- continue
46
- candidate = Path(distribution.locate_file(relative))
47
- if candidate.is_file():
48
- return candidate
49
- raise RuntimeError("The installed FastPLMs distribution does not contain kernels.lock.")
50
 
51
 
52
  def _locked_entry(lock_path: Path, repository: str) -> dict[str, Any]:
53
  try:
54
- data = json.loads(lock_path.read_text(encoding="utf-8"))
55
  except (OSError, json.JSONDecodeError) as error:
56
- raise RuntimeError(f"Unable to read the packaged kernel lock: {lock_path}") from error
57
- if not isinstance(data, list):
58
  raise RuntimeError("kernels.lock must contain a JSON list.")
59
- if any(not isinstance(entry, dict) for entry in data):
60
  raise RuntimeError("Every kernels.lock entry must be a JSON object.")
61
- matches = [entry for entry in data if entry.get("repo_id") == repository]
62
  if len(matches) != 1:
63
  raise RuntimeError(
64
  f"kernels.lock must contain exactly one entry for {repository!r}; found {len(matches)}."
@@ -125,11 +109,11 @@ def _load_offline_locked_kernel(
125
  from kernels.variants import get_variants_local, resolve_variants
126
  except ImportError as error:
127
  raise RuntimeError(
128
- "Precompiled FlashAttention requires the FastPLMs 'flash' extra."
129
  ) from error
130
 
131
- parsed = get_variants_local(build_root)
132
- parsed_names = {variant.variant_str for variant in parsed}
133
  invalid = sorted(set(cached_names).difference(parsed_names))
134
  if invalid:
135
  raise RuntimeError(
@@ -137,7 +121,7 @@ def _load_offline_locked_kernel(
137
  f"{', '.join(invalid)}"
138
  )
139
 
140
- compatible, _ = resolve_variants(parsed)
141
  if len(compatible) != 1:
142
  names = ", ".join(variant.variant_str for variant in compatible) or "none"
143
  raise RuntimeError(
@@ -165,7 +149,7 @@ def load_locked_kernel(repository: str, revision: str) -> object:
165
  from kernels.lockfile import KernelLock
166
  except ImportError as error:
167
  raise RuntimeError(
168
- "Precompiled FlashAttention requires the FastPLMs 'flash' extra."
169
  ) from error
170
 
171
  lock_path = _kernel_lock_path()
 
2
 
3
  from __future__ import annotations
4
 
 
5
  import json
6
  import os
7
  from pathlib import Path
 
14
  import kernels # noqa: F401
15
  except ImportError as error:
16
  raise RuntimeError(
17
+ "Precompiled FlashAttention requires requirements/features/flash.in."
18
  ) from error
19
 
20
 
21
  def _kernel_lock_path() -> Path:
22
+ """Return the kernel lock from a Hub artifact or source checkout."""
23
  source_path = Path(__file__).resolve()
24
+ for candidate in (
25
  source_path.parents[1] / "kernels.lock",
26
  source_path.parents[3] / "kernels.lock",
27
+ ):
 
 
 
 
 
 
 
28
  if candidate.is_file():
29
  return candidate
30
 
31
+ raise RuntimeError(
32
+ "kernels.lock is missing from the Hugging Face artifact or source checkout."
33
+ )
 
 
 
 
 
 
 
 
34
 
35
 
36
  def _locked_entry(lock_path: Path, repository: str) -> dict[str, Any]:
37
  try:
38
+ lock_entries = json.loads(lock_path.read_text(encoding="utf-8"))
39
  except (OSError, json.JSONDecodeError) as error:
40
+ raise RuntimeError(f"Unable to read the kernel lock: {lock_path}") from error
41
+ if not isinstance(lock_entries, list):
42
  raise RuntimeError("kernels.lock must contain a JSON list.")
43
+ if any(not isinstance(entry, dict) for entry in lock_entries):
44
  raise RuntimeError("Every kernels.lock entry must be a JSON object.")
45
+ matches = [entry for entry in lock_entries if entry.get("repo_id") == repository]
46
  if len(matches) != 1:
47
  raise RuntimeError(
48
  f"kernels.lock must contain exactly one entry for {repository!r}; found {len(matches)}."
 
109
  from kernels.variants import get_variants_local, resolve_variants
110
  except ImportError as error:
111
  raise RuntimeError(
112
+ "Precompiled FlashAttention requires requirements/features/flash.in."
113
  ) from error
114
 
115
+ cached_variants = get_variants_local(build_root)
116
+ parsed_names = {variant.variant_str for variant in cached_variants}
117
  invalid = sorted(set(cached_names).difference(parsed_names))
118
  if invalid:
119
  raise RuntimeError(
 
121
  f"{', '.join(invalid)}"
122
  )
123
 
124
+ compatible, _ = resolve_variants(cached_variants)
125
  if len(compatible) != 1:
126
  names = ", ".join(variant.variant_str for variant in compatible) or "none"
127
  raise RuntimeError(
 
149
  from kernels.lockfile import KernelLock
150
  except ImportError as error:
151
  raise RuntimeError(
152
+ "Precompiled FlashAttention requires requirements/features/flash.in."
153
  ) from error
154
 
155
  lock_path = _kernel_lock_path()
fastplms/attention/interfaces.py CHANGED
@@ -2,11 +2,10 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  from collections.abc import Mapping
6
  from functools import partial
7
  from typing import Any
8
-
9
- import torch
10
  from transformers import AttentionInterface, AttentionMaskInterface
11
 
12
  from ._core import (
@@ -36,6 +35,7 @@ def _kernels_attention_forward(
36
  FastPLMs kernel adapter uses the latter layout internally.
37
  """
38
 
 
39
  dropout = float(kwargs.get("dropout", 0.0) or 0.0)
40
  if module.training and dropout:
41
  raise RuntimeError(
@@ -45,15 +45,15 @@ def _kernels_attention_forward(
45
  causal = bool(kwargs.get("is_causal", getattr(module, "is_causal", False)))
46
  softmax_scale = kwargs.get("scaling")
47
  output = kernels_flash_attention_func(
48
- query_states=query.transpose(1, 2).contiguous(),
49
- key_states=key.transpose(1, 2).contiguous(),
50
- value_states=value.transpose(1, 2).contiguous(),
51
  attention_mask_2d=attention_mask,
52
  causal=causal,
53
  softmax_scale=softmax_scale,
54
  implementation=implementation,
55
- )
56
- return output, None
57
 
58
 
59
  # Keep FastPLMs' kernels-only adapters local to this registry instance.
 
2
 
3
  from __future__ import annotations
4
 
5
+ import torch
6
  from collections.abc import Mapping
7
  from functools import partial
8
  from typing import Any
 
 
9
  from transformers import AttentionInterface, AttentionMaskInterface
10
 
11
  from ._core import (
 
35
  FastPLMs kernel adapter uses the latter layout internally.
36
  """
37
 
38
+ # query, key, value: (b, h, l, d); attention_mask: (b, l) or None
39
  dropout = float(kwargs.get("dropout", 0.0) or 0.0)
40
  if module.training and dropout:
41
  raise RuntimeError(
 
45
  causal = bool(kwargs.get("is_causal", getattr(module, "is_causal", False)))
46
  softmax_scale = kwargs.get("scaling")
47
  output = kernels_flash_attention_func(
48
+ query_states=query.transpose(1, 2).contiguous(), # (b, l, h, d)
49
+ key_states=key.transpose(1, 2).contiguous(), # (b, l, h, d)
50
+ value_states=value.transpose(1, 2).contiguous(), # (b, l, h, d)
51
  attention_mask_2d=attention_mask,
52
  causal=causal,
53
  softmax_scale=softmax_scale,
54
  implementation=implementation,
55
+ ) # (b, l, h, d)
56
+ return output, None # (b, l, h, d), None
57
 
58
 
59
  # Keep FastPLMs' kernels-only adapters local to this registry instance.
fastplms/embeddings/__init__.py CHANGED
@@ -33,6 +33,7 @@ from .types import (
33
  TensorValue,
34
  )
35
 
 
36
  __all__ = [
37
  "DEFAULT_SHARD_SIZE",
38
  "POOLING_NAMES",
 
33
  TensorValue,
34
  )
35
 
36
+
37
  __all__ = [
38
  "DEFAULT_SHARD_SIZE",
39
  "POOLING_NAMES",
fastplms/embeddings/pooling.py CHANGED
@@ -3,15 +3,16 @@
3
  from __future__ import annotations
4
 
5
  import math
6
- from collections.abc import Sequence
7
-
8
  import torch
 
9
  from torch import Tensor
10
 
 
11
  POOLING_NAMES = frozenset({"mean", "max", "norm", "median", "std", "var", "cls", "parti"})
12
 
13
 
14
  def _validate_inputs(X: Tensor, M: Tensor) -> Tensor:
 
15
  if not isinstance(X, Tensor) or not isinstance(M, Tensor):
16
  raise TypeError("X and M must be tensors.")
17
  if X.ndim != 3:
@@ -24,12 +25,12 @@ def _validate_inputs(X: Tensor, M: Tensor) -> Tensor:
24
  raise TypeError("M must be a boolean or binary numeric residue mask.")
25
  if not bool(torch.isfinite(M).all()) or not bool(((M == 0) | (M == 1)).all()):
26
  raise ValueError("M must contain only finite binary mask values.")
27
- M = M.to(device=X.device, dtype=torch.bool)
28
  if not bool(M.any(dim=1).all()):
29
  raise ValueError("Every sample must contain at least one biological residue.")
30
  if not bool((torch.isfinite(X) | ~M.unsqueeze(-1)).all()):
31
  raise ValueError("Biological residue embeddings produced non-finite output.")
32
- return M
33
 
34
 
35
  def _pooled_attention(attentions: Tensor | Sequence[Tensor], *, batch_size: int) -> Tensor:
@@ -43,27 +44,27 @@ def _pooled_attention(attentions: Tensor | Sequence[Tensor], *, batch_size: int)
43
  if isinstance(attentions, Sequence):
44
  if not attentions:
45
  raise ValueError("parti received an empty attention sequence.")
46
- # Each A_i has shape (b, h, l, l).
47
- A = torch.stack(tuple(attentions), dim=1)
48
  else:
49
- A = attentions
50
 
51
  if A.ndim == 5:
52
  if A.shape[0] != batch_size and A.shape[1] == batch_size:
53
- A = A.transpose(0, 1)
54
  if A.shape[0] != batch_size:
55
  raise ValueError("Five-dimensional attentions must use (b, n, h, l, l).")
56
- A = A.flatten(1, 2).amax(dim=1)
57
  elif A.ndim == 4:
58
  if A.shape[0] != batch_size:
59
  raise ValueError("Four-dimensional attentions must use (b, h, l, l).")
60
- A = A.amax(dim=1)
61
  elif A.ndim == 3:
62
  if A.shape[0] != batch_size:
63
  raise ValueError("Three-dimensional attentions must use (b, l, l).")
64
  else:
65
  raise ValueError("Attentions must have shape (b, l, l), (b, h, l, l), or (b, n, h, l, l).")
66
- return A
67
 
68
 
69
  def pagerank_weights(
@@ -79,6 +80,7 @@ def pagerank_weights(
79
  probabilities; dangling rows transition uniformly.
80
  """
81
 
 
82
  if not isinstance(A, Tensor):
83
  raise TypeError("A must be a tensor.")
84
  if A.ndim != 2 or A.shape[0] != A.shape[1]:
@@ -103,19 +105,23 @@ def pagerank_weights(
103
  if not bool(torch.isfinite(A).all()):
104
  raise ValueError("A must contain only finite attention values.")
105
  work_dtype = torch.float64 if A.dtype == torch.float64 else torch.float32
106
- P = A.detach().to(dtype=work_dtype).clamp_min(0)
107
- row_sum = P.sum(dim=-1, keepdim=True)
108
- uniform = torch.full_like(P, 1.0 / length)
109
- P = torch.where(row_sum > 0, P / row_sum.clamp_min(torch.finfo(work_dtype).tiny), uniform)
110
- p = torch.full((length,), 1.0 / length, device=P.device, dtype=work_dtype)
 
 
 
 
111
  teleport = (1.0 - damping) / length
112
  for _ in range(max_iterations):
113
- p_next = teleport + damping * (P.transpose(0, 1) @ p)
114
  if torch.linalg.vector_norm(p_next - p, ord=1) <= tolerance:
115
- p = p_next
116
  break
117
- p = p_next
118
- return p / p.sum()
119
 
120
 
121
  class Pooler:
@@ -157,28 +163,29 @@ class Pooler:
157
  attentions: Tensor | Sequence[Tensor] | None = None,
158
  attention_backend: str | None = None,
159
  ) -> Tensor:
160
- M = _validate_inputs(X, residue_mask)
161
- M_expanded = M.unsqueeze(-1)
162
- count = M_expanded.sum(dim=1).clamp_min(1)
163
- X_residues = X.masked_fill(~M_expanded, 0)
 
164
  outputs: list[Tensor] = []
165
 
166
  for name in self.names:
167
  if name == "mean":
168
- Y = X_residues.sum(dim=1) / count
169
  elif name == "max":
170
- Y = X.masked_fill(~M_expanded, -torch.inf).max(dim=1).values
171
  elif name == "norm":
172
- Y = torch.linalg.vector_norm(X_residues, ord=2, dim=1)
173
  elif name == "median":
174
- Y = X.masked_fill(~M_expanded, torch.nan).nanmedian(dim=1).values
175
  elif name in {"var", "std"}:
176
- mean = X_residues.sum(dim=1, keepdim=True) / count.unsqueeze(1)
177
- centered = (X - mean).masked_fill(~M_expanded, 0)
178
- variance = (centered**2).sum(dim=1) / count
179
- Y = variance.sqrt() if name == "std" else variance
180
  elif name == "cls":
181
- Y = X[:, 0]
182
  else:
183
  if attention_backend != "eager":
184
  raise ValueError(
@@ -189,14 +196,15 @@ class Pooler:
189
  raise ValueError("parti requires model attention matrices.")
190
  if int(M.sum(dim=1).max().item()) > 2048:
191
  raise ValueError("parti supports at most 2,048 biological residues.")
192
- A = _pooled_attention(attentions, batch_size=X.shape[0]).to(X.device)
193
  pooled: list[Tensor] = []
194
  for X_i, M_i, A_i in zip(X, M, A, strict=True):
195
- indices = M_i.nonzero(as_tuple=True)[0]
196
- A_residue = A_i.index_select(0, indices).index_select(1, indices)
197
- w = pagerank_weights(A_residue).to(dtype=X.dtype)
198
- pooled.append(w @ X_i.index_select(0, indices))
199
- Y = torch.stack(pooled)
 
200
  if not bool(torch.isfinite(Y).all()):
201
  raise ValueError(
202
  f"Pooling operation {name!r} produced non-finite output from "
@@ -204,7 +212,7 @@ class Pooler:
204
  )
205
  outputs.append(Y)
206
 
207
- return torch.cat(outputs, dim=-1)
208
 
209
 
210
  __all__ = ["POOLING_NAMES", "Pooler", "pagerank_weights"]
 
3
  from __future__ import annotations
4
 
5
  import math
 
 
6
  import torch
7
+ from collections.abc import Sequence
8
  from torch import Tensor
9
 
10
+
11
  POOLING_NAMES = frozenset({"mean", "max", "norm", "median", "std", "var", "cls", "parti"})
12
 
13
 
14
  def _validate_inputs(X: Tensor, M: Tensor) -> Tensor:
15
+ # X: (b, l, d); M: (b, l)
16
  if not isinstance(X, Tensor) or not isinstance(M, Tensor):
17
  raise TypeError("X and M must be tensors.")
18
  if X.ndim != 3:
 
25
  raise TypeError("M must be a boolean or binary numeric residue mask.")
26
  if not bool(torch.isfinite(M).all()) or not bool(((M == 0) | (M == 1)).all()):
27
  raise ValueError("M must contain only finite binary mask values.")
28
+ M = M.to(device=X.device, dtype=torch.bool) # (b, l)
29
  if not bool(M.any(dim=1).all()):
30
  raise ValueError("Every sample must contain at least one biological residue.")
31
  if not bool((torch.isfinite(X) | ~M.unsqueeze(-1)).all()):
32
  raise ValueError("Biological residue embeddings produced non-finite output.")
33
+ return M # (b, l)
34
 
35
 
36
  def _pooled_attention(attentions: Tensor | Sequence[Tensor], *, batch_size: int) -> Tensor:
 
44
  if isinstance(attentions, Sequence):
45
  if not attentions:
46
  raise ValueError("parti received an empty attention sequence.")
47
+ # Each A_i: (b, h, l, l).
48
+ A = torch.stack(tuple(attentions), dim=1) # (b, n, h, l, l)
49
  else:
50
+ A = attentions # (b, ..., l, l)
51
 
52
  if A.ndim == 5:
53
  if A.shape[0] != batch_size and A.shape[1] == batch_size:
54
+ A = A.transpose(0, 1) # (b, n, h, l, l)
55
  if A.shape[0] != batch_size:
56
  raise ValueError("Five-dimensional attentions must use (b, n, h, l, l).")
57
+ A = A.flatten(1, 2).amax(dim=1) # (b, l, l)
58
  elif A.ndim == 4:
59
  if A.shape[0] != batch_size:
60
  raise ValueError("Four-dimensional attentions must use (b, h, l, l).")
61
+ A = A.amax(dim=1) # (b, l, l)
62
  elif A.ndim == 3:
63
  if A.shape[0] != batch_size:
64
  raise ValueError("Three-dimensional attentions must use (b, l, l).")
65
  else:
66
  raise ValueError("Attentions must have shape (b, l, l), (b, h, l, l), or (b, n, h, l, l).")
67
+ return A # (b, l, l)
68
 
69
 
70
  def pagerank_weights(
 
80
  probabilities; dangling rows transition uniformly.
81
  """
82
 
83
+ # A: (l, l)
84
  if not isinstance(A, Tensor):
85
  raise TypeError("A must be a tensor.")
86
  if A.ndim != 2 or A.shape[0] != A.shape[1]:
 
105
  if not bool(torch.isfinite(A).all()):
106
  raise ValueError("A must contain only finite attention values.")
107
  work_dtype = torch.float64 if A.dtype == torch.float64 else torch.float32
108
+ P = A.detach().to(dtype=work_dtype).clamp_min(0) # (l, l)
109
+ row_sum = P.sum(dim=-1, keepdim=True) # (l, 1)
110
+ uniform = torch.full_like(P, 1.0 / length) # (l, l)
111
+ P = torch.where( # (l, l)
112
+ row_sum > 0,
113
+ P / row_sum.clamp_min(torch.finfo(work_dtype).tiny),
114
+ uniform,
115
+ )
116
+ p = torch.full((length,), 1.0 / length, device=P.device, dtype=work_dtype) # (l,)
117
  teleport = (1.0 - damping) / length
118
  for _ in range(max_iterations):
119
+ p_next = teleport + damping * (P.transpose(0, 1) @ p) # (l,)
120
  if torch.linalg.vector_norm(p_next - p, ord=1) <= tolerance:
121
+ p = p_next # (l,)
122
  break
123
+ p = p_next # (l,)
124
+ return p / p.sum() # (l,)
125
 
126
 
127
  class Pooler:
 
163
  attentions: Tensor | Sequence[Tensor] | None = None,
164
  attention_backend: str | None = None,
165
  ) -> Tensor:
166
+ # X: (b, l, d); residue_mask: (b, l)
167
+ M = _validate_inputs(X, residue_mask) # (b, l)
168
+ M_expanded = M.unsqueeze(-1) # (b, l, 1)
169
+ count = M_expanded.sum(dim=1).clamp_min(1) # (b, 1)
170
+ X_residues = X.masked_fill(~M_expanded, 0) # (b, l, d)
171
  outputs: list[Tensor] = []
172
 
173
  for name in self.names:
174
  if name == "mean":
175
+ Y = X_residues.sum(dim=1) / count # (b, d)
176
  elif name == "max":
177
+ Y = X.masked_fill(~M_expanded, -torch.inf).max(dim=1).values # (b, d)
178
  elif name == "norm":
179
+ Y = torch.linalg.vector_norm(X_residues, ord=2, dim=1) # (b, d)
180
  elif name == "median":
181
+ Y = X.masked_fill(~M_expanded, torch.nan).nanmedian(dim=1).values # (b, d)
182
  elif name in {"var", "std"}:
183
+ mean = X_residues.sum(dim=1, keepdim=True) / count.unsqueeze(1) # (b, 1, d)
184
+ centered = (X - mean).masked_fill(~M_expanded, 0) # (b, l, d)
185
+ variance = (centered**2).sum(dim=1) / count # (b, d)
186
+ Y = variance.sqrt() if name == "std" else variance # (b, d)
187
  elif name == "cls":
188
+ Y = X[:, 0] # (b, d)
189
  else:
190
  if attention_backend != "eager":
191
  raise ValueError(
 
196
  raise ValueError("parti requires model attention matrices.")
197
  if int(M.sum(dim=1).max().item()) > 2048:
198
  raise ValueError("parti supports at most 2,048 biological residues.")
199
+ A = _pooled_attention(attentions, batch_size=X.shape[0]).to(X.device) # (b, l, l)
200
  pooled: list[Tensor] = []
201
  for X_i, M_i, A_i in zip(X, M, A, strict=True):
202
+ # X_i: (l, d); M_i: (l,); A_i: (l, l)
203
+ indices = M_i.nonzero(as_tuple=True)[0] # (r,)
204
+ A_residue = A_i.index_select(0, indices).index_select(1, indices) # (r, r)
205
+ w = pagerank_weights(A_residue).to(dtype=X.dtype) # (r,)
206
+ pooled.append(w @ X_i.index_select(0, indices)) # (d,)
207
+ Y = torch.stack(pooled) # (b, d)
208
  if not bool(torch.isfinite(Y).all()):
209
  raise ValueError(
210
  f"Pooling operation {name!r} produced non-finite output from "
 
212
  )
213
  outputs.append(Y)
214
 
215
+ return torch.cat(outputs, dim=-1) # (b, len(self.names) * d)
216
 
217
 
218
  __all__ = ["POOLING_NAMES", "Pooler", "pagerank_weights"]
fastplms/embeddings/runner.py CHANGED
@@ -7,12 +7,11 @@ import json
7
  import platform
8
  import sqlite3
9
  import tempfile
 
10
  from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
11
  from contextlib import contextmanager
12
  from pathlib import Path
13
  from typing import Any, overload
14
-
15
- import torch
16
  from torch import Tensor
17
 
18
  from .pooling import Pooler
@@ -35,6 +34,7 @@ from .types import (
35
  LazyTensorReference,
36
  )
37
 
 
38
  _MAX_PARTI_RESIDUES = 2_048
39
  _RUN_FINGERPRINT_SCHEMA_VERSION = 3
40
  _MODEL_STATE_HASH_CHUNK_BYTES = 16 * 1024**2
@@ -45,6 +45,7 @@ _SUPPORTED_STORAGE_FORMATS = frozenset({"safetensors", "sqlite"})
45
  def _validate_parti_length(M: Tensor) -> None:
46
  """Reject an oversized attention graph before model inference."""
47
 
 
48
  n_residues = int(M.to(dtype=torch.int64).sum(dim=1).max().item())
49
  if n_residues > _MAX_PARTI_RESIDUES:
50
  raise ValueError(f"parti supports at most {_MAX_PARTI_RESIDUES:,} biological residues.")
@@ -58,16 +59,17 @@ def select_hidden_state_embeddings(
58
  store_all_hidden_states: bool = False,
59
  ) -> Tensor:
60
  """Select one hidden state or stack every state without changing values."""
 
61
  if store_all_hidden_states:
62
  if not hidden_states:
63
  raise ValueError("store_all_hidden_states requires model hidden states.")
64
  # H has shape (b, n, l, d), where n follows the model's output order.
65
- return torch.stack(hidden_states, dim=1)
66
  if hidden_state_index == -1:
67
- return last_hidden_state
68
  if not hidden_states:
69
  raise ValueError("hidden_state_index requires model hidden states.")
70
- return hidden_states[hidden_state_index]
71
 
72
 
73
  def iter_fasta(path: str | Path) -> Iterator[EmbeddingInput]:
@@ -508,12 +510,17 @@ def _biological_residue_mask(
508
  ) -> Tensor:
509
  """Remove padding and tokenizer-declared special tokens from M."""
510
 
511
- M = attention_mask.to(dtype=torch.bool)
 
512
  special_ids = tuple(int(token_id) for token_id in getattr(tokenizer, "all_special_ids", ()))
513
  if special_ids:
514
- specials = torch.tensor(special_ids, device=input_ids.device, dtype=input_ids.dtype)
515
- M = M & ~torch.isin(input_ids, specials)
516
- return M
 
 
 
 
517
 
518
 
519
  def _generic_embedding_batch(
@@ -535,20 +542,23 @@ def _generic_embedding_batch(
535
  output = model._embed(sequences, return_attention_mask=True, **model_kwargs)
536
  if not isinstance(output, tuple) or len(output) != 2:
537
  raise TypeError("E1 _embed must return (X, residue_mask).")
538
- X, M = output
539
  preparer = getattr(model, "prep_tokens", None)
540
  if preparer is not None and hasattr(preparer, "get_batch_kwargs"):
541
  prepared = preparer.get_batch_kwargs(sequences, device=X.device)
542
- input_ids = prepared["input_ids"]
543
- boundary_ids = preparer.boundary_token_ids.to(
544
  device=input_ids.device, dtype=input_ids.dtype
545
  )
546
  # E1 wraps each raw sequence in BOS, context-label, terminal-label,
547
  # and EOS tokens. Only amino-acid rows are biological residues.
548
- M = M.to(dtype=torch.bool) & ~torch.isin(input_ids, boundary_ids)
549
  if need_attentions:
550
  raise ValueError("parti is not available for tokenizer-free E1 embedding.")
551
- return EmbeddingBatch(X=X, residue_mask=M.to(dtype=torch.bool))
 
 
 
552
  if tokenizer is None:
553
  raise ValueError("A tokenizer is required for this model's embedding path.")
554
 
@@ -572,14 +582,17 @@ def _generic_embedding_batch(
572
  else:
573
  encoded = tokenizer(sequences, **tokenize_kwargs)
574
  device = _model_device(model)
575
- input_ids = encoded["input_ids"].to(device)
576
- attention_mask = encoded.get("attention_mask", input_ids.new_ones(input_ids.shape)).to(device)
577
- M = _biological_residue_mask(input_ids, attention_mask, tokenizer)
 
 
 
578
  if need_attentions:
579
  # Validate l before either the backbone or its quadratic attention graph
580
  # is materialized. M has shape (b, l).
581
  _validate_parti_length(M)
582
- X = model._embed(input_ids, attention_mask, **model_kwargs)
583
  attentions = None
584
  if need_attentions:
585
  output = model(
@@ -588,10 +601,14 @@ def _generic_embedding_batch(
588
  output_attentions=True,
589
  return_dict=True,
590
  )
591
- attentions = getattr(output, "attentions", None)
592
  if attentions is None:
593
  raise ValueError("The model did not return attentions required by parti.")
594
- return EmbeddingBatch(X=X, residue_mask=M, attentions=attentions)
 
 
 
 
595
 
596
 
597
  def _first_metadata_value(*values: Any) -> Any:
@@ -638,6 +655,7 @@ def _model_identity_metadata(model: Any) -> dict[str, Any]:
638
  def _bounded_tensor_chunks(X: Tensor, max_elements: int) -> Iterable[Tensor]:
639
  """Yield X in logical row-major order without materializing a full copy."""
640
 
 
641
  if X.numel() == 0:
642
  return
643
  if X.ndim == 0:
@@ -649,7 +667,7 @@ def _bounded_tensor_chunks(X: Tensor, max_elements: int) -> Iterable[Tensor]:
649
  if trailing_elements <= max_elements:
650
  rows_per_chunk = max(1, max_elements // trailing_elements)
651
  for start in range(0, X.shape[0], rows_per_chunk):
652
- yield X[start : start + rows_per_chunk]
653
  return
654
  for row in X:
655
  yield from _bounded_tensor_chunks(row, max_elements)
@@ -685,7 +703,7 @@ def _model_state_sha256(model: Any) -> str:
685
  digest.update(header)
686
  max_elements = max(1, _MODEL_STATE_HASH_CHUNK_BYTES // value.element_size())
687
  for chunk in _bounded_tensor_chunks(value.detach(), max_elements):
688
- cpu_chunk = chunk.to(device="cpu").contiguous()
689
  digest.update(cpu_chunk.reshape(-1).view(torch.uint8).numpy().tobytes())
690
  return digest.hexdigest()
691
 
@@ -1306,22 +1324,24 @@ def embed_dataset(
1306
  normalized_decoder_inputs[position] for position in batch_positions
1307
  ]
1308
  if decoder_input_ids is not None:
1309
- indices = torch.tensor(
 
1310
  batch_positions,
1311
  device=decoder_input_ids.device,
1312
  dtype=torch.long,
1313
  )
1314
- batch_model_kwargs["decoder_input_ids"] = decoder_input_ids.index_select(
1315
- 0, indices
1316
  )
1317
  if decoder_attention_mask is not None:
1318
- indices = torch.tensor(
 
1319
  batch_positions,
1320
  device=decoder_attention_mask.device,
1321
  dtype=torch.long,
1322
  )
1323
  batch_model_kwargs["decoder_attention_mask"] = (
1324
- decoder_attention_mask.index_select(0, indices)
1325
  )
1326
  custom_batch = _embedding_batch_fn or getattr(model, "_embedding_batch", None)
1327
  if custom_batch is not None:
@@ -1348,8 +1368,8 @@ def embed_dataset(
1348
  need_attentions=need_attentions,
1349
  model_kwargs=batch_model_kwargs,
1350
  )
1351
- X = batch.X
1352
- raw_mask = batch.residue_mask
1353
  if not isinstance(X, Tensor) or not isinstance(raw_mask, Tensor):
1354
  raise TypeError("Embedding batches must provide Tensor X and residue_mask.")
1355
  if X.is_meta or raw_mask.is_meta:
@@ -1360,7 +1380,7 @@ def embed_dataset(
1360
  raise ValueError("Embedding residue_mask must contain finite binary values.")
1361
  if not bool(((raw_mask == 0) | (raw_mask == 1)).all()):
1362
  raise ValueError("Embedding residue_mask must contain finite binary values.")
1363
- M = raw_mask.to(device=X.device, dtype=torch.bool)
1364
  valid_X_shape = (
1365
  X.ndim == 3
1366
  and X.shape[0] == len(batch_records)
@@ -1384,7 +1404,7 @@ def embed_dataset(
1384
  )
1385
  if not bool(M.any(dim=1).all()):
1386
  raise ValueError("Every embedding sample must contain a biological residue.")
1387
- finite_selected = (
1388
  torch.isfinite(X) | ~M.unsqueeze(-1)
1389
  if X.ndim == 3
1390
  else torch.isfinite(X) | ~M[:, None, :, None]
@@ -1395,28 +1415,32 @@ def embed_dataset(
1395
  # Validate the biological graph only after mask integrity is established.
1396
  _validate_parti_length(M)
1397
  if dtype is not None:
1398
- X = X.to(dtype=dtype)
1399
 
1400
  if full_embeddings:
1401
  if X.ndim == 4:
1402
  values = [
1403
- X_i[:, M_i, :].detach().cpu() for X_i, M_i in zip(X, M, strict=True)
 
1404
  ]
1405
  else:
1406
- values = [X_i[M_i].detach().cpu() for X_i, M_i in zip(X, M, strict=True)]
 
 
 
1407
  else:
1408
  if pooler is None:
1409
  raise RuntimeError(
1410
  "Pooled embedding output was requested without an initialized pooler."
1411
  )
1412
- Y = pooler(
1413
  X,
1414
  M,
1415
  attentions=batch.attentions,
1416
  attention_backend=attention_backend,
1417
  )
1418
  pool_slices = pooler.output_slices(X.shape[-1])
1419
- values = list(Y.detach().cpu().unbind(0))
1420
  for position, record, value in zip(
1421
  batch_positions, batch_records, values, strict=True
1422
  ):
 
7
  import platform
8
  import sqlite3
9
  import tempfile
10
+ import torch
11
  from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
12
  from contextlib import contextmanager
13
  from pathlib import Path
14
  from typing import Any, overload
 
 
15
  from torch import Tensor
16
 
17
  from .pooling import Pooler
 
34
  LazyTensorReference,
35
  )
36
 
37
+
38
  _MAX_PARTI_RESIDUES = 2_048
39
  _RUN_FINGERPRINT_SCHEMA_VERSION = 3
40
  _MODEL_STATE_HASH_CHUNK_BYTES = 16 * 1024**2
 
45
  def _validate_parti_length(M: Tensor) -> None:
46
  """Reject an oversized attention graph before model inference."""
47
 
48
+ # M: (b, l)
49
  n_residues = int(M.to(dtype=torch.int64).sum(dim=1).max().item())
50
  if n_residues > _MAX_PARTI_RESIDUES:
51
  raise ValueError(f"parti supports at most {_MAX_PARTI_RESIDUES:,} biological residues.")
 
59
  store_all_hidden_states: bool = False,
60
  ) -> Tensor:
61
  """Select one hidden state or stack every state without changing values."""
62
+ # last_hidden_state and each hidden_states entry: (b, l, d)
63
  if store_all_hidden_states:
64
  if not hidden_states:
65
  raise ValueError("store_all_hidden_states requires model hidden states.")
66
  # H has shape (b, n, l, d), where n follows the model's output order.
67
+ return torch.stack(hidden_states, dim=1) # (b, n, l, d)
68
  if hidden_state_index == -1:
69
+ return last_hidden_state # (b, l, d)
70
  if not hidden_states:
71
  raise ValueError("hidden_state_index requires model hidden states.")
72
+ return hidden_states[hidden_state_index] # (b, l, d)
73
 
74
 
75
  def iter_fasta(path: str | Path) -> Iterator[EmbeddingInput]:
 
510
  ) -> Tensor:
511
  """Remove padding and tokenizer-declared special tokens from M."""
512
 
513
+ # input_ids, attention_mask: (b, l)
514
+ M = attention_mask.to(dtype=torch.bool) # (b, l)
515
  special_ids = tuple(int(token_id) for token_id in getattr(tokenizer, "all_special_ids", ()))
516
  if special_ids:
517
+ specials = torch.tensor( # (n_special,)
518
+ special_ids,
519
+ device=input_ids.device,
520
+ dtype=input_ids.dtype,
521
+ )
522
+ M = M & ~torch.isin(input_ids, specials) # (b, l)
523
+ return M # (b, l)
524
 
525
 
526
  def _generic_embedding_batch(
 
542
  output = model._embed(sequences, return_attention_mask=True, **model_kwargs)
543
  if not isinstance(output, tuple) or len(output) != 2:
544
  raise TypeError("E1 _embed must return (X, residue_mask).")
545
+ X, M = output # (b, l, d), (b, l)
546
  preparer = getattr(model, "prep_tokens", None)
547
  if preparer is not None and hasattr(preparer, "get_batch_kwargs"):
548
  prepared = preparer.get_batch_kwargs(sequences, device=X.device)
549
+ input_ids = prepared["input_ids"] # (b, l)
550
+ boundary_ids = preparer.boundary_token_ids.to( # (n_boundary,)
551
  device=input_ids.device, dtype=input_ids.dtype
552
  )
553
  # E1 wraps each raw sequence in BOS, context-label, terminal-label,
554
  # and EOS tokens. Only amino-acid rows are biological residues.
555
+ M = M.to(dtype=torch.bool) & ~torch.isin(input_ids, boundary_ids) # (b, l)
556
  if need_attentions:
557
  raise ValueError("parti is not available for tokenizer-free E1 embedding.")
558
+ return EmbeddingBatch( # X: (b, l, d); residue_mask: (b, l)
559
+ X=X,
560
+ residue_mask=M.to(dtype=torch.bool),
561
+ )
562
  if tokenizer is None:
563
  raise ValueError("A tokenizer is required for this model's embedding path.")
564
 
 
582
  else:
583
  encoded = tokenizer(sequences, **tokenize_kwargs)
584
  device = _model_device(model)
585
+ input_ids = encoded["input_ids"].to(device) # (b, l)
586
+ attention_mask = encoded.get( # (b, l)
587
+ "attention_mask",
588
+ input_ids.new_ones(input_ids.shape),
589
+ ).to(device)
590
+ M = _biological_residue_mask(input_ids, attention_mask, tokenizer) # (b, l)
591
  if need_attentions:
592
  # Validate l before either the backbone or its quadratic attention graph
593
  # is materialized. M has shape (b, l).
594
  _validate_parti_length(M)
595
+ X = model._embed(input_ids, attention_mask, **model_kwargs) # (b, l, d)
596
  attentions = None
597
  if need_attentions:
598
  output = model(
 
601
  output_attentions=True,
602
  return_dict=True,
603
  )
604
+ attentions = getattr(output, "attentions", None) # each: (b, h, l, l)
605
  if attentions is None:
606
  raise ValueError("The model did not return attentions required by parti.")
607
+ return EmbeddingBatch( # X: (b, l, d); M: (b, l)
608
+ X=X,
609
+ residue_mask=M,
610
+ attentions=attentions,
611
+ )
612
 
613
 
614
  def _first_metadata_value(*values: Any) -> Any:
 
655
  def _bounded_tensor_chunks(X: Tensor, max_elements: int) -> Iterable[Tensor]:
656
  """Yield X in logical row-major order without materializing a full copy."""
657
 
658
+ # X: (...)
659
  if X.numel() == 0:
660
  return
661
  if X.ndim == 0:
 
667
  if trailing_elements <= max_elements:
668
  rows_per_chunk = max(1, max_elements // trailing_elements)
669
  for start in range(0, X.shape[0], rows_per_chunk):
670
+ yield X[start : start + rows_per_chunk] # (chunk_rows, ...)
671
  return
672
  for row in X:
673
  yield from _bounded_tensor_chunks(row, max_elements)
 
703
  digest.update(header)
704
  max_elements = max(1, _MODEL_STATE_HASH_CHUNK_BYTES // value.element_size())
705
  for chunk in _bounded_tensor_chunks(value.detach(), max_elements):
706
+ cpu_chunk = chunk.to(device="cpu").contiguous() # chunk.shape
707
  digest.update(cpu_chunk.reshape(-1).view(torch.uint8).numpy().tobytes())
708
  return digest.hexdigest()
709
 
 
1324
  normalized_decoder_inputs[position] for position in batch_positions
1325
  ]
1326
  if decoder_input_ids is not None:
1327
+ # decoder_input_ids: (n_records, l_decoder)
1328
+ indices = torch.tensor( # (b,)
1329
  batch_positions,
1330
  device=decoder_input_ids.device,
1331
  dtype=torch.long,
1332
  )
1333
+ batch_model_kwargs["decoder_input_ids"] = ( # (b, l_decoder)
1334
+ decoder_input_ids.index_select(0, indices)
1335
  )
1336
  if decoder_attention_mask is not None:
1337
+ # decoder_attention_mask: (n_records, l_decoder)
1338
+ indices = torch.tensor( # (b,)
1339
  batch_positions,
1340
  device=decoder_attention_mask.device,
1341
  dtype=torch.long,
1342
  )
1343
  batch_model_kwargs["decoder_attention_mask"] = (
1344
+ decoder_attention_mask.index_select(0, indices) # (b, l_decoder)
1345
  )
1346
  custom_batch = _embedding_batch_fn or getattr(model, "_embedding_batch", None)
1347
  if custom_batch is not None:
 
1368
  need_attentions=need_attentions,
1369
  model_kwargs=batch_model_kwargs,
1370
  )
1371
+ X = batch.X # (b, l, d) or (b, n_states, l, d)
1372
+ raw_mask = batch.residue_mask # (b, l)
1373
  if not isinstance(X, Tensor) or not isinstance(raw_mask, Tensor):
1374
  raise TypeError("Embedding batches must provide Tensor X and residue_mask.")
1375
  if X.is_meta or raw_mask.is_meta:
 
1380
  raise ValueError("Embedding residue_mask must contain finite binary values.")
1381
  if not bool(((raw_mask == 0) | (raw_mask == 1)).all()):
1382
  raise ValueError("Embedding residue_mask must contain finite binary values.")
1383
+ M = raw_mask.to(device=X.device, dtype=torch.bool) # (b, l)
1384
  valid_X_shape = (
1385
  X.ndim == 3
1386
  and X.shape[0] == len(batch_records)
 
1404
  )
1405
  if not bool(M.any(dim=1).all()):
1406
  raise ValueError("Every embedding sample must contain a biological residue.")
1407
+ finite_selected = ( # X.shape
1408
  torch.isfinite(X) | ~M.unsqueeze(-1)
1409
  if X.ndim == 3
1410
  else torch.isfinite(X) | ~M[:, None, :, None]
 
1415
  # Validate the biological graph only after mask integrity is established.
1416
  _validate_parti_length(M)
1417
  if dtype is not None:
1418
+ X = X.to(dtype=dtype) # unchanged shape
1419
 
1420
  if full_embeddings:
1421
  if X.ndim == 4:
1422
  values = [
1423
+ X_i[:, M_i, :].detach().cpu() # (n_states, r_i, d)
1424
+ for X_i, M_i in zip(X, M, strict=True)
1425
  ]
1426
  else:
1427
+ values = [
1428
+ X_i[M_i].detach().cpu() # (r_i, d)
1429
+ for X_i, M_i in zip(X, M, strict=True)
1430
+ ]
1431
  else:
1432
  if pooler is None:
1433
  raise RuntimeError(
1434
  "Pooled embedding output was requested without an initialized pooler."
1435
  )
1436
+ Y = pooler( # (b, n_poolers * d)
1437
  X,
1438
  M,
1439
  attentions=batch.attentions,
1440
  attention_backend=attention_backend,
1441
  )
1442
  pool_slices = pooler.output_slices(X.shape[-1])
1443
+ values = list(Y.detach().cpu().unbind(0)) # each: (n_poolers * d,)
1444
  for position, record, value in zip(
1445
  batch_positions, batch_records, values, strict=True
1446
  ):
fastplms/embeddings/storage.py CHANGED
@@ -7,14 +7,13 @@ import io
7
  import json
8
  import sqlite3
9
  import struct
 
 
10
  from bisect import bisect_right
11
  from collections.abc import Iterable, Iterator, Sequence
12
  from pathlib import Path
13
  from typing import Any, cast, overload
14
  from uuid import uuid4
15
-
16
- import numpy as np
17
- import torch
18
  from torch import Tensor
19
 
20
  from .types import (
@@ -23,6 +22,7 @@ from .types import (
23
  LazyTensorReference,
24
  )
25
 
 
26
  _DTYPE_NAMES: dict[torch.dtype, str] = {
27
  torch.float16: "float16",
28
  torch.bfloat16: "bfloat16",
@@ -80,22 +80,24 @@ def _persistent_metadata(
80
  def _tensor_bytes(X: Tensor) -> bytes:
81
  """Return the exact contiguous byte representation of X."""
82
 
83
- X = X.detach().cpu().contiguous()
 
84
  return X.view(torch.uint8).numpy().tobytes()
85
 
86
 
87
  def _bounded_tensor_chunks(X: Tensor, max_bytes: int) -> Iterator[Tensor]:
88
  """Yield row-major CPU chunks without materializing one full byte string."""
89
 
90
- flattened = X.detach().to(device="cpu").reshape(-1)
 
91
  if flattened.numel() == 0:
92
  return
93
  chunk_elements = max(1, max_bytes // flattened.element_size())
94
  for start in range(0, flattened.numel(), chunk_elements):
95
- chunk = flattened[start : start + chunk_elements]
96
  if chunk.stride(0) != 1:
97
- chunk = chunk.clone(memory_format=torch.contiguous_format)
98
- yield chunk
99
 
100
 
101
  def _tensor_hash_chunks(X: Tensor) -> Iterator[bytes]:
@@ -136,9 +138,9 @@ def _decode_tensor(dtype_name: str, shape_json: str, data: bytes) -> Tensor:
136
  raise ValueError(f"Unsupported stored dtype {dtype_name!r}.") from error
137
  shape = tuple(json.loads(shape_json))
138
  # uint8 is used only as a byte-level carrier, preserving BF16 bits exactly.
139
- byte_array = np.frombuffer(data, dtype=np.uint8).copy()
140
- X = torch.from_numpy(byte_array).view(dtype)
141
- return X.reshape(shape).clone()
142
 
143
 
144
  def _index_path(path: str | Path) -> Path:
@@ -651,7 +653,7 @@ class SafetensorsStreamWriter:
651
 
652
  for record in records:
653
  position = self._record_count + len(self._pending)
654
- tensor = record.load_tensor().detach().cpu().contiguous()
655
  if tensor.dtype not in _DTYPE_NAMES:
656
  raise TypeError(f"Unsupported tensor dtype {tensor.dtype}.")
657
  nbytes = tensor.numel() * tensor.element_size()
@@ -949,7 +951,7 @@ def save_sqlite_result(result: EmbeddingResult, path: str | Path) -> EmbeddingRe
949
  (run_id, metadata_json),
950
  )
951
  for position, record in enumerate(result):
952
- X = record.load_tensor().detach().cpu().contiguous()
953
  dtype_name, shape_json, data = _encode_tensor(X)
954
  digest = tensor_sha256(X)
955
  connection.execute(
@@ -1053,7 +1055,7 @@ def append_sqlite_records(
1053
  )
1054
  for offset, record in enumerate(records):
1055
  position = start_position + offset
1056
- X = record.load_tensor().detach().cpu().contiguous()
1057
  dtype_name, shape_json, data = _encode_tensor(X)
1058
  digest = tensor_sha256(X)
1059
  connection.execute(
@@ -1414,6 +1416,7 @@ def load_legacy_pth(path: str | Path, *, allow_unsafe_pickle: bool = False) -> E
1414
  raise ValueError("A legacy .pth embedding file must contain a mapping.")
1415
  records: list[EmbeddingRecord] = []
1416
  for position, (sequence, X) in enumerate(payload.items()):
 
1417
  if not isinstance(sequence, str) or not isinstance(X, Tensor):
1418
  raise ValueError("Legacy embedding mappings must use str keys and Tensor values.")
1419
  records.append(EmbeddingRecord(str(position), sequence, X.detach().cpu()))
@@ -1450,8 +1453,10 @@ def _decode_legacy_sqlite_blob(
1450
  expected = int(np.prod(shape, dtype=np.int64)) * numpy_dtype.itemsize
1451
  if len(data) - offset != expected:
1452
  raise ValueError("Legacy compact embedding payload length does not match shape.")
1453
- array = np.frombuffer(data, dtype=numpy_dtype, offset=offset).copy().reshape(shape)
1454
- return torch.from_numpy(array).to(dtype=target_dtype)
 
 
1455
 
1456
  try:
1457
  loaded = torch.load(io.BytesIO(data), map_location="cpu", weights_only=True)
@@ -1470,11 +1475,13 @@ def _decode_legacy_sqlite_blob(
1470
  raise ValueError(
1471
  "Legacy raw FP32 payload length does not match fallback_shape."
1472
  ) from safe_error
1473
- array = np.frombuffer(data, dtype=np.float32).copy().reshape(fallback_shape)
1474
- return torch.from_numpy(array)
 
 
1475
  if not isinstance(loaded, Tensor):
1476
  raise ValueError("Legacy serialized embedding payload must contain one tensor.")
1477
- return loaded.detach().cpu()
1478
 
1479
 
1480
  def convert_legacy_sqlite(
 
7
  import json
8
  import sqlite3
9
  import struct
10
+ import numpy as np
11
+ import torch
12
  from bisect import bisect_right
13
  from collections.abc import Iterable, Iterator, Sequence
14
  from pathlib import Path
15
  from typing import Any, cast, overload
16
  from uuid import uuid4
 
 
 
17
  from torch import Tensor
18
 
19
  from .types import (
 
22
  LazyTensorReference,
23
  )
24
 
25
+
26
  _DTYPE_NAMES: dict[torch.dtype, str] = {
27
  torch.float16: "float16",
28
  torch.bfloat16: "bfloat16",
 
80
  def _tensor_bytes(X: Tensor) -> bytes:
81
  """Return the exact contiguous byte representation of X."""
82
 
83
+ # X: (...)
84
+ X = X.detach().cpu().contiguous() # (...)
85
  return X.view(torch.uint8).numpy().tobytes()
86
 
87
 
88
  def _bounded_tensor_chunks(X: Tensor, max_bytes: int) -> Iterator[Tensor]:
89
  """Yield row-major CPU chunks without materializing one full byte string."""
90
 
91
+ # X: (...)
92
+ flattened = X.detach().to(device="cpu").reshape(-1) # (n,)
93
  if flattened.numel() == 0:
94
  return
95
  chunk_elements = max(1, max_bytes // flattened.element_size())
96
  for start in range(0, flattened.numel(), chunk_elements):
97
+ chunk = flattened[start : start + chunk_elements] # (n_chunk,)
98
  if chunk.stride(0) != 1:
99
+ chunk = chunk.clone(memory_format=torch.contiguous_format) # (n_chunk,)
100
+ yield chunk # (n_chunk,)
101
 
102
 
103
  def _tensor_hash_chunks(X: Tensor) -> Iterator[bytes]:
 
138
  raise ValueError(f"Unsupported stored dtype {dtype_name!r}.") from error
139
  shape = tuple(json.loads(shape_json))
140
  # uint8 is used only as a byte-level carrier, preserving BF16 bits exactly.
141
+ byte_array = np.frombuffer(data, dtype=np.uint8).copy() # (n_bytes,)
142
+ X = torch.from_numpy(byte_array).view(dtype) # (n_elements,)
143
+ return X.reshape(shape).clone() # shape
144
 
145
 
146
  def _index_path(path: str | Path) -> Path:
 
653
 
654
  for record in records:
655
  position = self._record_count + len(self._pending)
656
+ tensor = record.load_tensor().detach().cpu().contiguous() # (...)
657
  if tensor.dtype not in _DTYPE_NAMES:
658
  raise TypeError(f"Unsupported tensor dtype {tensor.dtype}.")
659
  nbytes = tensor.numel() * tensor.element_size()
 
951
  (run_id, metadata_json),
952
  )
953
  for position, record in enumerate(result):
954
+ X = record.load_tensor().detach().cpu().contiguous() # (...)
955
  dtype_name, shape_json, data = _encode_tensor(X)
956
  digest = tensor_sha256(X)
957
  connection.execute(
 
1055
  )
1056
  for offset, record in enumerate(records):
1057
  position = start_position + offset
1058
+ X = record.load_tensor().detach().cpu().contiguous() # (...)
1059
  dtype_name, shape_json, data = _encode_tensor(X)
1060
  digest = tensor_sha256(X)
1061
  connection.execute(
 
1416
  raise ValueError("A legacy .pth embedding file must contain a mapping.")
1417
  records: list[EmbeddingRecord] = []
1418
  for position, (sequence, X) in enumerate(payload.items()):
1419
+ # X: (...)
1420
  if not isinstance(sequence, str) or not isinstance(X, Tensor):
1421
  raise ValueError("Legacy embedding mappings must use str keys and Tensor values.")
1422
  records.append(EmbeddingRecord(str(position), sequence, X.detach().cpu()))
 
1453
  expected = int(np.prod(shape, dtype=np.int64)) * numpy_dtype.itemsize
1454
  if len(data) - offset != expected:
1455
  raise ValueError("Legacy compact embedding payload length does not match shape.")
1456
+ array = ( # shape
1457
+ np.frombuffer(data, dtype=numpy_dtype, offset=offset).copy().reshape(shape)
1458
+ )
1459
+ return torch.from_numpy(array).to(dtype=target_dtype) # shape
1460
 
1461
  try:
1462
  loaded = torch.load(io.BytesIO(data), map_location="cpu", weights_only=True)
 
1475
  raise ValueError(
1476
  "Legacy raw FP32 payload length does not match fallback_shape."
1477
  ) from safe_error
1478
+ array = np.frombuffer(data, dtype=np.float32).copy().reshape( # fallback_shape
1479
+ fallback_shape
1480
+ )
1481
+ return torch.from_numpy(array) # fallback_shape
1482
  if not isinstance(loaded, Tensor):
1483
  raise ValueError("Legacy serialized embedding payload must contain one tensor.")
1484
+ return loaded.detach().cpu() # (...)
1485
 
1486
 
1487
  def convert_legacy_sqlite(
fastplms/embeddings/types.py CHANGED
@@ -5,7 +5,6 @@ from __future__ import annotations
5
  from collections.abc import Callable, Iterator, Mapping, Sequence
6
  from dataclasses import dataclass, field
7
  from typing import Any, Literal, overload
8
-
9
  from torch import Tensor
10
 
11
 
@@ -39,7 +38,7 @@ class LazyTensorReference:
39
 
40
  if not isinstance(verify, bool):
41
  raise TypeError("verify must be a boolean.")
42
- X = self._loader()
43
  if not isinstance(X, Tensor):
44
  raise TypeError(f"Stored tensor loader for {self.key!r} must return a Tensor.")
45
  if tuple(X.shape) != self.shape:
@@ -57,7 +56,7 @@ class LazyTensorReference:
57
  digest = tensor_sha256(X)
58
  if digest != self.sha256:
59
  raise ValueError(f"Stored tensor {self.key!r} failed SHA-256 verification.")
60
- return X
61
 
62
 
63
  TensorValue = Tensor | LazyTensorReference
@@ -85,8 +84,8 @@ class EmbeddingRecord:
85
  if not isinstance(verify, bool):
86
  raise TypeError("verify must be a boolean.")
87
  if isinstance(self.tensor, LazyTensorReference):
88
- return self.tensor.load(verify=verify)
89
- return self.tensor
90
 
91
 
92
  class EmbeddingResult(Sequence[EmbeddingRecord]):
 
5
  from collections.abc import Callable, Iterator, Mapping, Sequence
6
  from dataclasses import dataclass, field
7
  from typing import Any, Literal, overload
 
8
  from torch import Tensor
9
 
10
 
 
38
 
39
  if not isinstance(verify, bool):
40
  raise TypeError("verify must be a boolean.")
41
+ X = self._loader() # self.shape
42
  if not isinstance(X, Tensor):
43
  raise TypeError(f"Stored tensor loader for {self.key!r} must return a Tensor.")
44
  if tuple(X.shape) != self.shape:
 
56
  digest = tensor_sha256(X)
57
  if digest != self.sha256:
58
  raise ValueError(f"Stored tensor {self.key!r} failed SHA-256 verification.")
59
+ return X # self.shape
60
 
61
 
62
  TensorValue = Tensor | LazyTensorReference
 
84
  if not isinstance(verify, bool):
85
  raise TypeError("verify must be a boolean.")
86
  if isinstance(self.tensor, LazyTensorReference):
87
+ return self.tensor.load(verify=verify) # (...)
88
+ return self.tensor # (...)
89
 
90
 
91
  class EmbeddingResult(Sequence[EmbeddingRecord]):
fastplms/models/__init__.py CHANGED
@@ -7,4 +7,5 @@ tokenizers, compile kernels, or initialize an accelerator runtime.
7
 
8
  from __future__ import annotations
9
 
 
10
  __all__: tuple[str, ...] = ()
 
7
 
8
  from __future__ import annotations
9
 
10
+
11
  __all__: tuple[str, ...] = ()
fastplms/models/_esm_rotary.py CHANGED
@@ -15,8 +15,9 @@ from torch import nn
15
  def _rotate_half(tensor: torch.Tensor) -> torch.Tensor:
16
  """Rotate the final dimension of X by 90 degrees in paired subspaces."""
17
 
18
- first, second = tensor.chunk(2, dim=-1)
19
- return torch.cat((-second, first), dim=-1)
 
20
 
21
 
22
  def apply_rotary_pos_emb(
@@ -26,9 +27,10 @@ def apply_rotary_pos_emb(
26
  ) -> torch.Tensor:
27
  """Apply cached rotary factors to X with shape ``(b, h, l, d)``."""
28
 
29
- cos = cos[:, :, : tensor.shape[-2], :]
30
- sin = sin[:, :, : tensor.shape[-2], :]
31
- return tensor * cos + _rotate_half(tensor) * sin
 
32
 
33
 
34
  class RotaryEmbedding(nn.Module):
@@ -38,7 +40,9 @@ class RotaryEmbedding(nn.Module):
38
 
39
  def __init__(self, dim: int) -> None:
40
  super().__init__()
41
- frequencies = 1.0 / (10_000 ** (torch.arange(0, dim, 2, dtype=torch.int64).float() / dim))
 
 
42
  # Keep this persistent to preserve the historical checkpoint schema.
43
  self.register_buffer("inv_freq", frequencies)
44
  self._seq_len_cached: int | None = None
@@ -50,6 +54,7 @@ class RotaryEmbedding(nn.Module):
50
  tensor: torch.Tensor,
51
  seq_dimension: int = 2,
52
  ) -> tuple[torch.Tensor, torch.Tensor]:
 
53
  seq_len = tensor.shape[seq_dimension]
54
  cache_stale = (
55
  self._cos_cached is None
@@ -59,23 +64,29 @@ class RotaryEmbedding(nn.Module):
59
  )
60
  if cache_stale:
61
  self._seq_len_cached = seq_len
62
- positions = torch.arange(seq_len, device=tensor.device).type_as(self.inv_freq)
63
- angles = torch.outer(positions, self.inv_freq)
64
- angles = torch.cat((angles, angles), dim=-1).to(tensor.device)
65
- self._cos_cached = angles.cos()[None, None, :, :]
66
- self._sin_cached = angles.sin()[None, None, :, :]
 
 
67
 
68
  assert self._cos_cached is not None
69
  assert self._sin_cached is not None
70
- return self._cos_cached, self._sin_cached
71
 
72
  def forward(
73
  self,
74
  query: torch.Tensor,
75
  key: torch.Tensor,
76
  ) -> tuple[torch.Tensor, torch.Tensor]:
77
- cos, sin = self._update_cos_sin_tables(key, seq_dimension=-2)
 
 
 
 
78
  return (
79
- apply_rotary_pos_emb(query, cos, sin).to(dtype=query.dtype),
80
- apply_rotary_pos_emb(key, cos, sin).to(dtype=key.dtype),
81
  )
 
15
  def _rotate_half(tensor: torch.Tensor) -> torch.Tensor:
16
  """Rotate the final dimension of X by 90 degrees in paired subspaces."""
17
 
18
+ # tensor: (..., d)
19
+ first, second = tensor.chunk(2, dim=-1) # (..., d / 2), (..., d / 2)
20
+ return torch.cat((-second, first), dim=-1) # (..., d)
21
 
22
 
23
  def apply_rotary_pos_emb(
 
27
  ) -> torch.Tensor:
28
  """Apply cached rotary factors to X with shape ``(b, h, l, d)``."""
29
 
30
+ # tensor: (b, h, l, d); cos, sin: (1, 1, l_cache, d)
31
+ cos = cos[:, :, : tensor.shape[-2], :] # (1, 1, l, d)
32
+ sin = sin[:, :, : tensor.shape[-2], :] # (1, 1, l, d)
33
+ return tensor * cos + _rotate_half(tensor) * sin # (b, h, l, d)
34
 
35
 
36
  class RotaryEmbedding(nn.Module):
 
40
 
41
  def __init__(self, dim: int) -> None:
42
  super().__init__()
43
+ frequencies = 1.0 / ( # (d / 2,)
44
+ 10_000 ** (torch.arange(0, dim, 2, dtype=torch.int64).float() / dim)
45
+ )
46
  # Keep this persistent to preserve the historical checkpoint schema.
47
  self.register_buffer("inv_freq", frequencies)
48
  self._seq_len_cached: int | None = None
 
54
  tensor: torch.Tensor,
55
  seq_dimension: int = 2,
56
  ) -> tuple[torch.Tensor, torch.Tensor]:
57
+ # tensor: (..., l, d)
58
  seq_len = tensor.shape[seq_dimension]
59
  cache_stale = (
60
  self._cos_cached is None
 
64
  )
65
  if cache_stale:
66
  self._seq_len_cached = seq_len
67
+ positions = torch.arange(seq_len, device=tensor.device).type_as( # (l,)
68
+ self.inv_freq
69
+ )
70
+ angles = torch.outer(positions, self.inv_freq) # (l, d / 2)
71
+ angles = torch.cat((angles, angles), dim=-1).to(tensor.device) # (l, d)
72
+ self._cos_cached = angles.cos()[None, None, :, :] # (1, 1, l, d)
73
+ self._sin_cached = angles.sin()[None, None, :, :] # (1, 1, l, d)
74
 
75
  assert self._cos_cached is not None
76
  assert self._sin_cached is not None
77
+ return self._cos_cached, self._sin_cached # (1, 1, l, d), (1, 1, l, d)
78
 
79
  def forward(
80
  self,
81
  query: torch.Tensor,
82
  key: torch.Tensor,
83
  ) -> tuple[torch.Tensor, torch.Tensor]:
84
+ # query, key: (b, h, l, d)
85
+ cos, sin = self._update_cos_sin_tables( # (1, 1, l, d), (1, 1, l, d)
86
+ key,
87
+ seq_dimension=-2,
88
+ )
89
  return (
90
+ apply_rotary_pos_emb(query, cos, sin).to(dtype=query.dtype), # (b, h, l, d)
91
+ apply_rotary_pos_emb(key, cos, sin).to(dtype=key.dtype), # (b, h, l, d)
92
  )
fastplms/models/esmfold/modeling_fast_esmfold.py CHANGED
@@ -14,12 +14,11 @@ depend on the pinned fair-esm or OpenFold parity repositories.
14
 
15
  from __future__ import annotations
16
 
 
 
17
  from contextvars import ContextVar
18
  from dataclasses import dataclass
19
  from typing import Any
20
-
21
- import torch
22
- import torch.nn as nn
23
  from einops import rearrange
24
  from torch.nn import functional as F
25
  from transformers.modeling_outputs import ModelOutput
@@ -38,6 +37,7 @@ from transformers.models.esm.openfold_utils import residue_constants
38
 
39
  from fastplms.models._esm_rotary import RotaryEmbedding
40
 
 
41
  # Hub composite artifacts define these shared names earlier in the assembled file.
42
  try:
43
  from fastplms.attention import (
@@ -70,11 +70,6 @@ except ModuleNotFoundError as error:
70
  # Legacy flat Hub composites define every shared symbol above this block.
71
 
72
 
73
- # =============================================================================
74
- # Output Dataclass
75
- # =============================================================================
76
-
77
-
78
  @dataclass
79
  class FastEsmEncoderOutput(ModelOutput):
80
  last_hidden_state: torch.Tensor | None = None
@@ -137,12 +132,13 @@ def _align_internal_esm_attentions(
137
  ) -> tuple[torch.Tensor, ...]:
138
  """Remove internal BOS/EOS positions while preserving public padding slots."""
139
 
 
140
  batch_size, sequence_length = residue_mask.shape
141
- public_positions = torch.arange(sequence_length, device=residue_mask.device)
142
- valid_lengths = residue_mask.to(dtype=torch.int64).sum(dim=-1, keepdim=True)
143
  # Internal layout is BOS, compact biological residues, EOS, then padding.
144
  # Public padding positions therefore advance by two rather than one.
145
- internal_positions = public_positions.unsqueeze(0) + 1
146
  internal_positions = internal_positions + (
147
  public_positions.unsqueeze(0) >= valid_lengths
148
  ).to(dtype=torch.int64)
@@ -175,13 +171,8 @@ def _align_internal_esm_attentions(
175
  return tuple(aligned)
176
 
177
 
178
- # =============================================================================
179
- # FastESM2 Attention Layers (multi-backend: SDPA, Flash, Flex)
180
- # =============================================================================
181
-
182
-
183
  class EsmSelfAttention(nn.Module):
184
- def __init__(self, config, position_embedding_type: str | None = None):
185
  super().__init__()
186
  if config.hidden_size % config.num_attention_heads != 0:
187
  raise ValueError(
@@ -213,11 +204,12 @@ class EsmSelfAttention(nn.Module):
213
  flex_block_mask: BlockMask | None = None,
214
  output_attentions: bool = False,
215
  ) -> tuple[torch.Tensor, torch.Tensor | None]:
 
216
  batch_size, seq_length = hidden_states.shape[:-1]
217
  hidden_shape = (batch_size, seq_length, -1, self.attention_head_size)
218
- query_heads = self.query(hidden_states).view(hidden_shape).transpose(1, 2)
219
- key_heads = self.key(hidden_states).view(hidden_shape).transpose(1, 2)
220
- value_heads = self.value(hidden_states).view(hidden_shape).transpose(1, 2)
221
 
222
  query_heads = query_heads * self.scale
223
 
@@ -285,14 +277,17 @@ class EsmSelfAttention(nn.Module):
285
  value_heads: torch.Tensor,
286
  attention_mask_4d: torch.Tensor | None = None,
287
  ) -> tuple[torch.Tensor, torch.Tensor]:
288
- attn_weights = torch.matmul(query_heads, key_heads.transpose(-1, -2))
 
 
 
289
  if attention_mask_4d is not None:
290
  attn_weights = attn_weights.masked_fill(attention_mask_4d.logical_not(), float("-inf"))
291
  attn_weights = F.softmax(attn_weights, dim=-1)
292
  if self.dropout_prob > 0 and self.training:
293
  attn_weights = F.dropout(attn_weights, p=self.dropout_prob, training=self.training)
294
- context_heads = torch.matmul(attn_weights, value_heads)
295
- attn_output = rearrange(context_heads, "b h s d -> b s (h d)")
296
  return attn_output, attn_weights
297
 
298
  def _kernels_flash_attn(
@@ -358,7 +353,7 @@ class EsmSelfAttention(nn.Module):
358
 
359
 
360
  class EsmAttention(nn.Module):
361
- def __init__(self, config):
362
  super().__init__()
363
  self.self = EsmSelfAttention(config)
364
  self.output = EsmSelfOutput(config)
@@ -385,7 +380,7 @@ class EsmAttention(nn.Module):
385
 
386
 
387
  class EsmLayer(nn.Module):
388
- def __init__(self, config):
389
  super().__init__()
390
  self.attention = EsmAttention(config)
391
  self.intermediate = EsmIntermediate(config)
@@ -417,7 +412,7 @@ class EsmLayer(nn.Module):
417
 
418
 
419
  class FastEsmEncoder(nn.Module):
420
- def __init__(self, config):
421
  super().__init__()
422
  self.config = config
423
  self.attention_backend = resolve_attention_backend(config.attn_backend)
@@ -431,6 +426,7 @@ class FastEsmEncoder(nn.Module):
431
  output_hidden_states: bool = False,
432
  output_attentions: bool = False,
433
  ) -> FastEsmEncoderOutput:
 
434
  all_hidden_states = () if output_hidden_states else None
435
  all_attentions = () if output_attentions else None
436
 
@@ -476,11 +472,6 @@ class FastEsmEncoder(nn.Module):
476
  )
477
 
478
 
479
- # =============================================================================
480
- # FastESM Backbone (replaces EsmModel inside ESMFold)
481
- # =============================================================================
482
-
483
-
484
  class FastEsmBackbone(nn.Module):
485
  """FastESM2 backbone with multi-backend attention. Drop-in replacement for
486
  transformers.EsmModel inside EsmForProteinFolding.
@@ -489,7 +480,7 @@ class FastEsmBackbone(nn.Module):
489
  masked-LM head are therefore omitted from this structure-only backbone.
490
  """
491
 
492
- def __init__(self, config):
493
  super().__init__()
494
  self.config = config
495
  self.embeddings = EsmEmbeddings(config)
@@ -537,11 +528,6 @@ class FastEsmBackbone(nn.Module):
537
  return output if return_dict else output.to_tuple()
538
 
539
 
540
- # =============================================================================
541
- # FastEsmFoldConfig
542
- # =============================================================================
543
-
544
-
545
  class FastEsmFoldConfig(EsmConfig):
546
  model_type = "fast_esmfold"
547
 
@@ -554,11 +540,6 @@ class FastEsmFoldConfig(EsmConfig):
554
  self.attn_backend = attn_backend
555
 
556
 
557
- # =============================================================================
558
- # FastEsmForProteinFolding
559
- # =============================================================================
560
-
561
-
562
  class FastEsmForProteinFolding(FastPLMsAttentionMixin, EsmForProteinFolding):
563
  """ESMFold with FastESM2 attention backends.
564
 
@@ -576,7 +557,7 @@ class FastEsmForProteinFolding(FastPLMsAttentionMixin, EsmForProteinFolding):
576
  _supports_flash_attn_3 = False
577
  _fastplms_attention_implementations = ("eager", "sdpa", "flex_attention")
578
 
579
- def __init__(self, config: FastEsmFoldConfig):
580
  super().__init__(config)
581
 
582
  # Replace the standard ESM2 backbone with the multi-backend FastESM2
 
14
 
15
  from __future__ import annotations
16
 
17
+ import torch
18
+ import torch.nn as nn
19
  from contextvars import ContextVar
20
  from dataclasses import dataclass
21
  from typing import Any
 
 
 
22
  from einops import rearrange
23
  from torch.nn import functional as F
24
  from transformers.modeling_outputs import ModelOutput
 
37
 
38
  from fastplms.models._esm_rotary import RotaryEmbedding
39
 
40
+
41
  # Hub composite artifacts define these shared names earlier in the assembled file.
42
  try:
43
  from fastplms.attention import (
 
70
  # Legacy flat Hub composites define every shared symbol above this block.
71
 
72
 
 
 
 
 
 
73
  @dataclass
74
  class FastEsmEncoderOutput(ModelOutput):
75
  last_hidden_state: torch.Tensor | None = None
 
132
  ) -> tuple[torch.Tensor, ...]:
133
  """Remove internal BOS/EOS positions while preserving public padding slots."""
134
 
135
+ # attention: (b, h, l + 2, l + 2); residue_mask: (b, l)
136
  batch_size, sequence_length = residue_mask.shape
137
+ public_positions = torch.arange(sequence_length, device=residue_mask.device) # (l,)
138
+ valid_lengths = residue_mask.to(dtype=torch.int64).sum(dim=-1, keepdim=True) # (b, 1)
139
  # Internal layout is BOS, compact biological residues, EOS, then padding.
140
  # Public padding positions therefore advance by two rather than one.
141
+ internal_positions = public_positions.unsqueeze(0) + 1 # (1, l)
142
  internal_positions = internal_positions + (
143
  public_positions.unsqueeze(0) >= valid_lengths
144
  ).to(dtype=torch.int64)
 
171
  return tuple(aligned)
172
 
173
 
 
 
 
 
 
174
  class EsmSelfAttention(nn.Module):
175
+ def __init__(self, config, position_embedding_type: str | None = None) -> None:
176
  super().__init__()
177
  if config.hidden_size % config.num_attention_heads != 0:
178
  raise ValueError(
 
204
  flex_block_mask: BlockMask | None = None,
205
  output_attentions: bool = False,
206
  ) -> tuple[torch.Tensor, torch.Tensor | None]:
207
+ # hidden_states: (b, l, d)
208
  batch_size, seq_length = hidden_states.shape[:-1]
209
  hidden_shape = (batch_size, seq_length, -1, self.attention_head_size)
210
+ query_heads = self.query(hidden_states).view(hidden_shape).transpose(1, 2) # (b, h, l, d_h)
211
+ key_heads = self.key(hidden_states).view(hidden_shape).transpose(1, 2) # (b, h, l, d_h)
212
+ value_heads = self.value(hidden_states).view(hidden_shape).transpose(1, 2) # (b, h, l, d_h)
213
 
214
  query_heads = query_heads * self.scale
215
 
 
277
  value_heads: torch.Tensor,
278
  attention_mask_4d: torch.Tensor | None = None,
279
  ) -> tuple[torch.Tensor, torch.Tensor]:
280
+ # query_heads, key_heads, value_heads: (b, h, l, d_h)
281
+ attn_weights = torch.matmul(
282
+ query_heads, key_heads.transpose(-1, -2)
283
+ ) # (b, h, l, l)
284
  if attention_mask_4d is not None:
285
  attn_weights = attn_weights.masked_fill(attention_mask_4d.logical_not(), float("-inf"))
286
  attn_weights = F.softmax(attn_weights, dim=-1)
287
  if self.dropout_prob > 0 and self.training:
288
  attn_weights = F.dropout(attn_weights, p=self.dropout_prob, training=self.training)
289
+ context_heads = torch.matmul(attn_weights, value_heads) # (b, h, l, d_h)
290
+ attn_output = rearrange(context_heads, "b h s d -> b s (h d)") # (b, l, d)
291
  return attn_output, attn_weights
292
 
293
  def _kernels_flash_attn(
 
353
 
354
 
355
  class EsmAttention(nn.Module):
356
+ def __init__(self, config) -> None:
357
  super().__init__()
358
  self.self = EsmSelfAttention(config)
359
  self.output = EsmSelfOutput(config)
 
380
 
381
 
382
  class EsmLayer(nn.Module):
383
+ def __init__(self, config) -> None:
384
  super().__init__()
385
  self.attention = EsmAttention(config)
386
  self.intermediate = EsmIntermediate(config)
 
412
 
413
 
414
  class FastEsmEncoder(nn.Module):
415
+ def __init__(self, config) -> None:
416
  super().__init__()
417
  self.config = config
418
  self.attention_backend = resolve_attention_backend(config.attn_backend)
 
426
  output_hidden_states: bool = False,
427
  output_attentions: bool = False,
428
  ) -> FastEsmEncoderOutput:
429
+ # hidden_states: (b, l, d); attention_mask: (b, l)
430
  all_hidden_states = () if output_hidden_states else None
431
  all_attentions = () if output_attentions else None
432
 
 
472
  )
473
 
474
 
 
 
 
 
 
475
  class FastEsmBackbone(nn.Module):
476
  """FastESM2 backbone with multi-backend attention. Drop-in replacement for
477
  transformers.EsmModel inside EsmForProteinFolding.
 
480
  masked-LM head are therefore omitted from this structure-only backbone.
481
  """
482
 
483
+ def __init__(self, config) -> None:
484
  super().__init__()
485
  self.config = config
486
  self.embeddings = EsmEmbeddings(config)
 
528
  return output if return_dict else output.to_tuple()
529
 
530
 
 
 
 
 
 
531
  class FastEsmFoldConfig(EsmConfig):
532
  model_type = "fast_esmfold"
533
 
 
540
  self.attn_backend = attn_backend
541
 
542
 
 
 
 
 
 
543
  class FastEsmForProteinFolding(FastPLMsAttentionMixin, EsmForProteinFolding):
544
  """ESMFold with FastESM2 attention backends.
545
 
 
557
  _supports_flash_attn_3 = False
558
  _fastplms_attention_implementations = ("eager", "sdpa", "flex_attention")
559
 
560
+ def __init__(self, config: FastEsmFoldConfig) -> None:
561
  super().__init__(config)
562
 
563
  # Replace the standard ESM2 backbone with the multi-backend FastESM2
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 != "e38948c5e408464e3174518d3c907f85ee3ea37c6d5fcbe77da921d7adbda818":
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,8 +179,8 @@ def _install_runtime():
223
  return package
224
 
225
  _install_runtime()
226
- _module_225 = _import_without_bytecode("fastplms.models.esmfold.modeling_fast_esmfold")
227
- FastEsmFoldConfig = _module_225.FastEsmFoldConfig
228
  FastEsmFoldConfig.__module__ = __name__
229
- FastEsmForProteinFolding = _module_225.FastEsmForProteinFolding
230
  FastEsmForProteinFolding.__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 != "899b79836910097d523aa944c1342d227221c535365e90dfd15ba72dabbb1875":
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.esmfold.modeling_fast_esmfold")
183
+ FastEsmFoldConfig = _module_181.FastEsmFoldConfig
184
  FastEsmFoldConfig.__module__ = __name__
185
+ FastEsmForProteinFolding = _module_181.FastEsmForProteinFolding
186
  FastEsmForProteinFolding.__module__ = __name__
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Direct runtime dependencies for Synthyra/FastESMFold.
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
+ accelerate>=1.10,<2
12
+ biopython>=1.85,<2
13
+ biotite>=1.4,<2
14
+ brotli>=1.1,<2
15
+ msgpack>=1.1,<2
16
+ msgpack-numpy>=0.4.8,<1
17
+ omegaconf>=2.3,<3
18
+ rdkit>=2025.9,<2027
19
+ scipy>=1.15,<2
20
+ zstandard>=0.23,<1
runtime-attestation.json CHANGED
@@ -6,41 +6,42 @@
6
  "LICENSES/openfold/LICENSE": "sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
7
  "LICENSES/openfold/MODIFICATIONS.md": "sha256:fd6f0aa1086a0c996cf967b326d18e965660cda0ad5c7f36a3474a8490720da3",
8
  "LICENSES/openfold/PROVENANCE.md": "sha256:48c903db43a217a3126afaefbac60b7ddac7efda2dfcc0cbff0bffc7d6c30081",
9
- "README.md": "sha256:578d1eca99c24e5e6f0ad6721fe344e4a0e84992030cc9477ebb767b18532c82",
10
  "THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
11
- "config.json": "sha256:d32b8505ccd23422e3b67dabbcdd2d7c4b505cd1386c07342036b57cfd6cfdce",
12
- "fastplms/__init__.py": "sha256:4fb3196022ca8ec699d59d09bdbc5f0184195552b773698ab9b061fe3cd7df12",
13
- "fastplms/attention/__init__.py": "sha256:f60b9fecfb4bcb37a4e7c26dc2f752b9035f9cbad627b4a84213f3a92ec88f7d",
14
- "fastplms/attention/_core.py": "sha256:8f7ec5b65bd8b6c6fa4951d50d1c0e499abf03ae00914794b51fc410201e3e33",
15
- "fastplms/attention/_kernel_lock.py": "sha256:85d8521a2af5f94fad3948af3814db0c866c414ee4d43df797b9bd6f980e947b",
16
- "fastplms/attention/interfaces.py": "sha256:1c6f06a8e411e0f9bf6d230522205c93ae46ea58864006fbb892aa05e5ca5749",
17
- "fastplms/embeddings/__init__.py": "sha256:47ff8cdf682d44037dd9edab133e2e60675d60786bd5cf0bffd1998f31985555",
18
- "fastplms/embeddings/pooling.py": "sha256:a140266ed6b1cc344c8507edc5c6c4f2dce464c3db70ba4b16c7ac2ba2fad96e",
19
- "fastplms/embeddings/runner.py": "sha256:23ee4727a918d6d331f7a0f89b823d149f1a791f0c5586e3496d7b6eb2ce97e0",
20
- "fastplms/embeddings/storage.py": "sha256:3fbe2bab75092e5a4cadf4d27e4752181d597469a65a55db085ceef808ed418e",
21
- "fastplms/embeddings/types.py": "sha256:119718a20989d1ae5a60fabc0f5e98bdc172c5163b04db3d4554ac3956b30e52",
22
  "fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
23
- "fastplms/models/__init__.py": "sha256:5e48c2cb3877aa6f42f3b5411d53b16bba2e32827bbde634f47f174c5cb36f86",
24
- "fastplms/models/_esm_rotary.py": "sha256:4dcee8af74adf2cfcabeeaadf4915f1da5fb6c0672d583bf3ee5478ac1290805",
25
  "fastplms/models/esmfold/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
26
- "fastplms/models/esmfold/modeling_fast_esmfold.py": "sha256:8c1d353f3203d8e6bfea6637463cb38da9e18b13ad6f60c672f213f5af4c2194",
27
- "fastplms/registry.py": "sha256:afca271911b651a882345b74a58366494a1d784e4a48c8683a87f5508f4ba16e",
28
- "fastplms/runtime.py": "sha256:110018646d6f248cedab140a030c3065e1b062b61f6aff659c231e538614bc01",
29
- "fastplms_bundle.py": "sha256:b3d35ed70588b9cc5aeb3fc30828b6ca8b4280720a6564990fd63048ee300230",
30
- "modeling_fastplms.py": "sha256:bbeabe795fd09dc3e99804caa397a24b6ffcc03a92ec4ffc13f74572ecfeea52",
 
31
  "special_tokens_map.json": "sha256:5f89dfed5b1bcb31b03a93b608b8c7fc5ac8465f2f4b839f6f6f4aa87d683407",
32
  "tokenizer_config.json": "sha256:01218ac34c07219089c4c5de01c3196cc44bd0bb9cc08ae3777c1a6712ce2be6",
33
  "vocab.txt": "sha256:927f51c652e9367fc6a8403c1993df3633df51d79df602ce1c207bf68ad4d867"
34
  },
35
  "model_id": "esmfold",
36
  "redistributable": true,
37
- "release_tool_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
38
- "release_tool_sha256": "19b4247aba04a74de5069397518374ab2c3b9e0ce18ba9f36de34f0f4c5d1ceb",
39
- "runtime_bundle_sha256": "e38948c5e408464e3174518d3c907f85ee3ea37c6d5fcbe77da921d7adbda818",
40
- "runtime_revision": "95691a87c781f052f65b6ab3ca99dfce4ebb59c7",
41
  "schema_version": 2,
42
  "scope": "runtime-only",
43
- "source_tree_sha256": "6dbdf570cd69776d3f0239168f6388b979affda62c6ba4c78e2d9fac1d81bd6d",
44
  "weights": {
45
  "repo_id": "Synthyra/FastESMFold",
46
  "revision": "b88c8cb50d19b2cf7ab4fee4b0a61f5e02da7823"
 
6
  "LICENSES/openfold/LICENSE": "sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
7
  "LICENSES/openfold/MODIFICATIONS.md": "sha256:fd6f0aa1086a0c996cf967b326d18e965660cda0ad5c7f36a3474a8490720da3",
8
  "LICENSES/openfold/PROVENANCE.md": "sha256:48c903db43a217a3126afaefbac60b7ddac7efda2dfcc0cbff0bffc7d6c30081",
9
+ "README.md": "sha256:c9cb675e868ac51995140b645b3f1ebf7b1ba3e7f4ee588d688e591a842dd7cf",
10
  "THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
11
+ "config.json": "sha256:0cdbcea85df251b0562a08fcf43ddb7a93791702645b2cecbd5908cc29406c3b",
12
+ "fastplms/__init__.py": "sha256:8503e02debf24abc6d33c1913ff3eb9f245e63f7e62017e220d072c7393fdf2c",
13
+ "fastplms/attention/__init__.py": "sha256:ab3c0b6156968f418f3c97ea664959cb978f4137717b4362213bcfe25ab5e3d6",
14
+ "fastplms/attention/_core.py": "sha256:09d0f71a5ea2ad2138c9441f6a2f72bbf8e72729be41d8db737b163f36ce47da",
15
+ "fastplms/attention/_kernel_lock.py": "sha256:56455eb57cee9af08438eda52b775c9cff3cf994cadb9b116ce2ee10fd5f1801",
16
+ "fastplms/attention/interfaces.py": "sha256:6562a342961022fc2848d7004bd01fddaf8be8e08e782d0856e5c30a74232623",
17
+ "fastplms/embeddings/__init__.py": "sha256:cf0648de905a00ad16c0aece328eeb05f25739a4cf89b47b915e0f2b19c3ce26",
18
+ "fastplms/embeddings/pooling.py": "sha256:9bed8c466fce053333de9e25b04d1ab304f2954f403ffa0f5a3c749e8277b3ce",
19
+ "fastplms/embeddings/runner.py": "sha256:a7839763ad641a63d0e2671dcf34aa7b2a8114198950b64254188521b27453a8",
20
+ "fastplms/embeddings/storage.py": "sha256:98cf725d26766f32300f105345129ca6cf02ebd10628e746a7e71e107bc911a6",
21
+ "fastplms/embeddings/types.py": "sha256:c58f8c6a37a11a937c3f77b4e1617d5f213923d98ecaffddcdd283bd9dc42fe7",
22
  "fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
23
+ "fastplms/models/__init__.py": "sha256:d3dd084f5e8fafcf2d0fe4a26e4ab9284d3f9414b1f9a5bd86e308a406447c43",
24
+ "fastplms/models/_esm_rotary.py": "sha256:8e041bc4a056373e07fbf598e6acb6075f889d1bce12057715369804befb37ec",
25
  "fastplms/models/esmfold/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
26
+ "fastplms/models/esmfold/modeling_fast_esmfold.py": "sha256:b39881ba275b3d56a3d31cc808789d614bebdacf4016786137a1d8abb2a24d76",
27
+ "fastplms/registry.py": "sha256:cb37799dae5a4c0c780089ab2cca4cc56300c1c5f515b275892aa4e1c4b97eb6",
28
+ "fastplms/runtime.py": "sha256:9521c37fcd168bf9a4137277fb73672403ccf30d70d4845aa879fb22f42ddeb6",
29
+ "fastplms_bundle.py": "sha256:ac2a57bf1862e59a0ff64051142576c9265347c3c9f1eb6c8ba682911ca91995",
30
+ "modeling_fastplms.py": "sha256:f61068903f5f0f8e2148b2130be93d794fb563126cf1ef7bfb8d401bd2068aa6",
31
+ "requirements.txt": "sha256:0c1df6dc5a2f499d43b9d9e48a44df8917a5f3cb2e20fdd178ab80aa8f24e9af",
32
  "special_tokens_map.json": "sha256:5f89dfed5b1bcb31b03a93b608b8c7fc5ac8465f2f4b839f6f6f4aa87d683407",
33
  "tokenizer_config.json": "sha256:01218ac34c07219089c4c5de01c3196cc44bd0bb9cc08ae3777c1a6712ce2be6",
34
  "vocab.txt": "sha256:927f51c652e9367fc6a8403c1993df3633df51d79df602ce1c207bf68ad4d867"
35
  },
36
  "model_id": "esmfold",
37
  "redistributable": true,
38
+ "release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
39
+ "release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
40
+ "runtime_bundle_sha256": "899b79836910097d523aa944c1342d227221c535365e90dfd15ba72dabbb1875",
41
+ "runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
42
  "schema_version": 2,
43
  "scope": "runtime-only",
44
+ "source_tree_sha256": "45689da1c4bc32afb64352fe83809f9c506186de963e5db3968613d40b796644",
45
  "weights": {
46
  "repo_id": "Synthyra/FastESMFold",
47
  "revision": "b88c8cb50d19b2cf7ab4fee4b0a61f5e02da7823"