Instructions to use Synthyra/ESM2-3B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Synthyra/ESM2-3B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("fill-mask", model="Synthyra/ESM2-3B", trust_remote_code=True)# Load model directly from transformers import AutoModelForMaskedLM model = AutoModelForMaskedLM.from_pretrained("Synthyra/ESM2-3B", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Update FastPLMs runtime and model cards
Browse filesAdd-only FastPLMs files-only publication. Checkpoint weights and complete-artifact attestations are unchanged.
- README.md +142 -53
- config.json +5 -5
- fastplms/__init__.py +1 -0
- fastplms/attention/__init__.py +1 -0
- fastplms/attention/_core.py +68 -47
- fastplms/attention/_kernel_lock.py +17 -33
- fastplms/attention/interfaces.py +7 -7
- fastplms/embeddings/__init__.py +1 -0
- fastplms/embeddings/pooling.py +48 -40
- fastplms/embeddings/runner.py +60 -36
- fastplms/embeddings/storage.py +26 -19
- fastplms/embeddings/types.py +4 -5
- fastplms/models/__init__.py +1 -0
- fastplms/models/_esm_rotary.py +26 -15
- fastplms/models/esm2/modeling_fastesm.py +87 -68
- fastplms/models/ttt.py +97 -81
- fastplms/registry.py +45 -44
- fastplms/runtime.py +1 -0
- fastplms_bundle.py +0 -0
- modeling_fastplms.py +29 -73
- requirements.txt +11 -0
- runtime-attestation.json +26 -25
README.md
CHANGED
|
@@ -17,16 +17,35 @@ Supported Transformers entry points are `AutoConfig`, `AutoModel`,
|
|
| 17 |
`AutoModelForMaskedLM`, `AutoModelForSequenceClassification`,
|
| 18 |
`AutoModelForTokenClassification`.
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
## Install and platform requirements
|
| 21 |
|
| 22 |
-
Install the
|
| 23 |
|
| 24 |
```bash
|
| 25 |
-
python -m pip install \
|
| 26 |
-
"
|
| 27 |
```
|
| 28 |
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
| 30 |
access on first download. For an air-gapped run, first build the manifest-pinned
|
| 31 |
local artifact and use the offline form shown in the example.
|
| 32 |
|
|
@@ -39,23 +58,23 @@ model_id = "Synthyra/ESM2-3B"
|
|
| 39 |
model = AutoModel.from_pretrained(
|
| 40 |
model_id,
|
| 41 |
trust_remote_code=True,
|
|
|
|
| 42 |
).eval()
|
| 43 |
```
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
`flash_attention_3`.
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
For BF16 execution, this family uses FP32 parameters with CUDA BF16 autocast.
|
| 59 |
|
| 60 |
## Tokenization and forward inference
|
| 61 |
|
|
@@ -85,43 +104,119 @@ print(output.last_hidden_state.shape)
|
|
| 85 |
|
| 86 |
## Dataset embeddings
|
| 87 |
|
| 88 |
-
The shared embedding
|
| 89 |
-
|
| 90 |
-
FASTA path. Results preserve order and duplicate identifiers:
|
| 91 |
|
| 92 |
```python
|
| 93 |
-
|
| 94 |
["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"],
|
| 95 |
batch_size=2,
|
| 96 |
pooling=("mean", "std"),
|
| 97 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
```
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
-
|
|
|
|
| 110 |
|
| 111 |
```python
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
format="sqlite",
|
| 118 |
-
resume=True,
|
| 119 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
```
|
| 121 |
|
| 122 |
-
|
| 123 |
-
and
|
| 124 |
-
existing run.
|
| 125 |
|
| 126 |
## Masked language modeling and contacts
|
| 127 |
|
|
@@ -174,7 +269,7 @@ required.
|
|
| 174 |
- Precision policies: `default`
|
| 175 |
- BF16 execution: `fp32_parameters_autocast`
|
| 176 |
- Generation contract: `not_applicable`
|
| 177 |
-
-
|
| 178 |
- Weight publication allowed: `true`
|
| 179 |
- Weight license status: `resolved`
|
| 180 |
- Redistributable: `true`
|
|
@@ -185,28 +280,22 @@ required.
|
|
| 185 |
- FastPLMs weights: `Synthyra/ESM2-3B`
|
| 186 |
- Runtime revision: recorded separately in the built artifact and published commit
|
| 187 |
- Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
|
| 188 |
-
- Generator/schema version and complete/runtime-only attestations: recorded in `provenance.json`
|
| 189 |
- Official checkpoint: `facebook/esm2_t36_3B_UR50D`
|
| 190 |
- Artifact source: `fast`
|
| 191 |
- State transform: `esm2_hf_to_fastplms_v1`
|
| 192 |
-
- BF16 execution: `fp32_parameters_autocast`
|
| 193 |
- Pinned upstreams: `fair-esm`
|
| 194 |
-
- Reference container: `reference-esm2`
|
| 195 |
- Release tiers: `check`, `compliance`, `feature`, `artifact`, `benchmark`
|
| 196 |
- Unresolved required file identities: `0`
|
| 197 |
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
release blocker.
|
| 201 |
|
| 202 |
## Validation boundary
|
| 203 |
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
passed, that one backend is faster, or that an output has biological or
|
| 209 |
-
therapeutic validity.
|
| 210 |
|
| 211 |
## License
|
| 212 |
|
|
|
|
| 17 |
`AutoModelForMaskedLM`, `AutoModelForSequenceClassification`,
|
| 18 |
`AutoModelForTokenClassification`.
|
| 19 |
|
| 20 |
+
## Capabilities
|
| 21 |
+
|
| 22 |
+
| Feature | Status |
|
| 23 |
+
| --- | --- |
|
| 24 |
+
| Sequence classification | Supported: base weights with an untrained task head |
|
| 25 |
+
| Token classification | Supported: base weights with an untrained task head |
|
| 26 |
+
| PEFT fine-tuning | Supported pattern: preserve the separately trained `classifier` |
|
| 27 |
+
| Embeddings | Supported: shared ordered embedding API |
|
| 28 |
+
| Test-time training | Supported: low-rank masked-residue adaptation |
|
| 29 |
+
| Attention variants | Supported: `eager`, `sdpa`, `flex_attention`, `flash_attention_2`, `flash_attention_3` |
|
| 30 |
+
| Compliance | Declared: exact release evidence is required |
|
| 31 |
+
|
| 32 |
+
A supported interface is not a pretrained downstream predictor. Classification
|
| 33 |
+
heads start untrained, and declared compliance metadata is not a claim that an
|
| 34 |
+
arbitrary local build passed its release gate.
|
| 35 |
+
|
| 36 |
## Install and platform requirements
|
| 37 |
|
| 38 |
+
Install the direct dependencies published with this model:
|
| 39 |
|
| 40 |
```bash
|
| 41 |
+
python -m pip install -r \
|
| 42 |
+
"https://huggingface.co/Synthyra/ESM2-3B/resolve/main/requirements.txt"
|
| 43 |
```
|
| 44 |
|
| 45 |
+
The FastPLMs implementation itself is embedded in the model repository and loaded
|
| 46 |
+
by Transformers through `trust_remote_code=True`.
|
| 47 |
+
|
| 48 |
+
Python 3.11-3.14, PyTorch 2.13, and Transformers 5.13 are required. The artifact requirements include the direct FlashAttention loader dependency. FlashAttention also requires compatible CUDA hardware and BF16 execution. The Hub quick start below requires network
|
| 49 |
access on first download. For an air-gapped run, first build the manifest-pinned
|
| 50 |
local artifact and use the offline form shown in the example.
|
| 51 |
|
|
|
|
| 58 |
model = AutoModel.from_pretrained(
|
| 59 |
model_id,
|
| 60 |
trust_remote_code=True,
|
| 61 |
+
attn_implementation="sdpa",
|
| 62 |
).eval()
|
| 63 |
```
|
| 64 |
|
| 65 |
+
For offline validation, replace `model_id` with the manifest-built
|
| 66 |
+
`dist/hub/ESM2-3B` path and pass `local_files_only=True`.
|
| 67 |
+
|
| 68 |
+
## Attention and compliance
|
| 69 |
+
|
| 70 |
+
The quick start selects `sdpa` explicitly. Declared variants are `eager`, `sdpa`, `flex_attention`, `flash_attention_2`,
|
| 71 |
+
`flash_attention_3`. An unavailable requested backend raises instead of
|
| 72 |
+
silently switching implementations.
|
| 73 |
+
`output_attentions=True` may use the documented, one-call eager fallback solely
|
| 74 |
+
to materialize attention tensors; the configured backend remains unchanged.
|
| 75 |
+
|
| 76 |
+
This family declares the `compliance` tier. Release evidence binds the exact
|
| 77 |
+
checkpoint, backend, dtype, hardware, inputs, and reference revision.
|
|
|
|
| 78 |
|
| 79 |
## Tokenization and forward inference
|
| 80 |
|
|
|
|
| 104 |
|
| 105 |
## Dataset embeddings
|
| 106 |
|
| 107 |
+
The shared embedding mixin preserves input order and biological-position
|
| 108 |
+
masking. It accepts sequences, identified records, mappings, or a FASTA path:
|
|
|
|
| 109 |
|
| 110 |
```python
|
| 111 |
+
pooled = model.embed_dataset(
|
| 112 |
["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"],
|
| 113 |
batch_size=2,
|
| 114 |
pooling=("mean", "std"),
|
| 115 |
)
|
| 116 |
+
residues = model.embed_dataset(
|
| 117 |
+
["MSTNPKPQRKTKRNT"],
|
| 118 |
+
full_embeddings=True,
|
| 119 |
+
)
|
| 120 |
+
print(pooled[0].tensor.shape) # (2 * d,)
|
| 121 |
+
print(residues[0].tensor.shape) # (l, d)
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
Set `output` and `format="safetensors"` or `"sqlite"` for transactional,
|
| 125 |
+
bounded-memory persistence. Resume verifies input order, model state, tokenizer
|
| 126 |
+
policy, backend, dtype, and pooling configuration before appending.
|
| 127 |
+
|
| 128 |
+
## Downstream classification
|
| 129 |
+
|
| 130 |
+
Both downstream AutoClasses reuse the checkpoint backbone and initialize a new,
|
| 131 |
+
untrained `classifier`. Sequence labels have shape `(b,)`; residue labels have
|
| 132 |
+
shape `(b, l)` and use `-100` outside biological positions:
|
| 133 |
+
|
| 134 |
+
```python
|
| 135 |
+
import torch
|
| 136 |
+
from transformers import AutoTokenizer
|
| 137 |
+
from transformers import (
|
| 138 |
+
AutoModelForSequenceClassification,
|
| 139 |
+
AutoModelForTokenClassification,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
model_id = "Synthyra/ESM2-3B"
|
| 143 |
+
sequence_model = AutoModelForSequenceClassification.from_pretrained(
|
| 144 |
+
model_id, num_labels=2, trust_remote_code=True
|
| 145 |
+
).eval()
|
| 146 |
+
token_model = AutoModelForTokenClassification.from_pretrained(
|
| 147 |
+
model_id, num_labels=3, trust_remote_code=True
|
| 148 |
+
).eval()
|
| 149 |
+
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
| 150 |
+
sequences = ["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"]
|
| 151 |
+
batch = tokenizer(sequences, padding=True, return_tensors="pt")
|
| 152 |
+
biological = batch["attention_mask"].bool()
|
| 153 |
+
for special_id in tokenizer.all_special_ids:
|
| 154 |
+
biological &= batch["input_ids"].ne(special_id)
|
| 155 |
+
|
| 156 |
+
sequence_labels = torch.zeros(len(sequences), dtype=torch.long)
|
| 157 |
+
token_labels = torch.full_like(batch["input_ids"], -100)
|
| 158 |
+
token_labels[biological] = 0
|
| 159 |
+
|
| 160 |
+
with torch.inference_mode():
|
| 161 |
+
sequence_output = sequence_model(**batch, labels=sequence_labels)
|
| 162 |
+
token_output = token_model(**batch, labels=token_labels)
|
| 163 |
+
print(sequence_output.logits.shape) # (b, 2)
|
| 164 |
+
print(token_output.logits.shape) # (b, l, 3)
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
## PEFT fine-tuning
|
| 168 |
+
|
| 169 |
+
Install the direct training dependencies, then attach LoRA to the loaded checkpoint:
|
| 170 |
+
|
| 171 |
+
```bash
|
| 172 |
+
python -m pip install "datasets>=4.8,<5" "peft>=0.19,<0.20"
|
| 173 |
+
```
|
| 174 |
|
| 175 |
+
```python
|
| 176 |
+
from peft import LoraConfig, TaskType, get_peft_model
|
| 177 |
+
|
| 178 |
+
peft_model = get_peft_model(
|
| 179 |
+
sequence_model,
|
| 180 |
+
LoraConfig(
|
| 181 |
+
task_type=TaskType.SEQ_CLS,
|
| 182 |
+
r=8,
|
| 183 |
+
lora_alpha=16,
|
| 184 |
+
target_modules="all-linear",
|
| 185 |
+
modules_to_save=["classifier"],
|
| 186 |
+
),
|
| 187 |
+
)
|
| 188 |
```
|
| 189 |
|
| 190 |
+
This checkpoint advertises a classification head, so the separately trained
|
| 191 |
+
`classifier` is saved with the adapter.
|
| 192 |
+
All FastPLMs checkpoints follow the Transformers `PreTrainedModel` contract and
|
| 193 |
+
can be adapted with PEFT. The ESM2-specific shipped CLI is an example, not a
|
| 194 |
+
support boundary. Record the target modules, base revision, data identity, and
|
| 195 |
+
trainable parameter scope.
|
| 196 |
+
|
| 197 |
+
## Test-time training
|
| 198 |
|
| 199 |
+
TTT samples masked views of one protein and updates only injected low-rank
|
| 200 |
+
adapters. Base checkpoint weights remain frozen:
|
| 201 |
|
| 202 |
```python
|
| 203 |
+
from transformers import AutoModelForMaskedLM
|
| 204 |
+
|
| 205 |
+
ttt_model = AutoModelForMaskedLM.from_pretrained(
|
| 206 |
+
"Synthyra/ESM2-3B",
|
| 207 |
+
trust_remote_code=True,
|
|
|
|
|
|
|
| 208 |
)
|
| 209 |
+
metrics = ttt_model.ttt(
|
| 210 |
+
seq="MSTNPKPQRKTKRNT",
|
| 211 |
+
ttt_config={"steps": 3, "batch_size": 1, "seed": 7},
|
| 212 |
+
)
|
| 213 |
+
ttt_model.save_pretrained("adapted", safe_serialization=True)
|
| 214 |
+
ttt_model.ttt_reset()
|
| 215 |
+
print(metrics)
|
| 216 |
```
|
| 217 |
|
| 218 |
+
Persisted adapters retain their deterministic reset state. TTT adds latency
|
| 219 |
+
and memory, can worsen an output, and does not establish biological function.
|
|
|
|
| 220 |
|
| 221 |
## Masked language modeling and contacts
|
| 222 |
|
|
|
|
| 269 |
- Precision policies: `default`
|
| 270 |
- BF16 execution: `fp32_parameters_autocast`
|
| 271 |
- Generation contract: `not_applicable`
|
| 272 |
+
- Artifact dependency set: `core`
|
| 273 |
- Weight publication allowed: `true`
|
| 274 |
- Weight license status: `resolved`
|
| 275 |
- Redistributable: `true`
|
|
|
|
| 280 |
- FastPLMs weights: `Synthyra/ESM2-3B`
|
| 281 |
- Runtime revision: recorded separately in the built artifact and published commit
|
| 282 |
- Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
|
|
|
|
| 283 |
- Official checkpoint: `facebook/esm2_t36_3B_UR50D`
|
| 284 |
- Artifact source: `fast`
|
| 285 |
- State transform: `esm2_hf_to_fastplms_v1`
|
|
|
|
| 286 |
- Pinned upstreams: `fair-esm`
|
|
|
|
| 287 |
- Release tiers: `check`, `compliance`, `feature`, `artifact`, `benchmark`
|
| 288 |
- Unresolved required file identities: `0`
|
| 289 |
|
| 290 |
+
`provenance.json` records exact file identities, conversion, source revisions,
|
| 291 |
+
legal texts, schema, and attestations. A nonzero unresolved count blocks release.
|
|
|
|
| 292 |
|
| 293 |
## Validation boundary
|
| 294 |
|
| 295 |
+
Declared tiers compare applicable configuration, tokenizer behavior, state,
|
| 296 |
+
and representative inference with the pinned reference. Metadata alone does
|
| 297 |
+
not claim a build passed, a backend is faster, or an output is biologically
|
| 298 |
+
valid.
|
|
|
|
|
|
|
| 299 |
|
| 300 |
## License
|
| 301 |
|
config.json
CHANGED
|
@@ -19,11 +19,11 @@
|
|
| 19 |
"fastplms_checkpoint_repo_id": "Synthyra/ESM2-3B",
|
| 20 |
"fastplms_checkpoint_revision": "ff89d0180f414ab9c677219a25da79bf09185456",
|
| 21 |
"fastplms_model_id": "esm2_3b",
|
| 22 |
-
"fastplms_release_tool_revision": "
|
| 23 |
-
"fastplms_release_tool_sha256": "
|
| 24 |
-
"fastplms_runtime_bundle_sha256": "
|
| 25 |
-
"fastplms_runtime_revision": "
|
| 26 |
-
"fastplms_source_tree_sha256": "
|
| 27 |
"fastplms_weights_revision": "ff89d0180f414ab9c677219a25da79bf09185456",
|
| 28 |
"hidden_act": "gelu",
|
| 29 |
"hidden_dropout_prob": 0.0,
|
|
|
|
| 19 |
"fastplms_checkpoint_repo_id": "Synthyra/ESM2-3B",
|
| 20 |
"fastplms_checkpoint_revision": "ff89d0180f414ab9c677219a25da79bf09185456",
|
| 21 |
"fastplms_model_id": "esm2_3b",
|
| 22 |
+
"fastplms_release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 23 |
+
"fastplms_release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
|
| 24 |
+
"fastplms_runtime_bundle_sha256": "f22353ef386f607784889dc303488a2dc5e99b009a5e423ab3eff1f1eb03a9a4",
|
| 25 |
+
"fastplms_runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 26 |
+
"fastplms_source_tree_sha256": "818bf2bf5f573adc842b55f4f8feff926ddc25f904c5769ff5c382077d3a78a2",
|
| 27 |
"fastplms_weights_revision": "ff89d0180f414ab9c677219a25da79bf09185456",
|
| 28 |
"hidden_act": "gelu",
|
| 29 |
"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 |
-
|
| 437 |
-
|
|
|
|
| 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(
|
| 558 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 19 |
) from error
|
| 20 |
|
| 21 |
|
| 22 |
def _kernel_lock_path() -> Path:
|
| 23 |
-
"""Return the lock from
|
| 24 |
source_path = Path(__file__).resolve()
|
| 25 |
-
|
| 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 |
-
|
| 40 |
-
|
| 41 |
-
|
| 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 |
-
|
| 55 |
except (OSError, json.JSONDecodeError) as error:
|
| 56 |
-
raise RuntimeError(f"Unable to read the
|
| 57 |
-
if not isinstance(
|
| 58 |
raise RuntimeError("kernels.lock must contain a JSON list.")
|
| 59 |
-
if any(not isinstance(entry, dict) for entry in
|
| 60 |
raise RuntimeError("Every kernels.lock entry must be a JSON object.")
|
| 61 |
-
matches = [entry for entry in
|
| 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
|
| 129 |
) from error
|
| 130 |
|
| 131 |
-
|
| 132 |
-
parsed_names = {variant.variant_str for variant in
|
| 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(
|
| 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
|
| 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
|
| 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(
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
| 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 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 512 |
special_ids = tuple(int(token_id) for token_id in getattr(tokenizer, "all_special_ids", ()))
|
| 513 |
if special_ids:
|
| 514 |
-
specials = torch.tensor(
|
| 515 |
-
|
| 516 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 577 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 1310 |
batch_positions,
|
| 1311 |
device=decoder_input_ids.device,
|
| 1312 |
dtype=torch.long,
|
| 1313 |
)
|
| 1314 |
-
batch_model_kwargs["decoder_input_ids"] =
|
| 1315 |
-
0, indices
|
| 1316 |
)
|
| 1317 |
if decoder_attention_mask is not None:
|
| 1318 |
-
|
|
|
|
| 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()
|
|
|
|
| 1404 |
]
|
| 1405 |
else:
|
| 1406 |
-
values = [
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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 =
|
| 1454 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 19 |
-
|
|
|
|
| 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 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
| 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 / (
|
|
|
|
|
|
|
| 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(
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/esm2/modeling_fastesm.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
-
from dataclasses import dataclass
|
| 4 |
-
from typing import Any, ClassVar
|
| 5 |
-
|
| 6 |
import torch
|
| 7 |
import torch.nn as nn
|
|
|
|
|
|
|
| 8 |
from einops import rearrange
|
| 9 |
from torch.nn import functional as F
|
| 10 |
from transformers import EsmTokenizer, PretrainedConfig, PreTrainedModel
|
|
@@ -27,6 +26,7 @@ from transformers.models.esm.modeling_esm import (
|
|
| 27 |
|
| 28 |
from fastplms.models._esm_rotary import RotaryEmbedding
|
| 29 |
|
|
|
|
| 30 |
try:
|
| 31 |
from fastplms.attention import (
|
| 32 |
AttentionBackend,
|
|
@@ -199,7 +199,7 @@ class FastEsmTokenizer(EsmTokenizer):
|
|
| 199 |
|
| 200 |
|
| 201 |
class EsmSelfAttention(nn.Module):
|
| 202 |
-
def __init__(self, config, position_embedding_type: str | None = None):
|
| 203 |
super().__init__()
|
| 204 |
if config.hidden_size % config.num_attention_heads != 0:
|
| 205 |
raise ValueError(
|
|
@@ -233,18 +233,22 @@ class EsmSelfAttention(nn.Module):
|
|
| 233 |
output_attentions: bool = False,
|
| 234 |
output_s_max: bool = False,
|
| 235 |
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
|
|
|
|
| 236 |
batch_size, seq_length = hidden_states.shape[:-1]
|
| 237 |
hidden_shape = (batch_size, seq_length, -1, self.attention_head_size)
|
| 238 |
-
query_heads = self.query(hidden_states).view(hidden_shape).transpose(1, 2)
|
| 239 |
-
key_heads = self.key(hidden_states).view(hidden_shape).transpose(1, 2)
|
| 240 |
-
value_heads = self.value(hidden_states).view(hidden_shape).transpose(1, 2)
|
| 241 |
|
| 242 |
-
query_heads = query_heads * self.scale
|
| 243 |
|
| 244 |
if self.position_embedding_type == "rotary":
|
| 245 |
-
query_heads, key_heads = self.rotary_embeddings(
|
|
|
|
|
|
|
|
|
|
| 246 |
|
| 247 |
-
attn_output, attn_weights, s_max = self._attn(
|
| 248 |
query_heads,
|
| 249 |
key_heads,
|
| 250 |
value_heads,
|
|
@@ -254,7 +258,7 @@ class EsmSelfAttention(nn.Module):
|
|
| 254 |
output_attentions=output_attentions,
|
| 255 |
output_s_max=output_s_max,
|
| 256 |
)
|
| 257 |
-
return attn_output, attn_weights, s_max
|
| 258 |
|
| 259 |
def _attn(
|
| 260 |
self,
|
|
@@ -313,10 +317,13 @@ class EsmSelfAttention(nn.Module):
|
|
| 313 |
def _compute_s_max(
|
| 314 |
self, query_heads: torch.Tensor, key_heads: torch.Tensor
|
| 315 |
) -> list[torch.Tensor]:
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
|
|
|
|
|
|
|
|
|
| 320 |
|
| 321 |
def _manual_attn(
|
| 322 |
self,
|
|
@@ -326,16 +333,24 @@ class EsmSelfAttention(nn.Module):
|
|
| 326 |
attention_mask_4d: torch.Tensor | None = None,
|
| 327 |
output_s_max: bool = False,
|
| 328 |
) -> tuple[torch.Tensor, torch.Tensor, list[torch.Tensor] | None]:
|
| 329 |
-
|
|
|
|
| 330 |
if attention_mask_4d is not None:
|
| 331 |
-
attn_weights = attn_weights.masked_fill(
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
| 333 |
if self.dropout_prob > 0 and self.training:
|
| 334 |
-
attn_weights = F.dropout(
|
| 335 |
-
|
| 336 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
s_max = self._compute_s_max(query_heads, key_heads) if output_s_max else None
|
| 338 |
-
return attn_output, attn_weights, s_max
|
| 339 |
|
| 340 |
def _kernels_flash_attn(
|
| 341 |
self,
|
|
@@ -344,14 +359,14 @@ class EsmSelfAttention(nn.Module):
|
|
| 344 |
value_heads: torch.Tensor,
|
| 345 |
attention_mask_2d: torch.Tensor | None = None,
|
| 346 |
) -> tuple[torch.Tensor, None]:
|
| 347 |
-
query_tokens = query_heads.transpose(1, 2).contiguous()
|
| 348 |
-
key_tokens = key_heads.transpose(1, 2).contiguous()
|
| 349 |
-
value_tokens = value_heads.transpose(1, 2).contiguous()
|
| 350 |
# Q has been pre-scaled by self.scale = 1/sqrt(head_dim) in forward().
|
| 351 |
# Pass softmax_scale=1.0 to prevent the kernel from applying its default
|
| 352 |
# 1/sqrt(head_dim) scale on top (which would yield effective scale
|
| 353 |
# 1/head_dim and break parity vs sdpa).
|
| 354 |
-
attn_output = kernels_flash_attention_func(
|
| 355 |
query_states=query_tokens,
|
| 356 |
key_states=key_tokens,
|
| 357 |
value_states=value_tokens,
|
|
@@ -360,7 +375,7 @@ class EsmSelfAttention(nn.Module):
|
|
| 360 |
softmax_scale=1.0,
|
| 361 |
implementation=self.attn_backend.value,
|
| 362 |
)
|
| 363 |
-
return rearrange(attn_output, "b s h d -> b s (h d)"), None
|
| 364 |
|
| 365 |
def _flex_attn(
|
| 366 |
self,
|
|
@@ -378,10 +393,10 @@ class EsmSelfAttention(nn.Module):
|
|
| 378 |
shape=tuple(query_heads.shape),
|
| 379 |
mask_semantics="padding",
|
| 380 |
)
|
| 381 |
-
context_heads = fn(
|
| 382 |
query_heads, key_heads, value_heads, block_mask=flex_block_mask, scale=1.0
|
| 383 |
)
|
| 384 |
-
return rearrange(context_heads, "b h s d -> b s (h d)"), None
|
| 385 |
|
| 386 |
def _sdpa_attn(
|
| 387 |
self,
|
|
@@ -402,7 +417,7 @@ class EsmSelfAttention(nn.Module):
|
|
| 402 |
|
| 403 |
|
| 404 |
class EsmAttention(nn.Module):
|
| 405 |
-
def __init__(self, config):
|
| 406 |
super().__init__()
|
| 407 |
self.self = EsmSelfAttention(config)
|
| 408 |
self.output = EsmSelfOutput(config)
|
|
@@ -417,8 +432,9 @@ class EsmAttention(nn.Module):
|
|
| 417 |
output_attentions: bool = False,
|
| 418 |
output_s_max: bool = False,
|
| 419 |
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
|
| 420 |
-
|
| 421 |
-
|
|
|
|
| 422 |
hidden_states_ln,
|
| 423 |
attention_mask_2d=attention_mask_2d,
|
| 424 |
attention_mask_4d=attention_mask_4d,
|
|
@@ -426,12 +442,12 @@ class EsmAttention(nn.Module):
|
|
| 426 |
output_attentions=output_attentions,
|
| 427 |
output_s_max=output_s_max,
|
| 428 |
)
|
| 429 |
-
attention_output = self.output(attn_output, hidden_states)
|
| 430 |
-
return attention_output, attn_weights, s_max
|
| 431 |
|
| 432 |
|
| 433 |
class EsmLayer(nn.Module):
|
| 434 |
-
def __init__(self, config):
|
| 435 |
super().__init__()
|
| 436 |
self.chunk_size_feed_forward = config.chunk_size_feed_forward
|
| 437 |
self.seq_len_dim = 1
|
|
@@ -449,7 +465,7 @@ class EsmLayer(nn.Module):
|
|
| 449 |
output_attentions: bool = False,
|
| 450 |
output_s_max: bool = False,
|
| 451 |
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
|
| 452 |
-
attention_output, attn_weights, s_max = self.attention(
|
| 453 |
hidden_states,
|
| 454 |
attention_mask_2d=attention_mask_2d,
|
| 455 |
attention_mask_4d=attention_mask_4d,
|
|
@@ -457,18 +473,19 @@ class EsmLayer(nn.Module):
|
|
| 457 |
output_attentions=output_attentions,
|
| 458 |
output_s_max=output_s_max,
|
| 459 |
)
|
| 460 |
-
layer_output = self.feed_forward_chunk(attention_output)
|
| 461 |
-
return layer_output, attn_weights, s_max
|
| 462 |
|
| 463 |
-
def feed_forward_chunk(self, attention_output):
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
|
|
|
| 468 |
|
| 469 |
|
| 470 |
class EsmEncoder(nn.Module):
|
| 471 |
-
def __init__(self, config):
|
| 472 |
super().__init__()
|
| 473 |
self.config = config
|
| 474 |
self.attention_backend = resolve_attention_backend(config.attn_backend)
|
|
@@ -484,6 +501,7 @@ class EsmEncoder(nn.Module):
|
|
| 484 |
output_attentions: bool = False,
|
| 485 |
output_s_max: bool = False,
|
| 486 |
) -> FastEsmEncoderOutput:
|
|
|
|
| 487 |
all_hidden_states = () if output_hidden_states else None
|
| 488 |
all_attentions = () if output_attentions else None
|
| 489 |
full_s_max = () if output_s_max else None
|
|
@@ -507,6 +525,7 @@ class EsmEncoder(nn.Module):
|
|
| 507 |
all_hidden_states = (*all_hidden_states, hidden_states)
|
| 508 |
|
| 509 |
if self.gradient_checkpointing and self.training:
|
|
|
|
| 510 |
hidden_states, attn_weights, s_max = self._gradient_checkpointing_func(
|
| 511 |
layer_module.__call__,
|
| 512 |
hidden_states,
|
|
@@ -517,7 +536,7 @@ class EsmEncoder(nn.Module):
|
|
| 517 |
output_s_max,
|
| 518 |
)
|
| 519 |
else:
|
| 520 |
-
hidden_states, attn_weights, s_max = layer_module(
|
| 521 |
hidden_states,
|
| 522 |
attention_mask_2d=attention_mask_2d,
|
| 523 |
attention_mask_4d=attention_mask_4d,
|
|
@@ -532,7 +551,7 @@ class EsmEncoder(nn.Module):
|
|
| 532 |
full_s_max = (*full_s_max, s_max)
|
| 533 |
|
| 534 |
if self.emb_layer_norm_after:
|
| 535 |
-
hidden_states = self.emb_layer_norm_after(hidden_states)
|
| 536 |
|
| 537 |
if output_hidden_states:
|
| 538 |
all_hidden_states = (*all_hidden_states, hidden_states)
|
|
@@ -756,7 +775,7 @@ class FAST_ESM_ENCODER(FastEsmPreTrainedModel, EmbeddingMixin):
|
|
| 756 |
|
| 757 |
|
| 758 |
class FastEsmModel(FastEsmPreTrainedModel, EmbeddingMixin):
|
| 759 |
-
def __init__(self, config, add_pooling_layer: bool | None = None, **kwargs):
|
| 760 |
FastEsmPreTrainedModel.__init__(self, config, **kwargs)
|
| 761 |
self.config = config
|
| 762 |
self.esm = FAST_ESM_ENCODER(config)
|
|
@@ -822,8 +841,8 @@ class FastEsmModel(FastEsmPreTrainedModel, EmbeddingMixin):
|
|
| 822 |
output_s_max=output_s_max,
|
| 823 |
return_dict=True,
|
| 824 |
)
|
| 825 |
-
sequence_output = outputs.last_hidden_state
|
| 826 |
-
pooled_output = self.pooler(sequence_output) if self.pooler is not None else None
|
| 827 |
|
| 828 |
result = FastEsmEncoderOutput(
|
| 829 |
last_hidden_state=sequence_output,
|
|
@@ -836,7 +855,7 @@ class FastEsmModel(FastEsmPreTrainedModel, EmbeddingMixin):
|
|
| 836 |
|
| 837 |
|
| 838 |
class FastEsmForMaskedLM(FastPLMTestTimeTrainingMixin, FastEsmPreTrainedModel, EmbeddingMixin):
|
| 839 |
-
def __init__(self, config, **kwargs):
|
| 840 |
FastEsmPreTrainedModel.__init__(self, config, **kwargs)
|
| 841 |
self.esm = FAST_ESM_ENCODER(config, add_pooling_layer=False)
|
| 842 |
self.lm_head = EsmLMHead(config)
|
|
@@ -913,13 +932,13 @@ class FastEsmForMaskedLM(FastPLMTestTimeTrainingMixin, FastEsmPreTrainedModel, E
|
|
| 913 |
output_s_max=output_s_max,
|
| 914 |
return_dict=True,
|
| 915 |
)
|
| 916 |
-
sequence_output = outputs.last_hidden_state
|
| 917 |
-
prediction_scores = self.lm_head(sequence_output)
|
| 918 |
|
| 919 |
loss = None
|
| 920 |
if labels is not None:
|
| 921 |
-
labels = labels.to(prediction_scores.device)
|
| 922 |
-
loss = self.loss_fct(
|
| 923 |
prediction_scores.view(-1, self.config.vocab_size), labels.view(-1)
|
| 924 |
)
|
| 925 |
|
|
@@ -935,7 +954,7 @@ class FastEsmForMaskedLM(FastPLMTestTimeTrainingMixin, FastEsmPreTrainedModel, E
|
|
| 935 |
|
| 936 |
|
| 937 |
class FastEsmForSequenceClassification(FastEsmPreTrainedModel, EmbeddingMixin):
|
| 938 |
-
def __init__(self, config, **kwargs):
|
| 939 |
FastEsmPreTrainedModel.__init__(self, config, **kwargs)
|
| 940 |
self.num_labels = config.num_labels
|
| 941 |
self.config = config
|
|
@@ -994,12 +1013,12 @@ class FastEsmForSequenceClassification(FastEsmPreTrainedModel, EmbeddingMixin):
|
|
| 994 |
output_s_max=output_s_max,
|
| 995 |
return_dict=True,
|
| 996 |
)
|
| 997 |
-
sequence_output = outputs.last_hidden_state
|
| 998 |
-
logits = self.classifier(sequence_output)
|
| 999 |
|
| 1000 |
loss = None
|
| 1001 |
if labels is not None:
|
| 1002 |
-
labels = labels.to(logits.device)
|
| 1003 |
if self.config.problem_type is None:
|
| 1004 |
if self.num_labels == 1:
|
| 1005 |
self.config.problem_type = "regression"
|
|
@@ -1012,13 +1031,13 @@ class FastEsmForSequenceClassification(FastEsmPreTrainedModel, EmbeddingMixin):
|
|
| 1012 |
|
| 1013 |
if self.config.problem_type == "regression":
|
| 1014 |
if self.num_labels == 1:
|
| 1015 |
-
loss = self.mse(logits.squeeze(), labels.squeeze())
|
| 1016 |
else:
|
| 1017 |
-
loss = self.mse(logits, labels)
|
| 1018 |
elif self.config.problem_type == "single_label_classification":
|
| 1019 |
-
loss = self.ce(logits.view(-1, self.num_labels), labels.view(-1))
|
| 1020 |
elif self.config.problem_type == "multi_label_classification":
|
| 1021 |
-
loss = self.bce(logits, labels)
|
| 1022 |
|
| 1023 |
result = EsmSequenceClassifierOutput(
|
| 1024 |
loss=loss,
|
|
@@ -1031,7 +1050,7 @@ class FastEsmForSequenceClassification(FastEsmPreTrainedModel, EmbeddingMixin):
|
|
| 1031 |
|
| 1032 |
|
| 1033 |
class FastEsmForTokenClassification(FastEsmPreTrainedModel, EmbeddingMixin):
|
| 1034 |
-
def __init__(self, config, **kwargs):
|
| 1035 |
FastEsmPreTrainedModel.__init__(self, config, **kwargs)
|
| 1036 |
self.num_labels = config.num_labels
|
| 1037 |
self.esm = FAST_ESM_ENCODER(config, add_pooling_layer=False)
|
|
@@ -1088,14 +1107,14 @@ class FastEsmForTokenClassification(FastEsmPreTrainedModel, EmbeddingMixin):
|
|
| 1088 |
output_s_max=output_s_max,
|
| 1089 |
return_dict=True,
|
| 1090 |
)
|
| 1091 |
-
sequence_output = outputs.last_hidden_state
|
| 1092 |
-
sequence_output = self.dropout(sequence_output)
|
| 1093 |
-
logits = self.classifier(sequence_output)
|
| 1094 |
|
| 1095 |
loss = None
|
| 1096 |
if labels is not None:
|
| 1097 |
-
labels = labels.to(logits.device)
|
| 1098 |
-
loss = self.loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
|
| 1099 |
|
| 1100 |
result = EsmTokenClassifierOutput(
|
| 1101 |
loss=loss,
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
|
|
|
|
|
|
| 3 |
import torch
|
| 4 |
import torch.nn as nn
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from typing import Any, ClassVar
|
| 7 |
from einops import rearrange
|
| 8 |
from torch.nn import functional as F
|
| 9 |
from transformers import EsmTokenizer, PretrainedConfig, PreTrainedModel
|
|
|
|
| 26 |
|
| 27 |
from fastplms.models._esm_rotary import RotaryEmbedding
|
| 28 |
|
| 29 |
+
|
| 30 |
try:
|
| 31 |
from fastplms.attention import (
|
| 32 |
AttentionBackend,
|
|
|
|
| 199 |
|
| 200 |
|
| 201 |
class EsmSelfAttention(nn.Module):
|
| 202 |
+
def __init__(self, config, position_embedding_type: str | None = None) -> None:
|
| 203 |
super().__init__()
|
| 204 |
if config.hidden_size % config.num_attention_heads != 0:
|
| 205 |
raise ValueError(
|
|
|
|
| 233 |
output_attentions: bool = False,
|
| 234 |
output_s_max: bool = False,
|
| 235 |
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
|
| 236 |
+
# hidden_states: (b, l, d); masks: (b, l) and (b, 1, 1, l)
|
| 237 |
batch_size, seq_length = hidden_states.shape[:-1]
|
| 238 |
hidden_shape = (batch_size, seq_length, -1, self.attention_head_size)
|
| 239 |
+
query_heads = self.query(hidden_states).view(hidden_shape).transpose(1, 2) # (b, h, l, d_h)
|
| 240 |
+
key_heads = self.key(hidden_states).view(hidden_shape).transpose(1, 2) # (b, h, l, d_h)
|
| 241 |
+
value_heads = self.value(hidden_states).view(hidden_shape).transpose(1, 2) # (b, h, l, d_h)
|
| 242 |
|
| 243 |
+
query_heads = query_heads * self.scale # (b, h, l, d_h)
|
| 244 |
|
| 245 |
if self.position_embedding_type == "rotary":
|
| 246 |
+
query_heads, key_heads = self.rotary_embeddings( # both (b, h, l, d_h)
|
| 247 |
+
query_heads,
|
| 248 |
+
key_heads,
|
| 249 |
+
)
|
| 250 |
|
| 251 |
+
attn_output, attn_weights, s_max = self._attn( # (b, l, d), (b, h, l, l), heads
|
| 252 |
query_heads,
|
| 253 |
key_heads,
|
| 254 |
value_heads,
|
|
|
|
| 258 |
output_attentions=output_attentions,
|
| 259 |
output_s_max=output_s_max,
|
| 260 |
)
|
| 261 |
+
return attn_output, attn_weights, s_max # (b, l, d), optional (b, h, l, l), heads
|
| 262 |
|
| 263 |
def _attn(
|
| 264 |
self,
|
|
|
|
| 317 |
def _compute_s_max(
|
| 318 |
self, query_heads: torch.Tensor, key_heads: torch.Tensor
|
| 319 |
) -> list[torch.Tensor]:
|
| 320 |
+
# query_heads, key_heads: (b, h, l, d_h)
|
| 321 |
+
q_norm = torch.linalg.vector_norm(query_heads, dim=-1) # (b, h, l)
|
| 322 |
+
k_norm = torch.linalg.vector_norm(key_heads, dim=-1) # (b, h, l)
|
| 323 |
+
s_max_bound = ( # (h,)
|
| 324 |
+
q_norm.max(dim=-1).values * k_norm.max(dim=-1).values
|
| 325 |
+
).max(dim=0).values
|
| 326 |
+
return [s_max_bound[h] for h in range(self.num_attention_heads)] # h scalars
|
| 327 |
|
| 328 |
def _manual_attn(
|
| 329 |
self,
|
|
|
|
| 333 |
attention_mask_4d: torch.Tensor | None = None,
|
| 334 |
output_s_max: bool = False,
|
| 335 |
) -> tuple[torch.Tensor, torch.Tensor, list[torch.Tensor] | None]:
|
| 336 |
+
# query_heads, key_heads, value_heads: (b, h, l, d_h)
|
| 337 |
+
attn_weights = torch.matmul(query_heads, key_heads.transpose(-1, -2)) # (b, h, l, l)
|
| 338 |
if attention_mask_4d is not None:
|
| 339 |
+
attn_weights = attn_weights.masked_fill( # (b, h, l, l)
|
| 340 |
+
attention_mask_4d.logical_not(),
|
| 341 |
+
float("-inf"),
|
| 342 |
+
)
|
| 343 |
+
attn_weights = F.softmax(attn_weights, dim=-1) # (b, h, l, l)
|
| 344 |
if self.dropout_prob > 0 and self.training:
|
| 345 |
+
attn_weights = F.dropout( # (b, h, l, l)
|
| 346 |
+
attn_weights,
|
| 347 |
+
p=self.dropout_prob,
|
| 348 |
+
training=self.training,
|
| 349 |
+
)
|
| 350 |
+
context_heads = torch.matmul(attn_weights, value_heads) # (b, h, l, d_h)
|
| 351 |
+
attn_output = rearrange(context_heads, "b h s d -> b s (h d)") # (b, l, d)
|
| 352 |
s_max = self._compute_s_max(query_heads, key_heads) if output_s_max else None
|
| 353 |
+
return attn_output, attn_weights, s_max # (b, l, d), (b, h, l, l), heads
|
| 354 |
|
| 355 |
def _kernels_flash_attn(
|
| 356 |
self,
|
|
|
|
| 359 |
value_heads: torch.Tensor,
|
| 360 |
attention_mask_2d: torch.Tensor | None = None,
|
| 361 |
) -> tuple[torch.Tensor, None]:
|
| 362 |
+
query_tokens = query_heads.transpose(1, 2).contiguous() # (b, l, h, d_h)
|
| 363 |
+
key_tokens = key_heads.transpose(1, 2).contiguous() # (b, l, h, d_h)
|
| 364 |
+
value_tokens = value_heads.transpose(1, 2).contiguous() # (b, l, h, d_h)
|
| 365 |
# Q has been pre-scaled by self.scale = 1/sqrt(head_dim) in forward().
|
| 366 |
# Pass softmax_scale=1.0 to prevent the kernel from applying its default
|
| 367 |
# 1/sqrt(head_dim) scale on top (which would yield effective scale
|
| 368 |
# 1/head_dim and break parity vs sdpa).
|
| 369 |
+
attn_output = kernels_flash_attention_func( # (b, l, h, d_h)
|
| 370 |
query_states=query_tokens,
|
| 371 |
key_states=key_tokens,
|
| 372 |
value_states=value_tokens,
|
|
|
|
| 375 |
softmax_scale=1.0,
|
| 376 |
implementation=self.attn_backend.value,
|
| 377 |
)
|
| 378 |
+
return rearrange(attn_output, "b s h d -> b s (h d)"), None # (b, l, d), None
|
| 379 |
|
| 380 |
def _flex_attn(
|
| 381 |
self,
|
|
|
|
| 393 |
shape=tuple(query_heads.shape),
|
| 394 |
mask_semantics="padding",
|
| 395 |
)
|
| 396 |
+
context_heads = fn( # (b, h, l, d_h)
|
| 397 |
query_heads, key_heads, value_heads, block_mask=flex_block_mask, scale=1.0
|
| 398 |
)
|
| 399 |
+
return rearrange(context_heads, "b h s d -> b s (h d)"), None # (b, l, d), None
|
| 400 |
|
| 401 |
def _sdpa_attn(
|
| 402 |
self,
|
|
|
|
| 417 |
|
| 418 |
|
| 419 |
class EsmAttention(nn.Module):
|
| 420 |
+
def __init__(self, config) -> None:
|
| 421 |
super().__init__()
|
| 422 |
self.self = EsmSelfAttention(config)
|
| 423 |
self.output = EsmSelfOutput(config)
|
|
|
|
| 432 |
output_attentions: bool = False,
|
| 433 |
output_s_max: bool = False,
|
| 434 |
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
|
| 435 |
+
# hidden_states: (b, l, d)
|
| 436 |
+
hidden_states_ln = self.LayerNorm(hidden_states) # (b, l, d)
|
| 437 |
+
attn_output, attn_weights, s_max = self.self( # (b, l, d), optional (b, h, l, l), heads
|
| 438 |
hidden_states_ln,
|
| 439 |
attention_mask_2d=attention_mask_2d,
|
| 440 |
attention_mask_4d=attention_mask_4d,
|
|
|
|
| 442 |
output_attentions=output_attentions,
|
| 443 |
output_s_max=output_s_max,
|
| 444 |
)
|
| 445 |
+
attention_output = self.output(attn_output, hidden_states) # (b, l, d)
|
| 446 |
+
return attention_output, attn_weights, s_max # (b, l, d), optional weights, heads
|
| 447 |
|
| 448 |
|
| 449 |
class EsmLayer(nn.Module):
|
| 450 |
+
def __init__(self, config) -> None:
|
| 451 |
super().__init__()
|
| 452 |
self.chunk_size_feed_forward = config.chunk_size_feed_forward
|
| 453 |
self.seq_len_dim = 1
|
|
|
|
| 465 |
output_attentions: bool = False,
|
| 466 |
output_s_max: bool = False,
|
| 467 |
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
|
| 468 |
+
attention_output, attn_weights, s_max = self.attention( # (b, l, d), weights, heads
|
| 469 |
hidden_states,
|
| 470 |
attention_mask_2d=attention_mask_2d,
|
| 471 |
attention_mask_4d=attention_mask_4d,
|
|
|
|
| 473 |
output_attentions=output_attentions,
|
| 474 |
output_s_max=output_s_max,
|
| 475 |
)
|
| 476 |
+
layer_output = self.feed_forward_chunk(attention_output) # (b, l, d)
|
| 477 |
+
return layer_output, attn_weights, s_max # (b, l, d), weights, heads
|
| 478 |
|
| 479 |
+
def feed_forward_chunk(self, attention_output: torch.Tensor) -> torch.Tensor:
|
| 480 |
+
# attention_output: (b, l, d)
|
| 481 |
+
attention_output_ln = self.LayerNorm(attention_output) # (b, l, d)
|
| 482 |
+
intermediate_output = self.intermediate(attention_output_ln) # (b, l, d_ff)
|
| 483 |
+
layer_output = self.output(intermediate_output, attention_output) # (b, l, d)
|
| 484 |
+
return layer_output # (b, l, d)
|
| 485 |
|
| 486 |
|
| 487 |
class EsmEncoder(nn.Module):
|
| 488 |
+
def __init__(self, config) -> None:
|
| 489 |
super().__init__()
|
| 490 |
self.config = config
|
| 491 |
self.attention_backend = resolve_attention_backend(config.attn_backend)
|
|
|
|
| 501 |
output_attentions: bool = False,
|
| 502 |
output_s_max: bool = False,
|
| 503 |
) -> FastEsmEncoderOutput:
|
| 504 |
+
# hidden_states: (b, l, d); attention_mask: (b, l)
|
| 505 |
all_hidden_states = () if output_hidden_states else None
|
| 506 |
all_attentions = () if output_attentions else None
|
| 507 |
full_s_max = () if output_s_max else None
|
|
|
|
| 525 |
all_hidden_states = (*all_hidden_states, hidden_states)
|
| 526 |
|
| 527 |
if self.gradient_checkpointing and self.training:
|
| 528 |
+
# hidden_states: (b, l, d)
|
| 529 |
hidden_states, attn_weights, s_max = self._gradient_checkpointing_func(
|
| 530 |
layer_module.__call__,
|
| 531 |
hidden_states,
|
|
|
|
| 536 |
output_s_max,
|
| 537 |
)
|
| 538 |
else:
|
| 539 |
+
hidden_states, attn_weights, s_max = layer_module( # (b, l, d), weights, heads
|
| 540 |
hidden_states,
|
| 541 |
attention_mask_2d=attention_mask_2d,
|
| 542 |
attention_mask_4d=attention_mask_4d,
|
|
|
|
| 551 |
full_s_max = (*full_s_max, s_max)
|
| 552 |
|
| 553 |
if self.emb_layer_norm_after:
|
| 554 |
+
hidden_states = self.emb_layer_norm_after(hidden_states) # (b, l, d)
|
| 555 |
|
| 556 |
if output_hidden_states:
|
| 557 |
all_hidden_states = (*all_hidden_states, hidden_states)
|
|
|
|
| 775 |
|
| 776 |
|
| 777 |
class FastEsmModel(FastEsmPreTrainedModel, EmbeddingMixin):
|
| 778 |
+
def __init__(self, config, add_pooling_layer: bool | None = None, **kwargs) -> None:
|
| 779 |
FastEsmPreTrainedModel.__init__(self, config, **kwargs)
|
| 780 |
self.config = config
|
| 781 |
self.esm = FAST_ESM_ENCODER(config)
|
|
|
|
| 841 |
output_s_max=output_s_max,
|
| 842 |
return_dict=True,
|
| 843 |
)
|
| 844 |
+
sequence_output = outputs.last_hidden_state # (b, l, d)
|
| 845 |
+
pooled_output = self.pooler(sequence_output) if self.pooler is not None else None # (b, d)
|
| 846 |
|
| 847 |
result = FastEsmEncoderOutput(
|
| 848 |
last_hidden_state=sequence_output,
|
|
|
|
| 855 |
|
| 856 |
|
| 857 |
class FastEsmForMaskedLM(FastPLMTestTimeTrainingMixin, FastEsmPreTrainedModel, EmbeddingMixin):
|
| 858 |
+
def __init__(self, config, **kwargs) -> None:
|
| 859 |
FastEsmPreTrainedModel.__init__(self, config, **kwargs)
|
| 860 |
self.esm = FAST_ESM_ENCODER(config, add_pooling_layer=False)
|
| 861 |
self.lm_head = EsmLMHead(config)
|
|
|
|
| 932 |
output_s_max=output_s_max,
|
| 933 |
return_dict=True,
|
| 934 |
)
|
| 935 |
+
sequence_output = outputs.last_hidden_state # (b, l, d)
|
| 936 |
+
prediction_scores = self.lm_head(sequence_output) # (b, l, c)
|
| 937 |
|
| 938 |
loss = None
|
| 939 |
if labels is not None:
|
| 940 |
+
labels = labels.to(prediction_scores.device) # (b, l)
|
| 941 |
+
loss = self.loss_fct( # ()
|
| 942 |
prediction_scores.view(-1, self.config.vocab_size), labels.view(-1)
|
| 943 |
)
|
| 944 |
|
|
|
|
| 954 |
|
| 955 |
|
| 956 |
class FastEsmForSequenceClassification(FastEsmPreTrainedModel, EmbeddingMixin):
|
| 957 |
+
def __init__(self, config, **kwargs) -> None:
|
| 958 |
FastEsmPreTrainedModel.__init__(self, config, **kwargs)
|
| 959 |
self.num_labels = config.num_labels
|
| 960 |
self.config = config
|
|
|
|
| 1013 |
output_s_max=output_s_max,
|
| 1014 |
return_dict=True,
|
| 1015 |
)
|
| 1016 |
+
sequence_output = outputs.last_hidden_state # (b, l, d)
|
| 1017 |
+
logits = self.classifier(sequence_output) # (b, c)
|
| 1018 |
|
| 1019 |
loss = None
|
| 1020 |
if labels is not None:
|
| 1021 |
+
labels = labels.to(logits.device) # (b,) or (b, c)
|
| 1022 |
if self.config.problem_type is None:
|
| 1023 |
if self.num_labels == 1:
|
| 1024 |
self.config.problem_type = "regression"
|
|
|
|
| 1031 |
|
| 1032 |
if self.config.problem_type == "regression":
|
| 1033 |
if self.num_labels == 1:
|
| 1034 |
+
loss = self.mse(logits.squeeze(), labels.squeeze()) # ()
|
| 1035 |
else:
|
| 1036 |
+
loss = self.mse(logits, labels) # ()
|
| 1037 |
elif self.config.problem_type == "single_label_classification":
|
| 1038 |
+
loss = self.ce(logits.view(-1, self.num_labels), labels.view(-1)) # ()
|
| 1039 |
elif self.config.problem_type == "multi_label_classification":
|
| 1040 |
+
loss = self.bce(logits, labels) # ()
|
| 1041 |
|
| 1042 |
result = EsmSequenceClassifierOutput(
|
| 1043 |
loss=loss,
|
|
|
|
| 1050 |
|
| 1051 |
|
| 1052 |
class FastEsmForTokenClassification(FastEsmPreTrainedModel, EmbeddingMixin):
|
| 1053 |
+
def __init__(self, config, **kwargs) -> None:
|
| 1054 |
FastEsmPreTrainedModel.__init__(self, config, **kwargs)
|
| 1055 |
self.num_labels = config.num_labels
|
| 1056 |
self.esm = FAST_ESM_ENCODER(config, add_pooling_layer=False)
|
|
|
|
| 1107 |
output_s_max=output_s_max,
|
| 1108 |
return_dict=True,
|
| 1109 |
)
|
| 1110 |
+
sequence_output = outputs.last_hidden_state # (b, l, d)
|
| 1111 |
+
sequence_output = self.dropout(sequence_output) # (b, l, d)
|
| 1112 |
+
logits = self.classifier(sequence_output) # (b, l, c)
|
| 1113 |
|
| 1114 |
loss = None
|
| 1115 |
if labels is not None:
|
| 1116 |
+
labels = labels.to(logits.device) # (b, l)
|
| 1117 |
+
loss = self.loss_fct(logits.view(-1, self.num_labels), labels.view(-1)) # ()
|
| 1118 |
|
| 1119 |
result = EsmTokenClassifierOutput(
|
| 1120 |
loss=loss,
|
fastplms/models/ttt.py
CHANGED
|
@@ -3,12 +3,13 @@ from __future__ import annotations
|
|
| 3 |
import contextlib
|
| 4 |
import math
|
| 5 |
import numbers
|
| 6 |
-
import typing as T
|
| 7 |
-
from dataclasses import asdict, dataclass, fields
|
| 8 |
-
|
| 9 |
import torch
|
| 10 |
import torch.nn as nn
|
| 11 |
import torch.nn.functional as F
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
_STANDARD_AMINO_ACIDS = "ACDEFGHIKLMNPQRSTVWY"
|
| 14 |
_TTT_SERIALIZATION_VERSION = 1
|
|
@@ -42,7 +43,7 @@ class TTTConfig:
|
|
| 42 |
self.verify()
|
| 43 |
|
| 44 |
@classmethod
|
| 45 |
-
def from_kwargs(cls, **kwargs:
|
| 46 |
valid_names = {field.name for field in fields(cls)}
|
| 47 |
unknown_names = set(kwargs) - valid_names
|
| 48 |
if unknown_names:
|
|
@@ -53,7 +54,7 @@ class TTTConfig:
|
|
| 53 |
kwargs["lora_target_modules"] = tuple(kwargs["lora_target_modules"])
|
| 54 |
return cls(**kwargs)
|
| 55 |
|
| 56 |
-
def merged(self, overrides:
|
| 57 |
if overrides is None:
|
| 58 |
return self
|
| 59 |
if isinstance(overrides, TTTConfig):
|
|
@@ -65,7 +66,7 @@ class TTTConfig:
|
|
| 65 |
values[name] = value
|
| 66 |
return TTTConfig(**values)
|
| 67 |
|
| 68 |
-
def to_dict(self) -> dict[str,
|
| 69 |
return asdict(self)
|
| 70 |
|
| 71 |
def verify(self) -> None:
|
|
@@ -224,9 +225,12 @@ class LoraInjectedLinear(nn.Module):
|
|
| 224 |
return self.linear._parameters["bias"]
|
| 225 |
|
| 226 |
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
def reset_lora_parameters(self) -> None:
|
| 232 |
with torch.no_grad():
|
|
@@ -235,7 +239,7 @@ class LoraInjectedLinear(nn.Module):
|
|
| 235 |
|
| 236 |
|
| 237 |
class FastPLMTestTimeTrainingMixin:
|
| 238 |
-
def init_ttt(self, ttt_config: TTTConfig |
|
| 239 |
base_config = self.__dict__.get("_ttt_cfg")
|
| 240 |
if base_config is None:
|
| 241 |
base_config = TTTConfig()
|
|
@@ -245,7 +249,7 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 245 |
serialized = getattr(getattr(self, "config", None), "fastplms_ttt", None)
|
| 246 |
serialized_initialized = False
|
| 247 |
if serialized is not None:
|
| 248 |
-
if not isinstance(serialized,
|
| 249 |
raise ValueError("config.fastplms_ttt must be a mapping.")
|
| 250 |
version = serialized.get("version")
|
| 251 |
if version != _TTT_SERIALIZATION_VERSION:
|
|
@@ -254,7 +258,7 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 254 |
f"{version!r}; expected {_TTT_SERIALIZATION_VERSION}."
|
| 255 |
)
|
| 256 |
serialized_config = serialized.get("config")
|
| 257 |
-
if not isinstance(serialized_config,
|
| 258 |
raise ValueError("Serialized FastPLMs TTT state is missing its config mapping.")
|
| 259 |
configured = TTTConfig.from_kwargs(**dict(serialized_config))
|
| 260 |
initialized_value = serialized.get("initialized", False)
|
|
@@ -285,15 +289,15 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 285 |
self,
|
| 286 |
seq: str | list[str] | None = None,
|
| 287 |
input_ids: torch.Tensor | None = None,
|
| 288 |
-
**kwargs:
|
| 289 |
) -> torch.Tensor | dict[str, torch.Tensor]:
|
| 290 |
del kwargs
|
| 291 |
if input_ids is not None:
|
| 292 |
-
return input_ids
|
| 293 |
if seq is None:
|
| 294 |
raise ValueError("Pass either seq or input_ids for TTT.")
|
| 295 |
tokenized = self.tokenizer(seq, return_tensors="pt", padding=True)
|
| 296 |
-
return tokenized["input_ids"]
|
| 297 |
|
| 298 |
def _ttt_mask_token(self) -> int:
|
| 299 |
return int(self.tokenizer.mask_token_id)
|
|
@@ -302,6 +306,7 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 302 |
return int(self.tokenizer.pad_token_id)
|
| 303 |
|
| 304 |
def _ttt_replacement_tokens(self, input_ids: torch.Tensor) -> torch.Tensor:
|
|
|
|
| 305 |
tokenizer = self.tokenizer
|
| 306 |
special_ids = set(tokenizer.all_special_ids)
|
| 307 |
vocab_size = int(self.config.vocab_size)
|
|
@@ -309,13 +314,13 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 309 |
if unknown_id is not None:
|
| 310 |
special_ids.add(int(unknown_id))
|
| 311 |
|
| 312 |
-
vocab:
|
| 313 |
get_vocab = getattr(tokenizer, "get_vocab", None)
|
| 314 |
if callable(get_vocab):
|
| 315 |
vocab = get_vocab()
|
| 316 |
-
elif isinstance(getattr(tokenizer, "vocab", None),
|
| 317 |
vocab = tokenizer.vocab
|
| 318 |
-
elif isinstance(getattr(tokenizer, "_token_to_id", None),
|
| 319 |
vocab = tokenizer._token_to_id
|
| 320 |
|
| 321 |
ids: list[int] = []
|
|
@@ -334,20 +339,20 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 334 |
"TTT could not resolve any canonical amino-acid token IDs from the tokenizer; "
|
| 335 |
"refusing to sample arbitrary or reserved vocabulary entries."
|
| 336 |
)
|
| 337 |
-
return torch.tensor(ids, device=input_ids.device, dtype=input_ids.dtype)
|
| 338 |
|
| 339 |
def _ttt_predict_logits(
|
| 340 |
self,
|
| 341 |
batch: torch.Tensor | dict[str, torch.Tensor],
|
| 342 |
-
**kwargs:
|
| 343 |
) -> torch.Tensor:
|
| 344 |
del kwargs
|
| 345 |
if isinstance(batch, dict):
|
| 346 |
output = self(**batch)
|
| 347 |
-
return output.logits
|
| 348 |
-
attention_mask = batch.ne(self._ttt_padding_token())
|
| 349 |
output = self(input_ids=batch, attention_mask=attention_mask)
|
| 350 |
-
return output.logits
|
| 351 |
|
| 352 |
def _ttt_eval_step(
|
| 353 |
self,
|
|
@@ -355,8 +360,8 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 355 |
loss: float,
|
| 356 |
seq: str | list[str] | None = None,
|
| 357 |
input_ids: torch.Tensor | None = None,
|
| 358 |
-
**kwargs:
|
| 359 |
-
) -> tuple[dict[str,
|
| 360 |
del step, loss, seq, input_ids, kwargs
|
| 361 |
return {}, None
|
| 362 |
|
|
@@ -443,8 +448,8 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 443 |
for module in self._ttt_lora_modules():
|
| 444 |
snapshot.append(
|
| 445 |
{
|
| 446 |
-
"lora_down.weight": module.lora_down.weight.detach().clone(),
|
| 447 |
-
"lora_up.weight": module.lora_up.weight.detach().clone(),
|
| 448 |
}
|
| 449 |
)
|
| 450 |
if not snapshot:
|
|
@@ -473,14 +478,14 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 473 |
for module in self._ttt_lora_modules():
|
| 474 |
module.reset_lora_parameters()
|
| 475 |
|
| 476 |
-
def _ttt_serialized_contract(self) -> dict[str,
|
| 477 |
return {
|
| 478 |
"version": _TTT_SERIALIZATION_VERSION,
|
| 479 |
"initialized": bool(self._ttt_initialized),
|
| 480 |
"config": self.ttt_config.to_dict(),
|
| 481 |
}
|
| 482 |
|
| 483 |
-
def save_pretrained(self, save_directory:
|
| 484 |
"""Save initialized adapters, their reset baseline, and the TTT config.
|
| 485 |
|
| 486 |
Adapter injection changes the module tree, so the serialized config must
|
|
@@ -523,16 +528,16 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 523 |
device: torch.device,
|
| 524 |
) -> torch.Tensor | dict[str, torch.Tensor]:
|
| 525 |
if isinstance(batch, dict):
|
| 526 |
-
return {name: tensor.to(device) for name, tensor in batch.items()}
|
| 527 |
-
return batch.to(device)
|
| 528 |
|
| 529 |
def _ttt_input_ids_from_batch(
|
| 530 |
self,
|
| 531 |
batch: torch.Tensor | dict[str, torch.Tensor],
|
| 532 |
) -> torch.Tensor:
|
| 533 |
if isinstance(batch, dict):
|
| 534 |
-
return batch["input_ids"]
|
| 535 |
-
return batch
|
| 536 |
|
| 537 |
def _ttt_set_input_ids(
|
| 538 |
self,
|
|
@@ -541,13 +546,14 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 541 |
) -> torch.Tensor | dict[str, torch.Tensor]:
|
| 542 |
if isinstance(batch, dict):
|
| 543 |
updated = dict(batch)
|
| 544 |
-
updated["input_ids"] = input_ids
|
| 545 |
return updated
|
| 546 |
-
return input_ids
|
| 547 |
|
| 548 |
def _ttt_non_special_mask(self, input_ids: torch.Tensor) -> torch.Tensor:
|
| 549 |
-
|
| 550 |
-
|
|
|
|
| 551 |
|
| 552 |
def _ttt_validate_tokenized_batch(
|
| 553 |
self,
|
|
@@ -570,12 +576,14 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 570 |
"DPLM2 TTT could not resolve the structure-token boundary safely."
|
| 571 |
)
|
| 572 |
pad_token = self._ttt_padding_token()
|
| 573 |
-
generic_aa_special_ids = torch.tensor(
|
| 574 |
[int(self.config.vocab_size) + offset for offset in range(4)],
|
| 575 |
device=input_ids.device,
|
| 576 |
dtype=input_ids.dtype,
|
| 577 |
)
|
| 578 |
-
is_structure = input_ids.ge(int(struct_boundary)) & input_ids.ne(
|
|
|
|
|
|
|
| 579 |
is_structure &= ~torch.isin(input_ids, generic_aa_special_ids)
|
| 580 |
if bool(is_structure.any()):
|
| 581 |
raise ValueError(
|
|
@@ -584,8 +592,11 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 584 |
)
|
| 585 |
|
| 586 |
if isinstance(batch, dict) and "type_ids" in batch:
|
| 587 |
-
type_ids = batch["type_ids"]
|
| 588 |
-
attention_mask = batch.get(
|
|
|
|
|
|
|
|
|
|
| 589 |
if bool(((type_ids == int(self.config.struct_type)) & attention_mask).any()):
|
| 590 |
raise ValueError(
|
| 591 |
"DPLM2 TTT currently supports amino-acid-only inputs; structure "
|
|
@@ -607,13 +618,15 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 607 |
cfg = self.ttt_config
|
| 608 |
if input_ids.shape[1] <= cfg.crop_size:
|
| 609 |
return batch
|
| 610 |
-
position_has_residue =
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
|
|
|
|
|
|
| 614 |
if valid_starts.numel() == 0:
|
| 615 |
raise ValueError("TTT could not find a crop containing a biological residue token.")
|
| 616 |
-
selected = torch.randint(
|
| 617 |
valid_starts.numel(),
|
| 618 |
(1,),
|
| 619 |
generator=generator,
|
|
@@ -625,11 +638,11 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 625 |
cropped = {}
|
| 626 |
for name, tensor in batch.items():
|
| 627 |
if tensor.ndim >= 2 and tensor.shape[1] == input_ids.shape[1]:
|
| 628 |
-
cropped[name] = tensor[:, start:end]
|
| 629 |
else:
|
| 630 |
cropped[name] = tensor
|
| 631 |
return cropped
|
| 632 |
-
return input_ids[:, start:end]
|
| 633 |
|
| 634 |
def _ttt_sample_batch(
|
| 635 |
self,
|
|
@@ -638,69 +651,69 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 638 |
) -> tuple[torch.Tensor | dict[str, torch.Tensor], torch.Tensor]:
|
| 639 |
cfg = self.ttt_config
|
| 640 |
batch = self._ttt_sample_crop(tokenized, generator)
|
| 641 |
-
input_ids = self._ttt_input_ids_from_batch(batch)
|
| 642 |
-
row_has_residue = self._ttt_non_special_mask(input_ids).any(dim=1)
|
| 643 |
-
eligible_rows = torch.where(row_has_residue)[0]
|
| 644 |
if eligible_rows.numel() == 0:
|
| 645 |
raise ValueError(
|
| 646 |
"TTT sampled batch contains no trainable biological residue tokens."
|
| 647 |
)
|
| 648 |
-
sampled_row_indices = torch.randint(
|
| 649 |
eligible_rows.numel(),
|
| 650 |
(cfg.batch_size,),
|
| 651 |
generator=generator,
|
| 652 |
device=input_ids.device,
|
| 653 |
)
|
| 654 |
-
rows = eligible_rows[sampled_row_indices]
|
| 655 |
if isinstance(batch, dict):
|
| 656 |
sampled: torch.Tensor | dict[str, torch.Tensor] = {}
|
| 657 |
for name, tensor in batch.items():
|
| 658 |
if tensor.ndim >= 1 and tensor.shape[0] == input_ids.shape[0]:
|
| 659 |
-
sampled[name] = tensor.index_select(0, rows)
|
| 660 |
else:
|
| 661 |
sampled[name] = tensor
|
| 662 |
else:
|
| 663 |
-
sampled = input_ids.index_select(0, rows)
|
| 664 |
|
| 665 |
-
sampled_ids = self._ttt_input_ids_from_batch(sampled)
|
| 666 |
-
labels = sampled_ids.clone()
|
| 667 |
-
non_special = self._ttt_non_special_mask(sampled_ids)
|
| 668 |
-
label_mask = torch.zeros_like(non_special)
|
| 669 |
for row_idx in range(sampled_ids.shape[0]):
|
| 670 |
-
candidate_positions = torch.where(non_special[row_idx])[0]
|
| 671 |
if candidate_positions.numel() == 0:
|
| 672 |
continue
|
| 673 |
num_mask = max(1, round(candidate_positions.numel() * cfg.mask_ratio))
|
| 674 |
-
order = torch.randperm(
|
| 675 |
candidate_positions.numel(),
|
| 676 |
generator=generator,
|
| 677 |
device=sampled_ids.device,
|
| 678 |
)
|
| 679 |
-
chosen = candidate_positions[order[:num_mask]]
|
| 680 |
label_mask[row_idx, chosen] = True
|
| 681 |
-
labels = labels.masked_fill(~label_mask, -100)
|
| 682 |
|
| 683 |
-
masked_ids = sampled_ids.clone()
|
| 684 |
-
chosen_positions = torch.where(label_mask)
|
| 685 |
if chosen_positions[0].numel() > 0:
|
| 686 |
-
random_values = torch.rand(
|
| 687 |
chosen_positions[0].shape,
|
| 688 |
generator=generator,
|
| 689 |
device=sampled_ids.device,
|
| 690 |
)
|
| 691 |
-
leave = random_values < cfg.bert_leave_prob
|
| 692 |
-
replace = (random_values >= cfg.bert_leave_prob) & (
|
| 693 |
random_values < cfg.bert_leave_prob + cfg.bert_replace_prob
|
| 694 |
)
|
| 695 |
-
mask = ~(leave | replace)
|
| 696 |
if mask.any():
|
| 697 |
masked_ids[
|
| 698 |
chosen_positions[0][mask],
|
| 699 |
chosen_positions[1][mask],
|
| 700 |
] = self._ttt_mask_token()
|
| 701 |
if replace.any():
|
| 702 |
-
replacement_tokens = self._ttt_replacement_tokens(sampled_ids)
|
| 703 |
-
replacement_idx = torch.randint(
|
| 704 |
replacement_tokens.shape[0],
|
| 705 |
(int(replace.sum().item()),),
|
| 706 |
generator=generator,
|
|
@@ -711,10 +724,10 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 711 |
chosen_positions[1][replace],
|
| 712 |
] = replacement_tokens[replacement_idx]
|
| 713 |
|
| 714 |
-
return self._ttt_set_input_ids(sampled, masked_ids), labels
|
| 715 |
|
| 716 |
@contextlib.contextmanager
|
| 717 |
-
def _ttt_seed_scope(self, seed: int | None) ->
|
| 718 |
if seed is None:
|
| 719 |
yield
|
| 720 |
return
|
|
@@ -736,9 +749,9 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 736 |
self,
|
| 737 |
seq: str | list[str] | None = None,
|
| 738 |
input_ids: torch.Tensor | None = None,
|
| 739 |
-
ttt_config: TTTConfig |
|
| 740 |
-
**kwargs:
|
| 741 |
-
) -> dict[str,
|
| 742 |
if ttt_config is not None:
|
| 743 |
if "_ttt_initialized" in self.__dict__ and self._ttt_initialized:
|
| 744 |
next_cfg = self.ttt_config.merged(ttt_config)
|
|
@@ -788,7 +801,7 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 788 |
module_modes = {module: module.training for module in self.modules()}
|
| 789 |
requires_grad = {param: param.requires_grad for param in self.parameters()}
|
| 790 |
losses: list[float] = []
|
| 791 |
-
step_metrics: list[dict[str,
|
| 792 |
best_state: list[dict[str, torch.Tensor]] | None = None
|
| 793 |
best_metric: float | None = None
|
| 794 |
best_step = 0
|
|
@@ -805,14 +818,17 @@ class FastPLMTestTimeTrainingMixin:
|
|
| 805 |
optimizer.zero_grad(set_to_none=True)
|
| 806 |
total_micro_steps = cfg.steps * cfg.ags
|
| 807 |
for micro_step in range(total_micro_steps):
|
| 808 |
-
batch, labels = self._ttt_sample_batch(
|
|
|
|
|
|
|
|
|
|
| 809 |
if not bool(labels.ne(-100).any()):
|
| 810 |
raise RuntimeError(
|
| 811 |
"TTT produced an all-ignored label batch; refusing a NaN update."
|
| 812 |
)
|
| 813 |
-
logits = self._ttt_predict_logits(batch, **kwargs)
|
| 814 |
-
labels = labels.to(device=logits.device)
|
| 815 |
-
loss = F.cross_entropy(
|
| 816 |
logits.reshape(-1, logits.shape[-1]),
|
| 817 |
labels.reshape(-1),
|
| 818 |
ignore_index=-100,
|
|
|
|
| 3 |
import contextlib
|
| 4 |
import math
|
| 5 |
import numbers
|
|
|
|
|
|
|
|
|
|
| 6 |
import torch
|
| 7 |
import torch.nn as nn
|
| 8 |
import torch.nn.functional as F
|
| 9 |
+
from collections.abc import Iterator, Mapping
|
| 10 |
+
from dataclasses import asdict, dataclass, fields
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
|
| 14 |
_STANDARD_AMINO_ACIDS = "ACDEFGHIKLMNPQRSTVWY"
|
| 15 |
_TTT_SERIALIZATION_VERSION = 1
|
|
|
|
| 43 |
self.verify()
|
| 44 |
|
| 45 |
@classmethod
|
| 46 |
+
def from_kwargs(cls, **kwargs: Any) -> TTTConfig:
|
| 47 |
valid_names = {field.name for field in fields(cls)}
|
| 48 |
unknown_names = set(kwargs) - valid_names
|
| 49 |
if unknown_names:
|
|
|
|
| 54 |
kwargs["lora_target_modules"] = tuple(kwargs["lora_target_modules"])
|
| 55 |
return cls(**kwargs)
|
| 56 |
|
| 57 |
+
def merged(self, overrides: Mapping[str, Any] | TTTConfig | None) -> TTTConfig:
|
| 58 |
if overrides is None:
|
| 59 |
return self
|
| 60 |
if isinstance(overrides, TTTConfig):
|
|
|
|
| 66 |
values[name] = value
|
| 67 |
return TTTConfig(**values)
|
| 68 |
|
| 69 |
+
def to_dict(self) -> dict[str, Any]:
|
| 70 |
return asdict(self)
|
| 71 |
|
| 72 |
def verify(self) -> None:
|
|
|
|
| 225 |
return self.linear._parameters["bias"]
|
| 226 |
|
| 227 |
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 228 |
+
# x: (..., d_in)
|
| 229 |
+
base = self.linear(x) # (..., d_out)
|
| 230 |
+
delta = ( # (..., d_out)
|
| 231 |
+
self.lora_up(self.lora_down(x.to(dtype=torch.float32))) * self.scale
|
| 232 |
+
)
|
| 233 |
+
return base + delta.to(dtype=base.dtype) # (..., d_out)
|
| 234 |
|
| 235 |
def reset_lora_parameters(self) -> None:
|
| 236 |
with torch.no_grad():
|
|
|
|
| 239 |
|
| 240 |
|
| 241 |
class FastPLMTestTimeTrainingMixin:
|
| 242 |
+
def init_ttt(self, ttt_config: TTTConfig | Mapping[str, Any] | None = None) -> None:
|
| 243 |
base_config = self.__dict__.get("_ttt_cfg")
|
| 244 |
if base_config is None:
|
| 245 |
base_config = TTTConfig()
|
|
|
|
| 249 |
serialized = getattr(getattr(self, "config", None), "fastplms_ttt", None)
|
| 250 |
serialized_initialized = False
|
| 251 |
if serialized is not None:
|
| 252 |
+
if not isinstance(serialized, Mapping):
|
| 253 |
raise ValueError("config.fastplms_ttt must be a mapping.")
|
| 254 |
version = serialized.get("version")
|
| 255 |
if version != _TTT_SERIALIZATION_VERSION:
|
|
|
|
| 258 |
f"{version!r}; expected {_TTT_SERIALIZATION_VERSION}."
|
| 259 |
)
|
| 260 |
serialized_config = serialized.get("config")
|
| 261 |
+
if not isinstance(serialized_config, Mapping):
|
| 262 |
raise ValueError("Serialized FastPLMs TTT state is missing its config mapping.")
|
| 263 |
configured = TTTConfig.from_kwargs(**dict(serialized_config))
|
| 264 |
initialized_value = serialized.get("initialized", False)
|
|
|
|
| 289 |
self,
|
| 290 |
seq: str | list[str] | None = None,
|
| 291 |
input_ids: torch.Tensor | None = None,
|
| 292 |
+
**kwargs: Any,
|
| 293 |
) -> torch.Tensor | dict[str, torch.Tensor]:
|
| 294 |
del kwargs
|
| 295 |
if input_ids is not None:
|
| 296 |
+
return input_ids # (b, l)
|
| 297 |
if seq is None:
|
| 298 |
raise ValueError("Pass either seq or input_ids for TTT.")
|
| 299 |
tokenized = self.tokenizer(seq, return_tensors="pt", padding=True)
|
| 300 |
+
return tokenized["input_ids"] # (b, l)
|
| 301 |
|
| 302 |
def _ttt_mask_token(self) -> int:
|
| 303 |
return int(self.tokenizer.mask_token_id)
|
|
|
|
| 306 |
return int(self.tokenizer.pad_token_id)
|
| 307 |
|
| 308 |
def _ttt_replacement_tokens(self, input_ids: torch.Tensor) -> torch.Tensor:
|
| 309 |
+
# input_ids: (b, l)
|
| 310 |
tokenizer = self.tokenizer
|
| 311 |
special_ids = set(tokenizer.all_special_ids)
|
| 312 |
vocab_size = int(self.config.vocab_size)
|
|
|
|
| 314 |
if unknown_id is not None:
|
| 315 |
special_ids.add(int(unknown_id))
|
| 316 |
|
| 317 |
+
vocab: Mapping[str, Any] = {}
|
| 318 |
get_vocab = getattr(tokenizer, "get_vocab", None)
|
| 319 |
if callable(get_vocab):
|
| 320 |
vocab = get_vocab()
|
| 321 |
+
elif isinstance(getattr(tokenizer, "vocab", None), Mapping):
|
| 322 |
vocab = tokenizer.vocab
|
| 323 |
+
elif isinstance(getattr(tokenizer, "_token_to_id", None), Mapping):
|
| 324 |
vocab = tokenizer._token_to_id
|
| 325 |
|
| 326 |
ids: list[int] = []
|
|
|
|
| 339 |
"TTT could not resolve any canonical amino-acid token IDs from the tokenizer; "
|
| 340 |
"refusing to sample arbitrary or reserved vocabulary entries."
|
| 341 |
)
|
| 342 |
+
return torch.tensor(ids, device=input_ids.device, dtype=input_ids.dtype) # (c_aa,)
|
| 343 |
|
| 344 |
def _ttt_predict_logits(
|
| 345 |
self,
|
| 346 |
batch: torch.Tensor | dict[str, torch.Tensor],
|
| 347 |
+
**kwargs: Any,
|
| 348 |
) -> torch.Tensor:
|
| 349 |
del kwargs
|
| 350 |
if isinstance(batch, dict):
|
| 351 |
output = self(**batch)
|
| 352 |
+
return output.logits # (b, l, c)
|
| 353 |
+
attention_mask = batch.ne(self._ttt_padding_token()) # (b, l)
|
| 354 |
output = self(input_ids=batch, attention_mask=attention_mask)
|
| 355 |
+
return output.logits # (b, l, c)
|
| 356 |
|
| 357 |
def _ttt_eval_step(
|
| 358 |
self,
|
|
|
|
| 360 |
loss: float,
|
| 361 |
seq: str | list[str] | None = None,
|
| 362 |
input_ids: torch.Tensor | None = None,
|
| 363 |
+
**kwargs: Any,
|
| 364 |
+
) -> tuple[dict[str, Any], float | None]:
|
| 365 |
del step, loss, seq, input_ids, kwargs
|
| 366 |
return {}, None
|
| 367 |
|
|
|
|
| 448 |
for module in self._ttt_lora_modules():
|
| 449 |
snapshot.append(
|
| 450 |
{
|
| 451 |
+
"lora_down.weight": module.lora_down.weight.detach().clone(), # (r, d_in)
|
| 452 |
+
"lora_up.weight": module.lora_up.weight.detach().clone(), # (d_out, r)
|
| 453 |
}
|
| 454 |
)
|
| 455 |
if not snapshot:
|
|
|
|
| 478 |
for module in self._ttt_lora_modules():
|
| 479 |
module.reset_lora_parameters()
|
| 480 |
|
| 481 |
+
def _ttt_serialized_contract(self) -> dict[str, Any]:
|
| 482 |
return {
|
| 483 |
"version": _TTT_SERIALIZATION_VERSION,
|
| 484 |
"initialized": bool(self._ttt_initialized),
|
| 485 |
"config": self.ttt_config.to_dict(),
|
| 486 |
}
|
| 487 |
|
| 488 |
+
def save_pretrained(self, save_directory: Any, *args: Any, **kwargs: Any) -> Any:
|
| 489 |
"""Save initialized adapters, their reset baseline, and the TTT config.
|
| 490 |
|
| 491 |
Adapter injection changes the module tree, so the serialized config must
|
|
|
|
| 528 |
device: torch.device,
|
| 529 |
) -> torch.Tensor | dict[str, torch.Tensor]:
|
| 530 |
if isinstance(batch, dict):
|
| 531 |
+
return {name: tensor.to(device) for name, tensor in batch.items()} # unchanged shapes
|
| 532 |
+
return batch.to(device) # unchanged shape
|
| 533 |
|
| 534 |
def _ttt_input_ids_from_batch(
|
| 535 |
self,
|
| 536 |
batch: torch.Tensor | dict[str, torch.Tensor],
|
| 537 |
) -> torch.Tensor:
|
| 538 |
if isinstance(batch, dict):
|
| 539 |
+
return batch["input_ids"] # (b, l)
|
| 540 |
+
return batch # (b, l)
|
| 541 |
|
| 542 |
def _ttt_set_input_ids(
|
| 543 |
self,
|
|
|
|
| 546 |
) -> torch.Tensor | dict[str, torch.Tensor]:
|
| 547 |
if isinstance(batch, dict):
|
| 548 |
updated = dict(batch)
|
| 549 |
+
updated["input_ids"] = input_ids # (b, l)
|
| 550 |
return updated
|
| 551 |
+
return input_ids # (b, l)
|
| 552 |
|
| 553 |
def _ttt_non_special_mask(self, input_ids: torch.Tensor) -> torch.Tensor:
|
| 554 |
+
# input_ids: (b, l)
|
| 555 |
+
residue_ids = self._ttt_replacement_tokens(input_ids) # (c_aa,)
|
| 556 |
+
return torch.isin(input_ids, residue_ids) # (b, l)
|
| 557 |
|
| 558 |
def _ttt_validate_tokenized_batch(
|
| 559 |
self,
|
|
|
|
| 576 |
"DPLM2 TTT could not resolve the structure-token boundary safely."
|
| 577 |
)
|
| 578 |
pad_token = self._ttt_padding_token()
|
| 579 |
+
generic_aa_special_ids = torch.tensor( # (4,)
|
| 580 |
[int(self.config.vocab_size) + offset for offset in range(4)],
|
| 581 |
device=input_ids.device,
|
| 582 |
dtype=input_ids.dtype,
|
| 583 |
)
|
| 584 |
+
is_structure = input_ids.ge(int(struct_boundary)) & input_ids.ne( # (b, l)
|
| 585 |
+
pad_token
|
| 586 |
+
)
|
| 587 |
is_structure &= ~torch.isin(input_ids, generic_aa_special_ids)
|
| 588 |
if bool(is_structure.any()):
|
| 589 |
raise ValueError(
|
|
|
|
| 592 |
)
|
| 593 |
|
| 594 |
if isinstance(batch, dict) and "type_ids" in batch:
|
| 595 |
+
type_ids = batch["type_ids"] # (b, l)
|
| 596 |
+
attention_mask = batch.get( # (b, l)
|
| 597 |
+
"attention_mask",
|
| 598 |
+
input_ids.ne(pad_token),
|
| 599 |
+
).bool()
|
| 600 |
if bool(((type_ids == int(self.config.struct_type)) & attention_mask).any()):
|
| 601 |
raise ValueError(
|
| 602 |
"DPLM2 TTT currently supports amino-acid-only inputs; structure "
|
|
|
|
| 618 |
cfg = self.ttt_config
|
| 619 |
if input_ids.shape[1] <= cfg.crop_size:
|
| 620 |
return batch
|
| 621 |
+
position_has_residue = ( # (l,)
|
| 622 |
+
self._ttt_non_special_mask(input_ids).any(dim=0).to(torch.int64)
|
| 623 |
+
)
|
| 624 |
+
prefix = F.pad(position_has_residue.cumsum(dim=0), (1, 0)) # (l + 1,)
|
| 625 |
+
window_counts = prefix[cfg.crop_size :] - prefix[: -cfg.crop_size] # (l-crop+1,)
|
| 626 |
+
valid_starts = torch.where(window_counts > 0)[0] # (n_valid,)
|
| 627 |
if valid_starts.numel() == 0:
|
| 628 |
raise ValueError("TTT could not find a crop containing a biological residue token.")
|
| 629 |
+
selected = torch.randint( # (1,)
|
| 630 |
valid_starts.numel(),
|
| 631 |
(1,),
|
| 632 |
generator=generator,
|
|
|
|
| 638 |
cropped = {}
|
| 639 |
for name, tensor in batch.items():
|
| 640 |
if tensor.ndim >= 2 and tensor.shape[1] == input_ids.shape[1]:
|
| 641 |
+
cropped[name] = tensor[:, start:end] # (b, crop_size, ...)
|
| 642 |
else:
|
| 643 |
cropped[name] = tensor
|
| 644 |
return cropped
|
| 645 |
+
return input_ids[:, start:end] # (b, crop_size)
|
| 646 |
|
| 647 |
def _ttt_sample_batch(
|
| 648 |
self,
|
|
|
|
| 651 |
) -> tuple[torch.Tensor | dict[str, torch.Tensor], torch.Tensor]:
|
| 652 |
cfg = self.ttt_config
|
| 653 |
batch = self._ttt_sample_crop(tokenized, generator)
|
| 654 |
+
input_ids = self._ttt_input_ids_from_batch(batch) # (b, l)
|
| 655 |
+
row_has_residue = self._ttt_non_special_mask(input_ids).any(dim=1) # (b,)
|
| 656 |
+
eligible_rows = torch.where(row_has_residue)[0] # (n_eligible,)
|
| 657 |
if eligible_rows.numel() == 0:
|
| 658 |
raise ValueError(
|
| 659 |
"TTT sampled batch contains no trainable biological residue tokens."
|
| 660 |
)
|
| 661 |
+
sampled_row_indices = torch.randint( # (b_sample,)
|
| 662 |
eligible_rows.numel(),
|
| 663 |
(cfg.batch_size,),
|
| 664 |
generator=generator,
|
| 665 |
device=input_ids.device,
|
| 666 |
)
|
| 667 |
+
rows = eligible_rows[sampled_row_indices] # (b_sample,)
|
| 668 |
if isinstance(batch, dict):
|
| 669 |
sampled: torch.Tensor | dict[str, torch.Tensor] = {}
|
| 670 |
for name, tensor in batch.items():
|
| 671 |
if tensor.ndim >= 1 and tensor.shape[0] == input_ids.shape[0]:
|
| 672 |
+
sampled[name] = tensor.index_select(0, rows) # (b_sample, ...)
|
| 673 |
else:
|
| 674 |
sampled[name] = tensor
|
| 675 |
else:
|
| 676 |
+
sampled = input_ids.index_select(0, rows) # (b_sample, l)
|
| 677 |
|
| 678 |
+
sampled_ids = self._ttt_input_ids_from_batch(sampled) # (b_sample, l)
|
| 679 |
+
labels = sampled_ids.clone() # (b_sample, l)
|
| 680 |
+
non_special = self._ttt_non_special_mask(sampled_ids) # (b_sample, l)
|
| 681 |
+
label_mask = torch.zeros_like(non_special) # (b_sample, l)
|
| 682 |
for row_idx in range(sampled_ids.shape[0]):
|
| 683 |
+
candidate_positions = torch.where(non_special[row_idx])[0] # (n_candidates,)
|
| 684 |
if candidate_positions.numel() == 0:
|
| 685 |
continue
|
| 686 |
num_mask = max(1, round(candidate_positions.numel() * cfg.mask_ratio))
|
| 687 |
+
order = torch.randperm( # (n_candidates,)
|
| 688 |
candidate_positions.numel(),
|
| 689 |
generator=generator,
|
| 690 |
device=sampled_ids.device,
|
| 691 |
)
|
| 692 |
+
chosen = candidate_positions[order[:num_mask]] # (n_mask,)
|
| 693 |
label_mask[row_idx, chosen] = True
|
| 694 |
+
labels = labels.masked_fill(~label_mask, -100) # (b_sample, l)
|
| 695 |
|
| 696 |
+
masked_ids = sampled_ids.clone() # (b_sample, l)
|
| 697 |
+
chosen_positions = torch.where(label_mask) # two (n_chosen,) tensors
|
| 698 |
if chosen_positions[0].numel() > 0:
|
| 699 |
+
random_values = torch.rand( # (n_chosen,)
|
| 700 |
chosen_positions[0].shape,
|
| 701 |
generator=generator,
|
| 702 |
device=sampled_ids.device,
|
| 703 |
)
|
| 704 |
+
leave = random_values < cfg.bert_leave_prob # (n_chosen,)
|
| 705 |
+
replace = (random_values >= cfg.bert_leave_prob) & ( # (n_chosen,)
|
| 706 |
random_values < cfg.bert_leave_prob + cfg.bert_replace_prob
|
| 707 |
)
|
| 708 |
+
mask = ~(leave | replace) # (n_chosen,)
|
| 709 |
if mask.any():
|
| 710 |
masked_ids[
|
| 711 |
chosen_positions[0][mask],
|
| 712 |
chosen_positions[1][mask],
|
| 713 |
] = self._ttt_mask_token()
|
| 714 |
if replace.any():
|
| 715 |
+
replacement_tokens = self._ttt_replacement_tokens(sampled_ids) # (c_aa,)
|
| 716 |
+
replacement_idx = torch.randint( # (n_replace,)
|
| 717 |
replacement_tokens.shape[0],
|
| 718 |
(int(replace.sum().item()),),
|
| 719 |
generator=generator,
|
|
|
|
| 724 |
chosen_positions[1][replace],
|
| 725 |
] = replacement_tokens[replacement_idx]
|
| 726 |
|
| 727 |
+
return self._ttt_set_input_ids(sampled, masked_ids), labels # batch, (b_sample, l)
|
| 728 |
|
| 729 |
@contextlib.contextmanager
|
| 730 |
+
def _ttt_seed_scope(self, seed: int | None) -> Iterator[None]:
|
| 731 |
if seed is None:
|
| 732 |
yield
|
| 733 |
return
|
|
|
|
| 749 |
self,
|
| 750 |
seq: str | list[str] | None = None,
|
| 751 |
input_ids: torch.Tensor | None = None,
|
| 752 |
+
ttt_config: TTTConfig | Mapping[str, Any] | None = None,
|
| 753 |
+
**kwargs: Any,
|
| 754 |
+
) -> dict[str, Any]:
|
| 755 |
if ttt_config is not None:
|
| 756 |
if "_ttt_initialized" in self.__dict__ and self._ttt_initialized:
|
| 757 |
next_cfg = self.ttt_config.merged(ttt_config)
|
|
|
|
| 801 |
module_modes = {module: module.training for module in self.modules()}
|
| 802 |
requires_grad = {param: param.requires_grad for param in self.parameters()}
|
| 803 |
losses: list[float] = []
|
| 804 |
+
step_metrics: list[dict[str, Any]] = []
|
| 805 |
best_state: list[dict[str, torch.Tensor]] | None = None
|
| 806 |
best_metric: float | None = None
|
| 807 |
best_step = 0
|
|
|
|
| 818 |
optimizer.zero_grad(set_to_none=True)
|
| 819 |
total_micro_steps = cfg.steps * cfg.ags
|
| 820 |
for micro_step in range(total_micro_steps):
|
| 821 |
+
batch, labels = self._ttt_sample_batch( # batch, (b_sample, l)
|
| 822 |
+
tokenized,
|
| 823 |
+
generator,
|
| 824 |
+
)
|
| 825 |
if not bool(labels.ne(-100).any()):
|
| 826 |
raise RuntimeError(
|
| 827 |
"TTT produced an all-ignored label batch; refusing a NaN update."
|
| 828 |
)
|
| 829 |
+
logits = self._ttt_predict_logits(batch, **kwargs) # (b_sample, l, c)
|
| 830 |
+
labels = labels.to(device=logits.device) # (b_sample, l)
|
| 831 |
+
loss = F.cross_entropy( # ()
|
| 832 |
logits.reshape(-1, logits.shape[-1]),
|
| 833 |
labels.reshape(-1),
|
| 834 |
ignore_index=-100,
|
fastplms/registry.py
CHANGED
|
@@ -18,6 +18,7 @@ from types import MappingProxyType
|
|
| 18 |
from typing import Any, Literal, cast
|
| 19 |
from urllib.parse import urlparse
|
| 20 |
|
|
|
|
| 21 |
_HEX_RE = re.compile(r"^[0-9a-f]+$")
|
| 22 |
_IDENTIFIER_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
|
| 23 |
_HUB_LICENSE_NAME_RE = re.compile(r"[^a-z0-9.]+")
|
|
@@ -559,20 +560,20 @@ def _require_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple
|
|
| 559 |
value = table.get(key)
|
| 560 |
if not isinstance(value, list) or not value or any(not isinstance(item, str) for item in value):
|
| 561 |
raise RegistryError(f"{context}.{key} must be a non-empty string array.")
|
| 562 |
-
|
| 563 |
-
if len(set(
|
| 564 |
raise RegistryError(f"{context}.{key} contains duplicate values.")
|
| 565 |
-
return
|
| 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 |
-
|
| 573 |
-
if len(set(
|
| 574 |
raise RegistryError(f"{context}.{key} contains duplicate values.")
|
| 575 |
-
return
|
| 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 |
-
|
| 661 |
-
paths = [item.path for item in
|
| 662 |
if len(paths) != len(set(paths)):
|
| 663 |
raise RegistryError(f"{context}.{key} contains duplicate paths.")
|
| 664 |
-
return
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 749 |
-
paths = [asset.path for asset in
|
| 750 |
-
urls = [asset.url for asset in
|
| 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(
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 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(
|
| 846 |
raise RegistryError("The manifest must pin both FlashAttention kernel versions.")
|
| 847 |
-
return
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 1413 |
except (UnicodeDecodeError, tomllib.TOMLDecodeError) as error:
|
| 1414 |
raise RegistryError(f"Unable to parse model manifest: {error}") from error
|
| 1415 |
-
_reject_unknown_fields(
|
| 1416 |
-
if
|
| 1417 |
raise RegistryError("Unsupported model manifest schema_version; expected 1.")
|
| 1418 |
-
legal_files = _require_digest_list(
|
| 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(
|
| 1423 |
-
upstreams = _parse_upstreams(
|
| 1424 |
-
families = _parse_families(
|
| 1425 |
-
runtime_assets = _parse_runtime_assets(
|
| 1426 |
-
models = _parse_models(
|
| 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
|
| 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 != "
|
| 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(
|
| 118 |
incoming = _runtime_file_hashes(package_root)
|
| 119 |
-
known =
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 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 |
-
|
| 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 |
-
|
| 164 |
-
roots = list(getattr(
|
| 165 |
if str(package_root) not in roots:
|
| 166 |
roots.append(str(package_root))
|
| 167 |
-
|
| 168 |
temporaries = list(
|
| 169 |
-
getattr(
|
| 170 |
)
|
| 171 |
for temporary in _RUNTIME_TEMPORARIES:
|
| 172 |
if temporary not in temporaries:
|
| 173 |
temporaries.append(temporary)
|
| 174 |
-
|
| 175 |
-
hashes = set(getattr(
|
| 176 |
hashes.add(RUNTIME_HASH)
|
| 177 |
-
|
| 178 |
_extend_loaded_package_paths(package_root)
|
| 179 |
-
return
|
| 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 |
-
|
| 191 |
-
hashes = getattr(
|
| 192 |
if RUNTIME_HASH in hashes:
|
| 193 |
-
return
|
| 194 |
package_root = _ensure_runtime()
|
| 195 |
-
if
|
| 196 |
-
return _merge_runtime(
|
| 197 |
spec = importlib.util.spec_from_file_location(
|
| 198 |
"fastplms",
|
| 199 |
package_root / "__init__.py",
|
|
@@ -223,14 +179,14 @@ def _install_runtime():
|
|
| 223 |
return package
|
| 224 |
|
| 225 |
_install_runtime()
|
| 226 |
-
|
| 227 |
-
FastEsmConfig =
|
| 228 |
FastEsmConfig.__module__ = __name__
|
| 229 |
-
FastEsmForMaskedLM =
|
| 230 |
FastEsmForMaskedLM.__module__ = __name__
|
| 231 |
-
FastEsmForSequenceClassification =
|
| 232 |
FastEsmForSequenceClassification.__module__ = __name__
|
| 233 |
-
FastEsmForTokenClassification =
|
| 234 |
FastEsmForTokenClassification.__module__ = __name__
|
| 235 |
-
FastEsmModel =
|
| 236 |
FastEsmModel.__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 != "f22353ef386f607784889dc303488a2dc5e99b009a5e423ab3eff1f1eb03a9a4":
|
| 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.esm2.modeling_fastesm")
|
| 183 |
+
FastEsmConfig = _module_181.FastEsmConfig
|
| 184 |
FastEsmConfig.__module__ = __name__
|
| 185 |
+
FastEsmForMaskedLM = _module_181.FastEsmForMaskedLM
|
| 186 |
FastEsmForMaskedLM.__module__ = __name__
|
| 187 |
+
FastEsmForSequenceClassification = _module_181.FastEsmForSequenceClassification
|
| 188 |
FastEsmForSequenceClassification.__module__ = __name__
|
| 189 |
+
FastEsmForTokenClassification = _module_181.FastEsmForTokenClassification
|
| 190 |
FastEsmForTokenClassification.__module__ = __name__
|
| 191 |
+
FastEsmModel = _module_181.FastEsmModel
|
| 192 |
FastEsmModel.__module__ = __name__
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Direct runtime dependencies for Synthyra/ESM2-3B.
|
| 2 |
+
# FastPLMs source is embedded in this model repository.
|
| 3 |
+
torch>=2.13,<2.14
|
| 4 |
+
transformers>=5.13,<5.14
|
| 5 |
+
huggingface-hub>=0.34,<2
|
| 6 |
+
tokenizers>=0.22,<0.23
|
| 7 |
+
safetensors>=0.5,<1
|
| 8 |
+
numpy>=1.26,<3
|
| 9 |
+
einops>=0.8,<1
|
| 10 |
+
tqdm>=4.67,<5
|
| 11 |
+
kernels>=0.15,<0.16
|
runtime-attestation.json
CHANGED
|
@@ -3,43 +3,44 @@
|
|
| 3 |
"LICENSES/FastPLMs-Apache-2.0.txt": "sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
|
| 4 |
"LICENSES/fair-esm/LICENSE": "sha256:da6d3703ed11cbe42bd212c725957c98da23cbff1998c05fa4b3d976d1a58e93",
|
| 5 |
"LICENSES/fair-esm/PROVENANCE.md": "sha256:950adb94daf15e646ddf226dacfe2a8e77801aa0793e439a9a3490a48eb666e7",
|
| 6 |
-
"README.md": "sha256:
|
| 7 |
"THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
|
| 8 |
-
"config.json": "sha256:
|
| 9 |
-
"fastplms/__init__.py": "sha256:
|
| 10 |
-
"fastplms/attention/__init__.py": "sha256:
|
| 11 |
-
"fastplms/attention/_core.py": "sha256:
|
| 12 |
-
"fastplms/attention/_kernel_lock.py": "sha256:
|
| 13 |
-
"fastplms/attention/interfaces.py": "sha256:
|
| 14 |
-
"fastplms/embeddings/__init__.py": "sha256:
|
| 15 |
-
"fastplms/embeddings/pooling.py": "sha256:
|
| 16 |
-
"fastplms/embeddings/runner.py": "sha256:
|
| 17 |
-
"fastplms/embeddings/storage.py": "sha256:
|
| 18 |
-
"fastplms/embeddings/types.py": "sha256:
|
| 19 |
"fastplms/kernels.lock": "sha256:8e274e6f8f25fe272d95bb42849f672ebb897a67511e82be2b86c2a4b4962cdd",
|
| 20 |
"fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
|
| 21 |
-
"fastplms/models/__init__.py": "sha256:
|
| 22 |
-
"fastplms/models/_esm_rotary.py": "sha256:
|
| 23 |
"fastplms/models/esm2/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
| 24 |
-
"fastplms/models/esm2/modeling_fastesm.py": "sha256:
|
| 25 |
-
"fastplms/models/ttt.py": "sha256:
|
| 26 |
-
"fastplms/registry.py": "sha256:
|
| 27 |
-
"fastplms/runtime.py": "sha256:
|
| 28 |
-
"fastplms_bundle.py": "sha256:
|
| 29 |
-
"modeling_fastplms.py": "sha256:
|
|
|
|
| 30 |
"special_tokens_map.json": "sha256:3aedcd4211c0d43aec4e607ff60a63255f3174ead795e997350f09a5f8cd9ee1",
|
| 31 |
"tokenizer_config.json": "sha256:7e9161ecdb548ec45a41cbc6b24aa4476fdd418461f491c4207baa99419a29ad",
|
| 32 |
"vocab.txt": "sha256:0b82cc0a7c7cf9e567b1e5892d793285b9fbae822c964ca48696f7db44598e03"
|
| 33 |
},
|
| 34 |
"model_id": "esm2_3b",
|
| 35 |
"redistributable": true,
|
| 36 |
-
"release_tool_revision": "
|
| 37 |
-
"release_tool_sha256": "
|
| 38 |
-
"runtime_bundle_sha256": "
|
| 39 |
-
"runtime_revision": "
|
| 40 |
"schema_version": 2,
|
| 41 |
"scope": "runtime-only",
|
| 42 |
-
"source_tree_sha256": "
|
| 43 |
"weights": {
|
| 44 |
"repo_id": "Synthyra/ESM2-3B",
|
| 45 |
"revision": "ff89d0180f414ab9c677219a25da79bf09185456"
|
|
|
|
| 3 |
"LICENSES/FastPLMs-Apache-2.0.txt": "sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
|
| 4 |
"LICENSES/fair-esm/LICENSE": "sha256:da6d3703ed11cbe42bd212c725957c98da23cbff1998c05fa4b3d976d1a58e93",
|
| 5 |
"LICENSES/fair-esm/PROVENANCE.md": "sha256:950adb94daf15e646ddf226dacfe2a8e77801aa0793e439a9a3490a48eb666e7",
|
| 6 |
+
"README.md": "sha256:1775e5fe2c71285b4c5c8b8af9adbe0e4694b90d3922069b52ae5852fa74482a",
|
| 7 |
"THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
|
| 8 |
+
"config.json": "sha256:147c67ca43b3a952ab393892a953a5be4a1bc61c2d89713d280f0a09144ba730",
|
| 9 |
+
"fastplms/__init__.py": "sha256:8503e02debf24abc6d33c1913ff3eb9f245e63f7e62017e220d072c7393fdf2c",
|
| 10 |
+
"fastplms/attention/__init__.py": "sha256:ab3c0b6156968f418f3c97ea664959cb978f4137717b4362213bcfe25ab5e3d6",
|
| 11 |
+
"fastplms/attention/_core.py": "sha256:09d0f71a5ea2ad2138c9441f6a2f72bbf8e72729be41d8db737b163f36ce47da",
|
| 12 |
+
"fastplms/attention/_kernel_lock.py": "sha256:56455eb57cee9af08438eda52b775c9cff3cf994cadb9b116ce2ee10fd5f1801",
|
| 13 |
+
"fastplms/attention/interfaces.py": "sha256:6562a342961022fc2848d7004bd01fddaf8be8e08e782d0856e5c30a74232623",
|
| 14 |
+
"fastplms/embeddings/__init__.py": "sha256:cf0648de905a00ad16c0aece328eeb05f25739a4cf89b47b915e0f2b19c3ce26",
|
| 15 |
+
"fastplms/embeddings/pooling.py": "sha256:9bed8c466fce053333de9e25b04d1ab304f2954f403ffa0f5a3c749e8277b3ce",
|
| 16 |
+
"fastplms/embeddings/runner.py": "sha256:a7839763ad641a63d0e2671dcf34aa7b2a8114198950b64254188521b27453a8",
|
| 17 |
+
"fastplms/embeddings/storage.py": "sha256:98cf725d26766f32300f105345129ca6cf02ebd10628e746a7e71e107bc911a6",
|
| 18 |
+
"fastplms/embeddings/types.py": "sha256:c58f8c6a37a11a937c3f77b4e1617d5f213923d98ecaffddcdd283bd9dc42fe7",
|
| 19 |
"fastplms/kernels.lock": "sha256:8e274e6f8f25fe272d95bb42849f672ebb897a67511e82be2b86c2a4b4962cdd",
|
| 20 |
"fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
|
| 21 |
+
"fastplms/models/__init__.py": "sha256:d3dd084f5e8fafcf2d0fe4a26e4ab9284d3f9414b1f9a5bd86e308a406447c43",
|
| 22 |
+
"fastplms/models/_esm_rotary.py": "sha256:8e041bc4a056373e07fbf598e6acb6075f889d1bce12057715369804befb37ec",
|
| 23 |
"fastplms/models/esm2/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
| 24 |
+
"fastplms/models/esm2/modeling_fastesm.py": "sha256:119462776260e6eea6409c7203491d9100bfa51418986e9354c2b12f3684d31b",
|
| 25 |
+
"fastplms/models/ttt.py": "sha256:f1d7f9298a930f4c40f8e321ffa08b09d1c5b8492bca623bdc7bfd03ec85774d",
|
| 26 |
+
"fastplms/registry.py": "sha256:cb37799dae5a4c0c780089ab2cca4cc56300c1c5f515b275892aa4e1c4b97eb6",
|
| 27 |
+
"fastplms/runtime.py": "sha256:9521c37fcd168bf9a4137277fb73672403ccf30d70d4845aa879fb22f42ddeb6",
|
| 28 |
+
"fastplms_bundle.py": "sha256:96be83e24e3ce995195d5a51e06d360e80ae1a60a5483494d2da74e0c7e601d6",
|
| 29 |
+
"modeling_fastplms.py": "sha256:ca2bed6fb4e70e8e24f271abed55283c8f179d6794c5d860a11f09a67e4260dc",
|
| 30 |
+
"requirements.txt": "sha256:a93eb8b0b6c7a4b3453d0af7465d72c1147d29a556be5182fe63a02194006767",
|
| 31 |
"special_tokens_map.json": "sha256:3aedcd4211c0d43aec4e607ff60a63255f3174ead795e997350f09a5f8cd9ee1",
|
| 32 |
"tokenizer_config.json": "sha256:7e9161ecdb548ec45a41cbc6b24aa4476fdd418461f491c4207baa99419a29ad",
|
| 33 |
"vocab.txt": "sha256:0b82cc0a7c7cf9e567b1e5892d793285b9fbae822c964ca48696f7db44598e03"
|
| 34 |
},
|
| 35 |
"model_id": "esm2_3b",
|
| 36 |
"redistributable": true,
|
| 37 |
+
"release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 38 |
+
"release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
|
| 39 |
+
"runtime_bundle_sha256": "f22353ef386f607784889dc303488a2dc5e99b009a5e423ab3eff1f1eb03a9a4",
|
| 40 |
+
"runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 41 |
"schema_version": 2,
|
| 42 |
"scope": "runtime-only",
|
| 43 |
+
"source_tree_sha256": "818bf2bf5f573adc842b55f4f8feff926ddc25f904c5769ff5c382077d3a78a2",
|
| 44 |
"weights": {
|
| 45 |
"repo_id": "Synthyra/ESM2-3B",
|
| 46 |
"revision": "ff89d0180f414ab9c677219a25da79bf09185456"
|