Transformers
Safetensors
t5
text2text-generation
protein-language-model
fastplms
custom_code
text-generation-inference
Instructions to use Synthyra/ANKH2_large with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Synthyra/ANKH2_large with Transformers:
# Load model directly from transformers import AutoTokenizer, AutoModelForSeq2SeqLM tokenizer = AutoTokenizer.from_pretrained("Synthyra/ANKH2_large", trust_remote_code=True) model = AutoModelForSeq2SeqLM.from_pretrained("Synthyra/ANKH2_large", 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 -45
- 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/ankh/modeling_ankh.py +43 -25
- fastplms/models/ttt.py +97 -81
- fastplms/registry.py +45 -44
- fastplms/runtime.py +1 -0
- fastplms_bundle.py +0 -0
- modeling_fastplms.py +30 -74
- requirements.txt +10 -0
- runtime-attestation.json +25 -24
README.md
CHANGED
|
@@ -18,15 +18,34 @@ Supported Transformers entry points are `AutoConfig`, `AutoModel`,
|
|
| 18 |
`AutoModelForMaskedLM`, `AutoModelForSeq2SeqLM`,
|
| 19 |
`AutoModelForSequenceClassification`, `AutoModelForTokenClassification`.
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
## Install and platform requirements
|
| 22 |
|
| 23 |
-
Install the
|
| 24 |
|
| 25 |
```bash
|
| 26 |
-
python -m pip install \
|
| 27 |
-
"
|
| 28 |
```
|
| 29 |
|
|
|
|
|
|
|
|
|
|
| 30 |
Python 3.11-3.14, PyTorch 2.13, and Transformers 5.13 are required. The declared CPU gate covers tiny offline contracts; published checkpoint throughput and parity require the documented device tier. The Hub quick start below requires network
|
| 31 |
access on first download. For an air-gapped run, first build the manifest-pinned
|
| 32 |
local artifact and use the offline form shown in the example.
|
|
@@ -40,22 +59,22 @@ model_id = "Synthyra/ANKH2_large"
|
|
| 40 |
model = AutoModel.from_pretrained(
|
| 41 |
model_id,
|
| 42 |
trust_remote_code=True,
|
|
|
|
| 43 |
).eval()
|
| 44 |
```
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
calls are unchanged.
|
| 58 |
-
For BF16 execution, this family uses parameters loaded directly in BF16.
|
| 59 |
|
| 60 |
## Tokenization and forward inference
|
| 61 |
|
|
@@ -86,8 +105,8 @@ print(output.last_hidden_state.shape)
|
|
| 86 |
|
| 87 |
## Dataset embeddings
|
| 88 |
|
| 89 |
-
Dataset embeddings default to the encoder
|
| 90 |
-
|
| 91 |
|
| 92 |
```python
|
| 93 |
encoder_result = model.embed_dataset(
|
|
@@ -96,20 +115,11 @@ encoder_result = model.embed_dataset(
|
|
| 96 |
hidden_state_index=-1,
|
| 97 |
full_embeddings=True,
|
| 98 |
)
|
| 99 |
-
|
| 100 |
-
["MSTNPKPQRKTKRNT"],
|
| 101 |
-
hidden_state_source="encoder",
|
| 102 |
-
store_all_hidden_states=True,
|
| 103 |
-
full_embeddings=True,
|
| 104 |
-
)
|
| 105 |
```
|
| 106 |
|
| 107 |
Decoder representations require `AutoModelForSeq2SeqLM` and exactly one
|
| 108 |
-
|
| 109 |
-
does not infer a shifted source sequence because official tasks use prompts,
|
| 110 |
-
sentinel tokens, or generated tokens that depend on the task. Protein inputs
|
| 111 |
-
remain raw residue strings and sentinel prompts remain tight, as in
|
| 112 |
-
`M<extra_id_0>`:
|
| 113 |
|
| 114 |
```python
|
| 115 |
from transformers import AutoModelForSeq2SeqLM
|
|
@@ -125,12 +135,105 @@ decoder_result = seq2seq.embed_dataset(
|
|
| 125 |
decoder_inputs=["M<extra_id_0>"],
|
| 126 |
full_embeddings=True,
|
| 127 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
```
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
positions. Persisted results record the selected stack and layer, decoder input
|
| 133 |
-
and mask fingerprints, input-position alignment, and biological-mask policy.
|
| 134 |
|
| 135 |
## Encoder and sequence-to-sequence use
|
| 136 |
|
|
@@ -177,7 +280,7 @@ masked-LM extension and is not an official ANKH head.
|
|
| 177 |
- Precision policies: `default`
|
| 178 |
- BF16 execution: `static_parameters`
|
| 179 |
- Generation contract: `required`
|
| 180 |
-
-
|
| 181 |
- Weight publication allowed: `true`
|
| 182 |
- Weight license status: `resolved`
|
| 183 |
- Redistributable: `true`
|
|
@@ -188,30 +291,24 @@ masked-LM extension and is not an official ANKH head.
|
|
| 188 |
- FastPLMs weights: `Synthyra/ANKH2_large`
|
| 189 |
- Runtime revision: recorded separately in the built artifact and published commit
|
| 190 |
- Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
|
| 191 |
-
- Generator/schema version and complete/runtime-only attestations: recorded in `provenance.json`
|
| 192 |
- Canonical transformed state SHA-256: `597c4fe2fa8711f11a25317905f1d62fa92905e55fdd5c0a79614cd9c9d2bca3`
|
| 193 |
- Conversion equality attestation: recorded in `provenance.json`
|
| 194 |
- Official checkpoint: `ElnaggarLab/ankh2-ext2`
|
| 195 |
- Artifact source: `official`
|
| 196 |
- State transform: `ankh_t5_to_fastplms_v1`
|
| 197 |
-
- BF16 execution: `static_parameters`
|
| 198 |
- Pinned upstreams: `ankh`
|
| 199 |
-
- Reference container: `reference-ankh`
|
| 200 |
- Release tiers: `check`, `compliance`, `feature`, `artifact`, `benchmark`
|
| 201 |
- Unresolved required file identities: `0`
|
| 202 |
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
release blocker.
|
| 206 |
|
| 207 |
## Validation boundary
|
| 208 |
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
passed, that one backend is faster, or that an output has biological or
|
| 214 |
-
therapeutic validity.
|
| 215 |
|
| 216 |
## License
|
| 217 |
|
|
|
|
| 18 |
`AutoModelForMaskedLM`, `AutoModelForSeq2SeqLM`,
|
| 19 |
`AutoModelForSequenceClassification`, `AutoModelForTokenClassification`.
|
| 20 |
|
| 21 |
+
## Capabilities
|
| 22 |
+
|
| 23 |
+
| Feature | Status |
|
| 24 |
+
| --- | --- |
|
| 25 |
+
| Sequence classification | Supported: base weights with an untrained task head |
|
| 26 |
+
| Token classification | Supported: base weights with an untrained task head |
|
| 27 |
+
| PEFT fine-tuning | Supported pattern: preserve the separately trained `classifier` |
|
| 28 |
+
| Embeddings | Special: encoder or explicitly prepared decoder states |
|
| 29 |
+
| Test-time training | Supported: low-rank masked-residue adaptation |
|
| 30 |
+
| Attention variants | Supported: `eager`, `sdpa` |
|
| 31 |
+
| Compliance | Declared: exact release evidence is required |
|
| 32 |
+
|
| 33 |
+
A supported interface is not a pretrained downstream predictor. Classification
|
| 34 |
+
heads start untrained, and declared compliance metadata is not a claim that an
|
| 35 |
+
arbitrary local build passed its release gate.
|
| 36 |
+
|
| 37 |
## Install and platform requirements
|
| 38 |
|
| 39 |
+
Install the direct dependencies published with this model:
|
| 40 |
|
| 41 |
```bash
|
| 42 |
+
python -m pip install -r \
|
| 43 |
+
"https://huggingface.co/Synthyra/ANKH2_large/resolve/main/requirements.txt"
|
| 44 |
```
|
| 45 |
|
| 46 |
+
The FastPLMs implementation itself is embedded in the model repository and loaded
|
| 47 |
+
by Transformers through `trust_remote_code=True`.
|
| 48 |
+
|
| 49 |
Python 3.11-3.14, PyTorch 2.13, and Transformers 5.13 are required. The declared CPU gate covers tiny offline contracts; published checkpoint throughput and parity require the documented device tier. The Hub quick start below requires network
|
| 50 |
access on first download. For an air-gapped run, first build the manifest-pinned
|
| 51 |
local artifact and use the offline form shown in the example.
|
|
|
|
| 59 |
model = AutoModel.from_pretrained(
|
| 60 |
model_id,
|
| 61 |
trust_remote_code=True,
|
| 62 |
+
attn_implementation="sdpa",
|
| 63 |
).eval()
|
| 64 |
```
|
| 65 |
|
| 66 |
+
For offline validation, replace `model_id` with the manifest-built
|
| 67 |
+
`dist/hub/ANKH2_large` path and pass `local_files_only=True`.
|
| 68 |
+
|
| 69 |
+
## Attention and compliance
|
| 70 |
|
| 71 |
+
The quick start selects `sdpa` explicitly. Declared variants are `eager`, `sdpa`. An unavailable requested backend raises
|
| 72 |
+
instead of silently switching implementations.
|
| 73 |
+
`output_attentions=True` may use the documented, one-call eager fallback solely
|
| 74 |
+
to materialize attention tensors; the configured backend remains unchanged.
|
| 75 |
+
|
| 76 |
+
This family declares the `compliance` tier. Release evidence binds the exact
|
| 77 |
+
checkpoint, backend, dtype, hardware, inputs, and reference revision.
|
|
|
|
|
|
|
| 78 |
|
| 79 |
## Tokenization and forward inference
|
| 80 |
|
|
|
|
| 105 |
|
| 106 |
## Dataset embeddings
|
| 107 |
|
| 108 |
+
Dataset embeddings default to the encoder final state. Select a native encoder
|
| 109 |
+
layer directly:
|
| 110 |
|
| 111 |
```python
|
| 112 |
encoder_result = model.embed_dataset(
|
|
|
|
| 115 |
hidden_state_index=-1,
|
| 116 |
full_embeddings=True,
|
| 117 |
)
|
| 118 |
+
print(encoder_result[0].tensor.shape) # (l, d)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
```
|
| 120 |
|
| 121 |
Decoder representations require `AutoModelForSeq2SeqLM` and exactly one
|
| 122 |
+
aligned decoder input. ANKH does not invent a shifted target:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
```python
|
| 125 |
from transformers import AutoModelForSeq2SeqLM
|
|
|
|
| 135 |
decoder_inputs=["M<extra_id_0>"],
|
| 136 |
full_embeddings=True,
|
| 137 |
)
|
| 138 |
+
print(decoder_result[0].tensor.shape) # (decoder_length, d)
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
Pooling excludes boundary, padding, sentinel, and other non-biological
|
| 142 |
+
positions. Persisted results record the selected stack, layer, inputs, masks,
|
| 143 |
+
and alignment policy.
|
| 144 |
+
|
| 145 |
+
## Downstream classification
|
| 146 |
+
|
| 147 |
+
Both downstream AutoClasses reuse the checkpoint backbone and initialize a new,
|
| 148 |
+
untrained `classifier`. Sequence labels have shape `(b,)`; residue labels have
|
| 149 |
+
shape `(b, l)` and use `-100` outside biological positions:
|
| 150 |
+
|
| 151 |
+
```python
|
| 152 |
+
import torch
|
| 153 |
+
from transformers import AutoTokenizer
|
| 154 |
+
from transformers import (
|
| 155 |
+
AutoModelForSequenceClassification,
|
| 156 |
+
AutoModelForTokenClassification,
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
model_id = "Synthyra/ANKH2_large"
|
| 160 |
+
sequence_model = AutoModelForSequenceClassification.from_pretrained(
|
| 161 |
+
model_id, num_labels=2, trust_remote_code=True
|
| 162 |
+
).eval()
|
| 163 |
+
token_model = AutoModelForTokenClassification.from_pretrained(
|
| 164 |
+
model_id, num_labels=3, trust_remote_code=True
|
| 165 |
+
).eval()
|
| 166 |
+
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
| 167 |
+
sequences = ["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"]
|
| 168 |
+
batch = tokenizer(sequences, padding=True, return_tensors="pt")
|
| 169 |
+
biological = batch["attention_mask"].bool()
|
| 170 |
+
for special_id in tokenizer.all_special_ids:
|
| 171 |
+
biological &= batch["input_ids"].ne(special_id)
|
| 172 |
+
|
| 173 |
+
sequence_labels = torch.zeros(len(sequences), dtype=torch.long)
|
| 174 |
+
token_labels = torch.full_like(batch["input_ids"], -100)
|
| 175 |
+
token_labels[biological] = 0
|
| 176 |
+
|
| 177 |
+
with torch.inference_mode():
|
| 178 |
+
sequence_output = sequence_model(**batch, labels=sequence_labels)
|
| 179 |
+
token_output = token_model(**batch, labels=token_labels)
|
| 180 |
+
print(sequence_output.logits.shape) # (b, 2)
|
| 181 |
+
print(token_output.logits.shape) # (b, l, 3)
|
| 182 |
+
```
|
| 183 |
+
|
| 184 |
+
## PEFT fine-tuning
|
| 185 |
+
|
| 186 |
+
Install the direct training dependencies, then attach LoRA to the loaded checkpoint:
|
| 187 |
+
|
| 188 |
+
```bash
|
| 189 |
+
python -m pip install "datasets>=4.8,<5" "peft>=0.19,<0.20"
|
| 190 |
+
```
|
| 191 |
+
|
| 192 |
+
```python
|
| 193 |
+
from peft import LoraConfig, TaskType, get_peft_model
|
| 194 |
+
|
| 195 |
+
peft_model = get_peft_model(
|
| 196 |
+
sequence_model,
|
| 197 |
+
LoraConfig(
|
| 198 |
+
task_type=TaskType.SEQ_CLS,
|
| 199 |
+
r=8,
|
| 200 |
+
lora_alpha=16,
|
| 201 |
+
target_modules="all-linear",
|
| 202 |
+
modules_to_save=["classifier"],
|
| 203 |
+
),
|
| 204 |
+
)
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
This checkpoint advertises a classification head, so the separately trained
|
| 208 |
+
`classifier` is saved with the adapter.
|
| 209 |
+
All FastPLMs checkpoints follow the Transformers `PreTrainedModel` contract and
|
| 210 |
+
can be adapted with PEFT. The ESM2-specific shipped CLI is an example, not a
|
| 211 |
+
support boundary. Record the target modules, base revision, data identity, and
|
| 212 |
+
trainable parameter scope.
|
| 213 |
+
|
| 214 |
+
## Test-time training
|
| 215 |
+
|
| 216 |
+
TTT samples masked views of one protein and updates only injected low-rank
|
| 217 |
+
adapters. Base checkpoint weights remain frozen:
|
| 218 |
+
|
| 219 |
+
```python
|
| 220 |
+
from transformers import AutoModelForMaskedLM
|
| 221 |
+
|
| 222 |
+
ttt_model = AutoModelForMaskedLM.from_pretrained(
|
| 223 |
+
"Synthyra/ANKH2_large",
|
| 224 |
+
trust_remote_code=True,
|
| 225 |
+
)
|
| 226 |
+
metrics = ttt_model.ttt(
|
| 227 |
+
seq="MSTNPKPQRKTKRNT",
|
| 228 |
+
ttt_config={"steps": 3, "batch_size": 1, "seed": 7},
|
| 229 |
+
)
|
| 230 |
+
ttt_model.save_pretrained("adapted", safe_serialization=True)
|
| 231 |
+
ttt_model.ttt_reset()
|
| 232 |
+
print(metrics)
|
| 233 |
```
|
| 234 |
|
| 235 |
+
Persisted adapters retain their deterministic reset state. TTT adds latency
|
| 236 |
+
and memory, can worsen an output, and does not establish biological function.
|
|
|
|
|
|
|
| 237 |
|
| 238 |
## Encoder and sequence-to-sequence use
|
| 239 |
|
|
|
|
| 280 |
- Precision policies: `default`
|
| 281 |
- BF16 execution: `static_parameters`
|
| 282 |
- Generation contract: `required`
|
| 283 |
+
- Artifact dependency set: `core`
|
| 284 |
- Weight publication allowed: `true`
|
| 285 |
- Weight license status: `resolved`
|
| 286 |
- Redistributable: `true`
|
|
|
|
| 291 |
- FastPLMs weights: `Synthyra/ANKH2_large`
|
| 292 |
- Runtime revision: recorded separately in the built artifact and published commit
|
| 293 |
- Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
|
|
|
|
| 294 |
- Canonical transformed state SHA-256: `597c4fe2fa8711f11a25317905f1d62fa92905e55fdd5c0a79614cd9c9d2bca3`
|
| 295 |
- Conversion equality attestation: recorded in `provenance.json`
|
| 296 |
- Official checkpoint: `ElnaggarLab/ankh2-ext2`
|
| 297 |
- Artifact source: `official`
|
| 298 |
- State transform: `ankh_t5_to_fastplms_v1`
|
|
|
|
| 299 |
- Pinned upstreams: `ankh`
|
|
|
|
| 300 |
- Release tiers: `check`, `compliance`, `feature`, `artifact`, `benchmark`
|
| 301 |
- Unresolved required file identities: `0`
|
| 302 |
|
| 303 |
+
`provenance.json` records exact file identities, conversion, source revisions,
|
| 304 |
+
legal texts, schema, and attestations. A nonzero unresolved count blocks release.
|
|
|
|
| 305 |
|
| 306 |
## Validation boundary
|
| 307 |
|
| 308 |
+
Declared tiers compare applicable configuration, tokenizer behavior, state,
|
| 309 |
+
and representative inference with the pinned reference. Metadata alone does
|
| 310 |
+
not claim a build passed, a backend is faster, or an output is biologically
|
| 311 |
+
valid.
|
|
|
|
|
|
|
| 312 |
|
| 313 |
## License
|
| 314 |
|
config.json
CHANGED
|
@@ -22,11 +22,11 @@
|
|
| 22 |
"fastplms_checkpoint_repo_id": "ElnaggarLab/ankh2-ext2",
|
| 23 |
"fastplms_checkpoint_revision": "aa9b9fa72288c47d9f618ce80c011e24b54e17a8",
|
| 24 |
"fastplms_model_id": "ankh2_large",
|
| 25 |
-
"fastplms_release_tool_revision": "
|
| 26 |
-
"fastplms_release_tool_sha256": "
|
| 27 |
-
"fastplms_runtime_bundle_sha256": "
|
| 28 |
-
"fastplms_runtime_revision": "
|
| 29 |
-
"fastplms_source_tree_sha256": "
|
| 30 |
"fastplms_weights_revision": "aa9b9fa72288c47d9f618ce80c011e24b54e17a8",
|
| 31 |
"feed_forward_proj": "gated-silu",
|
| 32 |
"initializer_factor": 1.0,
|
|
|
|
| 22 |
"fastplms_checkpoint_repo_id": "ElnaggarLab/ankh2-ext2",
|
| 23 |
"fastplms_checkpoint_revision": "aa9b9fa72288c47d9f618ce80c011e24b54e17a8",
|
| 24 |
"fastplms_model_id": "ankh2_large",
|
| 25 |
+
"fastplms_release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 26 |
+
"fastplms_release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
|
| 27 |
+
"fastplms_runtime_bundle_sha256": "437c5f5dcc809678e99b8e36d962e78b7960170c81d0b6289d2baeef7920e40f",
|
| 28 |
+
"fastplms_runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 29 |
+
"fastplms_source_tree_sha256": "44f108a32fffbe0e689434fbff109aea7d03a7085a550ed5771f2eac434f130d",
|
| 30 |
"fastplms_weights_revision": "aa9b9fa72288c47d9f618ce80c011e24b54e17a8",
|
| 31 |
"feed_forward_proj": "gated-silu",
|
| 32 |
"initializer_factor": 1.0,
|
fastplms/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
| 9 |
from importlib import import_module
|
| 10 |
from typing import Any
|
| 11 |
|
|
|
|
| 12 |
__version__ = "1.0.0"
|
| 13 |
|
| 14 |
_LAZY_EXPORTS = {
|
|
|
|
| 9 |
from importlib import import_module
|
| 10 |
from typing import Any
|
| 11 |
|
| 12 |
+
|
| 13 |
__version__ = "1.0.0"
|
| 14 |
|
| 15 |
_LAZY_EXPORTS = {
|
fastplms/attention/__init__.py
CHANGED
|
@@ -32,6 +32,7 @@ from .interfaces import (
|
|
| 32 |
validate_transformers_attention_interfaces,
|
| 33 |
)
|
| 34 |
|
|
|
|
| 35 |
__all__ = [
|
| 36 |
"FASTPLMS_ATTENTION_FUNCTIONS",
|
| 37 |
"FASTPLMS_ATTENTION_MASKS",
|
|
|
|
| 32 |
validate_transformers_attention_interfaces,
|
| 33 |
)
|
| 34 |
|
| 35 |
+
|
| 36 |
__all__ = [
|
| 37 |
"FASTPLMS_ATTENTION_FUNCTIONS",
|
| 38 |
"FASTPLMS_ATTENTION_MASKS",
|
fastplms/attention/_core.py
CHANGED
|
@@ -8,17 +8,17 @@ FastPLMs never downloads or compiles code.
|
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
import warnings
|
|
|
|
| 11 |
from collections import OrderedDict
|
| 12 |
from collections.abc import Callable
|
| 13 |
from enum import Enum
|
| 14 |
from threading import RLock
|
| 15 |
-
|
| 16 |
-
import torch
|
| 17 |
from einops import rearrange
|
| 18 |
from torch.nn import functional as F
|
| 19 |
|
| 20 |
from ._kernel_lock import load_locked_kernel
|
| 21 |
|
|
|
|
| 22 |
try:
|
| 23 |
from torch.nn.attention.flex_attention import BlockMask, create_block_mask, flex_attention
|
| 24 |
except ImportError:
|
|
@@ -117,12 +117,12 @@ def _get_flex_block_mask(
|
|
| 117 |
raise RuntimeError(
|
| 118 |
"'flex_attention' was requested, but torch.create_block_mask is unavailable."
|
| 119 |
)
|
| 120 |
-
pattern = mask_pattern.detach().to(device=device).contiguous()
|
| 121 |
# One device-to-host transfer is required for an exact cache identity. Use
|
| 122 |
# the contiguous buffer directly instead of materializing one Python int
|
| 123 |
# per byte, which is prohibitively expensive for long batched sequences.
|
| 124 |
-
host_pattern = pattern.to(device="cpu").contiguous()
|
| 125 |
-
pattern_bytes = host_pattern.view(torch.uint8).numpy().tobytes(order="C")
|
| 126 |
cache_key = (
|
| 127 |
str(device),
|
| 128 |
None if dtype is None else str(dtype),
|
|
@@ -203,6 +203,7 @@ def _validate_kernels_flash_dtype(
|
|
| 203 |
) -> torch.dtype:
|
| 204 |
"""Reject dtypes outside the immutable kernel manifest before dispatch."""
|
| 205 |
|
|
|
|
| 206 |
tensor_dtypes = {query_states.dtype, key_states.dtype, value_states.dtype}
|
| 207 |
if len(tensor_dtypes) != 1:
|
| 208 |
observed = ", ".join(sorted(str(dtype) for dtype in tensor_dtypes))
|
|
@@ -243,6 +244,7 @@ def _validate_kernels_flash_device(
|
|
| 243 |
) -> torch.device:
|
| 244 |
"""Require Q, K, and V on one CUDA device before loading a kernel."""
|
| 245 |
|
|
|
|
| 246 |
devices = (query_states.device, key_states.device, value_states.device)
|
| 247 |
if len(set(devices)) != 1:
|
| 248 |
observed = ", ".join(str(device) for device in devices)
|
|
@@ -284,9 +286,10 @@ def _kernels_flash_forward(
|
|
| 284 |
Failing to override when Q is pre-scaled applies the scale twice and breaks
|
| 285 |
parity with eager attention and SDPA.
|
| 286 |
"""
|
|
|
|
| 287 |
flash_kernel, flash_kernel_variant = _ensure_flash_kernels_loaded(implementation)
|
| 288 |
if flash_kernel_variant == "flash_attn2":
|
| 289 |
-
output = flash_kernel.flash_attn_func(
|
| 290 |
q=query_states,
|
| 291 |
k=key_states,
|
| 292 |
v=value_states,
|
|
@@ -294,9 +297,9 @@ def _kernels_flash_forward(
|
|
| 294 |
softmax_scale=softmax_scale,
|
| 295 |
causal=causal,
|
| 296 |
)
|
| 297 |
-
return output[0] if isinstance(output, tuple) else output
|
| 298 |
if flash_kernel_variant == "flash_attn3":
|
| 299 |
-
output = flash_kernel.flash_attn_func(
|
| 300 |
q=query_states,
|
| 301 |
k=key_states,
|
| 302 |
v=value_states,
|
|
@@ -304,8 +307,8 @@ def _kernels_flash_forward(
|
|
| 304 |
causal=causal,
|
| 305 |
)
|
| 306 |
if isinstance(output, tuple):
|
| 307 |
-
return output[0]
|
| 308 |
-
return output
|
| 309 |
raise RuntimeError(f"Unsupported FlashAttention kernel variant: {flash_kernel_variant}")
|
| 310 |
|
| 311 |
|
|
@@ -326,9 +329,11 @@ def _kernels_flash_varlen_forward(
|
|
| 326 |
See `_kernels_flash_forward` docstring for why `softmax_scale=1.0` must be
|
| 327 |
passed when Q has been pre-scaled by the caller.
|
| 328 |
"""
|
|
|
|
|
|
|
| 329 |
flash_kernel, flash_kernel_variant = _ensure_flash_kernels_loaded(implementation)
|
| 330 |
if flash_kernel_variant == "flash_attn2":
|
| 331 |
-
output = flash_kernel.flash_attn_varlen_func(
|
| 332 |
q=query_states,
|
| 333 |
k=key_states,
|
| 334 |
v=value_states,
|
|
@@ -340,9 +345,9 @@ def _kernels_flash_varlen_forward(
|
|
| 340 |
softmax_scale=softmax_scale,
|
| 341 |
causal=causal,
|
| 342 |
)
|
| 343 |
-
return output[0] if isinstance(output, tuple) else output
|
| 344 |
if flash_kernel_variant == "flash_attn3":
|
| 345 |
-
output = flash_kernel.flash_attn_varlen_func(
|
| 346 |
q=query_states,
|
| 347 |
k=key_states,
|
| 348 |
v=value_states,
|
|
@@ -354,8 +359,8 @@ def _kernels_flash_varlen_forward(
|
|
| 354 |
causal=causal,
|
| 355 |
)
|
| 356 |
if isinstance(output, tuple):
|
| 357 |
-
return output[0]
|
| 358 |
-
return output
|
| 359 |
raise RuntimeError(f"Unsupported FlashAttention kernel variant: {flash_kernel_variant}")
|
| 360 |
|
| 361 |
|
|
@@ -364,6 +369,7 @@ def _kernels_flash_varlen_forward(
|
|
| 364 |
class IndexFirstAxis(torch.autograd.Function):
|
| 365 |
@staticmethod
|
| 366 |
def forward(ctx, input, indices) -> torch.Tensor:
|
|
|
|
| 367 |
ctx.save_for_backward(indices)
|
| 368 |
if input.ndim < 2:
|
| 369 |
raise ValueError(
|
|
@@ -377,12 +383,13 @@ class IndexFirstAxis(torch.autograd.Function):
|
|
| 377 |
)
|
| 378 |
ctx.first_axis_dim, other_shape = input.shape[0], input.shape[1:]
|
| 379 |
second_dim = other_shape.numel()
|
| 380 |
-
return torch.gather(
|
| 381 |
rearrange(input, "b ... -> b (...)"), 0, indices.unsqueeze(1).expand(-1, second_dim)
|
| 382 |
).reshape(-1, *other_shape)
|
| 383 |
|
| 384 |
@staticmethod
|
| 385 |
def backward(ctx, grad_output) -> tuple[torch.Tensor, None]:
|
|
|
|
| 386 |
(indices,) = ctx.saved_tensors
|
| 387 |
if grad_output.ndim < 2:
|
| 388 |
raise RuntimeError(
|
|
@@ -390,19 +397,20 @@ class IndexFirstAxis(torch.autograd.Function):
|
|
| 390 |
"two dimensions."
|
| 391 |
)
|
| 392 |
other_shape = grad_output.shape[1:]
|
| 393 |
-
grad_output = rearrange(grad_output, "b ... -> b (...)")
|
| 394 |
-
grad_input = torch.zeros(
|
| 395 |
[ctx.first_axis_dim, grad_output.shape[1]],
|
| 396 |
device=grad_output.device,
|
| 397 |
dtype=grad_output.dtype,
|
| 398 |
)
|
| 399 |
grad_input.scatter_(0, indices.unsqueeze(1).expand(-1, grad_output.shape[1]), grad_output)
|
| 400 |
-
return grad_input.reshape(ctx.first_axis_dim, *other_shape), None
|
| 401 |
|
| 402 |
|
| 403 |
class IndexPutFirstAxis(torch.autograd.Function):
|
| 404 |
@staticmethod
|
| 405 |
def forward(ctx, values, indices, first_axis_dim) -> torch.Tensor:
|
|
|
|
| 406 |
ctx.save_for_backward(indices)
|
| 407 |
if indices.ndim != 1:
|
| 408 |
raise ValueError(
|
|
@@ -414,16 +422,17 @@ class IndexPutFirstAxis(torch.autograd.Function):
|
|
| 414 |
"index_put_first_axis values must have at least two dimensions; "
|
| 415 |
f"received shape {tuple(values.shape)}."
|
| 416 |
)
|
| 417 |
-
output = torch.zeros(
|
| 418 |
first_axis_dim, *values.shape[1:], device=values.device, dtype=values.dtype
|
| 419 |
)
|
| 420 |
output[indices] = values
|
| 421 |
-
return output
|
| 422 |
|
| 423 |
@staticmethod
|
| 424 |
def backward(ctx, grad_output) -> tuple[torch.Tensor, None, None]:
|
|
|
|
| 425 |
(indices,) = ctx.saved_tensors
|
| 426 |
-
return grad_output[indices], None, None
|
| 427 |
|
| 428 |
|
| 429 |
index_first_axis = IndexFirstAxis.apply
|
|
@@ -433,8 +442,9 @@ index_put_first_axis = IndexPutFirstAxis.apply
|
|
| 433 |
def pad_input(
|
| 434 |
hidden_states: torch.Tensor, indices: torch.Tensor, batch: int, seqlen: int
|
| 435 |
) -> torch.Tensor:
|
| 436 |
-
|
| 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/ankh/modeling_ankh.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import math
|
|
|
|
|
|
|
| 4 |
from collections.abc import Mapping, Sequence
|
| 5 |
from dataclasses import dataclass
|
| 6 |
from numbers import Real
|
| 7 |
from typing import Any, ClassVar
|
| 8 |
-
|
| 9 |
-
import torch
|
| 10 |
-
import torch.nn as nn
|
| 11 |
from tokenizers import pre_tokenizers
|
| 12 |
from torch.nn import functional as F
|
| 13 |
from transformers import (
|
|
@@ -23,6 +22,7 @@ from transformers.modeling_outputs import (
|
|
| 23 |
TokenClassifierOutput,
|
| 24 |
)
|
| 25 |
|
|
|
|
| 26 |
try:
|
| 27 |
from fastplms.attention import (
|
| 28 |
AttentionBackend,
|
|
@@ -405,13 +405,14 @@ def _biological_token_mask(
|
|
| 405 |
class AnkhRMSNorm(nn.Module):
|
| 406 |
"""T5-style RMS layer norm: scales without mean subtraction or bias."""
|
| 407 |
|
| 408 |
-
def __init__(self, hidden_size: int, eps: float = 1e-6):
|
| 409 |
super().__init__()
|
| 410 |
self.weight = nn.Parameter(torch.ones(hidden_size))
|
| 411 |
self.variance_epsilon = eps
|
| 412 |
|
| 413 |
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 414 |
-
|
|
|
|
| 415 |
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
|
| 416 |
return self.weight * hidden_states.to(self.weight.dtype)
|
| 417 |
|
|
@@ -425,7 +426,7 @@ def _gelu_new(x: torch.Tensor) -> torch.Tensor:
|
|
| 425 |
class AnkhGatedFFN(nn.Module):
|
| 426 |
"""T5-style gated feed-forward: activation(wi_0(x)) * wi_1(x) -> wo."""
|
| 427 |
|
| 428 |
-
def __init__(self, config: FastAnkhConfig):
|
| 429 |
super().__init__()
|
| 430 |
self.wi_0 = nn.Linear(config.d_model, config.d_ff, bias=False)
|
| 431 |
self.wi_1 = nn.Linear(config.d_model, config.d_ff, bias=False)
|
|
@@ -434,6 +435,7 @@ class AnkhGatedFFN(nn.Module):
|
|
| 434 |
self.dropout = nn.Dropout(config.dropout_rate)
|
| 435 |
|
| 436 |
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
|
|
|
| 437 |
hidden_states = self.act(self.wi_0(hidden_states)) * self.wi_1(hidden_states)
|
| 438 |
return self.wo(self.dropout(hidden_states))
|
| 439 |
|
|
@@ -451,7 +453,11 @@ class AnkhSelfAttention(nn.Module):
|
|
| 451 |
receive the precomputed bias through the forward call.
|
| 452 |
"""
|
| 453 |
|
| 454 |
-
def __init__(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
super().__init__()
|
| 456 |
self.num_heads = config.num_heads
|
| 457 |
self.d_kv = config.d_kv
|
|
@@ -515,8 +521,8 @@ class AnkhSelfAttention(nn.Module):
|
|
| 515 |
num_buckets=self.relative_attention_num_buckets,
|
| 516 |
max_distance=self.relative_attention_max_distance,
|
| 517 |
)
|
| 518 |
-
values = self.relative_attention_bias(buckets) #
|
| 519 |
-
return values.permute(2, 0, 1).unsqueeze(0) #
|
| 520 |
|
| 521 |
# ---- Forward ----
|
| 522 |
|
|
@@ -529,12 +535,13 @@ class AnkhSelfAttention(nn.Module):
|
|
| 529 |
effective_backend: AttentionBackend | None = None,
|
| 530 |
) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]:
|
| 531 |
"""Returns (attn_output, attn_weights_or_none, position_bias)."""
|
|
|
|
| 532 |
batch_size, seq_length = hidden_states.shape[:2]
|
| 533 |
hidden_shape = (batch_size, seq_length, self.num_heads, self.d_kv)
|
| 534 |
|
| 535 |
-
query_heads = self.q(hidden_states).view(hidden_shape).transpose(1, 2)
|
| 536 |
-
key_heads = self.k(hidden_states).view(hidden_shape).transpose(1, 2)
|
| 537 |
-
value_heads = self.v(hidden_states).view(hidden_shape).transpose(1, 2)
|
| 538 |
|
| 539 |
# The first layer computes the bias once; later layers reuse it.
|
| 540 |
if position_bias is None and self.has_relative_attention_bias:
|
|
@@ -572,7 +579,7 @@ class AnkhSelfAttention(nn.Module):
|
|
| 572 |
value_heads: torch.Tensor,
|
| 573 |
position_bias: torch.Tensor | None,
|
| 574 |
) -> torch.Tensor:
|
| 575 |
-
#
|
| 576 |
# Never mutate torch.backends.cuda process-global reduction policy from
|
| 577 |
# a model forward. Concurrent model requests must not change each
|
| 578 |
# other's numerical behavior or restore a stale process setting.
|
|
@@ -583,7 +590,7 @@ class AnkhSelfAttention(nn.Module):
|
|
| 583 |
attn_mask=position_bias,
|
| 584 |
dropout_p=self.dropout_prob if self.training else 0.0,
|
| 585 |
scale=self.scale,
|
| 586 |
-
)
|
| 587 |
return (
|
| 588 |
context_heads.transpose(1, 2)
|
| 589 |
.contiguous()
|
|
@@ -597,7 +604,10 @@ class AnkhSelfAttention(nn.Module):
|
|
| 597 |
value_heads: torch.Tensor,
|
| 598 |
position_bias: torch.Tensor | None,
|
| 599 |
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 600 |
-
|
|
|
|
|
|
|
|
|
|
| 601 |
if position_bias is not None:
|
| 602 |
attn_weights = attn_weights + position_bias
|
| 603 |
attn_weights = F.softmax(attn_weights.float(), dim=-1).type_as(attn_weights)
|
|
@@ -607,7 +617,7 @@ class AnkhSelfAttention(nn.Module):
|
|
| 607 |
p=self.dropout_prob,
|
| 608 |
training=self.training,
|
| 609 |
)
|
| 610 |
-
context_heads = torch.matmul(attn_weights, value_heads)
|
| 611 |
attn_output = (
|
| 612 |
context_heads.transpose(1, 2)
|
| 613 |
.contiguous()
|
|
@@ -624,7 +634,11 @@ class AnkhSelfAttention(nn.Module):
|
|
| 624 |
class AnkhSelfAttentionLayer(nn.Module):
|
| 625 |
"""Wraps AnkhSelfAttention + layer_norm to match T5Block.layer[0] key naming."""
|
| 626 |
|
| 627 |
-
def __init__(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
super().__init__()
|
| 629 |
self.SelfAttention = AnkhSelfAttention(config, has_relative_attention_bias)
|
| 630 |
self.layer_norm = AnkhRMSNorm(config.d_model, eps=config.layer_norm_epsilon)
|
|
@@ -653,7 +667,7 @@ class AnkhSelfAttentionLayer(nn.Module):
|
|
| 653 |
class AnkhFFLayer(nn.Module):
|
| 654 |
"""Wraps AnkhGatedFFN + layer_norm to match T5Block.layer[1] key naming."""
|
| 655 |
|
| 656 |
-
def __init__(self, config: FastAnkhConfig):
|
| 657 |
super().__init__()
|
| 658 |
self.DenseReluDense = AnkhGatedFFN(config)
|
| 659 |
self.layer_norm = AnkhRMSNorm(config.d_model, eps=config.layer_norm_epsilon)
|
|
@@ -668,7 +682,11 @@ class AnkhFFLayer(nn.Module):
|
|
| 668 |
class AnkhBlock(nn.Module):
|
| 669 |
"""Single transformer block with T5-compatible .layer ModuleList naming."""
|
| 670 |
|
| 671 |
-
def __init__(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
super().__init__()
|
| 673 |
self.layer = nn.ModuleList(
|
| 674 |
[
|
|
@@ -782,7 +800,7 @@ class FAST_ANKH_ENCODER(AnkhPreTrainedModel, EmbeddingMixin):
|
|
| 782 |
block.{i}.layer.1.DenseReluDense.*, final_layer_norm.*.
|
| 783 |
"""
|
| 784 |
|
| 785 |
-
def __init__(self, config: FastAnkhConfig, **kwargs):
|
| 786 |
AnkhPreTrainedModel.__init__(self, config, **kwargs)
|
| 787 |
self.config = config
|
| 788 |
|
|
@@ -965,7 +983,7 @@ class FastAnkhModel(AnkhPreTrainedModel, EmbeddingMixin):
|
|
| 965 |
r"^lm_head\.",
|
| 966 |
]
|
| 967 |
|
| 968 |
-
def __init__(self, config: FastAnkhConfig, **kwargs):
|
| 969 |
AnkhPreTrainedModel.__init__(self, config, **kwargs)
|
| 970 |
self.config = config
|
| 971 |
self.shared = nn.Embedding(config.vocab_size, config.d_model)
|
|
@@ -1029,7 +1047,7 @@ class FastAnkhForMaskedLMExtension(
|
|
| 1029 |
_tied_weights_keys: ClassVar[dict[str, str]] = {"encoder.embed_tokens.weight": "shared.weight"}
|
| 1030 |
_keys_to_ignore_on_load_unexpected: ClassVar[list[str]] = [r"^decoder\."]
|
| 1031 |
|
| 1032 |
-
def __init__(self, config: FastAnkhConfig, **kwargs):
|
| 1033 |
# The historical Synthyra extension stores an independent output head.
|
| 1034 |
config.tie_word_embeddings = False
|
| 1035 |
AnkhPreTrainedModel.__init__(self, config, **kwargs)
|
|
@@ -1156,7 +1174,7 @@ class FastAnkhForConditionalGeneration(
|
|
| 1156 |
embedding_unsupported_pooling = ("cls",)
|
| 1157 |
_fastplms_attention_implementations = ("eager",)
|
| 1158 |
|
| 1159 |
-
def __init__(self, config: FastAnkhConfig, **kwargs):
|
| 1160 |
requested_backend = getattr(config, "_attn_implementation", None) or config.attn_backend
|
| 1161 |
if requested_backend not in (None, "eager"):
|
| 1162 |
raise ValueError(
|
|
@@ -1448,7 +1466,7 @@ class FastAnkhForSequenceClassification(AnkhPreTrainedModel, EmbeddingMixin):
|
|
| 1448 |
r"^lm_head\.",
|
| 1449 |
]
|
| 1450 |
|
| 1451 |
-
def __init__(self, config: FastAnkhConfig, **kwargs):
|
| 1452 |
AnkhPreTrainedModel.__init__(self, config, **kwargs)
|
| 1453 |
self.num_labels = config.num_labels
|
| 1454 |
self.config = config
|
|
@@ -1555,7 +1573,7 @@ class FastAnkhForTokenClassification(AnkhPreTrainedModel, EmbeddingMixin):
|
|
| 1555 |
r"^lm_head\.",
|
| 1556 |
]
|
| 1557 |
|
| 1558 |
-
def __init__(self, config: FastAnkhConfig, **kwargs):
|
| 1559 |
AnkhPreTrainedModel.__init__(self, config, **kwargs)
|
| 1560 |
self.num_labels = config.num_labels
|
| 1561 |
self.shared = nn.Embedding(config.vocab_size, config.d_model)
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import math
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
from collections.abc import Mapping, Sequence
|
| 7 |
from dataclasses import dataclass
|
| 8 |
from numbers import Real
|
| 9 |
from typing import Any, ClassVar
|
|
|
|
|
|
|
|
|
|
| 10 |
from tokenizers import pre_tokenizers
|
| 11 |
from torch.nn import functional as F
|
| 12 |
from transformers import (
|
|
|
|
| 22 |
TokenClassifierOutput,
|
| 23 |
)
|
| 24 |
|
| 25 |
+
|
| 26 |
try:
|
| 27 |
from fastplms.attention import (
|
| 28 |
AttentionBackend,
|
|
|
|
| 405 |
class AnkhRMSNorm(nn.Module):
|
| 406 |
"""T5-style RMS layer norm: scales without mean subtraction or bias."""
|
| 407 |
|
| 408 |
+
def __init__(self, hidden_size: int, eps: float = 1e-6) -> None:
|
| 409 |
super().__init__()
|
| 410 |
self.weight = nn.Parameter(torch.ones(hidden_size))
|
| 411 |
self.variance_epsilon = eps
|
| 412 |
|
| 413 |
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 414 |
+
# hidden_states: (..., d)
|
| 415 |
+
variance = hidden_states.to(torch.float32).pow(2).mean(-1, keepdim=True) # (..., 1)
|
| 416 |
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
|
| 417 |
return self.weight * hidden_states.to(self.weight.dtype)
|
| 418 |
|
|
|
|
| 426 |
class AnkhGatedFFN(nn.Module):
|
| 427 |
"""T5-style gated feed-forward: activation(wi_0(x)) * wi_1(x) -> wo."""
|
| 428 |
|
| 429 |
+
def __init__(self, config: FastAnkhConfig) -> None:
|
| 430 |
super().__init__()
|
| 431 |
self.wi_0 = nn.Linear(config.d_model, config.d_ff, bias=False)
|
| 432 |
self.wi_1 = nn.Linear(config.d_model, config.d_ff, bias=False)
|
|
|
|
| 435 |
self.dropout = nn.Dropout(config.dropout_rate)
|
| 436 |
|
| 437 |
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 438 |
+
# hidden_states: (b, l, d)
|
| 439 |
hidden_states = self.act(self.wi_0(hidden_states)) * self.wi_1(hidden_states)
|
| 440 |
return self.wo(self.dropout(hidden_states))
|
| 441 |
|
|
|
|
| 453 |
receive the precomputed bias through the forward call.
|
| 454 |
"""
|
| 455 |
|
| 456 |
+
def __init__(
|
| 457 |
+
self,
|
| 458 |
+
config: FastAnkhConfig,
|
| 459 |
+
has_relative_attention_bias: bool = False,
|
| 460 |
+
) -> None:
|
| 461 |
super().__init__()
|
| 462 |
self.num_heads = config.num_heads
|
| 463 |
self.d_kv = config.d_kv
|
|
|
|
| 521 |
num_buckets=self.relative_attention_num_buckets,
|
| 522 |
max_distance=self.relative_attention_max_distance,
|
| 523 |
)
|
| 524 |
+
values = self.relative_attention_bias(buckets) # (q, k, h)
|
| 525 |
+
return values.permute(2, 0, 1).unsqueeze(0) # (1, h, q, k)
|
| 526 |
|
| 527 |
# ---- Forward ----
|
| 528 |
|
|
|
|
| 535 |
effective_backend: AttentionBackend | None = None,
|
| 536 |
) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]:
|
| 537 |
"""Returns (attn_output, attn_weights_or_none, position_bias)."""
|
| 538 |
+
# hidden_states: (b, l, d)
|
| 539 |
batch_size, seq_length = hidden_states.shape[:2]
|
| 540 |
hidden_shape = (batch_size, seq_length, self.num_heads, self.d_kv)
|
| 541 |
|
| 542 |
+
query_heads = self.q(hidden_states).view(hidden_shape).transpose(1, 2) # (b, h, l, d_h)
|
| 543 |
+
key_heads = self.k(hidden_states).view(hidden_shape).transpose(1, 2) # (b, h, l, d_h)
|
| 544 |
+
value_heads = self.v(hidden_states).view(hidden_shape).transpose(1, 2) # (b, h, l, d_h)
|
| 545 |
|
| 546 |
# The first layer computes the bias once; later layers reuse it.
|
| 547 |
if position_bias is None and self.has_relative_attention_bias:
|
|
|
|
| 579 |
value_heads: torch.Tensor,
|
| 580 |
position_bias: torch.Tensor | None,
|
| 581 |
) -> torch.Tensor:
|
| 582 |
+
# position_bias: (1, h, l, l), including padding
|
| 583 |
# Never mutate torch.backends.cuda process-global reduction policy from
|
| 584 |
# a model forward. Concurrent model requests must not change each
|
| 585 |
# other's numerical behavior or restore a stale process setting.
|
|
|
|
| 590 |
attn_mask=position_bias,
|
| 591 |
dropout_p=self.dropout_prob if self.training else 0.0,
|
| 592 |
scale=self.scale,
|
| 593 |
+
) # (b, h, l, d_h)
|
| 594 |
return (
|
| 595 |
context_heads.transpose(1, 2)
|
| 596 |
.contiguous()
|
|
|
|
| 604 |
value_heads: torch.Tensor,
|
| 605 |
position_bias: torch.Tensor | None,
|
| 606 |
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 607 |
+
# query_heads, key_heads, value_heads: (b, h, l, d_h)
|
| 608 |
+
attn_weights = (
|
| 609 |
+
torch.matmul(query_heads, key_heads.transpose(-1, -2)) * self.scale
|
| 610 |
+
) # (b, h, l, l)
|
| 611 |
if position_bias is not None:
|
| 612 |
attn_weights = attn_weights + position_bias
|
| 613 |
attn_weights = F.softmax(attn_weights.float(), dim=-1).type_as(attn_weights)
|
|
|
|
| 617 |
p=self.dropout_prob,
|
| 618 |
training=self.training,
|
| 619 |
)
|
| 620 |
+
context_heads = torch.matmul(attn_weights, value_heads) # (b, h, l, d_h)
|
| 621 |
attn_output = (
|
| 622 |
context_heads.transpose(1, 2)
|
| 623 |
.contiguous()
|
|
|
|
| 634 |
class AnkhSelfAttentionLayer(nn.Module):
|
| 635 |
"""Wraps AnkhSelfAttention + layer_norm to match T5Block.layer[0] key naming."""
|
| 636 |
|
| 637 |
+
def __init__(
|
| 638 |
+
self,
|
| 639 |
+
config: FastAnkhConfig,
|
| 640 |
+
has_relative_attention_bias: bool = False,
|
| 641 |
+
) -> None:
|
| 642 |
super().__init__()
|
| 643 |
self.SelfAttention = AnkhSelfAttention(config, has_relative_attention_bias)
|
| 644 |
self.layer_norm = AnkhRMSNorm(config.d_model, eps=config.layer_norm_epsilon)
|
|
|
|
| 667 |
class AnkhFFLayer(nn.Module):
|
| 668 |
"""Wraps AnkhGatedFFN + layer_norm to match T5Block.layer[1] key naming."""
|
| 669 |
|
| 670 |
+
def __init__(self, config: FastAnkhConfig) -> None:
|
| 671 |
super().__init__()
|
| 672 |
self.DenseReluDense = AnkhGatedFFN(config)
|
| 673 |
self.layer_norm = AnkhRMSNorm(config.d_model, eps=config.layer_norm_epsilon)
|
|
|
|
| 682 |
class AnkhBlock(nn.Module):
|
| 683 |
"""Single transformer block with T5-compatible .layer ModuleList naming."""
|
| 684 |
|
| 685 |
+
def __init__(
|
| 686 |
+
self,
|
| 687 |
+
config: FastAnkhConfig,
|
| 688 |
+
has_relative_attention_bias: bool = False,
|
| 689 |
+
) -> None:
|
| 690 |
super().__init__()
|
| 691 |
self.layer = nn.ModuleList(
|
| 692 |
[
|
|
|
|
| 800 |
block.{i}.layer.1.DenseReluDense.*, final_layer_norm.*.
|
| 801 |
"""
|
| 802 |
|
| 803 |
+
def __init__(self, config: FastAnkhConfig, **kwargs) -> None:
|
| 804 |
AnkhPreTrainedModel.__init__(self, config, **kwargs)
|
| 805 |
self.config = config
|
| 806 |
|
|
|
|
| 983 |
r"^lm_head\.",
|
| 984 |
]
|
| 985 |
|
| 986 |
+
def __init__(self, config: FastAnkhConfig, **kwargs) -> None:
|
| 987 |
AnkhPreTrainedModel.__init__(self, config, **kwargs)
|
| 988 |
self.config = config
|
| 989 |
self.shared = nn.Embedding(config.vocab_size, config.d_model)
|
|
|
|
| 1047 |
_tied_weights_keys: ClassVar[dict[str, str]] = {"encoder.embed_tokens.weight": "shared.weight"}
|
| 1048 |
_keys_to_ignore_on_load_unexpected: ClassVar[list[str]] = [r"^decoder\."]
|
| 1049 |
|
| 1050 |
+
def __init__(self, config: FastAnkhConfig, **kwargs) -> None:
|
| 1051 |
# The historical Synthyra extension stores an independent output head.
|
| 1052 |
config.tie_word_embeddings = False
|
| 1053 |
AnkhPreTrainedModel.__init__(self, config, **kwargs)
|
|
|
|
| 1174 |
embedding_unsupported_pooling = ("cls",)
|
| 1175 |
_fastplms_attention_implementations = ("eager",)
|
| 1176 |
|
| 1177 |
+
def __init__(self, config: FastAnkhConfig, **kwargs) -> None:
|
| 1178 |
requested_backend = getattr(config, "_attn_implementation", None) or config.attn_backend
|
| 1179 |
if requested_backend not in (None, "eager"):
|
| 1180 |
raise ValueError(
|
|
|
|
| 1466 |
r"^lm_head\.",
|
| 1467 |
]
|
| 1468 |
|
| 1469 |
+
def __init__(self, config: FastAnkhConfig, **kwargs) -> None:
|
| 1470 |
AnkhPreTrainedModel.__init__(self, config, **kwargs)
|
| 1471 |
self.num_labels = config.num_labels
|
| 1472 |
self.config = config
|
|
|
|
| 1573 |
r"^lm_head\.",
|
| 1574 |
]
|
| 1575 |
|
| 1576 |
+
def __init__(self, config: FastAnkhConfig, **kwargs) -> None:
|
| 1577 |
AnkhPreTrainedModel.__init__(self, config, **kwargs)
|
| 1578 |
self.num_labels = config.num_labels
|
| 1579 |
self.shared = nn.Embedding(config.vocab_size, config.d_model)
|
fastplms/models/ttt.py
CHANGED
|
@@ -3,12 +3,13 @@ from __future__ import annotations
|
|
| 3 |
import contextlib
|
| 4 |
import math
|
| 5 |
import numbers
|
| 6 |
-
import typing as T
|
| 7 |
-
from dataclasses import asdict, dataclass, fields
|
| 8 |
-
|
| 9 |
import torch
|
| 10 |
import torch.nn as nn
|
| 11 |
import torch.nn.functional as F
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
_STANDARD_AMINO_ACIDS = "ACDEFGHIKLMNPQRSTVWY"
|
| 14 |
_TTT_SERIALIZATION_VERSION = 1
|
|
@@ -42,7 +43,7 @@ class TTTConfig:
|
|
| 42 |
self.verify()
|
| 43 |
|
| 44 |
@classmethod
|
| 45 |
-
def from_kwargs(cls, **kwargs:
|
| 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,16 +179,16 @@ def _install_runtime():
|
|
| 223 |
return package
|
| 224 |
|
| 225 |
_install_runtime()
|
| 226 |
-
|
| 227 |
-
FastAnkhConfig =
|
| 228 |
FastAnkhConfig.__module__ = __name__
|
| 229 |
-
FastAnkhForConditionalGeneration =
|
| 230 |
FastAnkhForConditionalGeneration.__module__ = __name__
|
| 231 |
-
FastAnkhForMaskedLMExtension =
|
| 232 |
FastAnkhForMaskedLMExtension.__module__ = __name__
|
| 233 |
-
FastAnkhForSequenceClassification =
|
| 234 |
FastAnkhForSequenceClassification.__module__ = __name__
|
| 235 |
-
FastAnkhForTokenClassification =
|
| 236 |
FastAnkhForTokenClassification.__module__ = __name__
|
| 237 |
-
FastAnkhModel =
|
| 238 |
FastAnkhModel.__module__ = __name__
|
|
|
|
| 1 |
+
"""Generated bridge to the embedded FastPLMs runtime sources."""
|
| 2 |
|
| 3 |
import base64
|
| 4 |
import hashlib
|
|
|
|
| 6 |
import importlib.util
|
| 7 |
import sys
|
| 8 |
import tempfile
|
|
|
|
| 9 |
from io import BytesIO
|
| 10 |
from pathlib import Path
|
| 11 |
from zipfile import ZIP_DEFLATED, ZipFile
|
| 12 |
|
| 13 |
from .fastplms_bundle import RUNTIME_DATA, RUNTIME_HASH
|
| 14 |
|
| 15 |
+
if RUNTIME_HASH != "437c5f5dcc809678e99b8e36d962e78b7960170c81d0b6289d2baeef7920e40f":
|
| 16 |
raise RuntimeError("FastPLMs runtime identity differs from the bridge.")
|
| 17 |
|
| 18 |
_RUNTIME_TEMPORARIES = []
|
|
|
|
| 82 |
result[relative.as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
|
| 83 |
return result
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
def _extend_loaded_package_paths(package_root):
|
| 86 |
for name, module in list(sys.modules.items()):
|
| 87 |
if name != "fastplms" and not name.startswith("fastplms."):
|
|
|
|
| 95 |
if candidate.is_dir() and candidate_text not in paths:
|
| 96 |
paths.append(candidate_text)
|
| 97 |
|
| 98 |
+
def _merge_runtime(package, package_root):
|
| 99 |
incoming = _runtime_file_hashes(package_root)
|
| 100 |
+
known = getattr(package, "__fastplms_artifact_runtime_files__", None)
|
| 101 |
+
if not isinstance(known, dict):
|
| 102 |
+
raise RuntimeError(
|
| 103 |
+
"A non-artifact fastplms module is already loaded. Load the Hub artifact "
|
| 104 |
+
"in a separate Python process."
|
| 105 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
conflicts = sorted(
|
| 107 |
relative
|
| 108 |
for relative, digest in incoming.items()
|
|
|
|
| 114 |
+ ", ".join(repr(path) for path in conflicts[:5])
|
| 115 |
+ ". Load incompatible releases in separate Python processes."
|
| 116 |
)
|
| 117 |
+
known = dict(known)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
known.update(incoming)
|
| 119 |
+
package.__fastplms_artifact_runtime_files__ = known
|
| 120 |
+
roots = list(getattr(package, "__fastplms_artifact_runtime_roots__", ()))
|
| 121 |
if str(package_root) not in roots:
|
| 122 |
roots.append(str(package_root))
|
| 123 |
+
package.__fastplms_artifact_runtime_roots__ = tuple(roots)
|
| 124 |
temporaries = list(
|
| 125 |
+
getattr(package, "__fastplms_artifact_runtime_temporaries__", ())
|
| 126 |
)
|
| 127 |
for temporary in _RUNTIME_TEMPORARIES:
|
| 128 |
if temporary not in temporaries:
|
| 129 |
temporaries.append(temporary)
|
| 130 |
+
package.__fastplms_artifact_runtime_temporaries__ = tuple(temporaries)
|
| 131 |
+
hashes = set(getattr(package, "__fastplms_artifact_runtime_hashes__", ()))
|
| 132 |
hashes.add(RUNTIME_HASH)
|
| 133 |
+
package.__fastplms_artifact_runtime_hashes__ = frozenset(hashes)
|
| 134 |
_extend_loaded_package_paths(package_root)
|
| 135 |
+
return package
|
| 136 |
|
| 137 |
def _import_without_bytecode(module_name):
|
| 138 |
previous = sys.dont_write_bytecode
|
|
|
|
| 143 |
sys.dont_write_bytecode = previous
|
| 144 |
|
| 145 |
def _install_runtime():
|
| 146 |
+
package = sys.modules.get("fastplms")
|
| 147 |
+
hashes = getattr(package, "__fastplms_artifact_runtime_hashes__", ())
|
| 148 |
if RUNTIME_HASH in hashes:
|
| 149 |
+
return package
|
| 150 |
package_root = _ensure_runtime()
|
| 151 |
+
if package is not None:
|
| 152 |
+
return _merge_runtime(package, package_root)
|
| 153 |
spec = importlib.util.spec_from_file_location(
|
| 154 |
"fastplms",
|
| 155 |
package_root / "__init__.py",
|
|
|
|
| 179 |
return package
|
| 180 |
|
| 181 |
_install_runtime()
|
| 182 |
+
_module_181 = _import_without_bytecode("fastplms.models.ankh.modeling_ankh")
|
| 183 |
+
FastAnkhConfig = _module_181.FastAnkhConfig
|
| 184 |
FastAnkhConfig.__module__ = __name__
|
| 185 |
+
FastAnkhForConditionalGeneration = _module_181.FastAnkhForConditionalGeneration
|
| 186 |
FastAnkhForConditionalGeneration.__module__ = __name__
|
| 187 |
+
FastAnkhForMaskedLMExtension = _module_181.FastAnkhForMaskedLMExtension
|
| 188 |
FastAnkhForMaskedLMExtension.__module__ = __name__
|
| 189 |
+
FastAnkhForSequenceClassification = _module_181.FastAnkhForSequenceClassification
|
| 190 |
FastAnkhForSequenceClassification.__module__ = __name__
|
| 191 |
+
FastAnkhForTokenClassification = _module_181.FastAnkhForTokenClassification
|
| 192 |
FastAnkhForTokenClassification.__module__ = __name__
|
| 193 |
+
FastAnkhModel = _module_181.FastAnkhModel
|
| 194 |
FastAnkhModel.__module__ = __name__
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Direct runtime dependencies for Synthyra/ANKH2_large.
|
| 2 |
+
# FastPLMs source is embedded in this model repository.
|
| 3 |
+
torch>=2.13,<2.14
|
| 4 |
+
transformers>=5.13,<5.14
|
| 5 |
+
huggingface-hub>=0.34,<2
|
| 6 |
+
tokenizers>=0.22,<0.23
|
| 7 |
+
safetensors>=0.5,<1
|
| 8 |
+
numpy>=1.26,<3
|
| 9 |
+
einops>=0.8,<1
|
| 10 |
+
tqdm>=4.67,<5
|
runtime-attestation.json
CHANGED
|
@@ -2,41 +2,42 @@
|
|
| 2 |
"files": {
|
| 3 |
"LICENSES/FastPLMs-Apache-2.0.txt": "sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
|
| 4 |
"LICENSES/ankh/LICENSE.md": "sha256:cd041d7f9f52936e8824ac3f754e9c67410763205fc8a7020ba74fc8b6edc088",
|
| 5 |
-
"README.md": "sha256:
|
| 6 |
"THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
|
| 7 |
-
"config.json": "sha256:
|
| 8 |
-
"fastplms/__init__.py": "sha256:
|
| 9 |
-
"fastplms/attention/__init__.py": "sha256:
|
| 10 |
-
"fastplms/attention/_core.py": "sha256:
|
| 11 |
-
"fastplms/attention/_kernel_lock.py": "sha256:
|
| 12 |
-
"fastplms/attention/interfaces.py": "sha256:
|
| 13 |
-
"fastplms/embeddings/__init__.py": "sha256:
|
| 14 |
-
"fastplms/embeddings/pooling.py": "sha256:
|
| 15 |
-
"fastplms/embeddings/runner.py": "sha256:
|
| 16 |
-
"fastplms/embeddings/storage.py": "sha256:
|
| 17 |
-
"fastplms/embeddings/types.py": "sha256:
|
| 18 |
"fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
|
| 19 |
-
"fastplms/models/__init__.py": "sha256:
|
| 20 |
"fastplms/models/ankh/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
| 21 |
-
"fastplms/models/ankh/modeling_ankh.py": "sha256:
|
| 22 |
-
"fastplms/models/ttt.py": "sha256:
|
| 23 |
-
"fastplms/registry.py": "sha256:
|
| 24 |
-
"fastplms/runtime.py": "sha256:
|
| 25 |
-
"fastplms_bundle.py": "sha256:
|
| 26 |
-
"modeling_fastplms.py": "sha256:
|
|
|
|
| 27 |
"special_tokens_map.json": "sha256:c8995d2f8037fe3a8cfdef30475365e1c314c417880b75abaf8296e3c05d42d6",
|
| 28 |
"tokenizer.json": "sha256:b4533f607d9fd665f2d9d94b0cf71870a6fc2fc2ae7cbd516d0e43a9efb406fd",
|
| 29 |
"tokenizer_config.json": "sha256:dba1f9315ded007fc2e5bcb2ed4bfb6fff81c9becdc97b73a5713ba5ddc01afc"
|
| 30 |
},
|
| 31 |
"model_id": "ankh2_large",
|
| 32 |
"redistributable": true,
|
| 33 |
-
"release_tool_revision": "
|
| 34 |
-
"release_tool_sha256": "
|
| 35 |
-
"runtime_bundle_sha256": "
|
| 36 |
-
"runtime_revision": "
|
| 37 |
"schema_version": 2,
|
| 38 |
"scope": "runtime-only",
|
| 39 |
-
"source_tree_sha256": "
|
| 40 |
"weights": {
|
| 41 |
"repo_id": "Synthyra/ANKH2_large",
|
| 42 |
"revision": "729167c1980316ae61691338838447491926033f"
|
|
|
|
| 2 |
"files": {
|
| 3 |
"LICENSES/FastPLMs-Apache-2.0.txt": "sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
|
| 4 |
"LICENSES/ankh/LICENSE.md": "sha256:cd041d7f9f52936e8824ac3f754e9c67410763205fc8a7020ba74fc8b6edc088",
|
| 5 |
+
"README.md": "sha256:131315548dc250a4359361d16d9355080d1598c8bff6fc9902abe68c9e966cb2",
|
| 6 |
"THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
|
| 7 |
+
"config.json": "sha256:c2e2327b8765473cbe9004169bbd860ff7dca916cfd66653c4a08c6c3a419bce",
|
| 8 |
+
"fastplms/__init__.py": "sha256:8503e02debf24abc6d33c1913ff3eb9f245e63f7e62017e220d072c7393fdf2c",
|
| 9 |
+
"fastplms/attention/__init__.py": "sha256:ab3c0b6156968f418f3c97ea664959cb978f4137717b4362213bcfe25ab5e3d6",
|
| 10 |
+
"fastplms/attention/_core.py": "sha256:09d0f71a5ea2ad2138c9441f6a2f72bbf8e72729be41d8db737b163f36ce47da",
|
| 11 |
+
"fastplms/attention/_kernel_lock.py": "sha256:56455eb57cee9af08438eda52b775c9cff3cf994cadb9b116ce2ee10fd5f1801",
|
| 12 |
+
"fastplms/attention/interfaces.py": "sha256:6562a342961022fc2848d7004bd01fddaf8be8e08e782d0856e5c30a74232623",
|
| 13 |
+
"fastplms/embeddings/__init__.py": "sha256:cf0648de905a00ad16c0aece328eeb05f25739a4cf89b47b915e0f2b19c3ce26",
|
| 14 |
+
"fastplms/embeddings/pooling.py": "sha256:9bed8c466fce053333de9e25b04d1ab304f2954f403ffa0f5a3c749e8277b3ce",
|
| 15 |
+
"fastplms/embeddings/runner.py": "sha256:a7839763ad641a63d0e2671dcf34aa7b2a8114198950b64254188521b27453a8",
|
| 16 |
+
"fastplms/embeddings/storage.py": "sha256:98cf725d26766f32300f105345129ca6cf02ebd10628e746a7e71e107bc911a6",
|
| 17 |
+
"fastplms/embeddings/types.py": "sha256:c58f8c6a37a11a937c3f77b4e1617d5f213923d98ecaffddcdd283bd9dc42fe7",
|
| 18 |
"fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
|
| 19 |
+
"fastplms/models/__init__.py": "sha256:d3dd084f5e8fafcf2d0fe4a26e4ab9284d3f9414b1f9a5bd86e308a406447c43",
|
| 20 |
"fastplms/models/ankh/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
| 21 |
+
"fastplms/models/ankh/modeling_ankh.py": "sha256:9ff235c9bd529a4c2ba63293376e6479161d364e7a4884c4063a0eb3ed1c5719",
|
| 22 |
+
"fastplms/models/ttt.py": "sha256:f1d7f9298a930f4c40f8e321ffa08b09d1c5b8492bca623bdc7bfd03ec85774d",
|
| 23 |
+
"fastplms/registry.py": "sha256:cb37799dae5a4c0c780089ab2cca4cc56300c1c5f515b275892aa4e1c4b97eb6",
|
| 24 |
+
"fastplms/runtime.py": "sha256:9521c37fcd168bf9a4137277fb73672403ccf30d70d4845aa879fb22f42ddeb6",
|
| 25 |
+
"fastplms_bundle.py": "sha256:af54f8786c1124b45e07ef857486e14163bb79266fa98fc2002a82296ce89fbb",
|
| 26 |
+
"modeling_fastplms.py": "sha256:37b0ea758dc1a50e7ca7225a175ca983e109efe4342759055c58520ef271f49f",
|
| 27 |
+
"requirements.txt": "sha256:3ae7e085e6ec8ed936981f14e58d1199b4a26ba79cf90cb5d01585946ad8a96a",
|
| 28 |
"special_tokens_map.json": "sha256:c8995d2f8037fe3a8cfdef30475365e1c314c417880b75abaf8296e3c05d42d6",
|
| 29 |
"tokenizer.json": "sha256:b4533f607d9fd665f2d9d94b0cf71870a6fc2fc2ae7cbd516d0e43a9efb406fd",
|
| 30 |
"tokenizer_config.json": "sha256:dba1f9315ded007fc2e5bcb2ed4bfb6fff81c9becdc97b73a5713ba5ddc01afc"
|
| 31 |
},
|
| 32 |
"model_id": "ankh2_large",
|
| 33 |
"redistributable": true,
|
| 34 |
+
"release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 35 |
+
"release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
|
| 36 |
+
"runtime_bundle_sha256": "437c5f5dcc809678e99b8e36d962e78b7960170c81d0b6289d2baeef7920e40f",
|
| 37 |
+
"runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 38 |
"schema_version": 2,
|
| 39 |
"scope": "runtime-only",
|
| 40 |
+
"source_tree_sha256": "44f108a32fffbe0e689434fbff109aea7d03a7085a550ed5771f2eac434f130d",
|
| 41 |
"weights": {
|
| 42 |
"repo_id": "Synthyra/ANKH2_large",
|
| 43 |
"revision": "729167c1980316ae61691338838447491926033f"
|