Instructions to use Synthyra/ESMplusplus_large with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Synthyra/ESMplusplus_large with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("fill-mask", model="Synthyra/ESMplusplus_large", trust_remote_code=True)# Load model directly from transformers import AutoModelForMaskedLM model = AutoModelForMaskedLM.from_pretrained("Synthyra/ESMplusplus_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 +116 -103
- config.json +5 -5
- fastplms/__init__.py +1 -0
- fastplms/attention/__init__.py +1 -0
- fastplms/attention/_core.py +68 -47
- fastplms/attention/_kernel_lock.py +17 -33
- fastplms/attention/interfaces.py +7 -7
- fastplms/embeddings/__init__.py +1 -0
- fastplms/embeddings/pooling.py +48 -40
- fastplms/embeddings/runner.py +60 -36
- fastplms/embeddings/storage.py +26 -19
- fastplms/embeddings/types.py +4 -5
- fastplms/models/__init__.py +1 -0
- fastplms/models/esm_plusplus/modeling_esm_plusplus.py +35 -35
- fastplms/models/ttt.py +97 -81
- fastplms/registry.py +45 -44
- fastplms/runtime.py +1 -0
- fastplms_bundle.py +0 -0
- modeling_fastplms.py +27 -71
- requirements.txt +11 -0
- runtime-attestation.json +25 -24
README.md
CHANGED
|
@@ -16,16 +16,35 @@ Accepted inputs are amino-acid sequences tokenized to residue IDs.
|
|
| 16 |
Supported Transformers entry points are `AutoConfig`, `AutoModel`,
|
| 17 |
`AutoModelForMaskedLM`.
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
## Install and platform requirements
|
| 20 |
|
| 21 |
-
Install the
|
| 22 |
|
| 23 |
```bash
|
| 24 |
-
python -m pip install \
|
| 25 |
-
"
|
| 26 |
```
|
| 27 |
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
| 29 |
access on first download. For an air-gapped run, first build the manifest-pinned
|
| 30 |
local artifact and use the offline form shown in the example.
|
| 31 |
|
|
@@ -38,23 +57,23 @@ model_id = "Synthyra/ESMplusplus_large"
|
|
| 38 |
model = AutoModel.from_pretrained(
|
| 39 |
model_id,
|
| 40 |
trust_remote_code=True,
|
|
|
|
| 41 |
).eval()
|
| 42 |
```
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
`flash_attention_3`.
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
For BF16 execution, this family uses parameters loaded directly in BF16.
|
| 58 |
|
| 59 |
## Tokenization and forward inference
|
| 60 |
|
|
@@ -84,43 +103,78 @@ print(output.last_hidden_state.shape)
|
|
| 84 |
|
| 85 |
## Dataset embeddings
|
| 86 |
|
| 87 |
-
The shared embedding
|
| 88 |
-
|
| 89 |
-
FASTA path. Results preserve order and duplicate identifiers:
|
| 90 |
|
| 91 |
```python
|
| 92 |
-
|
| 93 |
["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"],
|
| 94 |
batch_size=2,
|
| 95 |
pooling=("mean", "std"),
|
| 96 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
```
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
-
|
|
|
|
| 109 |
|
| 110 |
```python
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
| 118 |
)
|
|
|
|
|
|
|
|
|
|
| 119 |
```
|
| 120 |
|
| 121 |
-
|
| 122 |
-
and
|
| 123 |
-
existing run.
|
| 124 |
|
| 125 |
## ESMC behavior
|
| 126 |
|
|
@@ -133,14 +187,9 @@ Those deviations produce diagnostic warnings rather than strict parity
|
|
| 133 |
failures; dispatch integrity, masks, finite outputs, shapes, and catastrophic
|
| 134 |
biological disagreement remain hard gates.
|
| 135 |
|
| 136 |
-
The current
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
supported, non-experimental interfaces, but requests fail closed without
|
| 140 |
-
dispatch on this image. Prior real FlashAttention 2 execution was captured in
|
| 141 |
-
separate workstation JUnit, but that immutable report and environment
|
| 142 |
-
attestation are not bundled in this repository. It is not reused as a current
|
| 143 |
-
ESMC release distribution or numerical claim.
|
| 144 |
|
| 145 |
When `sequence_id` is supplied, it is authoritative for ESMC attention grouping
|
| 146 |
and padding, and `attention_mask` is ignored. Values greater than or equal to
|
|
@@ -149,48 +198,18 @@ use `attention_mask` as the padding contract.
|
|
| 149 |
|
| 150 |
| Backend | Support | Measurement status |
|
| 151 |
| --- | --- | --- |
|
| 152 |
-
| `sdpa` | Recommended fidelity path | Pending
|
| 153 |
-
| `eager` | Supported | Pending
|
| 154 |
-
| `flash_attention_2` | Supported |
|
| 155 |
-
| `flex_attention` | Supported, numerically divergent | Pending
|
| 156 |
-
| `flash_attention_3` | Supported, numerically divergent |
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
## Locked oracle package compatibility exception
|
| 166 |
-
|
| 167 |
-
The frozen oracle lock permits exactly one nonzero `pip check` diagnostic:
|
| 168 |
-
`nvidia-cusparselt-cu13 0.8.1 is not supported on this platform`. It applies only to
|
| 169 |
-
`nvidia-cusparselt-cu13==0.8.1` on
|
| 170 |
-
`NVIDIA GH200 480GB` / `linux` /
|
| 171 |
-
`aarch64`. The vendor filename tag is
|
| 172 |
-
`py3-none-manylinux2014_aarch64`, while the wheel metadata declares
|
| 173 |
-
`py3-none-manylinux2014_sbsa`. The exact wheel is
|
| 174 |
-
`nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl` with SHA-256 `4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f`.
|
| 175 |
-
FastPLMs accepts this vendor metadata mismatch only after the lock, installed
|
| 176 |
-
inventory, wheel bytes, metadata tag, and target identity all match. The wheel
|
| 177 |
-
is not rewritten (`validated-vendor-metadata-exception-no-wheel-rewrite`). Any additional diagnostic or
|
| 178 |
-
identity drift fails closed.
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
No number is inferred from a threshold or copied from a different checkpoint.
|
| 182 |
-
Before release, replace each pending cell only through the strict 30-record
|
| 183 |
-
ingestion gate tied to this exact Hub revision, runtime revision, BF16 dtype,
|
| 184 |
-
the current exact GH200/aarch64 accelerator and container identity, and both
|
| 185 |
-
locked sequence panels. The set contains 18 eager/SDPA/Flex measurement records
|
| 186 |
-
and 12 structured Flash locked-platform unavailable records. H100 and H200
|
| 187 |
-
remain Hopper-class deployment examples, but they are not current ESMC release-confirmation evidence.
|
| 188 |
-
Results are never
|
| 189 |
-
carried across device, platform, source, lock, or image identities.
|
| 190 |
-
Diagnostic JSON is written under `artifacts/diagnostics/esmc/`. Catastrophe
|
| 191 |
-
guardrails (relative L2 `0.25`, Q99.9 `0.50`, residue cosine `0.90`, pooled
|
| 192 |
-
cosine `0.95`, top-1 `0.80`, Jensen-Shannon `0.05`) detect corruption and do
|
| 193 |
-
not constitute parity or quality claims.
|
| 194 |
|
| 195 |
## Runtime contract
|
| 196 |
|
|
@@ -201,7 +220,7 @@ not constitute parity or quality claims.
|
|
| 201 |
- Precision policies: `default`
|
| 202 |
- BF16 execution: `static_parameters`
|
| 203 |
- Generation contract: `not_applicable`
|
| 204 |
-
-
|
| 205 |
- Weight publication allowed: `true`
|
| 206 |
- Weight license status: `resolved`
|
| 207 |
- Redistributable: `true`
|
|
@@ -212,28 +231,22 @@ not constitute parity or quality claims.
|
|
| 212 |
- FastPLMs weights: `Synthyra/ESMplusplus_large`
|
| 213 |
- Runtime revision: recorded separately in the built artifact and published commit
|
| 214 |
- Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
|
| 215 |
-
- Generator/schema version and complete/runtime-only attestations: recorded in `provenance.json`
|
| 216 |
- Official checkpoint: `biohub/ESMC-600M`
|
| 217 |
- Artifact source: `fast`
|
| 218 |
- State transform: `esmc_to_fastplms_v1`
|
| 219 |
-
- BF16 execution: `static_parameters`
|
| 220 |
- Pinned upstreams: `biohub-esm`, `biohub-transformers`
|
| 221 |
-
- Reference container: `reference-biohub-esm`
|
| 222 |
- Release tiers: `check`, `compliance`, `feature`, `artifact`, `benchmark`
|
| 223 |
- Unresolved required file identities: `0`
|
| 224 |
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
release blocker.
|
| 228 |
|
| 229 |
## Validation boundary
|
| 230 |
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
passed, that one backend is faster, or that an output has biological or
|
| 236 |
-
therapeutic validity.
|
| 237 |
|
| 238 |
## License
|
| 239 |
|
|
|
|
| 16 |
Supported Transformers entry points are `AutoConfig`, `AutoModel`,
|
| 17 |
`AutoModelForMaskedLM`.
|
| 18 |
|
| 19 |
+
## Capabilities
|
| 20 |
+
|
| 21 |
+
| Feature | Status |
|
| 22 |
+
| --- | --- |
|
| 23 |
+
| Sequence classification | Unavailable: no advertised AutoClass |
|
| 24 |
+
| Token classification | Unavailable: no advertised AutoClass |
|
| 25 |
+
| PEFT fine-tuning | Supported pattern: attach LoRA to the pretrained model |
|
| 26 |
+
| Embeddings | Supported: shared ordered embedding API |
|
| 27 |
+
| Test-time training | Supported: low-rank masked-residue adaptation |
|
| 28 |
+
| Attention variants | Special: SDPA fidelity path; alternate backends have explicit bands |
|
| 29 |
+
| Compliance | Declared: exact release evidence is required |
|
| 30 |
+
|
| 31 |
+
A supported interface is not a pretrained downstream predictor. Classification
|
| 32 |
+
heads start untrained, and declared compliance metadata is not a claim that an
|
| 33 |
+
arbitrary local build passed its release gate.
|
| 34 |
+
|
| 35 |
## Install and platform requirements
|
| 36 |
|
| 37 |
+
Install the direct dependencies published with this model:
|
| 38 |
|
| 39 |
```bash
|
| 40 |
+
python -m pip install -r \
|
| 41 |
+
"https://huggingface.co/Synthyra/ESMplusplus_large/resolve/main/requirements.txt"
|
| 42 |
```
|
| 43 |
|
| 44 |
+
The FastPLMs implementation itself is embedded in the model repository and loaded
|
| 45 |
+
by Transformers through `trust_remote_code=True`.
|
| 46 |
+
|
| 47 |
+
Python 3.11-3.14, PyTorch 2.13, and Transformers 5.13 are required. The artifact requirements include the direct FlashAttention loader dependency. FlashAttention also requires compatible CUDA hardware and BF16 execution. The Hub quick start below requires network
|
| 48 |
access on first download. For an air-gapped run, first build the manifest-pinned
|
| 49 |
local artifact and use the offline form shown in the example.
|
| 50 |
|
|
|
|
| 57 |
model = AutoModel.from_pretrained(
|
| 58 |
model_id,
|
| 59 |
trust_remote_code=True,
|
| 60 |
+
attn_implementation="sdpa",
|
| 61 |
).eval()
|
| 62 |
```
|
| 63 |
|
| 64 |
+
For offline validation, replace `model_id` with the manifest-built
|
| 65 |
+
`dist/hub/ESMplusplus_large` path and pass `local_files_only=True`.
|
| 66 |
+
|
| 67 |
+
## Attention and compliance
|
| 68 |
+
|
| 69 |
+
The quick start selects `sdpa` explicitly. Declared variants are `eager`, `sdpa`, `flex_attention`, `flash_attention_2`,
|
| 70 |
+
`flash_attention_3`. An unavailable requested backend raises instead of
|
| 71 |
+
silently switching implementations.
|
| 72 |
+
`output_attentions=True` may use the documented, one-call eager fallback solely
|
| 73 |
+
to materialize attention tensors; the configured backend remains unchanged.
|
| 74 |
+
|
| 75 |
+
This family declares the `compliance` tier. Release evidence binds the exact
|
| 76 |
+
checkpoint, backend, dtype, hardware, inputs, and reference revision.
|
|
|
|
| 77 |
|
| 78 |
## Tokenization and forward inference
|
| 79 |
|
|
|
|
| 103 |
|
| 104 |
## Dataset embeddings
|
| 105 |
|
| 106 |
+
The shared embedding mixin preserves input order and biological-position
|
| 107 |
+
masking. It accepts sequences, identified records, mappings, or a FASTA path:
|
|
|
|
| 108 |
|
| 109 |
```python
|
| 110 |
+
pooled = model.embed_dataset(
|
| 111 |
["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"],
|
| 112 |
batch_size=2,
|
| 113 |
pooling=("mean", "std"),
|
| 114 |
)
|
| 115 |
+
residues = model.embed_dataset(
|
| 116 |
+
["MSTNPKPQRKTKRNT"],
|
| 117 |
+
full_embeddings=True,
|
| 118 |
+
)
|
| 119 |
+
print(pooled[0].tensor.shape) # (2 * d,)
|
| 120 |
+
print(residues[0].tensor.shape) # (l, d)
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
Set `output` and `format="safetensors"` or `"sqlite"` for transactional,
|
| 124 |
+
bounded-memory persistence. Resume verifies input order, model state, tokenizer
|
| 125 |
+
policy, backend, dtype, and pooling configuration before appending.
|
| 126 |
+
|
| 127 |
+
## PEFT fine-tuning
|
| 128 |
+
|
| 129 |
+
Install the direct training dependencies, then attach LoRA to the loaded checkpoint:
|
| 130 |
+
|
| 131 |
+
```bash
|
| 132 |
+
python -m pip install "datasets>=4.8,<5" "peft>=0.19,<0.20"
|
| 133 |
+
```
|
| 134 |
|
| 135 |
+
```python
|
| 136 |
+
from peft import LoraConfig, get_peft_model
|
| 137 |
+
|
| 138 |
+
peft_model = get_peft_model(
|
| 139 |
+
model,
|
| 140 |
+
LoraConfig(
|
| 141 |
+
r=8,
|
| 142 |
+
lora_alpha=16,
|
| 143 |
+
target_modules="all-linear",
|
| 144 |
+
),
|
| 145 |
+
)
|
| 146 |
```
|
| 147 |
|
| 148 |
+
This checkpoint has no advertised classifier. Supply the task-specific
|
| 149 |
+
objective and preserve any new head through `modules_to_save`.
|
| 150 |
+
All FastPLMs checkpoints follow the Transformers `PreTrainedModel` contract and
|
| 151 |
+
can be adapted with PEFT. The ESM2-specific shipped CLI is an example, not a
|
| 152 |
+
support boundary. Record the target modules, base revision, data identity, and
|
| 153 |
+
trainable parameter scope.
|
| 154 |
+
|
| 155 |
+
## Test-time training
|
| 156 |
|
| 157 |
+
TTT samples masked views of one protein and updates only injected low-rank
|
| 158 |
+
adapters. Base checkpoint weights remain frozen:
|
| 159 |
|
| 160 |
```python
|
| 161 |
+
from transformers import AutoModelForMaskedLM
|
| 162 |
+
|
| 163 |
+
ttt_model = AutoModelForMaskedLM.from_pretrained(
|
| 164 |
+
"Synthyra/ESMplusplus_large",
|
| 165 |
+
trust_remote_code=True,
|
| 166 |
+
)
|
| 167 |
+
metrics = ttt_model.ttt(
|
| 168 |
+
seq="MSTNPKPQRKTKRNT",
|
| 169 |
+
ttt_config={"steps": 3, "batch_size": 1, "seed": 7},
|
| 170 |
)
|
| 171 |
+
ttt_model.save_pretrained("adapted", safe_serialization=True)
|
| 172 |
+
ttt_model.ttt_reset()
|
| 173 |
+
print(metrics)
|
| 174 |
```
|
| 175 |
|
| 176 |
+
Persisted adapters retain their deterministic reset state. TTT adds latency
|
| 177 |
+
and memory, can worsen an output, and does not establish biological function.
|
|
|
|
| 178 |
|
| 179 |
## ESMC behavior
|
| 180 |
|
|
|
|
| 187 |
failures; dispatch integrity, masks, finite outputs, shapes, and catastrophic
|
| 188 |
biological disagreement remain hard gates.
|
| 189 |
|
| 190 |
+
The current GH200/aarch64 release environment validates eager, SDPA, and Flex.
|
| 191 |
+
Flash requests fail closed because compatible locked kernels are unavailable
|
| 192 |
+
on this platform.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
When `sequence_id` is supplied, it is authoritative for ESMC attention grouping
|
| 195 |
and padding, and `attention_mask` is ignored. Values greater than or equal to
|
|
|
|
| 198 |
|
| 199 |
| Backend | Support | Measurement status |
|
| 200 |
| --- | --- | --- |
|
| 201 |
+
| `sdpa` | Recommended fidelity path | Pending release measurement |
|
| 202 |
+
| `eager` | Supported | Pending release measurement |
|
| 203 |
+
| `flash_attention_2` | Supported | Unavailable on current GH200/aarch64 lock |
|
| 204 |
+
| `flex_attention` | Supported, numerically divergent | Pending release measurement |
|
| 205 |
+
| `flash_attention_3` | Supported, numerically divergent | Unavailable on current GH200/aarch64 lock |
|
| 206 |
+
|
| 207 |
+
Detailed backend measurements, release guardrails, and the GH200 package
|
| 208 |
+
compatibility exception are maintained in the
|
| 209 |
+
[attention backend guide](https://github.com/Synthyra/FastPLMs/blob/main/docs/attention_backends.md)
|
| 210 |
+
and
|
| 211 |
+
[release evidence manifest](https://github.com/Synthyra/FastPLMs/blob/main/docs/generated/capability_evidence.md).
|
| 212 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
## Runtime contract
|
| 215 |
|
|
|
|
| 220 |
- Precision policies: `default`
|
| 221 |
- BF16 execution: `static_parameters`
|
| 222 |
- Generation contract: `not_applicable`
|
| 223 |
+
- Artifact dependency set: `core`
|
| 224 |
- Weight publication allowed: `true`
|
| 225 |
- Weight license status: `resolved`
|
| 226 |
- Redistributable: `true`
|
|
|
|
| 231 |
- FastPLMs weights: `Synthyra/ESMplusplus_large`
|
| 232 |
- Runtime revision: recorded separately in the built artifact and published commit
|
| 233 |
- Source-tree and runtime-bundle SHA-256: recorded in `provenance.json`
|
|
|
|
| 234 |
- Official checkpoint: `biohub/ESMC-600M`
|
| 235 |
- Artifact source: `fast`
|
| 236 |
- State transform: `esmc_to_fastplms_v1`
|
|
|
|
| 237 |
- Pinned upstreams: `biohub-esm`, `biohub-transformers`
|
|
|
|
| 238 |
- Release tiers: `check`, `compliance`, `feature`, `artifact`, `benchmark`
|
| 239 |
- Unresolved required file identities: `0`
|
| 240 |
|
| 241 |
+
`provenance.json` records exact file identities, conversion, source revisions,
|
| 242 |
+
legal texts, schema, and attestations. A nonzero unresolved count blocks release.
|
|
|
|
| 243 |
|
| 244 |
## Validation boundary
|
| 245 |
|
| 246 |
+
Declared tiers compare applicable configuration, tokenizer behavior, state,
|
| 247 |
+
and representative inference with the pinned reference. Metadata alone does
|
| 248 |
+
not claim a build passed, a backend is faster, or an output is biologically
|
| 249 |
+
valid.
|
|
|
|
|
|
|
| 250 |
|
| 251 |
## License
|
| 252 |
|
config.json
CHANGED
|
@@ -13,11 +13,11 @@
|
|
| 13 |
"fastplms_checkpoint_repo_id": "Synthyra/ESMplusplus_large",
|
| 14 |
"fastplms_checkpoint_revision": "f813401638b3fddab09748aec1ad2bf537aa4208",
|
| 15 |
"fastplms_model_id": "esmc_large",
|
| 16 |
-
"fastplms_release_tool_revision": "
|
| 17 |
-
"fastplms_release_tool_sha256": "
|
| 18 |
-
"fastplms_runtime_bundle_sha256": "
|
| 19 |
-
"fastplms_runtime_revision": "
|
| 20 |
-
"fastplms_source_tree_sha256": "
|
| 21 |
"fastplms_weights_revision": "f813401638b3fddab09748aec1ad2bf537aa4208",
|
| 22 |
"hidden_size": 1152,
|
| 23 |
"initializer_range": 0.02,
|
|
|
|
| 13 |
"fastplms_checkpoint_repo_id": "Synthyra/ESMplusplus_large",
|
| 14 |
"fastplms_checkpoint_revision": "f813401638b3fddab09748aec1ad2bf537aa4208",
|
| 15 |
"fastplms_model_id": "esmc_large",
|
| 16 |
+
"fastplms_release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 17 |
+
"fastplms_release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
|
| 18 |
+
"fastplms_runtime_bundle_sha256": "d73f544a7623d12eff2ba407e2d7e86417b7d731e36a2931515930eae7d7e8e5",
|
| 19 |
+
"fastplms_runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 20 |
+
"fastplms_source_tree_sha256": "bed9c7b1e1b0500dcd370e92e1f83b47494f9167d37b16e248f9d17e0135f822",
|
| 21 |
"fastplms_weights_revision": "f813401638b3fddab09748aec1ad2bf537aa4208",
|
| 22 |
"hidden_size": 1152,
|
| 23 |
"initializer_range": 0.02,
|
fastplms/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
| 9 |
from importlib import import_module
|
| 10 |
from typing import Any
|
| 11 |
|
|
|
|
| 12 |
__version__ = "1.0.0"
|
| 13 |
|
| 14 |
_LAZY_EXPORTS = {
|
|
|
|
| 9 |
from importlib import import_module
|
| 10 |
from typing import Any
|
| 11 |
|
| 12 |
+
|
| 13 |
__version__ = "1.0.0"
|
| 14 |
|
| 15 |
_LAZY_EXPORTS = {
|
fastplms/attention/__init__.py
CHANGED
|
@@ -32,6 +32,7 @@ from .interfaces import (
|
|
| 32 |
validate_transformers_attention_interfaces,
|
| 33 |
)
|
| 34 |
|
|
|
|
| 35 |
__all__ = [
|
| 36 |
"FASTPLMS_ATTENTION_FUNCTIONS",
|
| 37 |
"FASTPLMS_ATTENTION_MASKS",
|
|
|
|
| 32 |
validate_transformers_attention_interfaces,
|
| 33 |
)
|
| 34 |
|
| 35 |
+
|
| 36 |
__all__ = [
|
| 37 |
"FASTPLMS_ATTENTION_FUNCTIONS",
|
| 38 |
"FASTPLMS_ATTENTION_MASKS",
|
fastplms/attention/_core.py
CHANGED
|
@@ -8,17 +8,17 @@ FastPLMs never downloads or compiles code.
|
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
import warnings
|
|
|
|
| 11 |
from collections import OrderedDict
|
| 12 |
from collections.abc import Callable
|
| 13 |
from enum import Enum
|
| 14 |
from threading import RLock
|
| 15 |
-
|
| 16 |
-
import torch
|
| 17 |
from einops import rearrange
|
| 18 |
from torch.nn import functional as F
|
| 19 |
|
| 20 |
from ._kernel_lock import load_locked_kernel
|
| 21 |
|
|
|
|
| 22 |
try:
|
| 23 |
from torch.nn.attention.flex_attention import BlockMask, create_block_mask, flex_attention
|
| 24 |
except ImportError:
|
|
@@ -117,12 +117,12 @@ def _get_flex_block_mask(
|
|
| 117 |
raise RuntimeError(
|
| 118 |
"'flex_attention' was requested, but torch.create_block_mask is unavailable."
|
| 119 |
)
|
| 120 |
-
pattern = mask_pattern.detach().to(device=device).contiguous()
|
| 121 |
# One device-to-host transfer is required for an exact cache identity. Use
|
| 122 |
# the contiguous buffer directly instead of materializing one Python int
|
| 123 |
# per byte, which is prohibitively expensive for long batched sequences.
|
| 124 |
-
host_pattern = pattern.to(device="cpu").contiguous()
|
| 125 |
-
pattern_bytes = host_pattern.view(torch.uint8).numpy().tobytes(order="C")
|
| 126 |
cache_key = (
|
| 127 |
str(device),
|
| 128 |
None if dtype is None else str(dtype),
|
|
@@ -203,6 +203,7 @@ def _validate_kernels_flash_dtype(
|
|
| 203 |
) -> torch.dtype:
|
| 204 |
"""Reject dtypes outside the immutable kernel manifest before dispatch."""
|
| 205 |
|
|
|
|
| 206 |
tensor_dtypes = {query_states.dtype, key_states.dtype, value_states.dtype}
|
| 207 |
if len(tensor_dtypes) != 1:
|
| 208 |
observed = ", ".join(sorted(str(dtype) for dtype in tensor_dtypes))
|
|
@@ -243,6 +244,7 @@ def _validate_kernels_flash_device(
|
|
| 243 |
) -> torch.device:
|
| 244 |
"""Require Q, K, and V on one CUDA device before loading a kernel."""
|
| 245 |
|
|
|
|
| 246 |
devices = (query_states.device, key_states.device, value_states.device)
|
| 247 |
if len(set(devices)) != 1:
|
| 248 |
observed = ", ".join(str(device) for device in devices)
|
|
@@ -284,9 +286,10 @@ def _kernels_flash_forward(
|
|
| 284 |
Failing to override when Q is pre-scaled applies the scale twice and breaks
|
| 285 |
parity with eager attention and SDPA.
|
| 286 |
"""
|
|
|
|
| 287 |
flash_kernel, flash_kernel_variant = _ensure_flash_kernels_loaded(implementation)
|
| 288 |
if flash_kernel_variant == "flash_attn2":
|
| 289 |
-
output = flash_kernel.flash_attn_func(
|
| 290 |
q=query_states,
|
| 291 |
k=key_states,
|
| 292 |
v=value_states,
|
|
@@ -294,9 +297,9 @@ def _kernels_flash_forward(
|
|
| 294 |
softmax_scale=softmax_scale,
|
| 295 |
causal=causal,
|
| 296 |
)
|
| 297 |
-
return output[0] if isinstance(output, tuple) else output
|
| 298 |
if flash_kernel_variant == "flash_attn3":
|
| 299 |
-
output = flash_kernel.flash_attn_func(
|
| 300 |
q=query_states,
|
| 301 |
k=key_states,
|
| 302 |
v=value_states,
|
|
@@ -304,8 +307,8 @@ def _kernels_flash_forward(
|
|
| 304 |
causal=causal,
|
| 305 |
)
|
| 306 |
if isinstance(output, tuple):
|
| 307 |
-
return output[0]
|
| 308 |
-
return output
|
| 309 |
raise RuntimeError(f"Unsupported FlashAttention kernel variant: {flash_kernel_variant}")
|
| 310 |
|
| 311 |
|
|
@@ -326,9 +329,11 @@ def _kernels_flash_varlen_forward(
|
|
| 326 |
See `_kernels_flash_forward` docstring for why `softmax_scale=1.0` must be
|
| 327 |
passed when Q has been pre-scaled by the caller.
|
| 328 |
"""
|
|
|
|
|
|
|
| 329 |
flash_kernel, flash_kernel_variant = _ensure_flash_kernels_loaded(implementation)
|
| 330 |
if flash_kernel_variant == "flash_attn2":
|
| 331 |
-
output = flash_kernel.flash_attn_varlen_func(
|
| 332 |
q=query_states,
|
| 333 |
k=key_states,
|
| 334 |
v=value_states,
|
|
@@ -340,9 +345,9 @@ def _kernels_flash_varlen_forward(
|
|
| 340 |
softmax_scale=softmax_scale,
|
| 341 |
causal=causal,
|
| 342 |
)
|
| 343 |
-
return output[0] if isinstance(output, tuple) else output
|
| 344 |
if flash_kernel_variant == "flash_attn3":
|
| 345 |
-
output = flash_kernel.flash_attn_varlen_func(
|
| 346 |
q=query_states,
|
| 347 |
k=key_states,
|
| 348 |
v=value_states,
|
|
@@ -354,8 +359,8 @@ def _kernels_flash_varlen_forward(
|
|
| 354 |
causal=causal,
|
| 355 |
)
|
| 356 |
if isinstance(output, tuple):
|
| 357 |
-
return output[0]
|
| 358 |
-
return output
|
| 359 |
raise RuntimeError(f"Unsupported FlashAttention kernel variant: {flash_kernel_variant}")
|
| 360 |
|
| 361 |
|
|
@@ -364,6 +369,7 @@ def _kernels_flash_varlen_forward(
|
|
| 364 |
class IndexFirstAxis(torch.autograd.Function):
|
| 365 |
@staticmethod
|
| 366 |
def forward(ctx, input, indices) -> torch.Tensor:
|
|
|
|
| 367 |
ctx.save_for_backward(indices)
|
| 368 |
if input.ndim < 2:
|
| 369 |
raise ValueError(
|
|
@@ -377,12 +383,13 @@ class IndexFirstAxis(torch.autograd.Function):
|
|
| 377 |
)
|
| 378 |
ctx.first_axis_dim, other_shape = input.shape[0], input.shape[1:]
|
| 379 |
second_dim = other_shape.numel()
|
| 380 |
-
return torch.gather(
|
| 381 |
rearrange(input, "b ... -> b (...)"), 0, indices.unsqueeze(1).expand(-1, second_dim)
|
| 382 |
).reshape(-1, *other_shape)
|
| 383 |
|
| 384 |
@staticmethod
|
| 385 |
def backward(ctx, grad_output) -> tuple[torch.Tensor, None]:
|
|
|
|
| 386 |
(indices,) = ctx.saved_tensors
|
| 387 |
if grad_output.ndim < 2:
|
| 388 |
raise RuntimeError(
|
|
@@ -390,19 +397,20 @@ class IndexFirstAxis(torch.autograd.Function):
|
|
| 390 |
"two dimensions."
|
| 391 |
)
|
| 392 |
other_shape = grad_output.shape[1:]
|
| 393 |
-
grad_output = rearrange(grad_output, "b ... -> b (...)")
|
| 394 |
-
grad_input = torch.zeros(
|
| 395 |
[ctx.first_axis_dim, grad_output.shape[1]],
|
| 396 |
device=grad_output.device,
|
| 397 |
dtype=grad_output.dtype,
|
| 398 |
)
|
| 399 |
grad_input.scatter_(0, indices.unsqueeze(1).expand(-1, grad_output.shape[1]), grad_output)
|
| 400 |
-
return grad_input.reshape(ctx.first_axis_dim, *other_shape), None
|
| 401 |
|
| 402 |
|
| 403 |
class IndexPutFirstAxis(torch.autograd.Function):
|
| 404 |
@staticmethod
|
| 405 |
def forward(ctx, values, indices, first_axis_dim) -> torch.Tensor:
|
|
|
|
| 406 |
ctx.save_for_backward(indices)
|
| 407 |
if indices.ndim != 1:
|
| 408 |
raise ValueError(
|
|
@@ -414,16 +422,17 @@ class IndexPutFirstAxis(torch.autograd.Function):
|
|
| 414 |
"index_put_first_axis values must have at least two dimensions; "
|
| 415 |
f"received shape {tuple(values.shape)}."
|
| 416 |
)
|
| 417 |
-
output = torch.zeros(
|
| 418 |
first_axis_dim, *values.shape[1:], device=values.device, dtype=values.dtype
|
| 419 |
)
|
| 420 |
output[indices] = values
|
| 421 |
-
return output
|
| 422 |
|
| 423 |
@staticmethod
|
| 424 |
def backward(ctx, grad_output) -> tuple[torch.Tensor, None, None]:
|
|
|
|
| 425 |
(indices,) = ctx.saved_tensors
|
| 426 |
-
return grad_output[indices], None, None
|
| 427 |
|
| 428 |
|
| 429 |
index_first_axis = IndexFirstAxis.apply
|
|
@@ -433,8 +442,9 @@ index_put_first_axis = IndexPutFirstAxis.apply
|
|
| 433 |
def pad_input(
|
| 434 |
hidden_states: torch.Tensor, indices: torch.Tensor, batch: int, seqlen: int
|
| 435 |
) -> torch.Tensor:
|
| 436 |
-
|
| 437 |
-
|
|
|
|
| 438 |
|
| 439 |
|
| 440 |
def _unpad_input(
|
|
@@ -450,18 +460,19 @@ def _unpad_input(
|
|
| 450 |
tuple[torch.Tensor, torch.Tensor],
|
| 451 |
tuple[int, int],
|
| 452 |
]:
|
|
|
|
| 453 |
batch_size, seq_len, num_heads, head_dim = query_layer.shape
|
| 454 |
-
seqlens = attention_mask_2d.sum(dim=1).int()
|
| 455 |
-
cu_seqlens = F.pad(seqlens.cumsum(0, dtype=torch.int32), (1, 0))
|
| 456 |
max_seqlen = int(seqlens.max().item())
|
| 457 |
-
indices = attention_mask_2d.flatten().nonzero(as_tuple=False).flatten()
|
| 458 |
-
query_layer = index_first_axis(
|
| 459 |
query_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
|
| 460 |
)
|
| 461 |
-
key_layer = index_first_axis(
|
| 462 |
key_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
|
| 463 |
)
|
| 464 |
-
value_layer = index_first_axis(
|
| 465 |
value_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
|
| 466 |
)
|
| 467 |
return (
|
|
@@ -482,6 +493,7 @@ def _validate_flash_padding_mask(
|
|
| 482 |
) -> torch.Tensor:
|
| 483 |
"""Validate the self-attention padding mask used by the varlen kernels."""
|
| 484 |
|
|
|
|
| 485 |
if attention_mask_2d.ndim != 2:
|
| 486 |
raise ValueError("FlashAttention padding masks must have shape (batch, sequence_length).")
|
| 487 |
expected_shape = query_states.shape[:2]
|
|
@@ -497,7 +509,7 @@ def _validate_flash_padding_mask(
|
|
| 497 |
)
|
| 498 |
if attention_mask_2d.device != query_states.device:
|
| 499 |
raise ValueError("FlashAttention padding mask and Q, K, and V must be on the same device.")
|
| 500 |
-
return attention_mask_2d.to(dtype=torch.bool)
|
| 501 |
|
| 502 |
|
| 503 |
def kernels_flash_attention_func(
|
|
@@ -521,6 +533,8 @@ def kernels_flash_attention_func(
|
|
| 521 |
`softmax_scale=1.0`. Otherwise the flash kernel applies its default scale
|
| 522 |
again, yielding an effective `1/head_dim` scale that drifts across layers.
|
| 523 |
"""
|
|
|
|
|
|
|
| 524 |
_validate_kernels_flash_device(
|
| 525 |
query_states,
|
| 526 |
key_states,
|
|
@@ -534,11 +548,11 @@ def kernels_flash_attention_func(
|
|
| 534 |
implementation,
|
| 535 |
)
|
| 536 |
if query_states.dtype != runtime_dtype:
|
| 537 |
-
query_states = query_states.to(dtype=runtime_dtype)
|
| 538 |
-
key_states = key_states.to(dtype=runtime_dtype)
|
| 539 |
-
value_states = value_states.to(dtype=runtime_dtype)
|
| 540 |
if attention_mask_2d is not None:
|
| 541 |
-
attention_mask_2d = _validate_flash_padding_mask(
|
| 542 |
query_states,
|
| 543 |
key_states,
|
| 544 |
value_states,
|
|
@@ -554,8 +568,13 @@ def kernels_flash_attention_func(
|
|
| 554 |
indices_q,
|
| 555 |
(cu_seqlens_q, cu_seqlens_k),
|
| 556 |
(max_seqlen_q, max_seqlen_k),
|
| 557 |
-
) = _unpad_input(
|
| 558 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
query_states=query_states,
|
| 560 |
key_states=key_states,
|
| 561 |
value_states=value_states,
|
|
@@ -567,10 +586,10 @@ def kernels_flash_attention_func(
|
|
| 567 |
softmax_scale=softmax_scale,
|
| 568 |
implementation=implementation,
|
| 569 |
)
|
| 570 |
-
output = pad_input(attn_output_unpad, indices_q, batch_size, q_len)
|
| 571 |
-
return output.masked_fill(~attention_mask_2d[:, :, None, None], 0)
|
| 572 |
else:
|
| 573 |
-
return _kernels_flash_forward(
|
| 574 |
query_states=query_states,
|
| 575 |
key_states=key_states,
|
| 576 |
value_states=value_states,
|
|
@@ -706,6 +725,7 @@ def get_attention_mask(
|
|
| 706 |
|
| 707 |
Returns (attention_mask_2d, attention_mask_4d, flex_block_mask).
|
| 708 |
"""
|
|
|
|
| 709 |
if attention_mask is None:
|
| 710 |
return None, None, None
|
| 711 |
|
|
@@ -720,14 +740,14 @@ def get_attention_mask(
|
|
| 720 |
"attention_mask shape must match the input batch and sequence dimensions; "
|
| 721 |
f"expected {expected_shape}, received {tuple(attention_mask.shape)}."
|
| 722 |
)
|
| 723 |
-
attention_mask_2d = attention_mask.to(device=device, dtype=torch.bool)
|
| 724 |
if not bool(attention_mask_2d.any(dim=1).all()):
|
| 725 |
raise ValueError("attention_mask must keep at least one valid key per batch row.")
|
| 726 |
|
| 727 |
effective_backend = resolve_attention_backend(effective_backend)
|
| 728 |
|
| 729 |
if effective_backend.is_flash:
|
| 730 |
-
return attention_mask_2d, None, None
|
| 731 |
|
| 732 |
if effective_backend == AttentionBackend.FLEX_ATTENTION:
|
| 733 |
if create_block_mask is None:
|
|
@@ -751,12 +771,12 @@ def get_attention_mask(
|
|
| 751 |
mask_semantics=mask_semantics,
|
| 752 |
mask_mod=mask_mod,
|
| 753 |
)
|
| 754 |
-
return attention_mask_2d, None, flex_block_mask
|
| 755 |
|
| 756 |
# SDPA/manual masks only keys. Padding queries still attend to real keys, so
|
| 757 |
# their outputs stay finite instead of softmaxing over all -inf scores.
|
| 758 |
-
attention_mask_4d = attention_mask_2d[:, None, None, :]
|
| 759 |
-
return attention_mask_2d, attention_mask_4d, None
|
| 760 |
|
| 761 |
|
| 762 |
def bool_to_additive_mask(
|
|
@@ -770,10 +790,11 @@ def bool_to_additive_mask(
|
|
| 770 |
That silently drops the mask. Always allocate a float tensor first, then fill it.
|
| 771 |
This helper is the sanctioned way to build an SDPA additive mask from a bool validity mask.
|
| 772 |
"""
|
|
|
|
| 773 |
if bool_mask.dtype != torch.bool:
|
| 774 |
raise TypeError(
|
| 775 |
f"bool_to_additive_mask requires a bool tensor, got dtype={bool_mask.dtype}"
|
| 776 |
)
|
| 777 |
-
additive = torch.zeros_like(bool_mask, dtype=dtype)
|
| 778 |
additive.masked_fill_(bool_mask.logical_not(), float("-inf"))
|
| 779 |
-
return additive
|
|
|
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
import warnings
|
| 11 |
+
import torch
|
| 12 |
from collections import OrderedDict
|
| 13 |
from collections.abc import Callable
|
| 14 |
from enum import Enum
|
| 15 |
from threading import RLock
|
|
|
|
|
|
|
| 16 |
from einops import rearrange
|
| 17 |
from torch.nn import functional as F
|
| 18 |
|
| 19 |
from ._kernel_lock import load_locked_kernel
|
| 20 |
|
| 21 |
+
|
| 22 |
try:
|
| 23 |
from torch.nn.attention.flex_attention import BlockMask, create_block_mask, flex_attention
|
| 24 |
except ImportError:
|
|
|
|
| 117 |
raise RuntimeError(
|
| 118 |
"'flex_attention' was requested, but torch.create_block_mask is unavailable."
|
| 119 |
)
|
| 120 |
+
pattern = mask_pattern.detach().to(device=device).contiguous() # mask_pattern.shape
|
| 121 |
# One device-to-host transfer is required for an exact cache identity. Use
|
| 122 |
# the contiguous buffer directly instead of materializing one Python int
|
| 123 |
# per byte, which is prohibitively expensive for long batched sequences.
|
| 124 |
+
host_pattern = pattern.to(device="cpu").contiguous() # mask_pattern.shape
|
| 125 |
+
pattern_bytes = host_pattern.view(torch.uint8).numpy().tobytes(order="C") # bytes
|
| 126 |
cache_key = (
|
| 127 |
str(device),
|
| 128 |
None if dtype is None else str(dtype),
|
|
|
|
| 203 |
) -> torch.dtype:
|
| 204 |
"""Reject dtypes outside the immutable kernel manifest before dispatch."""
|
| 205 |
|
| 206 |
+
# query_states, key_states, value_states: (b, l, h, d) or (t, h, d)
|
| 207 |
tensor_dtypes = {query_states.dtype, key_states.dtype, value_states.dtype}
|
| 208 |
if len(tensor_dtypes) != 1:
|
| 209 |
observed = ", ".join(sorted(str(dtype) for dtype in tensor_dtypes))
|
|
|
|
| 244 |
) -> torch.device:
|
| 245 |
"""Require Q, K, and V on one CUDA device before loading a kernel."""
|
| 246 |
|
| 247 |
+
# query_states, key_states, value_states: (b, l, h, d) or (t, h, d)
|
| 248 |
devices = (query_states.device, key_states.device, value_states.device)
|
| 249 |
if len(set(devices)) != 1:
|
| 250 |
observed = ", ".join(str(device) for device in devices)
|
|
|
|
| 286 |
Failing to override when Q is pre-scaled applies the scale twice and breaks
|
| 287 |
parity with eager attention and SDPA.
|
| 288 |
"""
|
| 289 |
+
# query_states, key_states, value_states: (b, l, h, d)
|
| 290 |
flash_kernel, flash_kernel_variant = _ensure_flash_kernels_loaded(implementation)
|
| 291 |
if flash_kernel_variant == "flash_attn2":
|
| 292 |
+
output = flash_kernel.flash_attn_func( # (b, l, h, d) or tuple with that first
|
| 293 |
q=query_states,
|
| 294 |
k=key_states,
|
| 295 |
v=value_states,
|
|
|
|
| 297 |
softmax_scale=softmax_scale,
|
| 298 |
causal=causal,
|
| 299 |
)
|
| 300 |
+
return output[0] if isinstance(output, tuple) else output # (b, l, h, d)
|
| 301 |
if flash_kernel_variant == "flash_attn3":
|
| 302 |
+
output = flash_kernel.flash_attn_func( # (b, l, h, d) or tuple with that first
|
| 303 |
q=query_states,
|
| 304 |
k=key_states,
|
| 305 |
v=value_states,
|
|
|
|
| 307 |
causal=causal,
|
| 308 |
)
|
| 309 |
if isinstance(output, tuple):
|
| 310 |
+
return output[0] # (b, l, h, d)
|
| 311 |
+
return output # (b, l, h, d)
|
| 312 |
raise RuntimeError(f"Unsupported FlashAttention kernel variant: {flash_kernel_variant}")
|
| 313 |
|
| 314 |
|
|
|
|
| 329 |
See `_kernels_flash_forward` docstring for why `softmax_scale=1.0` must be
|
| 330 |
passed when Q has been pre-scaled by the caller.
|
| 331 |
"""
|
| 332 |
+
# query_states, key_states, value_states: (t, h, d)
|
| 333 |
+
# cu_seqlens_q, cu_seqlens_k: (b + 1,)
|
| 334 |
flash_kernel, flash_kernel_variant = _ensure_flash_kernels_loaded(implementation)
|
| 335 |
if flash_kernel_variant == "flash_attn2":
|
| 336 |
+
output = flash_kernel.flash_attn_varlen_func( # (t, h, d) or tuple with that first
|
| 337 |
q=query_states,
|
| 338 |
k=key_states,
|
| 339 |
v=value_states,
|
|
|
|
| 345 |
softmax_scale=softmax_scale,
|
| 346 |
causal=causal,
|
| 347 |
)
|
| 348 |
+
return output[0] if isinstance(output, tuple) else output # (t, h, d)
|
| 349 |
if flash_kernel_variant == "flash_attn3":
|
| 350 |
+
output = flash_kernel.flash_attn_varlen_func( # (t, h, d) or tuple with that first
|
| 351 |
q=query_states,
|
| 352 |
k=key_states,
|
| 353 |
v=value_states,
|
|
|
|
| 359 |
causal=causal,
|
| 360 |
)
|
| 361 |
if isinstance(output, tuple):
|
| 362 |
+
return output[0] # (t, h, d)
|
| 363 |
+
return output # (t, h, d)
|
| 364 |
raise RuntimeError(f"Unsupported FlashAttention kernel variant: {flash_kernel_variant}")
|
| 365 |
|
| 366 |
|
|
|
|
| 369 |
class IndexFirstAxis(torch.autograd.Function):
|
| 370 |
@staticmethod
|
| 371 |
def forward(ctx, input, indices) -> torch.Tensor:
|
| 372 |
+
# input: (n, ...); indices: (m,)
|
| 373 |
ctx.save_for_backward(indices)
|
| 374 |
if input.ndim < 2:
|
| 375 |
raise ValueError(
|
|
|
|
| 383 |
)
|
| 384 |
ctx.first_axis_dim, other_shape = input.shape[0], input.shape[1:]
|
| 385 |
second_dim = other_shape.numel()
|
| 386 |
+
return torch.gather( # (m, ...)
|
| 387 |
rearrange(input, "b ... -> b (...)"), 0, indices.unsqueeze(1).expand(-1, second_dim)
|
| 388 |
).reshape(-1, *other_shape)
|
| 389 |
|
| 390 |
@staticmethod
|
| 391 |
def backward(ctx, grad_output) -> tuple[torch.Tensor, None]:
|
| 392 |
+
# grad_output: (m, ...)
|
| 393 |
(indices,) = ctx.saved_tensors
|
| 394 |
if grad_output.ndim < 2:
|
| 395 |
raise RuntimeError(
|
|
|
|
| 397 |
"two dimensions."
|
| 398 |
)
|
| 399 |
other_shape = grad_output.shape[1:]
|
| 400 |
+
grad_output = rearrange(grad_output, "b ... -> b (...)") # (m, product(...))
|
| 401 |
+
grad_input = torch.zeros( # (n, product(...))
|
| 402 |
[ctx.first_axis_dim, grad_output.shape[1]],
|
| 403 |
device=grad_output.device,
|
| 404 |
dtype=grad_output.dtype,
|
| 405 |
)
|
| 406 |
grad_input.scatter_(0, indices.unsqueeze(1).expand(-1, grad_output.shape[1]), grad_output)
|
| 407 |
+
return grad_input.reshape(ctx.first_axis_dim, *other_shape), None # (n, ...), None
|
| 408 |
|
| 409 |
|
| 410 |
class IndexPutFirstAxis(torch.autograd.Function):
|
| 411 |
@staticmethod
|
| 412 |
def forward(ctx, values, indices, first_axis_dim) -> torch.Tensor:
|
| 413 |
+
# values: (m, ...); indices: (m,)
|
| 414 |
ctx.save_for_backward(indices)
|
| 415 |
if indices.ndim != 1:
|
| 416 |
raise ValueError(
|
|
|
|
| 422 |
"index_put_first_axis values must have at least two dimensions; "
|
| 423 |
f"received shape {tuple(values.shape)}."
|
| 424 |
)
|
| 425 |
+
output = torch.zeros( # (n, ...)
|
| 426 |
first_axis_dim, *values.shape[1:], device=values.device, dtype=values.dtype
|
| 427 |
)
|
| 428 |
output[indices] = values
|
| 429 |
+
return output # (n, ...)
|
| 430 |
|
| 431 |
@staticmethod
|
| 432 |
def backward(ctx, grad_output) -> tuple[torch.Tensor, None, None]:
|
| 433 |
+
# grad_output: (n, ...)
|
| 434 |
(indices,) = ctx.saved_tensors
|
| 435 |
+
return grad_output[indices], None, None # (m, ...), None, None
|
| 436 |
|
| 437 |
|
| 438 |
index_first_axis = IndexFirstAxis.apply
|
|
|
|
| 442 |
def pad_input(
|
| 443 |
hidden_states: torch.Tensor, indices: torch.Tensor, batch: int, seqlen: int
|
| 444 |
) -> torch.Tensor:
|
| 445 |
+
# hidden_states: (t, ...); indices: (t,)
|
| 446 |
+
output = index_put_first_axis(hidden_states, indices, batch * seqlen) # (b * l, ...)
|
| 447 |
+
return rearrange(output, "(b s) ... -> b s ...", b=batch) # (b, l, ...)
|
| 448 |
|
| 449 |
|
| 450 |
def _unpad_input(
|
|
|
|
| 460 |
tuple[torch.Tensor, torch.Tensor],
|
| 461 |
tuple[int, int],
|
| 462 |
]:
|
| 463 |
+
# query_layer, key_layer, value_layer: (b, l, h, d); attention_mask_2d: (b, l)
|
| 464 |
batch_size, seq_len, num_heads, head_dim = query_layer.shape
|
| 465 |
+
seqlens = attention_mask_2d.sum(dim=1).int() # (b,)
|
| 466 |
+
cu_seqlens = F.pad(seqlens.cumsum(0, dtype=torch.int32), (1, 0)) # (b + 1,)
|
| 467 |
max_seqlen = int(seqlens.max().item())
|
| 468 |
+
indices = attention_mask_2d.flatten().nonzero(as_tuple=False).flatten() # (t,)
|
| 469 |
+
query_layer = index_first_axis( # (t, h, d)
|
| 470 |
query_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
|
| 471 |
)
|
| 472 |
+
key_layer = index_first_axis( # (t, h, d)
|
| 473 |
key_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
|
| 474 |
)
|
| 475 |
+
value_layer = index_first_axis( # (t, h, d)
|
| 476 |
value_layer.reshape(batch_size * seq_len, num_heads, head_dim), indices
|
| 477 |
)
|
| 478 |
return (
|
|
|
|
| 493 |
) -> torch.Tensor:
|
| 494 |
"""Validate the self-attention padding mask used by the varlen kernels."""
|
| 495 |
|
| 496 |
+
# query_states, key_states, value_states: (b, l, h, d); attention_mask_2d: (b, l)
|
| 497 |
if attention_mask_2d.ndim != 2:
|
| 498 |
raise ValueError("FlashAttention padding masks must have shape (batch, sequence_length).")
|
| 499 |
expected_shape = query_states.shape[:2]
|
|
|
|
| 509 |
)
|
| 510 |
if attention_mask_2d.device != query_states.device:
|
| 511 |
raise ValueError("FlashAttention padding mask and Q, K, and V must be on the same device.")
|
| 512 |
+
return attention_mask_2d.to(dtype=torch.bool) # (b, l)
|
| 513 |
|
| 514 |
|
| 515 |
def kernels_flash_attention_func(
|
|
|
|
| 533 |
`softmax_scale=1.0`. Otherwise the flash kernel applies its default scale
|
| 534 |
again, yielding an effective `1/head_dim` scale that drifts across layers.
|
| 535 |
"""
|
| 536 |
+
# query_states, key_states, value_states: (b, l, h, d)
|
| 537 |
+
# attention_mask_2d: (b, l) or None
|
| 538 |
_validate_kernels_flash_device(
|
| 539 |
query_states,
|
| 540 |
key_states,
|
|
|
|
| 548 |
implementation,
|
| 549 |
)
|
| 550 |
if query_states.dtype != runtime_dtype:
|
| 551 |
+
query_states = query_states.to(dtype=runtime_dtype) # (b, l, h, d)
|
| 552 |
+
key_states = key_states.to(dtype=runtime_dtype) # (b, l, h, d)
|
| 553 |
+
value_states = value_states.to(dtype=runtime_dtype) # (b, l, h, d)
|
| 554 |
if attention_mask_2d is not None:
|
| 555 |
+
attention_mask_2d = _validate_flash_padding_mask( # (b, l)
|
| 556 |
query_states,
|
| 557 |
key_states,
|
| 558 |
value_states,
|
|
|
|
| 568 |
indices_q,
|
| 569 |
(cu_seqlens_q, cu_seqlens_k),
|
| 570 |
(max_seqlen_q, max_seqlen_k),
|
| 571 |
+
) = _unpad_input( # (t, h, d), (t, h, d), (t, h, d), (t,), (b + 1,), scalars
|
| 572 |
+
query_states,
|
| 573 |
+
key_states,
|
| 574 |
+
value_states,
|
| 575 |
+
attention_mask_2d,
|
| 576 |
+
)
|
| 577 |
+
attn_output_unpad = _kernels_flash_varlen_forward( # (t, h, d)
|
| 578 |
query_states=query_states,
|
| 579 |
key_states=key_states,
|
| 580 |
value_states=value_states,
|
|
|
|
| 586 |
softmax_scale=softmax_scale,
|
| 587 |
implementation=implementation,
|
| 588 |
)
|
| 589 |
+
output = pad_input(attn_output_unpad, indices_q, batch_size, q_len) # (b, l, h, d)
|
| 590 |
+
return output.masked_fill(~attention_mask_2d[:, :, None, None], 0) # (b, l, h, d)
|
| 591 |
else:
|
| 592 |
+
return _kernels_flash_forward( # (b, l, h, d)
|
| 593 |
query_states=query_states,
|
| 594 |
key_states=key_states,
|
| 595 |
value_states=value_states,
|
|
|
|
| 725 |
|
| 726 |
Returns (attention_mask_2d, attention_mask_4d, flex_block_mask).
|
| 727 |
"""
|
| 728 |
+
# attention_mask: (b, l) or None
|
| 729 |
if attention_mask is None:
|
| 730 |
return None, None, None
|
| 731 |
|
|
|
|
| 740 |
"attention_mask shape must match the input batch and sequence dimensions; "
|
| 741 |
f"expected {expected_shape}, received {tuple(attention_mask.shape)}."
|
| 742 |
)
|
| 743 |
+
attention_mask_2d = attention_mask.to(device=device, dtype=torch.bool) # (b, l)
|
| 744 |
if not bool(attention_mask_2d.any(dim=1).all()):
|
| 745 |
raise ValueError("attention_mask must keep at least one valid key per batch row.")
|
| 746 |
|
| 747 |
effective_backend = resolve_attention_backend(effective_backend)
|
| 748 |
|
| 749 |
if effective_backend.is_flash:
|
| 750 |
+
return attention_mask_2d, None, None # (b, l), None, None
|
| 751 |
|
| 752 |
if effective_backend == AttentionBackend.FLEX_ATTENTION:
|
| 753 |
if create_block_mask is None:
|
|
|
|
| 771 |
mask_semantics=mask_semantics,
|
| 772 |
mask_mod=mask_mod,
|
| 773 |
)
|
| 774 |
+
return attention_mask_2d, None, flex_block_mask # (b, l), None, BlockMask
|
| 775 |
|
| 776 |
# SDPA/manual masks only keys. Padding queries still attend to real keys, so
|
| 777 |
# their outputs stay finite instead of softmaxing over all -inf scores.
|
| 778 |
+
attention_mask_4d = attention_mask_2d[:, None, None, :] # (b, 1, 1, l)
|
| 779 |
+
return attention_mask_2d, attention_mask_4d, None # (b, l), (b, 1, 1, l), None
|
| 780 |
|
| 781 |
|
| 782 |
def bool_to_additive_mask(
|
|
|
|
| 790 |
That silently drops the mask. Always allocate a float tensor first, then fill it.
|
| 791 |
This helper is the sanctioned way to build an SDPA additive mask from a bool validity mask.
|
| 792 |
"""
|
| 793 |
+
# bool_mask: (...)
|
| 794 |
if bool_mask.dtype != torch.bool:
|
| 795 |
raise TypeError(
|
| 796 |
f"bool_to_additive_mask requires a bool tensor, got dtype={bool_mask.dtype}"
|
| 797 |
)
|
| 798 |
+
additive = torch.zeros_like(bool_mask, dtype=dtype) # (...)
|
| 799 |
additive.masked_fill_(bool_mask.logical_not(), float("-inf"))
|
| 800 |
+
return additive # (...)
|
fastplms/attention/_kernel_lock.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
import importlib.metadata
|
| 6 |
import json
|
| 7 |
import os
|
| 8 |
from pathlib import Path
|
|
@@ -15,50 +14,35 @@ def require_kernels_package() -> None:
|
|
| 15 |
import kernels # noqa: F401
|
| 16 |
except ImportError as error:
|
| 17 |
raise RuntimeError(
|
| 18 |
-
"Precompiled FlashAttention requires
|
| 19 |
) from error
|
| 20 |
|
| 21 |
|
| 22 |
def _kernel_lock_path() -> Path:
|
| 23 |
-
"""Return the lock from
|
| 24 |
source_path = Path(__file__).resolve()
|
| 25 |
-
|
| 26 |
source_path.parents[1] / "kernels.lock",
|
| 27 |
source_path.parents[3] / "kernels.lock",
|
| 28 |
-
|
| 29 |
-
try:
|
| 30 |
-
import fastplms
|
| 31 |
-
|
| 32 |
-
candidates.extend(Path(root) / "kernels.lock" for root in fastplms.__path__)
|
| 33 |
-
except (ImportError, AttributeError):
|
| 34 |
-
pass
|
| 35 |
-
for candidate in candidates:
|
| 36 |
if candidate.is_file():
|
| 37 |
return candidate
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
raise RuntimeError("FastPLMs was installed without kernels.lock.") from error
|
| 43 |
-
for relative in distribution.files or ():
|
| 44 |
-
if relative.name != "kernels.lock":
|
| 45 |
-
continue
|
| 46 |
-
candidate = Path(distribution.locate_file(relative))
|
| 47 |
-
if candidate.is_file():
|
| 48 |
-
return candidate
|
| 49 |
-
raise RuntimeError("The installed FastPLMs distribution does not contain kernels.lock.")
|
| 50 |
|
| 51 |
|
| 52 |
def _locked_entry(lock_path: Path, repository: str) -> dict[str, Any]:
|
| 53 |
try:
|
| 54 |
-
|
| 55 |
except (OSError, json.JSONDecodeError) as error:
|
| 56 |
-
raise RuntimeError(f"Unable to read the
|
| 57 |
-
if not isinstance(
|
| 58 |
raise RuntimeError("kernels.lock must contain a JSON list.")
|
| 59 |
-
if any(not isinstance(entry, dict) for entry in
|
| 60 |
raise RuntimeError("Every kernels.lock entry must be a JSON object.")
|
| 61 |
-
matches = [entry for entry in
|
| 62 |
if len(matches) != 1:
|
| 63 |
raise RuntimeError(
|
| 64 |
f"kernels.lock must contain exactly one entry for {repository!r}; found {len(matches)}."
|
|
@@ -125,11 +109,11 @@ def _load_offline_locked_kernel(
|
|
| 125 |
from kernels.variants import get_variants_local, resolve_variants
|
| 126 |
except ImportError as error:
|
| 127 |
raise RuntimeError(
|
| 128 |
-
"Precompiled FlashAttention requires
|
| 129 |
) from error
|
| 130 |
|
| 131 |
-
|
| 132 |
-
parsed_names = {variant.variant_str for variant in
|
| 133 |
invalid = sorted(set(cached_names).difference(parsed_names))
|
| 134 |
if invalid:
|
| 135 |
raise RuntimeError(
|
|
@@ -137,7 +121,7 @@ def _load_offline_locked_kernel(
|
|
| 137 |
f"{', '.join(invalid)}"
|
| 138 |
)
|
| 139 |
|
| 140 |
-
compatible, _ = resolve_variants(
|
| 141 |
if len(compatible) != 1:
|
| 142 |
names = ", ".join(variant.variant_str for variant in compatible) or "none"
|
| 143 |
raise RuntimeError(
|
|
@@ -165,7 +149,7 @@ def load_locked_kernel(repository: str, revision: str) -> object:
|
|
| 165 |
from kernels.lockfile import KernelLock
|
| 166 |
except ImportError as error:
|
| 167 |
raise RuntimeError(
|
| 168 |
-
"Precompiled FlashAttention requires
|
| 169 |
) from error
|
| 170 |
|
| 171 |
lock_path = _kernel_lock_path()
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
import json
|
| 6 |
import os
|
| 7 |
from pathlib import Path
|
|
|
|
| 14 |
import kernels # noqa: F401
|
| 15 |
except ImportError as error:
|
| 16 |
raise RuntimeError(
|
| 17 |
+
"Precompiled FlashAttention requires requirements/features/flash.in."
|
| 18 |
) from error
|
| 19 |
|
| 20 |
|
| 21 |
def _kernel_lock_path() -> Path:
|
| 22 |
+
"""Return the kernel lock from a Hub artifact or source checkout."""
|
| 23 |
source_path = Path(__file__).resolve()
|
| 24 |
+
for candidate in (
|
| 25 |
source_path.parents[1] / "kernels.lock",
|
| 26 |
source_path.parents[3] / "kernels.lock",
|
| 27 |
+
):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
if candidate.is_file():
|
| 29 |
return candidate
|
| 30 |
|
| 31 |
+
raise RuntimeError(
|
| 32 |
+
"kernels.lock is missing from the Hugging Face artifact or source checkout."
|
| 33 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
|
| 36 |
def _locked_entry(lock_path: Path, repository: str) -> dict[str, Any]:
|
| 37 |
try:
|
| 38 |
+
lock_entries = json.loads(lock_path.read_text(encoding="utf-8"))
|
| 39 |
except (OSError, json.JSONDecodeError) as error:
|
| 40 |
+
raise RuntimeError(f"Unable to read the kernel lock: {lock_path}") from error
|
| 41 |
+
if not isinstance(lock_entries, list):
|
| 42 |
raise RuntimeError("kernels.lock must contain a JSON list.")
|
| 43 |
+
if any(not isinstance(entry, dict) for entry in lock_entries):
|
| 44 |
raise RuntimeError("Every kernels.lock entry must be a JSON object.")
|
| 45 |
+
matches = [entry for entry in lock_entries if entry.get("repo_id") == repository]
|
| 46 |
if len(matches) != 1:
|
| 47 |
raise RuntimeError(
|
| 48 |
f"kernels.lock must contain exactly one entry for {repository!r}; found {len(matches)}."
|
|
|
|
| 109 |
from kernels.variants import get_variants_local, resolve_variants
|
| 110 |
except ImportError as error:
|
| 111 |
raise RuntimeError(
|
| 112 |
+
"Precompiled FlashAttention requires requirements/features/flash.in."
|
| 113 |
) from error
|
| 114 |
|
| 115 |
+
cached_variants = get_variants_local(build_root)
|
| 116 |
+
parsed_names = {variant.variant_str for variant in cached_variants}
|
| 117 |
invalid = sorted(set(cached_names).difference(parsed_names))
|
| 118 |
if invalid:
|
| 119 |
raise RuntimeError(
|
|
|
|
| 121 |
f"{', '.join(invalid)}"
|
| 122 |
)
|
| 123 |
|
| 124 |
+
compatible, _ = resolve_variants(cached_variants)
|
| 125 |
if len(compatible) != 1:
|
| 126 |
names = ", ".join(variant.variant_str for variant in compatible) or "none"
|
| 127 |
raise RuntimeError(
|
|
|
|
| 149 |
from kernels.lockfile import KernelLock
|
| 150 |
except ImportError as error:
|
| 151 |
raise RuntimeError(
|
| 152 |
+
"Precompiled FlashAttention requires requirements/features/flash.in."
|
| 153 |
) from error
|
| 154 |
|
| 155 |
lock_path = _kernel_lock_path()
|
fastplms/attention/interfaces.py
CHANGED
|
@@ -2,11 +2,10 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
from collections.abc import Mapping
|
| 6 |
from functools import partial
|
| 7 |
from typing import Any
|
| 8 |
-
|
| 9 |
-
import torch
|
| 10 |
from transformers import AttentionInterface, AttentionMaskInterface
|
| 11 |
|
| 12 |
from ._core import (
|
|
@@ -36,6 +35,7 @@ def _kernels_attention_forward(
|
|
| 36 |
FastPLMs kernel adapter uses the latter layout internally.
|
| 37 |
"""
|
| 38 |
|
|
|
|
| 39 |
dropout = float(kwargs.get("dropout", 0.0) or 0.0)
|
| 40 |
if module.training and dropout:
|
| 41 |
raise RuntimeError(
|
|
@@ -45,15 +45,15 @@ def _kernels_attention_forward(
|
|
| 45 |
causal = bool(kwargs.get("is_causal", getattr(module, "is_causal", False)))
|
| 46 |
softmax_scale = kwargs.get("scaling")
|
| 47 |
output = kernels_flash_attention_func(
|
| 48 |
-
query_states=query.transpose(1, 2).contiguous(),
|
| 49 |
-
key_states=key.transpose(1, 2).contiguous(),
|
| 50 |
-
value_states=value.transpose(1, 2).contiguous(),
|
| 51 |
attention_mask_2d=attention_mask,
|
| 52 |
causal=causal,
|
| 53 |
softmax_scale=softmax_scale,
|
| 54 |
implementation=implementation,
|
| 55 |
-
)
|
| 56 |
-
return output, None
|
| 57 |
|
| 58 |
|
| 59 |
# Keep FastPLMs' kernels-only adapters local to this registry instance.
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import torch
|
| 6 |
from collections.abc import Mapping
|
| 7 |
from functools import partial
|
| 8 |
from typing import Any
|
|
|
|
|
|
|
| 9 |
from transformers import AttentionInterface, AttentionMaskInterface
|
| 10 |
|
| 11 |
from ._core import (
|
|
|
|
| 35 |
FastPLMs kernel adapter uses the latter layout internally.
|
| 36 |
"""
|
| 37 |
|
| 38 |
+
# query, key, value: (b, h, l, d); attention_mask: (b, l) or None
|
| 39 |
dropout = float(kwargs.get("dropout", 0.0) or 0.0)
|
| 40 |
if module.training and dropout:
|
| 41 |
raise RuntimeError(
|
|
|
|
| 45 |
causal = bool(kwargs.get("is_causal", getattr(module, "is_causal", False)))
|
| 46 |
softmax_scale = kwargs.get("scaling")
|
| 47 |
output = kernels_flash_attention_func(
|
| 48 |
+
query_states=query.transpose(1, 2).contiguous(), # (b, l, h, d)
|
| 49 |
+
key_states=key.transpose(1, 2).contiguous(), # (b, l, h, d)
|
| 50 |
+
value_states=value.transpose(1, 2).contiguous(), # (b, l, h, d)
|
| 51 |
attention_mask_2d=attention_mask,
|
| 52 |
causal=causal,
|
| 53 |
softmax_scale=softmax_scale,
|
| 54 |
implementation=implementation,
|
| 55 |
+
) # (b, l, h, d)
|
| 56 |
+
return output, None # (b, l, h, d), None
|
| 57 |
|
| 58 |
|
| 59 |
# Keep FastPLMs' kernels-only adapters local to this registry instance.
|
fastplms/embeddings/__init__.py
CHANGED
|
@@ -33,6 +33,7 @@ from .types import (
|
|
| 33 |
TensorValue,
|
| 34 |
)
|
| 35 |
|
|
|
|
| 36 |
__all__ = [
|
| 37 |
"DEFAULT_SHARD_SIZE",
|
| 38 |
"POOLING_NAMES",
|
|
|
|
| 33 |
TensorValue,
|
| 34 |
)
|
| 35 |
|
| 36 |
+
|
| 37 |
__all__ = [
|
| 38 |
"DEFAULT_SHARD_SIZE",
|
| 39 |
"POOLING_NAMES",
|
fastplms/embeddings/pooling.py
CHANGED
|
@@ -3,15 +3,16 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import math
|
| 6 |
-
from collections.abc import Sequence
|
| 7 |
-
|
| 8 |
import torch
|
|
|
|
| 9 |
from torch import Tensor
|
| 10 |
|
|
|
|
| 11 |
POOLING_NAMES = frozenset({"mean", "max", "norm", "median", "std", "var", "cls", "parti"})
|
| 12 |
|
| 13 |
|
| 14 |
def _validate_inputs(X: Tensor, M: Tensor) -> Tensor:
|
|
|
|
| 15 |
if not isinstance(X, Tensor) or not isinstance(M, Tensor):
|
| 16 |
raise TypeError("X and M must be tensors.")
|
| 17 |
if X.ndim != 3:
|
|
@@ -24,12 +25,12 @@ def _validate_inputs(X: Tensor, M: Tensor) -> Tensor:
|
|
| 24 |
raise TypeError("M must be a boolean or binary numeric residue mask.")
|
| 25 |
if not bool(torch.isfinite(M).all()) or not bool(((M == 0) | (M == 1)).all()):
|
| 26 |
raise ValueError("M must contain only finite binary mask values.")
|
| 27 |
-
M = M.to(device=X.device, dtype=torch.bool)
|
| 28 |
if not bool(M.any(dim=1).all()):
|
| 29 |
raise ValueError("Every sample must contain at least one biological residue.")
|
| 30 |
if not bool((torch.isfinite(X) | ~M.unsqueeze(-1)).all()):
|
| 31 |
raise ValueError("Biological residue embeddings produced non-finite output.")
|
| 32 |
-
return M
|
| 33 |
|
| 34 |
|
| 35 |
def _pooled_attention(attentions: Tensor | Sequence[Tensor], *, batch_size: int) -> Tensor:
|
|
@@ -43,27 +44,27 @@ def _pooled_attention(attentions: Tensor | Sequence[Tensor], *, batch_size: int)
|
|
| 43 |
if isinstance(attentions, Sequence):
|
| 44 |
if not attentions:
|
| 45 |
raise ValueError("parti received an empty attention sequence.")
|
| 46 |
-
# Each A_i
|
| 47 |
-
A = torch.stack(tuple(attentions), dim=1)
|
| 48 |
else:
|
| 49 |
-
A = attentions
|
| 50 |
|
| 51 |
if A.ndim == 5:
|
| 52 |
if A.shape[0] != batch_size and A.shape[1] == batch_size:
|
| 53 |
-
A = A.transpose(0, 1)
|
| 54 |
if A.shape[0] != batch_size:
|
| 55 |
raise ValueError("Five-dimensional attentions must use (b, n, h, l, l).")
|
| 56 |
-
A = A.flatten(1, 2).amax(dim=1)
|
| 57 |
elif A.ndim == 4:
|
| 58 |
if A.shape[0] != batch_size:
|
| 59 |
raise ValueError("Four-dimensional attentions must use (b, h, l, l).")
|
| 60 |
-
A = A.amax(dim=1)
|
| 61 |
elif A.ndim == 3:
|
| 62 |
if A.shape[0] != batch_size:
|
| 63 |
raise ValueError("Three-dimensional attentions must use (b, l, l).")
|
| 64 |
else:
|
| 65 |
raise ValueError("Attentions must have shape (b, l, l), (b, h, l, l), or (b, n, h, l, l).")
|
| 66 |
-
return A
|
| 67 |
|
| 68 |
|
| 69 |
def pagerank_weights(
|
|
@@ -79,6 +80,7 @@ def pagerank_weights(
|
|
| 79 |
probabilities; dangling rows transition uniformly.
|
| 80 |
"""
|
| 81 |
|
|
|
|
| 82 |
if not isinstance(A, Tensor):
|
| 83 |
raise TypeError("A must be a tensor.")
|
| 84 |
if A.ndim != 2 or A.shape[0] != A.shape[1]:
|
|
@@ -103,19 +105,23 @@ def pagerank_weights(
|
|
| 103 |
if not bool(torch.isfinite(A).all()):
|
| 104 |
raise ValueError("A must contain only finite attention values.")
|
| 105 |
work_dtype = torch.float64 if A.dtype == torch.float64 else torch.float32
|
| 106 |
-
P = A.detach().to(dtype=work_dtype).clamp_min(0)
|
| 107 |
-
row_sum = P.sum(dim=-1, keepdim=True)
|
| 108 |
-
uniform = torch.full_like(P, 1.0 / length)
|
| 109 |
-
P = torch.where(
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
teleport = (1.0 - damping) / length
|
| 112 |
for _ in range(max_iterations):
|
| 113 |
-
p_next = teleport + damping * (P.transpose(0, 1) @ p)
|
| 114 |
if torch.linalg.vector_norm(p_next - p, ord=1) <= tolerance:
|
| 115 |
-
p = p_next
|
| 116 |
break
|
| 117 |
-
p = p_next
|
| 118 |
-
return p / p.sum()
|
| 119 |
|
| 120 |
|
| 121 |
class Pooler:
|
|
@@ -157,28 +163,29 @@ class Pooler:
|
|
| 157 |
attentions: Tensor | Sequence[Tensor] | None = None,
|
| 158 |
attention_backend: str | None = None,
|
| 159 |
) -> Tensor:
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
| 164 |
outputs: list[Tensor] = []
|
| 165 |
|
| 166 |
for name in self.names:
|
| 167 |
if name == "mean":
|
| 168 |
-
Y = X_residues.sum(dim=1) / count
|
| 169 |
elif name == "max":
|
| 170 |
-
Y = X.masked_fill(~M_expanded, -torch.inf).max(dim=1).values
|
| 171 |
elif name == "norm":
|
| 172 |
-
Y = torch.linalg.vector_norm(X_residues, ord=2, dim=1)
|
| 173 |
elif name == "median":
|
| 174 |
-
Y = X.masked_fill(~M_expanded, torch.nan).nanmedian(dim=1).values
|
| 175 |
elif name in {"var", "std"}:
|
| 176 |
-
mean = X_residues.sum(dim=1, keepdim=True) / count.unsqueeze(1)
|
| 177 |
-
centered = (X - mean).masked_fill(~M_expanded, 0)
|
| 178 |
-
variance = (centered**2).sum(dim=1) / count
|
| 179 |
-
Y = variance.sqrt() if name == "std" else variance
|
| 180 |
elif name == "cls":
|
| 181 |
-
Y = X[:, 0]
|
| 182 |
else:
|
| 183 |
if attention_backend != "eager":
|
| 184 |
raise ValueError(
|
|
@@ -189,14 +196,15 @@ class Pooler:
|
|
| 189 |
raise ValueError("parti requires model attention matrices.")
|
| 190 |
if int(M.sum(dim=1).max().item()) > 2048:
|
| 191 |
raise ValueError("parti supports at most 2,048 biological residues.")
|
| 192 |
-
A = _pooled_attention(attentions, batch_size=X.shape[0]).to(X.device)
|
| 193 |
pooled: list[Tensor] = []
|
| 194 |
for X_i, M_i, A_i in zip(X, M, A, strict=True):
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
| 200 |
if not bool(torch.isfinite(Y).all()):
|
| 201 |
raise ValueError(
|
| 202 |
f"Pooling operation {name!r} produced non-finite output from "
|
|
@@ -204,7 +212,7 @@ class Pooler:
|
|
| 204 |
)
|
| 205 |
outputs.append(Y)
|
| 206 |
|
| 207 |
-
return torch.cat(outputs, dim=-1)
|
| 208 |
|
| 209 |
|
| 210 |
__all__ = ["POOLING_NAMES", "Pooler", "pagerank_weights"]
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import math
|
|
|
|
|
|
|
| 6 |
import torch
|
| 7 |
+
from collections.abc import Sequence
|
| 8 |
from torch import Tensor
|
| 9 |
|
| 10 |
+
|
| 11 |
POOLING_NAMES = frozenset({"mean", "max", "norm", "median", "std", "var", "cls", "parti"})
|
| 12 |
|
| 13 |
|
| 14 |
def _validate_inputs(X: Tensor, M: Tensor) -> Tensor:
|
| 15 |
+
# X: (b, l, d); M: (b, l)
|
| 16 |
if not isinstance(X, Tensor) or not isinstance(M, Tensor):
|
| 17 |
raise TypeError("X and M must be tensors.")
|
| 18 |
if X.ndim != 3:
|
|
|
|
| 25 |
raise TypeError("M must be a boolean or binary numeric residue mask.")
|
| 26 |
if not bool(torch.isfinite(M).all()) or not bool(((M == 0) | (M == 1)).all()):
|
| 27 |
raise ValueError("M must contain only finite binary mask values.")
|
| 28 |
+
M = M.to(device=X.device, dtype=torch.bool) # (b, l)
|
| 29 |
if not bool(M.any(dim=1).all()):
|
| 30 |
raise ValueError("Every sample must contain at least one biological residue.")
|
| 31 |
if not bool((torch.isfinite(X) | ~M.unsqueeze(-1)).all()):
|
| 32 |
raise ValueError("Biological residue embeddings produced non-finite output.")
|
| 33 |
+
return M # (b, l)
|
| 34 |
|
| 35 |
|
| 36 |
def _pooled_attention(attentions: Tensor | Sequence[Tensor], *, batch_size: int) -> Tensor:
|
|
|
|
| 44 |
if isinstance(attentions, Sequence):
|
| 45 |
if not attentions:
|
| 46 |
raise ValueError("parti received an empty attention sequence.")
|
| 47 |
+
# Each A_i: (b, h, l, l).
|
| 48 |
+
A = torch.stack(tuple(attentions), dim=1) # (b, n, h, l, l)
|
| 49 |
else:
|
| 50 |
+
A = attentions # (b, ..., l, l)
|
| 51 |
|
| 52 |
if A.ndim == 5:
|
| 53 |
if A.shape[0] != batch_size and A.shape[1] == batch_size:
|
| 54 |
+
A = A.transpose(0, 1) # (b, n, h, l, l)
|
| 55 |
if A.shape[0] != batch_size:
|
| 56 |
raise ValueError("Five-dimensional attentions must use (b, n, h, l, l).")
|
| 57 |
+
A = A.flatten(1, 2).amax(dim=1) # (b, l, l)
|
| 58 |
elif A.ndim == 4:
|
| 59 |
if A.shape[0] != batch_size:
|
| 60 |
raise ValueError("Four-dimensional attentions must use (b, h, l, l).")
|
| 61 |
+
A = A.amax(dim=1) # (b, l, l)
|
| 62 |
elif A.ndim == 3:
|
| 63 |
if A.shape[0] != batch_size:
|
| 64 |
raise ValueError("Three-dimensional attentions must use (b, l, l).")
|
| 65 |
else:
|
| 66 |
raise ValueError("Attentions must have shape (b, l, l), (b, h, l, l), or (b, n, h, l, l).")
|
| 67 |
+
return A # (b, l, l)
|
| 68 |
|
| 69 |
|
| 70 |
def pagerank_weights(
|
|
|
|
| 80 |
probabilities; dangling rows transition uniformly.
|
| 81 |
"""
|
| 82 |
|
| 83 |
+
# A: (l, l)
|
| 84 |
if not isinstance(A, Tensor):
|
| 85 |
raise TypeError("A must be a tensor.")
|
| 86 |
if A.ndim != 2 or A.shape[0] != A.shape[1]:
|
|
|
|
| 105 |
if not bool(torch.isfinite(A).all()):
|
| 106 |
raise ValueError("A must contain only finite attention values.")
|
| 107 |
work_dtype = torch.float64 if A.dtype == torch.float64 else torch.float32
|
| 108 |
+
P = A.detach().to(dtype=work_dtype).clamp_min(0) # (l, l)
|
| 109 |
+
row_sum = P.sum(dim=-1, keepdim=True) # (l, 1)
|
| 110 |
+
uniform = torch.full_like(P, 1.0 / length) # (l, l)
|
| 111 |
+
P = torch.where( # (l, l)
|
| 112 |
+
row_sum > 0,
|
| 113 |
+
P / row_sum.clamp_min(torch.finfo(work_dtype).tiny),
|
| 114 |
+
uniform,
|
| 115 |
+
)
|
| 116 |
+
p = torch.full((length,), 1.0 / length, device=P.device, dtype=work_dtype) # (l,)
|
| 117 |
teleport = (1.0 - damping) / length
|
| 118 |
for _ in range(max_iterations):
|
| 119 |
+
p_next = teleport + damping * (P.transpose(0, 1) @ p) # (l,)
|
| 120 |
if torch.linalg.vector_norm(p_next - p, ord=1) <= tolerance:
|
| 121 |
+
p = p_next # (l,)
|
| 122 |
break
|
| 123 |
+
p = p_next # (l,)
|
| 124 |
+
return p / p.sum() # (l,)
|
| 125 |
|
| 126 |
|
| 127 |
class Pooler:
|
|
|
|
| 163 |
attentions: Tensor | Sequence[Tensor] | None = None,
|
| 164 |
attention_backend: str | None = None,
|
| 165 |
) -> Tensor:
|
| 166 |
+
# X: (b, l, d); residue_mask: (b, l)
|
| 167 |
+
M = _validate_inputs(X, residue_mask) # (b, l)
|
| 168 |
+
M_expanded = M.unsqueeze(-1) # (b, l, 1)
|
| 169 |
+
count = M_expanded.sum(dim=1).clamp_min(1) # (b, 1)
|
| 170 |
+
X_residues = X.masked_fill(~M_expanded, 0) # (b, l, d)
|
| 171 |
outputs: list[Tensor] = []
|
| 172 |
|
| 173 |
for name in self.names:
|
| 174 |
if name == "mean":
|
| 175 |
+
Y = X_residues.sum(dim=1) / count # (b, d)
|
| 176 |
elif name == "max":
|
| 177 |
+
Y = X.masked_fill(~M_expanded, -torch.inf).max(dim=1).values # (b, d)
|
| 178 |
elif name == "norm":
|
| 179 |
+
Y = torch.linalg.vector_norm(X_residues, ord=2, dim=1) # (b, d)
|
| 180 |
elif name == "median":
|
| 181 |
+
Y = X.masked_fill(~M_expanded, torch.nan).nanmedian(dim=1).values # (b, d)
|
| 182 |
elif name in {"var", "std"}:
|
| 183 |
+
mean = X_residues.sum(dim=1, keepdim=True) / count.unsqueeze(1) # (b, 1, d)
|
| 184 |
+
centered = (X - mean).masked_fill(~M_expanded, 0) # (b, l, d)
|
| 185 |
+
variance = (centered**2).sum(dim=1) / count # (b, d)
|
| 186 |
+
Y = variance.sqrt() if name == "std" else variance # (b, d)
|
| 187 |
elif name == "cls":
|
| 188 |
+
Y = X[:, 0] # (b, d)
|
| 189 |
else:
|
| 190 |
if attention_backend != "eager":
|
| 191 |
raise ValueError(
|
|
|
|
| 196 |
raise ValueError("parti requires model attention matrices.")
|
| 197 |
if int(M.sum(dim=1).max().item()) > 2048:
|
| 198 |
raise ValueError("parti supports at most 2,048 biological residues.")
|
| 199 |
+
A = _pooled_attention(attentions, batch_size=X.shape[0]).to(X.device) # (b, l, l)
|
| 200 |
pooled: list[Tensor] = []
|
| 201 |
for X_i, M_i, A_i in zip(X, M, A, strict=True):
|
| 202 |
+
# X_i: (l, d); M_i: (l,); A_i: (l, l)
|
| 203 |
+
indices = M_i.nonzero(as_tuple=True)[0] # (r,)
|
| 204 |
+
A_residue = A_i.index_select(0, indices).index_select(1, indices) # (r, r)
|
| 205 |
+
w = pagerank_weights(A_residue).to(dtype=X.dtype) # (r,)
|
| 206 |
+
pooled.append(w @ X_i.index_select(0, indices)) # (d,)
|
| 207 |
+
Y = torch.stack(pooled) # (b, d)
|
| 208 |
if not bool(torch.isfinite(Y).all()):
|
| 209 |
raise ValueError(
|
| 210 |
f"Pooling operation {name!r} produced non-finite output from "
|
|
|
|
| 212 |
)
|
| 213 |
outputs.append(Y)
|
| 214 |
|
| 215 |
+
return torch.cat(outputs, dim=-1) # (b, len(self.names) * d)
|
| 216 |
|
| 217 |
|
| 218 |
__all__ = ["POOLING_NAMES", "Pooler", "pagerank_weights"]
|
fastplms/embeddings/runner.py
CHANGED
|
@@ -7,12 +7,11 @@ import json
|
|
| 7 |
import platform
|
| 8 |
import sqlite3
|
| 9 |
import tempfile
|
|
|
|
| 10 |
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
|
| 11 |
from contextlib import contextmanager
|
| 12 |
from pathlib import Path
|
| 13 |
from typing import Any, overload
|
| 14 |
-
|
| 15 |
-
import torch
|
| 16 |
from torch import Tensor
|
| 17 |
|
| 18 |
from .pooling import Pooler
|
|
@@ -35,6 +34,7 @@ from .types import (
|
|
| 35 |
LazyTensorReference,
|
| 36 |
)
|
| 37 |
|
|
|
|
| 38 |
_MAX_PARTI_RESIDUES = 2_048
|
| 39 |
_RUN_FINGERPRINT_SCHEMA_VERSION = 3
|
| 40 |
_MODEL_STATE_HASH_CHUNK_BYTES = 16 * 1024**2
|
|
@@ -45,6 +45,7 @@ _SUPPORTED_STORAGE_FORMATS = frozenset({"safetensors", "sqlite"})
|
|
| 45 |
def _validate_parti_length(M: Tensor) -> None:
|
| 46 |
"""Reject an oversized attention graph before model inference."""
|
| 47 |
|
|
|
|
| 48 |
n_residues = int(M.to(dtype=torch.int64).sum(dim=1).max().item())
|
| 49 |
if n_residues > _MAX_PARTI_RESIDUES:
|
| 50 |
raise ValueError(f"parti supports at most {_MAX_PARTI_RESIDUES:,} biological residues.")
|
|
@@ -58,16 +59,17 @@ def select_hidden_state_embeddings(
|
|
| 58 |
store_all_hidden_states: bool = False,
|
| 59 |
) -> Tensor:
|
| 60 |
"""Select one hidden state or stack every state without changing values."""
|
|
|
|
| 61 |
if store_all_hidden_states:
|
| 62 |
if not hidden_states:
|
| 63 |
raise ValueError("store_all_hidden_states requires model hidden states.")
|
| 64 |
# H has shape (b, n, l, d), where n follows the model's output order.
|
| 65 |
-
return torch.stack(hidden_states, dim=1)
|
| 66 |
if hidden_state_index == -1:
|
| 67 |
-
return last_hidden_state
|
| 68 |
if not hidden_states:
|
| 69 |
raise ValueError("hidden_state_index requires model hidden states.")
|
| 70 |
-
return hidden_states[hidden_state_index]
|
| 71 |
|
| 72 |
|
| 73 |
def iter_fasta(path: str | Path) -> Iterator[EmbeddingInput]:
|
|
@@ -508,12 +510,17 @@ def _biological_residue_mask(
|
|
| 508 |
) -> Tensor:
|
| 509 |
"""Remove padding and tokenizer-declared special tokens from M."""
|
| 510 |
|
| 511 |
-
|
|
|
|
| 512 |
special_ids = tuple(int(token_id) for token_id in getattr(tokenizer, "all_special_ids", ()))
|
| 513 |
if special_ids:
|
| 514 |
-
specials = torch.tensor(
|
| 515 |
-
|
| 516 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
|
| 518 |
|
| 519 |
def _generic_embedding_batch(
|
|
@@ -535,20 +542,23 @@ def _generic_embedding_batch(
|
|
| 535 |
output = model._embed(sequences, return_attention_mask=True, **model_kwargs)
|
| 536 |
if not isinstance(output, tuple) or len(output) != 2:
|
| 537 |
raise TypeError("E1 _embed must return (X, residue_mask).")
|
| 538 |
-
X, M = output
|
| 539 |
preparer = getattr(model, "prep_tokens", None)
|
| 540 |
if preparer is not None and hasattr(preparer, "get_batch_kwargs"):
|
| 541 |
prepared = preparer.get_batch_kwargs(sequences, device=X.device)
|
| 542 |
-
input_ids = prepared["input_ids"]
|
| 543 |
-
boundary_ids = preparer.boundary_token_ids.to(
|
| 544 |
device=input_ids.device, dtype=input_ids.dtype
|
| 545 |
)
|
| 546 |
# E1 wraps each raw sequence in BOS, context-label, terminal-label,
|
| 547 |
# and EOS tokens. Only amino-acid rows are biological residues.
|
| 548 |
-
M = M.to(dtype=torch.bool) & ~torch.isin(input_ids, boundary_ids)
|
| 549 |
if need_attentions:
|
| 550 |
raise ValueError("parti is not available for tokenizer-free E1 embedding.")
|
| 551 |
-
return EmbeddingBatch(X
|
|
|
|
|
|
|
|
|
|
| 552 |
if tokenizer is None:
|
| 553 |
raise ValueError("A tokenizer is required for this model's embedding path.")
|
| 554 |
|
|
@@ -572,14 +582,17 @@ def _generic_embedding_batch(
|
|
| 572 |
else:
|
| 573 |
encoded = tokenizer(sequences, **tokenize_kwargs)
|
| 574 |
device = _model_device(model)
|
| 575 |
-
input_ids = encoded["input_ids"].to(device)
|
| 576 |
-
attention_mask = encoded.get(
|
| 577 |
-
|
|
|
|
|
|
|
|
|
|
| 578 |
if need_attentions:
|
| 579 |
# Validate l before either the backbone or its quadratic attention graph
|
| 580 |
# is materialized. M has shape (b, l).
|
| 581 |
_validate_parti_length(M)
|
| 582 |
-
X = model._embed(input_ids, attention_mask, **model_kwargs)
|
| 583 |
attentions = None
|
| 584 |
if need_attentions:
|
| 585 |
output = model(
|
|
@@ -588,10 +601,14 @@ def _generic_embedding_batch(
|
|
| 588 |
output_attentions=True,
|
| 589 |
return_dict=True,
|
| 590 |
)
|
| 591 |
-
attentions = getattr(output, "attentions", None)
|
| 592 |
if attentions is None:
|
| 593 |
raise ValueError("The model did not return attentions required by parti.")
|
| 594 |
-
return EmbeddingBatch(X
|
|
|
|
|
|
|
|
|
|
|
|
|
| 595 |
|
| 596 |
|
| 597 |
def _first_metadata_value(*values: Any) -> Any:
|
|
@@ -638,6 +655,7 @@ def _model_identity_metadata(model: Any) -> dict[str, Any]:
|
|
| 638 |
def _bounded_tensor_chunks(X: Tensor, max_elements: int) -> Iterable[Tensor]:
|
| 639 |
"""Yield X in logical row-major order without materializing a full copy."""
|
| 640 |
|
|
|
|
| 641 |
if X.numel() == 0:
|
| 642 |
return
|
| 643 |
if X.ndim == 0:
|
|
@@ -649,7 +667,7 @@ def _bounded_tensor_chunks(X: Tensor, max_elements: int) -> Iterable[Tensor]:
|
|
| 649 |
if trailing_elements <= max_elements:
|
| 650 |
rows_per_chunk = max(1, max_elements // trailing_elements)
|
| 651 |
for start in range(0, X.shape[0], rows_per_chunk):
|
| 652 |
-
yield X[start : start + rows_per_chunk]
|
| 653 |
return
|
| 654 |
for row in X:
|
| 655 |
yield from _bounded_tensor_chunks(row, max_elements)
|
|
@@ -685,7 +703,7 @@ def _model_state_sha256(model: Any) -> str:
|
|
| 685 |
digest.update(header)
|
| 686 |
max_elements = max(1, _MODEL_STATE_HASH_CHUNK_BYTES // value.element_size())
|
| 687 |
for chunk in _bounded_tensor_chunks(value.detach(), max_elements):
|
| 688 |
-
cpu_chunk = chunk.to(device="cpu").contiguous()
|
| 689 |
digest.update(cpu_chunk.reshape(-1).view(torch.uint8).numpy().tobytes())
|
| 690 |
return digest.hexdigest()
|
| 691 |
|
|
@@ -1306,22 +1324,24 @@ def embed_dataset(
|
|
| 1306 |
normalized_decoder_inputs[position] for position in batch_positions
|
| 1307 |
]
|
| 1308 |
if decoder_input_ids is not None:
|
| 1309 |
-
|
|
|
|
| 1310 |
batch_positions,
|
| 1311 |
device=decoder_input_ids.device,
|
| 1312 |
dtype=torch.long,
|
| 1313 |
)
|
| 1314 |
-
batch_model_kwargs["decoder_input_ids"] =
|
| 1315 |
-
0, indices
|
| 1316 |
)
|
| 1317 |
if decoder_attention_mask is not None:
|
| 1318 |
-
|
|
|
|
| 1319 |
batch_positions,
|
| 1320 |
device=decoder_attention_mask.device,
|
| 1321 |
dtype=torch.long,
|
| 1322 |
)
|
| 1323 |
batch_model_kwargs["decoder_attention_mask"] = (
|
| 1324 |
-
decoder_attention_mask.index_select(0, indices)
|
| 1325 |
)
|
| 1326 |
custom_batch = _embedding_batch_fn or getattr(model, "_embedding_batch", None)
|
| 1327 |
if custom_batch is not None:
|
|
@@ -1348,8 +1368,8 @@ def embed_dataset(
|
|
| 1348 |
need_attentions=need_attentions,
|
| 1349 |
model_kwargs=batch_model_kwargs,
|
| 1350 |
)
|
| 1351 |
-
X = batch.X
|
| 1352 |
-
raw_mask = batch.residue_mask
|
| 1353 |
if not isinstance(X, Tensor) or not isinstance(raw_mask, Tensor):
|
| 1354 |
raise TypeError("Embedding batches must provide Tensor X and residue_mask.")
|
| 1355 |
if X.is_meta or raw_mask.is_meta:
|
|
@@ -1360,7 +1380,7 @@ def embed_dataset(
|
|
| 1360 |
raise ValueError("Embedding residue_mask must contain finite binary values.")
|
| 1361 |
if not bool(((raw_mask == 0) | (raw_mask == 1)).all()):
|
| 1362 |
raise ValueError("Embedding residue_mask must contain finite binary values.")
|
| 1363 |
-
M = raw_mask.to(device=X.device, dtype=torch.bool)
|
| 1364 |
valid_X_shape = (
|
| 1365 |
X.ndim == 3
|
| 1366 |
and X.shape[0] == len(batch_records)
|
|
@@ -1384,7 +1404,7 @@ def embed_dataset(
|
|
| 1384 |
)
|
| 1385 |
if not bool(M.any(dim=1).all()):
|
| 1386 |
raise ValueError("Every embedding sample must contain a biological residue.")
|
| 1387 |
-
finite_selected = (
|
| 1388 |
torch.isfinite(X) | ~M.unsqueeze(-1)
|
| 1389 |
if X.ndim == 3
|
| 1390 |
else torch.isfinite(X) | ~M[:, None, :, None]
|
|
@@ -1395,28 +1415,32 @@ def embed_dataset(
|
|
| 1395 |
# Validate the biological graph only after mask integrity is established.
|
| 1396 |
_validate_parti_length(M)
|
| 1397 |
if dtype is not None:
|
| 1398 |
-
X = X.to(dtype=dtype)
|
| 1399 |
|
| 1400 |
if full_embeddings:
|
| 1401 |
if X.ndim == 4:
|
| 1402 |
values = [
|
| 1403 |
-
X_i[:, M_i, :].detach().cpu()
|
|
|
|
| 1404 |
]
|
| 1405 |
else:
|
| 1406 |
-
values = [
|
|
|
|
|
|
|
|
|
|
| 1407 |
else:
|
| 1408 |
if pooler is None:
|
| 1409 |
raise RuntimeError(
|
| 1410 |
"Pooled embedding output was requested without an initialized pooler."
|
| 1411 |
)
|
| 1412 |
-
Y = pooler(
|
| 1413 |
X,
|
| 1414 |
M,
|
| 1415 |
attentions=batch.attentions,
|
| 1416 |
attention_backend=attention_backend,
|
| 1417 |
)
|
| 1418 |
pool_slices = pooler.output_slices(X.shape[-1])
|
| 1419 |
-
values = list(Y.detach().cpu().unbind(0))
|
| 1420 |
for position, record, value in zip(
|
| 1421 |
batch_positions, batch_records, values, strict=True
|
| 1422 |
):
|
|
|
|
| 7 |
import platform
|
| 8 |
import sqlite3
|
| 9 |
import tempfile
|
| 10 |
+
import torch
|
| 11 |
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
|
| 12 |
from contextlib import contextmanager
|
| 13 |
from pathlib import Path
|
| 14 |
from typing import Any, overload
|
|
|
|
|
|
|
| 15 |
from torch import Tensor
|
| 16 |
|
| 17 |
from .pooling import Pooler
|
|
|
|
| 34 |
LazyTensorReference,
|
| 35 |
)
|
| 36 |
|
| 37 |
+
|
| 38 |
_MAX_PARTI_RESIDUES = 2_048
|
| 39 |
_RUN_FINGERPRINT_SCHEMA_VERSION = 3
|
| 40 |
_MODEL_STATE_HASH_CHUNK_BYTES = 16 * 1024**2
|
|
|
|
| 45 |
def _validate_parti_length(M: Tensor) -> None:
|
| 46 |
"""Reject an oversized attention graph before model inference."""
|
| 47 |
|
| 48 |
+
# M: (b, l)
|
| 49 |
n_residues = int(M.to(dtype=torch.int64).sum(dim=1).max().item())
|
| 50 |
if n_residues > _MAX_PARTI_RESIDUES:
|
| 51 |
raise ValueError(f"parti supports at most {_MAX_PARTI_RESIDUES:,} biological residues.")
|
|
|
|
| 59 |
store_all_hidden_states: bool = False,
|
| 60 |
) -> Tensor:
|
| 61 |
"""Select one hidden state or stack every state without changing values."""
|
| 62 |
+
# last_hidden_state and each hidden_states entry: (b, l, d)
|
| 63 |
if store_all_hidden_states:
|
| 64 |
if not hidden_states:
|
| 65 |
raise ValueError("store_all_hidden_states requires model hidden states.")
|
| 66 |
# H has shape (b, n, l, d), where n follows the model's output order.
|
| 67 |
+
return torch.stack(hidden_states, dim=1) # (b, n, l, d)
|
| 68 |
if hidden_state_index == -1:
|
| 69 |
+
return last_hidden_state # (b, l, d)
|
| 70 |
if not hidden_states:
|
| 71 |
raise ValueError("hidden_state_index requires model hidden states.")
|
| 72 |
+
return hidden_states[hidden_state_index] # (b, l, d)
|
| 73 |
|
| 74 |
|
| 75 |
def iter_fasta(path: str | Path) -> Iterator[EmbeddingInput]:
|
|
|
|
| 510 |
) -> Tensor:
|
| 511 |
"""Remove padding and tokenizer-declared special tokens from M."""
|
| 512 |
|
| 513 |
+
# input_ids, attention_mask: (b, l)
|
| 514 |
+
M = attention_mask.to(dtype=torch.bool) # (b, l)
|
| 515 |
special_ids = tuple(int(token_id) for token_id in getattr(tokenizer, "all_special_ids", ()))
|
| 516 |
if special_ids:
|
| 517 |
+
specials = torch.tensor( # (n_special,)
|
| 518 |
+
special_ids,
|
| 519 |
+
device=input_ids.device,
|
| 520 |
+
dtype=input_ids.dtype,
|
| 521 |
+
)
|
| 522 |
+
M = M & ~torch.isin(input_ids, specials) # (b, l)
|
| 523 |
+
return M # (b, l)
|
| 524 |
|
| 525 |
|
| 526 |
def _generic_embedding_batch(
|
|
|
|
| 542 |
output = model._embed(sequences, return_attention_mask=True, **model_kwargs)
|
| 543 |
if not isinstance(output, tuple) or len(output) != 2:
|
| 544 |
raise TypeError("E1 _embed must return (X, residue_mask).")
|
| 545 |
+
X, M = output # (b, l, d), (b, l)
|
| 546 |
preparer = getattr(model, "prep_tokens", None)
|
| 547 |
if preparer is not None and hasattr(preparer, "get_batch_kwargs"):
|
| 548 |
prepared = preparer.get_batch_kwargs(sequences, device=X.device)
|
| 549 |
+
input_ids = prepared["input_ids"] # (b, l)
|
| 550 |
+
boundary_ids = preparer.boundary_token_ids.to( # (n_boundary,)
|
| 551 |
device=input_ids.device, dtype=input_ids.dtype
|
| 552 |
)
|
| 553 |
# E1 wraps each raw sequence in BOS, context-label, terminal-label,
|
| 554 |
# and EOS tokens. Only amino-acid rows are biological residues.
|
| 555 |
+
M = M.to(dtype=torch.bool) & ~torch.isin(input_ids, boundary_ids) # (b, l)
|
| 556 |
if need_attentions:
|
| 557 |
raise ValueError("parti is not available for tokenizer-free E1 embedding.")
|
| 558 |
+
return EmbeddingBatch( # X: (b, l, d); residue_mask: (b, l)
|
| 559 |
+
X=X,
|
| 560 |
+
residue_mask=M.to(dtype=torch.bool),
|
| 561 |
+
)
|
| 562 |
if tokenizer is None:
|
| 563 |
raise ValueError("A tokenizer is required for this model's embedding path.")
|
| 564 |
|
|
|
|
| 582 |
else:
|
| 583 |
encoded = tokenizer(sequences, **tokenize_kwargs)
|
| 584 |
device = _model_device(model)
|
| 585 |
+
input_ids = encoded["input_ids"].to(device) # (b, l)
|
| 586 |
+
attention_mask = encoded.get( # (b, l)
|
| 587 |
+
"attention_mask",
|
| 588 |
+
input_ids.new_ones(input_ids.shape),
|
| 589 |
+
).to(device)
|
| 590 |
+
M = _biological_residue_mask(input_ids, attention_mask, tokenizer) # (b, l)
|
| 591 |
if need_attentions:
|
| 592 |
# Validate l before either the backbone or its quadratic attention graph
|
| 593 |
# is materialized. M has shape (b, l).
|
| 594 |
_validate_parti_length(M)
|
| 595 |
+
X = model._embed(input_ids, attention_mask, **model_kwargs) # (b, l, d)
|
| 596 |
attentions = None
|
| 597 |
if need_attentions:
|
| 598 |
output = model(
|
|
|
|
| 601 |
output_attentions=True,
|
| 602 |
return_dict=True,
|
| 603 |
)
|
| 604 |
+
attentions = getattr(output, "attentions", None) # each: (b, h, l, l)
|
| 605 |
if attentions is None:
|
| 606 |
raise ValueError("The model did not return attentions required by parti.")
|
| 607 |
+
return EmbeddingBatch( # X: (b, l, d); M: (b, l)
|
| 608 |
+
X=X,
|
| 609 |
+
residue_mask=M,
|
| 610 |
+
attentions=attentions,
|
| 611 |
+
)
|
| 612 |
|
| 613 |
|
| 614 |
def _first_metadata_value(*values: Any) -> Any:
|
|
|
|
| 655 |
def _bounded_tensor_chunks(X: Tensor, max_elements: int) -> Iterable[Tensor]:
|
| 656 |
"""Yield X in logical row-major order without materializing a full copy."""
|
| 657 |
|
| 658 |
+
# X: (...)
|
| 659 |
if X.numel() == 0:
|
| 660 |
return
|
| 661 |
if X.ndim == 0:
|
|
|
|
| 667 |
if trailing_elements <= max_elements:
|
| 668 |
rows_per_chunk = max(1, max_elements // trailing_elements)
|
| 669 |
for start in range(0, X.shape[0], rows_per_chunk):
|
| 670 |
+
yield X[start : start + rows_per_chunk] # (chunk_rows, ...)
|
| 671 |
return
|
| 672 |
for row in X:
|
| 673 |
yield from _bounded_tensor_chunks(row, max_elements)
|
|
|
|
| 703 |
digest.update(header)
|
| 704 |
max_elements = max(1, _MODEL_STATE_HASH_CHUNK_BYTES // value.element_size())
|
| 705 |
for chunk in _bounded_tensor_chunks(value.detach(), max_elements):
|
| 706 |
+
cpu_chunk = chunk.to(device="cpu").contiguous() # chunk.shape
|
| 707 |
digest.update(cpu_chunk.reshape(-1).view(torch.uint8).numpy().tobytes())
|
| 708 |
return digest.hexdigest()
|
| 709 |
|
|
|
|
| 1324 |
normalized_decoder_inputs[position] for position in batch_positions
|
| 1325 |
]
|
| 1326 |
if decoder_input_ids is not None:
|
| 1327 |
+
# decoder_input_ids: (n_records, l_decoder)
|
| 1328 |
+
indices = torch.tensor( # (b,)
|
| 1329 |
batch_positions,
|
| 1330 |
device=decoder_input_ids.device,
|
| 1331 |
dtype=torch.long,
|
| 1332 |
)
|
| 1333 |
+
batch_model_kwargs["decoder_input_ids"] = ( # (b, l_decoder)
|
| 1334 |
+
decoder_input_ids.index_select(0, indices)
|
| 1335 |
)
|
| 1336 |
if decoder_attention_mask is not None:
|
| 1337 |
+
# decoder_attention_mask: (n_records, l_decoder)
|
| 1338 |
+
indices = torch.tensor( # (b,)
|
| 1339 |
batch_positions,
|
| 1340 |
device=decoder_attention_mask.device,
|
| 1341 |
dtype=torch.long,
|
| 1342 |
)
|
| 1343 |
batch_model_kwargs["decoder_attention_mask"] = (
|
| 1344 |
+
decoder_attention_mask.index_select(0, indices) # (b, l_decoder)
|
| 1345 |
)
|
| 1346 |
custom_batch = _embedding_batch_fn or getattr(model, "_embedding_batch", None)
|
| 1347 |
if custom_batch is not None:
|
|
|
|
| 1368 |
need_attentions=need_attentions,
|
| 1369 |
model_kwargs=batch_model_kwargs,
|
| 1370 |
)
|
| 1371 |
+
X = batch.X # (b, l, d) or (b, n_states, l, d)
|
| 1372 |
+
raw_mask = batch.residue_mask # (b, l)
|
| 1373 |
if not isinstance(X, Tensor) or not isinstance(raw_mask, Tensor):
|
| 1374 |
raise TypeError("Embedding batches must provide Tensor X and residue_mask.")
|
| 1375 |
if X.is_meta or raw_mask.is_meta:
|
|
|
|
| 1380 |
raise ValueError("Embedding residue_mask must contain finite binary values.")
|
| 1381 |
if not bool(((raw_mask == 0) | (raw_mask == 1)).all()):
|
| 1382 |
raise ValueError("Embedding residue_mask must contain finite binary values.")
|
| 1383 |
+
M = raw_mask.to(device=X.device, dtype=torch.bool) # (b, l)
|
| 1384 |
valid_X_shape = (
|
| 1385 |
X.ndim == 3
|
| 1386 |
and X.shape[0] == len(batch_records)
|
|
|
|
| 1404 |
)
|
| 1405 |
if not bool(M.any(dim=1).all()):
|
| 1406 |
raise ValueError("Every embedding sample must contain a biological residue.")
|
| 1407 |
+
finite_selected = ( # X.shape
|
| 1408 |
torch.isfinite(X) | ~M.unsqueeze(-1)
|
| 1409 |
if X.ndim == 3
|
| 1410 |
else torch.isfinite(X) | ~M[:, None, :, None]
|
|
|
|
| 1415 |
# Validate the biological graph only after mask integrity is established.
|
| 1416 |
_validate_parti_length(M)
|
| 1417 |
if dtype is not None:
|
| 1418 |
+
X = X.to(dtype=dtype) # unchanged shape
|
| 1419 |
|
| 1420 |
if full_embeddings:
|
| 1421 |
if X.ndim == 4:
|
| 1422 |
values = [
|
| 1423 |
+
X_i[:, M_i, :].detach().cpu() # (n_states, r_i, d)
|
| 1424 |
+
for X_i, M_i in zip(X, M, strict=True)
|
| 1425 |
]
|
| 1426 |
else:
|
| 1427 |
+
values = [
|
| 1428 |
+
X_i[M_i].detach().cpu() # (r_i, d)
|
| 1429 |
+
for X_i, M_i in zip(X, M, strict=True)
|
| 1430 |
+
]
|
| 1431 |
else:
|
| 1432 |
if pooler is None:
|
| 1433 |
raise RuntimeError(
|
| 1434 |
"Pooled embedding output was requested without an initialized pooler."
|
| 1435 |
)
|
| 1436 |
+
Y = pooler( # (b, n_poolers * d)
|
| 1437 |
X,
|
| 1438 |
M,
|
| 1439 |
attentions=batch.attentions,
|
| 1440 |
attention_backend=attention_backend,
|
| 1441 |
)
|
| 1442 |
pool_slices = pooler.output_slices(X.shape[-1])
|
| 1443 |
+
values = list(Y.detach().cpu().unbind(0)) # each: (n_poolers * d,)
|
| 1444 |
for position, record, value in zip(
|
| 1445 |
batch_positions, batch_records, values, strict=True
|
| 1446 |
):
|
fastplms/embeddings/storage.py
CHANGED
|
@@ -7,14 +7,13 @@ import io
|
|
| 7 |
import json
|
| 8 |
import sqlite3
|
| 9 |
import struct
|
|
|
|
|
|
|
| 10 |
from bisect import bisect_right
|
| 11 |
from collections.abc import Iterable, Iterator, Sequence
|
| 12 |
from pathlib import Path
|
| 13 |
from typing import Any, cast, overload
|
| 14 |
from uuid import uuid4
|
| 15 |
-
|
| 16 |
-
import numpy as np
|
| 17 |
-
import torch
|
| 18 |
from torch import Tensor
|
| 19 |
|
| 20 |
from .types import (
|
|
@@ -23,6 +22,7 @@ from .types import (
|
|
| 23 |
LazyTensorReference,
|
| 24 |
)
|
| 25 |
|
|
|
|
| 26 |
_DTYPE_NAMES: dict[torch.dtype, str] = {
|
| 27 |
torch.float16: "float16",
|
| 28 |
torch.bfloat16: "bfloat16",
|
|
@@ -80,22 +80,24 @@ def _persistent_metadata(
|
|
| 80 |
def _tensor_bytes(X: Tensor) -> bytes:
|
| 81 |
"""Return the exact contiguous byte representation of X."""
|
| 82 |
|
| 83 |
-
|
|
|
|
| 84 |
return X.view(torch.uint8).numpy().tobytes()
|
| 85 |
|
| 86 |
|
| 87 |
def _bounded_tensor_chunks(X: Tensor, max_bytes: int) -> Iterator[Tensor]:
|
| 88 |
"""Yield row-major CPU chunks without materializing one full byte string."""
|
| 89 |
|
| 90 |
-
|
|
|
|
| 91 |
if flattened.numel() == 0:
|
| 92 |
return
|
| 93 |
chunk_elements = max(1, max_bytes // flattened.element_size())
|
| 94 |
for start in range(0, flattened.numel(), chunk_elements):
|
| 95 |
-
chunk = flattened[start : start + chunk_elements]
|
| 96 |
if chunk.stride(0) != 1:
|
| 97 |
-
chunk = chunk.clone(memory_format=torch.contiguous_format)
|
| 98 |
-
yield chunk
|
| 99 |
|
| 100 |
|
| 101 |
def _tensor_hash_chunks(X: Tensor) -> Iterator[bytes]:
|
|
@@ -136,9 +138,9 @@ def _decode_tensor(dtype_name: str, shape_json: str, data: bytes) -> Tensor:
|
|
| 136 |
raise ValueError(f"Unsupported stored dtype {dtype_name!r}.") from error
|
| 137 |
shape = tuple(json.loads(shape_json))
|
| 138 |
# uint8 is used only as a byte-level carrier, preserving BF16 bits exactly.
|
| 139 |
-
byte_array = np.frombuffer(data, dtype=np.uint8).copy()
|
| 140 |
-
X = torch.from_numpy(byte_array).view(dtype)
|
| 141 |
-
return X.reshape(shape).clone()
|
| 142 |
|
| 143 |
|
| 144 |
def _index_path(path: str | Path) -> Path:
|
|
@@ -651,7 +653,7 @@ class SafetensorsStreamWriter:
|
|
| 651 |
|
| 652 |
for record in records:
|
| 653 |
position = self._record_count + len(self._pending)
|
| 654 |
-
tensor = record.load_tensor().detach().cpu().contiguous()
|
| 655 |
if tensor.dtype not in _DTYPE_NAMES:
|
| 656 |
raise TypeError(f"Unsupported tensor dtype {tensor.dtype}.")
|
| 657 |
nbytes = tensor.numel() * tensor.element_size()
|
|
@@ -949,7 +951,7 @@ def save_sqlite_result(result: EmbeddingResult, path: str | Path) -> EmbeddingRe
|
|
| 949 |
(run_id, metadata_json),
|
| 950 |
)
|
| 951 |
for position, record in enumerate(result):
|
| 952 |
-
X = record.load_tensor().detach().cpu().contiguous()
|
| 953 |
dtype_name, shape_json, data = _encode_tensor(X)
|
| 954 |
digest = tensor_sha256(X)
|
| 955 |
connection.execute(
|
|
@@ -1053,7 +1055,7 @@ def append_sqlite_records(
|
|
| 1053 |
)
|
| 1054 |
for offset, record in enumerate(records):
|
| 1055 |
position = start_position + offset
|
| 1056 |
-
X = record.load_tensor().detach().cpu().contiguous()
|
| 1057 |
dtype_name, shape_json, data = _encode_tensor(X)
|
| 1058 |
digest = tensor_sha256(X)
|
| 1059 |
connection.execute(
|
|
@@ -1414,6 +1416,7 @@ def load_legacy_pth(path: str | Path, *, allow_unsafe_pickle: bool = False) -> E
|
|
| 1414 |
raise ValueError("A legacy .pth embedding file must contain a mapping.")
|
| 1415 |
records: list[EmbeddingRecord] = []
|
| 1416 |
for position, (sequence, X) in enumerate(payload.items()):
|
|
|
|
| 1417 |
if not isinstance(sequence, str) or not isinstance(X, Tensor):
|
| 1418 |
raise ValueError("Legacy embedding mappings must use str keys and Tensor values.")
|
| 1419 |
records.append(EmbeddingRecord(str(position), sequence, X.detach().cpu()))
|
|
@@ -1450,8 +1453,10 @@ def _decode_legacy_sqlite_blob(
|
|
| 1450 |
expected = int(np.prod(shape, dtype=np.int64)) * numpy_dtype.itemsize
|
| 1451 |
if len(data) - offset != expected:
|
| 1452 |
raise ValueError("Legacy compact embedding payload length does not match shape.")
|
| 1453 |
-
array =
|
| 1454 |
-
|
|
|
|
|
|
|
| 1455 |
|
| 1456 |
try:
|
| 1457 |
loaded = torch.load(io.BytesIO(data), map_location="cpu", weights_only=True)
|
|
@@ -1470,11 +1475,13 @@ def _decode_legacy_sqlite_blob(
|
|
| 1470 |
raise ValueError(
|
| 1471 |
"Legacy raw FP32 payload length does not match fallback_shape."
|
| 1472 |
) from safe_error
|
| 1473 |
-
array = np.frombuffer(data, dtype=np.float32).copy().reshape(fallback_shape
|
| 1474 |
-
|
|
|
|
|
|
|
| 1475 |
if not isinstance(loaded, Tensor):
|
| 1476 |
raise ValueError("Legacy serialized embedding payload must contain one tensor.")
|
| 1477 |
-
return loaded.detach().cpu()
|
| 1478 |
|
| 1479 |
|
| 1480 |
def convert_legacy_sqlite(
|
|
|
|
| 7 |
import json
|
| 8 |
import sqlite3
|
| 9 |
import struct
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
from bisect import bisect_right
|
| 13 |
from collections.abc import Iterable, Iterator, Sequence
|
| 14 |
from pathlib import Path
|
| 15 |
from typing import Any, cast, overload
|
| 16 |
from uuid import uuid4
|
|
|
|
|
|
|
|
|
|
| 17 |
from torch import Tensor
|
| 18 |
|
| 19 |
from .types import (
|
|
|
|
| 22 |
LazyTensorReference,
|
| 23 |
)
|
| 24 |
|
| 25 |
+
|
| 26 |
_DTYPE_NAMES: dict[torch.dtype, str] = {
|
| 27 |
torch.float16: "float16",
|
| 28 |
torch.bfloat16: "bfloat16",
|
|
|
|
| 80 |
def _tensor_bytes(X: Tensor) -> bytes:
|
| 81 |
"""Return the exact contiguous byte representation of X."""
|
| 82 |
|
| 83 |
+
# X: (...)
|
| 84 |
+
X = X.detach().cpu().contiguous() # (...)
|
| 85 |
return X.view(torch.uint8).numpy().tobytes()
|
| 86 |
|
| 87 |
|
| 88 |
def _bounded_tensor_chunks(X: Tensor, max_bytes: int) -> Iterator[Tensor]:
|
| 89 |
"""Yield row-major CPU chunks without materializing one full byte string."""
|
| 90 |
|
| 91 |
+
# X: (...)
|
| 92 |
+
flattened = X.detach().to(device="cpu").reshape(-1) # (n,)
|
| 93 |
if flattened.numel() == 0:
|
| 94 |
return
|
| 95 |
chunk_elements = max(1, max_bytes // flattened.element_size())
|
| 96 |
for start in range(0, flattened.numel(), chunk_elements):
|
| 97 |
+
chunk = flattened[start : start + chunk_elements] # (n_chunk,)
|
| 98 |
if chunk.stride(0) != 1:
|
| 99 |
+
chunk = chunk.clone(memory_format=torch.contiguous_format) # (n_chunk,)
|
| 100 |
+
yield chunk # (n_chunk,)
|
| 101 |
|
| 102 |
|
| 103 |
def _tensor_hash_chunks(X: Tensor) -> Iterator[bytes]:
|
|
|
|
| 138 |
raise ValueError(f"Unsupported stored dtype {dtype_name!r}.") from error
|
| 139 |
shape = tuple(json.loads(shape_json))
|
| 140 |
# uint8 is used only as a byte-level carrier, preserving BF16 bits exactly.
|
| 141 |
+
byte_array = np.frombuffer(data, dtype=np.uint8).copy() # (n_bytes,)
|
| 142 |
+
X = torch.from_numpy(byte_array).view(dtype) # (n_elements,)
|
| 143 |
+
return X.reshape(shape).clone() # shape
|
| 144 |
|
| 145 |
|
| 146 |
def _index_path(path: str | Path) -> Path:
|
|
|
|
| 653 |
|
| 654 |
for record in records:
|
| 655 |
position = self._record_count + len(self._pending)
|
| 656 |
+
tensor = record.load_tensor().detach().cpu().contiguous() # (...)
|
| 657 |
if tensor.dtype not in _DTYPE_NAMES:
|
| 658 |
raise TypeError(f"Unsupported tensor dtype {tensor.dtype}.")
|
| 659 |
nbytes = tensor.numel() * tensor.element_size()
|
|
|
|
| 951 |
(run_id, metadata_json),
|
| 952 |
)
|
| 953 |
for position, record in enumerate(result):
|
| 954 |
+
X = record.load_tensor().detach().cpu().contiguous() # (...)
|
| 955 |
dtype_name, shape_json, data = _encode_tensor(X)
|
| 956 |
digest = tensor_sha256(X)
|
| 957 |
connection.execute(
|
|
|
|
| 1055 |
)
|
| 1056 |
for offset, record in enumerate(records):
|
| 1057 |
position = start_position + offset
|
| 1058 |
+
X = record.load_tensor().detach().cpu().contiguous() # (...)
|
| 1059 |
dtype_name, shape_json, data = _encode_tensor(X)
|
| 1060 |
digest = tensor_sha256(X)
|
| 1061 |
connection.execute(
|
|
|
|
| 1416 |
raise ValueError("A legacy .pth embedding file must contain a mapping.")
|
| 1417 |
records: list[EmbeddingRecord] = []
|
| 1418 |
for position, (sequence, X) in enumerate(payload.items()):
|
| 1419 |
+
# X: (...)
|
| 1420 |
if not isinstance(sequence, str) or not isinstance(X, Tensor):
|
| 1421 |
raise ValueError("Legacy embedding mappings must use str keys and Tensor values.")
|
| 1422 |
records.append(EmbeddingRecord(str(position), sequence, X.detach().cpu()))
|
|
|
|
| 1453 |
expected = int(np.prod(shape, dtype=np.int64)) * numpy_dtype.itemsize
|
| 1454 |
if len(data) - offset != expected:
|
| 1455 |
raise ValueError("Legacy compact embedding payload length does not match shape.")
|
| 1456 |
+
array = ( # shape
|
| 1457 |
+
np.frombuffer(data, dtype=numpy_dtype, offset=offset).copy().reshape(shape)
|
| 1458 |
+
)
|
| 1459 |
+
return torch.from_numpy(array).to(dtype=target_dtype) # shape
|
| 1460 |
|
| 1461 |
try:
|
| 1462 |
loaded = torch.load(io.BytesIO(data), map_location="cpu", weights_only=True)
|
|
|
|
| 1475 |
raise ValueError(
|
| 1476 |
"Legacy raw FP32 payload length does not match fallback_shape."
|
| 1477 |
) from safe_error
|
| 1478 |
+
array = np.frombuffer(data, dtype=np.float32).copy().reshape( # fallback_shape
|
| 1479 |
+
fallback_shape
|
| 1480 |
+
)
|
| 1481 |
+
return torch.from_numpy(array) # fallback_shape
|
| 1482 |
if not isinstance(loaded, Tensor):
|
| 1483 |
raise ValueError("Legacy serialized embedding payload must contain one tensor.")
|
| 1484 |
+
return loaded.detach().cpu() # (...)
|
| 1485 |
|
| 1486 |
|
| 1487 |
def convert_legacy_sqlite(
|
fastplms/embeddings/types.py
CHANGED
|
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|
| 5 |
from collections.abc import Callable, Iterator, Mapping, Sequence
|
| 6 |
from dataclasses import dataclass, field
|
| 7 |
from typing import Any, Literal, overload
|
| 8 |
-
|
| 9 |
from torch import Tensor
|
| 10 |
|
| 11 |
|
|
@@ -39,7 +38,7 @@ class LazyTensorReference:
|
|
| 39 |
|
| 40 |
if not isinstance(verify, bool):
|
| 41 |
raise TypeError("verify must be a boolean.")
|
| 42 |
-
X = self._loader()
|
| 43 |
if not isinstance(X, Tensor):
|
| 44 |
raise TypeError(f"Stored tensor loader for {self.key!r} must return a Tensor.")
|
| 45 |
if tuple(X.shape) != self.shape:
|
|
@@ -57,7 +56,7 @@ class LazyTensorReference:
|
|
| 57 |
digest = tensor_sha256(X)
|
| 58 |
if digest != self.sha256:
|
| 59 |
raise ValueError(f"Stored tensor {self.key!r} failed SHA-256 verification.")
|
| 60 |
-
return X
|
| 61 |
|
| 62 |
|
| 63 |
TensorValue = Tensor | LazyTensorReference
|
|
@@ -85,8 +84,8 @@ class EmbeddingRecord:
|
|
| 85 |
if not isinstance(verify, bool):
|
| 86 |
raise TypeError("verify must be a boolean.")
|
| 87 |
if isinstance(self.tensor, LazyTensorReference):
|
| 88 |
-
return self.tensor.load(verify=verify)
|
| 89 |
-
return self.tensor
|
| 90 |
|
| 91 |
|
| 92 |
class EmbeddingResult(Sequence[EmbeddingRecord]):
|
|
|
|
| 5 |
from collections.abc import Callable, Iterator, Mapping, Sequence
|
| 6 |
from dataclasses import dataclass, field
|
| 7 |
from typing import Any, Literal, overload
|
|
|
|
| 8 |
from torch import Tensor
|
| 9 |
|
| 10 |
|
|
|
|
| 38 |
|
| 39 |
if not isinstance(verify, bool):
|
| 40 |
raise TypeError("verify must be a boolean.")
|
| 41 |
+
X = self._loader() # self.shape
|
| 42 |
if not isinstance(X, Tensor):
|
| 43 |
raise TypeError(f"Stored tensor loader for {self.key!r} must return a Tensor.")
|
| 44 |
if tuple(X.shape) != self.shape:
|
|
|
|
| 56 |
digest = tensor_sha256(X)
|
| 57 |
if digest != self.sha256:
|
| 58 |
raise ValueError(f"Stored tensor {self.key!r} failed SHA-256 verification.")
|
| 59 |
+
return X # self.shape
|
| 60 |
|
| 61 |
|
| 62 |
TensorValue = Tensor | LazyTensorReference
|
|
|
|
| 84 |
if not isinstance(verify, bool):
|
| 85 |
raise TypeError("verify must be a boolean.")
|
| 86 |
if isinstance(self.tensor, LazyTensorReference):
|
| 87 |
+
return self.tensor.load(verify=verify) # (...)
|
| 88 |
+
return self.tensor # (...)
|
| 89 |
|
| 90 |
|
| 91 |
class EmbeddingResult(Sequence[EmbeddingRecord]):
|
fastplms/models/__init__.py
CHANGED
|
@@ -7,4 +7,5 @@ tokenizers, compile kernels, or initialize an accelerator runtime.
|
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
|
|
|
| 10 |
__all__: tuple[str, ...] = ()
|
|
|
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
+
|
| 11 |
__all__: tuple[str, ...] = ()
|
fastplms/models/esm_plusplus/modeling_esm_plusplus.py
CHANGED
|
@@ -3,13 +3,12 @@
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import math
|
| 6 |
-
from dataclasses import dataclass
|
| 7 |
-
from functools import partial
|
| 8 |
-
from typing import ClassVar
|
| 9 |
-
|
| 10 |
import torch
|
| 11 |
import torch.nn as nn
|
| 12 |
import torch.nn.functional as F
|
|
|
|
|
|
|
|
|
|
| 13 |
from einops import rearrange
|
| 14 |
from tokenizers import Tokenizer
|
| 15 |
from tokenizers.models import BPE
|
|
@@ -22,6 +21,7 @@ from transformers.modeling_outputs import (
|
|
| 22 |
TokenClassifierOutput,
|
| 23 |
)
|
| 24 |
|
|
|
|
| 25 |
try:
|
| 26 |
from fastplms.attention import (
|
| 27 |
AttentionBackend,
|
|
@@ -176,7 +176,7 @@ class RotaryEmbedding(torch.nn.Module):
|
|
| 176 |
scaling_factor: float = 1.0,
|
| 177 |
pos_idx_in_fp32: bool = True,
|
| 178 |
device: torch.device | None = None,
|
| 179 |
-
):
|
| 180 |
super().__init__()
|
| 181 |
self.dim, self.base = dim, float(base)
|
| 182 |
self.interleaved, self.scale_base = interleaved, scale_base
|
|
@@ -192,7 +192,7 @@ class RotaryEmbedding(torch.nn.Module):
|
|
| 192 |
self._cos_k_cached: torch.Tensor | None = None
|
| 193 |
self._sin_k_cached: torch.Tensor | None = None
|
| 194 |
|
| 195 |
-
def reset_parameters(self, device: torch.device | str | None = None):
|
| 196 |
"""Rebuild the non-persistent frequency buffers on ``device``."""
|
| 197 |
if device is not None:
|
| 198 |
buffer_device = torch.device(device)
|
|
@@ -250,14 +250,14 @@ class RotaryEmbedding(torch.nn.Module):
|
|
| 250 |
device: torch.device | None,
|
| 251 |
) -> torch.Tensor:
|
| 252 |
position_dtype = torch.float32 if self.pos_idx_in_fp32 else self.inv_freq.dtype
|
| 253 |
-
positions = torch.arange(token_count, device=device, dtype=position_dtype)
|
| 254 |
positions.div_(self.scaling_factor)
|
| 255 |
frequencies = (
|
| 256 |
self.inv_freq.to(torch.float32)
|
| 257 |
if self.pos_idx_in_fp32 and self.inv_freq.dtype != torch.float32
|
| 258 |
else self.inv_freq
|
| 259 |
)
|
| 260 |
-
return torch.outer(positions, frequencies)
|
| 261 |
|
| 262 |
def _update_cos_sin_cache(
|
| 263 |
self, seqlen: int, device: torch.device | None = None, dtype: torch.dtype | None = None
|
|
@@ -267,9 +267,9 @@ class RotaryEmbedding(torch.nn.Module):
|
|
| 267 |
return
|
| 268 |
|
| 269 |
self._seq_len_cached = seqlen
|
| 270 |
-
angles = self._rotary_angles(seqlen, device)
|
| 271 |
-
cos_angles = torch.cos(angles)
|
| 272 |
-
sin_angles = torch.sin(angles)
|
| 273 |
if self.scale is None:
|
| 274 |
self._cos_cached = cos_angles.to(dtype)
|
| 275 |
self._sin_cached = sin_angles.to(dtype)
|
|
@@ -314,7 +314,6 @@ class RotaryEmbedding(torch.nn.Module):
|
|
| 314 |
)
|
| 315 |
|
| 316 |
|
| 317 |
-
### Feedforward Network Components
|
| 318 |
def swiglu_correction_fn(expansion_ratio: float, d_model: int) -> int:
|
| 319 |
"""Compute corrected dimension for SwiGLU."""
|
| 320 |
return int(((expansion_ratio * d_model) + 255) // 256 * 256)
|
|
@@ -323,7 +322,7 @@ def swiglu_correction_fn(expansion_ratio: float, d_model: int) -> int:
|
|
| 323 |
class SwiGLU(nn.Module):
|
| 324 |
"""SwiGLU activation function."""
|
| 325 |
|
| 326 |
-
def __init__(self):
|
| 327 |
super().__init__()
|
| 328 |
|
| 329 |
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
@@ -341,7 +340,6 @@ def swiglu_ln_ffn(d_model: int, expansion_ratio: float) -> nn.Sequential:
|
|
| 341 |
)
|
| 342 |
|
| 343 |
|
| 344 |
-
### Attention
|
| 345 |
class MultiHeadAttention(nn.Module):
|
| 346 |
"""Multi-head attention with rotary embeddings and configurable backend.
|
| 347 |
|
|
@@ -356,7 +354,7 @@ class MultiHeadAttention(nn.Module):
|
|
| 356 |
d_model: int,
|
| 357 |
n_heads: int,
|
| 358 |
attn_backend: str = "sdpa",
|
| 359 |
-
):
|
| 360 |
super().__init__()
|
| 361 |
self.d_model = d_model
|
| 362 |
self.n_heads = n_heads
|
|
@@ -373,8 +371,9 @@ class MultiHeadAttention(nn.Module):
|
|
| 373 |
self.rotary = RotaryEmbedding(d_model // n_heads)
|
| 374 |
|
| 375 |
def _apply_rotary(self, q: torch.Tensor, k: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 376 |
-
q
|
| 377 |
-
|
|
|
|
| 378 |
q, k = self.rotary(q, k)
|
| 379 |
q = q.flatten(-2, -1)
|
| 380 |
k = k.flatten(-2, -1)
|
|
@@ -389,7 +388,8 @@ class MultiHeadAttention(nn.Module):
|
|
| 389 |
output_attentions: bool = False,
|
| 390 |
output_s_max: bool = False,
|
| 391 |
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
|
| 392 |
-
|
|
|
|
| 393 |
query_sequence, key_sequence, value_sequence = torch.chunk(qkv, 3, dim=-1)
|
| 394 |
query_sequence, key_sequence = (
|
| 395 |
self.q_ln(query_sequence).to(query_sequence.dtype),
|
|
@@ -398,7 +398,7 @@ class MultiHeadAttention(nn.Module):
|
|
| 398 |
query_sequence, key_sequence = self._apply_rotary(query_sequence, key_sequence)
|
| 399 |
query_heads, key_heads, value_heads = map(
|
| 400 |
self.reshaper, (query_sequence, key_sequence, value_sequence)
|
| 401 |
-
)
|
| 402 |
|
| 403 |
attn_output, attn_weights, s_max = self._attn(
|
| 404 |
query_heads,
|
|
@@ -461,8 +461,8 @@ class MultiHeadAttention(nn.Module):
|
|
| 461 |
def _compute_s_max(
|
| 462 |
self, query_heads: torch.Tensor, key_heads: torch.Tensor
|
| 463 |
) -> list[torch.Tensor]:
|
| 464 |
-
q_norm = torch.linalg.vector_norm(query_heads, dim=-1)
|
| 465 |
-
k_norm = torch.linalg.vector_norm(key_heads, dim=-1)
|
| 466 |
s_max_bound = (q_norm.max(dim=-1).values * k_norm.max(dim=-1).values).max(
|
| 467 |
dim=0
|
| 468 |
).values * self.scale
|
|
@@ -476,12 +476,15 @@ class MultiHeadAttention(nn.Module):
|
|
| 476 |
attention_mask_4d: torch.Tensor | None = None,
|
| 477 |
output_s_max: bool = False,
|
| 478 |
) -> tuple[torch.Tensor, torch.Tensor, list[torch.Tensor] | None]:
|
| 479 |
-
|
|
|
|
|
|
|
|
|
|
| 480 |
if attention_mask_4d is not None:
|
| 481 |
attn_weights = attn_weights.masked_fill(attention_mask_4d.logical_not(), float("-inf"))
|
| 482 |
attn_weights = F.softmax(attn_weights, dim=-1)
|
| 483 |
-
context_heads = torch.matmul(attn_weights, value_heads)
|
| 484 |
-
attn_output = rearrange(context_heads, "b h s d -> b s (h d)")
|
| 485 |
s_max = self._compute_s_max(query_heads, key_heads) if output_s_max else None
|
| 486 |
return attn_output, attn_weights, s_max
|
| 487 |
|
|
@@ -548,7 +551,6 @@ class MultiHeadAttention(nn.Module):
|
|
| 548 |
return rearrange(context_heads, "b h s d -> b s (h d)"), None
|
| 549 |
|
| 550 |
|
| 551 |
-
### Regression Head
|
| 552 |
def RegressionHead(d_model: int, output_dim: int, hidden_dim: int | None = None) -> nn.Module:
|
| 553 |
"""Create a regression head with optional hidden dimension.
|
| 554 |
|
|
@@ -566,7 +568,6 @@ def RegressionHead(d_model: int, output_dim: int, hidden_dim: int | None = None)
|
|
| 566 |
)
|
| 567 |
|
| 568 |
|
| 569 |
-
### Transformer Block
|
| 570 |
class UnifiedTransformerBlock(nn.Module):
|
| 571 |
"""Transformer block with attention and feedforward layers."""
|
| 572 |
|
|
@@ -578,7 +579,7 @@ class UnifiedTransformerBlock(nn.Module):
|
|
| 578 |
expansion_ratio: float = 8 / 3,
|
| 579 |
dropout: float = 0.0,
|
| 580 |
attn_backend: str = "sdpa",
|
| 581 |
-
):
|
| 582 |
super().__init__()
|
| 583 |
self.attn = MultiHeadAttention(d_model=d_model, n_heads=n_heads, attn_backend=attn_backend)
|
| 584 |
self.ffn = swiglu_ln_ffn(d_model, expansion_ratio)
|
|
@@ -607,7 +608,6 @@ class UnifiedTransformerBlock(nn.Module):
|
|
| 607 |
return x, attn_weights, s_max
|
| 608 |
|
| 609 |
|
| 610 |
-
### Model Outputs
|
| 611 |
@dataclass
|
| 612 |
class TransformerOutput(ModelOutput):
|
| 613 |
"""Output type for transformer encoder."""
|
|
@@ -640,7 +640,6 @@ class ESMplusplusTokenClassifierOutput(TokenClassifierOutput):
|
|
| 640 |
s_max: tuple[list[torch.Tensor], ...] | None = None
|
| 641 |
|
| 642 |
|
| 643 |
-
### Transformer Stack
|
| 644 |
class TransformerStack(nn.Module):
|
| 645 |
"""Stack of transformer blocks."""
|
| 646 |
|
|
@@ -651,7 +650,7 @@ class TransformerStack(nn.Module):
|
|
| 651 |
n_layers: int,
|
| 652 |
dropout: float = 0.0,
|
| 653 |
attn_backend: str = "sdpa",
|
| 654 |
-
):
|
| 655 |
super().__init__()
|
| 656 |
self.attention_backend = resolve_attention_backend(attn_backend)
|
| 657 |
self.blocks = nn.ModuleList(
|
|
@@ -690,6 +689,7 @@ class TransformerStack(nn.Module):
|
|
| 690 |
output_s_max: bool | None = False,
|
| 691 |
esmfold2_hidden_states: bool = False,
|
| 692 |
) -> TransformerOutput:
|
|
|
|
| 693 |
hidden_states = () if output_hidden_states else None
|
| 694 |
attentions = () if output_attentions else None
|
| 695 |
full_s_max = () if output_s_max else None
|
|
@@ -971,7 +971,7 @@ class ESMplusplusModel(PreTrainedESMplusplusModel, EmbeddingMixin):
|
|
| 971 |
|
| 972 |
config_class = ESMplusplusConfig
|
| 973 |
|
| 974 |
-
def __init__(self, config: ESMplusplusConfig, **kwargs):
|
| 975 |
PreTrainedESMplusplusModel.__init__(self, config, **kwargs)
|
| 976 |
self.config = config
|
| 977 |
self.vocab_size = config.vocab_size
|
|
@@ -1089,7 +1089,7 @@ class ESMplusplusForMaskedLM(
|
|
| 1089 |
|
| 1090 |
config_class = ESMplusplusConfig
|
| 1091 |
|
| 1092 |
-
def __init__(self, config: ESMplusplusConfig, **kwargs):
|
| 1093 |
PreTrainedESMplusplusModel.__init__(self, config, **kwargs)
|
| 1094 |
self.config = config
|
| 1095 |
self.vocab_size = config.vocab_size
|
|
@@ -1215,7 +1215,7 @@ class ESMplusplusForSequenceClassification(ESMplusplusForMaskedLM, EmbeddingMixi
|
|
| 1215 |
Extends the base ESM++ model with a classification head.
|
| 1216 |
"""
|
| 1217 |
|
| 1218 |
-
def __init__(self, config: ESMplusplusConfig, **kwargs):
|
| 1219 |
pooling_types = kwargs.pop("pooling_types", None)
|
| 1220 |
if pooling_types is None:
|
| 1221 |
pooling_types = config.classifier_pooling_types or ["mean", "var"]
|
|
@@ -1355,7 +1355,7 @@ class ESMplusplusForTokenClassification(ESMplusplusForMaskedLM, EmbeddingMixin):
|
|
| 1355 |
Extends the base ESM++ model with a token classification head.
|
| 1356 |
"""
|
| 1357 |
|
| 1358 |
-
def __init__(self, config: ESMplusplusConfig, **kwargs):
|
| 1359 |
ESMplusplusForMaskedLM.__init__(self, config, **kwargs)
|
| 1360 |
self.config = config
|
| 1361 |
self.num_labels = config.num_labels
|
|
@@ -1526,7 +1526,7 @@ class EsmSequenceTokenizer(PreTrainedTokenizerFast):
|
|
| 1526 |
**kwargs,
|
| 1527 |
)
|
| 1528 |
|
| 1529 |
-
#
|
| 1530 |
@property
|
| 1531 |
def bos_token(self):
|
| 1532 |
return self.cls_token
|
|
|
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import math
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
import torch
|
| 7 |
import torch.nn as nn
|
| 8 |
import torch.nn.functional as F
|
| 9 |
+
from dataclasses import dataclass
|
| 10 |
+
from functools import partial
|
| 11 |
+
from typing import ClassVar
|
| 12 |
from einops import rearrange
|
| 13 |
from tokenizers import Tokenizer
|
| 14 |
from tokenizers.models import BPE
|
|
|
|
| 21 |
TokenClassifierOutput,
|
| 22 |
)
|
| 23 |
|
| 24 |
+
|
| 25 |
try:
|
| 26 |
from fastplms.attention import (
|
| 27 |
AttentionBackend,
|
|
|
|
| 176 |
scaling_factor: float = 1.0,
|
| 177 |
pos_idx_in_fp32: bool = True,
|
| 178 |
device: torch.device | None = None,
|
| 179 |
+
) -> None:
|
| 180 |
super().__init__()
|
| 181 |
self.dim, self.base = dim, float(base)
|
| 182 |
self.interleaved, self.scale_base = interleaved, scale_base
|
|
|
|
| 192 |
self._cos_k_cached: torch.Tensor | None = None
|
| 193 |
self._sin_k_cached: torch.Tensor | None = None
|
| 194 |
|
| 195 |
+
def reset_parameters(self, device: torch.device | str | None = None) -> None:
|
| 196 |
"""Rebuild the non-persistent frequency buffers on ``device``."""
|
| 197 |
if device is not None:
|
| 198 |
buffer_device = torch.device(device)
|
|
|
|
| 250 |
device: torch.device | None,
|
| 251 |
) -> torch.Tensor:
|
| 252 |
position_dtype = torch.float32 if self.pos_idx_in_fp32 else self.inv_freq.dtype
|
| 253 |
+
positions = torch.arange(token_count, device=device, dtype=position_dtype) # (l,)
|
| 254 |
positions.div_(self.scaling_factor)
|
| 255 |
frequencies = (
|
| 256 |
self.inv_freq.to(torch.float32)
|
| 257 |
if self.pos_idx_in_fp32 and self.inv_freq.dtype != torch.float32
|
| 258 |
else self.inv_freq
|
| 259 |
)
|
| 260 |
+
return torch.outer(positions, frequencies) # (l, d / 2)
|
| 261 |
|
| 262 |
def _update_cos_sin_cache(
|
| 263 |
self, seqlen: int, device: torch.device | None = None, dtype: torch.dtype | None = None
|
|
|
|
| 267 |
return
|
| 268 |
|
| 269 |
self._seq_len_cached = seqlen
|
| 270 |
+
angles = self._rotary_angles(seqlen, device) # (l, d / 2)
|
| 271 |
+
cos_angles = torch.cos(angles) # (l, d / 2)
|
| 272 |
+
sin_angles = torch.sin(angles) # (l, d / 2)
|
| 273 |
if self.scale is None:
|
| 274 |
self._cos_cached = cos_angles.to(dtype)
|
| 275 |
self._sin_cached = sin_angles.to(dtype)
|
|
|
|
| 314 |
)
|
| 315 |
|
| 316 |
|
|
|
|
| 317 |
def swiglu_correction_fn(expansion_ratio: float, d_model: int) -> int:
|
| 318 |
"""Compute corrected dimension for SwiGLU."""
|
| 319 |
return int(((expansion_ratio * d_model) + 255) // 256 * 256)
|
|
|
|
| 322 |
class SwiGLU(nn.Module):
|
| 323 |
"""SwiGLU activation function."""
|
| 324 |
|
| 325 |
+
def __init__(self) -> None:
|
| 326 |
super().__init__()
|
| 327 |
|
| 328 |
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
|
|
| 340 |
)
|
| 341 |
|
| 342 |
|
|
|
|
| 343 |
class MultiHeadAttention(nn.Module):
|
| 344 |
"""Multi-head attention with rotary embeddings and configurable backend.
|
| 345 |
|
|
|
|
| 354 |
d_model: int,
|
| 355 |
n_heads: int,
|
| 356 |
attn_backend: str = "sdpa",
|
| 357 |
+
) -> None:
|
| 358 |
super().__init__()
|
| 359 |
self.d_model = d_model
|
| 360 |
self.n_heads = n_heads
|
|
|
|
| 371 |
self.rotary = RotaryEmbedding(d_model // n_heads)
|
| 372 |
|
| 373 |
def _apply_rotary(self, q: torch.Tensor, k: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 374 |
+
# q, k: (b, l, d)
|
| 375 |
+
q = q.unflatten(-1, (self.n_heads, self.d_head)) # (b, l, h, d_h)
|
| 376 |
+
k = k.unflatten(-1, (self.n_heads, self.d_head)) # (b, l, h, d_h)
|
| 377 |
q, k = self.rotary(q, k)
|
| 378 |
q = q.flatten(-2, -1)
|
| 379 |
k = k.flatten(-2, -1)
|
|
|
|
| 388 |
output_attentions: bool = False,
|
| 389 |
output_s_max: bool = False,
|
| 390 |
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
|
| 391 |
+
# x: (b, l, d)
|
| 392 |
+
qkv = self.layernorm_qkv(x) # (b, l, 3 * d)
|
| 393 |
query_sequence, key_sequence, value_sequence = torch.chunk(qkv, 3, dim=-1)
|
| 394 |
query_sequence, key_sequence = (
|
| 395 |
self.q_ln(query_sequence).to(query_sequence.dtype),
|
|
|
|
| 398 |
query_sequence, key_sequence = self._apply_rotary(query_sequence, key_sequence)
|
| 399 |
query_heads, key_heads, value_heads = map(
|
| 400 |
self.reshaper, (query_sequence, key_sequence, value_sequence)
|
| 401 |
+
) # each (b, h, l, d_h)
|
| 402 |
|
| 403 |
attn_output, attn_weights, s_max = self._attn(
|
| 404 |
query_heads,
|
|
|
|
| 461 |
def _compute_s_max(
|
| 462 |
self, query_heads: torch.Tensor, key_heads: torch.Tensor
|
| 463 |
) -> list[torch.Tensor]:
|
| 464 |
+
q_norm = torch.linalg.vector_norm(query_heads, dim=-1) # (b, h, l)
|
| 465 |
+
k_norm = torch.linalg.vector_norm(key_heads, dim=-1) # (b, h, l)
|
| 466 |
s_max_bound = (q_norm.max(dim=-1).values * k_norm.max(dim=-1).values).max(
|
| 467 |
dim=0
|
| 468 |
).values * self.scale
|
|
|
|
| 476 |
attention_mask_4d: torch.Tensor | None = None,
|
| 477 |
output_s_max: bool = False,
|
| 478 |
) -> tuple[torch.Tensor, torch.Tensor, list[torch.Tensor] | None]:
|
| 479 |
+
# query_heads, key_heads, value_heads: (b, h, l, d_h)
|
| 480 |
+
attn_weights = (
|
| 481 |
+
torch.matmul(query_heads, key_heads.transpose(-2, -1)) * self.scale
|
| 482 |
+
) # (b, h, l, l)
|
| 483 |
if attention_mask_4d is not None:
|
| 484 |
attn_weights = attn_weights.masked_fill(attention_mask_4d.logical_not(), float("-inf"))
|
| 485 |
attn_weights = F.softmax(attn_weights, dim=-1)
|
| 486 |
+
context_heads = torch.matmul(attn_weights, value_heads) # (b, h, l, d_h)
|
| 487 |
+
attn_output = rearrange(context_heads, "b h s d -> b s (h d)") # (b, l, d)
|
| 488 |
s_max = self._compute_s_max(query_heads, key_heads) if output_s_max else None
|
| 489 |
return attn_output, attn_weights, s_max
|
| 490 |
|
|
|
|
| 551 |
return rearrange(context_heads, "b h s d -> b s (h d)"), None
|
| 552 |
|
| 553 |
|
|
|
|
| 554 |
def RegressionHead(d_model: int, output_dim: int, hidden_dim: int | None = None) -> nn.Module:
|
| 555 |
"""Create a regression head with optional hidden dimension.
|
| 556 |
|
|
|
|
| 568 |
)
|
| 569 |
|
| 570 |
|
|
|
|
| 571 |
class UnifiedTransformerBlock(nn.Module):
|
| 572 |
"""Transformer block with attention and feedforward layers."""
|
| 573 |
|
|
|
|
| 579 |
expansion_ratio: float = 8 / 3,
|
| 580 |
dropout: float = 0.0,
|
| 581 |
attn_backend: str = "sdpa",
|
| 582 |
+
) -> None:
|
| 583 |
super().__init__()
|
| 584 |
self.attn = MultiHeadAttention(d_model=d_model, n_heads=n_heads, attn_backend=attn_backend)
|
| 585 |
self.ffn = swiglu_ln_ffn(d_model, expansion_ratio)
|
|
|
|
| 608 |
return x, attn_weights, s_max
|
| 609 |
|
| 610 |
|
|
|
|
| 611 |
@dataclass
|
| 612 |
class TransformerOutput(ModelOutput):
|
| 613 |
"""Output type for transformer encoder."""
|
|
|
|
| 640 |
s_max: tuple[list[torch.Tensor], ...] | None = None
|
| 641 |
|
| 642 |
|
|
|
|
| 643 |
class TransformerStack(nn.Module):
|
| 644 |
"""Stack of transformer blocks."""
|
| 645 |
|
|
|
|
| 650 |
n_layers: int,
|
| 651 |
dropout: float = 0.0,
|
| 652 |
attn_backend: str = "sdpa",
|
| 653 |
+
) -> None:
|
| 654 |
super().__init__()
|
| 655 |
self.attention_backend = resolve_attention_backend(attn_backend)
|
| 656 |
self.blocks = nn.ModuleList(
|
|
|
|
| 689 |
output_s_max: bool | None = False,
|
| 690 |
esmfold2_hidden_states: bool = False,
|
| 691 |
) -> TransformerOutput:
|
| 692 |
+
# x: (b, l, d); attention_mask, sequence_id: (b, l)
|
| 693 |
hidden_states = () if output_hidden_states else None
|
| 694 |
attentions = () if output_attentions else None
|
| 695 |
full_s_max = () if output_s_max else None
|
|
|
|
| 971 |
|
| 972 |
config_class = ESMplusplusConfig
|
| 973 |
|
| 974 |
+
def __init__(self, config: ESMplusplusConfig, **kwargs) -> None:
|
| 975 |
PreTrainedESMplusplusModel.__init__(self, config, **kwargs)
|
| 976 |
self.config = config
|
| 977 |
self.vocab_size = config.vocab_size
|
|
|
|
| 1089 |
|
| 1090 |
config_class = ESMplusplusConfig
|
| 1091 |
|
| 1092 |
+
def __init__(self, config: ESMplusplusConfig, **kwargs) -> None:
|
| 1093 |
PreTrainedESMplusplusModel.__init__(self, config, **kwargs)
|
| 1094 |
self.config = config
|
| 1095 |
self.vocab_size = config.vocab_size
|
|
|
|
| 1215 |
Extends the base ESM++ model with a classification head.
|
| 1216 |
"""
|
| 1217 |
|
| 1218 |
+
def __init__(self, config: ESMplusplusConfig, **kwargs) -> None:
|
| 1219 |
pooling_types = kwargs.pop("pooling_types", None)
|
| 1220 |
if pooling_types is None:
|
| 1221 |
pooling_types = config.classifier_pooling_types or ["mean", "var"]
|
|
|
|
| 1355 |
Extends the base ESM++ model with a token classification head.
|
| 1356 |
"""
|
| 1357 |
|
| 1358 |
+
def __init__(self, config: ESMplusplusConfig, **kwargs) -> None:
|
| 1359 |
ESMplusplusForMaskedLM.__init__(self, config, **kwargs)
|
| 1360 |
self.config = config
|
| 1361 |
self.num_labels = config.num_labels
|
|
|
|
| 1526 |
**kwargs,
|
| 1527 |
)
|
| 1528 |
|
| 1529 |
+
# ESMC does not use BOS, so expose the sequence-start token through the HF BOS fields.
|
| 1530 |
@property
|
| 1531 |
def bos_token(self):
|
| 1532 |
return self.cls_token
|
fastplms/models/ttt.py
CHANGED
|
@@ -3,12 +3,13 @@ from __future__ import annotations
|
|
| 3 |
import contextlib
|
| 4 |
import math
|
| 5 |
import numbers
|
| 6 |
-
import typing as T
|
| 7 |
-
from dataclasses import asdict, dataclass, fields
|
| 8 |
-
|
| 9 |
import torch
|
| 10 |
import torch.nn as nn
|
| 11 |
import torch.nn.functional as F
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
_STANDARD_AMINO_ACIDS = "ACDEFGHIKLMNPQRSTVWY"
|
| 14 |
_TTT_SERIALIZATION_VERSION = 1
|
|
@@ -42,7 +43,7 @@ class TTTConfig:
|
|
| 42 |
self.verify()
|
| 43 |
|
| 44 |
@classmethod
|
| 45 |
-
def from_kwargs(cls, **kwargs:
|
| 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,10 +179,10 @@ def _install_runtime():
|
|
| 223 |
return package
|
| 224 |
|
| 225 |
_install_runtime()
|
| 226 |
-
|
| 227 |
-
ESMplusplusConfig =
|
| 228 |
ESMplusplusConfig.__module__ = __name__
|
| 229 |
-
ESMplusplusForMaskedLM =
|
| 230 |
ESMplusplusForMaskedLM.__module__ = __name__
|
| 231 |
-
ESMplusplusModel =
|
| 232 |
ESMplusplusModel.__module__ = __name__
|
|
|
|
| 1 |
+
"""Generated bridge to the embedded FastPLMs runtime sources."""
|
| 2 |
|
| 3 |
import base64
|
| 4 |
import hashlib
|
|
|
|
| 6 |
import importlib.util
|
| 7 |
import sys
|
| 8 |
import tempfile
|
|
|
|
| 9 |
from io import BytesIO
|
| 10 |
from pathlib import Path
|
| 11 |
from zipfile import ZIP_DEFLATED, ZipFile
|
| 12 |
|
| 13 |
from .fastplms_bundle import RUNTIME_DATA, RUNTIME_HASH
|
| 14 |
|
| 15 |
+
if RUNTIME_HASH != "d73f544a7623d12eff2ba407e2d7e86417b7d731e36a2931515930eae7d7e8e5":
|
| 16 |
raise RuntimeError("FastPLMs runtime identity differs from the bridge.")
|
| 17 |
|
| 18 |
_RUNTIME_TEMPORARIES = []
|
|
|
|
| 82 |
result[relative.as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
|
| 83 |
return result
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
def _extend_loaded_package_paths(package_root):
|
| 86 |
for name, module in list(sys.modules.items()):
|
| 87 |
if name != "fastplms" and not name.startswith("fastplms."):
|
|
|
|
| 95 |
if candidate.is_dir() and candidate_text not in paths:
|
| 96 |
paths.append(candidate_text)
|
| 97 |
|
| 98 |
+
def _merge_runtime(package, package_root):
|
| 99 |
incoming = _runtime_file_hashes(package_root)
|
| 100 |
+
known = getattr(package, "__fastplms_artifact_runtime_files__", None)
|
| 101 |
+
if not isinstance(known, dict):
|
| 102 |
+
raise RuntimeError(
|
| 103 |
+
"A non-artifact fastplms module is already loaded. Load the Hub artifact "
|
| 104 |
+
"in a separate Python process."
|
| 105 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
conflicts = sorted(
|
| 107 |
relative
|
| 108 |
for relative, digest in incoming.items()
|
|
|
|
| 114 |
+ ", ".join(repr(path) for path in conflicts[:5])
|
| 115 |
+ ". Load incompatible releases in separate Python processes."
|
| 116 |
)
|
| 117 |
+
known = dict(known)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
known.update(incoming)
|
| 119 |
+
package.__fastplms_artifact_runtime_files__ = known
|
| 120 |
+
roots = list(getattr(package, "__fastplms_artifact_runtime_roots__", ()))
|
| 121 |
if str(package_root) not in roots:
|
| 122 |
roots.append(str(package_root))
|
| 123 |
+
package.__fastplms_artifact_runtime_roots__ = tuple(roots)
|
| 124 |
temporaries = list(
|
| 125 |
+
getattr(package, "__fastplms_artifact_runtime_temporaries__", ())
|
| 126 |
)
|
| 127 |
for temporary in _RUNTIME_TEMPORARIES:
|
| 128 |
if temporary not in temporaries:
|
| 129 |
temporaries.append(temporary)
|
| 130 |
+
package.__fastplms_artifact_runtime_temporaries__ = tuple(temporaries)
|
| 131 |
+
hashes = set(getattr(package, "__fastplms_artifact_runtime_hashes__", ()))
|
| 132 |
hashes.add(RUNTIME_HASH)
|
| 133 |
+
package.__fastplms_artifact_runtime_hashes__ = frozenset(hashes)
|
| 134 |
_extend_loaded_package_paths(package_root)
|
| 135 |
+
return package
|
| 136 |
|
| 137 |
def _import_without_bytecode(module_name):
|
| 138 |
previous = sys.dont_write_bytecode
|
|
|
|
| 143 |
sys.dont_write_bytecode = previous
|
| 144 |
|
| 145 |
def _install_runtime():
|
| 146 |
+
package = sys.modules.get("fastplms")
|
| 147 |
+
hashes = getattr(package, "__fastplms_artifact_runtime_hashes__", ())
|
| 148 |
if RUNTIME_HASH in hashes:
|
| 149 |
+
return package
|
| 150 |
package_root = _ensure_runtime()
|
| 151 |
+
if package is not None:
|
| 152 |
+
return _merge_runtime(package, package_root)
|
| 153 |
spec = importlib.util.spec_from_file_location(
|
| 154 |
"fastplms",
|
| 155 |
package_root / "__init__.py",
|
|
|
|
| 179 |
return package
|
| 180 |
|
| 181 |
_install_runtime()
|
| 182 |
+
_module_181 = _import_without_bytecode("fastplms.models.esm_plusplus.modeling_esm_plusplus")
|
| 183 |
+
ESMplusplusConfig = _module_181.ESMplusplusConfig
|
| 184 |
ESMplusplusConfig.__module__ = __name__
|
| 185 |
+
ESMplusplusForMaskedLM = _module_181.ESMplusplusForMaskedLM
|
| 186 |
ESMplusplusForMaskedLM.__module__ = __name__
|
| 187 |
+
ESMplusplusModel = _module_181.ESMplusplusModel
|
| 188 |
ESMplusplusModel.__module__ = __name__
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Direct runtime dependencies for Synthyra/ESMplusplus_large.
|
| 2 |
+
# FastPLMs source is embedded in this model repository.
|
| 3 |
+
torch>=2.13,<2.14
|
| 4 |
+
transformers>=5.13,<5.14
|
| 5 |
+
huggingface-hub>=0.34,<2
|
| 6 |
+
tokenizers>=0.22,<0.23
|
| 7 |
+
safetensors>=0.5,<1
|
| 8 |
+
numpy>=1.26,<3
|
| 9 |
+
einops>=0.8,<1
|
| 10 |
+
tqdm>=4.67,<5
|
| 11 |
+
kernels>=0.15,<0.16
|
runtime-attestation.json
CHANGED
|
@@ -4,42 +4,43 @@
|
|
| 4 |
"LICENSES/biohub-esm/LICENSE.md": "sha256:b63df9ca1dd96b3b21eec226b51b236d0bd152ac20eafc43aad46bf832b48d8a",
|
| 5 |
"LICENSES/biohub-esm/THIRD_PARTY_NOTICE.md": "sha256:5bff8515ba4e0f53abdc43714c180b79c5b606160497d98de741a369cb9b6a23",
|
| 6 |
"LICENSES/biohub-transformers/LICENSE": "sha256:77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049",
|
| 7 |
-
"README.md": "sha256:
|
| 8 |
"THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
|
| 9 |
-
"config.json": "sha256:
|
| 10 |
-
"fastplms/__init__.py": "sha256:
|
| 11 |
-
"fastplms/attention/__init__.py": "sha256:
|
| 12 |
-
"fastplms/attention/_core.py": "sha256:
|
| 13 |
-
"fastplms/attention/_kernel_lock.py": "sha256:
|
| 14 |
-
"fastplms/attention/interfaces.py": "sha256:
|
| 15 |
-
"fastplms/embeddings/__init__.py": "sha256:
|
| 16 |
-
"fastplms/embeddings/pooling.py": "sha256:
|
| 17 |
-
"fastplms/embeddings/runner.py": "sha256:
|
| 18 |
-
"fastplms/embeddings/storage.py": "sha256:
|
| 19 |
-
"fastplms/embeddings/types.py": "sha256:
|
| 20 |
"fastplms/kernels.lock": "sha256:8e274e6f8f25fe272d95bb42849f672ebb897a67511e82be2b86c2a4b4962cdd",
|
| 21 |
"fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
|
| 22 |
-
"fastplms/models/__init__.py": "sha256:
|
| 23 |
"fastplms/models/esm_plusplus/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
| 24 |
-
"fastplms/models/esm_plusplus/modeling_esm_plusplus.py": "sha256:
|
| 25 |
-
"fastplms/models/ttt.py": "sha256:
|
| 26 |
-
"fastplms/registry.py": "sha256:
|
| 27 |
-
"fastplms/runtime.py": "sha256:
|
| 28 |
-
"fastplms_bundle.py": "sha256:
|
| 29 |
-
"modeling_fastplms.py": "sha256:
|
|
|
|
| 30 |
"special_tokens_map.json": "sha256:0b7245ec86c8c3aeaf61523ba70dfa79be137e6283f127bd651adc30b4f15c74",
|
| 31 |
"tokenizer.json": "sha256:8d3447b278176e65fb3ef0224472927bf5fee3be46ea2bd77fad0111423cee1f",
|
| 32 |
"tokenizer_config.json": "sha256:e8d8e40c9f92b334f0272e80bb65ed4043cb9836523cbae899e9859e8cbb8833"
|
| 33 |
},
|
| 34 |
"model_id": "esmc_large",
|
| 35 |
"redistributable": true,
|
| 36 |
-
"release_tool_revision": "
|
| 37 |
-
"release_tool_sha256": "
|
| 38 |
-
"runtime_bundle_sha256": "
|
| 39 |
-
"runtime_revision": "
|
| 40 |
"schema_version": 2,
|
| 41 |
"scope": "runtime-only",
|
| 42 |
-
"source_tree_sha256": "
|
| 43 |
"weights": {
|
| 44 |
"repo_id": "Synthyra/ESMplusplus_large",
|
| 45 |
"revision": "f813401638b3fddab09748aec1ad2bf537aa4208"
|
|
|
|
| 4 |
"LICENSES/biohub-esm/LICENSE.md": "sha256:b63df9ca1dd96b3b21eec226b51b236d0bd152ac20eafc43aad46bf832b48d8a",
|
| 5 |
"LICENSES/biohub-esm/THIRD_PARTY_NOTICE.md": "sha256:5bff8515ba4e0f53abdc43714c180b79c5b606160497d98de741a369cb9b6a23",
|
| 6 |
"LICENSES/biohub-transformers/LICENSE": "sha256:77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049",
|
| 7 |
+
"README.md": "sha256:2115ee81a16a6c30e29ada82eaffdee331bf20360a5613850bdbfcc36046b077",
|
| 8 |
"THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
|
| 9 |
+
"config.json": "sha256:b0e9e94cc3b7d42bae5b7d1296eadfb7ba88b3e7ff87f5ea2f682764c58a51d3",
|
| 10 |
+
"fastplms/__init__.py": "sha256:8503e02debf24abc6d33c1913ff3eb9f245e63f7e62017e220d072c7393fdf2c",
|
| 11 |
+
"fastplms/attention/__init__.py": "sha256:ab3c0b6156968f418f3c97ea664959cb978f4137717b4362213bcfe25ab5e3d6",
|
| 12 |
+
"fastplms/attention/_core.py": "sha256:09d0f71a5ea2ad2138c9441f6a2f72bbf8e72729be41d8db737b163f36ce47da",
|
| 13 |
+
"fastplms/attention/_kernel_lock.py": "sha256:56455eb57cee9af08438eda52b775c9cff3cf994cadb9b116ce2ee10fd5f1801",
|
| 14 |
+
"fastplms/attention/interfaces.py": "sha256:6562a342961022fc2848d7004bd01fddaf8be8e08e782d0856e5c30a74232623",
|
| 15 |
+
"fastplms/embeddings/__init__.py": "sha256:cf0648de905a00ad16c0aece328eeb05f25739a4cf89b47b915e0f2b19c3ce26",
|
| 16 |
+
"fastplms/embeddings/pooling.py": "sha256:9bed8c466fce053333de9e25b04d1ab304f2954f403ffa0f5a3c749e8277b3ce",
|
| 17 |
+
"fastplms/embeddings/runner.py": "sha256:a7839763ad641a63d0e2671dcf34aa7b2a8114198950b64254188521b27453a8",
|
| 18 |
+
"fastplms/embeddings/storage.py": "sha256:98cf725d26766f32300f105345129ca6cf02ebd10628e746a7e71e107bc911a6",
|
| 19 |
+
"fastplms/embeddings/types.py": "sha256:c58f8c6a37a11a937c3f77b4e1617d5f213923d98ecaffddcdd283bd9dc42fe7",
|
| 20 |
"fastplms/kernels.lock": "sha256:8e274e6f8f25fe272d95bb42849f672ebb897a67511e82be2b86c2a4b4962cdd",
|
| 21 |
"fastplms/models.toml": "sha256:1b0d92911222f31b435bfee80251c9abe20b85392d65f8af8d285d1ccfbbc3c5",
|
| 22 |
+
"fastplms/models/__init__.py": "sha256:d3dd084f5e8fafcf2d0fe4a26e4ab9284d3f9414b1f9a5bd86e308a406447c43",
|
| 23 |
"fastplms/models/esm_plusplus/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
| 24 |
+
"fastplms/models/esm_plusplus/modeling_esm_plusplus.py": "sha256:ea812cc86c8cc5b3a41d82fdcbf56f999dd5599a181b1804b4c0f19b0f309557",
|
| 25 |
+
"fastplms/models/ttt.py": "sha256:f1d7f9298a930f4c40f8e321ffa08b09d1c5b8492bca623bdc7bfd03ec85774d",
|
| 26 |
+
"fastplms/registry.py": "sha256:cb37799dae5a4c0c780089ab2cca4cc56300c1c5f515b275892aa4e1c4b97eb6",
|
| 27 |
+
"fastplms/runtime.py": "sha256:9521c37fcd168bf9a4137277fb73672403ccf30d70d4845aa879fb22f42ddeb6",
|
| 28 |
+
"fastplms_bundle.py": "sha256:cd4e553471f54a7e7faf0fcc210dd75b8cbf990098116c040db3ff42fd947c43",
|
| 29 |
+
"modeling_fastplms.py": "sha256:486bbf6733e9e044c4d78cf2523f19ecd902b438dd37ee0e13356c01d5d7c761",
|
| 30 |
+
"requirements.txt": "sha256:32c675d7545024f2e9abae380ffd25690ec6e072f5d2cdb0cea9e25c57359fdd",
|
| 31 |
"special_tokens_map.json": "sha256:0b7245ec86c8c3aeaf61523ba70dfa79be137e6283f127bd651adc30b4f15c74",
|
| 32 |
"tokenizer.json": "sha256:8d3447b278176e65fb3ef0224472927bf5fee3be46ea2bd77fad0111423cee1f",
|
| 33 |
"tokenizer_config.json": "sha256:e8d8e40c9f92b334f0272e80bb65ed4043cb9836523cbae899e9859e8cbb8833"
|
| 34 |
},
|
| 35 |
"model_id": "esmc_large",
|
| 36 |
"redistributable": true,
|
| 37 |
+
"release_tool_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 38 |
+
"release_tool_sha256": "6d335c05aa49a232086a816deb25d248d1490529e5783acc3355b9bc6f03e0c2",
|
| 39 |
+
"runtime_bundle_sha256": "d73f544a7623d12eff2ba407e2d7e86417b7d731e36a2931515930eae7d7e8e5",
|
| 40 |
+
"runtime_revision": "e6dd397a9ad368c998d714f6bd64d40b533d1ed1",
|
| 41 |
"schema_version": 2,
|
| 42 |
"scope": "runtime-only",
|
| 43 |
+
"source_tree_sha256": "bed9c7b1e1b0500dcd370e92e1f83b47494f9167d37b16e248f9d17e0135f822",
|
| 44 |
"weights": {
|
| 45 |
"repo_id": "Synthyra/ESMplusplus_large",
|
| 46 |
"revision": "f813401638b3fddab09748aec1ad2bf537aa4208"
|