Instructions to use Synthyra/DPLM2-150M with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Synthyra/DPLM2-150M with Transformers:
# Load model directly from transformers import AutoTokenizer, EsmForDPLM2 tokenizer = AutoTokenizer.from_pretrained("Synthyra/DPLM2-150M", trust_remote_code=True) model = EsmForDPLM2.from_pretrained("Synthyra/DPLM2-150M", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Update FastPLMs runtime files
Browse filesAdd-only FastPLMs files-only publication. Checkpoint weights and complete-artifact attestations are unchanged.
- LICENSES/FastPLMs-Apache-2.0.txt +203 -0
- LICENSES/dplm/LICENSE +202 -0
- LICENSES/dplm/PROVENANCE.md +23 -0
- README.md +200 -127
- THIRD_PARTY_NOTICES.md +99 -0
- config.json +22 -14
- fastplms/__init__.py +48 -0
- fastplms/attention/__init__.py +63 -0
- fastplms/attention/_core.py +779 -0
- fastplms/attention/_kernel_lock.py +191 -0
- fastplms/attention/interfaces.py +242 -0
- fastplms/embeddings/__init__.py +65 -0
- fastplms/embeddings/pooling.py +210 -0
- fastplms/embeddings/runner.py +1559 -0
- fastplms/embeddings/storage.py +1594 -0
- fastplms/embeddings/types.py +187 -0
- fastplms/models.toml +1223 -0
- fastplms/models/__init__.py +10 -0
- fastplms/models/_diffusion_generation.py +510 -0
- fastplms/models/_esm_rotary.py +81 -0
- fastplms/models/dplm2/__init__.py +5 -0
- fastplms/models/dplm2/modeling_dplm2.py +1457 -0
- fastplms/models/dplm2/tokenization_dplm2.py +93 -0
- fastplms/models/ttt.py +866 -0
- fastplms/registry.py +1486 -0
- fastplms/runtime.py +68 -0
- fastplms_bundle.py +0 -0
- modeling_fastplms.py +239 -0
- runtime-attestation.json +49 -0
- special_tokens_map.json +9 -11
- tokenizer_config.json +8 -31
- vocab.txt +1 -1
LICENSES/FastPLMs-Apache-2.0.txt
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
PLEASE NOTE THE APACHE LICENSE ONLY APPLIES TO THE CODE IN THE FastPLMs GITHUB AND ASSOCIATED HUGGINGFACE REPOSITORIES, NOT NECESSARILY THE MODEL WEIGHTS. THOSE LICENSES CAN BE FOUND HERE https://github.com/Synthyra/FastPLMs/tree/main/LICENSES
|
| 2 |
+
|
| 3 |
+
Apache License
|
| 4 |
+
Version 2.0, January 2004
|
| 5 |
+
http://www.apache.org/licenses/
|
| 6 |
+
|
| 7 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 8 |
+
|
| 9 |
+
1. Definitions.
|
| 10 |
+
|
| 11 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 12 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 13 |
+
|
| 14 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 15 |
+
the copyright owner that is granting the License.
|
| 16 |
+
|
| 17 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 18 |
+
other entities that control, are controlled by, or are under common
|
| 19 |
+
control with that entity. For the purposes of this definition,
|
| 20 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 21 |
+
direction or management of such entity, whether by contract or
|
| 22 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 23 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 24 |
+
|
| 25 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 26 |
+
exercising permissions granted by this License.
|
| 27 |
+
|
| 28 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 29 |
+
including but not limited to software source code, documentation
|
| 30 |
+
source, and configuration files.
|
| 31 |
+
|
| 32 |
+
"Object" form shall mean any form resulting from mechanical
|
| 33 |
+
transformation or translation of a Source form, including but
|
| 34 |
+
not limited to compiled object code, generated documentation,
|
| 35 |
+
and conversions to other media types.
|
| 36 |
+
|
| 37 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 38 |
+
Object form, made available under the License, as indicated by a
|
| 39 |
+
copyright notice that is included in or attached to the work
|
| 40 |
+
(an example is provided in the Appendix below).
|
| 41 |
+
|
| 42 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 43 |
+
form, that is based on (or derived from) the Work and for which the
|
| 44 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 45 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 46 |
+
of this License, Derivative Works shall not include works that remain
|
| 47 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 48 |
+
the Work and Derivative Works thereof.
|
| 49 |
+
|
| 50 |
+
"Contribution" shall mean any work of authorship, including
|
| 51 |
+
the original version of the Work and any modifications or additions
|
| 52 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 53 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 54 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 55 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 56 |
+
means any form of electronic, verbal, or written communication sent
|
| 57 |
+
to the Licensor or its representatives, including but not limited to
|
| 58 |
+
communication on electronic mailing lists, source code control systems,
|
| 59 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 60 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 61 |
+
excluding communication that is conspicuously marked or otherwise
|
| 62 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 63 |
+
|
| 64 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 65 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 66 |
+
subsequently incorporated within the Work.
|
| 67 |
+
|
| 68 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 69 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 70 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 71 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 72 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 73 |
+
Work and such Derivative Works in Source or Object form.
|
| 74 |
+
|
| 75 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 76 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 77 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 78 |
+
(except as stated in this section) patent license to make, have made,
|
| 79 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 80 |
+
where such license applies only to those patent claims licensable
|
| 81 |
+
by such Contributor that are necessarily infringed by their
|
| 82 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 83 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 84 |
+
institute patent litigation against any entity (including a
|
| 85 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 86 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 87 |
+
or contributory patent infringement, then any patent licenses
|
| 88 |
+
granted to You under this License for that Work shall terminate
|
| 89 |
+
as of the date such litigation is filed.
|
| 90 |
+
|
| 91 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 92 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 93 |
+
modifications, and in Source or Object form, provided that You
|
| 94 |
+
meet the following conditions:
|
| 95 |
+
|
| 96 |
+
(a) You must give any other recipients of the Work or
|
| 97 |
+
Derivative Works a copy of this License; and
|
| 98 |
+
|
| 99 |
+
(b) You must cause any modified files to carry prominent notices
|
| 100 |
+
stating that You changed the files; and
|
| 101 |
+
|
| 102 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 103 |
+
that You distribute, all copyright, patent, trademark, and
|
| 104 |
+
attribution notices from the Source form of the Work,
|
| 105 |
+
excluding those notices that do not pertain to any part of
|
| 106 |
+
the Derivative Works; and
|
| 107 |
+
|
| 108 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 109 |
+
distribution, then any Derivative Works that You distribute must
|
| 110 |
+
include a readable copy of the attribution notices contained
|
| 111 |
+
within such NOTICE file, excluding those notices that do not
|
| 112 |
+
pertain to any part of the Derivative Works, in at least one
|
| 113 |
+
of the following places: within a NOTICE text file distributed
|
| 114 |
+
as part of the Derivative Works; within the Source form or
|
| 115 |
+
documentation, if provided along with the Derivative Works; or,
|
| 116 |
+
within a display generated by the Derivative Works, if and
|
| 117 |
+
wherever such third-party notices normally appear. The contents
|
| 118 |
+
of the NOTICE file are for informational purposes only and
|
| 119 |
+
do not modify the License. You may add Your own attribution
|
| 120 |
+
notices within Derivative Works that You distribute, alongside
|
| 121 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 122 |
+
that such additional attribution notices cannot be construed
|
| 123 |
+
as modifying the License.
|
| 124 |
+
|
| 125 |
+
You may add Your own copyright statement to Your modifications and
|
| 126 |
+
may provide additional or different license terms and conditions
|
| 127 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 128 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 129 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 130 |
+
the conditions stated in this License.
|
| 131 |
+
|
| 132 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 133 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 134 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 135 |
+
this License, without any additional terms or conditions.
|
| 136 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 137 |
+
the terms of any separate license agreement you may have executed
|
| 138 |
+
with Licensor regarding such Contributions.
|
| 139 |
+
|
| 140 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 141 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 142 |
+
except as required for reasonable and customary use in describing the
|
| 143 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 144 |
+
|
| 145 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 146 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 147 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 148 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 149 |
+
implied, including, without limitation, any warranties or conditions
|
| 150 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 151 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 152 |
+
appropriateness of using or redistributing the Work and assume any
|
| 153 |
+
risks associated with Your exercise of permissions under this License.
|
| 154 |
+
|
| 155 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 156 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 157 |
+
unless required by applicable law (such as deliberate and grossly
|
| 158 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 159 |
+
liable to You for damages, including any direct, indirect, special,
|
| 160 |
+
incidental, or consequential damages of any character arising as a
|
| 161 |
+
result of this License or out of the use or inability to use the
|
| 162 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 163 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 164 |
+
other commercial damages or losses), even if such Contributor
|
| 165 |
+
has been advised of the possibility of such damages.
|
| 166 |
+
|
| 167 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 168 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 169 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 170 |
+
or other liability obligations and/or rights consistent with this
|
| 171 |
+
License. However, in accepting such obligations, You may act only
|
| 172 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 173 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 174 |
+
defend, and hold each Contributor harmless for any liability
|
| 175 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 176 |
+
of your accepting any such warranty or additional liability.
|
| 177 |
+
|
| 178 |
+
END OF TERMS AND CONDITIONS
|
| 179 |
+
|
| 180 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 181 |
+
|
| 182 |
+
To apply the Apache License to your work, attach the following
|
| 183 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 184 |
+
replaced with your own identifying information. (Don't include
|
| 185 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 186 |
+
comment syntax for the file format. We also recommend that a
|
| 187 |
+
file or class name and description of purpose be included on the
|
| 188 |
+
same "printed page" as the copyright notice for easier
|
| 189 |
+
identification within third-party archives.
|
| 190 |
+
|
| 191 |
+
Copyright [yyyy] [name of copyright owner]
|
| 192 |
+
|
| 193 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 194 |
+
you may not use this file except in compliance with the License.
|
| 195 |
+
You may obtain a copy of the License at
|
| 196 |
+
|
| 197 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 198 |
+
|
| 199 |
+
Unless required by applicable law or agreed to in writing, software
|
| 200 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 201 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 202 |
+
See the License for the specific language governing permissions and
|
| 203 |
+
limitations under the License.
|
LICENSES/dplm/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|
LICENSES/dplm/PROVENANCE.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DPLM checkpoint license provenance
|
| 2 |
+
|
| 3 |
+
FastPLMs uses the ByteDance DPLM repository at immutable revision
|
| 4 |
+
`8a2e15e53416b4536f03f79ad1f6f6a9cbd5e19d` as the official source for both
|
| 5 |
+
DPLM1 and DPLM2.
|
| 6 |
+
|
| 7 |
+
At that revision:
|
| 8 |
+
|
| 9 |
+
- the repository contains the complete [Apache License 2.0](https://github.com/bytedance/dplm/blob/8a2e15e53416b4536f03f79ad1f6f6a9cbd5e19d/LICENSE); and
|
| 10 |
+
- the [official README](https://github.com/bytedance/dplm/blob/8a2e15e53416b4536f03f79ad1f6f6a9cbd5e19d/README.md#overview)
|
| 11 |
+
defines the repository release as including the pretrained weights for the
|
| 12 |
+
DPLM family, specifically DPLM1 and DPLM2, alongside training and inference
|
| 13 |
+
implementations.
|
| 14 |
+
|
| 15 |
+
FastPLMs therefore records the official DPLM1 and DPLM2 checkpoint weights as
|
| 16 |
+
Apache-2.0. Converted Synthyra checkpoints retain that license and include the
|
| 17 |
+
verbatim upstream `LICENSE`. The deterministic conversion identifiers are
|
| 18 |
+
`dplm_to_fastplms_v1` and `dplm2_to_fastplms_v1`; neither adds restrictions to
|
| 19 |
+
the upstream terms.
|
| 20 |
+
|
| 21 |
+
Complete publication is permitted only after the ordinary FastPLMs artifact,
|
| 22 |
+
state-parity, legal-inventory, and atomic-publication checks pass. This record
|
| 23 |
+
does not change the terms of third-party training data or downstream outputs.
|
README.md
CHANGED
|
@@ -1,127 +1,200 @@
|
|
| 1 |
-
---
|
| 2 |
-
library_name: transformers
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
``
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
``
|
| 20 |
-
|
| 21 |
-
##
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
```
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
##
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: transformers
|
| 3 |
+
license: "apache-2.0"
|
| 4 |
+
tags:
|
| 5 |
+
- protein-language-model
|
| 6 |
+
- fastplms
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
<!-- Generated from src/fastplms/models.toml. Do not edit. -->
|
| 10 |
+
|
| 11 |
+
# Synthyra/DPLM2-150M
|
| 12 |
+
|
| 13 |
+
This checkpoint packages the FastPLMs `DPLM2` implementation.
|
| 14 |
+
|
| 15 |
+
Accepted inputs are tokenized amino-acid and structure tracks with explicit
|
| 16 |
+
modality boundaries.
|
| 17 |
+
Supported Transformers entry points are `AutoConfig`, `AutoModel`,
|
| 18 |
+
`AutoModelForMaskedLM`, `AutoModelForSequenceClassification`,
|
| 19 |
+
`AutoModelForTokenClassification`.
|
| 20 |
+
|
| 21 |
+
## Install and platform requirements
|
| 22 |
+
|
| 23 |
+
Install FastPLMs from the exact source revision paired with this model card:
|
| 24 |
+
|
| 25 |
+
```bash
|
| 26 |
+
python -m pip install \
|
| 27 |
+
"fastplms @ git+https://github.com/Synthyra/FastPLMs.git@1b9ce023f1e06571cf3e6324be0610ffa53e0a4a"
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
Python 3.11-3.14, PyTorch 2.13, and Transformers 5.13 are required. The declared CPU gate covers tiny offline contracts; published checkpoint throughput and parity require the documented device tier. The Hub quick start below requires network
|
| 31 |
+
access on first download. For an air-gapped run, first build the manifest-pinned
|
| 32 |
+
local artifact and use the offline form shown in the example.
|
| 33 |
+
|
| 34 |
+
## Quick start
|
| 35 |
+
|
| 36 |
+
```python
|
| 37 |
+
from transformers import AutoModel
|
| 38 |
+
|
| 39 |
+
model_id = "Synthyra/DPLM2-150M"
|
| 40 |
+
model = AutoModel.from_pretrained(
|
| 41 |
+
model_id,
|
| 42 |
+
trust_remote_code=True,
|
| 43 |
+
).eval()
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
This example uses the published Hub repository. For offline validation, build
|
| 47 |
+
the manifest-pinned artifact and replace `model_id` with its local
|
| 48 |
+
`dist/hub/DPLM2-150M` path, then pass `local_files_only=True`.
|
| 49 |
+
|
| 50 |
+
Leave attention unspecified for the Transformers default. Supported explicit
|
| 51 |
+
choices are `sdpa`.
|
| 52 |
+
Pass the selected name through `attn_implementation`.
|
| 53 |
+
When an optimized backend cannot return full attention tensors,
|
| 54 |
+
`output_attentions=True` emits one explicit runtime warning and uses a correctly
|
| 55 |
+
masked eager implementation for that call only. The warning identifies the
|
| 56 |
+
configured backend, effective backend, and reason. Configuration and later
|
| 57 |
+
calls are unchanged.
|
| 58 |
+
For BF16 execution, this family uses FP32 parameters with CUDA BF16 autocast.
|
| 59 |
+
|
| 60 |
+
## Dataset embeddings
|
| 61 |
+
|
| 62 |
+
The shared embedding API accepts sequences, `(id, sequence)` pairs,
|
| 63 |
+
`EmbeddingInput` records, insertion-ordered `{id: sequence}` mappings, or a
|
| 64 |
+
FASTA path. Results preserve order and duplicate identifiers:
|
| 65 |
+
|
| 66 |
+
```python
|
| 67 |
+
result = model.embed_dataset(
|
| 68 |
+
["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"],
|
| 69 |
+
batch_size=2,
|
| 70 |
+
pooling=("mean", "std"),
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
for record in result:
|
| 74 |
+
print(record.id, record.sequence, record.tensor.shape)
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
Set `full_embeddings=True` for one residue tensor with shape `(l, d)` per
|
| 78 |
+
sequence. Set `output` to a directory for bounded-memory, transactional
|
| 79 |
+
safetensors with ordered-prefix resume, or choose `format="sqlite"` for
|
| 80 |
+
batch-level database commits and exact resume. Pooling excludes boundary,
|
| 81 |
+
padding, and other non-biological positions.
|
| 82 |
+
|
| 83 |
+
For a long FASTA run, stream completed batches into SQLite:
|
| 84 |
+
|
| 85 |
+
```python
|
| 86 |
+
persisted = model.embed_dataset(
|
| 87 |
+
"proteins.fasta",
|
| 88 |
+
batch_size=64,
|
| 89 |
+
pooling=("mean",),
|
| 90 |
+
output="protein-embeddings.sqlite",
|
| 91 |
+
format="sqlite",
|
| 92 |
+
resume=True,
|
| 93 |
+
)
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
Resume verifies the input order, model state, tokenizer policy, backend, dtype,
|
| 97 |
+
and pooling configuration. It never appends incompatible records to an
|
| 98 |
+
existing run.
|
| 99 |
+
|
| 100 |
+
## Amino-acid and structure co-generation
|
| 101 |
+
|
| 102 |
+
DPLM2 uses separate structure and amino-acid tracks with modality-specific
|
| 103 |
+
boundary and mask tokens:
|
| 104 |
+
|
| 105 |
+
```python
|
| 106 |
+
import torch
|
| 107 |
+
from transformers import AutoModelForMaskedLM, AutoTokenizer
|
| 108 |
+
|
| 109 |
+
model_id = "Synthyra/DPLM2-150M"
|
| 110 |
+
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
| 111 |
+
generator = AutoModelForMaskedLM.from_pretrained(
|
| 112 |
+
model_id,
|
| 113 |
+
trust_remote_code=True,
|
| 114 |
+
).cuda().eval()
|
| 115 |
+
vocab = tokenizer.get_vocab()
|
| 116 |
+
l = 64
|
| 117 |
+
structure = [
|
| 118 |
+
vocab["<cls_struct>"],
|
| 119 |
+
*([vocab["<mask_struct>"]] * l),
|
| 120 |
+
vocab["<eos_struct>"],
|
| 121 |
+
]
|
| 122 |
+
amino_acids = [
|
| 123 |
+
vocab["<cls_aa>"],
|
| 124 |
+
*([vocab["<mask_aa>"]] * l),
|
| 125 |
+
vocab["<eos_aa>"],
|
| 126 |
+
]
|
| 127 |
+
input_ids = torch.tensor([structure + amino_acids], device="cuda")
|
| 128 |
+
|
| 129 |
+
with torch.inference_mode():
|
| 130 |
+
generated = generator.generate(input_ids, max_iter=100)["output_tokens"]
|
| 131 |
+
print(generated.shape)
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
Generic `cls_token`, `eos_token`, `mask_token`, and `unk_token` aliases are
|
| 135 |
+
intentionally unset. Callers constructing multimodal tensors must choose the
|
| 136 |
+
amino-acid or structure token explicitly. Raw amino-acid sequences remain
|
| 137 |
+
supported by `model.embed_dataset(...)`.
|
| 138 |
+
|
| 139 |
+
Plain `AutoModel` omits the optional ESM pooler because this co-generation
|
| 140 |
+
checkpoint contains no trained pooler weights. Pass `add_pooling_layer=True`
|
| 141 |
+
only when intentionally initializing and training that head.
|
| 142 |
+
|
| 143 |
+
The checkpoint weights are Apache-2.0. The pinned ByteDance
|
| 144 |
+
[LICENSE](https://github.com/bytedance/dplm/blob/8a2e15e53416b4536f03f79ad1f6f6a9cbd5e19d/LICENSE) and [README](https://github.com/bytedance/dplm/blob/8a2e15e53416b4536f03f79ad1f6f6a9cbd5e19d/README.md#overview) provide the immutable license
|
| 145 |
+
basis for the pretrained DPLM1 and DPLM2 weights. Complete publication remains
|
| 146 |
+
subject to all artifact, legal, parity, and atomic-publication preflights.
|
| 147 |
+
|
| 148 |
+
## Runtime contract
|
| 149 |
+
|
| 150 |
+
- Public input: Tokenized amino-acid and structure tracks with explicit modality boundaries
|
| 151 |
+
- Advertised AutoClasses: `AutoConfig`, `AutoModel`, `AutoModelForMaskedLM`, `AutoModelForSequenceClassification`, `AutoModelForTokenClassification`
|
| 152 |
+
- AutoClass weight status: `AutoConfig` = `FastPLMs extension`, `AutoModel` = `pretrained`, `AutoModelForMaskedLM` = `pretrained`, `AutoModelForSequenceClassification` = `base weights + untrained task head`, `AutoModelForTokenClassification` = `base weights + untrained task head`
|
| 153 |
+
- Attention implementations: `sdpa`
|
| 154 |
+
- Precision policies: `default`
|
| 155 |
+
- BF16 execution: `fp32_parameters_autocast`
|
| 156 |
+
- Generation contract: `required`
|
| 157 |
+
- Optional dependency group: `core`
|
| 158 |
+
- Weight publication allowed: `true`
|
| 159 |
+
- Weight license status: `resolved`
|
| 160 |
+
- Redistributable: `true`
|
| 161 |
+
- Complete weight publication required: `false`
|
| 162 |
+
|
| 163 |
+
## Provenance
|
| 164 |
+
|
| 165 |
+
- FastPLMs weights: `Synthyra/DPLM2-150M@182745b8dc5661f898481a4fa60a7af9d53385c4`
|
| 166 |
+
- Runtime revision: `1b9ce023f1e06571cf3e6324be0610ffa53e0a4a`
|
| 167 |
+
- Runtime source-tree SHA-256: `5de51d1c5c03b023e3db0af2d6a8849140af8011164caf84722faa356428efe1`
|
| 168 |
+
- Runtime bundle SHA-256: `3ca371517961576b5af3c51ca4f07e486d91a11c8da3b0d7916d0bbc8f76be72`
|
| 169 |
+
- Generator/schema version and complete/runtime-only attestations: recorded in `provenance.json`
|
| 170 |
+
- Canonical transformed state SHA-256: `82e1751f59052b8de72b082517557db47947e8d9b4ac2f11278369e6c0cbf001`
|
| 171 |
+
- Conversion equality attestation: recorded in `provenance.json`
|
| 172 |
+
- Official checkpoint: `airkingbd/dplm2_150m@3451d984d06497f835ed49634bd68c9dfb54d730`
|
| 173 |
+
- Artifact source: `official`
|
| 174 |
+
- State transform: `dplm2_to_fastplms_v1`
|
| 175 |
+
- BF16 execution: `fp32_parameters_autocast`
|
| 176 |
+
- Tokenizer class: `fastplms.models.dplm2.tokenization_dplm2.DPLM2Tokenizer`
|
| 177 |
+
- Pinned upstreams: `dplm`
|
| 178 |
+
- Reference container: `reference-dplm`
|
| 179 |
+
- Release tiers: `check`, `compliance`, `feature`, `artifact`, `benchmark`
|
| 180 |
+
- Unresolved required file identities: `0`
|
| 181 |
+
|
| 182 |
+
The local artifact records exact file identities, conversion provenance, source
|
| 183 |
+
revisions, and legal texts in `provenance.json`. A nonzero unresolved count is a
|
| 184 |
+
release blocker.
|
| 185 |
+
|
| 186 |
+
## Validation boundary
|
| 187 |
+
|
| 188 |
+
For tiers declared by the manifest, the release contract compares applicable
|
| 189 |
+
semantic configuration, tokenizer behavior, state keys, shapes, dtypes,
|
| 190 |
+
values, aliases, and representative inference with the pinned official
|
| 191 |
+
implementation. This metadata does not by itself claim that a particular build
|
| 192 |
+
passed, that one backend is faster, or that an output has biological or
|
| 193 |
+
therapeutic validity.
|
| 194 |
+
|
| 195 |
+
## License
|
| 196 |
+
|
| 197 |
+
Checkpoint terms: Apache-2.0. The Hub model-card identifier is
|
| 198 |
+
`apache-2.0`. Applicable source licenses, notices, attribution,
|
| 199 |
+
and conversion records are distributed with the local artifact. Review them
|
| 200 |
+
before use.
|
THIRD_PARTY_NOTICES.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Third-party notices
|
| 2 |
+
|
| 3 |
+
FastPLMs implements interfaces and checkpoint mappings for independently
|
| 4 |
+
released protein models. The pinned repositories under `vendor/upstream/` are
|
| 5 |
+
parity oracles. Production code does not import them, and runtime images do not
|
| 6 |
+
contain them.
|
| 7 |
+
|
| 8 |
+
This notice is informational and is not legal advice. A checkpoint license can
|
| 9 |
+
differ from the license covering its source implementation. The typed inventory
|
| 10 |
+
in `src/fastplms/models.toml` and the verbatim files under `LICENSES/` are the
|
| 11 |
+
distribution record.
|
| 12 |
+
|
| 13 |
+
## ANKH
|
| 14 |
+
|
| 15 |
+
The pinned ANKH implementation and the mirrored ANKH checkpoints are identified
|
| 16 |
+
as CC BY-NC-SA 4.0. FastPLMs displays those terms but does not enforce them in
|
| 17 |
+
software. Users are responsible for determining whether their use and
|
| 18 |
+
redistribution comply. The complete text is in `LICENSES/ankh/LICENSE.md`.
|
| 19 |
+
|
| 20 |
+
## Profluent-E1
|
| 21 |
+
|
| 22 |
+
Profluent identifies its E1 model code as Apache-2.0. The E1 weights and full
|
| 23 |
+
release are subject to the Profluent-E1 Clickthrough License Agreement and the
|
| 24 |
+
incorporated attribution requirements. Any E1 distribution must retain all of
|
| 25 |
+
the following files:
|
| 26 |
+
|
| 27 |
+
- `LICENSES/e1/LICENSE`, the Profluent-E1 agreement
|
| 28 |
+
- `LICENSES/e1/ATTRIBUTION`, the attribution guidelines
|
| 29 |
+
- `LICENSES/e1/NOTICE`, the required notice
|
| 30 |
+
- `LICENSES/e1/Apache-2.0.txt`, the code license
|
| 31 |
+
- `LICENSES/e1/BSD-3-Clause.txt`, covering the FlashAttention-derived padding
|
| 32 |
+
utility identified by the official E1 source
|
| 33 |
+
- `LICENSES/e1/MODIFICATIONS.md`, the FastPLMs modified-file notice
|
| 34 |
+
|
| 35 |
+
The exact text `Profluent-E1` must remain prominently displayed in E1
|
| 36 |
+
documentation and at each launch of an executable E1 workflow, as required by
|
| 37 |
+
the upstream attribution guidelines. Certain commercial outputs, including
|
| 38 |
+
specified pharmaceutical and target-related outputs, can require the separate
|
| 39 |
+
`Built with Profluent-E1` statement described in `ATTRIBUTION`.
|
| 40 |
+
|
| 41 |
+
## DPLM
|
| 42 |
+
|
| 43 |
+
The pinned ByteDance DPLM repository is Apache-2.0. Its
|
| 44 |
+
[README](https://github.com/bytedance/dplm/blob/8a2e15e53416b4536f03f79ad1f6f6a9cbd5e19d/README.md#overview)
|
| 45 |
+
explicitly defines the repository release as including pretrained DPLM1 and
|
| 46 |
+
DPLM2 weights, and the same revision carries the complete
|
| 47 |
+
[Apache-2.0 license](https://github.com/bytedance/dplm/blob/8a2e15e53416b4536f03f79ad1f6f6a9cbd5e19d/LICENSE).
|
| 48 |
+
FastPLMs records both checkpoint families as Apache-2.0 and distributes the
|
| 49 |
+
verbatim license plus `LICENSES/dplm/PROVENANCE.md`. Converted weights retain
|
| 50 |
+
those terms and remain subject to the ordinary artifact and publication gates.
|
| 51 |
+
|
| 52 |
+
## Biohub
|
| 53 |
+
|
| 54 |
+
The pinned Biohub ESM implementation is MIT and includes a separate
|
| 55 |
+
`THIRD_PARTY_NOTICE.md`; both files are distributed under
|
| 56 |
+
`LICENSES/biohub-esm/`. The pinned Biohub Transformers fork is Apache-2.0, with
|
| 57 |
+
its complete text under `LICENSES/biohub-transformers/`.
|
| 58 |
+
|
| 59 |
+
## Boltz
|
| 60 |
+
|
| 61 |
+
The pinned Boltz source is MIT. The verbatim notice is in
|
| 62 |
+
`LICENSES/boltz/LICENSE`.
|
| 63 |
+
|
| 64 |
+
## Meta ESM and OpenFold
|
| 65 |
+
|
| 66 |
+
The pinned Meta ESM source is MIT. The pinned OpenFold source is Apache-2.0.
|
| 67 |
+
Their verbatim texts and revision-specific provenance notices are under
|
| 68 |
+
`LICENSES/fair-esm/` and `LICENSES/openfold/`.
|
| 69 |
+
|
| 70 |
+
The native H100 ESMFold reference image applies the tracked
|
| 71 |
+
`docker/constraints/openfold-sm90.patch` to the copied OpenFold `setup.py`.
|
| 72 |
+
This build-only change restricts the CUDA extension to `sm90` and selects the
|
| 73 |
+
C++17 standard required by the reference PyTorch version. It leaves the pinned
|
| 74 |
+
submodule, extension source, model classes, checkpoint data, and public API
|
| 75 |
+
unchanged. The complete modified-file record is in
|
| 76 |
+
`LICENSES/openfold/MODIFICATIONS.md`.
|
| 77 |
+
|
| 78 |
+
The isolated reference image also includes Apache-2.0 PyTorch Lightning,
|
| 79 |
+
TorchMetrics, Lightning Utilities, and NVIDIA DLLogger. Their exact versions or
|
| 80 |
+
revision are pinned in `docker/constraints/esmfold.txt`; OpenFold imports them
|
| 81 |
+
eagerly, and FastPLMs production code does not depend on them. DLLogger's exact
|
| 82 |
+
source identity and installed-license handling are recorded in
|
| 83 |
+
`LICENSES/dllogger/PROVENANCE.md`.
|
| 84 |
+
|
| 85 |
+
## ProteinTTT
|
| 86 |
+
|
| 87 |
+
The optional test-time training workflow is validated against the pinned
|
| 88 |
+
ProteinTTT repository under its MIT license. Its verbatim license and
|
| 89 |
+
revision-specific provenance are under `LICENSES/protein-ttt/`.
|
| 90 |
+
|
| 91 |
+
## Conversion and packaging record
|
| 92 |
+
|
| 93 |
+
For every supported family, `src/fastplms/models.toml` records an immutable
|
| 94 |
+
official checkpoint revision, an immutable FastPLMs checkpoint revision, file
|
| 95 |
+
digests, a named state transformation, and a mechanism-level conversion record.
|
| 96 |
+
Generated artifacts reproduce that record in `provenance.json`. A release or
|
| 97 |
+
artifact build must fail when a required file identity, legal text, attribution
|
| 98 |
+
notice, modified-file notice, upstream revision, or conversion record is absent
|
| 99 |
+
or differs from its manifest digest.
|
config.json
CHANGED
|
@@ -1,42 +1,50 @@
|
|
| 1 |
{
|
| 2 |
-
"
|
|
|
|
| 3 |
"architectures": [
|
| 4 |
"EsmForDPLM2"
|
| 5 |
],
|
| 6 |
"attention_probs_dropout_prob": 0.0,
|
| 7 |
-
"attn_backend": "sdpa",
|
| 8 |
"auto_map": {
|
| 9 |
-
"AutoConfig": "
|
| 10 |
-
"AutoModel": "
|
| 11 |
-
"AutoModelForMaskedLM": "
|
| 12 |
-
"AutoModelForSequenceClassification": "
|
| 13 |
-
"AutoModelForTokenClassification": "
|
| 14 |
},
|
| 15 |
"classifier_dropout": null,
|
| 16 |
"dplm_type": "dplm2_esm",
|
| 17 |
-
"dtype": "float32",
|
| 18 |
"emb_layer_norm_before": false,
|
| 19 |
"esmfold_config": null,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
"hidden_act": "gelu",
|
| 21 |
"hidden_dropout_prob": 0.1,
|
| 22 |
"hidden_size": 640,
|
| 23 |
"initializer_range": 0.02,
|
| 24 |
"intermediate_size": 2560,
|
|
|
|
| 25 |
"is_folding_model": false,
|
| 26 |
"layer_norm_eps": 1e-05,
|
| 27 |
"mask_token_id": 32,
|
| 28 |
"max_position_embeddings": 1026,
|
| 29 |
-
"model_type": "
|
| 30 |
"num_attention_heads": 20,
|
| 31 |
"num_hidden_layers": 30,
|
| 32 |
"pad_token_id": 1,
|
| 33 |
-
"pad_type": 2,
|
| 34 |
"position_embedding_type": "rotary",
|
| 35 |
-
"struct_type": 0,
|
| 36 |
-
"tie_word_embeddings": false,
|
| 37 |
"token_dropout": true,
|
| 38 |
-
"
|
| 39 |
-
"
|
|
|
|
| 40 |
"vocab_list": null,
|
| 41 |
"vocab_size": 8229
|
| 42 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"_name_or_path": "facebook/esm2_t30_150M_UR50D",
|
| 3 |
+
"add_cross_attention": false,
|
| 4 |
"architectures": [
|
| 5 |
"EsmForDPLM2"
|
| 6 |
],
|
| 7 |
"attention_probs_dropout_prob": 0.0,
|
|
|
|
| 8 |
"auto_map": {
|
| 9 |
+
"AutoConfig": "modeling_fastplms.DPLM2Config",
|
| 10 |
+
"AutoModel": "modeling_fastplms.DPLM2Model",
|
| 11 |
+
"AutoModelForMaskedLM": "modeling_fastplms.DPLM2ForMaskedLM",
|
| 12 |
+
"AutoModelForSequenceClassification": "modeling_fastplms.DPLM2ForSequenceClassification",
|
| 13 |
+
"AutoModelForTokenClassification": "modeling_fastplms.DPLM2ForTokenClassification"
|
| 14 |
},
|
| 15 |
"classifier_dropout": null,
|
| 16 |
"dplm_type": "dplm2_esm",
|
|
|
|
| 17 |
"emb_layer_norm_before": false,
|
| 18 |
"esmfold_config": null,
|
| 19 |
+
"fastplms_checkpoint_hash": "f86bc2df968f20de59aa45cbcfbeabdc6100ebe41ed587271d30cb1dcefdb1ee",
|
| 20 |
+
"fastplms_checkpoint_repo_id": "airkingbd/dplm2_150m",
|
| 21 |
+
"fastplms_checkpoint_revision": "3451d984d06497f835ed49634bd68c9dfb54d730",
|
| 22 |
+
"fastplms_model_id": "dplm2_150m",
|
| 23 |
+
"fastplms_release_tool_revision": "1b9ce023f1e06571cf3e6324be0610ffa53e0a4a",
|
| 24 |
+
"fastplms_release_tool_sha256": "1459b5d7d13d9b07bd97b3eee764f2ce73623e15e32d07ddf6825c2a9509afb9",
|
| 25 |
+
"fastplms_runtime_bundle_sha256": "3ca371517961576b5af3c51ca4f07e486d91a11c8da3b0d7916d0bbc8f76be72",
|
| 26 |
+
"fastplms_runtime_revision": "1b9ce023f1e06571cf3e6324be0610ffa53e0a4a",
|
| 27 |
+
"fastplms_source_tree_sha256": "5de51d1c5c03b023e3db0af2d6a8849140af8011164caf84722faa356428efe1",
|
| 28 |
+
"fastplms_weights_revision": "3451d984d06497f835ed49634bd68c9dfb54d730",
|
| 29 |
"hidden_act": "gelu",
|
| 30 |
"hidden_dropout_prob": 0.1,
|
| 31 |
"hidden_size": 640,
|
| 32 |
"initializer_range": 0.02,
|
| 33 |
"intermediate_size": 2560,
|
| 34 |
+
"is_decoder": false,
|
| 35 |
"is_folding_model": false,
|
| 36 |
"layer_norm_eps": 1e-05,
|
| 37 |
"mask_token_id": 32,
|
| 38 |
"max_position_embeddings": 1026,
|
| 39 |
+
"model_type": "esm",
|
| 40 |
"num_attention_heads": 20,
|
| 41 |
"num_hidden_layers": 30,
|
| 42 |
"pad_token_id": 1,
|
|
|
|
| 43 |
"position_embedding_type": "rotary",
|
|
|
|
|
|
|
| 44 |
"token_dropout": true,
|
| 45 |
+
"torch_dtype": "float32",
|
| 46 |
+
"transformers_version": "4.39.2",
|
| 47 |
+
"use_cache": false,
|
| 48 |
"vocab_list": null,
|
| 49 |
"vocab_size": 8229
|
| 50 |
}
|
fastplms/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastPLMs public package interface.
|
| 2 |
+
|
| 3 |
+
The module uses lazy exports so importing :mod:`fastplms` does not initialize
|
| 4 |
+
Torch, download checkpoints, construct tokenizers, or compile kernels.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from importlib import import_module
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
__version__ = "1.0.0"
|
| 13 |
+
|
| 14 |
+
_LAZY_EXPORTS = {
|
| 15 |
+
"CheckpointSource": ("fastplms.registry", "CheckpointSource"),
|
| 16 |
+
"EmbeddingInput": ("fastplms.embeddings", "EmbeddingInput"),
|
| 17 |
+
"EmbeddingRecord": ("fastplms.embeddings", "EmbeddingRecord"),
|
| 18 |
+
"EmbeddingResult": ("fastplms.embeddings", "EmbeddingResult"),
|
| 19 |
+
"FileDigest": ("fastplms.registry", "FileDigest"),
|
| 20 |
+
"ModelFamily": ("fastplms.registry", "ModelFamily"),
|
| 21 |
+
"ModelRegistry": ("fastplms.registry", "ModelRegistry"),
|
| 22 |
+
"ModelSpec": ("fastplms.registry", "ModelSpec"),
|
| 23 |
+
"OracleAsset": ("fastplms.registry", "OracleAsset"),
|
| 24 |
+
"RegistryError": ("fastplms.registry", "RegistryError"),
|
| 25 |
+
"RuntimeProfile": ("fastplms.runtime", "RuntimeProfile"),
|
| 26 |
+
"UpstreamSource": ("fastplms.registry", "UpstreamSource"),
|
| 27 |
+
"embed_dataset": ("fastplms.embeddings", "embed_dataset"),
|
| 28 |
+
"get_model_registry": ("fastplms.registry", "get_model_registry"),
|
| 29 |
+
"get_model_spec": ("fastplms.registry", "get_model_spec"),
|
| 30 |
+
"load_model_registry": ("fastplms.registry", "load_model_registry"),
|
| 31 |
+
"runtime_profile": ("fastplms.runtime", "runtime_profile"),
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
__all__ = ["__version__", *_LAZY_EXPORTS]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def __getattr__(name: str) -> Any:
|
| 38 |
+
try:
|
| 39 |
+
module_name, attribute_name = _LAZY_EXPORTS[name]
|
| 40 |
+
except KeyError as error:
|
| 41 |
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from error
|
| 42 |
+
value = getattr(import_module(module_name), attribute_name)
|
| 43 |
+
globals()[name] = value
|
| 44 |
+
return value
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def __dir__() -> list[str]:
|
| 48 |
+
return sorted(set(globals()).union(__all__))
|
fastplms/attention/__init__.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared attention backends, masks, and optional optimized kernels."""
|
| 2 |
+
|
| 3 |
+
from ._core import (
|
| 4 |
+
VALID_ATTENTION_BACKENDS,
|
| 5 |
+
AttentionBackend,
|
| 6 |
+
BlockMask,
|
| 7 |
+
_ensure_flash_kernels_loaded,
|
| 8 |
+
_get_flex_attention_fn,
|
| 9 |
+
_get_flex_block_mask,
|
| 10 |
+
_kernels_flash_forward,
|
| 11 |
+
_kernels_flash_varlen_forward,
|
| 12 |
+
_unpad_input,
|
| 13 |
+
bool_to_additive_mask,
|
| 14 |
+
clear_flex_attention_caches,
|
| 15 |
+
create_block_mask,
|
| 16 |
+
flex_attention,
|
| 17 |
+
get_attention_mask,
|
| 18 |
+
get_attn_implementation,
|
| 19 |
+
index_first_axis,
|
| 20 |
+
index_put_first_axis,
|
| 21 |
+
kernels_flash_attention_func,
|
| 22 |
+
pad_input,
|
| 23 |
+
resolve_attention_backend,
|
| 24 |
+
resolve_attention_backend_for_call,
|
| 25 |
+
set_config_attn_implementation,
|
| 26 |
+
warn_attention_backend_fallback,
|
| 27 |
+
)
|
| 28 |
+
from .interfaces import (
|
| 29 |
+
FASTPLMS_ATTENTION_FUNCTIONS,
|
| 30 |
+
FASTPLMS_ATTENTION_MASKS,
|
| 31 |
+
FastPLMsAttentionMixin,
|
| 32 |
+
validate_transformers_attention_interfaces,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
__all__ = [
|
| 36 |
+
"FASTPLMS_ATTENTION_FUNCTIONS",
|
| 37 |
+
"FASTPLMS_ATTENTION_MASKS",
|
| 38 |
+
"VALID_ATTENTION_BACKENDS",
|
| 39 |
+
"AttentionBackend",
|
| 40 |
+
"BlockMask",
|
| 41 |
+
"FastPLMsAttentionMixin",
|
| 42 |
+
"_ensure_flash_kernels_loaded",
|
| 43 |
+
"_get_flex_attention_fn",
|
| 44 |
+
"_get_flex_block_mask",
|
| 45 |
+
"_kernels_flash_forward",
|
| 46 |
+
"_kernels_flash_varlen_forward",
|
| 47 |
+
"_unpad_input",
|
| 48 |
+
"bool_to_additive_mask",
|
| 49 |
+
"clear_flex_attention_caches",
|
| 50 |
+
"create_block_mask",
|
| 51 |
+
"flex_attention",
|
| 52 |
+
"get_attention_mask",
|
| 53 |
+
"get_attn_implementation",
|
| 54 |
+
"index_first_axis",
|
| 55 |
+
"index_put_first_axis",
|
| 56 |
+
"kernels_flash_attention_func",
|
| 57 |
+
"pad_input",
|
| 58 |
+
"resolve_attention_backend",
|
| 59 |
+
"resolve_attention_backend_for_call",
|
| 60 |
+
"set_config_attn_implementation",
|
| 61 |
+
"validate_transformers_attention_interfaces",
|
| 62 |
+
"warn_attention_backend_fallback",
|
| 63 |
+
]
|
fastplms/attention/_core.py
ADDED
|
@@ -0,0 +1,779 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Low-level attention kernels and mask construction.
|
| 2 |
+
|
| 3 |
+
The public backend contract lives in :mod:`fastplms.attention`. Optional
|
| 4 |
+
kernels are resolved only after a caller explicitly requests them, so importing
|
| 5 |
+
FastPLMs never downloads or compiles code.
|
| 6 |
+
"""
|
| 7 |
+
|
| 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:
|
| 25 |
+
create_block_mask = None
|
| 26 |
+
flex_attention = None
|
| 27 |
+
BlockMask = None
|
| 28 |
+
|
| 29 |
+
_MAX_FLEX_CACHE_ENTRIES = 128
|
| 30 |
+
_compiled_flex_attention: OrderedDict[tuple, object] = OrderedDict()
|
| 31 |
+
_flex_block_masks: OrderedDict[tuple, BlockMask] = OrderedDict()
|
| 32 |
+
_flex_cache_lock = RLock()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _remember(cache: OrderedDict, key: tuple, value):
|
| 36 |
+
"""Insert an item into a bounded least-recently-used cache."""
|
| 37 |
+
cache[key] = value
|
| 38 |
+
cache.move_to_end(key)
|
| 39 |
+
while len(cache) > _MAX_FLEX_CACHE_ENTRIES:
|
| 40 |
+
cache.popitem(last=False)
|
| 41 |
+
return value
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def clear_flex_attention_caches() -> None:
|
| 45 |
+
"""Drop FastPLMs-owned compiled Flex callables and block masks.
|
| 46 |
+
|
| 47 |
+
This deliberately does not call :func:`torch.compiler.reset`, which would
|
| 48 |
+
clear process-global Torch compilation state owned by unrelated models.
|
| 49 |
+
Active forwards retain their local references and can complete safely.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
with _flex_cache_lock:
|
| 53 |
+
_compiled_flex_attention.clear()
|
| 54 |
+
_flex_block_masks.clear()
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _get_flex_attention_fn(
|
| 58 |
+
*,
|
| 59 |
+
device: torch.device | None = None,
|
| 60 |
+
dtype: torch.dtype | None = None,
|
| 61 |
+
shape: tuple[int, ...] | None = None,
|
| 62 |
+
sequence_lengths: tuple[int, ...] | None = None,
|
| 63 |
+
mask_semantics: str = "padding",
|
| 64 |
+
):
|
| 65 |
+
"""Return a compiled Flex callable for an explicit execution signature.
|
| 66 |
+
|
| 67 |
+
Compilation depends on execution shape, device, dtype, and mask semantics.
|
| 68 |
+
Per-example padding lengths are represented by the ``BlockMask`` argument
|
| 69 |
+
and must not create a new compiled graph for every batch composition.
|
| 70 |
+
"""
|
| 71 |
+
if flex_attention is None:
|
| 72 |
+
return None
|
| 73 |
+
# Retain the keyword for compatibility with remote-code artifacts while
|
| 74 |
+
# deliberately excluding data-dependent lengths from the compile key.
|
| 75 |
+
del sequence_lengths
|
| 76 |
+
flex_mod = torch.nn.attention.flex_attention
|
| 77 |
+
if getattr(flex_mod, "_FLEX_ATTENTION_DISABLE_COMPILE_DEBUG", False):
|
| 78 |
+
return flex_attention
|
| 79 |
+
key = (
|
| 80 |
+
None if device is None else str(device),
|
| 81 |
+
None if dtype is None else str(dtype),
|
| 82 |
+
shape,
|
| 83 |
+
mask_semantics,
|
| 84 |
+
)
|
| 85 |
+
with _flex_cache_lock:
|
| 86 |
+
compiled = _compiled_flex_attention.get(key)
|
| 87 |
+
if compiled is None:
|
| 88 |
+
compiled = torch.compile(flex_attention, dynamic=False)
|
| 89 |
+
_remember(_compiled_flex_attention, key, compiled)
|
| 90 |
+
else:
|
| 91 |
+
_compiled_flex_attention.move_to_end(key)
|
| 92 |
+
return compiled
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _get_flex_block_mask(
|
| 96 |
+
*,
|
| 97 |
+
mask_pattern: torch.Tensor,
|
| 98 |
+
batch_size: int,
|
| 99 |
+
query_length: int,
|
| 100 |
+
key_value_length: int,
|
| 101 |
+
device: torch.device,
|
| 102 |
+
dtype: torch.dtype | None,
|
| 103 |
+
mask_semantics: str,
|
| 104 |
+
mask_mod: Callable[
|
| 105 |
+
[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor],
|
| 106 |
+
torch.Tensor,
|
| 107 |
+
],
|
| 108 |
+
) -> BlockMask:
|
| 109 |
+
"""Return a bounded, exact-pattern cached Flex ``BlockMask``.
|
| 110 |
+
|
| 111 |
+
The complete pattern is transferred to the host once to avoid a CUDA
|
| 112 |
+
synchronization per batch row. Execution dtype remains part of the key
|
| 113 |
+
because compiled Flex plans can specialize on it even though the pattern
|
| 114 |
+
tensor itself is boolean or integer.
|
| 115 |
+
"""
|
| 116 |
+
if create_block_mask is None:
|
| 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),
|
| 129 |
+
(batch_size, query_length, key_value_length),
|
| 130 |
+
str(pattern.dtype),
|
| 131 |
+
pattern_bytes,
|
| 132 |
+
mask_semantics,
|
| 133 |
+
)
|
| 134 |
+
with _flex_cache_lock:
|
| 135 |
+
flex_block_mask = _flex_block_masks.get(cache_key)
|
| 136 |
+
if flex_block_mask is None:
|
| 137 |
+
flex_block_mask = create_block_mask(
|
| 138 |
+
mask_mod,
|
| 139 |
+
batch_size,
|
| 140 |
+
1,
|
| 141 |
+
query_length,
|
| 142 |
+
key_value_length,
|
| 143 |
+
device=device,
|
| 144 |
+
)
|
| 145 |
+
_remember(_flex_block_masks, cache_key, flex_block_mask)
|
| 146 |
+
else:
|
| 147 |
+
_flex_block_masks.move_to_end(cache_key)
|
| 148 |
+
return flex_block_mask
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
# Hugging Face `kernels` exposes slightly different APIs for FlashAttention 2
|
| 152 |
+
# and 3. Detect the loaded variant once so every caller uses the same dispatch.
|
| 153 |
+
def _infer_kernels_flash_variant(kernel) -> str | None:
|
| 154 |
+
if hasattr(kernel, "fwd") and hasattr(kernel, "varlen_fwd"):
|
| 155 |
+
return "flash_attn2"
|
| 156 |
+
if hasattr(kernel, "flash_attn_func") and hasattr(kernel, "flash_attn_varlen_func"):
|
| 157 |
+
return "flash_attn3"
|
| 158 |
+
return None
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _load_kernels_flash(implementation: str) -> tuple[object, str]:
|
| 162 |
+
"""Load exactly the requested FlashAttention kernel.
|
| 163 |
+
|
| 164 |
+
Loading is deferred until backend selection. A FlashAttention-2 request
|
| 165 |
+
never falls through to FlashAttention-3, or vice versa.
|
| 166 |
+
"""
|
| 167 |
+
from fastplms.registry import get_model_registry
|
| 168 |
+
|
| 169 |
+
kernel_spec = get_model_registry().attention_kernels[implementation]
|
| 170 |
+
repository = kernel_spec.repository
|
| 171 |
+
try:
|
| 172 |
+
flash_kernel = load_locked_kernel(repository, kernel_spec.revision)
|
| 173 |
+
except Exception as error:
|
| 174 |
+
raise RuntimeError(
|
| 175 |
+
f"Unable to load the manifest-pinned kernel "
|
| 176 |
+
f"{repository}@{kernel_spec.revision} for {implementation!r}."
|
| 177 |
+
) from error
|
| 178 |
+
flash_kernel_variant = _infer_kernels_flash_variant(flash_kernel)
|
| 179 |
+
if flash_kernel_variant != kernel_spec.expected_variant:
|
| 180 |
+
raise RuntimeError(
|
| 181 |
+
f"{repository}@{kernel_spec.revision} exposed {flash_kernel_variant!r}; "
|
| 182 |
+
f"expected {kernel_spec.expected_variant!r}."
|
| 183 |
+
)
|
| 184 |
+
if not all(
|
| 185 |
+
callable(getattr(flash_kernel, name, None))
|
| 186 |
+
for name in ("flash_attn_func", "flash_attn_varlen_func")
|
| 187 |
+
):
|
| 188 |
+
raise RuntimeError(
|
| 189 |
+
f"{repository}@{kernel_spec.revision} does not expose the "
|
| 190 |
+
"autograd-enabled flash_attn_func and flash_attn_varlen_func APIs."
|
| 191 |
+
)
|
| 192 |
+
return flash_kernel, flash_kernel_variant
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
_FLASH_KERNELS: dict[str, tuple[object, str]] = {}
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def _validate_kernels_flash_dtype(
|
| 199 |
+
query_states: torch.Tensor,
|
| 200 |
+
key_states: torch.Tensor,
|
| 201 |
+
value_states: torch.Tensor,
|
| 202 |
+
implementation: str,
|
| 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))
|
| 209 |
+
raise RuntimeError(
|
| 210 |
+
f"{implementation!r} requires Q, K, and V to share one dtype; received {observed}."
|
| 211 |
+
)
|
| 212 |
+
runtime_dtype = query_states.dtype
|
| 213 |
+
if (
|
| 214 |
+
runtime_dtype == torch.float32
|
| 215 |
+
and query_states.is_cuda
|
| 216 |
+
and torch.is_autocast_enabled("cuda")
|
| 217 |
+
):
|
| 218 |
+
runtime_dtype = torch.get_autocast_dtype("cuda")
|
| 219 |
+
dtype_names = {
|
| 220 |
+
torch.float32: "float32",
|
| 221 |
+
torch.bfloat16: "bfloat16",
|
| 222 |
+
torch.float16: "float16",
|
| 223 |
+
}
|
| 224 |
+
runtime_dtype_name = dtype_names.get(runtime_dtype, str(runtime_dtype))
|
| 225 |
+
from fastplms.registry import get_model_registry
|
| 226 |
+
|
| 227 |
+
supported = get_model_registry().attention_kernels[implementation].dtypes
|
| 228 |
+
if runtime_dtype_name not in supported:
|
| 229 |
+
expected = ", ".join(supported)
|
| 230 |
+
raise RuntimeError(
|
| 231 |
+
f"{implementation!r} supports only manifest-declared dtype(s) {expected}; "
|
| 232 |
+
f"received {runtime_dtype_name}. Use CUDA BF16 autocast for FP32-resident "
|
| 233 |
+
"models."
|
| 234 |
+
)
|
| 235 |
+
return runtime_dtype
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def _validate_kernels_flash_device(
|
| 239 |
+
query_states: torch.Tensor,
|
| 240 |
+
key_states: torch.Tensor,
|
| 241 |
+
value_states: torch.Tensor,
|
| 242 |
+
implementation: str,
|
| 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)
|
| 249 |
+
raise RuntimeError(
|
| 250 |
+
f"{implementation!r} requires Q, K, and V on one device; received {observed}."
|
| 251 |
+
)
|
| 252 |
+
device = devices[0]
|
| 253 |
+
if device.type != "cuda" or not all(
|
| 254 |
+
tensor.is_cuda for tensor in (query_states, key_states, value_states)
|
| 255 |
+
):
|
| 256 |
+
raise RuntimeError(
|
| 257 |
+
f"{implementation!r} requires CUDA Q, K, and V; received device {device}."
|
| 258 |
+
)
|
| 259 |
+
return device
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def _ensure_flash_kernels_loaded(implementation: str) -> tuple[object, str]:
|
| 263 |
+
cached = _FLASH_KERNELS.get(implementation)
|
| 264 |
+
if cached is not None:
|
| 265 |
+
return cached
|
| 266 |
+
loaded = _load_kernels_flash(implementation)
|
| 267 |
+
_FLASH_KERNELS[implementation] = loaded
|
| 268 |
+
return loaded
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def _kernels_flash_forward(
|
| 272 |
+
query_states: torch.Tensor,
|
| 273 |
+
key_states: torch.Tensor,
|
| 274 |
+
value_states: torch.Tensor,
|
| 275 |
+
causal: bool = False,
|
| 276 |
+
softmax_scale: float | None = None,
|
| 277 |
+
implementation: str = "flash_attention_3",
|
| 278 |
+
) -> torch.Tensor:
|
| 279 |
+
"""Flash-attention forward, optionally overriding the softmax scale.
|
| 280 |
+
|
| 281 |
+
When `softmax_scale is None`, the flash kernel applies its default
|
| 282 |
+
`1 / sqrt(head_dim)`. Pass `softmax_scale=1.0` if the caller has already
|
| 283 |
+
pre-scaled Q (the convention used by ESM2, DPLM, DPLM2, E1, ESMFold).
|
| 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,
|
| 293 |
+
dropout_p=0.0,
|
| 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,
|
| 303 |
+
softmax_scale=softmax_scale,
|
| 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 |
+
|
| 312 |
+
def _kernels_flash_varlen_forward(
|
| 313 |
+
query_states: torch.Tensor,
|
| 314 |
+
key_states: torch.Tensor,
|
| 315 |
+
value_states: torch.Tensor,
|
| 316 |
+
cu_seqlens_q: torch.Tensor,
|
| 317 |
+
cu_seqlens_k: torch.Tensor,
|
| 318 |
+
max_seqlen_in_batch_q: int,
|
| 319 |
+
max_seqlen_in_batch_k: int,
|
| 320 |
+
causal: bool = False,
|
| 321 |
+
softmax_scale: float | None = None,
|
| 322 |
+
implementation: str = "flash_attention_3",
|
| 323 |
+
) -> torch.Tensor:
|
| 324 |
+
"""Varlen flash-attention forward, optionally overriding the softmax scale.
|
| 325 |
+
|
| 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,
|
| 335 |
+
cu_seqlens_q=cu_seqlens_q,
|
| 336 |
+
cu_seqlens_k=cu_seqlens_k,
|
| 337 |
+
max_seqlen_q=max_seqlen_in_batch_q,
|
| 338 |
+
max_seqlen_k=max_seqlen_in_batch_k,
|
| 339 |
+
dropout_p=0.0,
|
| 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,
|
| 349 |
+
cu_seqlens_q=cu_seqlens_q,
|
| 350 |
+
cu_seqlens_k=cu_seqlens_k,
|
| 351 |
+
max_seqlen_q=max_seqlen_in_batch_q,
|
| 352 |
+
max_seqlen_k=max_seqlen_in_batch_k,
|
| 353 |
+
softmax_scale=softmax_scale,
|
| 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 |
+
|
| 362 |
+
# Varlen flash attention runs only on real tokens. These helpers remove padding
|
| 363 |
+
# before the kernel call and restore the original padded batch shape afterward.
|
| 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(
|
| 370 |
+
"index_first_axis input must have at least two dimensions; "
|
| 371 |
+
f"received shape {tuple(input.shape)}."
|
| 372 |
+
)
|
| 373 |
+
if indices.ndim != 1:
|
| 374 |
+
raise ValueError(
|
| 375 |
+
"index_first_axis indices must be one-dimensional; "
|
| 376 |
+
f"received shape {tuple(indices.shape)}."
|
| 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(
|
| 389 |
+
"index_first_axis received an invalid gradient with fewer than "
|
| 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(
|
| 409 |
+
"index_put_first_axis indices must be one-dimensional; "
|
| 410 |
+
f"received shape {tuple(indices.shape)}."
|
| 411 |
+
)
|
| 412 |
+
if values.ndim < 2:
|
| 413 |
+
raise ValueError(
|
| 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
|
| 430 |
+
index_put_first_axis = IndexPutFirstAxis.apply
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
def pad_input(
|
| 434 |
+
hidden_states: torch.Tensor, indices: torch.Tensor, batch: int, seqlen: int
|
| 435 |
+
) -> torch.Tensor:
|
| 436 |
+
output = index_put_first_axis(hidden_states, indices, batch * seqlen)
|
| 437 |
+
return rearrange(output, "(b s) ... -> b s ...", b=batch)
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
def _unpad_input(
|
| 441 |
+
query_layer: torch.Tensor,
|
| 442 |
+
key_layer: torch.Tensor,
|
| 443 |
+
value_layer: torch.Tensor,
|
| 444 |
+
attention_mask_2d: torch.Tensor,
|
| 445 |
+
) -> tuple[
|
| 446 |
+
torch.Tensor,
|
| 447 |
+
torch.Tensor,
|
| 448 |
+
torch.Tensor,
|
| 449 |
+
torch.Tensor,
|
| 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 (
|
| 468 |
+
query_layer,
|
| 469 |
+
key_layer,
|
| 470 |
+
value_layer,
|
| 471 |
+
indices,
|
| 472 |
+
(cu_seqlens, cu_seqlens),
|
| 473 |
+
(max_seqlen, max_seqlen),
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def _validate_flash_padding_mask(
|
| 478 |
+
query_states: torch.Tensor,
|
| 479 |
+
key_states: torch.Tensor,
|
| 480 |
+
value_states: torch.Tensor,
|
| 481 |
+
attention_mask_2d: torch.Tensor,
|
| 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]
|
| 488 |
+
if tuple(attention_mask_2d.shape) != tuple(expected_shape):
|
| 489 |
+
raise ValueError(
|
| 490 |
+
"FlashAttention padding mask shape must match the query batch and "
|
| 491 |
+
f"sequence dimensions; expected {tuple(expected_shape)}, received "
|
| 492 |
+
f"{tuple(attention_mask_2d.shape)}."
|
| 493 |
+
)
|
| 494 |
+
if key_states.shape[:2] != expected_shape or value_states.shape[:2] != expected_shape:
|
| 495 |
+
raise ValueError(
|
| 496 |
+
"Masked FlashAttention requires Q, K, and V to share batch and sequence dimensions."
|
| 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(
|
| 504 |
+
query_states: torch.Tensor,
|
| 505 |
+
key_states: torch.Tensor,
|
| 506 |
+
value_states: torch.Tensor,
|
| 507 |
+
attention_mask_2d: torch.Tensor | None = None,
|
| 508 |
+
causal: bool = False,
|
| 509 |
+
softmax_scale: float | None = None,
|
| 510 |
+
implementation: str = "flash_attention_3",
|
| 511 |
+
) -> torch.Tensor:
|
| 512 |
+
"""Public flash-attention entry point with optional padding handling.
|
| 513 |
+
|
| 514 |
+
`softmax_scale`:
|
| 515 |
+
None -> kernel applies its default `1 / sqrt(head_dim)`.
|
| 516 |
+
float -> kernel uses the given scale (pass 1.0 when Q is pre-scaled
|
| 517 |
+
by the caller).
|
| 518 |
+
|
| 519 |
+
Caller contract: if a model family pre-scales Q by `1/sqrt(head_dim)`
|
| 520 |
+
before calling this function (ESM2, DPLM, DPLM2, E1, and ESMFold do), pass
|
| 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,
|
| 527 |
+
value_states,
|
| 528 |
+
implementation,
|
| 529 |
+
)
|
| 530 |
+
runtime_dtype = _validate_kernels_flash_dtype(
|
| 531 |
+
query_states,
|
| 532 |
+
key_states,
|
| 533 |
+
value_states,
|
| 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,
|
| 545 |
+
attention_mask_2d,
|
| 546 |
+
)
|
| 547 |
+
_ensure_flash_kernels_loaded(implementation)
|
| 548 |
+
if attention_mask_2d is not None:
|
| 549 |
+
batch_size, q_len = query_states.shape[:2]
|
| 550 |
+
(
|
| 551 |
+
query_states,
|
| 552 |
+
key_states,
|
| 553 |
+
value_states,
|
| 554 |
+
indices_q,
|
| 555 |
+
(cu_seqlens_q, cu_seqlens_k),
|
| 556 |
+
(max_seqlen_q, max_seqlen_k),
|
| 557 |
+
) = _unpad_input(query_states, key_states, value_states, attention_mask_2d)
|
| 558 |
+
attn_output_unpad = _kernels_flash_varlen_forward(
|
| 559 |
+
query_states=query_states,
|
| 560 |
+
key_states=key_states,
|
| 561 |
+
value_states=value_states,
|
| 562 |
+
cu_seqlens_q=cu_seqlens_q,
|
| 563 |
+
cu_seqlens_k=cu_seqlens_k,
|
| 564 |
+
max_seqlen_in_batch_q=max_seqlen_q,
|
| 565 |
+
max_seqlen_in_batch_k=max_seqlen_k,
|
| 566 |
+
causal=causal,
|
| 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,
|
| 577 |
+
causal=causal,
|
| 578 |
+
softmax_scale=softmax_scale,
|
| 579 |
+
implementation=implementation,
|
| 580 |
+
)
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
# User-facing backend strings follow the Transformers attention interface.
|
| 584 |
+
# Keep ``str`` plus ``Enum`` so stringification stays compatible with existing
|
| 585 |
+
# configuration serialization rather than adopting ``StrEnum.__str__``.
|
| 586 |
+
class AttentionBackend(str, Enum): # noqa: UP042
|
| 587 |
+
EAGER = "eager"
|
| 588 |
+
SDPA = "sdpa"
|
| 589 |
+
FLEX_ATTENTION = "flex_attention"
|
| 590 |
+
FLASH_ATTENTION_2 = "flash_attention_2"
|
| 591 |
+
FLASH_ATTENTION_3 = "flash_attention_3"
|
| 592 |
+
|
| 593 |
+
# Internal spelling retained to keep attention modules concise. It is an
|
| 594 |
+
# enum alias, not an accepted public backend string.
|
| 595 |
+
FLEX = FLEX_ATTENTION
|
| 596 |
+
|
| 597 |
+
@property
|
| 598 |
+
def is_flash(self) -> bool:
|
| 599 |
+
return self in {
|
| 600 |
+
AttentionBackend.FLASH_ATTENTION_2,
|
| 601 |
+
AttentionBackend.FLASH_ATTENTION_3,
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
VALID_ATTENTION_BACKENDS = tuple(b.value for b in AttentionBackend)
|
| 606 |
+
|
| 607 |
+
|
| 608 |
+
def warn_attention_backend_fallback(
|
| 609 |
+
requested_backend: str | AttentionBackend,
|
| 610 |
+
*,
|
| 611 |
+
effective_backend: str | AttentionBackend,
|
| 612 |
+
reason: str,
|
| 613 |
+
) -> None:
|
| 614 |
+
"""Warn when one forward call cannot honor the configured backend."""
|
| 615 |
+
|
| 616 |
+
requested = resolve_attention_backend(requested_backend).value
|
| 617 |
+
effective = resolve_attention_backend(effective_backend).value
|
| 618 |
+
if requested == effective:
|
| 619 |
+
return
|
| 620 |
+
warnings.warn(
|
| 621 |
+
f"{reason} The requested {requested!r} attention implementation cannot "
|
| 622 |
+
f"satisfy this call, so FastPLMs is using {effective!r} attention for this "
|
| 623 |
+
"call only. This can change performance and memory use; the configured "
|
| 624 |
+
"backend remains unchanged for subsequent calls.",
|
| 625 |
+
RuntimeWarning,
|
| 626 |
+
stacklevel=3,
|
| 627 |
+
)
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
def resolve_attention_backend_for_call(
|
| 631 |
+
requested_backend: str | AttentionBackend,
|
| 632 |
+
*,
|
| 633 |
+
output_attentions: bool,
|
| 634 |
+
) -> AttentionBackend:
|
| 635 |
+
"""Resolve the effective backend for one call and report substitutions once."""
|
| 636 |
+
|
| 637 |
+
requested = resolve_attention_backend(requested_backend)
|
| 638 |
+
if not output_attentions or requested == AttentionBackend.EAGER:
|
| 639 |
+
return requested
|
| 640 |
+
warn_attention_backend_fallback(
|
| 641 |
+
requested,
|
| 642 |
+
effective_backend=AttentionBackend.EAGER,
|
| 643 |
+
reason=(
|
| 644 |
+
"output_attentions=True requires the full materialized attention probability "
|
| 645 |
+
"matrix, which optimized PyTorch attention APIs do not return."
|
| 646 |
+
),
|
| 647 |
+
)
|
| 648 |
+
return AttentionBackend.EAGER
|
| 649 |
+
|
| 650 |
+
|
| 651 |
+
def resolve_attention_backend(
|
| 652 |
+
requested_backend: str | AttentionBackend | None,
|
| 653 |
+
) -> AttentionBackend:
|
| 654 |
+
"""Validate a backend without silently substituting another implementation."""
|
| 655 |
+
if requested_backend is None:
|
| 656 |
+
requested_backend = AttentionBackend.SDPA.value
|
| 657 |
+
if isinstance(requested_backend, AttentionBackend):
|
| 658 |
+
resolved = requested_backend
|
| 659 |
+
else:
|
| 660 |
+
try:
|
| 661 |
+
resolved = AttentionBackend(requested_backend)
|
| 662 |
+
except ValueError as error:
|
| 663 |
+
raise ValueError(
|
| 664 |
+
f"Unsupported attention implementation {requested_backend!r}; "
|
| 665 |
+
f"expected one of {VALID_ATTENTION_BACKENDS}."
|
| 666 |
+
) from error
|
| 667 |
+
if resolved == AttentionBackend.FLEX_ATTENTION and flex_attention is None:
|
| 668 |
+
raise RuntimeError(
|
| 669 |
+
"'flex_attention' was requested, but this PyTorch build does not provide it."
|
| 670 |
+
)
|
| 671 |
+
return resolved
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
def get_attn_implementation(config) -> str:
|
| 675 |
+
"""Read the Transformers attention setting, defaulting to SDPA."""
|
| 676 |
+
requested = getattr(config, "_attn_implementation", None)
|
| 677 |
+
if requested is None:
|
| 678 |
+
requested = getattr(config, "attn_backend", None)
|
| 679 |
+
return resolve_attention_backend(requested).value
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
def set_config_attn_implementation(config, implementation: str) -> str:
|
| 683 |
+
"""Set both the Transformers field and the internal dispatch field."""
|
| 684 |
+
resolved = resolve_attention_backend(implementation).value
|
| 685 |
+
if hasattr(config, "_attn_implementation_internal"):
|
| 686 |
+
config._attn_implementation_internal = resolved
|
| 687 |
+
else:
|
| 688 |
+
config._attn_implementation = resolved
|
| 689 |
+
# Existing checkpoint configs contain this field. Keeping it synchronized
|
| 690 |
+
# preserves their state schema while the public API uses attn_implementation.
|
| 691 |
+
config.attn_backend = resolved
|
| 692 |
+
return resolved
|
| 693 |
+
|
| 694 |
+
|
| 695 |
+
@torch.compiler.disable
|
| 696 |
+
def get_attention_mask(
|
| 697 |
+
effective_backend: AttentionBackend,
|
| 698 |
+
batch_size: int,
|
| 699 |
+
seq_len: int,
|
| 700 |
+
device: torch.device,
|
| 701 |
+
attention_mask: torch.Tensor | None = None,
|
| 702 |
+
dtype: torch.dtype | None = None,
|
| 703 |
+
mask_semantics: str = "padding",
|
| 704 |
+
) -> tuple[torch.Tensor | None, torch.Tensor | None, BlockMask | None]:
|
| 705 |
+
"""Build padding masks once for all encoder layers.
|
| 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 |
+
|
| 712 |
+
if attention_mask.ndim != 2:
|
| 713 |
+
raise ValueError(
|
| 714 |
+
"attention_mask must have shape (batch, sequence_length); "
|
| 715 |
+
f"received rank {attention_mask.ndim} with shape {tuple(attention_mask.shape)}."
|
| 716 |
+
)
|
| 717 |
+
expected_shape = (batch_size, seq_len)
|
| 718 |
+
if tuple(attention_mask.shape) != expected_shape:
|
| 719 |
+
raise ValueError(
|
| 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:
|
| 734 |
+
raise RuntimeError(
|
| 735 |
+
"'flex_attention' was requested, but torch.create_block_mask is unavailable."
|
| 736 |
+
)
|
| 737 |
+
def mask_mod(batch_idx, head_idx, q_idx, kv_idx):
|
| 738 |
+
del head_idx, q_idx
|
| 739 |
+
# Match eager and SDPA: padding masks suppress invalid keys only.
|
| 740 |
+
# Invalid queries still attend to real keys and therefore remain
|
| 741 |
+
# finite; downstream residue masks exclude their outputs.
|
| 742 |
+
return attention_mask_2d[batch_idx, kv_idx]
|
| 743 |
+
|
| 744 |
+
flex_block_mask = _get_flex_block_mask(
|
| 745 |
+
mask_pattern=attention_mask_2d,
|
| 746 |
+
batch_size=batch_size,
|
| 747 |
+
query_length=seq_len,
|
| 748 |
+
key_value_length=seq_len,
|
| 749 |
+
device=device,
|
| 750 |
+
dtype=dtype,
|
| 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(
|
| 763 |
+
bool_mask: torch.Tensor,
|
| 764 |
+
dtype: torch.dtype,
|
| 765 |
+
) -> torch.Tensor:
|
| 766 |
+
"""Convert a bool mask (True = valid) to a float additive mask (0.0 valid, -inf invalid).
|
| 767 |
+
|
| 768 |
+
Why this exists: calling `bool_mask.masked_fill(bool_mask.logical_not(), float('-inf'))`
|
| 769 |
+
directly on a bool tensor returns a bool tensor because `-inf` casts to `True`.
|
| 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
|
fastplms/attention/_kernel_lock.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Resolve and validate Hugging Face kernels before importing their binaries."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import importlib.metadata
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def require_kernels_package() -> None:
|
| 13 |
+
"""Fail early when the precompiled-kernel runtime is not installed."""
|
| 14 |
+
try:
|
| 15 |
+
import kernels # noqa: F401
|
| 16 |
+
except ImportError as error:
|
| 17 |
+
raise RuntimeError(
|
| 18 |
+
"Precompiled FlashAttention requires the FastPLMs 'flash' extra."
|
| 19 |
+
) from error
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _kernel_lock_path() -> Path:
|
| 23 |
+
"""Return the lock from an artifact, checkout, or installed distribution."""
|
| 24 |
+
source_path = Path(__file__).resolve()
|
| 25 |
+
candidates = [
|
| 26 |
+
source_path.parents[1] / "kernels.lock",
|
| 27 |
+
source_path.parents[3] / "kernels.lock",
|
| 28 |
+
]
|
| 29 |
+
try:
|
| 30 |
+
import fastplms
|
| 31 |
+
|
| 32 |
+
candidates.extend(Path(root) / "kernels.lock" for root in fastplms.__path__)
|
| 33 |
+
except (ImportError, AttributeError):
|
| 34 |
+
pass
|
| 35 |
+
for candidate in candidates:
|
| 36 |
+
if candidate.is_file():
|
| 37 |
+
return candidate
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
distribution = importlib.metadata.distribution("fastplms")
|
| 41 |
+
except importlib.metadata.PackageNotFoundError as error:
|
| 42 |
+
raise RuntimeError("FastPLMs was installed without kernels.lock.") from error
|
| 43 |
+
for relative in distribution.files or ():
|
| 44 |
+
if relative.name != "kernels.lock":
|
| 45 |
+
continue
|
| 46 |
+
candidate = Path(distribution.locate_file(relative))
|
| 47 |
+
if candidate.is_file():
|
| 48 |
+
return candidate
|
| 49 |
+
raise RuntimeError("The installed FastPLMs distribution does not contain kernels.lock.")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _locked_entry(lock_path: Path, repository: str) -> dict[str, Any]:
|
| 53 |
+
try:
|
| 54 |
+
data = json.loads(lock_path.read_text(encoding="utf-8"))
|
| 55 |
+
except (OSError, json.JSONDecodeError) as error:
|
| 56 |
+
raise RuntimeError(f"Unable to read the packaged kernel lock: {lock_path}") from error
|
| 57 |
+
if not isinstance(data, list):
|
| 58 |
+
raise RuntimeError("kernels.lock must contain a JSON list.")
|
| 59 |
+
if any(not isinstance(entry, dict) for entry in data):
|
| 60 |
+
raise RuntimeError("Every kernels.lock entry must be a JSON object.")
|
| 61 |
+
matches = [entry for entry in data if entry.get("repo_id") == repository]
|
| 62 |
+
if len(matches) != 1:
|
| 63 |
+
raise RuntimeError(
|
| 64 |
+
f"kernels.lock must contain exactly one entry for {repository!r}; found {len(matches)}."
|
| 65 |
+
)
|
| 66 |
+
return matches[0]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _offline_mode() -> bool:
|
| 70 |
+
"""Return whether Hub access was explicitly disabled for this process."""
|
| 71 |
+
|
| 72 |
+
enabled_values = {"1", "on", "true", "yes"}
|
| 73 |
+
return any(
|
| 74 |
+
os.environ.get(name, "").strip().lower() in enabled_values
|
| 75 |
+
for name in ("HF_HUB_OFFLINE", "TRANSFORMERS_OFFLINE")
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _offline_snapshot_path(repository: str, revision: str) -> Path:
|
| 80 |
+
"""Locate one exact, possibly sparse, kernel snapshot without using Hub APIs."""
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
from huggingface_hub import constants
|
| 84 |
+
from huggingface_hub.file_download import repo_folder_name
|
| 85 |
+
except ImportError as error:
|
| 86 |
+
raise RuntimeError("Offline kernel loading requires huggingface-hub.") from error
|
| 87 |
+
|
| 88 |
+
cache_root = Path(os.environ.get("KERNELS_CACHE") or constants.HF_HUB_CACHE).resolve()
|
| 89 |
+
repository_root = (
|
| 90 |
+
cache_root / repo_folder_name(repo_id=repository, repo_type="kernel")
|
| 91 |
+
).resolve()
|
| 92 |
+
snapshot = repository_root / "snapshots" / revision
|
| 93 |
+
if not snapshot.is_dir():
|
| 94 |
+
raise RuntimeError(
|
| 95 |
+
f"The exact offline kernel snapshot {repository}@{revision} is not cached under "
|
| 96 |
+
f"{cache_root}. Run `kernels download` before enabling offline mode."
|
| 97 |
+
)
|
| 98 |
+
if repository_root not in snapshot.resolve().parents:
|
| 99 |
+
raise RuntimeError(f"Refusing kernel snapshot outside its cache repository: {snapshot}")
|
| 100 |
+
return snapshot
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _load_offline_locked_kernel(
|
| 104 |
+
repository: str,
|
| 105 |
+
revision: str,
|
| 106 |
+
variant_locks: dict[str, object],
|
| 107 |
+
) -> object:
|
| 108 |
+
"""Validate and import the one compatible variant from a sparse Hub snapshot."""
|
| 109 |
+
snapshot = _offline_snapshot_path(repository, revision)
|
| 110 |
+
build_root = snapshot / "build"
|
| 111 |
+
if not build_root.is_dir():
|
| 112 |
+
raise RuntimeError(f"The cached kernel snapshot has no build directory: {snapshot}")
|
| 113 |
+
|
| 114 |
+
cached_names = sorted(entry.name for entry in build_root.iterdir() if entry.is_dir())
|
| 115 |
+
unexpected = sorted(set(cached_names).difference(variant_locks))
|
| 116 |
+
if unexpected:
|
| 117 |
+
raise RuntimeError(
|
| 118 |
+
f"The cached {repository}@{revision} snapshot contains unlocked variants: "
|
| 119 |
+
f"{', '.join(unexpected)}"
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
from kernels import get_local_kernel
|
| 124 |
+
from kernels.utils import validate_kernel
|
| 125 |
+
from kernels.variants import get_variants_local, resolve_variants
|
| 126 |
+
except ImportError as error:
|
| 127 |
+
raise RuntimeError(
|
| 128 |
+
"Precompiled FlashAttention requires the FastPLMs 'flash' extra."
|
| 129 |
+
) from error
|
| 130 |
+
|
| 131 |
+
parsed = get_variants_local(build_root)
|
| 132 |
+
parsed_names = {variant.variant_str for variant in parsed}
|
| 133 |
+
invalid = sorted(set(cached_names).difference(parsed_names))
|
| 134 |
+
if invalid:
|
| 135 |
+
raise RuntimeError(
|
| 136 |
+
f"The cached {repository}@{revision} snapshot contains invalid variants: "
|
| 137 |
+
f"{', '.join(invalid)}"
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
compatible, _ = resolve_variants(parsed)
|
| 141 |
+
if len(compatible) != 1:
|
| 142 |
+
names = ", ".join(variant.variant_str for variant in compatible) or "none"
|
| 143 |
+
raise RuntimeError(
|
| 144 |
+
f"Expected exactly one compatible cached variant for {repository}@{revision}; "
|
| 145 |
+
f"found {names}."
|
| 146 |
+
)
|
| 147 |
+
variant_name = compatible[0].variant_str
|
| 148 |
+
variant_lock = variant_locks.get(variant_name)
|
| 149 |
+
expected_hash = getattr(variant_lock, "hash", None)
|
| 150 |
+
if not isinstance(expected_hash, str) or not expected_hash.startswith("sha256-"):
|
| 151 |
+
raise RuntimeError(f"The kernel lock for {variant_name} has no valid SHA-256 digest.")
|
| 152 |
+
|
| 153 |
+
# Hash validation deliberately happens before import. This operates on the
|
| 154 |
+
# sparse snapshot produced by `kernels download` and avoids Hub 1.23's
|
| 155 |
+
# full-snapshot completeness check in offline mode.
|
| 156 |
+
validate_kernel(repo_path=snapshot, variant=variant_name, hash=expected_hash)
|
| 157 |
+
return get_local_kernel(build_root / variant_name)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def load_locked_kernel(repository: str, revision: str) -> object:
|
| 161 |
+
"""Download, hash-validate, then import one immutable precompiled kernel."""
|
| 162 |
+
require_kernels_package()
|
| 163 |
+
try:
|
| 164 |
+
from kernels import get_local_kernel, install_kernel
|
| 165 |
+
from kernels.lockfile import KernelLock
|
| 166 |
+
except ImportError as error:
|
| 167 |
+
raise RuntimeError(
|
| 168 |
+
"Precompiled FlashAttention requires the FastPLMs 'flash' extra."
|
| 169 |
+
) from error
|
| 170 |
+
|
| 171 |
+
lock_path = _kernel_lock_path()
|
| 172 |
+
kernel_lock = KernelLock.from_json(_locked_entry(lock_path, repository))
|
| 173 |
+
if kernel_lock.sha != revision:
|
| 174 |
+
raise RuntimeError(
|
| 175 |
+
f"The typed manifest pins {repository}@{revision}, but kernels.lock pins "
|
| 176 |
+
f"{kernel_lock.sha}."
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
if _offline_mode():
|
| 180 |
+
return _load_offline_locked_kernel(repository, revision, kernel_lock.variants)
|
| 181 |
+
|
| 182 |
+
# `install_kernel` downloads data without importing it and validates the
|
| 183 |
+
# selected build against the tracked variant hash. Only then is the exact
|
| 184 |
+
# validated path imported directly. Offline mode uses the sparse-cache
|
| 185 |
+
# resolver above because Hub 1.23 rejects partial snapshots as incomplete.
|
| 186 |
+
validated_path = install_kernel(
|
| 187 |
+
repository,
|
| 188 |
+
revision=kernel_lock.sha,
|
| 189 |
+
variant_locks=kernel_lock.variants,
|
| 190 |
+
)
|
| 191 |
+
return get_local_kernel(validated_path)
|
fastplms/attention/interfaces.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Transformers-compatible attention selection for FastPLMs models."""
|
| 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 (
|
| 13 |
+
AttentionBackend,
|
| 14 |
+
get_attn_implementation,
|
| 15 |
+
kernels_flash_attention_func,
|
| 16 |
+
resolve_attention_backend,
|
| 17 |
+
set_config_attn_implementation,
|
| 18 |
+
)
|
| 19 |
+
from ._kernel_lock import require_kernels_package
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _kernels_attention_forward(
|
| 23 |
+
module: torch.nn.Module,
|
| 24 |
+
query: torch.Tensor,
|
| 25 |
+
key: torch.Tensor,
|
| 26 |
+
value: torch.Tensor,
|
| 27 |
+
attention_mask: torch.Tensor | None,
|
| 28 |
+
*,
|
| 29 |
+
implementation: str,
|
| 30 |
+
**kwargs: Any,
|
| 31 |
+
) -> tuple[torch.Tensor, None]:
|
| 32 |
+
"""Run one canonical FlashAttention backend through Hugging Face kernels.
|
| 33 |
+
|
| 34 |
+
Transformers attention functions receive Q, K, and V with shape
|
| 35 |
+
(b, h, l, d) and return an output with shape (b, l, h, d). The shared
|
| 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(
|
| 42 |
+
"Hugging Face kernels FlashAttention is inference-only when attention dropout "
|
| 43 |
+
"is nonzero. Use SDPA for this training configuration."
|
| 44 |
+
)
|
| 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.
|
| 60 |
+
# ``GeneralInterface.register`` updates Transformers' class-wide mapping, so
|
| 61 |
+
# using it here would replace the canonical FlashAttention handlers for every
|
| 62 |
+
# model in the process, including models unrelated to FastPLMs.
|
| 63 |
+
FASTPLMS_ATTENTION_FUNCTIONS = AttentionInterface()
|
| 64 |
+
FASTPLMS_ATTENTION_MASKS = AttentionMaskInterface()
|
| 65 |
+
FASTPLMS_ATTENTION_FUNCTIONS["flash_attention_2"] = partial(
|
| 66 |
+
_kernels_attention_forward,
|
| 67 |
+
implementation="flash_attention_2",
|
| 68 |
+
)
|
| 69 |
+
FASTPLMS_ATTENTION_FUNCTIONS["flash_attention_3"] = partial(
|
| 70 |
+
_kernels_attention_forward,
|
| 71 |
+
implementation="flash_attention_3",
|
| 72 |
+
)
|
| 73 |
+
for _flash_name in ("flash_attention_2", "flash_attention_3"):
|
| 74 |
+
FASTPLMS_ATTENTION_MASKS[_flash_name] = FASTPLMS_ATTENTION_MASKS[_flash_name]
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class FastPLMsAttentionMixin:
|
| 78 |
+
"""Synchronize Transformers attention selection with custom model layers.
|
| 79 |
+
|
| 80 |
+
Model families retain their checkpoint parameter names. Only runtime
|
| 81 |
+
attributes are updated when ``set_attn_implementation`` is called.
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
_supports_sdpa = True
|
| 85 |
+
_supports_flex_attn = True
|
| 86 |
+
# Transformers 5.13 uses the singular flag during model construction. A
|
| 87 |
+
# family opts in only when its manifest entry advertises at least one of
|
| 88 |
+
# the two FastPLMs kernels-only FlashAttention implementations.
|
| 89 |
+
_supports_flash_attn = False
|
| 90 |
+
_supports_flash_attn_2 = False
|
| 91 |
+
_supports_flash_attn_3 = False
|
| 92 |
+
_fastplms_attention_implementations = (
|
| 93 |
+
"eager",
|
| 94 |
+
"sdpa",
|
| 95 |
+
"flex_attention",
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
def _validate_attention_name(self, implementation: str) -> None:
|
| 99 |
+
if implementation not in self._fastplms_attention_implementations:
|
| 100 |
+
raise ValueError(
|
| 101 |
+
f"{type(self).__name__} does not support {implementation!r}; expected one of "
|
| 102 |
+
f"{self._fastplms_attention_implementations}."
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
def _check_and_adjust_attn_implementation(
|
| 106 |
+
self,
|
| 107 |
+
attn_implementation: str | None,
|
| 108 |
+
is_init_check: bool = False,
|
| 109 |
+
allow_all_kernels: bool = False,
|
| 110 |
+
) -> str:
|
| 111 |
+
"""Resolve attention without invoking Transformers' source-Flash probe.
|
| 112 |
+
|
| 113 |
+
The standard ``flash_attention_2`` and ``flash_attention_3`` names are
|
| 114 |
+
retained for the Transformers API, but FastPLMs resolves them only
|
| 115 |
+
through the exact Hugging Face ``kernels`` artifacts pinned by
|
| 116 |
+
``models.toml``. Repository-qualified or otherwise external kernels
|
| 117 |
+
are never accepted through this model hook.
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
if allow_all_kernels:
|
| 121 |
+
raise ValueError("FastPLMs does not load external attention kernels.")
|
| 122 |
+
if attn_implementation is None:
|
| 123 |
+
return super()._check_and_adjust_attn_implementation(
|
| 124 |
+
None,
|
| 125 |
+
is_init_check=is_init_check,
|
| 126 |
+
allow_all_kernels=False,
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
self._validate_attention_name(attn_implementation)
|
| 130 |
+
if attn_implementation in {"flash_attention_2", "flash_attention_3"}:
|
| 131 |
+
if not self._supports_flash_attn:
|
| 132 |
+
raise ValueError(
|
| 133 |
+
f"{type(self).__name__} does not advertise kernels-only FlashAttention."
|
| 134 |
+
)
|
| 135 |
+
# Validate the lightweight Python dependency here, but defer binary
|
| 136 |
+
# download and import until Q, K, and V have passed the CUDA gate.
|
| 137 |
+
require_kernels_package()
|
| 138 |
+
return attn_implementation
|
| 139 |
+
|
| 140 |
+
return super()._check_and_adjust_attn_implementation(
|
| 141 |
+
attn_implementation,
|
| 142 |
+
is_init_check=is_init_check,
|
| 143 |
+
allow_all_kernels=False,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
def __init__(self, config, *args: Any, **kwargs: Any) -> None:
|
| 147 |
+
sentinel = object()
|
| 148 |
+
internal = getattr(config, "_attn_implementation_internal", sentinel)
|
| 149 |
+
canonical = (
|
| 150 |
+
getattr(config, "_attn_implementation", None) if internal is sentinel else internal
|
| 151 |
+
)
|
| 152 |
+
legacy = getattr(config, "attn_backend", None)
|
| 153 |
+
requested = canonical if canonical is not None else legacy
|
| 154 |
+
if requested is not None:
|
| 155 |
+
if not isinstance(requested, str):
|
| 156 |
+
raise TypeError(
|
| 157 |
+
"The configured attention implementation must be a string or None; "
|
| 158 |
+
f"received {type(requested).__name__}."
|
| 159 |
+
)
|
| 160 |
+
self._validate_attention_name(requested)
|
| 161 |
+
# ``PreTrainedModel.__init__`` resolves a missing Transformers
|
| 162 |
+
# implementation to the family default. Legacy FastPLMs configs
|
| 163 |
+
# persist their explicit choice in ``attn_backend``, so forward it
|
| 164 |
+
# into the canonical Transformers field before the base class can
|
| 165 |
+
# replace it with SDPA. A non-None canonical value still wins,
|
| 166 |
+
# including an explicit ``attn_implementation=...`` load override.
|
| 167 |
+
if canonical is None and legacy is not None:
|
| 168 |
+
set_config_attn_implementation(config, legacy)
|
| 169 |
+
super().__init__(config, *args, **kwargs)
|
| 170 |
+
# Transformers resolves an unspecified implementation during the base
|
| 171 |
+
# model initialization. Synchronize that choice before family layers
|
| 172 |
+
# are constructed.
|
| 173 |
+
resolved = get_attn_implementation(config)
|
| 174 |
+
self._validate_attention_name(resolved)
|
| 175 |
+
set_config_attn_implementation(config, resolved)
|
| 176 |
+
|
| 177 |
+
def set_attn_implementation(
|
| 178 |
+
self,
|
| 179 |
+
attn_implementation: str | Mapping[str, str],
|
| 180 |
+
allow_all_kernels: bool = False,
|
| 181 |
+
) -> None:
|
| 182 |
+
"""Select an advertised backend and update every instantiated layer."""
|
| 183 |
+
if isinstance(attn_implementation, Mapping):
|
| 184 |
+
if set(attn_implementation) == {""}:
|
| 185 |
+
attn_implementation = attn_implementation[""]
|
| 186 |
+
else:
|
| 187 |
+
raise ValueError(
|
| 188 |
+
"FastPLMs models have one attention backbone; pass a string or {'': name}."
|
| 189 |
+
)
|
| 190 |
+
resolved_name = self._check_and_adjust_attn_implementation(
|
| 191 |
+
attn_implementation,
|
| 192 |
+
is_init_check=False,
|
| 193 |
+
allow_all_kernels=allow_all_kernels,
|
| 194 |
+
)
|
| 195 |
+
set_config_attn_implementation(self.config, resolved_name)
|
| 196 |
+
resolved = resolve_attention_backend(resolved_name)
|
| 197 |
+
for module in self.modules():
|
| 198 |
+
if module is self:
|
| 199 |
+
continue
|
| 200 |
+
for attribute in ("attn_backend", "attention_backend", "_attn_backend"):
|
| 201 |
+
if attribute not in module.__dict__:
|
| 202 |
+
continue
|
| 203 |
+
current = module.__dict__[attribute]
|
| 204 |
+
module.__dict__[attribute] = (
|
| 205 |
+
resolved if isinstance(current, AttentionBackend) else resolved_name
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def validate_transformers_attention_interfaces() -> None:
|
| 210 |
+
"""Verify that Transformers exposes functions and masks for every backend.
|
| 211 |
+
|
| 212 |
+
Transformers 5.13 registers these canonical names. The FastPLMs function
|
| 213 |
+
overrides remain instance-local and do not replace process-global handlers.
|
| 214 |
+
"""
|
| 215 |
+
function_registry = FASTPLMS_ATTENTION_FUNCTIONS
|
| 216 |
+
mask_registry = FASTPLMS_ATTENTION_MASKS
|
| 217 |
+
missing_functions = [
|
| 218 |
+
name
|
| 219 |
+
for name in (
|
| 220 |
+
"sdpa",
|
| 221 |
+
"flex_attention",
|
| 222 |
+
"flash_attention_2",
|
| 223 |
+
"flash_attention_3",
|
| 224 |
+
)
|
| 225 |
+
if name not in function_registry
|
| 226 |
+
]
|
| 227 |
+
missing_masks = [
|
| 228 |
+
name
|
| 229 |
+
for name in (
|
| 230 |
+
"eager",
|
| 231 |
+
"sdpa",
|
| 232 |
+
"flex_attention",
|
| 233 |
+
"flash_attention_2",
|
| 234 |
+
"flash_attention_3",
|
| 235 |
+
)
|
| 236 |
+
if name not in mask_registry
|
| 237 |
+
]
|
| 238 |
+
if missing_functions or missing_masks:
|
| 239 |
+
raise RuntimeError(
|
| 240 |
+
"Transformers attention registry is incomplete: "
|
| 241 |
+
f"functions={missing_functions}, masks={missing_masks}."
|
| 242 |
+
)
|
fastplms/embeddings/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ordered, residue-aware protein embedding utilities."""
|
| 2 |
+
|
| 3 |
+
from .pooling import POOLING_NAMES, Pooler, pagerank_weights
|
| 4 |
+
from .runner import (
|
| 5 |
+
EmbeddingMixin,
|
| 6 |
+
embed_dataset,
|
| 7 |
+
iter_fasta,
|
| 8 |
+
parse_fasta,
|
| 9 |
+
select_hidden_state_embeddings,
|
| 10 |
+
)
|
| 11 |
+
from .storage import (
|
| 12 |
+
DEFAULT_SHARD_SIZE,
|
| 13 |
+
append_sqlite_records,
|
| 14 |
+
convert_legacy_sqlite,
|
| 15 |
+
garbage_collect_safetensors_generations,
|
| 16 |
+
initialize_sqlite_run,
|
| 17 |
+
load_legacy_pth,
|
| 18 |
+
load_result,
|
| 19 |
+
load_safetensors_result,
|
| 20 |
+
load_sqlite_result,
|
| 21 |
+
save_result,
|
| 22 |
+
save_safetensors_result,
|
| 23 |
+
save_sqlite_result,
|
| 24 |
+
tensor_sha256,
|
| 25 |
+
update_sqlite_run_metadata,
|
| 26 |
+
)
|
| 27 |
+
from .types import (
|
| 28 |
+
EmbeddingBatch,
|
| 29 |
+
EmbeddingInput,
|
| 30 |
+
EmbeddingRecord,
|
| 31 |
+
EmbeddingResult,
|
| 32 |
+
LazyTensorReference,
|
| 33 |
+
TensorValue,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
__all__ = [
|
| 37 |
+
"DEFAULT_SHARD_SIZE",
|
| 38 |
+
"POOLING_NAMES",
|
| 39 |
+
"EmbeddingBatch",
|
| 40 |
+
"EmbeddingInput",
|
| 41 |
+
"EmbeddingMixin",
|
| 42 |
+
"EmbeddingRecord",
|
| 43 |
+
"EmbeddingResult",
|
| 44 |
+
"LazyTensorReference",
|
| 45 |
+
"Pooler",
|
| 46 |
+
"TensorValue",
|
| 47 |
+
"append_sqlite_records",
|
| 48 |
+
"convert_legacy_sqlite",
|
| 49 |
+
"embed_dataset",
|
| 50 |
+
"garbage_collect_safetensors_generations",
|
| 51 |
+
"initialize_sqlite_run",
|
| 52 |
+
"iter_fasta",
|
| 53 |
+
"load_legacy_pth",
|
| 54 |
+
"load_result",
|
| 55 |
+
"load_safetensors_result",
|
| 56 |
+
"load_sqlite_result",
|
| 57 |
+
"pagerank_weights",
|
| 58 |
+
"parse_fasta",
|
| 59 |
+
"save_result",
|
| 60 |
+
"save_safetensors_result",
|
| 61 |
+
"save_sqlite_result",
|
| 62 |
+
"select_hidden_state_embeddings",
|
| 63 |
+
"tensor_sha256",
|
| 64 |
+
"update_sqlite_run_metadata",
|
| 65 |
+
]
|
fastplms/embeddings/pooling.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Residue-aware pooling implemented entirely with PyTorch."""
|
| 2 |
+
|
| 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:
|
| 18 |
+
raise ValueError(f"X must have shape (b, l, d), got {tuple(X.shape)}.")
|
| 19 |
+
if not X.is_floating_point():
|
| 20 |
+
raise TypeError("X must use a floating-point embedding dtype.")
|
| 21 |
+
if M.shape != X.shape[:2]:
|
| 22 |
+
raise ValueError(f"M must have shape (b, l)={tuple(X.shape[:2])}, got {tuple(M.shape)}.")
|
| 23 |
+
if M.is_complex():
|
| 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:
|
| 36 |
+
"""Max-pool layer/head attention A to shape ``(b, l, l)``.
|
| 37 |
+
|
| 38 |
+
``parti`` historically keeps the strongest directed edge across the
|
| 39 |
+
available attention maps before PageRank. Replacing NetworkX with Torch
|
| 40 |
+
must not change that reduction.
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
if isinstance(attentions, Sequence):
|
| 44 |
+
if not attentions:
|
| 45 |
+
raise ValueError("parti received an empty attention sequence.")
|
| 46 |
+
# Each A_i has shape (b, h, l, l).
|
| 47 |
+
A = torch.stack(tuple(attentions), dim=1)
|
| 48 |
+
else:
|
| 49 |
+
A = attentions
|
| 50 |
+
|
| 51 |
+
if A.ndim == 5:
|
| 52 |
+
if A.shape[0] != batch_size and A.shape[1] == batch_size:
|
| 53 |
+
A = A.transpose(0, 1)
|
| 54 |
+
if A.shape[0] != batch_size:
|
| 55 |
+
raise ValueError("Five-dimensional attentions must use (b, n, h, l, l).")
|
| 56 |
+
A = A.flatten(1, 2).amax(dim=1)
|
| 57 |
+
elif A.ndim == 4:
|
| 58 |
+
if A.shape[0] != batch_size:
|
| 59 |
+
raise ValueError("Four-dimensional attentions must use (b, h, l, l).")
|
| 60 |
+
A = A.amax(dim=1)
|
| 61 |
+
elif A.ndim == 3:
|
| 62 |
+
if A.shape[0] != batch_size:
|
| 63 |
+
raise ValueError("Three-dimensional attentions must use (b, l, l).")
|
| 64 |
+
else:
|
| 65 |
+
raise ValueError("Attentions must have shape (b, l, l), (b, h, l, l), or (b, n, h, l, l).")
|
| 66 |
+
return A
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def pagerank_weights(
|
| 70 |
+
A: Tensor,
|
| 71 |
+
*,
|
| 72 |
+
damping: float = 0.85,
|
| 73 |
+
tolerance: float = 1e-6,
|
| 74 |
+
max_iterations: int = 100,
|
| 75 |
+
) -> Tensor:
|
| 76 |
+
"""Compute PageRank weights for a non-negative attention matrix A.
|
| 77 |
+
|
| 78 |
+
A has shape ``(l, l)``. Rows are normalized into transition
|
| 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]:
|
| 85 |
+
raise ValueError(f"A must be square, got shape {tuple(A.shape)}.")
|
| 86 |
+
if not A.is_floating_point():
|
| 87 |
+
raise TypeError("A must use a floating-point attention dtype.")
|
| 88 |
+
if not isinstance(damping, (int, float)) or isinstance(damping, bool):
|
| 89 |
+
raise TypeError("damping must be a finite float in [0, 1).")
|
| 90 |
+
if not math.isfinite(float(damping)) or not 0 <= damping < 1:
|
| 91 |
+
raise ValueError("damping must be a finite float in [0, 1).")
|
| 92 |
+
if not isinstance(tolerance, (int, float)) or isinstance(tolerance, bool):
|
| 93 |
+
raise TypeError("tolerance must be a positive finite float.")
|
| 94 |
+
if not math.isfinite(float(tolerance)) or tolerance <= 0:
|
| 95 |
+
raise ValueError("tolerance must be a positive finite float.")
|
| 96 |
+
if not isinstance(max_iterations, int) or isinstance(max_iterations, bool):
|
| 97 |
+
raise TypeError("max_iterations must be a positive integer.")
|
| 98 |
+
if max_iterations <= 0:
|
| 99 |
+
raise ValueError("max_iterations must be a positive integer.")
|
| 100 |
+
length = A.shape[0]
|
| 101 |
+
if length == 0:
|
| 102 |
+
raise ValueError("PageRank requires at least one residue.")
|
| 103 |
+
if not bool(torch.isfinite(A).all()):
|
| 104 |
+
raise ValueError("A must contain only finite attention values.")
|
| 105 |
+
work_dtype = torch.float64 if A.dtype == torch.float64 else torch.float32
|
| 106 |
+
P = A.detach().to(dtype=work_dtype).clamp_min(0)
|
| 107 |
+
row_sum = P.sum(dim=-1, keepdim=True)
|
| 108 |
+
uniform = torch.full_like(P, 1.0 / length)
|
| 109 |
+
P = torch.where(row_sum > 0, P / row_sum.clamp_min(torch.finfo(work_dtype).tiny), uniform)
|
| 110 |
+
p = torch.full((length,), 1.0 / length, device=P.device, dtype=work_dtype)
|
| 111 |
+
teleport = (1.0 - damping) / length
|
| 112 |
+
for _ in range(max_iterations):
|
| 113 |
+
p_next = teleport + damping * (P.transpose(0, 1) @ p)
|
| 114 |
+
if torch.linalg.vector_norm(p_next - p, ord=1) <= tolerance:
|
| 115 |
+
p = p_next
|
| 116 |
+
break
|
| 117 |
+
p = p_next
|
| 118 |
+
return p / p.sum()
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class Pooler:
|
| 122 |
+
"""Apply one or more pooling operations to biological residue rows."""
|
| 123 |
+
|
| 124 |
+
def __init__(self, pooling: str | Sequence[str] = ("mean",)) -> None:
|
| 125 |
+
pooling_value: object = pooling
|
| 126 |
+
if isinstance(pooling_value, (bytes, bytearray)) or not isinstance(
|
| 127 |
+
pooling_value, (str, Sequence)
|
| 128 |
+
):
|
| 129 |
+
raise TypeError("pooling must be a name or a sequence of names.")
|
| 130 |
+
names = (pooling_value,) if isinstance(pooling_value, str) else tuple(pooling_value)
|
| 131 |
+
if not all(isinstance(name, str) for name in names):
|
| 132 |
+
raise TypeError("pooling names must be strings.")
|
| 133 |
+
if not names:
|
| 134 |
+
raise ValueError("At least one pooling operation is required.")
|
| 135 |
+
unknown = set(names) - POOLING_NAMES
|
| 136 |
+
if unknown:
|
| 137 |
+
raise ValueError(f"Unknown pooling operations: {sorted(unknown)}.")
|
| 138 |
+
duplicates = sorted({name for name in names if names.count(name) > 1})
|
| 139 |
+
if duplicates:
|
| 140 |
+
raise ValueError(f"Duplicate pooling operations are not supported: {duplicates}.")
|
| 141 |
+
self.names = names
|
| 142 |
+
|
| 143 |
+
def output_slices(self, d: int) -> dict[str, tuple[int, int]]:
|
| 144 |
+
"""Return the output interval assigned to each pooler."""
|
| 145 |
+
|
| 146 |
+
if not isinstance(d, int) or isinstance(d, bool):
|
| 147 |
+
raise TypeError("d must be a positive integer.")
|
| 148 |
+
if d <= 0:
|
| 149 |
+
raise ValueError("d must be a positive integer.")
|
| 150 |
+
return {name: (i * d, (i + 1) * d) for i, name in enumerate(self.names)}
|
| 151 |
+
|
| 152 |
+
def __call__(
|
| 153 |
+
self,
|
| 154 |
+
X: Tensor,
|
| 155 |
+
residue_mask: Tensor,
|
| 156 |
+
*,
|
| 157 |
+
attentions: Tensor | Sequence[Tensor] | None = None,
|
| 158 |
+
attention_backend: str | None = None,
|
| 159 |
+
) -> Tensor:
|
| 160 |
+
M = _validate_inputs(X, residue_mask)
|
| 161 |
+
M_expanded = M.unsqueeze(-1)
|
| 162 |
+
count = M_expanded.sum(dim=1).clamp_min(1)
|
| 163 |
+
X_residues = X.masked_fill(~M_expanded, 0)
|
| 164 |
+
outputs: list[Tensor] = []
|
| 165 |
+
|
| 166 |
+
for name in self.names:
|
| 167 |
+
if name == "mean":
|
| 168 |
+
Y = X_residues.sum(dim=1) / count
|
| 169 |
+
elif name == "max":
|
| 170 |
+
Y = X.masked_fill(~M_expanded, -torch.inf).max(dim=1).values
|
| 171 |
+
elif name == "norm":
|
| 172 |
+
Y = torch.linalg.vector_norm(X_residues, ord=2, dim=1)
|
| 173 |
+
elif name == "median":
|
| 174 |
+
Y = X.masked_fill(~M_expanded, torch.nan).nanmedian(dim=1).values
|
| 175 |
+
elif name in {"var", "std"}:
|
| 176 |
+
mean = X_residues.sum(dim=1, keepdim=True) / count.unsqueeze(1)
|
| 177 |
+
centered = (X - mean).masked_fill(~M_expanded, 0)
|
| 178 |
+
variance = (centered**2).sum(dim=1) / count
|
| 179 |
+
Y = variance.sqrt() if name == "std" else variance
|
| 180 |
+
elif name == "cls":
|
| 181 |
+
Y = X[:, 0]
|
| 182 |
+
else:
|
| 183 |
+
if attention_backend != "eager":
|
| 184 |
+
raise ValueError(
|
| 185 |
+
"parti requires attn_implementation='eager' so full "
|
| 186 |
+
"attention matrices are available."
|
| 187 |
+
)
|
| 188 |
+
if attentions is None:
|
| 189 |
+
raise ValueError("parti requires model attention matrices.")
|
| 190 |
+
if int(M.sum(dim=1).max().item()) > 2048:
|
| 191 |
+
raise ValueError("parti supports at most 2,048 biological residues.")
|
| 192 |
+
A = _pooled_attention(attentions, batch_size=X.shape[0]).to(X.device)
|
| 193 |
+
pooled: list[Tensor] = []
|
| 194 |
+
for X_i, M_i, A_i in zip(X, M, A, strict=True):
|
| 195 |
+
indices = M_i.nonzero(as_tuple=True)[0]
|
| 196 |
+
A_residue = A_i.index_select(0, indices).index_select(1, indices)
|
| 197 |
+
w = pagerank_weights(A_residue).to(dtype=X.dtype)
|
| 198 |
+
pooled.append(w @ X_i.index_select(0, indices))
|
| 199 |
+
Y = torch.stack(pooled)
|
| 200 |
+
if not bool(torch.isfinite(Y).all()):
|
| 201 |
+
raise ValueError(
|
| 202 |
+
f"Pooling operation {name!r} produced non-finite output from "
|
| 203 |
+
"biological residue embeddings."
|
| 204 |
+
)
|
| 205 |
+
outputs.append(Y)
|
| 206 |
+
|
| 207 |
+
return torch.cat(outputs, dim=-1)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
__all__ = ["POOLING_NAMES", "Pooler", "pagerank_weights"]
|
fastplms/embeddings/runner.py
ADDED
|
@@ -0,0 +1,1559 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Model-independent dataset embedding orchestration."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
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
|
| 19 |
+
from .storage import (
|
| 20 |
+
SafetensorsStreamWriter,
|
| 21 |
+
append_sqlite_records,
|
| 22 |
+
initialize_sqlite_run,
|
| 23 |
+
load_result,
|
| 24 |
+
load_sqlite_result,
|
| 25 |
+
safetensors_result_exists,
|
| 26 |
+
save_result,
|
| 27 |
+
tensor_sha256,
|
| 28 |
+
update_sqlite_run_metadata,
|
| 29 |
+
)
|
| 30 |
+
from .types import (
|
| 31 |
+
EmbeddingBatch,
|
| 32 |
+
EmbeddingInput,
|
| 33 |
+
EmbeddingRecord,
|
| 34 |
+
EmbeddingResult,
|
| 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
|
| 41 |
+
_DEFAULT_BATCH_WINDOW_MULTIPLIER = 16
|
| 42 |
+
_SUPPORTED_STORAGE_FORMATS = frozenset({"safetensors", "sqlite"})
|
| 43 |
+
|
| 44 |
+
|
| 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.")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def select_hidden_state_embeddings(
|
| 54 |
+
last_hidden_state: Tensor,
|
| 55 |
+
hidden_states: tuple[Tensor, ...] | None,
|
| 56 |
+
*,
|
| 57 |
+
hidden_state_index: int = -1,
|
| 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]:
|
| 74 |
+
"""Yield FASTA records in source order without reading the file into memory."""
|
| 75 |
+
|
| 76 |
+
identifier: str | None = None
|
| 77 |
+
sequence_parts: list[str] = []
|
| 78 |
+
found_record = False
|
| 79 |
+
with Path(path).open("r", encoding="utf-8") as handle:
|
| 80 |
+
for line_number, raw_line in enumerate(handle, start=1):
|
| 81 |
+
line = raw_line.strip()
|
| 82 |
+
if not line:
|
| 83 |
+
continue
|
| 84 |
+
if line.startswith(">"):
|
| 85 |
+
if identifier is not None:
|
| 86 |
+
found_record = True
|
| 87 |
+
yield EmbeddingInput(identifier, "".join(sequence_parts))
|
| 88 |
+
identifier = line[1:].strip().split(maxsplit=1)[0]
|
| 89 |
+
if not identifier:
|
| 90 |
+
raise ValueError(f"Missing FASTA identifier on line {line_number}.")
|
| 91 |
+
sequence_parts = []
|
| 92 |
+
else:
|
| 93 |
+
if identifier is None:
|
| 94 |
+
raise ValueError(
|
| 95 |
+
f"Sequence data precedes the first FASTA header on line {line_number}."
|
| 96 |
+
)
|
| 97 |
+
sequence_parts.append("".join(line.split()))
|
| 98 |
+
if identifier is not None:
|
| 99 |
+
found_record = True
|
| 100 |
+
yield EmbeddingInput(identifier, "".join(sequence_parts))
|
| 101 |
+
if not found_record:
|
| 102 |
+
raise ValueError(f"No FASTA records found in {path}.")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def parse_fasta(path: str | Path) -> list[EmbeddingInput]:
|
| 106 |
+
"""Parse FASTA records while preserving identifiers, order, and duplicates."""
|
| 107 |
+
|
| 108 |
+
return list(iter_fasta(path))
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _normalize_input_item(
|
| 112 |
+
position: int,
|
| 113 |
+
item: str | EmbeddingInput | tuple[str, str],
|
| 114 |
+
) -> EmbeddingInput:
|
| 115 |
+
if isinstance(item, EmbeddingInput):
|
| 116 |
+
return item
|
| 117 |
+
if isinstance(item, str):
|
| 118 |
+
return EmbeddingInput(str(position), item)
|
| 119 |
+
if isinstance(item, tuple) and len(item) == 2:
|
| 120 |
+
return EmbeddingInput(str(item[0]), str(item[1]))
|
| 121 |
+
raise TypeError(
|
| 122 |
+
"inputs must contain sequences, EmbeddingInput values, or (id, sequence) tuples."
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
class _InputSpool(Sequence[EmbeddingInput]):
|
| 127 |
+
"""Immutable disk-backed normalized inputs with an incremental digest."""
|
| 128 |
+
|
| 129 |
+
def __init__(
|
| 130 |
+
self,
|
| 131 |
+
values: Iterable[str | EmbeddingInput | tuple[str, str]],
|
| 132 |
+
) -> None:
|
| 133 |
+
self._temporary: tempfile.TemporaryDirectory[str] | None = tempfile.TemporaryDirectory(
|
| 134 |
+
prefix="fastplms-inputs-"
|
| 135 |
+
)
|
| 136 |
+
self.path = Path(self._temporary.name) / "inputs.sqlite"
|
| 137 |
+
self._connection: sqlite3.Connection | None = sqlite3.connect(self.path)
|
| 138 |
+
self._connection.execute(
|
| 139 |
+
"CREATE TABLE inputs ("
|
| 140 |
+
"position INTEGER PRIMARY KEY, input_id TEXT NOT NULL, sequence TEXT NOT NULL)"
|
| 141 |
+
)
|
| 142 |
+
digest = hashlib.sha256()
|
| 143 |
+
count = 0
|
| 144 |
+
pending: list[tuple[int, str, str]] = []
|
| 145 |
+
try:
|
| 146 |
+
for position, item in enumerate(values):
|
| 147 |
+
record = _normalize_input_item(position, item)
|
| 148 |
+
for value in (record.id, record.sequence):
|
| 149 |
+
encoded = value.encode("utf-8")
|
| 150 |
+
digest.update(len(encoded).to_bytes(8, "big"))
|
| 151 |
+
digest.update(encoded)
|
| 152 |
+
pending.append((position, record.id, record.sequence))
|
| 153 |
+
count += 1
|
| 154 |
+
if len(pending) == 1_024:
|
| 155 |
+
self._connection.executemany("INSERT INTO inputs VALUES (?, ?, ?)", pending)
|
| 156 |
+
pending.clear()
|
| 157 |
+
if pending:
|
| 158 |
+
self._connection.executemany("INSERT INTO inputs VALUES (?, ?, ?)", pending)
|
| 159 |
+
if count == 0:
|
| 160 |
+
raise ValueError("inputs must contain at least one sequence.")
|
| 161 |
+
self._connection.commit()
|
| 162 |
+
self._connection.close()
|
| 163 |
+
self._connection = sqlite3.connect(
|
| 164 |
+
f"{self.path.resolve().as_uri()}?mode=ro",
|
| 165 |
+
uri=True,
|
| 166 |
+
)
|
| 167 |
+
except BaseException:
|
| 168 |
+
self.close()
|
| 169 |
+
raise
|
| 170 |
+
digest.update(count.to_bytes(8, "big"))
|
| 171 |
+
self.input_fingerprint = digest.hexdigest()
|
| 172 |
+
self._count = count
|
| 173 |
+
|
| 174 |
+
def _require_connection(self) -> sqlite3.Connection:
|
| 175 |
+
if self._connection is None:
|
| 176 |
+
raise RuntimeError("Input spool is closed.")
|
| 177 |
+
return self._connection
|
| 178 |
+
|
| 179 |
+
def __len__(self) -> int:
|
| 180 |
+
return self._count
|
| 181 |
+
|
| 182 |
+
def __iter__(self) -> Iterator[EmbeddingInput]:
|
| 183 |
+
cursor = self._require_connection().execute(
|
| 184 |
+
"SELECT input_id, sequence FROM inputs ORDER BY position"
|
| 185 |
+
)
|
| 186 |
+
while rows := cursor.fetchmany(1_024):
|
| 187 |
+
for input_id, sequence in rows:
|
| 188 |
+
yield EmbeddingInput(input_id, sequence)
|
| 189 |
+
|
| 190 |
+
@overload
|
| 191 |
+
def __getitem__(self, index: int, /) -> EmbeddingInput: ...
|
| 192 |
+
|
| 193 |
+
@overload
|
| 194 |
+
def __getitem__(self, index: slice, /) -> list[EmbeddingInput]: ...
|
| 195 |
+
|
| 196 |
+
def __getitem__(self, index: int | slice) -> EmbeddingInput | list[EmbeddingInput]:
|
| 197 |
+
connection = self._require_connection()
|
| 198 |
+
|
| 199 |
+
if isinstance(index, slice):
|
| 200 |
+
start, stop, step = index.indices(self._count)
|
| 201 |
+
if step != 1:
|
| 202 |
+
return [self[position] for position in range(start, stop, step)]
|
| 203 |
+
rows = connection.execute(
|
| 204 |
+
"SELECT input_id, sequence FROM inputs "
|
| 205 |
+
"WHERE position >= ? AND position < ? ORDER BY position",
|
| 206 |
+
(start, stop),
|
| 207 |
+
).fetchall()
|
| 208 |
+
return [EmbeddingInput(input_id, sequence) for input_id, sequence in rows]
|
| 209 |
+
position = index + self._count if index < 0 else index
|
| 210 |
+
if position < 0 or position >= self._count:
|
| 211 |
+
raise IndexError(index)
|
| 212 |
+
row = connection.execute(
|
| 213 |
+
"SELECT input_id, sequence FROM inputs WHERE position = ?", (position,)
|
| 214 |
+
).fetchone()
|
| 215 |
+
if row is None:
|
| 216 |
+
raise IndexError(index)
|
| 217 |
+
return EmbeddingInput(row[0], row[1])
|
| 218 |
+
|
| 219 |
+
def close(self) -> None:
|
| 220 |
+
connection = getattr(self, "_connection", None)
|
| 221 |
+
if connection is not None:
|
| 222 |
+
connection.close()
|
| 223 |
+
self._connection = None
|
| 224 |
+
temporary = getattr(self, "_temporary", None)
|
| 225 |
+
if temporary is not None:
|
| 226 |
+
temporary.cleanup()
|
| 227 |
+
self._temporary = None
|
| 228 |
+
|
| 229 |
+
def __del__(self) -> None:
|
| 230 |
+
self.close()
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def _normalize_inputs(
|
| 234 |
+
inputs: (Iterable[str | EmbeddingInput | tuple[str, str]] | Mapping[str, str] | str | Path),
|
| 235 |
+
*,
|
| 236 |
+
disk_backed: bool,
|
| 237 |
+
) -> Sequence[EmbeddingInput]:
|
| 238 |
+
is_fasta_path = isinstance(inputs, Path)
|
| 239 |
+
if isinstance(inputs, str):
|
| 240 |
+
try:
|
| 241 |
+
is_fasta_path = Path(inputs).is_file()
|
| 242 |
+
except OSError:
|
| 243 |
+
is_fasta_path = False
|
| 244 |
+
should_spool = disk_backed or is_fasta_path or not isinstance(inputs, (str, Sequence, Mapping))
|
| 245 |
+
values: Iterable[str | EmbeddingInput | tuple[str, str]]
|
| 246 |
+
if isinstance(inputs, Path):
|
| 247 |
+
values = iter_fasta(inputs)
|
| 248 |
+
elif isinstance(inputs, str):
|
| 249 |
+
values = iter_fasta(inputs) if is_fasta_path else [inputs]
|
| 250 |
+
elif isinstance(inputs, Mapping):
|
| 251 |
+
values = inputs.items()
|
| 252 |
+
else:
|
| 253 |
+
values = inputs
|
| 254 |
+
if should_spool:
|
| 255 |
+
return _InputSpool(values)
|
| 256 |
+
records: list[EmbeddingInput] = []
|
| 257 |
+
for position, item in enumerate(values):
|
| 258 |
+
records.append(_normalize_input_item(position, item))
|
| 259 |
+
if not records:
|
| 260 |
+
raise ValueError("inputs must contain at least one sequence.")
|
| 261 |
+
return records
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def _validate_untruncated_lengths(
|
| 265 |
+
records: Sequence[EmbeddingInput],
|
| 266 |
+
*,
|
| 267 |
+
max_length: int | None,
|
| 268 |
+
truncate: bool,
|
| 269 |
+
) -> None:
|
| 270 |
+
"""Fail before inference when a biological-residue limit would be exceeded."""
|
| 271 |
+
|
| 272 |
+
if max_length is None or truncate:
|
| 273 |
+
return
|
| 274 |
+
for position, record in enumerate(records):
|
| 275 |
+
residue_count = len(record.sequence)
|
| 276 |
+
if residue_count > max_length:
|
| 277 |
+
raise ValueError(
|
| 278 |
+
f"Input at position {position} with id {record.id!r} has "
|
| 279 |
+
f"{residue_count} biological residues, exceeding max_length={max_length} "
|
| 280 |
+
"while truncate=False."
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def _model_device(model: Any) -> torch.device:
|
| 285 |
+
try:
|
| 286 |
+
return torch.device(next(model.parameters()).device)
|
| 287 |
+
except (AttributeError, StopIteration):
|
| 288 |
+
return torch.device("cpu")
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
def _attention_backend(model: Any) -> str | None:
|
| 292 |
+
config = getattr(model, "config", None)
|
| 293 |
+
for name in ("_attn_implementation", "attn_implementation", "attn_backend"):
|
| 294 |
+
value = getattr(config, name, None)
|
| 295 |
+
if value:
|
| 296 |
+
return str(value)
|
| 297 |
+
return None
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def _attention_kernel_metadata(backend: str | None) -> dict[str, Any] | None:
|
| 301 |
+
if backend not in {"flash_attention_2", "flash_attention_3"}:
|
| 302 |
+
return None
|
| 303 |
+
from fastplms.registry import get_model_registry
|
| 304 |
+
|
| 305 |
+
spec = get_model_registry().attention_kernels[backend]
|
| 306 |
+
return {
|
| 307 |
+
"repository": spec.repository,
|
| 308 |
+
"revision": spec.revision,
|
| 309 |
+
"version": spec.version,
|
| 310 |
+
"expected_variant": spec.expected_variant,
|
| 311 |
+
"dtypes": list(spec.dtypes),
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def _fingerprint_jsonable(value: Any) -> Any:
|
| 316 |
+
if isinstance(value, Mapping):
|
| 317 |
+
return {str(key): _fingerprint_jsonable(item) for key, item in value.items()}
|
| 318 |
+
if isinstance(value, (list, tuple)):
|
| 319 |
+
return [_fingerprint_jsonable(item) for item in value]
|
| 320 |
+
if isinstance(value, (set, frozenset)):
|
| 321 |
+
return sorted((_fingerprint_jsonable(item) for item in value), key=repr)
|
| 322 |
+
if isinstance(value, Path):
|
| 323 |
+
return str(value)
|
| 324 |
+
if isinstance(value, Tensor):
|
| 325 |
+
return {
|
| 326 |
+
"dtype": str(value.dtype).removeprefix("torch."),
|
| 327 |
+
"shape": list(value.shape),
|
| 328 |
+
"sha256": tensor_sha256(value),
|
| 329 |
+
}
|
| 330 |
+
if isinstance(value, torch.dtype):
|
| 331 |
+
return str(value).removeprefix("torch.")
|
| 332 |
+
if isinstance(value, torch.device):
|
| 333 |
+
return str(value)
|
| 334 |
+
if value is None or isinstance(value, (str, int, float, bool)):
|
| 335 |
+
return value
|
| 336 |
+
return {
|
| 337 |
+
"class": f"{value.__class__.__module__}.{value.__class__.__qualname__}",
|
| 338 |
+
"value": str(value),
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
def _tokenizer_content_sha256(tokenizer: Any) -> str:
|
| 343 |
+
content: dict[str, Any] = {
|
| 344 |
+
"init_kwargs": getattr(tokenizer, "init_kwargs", None),
|
| 345 |
+
"special_tokens_map": getattr(tokenizer, "special_tokens_map", None),
|
| 346 |
+
"model_max_length": getattr(tokenizer, "model_max_length", None),
|
| 347 |
+
"padding_side": getattr(tokenizer, "padding_side", None),
|
| 348 |
+
"truncation_side": getattr(tokenizer, "truncation_side", None),
|
| 349 |
+
}
|
| 350 |
+
get_vocab = getattr(tokenizer, "get_vocab", None)
|
| 351 |
+
if callable(get_vocab):
|
| 352 |
+
content["vocabulary"] = get_vocab()
|
| 353 |
+
get_added_vocab = getattr(tokenizer, "get_added_vocab", None)
|
| 354 |
+
if callable(get_added_vocab):
|
| 355 |
+
content["added_vocabulary"] = get_added_vocab()
|
| 356 |
+
backend = getattr(tokenizer, "backend_tokenizer", None)
|
| 357 |
+
backend_to_str = getattr(backend, "to_str", None)
|
| 358 |
+
if callable(backend_to_str):
|
| 359 |
+
content["backend"] = backend_to_str()
|
| 360 |
+
serialized = json.dumps(
|
| 361 |
+
_fingerprint_jsonable(content),
|
| 362 |
+
sort_keys=True,
|
| 363 |
+
separators=(",", ":"),
|
| 364 |
+
ensure_ascii=False,
|
| 365 |
+
).encode()
|
| 366 |
+
return hashlib.sha256(serialized).hexdigest()
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def _tokenizer_metadata(model: Any, tokenizer: Any | None) -> dict[str, Any]:
|
| 370 |
+
resolved = tokenizer if tokenizer is not None else getattr(model, "tokenizer", None)
|
| 371 |
+
if resolved is None:
|
| 372 |
+
# Raw-sequence families such as E1 retain their loader context on the
|
| 373 |
+
# model/encoder rather than exposing a Transformers tokenizer. Bind the
|
| 374 |
+
# non-secret source policy to resume identity without serializing a Hub
|
| 375 |
+
# token or forcing lazy tokenizer initialization.
|
| 376 |
+
for candidate in (model, getattr(model, "model", None)):
|
| 377 |
+
settings = getattr(candidate, "__dict__", {}).get("_fastplms_tokenizer_kwargs")
|
| 378 |
+
if isinstance(settings, Mapping):
|
| 379 |
+
token_value = settings.get("token")
|
| 380 |
+
return {
|
| 381 |
+
"mode": "native-sequence",
|
| 382 |
+
"source": (
|
| 383 |
+
str(settings.get("tokenizer_source"))
|
| 384 |
+
if settings.get("tokenizer_source") is not None
|
| 385 |
+
else None
|
| 386 |
+
),
|
| 387 |
+
"revision": settings.get("revision"),
|
| 388 |
+
"cache_dir": (
|
| 389 |
+
str(settings.get("cache_dir"))
|
| 390 |
+
if settings.get("cache_dir") is not None
|
| 391 |
+
else None
|
| 392 |
+
),
|
| 393 |
+
"local_files_only": bool(settings.get("local_files_only", False)),
|
| 394 |
+
"token_policy": (
|
| 395 |
+
"disabled"
|
| 396 |
+
if token_value is False
|
| 397 |
+
else "provided"
|
| 398 |
+
if token_value is not None
|
| 399 |
+
else "default"
|
| 400 |
+
),
|
| 401 |
+
}
|
| 402 |
+
return {"mode": "native-sequence"}
|
| 403 |
+
return {
|
| 404 |
+
"mode": "tokenizer",
|
| 405 |
+
"class": f"{resolved.__class__.__module__}.{resolved.__class__.__qualname__}",
|
| 406 |
+
"name_or_path": getattr(resolved, "name_or_path", None),
|
| 407 |
+
"vocab_size": getattr(resolved, "vocab_size", None),
|
| 408 |
+
"special_token_ids": list(getattr(resolved, "all_special_ids", ())),
|
| 409 |
+
"content_sha256": _tokenizer_content_sha256(resolved),
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
@contextmanager
|
| 414 |
+
def _temporary_eval(model: Any) -> Iterator[None]:
|
| 415 |
+
was_training = getattr(model, "training", None)
|
| 416 |
+
eval_method = getattr(model, "eval", None)
|
| 417 |
+
train_method = getattr(model, "train", None)
|
| 418 |
+
if (
|
| 419 |
+
not isinstance(was_training, bool)
|
| 420 |
+
or not callable(eval_method)
|
| 421 |
+
or not callable(train_method)
|
| 422 |
+
):
|
| 423 |
+
yield
|
| 424 |
+
return
|
| 425 |
+
eval_method()
|
| 426 |
+
try:
|
| 427 |
+
yield
|
| 428 |
+
finally:
|
| 429 |
+
train_method(was_training)
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
def _software_versions() -> dict[str, str | None]:
|
| 433 |
+
try:
|
| 434 |
+
import fastplms
|
| 435 |
+
|
| 436 |
+
fastplms_version = fastplms.__version__
|
| 437 |
+
except (AttributeError, ImportError):
|
| 438 |
+
fastplms_version = None
|
| 439 |
+
try:
|
| 440 |
+
import safetensors
|
| 441 |
+
|
| 442 |
+
safetensors_version = safetensors.__version__
|
| 443 |
+
except ImportError:
|
| 444 |
+
safetensors_version = None
|
| 445 |
+
try:
|
| 446 |
+
import transformers
|
| 447 |
+
|
| 448 |
+
transformers_version = transformers.__version__
|
| 449 |
+
except ImportError:
|
| 450 |
+
transformers_version = None
|
| 451 |
+
return {
|
| 452 |
+
"fastplms": fastplms_version,
|
| 453 |
+
"python": platform.python_version(),
|
| 454 |
+
"safetensors": safetensors_version,
|
| 455 |
+
"torch": torch.__version__,
|
| 456 |
+
"torch_cuda": torch.version.cuda,
|
| 457 |
+
"transformers": transformers_version,
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def _adapter_identity_metadata(model: Any) -> dict[str, Any] | None:
|
| 462 |
+
"""Return deterministic PEFT/adapter identity without tensor payloads."""
|
| 463 |
+
|
| 464 |
+
peft_config = getattr(model, "peft_config", None)
|
| 465 |
+
if not isinstance(peft_config, Mapping) or not peft_config:
|
| 466 |
+
return None
|
| 467 |
+
configurations: dict[str, Any] = {}
|
| 468 |
+
for name, config in sorted(peft_config.items(), key=lambda item: str(item[0])):
|
| 469 |
+
to_dict = getattr(config, "to_dict", None)
|
| 470 |
+
if callable(to_dict):
|
| 471 |
+
value = to_dict()
|
| 472 |
+
else:
|
| 473 |
+
try:
|
| 474 |
+
value = vars(config)
|
| 475 |
+
except TypeError:
|
| 476 |
+
value = config
|
| 477 |
+
configurations[str(name)] = _fingerprint_jsonable(value)
|
| 478 |
+
active_adapters = getattr(model, "active_adapters", None)
|
| 479 |
+
if callable(active_adapters):
|
| 480 |
+
active_adapters = active_adapters()
|
| 481 |
+
return {
|
| 482 |
+
"active": _fingerprint_jsonable(active_adapters),
|
| 483 |
+
"configurations": configurations,
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
def _execution_identity_metadata(model: Any) -> dict[str, Any]:
|
| 488 |
+
"""Capture runtime policy that can change persisted numerical results."""
|
| 489 |
+
|
| 490 |
+
parameter_dtypes = sorted(
|
| 491 |
+
{
|
| 492 |
+
str(parameter.dtype).removeprefix("torch.")
|
| 493 |
+
for parameter in getattr(model, "parameters", lambda: ())()
|
| 494 |
+
}
|
| 495 |
+
)
|
| 496 |
+
return {
|
| 497 |
+
"device": _model_device(model).type,
|
| 498 |
+
"hf_device_map": _fingerprint_jsonable(getattr(model, "hf_device_map", None)),
|
| 499 |
+
"parameter_dtypes": parameter_dtypes,
|
| 500 |
+
"software": _software_versions(),
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
def _biological_residue_mask(
|
| 505 |
+
input_ids: Tensor,
|
| 506 |
+
attention_mask: Tensor,
|
| 507 |
+
tokenizer: Any,
|
| 508 |
+
) -> Tensor:
|
| 509 |
+
"""Remove padding and tokenizer-declared special tokens from M."""
|
| 510 |
+
|
| 511 |
+
M = attention_mask.to(dtype=torch.bool)
|
| 512 |
+
special_ids = tuple(int(token_id) for token_id in getattr(tokenizer, "all_special_ids", ()))
|
| 513 |
+
if special_ids:
|
| 514 |
+
specials = torch.tensor(special_ids, device=input_ids.device, dtype=input_ids.dtype)
|
| 515 |
+
M = M & ~torch.isin(input_ids, specials)
|
| 516 |
+
return M
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
def _generic_embedding_batch(
|
| 520 |
+
model: Any,
|
| 521 |
+
sequences: list[str],
|
| 522 |
+
*,
|
| 523 |
+
tokenizer: Any | None,
|
| 524 |
+
max_length: int | None,
|
| 525 |
+
truncate: bool,
|
| 526 |
+
need_attentions: bool,
|
| 527 |
+
model_kwargs: dict[str, Any],
|
| 528 |
+
) -> EmbeddingBatch:
|
| 529 |
+
config = getattr(model, "config", None)
|
| 530 |
+
model_type = str(getattr(config, "model_type", "")).lower()
|
| 531 |
+
if tokenizer is None:
|
| 532 |
+
tokenizer = getattr(model, "tokenizer", None)
|
| 533 |
+
|
| 534 |
+
if tokenizer is None and model_type == "e1":
|
| 535 |
+
output = model._embed(sequences, return_attention_mask=True, **model_kwargs)
|
| 536 |
+
if not isinstance(output, tuple) or len(output) != 2:
|
| 537 |
+
raise TypeError("E1 _embed must return (X, residue_mask).")
|
| 538 |
+
X, M = output
|
| 539 |
+
preparer = getattr(model, "prep_tokens", None)
|
| 540 |
+
if preparer is not None and hasattr(preparer, "get_batch_kwargs"):
|
| 541 |
+
prepared = preparer.get_batch_kwargs(sequences, device=X.device)
|
| 542 |
+
input_ids = prepared["input_ids"]
|
| 543 |
+
boundary_ids = preparer.boundary_token_ids.to(
|
| 544 |
+
device=input_ids.device, dtype=input_ids.dtype
|
| 545 |
+
)
|
| 546 |
+
# E1 wraps each raw sequence in BOS, context-label, terminal-label,
|
| 547 |
+
# and EOS tokens. Only amino-acid rows are biological residues.
|
| 548 |
+
M = M.to(dtype=torch.bool) & ~torch.isin(input_ids, boundary_ids)
|
| 549 |
+
if need_attentions:
|
| 550 |
+
raise ValueError("parti is not available for tokenizer-free E1 embedding.")
|
| 551 |
+
return EmbeddingBatch(X=X, residue_mask=M.to(dtype=torch.bool))
|
| 552 |
+
if tokenizer is None:
|
| 553 |
+
raise ValueError("A tokenizer is required for this model's embedding path.")
|
| 554 |
+
|
| 555 |
+
tokenize_kwargs: dict[str, Any] = {
|
| 556 |
+
"return_tensors": "pt",
|
| 557 |
+
"padding": True,
|
| 558 |
+
"truncation": truncate,
|
| 559 |
+
}
|
| 560 |
+
if max_length is not None and truncate:
|
| 561 |
+
# ``max_length`` is a biological-residue limit. Tokenizer limits include
|
| 562 |
+
# boundary tokens, so reserve their declared width instead of dropping
|
| 563 |
+
# residues at the exact boundary.
|
| 564 |
+
special_token_count = 0
|
| 565 |
+
num_special_tokens_to_add = getattr(tokenizer, "num_special_tokens_to_add", None)
|
| 566 |
+
if callable(num_special_tokens_to_add):
|
| 567 |
+
special_token_count = int(num_special_tokens_to_add(pair=False))
|
| 568 |
+
tokenize_kwargs["max_length"] = max_length + special_token_count
|
| 569 |
+
sequence_tokenizer = getattr(model, "_tokenize_sequence_batch", None)
|
| 570 |
+
if callable(sequence_tokenizer):
|
| 571 |
+
encoded = sequence_tokenizer(sequences, tokenizer=tokenizer, **tokenize_kwargs)
|
| 572 |
+
else:
|
| 573 |
+
encoded = tokenizer(sequences, **tokenize_kwargs)
|
| 574 |
+
device = _model_device(model)
|
| 575 |
+
input_ids = encoded["input_ids"].to(device)
|
| 576 |
+
attention_mask = encoded.get("attention_mask", input_ids.new_ones(input_ids.shape)).to(device)
|
| 577 |
+
M = _biological_residue_mask(input_ids, attention_mask, tokenizer)
|
| 578 |
+
if need_attentions:
|
| 579 |
+
# Validate l before either the backbone or its quadratic attention graph
|
| 580 |
+
# is materialized. M has shape (b, l).
|
| 581 |
+
_validate_parti_length(M)
|
| 582 |
+
X = model._embed(input_ids, attention_mask, **model_kwargs)
|
| 583 |
+
attentions = None
|
| 584 |
+
if need_attentions:
|
| 585 |
+
output = model(
|
| 586 |
+
input_ids=input_ids,
|
| 587 |
+
attention_mask=attention_mask,
|
| 588 |
+
output_attentions=True,
|
| 589 |
+
return_dict=True,
|
| 590 |
+
)
|
| 591 |
+
attentions = getattr(output, "attentions", None)
|
| 592 |
+
if attentions is None:
|
| 593 |
+
raise ValueError("The model did not return attentions required by parti.")
|
| 594 |
+
return EmbeddingBatch(X=X, residue_mask=M, attentions=attentions)
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
def _first_metadata_value(*values: Any) -> Any:
|
| 598 |
+
for value in values:
|
| 599 |
+
if isinstance(value, str):
|
| 600 |
+
if value.strip():
|
| 601 |
+
return value
|
| 602 |
+
elif value is not None:
|
| 603 |
+
return value
|
| 604 |
+
return None
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
def _model_identity_metadata(model: Any) -> dict[str, Any]:
|
| 608 |
+
"""Resolve model and checkpoint identity, including local artifact fallbacks."""
|
| 609 |
+
|
| 610 |
+
config = getattr(model, "config", None)
|
| 611 |
+
checkpoint_revision = _first_metadata_value(
|
| 612 |
+
getattr(config, "fastplms_checkpoint_revision", None),
|
| 613 |
+
getattr(config, "_commit_hash", None),
|
| 614 |
+
)
|
| 615 |
+
return {
|
| 616 |
+
"model_id": _first_metadata_value(
|
| 617 |
+
getattr(config, "fastplms_model_id", None),
|
| 618 |
+
getattr(config, "_name_or_path", None),
|
| 619 |
+
),
|
| 620 |
+
"model_revision": _first_metadata_value(
|
| 621 |
+
getattr(config, "_commit_hash", None),
|
| 622 |
+
checkpoint_revision,
|
| 623 |
+
),
|
| 624 |
+
"checkpoint_repo_id": getattr(config, "fastplms_checkpoint_repo_id", None),
|
| 625 |
+
"checkpoint_revision": checkpoint_revision,
|
| 626 |
+
"checkpoint_hash": _first_metadata_value(
|
| 627 |
+
getattr(model, "checkpoint_hash", None),
|
| 628 |
+
getattr(config, "checkpoint_hash", None),
|
| 629 |
+
getattr(config, "fastplms_checkpoint_hash", None),
|
| 630 |
+
),
|
| 631 |
+
"weights_revision": getattr(config, "fastplms_weights_revision", None),
|
| 632 |
+
"runtime_revision": getattr(config, "fastplms_runtime_revision", None),
|
| 633 |
+
"source_tree_sha256": getattr(config, "fastplms_source_tree_sha256", None),
|
| 634 |
+
"runtime_bundle_sha256": getattr(config, "fastplms_runtime_bundle_sha256", None),
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
|
| 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:
|
| 644 |
+
yield X
|
| 645 |
+
return
|
| 646 |
+
trailing_elements = 1
|
| 647 |
+
for size in X.shape[1:]:
|
| 648 |
+
trailing_elements *= int(size)
|
| 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)
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
def _model_state_sha256(model: Any) -> str:
|
| 659 |
+
"""Hash named parameters and persistent buffers using bounded CPU copies."""
|
| 660 |
+
|
| 661 |
+
# Never cache this digest from tensor identity or ``Tensor._version``.
|
| 662 |
+
# ``Parameter.data`` and independent tensor aliases can mutate shared storage
|
| 663 |
+
# without changing either signal, while persisted resume identity must bind
|
| 664 |
+
# the authoritative bytes visible at the start of this run.
|
| 665 |
+
state = model.state_dict(keep_vars=True)
|
| 666 |
+
digest = hashlib.sha256()
|
| 667 |
+
for name, value in sorted(state.items()):
|
| 668 |
+
if not isinstance(value, Tensor):
|
| 669 |
+
raise TypeError(f"Model state entry {name!r} is not a tensor.")
|
| 670 |
+
if value.is_meta:
|
| 671 |
+
raise ValueError(
|
| 672 |
+
f"Cannot fingerprint meta-device model state entry {name!r}; pass "
|
| 673 |
+
"model_state_fingerprint with a caller-owned state identity."
|
| 674 |
+
)
|
| 675 |
+
header = json.dumps(
|
| 676 |
+
{
|
| 677 |
+
"name": name,
|
| 678 |
+
"dtype": str(value.dtype).removeprefix("torch."),
|
| 679 |
+
"shape": list(value.shape),
|
| 680 |
+
},
|
| 681 |
+
sort_keys=True,
|
| 682 |
+
separators=(",", ":"),
|
| 683 |
+
).encode()
|
| 684 |
+
digest.update(len(header).to_bytes(8, "big"))
|
| 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 |
+
|
| 692 |
+
|
| 693 |
+
def _input_sha256(records: Iterable[EmbeddingInput]) -> str:
|
| 694 |
+
"""Hash an ordered input stream without constructing a duplicate JSON payload."""
|
| 695 |
+
|
| 696 |
+
precomputed = getattr(records, "input_fingerprint", None)
|
| 697 |
+
if isinstance(precomputed, str):
|
| 698 |
+
return precomputed
|
| 699 |
+
digest = hashlib.sha256()
|
| 700 |
+
count = 0
|
| 701 |
+
for record in records:
|
| 702 |
+
count += 1
|
| 703 |
+
for value in (record.id, record.sequence):
|
| 704 |
+
encoded = value.encode("utf-8")
|
| 705 |
+
digest.update(len(encoded).to_bytes(8, "big"))
|
| 706 |
+
digest.update(encoded)
|
| 707 |
+
digest.update(count.to_bytes(8, "big"))
|
| 708 |
+
return digest.hexdigest()
|
| 709 |
+
|
| 710 |
+
|
| 711 |
+
def _run_fingerprint(
|
| 712 |
+
model: Any,
|
| 713 |
+
records: Sequence[EmbeddingInput],
|
| 714 |
+
*,
|
| 715 |
+
pooling: Sequence[str],
|
| 716 |
+
full_embeddings: bool,
|
| 717 |
+
max_length: int | None,
|
| 718 |
+
truncate: bool,
|
| 719 |
+
dtype: torch.dtype | None,
|
| 720 |
+
model_kwargs: dict[str, Any],
|
| 721 |
+
tokenizer_metadata: dict[str, Any],
|
| 722 |
+
model_state_fingerprint: str | None,
|
| 723 |
+
persist_output: bool,
|
| 724 |
+
embedding_context: Mapping[str, Any],
|
| 725 |
+
batch_size: int,
|
| 726 |
+
batch_window_size: int,
|
| 727 |
+
max_tokens_per_batch: int | None,
|
| 728 |
+
) -> tuple[str, str, str | None, str]:
|
| 729 |
+
input_fingerprint = _input_sha256(records)
|
| 730 |
+
attention_backend = _attention_backend(model)
|
| 731 |
+
model_identity = _model_identity_metadata(model)
|
| 732 |
+
if model_state_fingerprint is None and persist_output:
|
| 733 |
+
resolved_model_state_fingerprint = _model_state_sha256(model)
|
| 734 |
+
model_state_fingerprint_source = "computed"
|
| 735 |
+
elif model_state_fingerprint is not None:
|
| 736 |
+
resolved_model_state_fingerprint = model_state_fingerprint.strip()
|
| 737 |
+
if not resolved_model_state_fingerprint:
|
| 738 |
+
raise ValueError("model_state_fingerprint must not be empty.")
|
| 739 |
+
model_state_fingerprint_source = "caller"
|
| 740 |
+
else:
|
| 741 |
+
resolved_model_state_fingerprint = None
|
| 742 |
+
model_state_fingerprint_source = "not-computed"
|
| 743 |
+
payload = {
|
| 744 |
+
"fingerprint_schema_version": _RUN_FINGERPRINT_SCHEMA_VERSION,
|
| 745 |
+
"input_fingerprint": input_fingerprint,
|
| 746 |
+
"model_state_fingerprint": resolved_model_state_fingerprint,
|
| 747 |
+
"model_state_fingerprint_source": model_state_fingerprint_source,
|
| 748 |
+
"model_class": f"{model.__class__.__module__}.{model.__class__.__qualname__}",
|
| 749 |
+
**model_identity,
|
| 750 |
+
"attention_backend": attention_backend,
|
| 751 |
+
"attention_kernel": _attention_kernel_metadata(attention_backend),
|
| 752 |
+
"layer": repr(
|
| 753 |
+
getattr(model, "embedding_layer", model_kwargs.get("hidden_state_index", -1))
|
| 754 |
+
),
|
| 755 |
+
"projection": getattr(model, "embedding_projection", None),
|
| 756 |
+
"esmc_source": getattr(model, "_esmc_source", None),
|
| 757 |
+
"esmc_revision": getattr(model, "_esmc_source_revision", None),
|
| 758 |
+
"esmc_files": getattr(model, "_esmc_source_files", None),
|
| 759 |
+
"token_policy": getattr(model, "embedding_token_policy", None),
|
| 760 |
+
"tokenizer": tokenizer_metadata,
|
| 761 |
+
"adapter": _adapter_identity_metadata(model),
|
| 762 |
+
"execution": _execution_identity_metadata(model),
|
| 763 |
+
"embedding_context": _fingerprint_jsonable(embedding_context),
|
| 764 |
+
"pooling": list(pooling),
|
| 765 |
+
"full_embeddings": full_embeddings,
|
| 766 |
+
"max_length": max_length,
|
| 767 |
+
"truncate": truncate,
|
| 768 |
+
"dtype": str(dtype) if dtype is not None else None,
|
| 769 |
+
"batching": {
|
| 770 |
+
"batch_size": batch_size,
|
| 771 |
+
"batch_window_size": batch_window_size,
|
| 772 |
+
"max_tokens_per_batch": max_tokens_per_batch,
|
| 773 |
+
"input_storage": ("disk-spool" if isinstance(records, _InputSpool) else "memory"),
|
| 774 |
+
},
|
| 775 |
+
"model_kwargs": {
|
| 776 |
+
key: _fingerprint_jsonable(value) for key, value in sorted(model_kwargs.items())
|
| 777 |
+
},
|
| 778 |
+
"residue_mask_policy": "attention-mask-minus-special-tokens",
|
| 779 |
+
}
|
| 780 |
+
run_fingerprint = hashlib.sha256(
|
| 781 |
+
json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
| 782 |
+
).hexdigest()
|
| 783 |
+
return (
|
| 784 |
+
input_fingerprint,
|
| 785 |
+
run_fingerprint,
|
| 786 |
+
resolved_model_state_fingerprint,
|
| 787 |
+
model_state_fingerprint_source,
|
| 788 |
+
)
|
| 789 |
+
|
| 790 |
+
|
| 791 |
+
def _output_exists(path: str | Path, format: str) -> bool:
|
| 792 |
+
path = Path(path)
|
| 793 |
+
if format == "sqlite":
|
| 794 |
+
return path.is_file()
|
| 795 |
+
return safetensors_result_exists(path)
|
| 796 |
+
|
| 797 |
+
|
| 798 |
+
def _output_descriptor(position: int, record: EmbeddingRecord) -> dict[str, Any]:
|
| 799 |
+
tensor = record.tensor
|
| 800 |
+
if isinstance(tensor, LazyTensorReference):
|
| 801 |
+
dtype = tensor.dtype
|
| 802 |
+
shape = tensor.shape
|
| 803 |
+
digest = tensor.sha256
|
| 804 |
+
else:
|
| 805 |
+
dtype = str(tensor.dtype).removeprefix("torch.")
|
| 806 |
+
shape = tuple(tensor.shape)
|
| 807 |
+
digest = tensor_sha256(tensor)
|
| 808 |
+
return {
|
| 809 |
+
"position": position,
|
| 810 |
+
"id": record.id,
|
| 811 |
+
"dtype": dtype,
|
| 812 |
+
"shape": shape,
|
| 813 |
+
"sha256": digest,
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
def _ordered_string_sha256(values: Sequence[str]) -> str:
|
| 818 |
+
digest = hashlib.sha256()
|
| 819 |
+
for value in values:
|
| 820 |
+
encoded = value.encode("utf-8")
|
| 821 |
+
digest.update(len(encoded).to_bytes(8, "big"))
|
| 822 |
+
digest.update(encoded)
|
| 823 |
+
digest.update(len(values).to_bytes(8, "big"))
|
| 824 |
+
return digest.hexdigest()
|
| 825 |
+
|
| 826 |
+
|
| 827 |
+
def _embedding_context(
|
| 828 |
+
model: Any,
|
| 829 |
+
records: Sequence[EmbeddingInput],
|
| 830 |
+
*,
|
| 831 |
+
hidden_state_source: str,
|
| 832 |
+
decoder_inputs: Sequence[str] | None,
|
| 833 |
+
decoder_input_ids: Tensor | None,
|
| 834 |
+
decoder_attention_mask: Tensor | None,
|
| 835 |
+
model_kwargs: Mapping[str, Any],
|
| 836 |
+
) -> tuple[dict[str, Any], tuple[str, ...] | None]:
|
| 837 |
+
if hidden_state_source not in {"encoder", "decoder"}:
|
| 838 |
+
raise ValueError("hidden_state_source must be 'encoder' or 'decoder'.")
|
| 839 |
+
hidden_state_index = model_kwargs.get("hidden_state_index", -1)
|
| 840 |
+
if not isinstance(hidden_state_index, int) or isinstance(hidden_state_index, bool):
|
| 841 |
+
raise TypeError("hidden_state_index must be an integer.")
|
| 842 |
+
store_all_hidden_states = model_kwargs.get("store_all_hidden_states", False)
|
| 843 |
+
if not isinstance(store_all_hidden_states, bool):
|
| 844 |
+
raise TypeError("store_all_hidden_states must be a boolean.")
|
| 845 |
+
normalized_decoder_inputs: tuple[str, ...] | None = None
|
| 846 |
+
has_decoder_inputs = decoder_inputs is not None
|
| 847 |
+
has_decoder_ids = decoder_input_ids is not None
|
| 848 |
+
if hidden_state_source == "encoder":
|
| 849 |
+
if has_decoder_inputs or has_decoder_ids or decoder_attention_mask is not None:
|
| 850 |
+
raise ValueError("Decoder inputs are only valid when hidden_state_source='decoder'.")
|
| 851 |
+
else:
|
| 852 |
+
if has_decoder_inputs == has_decoder_ids:
|
| 853 |
+
raise ValueError(
|
| 854 |
+
"Decoder embedding requires exactly one of decoder_inputs or decoder_input_ids."
|
| 855 |
+
)
|
| 856 |
+
decoder_input_fingerprint: str | None = None
|
| 857 |
+
if decoder_inputs is not None:
|
| 858 |
+
if isinstance(decoder_inputs, (str, bytes)) or not isinstance(decoder_inputs, Sequence):
|
| 859 |
+
raise TypeError("decoder_inputs must be an aligned sequence of strings.")
|
| 860 |
+
normalized_decoder_inputs = tuple(decoder_inputs)
|
| 861 |
+
if not all(isinstance(value, str) and value for value in normalized_decoder_inputs):
|
| 862 |
+
raise ValueError("decoder_inputs must contain non-empty strings.")
|
| 863 |
+
if len(normalized_decoder_inputs) != len(records):
|
| 864 |
+
raise ValueError("decoder_inputs must align one-to-one with embedding inputs.")
|
| 865 |
+
decoder_input_fingerprint = _ordered_string_sha256(normalized_decoder_inputs)
|
| 866 |
+
if decoder_attention_mask is not None:
|
| 867 |
+
raise ValueError("decoder_attention_mask requires decoder_input_ids.")
|
| 868 |
+
if decoder_input_ids is not None:
|
| 869 |
+
if not isinstance(decoder_input_ids, Tensor) or decoder_input_ids.ndim != 2:
|
| 870 |
+
raise ValueError("decoder_input_ids must have shape (batch, sequence).")
|
| 871 |
+
if decoder_input_ids.shape[0] != len(records):
|
| 872 |
+
raise ValueError("decoder_input_ids must align one-to-one with embedding inputs.")
|
| 873 |
+
if decoder_input_ids.dtype == torch.bool or decoder_input_ids.is_floating_point():
|
| 874 |
+
raise TypeError("decoder_input_ids must use an integer token dtype.")
|
| 875 |
+
decoder_input_fingerprint = tensor_sha256(decoder_input_ids)
|
| 876 |
+
decoder_mask_fingerprint: str | None = None
|
| 877 |
+
if decoder_attention_mask is not None:
|
| 878 |
+
if not isinstance(decoder_attention_mask, Tensor):
|
| 879 |
+
raise TypeError("decoder_attention_mask must be a tensor.")
|
| 880 |
+
if decoder_input_ids is None or decoder_attention_mask.shape != decoder_input_ids.shape:
|
| 881 |
+
raise ValueError("decoder_attention_mask must match decoder_input_ids shape.")
|
| 882 |
+
decoder_mask_fingerprint = tensor_sha256(decoder_attention_mask)
|
| 883 |
+
|
| 884 |
+
context: dict[str, Any] = {
|
| 885 |
+
"hidden_state_source": hidden_state_source,
|
| 886 |
+
"hidden_state_index": hidden_state_index,
|
| 887 |
+
"store_all_hidden_states": store_all_hidden_states,
|
| 888 |
+
"decoder_input_fingerprint": decoder_input_fingerprint,
|
| 889 |
+
"decoder_attention_mask_fingerprint": decoder_mask_fingerprint,
|
| 890 |
+
"decoder_alignment": "input-position" if hidden_state_source == "decoder" else None,
|
| 891 |
+
}
|
| 892 |
+
metadata_hook = getattr(model, "_embedding_metadata", None)
|
| 893 |
+
model_metadata: Mapping[str, Any] | None = None
|
| 894 |
+
if callable(metadata_hook):
|
| 895 |
+
model_metadata = metadata_hook(**context)
|
| 896 |
+
if not isinstance(model_metadata, Mapping):
|
| 897 |
+
raise TypeError("_embedding_metadata must return a mapping.")
|
| 898 |
+
context["model_embedding"] = _fingerprint_jsonable(model_metadata)
|
| 899 |
+
if hidden_state_source == "decoder":
|
| 900 |
+
has_decoder_batch = callable(getattr(model, "_embedding_batch", None))
|
| 901 |
+
declares_decoder_stack = (
|
| 902 |
+
model_metadata is not None and model_metadata.get("hidden_state_stack") == "decoder"
|
| 903 |
+
)
|
| 904 |
+
if not has_decoder_batch or not declares_decoder_stack:
|
| 905 |
+
raise ValueError(
|
| 906 |
+
f"{model.__class__.__name__} does not declare decoder embedding support."
|
| 907 |
+
)
|
| 908 |
+
return context, normalized_decoder_inputs
|
| 909 |
+
|
| 910 |
+
|
| 911 |
+
def _planned_batches(
|
| 912 |
+
records: Sequence[EmbeddingInput],
|
| 913 |
+
positions: range,
|
| 914 |
+
*,
|
| 915 |
+
batch_size: int,
|
| 916 |
+
max_tokens_per_batch: int | None,
|
| 917 |
+
max_length: int | None,
|
| 918 |
+
truncate: bool,
|
| 919 |
+
) -> Iterator[list[int]]:
|
| 920 |
+
"""Length-bucket one bounded window while retaining stable output positions."""
|
| 921 |
+
|
| 922 |
+
def effective_length(position: int) -> int:
|
| 923 |
+
length = len(records[position].sequence)
|
| 924 |
+
return min(length, max_length) if truncate and max_length is not None else length
|
| 925 |
+
|
| 926 |
+
ordered = sorted(positions, key=lambda position: (-effective_length(position), position))
|
| 927 |
+
batch: list[int] = []
|
| 928 |
+
longest = 0
|
| 929 |
+
for position in ordered:
|
| 930 |
+
length = effective_length(position)
|
| 931 |
+
if max_tokens_per_batch is not None and length > max_tokens_per_batch:
|
| 932 |
+
raise ValueError(
|
| 933 |
+
f"Input at position {position} has {length} residues, exceeding "
|
| 934 |
+
f"max_tokens_per_batch={max_tokens_per_batch}."
|
| 935 |
+
)
|
| 936 |
+
candidate_longest = max(longest, length)
|
| 937 |
+
exceeds_tokens = (
|
| 938 |
+
max_tokens_per_batch is not None
|
| 939 |
+
and candidate_longest * (len(batch) + 1) > max_tokens_per_batch
|
| 940 |
+
)
|
| 941 |
+
if batch and (len(batch) >= batch_size or exceeds_tokens):
|
| 942 |
+
yield batch
|
| 943 |
+
batch = []
|
| 944 |
+
longest = 0
|
| 945 |
+
batch.append(position)
|
| 946 |
+
longest = max(longest, length)
|
| 947 |
+
if batch:
|
| 948 |
+
yield batch
|
| 949 |
+
|
| 950 |
+
|
| 951 |
+
def embed_dataset(
|
| 952 |
+
model: Any,
|
| 953 |
+
inputs: (Iterable[str | EmbeddingInput | tuple[str, str]] | Mapping[str, str] | str | Path),
|
| 954 |
+
*,
|
| 955 |
+
batch_size: int = 2,
|
| 956 |
+
pooling: str | Sequence[str] | None = None,
|
| 957 |
+
full_embeddings: bool = False,
|
| 958 |
+
output: str | Path | None = None,
|
| 959 |
+
format: str = "safetensors",
|
| 960 |
+
resume: bool = True,
|
| 961 |
+
tokenizer: Any | None = None,
|
| 962 |
+
max_length: int | None = None,
|
| 963 |
+
truncate: bool = True,
|
| 964 |
+
dtype: torch.dtype | None = torch.float32,
|
| 965 |
+
shard_size: int = 2 * 1024**3,
|
| 966 |
+
model_state_fingerprint: str | None = None,
|
| 967 |
+
batch_window_size: int | None = None,
|
| 968 |
+
max_tokens_per_batch: int | None = None,
|
| 969 |
+
hidden_state_source: str = "encoder",
|
| 970 |
+
decoder_inputs: Sequence[str] | None = None,
|
| 971 |
+
decoder_input_ids: Tensor | None = None,
|
| 972 |
+
decoder_attention_mask: Tensor | None = None,
|
| 973 |
+
_embedding_batch_fn: Callable[..., EmbeddingBatch] | None = None,
|
| 974 |
+
_embedding_batch_identity: Mapping[str, Any] | None = None,
|
| 975 |
+
_allowed_unsupported_pooling: Sequence[str] = (),
|
| 976 |
+
**model_kwargs: Any,
|
| 977 |
+
) -> EmbeddingResult:
|
| 978 |
+
"""Embed protein sequences with stable ordering and residue-only pooling."""
|
| 979 |
+
|
| 980 |
+
for name, value in (
|
| 981 |
+
("batch_size", batch_size),
|
| 982 |
+
("shard_size", shard_size),
|
| 983 |
+
):
|
| 984 |
+
if not isinstance(value, int) or isinstance(value, bool):
|
| 985 |
+
raise TypeError(f"{name} must be a positive integer.")
|
| 986 |
+
if value <= 0:
|
| 987 |
+
raise ValueError(f"{name} must be a positive integer.")
|
| 988 |
+
for optional_name, optional_value in (
|
| 989 |
+
("max_length", max_length),
|
| 990 |
+
("max_tokens_per_batch", max_tokens_per_batch),
|
| 991 |
+
("batch_window_size", batch_window_size),
|
| 992 |
+
):
|
| 993 |
+
if optional_value is not None and (
|
| 994 |
+
not isinstance(optional_value, int) or isinstance(optional_value, bool)
|
| 995 |
+
):
|
| 996 |
+
raise TypeError(f"{optional_name} must be a positive integer when provided.")
|
| 997 |
+
if optional_value is not None and optional_value <= 0:
|
| 998 |
+
raise ValueError(f"{optional_name} must be a positive integer when provided.")
|
| 999 |
+
for name, value in (
|
| 1000 |
+
("full_embeddings", full_embeddings),
|
| 1001 |
+
("resume", resume),
|
| 1002 |
+
("truncate", truncate),
|
| 1003 |
+
):
|
| 1004 |
+
if not isinstance(value, bool):
|
| 1005 |
+
raise TypeError(f"{name} must be a boolean.")
|
| 1006 |
+
if not isinstance(format, str):
|
| 1007 |
+
raise TypeError("format must be a string.")
|
| 1008 |
+
if output is not None and not isinstance(output, (str, Path)):
|
| 1009 |
+
raise TypeError("output must be a path or None.")
|
| 1010 |
+
if model_state_fingerprint is not None and (
|
| 1011 |
+
not isinstance(model_state_fingerprint, str) or not model_state_fingerprint
|
| 1012 |
+
):
|
| 1013 |
+
raise ValueError("model_state_fingerprint must be a non-empty string when provided.")
|
| 1014 |
+
if hidden_state_source not in {"encoder", "decoder"}:
|
| 1015 |
+
raise ValueError("hidden_state_source must be 'encoder' or 'decoder'.")
|
| 1016 |
+
hidden_state_index = model_kwargs.get("hidden_state_index", -1)
|
| 1017 |
+
if not isinstance(hidden_state_index, int) or isinstance(hidden_state_index, bool):
|
| 1018 |
+
raise TypeError("hidden_state_index must be an integer.")
|
| 1019 |
+
store_all_hidden_states = model_kwargs.get("store_all_hidden_states", False)
|
| 1020 |
+
if not isinstance(store_all_hidden_states, bool):
|
| 1021 |
+
raise TypeError("store_all_hidden_states must be a boolean.")
|
| 1022 |
+
if decoder_input_ids is not None:
|
| 1023 |
+
if not isinstance(decoder_input_ids, Tensor):
|
| 1024 |
+
raise TypeError("decoder_input_ids must be a tensor.")
|
| 1025 |
+
if decoder_input_ids.is_meta:
|
| 1026 |
+
raise ValueError("decoder_input_ids cannot be a meta tensor.")
|
| 1027 |
+
if decoder_input_ids.ndim != 2 or decoder_input_ids.shape[1] == 0:
|
| 1028 |
+
raise ValueError("decoder_input_ids must have non-empty shape (batch, sequence).")
|
| 1029 |
+
if decoder_input_ids.dtype not in {torch.int32, torch.int64}:
|
| 1030 |
+
raise TypeError("decoder_input_ids must use torch.int32 or torch.int64.")
|
| 1031 |
+
if decoder_attention_mask is not None:
|
| 1032 |
+
if not isinstance(decoder_attention_mask, Tensor):
|
| 1033 |
+
raise TypeError("decoder_attention_mask must be a tensor.")
|
| 1034 |
+
if decoder_attention_mask.is_meta:
|
| 1035 |
+
raise ValueError("decoder_attention_mask cannot be a meta tensor.")
|
| 1036 |
+
if decoder_attention_mask.is_complex() or not bool(
|
| 1037 |
+
torch.isfinite(decoder_attention_mask).all()
|
| 1038 |
+
):
|
| 1039 |
+
raise ValueError("decoder_attention_mask must contain finite binary values.")
|
| 1040 |
+
if not bool(((decoder_attention_mask == 0) | (decoder_attention_mask == 1)).all()):
|
| 1041 |
+
raise ValueError("decoder_attention_mask must contain finite binary values.")
|
| 1042 |
+
pooling_names = (
|
| 1043 |
+
(("mean",) if not full_embeddings else ())
|
| 1044 |
+
if pooling is None
|
| 1045 |
+
else ((pooling,) if isinstance(pooling, str) else tuple(pooling))
|
| 1046 |
+
)
|
| 1047 |
+
if full_embeddings and pooling is not None:
|
| 1048 |
+
raise ValueError("full_embeddings=True cannot be combined with pooling.")
|
| 1049 |
+
if not full_embeddings and not pooling_names:
|
| 1050 |
+
raise ValueError("pooling is required unless full_embeddings=True.")
|
| 1051 |
+
pooler = Pooler(pooling_names) if pooling_names else None
|
| 1052 |
+
|
| 1053 |
+
if batch_size <= 0:
|
| 1054 |
+
raise ValueError("batch_size must be positive.")
|
| 1055 |
+
if format == "pth" or (output is not None and Path(output).suffix.lower() == ".pth"):
|
| 1056 |
+
raise ValueError("Writing pickle-based .pth embeddings is not supported.")
|
| 1057 |
+
if format not in _SUPPORTED_STORAGE_FORMATS:
|
| 1058 |
+
raise ValueError("format must be 'safetensors' or 'sqlite'.")
|
| 1059 |
+
if max_length is not None and max_length <= 0:
|
| 1060 |
+
raise ValueError("max_length must be positive when provided.")
|
| 1061 |
+
if max_tokens_per_batch is not None and max_tokens_per_batch <= 0:
|
| 1062 |
+
raise ValueError("max_tokens_per_batch must be positive when provided.")
|
| 1063 |
+
if not isinstance(dtype, (torch.dtype, type(None))):
|
| 1064 |
+
raise TypeError("dtype must be a torch.dtype or None.")
|
| 1065 |
+
if batch_window_size is not None and batch_window_size <= 0:
|
| 1066 |
+
raise ValueError("batch_window_size must be positive when provided.")
|
| 1067 |
+
if _embedding_batch_fn is not None and not callable(_embedding_batch_fn):
|
| 1068 |
+
raise TypeError("_embedding_batch_fn must be callable when provided.")
|
| 1069 |
+
if _embedding_batch_fn is not None and _embedding_batch_identity is None:
|
| 1070 |
+
raise ValueError(
|
| 1071 |
+
"_embedding_batch_identity is required with _embedding_batch_fn so persisted "
|
| 1072 |
+
"runs bind the family-specific embedding behavior."
|
| 1073 |
+
)
|
| 1074 |
+
if _embedding_batch_identity is not None and not isinstance(_embedding_batch_identity, Mapping):
|
| 1075 |
+
raise TypeError("_embedding_batch_identity must be a mapping when provided.")
|
| 1076 |
+
if isinstance(_allowed_unsupported_pooling, (str, bytes)) or not isinstance(
|
| 1077 |
+
_allowed_unsupported_pooling, Sequence
|
| 1078 |
+
):
|
| 1079 |
+
raise TypeError("_allowed_unsupported_pooling must be a sequence of pooler names.")
|
| 1080 |
+
if not all(isinstance(name, str) for name in _allowed_unsupported_pooling):
|
| 1081 |
+
raise TypeError("_allowed_unsupported_pooling must contain only strings.")
|
| 1082 |
+
allowed_unsupported_pooling = frozenset(_allowed_unsupported_pooling)
|
| 1083 |
+
if allowed_unsupported_pooling and _embedding_batch_fn is None:
|
| 1084 |
+
raise ValueError(
|
| 1085 |
+
"_allowed_unsupported_pooling is only valid with a family-specific _embedding_batch_fn."
|
| 1086 |
+
)
|
| 1087 |
+
resolved_batch_window_size = (
|
| 1088 |
+
batch_size * _DEFAULT_BATCH_WINDOW_MULTIPLIER
|
| 1089 |
+
if batch_window_size is None
|
| 1090 |
+
else batch_window_size
|
| 1091 |
+
)
|
| 1092 |
+
if resolved_batch_window_size < batch_size:
|
| 1093 |
+
raise ValueError("batch_window_size must be at least batch_size.")
|
| 1094 |
+
records = _normalize_inputs(inputs, disk_backed=output is not None)
|
| 1095 |
+
_validate_untruncated_lengths(
|
| 1096 |
+
records,
|
| 1097 |
+
max_length=max_length,
|
| 1098 |
+
truncate=truncate,
|
| 1099 |
+
)
|
| 1100 |
+
pooling_names = (
|
| 1101 |
+
(("mean",) if not full_embeddings else ())
|
| 1102 |
+
if pooling is None
|
| 1103 |
+
else ((pooling,) if isinstance(pooling, str) else tuple(pooling))
|
| 1104 |
+
)
|
| 1105 |
+
if full_embeddings:
|
| 1106 |
+
if pooling is not None:
|
| 1107 |
+
raise ValueError("full_embeddings=True cannot be combined with pooling.")
|
| 1108 |
+
elif not pooling_names:
|
| 1109 |
+
raise ValueError("pooling is required unless full_embeddings=True.")
|
| 1110 |
+
store_all_hidden_states = bool(model_kwargs.get("store_all_hidden_states", False))
|
| 1111 |
+
if store_all_hidden_states and not full_embeddings:
|
| 1112 |
+
raise ValueError("store_all_hidden_states=True requires full_embeddings=True.")
|
| 1113 |
+
|
| 1114 |
+
unsupported = set(getattr(model, "embedding_unsupported_pooling", ()))
|
| 1115 |
+
unknown_pooling_overrides = allowed_unsupported_pooling.difference(unsupported)
|
| 1116 |
+
if unknown_pooling_overrides:
|
| 1117 |
+
raise ValueError(
|
| 1118 |
+
"_allowed_unsupported_pooling may only override poolers declared unsupported "
|
| 1119 |
+
f"by the model; unknown overrides: {sorted(unknown_pooling_overrides)}."
|
| 1120 |
+
)
|
| 1121 |
+
unsupported.difference_update(allowed_unsupported_pooling)
|
| 1122 |
+
requested_unsupported = unsupported.intersection(pooling_names)
|
| 1123 |
+
if requested_unsupported:
|
| 1124 |
+
raise ValueError(
|
| 1125 |
+
f"{model.__class__.__name__} does not support pooling operations "
|
| 1126 |
+
f"{sorted(requested_unsupported)}."
|
| 1127 |
+
)
|
| 1128 |
+
|
| 1129 |
+
# Constructing the pooler validates names and duplicate operations before
|
| 1130 |
+
# any checkpoint hashing, tokenization, or inference occurs.
|
| 1131 |
+
pooler = Pooler(pooling_names) if pooling_names else None
|
| 1132 |
+
embedding_context, normalized_decoder_inputs = _embedding_context(
|
| 1133 |
+
model,
|
| 1134 |
+
records,
|
| 1135 |
+
hidden_state_source=hidden_state_source,
|
| 1136 |
+
decoder_inputs=decoder_inputs,
|
| 1137 |
+
decoder_input_ids=decoder_input_ids,
|
| 1138 |
+
decoder_attention_mask=decoder_attention_mask,
|
| 1139 |
+
model_kwargs=model_kwargs,
|
| 1140 |
+
)
|
| 1141 |
+
if _embedding_batch_identity is not None:
|
| 1142 |
+
embedding_context["family_adapter"] = _fingerprint_jsonable(_embedding_batch_identity)
|
| 1143 |
+
if allowed_unsupported_pooling:
|
| 1144 |
+
embedding_context["family_adapter_pooling_override"] = sorted(
|
| 1145 |
+
allowed_unsupported_pooling
|
| 1146 |
+
)
|
| 1147 |
+
|
| 1148 |
+
tokenizer_metadata = _tokenizer_metadata(model, tokenizer)
|
| 1149 |
+
(
|
| 1150 |
+
input_fingerprint,
|
| 1151 |
+
run_fingerprint,
|
| 1152 |
+
resolved_model_state_fingerprint,
|
| 1153 |
+
model_state_fingerprint_source,
|
| 1154 |
+
) = _run_fingerprint(
|
| 1155 |
+
model,
|
| 1156 |
+
records,
|
| 1157 |
+
pooling=pooling_names,
|
| 1158 |
+
full_embeddings=full_embeddings,
|
| 1159 |
+
max_length=max_length,
|
| 1160 |
+
truncate=truncate,
|
| 1161 |
+
dtype=dtype,
|
| 1162 |
+
model_kwargs=model_kwargs,
|
| 1163 |
+
tokenizer_metadata=tokenizer_metadata,
|
| 1164 |
+
model_state_fingerprint=model_state_fingerprint,
|
| 1165 |
+
persist_output=output is not None,
|
| 1166 |
+
embedding_context=embedding_context,
|
| 1167 |
+
batch_size=batch_size,
|
| 1168 |
+
batch_window_size=resolved_batch_window_size,
|
| 1169 |
+
max_tokens_per_batch=max_tokens_per_batch,
|
| 1170 |
+
)
|
| 1171 |
+
output_already_exists = output is not None and _output_exists(output, format)
|
| 1172 |
+
existing: EmbeddingResult | None = None
|
| 1173 |
+
start_position = 0
|
| 1174 |
+
if output is not None and resume and output_already_exists:
|
| 1175 |
+
if format == "sqlite":
|
| 1176 |
+
try:
|
| 1177 |
+
existing = load_sqlite_result(output, run_id=run_fingerprint)
|
| 1178 |
+
except KeyError:
|
| 1179 |
+
existing = load_result(output, format=format)
|
| 1180 |
+
else:
|
| 1181 |
+
existing = load_result(output, format=format)
|
| 1182 |
+
if existing.metadata.get("fingerprint_schema_version") != (_RUN_FINGERPRINT_SCHEMA_VERSION):
|
| 1183 |
+
raise ValueError(
|
| 1184 |
+
"Existing embeddings use an incompatible run fingerprint schema; "
|
| 1185 |
+
"choose another output or set resume=False."
|
| 1186 |
+
)
|
| 1187 |
+
if existing.metadata.get("run_fingerprint") != run_fingerprint:
|
| 1188 |
+
raise ValueError(
|
| 1189 |
+
"Existing embeddings were produced by a different run fingerprint; "
|
| 1190 |
+
"choose another output or set resume=False."
|
| 1191 |
+
)
|
| 1192 |
+
if len(existing) > len(records):
|
| 1193 |
+
raise ValueError(
|
| 1194 |
+
"Existing embeddings are not an ordered prefix of the requested inputs."
|
| 1195 |
+
)
|
| 1196 |
+
prefix_matches = all(
|
| 1197 |
+
(observed.id, observed.sequence) == (expected.id, expected.sequence)
|
| 1198 |
+
for expected, observed in zip(records, existing, strict=False)
|
| 1199 |
+
)
|
| 1200 |
+
if not prefix_matches:
|
| 1201 |
+
raise ValueError(
|
| 1202 |
+
"Existing embeddings are not an ordered prefix of the requested inputs."
|
| 1203 |
+
)
|
| 1204 |
+
if len(existing) == len(records) and existing.metadata.get("complete", True):
|
| 1205 |
+
return existing
|
| 1206 |
+
start_position = len(existing)
|
| 1207 |
+
|
| 1208 |
+
sqlite_run_id: str | None = None
|
| 1209 |
+
sqlite_replace_on_first_commit = False
|
| 1210 |
+
sqlite_initial_metadata: dict[str, Any] | None = None
|
| 1211 |
+
if output is not None and format == "sqlite":
|
| 1212 |
+
sqlite_initial_metadata = {
|
| 1213 |
+
"format_version": 1,
|
| 1214 |
+
"fingerprint_schema_version": _RUN_FINGERPRINT_SCHEMA_VERSION,
|
| 1215 |
+
"run_fingerprint": run_fingerprint,
|
| 1216 |
+
"input_fingerprint": input_fingerprint,
|
| 1217 |
+
"model_state_fingerprint": resolved_model_state_fingerprint,
|
| 1218 |
+
"model_state_fingerprint_source": model_state_fingerprint_source,
|
| 1219 |
+
"complete": False,
|
| 1220 |
+
}
|
| 1221 |
+
sqlite_run_id = run_fingerprint
|
| 1222 |
+
if not resume and output_already_exists:
|
| 1223 |
+
try:
|
| 1224 |
+
load_sqlite_result(output, run_id=run_fingerprint)
|
| 1225 |
+
except KeyError:
|
| 1226 |
+
pass
|
| 1227 |
+
else:
|
| 1228 |
+
# Keep an exact prior run readable until replacement inference
|
| 1229 |
+
# has produced the first complete commit window.
|
| 1230 |
+
sqlite_replace_on_first_commit = True
|
| 1231 |
+
if not sqlite_replace_on_first_commit:
|
| 1232 |
+
initialize_sqlite_run(
|
| 1233 |
+
output,
|
| 1234 |
+
sqlite_initial_metadata,
|
| 1235 |
+
resume=resume,
|
| 1236 |
+
)
|
| 1237 |
+
|
| 1238 |
+
stream_safetensors = output is not None and format == "safetensors"
|
| 1239 |
+
attention_backend = _attention_backend(model)
|
| 1240 |
+
output_records: list[EmbeddingRecord] = (
|
| 1241 |
+
[] if sqlite_run_id is not None or stream_safetensors else list(existing or ())
|
| 1242 |
+
)
|
| 1243 |
+
output_descriptors: list[dict[str, Any]] | None = [] if output is None else None
|
| 1244 |
+
pool_slices: dict[str, tuple[int, int]] = {}
|
| 1245 |
+
if existing and pooler is not None:
|
| 1246 |
+
pooled_width = existing[0].load_tensor().shape[-1]
|
| 1247 |
+
if pooled_width % len(pooling_names) != 0:
|
| 1248 |
+
raise ValueError("Stored pooled width is inconsistent with pooling metadata.")
|
| 1249 |
+
pool_slices = pooler.output_slices(pooled_width // len(pooling_names))
|
| 1250 |
+
|
| 1251 |
+
safetensors_writer: SafetensorsStreamWriter | None = None
|
| 1252 |
+
if stream_safetensors:
|
| 1253 |
+
if output is None:
|
| 1254 |
+
raise RuntimeError("Safetensors streaming was enabled without an output destination.")
|
| 1255 |
+
transactional_overwrite = output_already_exists and not resume
|
| 1256 |
+
safetensors_writer = SafetensorsStreamWriter(
|
| 1257 |
+
output,
|
| 1258 |
+
{
|
| 1259 |
+
"format_version": 1,
|
| 1260 |
+
"fingerprint_schema_version": _RUN_FINGERPRINT_SCHEMA_VERSION,
|
| 1261 |
+
"run_fingerprint": run_fingerprint,
|
| 1262 |
+
"input_fingerprint": input_fingerprint,
|
| 1263 |
+
"model_state_fingerprint": resolved_model_state_fingerprint,
|
| 1264 |
+
"model_state_fingerprint_source": model_state_fingerprint_source,
|
| 1265 |
+
"complete": False,
|
| 1266 |
+
},
|
| 1267 |
+
shard_size=shard_size,
|
| 1268 |
+
existing=existing or (),
|
| 1269 |
+
reuse_existing=bool(resume and existing is not None),
|
| 1270 |
+
publish_initial=not transactional_overwrite,
|
| 1271 |
+
publish_incremental=not transactional_overwrite,
|
| 1272 |
+
)
|
| 1273 |
+
need_attentions = "parti" in pooling_names
|
| 1274 |
+
|
| 1275 |
+
config = getattr(model, "config", None)
|
| 1276 |
+
model_type = str(getattr(config, "model_type", "")).lower()
|
| 1277 |
+
resolved_tokenizer = tokenizer if tokenizer is not None else getattr(model, "tokenizer", None)
|
| 1278 |
+
with _temporary_eval(model), torch.inference_mode():
|
| 1279 |
+
for window_start in range(start_position, len(records), resolved_batch_window_size):
|
| 1280 |
+
window_stop = min(window_start + resolved_batch_window_size, len(records))
|
| 1281 |
+
window_records = records[window_start:window_stop]
|
| 1282 |
+
if not isinstance(window_records, Sequence):
|
| 1283 |
+
raise RuntimeError("The immutable embedding spool returned a non-sequence window.")
|
| 1284 |
+
window_results: dict[int, EmbeddingRecord] = {}
|
| 1285 |
+
for local_positions in _planned_batches(
|
| 1286 |
+
window_records,
|
| 1287 |
+
range(len(window_records)),
|
| 1288 |
+
batch_size=batch_size,
|
| 1289 |
+
max_tokens_per_batch=max_tokens_per_batch,
|
| 1290 |
+
max_length=max_length,
|
| 1291 |
+
truncate=truncate,
|
| 1292 |
+
):
|
| 1293 |
+
batch_positions = [window_start + position for position in local_positions]
|
| 1294 |
+
batch_records = [window_records[position] for position in local_positions]
|
| 1295 |
+
sequences = [
|
| 1296 |
+
record.sequence[:max_length]
|
| 1297 |
+
if truncate and max_length is not None
|
| 1298 |
+
else record.sequence
|
| 1299 |
+
for record in batch_records
|
| 1300 |
+
]
|
| 1301 |
+
batch_model_kwargs = dict(model_kwargs)
|
| 1302 |
+
if model_type == "fast_ankh" or hidden_state_source == "decoder":
|
| 1303 |
+
batch_model_kwargs["hidden_state_source"] = hidden_state_source
|
| 1304 |
+
if normalized_decoder_inputs is not None:
|
| 1305 |
+
batch_model_kwargs["decoder_inputs"] = [
|
| 1306 |
+
normalized_decoder_inputs[position] for position in batch_positions
|
| 1307 |
+
]
|
| 1308 |
+
if decoder_input_ids is not None:
|
| 1309 |
+
indices = torch.tensor(
|
| 1310 |
+
batch_positions,
|
| 1311 |
+
device=decoder_input_ids.device,
|
| 1312 |
+
dtype=torch.long,
|
| 1313 |
+
)
|
| 1314 |
+
batch_model_kwargs["decoder_input_ids"] = decoder_input_ids.index_select(
|
| 1315 |
+
0, indices
|
| 1316 |
+
)
|
| 1317 |
+
if decoder_attention_mask is not None:
|
| 1318 |
+
indices = torch.tensor(
|
| 1319 |
+
batch_positions,
|
| 1320 |
+
device=decoder_attention_mask.device,
|
| 1321 |
+
dtype=torch.long,
|
| 1322 |
+
)
|
| 1323 |
+
batch_model_kwargs["decoder_attention_mask"] = (
|
| 1324 |
+
decoder_attention_mask.index_select(0, indices)
|
| 1325 |
+
)
|
| 1326 |
+
custom_batch = _embedding_batch_fn or getattr(model, "_embedding_batch", None)
|
| 1327 |
+
if custom_batch is not None:
|
| 1328 |
+
if model_type == "fast_ankh":
|
| 1329 |
+
batch = custom_batch(
|
| 1330 |
+
sequences,
|
| 1331 |
+
tokenizer=resolved_tokenizer,
|
| 1332 |
+
max_length=max_length,
|
| 1333 |
+
truncate=truncate,
|
| 1334 |
+
need_attentions=need_attentions,
|
| 1335 |
+
**batch_model_kwargs,
|
| 1336 |
+
)
|
| 1337 |
+
else:
|
| 1338 |
+
batch = custom_batch(sequences, **batch_model_kwargs)
|
| 1339 |
+
if not isinstance(batch, EmbeddingBatch):
|
| 1340 |
+
raise TypeError("_embedding_batch must return EmbeddingBatch.")
|
| 1341 |
+
else:
|
| 1342 |
+
batch = _generic_embedding_batch(
|
| 1343 |
+
model,
|
| 1344 |
+
sequences,
|
| 1345 |
+
tokenizer=tokenizer,
|
| 1346 |
+
max_length=max_length,
|
| 1347 |
+
truncate=truncate,
|
| 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:
|
| 1356 |
+
raise ValueError("Embedding batches cannot contain meta tensors.")
|
| 1357 |
+
if not X.is_floating_point():
|
| 1358 |
+
raise TypeError("Embedding batches must use a floating-point X dtype.")
|
| 1359 |
+
if raw_mask.is_complex() or not bool(torch.isfinite(raw_mask).all()):
|
| 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)
|
| 1367 |
+
and X.shape[-1] > 0
|
| 1368 |
+
and M.shape == X.shape[:2]
|
| 1369 |
+
)
|
| 1370 |
+
valid_all_states_shape = (
|
| 1371 |
+
X.ndim == 4
|
| 1372 |
+
and store_all_hidden_states
|
| 1373 |
+
and full_embeddings
|
| 1374 |
+
and X.shape[0] == len(batch_records)
|
| 1375 |
+
and X.shape[1] > 0
|
| 1376 |
+
and X.shape[-1] > 0
|
| 1377 |
+
and M.shape == (X.shape[0], X.shape[2])
|
| 1378 |
+
)
|
| 1379 |
+
if not (valid_X_shape or valid_all_states_shape):
|
| 1380 |
+
raise ValueError(
|
| 1381 |
+
"Embedding batches must provide X with shape (b, l, d), or "
|
| 1382 |
+
"(b, states, l, d) when storing all hidden states, and "
|
| 1383 |
+
"residue_mask with shape (b, l)."
|
| 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]
|
| 1391 |
+
)
|
| 1392 |
+
if not bool(finite_selected.all()):
|
| 1393 |
+
raise ValueError("Biological residue embeddings produced non-finite output.")
|
| 1394 |
+
if need_attentions:
|
| 1395 |
+
# Validate the biological graph only after mask integrity is established.
|
| 1396 |
+
_validate_parti_length(M)
|
| 1397 |
+
if dtype is not None:
|
| 1398 |
+
X = X.to(dtype=dtype)
|
| 1399 |
+
|
| 1400 |
+
if full_embeddings:
|
| 1401 |
+
if X.ndim == 4:
|
| 1402 |
+
values = [
|
| 1403 |
+
X_i[:, M_i, :].detach().cpu() for X_i, M_i in zip(X, M, strict=True)
|
| 1404 |
+
]
|
| 1405 |
+
else:
|
| 1406 |
+
values = [X_i[M_i].detach().cpu() for X_i, M_i in zip(X, M, strict=True)]
|
| 1407 |
+
else:
|
| 1408 |
+
if pooler is None:
|
| 1409 |
+
raise RuntimeError(
|
| 1410 |
+
"Pooled embedding output was requested without an initialized pooler."
|
| 1411 |
+
)
|
| 1412 |
+
Y = pooler(
|
| 1413 |
+
X,
|
| 1414 |
+
M,
|
| 1415 |
+
attentions=batch.attentions,
|
| 1416 |
+
attention_backend=attention_backend,
|
| 1417 |
+
)
|
| 1418 |
+
pool_slices = pooler.output_slices(X.shape[-1])
|
| 1419 |
+
values = list(Y.detach().cpu().unbind(0))
|
| 1420 |
+
for position, record, value in zip(
|
| 1421 |
+
batch_positions, batch_records, values, strict=True
|
| 1422 |
+
):
|
| 1423 |
+
window_results[position] = EmbeddingRecord(record.id, record.sequence, value)
|
| 1424 |
+
|
| 1425 |
+
new_records = [
|
| 1426 |
+
window_results[position] for position in range(window_start, window_stop)
|
| 1427 |
+
]
|
| 1428 |
+
if output_descriptors is not None:
|
| 1429 |
+
output_descriptors.extend(
|
| 1430 |
+
_output_descriptor(window_start + offset, record)
|
| 1431 |
+
for offset, record in enumerate(new_records)
|
| 1432 |
+
)
|
| 1433 |
+
if output is not None and sqlite_run_id is not None:
|
| 1434 |
+
append_sqlite_records(
|
| 1435 |
+
output,
|
| 1436 |
+
sqlite_run_id,
|
| 1437 |
+
window_start,
|
| 1438 |
+
new_records,
|
| 1439 |
+
replace_metadata=(
|
| 1440 |
+
sqlite_initial_metadata if sqlite_replace_on_first_commit else None
|
| 1441 |
+
),
|
| 1442 |
+
)
|
| 1443 |
+
sqlite_replace_on_first_commit = False
|
| 1444 |
+
elif safetensors_writer is not None:
|
| 1445 |
+
safetensors_writer.append(new_records)
|
| 1446 |
+
else:
|
| 1447 |
+
output_records.extend(new_records)
|
| 1448 |
+
|
| 1449 |
+
software_versions = _software_versions()
|
| 1450 |
+
projection = getattr(model, "embedding_projection", None)
|
| 1451 |
+
resolved_layer = getattr(
|
| 1452 |
+
model,
|
| 1453 |
+
"embedding_layer",
|
| 1454 |
+
model_kwargs.get("hidden_state_index", -1),
|
| 1455 |
+
)
|
| 1456 |
+
token_policy = getattr(
|
| 1457 |
+
model,
|
| 1458 |
+
"embedding_token_policy",
|
| 1459 |
+
{
|
| 1460 |
+
"unit": "residue",
|
| 1461 |
+
"include": ["biological residues"],
|
| 1462 |
+
"exclude": [
|
| 1463 |
+
"BOS",
|
| 1464 |
+
"EOS",
|
| 1465 |
+
"padding",
|
| 1466 |
+
"chain delimiters",
|
| 1467 |
+
"non-protein tokens",
|
| 1468 |
+
],
|
| 1469 |
+
},
|
| 1470 |
+
)
|
| 1471 |
+
model_identity = _model_identity_metadata(model)
|
| 1472 |
+
metadata: dict[str, Any] = {
|
| 1473 |
+
"format_version": 1,
|
| 1474 |
+
"fingerprint_schema_version": _RUN_FINGERPRINT_SCHEMA_VERSION,
|
| 1475 |
+
"run_fingerprint": run_fingerprint,
|
| 1476 |
+
"input_fingerprint": input_fingerprint,
|
| 1477 |
+
"model_state_fingerprint": resolved_model_state_fingerprint,
|
| 1478 |
+
"model_state_fingerprint_source": model_state_fingerprint_source,
|
| 1479 |
+
"model_class": f"{model.__class__.__module__}.{model.__class__.__qualname__}",
|
| 1480 |
+
**model_identity,
|
| 1481 |
+
"dtype": str(dtype).removeprefix("torch.") if dtype is not None else "model",
|
| 1482 |
+
"attention_backend": attention_backend,
|
| 1483 |
+
"attention_kernel": _attention_kernel_metadata(attention_backend),
|
| 1484 |
+
"layer": resolved_layer,
|
| 1485 |
+
"projection": projection,
|
| 1486 |
+
"esmc_source": getattr(model, "_esmc_source", None),
|
| 1487 |
+
"esmc_revision": getattr(model, "_esmc_source_revision", None),
|
| 1488 |
+
"esmc_files": getattr(model, "_esmc_source_files", None),
|
| 1489 |
+
"token_policy": token_policy,
|
| 1490 |
+
"tokenizer": tokenizer_metadata,
|
| 1491 |
+
**embedding_context,
|
| 1492 |
+
"pooling": list(pooling_names),
|
| 1493 |
+
"pool_slices": pool_slices,
|
| 1494 |
+
"full_embeddings": full_embeddings,
|
| 1495 |
+
"max_length": max_length,
|
| 1496 |
+
"truncate": truncate,
|
| 1497 |
+
"truncation": {"enabled": truncate, "max_length": max_length},
|
| 1498 |
+
"batching": {
|
| 1499 |
+
"batch_size": batch_size,
|
| 1500 |
+
"batch_window_size": resolved_batch_window_size,
|
| 1501 |
+
"max_tokens_per_batch": max_tokens_per_batch,
|
| 1502 |
+
"input_storage": ("disk-spool" if isinstance(records, _InputSpool) else "memory"),
|
| 1503 |
+
"ordering": "bounded-length-bucketed-stable-output",
|
| 1504 |
+
"resume_commit_granularity": (
|
| 1505 |
+
"not-applicable"
|
| 1506 |
+
if output is None
|
| 1507 |
+
else "batch-window"
|
| 1508 |
+
if format == "sqlite"
|
| 1509 |
+
else "shard-flush"
|
| 1510 |
+
),
|
| 1511 |
+
},
|
| 1512 |
+
"residue_mask_policy": "biological-residues-only",
|
| 1513 |
+
"record_count": len(records),
|
| 1514 |
+
"descriptor_index": (
|
| 1515 |
+
"memory-metadata"
|
| 1516 |
+
if output is None
|
| 1517 |
+
else "sqlite-records"
|
| 1518 |
+
if format == "sqlite"
|
| 1519 |
+
else "safetensors-generation-index"
|
| 1520 |
+
),
|
| 1521 |
+
"storage_format": format if output is not None else "memory",
|
| 1522 |
+
"software": software_versions,
|
| 1523 |
+
"execution": _execution_identity_metadata(model),
|
| 1524 |
+
"adapter": _adapter_identity_metadata(model),
|
| 1525 |
+
"torch_version": software_versions["torch"],
|
| 1526 |
+
"transformers_version": software_versions["transformers"],
|
| 1527 |
+
"complete": True,
|
| 1528 |
+
}
|
| 1529 |
+
if output_descriptors is not None:
|
| 1530 |
+
metadata["outputs"] = output_descriptors
|
| 1531 |
+
metadata["tensor_hashes"] = [item["sha256"] for item in output_descriptors]
|
| 1532 |
+
status = getattr(model, "esmc_precision_status", None)
|
| 1533 |
+
if status is not None:
|
| 1534 |
+
metadata["esmc_precision"] = status.as_dict() if hasattr(status, "as_dict") else status
|
| 1535 |
+
if output is not None and sqlite_run_id is not None:
|
| 1536 |
+
update_sqlite_run_metadata(output, sqlite_run_id, metadata)
|
| 1537 |
+
return load_sqlite_result(output, run_id=sqlite_run_id)
|
| 1538 |
+
if safetensors_writer is not None:
|
| 1539 |
+
return safetensors_writer.publish(complete=True, metadata=metadata)
|
| 1540 |
+
result = EmbeddingResult(output_records, metadata)
|
| 1541 |
+
if output is not None:
|
| 1542 |
+
return save_result(result, output, format=format, shard_size=shard_size)
|
| 1543 |
+
return result
|
| 1544 |
+
|
| 1545 |
+
|
| 1546 |
+
class EmbeddingMixin:
|
| 1547 |
+
"""Small delegation mixin shared by FastPLMs model classes."""
|
| 1548 |
+
|
| 1549 |
+
def embed_dataset(self, inputs: Any, **kwargs: Any) -> EmbeddingResult:
|
| 1550 |
+
return embed_dataset(self, inputs, **kwargs)
|
| 1551 |
+
|
| 1552 |
+
|
| 1553 |
+
__all__ = [
|
| 1554 |
+
"EmbeddingMixin",
|
| 1555 |
+
"embed_dataset",
|
| 1556 |
+
"iter_fasta",
|
| 1557 |
+
"parse_fasta",
|
| 1558 |
+
"select_hidden_state_embeddings",
|
| 1559 |
+
]
|
fastplms/embeddings/storage.py
ADDED
|
@@ -0,0 +1,1594 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Lossless, reproducible storage for :mod:`fastplms.embeddings`."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
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 (
|
| 21 |
+
EmbeddingRecord,
|
| 22 |
+
EmbeddingResult,
|
| 23 |
+
LazyTensorReference,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
_DTYPE_NAMES: dict[torch.dtype, str] = {
|
| 27 |
+
torch.float16: "float16",
|
| 28 |
+
torch.bfloat16: "bfloat16",
|
| 29 |
+
torch.float32: "float32",
|
| 30 |
+
torch.float64: "float64",
|
| 31 |
+
torch.int64: "int64",
|
| 32 |
+
torch.int32: "int32",
|
| 33 |
+
torch.int16: "int16",
|
| 34 |
+
torch.int8: "int8",
|
| 35 |
+
torch.uint8: "uint8",
|
| 36 |
+
torch.bool: "bool",
|
| 37 |
+
}
|
| 38 |
+
_NAME_DTYPES = {name: dtype for dtype, name in _DTYPE_NAMES.items()}
|
| 39 |
+
DEFAULT_SHARD_SIZE = 2 * 1024**3
|
| 40 |
+
_MAX_RECORDS_PER_DESCRIPTOR_SHARD = 1_024
|
| 41 |
+
_TENSOR_HASH_CHUNK_BYTES = 16 * 1024**2
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _jsonable(value: Any) -> Any:
|
| 45 |
+
if isinstance(value, dict):
|
| 46 |
+
return {str(key): _jsonable(item) for key, item in value.items()}
|
| 47 |
+
if isinstance(value, (list, tuple)):
|
| 48 |
+
return [_jsonable(item) for item in value]
|
| 49 |
+
if isinstance(value, Path):
|
| 50 |
+
return str(value)
|
| 51 |
+
if isinstance(value, torch.dtype):
|
| 52 |
+
return str(value).removeprefix("torch.")
|
| 53 |
+
if isinstance(value, torch.device):
|
| 54 |
+
return str(value)
|
| 55 |
+
if value is None or isinstance(value, (str, int, float, bool)):
|
| 56 |
+
return value
|
| 57 |
+
return repr(value)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _persistent_metadata(
|
| 61 |
+
metadata: dict[str, Any],
|
| 62 |
+
*,
|
| 63 |
+
descriptor_index: str,
|
| 64 |
+
record_count: int | None = None,
|
| 65 |
+
) -> dict[str, Any]:
|
| 66 |
+
"""Remove per-record copies from metadata and identify the authoritative index."""
|
| 67 |
+
|
| 68 |
+
cleaned_value = _jsonable(metadata)
|
| 69 |
+
if not isinstance(cleaned_value, dict):
|
| 70 |
+
raise TypeError("Embedding metadata must serialize to a JSON object.")
|
| 71 |
+
cleaned: dict[str, Any] = cleaned_value
|
| 72 |
+
cleaned.pop("outputs", None)
|
| 73 |
+
cleaned.pop("tensor_hashes", None)
|
| 74 |
+
cleaned["descriptor_index"] = descriptor_index
|
| 75 |
+
if record_count is not None:
|
| 76 |
+
cleaned["record_count"] = record_count
|
| 77 |
+
return cleaned
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _tensor_bytes(X: Tensor) -> bytes:
|
| 81 |
+
"""Return the exact contiguous byte representation of X."""
|
| 82 |
+
|
| 83 |
+
X = X.detach().cpu().contiguous()
|
| 84 |
+
return X.view(torch.uint8).numpy().tobytes()
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _bounded_tensor_chunks(X: Tensor, max_bytes: int) -> Iterator[Tensor]:
|
| 88 |
+
"""Yield row-major CPU chunks without materializing one full byte string."""
|
| 89 |
+
|
| 90 |
+
flattened = X.detach().to(device="cpu").reshape(-1)
|
| 91 |
+
if flattened.numel() == 0:
|
| 92 |
+
return
|
| 93 |
+
chunk_elements = max(1, max_bytes // flattened.element_size())
|
| 94 |
+
for start in range(0, flattened.numel(), chunk_elements):
|
| 95 |
+
chunk = flattened[start : start + chunk_elements]
|
| 96 |
+
if chunk.stride(0) != 1:
|
| 97 |
+
chunk = chunk.clone(memory_format=torch.contiguous_format)
|
| 98 |
+
yield chunk
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _tensor_hash_chunks(X: Tensor) -> Iterator[bytes]:
|
| 102 |
+
for chunk in _bounded_tensor_chunks(X, _TENSOR_HASH_CHUNK_BYTES):
|
| 103 |
+
yield chunk.view(torch.uint8).numpy().tobytes()
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def tensor_sha256(X: Tensor) -> str:
|
| 107 |
+
"""Hash dtype, shape, and exact tensor bytes."""
|
| 108 |
+
|
| 109 |
+
if not isinstance(X, Tensor):
|
| 110 |
+
raise TypeError("X must be a tensor.")
|
| 111 |
+
if X.dtype not in _DTYPE_NAMES:
|
| 112 |
+
raise TypeError(f"Unsupported tensor dtype {X.dtype}.")
|
| 113 |
+
if X.is_meta:
|
| 114 |
+
raise ValueError("Cannot hash a meta tensor without storage.")
|
| 115 |
+
if X.layout != torch.strided:
|
| 116 |
+
raise TypeError("Only strided tensors can be hashed.")
|
| 117 |
+
digest = hashlib.sha256()
|
| 118 |
+
digest.update(_DTYPE_NAMES[X.dtype].encode())
|
| 119 |
+
digest.update(json.dumps(tuple(X.shape)).encode())
|
| 120 |
+
for chunk in _tensor_hash_chunks(X):
|
| 121 |
+
digest.update(chunk)
|
| 122 |
+
return digest.hexdigest()
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def _encode_tensor(X: Tensor) -> tuple[str, str, bytes]:
|
| 126 |
+
if X.dtype not in _DTYPE_NAMES:
|
| 127 |
+
raise TypeError(f"Unsupported tensor dtype {X.dtype}.")
|
| 128 |
+
shape = json.dumps(tuple(X.shape), separators=(",", ":"))
|
| 129 |
+
return _DTYPE_NAMES[X.dtype], shape, _tensor_bytes(X)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _decode_tensor(dtype_name: str, shape_json: str, data: bytes) -> Tensor:
|
| 133 |
+
try:
|
| 134 |
+
dtype = _NAME_DTYPES[dtype_name]
|
| 135 |
+
except KeyError as error:
|
| 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:
|
| 145 |
+
path = Path(path)
|
| 146 |
+
if path.suffix == ".json":
|
| 147 |
+
return path
|
| 148 |
+
if path.suffix == ".safetensors":
|
| 149 |
+
return path.with_suffix(".json")
|
| 150 |
+
return path / "index.json"
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def _run_manifest_path(path: str | Path) -> Path:
|
| 154 |
+
path = Path(path)
|
| 155 |
+
if path.name == "index.json":
|
| 156 |
+
return path.with_name("run.json")
|
| 157 |
+
if path.suffix == ".json":
|
| 158 |
+
return path.with_name(f"{path.stem}.run.json")
|
| 159 |
+
if path.suffix == ".safetensors":
|
| 160 |
+
return path.with_suffix(".run.json")
|
| 161 |
+
return path / "run.json"
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def _resolve_index_child(root: Path, relative: str, *, label: str) -> Path:
|
| 165 |
+
relative_path = Path(relative)
|
| 166 |
+
candidate = (root / relative_path).resolve()
|
| 167 |
+
if relative_path.is_absolute() or candidate.parent != root.resolve():
|
| 168 |
+
raise ValueError(f"Safetensors {label} references a file outside its output directory.")
|
| 169 |
+
return candidate
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def _canonical_json_bytes(payload: dict[str, Any]) -> bytes:
|
| 173 |
+
return (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode("utf-8")
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def _load_authoritative_index(
|
| 177 |
+
path: str | Path,
|
| 178 |
+
) -> tuple[dict[str, Any], Path, dict[str, Any]]:
|
| 179 |
+
"""Load the index selected by the atomic run-manifest commit record."""
|
| 180 |
+
|
| 181 |
+
stable_index_path = _index_path(path)
|
| 182 |
+
run_manifest_path = _run_manifest_path(path)
|
| 183 |
+
if not run_manifest_path.is_file():
|
| 184 |
+
raise ValueError(f"Missing safetensors run manifest: {run_manifest_path}.")
|
| 185 |
+
run_manifest = json.loads(run_manifest_path.read_text(encoding="utf-8"))
|
| 186 |
+
if not isinstance(run_manifest, dict):
|
| 187 |
+
raise ValueError("Safetensors run manifest must contain a JSON object.")
|
| 188 |
+
if run_manifest.get("format") != "fastplms-embedding-run":
|
| 189 |
+
raise ValueError(f"Not a FastPLMs embedding run manifest: {run_manifest_path}.")
|
| 190 |
+
version = run_manifest.get("version")
|
| 191 |
+
index_reference = run_manifest.get("index")
|
| 192 |
+
if not isinstance(index_reference, dict):
|
| 193 |
+
raise ValueError("Safetensors run manifest contains an invalid index reference.")
|
| 194 |
+
if version == 1:
|
| 195 |
+
snapshot = run_manifest.get("index_payload")
|
| 196 |
+
if isinstance(snapshot, dict):
|
| 197 |
+
payload = snapshot
|
| 198 |
+
index_bytes = _canonical_json_bytes(payload)
|
| 199 |
+
elif snapshot is None:
|
| 200 |
+
index_bytes = stable_index_path.read_bytes()
|
| 201 |
+
payload = json.loads(index_bytes.decode("utf-8"))
|
| 202 |
+
if not isinstance(payload, dict):
|
| 203 |
+
raise ValueError("Safetensors index must contain a JSON object.")
|
| 204 |
+
else:
|
| 205 |
+
raise ValueError("Safetensors run manifest contains an invalid index snapshot.")
|
| 206 |
+
expected = {
|
| 207 |
+
"file": stable_index_path.name,
|
| 208 |
+
"sha256": hashlib.sha256(index_bytes).hexdigest(),
|
| 209 |
+
}
|
| 210 |
+
index_path = stable_index_path
|
| 211 |
+
elif version == 2:
|
| 212 |
+
relative = index_reference.get("file")
|
| 213 |
+
if not isinstance(relative, str):
|
| 214 |
+
raise ValueError("Safetensors run manifest index file is invalid.")
|
| 215 |
+
index_path = _resolve_index_child(stable_index_path.parent, relative, label="run manifest")
|
| 216 |
+
index_bytes = index_path.read_bytes()
|
| 217 |
+
payload = json.loads(index_bytes.decode("utf-8"))
|
| 218 |
+
if not isinstance(payload, dict):
|
| 219 |
+
raise ValueError("Safetensors generation index must contain a JSON object.")
|
| 220 |
+
if payload.get("version") != 2:
|
| 221 |
+
raise ValueError("Safetensors v2 run manifest must reference a v2 generation index.")
|
| 222 |
+
expected = {
|
| 223 |
+
"file": relative,
|
| 224 |
+
"sha256": hashlib.sha256(index_bytes).hexdigest(),
|
| 225 |
+
}
|
| 226 |
+
else:
|
| 227 |
+
raise ValueError(f"Unsupported safetensors run manifest version {version!r}.")
|
| 228 |
+
if index_reference != expected:
|
| 229 |
+
raise ValueError("Safetensors run manifest does not match its index.")
|
| 230 |
+
if payload.get("format") != "fastplms-embedding-safetensors":
|
| 231 |
+
raise ValueError(f"Not a FastPLMs embedding index: {index_path}.")
|
| 232 |
+
record_count = payload.get("record_count")
|
| 233 |
+
if record_count is None:
|
| 234 |
+
legacy_records = payload.get("records", ())
|
| 235 |
+
if not isinstance(legacy_records, list):
|
| 236 |
+
raise ValueError("Safetensors index contains invalid records.")
|
| 237 |
+
record_count = len(legacy_records)
|
| 238 |
+
if not isinstance(record_count, int) or isinstance(record_count, bool) or record_count < 0:
|
| 239 |
+
raise ValueError("Safetensors record count must be a non-negative integer.")
|
| 240 |
+
if run_manifest.get("record_count") != record_count:
|
| 241 |
+
raise ValueError("Safetensors run manifest record count does not match its index.")
|
| 242 |
+
metadata = payload.get("metadata", {})
|
| 243 |
+
if not isinstance(metadata, dict):
|
| 244 |
+
raise ValueError("Safetensors index metadata must contain a JSON object.")
|
| 245 |
+
if metadata.get("record_count", record_count) != record_count:
|
| 246 |
+
raise ValueError("Safetensors metadata record count does not match its index.")
|
| 247 |
+
if version == 1 and run_manifest.get("metadata") != payload.get("metadata"):
|
| 248 |
+
raise ValueError("Safetensors run manifest metadata does not match its index.")
|
| 249 |
+
return payload, index_path, run_manifest
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def safetensors_result_exists(path: str | Path) -> bool:
|
| 253 |
+
"""Return whether an authoritative committed safetensors run exists."""
|
| 254 |
+
|
| 255 |
+
try:
|
| 256 |
+
_load_authoritative_index(path)
|
| 257 |
+
except (OSError, ValueError, json.JSONDecodeError):
|
| 258 |
+
return False
|
| 259 |
+
return True
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def _load_safetensor(path: Path, key: str) -> Tensor:
|
| 263 |
+
try:
|
| 264 |
+
from safetensors import safe_open
|
| 265 |
+
except ImportError as error:
|
| 266 |
+
raise ImportError("Loading embeddings requires the 'safetensors' package.") from error
|
| 267 |
+
with safe_open(path, framework="pt", device="cpu") as handle:
|
| 268 |
+
return cast(Tensor, handle.get_tensor(key))
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def _safetensors_shard_prefix(path: str | Path) -> str:
|
| 272 |
+
requested_path = Path(path)
|
| 273 |
+
if requested_path.suffix in {".json", ".safetensors"}:
|
| 274 |
+
return f"{requested_path.stem}-embeddings"
|
| 275 |
+
return "embeddings"
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def _authoritative_index_payload(path: str | Path) -> dict[str, Any] | None:
|
| 279 |
+
"""Return the last atomically committed generation index when available."""
|
| 280 |
+
|
| 281 |
+
try:
|
| 282 |
+
payload, _, _ = _load_authoritative_index(path)
|
| 283 |
+
except (OSError, ValueError, json.JSONDecodeError):
|
| 284 |
+
return None
|
| 285 |
+
return payload
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def _referenced_shards(
|
| 289 |
+
index_path: Path,
|
| 290 |
+
payload: dict[str, Any] | None = None,
|
| 291 |
+
) -> set[Path]:
|
| 292 |
+
if payload is None:
|
| 293 |
+
payload = _authoritative_index_payload(index_path)
|
| 294 |
+
if payload is None:
|
| 295 |
+
return set()
|
| 296 |
+
shards: set[Path] = set()
|
| 297 |
+
for descriptor_shard in payload.get("descriptor_shards", ()):
|
| 298 |
+
tensor_file = descriptor_shard.get("tensor_file")
|
| 299 |
+
if isinstance(tensor_file, str):
|
| 300 |
+
candidate = _resolve_index_child(
|
| 301 |
+
index_path.parent, tensor_file, label="descriptor index"
|
| 302 |
+
)
|
| 303 |
+
shards.add(candidate)
|
| 304 |
+
for item in payload.get("records", ()):
|
| 305 |
+
relative = item.get("tensor", {}).get("file")
|
| 306 |
+
if not isinstance(relative, str):
|
| 307 |
+
continue
|
| 308 |
+
candidate = (index_path.parent / relative).resolve()
|
| 309 |
+
if candidate.parent == index_path.parent.resolve():
|
| 310 |
+
shards.add(candidate)
|
| 311 |
+
return shards
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def _validate_tensor_descriptor(
|
| 315 |
+
tensor: dict[str, Any],
|
| 316 |
+
) -> tuple[str, str, tuple[int, ...], str]:
|
| 317 |
+
key = tensor.get("key")
|
| 318 |
+
if not isinstance(key, str) or not key:
|
| 319 |
+
raise ValueError("Safetensors descriptor tensor key is invalid.")
|
| 320 |
+
dtype = tensor.get("dtype")
|
| 321 |
+
if not isinstance(dtype, str) or dtype not in _NAME_DTYPES:
|
| 322 |
+
raise ValueError("Safetensors descriptor tensor dtype is invalid.")
|
| 323 |
+
raw_shape = tensor.get("shape")
|
| 324 |
+
if not isinstance(raw_shape, (list, tuple)) or not all(
|
| 325 |
+
isinstance(dimension, int) and not isinstance(dimension, bool) and dimension >= 0
|
| 326 |
+
for dimension in raw_shape
|
| 327 |
+
):
|
| 328 |
+
raise ValueError("Safetensors descriptor tensor shape is invalid.")
|
| 329 |
+
sha256 = tensor.get("sha256")
|
| 330 |
+
if (
|
| 331 |
+
not isinstance(sha256, str)
|
| 332 |
+
or len(sha256) != 64
|
| 333 |
+
or sha256 != sha256.lower()
|
| 334 |
+
or any(character not in "0123456789abcdef" for character in sha256)
|
| 335 |
+
):
|
| 336 |
+
raise ValueError("Safetensors descriptor tensor SHA-256 is invalid.")
|
| 337 |
+
return key, dtype, tuple(raw_shape), sha256
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def _record_from_safetensors_descriptor(root: Path, item: dict[str, Any]) -> EmbeddingRecord:
|
| 341 |
+
if not isinstance(item, dict):
|
| 342 |
+
raise ValueError("Safetensors record descriptor must contain a JSON object.")
|
| 343 |
+
record_id = item.get("id")
|
| 344 |
+
sequence = item.get("sequence")
|
| 345 |
+
if not isinstance(record_id, str) or not record_id:
|
| 346 |
+
raise ValueError("Safetensors descriptor record ID is invalid.")
|
| 347 |
+
if not isinstance(sequence, str) or not sequence:
|
| 348 |
+
raise ValueError("Safetensors descriptor sequence is invalid.")
|
| 349 |
+
tensor = item.get("tensor")
|
| 350 |
+
if not isinstance(tensor, dict):
|
| 351 |
+
raise ValueError("Safetensors descriptor is missing tensor metadata.")
|
| 352 |
+
relative = tensor.get("file")
|
| 353 |
+
if not isinstance(relative, str) or not relative:
|
| 354 |
+
raise ValueError("Safetensors descriptor tensor file is invalid.")
|
| 355 |
+
key, dtype, shape, sha256 = _validate_tensor_descriptor(tensor)
|
| 356 |
+
tensor_path = _resolve_index_child(root, relative, label="descriptor")
|
| 357 |
+
if not tensor_path.is_file():
|
| 358 |
+
raise ValueError(f"Safetensors tensor shard is missing: {relative}.")
|
| 359 |
+
|
| 360 |
+
def load_tensor() -> Tensor:
|
| 361 |
+
return _load_safetensor(tensor_path, key)
|
| 362 |
+
|
| 363 |
+
reference = LazyTensorReference(
|
| 364 |
+
source=str(tensor_path),
|
| 365 |
+
key=key,
|
| 366 |
+
dtype=dtype,
|
| 367 |
+
shape=shape,
|
| 368 |
+
sha256=sha256,
|
| 369 |
+
_loader=load_tensor,
|
| 370 |
+
)
|
| 371 |
+
return EmbeddingRecord(record_id, sequence, reference)
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
class _SafetensorsRecordSequence(Sequence[EmbeddingRecord]):
|
| 375 |
+
"""Lazy immutable view over bounded descriptor JSONL shards."""
|
| 376 |
+
|
| 377 |
+
_fastplms_immutable_sequence = True
|
| 378 |
+
|
| 379 |
+
def __init__(self, root: Path, descriptor_shards: Sequence[dict[str, Any]]) -> None:
|
| 380 |
+
if not isinstance(descriptor_shards, (list, tuple)):
|
| 381 |
+
raise ValueError("Safetensors generation index has invalid descriptor shards.")
|
| 382 |
+
self.root = root
|
| 383 |
+
self.shards = tuple(descriptor_shards)
|
| 384 |
+
cumulative: list[int] = []
|
| 385 |
+
total = 0
|
| 386 |
+
for shard in self.shards:
|
| 387 |
+
if not isinstance(shard, dict):
|
| 388 |
+
raise ValueError("Safetensors descriptor shard entry is invalid.")
|
| 389 |
+
relative = shard.get("file")
|
| 390 |
+
declared_count = shard.get("count")
|
| 391 |
+
if (
|
| 392 |
+
not isinstance(declared_count, int)
|
| 393 |
+
or isinstance(declared_count, bool)
|
| 394 |
+
or declared_count < 0
|
| 395 |
+
):
|
| 396 |
+
raise ValueError("Safetensors descriptor shard count is invalid.")
|
| 397 |
+
declared_sha256 = shard.get("sha256")
|
| 398 |
+
if not isinstance(declared_sha256, str) or len(declared_sha256) != 64:
|
| 399 |
+
raise ValueError("Safetensors descriptor shard SHA-256 is invalid.")
|
| 400 |
+
if not isinstance(relative, str):
|
| 401 |
+
raise ValueError("Safetensors descriptor index file is invalid.")
|
| 402 |
+
descriptor_path = _resolve_index_child(root, relative, label="index")
|
| 403 |
+
tensor_file = shard.get("tensor_file")
|
| 404 |
+
if not isinstance(tensor_file, str):
|
| 405 |
+
raise ValueError("Safetensors descriptor tensor file is invalid.")
|
| 406 |
+
tensor_path = _resolve_index_child(root, tensor_file, label="index")
|
| 407 |
+
if not tensor_path.is_file():
|
| 408 |
+
raise ValueError(f"Safetensors tensor shard is missing: {tensor_file}.")
|
| 409 |
+
digest = hashlib.sha256()
|
| 410 |
+
count = 0
|
| 411 |
+
with descriptor_path.open("rb") as handle:
|
| 412 |
+
for line in handle:
|
| 413 |
+
digest.update(line)
|
| 414 |
+
if line.strip():
|
| 415 |
+
item = json.loads(line)
|
| 416 |
+
if not isinstance(item, dict):
|
| 417 |
+
raise ValueError("Safetensors record descriptor must be a JSON object.")
|
| 418 |
+
item_tensor = item.get("tensor")
|
| 419 |
+
if not isinstance(item_tensor, dict):
|
| 420 |
+
raise ValueError("Safetensors descriptor is missing tensor metadata.")
|
| 421 |
+
item_tensor_file = item_tensor.get("file")
|
| 422 |
+
if not isinstance(item_tensor_file, str):
|
| 423 |
+
raise ValueError("Safetensors descriptor tensor file is invalid.")
|
| 424 |
+
_resolve_index_child(root, item_tensor_file, label="descriptor")
|
| 425 |
+
if item_tensor_file != tensor_file:
|
| 426 |
+
raise ValueError(
|
| 427 |
+
"Safetensors descriptor tensor file does not match its shard."
|
| 428 |
+
)
|
| 429 |
+
count += 1
|
| 430 |
+
_validate_tensor_descriptor(item_tensor)
|
| 431 |
+
if digest.hexdigest() != declared_sha256 or count != declared_count:
|
| 432 |
+
raise ValueError(
|
| 433 |
+
f"Safetensors descriptor shard failed integrity validation: {relative}."
|
| 434 |
+
)
|
| 435 |
+
total += count
|
| 436 |
+
cumulative.append(total)
|
| 437 |
+
self._cumulative = tuple(cumulative)
|
| 438 |
+
self._count = total
|
| 439 |
+
|
| 440 |
+
def __len__(self) -> int:
|
| 441 |
+
return self._count
|
| 442 |
+
|
| 443 |
+
def _iter_shard(self, shard_index: int) -> Iterator[EmbeddingRecord]:
|
| 444 |
+
descriptor_path = _resolve_index_child(
|
| 445 |
+
self.root, str(self.shards[shard_index]["file"]), label="index"
|
| 446 |
+
)
|
| 447 |
+
with descriptor_path.open("r", encoding="utf-8") as handle:
|
| 448 |
+
for line in handle:
|
| 449 |
+
if line.strip():
|
| 450 |
+
yield _record_from_safetensors_descriptor(self.root, json.loads(line))
|
| 451 |
+
|
| 452 |
+
def __iter__(self) -> Iterator[EmbeddingRecord]:
|
| 453 |
+
for shard_index in range(len(self.shards)):
|
| 454 |
+
yield from self._iter_shard(shard_index)
|
| 455 |
+
|
| 456 |
+
@overload
|
| 457 |
+
def __getitem__(self, index: int, /) -> EmbeddingRecord: ...
|
| 458 |
+
|
| 459 |
+
@overload
|
| 460 |
+
def __getitem__(self, index: slice, /) -> Sequence[EmbeddingRecord]: ...
|
| 461 |
+
|
| 462 |
+
def __getitem__(self, index: int | slice) -> EmbeddingRecord | Sequence[EmbeddingRecord]:
|
| 463 |
+
if isinstance(index, slice):
|
| 464 |
+
start, stop, step = index.indices(self._count)
|
| 465 |
+
return [self[position] for position in range(start, stop, step)]
|
| 466 |
+
position = index + self._count if index < 0 else index
|
| 467 |
+
if position < 0 or position >= self._count:
|
| 468 |
+
raise IndexError(index)
|
| 469 |
+
shard_index = bisect_right(self._cumulative, position)
|
| 470 |
+
previous = self._cumulative[shard_index - 1] if shard_index else 0
|
| 471 |
+
local_position = position - previous
|
| 472 |
+
for offset, record in enumerate(self._iter_shard(shard_index)):
|
| 473 |
+
if offset == local_position:
|
| 474 |
+
return record
|
| 475 |
+
raise IndexError(index)
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
class SafetensorsStreamWriter:
|
| 479 |
+
"""Bounded-memory, resumable publisher with immutable retained generations."""
|
| 480 |
+
|
| 481 |
+
def __init__(
|
| 482 |
+
self,
|
| 483 |
+
path: str | Path,
|
| 484 |
+
metadata: dict[str, Any],
|
| 485 |
+
*,
|
| 486 |
+
shard_size: int = DEFAULT_SHARD_SIZE,
|
| 487 |
+
existing: Iterable[EmbeddingRecord] = (),
|
| 488 |
+
reuse_existing: bool = False,
|
| 489 |
+
publish_initial: bool = True,
|
| 490 |
+
publish_incremental: bool = True,
|
| 491 |
+
) -> None:
|
| 492 |
+
try:
|
| 493 |
+
from safetensors.torch import save_file
|
| 494 |
+
except ImportError as error:
|
| 495 |
+
raise ImportError("Saving embeddings requires the 'safetensors' package.") from error
|
| 496 |
+
if shard_size <= 0:
|
| 497 |
+
raise ValueError("shard_size must be positive.")
|
| 498 |
+
|
| 499 |
+
self.path = Path(path)
|
| 500 |
+
self.index_path = _index_path(path)
|
| 501 |
+
self.run_manifest_path = _run_manifest_path(path)
|
| 502 |
+
self.index_path.parent.mkdir(parents=True, exist_ok=True)
|
| 503 |
+
self.metadata = _persistent_metadata(
|
| 504 |
+
metadata,
|
| 505 |
+
descriptor_index="safetensors-generation-index",
|
| 506 |
+
record_count=0,
|
| 507 |
+
)
|
| 508 |
+
self.shard_size = shard_size
|
| 509 |
+
self.publish_incremental = publish_incremental
|
| 510 |
+
self._save_file = save_file
|
| 511 |
+
authoritative_payload = _authoritative_index_payload(path)
|
| 512 |
+
prefix = _safetensors_shard_prefix(path)
|
| 513 |
+
# A random generation identity prevents a new writer from reusing a
|
| 514 |
+
# previously published or interrupted generation name. Published files
|
| 515 |
+
# are immutable and remain available to lazy readers until explicit GC.
|
| 516 |
+
self._generation = uuid4().hex
|
| 517 |
+
self._prefix = prefix
|
| 518 |
+
self._shard_index = 0
|
| 519 |
+
self._seed_index = 0
|
| 520 |
+
self._commit_index = 0
|
| 521 |
+
self._descriptor_shards: list[dict[str, Any]] = []
|
| 522 |
+
self._record_count = 0
|
| 523 |
+
self._current: dict[str, Tensor] = {}
|
| 524 |
+
self._pending: list[tuple[EmbeddingRecord, str, str, tuple[int, ...], str]] = []
|
| 525 |
+
self._current_size = 0
|
| 526 |
+
if reuse_existing:
|
| 527 |
+
if authoritative_payload is None:
|
| 528 |
+
raise ValueError("Cannot resume without an authoritative safetensors index.")
|
| 529 |
+
authoritative_metadata = authoritative_payload.get("metadata")
|
| 530 |
+
if not isinstance(authoritative_metadata, dict) or authoritative_metadata.get(
|
| 531 |
+
"run_fingerprint"
|
| 532 |
+
) != self.metadata.get("run_fingerprint"):
|
| 533 |
+
raise ValueError("Cannot resume a safetensors run with a different fingerprint.")
|
| 534 |
+
expected_prefix_length = (
|
| 535 |
+
len(existing) if isinstance(existing, Sequence) else sum(1 for _ in existing)
|
| 536 |
+
)
|
| 537 |
+
if authoritative_payload.get("version") == 2:
|
| 538 |
+
self._descriptor_shards = list(authoritative_payload.get("descriptor_shards", ()))
|
| 539 |
+
self._record_count = int(authoritative_payload.get("record_count", 0))
|
| 540 |
+
else:
|
| 541 |
+
legacy_records = list(authoritative_payload.get("records", ()))
|
| 542 |
+
self._record_count = len(legacy_records)
|
| 543 |
+
if legacy_records:
|
| 544 |
+
self._descriptor_shards.extend(self._write_descriptor_seed(legacy_records))
|
| 545 |
+
if expected_prefix_length != self._record_count:
|
| 546 |
+
raise ValueError(
|
| 547 |
+
"The resumable safetensors prefix does not match the validated "
|
| 548 |
+
"embedding records."
|
| 549 |
+
)
|
| 550 |
+
|
| 551 |
+
if publish_initial:
|
| 552 |
+
self._publish_metadata(complete=False)
|
| 553 |
+
|
| 554 |
+
def _write_descriptor_file(
|
| 555 |
+
self,
|
| 556 |
+
name: str,
|
| 557 |
+
descriptors: Sequence[dict[str, Any]],
|
| 558 |
+
*,
|
| 559 |
+
tensor_file: str,
|
| 560 |
+
) -> dict[str, Any]:
|
| 561 |
+
temporary = self.index_path.parent / f".{name}.tmp"
|
| 562 |
+
destination = self.index_path.parent / name
|
| 563 |
+
if temporary.exists() or destination.exists():
|
| 564 |
+
raise FileExistsError(
|
| 565 |
+
f"Refusing to reuse immutable safetensors generation path {destination}."
|
| 566 |
+
)
|
| 567 |
+
digest = hashlib.sha256()
|
| 568 |
+
with temporary.open("wb") as handle:
|
| 569 |
+
for item in descriptors:
|
| 570 |
+
encoded = (
|
| 571 |
+
json.dumps(item, sort_keys=True, separators=(",", ":")).encode("utf-8") + b"\n"
|
| 572 |
+
)
|
| 573 |
+
handle.write(encoded)
|
| 574 |
+
digest.update(encoded)
|
| 575 |
+
temporary.replace(destination)
|
| 576 |
+
return {
|
| 577 |
+
"file": name,
|
| 578 |
+
"sha256": digest.hexdigest(),
|
| 579 |
+
"count": len(descriptors),
|
| 580 |
+
"tensor_file": tensor_file,
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
def _write_descriptor_seed(self, records: Sequence[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 584 |
+
groups: list[tuple[str, list[dict[str, Any]]]] = []
|
| 585 |
+
for record in records:
|
| 586 |
+
tensor_file = str(record["tensor"]["file"])
|
| 587 |
+
if (
|
| 588 |
+
not groups
|
| 589 |
+
or groups[-1][0] != tensor_file
|
| 590 |
+
or len(groups[-1][1]) == _MAX_RECORDS_PER_DESCRIPTOR_SHARD
|
| 591 |
+
):
|
| 592 |
+
groups.append((tensor_file, []))
|
| 593 |
+
groups[-1][1].append(record)
|
| 594 |
+
descriptor_shards: list[dict[str, Any]] = []
|
| 595 |
+
for tensor_file, descriptors in groups:
|
| 596 |
+
self._seed_index += 1
|
| 597 |
+
name = (
|
| 598 |
+
f"{self._prefix}-records-run-{self._generation}-seed-{self._seed_index:05d}.jsonl"
|
| 599 |
+
)
|
| 600 |
+
descriptor_shards.append(
|
| 601 |
+
self._write_descriptor_file(name, descriptors, tensor_file=tensor_file)
|
| 602 |
+
)
|
| 603 |
+
return descriptor_shards
|
| 604 |
+
|
| 605 |
+
def _write_shard(self) -> None:
|
| 606 |
+
if not self._current:
|
| 607 |
+
return
|
| 608 |
+
self._shard_index += 1
|
| 609 |
+
name = f"{self._prefix}-run-{self._generation}-{self._shard_index:05d}.safetensors"
|
| 610 |
+
temporary = self.index_path.parent / f".{name}.tmp"
|
| 611 |
+
destination = self.index_path.parent / name
|
| 612 |
+
if temporary.exists() or destination.exists():
|
| 613 |
+
raise FileExistsError(
|
| 614 |
+
f"Refusing to reuse immutable safetensors generation path {destination}."
|
| 615 |
+
)
|
| 616 |
+
self._save_file(self._current, temporary)
|
| 617 |
+
temporary.replace(destination)
|
| 618 |
+
descriptors: list[dict[str, Any]] = []
|
| 619 |
+
for record, key, dtype_name, shape, digest in self._pending:
|
| 620 |
+
descriptors.append(
|
| 621 |
+
{
|
| 622 |
+
"id": record.id,
|
| 623 |
+
"sequence": record.sequence,
|
| 624 |
+
"tensor": {
|
| 625 |
+
"file": name,
|
| 626 |
+
"key": key,
|
| 627 |
+
"dtype": dtype_name,
|
| 628 |
+
"shape": list(shape),
|
| 629 |
+
"sha256": digest,
|
| 630 |
+
},
|
| 631 |
+
}
|
| 632 |
+
)
|
| 633 |
+
descriptor_name = (
|
| 634 |
+
f"{self._prefix}-records-run-{self._generation}-{self._shard_index:05d}.jsonl"
|
| 635 |
+
)
|
| 636 |
+
self._descriptor_shards.append(
|
| 637 |
+
self._write_descriptor_file(descriptor_name, descriptors, tensor_file=name)
|
| 638 |
+
)
|
| 639 |
+
self._record_count += len(descriptors)
|
| 640 |
+
self._current = {}
|
| 641 |
+
self._pending = []
|
| 642 |
+
self._current_size = 0
|
| 643 |
+
|
| 644 |
+
def append(
|
| 645 |
+
self,
|
| 646 |
+
records: Iterable[EmbeddingRecord],
|
| 647 |
+
*,
|
| 648 |
+
publish: bool | None = None,
|
| 649 |
+
) -> None:
|
| 650 |
+
"""Persist records while retaining at most one shard of tensors."""
|
| 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()
|
| 658 |
+
if nbytes > self.shard_size:
|
| 659 |
+
raise ValueError(
|
| 660 |
+
f"Embedding {position} requires {nbytes} bytes and cannot fit in a "
|
| 661 |
+
f"{self.shard_size}-byte safetensors shard."
|
| 662 |
+
)
|
| 663 |
+
if self._current and (
|
| 664 |
+
self._current_size + nbytes > self.shard_size
|
| 665 |
+
or len(self._pending) == _MAX_RECORDS_PER_DESCRIPTOR_SHARD
|
| 666 |
+
):
|
| 667 |
+
self._write_shard()
|
| 668 |
+
if self.publish_incremental:
|
| 669 |
+
self._publish_metadata(complete=False)
|
| 670 |
+
position = self._record_count
|
| 671 |
+
key = f"embedding_{position:08d}"
|
| 672 |
+
self._current[key] = tensor
|
| 673 |
+
self._current_size += nbytes
|
| 674 |
+
self._pending.append(
|
| 675 |
+
(
|
| 676 |
+
record,
|
| 677 |
+
key,
|
| 678 |
+
_DTYPE_NAMES[tensor.dtype],
|
| 679 |
+
tuple(tensor.shape),
|
| 680 |
+
tensor_sha256(tensor),
|
| 681 |
+
)
|
| 682 |
+
)
|
| 683 |
+
if publish:
|
| 684 |
+
self.publish(complete=False)
|
| 685 |
+
|
| 686 |
+
def _publish_metadata(
|
| 687 |
+
self,
|
| 688 |
+
*,
|
| 689 |
+
complete: bool,
|
| 690 |
+
metadata: dict[str, Any] | None = None,
|
| 691 |
+
) -> EmbeddingResult:
|
| 692 |
+
"""Atomically expose one self-consistent metadata generation."""
|
| 693 |
+
|
| 694 |
+
if metadata is not None:
|
| 695 |
+
self.metadata = _persistent_metadata(
|
| 696 |
+
metadata,
|
| 697 |
+
descriptor_index="safetensors-generation-index",
|
| 698 |
+
)
|
| 699 |
+
self.metadata["complete"] = complete
|
| 700 |
+
self.metadata["record_count"] = self._record_count
|
| 701 |
+
self._commit_index += 1
|
| 702 |
+
payload = {
|
| 703 |
+
"version": 2,
|
| 704 |
+
"format": "fastplms-embedding-safetensors",
|
| 705 |
+
"metadata": self.metadata,
|
| 706 |
+
"record_count": self._record_count,
|
| 707 |
+
"descriptor_shards": self._descriptor_shards,
|
| 708 |
+
}
|
| 709 |
+
generation_index_name = (
|
| 710 |
+
f"{self._prefix}-index-run-{self._generation}-{self._commit_index:05d}.json"
|
| 711 |
+
)
|
| 712 |
+
generation_index_path = self.index_path.parent / generation_index_name
|
| 713 |
+
temporary_generation_index = generation_index_path.with_name(
|
| 714 |
+
f".{generation_index_path.name}.tmp"
|
| 715 |
+
)
|
| 716 |
+
if temporary_generation_index.exists() or generation_index_path.exists():
|
| 717 |
+
raise FileExistsError(
|
| 718 |
+
f"Refusing to reuse immutable safetensors generation index {generation_index_path}."
|
| 719 |
+
)
|
| 720 |
+
encoded_index = _canonical_json_bytes(payload)
|
| 721 |
+
temporary_generation_index.write_bytes(encoded_index)
|
| 722 |
+
temporary_generation_index.replace(generation_index_path)
|
| 723 |
+
|
| 724 |
+
index_sha256 = hashlib.sha256(encoded_index).hexdigest()
|
| 725 |
+
index_reference = {
|
| 726 |
+
"file": generation_index_name,
|
| 727 |
+
"sha256": index_sha256,
|
| 728 |
+
}
|
| 729 |
+
run_manifest = {
|
| 730 |
+
"version": 2,
|
| 731 |
+
"format": "fastplms-embedding-run",
|
| 732 |
+
"index": index_reference,
|
| 733 |
+
"record_count": self._record_count,
|
| 734 |
+
}
|
| 735 |
+
pointer_identity = f"{self._generation}-{self._commit_index:05d}"
|
| 736 |
+
temporary_manifest = self.run_manifest_path.with_name(
|
| 737 |
+
f".{self.run_manifest_path.name}.{pointer_identity}.tmp"
|
| 738 |
+
)
|
| 739 |
+
temporary_manifest.write_bytes(_canonical_json_bytes(run_manifest))
|
| 740 |
+
temporary_manifest.replace(self.run_manifest_path)
|
| 741 |
+
|
| 742 |
+
# ``index.json`` is a non-authoritative convenience pointer. The run
|
| 743 |
+
# manifest is committed first, so interruption here cannot invalidate
|
| 744 |
+
# the newly committed generation.
|
| 745 |
+
stable_pointer = {
|
| 746 |
+
"version": 2,
|
| 747 |
+
"format": "fastplms-embedding-index-pointer",
|
| 748 |
+
"index": index_reference,
|
| 749 |
+
}
|
| 750 |
+
temporary_index = self.index_path.with_name(
|
| 751 |
+
f".{self.index_path.name}.{pointer_identity}.tmp"
|
| 752 |
+
)
|
| 753 |
+
temporary_index.write_bytes(_canonical_json_bytes(stable_pointer))
|
| 754 |
+
temporary_index.replace(self.index_path)
|
| 755 |
+
|
| 756 |
+
return load_safetensors_result(self.index_path)
|
| 757 |
+
|
| 758 |
+
def publish(
|
| 759 |
+
self,
|
| 760 |
+
*,
|
| 761 |
+
complete: bool,
|
| 762 |
+
metadata: dict[str, Any] | None = None,
|
| 763 |
+
) -> EmbeddingResult:
|
| 764 |
+
"""Flush the current shard and atomically expose a consistent generation."""
|
| 765 |
+
|
| 766 |
+
self._write_shard()
|
| 767 |
+
return self._publish_metadata(complete=complete, metadata=metadata)
|
| 768 |
+
|
| 769 |
+
|
| 770 |
+
def save_safetensors_result(
|
| 771 |
+
result: EmbeddingResult,
|
| 772 |
+
path: str | Path,
|
| 773 |
+
*,
|
| 774 |
+
shard_size: int = DEFAULT_SHARD_SIZE,
|
| 775 |
+
) -> EmbeddingResult:
|
| 776 |
+
"""Write sharded safetensors without materializing the full result."""
|
| 777 |
+
|
| 778 |
+
writer = SafetensorsStreamWriter(
|
| 779 |
+
path,
|
| 780 |
+
result.metadata,
|
| 781 |
+
shard_size=shard_size,
|
| 782 |
+
publish_initial=False,
|
| 783 |
+
publish_incremental=False,
|
| 784 |
+
)
|
| 785 |
+
writer.append(result, publish=False)
|
| 786 |
+
return writer.publish(complete=bool(result.metadata.get("complete", True)))
|
| 787 |
+
|
| 788 |
+
|
| 789 |
+
def load_safetensors_result(path: str | Path) -> EmbeddingResult:
|
| 790 |
+
"""Load an indexed safetensors result without loading tensor payloads."""
|
| 791 |
+
|
| 792 |
+
payload, index_path, _ = _load_authoritative_index(path)
|
| 793 |
+
if payload.get("version") == 2:
|
| 794 |
+
lazy_records = _SafetensorsRecordSequence(
|
| 795 |
+
index_path.parent, payload.get("descriptor_shards", ())
|
| 796 |
+
)
|
| 797 |
+
if len(lazy_records) != payload.get("record_count"):
|
| 798 |
+
raise ValueError("Safetensors descriptor count does not match its generation index.")
|
| 799 |
+
return EmbeddingResult(lazy_records, payload.get("metadata", {}))
|
| 800 |
+
|
| 801 |
+
records: list[EmbeddingRecord] = []
|
| 802 |
+
for item in payload["records"]:
|
| 803 |
+
records.append(_record_from_safetensors_descriptor(index_path.parent, item))
|
| 804 |
+
return EmbeddingResult(records, payload.get("metadata", {}))
|
| 805 |
+
|
| 806 |
+
|
| 807 |
+
def garbage_collect_safetensors_generations(
|
| 808 |
+
path: str | Path,
|
| 809 |
+
*,
|
| 810 |
+
dry_run: bool = True,
|
| 811 |
+
confirm_no_active_readers_or_writers: bool = False,
|
| 812 |
+
) -> tuple[Path, ...]:
|
| 813 |
+
"""Remove non-authoritative generations after an explicit exclusivity check.
|
| 814 |
+
|
| 815 |
+
Safetensors results retain immutable historical generations because an
|
| 816 |
+
already-open :class:`EmbeddingResult` resolves tensors through those exact
|
| 817 |
+
descriptor and shard paths. Destructive collection is therefore safe only
|
| 818 |
+
when the caller guarantees that no reader or writer for ``path`` remains
|
| 819 |
+
active. ``dry_run=True`` is the default and returns the paths that would be
|
| 820 |
+
removed without changing the output directory.
|
| 821 |
+
"""
|
| 822 |
+
|
| 823 |
+
if not isinstance(dry_run, bool):
|
| 824 |
+
raise TypeError("dry_run must be a bool.")
|
| 825 |
+
if not isinstance(confirm_no_active_readers_or_writers, bool):
|
| 826 |
+
raise TypeError("confirm_no_active_readers_or_writers must be a bool.")
|
| 827 |
+
if not dry_run and not confirm_no_active_readers_or_writers:
|
| 828 |
+
raise ValueError(
|
| 829 |
+
"Destructive safetensors generation collection requires "
|
| 830 |
+
"confirm_no_active_readers_or_writers=True."
|
| 831 |
+
)
|
| 832 |
+
|
| 833 |
+
# Validate the full descriptor graph before identifying anything as stale.
|
| 834 |
+
load_safetensors_result(path)
|
| 835 |
+
payload, authoritative_index_path, _ = _load_authoritative_index(path)
|
| 836 |
+
stable_index_path = _index_path(path)
|
| 837 |
+
run_manifest_path = _run_manifest_path(path)
|
| 838 |
+
root = stable_index_path.parent
|
| 839 |
+
prefix = _safetensors_shard_prefix(path)
|
| 840 |
+
protected = {
|
| 841 |
+
stable_index_path.resolve(),
|
| 842 |
+
run_manifest_path.resolve(),
|
| 843 |
+
authoritative_index_path.resolve(),
|
| 844 |
+
*_referenced_shards(stable_index_path, payload),
|
| 845 |
+
}
|
| 846 |
+
for descriptor_shard in payload.get("descriptor_shards", ()):
|
| 847 |
+
relative = descriptor_shard.get("file")
|
| 848 |
+
if isinstance(relative, str):
|
| 849 |
+
protected.add(_resolve_index_child(root, relative, label="index").resolve())
|
| 850 |
+
|
| 851 |
+
candidates: set[Path] = set()
|
| 852 |
+
for pattern in (
|
| 853 |
+
f"{prefix}-run-*-*.safetensors",
|
| 854 |
+
f"{prefix}-records-run-*.jsonl",
|
| 855 |
+
f"{prefix}-index-run-*.json",
|
| 856 |
+
f".{prefix}-*.tmp",
|
| 857 |
+
):
|
| 858 |
+
candidates.update(root.glob(pattern))
|
| 859 |
+
candidates.update(root.glob(f".{stable_index_path.name}.*.tmp"))
|
| 860 |
+
candidates.update(root.glob(f".{run_manifest_path.name}.*.tmp"))
|
| 861 |
+
|
| 862 |
+
stale = tuple(
|
| 863 |
+
sorted(
|
| 864 |
+
(candidate for candidate in candidates if candidate.resolve() not in protected),
|
| 865 |
+
key=lambda candidate: candidate.name,
|
| 866 |
+
)
|
| 867 |
+
)
|
| 868 |
+
if not dry_run:
|
| 869 |
+
for candidate in stale:
|
| 870 |
+
candidate.unlink(missing_ok=True)
|
| 871 |
+
return stale
|
| 872 |
+
|
| 873 |
+
|
| 874 |
+
def _ensure_sqlite_schema(connection: sqlite3.Connection) -> None:
|
| 875 |
+
connection.executescript(
|
| 876 |
+
"""
|
| 877 |
+
PRAGMA foreign_keys = ON;
|
| 878 |
+
CREATE TABLE IF NOT EXISTS runs (
|
| 879 |
+
run_id TEXT PRIMARY KEY,
|
| 880 |
+
metadata_json TEXT NOT NULL,
|
| 881 |
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 882 |
+
published_order INTEGER
|
| 883 |
+
);
|
| 884 |
+
CREATE TABLE IF NOT EXISTS tensors (
|
| 885 |
+
run_id TEXT NOT NULL,
|
| 886 |
+
position INTEGER NOT NULL,
|
| 887 |
+
dtype TEXT NOT NULL,
|
| 888 |
+
shape_json TEXT NOT NULL,
|
| 889 |
+
data BLOB NOT NULL,
|
| 890 |
+
sha256 TEXT NOT NULL,
|
| 891 |
+
PRIMARY KEY (run_id, position),
|
| 892 |
+
FOREIGN KEY (run_id) REFERENCES runs(run_id) ON DELETE CASCADE
|
| 893 |
+
);
|
| 894 |
+
CREATE TABLE IF NOT EXISTS records (
|
| 895 |
+
run_id TEXT NOT NULL,
|
| 896 |
+
position INTEGER NOT NULL,
|
| 897 |
+
record_id TEXT NOT NULL,
|
| 898 |
+
sequence TEXT NOT NULL,
|
| 899 |
+
PRIMARY KEY (run_id, position),
|
| 900 |
+
FOREIGN KEY (run_id, position) REFERENCES tensors(run_id, position)
|
| 901 |
+
ON DELETE CASCADE
|
| 902 |
+
);
|
| 903 |
+
"""
|
| 904 |
+
)
|
| 905 |
+
run_columns = {str(row[1]) for row in connection.execute("PRAGMA table_info(runs)").fetchall()}
|
| 906 |
+
if "published_order" not in run_columns:
|
| 907 |
+
connection.execute("ALTER TABLE runs ADD COLUMN published_order INTEGER")
|
| 908 |
+
# Databases created before staged publication exposed every stored run.
|
| 909 |
+
# Preserve that view for historical runs containing committed records.
|
| 910 |
+
connection.execute(
|
| 911 |
+
"UPDATE runs SET published_order = rowid "
|
| 912 |
+
"WHERE published_order IS NULL AND EXISTS ("
|
| 913 |
+
"SELECT 1 FROM records WHERE records.run_id = runs.run_id)"
|
| 914 |
+
)
|
| 915 |
+
connection.execute(
|
| 916 |
+
"CREATE INDEX IF NOT EXISTS runs_published_order_idx ON runs(published_order)"
|
| 917 |
+
)
|
| 918 |
+
if "published_order" not in run_columns:
|
| 919 |
+
# Schema upgrades run before callers open their data transaction.
|
| 920 |
+
# End the migration transaction explicitly so BEGIN IMMEDIATE below
|
| 921 |
+
# remains valid on existing databases.
|
| 922 |
+
connection.commit()
|
| 923 |
+
|
| 924 |
+
|
| 925 |
+
def save_sqlite_result(result: EmbeddingResult, path: str | Path) -> EmbeddingResult:
|
| 926 |
+
"""Transactionally store an ordered result in normalized SQLite tables."""
|
| 927 |
+
|
| 928 |
+
path = Path(path)
|
| 929 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 930 |
+
run_id = str(result.metadata.get("run_fingerprint", ""))
|
| 931 |
+
if not run_id:
|
| 932 |
+
raise ValueError("SQLite results require metadata['run_fingerprint'].")
|
| 933 |
+
metadata_json = json.dumps(
|
| 934 |
+
_persistent_metadata(
|
| 935 |
+
result.metadata,
|
| 936 |
+
descriptor_index="sqlite-records",
|
| 937 |
+
record_count=len(result),
|
| 938 |
+
),
|
| 939 |
+
sort_keys=True,
|
| 940 |
+
)
|
| 941 |
+
with sqlite3.connect(path, timeout=30) as connection:
|
| 942 |
+
_ensure_sqlite_schema(connection)
|
| 943 |
+
connection.execute("PRAGMA journal_mode = WAL")
|
| 944 |
+
connection.execute("BEGIN IMMEDIATE")
|
| 945 |
+
connection.execute("DELETE FROM runs WHERE run_id = ?", (run_id,))
|
| 946 |
+
connection.execute(
|
| 947 |
+
"INSERT INTO runs(run_id, metadata_json, published_order) "
|
| 948 |
+
"SELECT ?, ?, COALESCE(MAX(published_order), 0) + 1 FROM runs",
|
| 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(
|
| 956 |
+
"INSERT INTO tensors VALUES (?, ?, ?, ?, ?, ?)",
|
| 957 |
+
(run_id, position, dtype_name, shape_json, data, digest),
|
| 958 |
+
)
|
| 959 |
+
connection.execute(
|
| 960 |
+
"INSERT INTO records VALUES (?, ?, ?, ?)",
|
| 961 |
+
(run_id, position, record.id, record.sequence),
|
| 962 |
+
)
|
| 963 |
+
connection.commit()
|
| 964 |
+
return load_sqlite_result(path, run_id=run_id)
|
| 965 |
+
|
| 966 |
+
|
| 967 |
+
def initialize_sqlite_run(
|
| 968 |
+
path: str | Path,
|
| 969 |
+
metadata: dict[str, Any],
|
| 970 |
+
*,
|
| 971 |
+
resume: bool,
|
| 972 |
+
) -> str:
|
| 973 |
+
"""Create a resumable SQLite run without buffering tensor results."""
|
| 974 |
+
|
| 975 |
+
path = Path(path)
|
| 976 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 977 |
+
run_id = str(metadata.get("run_fingerprint", ""))
|
| 978 |
+
if not run_id:
|
| 979 |
+
raise ValueError("SQLite runs require metadata['run_fingerprint'].")
|
| 980 |
+
with sqlite3.connect(path, timeout=30) as connection:
|
| 981 |
+
_ensure_sqlite_schema(connection)
|
| 982 |
+
connection.execute("PRAGMA journal_mode = WAL")
|
| 983 |
+
connection.execute("BEGIN IMMEDIATE")
|
| 984 |
+
exists = connection.execute("SELECT 1 FROM runs WHERE run_id = ?", (run_id,)).fetchone()
|
| 985 |
+
if exists and not resume:
|
| 986 |
+
connection.execute("DELETE FROM runs WHERE run_id = ?", (run_id,))
|
| 987 |
+
exists = None
|
| 988 |
+
if exists is None:
|
| 989 |
+
initial_metadata = _persistent_metadata(
|
| 990 |
+
metadata,
|
| 991 |
+
descriptor_index="sqlite-records",
|
| 992 |
+
record_count=0,
|
| 993 |
+
)
|
| 994 |
+
connection.execute(
|
| 995 |
+
"INSERT INTO runs(run_id, metadata_json) VALUES (?, ?)",
|
| 996 |
+
(run_id, json.dumps(initial_metadata, sort_keys=True)),
|
| 997 |
+
)
|
| 998 |
+
connection.commit()
|
| 999 |
+
return run_id
|
| 1000 |
+
|
| 1001 |
+
|
| 1002 |
+
def append_sqlite_records(
|
| 1003 |
+
path: str | Path,
|
| 1004 |
+
run_id: str,
|
| 1005 |
+
start_position: int,
|
| 1006 |
+
records: list[EmbeddingRecord],
|
| 1007 |
+
*,
|
| 1008 |
+
replace_metadata: dict[str, Any] | None = None,
|
| 1009 |
+
) -> None:
|
| 1010 |
+
"""Commit one ordered embedding batch so an interrupted run can resume."""
|
| 1011 |
+
|
| 1012 |
+
if not isinstance(run_id, str) or not run_id:
|
| 1013 |
+
raise ValueError("run_id must be a non-empty string.")
|
| 1014 |
+
if not isinstance(start_position, int) or isinstance(start_position, bool):
|
| 1015 |
+
raise TypeError("start_position must be a non-negative integer.")
|
| 1016 |
+
if start_position < 0:
|
| 1017 |
+
raise ValueError("start_position must be a non-negative integer.")
|
| 1018 |
+
if not isinstance(records, list) or not all(
|
| 1019 |
+
isinstance(record, EmbeddingRecord) for record in records
|
| 1020 |
+
):
|
| 1021 |
+
raise TypeError("records must be a list of EmbeddingRecord values.")
|
| 1022 |
+
|
| 1023 |
+
with sqlite3.connect(Path(path), timeout=30) as connection:
|
| 1024 |
+
_ensure_sqlite_schema(connection)
|
| 1025 |
+
connection.execute("PRAGMA journal_mode = WAL")
|
| 1026 |
+
connection.execute("BEGIN IMMEDIATE")
|
| 1027 |
+
if replace_metadata is not None:
|
| 1028 |
+
replacement_run_id = str(replace_metadata.get("run_fingerprint", ""))
|
| 1029 |
+
if replacement_run_id != run_id:
|
| 1030 |
+
raise ValueError("Replacement metadata must match the SQLite run ID.")
|
| 1031 |
+
initial_metadata = _persistent_metadata(
|
| 1032 |
+
replace_metadata,
|
| 1033 |
+
descriptor_index="sqlite-records",
|
| 1034 |
+
record_count=0,
|
| 1035 |
+
)
|
| 1036 |
+
connection.execute("DELETE FROM runs WHERE run_id = ?", (run_id,))
|
| 1037 |
+
connection.execute(
|
| 1038 |
+
"INSERT INTO runs(run_id, metadata_json) VALUES (?, ?)",
|
| 1039 |
+
(run_id, json.dumps(initial_metadata, sort_keys=True)),
|
| 1040 |
+
)
|
| 1041 |
+
if connection.execute("SELECT 1 FROM runs WHERE run_id = ?", (run_id,)).fetchone() is None:
|
| 1042 |
+
raise KeyError(f"Missing SQLite embedding run {run_id}.")
|
| 1043 |
+
current_count, minimum_position, maximum_position = connection.execute(
|
| 1044 |
+
"SELECT COUNT(*), MIN(position), MAX(position) FROM records WHERE run_id = ?",
|
| 1045 |
+
(run_id,),
|
| 1046 |
+
).fetchone()
|
| 1047 |
+
if current_count and (minimum_position != 0 or maximum_position != current_count - 1):
|
| 1048 |
+
raise ValueError("SQLite embedding run has a non-contiguous record prefix.")
|
| 1049 |
+
if start_position != current_count:
|
| 1050 |
+
raise ValueError(
|
| 1051 |
+
f"start_position={start_position} does not match the contiguous "
|
| 1052 |
+
f"SQLite prefix length {current_count}."
|
| 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(
|
| 1060 |
+
"INSERT INTO tensors VALUES (?, ?, ?, ?, ?, ?)",
|
| 1061 |
+
(run_id, position, dtype_name, shape_json, data, digest),
|
| 1062 |
+
)
|
| 1063 |
+
connection.execute(
|
| 1064 |
+
"INSERT INTO records VALUES (?, ?, ?, ?)",
|
| 1065 |
+
(run_id, position, record.id, record.sequence),
|
| 1066 |
+
)
|
| 1067 |
+
row = connection.execute(
|
| 1068 |
+
"SELECT metadata_json FROM runs WHERE run_id = ?", (run_id,)
|
| 1069 |
+
).fetchone()
|
| 1070 |
+
if row is None:
|
| 1071 |
+
raise KeyError(f"Missing SQLite embedding run {run_id}.")
|
| 1072 |
+
metadata = json.loads(row[0])
|
| 1073 |
+
if not isinstance(metadata, dict):
|
| 1074 |
+
raise ValueError("SQLite run metadata must contain a JSON object.")
|
| 1075 |
+
metadata["record_count"] = start_position + len(records)
|
| 1076 |
+
metadata["descriptor_index"] = "sqlite-records"
|
| 1077 |
+
connection.execute(
|
| 1078 |
+
"UPDATE runs SET metadata_json = ? WHERE run_id = ?",
|
| 1079 |
+
(json.dumps(metadata, sort_keys=True), run_id),
|
| 1080 |
+
)
|
| 1081 |
+
if records:
|
| 1082 |
+
connection.execute(
|
| 1083 |
+
"UPDATE runs SET published_order = ("
|
| 1084 |
+
"SELECT COALESCE(MAX(published_order), 0) + 1 FROM runs"
|
| 1085 |
+
") WHERE run_id = ? AND published_order IS NULL",
|
| 1086 |
+
(run_id,),
|
| 1087 |
+
)
|
| 1088 |
+
connection.commit()
|
| 1089 |
+
|
| 1090 |
+
|
| 1091 |
+
def update_sqlite_run_metadata(path: str | Path, run_id: str, metadata: dict[str, Any]) -> None:
|
| 1092 |
+
"""Finalize reproducibility metadata after the last streamed batch."""
|
| 1093 |
+
|
| 1094 |
+
with sqlite3.connect(Path(path), timeout=30) as connection:
|
| 1095 |
+
row = connection.execute(
|
| 1096 |
+
"SELECT COUNT(*) FROM records WHERE run_id = ?", (run_id,)
|
| 1097 |
+
).fetchone()
|
| 1098 |
+
record_count = int(row[0]) if row is not None else 0
|
| 1099 |
+
cleaned_metadata = _persistent_metadata(
|
| 1100 |
+
metadata,
|
| 1101 |
+
descriptor_index="sqlite-records",
|
| 1102 |
+
record_count=record_count,
|
| 1103 |
+
)
|
| 1104 |
+
updated = connection.execute(
|
| 1105 |
+
"UPDATE runs SET metadata_json = ? WHERE run_id = ?",
|
| 1106 |
+
(json.dumps(cleaned_metadata, sort_keys=True), run_id),
|
| 1107 |
+
).rowcount
|
| 1108 |
+
if updated != 1:
|
| 1109 |
+
raise KeyError(f"Missing SQLite embedding run {run_id}.")
|
| 1110 |
+
connection.commit()
|
| 1111 |
+
|
| 1112 |
+
|
| 1113 |
+
def _connect_sqlite_read_only(path: Path) -> sqlite3.Connection:
|
| 1114 |
+
if not path.is_file():
|
| 1115 |
+
raise FileNotFoundError(path)
|
| 1116 |
+
return sqlite3.connect(f"{path.resolve().as_uri()}?mode=ro", uri=True, timeout=30)
|
| 1117 |
+
|
| 1118 |
+
|
| 1119 |
+
def _validate_sqlite_result_schema(connection: sqlite3.Connection, path: Path) -> None:
|
| 1120 |
+
tables = {
|
| 1121 |
+
str(row[0])
|
| 1122 |
+
for row in connection.execute(
|
| 1123 |
+
"SELECT name FROM sqlite_master WHERE type = 'table'"
|
| 1124 |
+
).fetchall()
|
| 1125 |
+
}
|
| 1126 |
+
required = {"runs", "records", "tensors"}
|
| 1127 |
+
if not required.issubset(tables):
|
| 1128 |
+
raise ValueError(
|
| 1129 |
+
f"Not a FastPLMs embedding SQLite database: {path}. "
|
| 1130 |
+
"Use convert_legacy_sqlite() for a legacy embeddings table."
|
| 1131 |
+
)
|
| 1132 |
+
|
| 1133 |
+
|
| 1134 |
+
def _load_sqlite_tensor(path: Path, run_id: str, position: int) -> Tensor:
|
| 1135 |
+
with _connect_sqlite_read_only(path) as connection:
|
| 1136 |
+
row = connection.execute(
|
| 1137 |
+
"SELECT dtype, shape_json, data FROM tensors WHERE run_id = ? AND position = ?",
|
| 1138 |
+
(run_id, position),
|
| 1139 |
+
).fetchone()
|
| 1140 |
+
if row is None:
|
| 1141 |
+
raise KeyError(f"Missing SQLite tensor {run_id}:{position}.")
|
| 1142 |
+
return _decode_tensor(*row)
|
| 1143 |
+
|
| 1144 |
+
|
| 1145 |
+
def _validate_sqlite_descriptor_row(
|
| 1146 |
+
row: Sequence[Any],
|
| 1147 |
+
) -> tuple[int, str, str, str, str, str]:
|
| 1148 |
+
if len(row) != 6:
|
| 1149 |
+
raise ValueError("SQLite embedding descriptor has an invalid column count.")
|
| 1150 |
+
position, record_id, sequence, dtype_name, shape_json, digest = row
|
| 1151 |
+
if not isinstance(position, int) or isinstance(position, bool) or position < 0:
|
| 1152 |
+
raise ValueError("SQLite embedding position is invalid.")
|
| 1153 |
+
if not isinstance(record_id, str) or not record_id:
|
| 1154 |
+
raise ValueError("SQLite embedding record ID is invalid.")
|
| 1155 |
+
if not isinstance(sequence, str) or not sequence:
|
| 1156 |
+
raise ValueError("SQLite embedding sequence is invalid.")
|
| 1157 |
+
if not isinstance(shape_json, str):
|
| 1158 |
+
raise ValueError("SQLite embedding tensor shape is invalid.")
|
| 1159 |
+
try:
|
| 1160 |
+
shape = json.loads(shape_json)
|
| 1161 |
+
except json.JSONDecodeError as error:
|
| 1162 |
+
raise ValueError("SQLite embedding tensor shape is invalid.") from error
|
| 1163 |
+
_validate_tensor_descriptor(
|
| 1164 |
+
{
|
| 1165 |
+
"key": f"embedding_{position}",
|
| 1166 |
+
"dtype": dtype_name,
|
| 1167 |
+
"shape": shape,
|
| 1168 |
+
"sha256": digest,
|
| 1169 |
+
}
|
| 1170 |
+
)
|
| 1171 |
+
return position, record_id, sequence, dtype_name, shape_json, digest
|
| 1172 |
+
|
| 1173 |
+
|
| 1174 |
+
def _sqlite_record_from_row(path: Path, run_id: str, row: Sequence[Any]) -> EmbeddingRecord:
|
| 1175 |
+
position, record_id, sequence, dtype_name, shape_json, digest = _validate_sqlite_descriptor_row(
|
| 1176 |
+
row
|
| 1177 |
+
)
|
| 1178 |
+
|
| 1179 |
+
def load_tensor() -> Tensor:
|
| 1180 |
+
return _load_sqlite_tensor(path, run_id, position)
|
| 1181 |
+
|
| 1182 |
+
reference = LazyTensorReference(
|
| 1183 |
+
source=str(path),
|
| 1184 |
+
key=f"{run_id}:{position}",
|
| 1185 |
+
dtype=dtype_name,
|
| 1186 |
+
shape=tuple(json.loads(shape_json)),
|
| 1187 |
+
sha256=digest,
|
| 1188 |
+
_loader=load_tensor,
|
| 1189 |
+
)
|
| 1190 |
+
return EmbeddingRecord(record_id, sequence, reference)
|
| 1191 |
+
|
| 1192 |
+
|
| 1193 |
+
class _SQLiteRecordSequence(Sequence[EmbeddingRecord]):
|
| 1194 |
+
"""Lazy immutable descriptor view over one SQLite embedding run."""
|
| 1195 |
+
|
| 1196 |
+
_fastplms_immutable_sequence = True
|
| 1197 |
+
|
| 1198 |
+
def __init__(self, path: Path, run_id: str, count: int) -> None:
|
| 1199 |
+
self.path = path
|
| 1200 |
+
self.run_id = run_id
|
| 1201 |
+
self._count = count
|
| 1202 |
+
|
| 1203 |
+
@staticmethod
|
| 1204 |
+
def _row_query() -> str:
|
| 1205 |
+
return (
|
| 1206 |
+
"SELECT r.position, r.record_id, r.sequence, t.dtype, t.shape_json, t.sha256 "
|
| 1207 |
+
"FROM records r JOIN tensors t USING (run_id, position) "
|
| 1208 |
+
"WHERE r.run_id = ?"
|
| 1209 |
+
)
|
| 1210 |
+
|
| 1211 |
+
def __len__(self) -> int:
|
| 1212 |
+
return self._count
|
| 1213 |
+
|
| 1214 |
+
def __iter__(self) -> Iterator[EmbeddingRecord]:
|
| 1215 |
+
with _connect_sqlite_read_only(self.path) as connection:
|
| 1216 |
+
cursor = connection.execute(f"{self._row_query()} ORDER BY r.position", (self.run_id,))
|
| 1217 |
+
while rows := cursor.fetchmany(1_024):
|
| 1218 |
+
for row in rows:
|
| 1219 |
+
yield _sqlite_record_from_row(self.path, self.run_id, row)
|
| 1220 |
+
|
| 1221 |
+
@overload
|
| 1222 |
+
def __getitem__(self, index: int, /) -> EmbeddingRecord: ...
|
| 1223 |
+
|
| 1224 |
+
@overload
|
| 1225 |
+
def __getitem__(self, index: slice, /) -> Sequence[EmbeddingRecord]: ...
|
| 1226 |
+
|
| 1227 |
+
def __getitem__(self, index: int | slice) -> EmbeddingRecord | Sequence[EmbeddingRecord]:
|
| 1228 |
+
if isinstance(index, slice):
|
| 1229 |
+
start, stop, step = index.indices(self._count)
|
| 1230 |
+
return [self[position] for position in range(start, stop, step)]
|
| 1231 |
+
position = index + self._count if index < 0 else index
|
| 1232 |
+
if position < 0 or position >= self._count:
|
| 1233 |
+
raise IndexError(index)
|
| 1234 |
+
with _connect_sqlite_read_only(self.path) as connection:
|
| 1235 |
+
row = connection.execute(
|
| 1236 |
+
f"{self._row_query()} AND r.position = ?",
|
| 1237 |
+
(self.run_id, position),
|
| 1238 |
+
).fetchone()
|
| 1239 |
+
if row is None:
|
| 1240 |
+
raise IndexError(index)
|
| 1241 |
+
return _sqlite_record_from_row(self.path, self.run_id, row)
|
| 1242 |
+
|
| 1243 |
+
|
| 1244 |
+
def load_sqlite_result(
|
| 1245 |
+
path: str | Path,
|
| 1246 |
+
*,
|
| 1247 |
+
run_id: str | None = None,
|
| 1248 |
+
positions: Iterable[int] | None = None,
|
| 1249 |
+
record_ids: Iterable[str] | None = None,
|
| 1250 |
+
sequences: Iterable[str] | None = None,
|
| 1251 |
+
) -> EmbeddingResult:
|
| 1252 |
+
"""Load one SQLite run read-only, optionally in explicit selector order.
|
| 1253 |
+
|
| 1254 |
+
Exactly one selector may be supplied. Repeated selectors are retained. An
|
| 1255 |
+
ID or sequence selector that matches multiple stored rows returns those
|
| 1256 |
+
rows in their original order for every occurrence of that selector.
|
| 1257 |
+
"""
|
| 1258 |
+
|
| 1259 |
+
path = Path(path).resolve()
|
| 1260 |
+
supplied_selectors = sum(
|
| 1261 |
+
selector is not None for selector in (positions, record_ids, sequences)
|
| 1262 |
+
)
|
| 1263 |
+
if supplied_selectors > 1:
|
| 1264 |
+
raise ValueError("Choose at most one of positions, record_ids, or sequences.")
|
| 1265 |
+
normalized_positions = tuple(positions) if positions is not None else None
|
| 1266 |
+
normalized_ids = tuple(record_ids) if record_ids is not None else None
|
| 1267 |
+
normalized_sequences = tuple(sequences) if sequences is not None else None
|
| 1268 |
+
if normalized_positions is not None and not all(
|
| 1269 |
+
isinstance(position, int) and not isinstance(position, bool) and position >= 0
|
| 1270 |
+
for position in normalized_positions
|
| 1271 |
+
):
|
| 1272 |
+
raise ValueError("positions must contain non-negative integers.")
|
| 1273 |
+
for name, values in (
|
| 1274 |
+
("record_ids", normalized_ids),
|
| 1275 |
+
("sequences", normalized_sequences),
|
| 1276 |
+
):
|
| 1277 |
+
if values is not None and not all(isinstance(value, str) for value in values):
|
| 1278 |
+
raise TypeError(f"{name} must contain strings.")
|
| 1279 |
+
|
| 1280 |
+
with _connect_sqlite_read_only(path) as connection:
|
| 1281 |
+
_validate_sqlite_result_schema(connection, path)
|
| 1282 |
+
if run_id is None:
|
| 1283 |
+
run_columns = {
|
| 1284 |
+
str(info[1]) for info in connection.execute("PRAGMA table_info(runs)").fetchall()
|
| 1285 |
+
}
|
| 1286 |
+
if "published_order" in run_columns:
|
| 1287 |
+
row = connection.execute(
|
| 1288 |
+
"SELECT run_id, metadata_json FROM runs "
|
| 1289 |
+
"WHERE published_order IS NOT NULL "
|
| 1290 |
+
"ORDER BY published_order DESC, rowid DESC LIMIT 1"
|
| 1291 |
+
).fetchone()
|
| 1292 |
+
else:
|
| 1293 |
+
row = connection.execute(
|
| 1294 |
+
"SELECT run_id, metadata_json FROM runs "
|
| 1295 |
+
"ORDER BY created_at DESC, rowid DESC LIMIT 1"
|
| 1296 |
+
).fetchone()
|
| 1297 |
+
else:
|
| 1298 |
+
row = connection.execute(
|
| 1299 |
+
"SELECT run_id, metadata_json FROM runs WHERE run_id = ?", (run_id,)
|
| 1300 |
+
).fetchone()
|
| 1301 |
+
if row is None:
|
| 1302 |
+
raise KeyError(f"No embedding run found in {path}.")
|
| 1303 |
+
selected_run, metadata_json = row
|
| 1304 |
+
metadata = json.loads(metadata_json)
|
| 1305 |
+
if not isinstance(metadata, dict):
|
| 1306 |
+
raise ValueError("SQLite run metadata must contain a JSON object.")
|
| 1307 |
+
row_prefix = (
|
| 1308 |
+
"SELECT r.position, r.record_id, r.sequence, t.dtype, t.shape_json, t.sha256 "
|
| 1309 |
+
"FROM records r JOIN tensors t USING (run_id, position) "
|
| 1310 |
+
"WHERE r.run_id = ?"
|
| 1311 |
+
)
|
| 1312 |
+
record_count, minimum_position, maximum_position = connection.execute(
|
| 1313 |
+
"SELECT COUNT(*), MIN(position), MAX(position) FROM records WHERE run_id = ?",
|
| 1314 |
+
(selected_run,),
|
| 1315 |
+
).fetchone()
|
| 1316 |
+
(tensor_count,) = connection.execute(
|
| 1317 |
+
"SELECT COUNT(*) FROM tensors WHERE run_id = ?", (selected_run,)
|
| 1318 |
+
).fetchone()
|
| 1319 |
+
(joined_count,) = connection.execute(
|
| 1320 |
+
"SELECT COUNT(*) FROM records r JOIN tensors t USING (run_id, position) "
|
| 1321 |
+
"WHERE r.run_id = ?",
|
| 1322 |
+
(selected_run,),
|
| 1323 |
+
).fetchone()
|
| 1324 |
+
if (
|
| 1325 |
+
tensor_count != record_count
|
| 1326 |
+
or joined_count != record_count
|
| 1327 |
+
or (record_count and (minimum_position != 0 or maximum_position != record_count - 1))
|
| 1328 |
+
):
|
| 1329 |
+
raise ValueError("SQLite embedding run has inconsistent or non-contiguous records.")
|
| 1330 |
+
metadata_count = metadata.get("record_count")
|
| 1331 |
+
if (
|
| 1332 |
+
not isinstance(metadata_count, int)
|
| 1333 |
+
or isinstance(metadata_count, bool)
|
| 1334 |
+
or metadata_count != record_count
|
| 1335 |
+
):
|
| 1336 |
+
raise ValueError("SQLite metadata record count does not match stored records.")
|
| 1337 |
+
descriptor_cursor = connection.execute(f"{row_prefix} ORDER BY r.position", (selected_run,))
|
| 1338 |
+
while descriptor_rows := descriptor_cursor.fetchmany(1_024):
|
| 1339 |
+
for descriptor_row in descriptor_rows:
|
| 1340 |
+
_validate_sqlite_descriptor_row(descriptor_row)
|
| 1341 |
+
if supplied_selectors == 0:
|
| 1342 |
+
rows: list[tuple[Any, ...]] | None = None
|
| 1343 |
+
else:
|
| 1344 |
+
selector_values: tuple[Any, ...]
|
| 1345 |
+
selector_column: str
|
| 1346 |
+
if normalized_positions is not None:
|
| 1347 |
+
selector_values = normalized_positions
|
| 1348 |
+
selector_column = "r.position"
|
| 1349 |
+
elif normalized_ids is not None:
|
| 1350 |
+
selector_values = normalized_ids
|
| 1351 |
+
selector_column = "r.record_id"
|
| 1352 |
+
else:
|
| 1353 |
+
if normalized_sequences is None:
|
| 1354 |
+
raise RuntimeError("Filtered SQLite retrieval resolved no selector values.")
|
| 1355 |
+
selector_values = normalized_sequences
|
| 1356 |
+
selector_column = "r.sequence"
|
| 1357 |
+
fetched: list[tuple[Any, ...]] = []
|
| 1358 |
+
unique_values = tuple(dict.fromkeys(selector_values))
|
| 1359 |
+
for start in range(0, len(unique_values), 900):
|
| 1360 |
+
chunk = unique_values[start : start + 900]
|
| 1361 |
+
placeholders = ",".join("?" for _ in chunk)
|
| 1362 |
+
fetched.extend(
|
| 1363 |
+
connection.execute(
|
| 1364 |
+
f"{row_prefix} AND {selector_column} IN ({placeholders}) "
|
| 1365 |
+
"ORDER BY r.position",
|
| 1366 |
+
(selected_run, *chunk),
|
| 1367 |
+
).fetchall()
|
| 1368 |
+
)
|
| 1369 |
+
value_index = (
|
| 1370 |
+
0 if normalized_positions is not None else (1 if normalized_ids is not None else 2)
|
| 1371 |
+
)
|
| 1372 |
+
matched: dict[Any, list[tuple[Any, ...]]] = {}
|
| 1373 |
+
for fetched_row in sorted(fetched, key=lambda item: int(item[0])):
|
| 1374 |
+
matched.setdefault(fetched_row[value_index], []).append(fetched_row)
|
| 1375 |
+
missing = [value for value in selector_values if value not in matched]
|
| 1376 |
+
if missing:
|
| 1377 |
+
raise KeyError(f"SQLite embedding selectors were not found: {missing!r}.")
|
| 1378 |
+
rows = [
|
| 1379 |
+
fetched_row for value in selector_values for fetched_row in matched.get(value, ())
|
| 1380 |
+
]
|
| 1381 |
+
|
| 1382 |
+
if rows is None:
|
| 1383 |
+
return EmbeddingResult(
|
| 1384 |
+
_SQLiteRecordSequence(path, selected_run, int(record_count)),
|
| 1385 |
+
metadata,
|
| 1386 |
+
)
|
| 1387 |
+
records = [_sqlite_record_from_row(path, selected_run, selected_row) for selected_row in rows]
|
| 1388 |
+
if supplied_selectors:
|
| 1389 |
+
metadata = dict(metadata)
|
| 1390 |
+
metadata["selection"] = {
|
| 1391 |
+
"kind": (
|
| 1392 |
+
"positions"
|
| 1393 |
+
if normalized_positions is not None
|
| 1394 |
+
else "record_ids"
|
| 1395 |
+
if normalized_ids is not None
|
| 1396 |
+
else "sequences"
|
| 1397 |
+
),
|
| 1398 |
+
"count": len(rows),
|
| 1399 |
+
"duplicate_policy": "preserve-request-order",
|
| 1400 |
+
}
|
| 1401 |
+
return EmbeddingResult(records, metadata)
|
| 1402 |
+
|
| 1403 |
+
|
| 1404 |
+
def load_legacy_pth(path: str | Path, *, allow_unsafe_pickle: bool = False) -> EmbeddingResult:
|
| 1405 |
+
"""Import a legacy mapping-only ``.pth`` file after explicit opt-in."""
|
| 1406 |
+
|
| 1407 |
+
if not allow_unsafe_pickle:
|
| 1408 |
+
raise ValueError(
|
| 1409 |
+
"Legacy .pth loading can execute pickle payloads. Pass "
|
| 1410 |
+
"allow_unsafe_pickle=True only for a trusted file."
|
| 1411 |
+
)
|
| 1412 |
+
payload = torch.load(Path(path), map_location="cpu", weights_only=False)
|
| 1413 |
+
if not isinstance(payload, dict):
|
| 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()))
|
| 1420 |
+
return EmbeddingResult(records, {"format": "legacy-pth", "unsafe_pickle": True})
|
| 1421 |
+
|
| 1422 |
+
|
| 1423 |
+
_LEGACY_COMPACT_VERSION = 0x01
|
| 1424 |
+
_LEGACY_CODE_DTYPES: dict[int, tuple[np.dtype[Any], torch.dtype]] = {
|
| 1425 |
+
0: (np.dtype(np.float16), torch.float16),
|
| 1426 |
+
# Legacy BF16 blobs stored FP16 payload bytes and converted back to BF16.
|
| 1427 |
+
1: (np.dtype(np.float16), torch.bfloat16),
|
| 1428 |
+
2: (np.dtype(np.float32), torch.float32),
|
| 1429 |
+
}
|
| 1430 |
+
|
| 1431 |
+
|
| 1432 |
+
def _decode_legacy_sqlite_blob(
|
| 1433 |
+
data: bytes,
|
| 1434 |
+
*,
|
| 1435 |
+
fallback_shape: tuple[int, ...] | None,
|
| 1436 |
+
allow_unsafe_pickle: bool,
|
| 1437 |
+
) -> Tensor:
|
| 1438 |
+
if len(data) >= 6 and data[0] == _LEGACY_COMPACT_VERSION:
|
| 1439 |
+
dtype_code = int(data[1])
|
| 1440 |
+
if dtype_code not in _LEGACY_CODE_DTYPES:
|
| 1441 |
+
raise ValueError(f"Unsupported legacy compact dtype code {dtype_code}.")
|
| 1442 |
+
(ndim,) = struct.unpack_from("<i", data, 2)
|
| 1443 |
+
if ndim < 0 or ndim > 16 or len(data) < 6 + 4 * ndim:
|
| 1444 |
+
raise ValueError("Malformed legacy compact embedding header.")
|
| 1445 |
+
shape = tuple(int(value) for value in struct.unpack_from(f"<{ndim}i", data, 6))
|
| 1446 |
+
if any(size < 0 for size in shape):
|
| 1447 |
+
raise ValueError("Malformed negative legacy embedding dimension.")
|
| 1448 |
+
numpy_dtype, target_dtype = _LEGACY_CODE_DTYPES[dtype_code]
|
| 1449 |
+
offset = 6 + 4 * ndim
|
| 1450 |
+
expected = int(np.prod(shape, dtype=np.int64)) * numpy_dtype.itemsize
|
| 1451 |
+
if len(data) - offset != expected:
|
| 1452 |
+
raise ValueError("Legacy compact embedding payload length does not match shape.")
|
| 1453 |
+
array = np.frombuffer(data, dtype=numpy_dtype, offset=offset).copy().reshape(shape)
|
| 1454 |
+
return torch.from_numpy(array).to(dtype=target_dtype)
|
| 1455 |
+
|
| 1456 |
+
try:
|
| 1457 |
+
loaded = torch.load(io.BytesIO(data), map_location="cpu", weights_only=True)
|
| 1458 |
+
except Exception as safe_error:
|
| 1459 |
+
if allow_unsafe_pickle:
|
| 1460 |
+
loaded = torch.load(io.BytesIO(data), map_location="cpu", weights_only=False)
|
| 1461 |
+
elif fallback_shape is None:
|
| 1462 |
+
raise ValueError(
|
| 1463 |
+
"Legacy embedding blob is neither compact nor safely loadable. "
|
| 1464 |
+
"Provide fallback_shape for raw FP32 bytes, or set "
|
| 1465 |
+
"allow_unsafe_pickle=True only for a trusted database."
|
| 1466 |
+
) from safe_error
|
| 1467 |
+
else:
|
| 1468 |
+
expected = int(np.prod(fallback_shape, dtype=np.int64)) * 4
|
| 1469 |
+
if len(data) != expected:
|
| 1470 |
+
raise ValueError(
|
| 1471 |
+
"Legacy raw FP32 payload length does not match fallback_shape."
|
| 1472 |
+
) from safe_error
|
| 1473 |
+
array = np.frombuffer(data, dtype=np.float32).copy().reshape(fallback_shape)
|
| 1474 |
+
return torch.from_numpy(array)
|
| 1475 |
+
if not isinstance(loaded, Tensor):
|
| 1476 |
+
raise ValueError("Legacy serialized embedding payload must contain one tensor.")
|
| 1477 |
+
return loaded.detach().cpu()
|
| 1478 |
+
|
| 1479 |
+
|
| 1480 |
+
def convert_legacy_sqlite(
|
| 1481 |
+
source: str | Path,
|
| 1482 |
+
output: str | Path,
|
| 1483 |
+
*,
|
| 1484 |
+
fallback_shape: tuple[int, ...] | None = None,
|
| 1485 |
+
allow_unsafe_pickle: bool = False,
|
| 1486 |
+
metadata: dict[str, Any] | None = None,
|
| 1487 |
+
) -> EmbeddingResult:
|
| 1488 |
+
"""Convert the v0 ``embeddings(sequence, embedding)`` database safely.
|
| 1489 |
+
|
| 1490 |
+
The source is opened read-only. Compact blobs and ``weights_only`` Torch
|
| 1491 |
+
tensors are accepted by default. Unsafe general pickle deserialization
|
| 1492 |
+
remains an explicit opt-in.
|
| 1493 |
+
"""
|
| 1494 |
+
|
| 1495 |
+
source_path = Path(source)
|
| 1496 |
+
output_path = Path(output)
|
| 1497 |
+
if source_path.resolve() == output_path.resolve():
|
| 1498 |
+
raise ValueError("Legacy SQLite conversion requires a different output path.")
|
| 1499 |
+
if fallback_shape is not None and (
|
| 1500 |
+
not fallback_shape or any(not isinstance(size, int) or size < 0 for size in fallback_shape)
|
| 1501 |
+
):
|
| 1502 |
+
raise ValueError("fallback_shape must contain non-negative integer dimensions.")
|
| 1503 |
+
with _connect_sqlite_read_only(source_path) as connection:
|
| 1504 |
+
columns = {
|
| 1505 |
+
str(row[1]) for row in connection.execute("PRAGMA table_info(embeddings)").fetchall()
|
| 1506 |
+
}
|
| 1507 |
+
if not {"sequence", "embedding"}.issubset(columns):
|
| 1508 |
+
raise ValueError("Legacy SQLite database must contain embeddings(sequence, embedding).")
|
| 1509 |
+
rows = connection.execute(
|
| 1510 |
+
"SELECT sequence, embedding FROM embeddings ORDER BY rowid"
|
| 1511 |
+
).fetchall()
|
| 1512 |
+
if not rows:
|
| 1513 |
+
raise ValueError("Legacy SQLite database contains no embeddings.")
|
| 1514 |
+
|
| 1515 |
+
records: list[EmbeddingRecord] = []
|
| 1516 |
+
content_digest = hashlib.sha256()
|
| 1517 |
+
for position, (sequence, data) in enumerate(rows):
|
| 1518 |
+
if not isinstance(sequence, str) or not sequence:
|
| 1519 |
+
raise ValueError("Legacy embedding sequences must be non-empty strings.")
|
| 1520 |
+
if not isinstance(data, bytes):
|
| 1521 |
+
data = bytes(data)
|
| 1522 |
+
tensor = _decode_legacy_sqlite_blob(
|
| 1523 |
+
data,
|
| 1524 |
+
fallback_shape=fallback_shape,
|
| 1525 |
+
allow_unsafe_pickle=allow_unsafe_pickle,
|
| 1526 |
+
)
|
| 1527 |
+
tensor_digest = tensor_sha256(tensor)
|
| 1528 |
+
for value in (sequence.encode("utf-8"), tensor_digest.encode("ascii")):
|
| 1529 |
+
content_digest.update(len(value).to_bytes(8, "big"))
|
| 1530 |
+
content_digest.update(value)
|
| 1531 |
+
records.append(EmbeddingRecord(str(position), sequence, tensor))
|
| 1532 |
+
|
| 1533 |
+
content_sha256 = content_digest.hexdigest()
|
| 1534 |
+
run_fingerprint = hashlib.sha256(
|
| 1535 |
+
f"fastplms-legacy-sqlite-v1:{content_sha256}".encode("ascii")
|
| 1536 |
+
).hexdigest()
|
| 1537 |
+
converted_metadata: dict[str, Any] = {
|
| 1538 |
+
"format_version": 1,
|
| 1539 |
+
"run_fingerprint": run_fingerprint,
|
| 1540 |
+
"source_format": "legacy-fastplms-sqlite-v0",
|
| 1541 |
+
"source_content_sha256": content_sha256,
|
| 1542 |
+
"unsafe_pickle": allow_unsafe_pickle,
|
| 1543 |
+
"complete": True,
|
| 1544 |
+
}
|
| 1545 |
+
if metadata:
|
| 1546 |
+
converted_metadata["conversion_metadata"] = _jsonable(metadata)
|
| 1547 |
+
return save_sqlite_result(
|
| 1548 |
+
EmbeddingResult(records, converted_metadata),
|
| 1549 |
+
output_path,
|
| 1550 |
+
)
|
| 1551 |
+
|
| 1552 |
+
|
| 1553 |
+
def save_result(
|
| 1554 |
+
result: EmbeddingResult,
|
| 1555 |
+
path: str | Path,
|
| 1556 |
+
*,
|
| 1557 |
+
format: str = "safetensors",
|
| 1558 |
+
shard_size: int = DEFAULT_SHARD_SIZE,
|
| 1559 |
+
) -> EmbeddingResult:
|
| 1560 |
+
if format == "safetensors":
|
| 1561 |
+
return save_safetensors_result(result, path, shard_size=shard_size)
|
| 1562 |
+
if format == "sqlite":
|
| 1563 |
+
return save_sqlite_result(result, path)
|
| 1564 |
+
if format == "pth":
|
| 1565 |
+
raise ValueError("Writing pickle-based .pth embeddings is not supported.")
|
| 1566 |
+
raise ValueError("format must be 'safetensors' or 'sqlite'.")
|
| 1567 |
+
|
| 1568 |
+
|
| 1569 |
+
def load_result(path: str | Path, *, format: str = "safetensors") -> EmbeddingResult:
|
| 1570 |
+
if format == "safetensors":
|
| 1571 |
+
return load_safetensors_result(path)
|
| 1572 |
+
if format == "sqlite":
|
| 1573 |
+
return load_sqlite_result(path)
|
| 1574 |
+
raise ValueError("format must be 'safetensors' or 'sqlite'.")
|
| 1575 |
+
|
| 1576 |
+
|
| 1577 |
+
__all__ = [
|
| 1578 |
+
"DEFAULT_SHARD_SIZE",
|
| 1579 |
+
"SafetensorsStreamWriter",
|
| 1580 |
+
"append_sqlite_records",
|
| 1581 |
+
"convert_legacy_sqlite",
|
| 1582 |
+
"garbage_collect_safetensors_generations",
|
| 1583 |
+
"initialize_sqlite_run",
|
| 1584 |
+
"load_legacy_pth",
|
| 1585 |
+
"load_result",
|
| 1586 |
+
"load_safetensors_result",
|
| 1587 |
+
"load_sqlite_result",
|
| 1588 |
+
"safetensors_result_exists",
|
| 1589 |
+
"save_result",
|
| 1590 |
+
"save_safetensors_result",
|
| 1591 |
+
"save_sqlite_result",
|
| 1592 |
+
"tensor_sha256",
|
| 1593 |
+
"update_sqlite_run_metadata",
|
| 1594 |
+
]
|
fastplms/embeddings/types.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Public value types for dataset embedding."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 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 |
+
|
| 12 |
+
@dataclass(frozen=True, slots=True)
|
| 13 |
+
class EmbeddingInput:
|
| 14 |
+
"""One named protein sequence supplied to :func:`embed_dataset`."""
|
| 15 |
+
|
| 16 |
+
id: str
|
| 17 |
+
sequence: str
|
| 18 |
+
|
| 19 |
+
def __post_init__(self) -> None:
|
| 20 |
+
if not isinstance(self.id, str) or not self.id:
|
| 21 |
+
raise ValueError("EmbeddingInput.id must be a non-empty string.")
|
| 22 |
+
if not isinstance(self.sequence, str) or not self.sequence:
|
| 23 |
+
raise ValueError("EmbeddingInput.sequence must be a non-empty string.")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass(frozen=True, slots=True)
|
| 27 |
+
class LazyTensorReference:
|
| 28 |
+
"""A tensor stored outside memory and loaded only when requested."""
|
| 29 |
+
|
| 30 |
+
source: str
|
| 31 |
+
key: str
|
| 32 |
+
dtype: str
|
| 33 |
+
shape: tuple[int, ...]
|
| 34 |
+
sha256: str
|
| 35 |
+
_loader: Callable[[], Tensor] = field(repr=False, compare=False)
|
| 36 |
+
|
| 37 |
+
def load(self, *, verify: bool = True) -> Tensor:
|
| 38 |
+
"""Load X and optionally verify its content digest."""
|
| 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:
|
| 46 |
+
raise ValueError(
|
| 47 |
+
f"Stored tensor {self.key!r} has shape {tuple(X.shape)}, expected {self.shape}."
|
| 48 |
+
)
|
| 49 |
+
dtype = str(X.dtype).removeprefix("torch.")
|
| 50 |
+
if dtype != self.dtype:
|
| 51 |
+
raise ValueError(
|
| 52 |
+
f"Stored tensor {self.key!r} has dtype {dtype!r}, expected {self.dtype!r}."
|
| 53 |
+
)
|
| 54 |
+
if verify:
|
| 55 |
+
from .storage import tensor_sha256
|
| 56 |
+
|
| 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
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@dataclass(frozen=True, slots=True)
|
| 67 |
+
class EmbeddingRecord:
|
| 68 |
+
"""One ordered embedding result."""
|
| 69 |
+
|
| 70 |
+
id: str
|
| 71 |
+
sequence: str
|
| 72 |
+
tensor: TensorValue
|
| 73 |
+
|
| 74 |
+
def __post_init__(self) -> None:
|
| 75 |
+
if not isinstance(self.id, str) or not self.id:
|
| 76 |
+
raise ValueError("EmbeddingRecord.id must be a non-empty string.")
|
| 77 |
+
if not isinstance(self.sequence, str) or not self.sequence:
|
| 78 |
+
raise ValueError("EmbeddingRecord.sequence must be a non-empty string.")
|
| 79 |
+
if not isinstance(self.tensor, (Tensor, LazyTensorReference)):
|
| 80 |
+
raise TypeError("EmbeddingRecord.tensor must be a Tensor or LazyTensorReference.")
|
| 81 |
+
|
| 82 |
+
def load_tensor(self, *, verify: bool = True) -> Tensor:
|
| 83 |
+
"""Return X regardless of whether this record is memory-backed or lazy."""
|
| 84 |
+
|
| 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]):
|
| 93 |
+
"""Ordered embedding records and the metadata needed to reproduce them."""
|
| 94 |
+
|
| 95 |
+
def __init__(
|
| 96 |
+
self,
|
| 97 |
+
records: Sequence[EmbeddingRecord],
|
| 98 |
+
metadata: Mapping[str, Any] | None = None,
|
| 99 |
+
) -> None:
|
| 100 |
+
self.records: Sequence[EmbeddingRecord] = (
|
| 101 |
+
records if getattr(records, "_fastplms_immutable_sequence", False) else tuple(records)
|
| 102 |
+
)
|
| 103 |
+
self.metadata = dict(metadata or {})
|
| 104 |
+
|
| 105 |
+
def __len__(self) -> int:
|
| 106 |
+
return len(self.records)
|
| 107 |
+
|
| 108 |
+
def __iter__(self) -> Iterator[EmbeddingRecord]:
|
| 109 |
+
return iter(self.records)
|
| 110 |
+
|
| 111 |
+
@overload
|
| 112 |
+
def __getitem__(self, index: int, /) -> EmbeddingRecord: ...
|
| 113 |
+
|
| 114 |
+
@overload
|
| 115 |
+
def __getitem__(self, index: slice, /) -> Sequence[EmbeddingRecord]: ...
|
| 116 |
+
|
| 117 |
+
def __getitem__(self, index: int | slice) -> EmbeddingRecord | Sequence[EmbeddingRecord]:
|
| 118 |
+
return self.records[index]
|
| 119 |
+
|
| 120 |
+
def as_dict(
|
| 121 |
+
self,
|
| 122 |
+
*,
|
| 123 |
+
key: Literal["id", "sequence"] = "id",
|
| 124 |
+
duplicates: Literal["error", "first", "last"] = "error",
|
| 125 |
+
materialize: bool = True,
|
| 126 |
+
) -> dict[str, TensorValue]:
|
| 127 |
+
"""Convert records to a mapping under an explicit duplicate policy."""
|
| 128 |
+
|
| 129 |
+
if key not in {"id", "sequence"}:
|
| 130 |
+
raise ValueError("key must be 'id' or 'sequence'.")
|
| 131 |
+
if duplicates not in {"error", "first", "last"}:
|
| 132 |
+
raise ValueError("duplicates must be 'error', 'first', or 'last'.")
|
| 133 |
+
if not isinstance(materialize, bool):
|
| 134 |
+
raise TypeError("materialize must be a boolean.")
|
| 135 |
+
output: dict[str, TensorValue] = {}
|
| 136 |
+
for record in self.records:
|
| 137 |
+
record_key = getattr(record, key)
|
| 138 |
+
if record_key in output:
|
| 139 |
+
if duplicates == "error":
|
| 140 |
+
raise ValueError(
|
| 141 |
+
f"Duplicate {key} {record_key!r}; choose duplicates='first' "
|
| 142 |
+
"or duplicates='last' explicitly."
|
| 143 |
+
)
|
| 144 |
+
if duplicates == "first":
|
| 145 |
+
continue
|
| 146 |
+
output[record_key] = record.load_tensor() if materialize else record.tensor
|
| 147 |
+
return output
|
| 148 |
+
|
| 149 |
+
def materialize(self, *, verify: bool = True) -> EmbeddingResult:
|
| 150 |
+
"""Return an equivalent result with every X loaded into CPU memory."""
|
| 151 |
+
|
| 152 |
+
if not isinstance(verify, bool):
|
| 153 |
+
raise TypeError("verify must be a boolean.")
|
| 154 |
+
return EmbeddingResult(
|
| 155 |
+
[
|
| 156 |
+
EmbeddingRecord(
|
| 157 |
+
id=record.id,
|
| 158 |
+
sequence=record.sequence,
|
| 159 |
+
tensor=record.load_tensor(verify=verify),
|
| 160 |
+
)
|
| 161 |
+
for record in self.records
|
| 162 |
+
],
|
| 163 |
+
self.metadata,
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
@dataclass(frozen=True, slots=True)
|
| 168 |
+
class EmbeddingBatch:
|
| 169 |
+
"""Internal model-to-runner contract.
|
| 170 |
+
|
| 171 |
+
``X`` has shape ``(b, l, d)`` and ``residue_mask`` has shape ``(b, l)``.
|
| 172 |
+
``attentions`` may contain layer/head attention matrices for ``parti``.
|
| 173 |
+
"""
|
| 174 |
+
|
| 175 |
+
X: Tensor
|
| 176 |
+
residue_mask: Tensor
|
| 177 |
+
attentions: Tensor | tuple[Tensor, ...] | None = None
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
__all__ = [
|
| 181 |
+
"EmbeddingBatch",
|
| 182 |
+
"EmbeddingInput",
|
| 183 |
+
"EmbeddingRecord",
|
| 184 |
+
"EmbeddingResult",
|
| 185 |
+
"LazyTensorReference",
|
| 186 |
+
"TensorValue",
|
| 187 |
+
]
|
fastplms/models.toml
ADDED
|
@@ -0,0 +1,1223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
schema_version = 1
|
| 2 |
+
legal_files = [
|
| 3 |
+
"LICENSE=sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
|
| 4 |
+
"THIRD_PARTY_NOTICES.md=sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
|
| 5 |
+
]
|
| 6 |
+
|
| 7 |
+
[[attention_kernels]]
|
| 8 |
+
implementation = "flash_attention_2"
|
| 9 |
+
repository = "kernels-community/flash-attn2"
|
| 10 |
+
revision = "db6b51744f0cd7061386442c09df890fc6d9f47e"
|
| 11 |
+
version = 2
|
| 12 |
+
expected_variant = "flash_attn2"
|
| 13 |
+
dtypes = ["bfloat16"]
|
| 14 |
+
|
| 15 |
+
[[attention_kernels]]
|
| 16 |
+
implementation = "flash_attention_3"
|
| 17 |
+
repository = "kernels-community/flash-attn3"
|
| 18 |
+
revision = "43f0bd269777115d94ff826e0d113ce9c1c9087b"
|
| 19 |
+
version = 1
|
| 20 |
+
expected_variant = "flash_attn3"
|
| 21 |
+
dtypes = ["bfloat16"]
|
| 22 |
+
|
| 23 |
+
[[runtime_assets]]
|
| 24 |
+
id = "esmfold2_ccd"
|
| 25 |
+
repository = "biohub/ESMFold2"
|
| 26 |
+
revision = "1ebf0e3481a5184eb6171d40615c79e384b48796"
|
| 27 |
+
path = "ccd.pkl"
|
| 28 |
+
sha256 = "9ff44b1927c6b9198e38ffe0928706827a09a350c15530beeeabebfa88038fc5"
|
| 29 |
+
size = 417306584
|
| 30 |
+
consumer_family = "esmfold2"
|
| 31 |
+
trust_kind = "hash_pinned_pickle"
|
| 32 |
+
license = "MIT"
|
| 33 |
+
offline_behavior = "requires_cached_verified_file"
|
| 34 |
+
|
| 35 |
+
[[upstreams]]
|
| 36 |
+
id = "ankh"
|
| 37 |
+
path = "vendor/upstream/ankh"
|
| 38 |
+
url = "https://github.com/agemagician/Ankh.git"
|
| 39 |
+
revision = "02b4e25ce5389b9e771c9df6e546c62af1216f8e"
|
| 40 |
+
license = "CC-BY-NC-SA-4.0"
|
| 41 |
+
license_files = ["LICENSE.md"]
|
| 42 |
+
license_digests = ["LICENSE.md=sha256:cd041d7f9f52936e8824ac3f754e9c67410763205fc8a7020ba74fc8b6edc088"]
|
| 43 |
+
distribution_files = ["LICENSE.md=sha256:cd041d7f9f52936e8824ac3f754e9c67410763205fc8a7020ba74fc8b6edc088"]
|
| 44 |
+
|
| 45 |
+
[[upstreams]]
|
| 46 |
+
id = "biohub-esm"
|
| 47 |
+
path = "vendor/upstream/biohub-esm"
|
| 48 |
+
url = "https://github.com/Biohub/esm.git"
|
| 49 |
+
revision = "82ee35553d39169d678f784c8d3f8712ffd7d2c4"
|
| 50 |
+
license = "MIT"
|
| 51 |
+
license_files = ["LICENSE.md", "THIRD_PARTY_NOTICE.md"]
|
| 52 |
+
license_digests = [
|
| 53 |
+
"LICENSE.md=sha256:b63df9ca1dd96b3b21eec226b51b236d0bd152ac20eafc43aad46bf832b48d8a",
|
| 54 |
+
"THIRD_PARTY_NOTICE.md=sha256:5bff8515ba4e0f53abdc43714c180b79c5b606160497d98de741a369cb9b6a23",
|
| 55 |
+
]
|
| 56 |
+
distribution_files = [
|
| 57 |
+
"LICENSE.md=sha256:b63df9ca1dd96b3b21eec226b51b236d0bd152ac20eafc43aad46bf832b48d8a",
|
| 58 |
+
"THIRD_PARTY_NOTICE.md=sha256:5bff8515ba4e0f53abdc43714c180b79c5b606160497d98de741a369cb9b6a23",
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
[[upstreams]]
|
| 62 |
+
id = "biohub-transformers"
|
| 63 |
+
path = "vendor/upstream/biohub-transformers"
|
| 64 |
+
url = "https://github.com/Biohub/transformers.git"
|
| 65 |
+
revision = "3a8956fb4d4ea16b0ec8e71deef2c2909b6a5cbf"
|
| 66 |
+
license = "Apache-2.0"
|
| 67 |
+
license_files = ["LICENSE"]
|
| 68 |
+
license_digests = ["LICENSE=sha256:77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049"]
|
| 69 |
+
distribution_files = ["LICENSE=sha256:77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049"]
|
| 70 |
+
|
| 71 |
+
[[upstreams]]
|
| 72 |
+
id = "boltz"
|
| 73 |
+
path = "vendor/upstream/boltz"
|
| 74 |
+
url = "https://github.com/jwohlwend/boltz.git"
|
| 75 |
+
revision = "b1ebfc46ecf57f5414e0d1a6f9027bbb122c53bc"
|
| 76 |
+
license = "MIT"
|
| 77 |
+
license_files = ["LICENSE"]
|
| 78 |
+
license_digests = ["LICENSE=sha256:f0667fd5e66c51e1ba8ddaa0249c6d7225b30037e02c45782d8f2c2943ac2617"]
|
| 79 |
+
distribution_files = ["LICENSE=sha256:f0667fd5e66c51e1ba8ddaa0249c6d7225b30037e02c45782d8f2c2943ac2617"]
|
| 80 |
+
|
| 81 |
+
[[upstreams]]
|
| 82 |
+
id = "dplm"
|
| 83 |
+
path = "vendor/upstream/dplm"
|
| 84 |
+
url = "https://github.com/bytedance/dplm.git"
|
| 85 |
+
revision = "8a2e15e53416b4536f03f79ad1f6f6a9cbd5e19d"
|
| 86 |
+
license = "Apache-2.0"
|
| 87 |
+
license_files = ["LICENSE"]
|
| 88 |
+
license_digests = ["LICENSE=sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"]
|
| 89 |
+
distribution_files = [
|
| 90 |
+
"LICENSE=sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
|
| 91 |
+
"PROVENANCE.md=sha256:a659f74be9073cf1ad2d2f7071531ca56959b421f111152cf4c41184ace5970e",
|
| 92 |
+
]
|
| 93 |
+
|
| 94 |
+
[[upstreams]]
|
| 95 |
+
id = "e1"
|
| 96 |
+
path = "vendor/upstream/e1"
|
| 97 |
+
url = "https://github.com/Profluent-AI/E1.git"
|
| 98 |
+
revision = "bfd2620a602248499f3d2583d85a7ecddf0b6e02"
|
| 99 |
+
license = "Apache-2.0 AND Profluent-E1-Agreement"
|
| 100 |
+
license_files = ["LICENSE", "ATTRIBUTION", "NOTICE"]
|
| 101 |
+
license_digests = [
|
| 102 |
+
"LICENSE=sha256:8ef1dd556091544db3044164a8015424a3dcb3450fb3765a81b88463551bbe81",
|
| 103 |
+
"ATTRIBUTION=sha256:deb22b250f6491b649eda5c63e080dd56486b8d2736cea6a52ef875436214367",
|
| 104 |
+
"NOTICE=sha256:6de9db0320b4ee82f665c0951d8fd4cd53701a659c9dbce9bc3e3ea6afc4c6b3",
|
| 105 |
+
]
|
| 106 |
+
distribution_files = [
|
| 107 |
+
"LICENSE=sha256:8ef1dd556091544db3044164a8015424a3dcb3450fb3765a81b88463551bbe81",
|
| 108 |
+
"ATTRIBUTION=sha256:deb22b250f6491b649eda5c63e080dd56486b8d2736cea6a52ef875436214367",
|
| 109 |
+
"NOTICE=sha256:6de9db0320b4ee82f665c0951d8fd4cd53701a659c9dbce9bc3e3ea6afc4c6b3",
|
| 110 |
+
"Apache-2.0.txt=sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
|
| 111 |
+
"BSD-3-Clause.txt=sha256:36e1987f2f17db7f8ad36cd7a37dbb7aeaaf0ab68b97ab4b9d3556f3a7a76ae8",
|
| 112 |
+
"MODIFICATIONS.md=sha256:2506f47c0f5475af8e8ff2cff13eb8b79e8e25a08a054cdd617bf336536750ca",
|
| 113 |
+
]
|
| 114 |
+
|
| 115 |
+
[[upstreams]]
|
| 116 |
+
id = "fair-esm"
|
| 117 |
+
path = "vendor/upstream/fair-esm"
|
| 118 |
+
url = "https://github.com/facebookresearch/esm.git"
|
| 119 |
+
revision = "2b369911bb5b4b0dda914521b9475cad1656b2ac"
|
| 120 |
+
license = "MIT"
|
| 121 |
+
license_files = ["LICENSE"]
|
| 122 |
+
license_digests = ["LICENSE=sha256:da6d3703ed11cbe42bd212c725957c98da23cbff1998c05fa4b3d976d1a58e93"]
|
| 123 |
+
distribution_files = [
|
| 124 |
+
"LICENSE=sha256:da6d3703ed11cbe42bd212c725957c98da23cbff1998c05fa4b3d976d1a58e93",
|
| 125 |
+
"PROVENANCE.md=sha256:950adb94daf15e646ddf226dacfe2a8e77801aa0793e439a9a3490a48eb666e7",
|
| 126 |
+
]
|
| 127 |
+
|
| 128 |
+
[[upstreams]]
|
| 129 |
+
id = "openfold"
|
| 130 |
+
path = "vendor/upstream/openfold"
|
| 131 |
+
url = "https://github.com/aqlaboratory/openfold.git"
|
| 132 |
+
revision = "4b41059694619831a7db195b7e0988fc4ff3a307"
|
| 133 |
+
license = "Apache-2.0"
|
| 134 |
+
license_files = ["LICENSE"]
|
| 135 |
+
license_digests = ["LICENSE=sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"]
|
| 136 |
+
distribution_files = [
|
| 137 |
+
"LICENSE=sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
|
| 138 |
+
"MODIFICATIONS.md=sha256:fd6f0aa1086a0c996cf967b326d18e965660cda0ad5c7f36a3474a8490720da3",
|
| 139 |
+
"PROVENANCE.md=sha256:48c903db43a217a3126afaefbac60b7ddac7efda2dfcc0cbff0bffc7d6c30081",
|
| 140 |
+
]
|
| 141 |
+
|
| 142 |
+
[[upstreams]]
|
| 143 |
+
id = "protein-ttt"
|
| 144 |
+
path = "vendor/upstream/protein-ttt"
|
| 145 |
+
url = "https://github.com/anton-bushuiev/ProteinTTT.git"
|
| 146 |
+
revision = "fde2817cd84b936167cc76ccabf31e5c0fe49962"
|
| 147 |
+
license = "MIT"
|
| 148 |
+
license_files = ["LICENSE"]
|
| 149 |
+
license_digests = ["LICENSE=sha256:bb01e7d5554f9e2e117172e56551452f68a7818df7bc8e71cd7a776a1d4ba3df"]
|
| 150 |
+
distribution_files = [
|
| 151 |
+
"LICENSE=sha256:bb01e7d5554f9e2e117172e56551452f68a7818df7bc8e71cd7a776a1d4ba3df",
|
| 152 |
+
"PROVENANCE.md=sha256:dc641c37353c2efd50ccbdb316ca4aae495ec02c1563e0e15bac92f75fc482e5",
|
| 153 |
+
]
|
| 154 |
+
|
| 155 |
+
[families.esm2]
|
| 156 |
+
architecture = "ESM2"
|
| 157 |
+
upstreams = ["fair-esm"]
|
| 158 |
+
tokenizer_mode = "tokenizer"
|
| 159 |
+
public_input = "Amino-acid sequences tokenized to residue IDs"
|
| 160 |
+
extra = "core"
|
| 161 |
+
reference_container = "reference-esm2"
|
| 162 |
+
reference_adapter = "tests.parity.support.reference_adapters.esm2"
|
| 163 |
+
attention = ["eager", "sdpa", "flex_attention", "flash_attention_2", "flash_attention_3"]
|
| 164 |
+
dtypes = ["float32", "bfloat16"]
|
| 165 |
+
bf16_execution = "fp32_parameters_autocast"
|
| 166 |
+
precisions = ["default"]
|
| 167 |
+
vram_tier = "sequence"
|
| 168 |
+
checkpoint_license = "MIT"
|
| 169 |
+
hub_license = "mit"
|
| 170 |
+
weights_publication_allowed = true
|
| 171 |
+
state_transform = "esm2_hf_to_fastplms_v1"
|
| 172 |
+
conversion_provenance = "Input: the pinned official ESM2 state dictionary. Transformation: apply the deterministic esm2_hf_to_fastplms_v1 key map while preserving tensor values and materializing the tied input/output embedding values as independent tensors. Output: the pinned Synthyra FastPLMs checkpoint. Validation: release parity compares exact keys and values after the declared non-aliasing transform, tokenizer behavior, and inference. Limitation: any numerical rewrite requires a new transform identifier and exact conversion test."
|
| 173 |
+
representative = "esm2_8m"
|
| 174 |
+
documentation = "docs/models.md#esm2"
|
| 175 |
+
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 176 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/_esm_rotary.py", "models/esm2", "models/ttt.py"]
|
| 177 |
+
auto_map = { AutoConfig = "fastplms.models.esm2.modeling_fastesm.FastEsmConfig", AutoModel = "fastplms.models.esm2.modeling_fastesm.FastEsmModel", AutoModelForMaskedLM = "fastplms.models.esm2.modeling_fastesm.FastEsmForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.esm2.modeling_fastesm.FastEsmForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.esm2.modeling_fastesm.FastEsmForTokenClassification" }
|
| 178 |
+
|
| 179 |
+
[families.esm_plusplus]
|
| 180 |
+
architecture = "ESMC"
|
| 181 |
+
upstreams = ["biohub-esm", "biohub-transformers"]
|
| 182 |
+
tokenizer_mode = "tokenizer"
|
| 183 |
+
public_input = "Amino-acid sequences tokenized to residue IDs"
|
| 184 |
+
extra = "core"
|
| 185 |
+
reference_container = "reference-biohub-esm"
|
| 186 |
+
reference_adapter = "tests.parity.support.reference_adapters.esm_plusplus"
|
| 187 |
+
attention = ["eager", "sdpa", "flex_attention", "flash_attention_2", "flash_attention_3"]
|
| 188 |
+
dtypes = ["float32", "bfloat16"]
|
| 189 |
+
bf16_execution = "static_parameters"
|
| 190 |
+
precisions = ["default"]
|
| 191 |
+
vram_tier = "sequence"
|
| 192 |
+
checkpoint_license = "MIT"
|
| 193 |
+
hub_license = "mit"
|
| 194 |
+
weights_publication_allowed = true
|
| 195 |
+
state_transform = "esmc_to_fastplms_v1"
|
| 196 |
+
conversion_provenance = "Input: the pinned Biohub ESMC checkpoint. Transformation: apply the deterministic esmc_to_fastplms_v1 parameter map into the FastPLMs ESMC modules. Output: the pinned Synthyra ESMplusplus checkpoint. Validation: release parity compares keys, shapes, dtypes, values, aliases, and live inference. Limitation: runtime attention and precision selection are not serialized weight transforms."
|
| 197 |
+
representative = "esmc_small"
|
| 198 |
+
documentation = "docs/models.md#esm-and-esmc"
|
| 199 |
+
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 200 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/esm_plusplus", "models/ttt.py"]
|
| 201 |
+
auto_map = { AutoConfig = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusConfig", AutoModel = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusModel", AutoModelForMaskedLM = "fastplms.models.esm_plusplus.modeling_esm_plusplus.ESMplusplusForMaskedLM" }
|
| 202 |
+
|
| 203 |
+
[families.esm3]
|
| 204 |
+
architecture = "ESM3"
|
| 205 |
+
upstreams = ["biohub-esm", "biohub-transformers"]
|
| 206 |
+
tokenizer_mode = "tokenizer"
|
| 207 |
+
public_input = "Sequence, structure, and function tracks prepared through the multimodal helpers"
|
| 208 |
+
extra = "core"
|
| 209 |
+
reference_container = "reference-biohub-esm"
|
| 210 |
+
reference_adapter = "tests.parity.support.reference_adapters.esm3"
|
| 211 |
+
attention = ["eager", "sdpa", "flex_attention"]
|
| 212 |
+
dtypes = ["float32", "bfloat16"]
|
| 213 |
+
bf16_execution = "fp32_parameters_autocast"
|
| 214 |
+
precisions = ["default"]
|
| 215 |
+
vram_tier = "large-sequence"
|
| 216 |
+
checkpoint_license = "MIT"
|
| 217 |
+
hub_license = "mit"
|
| 218 |
+
weights_publication_allowed = true
|
| 219 |
+
state_transform = "esm3_to_fastplms_v1"
|
| 220 |
+
conversion_provenance = "Input: the pinned Biohub ESM3 checkpoint. Transformation: apply the deterministic esm3_to_fastplms_v1 parameter map for the supported sequence and multimodal modules and expand BF16 checkpoint tensors to FP32 storage. Output: the pinned Synthyra ESM3 checkpoint. Validation: release parity compares exact state identity after the declared map and live feature behavior. Limitation: unsupported upstream modalities may not be inferred from this record."
|
| 221 |
+
representative = "esm3_small"
|
| 222 |
+
documentation = "docs/models.md#esm3"
|
| 223 |
+
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 224 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/esm3", "models/ttt.py"]
|
| 225 |
+
auto_map = { AutoConfig = "fastplms.models.esm3.modeling_esm3.FastESM3Config", AutoModel = "fastplms.models.esm3.modeling_esm3.FastESM3Model" }
|
| 226 |
+
|
| 227 |
+
[families.e1]
|
| 228 |
+
architecture = "E1"
|
| 229 |
+
upstreams = ["e1"]
|
| 230 |
+
tokenizer_mode = "sequence"
|
| 231 |
+
public_input = "Raw amino-acid sequences prepared by the native E1 adapter"
|
| 232 |
+
extra = "core"
|
| 233 |
+
reference_container = "reference-e1"
|
| 234 |
+
reference_adapter = "tests.parity.support.reference_adapters.e1"
|
| 235 |
+
attention = ["sdpa", "flex_attention"]
|
| 236 |
+
dtypes = ["float32", "bfloat16"]
|
| 237 |
+
bf16_execution = "static_parameters"
|
| 238 |
+
precisions = ["default"]
|
| 239 |
+
vram_tier = "sequence"
|
| 240 |
+
checkpoint_license = "Profluent-E1-Agreement"
|
| 241 |
+
hub_license = "other"
|
| 242 |
+
hub_license_name = "Profluent-E1 Clickthrough License Agreement"
|
| 243 |
+
hub_license_link = "https://github.com/Profluent-AI/E1/blob/bfd2620a602248499f3d2583d85a7ecddf0b6e02/LICENSE"
|
| 244 |
+
weights_publication_allowed = true
|
| 245 |
+
state_transform = "e1_to_fastplms_v1"
|
| 246 |
+
conversion_provenance = "Input: the pinned Profluent-E1 checkpoint and tokenizer-free sequence contract. Transformation: apply e1_to_fastplms_v1 to the FastPLMs encoder and official task heads, storing floating tensors in BF16. Output: the pinned Synthyra Profluent-E1 checkpoint. Validation: release parity covers state identity after the declared cast, sequence and RAG preparation, aliases, and inference. Limitation: the FastPLMs scoring extension is not represented as an official E1 head."
|
| 247 |
+
representative = "e1_150m"
|
| 248 |
+
documentation = "docs/models.md#e1"
|
| 249 |
+
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 250 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/e1", "models/ttt.py"]
|
| 251 |
+
auto_map = { AutoConfig = "fastplms.models.e1.modeling_e1.E1Config", AutoModel = "fastplms.models.e1.modeling_e1.E1Model", AutoModelForMaskedLM = "fastplms.models.e1.modeling_e1.E1ForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.e1.modeling_e1.E1ForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.e1.modeling_e1.E1ForTokenClassification" }
|
| 252 |
+
|
| 253 |
+
[families.dplm]
|
| 254 |
+
architecture = "DPLM"
|
| 255 |
+
upstreams = ["dplm"]
|
| 256 |
+
tokenizer_mode = "tokenizer"
|
| 257 |
+
public_input = "Amino-acid sequences tokenized to masked or partially masked residue IDs"
|
| 258 |
+
extra = "core"
|
| 259 |
+
reference_container = "reference-dplm"
|
| 260 |
+
reference_adapter = "tests.parity.support.reference_adapters.dplm"
|
| 261 |
+
attention = ["eager", "sdpa", "flex_attention", "flash_attention_3"]
|
| 262 |
+
dtypes = ["float32", "bfloat16"]
|
| 263 |
+
bf16_execution = "fp32_parameters_autocast"
|
| 264 |
+
precisions = ["default"]
|
| 265 |
+
vram_tier = "sequence"
|
| 266 |
+
checkpoint_license = "Apache-2.0"
|
| 267 |
+
hub_license = "apache-2.0"
|
| 268 |
+
weights_publication_allowed = true
|
| 269 |
+
state_transform = "dplm_to_fastplms_v1"
|
| 270 |
+
conversion_provenance = "Input: the pinned official DPLM1 checkpoint. Transformation: apply dplm_to_fastplms_v1, omitting the unused absolute-position table for rotary checkpoints and materializing the tied input/output embedding values as independent tensors. Output: the pinned Synthyra DPLM checkpoint. Validation: release parity compares exact state identity after the declared transform, tokenizer behavior, generation, and inference. License basis: the pinned ByteDance DPLM Apache-2.0 LICENSE and README explicitly scope the repository release to the pretrained DPLM1 and DPLM2 weights; immutable evidence is recorded in LICENSES/dplm/PROVENANCE.md. Limitation: redistribution remains subject to Apache-2.0 and the pinned provenance record; no broader rights are inferred."
|
| 271 |
+
representative = "dplm_150m"
|
| 272 |
+
documentation = "docs/models.md#dplm"
|
| 273 |
+
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 274 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/_diffusion_generation.py", "models/_esm_rotary.py", "models/dplm", "models/ttt.py"]
|
| 275 |
+
auto_map = { AutoConfig = "fastplms.models.dplm.modeling_dplm.DPLMConfig", AutoModel = "fastplms.models.dplm.modeling_dplm.DPLMModel", AutoModelForMaskedLM = "fastplms.models.dplm.modeling_dplm.DPLMForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.dplm.modeling_dplm.DPLMForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.dplm.modeling_dplm.DPLMForTokenClassification" }
|
| 276 |
+
|
| 277 |
+
[families.dplm2]
|
| 278 |
+
architecture = "DPLM2"
|
| 279 |
+
upstreams = ["dplm"]
|
| 280 |
+
tokenizer_mode = "tokenizer"
|
| 281 |
+
public_input = "Tokenized amino-acid and structure tracks with explicit modality boundaries"
|
| 282 |
+
extra = "core"
|
| 283 |
+
reference_container = "reference-dplm"
|
| 284 |
+
reference_adapter = "tests.parity.support.reference_adapters.dplm2"
|
| 285 |
+
attention = ["sdpa"]
|
| 286 |
+
dtypes = ["float32", "bfloat16"]
|
| 287 |
+
bf16_execution = "fp32_parameters_autocast"
|
| 288 |
+
precisions = ["default"]
|
| 289 |
+
vram_tier = "sequence"
|
| 290 |
+
checkpoint_license = "Apache-2.0"
|
| 291 |
+
hub_license = "apache-2.0"
|
| 292 |
+
weights_publication_allowed = true
|
| 293 |
+
state_transform = "dplm2_to_fastplms_v1"
|
| 294 |
+
conversion_provenance = "Input: the pinned official DPLM2 checkpoint. Transformation: apply dplm2_to_fastplms_v1, retaining the independent language-model head and trained encoder contact head while omitting the unused absolute-position table for rotary checkpoints. Output: the pinned Synthyra DPLM2 checkpoint. Validation: release parity compares exact keys and values after the declared omission, non-aliasing, tokenizer behavior, generation, and inference. License basis: the pinned ByteDance DPLM Apache-2.0 LICENSE and README explicitly scope the repository release to the pretrained DPLM1 and DPLM2 weights; immutable evidence is recorded in LICENSES/dplm/PROVENANCE.md. Limitation: no head exception is permitted by this record, and redistribution remains subject to Apache-2.0."
|
| 295 |
+
representative = "dplm2_150m"
|
| 296 |
+
documentation = "docs/models.md#dplm2"
|
| 297 |
+
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 298 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/_diffusion_generation.py", "models/_esm_rotary.py", "models/dplm2", "models/ttt.py"]
|
| 299 |
+
auto_map = { AutoConfig = "fastplms.models.dplm2.modeling_dplm2.DPLM2Config", AutoModel = "fastplms.models.dplm2.modeling_dplm2.DPLM2Model", AutoModelForMaskedLM = "fastplms.models.dplm2.modeling_dplm2.DPLM2ForMaskedLM", AutoModelForSequenceClassification = "fastplms.models.dplm2.modeling_dplm2.DPLM2ForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.dplm2.modeling_dplm2.DPLM2ForTokenClassification" }
|
| 300 |
+
tokenizer_class = "fastplms.models.dplm2.tokenization_dplm2.DPLM2Tokenizer"
|
| 301 |
+
|
| 302 |
+
[families.ankh]
|
| 303 |
+
architecture = "ANKH"
|
| 304 |
+
upstreams = ["ankh"]
|
| 305 |
+
tokenizer_mode = "tokenizer"
|
| 306 |
+
public_input = "Amino-acid sequences tokenized for encoder or sequence-to-sequence use"
|
| 307 |
+
extra = "core"
|
| 308 |
+
reference_container = "reference-ankh"
|
| 309 |
+
reference_adapter = "tests.parity.support.reference_adapters.ankh"
|
| 310 |
+
attention = ["eager", "sdpa"]
|
| 311 |
+
dtypes = ["float32", "bfloat16"]
|
| 312 |
+
bf16_execution = "static_parameters"
|
| 313 |
+
precisions = ["default"]
|
| 314 |
+
vram_tier = "large-sequence"
|
| 315 |
+
checkpoint_license = "CC-BY-NC-SA-4.0"
|
| 316 |
+
hub_license = "cc-by-nc-sa-4.0"
|
| 317 |
+
weights_publication_allowed = true
|
| 318 |
+
state_transform = "ankh_t5_to_fastplms_v1"
|
| 319 |
+
conversion_provenance = "Input: the pinned official ANKH T5 checkpoint. Transformation: apply ankh_t5_to_fastplms_v1 to the official encoder and sequence-to-sequence heads. Output: the pinned Synthyra ANKH checkpoint. Validation: release parity compares exact mapped state, tokenizer behavior, official heads, and inference. Limitation: the separately named FastPLMs masked-language-model extension is not an official ANKH head."
|
| 320 |
+
representative = "ankh_base"
|
| 321 |
+
documentation = "docs/models.md#ankh"
|
| 322 |
+
test_tiers = ["check", "compliance", "feature", "artifact", "benchmark"]
|
| 323 |
+
requires_complete_weight_publication = true
|
| 324 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/ankh", "models/ttt.py"]
|
| 325 |
+
auto_map = { AutoConfig = "fastplms.models.ankh.modeling_ankh.FastAnkhConfig", AutoModel = "fastplms.models.ankh.modeling_ankh.FastAnkhModel", AutoModelForMaskedLM = "fastplms.models.ankh.modeling_ankh.FastAnkhForMaskedLMExtension", AutoModelForSeq2SeqLM = "fastplms.models.ankh.modeling_ankh.FastAnkhForConditionalGeneration", AutoModelForSequenceClassification = "fastplms.models.ankh.modeling_ankh.FastAnkhForSequenceClassification", AutoModelForTokenClassification = "fastplms.models.ankh.modeling_ankh.FastAnkhForTokenClassification" }
|
| 326 |
+
|
| 327 |
+
[families.boltz2]
|
| 328 |
+
architecture = "Boltz2"
|
| 329 |
+
upstreams = ["boltz"]
|
| 330 |
+
tokenizer_mode = "structure"
|
| 331 |
+
public_input = "Raw amino-acid sequences through the convenience API, or prepared model features"
|
| 332 |
+
extra = "structure"
|
| 333 |
+
reference_container = "reference-boltz2"
|
| 334 |
+
reference_adapter = "tests.parity.support.reference_adapters.boltz"
|
| 335 |
+
attention = ["eager"]
|
| 336 |
+
dtypes = ["float32", "bfloat16"]
|
| 337 |
+
bf16_execution = "fp32_parameters_autocast"
|
| 338 |
+
precisions = ["default"]
|
| 339 |
+
vram_tier = "structure"
|
| 340 |
+
checkpoint_license = "MIT"
|
| 341 |
+
hub_license = "mit"
|
| 342 |
+
weights_publication_allowed = true
|
| 343 |
+
state_transform = "boltz2_inference_core_v1"
|
| 344 |
+
conversion_provenance = "Input: the pinned official Boltz2 checkpoint. Transformation: select and map the supported Boltz2 inference-core parameters with boltz2_inference_core_v1. Output: the pinned Synthyra Boltz2 checkpoint. Validation: release parity covers state identity for the declared subset, feature preparation, seeded inference, and structure outputs. Limitation: this record does not claim support for undeclared upstream training components."
|
| 345 |
+
representative = "boltz2"
|
| 346 |
+
documentation = "docs/models.md#boltz2"
|
| 347 |
+
test_tiers = ["structure", "artifact", "benchmark"]
|
| 348 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "models/boltz"]
|
| 349 |
+
auto_map = { AutoConfig = "fastplms.models.boltz.modeling_boltz2.Boltz2Config", AutoModel = "fastplms.models.boltz.modeling_boltz2.Boltz2Model" }
|
| 350 |
+
|
| 351 |
+
[families.esmfold]
|
| 352 |
+
architecture = "ESMFold"
|
| 353 |
+
upstreams = ["fair-esm", "openfold"]
|
| 354 |
+
tokenizer_mode = "structure"
|
| 355 |
+
public_input = "Raw amino-acid sequences through folding helpers, or prepared residue tensors"
|
| 356 |
+
extra = "structure"
|
| 357 |
+
reference_container = "reference-esmfold"
|
| 358 |
+
reference_adapter = "tests.parity.support.reference_adapters.esmfold"
|
| 359 |
+
attention = ["eager", "sdpa", "flex_attention"]
|
| 360 |
+
dtypes = ["float32", "bfloat16"]
|
| 361 |
+
bf16_execution = "fp32_parameters_autocast"
|
| 362 |
+
precisions = ["default"]
|
| 363 |
+
vram_tier = "structure"
|
| 364 |
+
checkpoint_license = "MIT"
|
| 365 |
+
hub_license = "mit"
|
| 366 |
+
weights_publication_allowed = true
|
| 367 |
+
state_transform = "esmfold_meta_to_fastplms_v1"
|
| 368 |
+
conversion_provenance = "Input: the pinned native Meta ESMFold checkpoint plus its pinned ESM2 backbone. Transformation: apply esmfold_meta_to_fastplms_v1 to map native ESM2 names into the structure-only FastPLMs backbone, retain folding tensors, omit five deterministically reconstructed geometry buffers, omit the folding-unused ESM2 masked-LM and contact-regression heads, and remove the obsolete random FastPLMs TTT head from earlier mirrors. Output: canonical FP32 FastPLMs ESMFold state with an explicit CUDA BF16-autocast execution path. Validation: release parity compares exact mapped keys, shapes, dtypes, values, aliases, semantic configuration, FP32 and BF16-compute seeded inference, and structure metrics with pLDDT normalized to (0, 1). Limitation: ESMFold TTT is rejected because the official checkpoint contains no trained masked-language-model head."
|
| 369 |
+
representative = "esmfold"
|
| 370 |
+
documentation = "docs/models.md#esmfold"
|
| 371 |
+
test_tiers = ["check", "compliance", "structure", "feature", "artifact", "benchmark"]
|
| 372 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/_esm_rotary.py", "models/esmfold"]
|
| 373 |
+
auto_map = { AutoConfig = "fastplms.models.esmfold.modeling_fast_esmfold.FastEsmFoldConfig", AutoModel = "fastplms.models.esmfold.modeling_fast_esmfold.FastEsmForProteinFolding" }
|
| 374 |
+
|
| 375 |
+
[families.esmfold2]
|
| 376 |
+
architecture = "ESMFold2"
|
| 377 |
+
upstreams = ["biohub-esm", "biohub-transformers", "protein-ttt"]
|
| 378 |
+
backbone_model = "esmc_6b"
|
| 379 |
+
tokenizer_mode = "structure"
|
| 380 |
+
public_input = "Raw amino-acid sequences or typed molecular-complex specifications; low-level forward accepts prepared feature tensors"
|
| 381 |
+
extra = "structure"
|
| 382 |
+
reference_container = "reference-esmfold2"
|
| 383 |
+
reference_adapter = "tests.parity.support.reference_adapters.esmfold2"
|
| 384 |
+
attention = ["eager", "sdpa", "flex_attention"]
|
| 385 |
+
dtypes = ["float32", "bfloat16"]
|
| 386 |
+
bf16_execution = "fp32_parameters_autocast"
|
| 387 |
+
precisions = ["auto", "fp32", "bf16", "fp8"]
|
| 388 |
+
experimental_precisions = ["fp8"]
|
| 389 |
+
vram_tier = "structure-6b"
|
| 390 |
+
checkpoint_license = "MIT"
|
| 391 |
+
hub_license = "mit"
|
| 392 |
+
weights_publication_allowed = true
|
| 393 |
+
state_transform = "identity"
|
| 394 |
+
conversion_provenance = "Input: each pinned Biohub ESMFold2 checkpoint and its separately pinned ESMC checkpoint. Transformation: apply identity to preserve the folding checkpoint exactly, load its parameters in FP32 for CUDA BF16-autocast execution, retain canonical BF16 ESMC weights, and optionally rebuild exactly 80 ESMC attention output projections as transient Transformer Engine linears. Output: the corresponding pinned Synthyra ESMFold2 checkpoint plus its declared ESMC precision policy. Validation: release parity covers exact canonical state, learned projection, prepared features, and seeded BF16 folding; experimental FP8 validation covers strict unavailable-device behavior, all four variants, and three BF16-to-FP8 reload cycles on the standard variant. Limitation: only the four manifest-listed ESMFold2 variants are supported; FP8 is experimental, applies only to inference-time ESMC execution, and requires direct CUDA loading with Transformer Engine availability."
|
| 395 |
+
representative = "esmfold2"
|
| 396 |
+
documentation = "docs/esmfold2.md"
|
| 397 |
+
test_tiers = ["check", "compliance", "structure", "feature", "artifact", "benchmark"]
|
| 398 |
+
runtime_paths = ["__init__.py", "registry.py", "runtime.py", "models.toml", "models/__init__.py", "attention", "embeddings", "models/esmfold2", "models/esm_plusplus", "models/ttt.py"]
|
| 399 |
+
auto_map = { AutoConfig = "fastplms.models.esmfold2.configuration_esmfold2.ESMFold2Config", AutoModel = "fastplms.models.esmfold2.modeling_esmfold2.ESMFold2Model" }
|
| 400 |
+
|
| 401 |
+
[[models]]
|
| 402 |
+
id = "esm2_8m"
|
| 403 |
+
family = "esm2"
|
| 404 |
+
size_category = "small"
|
| 405 |
+
generation_contract = "not_applicable"
|
| 406 |
+
official_golden = { metadata = "tests/goldens/esm2_8m.json=sha256:6975e86d1d8f27488bf2a676551feaa48cc19254c9d24b6acb09198122745609", tensors = "tests/goldens/esm2_8m.safetensors=sha256:b40217566c33c71988d28869de353be54a3b3ebfc21fdfd29056e88cf7e99f4c" }
|
| 407 |
+
fast_repo = "Synthyra/ESM2-8M"
|
| 408 |
+
fast_revision = "185ecbd45665d050a8dae326d91886d330c5f9d0"
|
| 409 |
+
fast_files = [
|
| 410 |
+
"config.json=git-sha1:46d0a7b517f59123c6ebc6d1011585731cbab259",
|
| 411 |
+
"model.safetensors=sha256:c824e6ded5fb71c72bc5ac05300699947819023cb26cdaf6897665e6b2645e1b",
|
| 412 |
+
"special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
|
| 413 |
+
"tokenizer_config.json=git-sha1:3cfc5db0c6790859a3bc2a4dc053a813acd65295",
|
| 414 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 415 |
+
]
|
| 416 |
+
official_repo = "facebook/esm2_t6_8M_UR50D"
|
| 417 |
+
official_revision = "c731040fcd8d73dceaa04b0a8e6329b345b0f5df"
|
| 418 |
+
official_files = [
|
| 419 |
+
"config.json=git-sha1:c2c6e65a87d9d20d47699ae236d605b80c741dd3",
|
| 420 |
+
"model.safetensors=sha256:24c5fa474c48f3b754b86efe752d5f189d2bcd88190fa2270fc92b2ef3034189",
|
| 421 |
+
"special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
|
| 422 |
+
"tokenizer_config.json=git-sha1:3f0d47e841e1cb75257aeaf76d156802899a217e",
|
| 423 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 424 |
+
]
|
| 425 |
+
|
| 426 |
+
[[models.oracle_assets]]
|
| 427 |
+
role = "weights"
|
| 428 |
+
path = "models/esm2_t6_8M_UR50D.pt"
|
| 429 |
+
url = "https://dl.fbaipublicfiles.com/fair-esm/models/esm2_t6_8M_UR50D.pt"
|
| 430 |
+
sha256 = "46f002a9870c9bdecd0ea887acb1f9a38a6b561e8f8bf8a6990b679b9d31b928"
|
| 431 |
+
size = 30099493
|
| 432 |
+
|
| 433 |
+
[[models.oracle_assets]]
|
| 434 |
+
role = "contact_regression"
|
| 435 |
+
path = "regression/esm2_t6_8M_UR50D-contact-regression.pt"
|
| 436 |
+
url = "https://dl.fbaipublicfiles.com/fair-esm/regression/esm2_t6_8M_UR50D-contact-regression.pt"
|
| 437 |
+
sha256 = "8f7a4557d57713b97ba0e484303007efb7230d25299c0ac47a0a1b12a87bbb9d"
|
| 438 |
+
size = 1511
|
| 439 |
+
|
| 440 |
+
[[models]]
|
| 441 |
+
id = "esm2_35m"
|
| 442 |
+
family = "esm2"
|
| 443 |
+
size_category = "small"
|
| 444 |
+
generation_contract = "not_applicable"
|
| 445 |
+
official_golden = { metadata = "tests/goldens/esm2_35m.json=sha256:e919d3ce6d20b6a942d27d92323814ae7594a0129dc9c4de27c5053e96675bcd", tensors = "tests/goldens/esm2_35m.safetensors=sha256:c9b8bb616cf884fb7744521a2fcc6eed23586342d11241e6c9ef16454ec31e17" }
|
| 446 |
+
fast_repo = "Synthyra/ESM2-35M"
|
| 447 |
+
fast_revision = "37ab9f56b41e365b3bd9e25d6fefe9150fd910f0"
|
| 448 |
+
fast_files = [
|
| 449 |
+
"config.json=git-sha1:4d428c9934572f39e2a00db162249971f37c88e4",
|
| 450 |
+
"model.safetensors=sha256:21d95ab6bb9aa91bfec87eff11da61a657b732f2df279cbddbae6a7f1f0bba9c",
|
| 451 |
+
"special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
|
| 452 |
+
"tokenizer_config.json=git-sha1:3cfc5db0c6790859a3bc2a4dc053a813acd65295",
|
| 453 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 454 |
+
]
|
| 455 |
+
official_repo = "facebook/esm2_t12_35M_UR50D"
|
| 456 |
+
official_revision = "6fbf070e65b0b7291e7bbcd451118c216cff79d8"
|
| 457 |
+
official_files = [
|
| 458 |
+
"config.json=git-sha1:3f64131bb610ed1ce482c4b5421fc358c785278f",
|
| 459 |
+
"model.safetensors=sha256:e35647818e0e064351d4531ed480d225a002567b4b2b93ad3a9246d753150fc0",
|
| 460 |
+
"special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
|
| 461 |
+
"tokenizer_config.json=git-sha1:3f0d47e841e1cb75257aeaf76d156802899a217e",
|
| 462 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 463 |
+
]
|
| 464 |
+
|
| 465 |
+
[[models.oracle_assets]]
|
| 466 |
+
role = "weights"
|
| 467 |
+
path = "models/esm2_t12_35M_UR50D.pt"
|
| 468 |
+
url = "https://dl.fbaipublicfiles.com/fair-esm/models/esm2_t12_35M_UR50D.pt"
|
| 469 |
+
sha256 = "7f21e80e61d16a71735163ef555d3009afb0c98da74c48e29df08606973cc55e"
|
| 470 |
+
size = 134095705
|
| 471 |
+
|
| 472 |
+
[[models.oracle_assets]]
|
| 473 |
+
role = "contact_regression"
|
| 474 |
+
path = "regression/esm2_t12_35M_UR50D-contact-regression.pt"
|
| 475 |
+
url = "https://dl.fbaipublicfiles.com/fair-esm/regression/esm2_t12_35M_UR50D-contact-regression.pt"
|
| 476 |
+
sha256 = "16641e05d830d0ce863dd152dbb8c2f3ddfa3c3ec2a66080152c8abad01d8585"
|
| 477 |
+
size = 1959
|
| 478 |
+
|
| 479 |
+
[[models]]
|
| 480 |
+
id = "esm2_150m"
|
| 481 |
+
family = "esm2"
|
| 482 |
+
size_category = "medium"
|
| 483 |
+
generation_contract = "not_applicable"
|
| 484 |
+
official_golden = { metadata = "tests/goldens/esm2_150m.json=sha256:c04c93486024ba0fa1c81fbfbe92ee79d1d4c7f1cfcc2c9886728522f752feab", tensors = "tests/goldens/esm2_150m.safetensors=sha256:c03fe9916dba137b452a6bbe944c7dc414db4019a6f0921e87b92d4bb6a8a42f" }
|
| 485 |
+
fast_repo = "Synthyra/ESM2-150M"
|
| 486 |
+
fast_revision = "979e0880dfc9e0c0080839b83d9d2dc05b92786a"
|
| 487 |
+
fast_files = [
|
| 488 |
+
"config.json=git-sha1:efeae2af182b7d34dc35740a45f157661e7acdf4",
|
| 489 |
+
"model.safetensors=sha256:d1f7c60f98c31af328381519a750972b6a31b13b97aa7cca2e71b5ae1b3f8f53",
|
| 490 |
+
"special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
|
| 491 |
+
"tokenizer_config.json=git-sha1:3cfc5db0c6790859a3bc2a4dc053a813acd65295",
|
| 492 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 493 |
+
]
|
| 494 |
+
official_repo = "facebook/esm2_t30_150M_UR50D"
|
| 495 |
+
official_revision = "a695f6045e2e32885fa60af20c13cb35398ce30c"
|
| 496 |
+
official_files = [
|
| 497 |
+
"config.json=git-sha1:52e04179e6fbad6663a94ea5cc44f09d764c5cd4",
|
| 498 |
+
"model.safetensors=sha256:c3f1da8aea53bddd32c246c86168c23b9fd72341fb9db9a94436f855f5053566",
|
| 499 |
+
"special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
|
| 500 |
+
"tokenizer_config.json=git-sha1:3f0d47e841e1cb75257aeaf76d156802899a217e",
|
| 501 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 502 |
+
]
|
| 503 |
+
|
| 504 |
+
[[models.oracle_assets]]
|
| 505 |
+
role = "weights"
|
| 506 |
+
path = "models/esm2_t30_150M_UR50D.pt"
|
| 507 |
+
url = "https://dl.fbaipublicfiles.com/fair-esm/models/esm2_t30_150M_UR50D.pt"
|
| 508 |
+
sha256 = "881c7176cf198ef8dec26a3c375d40eb58d0c33df95c22562ca6cc6d3f812c62"
|
| 509 |
+
size = 592774773
|
| 510 |
+
|
| 511 |
+
[[models.oracle_assets]]
|
| 512 |
+
role = "contact_regression"
|
| 513 |
+
path = "regression/esm2_t30_150M_UR50D-contact-regression.pt"
|
| 514 |
+
url = "https://dl.fbaipublicfiles.com/fair-esm/regression/esm2_t30_150M_UR50D-contact-regression.pt"
|
| 515 |
+
sha256 = "6a604b96722ed052eef8a094ad90b275ba2e987d406315dbed0bdc6b3c4238a7"
|
| 516 |
+
size = 3431
|
| 517 |
+
|
| 518 |
+
[[models]]
|
| 519 |
+
id = "esm2_650m"
|
| 520 |
+
family = "esm2"
|
| 521 |
+
size_category = "large"
|
| 522 |
+
generation_contract = "not_applicable"
|
| 523 |
+
official_golden = { metadata = "tests/goldens/esm2_650m.json=sha256:f18332172fcb3abf5dd2485fd55f5b0d193ad3b93a44cc744e0d02817c927477", tensors = "tests/goldens/esm2_650m.safetensors=sha256:c3a66b75add03628e62e238cb63da6a9e4d321f8160e84bdf2a131c096977f86" }
|
| 524 |
+
fast_repo = "Synthyra/ESM2-650M"
|
| 525 |
+
fast_revision = "ca0718a5d52b80d5c60dd76860e55e061a95fb0a"
|
| 526 |
+
fast_files = [
|
| 527 |
+
"config.json=git-sha1:88f6bd240680b29c3244df8292246048401f5caf",
|
| 528 |
+
"model.safetensors=sha256:a15142e94ecf36f0edde9b37796f591e609ebe1694ca411e93640f0ee384994a",
|
| 529 |
+
"special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
|
| 530 |
+
"tokenizer_config.json=git-sha1:3cfc5db0c6790859a3bc2a4dc053a813acd65295",
|
| 531 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 532 |
+
]
|
| 533 |
+
official_repo = "facebook/esm2_t33_650M_UR50D"
|
| 534 |
+
official_revision = "08e4846e537177426273712802403f7ba8261b6c"
|
| 535 |
+
official_files = [
|
| 536 |
+
"config.json=git-sha1:a956a25d277f30bd870d3760b9a116f19ead885e",
|
| 537 |
+
"model.safetensors=sha256:a08adabb949fa67ad3c14b509d04fd60368b35007b0095e3358f81200c4f4db0",
|
| 538 |
+
"special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
|
| 539 |
+
"tokenizer_config.json=git-sha1:3f0d47e841e1cb75257aeaf76d156802899a217e",
|
| 540 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 541 |
+
]
|
| 542 |
+
|
| 543 |
+
[[models.oracle_assets]]
|
| 544 |
+
role = "weights"
|
| 545 |
+
path = "models/esm2_t33_650M_UR50D.pt"
|
| 546 |
+
url = "https://dl.fbaipublicfiles.com/fair-esm/models/esm2_t33_650M_UR50D.pt"
|
| 547 |
+
sha256 = "ea9d0522b335a8778dea6535a65301f10208dece28cd5865482b0b1fc446168c"
|
| 548 |
+
size = 2604537549
|
| 549 |
+
|
| 550 |
+
[[models.oracle_assets]]
|
| 551 |
+
role = "contact_regression"
|
| 552 |
+
path = "regression/esm2_t33_650M_UR50D-contact-regression.pt"
|
| 553 |
+
url = "https://dl.fbaipublicfiles.com/fair-esm/regression/esm2_t33_650M_UR50D-contact-regression.pt"
|
| 554 |
+
sha256 = "8ffe6edbd4173dc8d45c2cd5cb27d43aad77ec26b4c768200c58ae1f96693575"
|
| 555 |
+
size = 3687
|
| 556 |
+
|
| 557 |
+
[[models]]
|
| 558 |
+
id = "esm2_3b"
|
| 559 |
+
family = "esm2"
|
| 560 |
+
size_category = "xlarge"
|
| 561 |
+
generation_contract = "not_applicable"
|
| 562 |
+
official_golden = { metadata = "tests/goldens/esm2_3b.json=sha256:5043b2333c57a34d54fac53916722d1acb4b6fd50395b9abafa805435b184a48", tensors = "tests/goldens/esm2_3b.safetensors=sha256:dfd5a8cb05d3e814a080185c4808c8e7ec2277f070f395562fcfbe4376789e4e" }
|
| 563 |
+
notes = "The pinned default SDPA BF16 path uses a checkpoint-specific numeric calibration: relative L2 target/hard limit 0.06/0.07, relative Q99.9 0.15/0.18, first-percentile residue cosine 0.994/0.992, and pooled cosine 0.998/0.997. Exact state identity and the global logits-distribution contract remain required."
|
| 564 |
+
fast_repo = "Synthyra/ESM2-3B"
|
| 565 |
+
fast_revision = "ff89d0180f414ab9c677219a25da79bf09185456"
|
| 566 |
+
fast_files = [
|
| 567 |
+
"config.json=git-sha1:94944ad6cabaa40a3ce1cbe6699cf464fdc1b2c0",
|
| 568 |
+
"model-00001-of-00003.safetensors=sha256:04b57854545c23779b562ee2ae22f10021ba0f4d586ba0ad482ee6eda187d562",
|
| 569 |
+
"model-00002-of-00003.safetensors=sha256:34954aaa05bc91635776ba6672946da5822626753d80db97b38c0538e9525102",
|
| 570 |
+
"model-00003-of-00003.safetensors=sha256:a6b3a55b9e3b2e1778de34c665c3dd17bdfdf6da9d6d5c97730c57168709ccae",
|
| 571 |
+
"special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
|
| 572 |
+
"tokenizer_config.json=git-sha1:3cfc5db0c6790859a3bc2a4dc053a813acd65295",
|
| 573 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 574 |
+
]
|
| 575 |
+
official_repo = "facebook/esm2_t36_3B_UR50D"
|
| 576 |
+
official_revision = "476b639933c8baad5ad09a60ac1a87f987b656fc"
|
| 577 |
+
official_files = [
|
| 578 |
+
"config.json=git-sha1:69e7563923f87d2d7439bfb83e5a19b44b46d71b",
|
| 579 |
+
"pytorch_model-00001-of-00002.bin=sha256:0f971f11c449d21422aa982b791619c10351972992c735f4c3cd43fe09790412",
|
| 580 |
+
"pytorch_model-00002-of-00002.bin=sha256:7560b46fc383c691fb74b915b7d4bcef40d3df181447f16ba4b298845e308d0c",
|
| 581 |
+
"special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
|
| 582 |
+
"tokenizer_config.json=git-sha1:3f0d47e841e1cb75257aeaf76d156802899a217e",
|
| 583 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 584 |
+
]
|
| 585 |
+
|
| 586 |
+
[[models.oracle_assets]]
|
| 587 |
+
role = "weights"
|
| 588 |
+
path = "models/esm2_t36_3B_UR50D.pt"
|
| 589 |
+
url = "https://dl.fbaipublicfiles.com/fair-esm/models/esm2_t36_3B_UR50D.pt"
|
| 590 |
+
sha256 = "7de8b4082ba15891959ab368b77ce3886697af1efb16d3c9e9e7b0c5d3f07500"
|
| 591 |
+
size = 5678116398
|
| 592 |
+
|
| 593 |
+
[[models.oracle_assets]]
|
| 594 |
+
role = "contact_regression"
|
| 595 |
+
path = "regression/esm2_t36_3B_UR50D-contact-regression.pt"
|
| 596 |
+
url = "https://dl.fbaipublicfiles.com/fair-esm/regression/esm2_t36_3B_UR50D-contact-regression.pt"
|
| 597 |
+
sha256 = "4da500eab246481dc9c8c95bc7b1d02f2803d761c380b0e95186d4a07d0fc84e"
|
| 598 |
+
size = 6759
|
| 599 |
+
|
| 600 |
+
[[models]]
|
| 601 |
+
id = "esmc_small"
|
| 602 |
+
family = "esm_plusplus"
|
| 603 |
+
size_category = "medium"
|
| 604 |
+
generation_contract = "not_applicable"
|
| 605 |
+
official_golden = { metadata = "tests/goldens/esmc_small.json=sha256:bb02652cf3cc484756b98ffa4ba55ed4c55870d2cea3342adb1d920ba9dfe10a", tensors = "tests/goldens/esmc_small.safetensors=sha256:03378d0f0fdd8161178ebb2c1f0da1b9776a726c8e8d3a10c009808a24de5654" }
|
| 606 |
+
notes = "Release contract: SDPA must match the pinned Biohub implementation bit-for-bit across every hidden state, last hidden state, logits, special token, and padding position. Eager and FlashAttention 2 are release-gated in BF16 against the pinned boundary-length and biological panels with a relative-L2 engineering target of 0.029, hard limit of 0.03, relative-Q99.9 target of 0.049, first-percentile residue-cosine target of 0.997, and Jensen-Shannon target of 0.0004. The global pooled-cosine and top-1 thresholds remain unchanged. Flex Attention and FlashAttention 3 remain selectable as opt-in alternatives, but they are not strict-parity choices: on the locked H100 BF16 generated-boundary panel, ESMC-6B Flex Attention exceeds the 0.03 relative-L2 hard limit and FlashAttention 3 falls below the 0.995 residue-cosine hard limit. The deviation is consistent with backend-specific BF16 kernel arithmetic; it is not a weight-conversion difference or silent fallback. Use SDPA for exact Biohub parity or FlashAttention 2 for release-gated acceleration."
|
| 607 |
+
fast_repo = "Synthyra/ESMplusplus_small"
|
| 608 |
+
fast_revision = "46c5f7d562e47d4c14165b424c71ab7db008e6fb"
|
| 609 |
+
fast_files = [
|
| 610 |
+
"config.json=git-sha1:df2f44187157b0cc371c48c887b77b1783679201",
|
| 611 |
+
"model.safetensors=sha256:d099223765bc4f1ae8d6c7e18561ce41df1d54073fdc5327ef0a229235a8f52a",
|
| 612 |
+
"special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
|
| 613 |
+
"tokenizer.json=git-sha1:f49735e56cebab0e791aeaae777757b7fd114f71",
|
| 614 |
+
"tokenizer_config.json=git-sha1:2985ed2b8aa8ecfb1d12f53f47d2b8a44cc21756",
|
| 615 |
+
]
|
| 616 |
+
official_repo = "biohub/ESMC-300M"
|
| 617 |
+
official_revision = "a59b831785f907e96e6a246b1d142bfb76df31ee"
|
| 618 |
+
official_files = [
|
| 619 |
+
"config.json=git-sha1:9a49eacf4e65c39f74381f0f0d240e3b89ef43d7",
|
| 620 |
+
"model.safetensors=sha256:0772d8fe64bb25e14fe6f23b80e3c9a7d215d0da3c6cba5bd356d7c0e0bb22cc",
|
| 621 |
+
"special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
|
| 622 |
+
"tokenizer.json=git-sha1:81c797f56768b22dec0301fa771f018b7e43e98c",
|
| 623 |
+
"tokenizer_config.json=git-sha1:2238856624f8d39f03af53a2576c2d9b18c82f61",
|
| 624 |
+
]
|
| 625 |
+
|
| 626 |
+
[[models]]
|
| 627 |
+
id = "esmc_large"
|
| 628 |
+
family = "esm_plusplus"
|
| 629 |
+
size_category = "large"
|
| 630 |
+
generation_contract = "not_applicable"
|
| 631 |
+
official_golden = { metadata = "tests/goldens/esmc_large.json=sha256:7a4d614f67b6fde417f3fd89f61e7ec442ae284769734b2b73e14945a816a8fd", tensors = "tests/goldens/esmc_large.safetensors=sha256:e13302df4cf7e8381552f1043a8fd0f31f3e0d50b2ab6009fb86b7940ae8ff79" }
|
| 632 |
+
notes = "Release contract: SDPA must match the pinned Biohub implementation bit-for-bit across every hidden state, last hidden state, logits, special token, and padding position. Eager and FlashAttention 2 are release-gated in BF16 against the pinned boundary-length and biological panels with a relative-L2 engineering target of 0.029, hard limit of 0.03, relative-Q99.9 target of 0.049, first-percentile residue-cosine target of 0.997, and Jensen-Shannon target of 0.0004. The global pooled-cosine and top-1 thresholds remain unchanged. Flex Attention and FlashAttention 3 remain selectable as opt-in alternatives, but they are not strict-parity choices: on the locked H100 BF16 generated-boundary panel, ESMC-6B Flex Attention exceeds the 0.03 relative-L2 hard limit and FlashAttention 3 falls below the 0.995 residue-cosine hard limit. The deviation is consistent with backend-specific BF16 kernel arithmetic; it is not a weight-conversion difference or silent fallback. Use SDPA for exact Biohub parity or FlashAttention 2 for release-gated acceleration."
|
| 633 |
+
fast_repo = "Synthyra/ESMplusplus_large"
|
| 634 |
+
fast_revision = "f813401638b3fddab09748aec1ad2bf537aa4208"
|
| 635 |
+
fast_files = [
|
| 636 |
+
"config.json=git-sha1:5736371902fe5d04e2859be30ac7dbd31b271b25",
|
| 637 |
+
"model.safetensors=sha256:4aff3f8c5de68c4d3e3824eb2c478e4a47355d3f849f3c745e5c8a5ee6cff851",
|
| 638 |
+
"special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
|
| 639 |
+
"tokenizer.json=git-sha1:f49735e56cebab0e791aeaae777757b7fd114f71",
|
| 640 |
+
"tokenizer_config.json=git-sha1:2985ed2b8aa8ecfb1d12f53f47d2b8a44cc21756",
|
| 641 |
+
]
|
| 642 |
+
official_repo = "biohub/ESMC-600M"
|
| 643 |
+
official_revision = "a7e82012c83126b9eedb055fea9fa84b6c02f094"
|
| 644 |
+
official_files = [
|
| 645 |
+
"config.json=git-sha1:71c8241dc28a5fb636248267a0927c0242b264c1",
|
| 646 |
+
"model.safetensors=sha256:e4232c30fd35fe2f57051ec88a703996ac94520580b4b836894207a3d45d9ff8",
|
| 647 |
+
"special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
|
| 648 |
+
"tokenizer.json=git-sha1:81c797f56768b22dec0301fa771f018b7e43e98c",
|
| 649 |
+
"tokenizer_config.json=git-sha1:2238856624f8d39f03af53a2576c2d9b18c82f61",
|
| 650 |
+
]
|
| 651 |
+
|
| 652 |
+
[[models]]
|
| 653 |
+
id = "esmc_6b"
|
| 654 |
+
family = "esm_plusplus"
|
| 655 |
+
size_category = "xlarge"
|
| 656 |
+
generation_contract = "not_applicable"
|
| 657 |
+
official_golden = { metadata = "tests/goldens/esmc_6b.json=sha256:e229d938719782f280fab22dfc4c43e86109fdb0cc523631168c5a491afaace3", tensors = "tests/goldens/esmc_6b.safetensors=sha256:a948945e985c7deaca7be8b7eed09c0a9521a2af3f2b10fc2ec7a7d2a0f99ada" }
|
| 658 |
+
notes = "Release contract: SDPA must match the pinned Biohub implementation bit-for-bit across every hidden state, last hidden state, logits, special token, and padding position. Eager and FlashAttention 2 are release-gated in BF16 against the pinned boundary-length and biological panels with a relative-L2 engineering target of 0.029, hard limit of 0.03, relative-Q99.9 target of 0.049, first-percentile residue-cosine target of 0.997, and Jensen-Shannon target of 0.0004. The global pooled-cosine and top-1 thresholds remain unchanged. Flex Attention and FlashAttention 3 remain selectable as opt-in alternatives, but they are not strict-parity choices: on the locked H100 BF16 generated-boundary panel, ESMC-6B Flex Attention exceeds the 0.03 relative-L2 hard limit and FlashAttention 3 falls below the 0.995 residue-cosine hard limit. The deviation is consistent with backend-specific BF16 kernel arithmetic; it is not a weight-conversion difference or silent fallback. Use SDPA for exact Biohub parity or FlashAttention 2 for release-gated acceleration."
|
| 659 |
+
fast_repo = "Synthyra/ESMplusplus_6B"
|
| 660 |
+
fast_revision = "0d579cce3b0f09efa6b3baddf6cc3fd8c9b616c8"
|
| 661 |
+
fast_files = [
|
| 662 |
+
"config.json=git-sha1:e740cbcf211f2511c70c25a1ff6017a757ba7a69",
|
| 663 |
+
"model-00001-of-00006.safetensors=sha256:d30d18703453019f2d2d050866309888720c28eebc9a10307d1ddf3799e85a65",
|
| 664 |
+
"model-00002-of-00006.safetensors=sha256:b3d85378ab5023f4160a96e9c8cbd4cc6f78a771a83c856e88d48112f555bc13",
|
| 665 |
+
"model-00003-of-00006.safetensors=sha256:52595519b59349c5c6e373e6f5ca4a3d48ea6dde345f7e61e24766df5fab0e5b",
|
| 666 |
+
"model-00004-of-00006.safetensors=sha256:e46c6113c89c6f3e9b072c1bef02d763a625c37bcd8f9da2ed9363891c9a0758",
|
| 667 |
+
"model-00005-of-00006.safetensors=sha256:6d92cb2bf9791de644de2ae86f8523d802ac3b4aaabfff0716ab6c2b97f6fb14",
|
| 668 |
+
"model-00006-of-00006.safetensors=sha256:5fc1a8632490bb34162823c35d0d591337b9e4195b22cc0560741397a6e9d0b3",
|
| 669 |
+
"special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
|
| 670 |
+
"tokenizer.json=git-sha1:f49735e56cebab0e791aeaae777757b7fd114f71",
|
| 671 |
+
"tokenizer_config.json=git-sha1:2985ed2b8aa8ecfb1d12f53f47d2b8a44cc21756",
|
| 672 |
+
]
|
| 673 |
+
official_repo = "biohub/ESMC-6B"
|
| 674 |
+
official_revision = "45b0fa5d7fb06faefbd5e3b89bdcef35d564e79a"
|
| 675 |
+
official_files = [
|
| 676 |
+
"config.json=git-sha1:19f5fb09e4f630fb5b748a497183c22a87ec5102",
|
| 677 |
+
"model-00001-of-00006.safetensors=sha256:bd90149ff223e6ac1a0cac6147a5ae0df20d3a21df4f65356a1f19cd14f4aa8a",
|
| 678 |
+
"model-00002-of-00006.safetensors=sha256:f75e2144d8269fe2eb4b3e0823fb089b94f176d8024153e85b8fb573a42294fa",
|
| 679 |
+
"model-00003-of-00006.safetensors=sha256:f699f01ecc9691d9c6470492765fe54b8b5d2e9f277c139e89427433ffdfe0b2",
|
| 680 |
+
"model-00004-of-00006.safetensors=sha256:46add1b7be098bbfdc3073884851ba3057f1b33ea23a158b650a37007dabd13d",
|
| 681 |
+
"model-00005-of-00006.safetensors=sha256:1e1cb62f060a34e18f54a31a76683ef888b8cec59e73315f5b31d25d45a1f88c",
|
| 682 |
+
"model-00006-of-00006.safetensors=sha256:56c73e13ae96e777ce65eee99364056069ef93b646470f352f83c5f1037b1b18",
|
| 683 |
+
"special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
|
| 684 |
+
"tokenizer.json=git-sha1:81c797f56768b22dec0301fa771f018b7e43e98c",
|
| 685 |
+
"tokenizer_config.json=git-sha1:2238856624f8d39f03af53a2576c2d9b18c82f61",
|
| 686 |
+
]
|
| 687 |
+
|
| 688 |
+
[[models]]
|
| 689 |
+
id = "esm3_small"
|
| 690 |
+
family = "esm3"
|
| 691 |
+
tokenizer_source = "esmc_small"
|
| 692 |
+
size_category = "large"
|
| 693 |
+
generation_contract = "not_applicable"
|
| 694 |
+
official_golden = { metadata = "tests/goldens/esm3_small.json=sha256:5470e8596cbba0e2882647eccbc53c36d8b48b0f3947d1fe0bcea68da1078c32", tensors = "tests/goldens/esm3_small.safetensors=sha256:d957922f810c9ab4c557d80d5aaaf6a3aab79a5a45e4638012a634a4134803b1" }
|
| 695 |
+
fast_repo = "Synthyra/ESM3_small"
|
| 696 |
+
fast_revision = "7ddb5a740f9e5f93933eb6410c0ee8684bc63ec1"
|
| 697 |
+
fast_files = [
|
| 698 |
+
"config.json=git-sha1:60526e2fdd8af9d4fba17f323775458ef5a1a1f9",
|
| 699 |
+
"model-00001-of-00002.safetensors=sha256:a4c9b736c4c59d51180e966005a164859b47d5cd36e1f8ecdea619fbd34a0e92",
|
| 700 |
+
"model-00002-of-00002.safetensors=sha256:bea60e4e91b03bb00b6cedd29b07606b8543f0869fb74454af7b26e216d80d2b",
|
| 701 |
+
"special_tokens_map.json=git-sha1:c907ee1dc19b24241749b32d665c291c7e6e8e4b",
|
| 702 |
+
"tokenizer.json=git-sha1:f49735e56cebab0e791aeaae777757b7fd114f71",
|
| 703 |
+
"tokenizer_config.json=git-sha1:2985ed2b8aa8ecfb1d12f53f47d2b8a44cc21756",
|
| 704 |
+
]
|
| 705 |
+
official_repo = "biohub/esm3-sm-open-v1"
|
| 706 |
+
official_revision = "47f0545b2b6daf26a93439a3cd610f4f7f3d5478"
|
| 707 |
+
official_files = [
|
| 708 |
+
"config.json=git-sha1:0967ef424bce6791893e9a57bb952f80fd536e93",
|
| 709 |
+
"data/weights/esm3_function_decoder_v0.pth=sha256:f76d074efcaccfe21365a4fa96f212dadd66798e1e49d809ab7ffbe025d227c9",
|
| 710 |
+
"data/weights/esm3_sm_open_v1.pth=sha256:5ead5a135c658068db6a4f1b933e72d6110992c4668822e1c0e2dcc53e38acd9",
|
| 711 |
+
"data/weights/esm3_structure_decoder_v0.pth=sha256:3b726258a44274792b40ce7ea307e10c5da09936368a4ffa2970264d909da65b",
|
| 712 |
+
"data/weights/esm3_structure_encoder_v0.pth=sha256:467acbaee703ba3ccde6e75241a912a316952e5ff071355f85c1d33c68704f40",
|
| 713 |
+
]
|
| 714 |
+
|
| 715 |
+
[[models]]
|
| 716 |
+
id = "e1_150m"
|
| 717 |
+
family = "e1"
|
| 718 |
+
size_category = "small"
|
| 719 |
+
generation_contract = "not_applicable"
|
| 720 |
+
official_golden = { metadata = "tests/goldens/e1_150m.json=sha256:701a64a6ab1a2fec5a427555b6af96232526c15cb3d5b4dc7fb253ac8f20b922", tensors = "tests/goldens/e1_150m.safetensors=sha256:6558bc8f1a7b20629eaaaa6f72601d0c2cdb859a5dc13595549b1773b6e2de41" }
|
| 721 |
+
fast_repo = "Synthyra/Profluent-E1-150M"
|
| 722 |
+
fast_revision = "7c5f3bbf697226a2e0900db7a100f9201774a907"
|
| 723 |
+
fast_files = [
|
| 724 |
+
"config.json=git-sha1:562ef21e722ca708064fc3d54d25b731d4ac8171",
|
| 725 |
+
"model.safetensors=sha256:d779ed3a4e23799aafc932dc09c9963428d10aa7075999b5f8851b39c76b67f6",
|
| 726 |
+
]
|
| 727 |
+
official_repo = "Profluent-Bio/E1-150m"
|
| 728 |
+
official_revision = "c4dbfe827e4aa6ed7f95eaef50dc1e084f4d77dc"
|
| 729 |
+
official_files = [
|
| 730 |
+
"config.json=git-sha1:485e649199b46fe6ee7456bebf7aae9b3d4baeab",
|
| 731 |
+
"model.safetensors=sha256:ba2656339005e6598642836acfdafde480fecc7e145ce0058eb54adf572c3484",
|
| 732 |
+
]
|
| 733 |
+
|
| 734 |
+
[[models]]
|
| 735 |
+
id = "e1_300m"
|
| 736 |
+
family = "e1"
|
| 737 |
+
size_category = "medium"
|
| 738 |
+
generation_contract = "not_applicable"
|
| 739 |
+
official_golden = { metadata = "tests/goldens/e1_300m.json=sha256:d3478f3f5957a0e0377864074dde0107de890019f96cb63548ee17ffb8f3ec3a", tensors = "tests/goldens/e1_300m.safetensors=sha256:92778b9ef95a803ddc84b3e3ca764c59e045872a94bcff0eb0cd47647732c188" }
|
| 740 |
+
fast_repo = "Synthyra/Profluent-E1-300M"
|
| 741 |
+
fast_revision = "5ef52c0ad2ae2578f40622696b763523810e8e26"
|
| 742 |
+
fast_files = [
|
| 743 |
+
"config.json=git-sha1:f5c91498b76a3e3282a0d716d87738abb1a1b6c1",
|
| 744 |
+
"model.safetensors=sha256:9271c4176a8a2e0905a0bb769570ba1c2978fb999a87da92db4cf2b041224864",
|
| 745 |
+
]
|
| 746 |
+
official_repo = "Profluent-Bio/E1-300m"
|
| 747 |
+
official_revision = "5a2871c587eadbcc9237bc686ea45e5b4d28dfb3"
|
| 748 |
+
official_files = [
|
| 749 |
+
"config.json=git-sha1:918cb09e6e96d4719ed85951f38c693360f9cdb8",
|
| 750 |
+
"model.safetensors=sha256:31e09a2542f45b04e6ce4adafb3b657f21e2d56d12bf68fd2266b1576a80bc9b",
|
| 751 |
+
]
|
| 752 |
+
|
| 753 |
+
[[models]]
|
| 754 |
+
id = "e1_600m"
|
| 755 |
+
family = "e1"
|
| 756 |
+
size_category = "large"
|
| 757 |
+
generation_contract = "not_applicable"
|
| 758 |
+
official_golden = { metadata = "tests/goldens/e1_600m.json=sha256:914be191c28141c1f84535cdb69ead0588a2057bb19d46c5bc7f3891a3d6739e", tensors = "tests/goldens/e1_600m.safetensors=sha256:22ed8417a4651ded255099f6d15c63c2c40552e700d2b0470d1adfde3a39c513" }
|
| 759 |
+
fast_repo = "Synthyra/Profluent-E1-600M"
|
| 760 |
+
fast_revision = "6c8bf0ec83b0e0178677c528b101efffd0677742"
|
| 761 |
+
fast_files = [
|
| 762 |
+
"config.json=git-sha1:1d35c0b35b473259875fd29ee80167487a0d6afe",
|
| 763 |
+
"model.safetensors=sha256:793483b1b3411eab73fe5214b94d1424ca0545992dfac6889cfc0186af472363",
|
| 764 |
+
]
|
| 765 |
+
official_repo = "Profluent-Bio/E1-600m"
|
| 766 |
+
official_revision = "52d959fb87a609d15cf223a485127b29ed5c382a"
|
| 767 |
+
official_files = [
|
| 768 |
+
"config.json=git-sha1:8a0a439ed4201462bc01189c9f8b43523b257b5c",
|
| 769 |
+
"model.safetensors=sha256:cfc108d4b98baaa62932331b40be265eae39dc382595bc3cde4a5ab55db1bf7a",
|
| 770 |
+
]
|
| 771 |
+
|
| 772 |
+
[[models]]
|
| 773 |
+
id = "dplm_150m"
|
| 774 |
+
family = "dplm"
|
| 775 |
+
size_category = "small"
|
| 776 |
+
generation_contract = "required"
|
| 777 |
+
official_golden = { metadata = "tests/goldens/dplm_150m.json=sha256:3228551fe3bed951db9ec97347143ec4462ce7c221ac240b7ce7730948c1dc1f", tensors = "tests/goldens/dplm_150m.safetensors=sha256:392992235195beed97ab8359b90a2e11e52f4326606f99a471447bed81d146bd" }
|
| 778 |
+
fast_repo = "Synthyra/DPLM-150M"
|
| 779 |
+
fast_revision = "90ba742754151a774f3b7ed580170d0a76b3e69d"
|
| 780 |
+
fast_files = [
|
| 781 |
+
"config.json=git-sha1:117ac2c1222152ef378abaad1f605e18c4a18ab0",
|
| 782 |
+
"model.safetensors=sha256:8bac5ac767ceb8deb511b272d32883f811768d56cb25e920cea94ba9b979ca14",
|
| 783 |
+
"special_tokens_map.json=git-sha1:ef5f0f7d7baf4947564eafcf79972d272cd80a15",
|
| 784 |
+
"tokenizer_config.json=git-sha1:80100348e3f2b8ab05b59f3352ea7631685083cd",
|
| 785 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 786 |
+
]
|
| 787 |
+
official_repo = "airkingbd/dplm_150m"
|
| 788 |
+
official_revision = "49b7125a5d28c6418fcc2f3c4fe799352ac1488b"
|
| 789 |
+
official_files = [
|
| 790 |
+
"config.json=git-sha1:4910cb02f1840e9ac577026f601829604af58c74",
|
| 791 |
+
"pytorch_model.bin=sha256:ea4eaa99536b60ed76f945f71a1a5e604f08447ec3def5104a93ca6001a59961",
|
| 792 |
+
"special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
|
| 793 |
+
"tokenizer_config.json=git-sha1:dbcdd9fb2e742627ee310713615e0d7aeed0c34e",
|
| 794 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 795 |
+
]
|
| 796 |
+
|
| 797 |
+
[[models]]
|
| 798 |
+
id = "dplm_650m"
|
| 799 |
+
family = "dplm"
|
| 800 |
+
size_category = "large"
|
| 801 |
+
generation_contract = "required"
|
| 802 |
+
official_golden = { metadata = "tests/goldens/dplm_650m.json=sha256:bf58d0ce73aaac7e6fb1923ef3d9adad67122df2a3dd414c3229488ef9587a6d", tensors = "tests/goldens/dplm_650m.safetensors=sha256:073f0a6abea7e48f28c2d921ff8329a28e22627f01979277cb324908a01b3378" }
|
| 803 |
+
fast_repo = "Synthyra/DPLM-650M"
|
| 804 |
+
fast_revision = "05dc16d97c5c028aed924c9ed681cee4ab609760"
|
| 805 |
+
fast_files = [
|
| 806 |
+
"config.json=git-sha1:3537150eb87b213a676d5840548625e220b60e8b",
|
| 807 |
+
"model.safetensors=sha256:e27a47b8ec1c078b3fccb36542210e20f0380c88828db2ca9acf3d8a25048bd8",
|
| 808 |
+
"special_tokens_map.json=git-sha1:ef5f0f7d7baf4947564eafcf79972d272cd80a15",
|
| 809 |
+
"tokenizer_config.json=git-sha1:80100348e3f2b8ab05b59f3352ea7631685083cd",
|
| 810 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 811 |
+
]
|
| 812 |
+
official_repo = "airkingbd/dplm_650m"
|
| 813 |
+
official_revision = "7a7e651baa667d094aba05e9dc1cf52a3332110a"
|
| 814 |
+
official_files = [
|
| 815 |
+
"config.json=git-sha1:625574d625a4178ca6966e9545fee56026c0b634",
|
| 816 |
+
"pytorch_model.bin=sha256:db4e54343a89e7600f41c3aacbc593db1b0caee82ec28cab25ff2ae090eba39c",
|
| 817 |
+
"special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
|
| 818 |
+
"tokenizer_config.json=git-sha1:dbcdd9fb2e742627ee310713615e0d7aeed0c34e",
|
| 819 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 820 |
+
]
|
| 821 |
+
|
| 822 |
+
[[models]]
|
| 823 |
+
id = "dplm_3b"
|
| 824 |
+
family = "dplm"
|
| 825 |
+
size_category = "xlarge"
|
| 826 |
+
generation_contract = "required"
|
| 827 |
+
official_golden = { metadata = "tests/goldens/dplm_3b.json=sha256:a5b6df8b9c7b371976892ec1d6c45581a32ad3a6325c6c0a0b3267012848c8ed", tensors = "tests/goldens/dplm_3b.safetensors=sha256:75b0a0854fc391133920b0feaaeb8f69ab7568a88b3759627aca1556c4338c1e" }
|
| 828 |
+
fast_repo = "Synthyra/DPLM-3B"
|
| 829 |
+
fast_revision = "7d764dd3d70ecf1ac0e64693de64a0064aacac65"
|
| 830 |
+
fast_files = [
|
| 831 |
+
"config.json=git-sha1:7f5baf9426be06760c86882948b0f4af2e681e22",
|
| 832 |
+
"model-00001-of-00003.safetensors=sha256:37b54855d087ef3e7d883464ae9d5ea3127ec15a16c6323d91ad16a6b98305c9",
|
| 833 |
+
"model-00002-of-00003.safetensors=sha256:042604fefb05ea8c360a48416ce7ba662a4f90b176b4baf646c5c1814c35e6e8",
|
| 834 |
+
"model-00003-of-00003.safetensors=sha256:b9ae04012665163c3fc9781dd04fcd69738ac20c07e615e98fc4483fd2c4de45",
|
| 835 |
+
"special_tokens_map.json=git-sha1:ef5f0f7d7baf4947564eafcf79972d272cd80a15",
|
| 836 |
+
"tokenizer_config.json=git-sha1:80100348e3f2b8ab05b59f3352ea7631685083cd",
|
| 837 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 838 |
+
]
|
| 839 |
+
official_repo = "airkingbd/dplm_3b"
|
| 840 |
+
official_revision = "53849d4a7fe944ae0b9cf2bbc0d2cc0054795b51"
|
| 841 |
+
official_files = [
|
| 842 |
+
"config.json=git-sha1:f6206456e8c2f22ebe1d37fce3b5d50fd8073e68",
|
| 843 |
+
"pytorch_model-00001-of-00004.bin=sha256:0bcb86a115fe744ed686756db143f78851304e855e2f83cec58681c6080ced5f",
|
| 844 |
+
"pytorch_model-00002-of-00004.bin=sha256:daf3324f3be949e7dd1c3c84b28da7fec5151b1890cb0904e73427266856a06f",
|
| 845 |
+
"pytorch_model-00003-of-00004.bin=sha256:dbbeb7924a21059854f994931e23590b054aa000b10370a71c052c4aa36e9246",
|
| 846 |
+
"pytorch_model-00004-of-00004.bin=sha256:21c01740d091487db43446489d8a893dea1fcc6f2e1c1991ece13945f7ab4e07",
|
| 847 |
+
"special_tokens_map.json=git-sha1:ba0f9b53dbbf27934f7555e5d31e37bdea9317f1",
|
| 848 |
+
"tokenizer_config.json=git-sha1:dbcdd9fb2e742627ee310713615e0d7aeed0c34e",
|
| 849 |
+
"vocab.txt=git-sha1:6b946952cc35537226f07fd70957ee2f848880d2",
|
| 850 |
+
]
|
| 851 |
+
|
| 852 |
+
[[models]]
|
| 853 |
+
id = "dplm2_150m"
|
| 854 |
+
family = "dplm2"
|
| 855 |
+
size_category = "small"
|
| 856 |
+
generation_contract = "required"
|
| 857 |
+
official_golden = { metadata = "tests/goldens/dplm2_150m.json=sha256:d269de779ea1503de72c77e7b2e6224afc9797bd945b40c571ff6faec782e4aa", tensors = "tests/goldens/dplm2_150m.safetensors=sha256:17fc26600938ba5364b8ecb96750786d33e9f92bcd4ea4df3e12a389340748eb" }
|
| 858 |
+
artifact_source = "official"
|
| 859 |
+
canonical_state_sha256 = "82e1751f59052b8de72b082517557db47947e8d9b4ac2f11278369e6c0cbf001"
|
| 860 |
+
fast_repo = "Synthyra/DPLM2-150M"
|
| 861 |
+
fast_revision = "182745b8dc5661f898481a4fa60a7af9d53385c4"
|
| 862 |
+
fast_files = [
|
| 863 |
+
"config.json=git-sha1:07905a2e4327d27d073cd0390f140aec2976125a",
|
| 864 |
+
"model.safetensors=sha256:0a7751b3113027b1d9c966a5bda2d6ab831855de7aaa047b911731665a7c3cc6",
|
| 865 |
+
"special_tokens_map.json=git-sha1:e6378d20e897b8806734e65fd3ef9cf42a17631b",
|
| 866 |
+
"tokenizer_config.json=git-sha1:f2090783e3368b7323aa877e2b740e09f0862259",
|
| 867 |
+
"vocab.txt=git-sha1:9706a4277a5c39dc9b4ec7b283e8eb130ceaa7f2",
|
| 868 |
+
]
|
| 869 |
+
official_repo = "airkingbd/dplm2_150m"
|
| 870 |
+
official_revision = "3451d984d06497f835ed49634bd68c9dfb54d730"
|
| 871 |
+
official_files = [
|
| 872 |
+
"config.json=git-sha1:20f1e55c64fdc4d1d30f7b1df64b6167fa23dc7c",
|
| 873 |
+
"pytorch_model.bin=sha256:be7f5cf9e421f59fcc437e63ce1c7391099a314a4e9a4f10b8688785fa581238",
|
| 874 |
+
"special_tokens_map.json=git-sha1:eb760e9f49a55145bbe0c64922d4ec2d3de1692a",
|
| 875 |
+
"tokenizer_config.json=git-sha1:fc8c21760dcff173955afb106859e5f015d4f757",
|
| 876 |
+
"vocab.txt=git-sha1:e133a3abd4350ddc3fc62548e162c8df7e62cf37",
|
| 877 |
+
]
|
| 878 |
+
|
| 879 |
+
[[models]]
|
| 880 |
+
id = "dplm2_650m"
|
| 881 |
+
family = "dplm2"
|
| 882 |
+
size_category = "large"
|
| 883 |
+
generation_contract = "required"
|
| 884 |
+
official_golden = { metadata = "tests/goldens/dplm2_650m.json=sha256:d9a7548f9af657a72d441ca70f27379863724fcce8ddd3da4f672104b7bfb772", tensors = "tests/goldens/dplm2_650m.safetensors=sha256:c4e0e467c252c3ac813363d2d4b17a5e3bd99e75fad315e76d97689b4655ddac" }
|
| 885 |
+
artifact_source = "official"
|
| 886 |
+
canonical_state_sha256 = "cba76b6602d2258de9fffff953b608d93cb8ef4a9e89b0bbd27e160c81e78bb4"
|
| 887 |
+
fast_repo = "Synthyra/DPLM2-650M"
|
| 888 |
+
fast_revision = "b9d8527a9473a54954fa2764f590b9ea1b435bb2"
|
| 889 |
+
fast_files = [
|
| 890 |
+
"config.json=git-sha1:3e079579b214d48a09db57f2c60be6a1acea5baf",
|
| 891 |
+
"model.safetensors=sha256:92db08c7dbfd6c5e03fbfeaea3f36b09640ee794dcf5ea8d550527869a9f1d63",
|
| 892 |
+
"special_tokens_map.json=git-sha1:e6378d20e897b8806734e65fd3ef9cf42a17631b",
|
| 893 |
+
"tokenizer_config.json=git-sha1:f2090783e3368b7323aa877e2b740e09f0862259",
|
| 894 |
+
"vocab.txt=git-sha1:9706a4277a5c39dc9b4ec7b283e8eb130ceaa7f2",
|
| 895 |
+
]
|
| 896 |
+
official_repo = "airkingbd/dplm2_650m"
|
| 897 |
+
official_revision = "0bc69b644976c6680ab7e26669854d1979e8876e"
|
| 898 |
+
official_files = [
|
| 899 |
+
"config.json=git-sha1:4cce8d9dc212cdace0e20e89169790bcf199c158",
|
| 900 |
+
"pytorch_model.bin=sha256:8d6e08cc05e4858064a714013c74cc88c9caa2cc8b12c34605a3c24bcd877cfb",
|
| 901 |
+
"special_tokens_map.json=git-sha1:eb760e9f49a55145bbe0c64922d4ec2d3de1692a",
|
| 902 |
+
"tokenizer_config.json=git-sha1:fc8c21760dcff173955afb106859e5f015d4f757",
|
| 903 |
+
"vocab.txt=git-sha1:e133a3abd4350ddc3fc62548e162c8df7e62cf37",
|
| 904 |
+
]
|
| 905 |
+
|
| 906 |
+
[[models]]
|
| 907 |
+
id = "dplm2_3b"
|
| 908 |
+
family = "dplm2"
|
| 909 |
+
size_category = "xlarge"
|
| 910 |
+
# The pinned public sampler fails before generation because cls_token_id is None.
|
| 911 |
+
# State, tokenizer, and inference parity remain required for this checkpoint.
|
| 912 |
+
generation_contract = "official_unavailable"
|
| 913 |
+
official_golden = { metadata = "tests/goldens/dplm2_3b.json=sha256:d6e0e02af53b13cb129192f06e264758aa21c9ebf4ee82411cf67037082d2329", tensors = "tests/goldens/dplm2_3b.safetensors=sha256:838b11824d08f83bcb0c0b3268e579f3a87dbfb965370cfe5c3f8793b96b1964" }
|
| 914 |
+
notes = "The pinned official DPLM2-3B sampler fails before generation, so live generation equivalence cannot be established for this checkpoint. State, tokenizer, and inference parity remain required."
|
| 915 |
+
artifact_source = "official"
|
| 916 |
+
canonical_state_sha256 = "8c46ec09115dbe6cbfb91d94ab5e906369d57e27fe620a7741c6f8cb1b6ca890"
|
| 917 |
+
fast_repo = "Synthyra/DPLM2-3B"
|
| 918 |
+
fast_revision = "2a63babe8848abf5233d31bd55891dff8285fc50"
|
| 919 |
+
fast_files = [
|
| 920 |
+
"config.json=git-sha1:5932b1d501fed28b84614e0d2c1ecc4e89f10d6e",
|
| 921 |
+
"model-00001-of-00003.safetensors=sha256:2ff393f6e8df1568ce075d50de69ff4e5e9d9886e5ec47e43d6c24df23459be3",
|
| 922 |
+
"model-00002-of-00003.safetensors=sha256:feb3cea852c2aa849cc30783a984a97f0d076990ade6606cda5e38bf2a5a9621",
|
| 923 |
+
"model-00003-of-00003.safetensors=sha256:9be363ddb98436af20901981ffbed2f1097377424987f6c1baad27d512b62e71",
|
| 924 |
+
"special_tokens_map.json=git-sha1:e6378d20e897b8806734e65fd3ef9cf42a17631b",
|
| 925 |
+
"tokenizer_config.json=git-sha1:f2090783e3368b7323aa877e2b740e09f0862259",
|
| 926 |
+
"vocab.txt=git-sha1:9706a4277a5c39dc9b4ec7b283e8eb130ceaa7f2",
|
| 927 |
+
]
|
| 928 |
+
official_repo = "airkingbd/dplm2_3b"
|
| 929 |
+
official_revision = "9e77567926f98d1b997ea9131a8eeb035b9bf827"
|
| 930 |
+
official_files = [
|
| 931 |
+
"config.json=git-sha1:22d51ce44cd6da8d819e0d00566987bb51d74753",
|
| 932 |
+
"pytorch_model-00001-of-00004.bin=sha256:d8c641eae6bf891581ec64d543169891b093e296f5679ac75c695bcf596b4211",
|
| 933 |
+
"pytorch_model-00002-of-00004.bin=sha256:6478ad86ec5fef3d1d26580493af2d8666009d3ff884f3f88548080c8bbf94b5",
|
| 934 |
+
"pytorch_model-00003-of-00004.bin=sha256:dde8f88dac4a6355488c2fb433ee12cd69f1169950566624fba43684d4d99dc6",
|
| 935 |
+
"pytorch_model-00004-of-00004.bin=sha256:17ec0145152bc10e4dd3b4c2edff337979f6b99ee7c7bfd6cf4e6dbd7262d079",
|
| 936 |
+
"special_tokens_map.json=git-sha1:eb760e9f49a55145bbe0c64922d4ec2d3de1692a",
|
| 937 |
+
"tokenizer_config.json=git-sha1:fc8c21760dcff173955afb106859e5f015d4f757",
|
| 938 |
+
"vocab.txt=git-sha1:e133a3abd4350ddc3fc62548e162c8df7e62cf37",
|
| 939 |
+
]
|
| 940 |
+
|
| 941 |
+
[[models]]
|
| 942 |
+
id = "ankh_base"
|
| 943 |
+
family = "ankh"
|
| 944 |
+
size_category = "medium"
|
| 945 |
+
generation_contract = "required"
|
| 946 |
+
official_golden = { metadata = "tests/goldens/ankh_base.json=sha256:ebce8d7de821827ee995789c9b38d79252d3b2f76888130b0a8a7eedafaefe2b", tensors = "tests/goldens/ankh_base.safetensors=sha256:f0e78aa15d11749e0c64ff57f9e88c51cec6538a0adf8951f839df70cc708b65" }
|
| 947 |
+
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
|
| 948 |
+
artifact_source = "official"
|
| 949 |
+
canonical_state_sha256 = "cdd8d30d88e5bf41f44e1eef4470d8e46607aba5f7c7c805b06c035b89c8c16f"
|
| 950 |
+
fast_repo = "Synthyra/ANKH_base"
|
| 951 |
+
fast_revision = "7ec329aae8e3e174bf22a1eb9e0e9fcc12b53092"
|
| 952 |
+
fast_files = [
|
| 953 |
+
"config.json=git-sha1:7e1cbce6d08f9bb64eee4410899b1c6b4054f418",
|
| 954 |
+
"model.safetensors=sha256:b0d3473cac1bda90e39cde54f2abe86da1fc84f872c833ca3415672776dccb95",
|
| 955 |
+
"special_tokens_map.json=git-sha1:a2d8d626c31389a935e197fb94072e2414a6e7d1",
|
| 956 |
+
"tokenizer.json=git-sha1:0734d752d12d0f46ac96467fbceb1c4bfbeee0be",
|
| 957 |
+
"tokenizer_config.json=git-sha1:db0b80de72d3b16242b9eda74ed4663e39c65bcf",
|
| 958 |
+
]
|
| 959 |
+
official_repo = "ElnaggarLab/ankh-base"
|
| 960 |
+
official_revision = "d99cb6b966530dfc2ae96bc69d9255c2a07308b0"
|
| 961 |
+
official_files = [
|
| 962 |
+
"config.json=git-sha1:abd44a36b5469e9a7cb019e4059b5ac1392d8422",
|
| 963 |
+
"pytorch_model.bin=sha256:9b2a886374f0ff4a893f4e7a989deed76bb2458c8998bd5202ea8e97d92ddcc3",
|
| 964 |
+
"special_tokens_map.json=git-sha1:55b145827029ae9672e50d4bb368540daacce791",
|
| 965 |
+
"tokenizer.json=git-sha1:212c5ef08819fa2463c6289ba4ef7db30e715c0a",
|
| 966 |
+
"tokenizer_config.json=git-sha1:a8a872ae3441e7cc85ce19210dff1e4c5d2d7bd0",
|
| 967 |
+
]
|
| 968 |
+
|
| 969 |
+
[[models]]
|
| 970 |
+
id = "ankh_large"
|
| 971 |
+
family = "ankh"
|
| 972 |
+
size_category = "large"
|
| 973 |
+
generation_contract = "required"
|
| 974 |
+
official_golden = { metadata = "tests/goldens/ankh_large.json=sha256:59492518b021de5cfaea87d672c9448c8558e99a3443ba2cc7ab544963196ecb", tensors = "tests/goldens/ankh_large.safetensors=sha256:3fb8d3ac27716d15a9ea92aeef6acf2b977bcc887d9b535000539e523673459b" }
|
| 975 |
+
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
|
| 976 |
+
artifact_source = "official"
|
| 977 |
+
canonical_state_sha256 = "e498a2e9aea76ef784cbe3e596c6b3f5e9a40e209ad837f7e3207099e4d74483"
|
| 978 |
+
fast_repo = "Synthyra/ANKH_large"
|
| 979 |
+
fast_revision = "3be3df34140f49dc4e65bd1f247e3ce819e7fc59"
|
| 980 |
+
fast_files = [
|
| 981 |
+
"config.json=git-sha1:272509deedb527e5c2c95b0c269194a44148fdcc",
|
| 982 |
+
"model.safetensors=sha256:e70b8f9755ac6bfe95d18359060ae9fe38fac63b12a89a886c83349d1adbaa53",
|
| 983 |
+
"special_tokens_map.json=git-sha1:a2d8d626c31389a935e197fb94072e2414a6e7d1",
|
| 984 |
+
"tokenizer.json=git-sha1:0734d752d12d0f46ac96467fbceb1c4bfbeee0be",
|
| 985 |
+
"tokenizer_config.json=git-sha1:2bcaff2567826f5f51188b00600d2c6e7bcea56e",
|
| 986 |
+
]
|
| 987 |
+
official_repo = "ElnaggarLab/ankh-large"
|
| 988 |
+
official_revision = "74b371dbfa3ee0a05d32ae74df0c2e0b82d6b9a6"
|
| 989 |
+
official_files = [
|
| 990 |
+
"config.json=git-sha1:1abf33e52ee3d6be67d780ec57d32ac2b27b5306",
|
| 991 |
+
"pytorch_model.bin=sha256:517b6e8b279dedcb477af240b35c46bd6eb3307723eb281e60d4b2c8a87b889b",
|
| 992 |
+
"special_tokens_map.json=git-sha1:55b145827029ae9672e50d4bb368540daacce791",
|
| 993 |
+
"tokenizer.json=git-sha1:212c5ef08819fa2463c6289ba4ef7db30e715c0a",
|
| 994 |
+
"tokenizer_config.json=git-sha1:d7fe02ba6f2b18d9ccfa19ac129c9fdc9ec24d09",
|
| 995 |
+
]
|
| 996 |
+
|
| 997 |
+
[[models]]
|
| 998 |
+
id = "ankh2_large"
|
| 999 |
+
family = "ankh"
|
| 1000 |
+
size_category = "large"
|
| 1001 |
+
generation_contract = "required"
|
| 1002 |
+
official_golden = { metadata = "tests/goldens/ankh2_large.json=sha256:e8df38994ca1a1e0c598ace34a0b257b264937e4fdbb01bc41544985116b02a4", tensors = "tests/goldens/ankh2_large.safetensors=sha256:25fe1569f55c635fab8fa49c1d62a889a35a2a738bad921f5764a85b58fd4b5d" }
|
| 1003 |
+
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
|
| 1004 |
+
artifact_source = "official"
|
| 1005 |
+
canonical_state_sha256 = "597c4fe2fa8711f11a25317905f1d62fa92905e55fdd5c0a79614cd9c9d2bca3"
|
| 1006 |
+
fast_repo = "Synthyra/ANKH2_large"
|
| 1007 |
+
fast_revision = "392de5ed52bbfd73b45f545e378aaebcff096d0e"
|
| 1008 |
+
fast_files = [
|
| 1009 |
+
"config.json=git-sha1:66b6adc7215743a98a3229958bbd1c9c42b6108b",
|
| 1010 |
+
"model.safetensors=sha256:be8e6242388d93b51cd9719a0e32cfc17a2e804786570c795ba332197eccb915",
|
| 1011 |
+
"special_tokens_map.json=git-sha1:a2d8d626c31389a935e197fb94072e2414a6e7d1",
|
| 1012 |
+
"tokenizer.json=git-sha1:0734d752d12d0f46ac96467fbceb1c4bfbeee0be",
|
| 1013 |
+
"tokenizer_config.json=git-sha1:db0b80de72d3b16242b9eda74ed4663e39c65bcf",
|
| 1014 |
+
]
|
| 1015 |
+
official_repo = "ElnaggarLab/ankh2-ext2"
|
| 1016 |
+
official_revision = "aa9b9fa72288c47d9f618ce80c011e24b54e17a8"
|
| 1017 |
+
official_files = [
|
| 1018 |
+
"config.json=git-sha1:9286bed4ecbc4f7113024919d16ec9719b0c0748",
|
| 1019 |
+
"generation_config.json=git-sha1:91f792e452403d46e170e206f9e50be5ddef9b9a",
|
| 1020 |
+
"pytorch_model.bin=sha256:2df583f28f111276ee22a7b76007f4297e9a69766d60bccd9c8d7169c06ac606",
|
| 1021 |
+
"special_tokens_map.json=git-sha1:55b145827029ae9672e50d4bb368540daacce791",
|
| 1022 |
+
"tokenizer.json=git-sha1:212c5ef08819fa2463c6289ba4ef7db30e715c0a",
|
| 1023 |
+
"tokenizer_config.json=git-sha1:854e5db75dae8b1e9dd39c5bae80dae5508b3e25",
|
| 1024 |
+
]
|
| 1025 |
+
|
| 1026 |
+
[[models]]
|
| 1027 |
+
id = "ankh3_large"
|
| 1028 |
+
family = "ankh"
|
| 1029 |
+
size_category = "large"
|
| 1030 |
+
generation_contract = "required"
|
| 1031 |
+
official_golden = { metadata = "tests/goldens/ankh3_large.json=sha256:2e5bb05b3baa5baa78f61fef7d2a2c669b0da5dbfaf6b50b12abd3e17253a961", tensors = "tests/goldens/ankh3_large.safetensors=sha256:e5c494ac418e0a2fe7bdad1376676d48960d58ec9e044d19bfffccb8c3288513" }
|
| 1032 |
+
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head."
|
| 1033 |
+
artifact_source = "official"
|
| 1034 |
+
canonical_state_sha256 = "60acb7ef86e85dc0c51fc1edf4c8e69a0480049723b6b2c95e6e9faa720c112a"
|
| 1035 |
+
fast_repo = "Synthyra/ANKH3_large"
|
| 1036 |
+
fast_revision = "53600f175f328f986f43e55ca8ceb14935d337a4"
|
| 1037 |
+
fast_files = [
|
| 1038 |
+
"config.json=git-sha1:432b09625d44a2eeab679fddb7495d42b560b7f9",
|
| 1039 |
+
"model.safetensors=sha256:9f50f58cf5b3a537a0a41aa918695c3a26d7985dd0b2266642d6f86324c9e7a1",
|
| 1040 |
+
"special_tokens_map.json=git-sha1:1fc3a4d6d4282e5201cd7c30d5c0a6a8bfa04f82",
|
| 1041 |
+
"tokenizer.json=git-sha1:3d14291df2d6db3a183c5c4fe133afb330cc44cf",
|
| 1042 |
+
"tokenizer_config.json=git-sha1:2005fec00a7ae9a49e248a1ecefbbd81c56674d6",
|
| 1043 |
+
]
|
| 1044 |
+
official_repo = "ElnaggarLab/ankh3-large"
|
| 1045 |
+
official_revision = "2be091622e8a393f0ef21735070084123c874b6e"
|
| 1046 |
+
official_files = [
|
| 1047 |
+
"config.json=git-sha1:f5278f77d158cdd8a173df888e3ed365e84a80a3",
|
| 1048 |
+
"generation_config.json=git-sha1:5767cc0cacebfd06884eb27ae1c796d3ca829fd2",
|
| 1049 |
+
"pytorch_model.bin=sha256:26321a345e07a25b21c6c41b651c4db91b420892e52c0dcbc55bd7a8f510f95b",
|
| 1050 |
+
"special_tokens_map.json=git-sha1:d596919b7fa2a197edd441ec3ec4685ecacd2de4",
|
| 1051 |
+
"spiece.model=sha256:f2b5e1bbd110b71ca9b2878e1fcd3265610076ecc97bd696e8a745c9bacc54e0",
|
| 1052 |
+
"tokenizer.json=git-sha1:90f0c94b43c81496b3ca81e3ec1c092ef2dd7fca",
|
| 1053 |
+
"tokenizer_config.json=git-sha1:0e699eebfa778698473b4faf1e66ef363b93fb21",
|
| 1054 |
+
]
|
| 1055 |
+
|
| 1056 |
+
[[models]]
|
| 1057 |
+
id = "ankh3_xl"
|
| 1058 |
+
family = "ankh"
|
| 1059 |
+
size_category = "xlarge"
|
| 1060 |
+
generation_contract = "required"
|
| 1061 |
+
official_golden = { metadata = "tests/goldens/ankh3_xl.json=sha256:66bb12e033e4163be225d636108a479393228a4f5061015c8af114e766c3c486", tensors = "tests/goldens/ankh3_xl.safetensors=sha256:72d34567d0228cb6f1ee701c578ed4039fead4346e3f161a52e0e74df28dc8ae" }
|
| 1062 |
+
notes = "ANKH parity covers the official encoder and sequence-to-sequence heads. AutoModelForMaskedLM exposes the separately named FastPLMs synthesized masked-LM extension and is not an official ANKH head. The official PyTorch shard index is deliberately excluded: the builder verifies every declared source shard directly and writes a new canonical safetensors index."
|
| 1063 |
+
artifact_source = "official"
|
| 1064 |
+
canonical_state_sha256 = "dd2188e0d2ca65232135714eef6de394239734d843ddae4928c7398685d858e7"
|
| 1065 |
+
fast_repo = "Synthyra/ANKH3_xl"
|
| 1066 |
+
fast_revision = "3cbf2c22c4f7d67bf0bfcbdcd500f41723e91d29"
|
| 1067 |
+
fast_files = [
|
| 1068 |
+
"config.json=git-sha1:23f6d78ddcb3a031b88f876eaaf04c2fafaea46f",
|
| 1069 |
+
"model-00001-of-00003.safetensors=sha256:39bd8f75cf98a67cf04055399f9fc401198f6fc2896b112aba9fd9ec9df52ab9",
|
| 1070 |
+
"model-00002-of-00003.safetensors=sha256:9ff73233b39d2c200abb78e66b320c014ec61431bd6e1af36fb188a3cfa24c34",
|
| 1071 |
+
"model-00003-of-00003.safetensors=sha256:c13125c02dbcd7f07bd412e9e085f2bca6624d2f1f45fedc95fb777f53161cbe",
|
| 1072 |
+
"special_tokens_map.json=git-sha1:1fc3a4d6d4282e5201cd7c30d5c0a6a8bfa04f82",
|
| 1073 |
+
"tokenizer.json=git-sha1:3d14291df2d6db3a183c5c4fe133afb330cc44cf",
|
| 1074 |
+
"tokenizer_config.json=git-sha1:2005fec00a7ae9a49e248a1ecefbbd81c56674d6",
|
| 1075 |
+
]
|
| 1076 |
+
official_repo = "ElnaggarLab/ankh3-xl"
|
| 1077 |
+
official_revision = "e00113df5c95ef71df7ea3f5a73d56bd00e473a4"
|
| 1078 |
+
official_files = [
|
| 1079 |
+
"config.json=git-sha1:f8997040e8913df75fd2eebe71a2a8eb750ed0d0",
|
| 1080 |
+
"generation_config.json=git-sha1:91f792e452403d46e170e206f9e50be5ddef9b9a",
|
| 1081 |
+
"pytorch_model-00001-of-00003.bin=sha256:2c9793cbee16697cd4149debe07d3a27143e280f6e970fa46042aae820fea981",
|
| 1082 |
+
"pytorch_model-00002-of-00003.bin=sha256:31c5a860e414513c829ae52affb0970d7cef2c0545df2d6e1338b6806ab7174b",
|
| 1083 |
+
"pytorch_model-00003-of-00003.bin=sha256:055a853bdd3623db95a637935aa299427e837cd8ea69fc04708b0262508bec75",
|
| 1084 |
+
"special_tokens_map.json=git-sha1:d596919b7fa2a197edd441ec3ec4685ecacd2de4",
|
| 1085 |
+
"spiece.model=sha256:f2b5e1bbd110b71ca9b2878e1fcd3265610076ecc97bd696e8a745c9bacc54e0",
|
| 1086 |
+
"tokenizer.json=git-sha1:90f0c94b43c81496b3ca81e3ec1c092ef2dd7fca",
|
| 1087 |
+
"tokenizer_config.json=git-sha1:0e699eebfa778698473b4faf1e66ef363b93fb21",
|
| 1088 |
+
]
|
| 1089 |
+
|
| 1090 |
+
[[models]]
|
| 1091 |
+
id = "boltz2"
|
| 1092 |
+
family = "boltz2"
|
| 1093 |
+
size_category = "structure"
|
| 1094 |
+
generation_contract = "not_applicable"
|
| 1095 |
+
notes = "Boltz2 is provisional in FastPLMs 1.0. Exact configuration, the declared inference-core state, feature preparation, and seeded execution remain tested, but native-environment BF16 end-to-end inference currently exceeds the fixed numerical-equivalence limits. FastPLMs therefore does not claim official inference equivalence for this checkpoint yet. Work on that numerical gap continues independently of the ESM++ and ESMFold2 release gates."
|
| 1096 |
+
fast_repo = "Synthyra/Boltz2"
|
| 1097 |
+
fast_revision = "3b148fc5efea109c065ec82ba8683d024de7134e"
|
| 1098 |
+
fast_files = [
|
| 1099 |
+
"config.json=git-sha1:8682ccb12e177e73bc7a351ff7e3af484bfb6fac",
|
| 1100 |
+
"model.safetensors=sha256:5c863fd200a1613a0e311071e2ad73ab350635e3fd336e6822cf45c52cb960e5",
|
| 1101 |
+
]
|
| 1102 |
+
official_repo = "boltz-community/boltz-2"
|
| 1103 |
+
official_revision = "6fdef46d763fee7fbb83ca5501ccceff43b85607"
|
| 1104 |
+
official_files = [
|
| 1105 |
+
"boltz2_conf.ckpt=sha256:090e82ac8c92f5e943fa1b39e7410a44027bea7243c0bbb3caa67a77fc1428e1",
|
| 1106 |
+
"mols.tar=sha256:39e076d96dbec6b4e86982bbda16f3a53a2a60c9bdc17828d88f6f9a0c7d1fd7",
|
| 1107 |
+
]
|
| 1108 |
+
|
| 1109 |
+
[[models]]
|
| 1110 |
+
id = "esmfold"
|
| 1111 |
+
family = "esmfold"
|
| 1112 |
+
size_category = "structure"
|
| 1113 |
+
generation_contract = "not_applicable"
|
| 1114 |
+
official_golden = { metadata = "tests/goldens/esmfold.json=sha256:380b9a96168410717d1f698feaabb826b1606444cbdeec86c2ea06d9ffe8f186", tensors = "tests/goldens/esmfold.safetensors=sha256:873b1b325a43d8e0f35f355c8914a2a9fe611cc48763875e9e6a22e09ec9ebcb" }
|
| 1115 |
+
fast_repo = "Synthyra/FastESMFold"
|
| 1116 |
+
fast_revision = "b88c8cb50d19b2cf7ab4fee4b0a61f5e02da7823"
|
| 1117 |
+
fast_files = [
|
| 1118 |
+
"config.json=git-sha1:18e0091dcbf6140bf68924d53c4c8917b9cd90b1",
|
| 1119 |
+
"model-00001-of-00003.safetensors=sha256:36fab9e5c96d409b2a34a8b4f1273acac8c07f119c32c4fcfa7d47bbcd55b83c",
|
| 1120 |
+
"model-00002-of-00003.safetensors=sha256:34954aaa05bc91635776ba6672946da5822626753d80db97b38c0538e9525102",
|
| 1121 |
+
"model-00003-of-00003.safetensors=sha256:2f1178cda0e6cff3b1e158e1acc59c83e3f4fc46e246388a5127bc56b8d9c4f2",
|
| 1122 |
+
"special_tokens_map.json=git-sha1:53cd95604a28eb7e23da763c8da23f5006ab2179",
|
| 1123 |
+
"tokenizer_config.json=git-sha1:10213f69b51b4b38876a29271b8f908e853a5800",
|
| 1124 |
+
"vocab.txt=git-sha1:eee0a1fc93c82568f78f086550fbd7c591cf423a",
|
| 1125 |
+
]
|
| 1126 |
+
official_repo = "facebook/esmfold_v1"
|
| 1127 |
+
official_revision = "75a3841ee059df2bf4d56688166c8fb459ddd97a"
|
| 1128 |
+
official_files = [
|
| 1129 |
+
"config.json=git-sha1:1232d0aee4be551021d8e70e66ed2b062df917bf",
|
| 1130 |
+
"pytorch_model.bin=sha256:2ee07356b125d1e3e57503c204111fd7323347fc4735d41d3caac57c2a78e116",
|
| 1131 |
+
"special_tokens_map.json=git-sha1:121c8d54f8ea66cdf678f48b3cb37c05b4de5c0d",
|
| 1132 |
+
"tokenizer_config.json=git-sha1:aad24fba9f1bad2d74ed79d414ddcd60e6b0f812",
|
| 1133 |
+
"vocab.txt=git-sha1:9abfdf5472c0ed970648b683b86ab131256b3e42",
|
| 1134 |
+
]
|
| 1135 |
+
|
| 1136 |
+
[[models.oracle_assets]]
|
| 1137 |
+
role = "weights"
|
| 1138 |
+
path = "models/esmfold_3B_v1.pt"
|
| 1139 |
+
url = "https://dl.fbaipublicfiles.com/fair-esm/models/esmfold_3B_v1.pt"
|
| 1140 |
+
sha256 = "e9a52579027e77d2d2e0a18218e755821f395730e86624cab9413dc117f5ca62"
|
| 1141 |
+
size = 2771653574
|
| 1142 |
+
|
| 1143 |
+
[[models]]
|
| 1144 |
+
id = "esmfold2"
|
| 1145 |
+
family = "esmfold2"
|
| 1146 |
+
size_category = "structure"
|
| 1147 |
+
generation_contract = "not_applicable"
|
| 1148 |
+
msa_conditioning = true
|
| 1149 |
+
official_golden = { metadata = "tests/goldens/esmfold2.json=sha256:f6e0ed1ec400b9a0fcc817db51774be968dc454b7a32645a07c479e42423ab20", tensors = "tests/goldens/esmfold2.safetensors=sha256:e4d6be4344c528e26b13f79a9303549e3de7e582da195c0078db3ce957fad420" }
|
| 1150 |
+
fast_repo = "Synthyra/ESMFold2"
|
| 1151 |
+
fast_revision = "cd5a0927cec585a778d983b99a8db23d2e9b281e"
|
| 1152 |
+
fast_files = [
|
| 1153 |
+
"config.json=git-sha1:67e81ff571f393f0b630cd5a22398bd84979c030",
|
| 1154 |
+
"model.safetensors=sha256:138fd4350d6892b81ce6be7ff9bf5a93ae9d4d3751f46a27438a3f9f0dcefa0e",
|
| 1155 |
+
]
|
| 1156 |
+
official_repo = "biohub/ESMFold2"
|
| 1157 |
+
official_revision = "1ebf0e3481a5184eb6171d40615c79e384b48796"
|
| 1158 |
+
official_files = [
|
| 1159 |
+
"config.json=git-sha1:0300c084b990b2bd600efd9f538aa5de27109fea",
|
| 1160 |
+
"model.safetensors=sha256:138fd4350d6892b81ce6be7ff9bf5a93ae9d4d3751f46a27438a3f9f0dcefa0e",
|
| 1161 |
+
]
|
| 1162 |
+
|
| 1163 |
+
[[models]]
|
| 1164 |
+
id = "esmfold2_fast"
|
| 1165 |
+
family = "esmfold2"
|
| 1166 |
+
size_category = "structure"
|
| 1167 |
+
generation_contract = "not_applicable"
|
| 1168 |
+
msa_conditioning = false
|
| 1169 |
+
official_golden = { metadata = "tests/goldens/esmfold2_fast.json=sha256:091b004c0b330217b59c12acd6da3d6edaf91e48d95f6d5f40fc20399cef9478", tensors = "tests/goldens/esmfold2_fast.safetensors=sha256:6e2e1cd07401538b4d9df994f82abe7a5b38a01e8d1ee26681e1216d44a81990" }
|
| 1170 |
+
fast_repo = "Synthyra/ESMFold2-Fast"
|
| 1171 |
+
fast_revision = "407875bfcaa42552bfcb25acd67ee1888b790170"
|
| 1172 |
+
fast_files = [
|
| 1173 |
+
"config.json=git-sha1:62ccca15a416a5dcbd02cd6ce161f432c7b4de58",
|
| 1174 |
+
"model.safetensors=sha256:60ca19f2898188beba92944365f7b909efd9c99212f5018af75cc47cd9a6184a",
|
| 1175 |
+
]
|
| 1176 |
+
official_repo = "biohub/ESMFold2-Fast"
|
| 1177 |
+
official_revision = "b28d8ace5e05e61e5bec1e6820cfd3e221819d12"
|
| 1178 |
+
official_files = [
|
| 1179 |
+
"config.json=git-sha1:c0ca526090fa7f8342ee4666d56e7fe3a4b8cbb2",
|
| 1180 |
+
"model.safetensors=sha256:60ca19f2898188beba92944365f7b909efd9c99212f5018af75cc47cd9a6184a",
|
| 1181 |
+
]
|
| 1182 |
+
|
| 1183 |
+
[[models]]
|
| 1184 |
+
id = "esmfold2_experimental_cutoff2025"
|
| 1185 |
+
family = "esmfold2"
|
| 1186 |
+
size_category = "structure"
|
| 1187 |
+
generation_contract = "not_applicable"
|
| 1188 |
+
msa_conditioning = true
|
| 1189 |
+
official_golden = { metadata = "tests/goldens/esmfold2_experimental_cutoff2025.json=sha256:cfd0e35b2bc468a0dc4f614d3acfa2fce004f96e9ae2433256ed095b829d55cc", tensors = "tests/goldens/esmfold2_experimental_cutoff2025.safetensors=sha256:9347466bbe803b6f5dc82e3356ca6cbbf2c2edd8765f9fd273385bda255019f6" }
|
| 1190 |
+
fast_repo = "Synthyra/ESMFold2-Experimental-Cutoff2025"
|
| 1191 |
+
fast_revision = "632ff4a9e68f1de78ee956a613267bdcdb5b354d"
|
| 1192 |
+
fast_files = [
|
| 1193 |
+
"config.json=git-sha1:41119745d38bc5503a0212ad923e75211dec565f",
|
| 1194 |
+
"model.safetensors=sha256:01358c317428d38535e3db513cab177336fc0f7fab0d84002e64b7741d5181b3",
|
| 1195 |
+
]
|
| 1196 |
+
official_repo = "biohub/ESMFold2-Experimental-Cutoff2025"
|
| 1197 |
+
official_revision = "56f94f5c1069ecde17512c96928850518340d287"
|
| 1198 |
+
official_files = [
|
| 1199 |
+
"config.json=git-sha1:79ed0dc0f867b8f09bfa004d6f77397c2ab9b38d",
|
| 1200 |
+
"model.safetensors=sha256:01358c317428d38535e3db513cab177336fc0f7fab0d84002e64b7741d5181b3",
|
| 1201 |
+
]
|
| 1202 |
+
auto_map = { AutoConfig = "fastplms.models.esmfold2.configuration_esmfold2.ESMFold2Config", AutoModel = "fastplms.models.esmfold2.modeling_esmfold2_experimental.ESMFold2ExperimentalModel" }
|
| 1203 |
+
|
| 1204 |
+
[[models]]
|
| 1205 |
+
id = "esmfold2_experimental_fast_cutoff2025"
|
| 1206 |
+
family = "esmfold2"
|
| 1207 |
+
size_category = "structure"
|
| 1208 |
+
generation_contract = "not_applicable"
|
| 1209 |
+
msa_conditioning = false
|
| 1210 |
+
official_golden = { metadata = "tests/goldens/esmfold2_experimental_fast_cutoff2025.json=sha256:1d0b2da4f1579243f37ae04bd4b834b747005cd8e8e7665e00d088123c43afd9", tensors = "tests/goldens/esmfold2_experimental_fast_cutoff2025.safetensors=sha256:516e216d05d7e6bee59e77126d3e595e2bb7821929433f00c259c5d5241964bb" }
|
| 1211 |
+
fast_repo = "Synthyra/ESMFold2-Experimental-Fast-Cutoff2025"
|
| 1212 |
+
fast_revision = "8f022c2514a6c32692aaca078a8391d6bc6c4bac"
|
| 1213 |
+
fast_files = [
|
| 1214 |
+
"config.json=git-sha1:b9d39e941050179ca51faaed58cbbd77778c1143",
|
| 1215 |
+
"model.safetensors=sha256:4e903b740ad6ad704ec60881bfd593e0d6c874a630ffa0f0838276e0b665088f",
|
| 1216 |
+
]
|
| 1217 |
+
official_repo = "biohub/ESMFold2-Experimental-Fast-Cutoff2025"
|
| 1218 |
+
official_revision = "74b88548bf19688b8727432db0d698cb2e1d8783"
|
| 1219 |
+
official_files = [
|
| 1220 |
+
"config.json=git-sha1:0333d68ddb12ed2f066741dcb801142f466c0a2c",
|
| 1221 |
+
"model.safetensors=sha256:4e903b740ad6ad704ec60881bfd593e0d6c874a630ffa0f0838276e0b665088f",
|
| 1222 |
+
]
|
| 1223 |
+
auto_map = { AutoConfig = "fastplms.models.esmfold2.configuration_esmfold2.ESMFold2Config", AutoModel = "fastplms.models.esmfold2.modeling_esmfold2_experimental.ESMFold2ExperimentalModel" }
|
fastplms/models/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Lazy model-family namespace for FastPLMs.
|
| 2 |
+
|
| 3 |
+
Model classes are resolved through Transformers AutoClasses and the typed
|
| 4 |
+
registry. Importing this package therefore does not load checkpoints, create
|
| 5 |
+
tokenizers, compile kernels, or initialize an accelerator runtime.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
__all__: tuple[str, ...] = ()
|
fastplms/models/_diffusion_generation.py
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Discrete diffusion generation shared by DPLM and DPLM2.
|
| 2 |
+
|
| 3 |
+
The implementation keeps model-specific vocabulary rules at the public entry
|
| 4 |
+
points and shares only the categorical sampling and confidence-based remasking
|
| 5 |
+
mechanism. It has no dependency on the pinned upstream checkout.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import math
|
| 11 |
+
from collections.abc import Iterable, Mapping
|
| 12 |
+
from contextlib import contextmanager
|
| 13 |
+
from typing import Any, Protocol
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
from tqdm.auto import tqdm
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class _MaskedLanguageModel(Protocol):
|
| 20 |
+
"""Structural type used by the two generation entry points."""
|
| 21 |
+
|
| 22 |
+
config: Any
|
| 23 |
+
|
| 24 |
+
def eval(self) -> Any: ...
|
| 25 |
+
|
| 26 |
+
def modules(self) -> Iterable[torch.nn.Module]: ...
|
| 27 |
+
|
| 28 |
+
def __call__(self, **kwargs: Any) -> Any: ...
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
_DPLM2_AA_BOUNDARY = 33
|
| 32 |
+
_DPLM2_AA_BOS = 0
|
| 33 |
+
_DPLM2_PAD = 1
|
| 34 |
+
_DPLM2_AA_EOS = 2
|
| 35 |
+
_DPLM2_AA_UNK = 3
|
| 36 |
+
_DPLM2_AA_X = 24
|
| 37 |
+
_DPLM2_AA_B = 25
|
| 38 |
+
_DPLM2_AA_U = 26
|
| 39 |
+
_DPLM2_AA_Z = 27
|
| 40 |
+
_DPLM2_AA_O = 28
|
| 41 |
+
_DPLM2_AA_MASK = 32
|
| 42 |
+
_DPLM2_STRUCT_BOS = 33
|
| 43 |
+
_DPLM2_STRUCT_EOS = 34
|
| 44 |
+
_DPLM2_STRUCT_UNK = 35
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@contextmanager
|
| 48 |
+
def _temporary_eval(model: _MaskedLanguageModel):
|
| 49 |
+
"""Run one generation forward in eval mode and restore every module flag."""
|
| 50 |
+
training_states = tuple((module, module.training) for module in model.modules())
|
| 51 |
+
model.eval()
|
| 52 |
+
try:
|
| 53 |
+
yield
|
| 54 |
+
finally:
|
| 55 |
+
for module, training in training_states:
|
| 56 |
+
module.training = training
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _resolve_max_iter(model: _MaskedLanguageModel, max_iter: int | None) -> int:
|
| 60 |
+
if max_iter is None:
|
| 61 |
+
max_iter = int(getattr(model.config, "num_diffusion_timesteps", 500))
|
| 62 |
+
if isinstance(max_iter, bool) or not isinstance(max_iter, int) or max_iter <= 0:
|
| 63 |
+
raise ValueError("max_iter must be a positive integer")
|
| 64 |
+
return max_iter
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _validate_inputs(
|
| 68 |
+
input_tokens: torch.Tensor,
|
| 69 |
+
partial_masks: torch.Tensor | None,
|
| 70 |
+
) -> torch.Tensor | None:
|
| 71 |
+
if input_tokens.ndim != 2 or input_tokens.dtype not in {
|
| 72 |
+
torch.int8,
|
| 73 |
+
torch.int16,
|
| 74 |
+
torch.int32,
|
| 75 |
+
torch.int64,
|
| 76 |
+
torch.uint8,
|
| 77 |
+
}:
|
| 78 |
+
raise ValueError("input_tokens must be an integer tensor with shape (b, l)")
|
| 79 |
+
if input_tokens.shape[-1] == 0:
|
| 80 |
+
raise ValueError("input_tokens must contain at least one token")
|
| 81 |
+
if partial_masks is None:
|
| 82 |
+
return None
|
| 83 |
+
if partial_masks.shape != input_tokens.shape or partial_masks.dtype != torch.bool:
|
| 84 |
+
raise ValueError("partial_masks must be boolean with the same shape as input_tokens")
|
| 85 |
+
if partial_masks.device != input_tokens.device:
|
| 86 |
+
raise ValueError("partial_masks and input_tokens must be on the same device")
|
| 87 |
+
return partial_masks
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _validate_temperature(temperature: float | None, *, default: float = 1.0) -> float:
|
| 91 |
+
if temperature is None:
|
| 92 |
+
temperature = default
|
| 93 |
+
temperature = float(temperature)
|
| 94 |
+
if not math.isfinite(temperature) or temperature < 0:
|
| 95 |
+
raise ValueError("temperature must be finite and non-negative")
|
| 96 |
+
return temperature
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def _steps(max_iter: int, *, show_progress: bool) -> Iterable[int]:
|
| 100 |
+
return tqdm(range(max_iter), desc="Decoding", disable=not show_progress)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _categorical(
|
| 104 |
+
logits: torch.Tensor,
|
| 105 |
+
*,
|
| 106 |
+
temperature: float,
|
| 107 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 108 |
+
if temperature == 0:
|
| 109 |
+
scores, tokens = logits.log_softmax(dim=-1).max(dim=-1)
|
| 110 |
+
return tokens, scores
|
| 111 |
+
distribution = torch.distributions.Categorical(logits=logits.div(temperature))
|
| 112 |
+
tokens = distribution.sample()
|
| 113 |
+
return tokens, distribution.log_prob(tokens)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def _gumbel_argmax(
|
| 117 |
+
logits: torch.Tensor,
|
| 118 |
+
*,
|
| 119 |
+
noise_scale: float,
|
| 120 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 121 |
+
uniform = torch.rand_like(logits)
|
| 122 |
+
noise = -torch.log(-torch.log(uniform + 1e-8) + 1e-8)
|
| 123 |
+
return _categorical(logits + noise_scale * noise, temperature=0.0)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def _top_p(logits: torch.Tensor, probability: float = 0.95) -> torch.Tensor:
|
| 127 |
+
"""Apply the nucleus filter used by the official DPLM samplers."""
|
| 128 |
+
|
| 129 |
+
original_shape = logits.shape
|
| 130 |
+
flattened = logits.reshape(-1, original_shape[-1])
|
| 131 |
+
sorted_logits, sorted_indices = flattened.sort(dim=-1, descending=True)
|
| 132 |
+
cumulative = sorted_logits.softmax(dim=-1).cumsum(dim=-1)
|
| 133 |
+
remove = cumulative > probability
|
| 134 |
+
remove[..., 1:] = remove[..., :-1].clone()
|
| 135 |
+
remove[..., 0] = False
|
| 136 |
+
sorted_logits.masked_fill_(remove, -math.inf)
|
| 137 |
+
return sorted_logits.gather(1, sorted_indices.argsort(dim=-1)).reshape(original_shape)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def _lowest_confidence_mask(
|
| 141 |
+
scores: torch.Tensor,
|
| 142 |
+
eligible: torch.Tensor,
|
| 143 |
+
*,
|
| 144 |
+
rate: float,
|
| 145 |
+
stochastic_temperature: float | None = None,
|
| 146 |
+
) -> torch.Tensor:
|
| 147 |
+
selection_scores = scores.masked_fill(~eligible, 1000.0)
|
| 148 |
+
if stochastic_temperature is not None:
|
| 149 |
+
uniform = torch.rand_like(selection_scores)
|
| 150 |
+
noise = -torch.log(-torch.log(uniform + 1e-8) + 1e-8)
|
| 151 |
+
selection_scores = selection_scores + stochastic_temperature * rate * noise
|
| 152 |
+
cutoff_index = (eligible.sum(dim=-1, keepdim=True).to(scores.dtype) * rate).long()
|
| 153 |
+
cutoff_index.clamp_(min=0, max=scores.shape[-1] - 1)
|
| 154 |
+
sorted_scores = selection_scores.sort(dim=-1).values
|
| 155 |
+
cutoff = sorted_scores.gather(dim=-1, index=cutoff_index)
|
| 156 |
+
return (selection_scores < cutoff) & eligible
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def _reparameterize(
|
| 160 |
+
output_tokens: torch.Tensor,
|
| 161 |
+
output_scores: torch.Tensor,
|
| 162 |
+
candidate_tokens: torch.Tensor,
|
| 163 |
+
candidate_scores: torch.Tensor,
|
| 164 |
+
active_mask: torch.Tensor,
|
| 165 |
+
eligible: torch.Tensor,
|
| 166 |
+
*,
|
| 167 |
+
mask_token_id: int,
|
| 168 |
+
rate: float,
|
| 169 |
+
stochastic_temperature: float | None = None,
|
| 170 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 171 |
+
remask = _lowest_confidence_mask(
|
| 172 |
+
candidate_scores,
|
| 173 |
+
eligible,
|
| 174 |
+
rate=rate,
|
| 175 |
+
stochastic_temperature=stochastic_temperature,
|
| 176 |
+
)
|
| 177 |
+
output_tokens.masked_fill_(remask, mask_token_id)
|
| 178 |
+
output_scores.masked_fill_(remask, -math.inf)
|
| 179 |
+
accept = active_mask & eligible & ~remask
|
| 180 |
+
output_tokens.masked_scatter_(accept, candidate_tokens[accept])
|
| 181 |
+
output_scores.masked_scatter_(accept, candidate_scores[accept])
|
| 182 |
+
return remask, output_tokens, output_scores
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def _logits(output: object) -> torch.Tensor:
|
| 186 |
+
value = output.get("logits") if isinstance(output, Mapping) else getattr(output, "logits", None)
|
| 187 |
+
if not torch.is_tensor(value):
|
| 188 |
+
raise RuntimeError("The masked-language model did not return logits")
|
| 189 |
+
return value
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def _suppress_token_ids(logits: torch.Tensor, token_ids: Iterable[int]) -> None:
|
| 193 |
+
vocabulary_size = logits.shape[-1]
|
| 194 |
+
for token_id in token_ids:
|
| 195 |
+
if 0 <= token_id < vocabulary_size:
|
| 196 |
+
logits[..., token_id] = -math.inf
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def _dplm_special_id(
|
| 200 |
+
model: _MaskedLanguageModel,
|
| 201 |
+
tokenizer: object | None,
|
| 202 |
+
name: str,
|
| 203 |
+
default: int,
|
| 204 |
+
) -> int:
|
| 205 |
+
value = getattr(model.config, name, None)
|
| 206 |
+
if value is None:
|
| 207 |
+
if tokenizer is None:
|
| 208 |
+
tokenizer = getattr(model, "tokenizer", None)
|
| 209 |
+
value = getattr(tokenizer, name, None)
|
| 210 |
+
return default if value is None else int(value)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def _dplm_resample_repeats(
|
| 214 |
+
model: _MaskedLanguageModel,
|
| 215 |
+
candidate_tokens: torch.Tensor,
|
| 216 |
+
candidate_scores: torch.Tensor,
|
| 217 |
+
*,
|
| 218 |
+
invalid_token_ids: tuple[int, ...],
|
| 219 |
+
mask_token_id: int,
|
| 220 |
+
ratio: float,
|
| 221 |
+
) -> None:
|
| 222 |
+
selected_rows: list[int] = []
|
| 223 |
+
resample_tokens: list[torch.Tensor] = []
|
| 224 |
+
resample_scores: list[torch.Tensor] = []
|
| 225 |
+
resample_masks: list[torch.Tensor] = []
|
| 226 |
+
for row_index, row in enumerate(candidate_tokens):
|
| 227 |
+
positions: dict[int, list[int]] = {}
|
| 228 |
+
for position, token in enumerate(row.tolist()):
|
| 229 |
+
positions.setdefault(int(token), []).append(position)
|
| 230 |
+
repeated = [indices for indices in positions.values() if len(indices) > row.numel() * ratio]
|
| 231 |
+
if not repeated:
|
| 232 |
+
continue
|
| 233 |
+
M = torch.zeros_like(row, dtype=torch.bool)
|
| 234 |
+
for indices in repeated:
|
| 235 |
+
M[indices] = True
|
| 236 |
+
selected_rows.append(row_index)
|
| 237 |
+
resample_masks.append(M)
|
| 238 |
+
resample_tokens.append(row.masked_fill(M, mask_token_id))
|
| 239 |
+
resample_scores.append(candidate_scores[row_index])
|
| 240 |
+
|
| 241 |
+
if not selected_rows:
|
| 242 |
+
return
|
| 243 |
+
X = torch.stack(resample_tokens)
|
| 244 |
+
S = torch.stack(resample_scores)
|
| 245 |
+
M = torch.stack(resample_masks)
|
| 246 |
+
with _temporary_eval(model), torch.no_grad():
|
| 247 |
+
logits = _logits(model(input_ids=X, return_dict=True))
|
| 248 |
+
if logits.dtype != S.dtype:
|
| 249 |
+
logits = logits.to(S.dtype)
|
| 250 |
+
_suppress_token_ids(logits, invalid_token_ids)
|
| 251 |
+
logits = _top_p(logits)
|
| 252 |
+
sampled_tokens, sampled_scores = _gumbel_argmax(logits, noise_scale=1.0)
|
| 253 |
+
X.masked_scatter_(M, sampled_tokens[M])
|
| 254 |
+
S.masked_scatter_(M, sampled_scores[M])
|
| 255 |
+
candidate_tokens[selected_rows] = X
|
| 256 |
+
candidate_scores[selected_rows] = S
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def generate_dplm(
|
| 260 |
+
model: _MaskedLanguageModel,
|
| 261 |
+
input_tokens: torch.Tensor,
|
| 262 |
+
*,
|
| 263 |
+
tokenizer: object | None = None,
|
| 264 |
+
max_iter: int | None = None,
|
| 265 |
+
temperature: float | None = None,
|
| 266 |
+
partial_masks: torch.Tensor | None = None,
|
| 267 |
+
sampling_strategy: str = "gumbel_argmax",
|
| 268 |
+
disable_resample: bool = False,
|
| 269 |
+
resample_ratio: float = 0.25,
|
| 270 |
+
show_progress: bool = False,
|
| 271 |
+
) -> torch.Tensor:
|
| 272 |
+
"""Generate DPLM sequences with the official iterative unmasking process.
|
| 273 |
+
|
| 274 |
+
``input_tokens`` is X with shape (b, l). ``partial_masks=True`` marks fixed
|
| 275 |
+
positions. The return value is the generated token tensor X with shape
|
| 276 |
+
(b, l), matching the official DPLM public API.
|
| 277 |
+
"""
|
| 278 |
+
|
| 279 |
+
partial_masks = _validate_inputs(input_tokens, partial_masks)
|
| 280 |
+
max_iter = _resolve_max_iter(model, max_iter)
|
| 281 |
+
# Upstream treats ``None`` as the falsey, zero-temperature branch for
|
| 282 |
+
# vanilla categorical sampling. Gumbel and argmax strategies ignore it.
|
| 283 |
+
temperature = _validate_temperature(temperature, default=0.0)
|
| 284 |
+
if sampling_strategy not in {"vanilla", "argmax", "gumbel_argmax"}:
|
| 285 |
+
raise ValueError(f"Unsupported DPLM sampling strategy: {sampling_strategy!r}")
|
| 286 |
+
if not 0 < float(resample_ratio) <= 1:
|
| 287 |
+
raise ValueError("resample_ratio must be in (0, 1]")
|
| 288 |
+
|
| 289 |
+
pad_id = _dplm_special_id(model, tokenizer, "pad_token_id", 1)
|
| 290 |
+
bos_id = _dplm_special_id(model, tokenizer, "bos_token_id", 0)
|
| 291 |
+
eos_id = _dplm_special_id(model, tokenizer, "eos_token_id", 2)
|
| 292 |
+
mask_id = _dplm_special_id(model, tokenizer, "mask_token_id", 32)
|
| 293 |
+
x_id = 24
|
| 294 |
+
X = input_tokens.clone()
|
| 295 |
+
mutable = X.ne(pad_id) & X.ne(bos_id) & X.ne(eos_id)
|
| 296 |
+
if partial_masks is not None:
|
| 297 |
+
mutable &= ~partial_masks
|
| 298 |
+
X.masked_fill_(mutable, mask_id)
|
| 299 |
+
S = torch.zeros_like(X, dtype=torch.float32)
|
| 300 |
+
active = mutable.clone()
|
| 301 |
+
invalid_ids = (mask_id, x_id, pad_id, bos_id, eos_id)
|
| 302 |
+
for step in _steps(max_iter, show_progress=show_progress):
|
| 303 |
+
with _temporary_eval(model), torch.no_grad():
|
| 304 |
+
logits = _logits(model(input_ids=X, return_dict=True))
|
| 305 |
+
if logits.dtype != S.dtype:
|
| 306 |
+
logits = logits.to(S.dtype)
|
| 307 |
+
_suppress_token_ids(logits, invalid_ids)
|
| 308 |
+
if sampling_strategy == "vanilla":
|
| 309 |
+
candidate_tokens, candidate_scores = _categorical(
|
| 310 |
+
logits,
|
| 311 |
+
temperature=temperature,
|
| 312 |
+
)
|
| 313 |
+
elif sampling_strategy == "argmax":
|
| 314 |
+
candidate_scores, candidate_tokens = logits.max(dim=-1)
|
| 315 |
+
else:
|
| 316 |
+
candidate_tokens, candidate_scores = _gumbel_argmax(logits, noise_scale=1.0)
|
| 317 |
+
if not disable_resample:
|
| 318 |
+
_dplm_resample_repeats(
|
| 319 |
+
model,
|
| 320 |
+
candidate_tokens,
|
| 321 |
+
candidate_scores,
|
| 322 |
+
invalid_token_ids=invalid_ids,
|
| 323 |
+
mask_token_id=mask_id,
|
| 324 |
+
ratio=float(resample_ratio),
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
eligible = X.ne(pad_id) & X.ne(bos_id) & X.ne(eos_id)
|
| 328 |
+
if partial_masks is not None:
|
| 329 |
+
eligible &= ~partial_masks
|
| 330 |
+
rate = 1.0 - (step + 1) / max_iter
|
| 331 |
+
active, X, S = _reparameterize(
|
| 332 |
+
X.clone(),
|
| 333 |
+
S.clone(),
|
| 334 |
+
candidate_tokens,
|
| 335 |
+
candidate_scores,
|
| 336 |
+
active,
|
| 337 |
+
eligible,
|
| 338 |
+
mask_token_id=mask_id,
|
| 339 |
+
rate=rate,
|
| 340 |
+
)
|
| 341 |
+
return X
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def _normalize_dplm2_special_ids(X: torch.Tensor, vocabulary_size: int) -> torch.Tensor:
|
| 345 |
+
normalized = X.clone()
|
| 346 |
+
replacements = {
|
| 347 |
+
vocabulary_size: _DPLM2_AA_EOS,
|
| 348 |
+
vocabulary_size + 1: _DPLM2_AA_UNK,
|
| 349 |
+
vocabulary_size + 2: _DPLM2_AA_BOS,
|
| 350 |
+
vocabulary_size + 3: _DPLM2_AA_MASK,
|
| 351 |
+
}
|
| 352 |
+
for generic_id, native_id in replacements.items():
|
| 353 |
+
normalized.masked_fill_(X.eq(generic_id), native_id)
|
| 354 |
+
return normalized
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def _dplm2_types(X: torch.Tensor) -> torch.Tensor:
|
| 358 |
+
valid = X.ne(_DPLM2_PAD)
|
| 359 |
+
types = ((X < _DPLM2_AA_BOUNDARY) & valid).to(torch.int64)
|
| 360 |
+
types.masked_fill_(~valid, 2)
|
| 361 |
+
return types
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
def _dplm2_mutable(X: torch.Tensor, partial_masks: torch.Tensor | None) -> torch.Tensor:
|
| 365 |
+
mutable = (
|
| 366 |
+
X.ne(_DPLM2_PAD)
|
| 367 |
+
& X.ne(_DPLM2_AA_BOS)
|
| 368 |
+
& X.ne(_DPLM2_AA_EOS)
|
| 369 |
+
& X.ne(_DPLM2_STRUCT_BOS)
|
| 370 |
+
& X.ne(_DPLM2_STRUCT_EOS)
|
| 371 |
+
)
|
| 372 |
+
if partial_masks is not None:
|
| 373 |
+
mutable &= ~partial_masks
|
| 374 |
+
return mutable
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def _dplm2_unmasking_temperature(strategy: str) -> float | None:
|
| 378 |
+
if strategy == "deterministic":
|
| 379 |
+
return None
|
| 380 |
+
if strategy.startswith("stochastic"):
|
| 381 |
+
suffix = strategy.removeprefix("stochastic")
|
| 382 |
+
value = 1.0 if not suffix else float(suffix)
|
| 383 |
+
if not math.isfinite(value) or value < 0:
|
| 384 |
+
raise ValueError("The stochastic unmasking temperature must be non-negative")
|
| 385 |
+
return value
|
| 386 |
+
raise ValueError(f"Unsupported DPLM2 unmasking strategy: {strategy!r}")
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def _annealing_temperature(strategy: str, step: int, max_iter: int) -> float | None:
|
| 390 |
+
if not strategy.startswith("annealing"):
|
| 391 |
+
return None
|
| 392 |
+
try:
|
| 393 |
+
maximum, minimum = map(float, strategy.split("@", maxsplit=1)[1].split(":"))
|
| 394 |
+
except (IndexError, ValueError) as error:
|
| 395 |
+
raise ValueError("Annealing must use the form 'annealing@maximum:minimum'") from error
|
| 396 |
+
if not all(math.isfinite(value) and value >= 0 for value in (maximum, minimum)):
|
| 397 |
+
raise ValueError("Annealing temperatures must be finite and non-negative")
|
| 398 |
+
rate = 1.0 - step / max_iter
|
| 399 |
+
return minimum + (maximum - minimum) * rate
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def generate_dplm2(
|
| 403 |
+
model: _MaskedLanguageModel,
|
| 404 |
+
input_tokens: torch.Tensor,
|
| 405 |
+
*,
|
| 406 |
+
max_iter: int | None = None,
|
| 407 |
+
temperature: float = 1.0,
|
| 408 |
+
partial_masks: torch.Tensor | None = None,
|
| 409 |
+
unmasking_strategy: str = "stochastic1.0",
|
| 410 |
+
sampling_strategy: str = "annealing@2.0:0.1",
|
| 411 |
+
show_progress: bool = False,
|
| 412 |
+
) -> dict[str, torch.Tensor]:
|
| 413 |
+
"""Generate packed DPLM2 sequence and structure tracks.
|
| 414 |
+
|
| 415 |
+
``input_tokens`` is X with shape (b, l). A packed co-generation input has
|
| 416 |
+
two equal-length modality tracks. ``partial_masks=True`` marks fixed
|
| 417 |
+
positions. The output mapping matches the official DPLM2 public API.
|
| 418 |
+
"""
|
| 419 |
+
|
| 420 |
+
partial_masks = _validate_inputs(input_tokens, partial_masks)
|
| 421 |
+
max_iter = _resolve_max_iter(model, max_iter)
|
| 422 |
+
temperature = _validate_temperature(temperature)
|
| 423 |
+
unmasking_temperature = _dplm2_unmasking_temperature(unmasking_strategy)
|
| 424 |
+
if sampling_strategy.startswith("annealing"):
|
| 425 |
+
_annealing_temperature(sampling_strategy, 0, max_iter)
|
| 426 |
+
elif sampling_strategy not in {"argmax", "gumbel_argmax"}:
|
| 427 |
+
raise ValueError(f"Unsupported DPLM2 sampling strategy: {sampling_strategy!r}")
|
| 428 |
+
vocabulary_size = int(model.config.vocab_size)
|
| 429 |
+
if vocabulary_size <= _DPLM2_STRUCT_UNK + 1:
|
| 430 |
+
raise ValueError("DPLM2 generation requires the multimodal vocabulary")
|
| 431 |
+
struct_mask_id = vocabulary_size - 1
|
| 432 |
+
|
| 433 |
+
X = _normalize_dplm2_special_ids(input_tokens, vocabulary_size)
|
| 434 |
+
if X.numel() and (X.min() < 0 or X.max() >= vocabulary_size):
|
| 435 |
+
raise ValueError("input_tokens contains an ID outside the DPLM2 vocabulary")
|
| 436 |
+
mutable = _dplm2_mutable(X, partial_masks)
|
| 437 |
+
types = _dplm2_types(X)
|
| 438 |
+
X.masked_fill_(mutable & types.eq(1), _DPLM2_AA_MASK)
|
| 439 |
+
X.masked_fill_(mutable & types.eq(0), struct_mask_id)
|
| 440 |
+
S = torch.zeros_like(X, dtype=torch.float32)
|
| 441 |
+
active = mutable.clone()
|
| 442 |
+
invalid_ids = (
|
| 443 |
+
_DPLM2_AA_BOS,
|
| 444 |
+
_DPLM2_AA_EOS,
|
| 445 |
+
_DPLM2_AA_MASK,
|
| 446 |
+
_DPLM2_STRUCT_BOS,
|
| 447 |
+
_DPLM2_STRUCT_EOS,
|
| 448 |
+
struct_mask_id,
|
| 449 |
+
_DPLM2_PAD,
|
| 450 |
+
_DPLM2_AA_UNK,
|
| 451 |
+
_DPLM2_STRUCT_UNK,
|
| 452 |
+
_DPLM2_AA_X,
|
| 453 |
+
_DPLM2_AA_B,
|
| 454 |
+
_DPLM2_AA_U,
|
| 455 |
+
_DPLM2_AA_Z,
|
| 456 |
+
_DPLM2_AA_O,
|
| 457 |
+
)
|
| 458 |
+
for step in _steps(max_iter, show_progress=show_progress):
|
| 459 |
+
eligible = _dplm2_mutable(X, partial_masks)
|
| 460 |
+
types = _dplm2_types(X)
|
| 461 |
+
with _temporary_eval(model), torch.no_grad():
|
| 462 |
+
logits = _logits(model(input_ids=X, return_dict=True)).log_softmax(dim=-1)
|
| 463 |
+
if logits.dtype != S.dtype:
|
| 464 |
+
logits = logits.to(S.dtype)
|
| 465 |
+
aa_rows, aa_columns = torch.where(types.eq(1) & eligible)
|
| 466 |
+
struct_rows, struct_columns = torch.where(types.eq(0) & eligible)
|
| 467 |
+
logits[aa_rows, aa_columns, _DPLM2_AA_BOUNDARY:] = -math.inf
|
| 468 |
+
logits[struct_rows, struct_columns, :_DPLM2_AA_BOUNDARY] = -math.inf
|
| 469 |
+
_suppress_token_ids(logits, invalid_ids)
|
| 470 |
+
logits = _top_p(logits)
|
| 471 |
+
|
| 472 |
+
if sampling_strategy == "argmax":
|
| 473 |
+
candidate_scores, candidate_tokens = logits.max(dim=-1)
|
| 474 |
+
elif sampling_strategy == "gumbel_argmax":
|
| 475 |
+
candidate_tokens, candidate_scores = _gumbel_argmax(
|
| 476 |
+
logits,
|
| 477 |
+
noise_scale=temperature,
|
| 478 |
+
)
|
| 479 |
+
candidate_tokens.masked_scatter_(~eligible, X[~eligible])
|
| 480 |
+
else:
|
| 481 |
+
annealed = _annealing_temperature(sampling_strategy, step, max_iter)
|
| 482 |
+
sample_temperature = temperature if annealed is None else annealed
|
| 483 |
+
candidate_tokens, candidate_scores = _categorical(
|
| 484 |
+
logits,
|
| 485 |
+
temperature=sample_temperature,
|
| 486 |
+
)
|
| 487 |
+
|
| 488 |
+
rate = 1.0 - (step + 1) / max_iter
|
| 489 |
+
new_active = torch.zeros_like(active)
|
| 490 |
+
for modality, mask_id in ((1, _DPLM2_AA_MASK), (0, struct_mask_id)):
|
| 491 |
+
modality_positions = types.eq(modality) & eligible
|
| 492 |
+
if not bool(modality_positions.any()):
|
| 493 |
+
continue
|
| 494 |
+
modality_active, X, S = _reparameterize(
|
| 495 |
+
X,
|
| 496 |
+
S,
|
| 497 |
+
candidate_tokens,
|
| 498 |
+
candidate_scores,
|
| 499 |
+
active,
|
| 500 |
+
modality_positions,
|
| 501 |
+
mask_token_id=mask_id,
|
| 502 |
+
rate=rate,
|
| 503 |
+
stochastic_temperature=unmasking_temperature,
|
| 504 |
+
)
|
| 505 |
+
new_active |= modality_active
|
| 506 |
+
active = new_active
|
| 507 |
+
return {"output_tokens": X}
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
__all__ = ["generate_dplm", "generate_dplm2"]
|
fastplms/models/_esm_rotary.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Stable ESM rotary embeddings independent of Transformers internals.
|
| 2 |
+
|
| 3 |
+
Transformers 5 changed both the name and call contract of its private ESM
|
| 4 |
+
rotary helper. FastPLMs checkpoints use the earlier two-tensor contract, so
|
| 5 |
+
the small mathematical primitive lives here instead of importing a private
|
| 6 |
+
Transformers implementation.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
from torch import nn
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _rotate_half(tensor: torch.Tensor) -> torch.Tensor:
|
| 16 |
+
"""Rotate the final dimension of X by 90 degrees in paired subspaces."""
|
| 17 |
+
|
| 18 |
+
first, second = tensor.chunk(2, dim=-1)
|
| 19 |
+
return torch.cat((-second, first), dim=-1)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def apply_rotary_pos_emb(
|
| 23 |
+
tensor: torch.Tensor,
|
| 24 |
+
cos: torch.Tensor,
|
| 25 |
+
sin: torch.Tensor,
|
| 26 |
+
) -> torch.Tensor:
|
| 27 |
+
"""Apply cached rotary factors to X with shape ``(b, h, l, d)``."""
|
| 28 |
+
|
| 29 |
+
cos = cos[:, :, : tensor.shape[-2], :]
|
| 30 |
+
sin = sin[:, :, : tensor.shape[-2], :]
|
| 31 |
+
return tensor * cos + _rotate_half(tensor) * sin
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class RotaryEmbedding(nn.Module):
|
| 35 |
+
"""Apply rotary position embeddings to query and key tensors."""
|
| 36 |
+
|
| 37 |
+
inv_freq: torch.Tensor
|
| 38 |
+
|
| 39 |
+
def __init__(self, dim: int) -> None:
|
| 40 |
+
super().__init__()
|
| 41 |
+
frequencies = 1.0 / (10_000 ** (torch.arange(0, dim, 2, dtype=torch.int64).float() / dim))
|
| 42 |
+
# Keep this persistent to preserve the historical checkpoint schema.
|
| 43 |
+
self.register_buffer("inv_freq", frequencies)
|
| 44 |
+
self._seq_len_cached: int | None = None
|
| 45 |
+
self._cos_cached: torch.Tensor | None = None
|
| 46 |
+
self._sin_cached: torch.Tensor | None = None
|
| 47 |
+
|
| 48 |
+
def _update_cos_sin_tables(
|
| 49 |
+
self,
|
| 50 |
+
tensor: torch.Tensor,
|
| 51 |
+
seq_dimension: int = 2,
|
| 52 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 53 |
+
seq_len = tensor.shape[seq_dimension]
|
| 54 |
+
cache_stale = (
|
| 55 |
+
self._cos_cached is None
|
| 56 |
+
or self._sin_cached is None
|
| 57 |
+
or self._seq_len_cached != seq_len
|
| 58 |
+
or self._cos_cached.device != tensor.device
|
| 59 |
+
)
|
| 60 |
+
if cache_stale:
|
| 61 |
+
self._seq_len_cached = seq_len
|
| 62 |
+
positions = torch.arange(seq_len, device=tensor.device).type_as(self.inv_freq)
|
| 63 |
+
angles = torch.outer(positions, self.inv_freq)
|
| 64 |
+
angles = torch.cat((angles, angles), dim=-1).to(tensor.device)
|
| 65 |
+
self._cos_cached = angles.cos()[None, None, :, :]
|
| 66 |
+
self._sin_cached = angles.sin()[None, None, :, :]
|
| 67 |
+
|
| 68 |
+
assert self._cos_cached is not None
|
| 69 |
+
assert self._sin_cached is not None
|
| 70 |
+
return self._cos_cached, self._sin_cached
|
| 71 |
+
|
| 72 |
+
def forward(
|
| 73 |
+
self,
|
| 74 |
+
query: torch.Tensor,
|
| 75 |
+
key: torch.Tensor,
|
| 76 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 77 |
+
cos, sin = self._update_cos_sin_tables(key, seq_dimension=-2)
|
| 78 |
+
return (
|
| 79 |
+
apply_rotary_pos_emb(query, cos, sin).to(dtype=query.dtype),
|
| 80 |
+
apply_rotary_pos_emb(key, cos, sin).to(dtype=key.dtype),
|
| 81 |
+
)
|
fastplms/models/dplm2/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DPLM2 model and tokenizer implementation."""
|
| 2 |
+
|
| 3 |
+
from .tokenization_dplm2 import DPLM2Tokenizer
|
| 4 |
+
|
| 5 |
+
__all__ = ["DPLM2Tokenizer"]
|
fastplms/models/dplm2/modeling_dplm2.py
ADDED
|
@@ -0,0 +1,1457 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastPLMs-compatible DPLM2 implementation.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import contextlib
|
| 8 |
+
import warnings
|
| 9 |
+
from collections.abc import Sequence
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
from typing import Any, ClassVar
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn as nn
|
| 15 |
+
from einops import rearrange
|
| 16 |
+
from torch.nn import functional as F
|
| 17 |
+
from torch.nn.attention import SDPBackend, sdpa_kernel
|
| 18 |
+
from transformers import AutoTokenizer
|
| 19 |
+
from transformers.modeling_outputs import (
|
| 20 |
+
BaseModelOutputWithPoolingAndCrossAttentions,
|
| 21 |
+
MaskedLMOutput,
|
| 22 |
+
ModelOutput,
|
| 23 |
+
SequenceClassifierOutput,
|
| 24 |
+
TokenClassifierOutput,
|
| 25 |
+
)
|
| 26 |
+
from transformers.models.esm.configuration_esm import EsmConfig
|
| 27 |
+
from transformers.models.esm.modeling_esm import (
|
| 28 |
+
EsmAttention,
|
| 29 |
+
EsmClassificationHead,
|
| 30 |
+
EsmContactPredictionHead,
|
| 31 |
+
EsmEmbeddings,
|
| 32 |
+
EsmEncoder,
|
| 33 |
+
EsmIntermediate,
|
| 34 |
+
EsmLayer,
|
| 35 |
+
EsmLMHead,
|
| 36 |
+
EsmOutput,
|
| 37 |
+
EsmPooler,
|
| 38 |
+
EsmPreTrainedModel,
|
| 39 |
+
EsmSelfAttention,
|
| 40 |
+
EsmSelfOutput,
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
from fastplms.models._diffusion_generation import generate_dplm2
|
| 44 |
+
from fastplms.models._esm_rotary import RotaryEmbedding, apply_rotary_pos_emb
|
| 45 |
+
from fastplms.models.dplm2.tokenization_dplm2 import DPLM2Tokenizer
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
from fastplms.attention import (
|
| 49 |
+
AttentionBackend,
|
| 50 |
+
FastPLMsAttentionMixin,
|
| 51 |
+
get_attention_mask,
|
| 52 |
+
resolve_attention_backend,
|
| 53 |
+
resolve_attention_backend_for_call,
|
| 54 |
+
)
|
| 55 |
+
from fastplms.embeddings import EmbeddingMixin, select_hidden_state_embeddings
|
| 56 |
+
from fastplms.models.ttt import FastPLMTestTimeTrainingMixin
|
| 57 |
+
except ModuleNotFoundError as error:
|
| 58 |
+
_COMPOSITE_REQUIRED_NAMES = (
|
| 59 |
+
"AttentionBackend",
|
| 60 |
+
"EmbeddingMixin",
|
| 61 |
+
"FastPLMsAttentionMixin",
|
| 62 |
+
"FastPLMTestTimeTrainingMixin",
|
| 63 |
+
"get_attention_mask",
|
| 64 |
+
"resolve_attention_backend",
|
| 65 |
+
"resolve_attention_backend_for_call",
|
| 66 |
+
"select_hidden_state_embeddings",
|
| 67 |
+
)
|
| 68 |
+
if error.name != "fastplms" or any(
|
| 69 |
+
name not in globals() for name in _COMPOSITE_REQUIRED_NAMES
|
| 70 |
+
):
|
| 71 |
+
raise
|
| 72 |
+
# Legacy flat Hub composites define every shared symbol above this block.
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _infer_modality_type(input_ids: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
|
| 76 |
+
input_mask = attention_mask.bool()
|
| 77 |
+
modality_type = ((input_ids < 33) & input_mask).int()
|
| 78 |
+
modality_type[~input_mask] = 2
|
| 79 |
+
return modality_type
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _normalize_dplm2_input_ids(input_ids: torch.Tensor, vocab_size: int) -> torch.Tensor:
|
| 83 |
+
if input_ids.numel() == 0:
|
| 84 |
+
return input_ids
|
| 85 |
+
|
| 86 |
+
normalized_input_ids = input_ids.clone()
|
| 87 |
+
generic_to_aa_special_ids = {
|
| 88 |
+
vocab_size: 2,
|
| 89 |
+
vocab_size + 1: 3,
|
| 90 |
+
vocab_size + 2: 0,
|
| 91 |
+
vocab_size + 3: 32,
|
| 92 |
+
}
|
| 93 |
+
for generic_id, aa_id in generic_to_aa_special_ids.items():
|
| 94 |
+
normalized_input_ids[input_ids == generic_id] = aa_id
|
| 95 |
+
|
| 96 |
+
valid_token_mask = normalized_input_ids.ge(0)
|
| 97 |
+
if valid_token_mask.any():
|
| 98 |
+
max_token_id = int(normalized_input_ids[valid_token_mask].max().item())
|
| 99 |
+
if max_token_id >= vocab_size:
|
| 100 |
+
raise ValueError(
|
| 101 |
+
f"Found token id {max_token_id} outside the DPLM2 embedding table "
|
| 102 |
+
f"(vocab_size={vocab_size}). Tokenizer special tokens must be normalized "
|
| 103 |
+
"before embedding."
|
| 104 |
+
)
|
| 105 |
+
return normalized_input_ids
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _validate_dplm2_model_inputs(
|
| 109 |
+
*,
|
| 110 |
+
input_ids: torch.Tensor | None,
|
| 111 |
+
inputs_embeds: torch.Tensor | None,
|
| 112 |
+
attention_mask: torch.Tensor | None,
|
| 113 |
+
type_ids: torch.Tensor | None,
|
| 114 |
+
hidden_size: int,
|
| 115 |
+
) -> tuple[int, int]:
|
| 116 |
+
if (input_ids is None) == (inputs_embeds is None):
|
| 117 |
+
raise ValueError("Specify exactly one of input_ids or inputs_embeds.")
|
| 118 |
+
|
| 119 |
+
if input_ids is not None:
|
| 120 |
+
if input_ids.ndim != 2:
|
| 121 |
+
raise ValueError(
|
| 122 |
+
f"input_ids must have shape (batch, seq_len), got {tuple(input_ids.shape)}."
|
| 123 |
+
)
|
| 124 |
+
batch_size, seq_len = input_ids.shape
|
| 125 |
+
else:
|
| 126 |
+
if inputs_embeds is None: # Defensive guard for static narrowing.
|
| 127 |
+
raise RuntimeError("inputs_embeds validation reached an invalid state.")
|
| 128 |
+
if inputs_embeds.ndim != 3:
|
| 129 |
+
raise ValueError(
|
| 130 |
+
"inputs_embeds must have shape (batch, seq_len, hidden_size), "
|
| 131 |
+
f"got {tuple(inputs_embeds.shape)}."
|
| 132 |
+
)
|
| 133 |
+
if inputs_embeds.shape[-1] != hidden_size:
|
| 134 |
+
raise ValueError(
|
| 135 |
+
f"inputs_embeds hidden size must be {hidden_size}, got {inputs_embeds.shape[-1]}."
|
| 136 |
+
)
|
| 137 |
+
batch_size, seq_len = inputs_embeds.shape[:2]
|
| 138 |
+
|
| 139 |
+
expected_shape = (batch_size, seq_len)
|
| 140 |
+
for name, value in (("attention_mask", attention_mask), ("type_ids", type_ids)):
|
| 141 |
+
if value is not None and tuple(value.shape) != expected_shape:
|
| 142 |
+
raise ValueError(f"{name} must have shape {expected_shape}, got {tuple(value.shape)}.")
|
| 143 |
+
return expected_shape
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def _has_packed_multimodal_layout(
|
| 147 |
+
type_ids: torch.Tensor | None,
|
| 148 |
+
aa_type: int,
|
| 149 |
+
struct_type: int,
|
| 150 |
+
pad_type: int,
|
| 151 |
+
) -> bool:
|
| 152 |
+
if type_ids is None:
|
| 153 |
+
return False
|
| 154 |
+
if type_ids.ndim != 2:
|
| 155 |
+
raise ValueError(
|
| 156 |
+
f"Expected type_ids to have shape (batch, seq_len), got {tuple(type_ids.shape)}"
|
| 157 |
+
)
|
| 158 |
+
seq_len = type_ids.shape[-1]
|
| 159 |
+
if seq_len % 2 != 0:
|
| 160 |
+
return False
|
| 161 |
+
|
| 162 |
+
half_len = seq_len // 2
|
| 163 |
+
first_half = type_ids[:, :half_len]
|
| 164 |
+
second_half = type_ids[:, half_len:]
|
| 165 |
+
|
| 166 |
+
first_is_aa = ((first_half == aa_type) | (first_half == pad_type)).all(dim=-1)
|
| 167 |
+
first_is_struct = ((first_half == struct_type) | (first_half == pad_type)).all(dim=-1)
|
| 168 |
+
second_is_aa = ((second_half == aa_type) | (second_half == pad_type)).all(dim=-1)
|
| 169 |
+
second_is_struct = ((second_half == struct_type) | (second_half == pad_type)).all(dim=-1)
|
| 170 |
+
first_count = first_half.ne(pad_type).sum(dim=-1)
|
| 171 |
+
second_count = second_half.ne(pad_type).sum(dim=-1)
|
| 172 |
+
modalities_are_separate = (first_is_aa & second_is_struct) | (first_is_struct & second_is_aa)
|
| 173 |
+
packed_rows = modalities_are_separate & first_count.gt(0) & first_count.eq(second_count)
|
| 174 |
+
return bool(packed_rows.all())
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
@dataclass
|
| 178 |
+
class DPLM2MaskedLMOutput(MaskedLMOutput):
|
| 179 |
+
"""Masked-LM output with DPLM2 extensions after the HF fields."""
|
| 180 |
+
|
| 181 |
+
s_max: tuple[list[torch.Tensor], ...] | None = None
|
| 182 |
+
last_hidden_state: torch.Tensor | None = None
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
@dataclass
|
| 186 |
+
class DPLM2ModelOutput(BaseModelOutputWithPoolingAndCrossAttentions):
|
| 187 |
+
"""Base-model output with optional attention diagnostics."""
|
| 188 |
+
|
| 189 |
+
s_max: tuple[list[torch.Tensor], ...] | None = None
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
@dataclass
|
| 193 |
+
class DPLM2SequenceClassifierOutput(SequenceClassifierOutput):
|
| 194 |
+
"""Sequence-classification output with optional attention diagnostics."""
|
| 195 |
+
|
| 196 |
+
s_max: tuple[list[torch.Tensor], ...] | None = None
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
@dataclass
|
| 200 |
+
class DPLM2TokenClassifierOutput(TokenClassifierOutput):
|
| 201 |
+
"""Token-classification output with optional attention diagnostics."""
|
| 202 |
+
|
| 203 |
+
s_max: tuple[list[torch.Tensor], ...] | None = None
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
@dataclass
|
| 207 |
+
class DPLM2EncoderOutput(ModelOutput):
|
| 208 |
+
last_hidden_state: torch.Tensor | None = None
|
| 209 |
+
hidden_states: tuple[torch.Tensor, ...] | None = None
|
| 210 |
+
attentions: tuple[torch.Tensor, ...] | None = None
|
| 211 |
+
s_max: tuple[list[torch.Tensor], ...] | None = None
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
class DPLM2Config(EsmConfig):
|
| 215 |
+
model_type = "dplm2"
|
| 216 |
+
|
| 217 |
+
def __init__(
|
| 218 |
+
self,
|
| 219 |
+
attn_backend: str | None = "sdpa",
|
| 220 |
+
add_pooling_layer: bool = False,
|
| 221 |
+
aa_type: int = 1,
|
| 222 |
+
struct_type: int = 0,
|
| 223 |
+
pad_type: int = 2,
|
| 224 |
+
**kwargs,
|
| 225 |
+
):
|
| 226 |
+
if kwargs.get("is_decoder", False) or kwargs.get("add_cross_attention", False):
|
| 227 |
+
raise ValueError(
|
| 228 |
+
"DPLM2 is encoder-only; is_decoder and add_cross_attention must be false."
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
# Published DPLM2 checkpoint configs inherited ``use_cache=true`` from
|
| 232 |
+
# EsmConfig even though the FastPLMs encoder has never implemented a KV
|
| 233 |
+
# cache. Keep those legacy artifacts loadable, but make the effective
|
| 234 |
+
# and newly serialized contract explicit and fail closed.
|
| 235 |
+
if kwargs.get("use_cache") is True:
|
| 236 |
+
warnings.warn(
|
| 237 |
+
"Legacy DPLM2 config requested use_cache=True, but DPLM2 is encoder-only "
|
| 238 |
+
"and does not implement KV caching; normalizing use_cache to False.",
|
| 239 |
+
UserWarning,
|
| 240 |
+
stacklevel=2,
|
| 241 |
+
)
|
| 242 |
+
kwargs["is_decoder"] = False
|
| 243 |
+
kwargs["add_cross_attention"] = False
|
| 244 |
+
kwargs["use_cache"] = False
|
| 245 |
+
super().__init__(**kwargs)
|
| 246 |
+
# DPLM2's published implementation and manifest expose SDPA only. An
|
| 247 |
+
# older checkpoint may omit this FastPLMs field (or serialize it as
|
| 248 |
+
# null), so normalize that legacy representation to the same explicit
|
| 249 |
+
# backend before Transformers chooses its own generic eager default.
|
| 250 |
+
self.attn_backend = "sdpa" if attn_backend is None else attn_backend
|
| 251 |
+
self.add_pooling_layer = add_pooling_layer
|
| 252 |
+
self.aa_type = aa_type
|
| 253 |
+
self.struct_type = struct_type
|
| 254 |
+
self.pad_type = pad_type
|
| 255 |
+
self.tie_word_embeddings = False
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
_TOKENIZER_LOAD_CONTEXT_KEYS = (
|
| 259 |
+
"cache_dir",
|
| 260 |
+
"force_download",
|
| 261 |
+
"local_files_only",
|
| 262 |
+
"proxies",
|
| 263 |
+
"revision",
|
| 264 |
+
"subfolder",
|
| 265 |
+
"token",
|
| 266 |
+
"trust_remote_code",
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
class DPLM2PreTrainedModel(FastPLMsAttentionMixin, EsmPreTrainedModel):
|
| 271 |
+
config_class = DPLM2Config
|
| 272 |
+
# All advertised wrappers install the encoder at ``self.esm``. Transformers
|
| 273 |
+
# uses this name both for ``base_model`` and checkpoint prefix reconciliation.
|
| 274 |
+
base_model_prefix = "esm"
|
| 275 |
+
supports_gradient_checkpointing = True
|
| 276 |
+
all_tied_weights_keys: ClassVar[dict[str, str]] = {}
|
| 277 |
+
_supports_flex_attn = False
|
| 278 |
+
_supports_flash_attn = False
|
| 279 |
+
_supports_flash_attn_2 = False
|
| 280 |
+
_supports_flash_attn_3 = False
|
| 281 |
+
_fastplms_attention_implementations = ("sdpa",)
|
| 282 |
+
|
| 283 |
+
@classmethod
|
| 284 |
+
def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs):
|
| 285 |
+
load_context = {key: kwargs[key] for key in _TOKENIZER_LOAD_CONTEXT_KEYS if key in kwargs}
|
| 286 |
+
if "token" not in load_context and "use_auth_token" in kwargs:
|
| 287 |
+
load_context["token"] = kwargs["use_auth_token"]
|
| 288 |
+
load_context["source"] = pretrained_model_name_or_path
|
| 289 |
+
|
| 290 |
+
loaded = super().from_pretrained(pretrained_model_name_or_path, *model_args, **kwargs)
|
| 291 |
+
model = loaded[0] if isinstance(loaded, tuple) else loaded
|
| 292 |
+
model.__dict__["_fastplms_tokenizer_load_context"] = load_context
|
| 293 |
+
model.__dict__["_fastplms_tokenizer"] = None
|
| 294 |
+
return loaded
|
| 295 |
+
|
| 296 |
+
@property
|
| 297 |
+
def tokenizer(self):
|
| 298 |
+
tokenizer = self.__dict__.get("_fastplms_tokenizer")
|
| 299 |
+
if tokenizer is None:
|
| 300 |
+
load_context = dict(self.__dict__.get("_fastplms_tokenizer_load_context") or {})
|
| 301 |
+
source = load_context.pop("source", None)
|
| 302 |
+
if source is None:
|
| 303 |
+
source = str(getattr(self.config, "_name_or_path", "")).strip()
|
| 304 |
+
if not source:
|
| 305 |
+
raise RuntimeError(
|
| 306 |
+
"DPLM2 tokenizer loading requires a model loaded with from_pretrained "
|
| 307 |
+
"so checkpoint provenance is available."
|
| 308 |
+
)
|
| 309 |
+
tokenizer_kwargs = {
|
| 310 |
+
key: value
|
| 311 |
+
for key, value in load_context.items()
|
| 312 |
+
if key in _TOKENIZER_LOAD_CONTEXT_KEYS and value is not None
|
| 313 |
+
}
|
| 314 |
+
resolved_revision = getattr(self.config, "_commit_hash", None)
|
| 315 |
+
if resolved_revision:
|
| 316 |
+
tokenizer_kwargs["revision"] = resolved_revision
|
| 317 |
+
tokenizer = DPLM2Tokenizer.from_pretrained(source, **tokenizer_kwargs)
|
| 318 |
+
self.__dict__["_fastplms_tokenizer"] = tokenizer
|
| 319 |
+
return tokenizer
|
| 320 |
+
|
| 321 |
+
@tokenizer.setter
|
| 322 |
+
def tokenizer(self, value) -> None:
|
| 323 |
+
self.__dict__["_fastplms_tokenizer"] = value
|
| 324 |
+
|
| 325 |
+
def _tokenize_sequence_batch(
|
| 326 |
+
self,
|
| 327 |
+
sequences: Sequence[str],
|
| 328 |
+
*,
|
| 329 |
+
tokenizer: Any | None = None,
|
| 330 |
+
**kwargs: Any,
|
| 331 |
+
) -> Any:
|
| 332 |
+
"""Tokenize raw amino-acid sequences with official DPLM2 boundaries."""
|
| 333 |
+
|
| 334 |
+
resolved = tokenizer if tokenizer is not None else self.tokenizer
|
| 335 |
+
sequence_list = [sequences] if isinstance(sequences, str) else sequences
|
| 336 |
+
formatted = [
|
| 337 |
+
f"{resolved.aa_cls_token}{sequence}{resolved.aa_eos_token}"
|
| 338 |
+
for sequence in sequence_list
|
| 339 |
+
]
|
| 340 |
+
return resolved(formatted, add_special_tokens=False, **kwargs)
|
| 341 |
+
|
| 342 |
+
@property
|
| 343 |
+
def attn_backend(self) -> str:
|
| 344 |
+
return self.config.attn_backend
|
| 345 |
+
|
| 346 |
+
@attn_backend.setter
|
| 347 |
+
def attn_backend(self, backend: str) -> None:
|
| 348 |
+
if backend not in self._fastplms_attention_implementations:
|
| 349 |
+
raise ValueError(
|
| 350 |
+
f"DPLM2 does not support {backend!r}; expected one of "
|
| 351 |
+
f"{self._fastplms_attention_implementations}."
|
| 352 |
+
)
|
| 353 |
+
self.config.attn_backend = backend
|
| 354 |
+
resolved = resolve_attention_backend(backend)
|
| 355 |
+
for module in self.modules():
|
| 356 |
+
if isinstance(module, ModifiedEsmEncoder):
|
| 357 |
+
module.attention_backend = resolved
|
| 358 |
+
elif isinstance(module, ModifiedEsmSelfAttention):
|
| 359 |
+
module.attn_backend = resolved
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
class ModifiedRotaryEmbedding(RotaryEmbedding):
|
| 363 |
+
def __init__(self, dim: int, aa_type: int, struct_type: int, pad_type: int):
|
| 364 |
+
super().__init__(dim)
|
| 365 |
+
self.aa_type = aa_type
|
| 366 |
+
self.struct_type = struct_type
|
| 367 |
+
self.pad_type = pad_type
|
| 368 |
+
|
| 369 |
+
def _has_multimodal_tokens(self, type_ids: torch.Tensor | None) -> bool:
|
| 370 |
+
# The split rotary path only works when the sequence tensor is already packed
|
| 371 |
+
# as two equal-length, modality-specific halves. Either track may come first.
|
| 372 |
+
# Plain protein batches can still contain high-ID special tokens, so mere
|
| 373 |
+
# modality presence is not enough.
|
| 374 |
+
return _has_packed_multimodal_layout(
|
| 375 |
+
type_ids=type_ids,
|
| 376 |
+
aa_type=self.aa_type,
|
| 377 |
+
struct_type=self.struct_type,
|
| 378 |
+
pad_type=self.pad_type,
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
def align_frequency_buffer(
|
| 382 |
+
self,
|
| 383 |
+
*,
|
| 384 |
+
device: torch.device,
|
| 385 |
+
dtype: torch.dtype,
|
| 386 |
+
) -> None:
|
| 387 |
+
"""Match the official model-wide ``to(device, dtype)`` conversion.
|
| 388 |
+
|
| 389 |
+
Transformers' meta-device loader converts parameters to the requested
|
| 390 |
+
dtype but can leave this persistent rotary buffer in FP32. The pinned
|
| 391 |
+
official implementation moves the complete module, including
|
| 392 |
+
``inv_freq``. Aligning the buffer before building rotary factors keeps
|
| 393 |
+
Q, K, and V in one dtype for every attention backend.
|
| 394 |
+
"""
|
| 395 |
+
|
| 396 |
+
if self.inv_freq.device == device and self.inv_freq.dtype == dtype:
|
| 397 |
+
return
|
| 398 |
+
self.inv_freq = self.inv_freq.to(device=device, dtype=dtype)
|
| 399 |
+
self._seq_len_cached = None
|
| 400 |
+
self._cos_cached = None
|
| 401 |
+
self._sin_cached = None
|
| 402 |
+
|
| 403 |
+
def _update_cos_sin_tables(
|
| 404 |
+
self,
|
| 405 |
+
x: torch.Tensor,
|
| 406 |
+
type_ids: torch.Tensor | None,
|
| 407 |
+
seq_dimension: int = 2,
|
| 408 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 409 |
+
seq_len = x.shape[seq_dimension]
|
| 410 |
+
if self._has_multimodal_tokens(type_ids):
|
| 411 |
+
seq_len = seq_len // 2
|
| 412 |
+
|
| 413 |
+
cache_is_stale = (
|
| 414 |
+
self._cos_cached is None
|
| 415 |
+
or self._sin_cached is None
|
| 416 |
+
or seq_len != self._seq_len_cached
|
| 417 |
+
or self._cos_cached.device != x.device
|
| 418 |
+
or self._cos_cached.dtype != self.inv_freq.dtype
|
| 419 |
+
)
|
| 420 |
+
if cache_is_stale:
|
| 421 |
+
self._seq_len_cached = seq_len
|
| 422 |
+
t = torch.arange(seq_len, device=x.device).type_as(self.inv_freq)
|
| 423 |
+
freqs = torch.outer(t, self.inv_freq)
|
| 424 |
+
# Match the official DPLM2 operation order: rotary factors inherit
|
| 425 |
+
# the frequency-buffer dtype. This keeps them in FP32 under BF16
|
| 426 |
+
# autocast, while a model explicitly converted to BF16 still builds
|
| 427 |
+
# BF16 factors and remains usable without autocast.
|
| 428 |
+
emb = torch.cat((freqs, freqs), dim=-1).to(device=x.device)
|
| 429 |
+
self._cos_cached = emb.cos()[None, None, :, :]
|
| 430 |
+
self._sin_cached = emb.sin()[None, None, :, :]
|
| 431 |
+
|
| 432 |
+
return self._cos_cached, self._sin_cached
|
| 433 |
+
|
| 434 |
+
def forward(
|
| 435 |
+
self,
|
| 436 |
+
q: torch.Tensor,
|
| 437 |
+
k: torch.Tensor,
|
| 438 |
+
type_ids: torch.Tensor | None,
|
| 439 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 440 |
+
self._cos_cached, self._sin_cached = self._update_cos_sin_tables(
|
| 441 |
+
k,
|
| 442 |
+
type_ids=type_ids,
|
| 443 |
+
seq_dimension=-2,
|
| 444 |
+
)
|
| 445 |
+
|
| 446 |
+
if self._has_multimodal_tokens(type_ids):
|
| 447 |
+
q_1, q_2 = q.chunk(2, dim=-2)
|
| 448 |
+
k_1, k_2 = k.chunk(2, dim=-2)
|
| 449 |
+
q_1 = apply_rotary_pos_emb(q_1, self._cos_cached, self._sin_cached)
|
| 450 |
+
q_2 = apply_rotary_pos_emb(q_2, self._cos_cached, self._sin_cached)
|
| 451 |
+
k_1 = apply_rotary_pos_emb(k_1, self._cos_cached, self._sin_cached)
|
| 452 |
+
k_2 = apply_rotary_pos_emb(k_2, self._cos_cached, self._sin_cached)
|
| 453 |
+
return torch.cat((q_1, q_2), dim=-2), torch.cat((k_1, k_2), dim=-2)
|
| 454 |
+
|
| 455 |
+
return (
|
| 456 |
+
apply_rotary_pos_emb(q, self._cos_cached, self._sin_cached),
|
| 457 |
+
apply_rotary_pos_emb(k, self._cos_cached, self._sin_cached),
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
class ModifiedEsmSelfAttention(EsmSelfAttention):
|
| 462 |
+
def __init__(self, config, position_embedding_type=None):
|
| 463 |
+
super().__init__(config, position_embedding_type)
|
| 464 |
+
self.config = config
|
| 465 |
+
self.scale = self.attention_head_size**-0.5
|
| 466 |
+
self.dropout_prob = config.attention_probs_dropout_prob
|
| 467 |
+
self.attn_backend = resolve_attention_backend(config.attn_backend)
|
| 468 |
+
self.rotary_embeddings = ModifiedRotaryEmbedding(
|
| 469 |
+
dim=self.attention_head_size,
|
| 470 |
+
aa_type=config.aa_type,
|
| 471 |
+
struct_type=config.struct_type,
|
| 472 |
+
pad_type=config.pad_type,
|
| 473 |
+
)
|
| 474 |
+
|
| 475 |
+
def forward(
|
| 476 |
+
self,
|
| 477 |
+
hidden_states: torch.Tensor,
|
| 478 |
+
attention_mask_4d: torch.Tensor | None = None,
|
| 479 |
+
output_attentions: bool = False,
|
| 480 |
+
output_s_max: bool = False,
|
| 481 |
+
type_ids: torch.Tensor | None = None,
|
| 482 |
+
effective_backend: AttentionBackend | None = None,
|
| 483 |
+
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
|
| 484 |
+
batch_size, seq_length = hidden_states.shape[:-1]
|
| 485 |
+
hidden_shape = (batch_size, seq_length, -1, self.attention_head_size)
|
| 486 |
+
query_heads = self.query(hidden_states).view(hidden_shape).transpose(1, 2)
|
| 487 |
+
key_heads = self.key(hidden_states).view(hidden_shape).transpose(1, 2)
|
| 488 |
+
value_heads = self.value(hidden_states).view(hidden_shape).transpose(1, 2)
|
| 489 |
+
|
| 490 |
+
query_heads = query_heads * self.scale
|
| 491 |
+
|
| 492 |
+
if self.position_embedding_type == "rotary":
|
| 493 |
+
self.rotary_embeddings.align_frequency_buffer(
|
| 494 |
+
device=query_heads.device,
|
| 495 |
+
dtype=self.query.weight.dtype,
|
| 496 |
+
)
|
| 497 |
+
query_heads, key_heads = self.rotary_embeddings(query_heads, key_heads, type_ids)
|
| 498 |
+
|
| 499 |
+
attn_output, attn_weights, s_max = self._attn(
|
| 500 |
+
query_heads,
|
| 501 |
+
key_heads,
|
| 502 |
+
value_heads,
|
| 503 |
+
attention_mask_4d=attention_mask_4d,
|
| 504 |
+
output_attentions=output_attentions,
|
| 505 |
+
output_s_max=output_s_max,
|
| 506 |
+
effective_backend=effective_backend,
|
| 507 |
+
)
|
| 508 |
+
return attn_output, attn_weights, s_max
|
| 509 |
+
|
| 510 |
+
def _attn(
|
| 511 |
+
self,
|
| 512 |
+
query_heads: torch.Tensor,
|
| 513 |
+
key_heads: torch.Tensor,
|
| 514 |
+
value_heads: torch.Tensor,
|
| 515 |
+
attention_mask_4d: torch.Tensor | None = None,
|
| 516 |
+
output_attentions: bool = False,
|
| 517 |
+
output_s_max: bool = False,
|
| 518 |
+
effective_backend: AttentionBackend | None = None,
|
| 519 |
+
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
|
| 520 |
+
if effective_backend is None:
|
| 521 |
+
effective_backend = resolve_attention_backend_for_call(
|
| 522 |
+
self.attn_backend,
|
| 523 |
+
output_attentions=output_attentions,
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
if effective_backend == AttentionBackend.EAGER:
|
| 527 |
+
attn_output, attn_weights, s_max = self._manual_attn(
|
| 528 |
+
query_heads, key_heads, value_heads, attention_mask_4d, output_s_max
|
| 529 |
+
)
|
| 530 |
+
return attn_output, attn_weights if output_attentions else None, s_max
|
| 531 |
+
|
| 532 |
+
if output_attentions:
|
| 533 |
+
raise AssertionError(
|
| 534 |
+
"DPLM2 output_attentions=True must resolve to eager attention for this call."
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
if effective_backend != AttentionBackend.SDPA:
|
| 538 |
+
raise AssertionError(f"Unsupported resolved backend: {effective_backend}")
|
| 539 |
+
attn_output, attn_weights = self._sdpa_attn(
|
| 540 |
+
query_heads,
|
| 541 |
+
key_heads,
|
| 542 |
+
value_heads,
|
| 543 |
+
attention_mask_4d,
|
| 544 |
+
)
|
| 545 |
+
|
| 546 |
+
s_max = self._compute_s_max(query_heads, key_heads) if output_s_max else None
|
| 547 |
+
return attn_output, attn_weights, s_max
|
| 548 |
+
|
| 549 |
+
@torch.no_grad()
|
| 550 |
+
def _compute_s_max(
|
| 551 |
+
self, query_heads: torch.Tensor, key_heads: torch.Tensor
|
| 552 |
+
) -> list[torch.Tensor]:
|
| 553 |
+
q_norm = torch.linalg.vector_norm(query_heads, dim=-1)
|
| 554 |
+
k_norm = torch.linalg.vector_norm(key_heads, dim=-1)
|
| 555 |
+
s_max_bound = (q_norm.max(dim=-1).values * k_norm.max(dim=-1).values).max(dim=0).values
|
| 556 |
+
return [s_max_bound[h] for h in range(self.num_attention_heads)]
|
| 557 |
+
|
| 558 |
+
def _manual_attn(
|
| 559 |
+
self,
|
| 560 |
+
query_heads: torch.Tensor,
|
| 561 |
+
key_heads: torch.Tensor,
|
| 562 |
+
value_heads: torch.Tensor,
|
| 563 |
+
attention_mask_4d: torch.Tensor | None = None,
|
| 564 |
+
output_s_max: bool = False,
|
| 565 |
+
) -> tuple[torch.Tensor, torch.Tensor, list[torch.Tensor] | None]:
|
| 566 |
+
attn_weights = torch.matmul(query_heads, key_heads.transpose(-1, -2))
|
| 567 |
+
if attention_mask_4d is not None:
|
| 568 |
+
attn_weights = attn_weights.masked_fill(attention_mask_4d.logical_not(), float("-inf"))
|
| 569 |
+
attn_weights = F.softmax(attn_weights, dim=-1)
|
| 570 |
+
if self.dropout_prob > 0 and self.training:
|
| 571 |
+
attn_weights = F.dropout(attn_weights, p=self.dropout_prob, training=self.training)
|
| 572 |
+
context_heads = torch.matmul(attn_weights, value_heads)
|
| 573 |
+
attn_output = rearrange(context_heads, "b h s d -> b s (h d)")
|
| 574 |
+
s_max = self._compute_s_max(query_heads, key_heads) if output_s_max else None
|
| 575 |
+
return attn_output, attn_weights, s_max
|
| 576 |
+
|
| 577 |
+
def _sdpa_attn(
|
| 578 |
+
self,
|
| 579 |
+
query_heads: torch.Tensor,
|
| 580 |
+
key_heads: torch.Tensor,
|
| 581 |
+
value_heads: torch.Tensor,
|
| 582 |
+
attention_mask_4d: torch.Tensor | None = None,
|
| 583 |
+
) -> tuple[torch.Tensor, None]:
|
| 584 |
+
# Pinned DPLM2 uses PyTorch's efficient SDPA kernel for its non-null
|
| 585 |
+
# padding mask. Newer PyTorch releases otherwise select cuDNN on H100,
|
| 586 |
+
# which exceeds the fixed deep-BF16 parity target. This is still the
|
| 587 |
+
# public SDPA operation and raises if its required CUDA kernel is absent.
|
| 588 |
+
kernel_context = (
|
| 589 |
+
sdpa_kernel(SDPBackend.EFFICIENT_ATTENTION)
|
| 590 |
+
if query_heads.is_cuda
|
| 591 |
+
else contextlib.nullcontext()
|
| 592 |
+
)
|
| 593 |
+
with kernel_context:
|
| 594 |
+
context_heads = F.scaled_dot_product_attention(
|
| 595 |
+
query_heads,
|
| 596 |
+
key_heads,
|
| 597 |
+
value_heads,
|
| 598 |
+
attn_mask=attention_mask_4d,
|
| 599 |
+
dropout_p=self.dropout_prob if self.training else 0.0,
|
| 600 |
+
scale=1.0,
|
| 601 |
+
)
|
| 602 |
+
return rearrange(context_heads, "b h s d -> b s (h d)"), None
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
class ModifiedEsmAttention(EsmAttention):
|
| 606 |
+
def __init__(self, config):
|
| 607 |
+
nn.Module.__init__(self)
|
| 608 |
+
self.self = ModifiedEsmSelfAttention(config)
|
| 609 |
+
self.output = EsmSelfOutput(config)
|
| 610 |
+
self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
|
| 611 |
+
|
| 612 |
+
def forward(
|
| 613 |
+
self,
|
| 614 |
+
hidden_states: torch.Tensor,
|
| 615 |
+
attention_mask_4d: torch.Tensor | None = None,
|
| 616 |
+
output_attentions: bool = False,
|
| 617 |
+
output_s_max: bool = False,
|
| 618 |
+
type_ids: torch.Tensor | None = None,
|
| 619 |
+
effective_backend: AttentionBackend | None = None,
|
| 620 |
+
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
|
| 621 |
+
hidden_states_ln = self.LayerNorm(hidden_states)
|
| 622 |
+
attn_output, attn_weights, s_max = self.self(
|
| 623 |
+
hidden_states_ln,
|
| 624 |
+
attention_mask_4d=attention_mask_4d,
|
| 625 |
+
output_attentions=output_attentions,
|
| 626 |
+
output_s_max=output_s_max,
|
| 627 |
+
type_ids=type_ids,
|
| 628 |
+
effective_backend=effective_backend,
|
| 629 |
+
)
|
| 630 |
+
attention_output = self.output(attn_output, hidden_states)
|
| 631 |
+
return attention_output, attn_weights, s_max
|
| 632 |
+
|
| 633 |
+
|
| 634 |
+
class ModifiedEsmLayer(EsmLayer):
|
| 635 |
+
def __init__(self, config):
|
| 636 |
+
nn.Module.__init__(self)
|
| 637 |
+
self.chunk_size_feed_forward = config.chunk_size_feed_forward
|
| 638 |
+
self.seq_len_dim = 1
|
| 639 |
+
self.attention = ModifiedEsmAttention(config)
|
| 640 |
+
self.intermediate = EsmIntermediate(config)
|
| 641 |
+
self.output = EsmOutput(config)
|
| 642 |
+
self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
|
| 643 |
+
|
| 644 |
+
def forward(
|
| 645 |
+
self,
|
| 646 |
+
hidden_states: torch.Tensor,
|
| 647 |
+
attention_mask_4d: torch.Tensor | None = None,
|
| 648 |
+
output_attentions: bool = False,
|
| 649 |
+
output_s_max: bool = False,
|
| 650 |
+
type_ids: torch.Tensor | None = None,
|
| 651 |
+
effective_backend: AttentionBackend | None = None,
|
| 652 |
+
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
|
| 653 |
+
attention_output, attn_weights, s_max = self.attention(
|
| 654 |
+
hidden_states,
|
| 655 |
+
attention_mask_4d=attention_mask_4d,
|
| 656 |
+
output_attentions=output_attentions,
|
| 657 |
+
output_s_max=output_s_max,
|
| 658 |
+
type_ids=type_ids,
|
| 659 |
+
effective_backend=effective_backend,
|
| 660 |
+
)
|
| 661 |
+
layer_output = self.feed_forward_chunk(attention_output)
|
| 662 |
+
return layer_output, attn_weights, s_max
|
| 663 |
+
|
| 664 |
+
|
| 665 |
+
class ModifiedEsmEncoder(EsmEncoder):
|
| 666 |
+
def __init__(self, config):
|
| 667 |
+
nn.Module.__init__(self)
|
| 668 |
+
self.config = config
|
| 669 |
+
self.attention_backend = resolve_attention_backend(config.attn_backend)
|
| 670 |
+
self.layer = nn.ModuleList(
|
| 671 |
+
[ModifiedEsmLayer(config) for _ in range(config.num_hidden_layers)]
|
| 672 |
+
)
|
| 673 |
+
self.emb_layer_norm_after = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
|
| 674 |
+
self.gradient_checkpointing = False
|
| 675 |
+
|
| 676 |
+
def forward(
|
| 677 |
+
self,
|
| 678 |
+
hidden_states: torch.Tensor,
|
| 679 |
+
attention_mask: torch.Tensor | None = None,
|
| 680 |
+
output_hidden_states: bool = False,
|
| 681 |
+
output_attentions: bool = False,
|
| 682 |
+
output_s_max: bool = False,
|
| 683 |
+
type_ids: torch.Tensor | None = None,
|
| 684 |
+
) -> DPLM2EncoderOutput:
|
| 685 |
+
first_parameter = next(self.parameters(), None)
|
| 686 |
+
if (
|
| 687 |
+
not self.training
|
| 688 |
+
and first_parameter is not None
|
| 689 |
+
and first_parameter.dtype == torch.bfloat16
|
| 690 |
+
):
|
| 691 |
+
raise RuntimeError(
|
| 692 |
+
"DPLM2 BF16 inference requires FP32-resident parameters under "
|
| 693 |
+
"CUDA BF16 autocast; static BF16 parameters do not meet the "
|
| 694 |
+
"declared parity contract."
|
| 695 |
+
)
|
| 696 |
+
all_hidden_states = () if output_hidden_states else None
|
| 697 |
+
all_attentions = () if output_attentions else None
|
| 698 |
+
full_s_max = () if output_s_max else None
|
| 699 |
+
|
| 700 |
+
effective_backend = resolve_attention_backend_for_call(
|
| 701 |
+
self.attention_backend,
|
| 702 |
+
output_attentions=output_attentions,
|
| 703 |
+
)
|
| 704 |
+
_, attention_mask_4d, _ = get_attention_mask(
|
| 705 |
+
effective_backend=effective_backend,
|
| 706 |
+
batch_size=hidden_states.shape[0],
|
| 707 |
+
seq_len=hidden_states.shape[1],
|
| 708 |
+
device=hidden_states.device,
|
| 709 |
+
attention_mask=attention_mask,
|
| 710 |
+
dtype=hidden_states.dtype,
|
| 711 |
+
mask_semantics="padding",
|
| 712 |
+
)
|
| 713 |
+
|
| 714 |
+
for layer_module in self.layer:
|
| 715 |
+
if output_hidden_states:
|
| 716 |
+
all_hidden_states = (*all_hidden_states, hidden_states)
|
| 717 |
+
|
| 718 |
+
if self.gradient_checkpointing and self.training:
|
| 719 |
+
hidden_states, attn_weights, s_max = self._gradient_checkpointing_func(
|
| 720 |
+
layer_module.__call__,
|
| 721 |
+
hidden_states,
|
| 722 |
+
attention_mask_4d,
|
| 723 |
+
output_attentions,
|
| 724 |
+
output_s_max,
|
| 725 |
+
type_ids,
|
| 726 |
+
effective_backend,
|
| 727 |
+
)
|
| 728 |
+
else:
|
| 729 |
+
hidden_states, attn_weights, s_max = layer_module(
|
| 730 |
+
hidden_states,
|
| 731 |
+
attention_mask_4d=attention_mask_4d,
|
| 732 |
+
output_attentions=output_attentions,
|
| 733 |
+
output_s_max=output_s_max,
|
| 734 |
+
type_ids=type_ids,
|
| 735 |
+
effective_backend=effective_backend,
|
| 736 |
+
)
|
| 737 |
+
|
| 738 |
+
if all_attentions is not None:
|
| 739 |
+
all_attentions = (*all_attentions, attn_weights)
|
| 740 |
+
if full_s_max is not None:
|
| 741 |
+
full_s_max = (*full_s_max, s_max)
|
| 742 |
+
|
| 743 |
+
if self.emb_layer_norm_after:
|
| 744 |
+
hidden_states = self.emb_layer_norm_after(hidden_states)
|
| 745 |
+
|
| 746 |
+
if output_hidden_states:
|
| 747 |
+
all_hidden_states = (*all_hidden_states, hidden_states)
|
| 748 |
+
|
| 749 |
+
return DPLM2EncoderOutput(
|
| 750 |
+
last_hidden_state=hidden_states,
|
| 751 |
+
hidden_states=all_hidden_states,
|
| 752 |
+
attentions=all_attentions,
|
| 753 |
+
s_max=full_s_max,
|
| 754 |
+
)
|
| 755 |
+
|
| 756 |
+
|
| 757 |
+
class FAST_DPLM2_ENCODER(DPLM2PreTrainedModel, EmbeddingMixin):
|
| 758 |
+
"""Inner encoder class that holds the actual ESM-style weights (embeddings, encoder)
|
| 759 |
+
so that the weight keys are prefixed with 'esm.' in the outer DPLM2Model,
|
| 760 |
+
matching pretrained DPLM2 checkpoints."""
|
| 761 |
+
|
| 762 |
+
def __init__(self, config, **kwargs):
|
| 763 |
+
DPLM2PreTrainedModel.__init__(self, config, **kwargs)
|
| 764 |
+
self.config = config
|
| 765 |
+
self.embeddings = EsmEmbeddings(config)
|
| 766 |
+
self.encoder = ModifiedEsmEncoder(config)
|
| 767 |
+
self.contact_head = EsmContactPredictionHead(
|
| 768 |
+
in_features=config.num_hidden_layers * config.num_attention_heads,
|
| 769 |
+
bias=True,
|
| 770 |
+
)
|
| 771 |
+
self.post_init()
|
| 772 |
+
|
| 773 |
+
def get_input_embeddings(self) -> nn.Module:
|
| 774 |
+
return self.embeddings.word_embeddings
|
| 775 |
+
|
| 776 |
+
def set_input_embeddings(self, value):
|
| 777 |
+
self.embeddings.word_embeddings = value
|
| 778 |
+
|
| 779 |
+
def predict_contacts(
|
| 780 |
+
self,
|
| 781 |
+
input_ids: torch.Tensor,
|
| 782 |
+
attention_mask: torch.Tensor | None = None,
|
| 783 |
+
) -> torch.Tensor:
|
| 784 |
+
"""Predict residue contacts with the checkpoint's tied contact head."""
|
| 785 |
+
input_ids = _normalize_dplm2_input_ids(input_ids, self.config.vocab_size)
|
| 786 |
+
if attention_mask is None:
|
| 787 |
+
attention_mask = input_ids.ne(self.config.pad_token_id)
|
| 788 |
+
type_ids = self._get_modality_type(input_ids, attention_mask)
|
| 789 |
+
attentions = self(
|
| 790 |
+
input_ids=input_ids,
|
| 791 |
+
attention_mask=attention_mask,
|
| 792 |
+
type_ids=type_ids,
|
| 793 |
+
output_attentions=True,
|
| 794 |
+
).attentions
|
| 795 |
+
if attentions is None:
|
| 796 |
+
raise RuntimeError("DPLM2 did not return attention maps for contact prediction.")
|
| 797 |
+
# A is the layer/head attention tensor; M marks valid tokens.
|
| 798 |
+
attention_tensor = torch.stack(attentions, dim=1)
|
| 799 |
+
residue_mask = attention_mask.to(dtype=attention_tensor.dtype)
|
| 800 |
+
attention_tensor = (
|
| 801 |
+
attention_tensor
|
| 802 |
+
* residue_mask[:, None, None, :, None]
|
| 803 |
+
* residue_mask[:, None, None, None, :]
|
| 804 |
+
)
|
| 805 |
+
return self.contact_head(input_ids, attention_tensor)
|
| 806 |
+
|
| 807 |
+
def _get_modality_type(
|
| 808 |
+
self, input_ids: torch.Tensor, attention_mask: torch.Tensor
|
| 809 |
+
) -> torch.Tensor:
|
| 810 |
+
return _infer_modality_type(input_ids, attention_mask)
|
| 811 |
+
|
| 812 |
+
def _embed(
|
| 813 |
+
self,
|
| 814 |
+
input_ids: torch.Tensor,
|
| 815 |
+
attention_mask: torch.Tensor | None = None,
|
| 816 |
+
hidden_state_index: int = -1,
|
| 817 |
+
store_all_hidden_states: bool = False,
|
| 818 |
+
) -> torch.Tensor:
|
| 819 |
+
input_ids = _normalize_dplm2_input_ids(input_ids, self.config.vocab_size)
|
| 820 |
+
if attention_mask is None:
|
| 821 |
+
attention_mask = input_ids.ne(self.config.pad_token_id)
|
| 822 |
+
type_ids = _infer_modality_type(input_ids, attention_mask)
|
| 823 |
+
token_embedding_output = self.embeddings(input_ids, attention_mask=attention_mask)
|
| 824 |
+
output_hidden_states = store_all_hidden_states or hidden_state_index != -1
|
| 825 |
+
encoder_outputs = self.encoder(
|
| 826 |
+
token_embedding_output,
|
| 827 |
+
attention_mask=attention_mask,
|
| 828 |
+
output_hidden_states=output_hidden_states,
|
| 829 |
+
output_attentions=False,
|
| 830 |
+
type_ids=type_ids,
|
| 831 |
+
)
|
| 832 |
+
return select_hidden_state_embeddings(
|
| 833 |
+
encoder_outputs.last_hidden_state,
|
| 834 |
+
encoder_outputs.hidden_states,
|
| 835 |
+
hidden_state_index=hidden_state_index,
|
| 836 |
+
store_all_hidden_states=store_all_hidden_states,
|
| 837 |
+
)
|
| 838 |
+
|
| 839 |
+
def forward(
|
| 840 |
+
self,
|
| 841 |
+
input_ids: torch.Tensor | None = None,
|
| 842 |
+
attention_mask: torch.Tensor | None = None,
|
| 843 |
+
position_ids: torch.Tensor | None = None,
|
| 844 |
+
inputs_embeds: torch.Tensor | None = None,
|
| 845 |
+
output_attentions: bool | None = None,
|
| 846 |
+
output_hidden_states: bool | None = None,
|
| 847 |
+
output_s_max: bool | None = False,
|
| 848 |
+
return_dict: bool | None = None,
|
| 849 |
+
type_ids: torch.Tensor | None = None,
|
| 850 |
+
) -> tuple[torch.Tensor, ...] | DPLM2EncoderOutput:
|
| 851 |
+
_validate_dplm2_model_inputs(
|
| 852 |
+
input_ids=input_ids,
|
| 853 |
+
inputs_embeds=inputs_embeds,
|
| 854 |
+
attention_mask=attention_mask,
|
| 855 |
+
type_ids=type_ids,
|
| 856 |
+
hidden_size=self.config.hidden_size,
|
| 857 |
+
)
|
| 858 |
+
output_attentions = (
|
| 859 |
+
output_attentions if output_attentions is not None else self.config.output_attentions
|
| 860 |
+
)
|
| 861 |
+
output_hidden_states = (
|
| 862 |
+
output_hidden_states
|
| 863 |
+
if output_hidden_states is not None
|
| 864 |
+
else self.config.output_hidden_states
|
| 865 |
+
)
|
| 866 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 867 |
+
|
| 868 |
+
if input_ids is not None:
|
| 869 |
+
input_ids = _normalize_dplm2_input_ids(input_ids, self.config.vocab_size)
|
| 870 |
+
token_embedding_output = self.embeddings(
|
| 871 |
+
input_ids=input_ids,
|
| 872 |
+
position_ids=position_ids,
|
| 873 |
+
attention_mask=attention_mask,
|
| 874 |
+
inputs_embeds=inputs_embeds,
|
| 875 |
+
)
|
| 876 |
+
encoder_outputs = self.encoder(
|
| 877 |
+
token_embedding_output,
|
| 878 |
+
attention_mask=attention_mask,
|
| 879 |
+
output_hidden_states=output_hidden_states,
|
| 880 |
+
output_attentions=output_attentions,
|
| 881 |
+
output_s_max=output_s_max,
|
| 882 |
+
type_ids=type_ids,
|
| 883 |
+
)
|
| 884 |
+
|
| 885 |
+
result = DPLM2EncoderOutput(
|
| 886 |
+
last_hidden_state=encoder_outputs.last_hidden_state,
|
| 887 |
+
hidden_states=encoder_outputs.hidden_states,
|
| 888 |
+
attentions=encoder_outputs.attentions,
|
| 889 |
+
s_max=encoder_outputs.s_max,
|
| 890 |
+
)
|
| 891 |
+
if not return_dict:
|
| 892 |
+
return result.to_tuple()
|
| 893 |
+
return result
|
| 894 |
+
|
| 895 |
+
|
| 896 |
+
class DPLM2Model(DPLM2PreTrainedModel, EmbeddingMixin):
|
| 897 |
+
config_class = DPLM2Config
|
| 898 |
+
|
| 899 |
+
def __init__(self, config, add_pooling_layer: bool | None = None):
|
| 900 |
+
DPLM2PreTrainedModel.__init__(self, config)
|
| 901 |
+
self.config = config
|
| 902 |
+
self.esm = FAST_DPLM2_ENCODER(config)
|
| 903 |
+
if add_pooling_layer is None:
|
| 904 |
+
add_pooling_layer = config.add_pooling_layer
|
| 905 |
+
config.add_pooling_layer = bool(add_pooling_layer)
|
| 906 |
+
self.pooler = EsmPooler(config) if add_pooling_layer else None
|
| 907 |
+
self.post_init()
|
| 908 |
+
|
| 909 |
+
def get_input_embeddings(self) -> nn.Module:
|
| 910 |
+
return self.esm.embeddings.word_embeddings
|
| 911 |
+
|
| 912 |
+
def set_input_embeddings(self, value):
|
| 913 |
+
self.esm.embeddings.word_embeddings = value
|
| 914 |
+
|
| 915 |
+
def predict_contacts(
|
| 916 |
+
self,
|
| 917 |
+
input_ids: torch.Tensor,
|
| 918 |
+
attention_mask: torch.Tensor | None = None,
|
| 919 |
+
) -> torch.Tensor:
|
| 920 |
+
return self.esm.predict_contacts(input_ids, attention_mask)
|
| 921 |
+
|
| 922 |
+
def _embed(
|
| 923 |
+
self,
|
| 924 |
+
input_ids: torch.Tensor,
|
| 925 |
+
attention_mask: torch.Tensor | None = None,
|
| 926 |
+
hidden_state_index: int = -1,
|
| 927 |
+
store_all_hidden_states: bool = False,
|
| 928 |
+
) -> torch.Tensor:
|
| 929 |
+
return self.esm._embed(
|
| 930 |
+
input_ids,
|
| 931 |
+
attention_mask,
|
| 932 |
+
hidden_state_index=hidden_state_index,
|
| 933 |
+
store_all_hidden_states=store_all_hidden_states,
|
| 934 |
+
)
|
| 935 |
+
|
| 936 |
+
def forward(
|
| 937 |
+
self,
|
| 938 |
+
input_ids: torch.Tensor | None = None,
|
| 939 |
+
attention_mask: torch.Tensor | None = None,
|
| 940 |
+
position_ids: torch.Tensor | None = None,
|
| 941 |
+
inputs_embeds: torch.Tensor | None = None,
|
| 942 |
+
output_attentions: bool | None = None,
|
| 943 |
+
output_hidden_states: bool | None = None,
|
| 944 |
+
output_s_max: bool | None = False,
|
| 945 |
+
return_dict: bool | None = None,
|
| 946 |
+
type_ids: torch.Tensor | None = None,
|
| 947 |
+
) -> tuple[torch.Tensor, ...] | DPLM2ModelOutput:
|
| 948 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 949 |
+
direct_dplm_esm = getattr(self.config, "dplm_type", None) == "dplm_esm"
|
| 950 |
+
_validate_dplm2_model_inputs(
|
| 951 |
+
input_ids=input_ids,
|
| 952 |
+
inputs_embeds=inputs_embeds,
|
| 953 |
+
attention_mask=attention_mask,
|
| 954 |
+
type_ids=type_ids,
|
| 955 |
+
hidden_size=self.config.hidden_size,
|
| 956 |
+
)
|
| 957 |
+
if inputs_embeds is not None and type_ids is None and not direct_dplm_esm:
|
| 958 |
+
raise ValueError(
|
| 959 |
+
"type_ids is required for multimodal DPLM2 calls that use inputs_embeds."
|
| 960 |
+
)
|
| 961 |
+
if input_ids is not None:
|
| 962 |
+
normalized_input_ids = _normalize_dplm2_input_ids(input_ids, self.config.vocab_size)
|
| 963 |
+
if attention_mask is None:
|
| 964 |
+
attention_mask = normalized_input_ids.ne(self.config.pad_token_id)
|
| 965 |
+
if type_ids is None and not direct_dplm_esm:
|
| 966 |
+
type_ids = _infer_modality_type(normalized_input_ids, attention_mask)
|
| 967 |
+
input_ids = normalized_input_ids
|
| 968 |
+
|
| 969 |
+
outputs = self.esm(
|
| 970 |
+
input_ids=input_ids,
|
| 971 |
+
attention_mask=attention_mask,
|
| 972 |
+
position_ids=position_ids,
|
| 973 |
+
inputs_embeds=inputs_embeds,
|
| 974 |
+
output_attentions=output_attentions,
|
| 975 |
+
output_hidden_states=output_hidden_states,
|
| 976 |
+
output_s_max=output_s_max,
|
| 977 |
+
return_dict=True,
|
| 978 |
+
type_ids=type_ids,
|
| 979 |
+
)
|
| 980 |
+
sequence_output = outputs.last_hidden_state
|
| 981 |
+
pooled_output = self.pooler(sequence_output) if self.pooler is not None else None
|
| 982 |
+
|
| 983 |
+
result = DPLM2ModelOutput(
|
| 984 |
+
last_hidden_state=sequence_output,
|
| 985 |
+
pooler_output=pooled_output,
|
| 986 |
+
hidden_states=outputs.hidden_states,
|
| 987 |
+
attentions=outputs.attentions,
|
| 988 |
+
s_max=outputs.s_max,
|
| 989 |
+
)
|
| 990 |
+
return result if return_dict else result.to_tuple()
|
| 991 |
+
|
| 992 |
+
|
| 993 |
+
class DPLM2ForMaskedLM(FastPLMTestTimeTrainingMixin, DPLM2PreTrainedModel, EmbeddingMixin):
|
| 994 |
+
config_class = DPLM2Config
|
| 995 |
+
|
| 996 |
+
def __init__(
|
| 997 |
+
self,
|
| 998 |
+
config,
|
| 999 |
+
dropout: float | None = None,
|
| 1000 |
+
vocab_size: int | None = None,
|
| 1001 |
+
):
|
| 1002 |
+
if dropout is not None:
|
| 1003 |
+
config.hidden_dropout_prob = dropout
|
| 1004 |
+
config.tie_word_embeddings = False
|
| 1005 |
+
if vocab_size is not None:
|
| 1006 |
+
config.vocab_size = vocab_size
|
| 1007 |
+
DPLM2PreTrainedModel.__init__(self, config)
|
| 1008 |
+
self.esm = FAST_DPLM2_ENCODER(config)
|
| 1009 |
+
self.lm_head = EsmLMHead(config)
|
| 1010 |
+
self.loss_fct = nn.CrossEntropyLoss()
|
| 1011 |
+
self.post_init()
|
| 1012 |
+
self.pad_id = config.pad_token_id
|
| 1013 |
+
self.contact_head = None
|
| 1014 |
+
self.init_ttt({"lora_target_replace_module": "ModifiedEsmAttention"})
|
| 1015 |
+
|
| 1016 |
+
def get_input_embeddings(self) -> nn.Module:
|
| 1017 |
+
return self.esm.get_input_embeddings()
|
| 1018 |
+
|
| 1019 |
+
def set_input_embeddings(self, value: nn.Module) -> None:
|
| 1020 |
+
self.esm.set_input_embeddings(value)
|
| 1021 |
+
|
| 1022 |
+
def get_output_embeddings(self):
|
| 1023 |
+
return self.lm_head.decoder
|
| 1024 |
+
|
| 1025 |
+
def set_output_embeddings(self, new_embeddings):
|
| 1026 |
+
old_bias = self.lm_head.bias
|
| 1027 |
+
new_vocab_size = int(new_embeddings.out_features)
|
| 1028 |
+
if old_bias.shape[0] != new_vocab_size:
|
| 1029 |
+
resized_bias = old_bias.new_zeros(new_vocab_size)
|
| 1030 |
+
copy_length = min(old_bias.shape[0], new_vocab_size)
|
| 1031 |
+
with torch.no_grad():
|
| 1032 |
+
resized_bias[:copy_length].copy_(old_bias[:copy_length])
|
| 1033 |
+
self.lm_head.bias = nn.Parameter(resized_bias)
|
| 1034 |
+
# EsmLMHead.forward adds this standalone bias after the decoder. HF's
|
| 1035 |
+
# generic LM-head resizer may create a biased Linear, which would apply
|
| 1036 |
+
# the bias twice and introduce an undeclared shared tensor on save.
|
| 1037 |
+
new_embeddings.bias = None
|
| 1038 |
+
self.lm_head.decoder = new_embeddings
|
| 1039 |
+
|
| 1040 |
+
def generate(
|
| 1041 |
+
self,
|
| 1042 |
+
input_tokens: torch.Tensor,
|
| 1043 |
+
max_iter: int | None = None,
|
| 1044 |
+
temperature: float = 1.0,
|
| 1045 |
+
partial_masks: torch.Tensor | None = None,
|
| 1046 |
+
unmasking_strategy: str = "stochastic1.0",
|
| 1047 |
+
sampling_strategy: str = "annealing@2.0:0.1",
|
| 1048 |
+
show_progress: bool = False,
|
| 1049 |
+
**kwargs,
|
| 1050 |
+
) -> dict[str, torch.Tensor]:
|
| 1051 |
+
"""Generate packed sequence and structure tokens with DPLM2 diffusion.
|
| 1052 |
+
|
| 1053 |
+
``input_tokens`` is X with shape (b, l). Positions marked ``True`` in
|
| 1054 |
+
``partial_masks`` remain fixed. The returned mapping contains
|
| 1055 |
+
``output_tokens``, matching the official DPLM2 public API.
|
| 1056 |
+
"""
|
| 1057 |
+
|
| 1058 |
+
if kwargs:
|
| 1059 |
+
names = ", ".join(sorted(kwargs))
|
| 1060 |
+
raise TypeError(f"Unexpected DPLM2 generation arguments: {names}")
|
| 1061 |
+
return generate_dplm2(
|
| 1062 |
+
self,
|
| 1063 |
+
input_tokens,
|
| 1064 |
+
max_iter=max_iter,
|
| 1065 |
+
temperature=temperature,
|
| 1066 |
+
partial_masks=partial_masks,
|
| 1067 |
+
unmasking_strategy=unmasking_strategy,
|
| 1068 |
+
sampling_strategy=sampling_strategy,
|
| 1069 |
+
show_progress=show_progress,
|
| 1070 |
+
)
|
| 1071 |
+
|
| 1072 |
+
def predict_contacts(
|
| 1073 |
+
self,
|
| 1074 |
+
input_ids: torch.Tensor,
|
| 1075 |
+
attention_mask: torch.Tensor | None = None,
|
| 1076 |
+
) -> torch.Tensor:
|
| 1077 |
+
"""Return the official ESM contact head output from the encoder."""
|
| 1078 |
+
return self.esm.predict_contacts(input_ids, attention_mask)
|
| 1079 |
+
|
| 1080 |
+
def _get_modality_type(
|
| 1081 |
+
self, input_ids: torch.Tensor, attention_mask: torch.Tensor
|
| 1082 |
+
) -> torch.Tensor:
|
| 1083 |
+
input_ids = _normalize_dplm2_input_ids(input_ids, self.config.vocab_size)
|
| 1084 |
+
return _infer_modality_type(input_ids, attention_mask)
|
| 1085 |
+
|
| 1086 |
+
def _embed(
|
| 1087 |
+
self,
|
| 1088 |
+
input_ids: torch.Tensor,
|
| 1089 |
+
attention_mask: torch.Tensor | None = None,
|
| 1090 |
+
hidden_state_index: int = -1,
|
| 1091 |
+
store_all_hidden_states: bool = False,
|
| 1092 |
+
) -> torch.Tensor:
|
| 1093 |
+
if attention_mask is None:
|
| 1094 |
+
attention_mask = input_ids.ne(self.pad_id)
|
| 1095 |
+
type_ids = self._get_modality_type(input_ids, attention_mask)
|
| 1096 |
+
output_hidden_states = store_all_hidden_states or hidden_state_index != -1
|
| 1097 |
+
outputs = self.esm(
|
| 1098 |
+
input_ids=input_ids,
|
| 1099 |
+
attention_mask=attention_mask,
|
| 1100 |
+
type_ids=type_ids,
|
| 1101 |
+
output_attentions=False,
|
| 1102 |
+
output_hidden_states=output_hidden_states,
|
| 1103 |
+
return_dict=True,
|
| 1104 |
+
)
|
| 1105 |
+
return select_hidden_state_embeddings(
|
| 1106 |
+
outputs.last_hidden_state,
|
| 1107 |
+
outputs.hidden_states,
|
| 1108 |
+
hidden_state_index=hidden_state_index,
|
| 1109 |
+
store_all_hidden_states=store_all_hidden_states,
|
| 1110 |
+
)
|
| 1111 |
+
|
| 1112 |
+
def _ttt_get_trainable_modules(self) -> list[nn.Module]:
|
| 1113 |
+
return [self.esm]
|
| 1114 |
+
|
| 1115 |
+
def _ttt_tokenize(
|
| 1116 |
+
self,
|
| 1117 |
+
seq: str | list[str] | None = None,
|
| 1118 |
+
input_ids: torch.Tensor | None = None,
|
| 1119 |
+
**kwargs: Any,
|
| 1120 |
+
) -> torch.Tensor:
|
| 1121 |
+
del kwargs
|
| 1122 |
+
if input_ids is not None:
|
| 1123 |
+
return input_ids
|
| 1124 |
+
if seq is None:
|
| 1125 |
+
raise ValueError("Pass either seq or input_ids for TTT.")
|
| 1126 |
+
sequences = [seq] if isinstance(seq, str) else seq
|
| 1127 |
+
tokenized = self._tokenize_sequence_batch(
|
| 1128 |
+
sequences,
|
| 1129 |
+
return_tensors="pt",
|
| 1130 |
+
padding=True,
|
| 1131 |
+
)
|
| 1132 |
+
return tokenized["input_ids"]
|
| 1133 |
+
|
| 1134 |
+
def _ttt_mask_token(self) -> int:
|
| 1135 |
+
return int(self.tokenizer._token_to_id[self.tokenizer.aa_mask_token])
|
| 1136 |
+
|
| 1137 |
+
def _ttt_replacement_tokens(self, input_ids: torch.Tensor) -> torch.Tensor:
|
| 1138 |
+
tokenizer = self.tokenizer
|
| 1139 |
+
special_ids = set(tokenizer.all_special_ids)
|
| 1140 |
+
struct_boundary = int(tokenizer._token_to_id[tokenizer.struct_cls_token])
|
| 1141 |
+
residue_ids = []
|
| 1142 |
+
for residue in "ACDEFGHIKLMNPQRSTVWY":
|
| 1143 |
+
token_id = tokenizer._token_to_id.get(residue)
|
| 1144 |
+
if (
|
| 1145 |
+
isinstance(token_id, int)
|
| 1146 |
+
and 0 <= token_id < struct_boundary
|
| 1147 |
+
and token_id not in special_ids
|
| 1148 |
+
and token_id not in residue_ids
|
| 1149 |
+
):
|
| 1150 |
+
residue_ids.append(token_id)
|
| 1151 |
+
if not residue_ids:
|
| 1152 |
+
raise RuntimeError("DPLM2 TTT amino-acid replacement set is empty.")
|
| 1153 |
+
if len(residue_ids) != 20:
|
| 1154 |
+
raise RuntimeError(
|
| 1155 |
+
"DPLM2 TTT requires all 20 canonical amino-acid replacement tokens; "
|
| 1156 |
+
f"resolved {len(residue_ids)}."
|
| 1157 |
+
)
|
| 1158 |
+
return torch.tensor(residue_ids, device=input_ids.device, dtype=input_ids.dtype)
|
| 1159 |
+
|
| 1160 |
+
def forward(
|
| 1161 |
+
self,
|
| 1162 |
+
input_ids: torch.Tensor | None = None,
|
| 1163 |
+
attention_mask: torch.Tensor | None = None,
|
| 1164 |
+
type_ids: torch.Tensor | None = None,
|
| 1165 |
+
inputs_embeds: torch.Tensor | None = None,
|
| 1166 |
+
labels: torch.Tensor | None = None,
|
| 1167 |
+
output_attentions: bool | None = None,
|
| 1168 |
+
output_hidden_states: bool | None = None,
|
| 1169 |
+
output_s_max: bool | None = False,
|
| 1170 |
+
return_dict: bool | None = None,
|
| 1171 |
+
) -> tuple[torch.Tensor] | DPLM2MaskedLMOutput:
|
| 1172 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 1173 |
+
direct_dplm_esm = getattr(self.config, "dplm_type", None) == "dplm_esm"
|
| 1174 |
+
_validate_dplm2_model_inputs(
|
| 1175 |
+
input_ids=input_ids,
|
| 1176 |
+
inputs_embeds=inputs_embeds,
|
| 1177 |
+
attention_mask=attention_mask,
|
| 1178 |
+
type_ids=type_ids,
|
| 1179 |
+
hidden_size=self.config.hidden_size,
|
| 1180 |
+
)
|
| 1181 |
+
|
| 1182 |
+
if attention_mask is None:
|
| 1183 |
+
if input_ids is None:
|
| 1184 |
+
raise ValueError(
|
| 1185 |
+
"attention_mask is required when DPLM2 is called with inputs_embeds."
|
| 1186 |
+
)
|
| 1187 |
+
attention_mask = input_ids.ne(self.pad_id)
|
| 1188 |
+
|
| 1189 |
+
encoder_input_ids = input_ids
|
| 1190 |
+
if input_ids is not None:
|
| 1191 |
+
input_ids = _normalize_dplm2_input_ids(input_ids, self.config.vocab_size)
|
| 1192 |
+
encoder_input_ids = input_ids
|
| 1193 |
+
if type_ids is None and not direct_dplm_esm:
|
| 1194 |
+
if input_ids is None:
|
| 1195 |
+
raise ValueError(
|
| 1196 |
+
"type_ids is required for multimodal DPLM2 calls that use inputs_embeds."
|
| 1197 |
+
)
|
| 1198 |
+
type_ids = self._get_modality_type(input_ids, attention_mask)
|
| 1199 |
+
|
| 1200 |
+
if input_ids is not None and inputs_embeds is None and not direct_dplm_esm:
|
| 1201 |
+
# The official multimodal wrapper applies the embedding block
|
| 1202 |
+
# once before entering EsmForDPLM2. The inner ESM model then
|
| 1203 |
+
# applies it a second time using these intermediate embeddings.
|
| 1204 |
+
inputs_embeds = self.esm.embeddings(
|
| 1205 |
+
input_ids=input_ids,
|
| 1206 |
+
attention_mask=attention_mask,
|
| 1207 |
+
)
|
| 1208 |
+
encoder_input_ids = None
|
| 1209 |
+
|
| 1210 |
+
outputs = self.esm(
|
| 1211 |
+
input_ids=encoder_input_ids,
|
| 1212 |
+
inputs_embeds=inputs_embeds,
|
| 1213 |
+
attention_mask=attention_mask,
|
| 1214 |
+
output_attentions=output_attentions,
|
| 1215 |
+
output_hidden_states=output_hidden_states,
|
| 1216 |
+
output_s_max=output_s_max,
|
| 1217 |
+
return_dict=True,
|
| 1218 |
+
type_ids=type_ids,
|
| 1219 |
+
)
|
| 1220 |
+
|
| 1221 |
+
sequence_output = outputs.last_hidden_state
|
| 1222 |
+
logits = self.lm_head(sequence_output)
|
| 1223 |
+
loss = None
|
| 1224 |
+
if labels is not None:
|
| 1225 |
+
labels = _normalize_dplm2_input_ids(labels, self.config.vocab_size)
|
| 1226 |
+
labels = labels.to(logits.device)
|
| 1227 |
+
loss = self.loss_fct(logits.view(-1, self.config.vocab_size), labels.view(-1))
|
| 1228 |
+
|
| 1229 |
+
result = DPLM2MaskedLMOutput(
|
| 1230 |
+
loss=loss,
|
| 1231 |
+
logits=logits,
|
| 1232 |
+
hidden_states=outputs.hidden_states,
|
| 1233 |
+
attentions=outputs.attentions,
|
| 1234 |
+
s_max=outputs.s_max,
|
| 1235 |
+
last_hidden_state=sequence_output,
|
| 1236 |
+
)
|
| 1237 |
+
return result if return_dict else result.to_tuple()
|
| 1238 |
+
|
| 1239 |
+
|
| 1240 |
+
class DPLM2ForSequenceClassification(DPLM2PreTrainedModel, EmbeddingMixin):
|
| 1241 |
+
config_class = DPLM2Config
|
| 1242 |
+
|
| 1243 |
+
def __init__(self, config):
|
| 1244 |
+
DPLM2PreTrainedModel.__init__(self, config)
|
| 1245 |
+
self.num_labels = config.num_labels
|
| 1246 |
+
self.esm = FAST_DPLM2_ENCODER(config)
|
| 1247 |
+
self.classifier = EsmClassificationHead(config)
|
| 1248 |
+
self.mse = nn.MSELoss()
|
| 1249 |
+
self.ce = nn.CrossEntropyLoss()
|
| 1250 |
+
self.bce = nn.BCEWithLogitsLoss()
|
| 1251 |
+
self.post_init()
|
| 1252 |
+
|
| 1253 |
+
def get_input_embeddings(self) -> nn.Module:
|
| 1254 |
+
return self.esm.get_input_embeddings()
|
| 1255 |
+
|
| 1256 |
+
def set_input_embeddings(self, value: nn.Module) -> None:
|
| 1257 |
+
self.esm.set_input_embeddings(value)
|
| 1258 |
+
|
| 1259 |
+
def _embed(
|
| 1260 |
+
self,
|
| 1261 |
+
input_ids: torch.Tensor,
|
| 1262 |
+
attention_mask: torch.Tensor | None = None,
|
| 1263 |
+
hidden_state_index: int = -1,
|
| 1264 |
+
store_all_hidden_states: bool = False,
|
| 1265 |
+
) -> torch.Tensor:
|
| 1266 |
+
return self.esm._embed(
|
| 1267 |
+
input_ids,
|
| 1268 |
+
attention_mask,
|
| 1269 |
+
hidden_state_index=hidden_state_index,
|
| 1270 |
+
store_all_hidden_states=store_all_hidden_states,
|
| 1271 |
+
)
|
| 1272 |
+
|
| 1273 |
+
def forward(
|
| 1274 |
+
self,
|
| 1275 |
+
input_ids: torch.Tensor | None = None,
|
| 1276 |
+
attention_mask: torch.Tensor | None = None,
|
| 1277 |
+
type_ids: torch.Tensor | None = None,
|
| 1278 |
+
inputs_embeds: torch.Tensor | None = None,
|
| 1279 |
+
labels: torch.Tensor | None = None,
|
| 1280 |
+
output_attentions: bool | None = None,
|
| 1281 |
+
output_hidden_states: bool | None = None,
|
| 1282 |
+
output_s_max: bool | None = False,
|
| 1283 |
+
return_dict: bool | None = None,
|
| 1284 |
+
) -> tuple[torch.Tensor, ...] | DPLM2SequenceClassifierOutput:
|
| 1285 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 1286 |
+
direct_dplm_esm = getattr(self.config, "dplm_type", None) == "dplm_esm"
|
| 1287 |
+
_validate_dplm2_model_inputs(
|
| 1288 |
+
input_ids=input_ids,
|
| 1289 |
+
inputs_embeds=inputs_embeds,
|
| 1290 |
+
attention_mask=attention_mask,
|
| 1291 |
+
type_ids=type_ids,
|
| 1292 |
+
hidden_size=self.config.hidden_size,
|
| 1293 |
+
)
|
| 1294 |
+
if inputs_embeds is not None and type_ids is None and not direct_dplm_esm:
|
| 1295 |
+
raise ValueError(
|
| 1296 |
+
"type_ids is required for multimodal DPLM2 calls that use inputs_embeds."
|
| 1297 |
+
)
|
| 1298 |
+
if input_ids is not None:
|
| 1299 |
+
input_ids = _normalize_dplm2_input_ids(input_ids, self.config.vocab_size)
|
| 1300 |
+
if attention_mask is None:
|
| 1301 |
+
attention_mask = input_ids.ne(self.config.pad_token_id)
|
| 1302 |
+
if type_ids is None and input_ids is not None and not direct_dplm_esm:
|
| 1303 |
+
type_ids = _infer_modality_type(input_ids, attention_mask)
|
| 1304 |
+
|
| 1305 |
+
outputs = self.esm(
|
| 1306 |
+
input_ids=input_ids,
|
| 1307 |
+
attention_mask=attention_mask,
|
| 1308 |
+
type_ids=type_ids,
|
| 1309 |
+
inputs_embeds=inputs_embeds,
|
| 1310 |
+
output_attentions=output_attentions,
|
| 1311 |
+
output_hidden_states=output_hidden_states,
|
| 1312 |
+
output_s_max=output_s_max,
|
| 1313 |
+
return_dict=True,
|
| 1314 |
+
)
|
| 1315 |
+
sequence_output = outputs.last_hidden_state
|
| 1316 |
+
logits = self.classifier(sequence_output)
|
| 1317 |
+
|
| 1318 |
+
loss = None
|
| 1319 |
+
if labels is not None:
|
| 1320 |
+
labels = labels.to(logits.device)
|
| 1321 |
+
if self.config.problem_type is None:
|
| 1322 |
+
if self.num_labels == 1:
|
| 1323 |
+
self.config.problem_type = "regression"
|
| 1324 |
+
elif self.num_labels > 1 and (
|
| 1325 |
+
labels.dtype == torch.long or labels.dtype == torch.int
|
| 1326 |
+
):
|
| 1327 |
+
self.config.problem_type = "single_label_classification"
|
| 1328 |
+
else:
|
| 1329 |
+
self.config.problem_type = "multi_label_classification"
|
| 1330 |
+
|
| 1331 |
+
if self.config.problem_type == "regression":
|
| 1332 |
+
if self.num_labels == 1:
|
| 1333 |
+
loss = self.mse(logits.squeeze(), labels.squeeze())
|
| 1334 |
+
else:
|
| 1335 |
+
loss = self.mse(logits, labels)
|
| 1336 |
+
elif self.config.problem_type == "single_label_classification":
|
| 1337 |
+
loss = self.ce(logits.view(-1, self.num_labels), labels.view(-1))
|
| 1338 |
+
elif self.config.problem_type == "multi_label_classification":
|
| 1339 |
+
loss = self.bce(logits, labels)
|
| 1340 |
+
|
| 1341 |
+
result = DPLM2SequenceClassifierOutput(
|
| 1342 |
+
loss=loss,
|
| 1343 |
+
logits=logits,
|
| 1344 |
+
hidden_states=outputs.hidden_states,
|
| 1345 |
+
attentions=outputs.attentions,
|
| 1346 |
+
s_max=outputs.s_max,
|
| 1347 |
+
)
|
| 1348 |
+
return result if return_dict else result.to_tuple()
|
| 1349 |
+
|
| 1350 |
+
|
| 1351 |
+
class DPLM2ForTokenClassification(DPLM2PreTrainedModel, EmbeddingMixin):
|
| 1352 |
+
config_class = DPLM2Config
|
| 1353 |
+
|
| 1354 |
+
def __init__(self, config):
|
| 1355 |
+
DPLM2PreTrainedModel.__init__(self, config)
|
| 1356 |
+
self.num_labels = config.num_labels
|
| 1357 |
+
self.esm = FAST_DPLM2_ENCODER(config)
|
| 1358 |
+
self.dropout = nn.Dropout(config.hidden_dropout_prob)
|
| 1359 |
+
self.classifier = nn.Linear(config.hidden_size, config.num_labels)
|
| 1360 |
+
self.loss_fct = nn.CrossEntropyLoss()
|
| 1361 |
+
self.post_init()
|
| 1362 |
+
|
| 1363 |
+
def get_input_embeddings(self) -> nn.Module:
|
| 1364 |
+
return self.esm.get_input_embeddings()
|
| 1365 |
+
|
| 1366 |
+
def set_input_embeddings(self, value: nn.Module) -> None:
|
| 1367 |
+
self.esm.set_input_embeddings(value)
|
| 1368 |
+
|
| 1369 |
+
def _embed(
|
| 1370 |
+
self,
|
| 1371 |
+
input_ids: torch.Tensor,
|
| 1372 |
+
attention_mask: torch.Tensor | None = None,
|
| 1373 |
+
hidden_state_index: int = -1,
|
| 1374 |
+
store_all_hidden_states: bool = False,
|
| 1375 |
+
) -> torch.Tensor:
|
| 1376 |
+
return self.esm._embed(
|
| 1377 |
+
input_ids,
|
| 1378 |
+
attention_mask,
|
| 1379 |
+
hidden_state_index=hidden_state_index,
|
| 1380 |
+
store_all_hidden_states=store_all_hidden_states,
|
| 1381 |
+
)
|
| 1382 |
+
|
| 1383 |
+
def forward(
|
| 1384 |
+
self,
|
| 1385 |
+
input_ids: torch.Tensor | None = None,
|
| 1386 |
+
attention_mask: torch.Tensor | None = None,
|
| 1387 |
+
type_ids: torch.Tensor | None = None,
|
| 1388 |
+
inputs_embeds: torch.Tensor | None = None,
|
| 1389 |
+
labels: torch.Tensor | None = None,
|
| 1390 |
+
output_attentions: bool | None = None,
|
| 1391 |
+
output_hidden_states: bool | None = None,
|
| 1392 |
+
output_s_max: bool | None = False,
|
| 1393 |
+
return_dict: bool | None = None,
|
| 1394 |
+
) -> tuple[torch.Tensor, ...] | DPLM2TokenClassifierOutput:
|
| 1395 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 1396 |
+
direct_dplm_esm = getattr(self.config, "dplm_type", None) == "dplm_esm"
|
| 1397 |
+
_validate_dplm2_model_inputs(
|
| 1398 |
+
input_ids=input_ids,
|
| 1399 |
+
inputs_embeds=inputs_embeds,
|
| 1400 |
+
attention_mask=attention_mask,
|
| 1401 |
+
type_ids=type_ids,
|
| 1402 |
+
hidden_size=self.config.hidden_size,
|
| 1403 |
+
)
|
| 1404 |
+
if inputs_embeds is not None and type_ids is None and not direct_dplm_esm:
|
| 1405 |
+
raise ValueError(
|
| 1406 |
+
"type_ids is required for multimodal DPLM2 calls that use inputs_embeds."
|
| 1407 |
+
)
|
| 1408 |
+
if input_ids is not None:
|
| 1409 |
+
input_ids = _normalize_dplm2_input_ids(input_ids, self.config.vocab_size)
|
| 1410 |
+
if attention_mask is None:
|
| 1411 |
+
attention_mask = input_ids.ne(self.config.pad_token_id)
|
| 1412 |
+
if type_ids is None and input_ids is not None and not direct_dplm_esm:
|
| 1413 |
+
type_ids = _infer_modality_type(input_ids, attention_mask)
|
| 1414 |
+
|
| 1415 |
+
outputs = self.esm(
|
| 1416 |
+
input_ids=input_ids,
|
| 1417 |
+
attention_mask=attention_mask,
|
| 1418 |
+
type_ids=type_ids,
|
| 1419 |
+
inputs_embeds=inputs_embeds,
|
| 1420 |
+
output_attentions=output_attentions,
|
| 1421 |
+
output_hidden_states=output_hidden_states,
|
| 1422 |
+
output_s_max=output_s_max,
|
| 1423 |
+
return_dict=True,
|
| 1424 |
+
)
|
| 1425 |
+
sequence_output = self.dropout(outputs.last_hidden_state)
|
| 1426 |
+
logits = self.classifier(sequence_output)
|
| 1427 |
+
|
| 1428 |
+
loss = None
|
| 1429 |
+
if labels is not None:
|
| 1430 |
+
labels = labels.to(logits.device)
|
| 1431 |
+
loss = self.loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
|
| 1432 |
+
|
| 1433 |
+
result = DPLM2TokenClassifierOutput(
|
| 1434 |
+
loss=loss,
|
| 1435 |
+
logits=logits,
|
| 1436 |
+
hidden_states=outputs.hidden_states,
|
| 1437 |
+
attentions=outputs.attentions,
|
| 1438 |
+
s_max=outputs.s_max,
|
| 1439 |
+
)
|
| 1440 |
+
return result if return_dict else result.to_tuple()
|
| 1441 |
+
|
| 1442 |
+
|
| 1443 |
+
# Importing the DPLM2 model implementation makes its paired tokenizer visible
|
| 1444 |
+
# to AutoTokenizer. This is registration only; it performs no I/O or downloads.
|
| 1445 |
+
try:
|
| 1446 |
+
AutoTokenizer.register(
|
| 1447 |
+
DPLM2Config,
|
| 1448 |
+
tokenizer_class=DPLM2Tokenizer,
|
| 1449 |
+
exist_ok=True,
|
| 1450 |
+
)
|
| 1451 |
+
except TypeError:
|
| 1452 |
+
# Transformers 4.x used this name; 5.x prefers tokenizer_class.
|
| 1453 |
+
AutoTokenizer.register(
|
| 1454 |
+
DPLM2Config,
|
| 1455 |
+
slow_tokenizer_class=DPLM2Tokenizer,
|
| 1456 |
+
exist_ok=True,
|
| 1457 |
+
)
|
fastplms/models/dplm2/tokenization_dplm2.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Independent DPLM2 amino-acid and structure tokenizer.
|
| 2 |
+
|
| 3 |
+
DPLM2 stores two token tracks in one vocabulary. Amino-acid tokens use their
|
| 4 |
+
own boundary, unknown, and mask tokens, while structure codes use a separate
|
| 5 |
+
set. The generic ``cls_token`` and ``eos_token`` aliases are deliberately not
|
| 6 |
+
defined because a caller must choose the modality-specific boundaries.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from typing import ClassVar
|
| 12 |
+
|
| 13 |
+
from transformers import AddedToken, EsmTokenizer, PreTrainedTokenizer
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class DPLM2Tokenizer(EsmTokenizer):
|
| 17 |
+
"""Tokenize the official DPLM2 amino-acid and structure vocabulary.
|
| 18 |
+
|
| 19 |
+
Input text is split against the complete pinned vocabulary. Amino-acid
|
| 20 |
+
sequences may be passed as contiguous characters and structure sequences
|
| 21 |
+
as whitespace-separated four-digit codes. Callers constructing a model
|
| 22 |
+
input add ``aa_*`` or ``struct_*`` boundaries explicitly and then use
|
| 23 |
+
``add_special_tokens=False``. The output token IDs preserve the official
|
| 24 |
+
multimodal vocabulary exactly.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
SPECIAL_TOKENS_ATTRIBUTES: ClassVar[list[str]] = [
|
| 28 |
+
"aa_cls_token",
|
| 29 |
+
"aa_eos_token",
|
| 30 |
+
"aa_unk_token",
|
| 31 |
+
"aa_mask_token",
|
| 32 |
+
"struct_cls_token",
|
| 33 |
+
"struct_eos_token",
|
| 34 |
+
"struct_unk_token",
|
| 35 |
+
"struct_mask_token",
|
| 36 |
+
"pad_token",
|
| 37 |
+
]
|
| 38 |
+
# The official tokenizer exposes no generic sequence-boundary aliases.
|
| 39 |
+
# Keeping these attributes explicitly set to None preserves that public
|
| 40 |
+
# contract on Transformers v5, whose custom special-token lookup is strict.
|
| 41 |
+
bos_token: ClassVar[None] = None
|
| 42 |
+
cls_token: ClassVar[None] = None
|
| 43 |
+
eos_token: ClassVar[None] = None
|
| 44 |
+
mask_token: ClassVar[None] = None
|
| 45 |
+
sep_token: ClassVar[None] = None
|
| 46 |
+
unk_token: ClassVar[None] = None
|
| 47 |
+
bos_token_id: ClassVar[None] = None
|
| 48 |
+
cls_token_id: ClassVar[None] = None
|
| 49 |
+
eos_token_id: ClassVar[None] = None
|
| 50 |
+
mask_token_id: ClassVar[None] = None
|
| 51 |
+
sep_token_id: ClassVar[None] = None
|
| 52 |
+
unk_token_id: ClassVar[None] = None
|
| 53 |
+
|
| 54 |
+
def __init__(
|
| 55 |
+
self,
|
| 56 |
+
vocab_file: str,
|
| 57 |
+
aa_cls_token: str | AddedToken = "<cls_aa>",
|
| 58 |
+
aa_eos_token: str | AddedToken = "<eos_aa>",
|
| 59 |
+
aa_unk_token: str | AddedToken = "<unk_aa>",
|
| 60 |
+
aa_mask_token: str | AddedToken = "<mask_aa>",
|
| 61 |
+
struct_cls_token: str | AddedToken = "<cls_struct>",
|
| 62 |
+
struct_eos_token: str | AddedToken = "<eos_struct>",
|
| 63 |
+
struct_unk_token: str | AddedToken = "<unk_struct>",
|
| 64 |
+
struct_mask_token: str | AddedToken = "<mask_struct>",
|
| 65 |
+
pad_token: str | AddedToken = "<pad>",
|
| 66 |
+
**kwargs: object,
|
| 67 |
+
) -> None:
|
| 68 |
+
with open(vocab_file, encoding="utf-8") as handle:
|
| 69 |
+
self.all_tokens = [line.strip() for line in handle.read().splitlines()]
|
| 70 |
+
self._id_to_token = dict(enumerate(self.all_tokens))
|
| 71 |
+
self._token_to_id = {token: token_id for token_id, token in self._id_to_token.items()}
|
| 72 |
+
|
| 73 |
+
# EsmTokenizer would install generic ESM boundary aliases. DPLM2 has
|
| 74 |
+
# modality-specific boundaries instead, so initialize the common
|
| 75 |
+
# tokenizer base with only the nine official special-token fields.
|
| 76 |
+
PreTrainedTokenizer.__init__(
|
| 77 |
+
self,
|
| 78 |
+
aa_cls_token=aa_cls_token,
|
| 79 |
+
aa_eos_token=aa_eos_token,
|
| 80 |
+
aa_unk_token=aa_unk_token,
|
| 81 |
+
aa_mask_token=aa_mask_token,
|
| 82 |
+
struct_cls_token=struct_cls_token,
|
| 83 |
+
struct_eos_token=struct_eos_token,
|
| 84 |
+
struct_unk_token=struct_unk_token,
|
| 85 |
+
struct_mask_token=struct_mask_token,
|
| 86 |
+
pad_token=pad_token,
|
| 87 |
+
**kwargs,
|
| 88 |
+
)
|
| 89 |
+
self.unique_no_split_tokens = self.all_tokens
|
| 90 |
+
self._update_trie(self.unique_no_split_tokens)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
__all__ = ["DPLM2Tokenizer"]
|
fastplms/models/ttt.py
ADDED
|
@@ -0,0 +1,866 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 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
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class TTTConfig:
|
| 19 |
+
lr: float = 4e-4
|
| 20 |
+
steps: int = 30
|
| 21 |
+
ags: int = 16
|
| 22 |
+
batch_size: int = 2
|
| 23 |
+
mask_ratio: float = 0.15
|
| 24 |
+
crop_size: int = 1024
|
| 25 |
+
bert_leave_prob: float = 0.1
|
| 26 |
+
bert_replace_prob: float = 0.1
|
| 27 |
+
optimizer: str = "sgd"
|
| 28 |
+
momentum: float = 0.0
|
| 29 |
+
weight_decay: float = 0.0
|
| 30 |
+
seed: int | None = 0
|
| 31 |
+
lora_rank: int = 8
|
| 32 |
+
lora_alpha: float = 32.0
|
| 33 |
+
lora_target_replace_module: str | None = None
|
| 34 |
+
lora_target_modules: tuple[str, ...] | None = None
|
| 35 |
+
initial_state_reset: bool = True
|
| 36 |
+
automatic_best_state_reset: bool = False
|
| 37 |
+
eval_each_step: bool = False
|
| 38 |
+
gradient_clip: bool = False
|
| 39 |
+
gradient_clip_max_norm: float = 1.0
|
| 40 |
+
|
| 41 |
+
def __post_init__(self) -> None:
|
| 42 |
+
self.verify()
|
| 43 |
+
|
| 44 |
+
@classmethod
|
| 45 |
+
def from_kwargs(cls, **kwargs: T.Any) -> TTTConfig:
|
| 46 |
+
valid_names = {field.name for field in fields(cls)}
|
| 47 |
+
unknown_names = set(kwargs) - valid_names
|
| 48 |
+
if unknown_names:
|
| 49 |
+
raise ValueError(f"Unknown TTTConfig fields: {sorted(unknown_names)}")
|
| 50 |
+
# JSON has no tuple type. Normalize the serialized representation while
|
| 51 |
+
# keeping the public constructor and runtime overrides type-strict.
|
| 52 |
+
if isinstance(kwargs.get("lora_target_modules"), list):
|
| 53 |
+
kwargs["lora_target_modules"] = tuple(kwargs["lora_target_modules"])
|
| 54 |
+
return cls(**kwargs)
|
| 55 |
+
|
| 56 |
+
def merged(self, overrides: T.Mapping[str, T.Any] | TTTConfig | None) -> TTTConfig:
|
| 57 |
+
if overrides is None:
|
| 58 |
+
return self
|
| 59 |
+
if isinstance(overrides, TTTConfig):
|
| 60 |
+
return overrides
|
| 61 |
+
values = {field.name: self.__dict__[field.name] for field in fields(self)}
|
| 62 |
+
for name, value in overrides.items():
|
| 63 |
+
if name not in values:
|
| 64 |
+
raise ValueError(f"Unknown TTTConfig field: {name}")
|
| 65 |
+
values[name] = value
|
| 66 |
+
return TTTConfig(**values)
|
| 67 |
+
|
| 68 |
+
def to_dict(self) -> dict[str, T.Any]:
|
| 69 |
+
return asdict(self)
|
| 70 |
+
|
| 71 |
+
def verify(self) -> None:
|
| 72 |
+
numeric_fields = {
|
| 73 |
+
"lr": self.lr,
|
| 74 |
+
"mask_ratio": self.mask_ratio,
|
| 75 |
+
"lora_alpha": self.lora_alpha,
|
| 76 |
+
"bert_leave_prob": self.bert_leave_prob,
|
| 77 |
+
"bert_replace_prob": self.bert_replace_prob,
|
| 78 |
+
"gradient_clip_max_norm": self.gradient_clip_max_norm,
|
| 79 |
+
"momentum": self.momentum,
|
| 80 |
+
"weight_decay": self.weight_decay,
|
| 81 |
+
}
|
| 82 |
+
for name, value in numeric_fields.items():
|
| 83 |
+
if isinstance(value, bool) or not isinstance(value, numbers.Real):
|
| 84 |
+
raise TypeError(f"TTT {name} must be a real number.")
|
| 85 |
+
if not math.isfinite(float(value)):
|
| 86 |
+
raise ValueError(f"TTT {name} must be finite.")
|
| 87 |
+
|
| 88 |
+
integer_fields = {
|
| 89 |
+
"steps": self.steps,
|
| 90 |
+
"ags": self.ags,
|
| 91 |
+
"batch_size": self.batch_size,
|
| 92 |
+
"crop_size": self.crop_size,
|
| 93 |
+
"lora_rank": self.lora_rank,
|
| 94 |
+
}
|
| 95 |
+
for name, value in integer_fields.items():
|
| 96 |
+
if isinstance(value, bool) or not isinstance(value, int):
|
| 97 |
+
raise TypeError(f"TTT {name} must be an integer.")
|
| 98 |
+
|
| 99 |
+
if self.seed is not None and (
|
| 100 |
+
isinstance(self.seed, bool) or not isinstance(self.seed, int)
|
| 101 |
+
):
|
| 102 |
+
raise TypeError("TTT seed must be None or an integer.")
|
| 103 |
+
|
| 104 |
+
boolean_fields = {
|
| 105 |
+
"initial_state_reset": self.initial_state_reset,
|
| 106 |
+
"automatic_best_state_reset": self.automatic_best_state_reset,
|
| 107 |
+
"eval_each_step": self.eval_each_step,
|
| 108 |
+
"gradient_clip": self.gradient_clip,
|
| 109 |
+
}
|
| 110 |
+
for name, value in boolean_fields.items():
|
| 111 |
+
if type(value) is not bool:
|
| 112 |
+
raise TypeError(f"TTT {name} must be a boolean.")
|
| 113 |
+
|
| 114 |
+
if self.lr <= 0.0:
|
| 115 |
+
raise ValueError("TTT learning rate must be positive.")
|
| 116 |
+
if self.steps < 1:
|
| 117 |
+
raise ValueError("TTT steps must be >= 1.")
|
| 118 |
+
if self.ags < 1:
|
| 119 |
+
raise ValueError("TTT gradient accumulation steps must be >= 1.")
|
| 120 |
+
if self.batch_size < 1:
|
| 121 |
+
raise ValueError("TTT batch_size must be >= 1.")
|
| 122 |
+
if not 0.0 < self.mask_ratio <= 1.0:
|
| 123 |
+
raise ValueError("TTT mask_ratio must be in (0, 1].")
|
| 124 |
+
if self.crop_size < 1:
|
| 125 |
+
raise ValueError("TTT crop_size must be >= 1.")
|
| 126 |
+
if self.lora_rank < 1:
|
| 127 |
+
raise ValueError("TTT v1 is LoRA-only, so lora_rank must be >= 1.")
|
| 128 |
+
if self.lora_alpha <= 0.0:
|
| 129 |
+
raise ValueError("TTT lora_alpha must be positive.")
|
| 130 |
+
if not isinstance(self.optimizer, str):
|
| 131 |
+
raise TypeError("TTT optimizer must be a string.")
|
| 132 |
+
if self.optimizer not in {"adamw", "sgd"}:
|
| 133 |
+
raise ValueError("TTT optimizer must be 'adamw' or 'sgd'.")
|
| 134 |
+
if self.momentum < 0.0:
|
| 135 |
+
raise ValueError("TTT momentum must be non-negative.")
|
| 136 |
+
if self.weight_decay < 0.0:
|
| 137 |
+
raise ValueError("TTT weight_decay must be non-negative.")
|
| 138 |
+
if not 0.0 <= self.bert_leave_prob <= 1.0:
|
| 139 |
+
raise ValueError("bert_leave_prob must be in [0, 1].")
|
| 140 |
+
if not 0.0 <= self.bert_replace_prob <= 1.0:
|
| 141 |
+
raise ValueError("bert_replace_prob must be in [0, 1].")
|
| 142 |
+
if self.bert_leave_prob + self.bert_replace_prob > 1.0:
|
| 143 |
+
raise ValueError("bert_leave_prob + bert_replace_prob must be <= 1.")
|
| 144 |
+
if self.gradient_clip and self.gradient_clip_max_norm <= 0.0:
|
| 145 |
+
raise ValueError("gradient_clip_max_norm must be positive.")
|
| 146 |
+
if self.lora_target_replace_module is not None:
|
| 147 |
+
if not isinstance(self.lora_target_replace_module, str):
|
| 148 |
+
raise TypeError("lora_target_replace_module must be None or a string.")
|
| 149 |
+
if not self.lora_target_replace_module.strip():
|
| 150 |
+
raise ValueError("lora_target_replace_module must not be empty.")
|
| 151 |
+
if self.lora_target_modules is not None:
|
| 152 |
+
if not isinstance(self.lora_target_modules, tuple):
|
| 153 |
+
raise TypeError("lora_target_modules must be None or a tuple of strings.")
|
| 154 |
+
if not self.lora_target_modules:
|
| 155 |
+
raise ValueError("lora_target_modules must not be empty.")
|
| 156 |
+
if any(not isinstance(name, str) for name in self.lora_target_modules):
|
| 157 |
+
raise TypeError("lora_target_modules must contain only strings.")
|
| 158 |
+
if any(not name.strip() for name in self.lora_target_modules):
|
| 159 |
+
raise ValueError(
|
| 160 |
+
"lora_target_modules must contain only non-empty strings."
|
| 161 |
+
)
|
| 162 |
+
if len(set(self.lora_target_modules)) != len(self.lora_target_modules):
|
| 163 |
+
raise ValueError("lora_target_modules must not contain duplicates.")
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
class LoraInjectedLinear(nn.Module):
|
| 167 |
+
"""ProteinTTT-compatible low-rank adapter.
|
| 168 |
+
|
| 169 |
+
``alpha`` is the direct adapter-output multiplier used by the pinned
|
| 170 |
+
ProteinTTT ``inject_trainable_lora(..., scale=lora_alpha)`` contract. It
|
| 171 |
+
is intentionally not divided by ``rank`` as it would be in the common
|
| 172 |
+
PEFT LoRA convention.
|
| 173 |
+
"""
|
| 174 |
+
|
| 175 |
+
def __init__(
|
| 176 |
+
self,
|
| 177 |
+
linear: nn.Module,
|
| 178 |
+
rank: int,
|
| 179 |
+
alpha: float,
|
| 180 |
+
generator: torch.Generator | None = None,
|
| 181 |
+
) -> None:
|
| 182 |
+
super().__init__()
|
| 183 |
+
weight = linear._parameters.get("weight")
|
| 184 |
+
if not isinstance(weight, torch.Tensor):
|
| 185 |
+
raise TypeError("LoRA targets must expose a tensor weight parameter.")
|
| 186 |
+
if weight.ndim != 2:
|
| 187 |
+
raise ValueError("LoRA can only wrap 2D linear weights.")
|
| 188 |
+
self.linear = linear
|
| 189 |
+
self.linear.requires_grad_(False)
|
| 190 |
+
self.rank = rank
|
| 191 |
+
# ProteinTTT names this setting ``lora_alpha`` but passes it directly
|
| 192 |
+
# to cloneofsimo/lora's ``scale`` argument. Preserve that numerical
|
| 193 |
+
# contract for parity and for saved FastPLMs TTT configurations.
|
| 194 |
+
self.scale = alpha
|
| 195 |
+
in_features = weight.shape[1]
|
| 196 |
+
out_features = weight.shape[0]
|
| 197 |
+
# ``nn.Linear`` initializes from the process-global CPU generator. Preserve
|
| 198 |
+
# that state when TTT supplies its own generator so lazy adapter injection
|
| 199 |
+
# is reproducible without perturbing the caller's RNG stream.
|
| 200 |
+
with torch.random.fork_rng(devices=[], enabled=generator is not None):
|
| 201 |
+
self.lora_down = nn.Linear(in_features, rank, bias=False, dtype=torch.float32)
|
| 202 |
+
self.lora_up = nn.Linear(rank, out_features, bias=False, dtype=torch.float32)
|
| 203 |
+
nn.init.normal_(self.lora_down.weight, std=1.0 / rank, generator=generator)
|
| 204 |
+
nn.init.zeros_(self.lora_up.weight)
|
| 205 |
+
self.lora_down.to(device=weight.device)
|
| 206 |
+
self.lora_up.to(device=weight.device)
|
| 207 |
+
self.register_buffer(
|
| 208 |
+
"_ttt_initial_lora_down",
|
| 209 |
+
self.lora_down.weight.detach().clone(),
|
| 210 |
+
persistent=True,
|
| 211 |
+
)
|
| 212 |
+
self.register_buffer(
|
| 213 |
+
"_ttt_initial_lora_up",
|
| 214 |
+
self.lora_up.weight.detach().clone(),
|
| 215 |
+
persistent=True,
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
@property
|
| 219 |
+
def weight(self) -> torch.Tensor:
|
| 220 |
+
return self.linear._parameters["weight"]
|
| 221 |
+
|
| 222 |
+
@property
|
| 223 |
+
def bias(self) -> torch.Tensor | None:
|
| 224 |
+
return self.linear._parameters["bias"]
|
| 225 |
+
|
| 226 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 227 |
+
base = self.linear(x)
|
| 228 |
+
delta = self.lora_up(self.lora_down(x.to(dtype=torch.float32))) * self.scale
|
| 229 |
+
return base + delta.to(dtype=base.dtype)
|
| 230 |
+
|
| 231 |
+
def reset_lora_parameters(self) -> None:
|
| 232 |
+
with torch.no_grad():
|
| 233 |
+
self.lora_down.weight.copy_(self._ttt_initial_lora_down)
|
| 234 |
+
self.lora_up.weight.copy_(self._ttt_initial_lora_up)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
class FastPLMTestTimeTrainingMixin:
|
| 238 |
+
def init_ttt(self, ttt_config: TTTConfig | T.Mapping[str, T.Any] | None = None) -> None:
|
| 239 |
+
base_config = self.__dict__.get("_ttt_cfg")
|
| 240 |
+
if base_config is None:
|
| 241 |
+
base_config = TTTConfig()
|
| 242 |
+
if not isinstance(base_config, TTTConfig):
|
| 243 |
+
raise TypeError("Existing TTT configuration must be a TTTConfig instance.")
|
| 244 |
+
configured = base_config.merged(ttt_config)
|
| 245 |
+
serialized = getattr(getattr(self, "config", None), "fastplms_ttt", None)
|
| 246 |
+
serialized_initialized = False
|
| 247 |
+
if serialized is not None:
|
| 248 |
+
if not isinstance(serialized, T.Mapping):
|
| 249 |
+
raise ValueError("config.fastplms_ttt must be a mapping.")
|
| 250 |
+
version = serialized.get("version")
|
| 251 |
+
if version != _TTT_SERIALIZATION_VERSION:
|
| 252 |
+
raise ValueError(
|
| 253 |
+
"Unsupported FastPLMs TTT serialization version "
|
| 254 |
+
f"{version!r}; expected {_TTT_SERIALIZATION_VERSION}."
|
| 255 |
+
)
|
| 256 |
+
serialized_config = serialized.get("config")
|
| 257 |
+
if not isinstance(serialized_config, T.Mapping):
|
| 258 |
+
raise ValueError("Serialized FastPLMs TTT state is missing its config mapping.")
|
| 259 |
+
configured = TTTConfig.from_kwargs(**dict(serialized_config))
|
| 260 |
+
initialized_value = serialized.get("initialized", False)
|
| 261 |
+
if type(initialized_value) is not bool:
|
| 262 |
+
raise ValueError("Serialized FastPLMs TTT initialized flag must be a boolean.")
|
| 263 |
+
serialized_initialized = initialized_value
|
| 264 |
+
|
| 265 |
+
self._ttt_cfg = configured
|
| 266 |
+
self._ttt_cfg.verify()
|
| 267 |
+
self._ttt_initialized = False
|
| 268 |
+
if serialized_initialized:
|
| 269 |
+
self._ttt_inject_lora()
|
| 270 |
+
self._ttt_initialized = True
|
| 271 |
+
|
| 272 |
+
@property
|
| 273 |
+
def ttt_config(self) -> TTTConfig:
|
| 274 |
+
if "_ttt_cfg" not in self.__dict__:
|
| 275 |
+
self.init_ttt()
|
| 276 |
+
return self._ttt_cfg
|
| 277 |
+
|
| 278 |
+
def _ttt_get_trainable_modules(self) -> list[nn.Module]:
|
| 279 |
+
return [self]
|
| 280 |
+
|
| 281 |
+
def _ttt_get_frozen_modules(self) -> list[nn.Module]:
|
| 282 |
+
return []
|
| 283 |
+
|
| 284 |
+
def _ttt_tokenize(
|
| 285 |
+
self,
|
| 286 |
+
seq: str | list[str] | None = None,
|
| 287 |
+
input_ids: torch.Tensor | None = None,
|
| 288 |
+
**kwargs: T.Any,
|
| 289 |
+
) -> torch.Tensor | dict[str, torch.Tensor]:
|
| 290 |
+
del kwargs
|
| 291 |
+
if input_ids is not None:
|
| 292 |
+
return input_ids
|
| 293 |
+
if seq is None:
|
| 294 |
+
raise ValueError("Pass either seq or input_ids for TTT.")
|
| 295 |
+
tokenized = self.tokenizer(seq, return_tensors="pt", padding=True)
|
| 296 |
+
return tokenized["input_ids"]
|
| 297 |
+
|
| 298 |
+
def _ttt_mask_token(self) -> int:
|
| 299 |
+
return int(self.tokenizer.mask_token_id)
|
| 300 |
+
|
| 301 |
+
def _ttt_padding_token(self) -> int:
|
| 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)
|
| 308 |
+
unknown_id = getattr(tokenizer, "unk_token_id", None)
|
| 309 |
+
if unknown_id is not None:
|
| 310 |
+
special_ids.add(int(unknown_id))
|
| 311 |
+
|
| 312 |
+
vocab: T.Mapping[str, T.Any] = {}
|
| 313 |
+
get_vocab = getattr(tokenizer, "get_vocab", None)
|
| 314 |
+
if callable(get_vocab):
|
| 315 |
+
vocab = get_vocab()
|
| 316 |
+
elif isinstance(getattr(tokenizer, "vocab", None), T.Mapping):
|
| 317 |
+
vocab = tokenizer.vocab
|
| 318 |
+
elif isinstance(getattr(tokenizer, "_token_to_id", None), T.Mapping):
|
| 319 |
+
vocab = tokenizer._token_to_id
|
| 320 |
+
|
| 321 |
+
ids: list[int] = []
|
| 322 |
+
convert = getattr(tokenizer, "convert_tokens_to_ids", None)
|
| 323 |
+
for amino_acid in _STANDARD_AMINO_ACIDS:
|
| 324 |
+
token_id = convert(amino_acid) if callable(convert) else vocab.get(amino_acid)
|
| 325 |
+
if (
|
| 326 |
+
isinstance(token_id, int)
|
| 327 |
+
and 0 <= token_id < vocab_size
|
| 328 |
+
and token_id not in special_ids
|
| 329 |
+
and token_id not in ids
|
| 330 |
+
):
|
| 331 |
+
ids.append(token_id)
|
| 332 |
+
if not ids:
|
| 333 |
+
raise ValueError(
|
| 334 |
+
"TTT could not resolve any canonical amino-acid token IDs from the tokenizer; "
|
| 335 |
+
"refusing to sample arbitrary or reserved vocabulary entries."
|
| 336 |
+
)
|
| 337 |
+
return torch.tensor(ids, device=input_ids.device, dtype=input_ids.dtype)
|
| 338 |
+
|
| 339 |
+
def _ttt_predict_logits(
|
| 340 |
+
self,
|
| 341 |
+
batch: torch.Tensor | dict[str, torch.Tensor],
|
| 342 |
+
**kwargs: T.Any,
|
| 343 |
+
) -> torch.Tensor:
|
| 344 |
+
del kwargs
|
| 345 |
+
if isinstance(batch, dict):
|
| 346 |
+
output = self(**batch)
|
| 347 |
+
return output.logits
|
| 348 |
+
attention_mask = batch.ne(self._ttt_padding_token())
|
| 349 |
+
output = self(input_ids=batch, attention_mask=attention_mask)
|
| 350 |
+
return output.logits
|
| 351 |
+
|
| 352 |
+
def _ttt_eval_step(
|
| 353 |
+
self,
|
| 354 |
+
step: int,
|
| 355 |
+
loss: float,
|
| 356 |
+
seq: str | list[str] | None = None,
|
| 357 |
+
input_ids: torch.Tensor | None = None,
|
| 358 |
+
**kwargs: T.Any,
|
| 359 |
+
) -> tuple[dict[str, T.Any], float | None]:
|
| 360 |
+
del step, loss, seq, input_ids, kwargs
|
| 361 |
+
return {}, None
|
| 362 |
+
|
| 363 |
+
def _ttt_is_lora_target(
|
| 364 |
+
self,
|
| 365 |
+
name: str,
|
| 366 |
+
full_name: str,
|
| 367 |
+
module: nn.Module,
|
| 368 |
+
active: bool,
|
| 369 |
+
target_modules: tuple[str, ...] | None,
|
| 370 |
+
) -> bool:
|
| 371 |
+
if not active:
|
| 372 |
+
return False
|
| 373 |
+
if isinstance(module, LoraInjectedLinear):
|
| 374 |
+
return False
|
| 375 |
+
if (
|
| 376 |
+
target_modules is not None
|
| 377 |
+
and name not in target_modules
|
| 378 |
+
and full_name not in target_modules
|
| 379 |
+
):
|
| 380 |
+
return False
|
| 381 |
+
if isinstance(module, nn.Linear):
|
| 382 |
+
return True
|
| 383 |
+
if "weight" not in module._parameters:
|
| 384 |
+
return False
|
| 385 |
+
weight = module._parameters["weight"]
|
| 386 |
+
if weight is None or weight.ndim != 2:
|
| 387 |
+
return False
|
| 388 |
+
return "Linear" in module.__class__.__name__
|
| 389 |
+
|
| 390 |
+
def _ttt_inject_lora(self) -> int:
|
| 391 |
+
cfg = self.ttt_config
|
| 392 |
+
cfg.verify()
|
| 393 |
+
target_class = cfg.lora_target_replace_module
|
| 394 |
+
target_modules = cfg.lora_target_modules
|
| 395 |
+
wrapped = 0
|
| 396 |
+
generator = None
|
| 397 |
+
if cfg.seed is not None:
|
| 398 |
+
generator = torch.Generator(device="cpu")
|
| 399 |
+
generator.manual_seed(cfg.seed)
|
| 400 |
+
|
| 401 |
+
def inject(module: nn.Module, prefix: str, active: bool) -> None:
|
| 402 |
+
nonlocal wrapped
|
| 403 |
+
for name, child in list(module.named_children()):
|
| 404 |
+
full_name = f"{prefix}.{name}" if prefix else name
|
| 405 |
+
child_active = active
|
| 406 |
+
if target_class is not None:
|
| 407 |
+
child_active = active or child.__class__.__name__ == target_class
|
| 408 |
+
if self._ttt_is_lora_target(name, full_name, child, child_active, target_modules):
|
| 409 |
+
setattr(
|
| 410 |
+
module,
|
| 411 |
+
name,
|
| 412 |
+
LoraInjectedLinear(
|
| 413 |
+
child,
|
| 414 |
+
rank=cfg.lora_rank,
|
| 415 |
+
alpha=cfg.lora_alpha,
|
| 416 |
+
generator=generator,
|
| 417 |
+
),
|
| 418 |
+
)
|
| 419 |
+
wrapped += 1
|
| 420 |
+
continue
|
| 421 |
+
inject(child, full_name, child_active)
|
| 422 |
+
|
| 423 |
+
for trainable_module in self._ttt_get_trainable_modules():
|
| 424 |
+
inject(trainable_module, "", target_class is None)
|
| 425 |
+
if wrapped == 0:
|
| 426 |
+
raise ValueError("TTT LoRA injection did not find any target modules.")
|
| 427 |
+
return wrapped
|
| 428 |
+
|
| 429 |
+
def _ttt_lora_modules(self) -> list[LoraInjectedLinear]:
|
| 430 |
+
return [module for module in self.modules() if isinstance(module, LoraInjectedLinear)]
|
| 431 |
+
|
| 432 |
+
def _ttt_lora_parameters(self) -> list[nn.Parameter]:
|
| 433 |
+
params: list[nn.Parameter] = []
|
| 434 |
+
for module in self._ttt_lora_modules():
|
| 435 |
+
params.extend(module.lora_down.parameters())
|
| 436 |
+
params.extend(module.lora_up.parameters())
|
| 437 |
+
if not params:
|
| 438 |
+
raise RuntimeError("TTT has no LoRA parameters.")
|
| 439 |
+
return params
|
| 440 |
+
|
| 441 |
+
def _ttt_snapshot_lora_state(self) -> list[dict[str, torch.Tensor]]:
|
| 442 |
+
snapshot = []
|
| 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:
|
| 451 |
+
raise RuntimeError("TTT has no LoRA state to snapshot.")
|
| 452 |
+
return snapshot
|
| 453 |
+
|
| 454 |
+
def _ttt_restore_lora_state(self, state: list[dict[str, torch.Tensor]]) -> None:
|
| 455 |
+
modules = self._ttt_lora_modules()
|
| 456 |
+
if len(modules) != len(state):
|
| 457 |
+
raise RuntimeError("TTT LoRA state/module count mismatch.")
|
| 458 |
+
with torch.no_grad():
|
| 459 |
+
for module, module_state in zip(modules, state, strict=True):
|
| 460 |
+
module.lora_down.weight.copy_(module_state["lora_down.weight"])
|
| 461 |
+
module.lora_up.weight.copy_(module_state["lora_up.weight"])
|
| 462 |
+
|
| 463 |
+
def _ttt_ensure_initialized(self) -> None:
|
| 464 |
+
if "_ttt_cfg" not in self.__dict__:
|
| 465 |
+
self.init_ttt()
|
| 466 |
+
if self._ttt_initialized:
|
| 467 |
+
return
|
| 468 |
+
self._ttt_inject_lora()
|
| 469 |
+
self._ttt_initialized = True
|
| 470 |
+
|
| 471 |
+
def ttt_reset(self) -> None:
|
| 472 |
+
self._ttt_ensure_initialized()
|
| 473 |
+
for module in self._ttt_lora_modules():
|
| 474 |
+
module.reset_lora_parameters()
|
| 475 |
+
|
| 476 |
+
def _ttt_serialized_contract(self) -> dict[str, T.Any]:
|
| 477 |
+
return {
|
| 478 |
+
"version": _TTT_SERIALIZATION_VERSION,
|
| 479 |
+
"initialized": bool(self._ttt_initialized),
|
| 480 |
+
"config": self.ttt_config.to_dict(),
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
def save_pretrained(self, save_directory: T.Any, *args: T.Any, **kwargs: T.Any) -> T.Any:
|
| 484 |
+
"""Save initialized adapters, their reset baseline, and the TTT config.
|
| 485 |
+
|
| 486 |
+
Adapter injection changes the module tree, so the serialized config must
|
| 487 |
+
reconstruct that tree before Transformers loads the state dict. Models
|
| 488 |
+
whose own state-dict hooks omit their trainable TTT modules fail closed
|
| 489 |
+
instead of producing an artifact that cannot restore the adaptation.
|
| 490 |
+
"""
|
| 491 |
+
|
| 492 |
+
if self._ttt_initialized:
|
| 493 |
+
state_keys = set(self.state_dict())
|
| 494 |
+
missing_adapter_keys = [
|
| 495 |
+
name
|
| 496 |
+
for name, _ in self.named_parameters()
|
| 497 |
+
if ".lora_" in name and name not in state_keys
|
| 498 |
+
]
|
| 499 |
+
if missing_adapter_keys:
|
| 500 |
+
raise RuntimeError(
|
| 501 |
+
"This model attaches TTT adapters to transient modules that its "
|
| 502 |
+
"checkpoint excludes, so save_pretrained cannot persist the adapted "
|
| 503 |
+
"state safely. Reset the model or use a model-specific adapter export."
|
| 504 |
+
)
|
| 505 |
+
self.config.fastplms_ttt = self._ttt_serialized_contract()
|
| 506 |
+
return super().save_pretrained(save_directory, *args, **kwargs)
|
| 507 |
+
|
| 508 |
+
def _ttt_make_optimizer(self) -> torch.optim.Optimizer:
|
| 509 |
+
cfg = self.ttt_config
|
| 510 |
+
params = self._ttt_lora_parameters()
|
| 511 |
+
if cfg.optimizer == "sgd":
|
| 512 |
+
return torch.optim.SGD(
|
| 513 |
+
params,
|
| 514 |
+
lr=cfg.lr,
|
| 515 |
+
momentum=cfg.momentum,
|
| 516 |
+
weight_decay=cfg.weight_decay,
|
| 517 |
+
)
|
| 518 |
+
return torch.optim.AdamW(params, lr=cfg.lr, weight_decay=cfg.weight_decay)
|
| 519 |
+
|
| 520 |
+
def _ttt_to_device(
|
| 521 |
+
self,
|
| 522 |
+
batch: torch.Tensor | dict[str, torch.Tensor],
|
| 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,
|
| 539 |
+
batch: torch.Tensor | dict[str, torch.Tensor],
|
| 540 |
+
input_ids: torch.Tensor,
|
| 541 |
+
) -> torch.Tensor | dict[str, torch.Tensor]:
|
| 542 |
+
if isinstance(batch, dict):
|
| 543 |
+
updated = dict(batch)
|
| 544 |
+
updated["input_ids"] = input_ids
|
| 545 |
+
return updated
|
| 546 |
+
return input_ids
|
| 547 |
+
|
| 548 |
+
def _ttt_non_special_mask(self, input_ids: torch.Tensor) -> torch.Tensor:
|
| 549 |
+
residue_ids = self._ttt_replacement_tokens(input_ids)
|
| 550 |
+
return torch.isin(input_ids, residue_ids)
|
| 551 |
+
|
| 552 |
+
def _ttt_validate_tokenized_batch(
|
| 553 |
+
self,
|
| 554 |
+
batch: torch.Tensor | dict[str, torch.Tensor],
|
| 555 |
+
) -> None:
|
| 556 |
+
input_ids = self._ttt_input_ids_from_batch(batch)
|
| 557 |
+
if input_ids.ndim != 2 or input_ids.shape[0] == 0 or input_ids.shape[1] == 0:
|
| 558 |
+
raise ValueError(
|
| 559 |
+
"TTT input_ids must have non-empty shape (batch, sequence); got "
|
| 560 |
+
f"{tuple(input_ids.shape)}."
|
| 561 |
+
)
|
| 562 |
+
|
| 563 |
+
if str(getattr(self.config, "model_type", "")) == "dplm2":
|
| 564 |
+
tokenizer = self.tokenizer
|
| 565 |
+
token_to_id = getattr(tokenizer, "_token_to_id", {})
|
| 566 |
+
struct_cls_token = getattr(tokenizer, "struct_cls_token", None)
|
| 567 |
+
struct_boundary = token_to_id.get(struct_cls_token)
|
| 568 |
+
if struct_boundary is None:
|
| 569 |
+
raise ValueError(
|
| 570 |
+
"DPLM2 TTT could not resolve the structure-token boundary safely."
|
| 571 |
+
)
|
| 572 |
+
pad_token = self._ttt_padding_token()
|
| 573 |
+
generic_aa_special_ids = torch.tensor(
|
| 574 |
+
[int(self.config.vocab_size) + offset for offset in range(4)],
|
| 575 |
+
device=input_ids.device,
|
| 576 |
+
dtype=input_ids.dtype,
|
| 577 |
+
)
|
| 578 |
+
is_structure = input_ids.ge(int(struct_boundary)) & input_ids.ne(pad_token)
|
| 579 |
+
is_structure &= ~torch.isin(input_ids, generic_aa_special_ids)
|
| 580 |
+
if bool(is_structure.any()):
|
| 581 |
+
raise ValueError(
|
| 582 |
+
"DPLM2 TTT currently supports amino-acid-only inputs. Packed or "
|
| 583 |
+
"structure-token inputs require a modality-specific corruption objective."
|
| 584 |
+
)
|
| 585 |
+
|
| 586 |
+
if isinstance(batch, dict) and "type_ids" in batch:
|
| 587 |
+
type_ids = batch["type_ids"]
|
| 588 |
+
attention_mask = batch.get("attention_mask", input_ids.ne(pad_token)).bool()
|
| 589 |
+
if bool(((type_ids == int(self.config.struct_type)) & attention_mask).any()):
|
| 590 |
+
raise ValueError(
|
| 591 |
+
"DPLM2 TTT currently supports amino-acid-only inputs; structure "
|
| 592 |
+
"type_ids are not accepted."
|
| 593 |
+
)
|
| 594 |
+
|
| 595 |
+
if not bool(self._ttt_non_special_mask(input_ids).any()):
|
| 596 |
+
raise ValueError(
|
| 597 |
+
"TTT input contains no trainable biological residue tokens after excluding "
|
| 598 |
+
"padding, boundary, mask, and reserved tokens."
|
| 599 |
+
)
|
| 600 |
+
|
| 601 |
+
def _ttt_sample_crop(
|
| 602 |
+
self,
|
| 603 |
+
batch: torch.Tensor | dict[str, torch.Tensor],
|
| 604 |
+
generator: torch.Generator,
|
| 605 |
+
) -> torch.Tensor | dict[str, torch.Tensor]:
|
| 606 |
+
input_ids = self._ttt_input_ids_from_batch(batch)
|
| 607 |
+
cfg = self.ttt_config
|
| 608 |
+
if input_ids.shape[1] <= cfg.crop_size:
|
| 609 |
+
return batch
|
| 610 |
+
position_has_residue = self._ttt_non_special_mask(input_ids).any(dim=0).to(torch.int64)
|
| 611 |
+
prefix = F.pad(position_has_residue.cumsum(dim=0), (1, 0))
|
| 612 |
+
window_counts = prefix[cfg.crop_size :] - prefix[: -cfg.crop_size]
|
| 613 |
+
valid_starts = torch.where(window_counts > 0)[0]
|
| 614 |
+
if valid_starts.numel() == 0:
|
| 615 |
+
raise ValueError("TTT could not find a crop containing a biological residue token.")
|
| 616 |
+
selected = torch.randint(
|
| 617 |
+
valid_starts.numel(),
|
| 618 |
+
(1,),
|
| 619 |
+
generator=generator,
|
| 620 |
+
device=input_ids.device,
|
| 621 |
+
)
|
| 622 |
+
start = int(valid_starts[selected].item())
|
| 623 |
+
end = start + cfg.crop_size
|
| 624 |
+
if isinstance(batch, dict):
|
| 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,
|
| 636 |
+
tokenized: torch.Tensor | dict[str, torch.Tensor],
|
| 637 |
+
generator: torch.Generator,
|
| 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,
|
| 707 |
+
device=sampled_ids.device,
|
| 708 |
+
)
|
| 709 |
+
masked_ids[
|
| 710 |
+
chosen_positions[0][replace],
|
| 711 |
+
chosen_positions[1][replace],
|
| 712 |
+
] = replacement_tokens[replacement_idx]
|
| 713 |
+
|
| 714 |
+
return self._ttt_set_input_ids(sampled, masked_ids), labels
|
| 715 |
+
|
| 716 |
+
@contextlib.contextmanager
|
| 717 |
+
def _ttt_seed_scope(self, seed: int | None) -> T.Iterator[None]:
|
| 718 |
+
if seed is None:
|
| 719 |
+
yield
|
| 720 |
+
return
|
| 721 |
+
cuda_devices = sorted(
|
| 722 |
+
{
|
| 723 |
+
parameter.device.index
|
| 724 |
+
for parameter in self.parameters()
|
| 725 |
+
if parameter.device.type == "cuda" and parameter.device.index is not None
|
| 726 |
+
}
|
| 727 |
+
)
|
| 728 |
+
with torch.random.fork_rng(devices=cuda_devices):
|
| 729 |
+
torch.random.default_generator.manual_seed(seed)
|
| 730 |
+
for device_index in cuda_devices:
|
| 731 |
+
with torch.cuda.device(device_index):
|
| 732 |
+
torch.cuda.manual_seed(seed)
|
| 733 |
+
yield
|
| 734 |
+
|
| 735 |
+
def ttt(
|
| 736 |
+
self,
|
| 737 |
+
seq: str | list[str] | None = None,
|
| 738 |
+
input_ids: torch.Tensor | None = None,
|
| 739 |
+
ttt_config: TTTConfig | T.Mapping[str, T.Any] | None = None,
|
| 740 |
+
**kwargs: T.Any,
|
| 741 |
+
) -> dict[str, T.Any]:
|
| 742 |
+
if ttt_config is not None:
|
| 743 |
+
if "_ttt_initialized" in self.__dict__ and self._ttt_initialized:
|
| 744 |
+
next_cfg = self.ttt_config.merged(ttt_config)
|
| 745 |
+
current_cfg = self.ttt_config
|
| 746 |
+
if next_cfg.lora_rank != current_cfg.lora_rank:
|
| 747 |
+
raise ValueError(
|
| 748 |
+
"Changing lora_rank after TTT initialization is not supported."
|
| 749 |
+
)
|
| 750 |
+
if next_cfg.lora_alpha != current_cfg.lora_alpha:
|
| 751 |
+
raise ValueError(
|
| 752 |
+
"Changing lora_alpha after TTT initialization is not supported."
|
| 753 |
+
)
|
| 754 |
+
if (
|
| 755 |
+
next_cfg.lora_target_replace_module
|
| 756 |
+
!= current_cfg.lora_target_replace_module
|
| 757 |
+
):
|
| 758 |
+
raise ValueError(
|
| 759 |
+
"Changing LoRA target class after TTT initialization is not supported."
|
| 760 |
+
)
|
| 761 |
+
if next_cfg.lora_target_modules != current_cfg.lora_target_modules:
|
| 762 |
+
raise ValueError(
|
| 763 |
+
"Changing LoRA target modules after TTT initialization is not supported."
|
| 764 |
+
)
|
| 765 |
+
self._ttt_cfg = next_cfg
|
| 766 |
+
else:
|
| 767 |
+
# Family constructors preconfigure the attention class that may
|
| 768 |
+
# receive LoRA adapters. A first-call mapping changes only the
|
| 769 |
+
# requested fields; rebuilding from TTTConfig defaults here
|
| 770 |
+
# would erase that family target immediately before injection.
|
| 771 |
+
self._ttt_cfg = self.ttt_config.merged(ttt_config)
|
| 772 |
+
self._ttt_cfg.verify()
|
| 773 |
+
|
| 774 |
+
cfg = self.ttt_config
|
| 775 |
+
device = next(self.parameters()).device
|
| 776 |
+
tokenized = self._ttt_tokenize(seq=seq, input_ids=input_ids, **kwargs)
|
| 777 |
+
tokenized = self._ttt_to_device(tokenized, device)
|
| 778 |
+
self._ttt_validate_tokenized_batch(tokenized)
|
| 779 |
+
self._ttt_ensure_initialized()
|
| 780 |
+
if cfg.initial_state_reset:
|
| 781 |
+
self.ttt_reset()
|
| 782 |
+
|
| 783 |
+
generator_device = device if device.type == "cuda" else torch.device("cpu")
|
| 784 |
+
generator = torch.Generator(device=generator_device)
|
| 785 |
+
if cfg.seed is not None:
|
| 786 |
+
generator.manual_seed(cfg.seed)
|
| 787 |
+
|
| 788 |
+
module_modes = {module: module.training for module in self.modules()}
|
| 789 |
+
requires_grad = {param: param.requires_grad for param in self.parameters()}
|
| 790 |
+
losses: list[float] = []
|
| 791 |
+
step_metrics: list[dict[str, T.Any]] = []
|
| 792 |
+
best_state: list[dict[str, torch.Tensor]] | None = None
|
| 793 |
+
best_metric: float | None = None
|
| 794 |
+
best_step = 0
|
| 795 |
+
|
| 796 |
+
with self._ttt_seed_scope(cfg.seed):
|
| 797 |
+
try:
|
| 798 |
+
self.train()
|
| 799 |
+
for param in self.parameters():
|
| 800 |
+
param.requires_grad_(False)
|
| 801 |
+
for param in self._ttt_lora_parameters():
|
| 802 |
+
param.requires_grad_(True)
|
| 803 |
+
|
| 804 |
+
optimizer = self._ttt_make_optimizer()
|
| 805 |
+
optimizer.zero_grad(set_to_none=True)
|
| 806 |
+
total_micro_steps = cfg.steps * cfg.ags
|
| 807 |
+
for micro_step in range(total_micro_steps):
|
| 808 |
+
batch, labels = self._ttt_sample_batch(tokenized, generator)
|
| 809 |
+
if not bool(labels.ne(-100).any()):
|
| 810 |
+
raise RuntimeError(
|
| 811 |
+
"TTT produced an all-ignored label batch; refusing a NaN update."
|
| 812 |
+
)
|
| 813 |
+
logits = self._ttt_predict_logits(batch, **kwargs)
|
| 814 |
+
labels = labels.to(device=logits.device)
|
| 815 |
+
loss = F.cross_entropy(
|
| 816 |
+
logits.reshape(-1, logits.shape[-1]),
|
| 817 |
+
labels.reshape(-1),
|
| 818 |
+
ignore_index=-100,
|
| 819 |
+
)
|
| 820 |
+
if not bool(torch.isfinite(loss)):
|
| 821 |
+
raise FloatingPointError(
|
| 822 |
+
f"TTT loss is non-finite at micro-step {micro_step + 1}."
|
| 823 |
+
)
|
| 824 |
+
(loss / cfg.ags).backward()
|
| 825 |
+
if (micro_step + 1) % cfg.ags != 0:
|
| 826 |
+
continue
|
| 827 |
+
|
| 828 |
+
if cfg.gradient_clip:
|
| 829 |
+
torch.nn.utils.clip_grad_norm_(
|
| 830 |
+
self._ttt_lora_parameters(),
|
| 831 |
+
cfg.gradient_clip_max_norm,
|
| 832 |
+
)
|
| 833 |
+
optimizer.step()
|
| 834 |
+
optimizer.zero_grad(set_to_none=True)
|
| 835 |
+
step = (micro_step + 1) // cfg.ags
|
| 836 |
+
loss_value = float(loss.detach().item())
|
| 837 |
+
losses.append(loss_value)
|
| 838 |
+
if cfg.eval_each_step:
|
| 839 |
+
metrics, metric = self._ttt_eval_step(
|
| 840 |
+
step=step,
|
| 841 |
+
loss=loss_value,
|
| 842 |
+
seq=seq,
|
| 843 |
+
input_ids=input_ids,
|
| 844 |
+
**kwargs,
|
| 845 |
+
)
|
| 846 |
+
if len(metrics) > 0:
|
| 847 |
+
step_metrics.append(metrics)
|
| 848 |
+
if metric is not None and (best_metric is None or metric > best_metric):
|
| 849 |
+
best_metric = metric
|
| 850 |
+
best_step = step
|
| 851 |
+
best_state = self._ttt_snapshot_lora_state()
|
| 852 |
+
|
| 853 |
+
if cfg.automatic_best_state_reset and best_state is not None:
|
| 854 |
+
self._ttt_restore_lora_state(best_state)
|
| 855 |
+
finally:
|
| 856 |
+
for param, value in requires_grad.items():
|
| 857 |
+
param.requires_grad_(value)
|
| 858 |
+
for module, training in module_modes.items():
|
| 859 |
+
module.train(training)
|
| 860 |
+
|
| 861 |
+
return {
|
| 862 |
+
"losses": losses,
|
| 863 |
+
"step_metrics": step_metrics,
|
| 864 |
+
"best_step": best_step,
|
| 865 |
+
"best_metric": best_metric,
|
| 866 |
+
}
|
fastplms/registry.py
ADDED
|
@@ -0,0 +1,1486 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Typed access to the FastPLMs model and provenance manifest.
|
| 2 |
+
|
| 3 |
+
The registry is intentionally independent of Torch and Transformers. Tooling can
|
| 4 |
+
therefore inspect supported checkpoints, licenses, and reference sources without
|
| 5 |
+
initializing a model runtime or downloading any files.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import re
|
| 11 |
+
import tomllib
|
| 12 |
+
from collections.abc import Iterator, Mapping
|
| 13 |
+
from dataclasses import dataclass
|
| 14 |
+
from functools import lru_cache
|
| 15 |
+
from importlib import resources
|
| 16 |
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
| 17 |
+
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.]+")
|
| 24 |
+
_WINDOWS_INVALID_PATH_CHARACTERS = frozenset('<>:"|?*')
|
| 25 |
+
_WINDOWS_RESERVED_PATH_NAMES = frozenset(
|
| 26 |
+
{"AUX", "CON", "NUL", "PRN"}
|
| 27 |
+
| {f"COM{index}" for index in range(1, 10)}
|
| 28 |
+
| {f"LPT{index}" for index in range(1, 10)}
|
| 29 |
+
)
|
| 30 |
+
_REPOSITORY_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*$")
|
| 31 |
+
_REFERENCE_CONTAINER_RE = re.compile(r"^reference-[a-z0-9]+(?:-[a-z0-9]+)*$")
|
| 32 |
+
_REFERENCE_ADAPTER_RE = re.compile(
|
| 33 |
+
r"^tests\.parity\.support\.reference_adapters\.[a-z_][a-z0-9_]*$"
|
| 34 |
+
)
|
| 35 |
+
_DOCUMENTATION_FRAGMENT_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
| 36 |
+
_ALLOWED_ATTENTION = frozenset(
|
| 37 |
+
{"eager", "sdpa", "flex_attention", "flash_attention_2", "flash_attention_3"}
|
| 38 |
+
)
|
| 39 |
+
_ALLOWED_DTYPES = frozenset({"float32", "bfloat16"})
|
| 40 |
+
_ALLOWED_PRECISIONS = frozenset({"default", "auto", "fp32", "bf16", "fp8"})
|
| 41 |
+
_ALLOWED_BF16_EXECUTIONS = frozenset({"static_parameters", "fp32_parameters_autocast"})
|
| 42 |
+
HUB_LICENSE_IDENTIFIERS = frozenset({"mit", "apache-2.0", "cc-by-nc-sa-4.0", "other"})
|
| 43 |
+
_ALLOWED_TOKENIZER_MODES = frozenset({"tokenizer", "sequence", "structure"})
|
| 44 |
+
_ALLOWED_SIZE_CATEGORIES = frozenset({"small", "medium", "large", "xlarge", "structure"})
|
| 45 |
+
RuntimeExtra = Literal["core", "structure"]
|
| 46 |
+
TestTier = Literal["check", "compliance", "structure", "feature", "artifact", "benchmark"]
|
| 47 |
+
VramTier = Literal["sequence", "large-sequence", "structure", "structure-6b"]
|
| 48 |
+
GenerationContract = Literal["not_applicable", "required", "official_unavailable"]
|
| 49 |
+
RuntimeAssetTrustKind = Literal["hash_pinned_pickle"]
|
| 50 |
+
Bf16Execution = Literal["static_parameters", "fp32_parameters_autocast"]
|
| 51 |
+
DtypeName = Literal["float32", "bfloat16"]
|
| 52 |
+
_ALLOWED_EXTRAS = frozenset({"core", "structure"})
|
| 53 |
+
_ALLOWED_TEST_TIERS = frozenset(
|
| 54 |
+
{"check", "compliance", "structure", "feature", "artifact", "benchmark"}
|
| 55 |
+
)
|
| 56 |
+
_ALLOWED_VRAM_TIERS = frozenset({"sequence", "large-sequence", "structure", "structure-6b"})
|
| 57 |
+
_ALLOWED_GENERATION_CONTRACTS = frozenset({"not_applicable", "required", "official_unavailable"})
|
| 58 |
+
_ALLOWED_RUNTIME_ASSET_TRUST_KINDS = frozenset({"hash_pinned_pickle"})
|
| 59 |
+
_ALLOWED_RUNTIME_ASSET_OFFLINE_BEHAVIORS = frozenset({"requires_cached_verified_file"})
|
| 60 |
+
_ALLOWED_AUTO_CLASSES = frozenset(
|
| 61 |
+
{
|
| 62 |
+
"AutoConfig",
|
| 63 |
+
"AutoModel",
|
| 64 |
+
"AutoModelForMaskedLM",
|
| 65 |
+
"AutoModelForProteinFolding",
|
| 66 |
+
"AutoModelForSequenceClassification",
|
| 67 |
+
"AutoModelForSeq2SeqLM",
|
| 68 |
+
"AutoModelForTokenClassification",
|
| 69 |
+
}
|
| 70 |
+
)
|
| 71 |
+
_WEIGHT_SUFFIXES = (".bin", ".ckpt", ".pt", ".pth", ".safetensors")
|
| 72 |
+
_ALLOWED_ORACLE_ASSET_ROLES = frozenset({"weights", "contact_regression"})
|
| 73 |
+
_FAIR_ESM_ASSET_HOST = "dl.fbaipublicfiles.com"
|
| 74 |
+
_ROOT_FIELDS = frozenset(
|
| 75 |
+
{
|
| 76 |
+
"schema_version",
|
| 77 |
+
"legal_files",
|
| 78 |
+
"attention_kernels",
|
| 79 |
+
"upstreams",
|
| 80 |
+
"families",
|
| 81 |
+
"models",
|
| 82 |
+
"runtime_assets",
|
| 83 |
+
}
|
| 84 |
+
)
|
| 85 |
+
_UPSTREAM_FIELDS = frozenset(
|
| 86 |
+
{
|
| 87 |
+
"id",
|
| 88 |
+
"path",
|
| 89 |
+
"url",
|
| 90 |
+
"revision",
|
| 91 |
+
"license",
|
| 92 |
+
"license_files",
|
| 93 |
+
"license_digests",
|
| 94 |
+
"distribution_files",
|
| 95 |
+
}
|
| 96 |
+
)
|
| 97 |
+
_FAMILY_FIELDS = frozenset(
|
| 98 |
+
{
|
| 99 |
+
"architecture",
|
| 100 |
+
"upstreams",
|
| 101 |
+
"tokenizer_mode",
|
| 102 |
+
"public_input",
|
| 103 |
+
"extra",
|
| 104 |
+
"reference_container",
|
| 105 |
+
"reference_adapter",
|
| 106 |
+
"attention",
|
| 107 |
+
"dtypes",
|
| 108 |
+
"bf16_execution",
|
| 109 |
+
"precisions",
|
| 110 |
+
"experimental_precisions",
|
| 111 |
+
"vram_tier",
|
| 112 |
+
"checkpoint_license",
|
| 113 |
+
"hub_license",
|
| 114 |
+
"hub_license_name",
|
| 115 |
+
"hub_license_link",
|
| 116 |
+
"state_transform",
|
| 117 |
+
"conversion_provenance",
|
| 118 |
+
"representative",
|
| 119 |
+
"documentation",
|
| 120 |
+
"test_tiers",
|
| 121 |
+
"runtime_paths",
|
| 122 |
+
"requires_complete_weight_publication",
|
| 123 |
+
"weights_publication_allowed",
|
| 124 |
+
"auto_map",
|
| 125 |
+
"tokenizer_class",
|
| 126 |
+
"backbone_model",
|
| 127 |
+
}
|
| 128 |
+
)
|
| 129 |
+
_MODEL_FIELDS = frozenset(
|
| 130 |
+
{
|
| 131 |
+
"id",
|
| 132 |
+
"family",
|
| 133 |
+
"size_category",
|
| 134 |
+
"generation_contract",
|
| 135 |
+
"fast_repo",
|
| 136 |
+
"fast_revision",
|
| 137 |
+
"fast_files",
|
| 138 |
+
"fast_unresolved_files",
|
| 139 |
+
"official_repo",
|
| 140 |
+
"official_revision",
|
| 141 |
+
"official_files",
|
| 142 |
+
"official_unresolved_files",
|
| 143 |
+
"oracle_assets",
|
| 144 |
+
"official_golden",
|
| 145 |
+
"artifact_source",
|
| 146 |
+
"canonical_state_sha256",
|
| 147 |
+
"tokenizer_source",
|
| 148 |
+
"auto_map",
|
| 149 |
+
"notes",
|
| 150 |
+
"msa_conditioning",
|
| 151 |
+
}
|
| 152 |
+
)
|
| 153 |
+
_RUNTIME_ASSET_FIELDS = frozenset(
|
| 154 |
+
{
|
| 155 |
+
"id",
|
| 156 |
+
"repository",
|
| 157 |
+
"revision",
|
| 158 |
+
"path",
|
| 159 |
+
"sha256",
|
| 160 |
+
"size",
|
| 161 |
+
"consumer_family",
|
| 162 |
+
"trust_kind",
|
| 163 |
+
"license",
|
| 164 |
+
"offline_behavior",
|
| 165 |
+
}
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
class RegistryError(ValueError):
|
| 170 |
+
"""Raised when the model manifest is incomplete or internally inconsistent."""
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def _portable_relative_path(value: str, context: str) -> PurePosixPath:
|
| 174 |
+
"""Return one normalized cross-platform relative path or fail closed."""
|
| 175 |
+
|
| 176 |
+
posix = PurePosixPath(value)
|
| 177 |
+
windows = PureWindowsPath(value)
|
| 178 |
+
unsafe_windows_part = any(
|
| 179 |
+
part.rstrip(" .") != part
|
| 180 |
+
or part.split(".", maxsplit=1)[0].upper() in _WINDOWS_RESERVED_PATH_NAMES
|
| 181 |
+
or any(
|
| 182 |
+
ord(character) < 32 or character in _WINDOWS_INVALID_PATH_CHARACTERS
|
| 183 |
+
for character in part
|
| 184 |
+
)
|
| 185 |
+
for part in posix.parts
|
| 186 |
+
)
|
| 187 |
+
if (
|
| 188 |
+
not value
|
| 189 |
+
or not posix.parts
|
| 190 |
+
or posix == PurePosixPath(".")
|
| 191 |
+
or posix.is_absolute()
|
| 192 |
+
or windows.is_absolute()
|
| 193 |
+
or windows.drive
|
| 194 |
+
or "\\" in value
|
| 195 |
+
or "." in posix.parts
|
| 196 |
+
or ".." in posix.parts
|
| 197 |
+
or value != posix.as_posix()
|
| 198 |
+
or any(
|
| 199 |
+
part.lower() in {".git", ".cache", "__pycache__"}
|
| 200 |
+
for part in posix.parts
|
| 201 |
+
)
|
| 202 |
+
or unsafe_windows_part
|
| 203 |
+
):
|
| 204 |
+
raise RegistryError(f"{context} is not portable: {value!r}")
|
| 205 |
+
return posix
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
@dataclass(frozen=True, slots=True)
|
| 209 |
+
class FileDigest:
|
| 210 |
+
"""Expected content identity for one pinned file."""
|
| 211 |
+
|
| 212 |
+
path: str
|
| 213 |
+
algorithm: str
|
| 214 |
+
digest: str
|
| 215 |
+
|
| 216 |
+
@classmethod
|
| 217 |
+
def parse(cls, value: str) -> FileDigest:
|
| 218 |
+
try:
|
| 219 |
+
path, encoded_digest = value.split("=", maxsplit=1)
|
| 220 |
+
algorithm, digest = encoded_digest.split(":", maxsplit=1)
|
| 221 |
+
except ValueError as error:
|
| 222 |
+
raise RegistryError("File digests must use '<path>=<algorithm>:<digest>'.") from error
|
| 223 |
+
|
| 224 |
+
_portable_relative_path(path, "Checkpoint file path")
|
| 225 |
+
|
| 226 |
+
expected_length = {"git-sha1": 40, "sha256": 64}.get(algorithm)
|
| 227 |
+
if expected_length is None:
|
| 228 |
+
raise RegistryError(f"Unsupported file digest algorithm: {algorithm!r}")
|
| 229 |
+
if len(digest) != expected_length or _HEX_RE.fullmatch(digest) is None:
|
| 230 |
+
raise RegistryError(f"Invalid {algorithm} digest for {path!r}: {digest!r}")
|
| 231 |
+
return cls(path=path, algorithm=algorithm, digest=digest)
|
| 232 |
+
|
| 233 |
+
@property
|
| 234 |
+
def encoded(self) -> str:
|
| 235 |
+
return f"{self.algorithm}:{self.digest}"
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
@dataclass(frozen=True, slots=True)
|
| 239 |
+
class CheckpointSource:
|
| 240 |
+
"""One immutable Hugging Face repository snapshot."""
|
| 241 |
+
|
| 242 |
+
repo_id: str
|
| 243 |
+
revision: str
|
| 244 |
+
files: tuple[FileDigest, ...]
|
| 245 |
+
unresolved_files: tuple[str, ...] = ()
|
| 246 |
+
|
| 247 |
+
@property
|
| 248 |
+
def file_map(self) -> Mapping[str, FileDigest]:
|
| 249 |
+
return MappingProxyType({item.path: item for item in self.files})
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
@dataclass(frozen=True, slots=True)
|
| 253 |
+
class OracleAsset:
|
| 254 |
+
"""Hash-pinned external file required by a native parity oracle."""
|
| 255 |
+
|
| 256 |
+
role: str
|
| 257 |
+
path: str
|
| 258 |
+
url: str
|
| 259 |
+
sha256: str
|
| 260 |
+
size: int
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
@dataclass(frozen=True, slots=True)
|
| 264 |
+
class RuntimeAsset:
|
| 265 |
+
"""Immutable runtime data with an explicit deserialization trust boundary."""
|
| 266 |
+
|
| 267 |
+
id: str
|
| 268 |
+
repository: str
|
| 269 |
+
revision: str
|
| 270 |
+
path: str
|
| 271 |
+
sha256: str
|
| 272 |
+
size: int
|
| 273 |
+
consumer_family: str
|
| 274 |
+
trust_kind: RuntimeAssetTrustKind
|
| 275 |
+
license_expression: str
|
| 276 |
+
offline_behavior: str
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
@dataclass(frozen=True, slots=True)
|
| 280 |
+
class OfficialGolden:
|
| 281 |
+
"""Hash-pinned official output bundle required by the check tier."""
|
| 282 |
+
|
| 283 |
+
metadata: FileDigest
|
| 284 |
+
tensors: FileDigest
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
@dataclass(frozen=True, slots=True)
|
| 288 |
+
class UpstreamSource:
|
| 289 |
+
"""Pinned official implementation used as a parity oracle."""
|
| 290 |
+
|
| 291 |
+
id: str
|
| 292 |
+
path: str
|
| 293 |
+
url: str
|
| 294 |
+
revision: str
|
| 295 |
+
license_expression: str
|
| 296 |
+
license_files: tuple[str, ...]
|
| 297 |
+
license_digests: tuple[FileDigest, ...] = ()
|
| 298 |
+
distribution_files: tuple[FileDigest, ...] = ()
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
@dataclass(frozen=True, slots=True)
|
| 302 |
+
class AttentionKernelSpec:
|
| 303 |
+
"""Immutable Hugging Face kernel used by one attention backend."""
|
| 304 |
+
|
| 305 |
+
implementation: str
|
| 306 |
+
repository: str
|
| 307 |
+
revision: str
|
| 308 |
+
version: int
|
| 309 |
+
expected_variant: str
|
| 310 |
+
dtypes: tuple[DtypeName, ...]
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
@dataclass(frozen=True, slots=True)
|
| 314 |
+
class ModelFamily:
|
| 315 |
+
"""Shared runtime and compliance contract for one architecture family."""
|
| 316 |
+
|
| 317 |
+
id: str
|
| 318 |
+
architecture: str
|
| 319 |
+
upstreams: tuple[str, ...]
|
| 320 |
+
tokenizer_mode: str
|
| 321 |
+
public_input: str
|
| 322 |
+
extra: RuntimeExtra
|
| 323 |
+
reference_container: str
|
| 324 |
+
reference_adapter: str
|
| 325 |
+
attention: tuple[str, ...]
|
| 326 |
+
dtypes: tuple[DtypeName, ...]
|
| 327 |
+
bf16_execution: Bf16Execution
|
| 328 |
+
precisions: tuple[str, ...]
|
| 329 |
+
vram_tier: VramTier
|
| 330 |
+
checkpoint_license: str
|
| 331 |
+
hub_license: str
|
| 332 |
+
state_transform: str
|
| 333 |
+
representative: str
|
| 334 |
+
documentation: str
|
| 335 |
+
test_tiers: tuple[TestTier, ...]
|
| 336 |
+
runtime_paths: tuple[str, ...]
|
| 337 |
+
auto_map_items: tuple[tuple[str, str], ...]
|
| 338 |
+
requires_complete_weight_publication: bool = False
|
| 339 |
+
weights_publication_allowed: bool = False
|
| 340 |
+
experimental_precisions: tuple[str, ...] = ()
|
| 341 |
+
tokenizer_class: str | None = None
|
| 342 |
+
hub_license_name: str | None = None
|
| 343 |
+
hub_license_link: str | None = None
|
| 344 |
+
conversion_provenance: str = ""
|
| 345 |
+
backbone_model: str | None = None
|
| 346 |
+
|
| 347 |
+
@property
|
| 348 |
+
def auto_map(self) -> Mapping[str, str]:
|
| 349 |
+
return MappingProxyType(dict(self.auto_map_items))
|
| 350 |
+
|
| 351 |
+
@property
|
| 352 |
+
def hub_license_metadata(self) -> Mapping[str, str]:
|
| 353 |
+
"""Return valid Hugging Face model-card license fields."""
|
| 354 |
+
|
| 355 |
+
metadata = {"license": self.hub_license}
|
| 356 |
+
if self.hub_license_name is not None:
|
| 357 |
+
# Hugging Face validates custom license names as lowercase slugs,
|
| 358 |
+
# while the manifest retains the reader-facing display name used
|
| 359 |
+
# in generated prose.
|
| 360 |
+
metadata["license_name"] = _HUB_LICENSE_NAME_RE.sub(
|
| 361 |
+
"-",
|
| 362 |
+
self.hub_license_name.lower(),
|
| 363 |
+
).strip("-.")
|
| 364 |
+
if self.hub_license_link is not None:
|
| 365 |
+
metadata["license_link"] = self.hub_license_link
|
| 366 |
+
return MappingProxyType(metadata)
|
| 367 |
+
|
| 368 |
+
@property
|
| 369 |
+
def stable_precisions(self) -> tuple[str, ...]:
|
| 370 |
+
"""Return precision policies covered by the release contract."""
|
| 371 |
+
|
| 372 |
+
experimental = set(self.experimental_precisions)
|
| 373 |
+
return tuple(precision for precision in self.precisions if precision not in experimental)
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
@dataclass(frozen=True, slots=True)
|
| 377 |
+
class ModelSpec:
|
| 378 |
+
"""Complete immutable source and runtime contract for one checkpoint."""
|
| 379 |
+
|
| 380 |
+
id: str
|
| 381 |
+
family: ModelFamily
|
| 382 |
+
fast: CheckpointSource
|
| 383 |
+
official: CheckpointSource
|
| 384 |
+
size_category: str
|
| 385 |
+
generation_contract: GenerationContract = "not_applicable"
|
| 386 |
+
oracle_assets: tuple[OracleAsset, ...] = ()
|
| 387 |
+
official_golden: OfficialGolden | None = None
|
| 388 |
+
artifact_source: str = "fast"
|
| 389 |
+
canonical_state_sha256: str | None = None
|
| 390 |
+
tokenizer_source_id: str | None = None
|
| 391 |
+
auto_map_items: tuple[tuple[str, str], ...] = ()
|
| 392 |
+
notes: str = ""
|
| 393 |
+
msa_conditioning: bool | None = None
|
| 394 |
+
|
| 395 |
+
@property
|
| 396 |
+
def is_deep_reference(self) -> bool:
|
| 397 |
+
return self.id == self.family.representative
|
| 398 |
+
|
| 399 |
+
@property
|
| 400 |
+
def auto_map(self) -> Mapping[str, str]:
|
| 401 |
+
if self.auto_map_items:
|
| 402 |
+
return MappingProxyType(dict(self.auto_map_items))
|
| 403 |
+
return self.family.auto_map
|
| 404 |
+
|
| 405 |
+
@property
|
| 406 |
+
def artifact_checkpoint(self) -> CheckpointSource:
|
| 407 |
+
"""Return the checkpoint selected for local artifact construction."""
|
| 408 |
+
|
| 409 |
+
return self.fast if self.artifact_source == "fast" else self.official
|
| 410 |
+
|
| 411 |
+
@property
|
| 412 |
+
def oracle_asset_map(self) -> Mapping[str, OracleAsset]:
|
| 413 |
+
"""Return native oracle assets keyed by their declared role."""
|
| 414 |
+
|
| 415 |
+
return MappingProxyType({asset.role: asset for asset in self.oracle_assets})
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
class ModelRegistry(Mapping[str, ModelSpec]):
|
| 419 |
+
"""Validated mapping of model IDs to typed model specifications."""
|
| 420 |
+
|
| 421 |
+
def __init__(
|
| 422 |
+
self,
|
| 423 |
+
*,
|
| 424 |
+
schema_version: int,
|
| 425 |
+
upstreams: Mapping[str, UpstreamSource],
|
| 426 |
+
families: Mapping[str, ModelFamily],
|
| 427 |
+
models: Mapping[str, ModelSpec],
|
| 428 |
+
runtime_assets: Mapping[str, RuntimeAsset] = MappingProxyType({}),
|
| 429 |
+
attention_kernels: Mapping[str, AttentionKernelSpec] = MappingProxyType({}),
|
| 430 |
+
legal_files: tuple[FileDigest, ...] = (),
|
| 431 |
+
) -> None:
|
| 432 |
+
self.schema_version = schema_version
|
| 433 |
+
self.upstreams = MappingProxyType(dict(upstreams))
|
| 434 |
+
self.attention_kernels = MappingProxyType(dict(attention_kernels))
|
| 435 |
+
self.families = MappingProxyType(dict(families))
|
| 436 |
+
self._models = MappingProxyType(dict(models))
|
| 437 |
+
self.runtime_assets = MappingProxyType(dict(runtime_assets))
|
| 438 |
+
self.legal_files = legal_files
|
| 439 |
+
|
| 440 |
+
def __getitem__(self, key: str) -> ModelSpec:
|
| 441 |
+
return self._models[key]
|
| 442 |
+
|
| 443 |
+
def __iter__(self) -> Iterator[str]:
|
| 444 |
+
return iter(self._models)
|
| 445 |
+
|
| 446 |
+
def __len__(self) -> int:
|
| 447 |
+
return len(self._models)
|
| 448 |
+
|
| 449 |
+
def by_family(self, family_id: str) -> tuple[ModelSpec, ...]:
|
| 450 |
+
if family_id not in self.families:
|
| 451 |
+
raise KeyError(family_id)
|
| 452 |
+
return tuple(model for model in self._models.values() if model.family.id == family_id)
|
| 453 |
+
|
| 454 |
+
def supported_attention_dtypes(
|
| 455 |
+
self,
|
| 456 |
+
family_id: str,
|
| 457 |
+
implementation: str,
|
| 458 |
+
) -> tuple[DtypeName, ...]:
|
| 459 |
+
"""Return manifest-supported dtypes for one family/backend pair."""
|
| 460 |
+
|
| 461 |
+
family = self.families[family_id]
|
| 462 |
+
if implementation not in family.attention:
|
| 463 |
+
raise KeyError(
|
| 464 |
+
f"Family {family_id!r} does not advertise attention backend "
|
| 465 |
+
f"{implementation!r}."
|
| 466 |
+
)
|
| 467 |
+
kernel = self.attention_kernels.get(implementation)
|
| 468 |
+
if kernel is None:
|
| 469 |
+
return family.dtypes
|
| 470 |
+
return tuple(dtype for dtype in family.dtypes if dtype in kernel.dtypes)
|
| 471 |
+
|
| 472 |
+
def require_resolved(self, model_id: str | None = None) -> None:
|
| 473 |
+
"""Fail release validation when required file identities remain unresolved."""
|
| 474 |
+
|
| 475 |
+
selected = self._models.values() if model_id is None else (self._models[model_id],)
|
| 476 |
+
unresolved: list[str] = []
|
| 477 |
+
for model in selected:
|
| 478 |
+
for label, checkpoint in (("fast", model.fast), ("official", model.official)):
|
| 479 |
+
for path in checkpoint.unresolved_files:
|
| 480 |
+
unresolved.append(f"{model.id}.{label}:{path}")
|
| 481 |
+
if unresolved:
|
| 482 |
+
detail = ", ".join(unresolved)
|
| 483 |
+
raise RegistryError(f"Release provenance is unresolved: {detail}")
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
def _reject_unknown_fields(
|
| 487 |
+
table: Mapping[str, Any],
|
| 488 |
+
allowed: frozenset[str],
|
| 489 |
+
context: str,
|
| 490 |
+
) -> None:
|
| 491 |
+
unknown = sorted(set(table).difference(allowed))
|
| 492 |
+
if unknown:
|
| 493 |
+
raise RegistryError(f"{context} contains unknown fields: {unknown}.")
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
def _require_str(table: Mapping[str, Any], key: str, context: str) -> str:
|
| 497 |
+
value = table.get(key)
|
| 498 |
+
if not isinstance(value, str) or not value.strip():
|
| 499 |
+
raise RegistryError(f"{context}.{key} must be a non-empty string.")
|
| 500 |
+
return value
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
def _require_enum(
|
| 504 |
+
table: Mapping[str, Any],
|
| 505 |
+
key: str,
|
| 506 |
+
context: str,
|
| 507 |
+
allowed: frozenset[str],
|
| 508 |
+
) -> str:
|
| 509 |
+
value = _require_str(table, key, context)
|
| 510 |
+
if value not in allowed:
|
| 511 |
+
raise RegistryError(
|
| 512 |
+
f"{context}.{key} must be one of {sorted(allowed)}; received {value!r}."
|
| 513 |
+
)
|
| 514 |
+
return value
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
def _parse_reference_container(table: Mapping[str, Any], context: str) -> str:
|
| 518 |
+
value = _require_str(table, "reference_container", context)
|
| 519 |
+
if _REFERENCE_CONTAINER_RE.fullmatch(value) is None:
|
| 520 |
+
raise RegistryError(
|
| 521 |
+
f"{context}.reference_container must be a portable 'reference-<name>' target."
|
| 522 |
+
)
|
| 523 |
+
return value
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
def _parse_reference_adapter(table: Mapping[str, Any], context: str) -> str:
|
| 527 |
+
value = _require_str(table, "reference_adapter", context)
|
| 528 |
+
if _REFERENCE_ADAPTER_RE.fullmatch(value) is None:
|
| 529 |
+
raise RegistryError(
|
| 530 |
+
f"{context}.reference_adapter must name one module under "
|
| 531 |
+
"tests.parity.support.reference_adapters."
|
| 532 |
+
)
|
| 533 |
+
return value
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
def _parse_documentation_path(table: Mapping[str, Any], context: str) -> str:
|
| 537 |
+
value = _require_str(table, "documentation", context)
|
| 538 |
+
if value.count("#") > 1 or "\\" in value:
|
| 539 |
+
raise RegistryError(f"{context}.documentation must be a portable documentation path.")
|
| 540 |
+
raw_path, separator, fragment = value.partition("#")
|
| 541 |
+
path = PurePosixPath(raw_path)
|
| 542 |
+
if (
|
| 543 |
+
path.is_absolute()
|
| 544 |
+
or ".." in path.parts
|
| 545 |
+
or len(path.parts) < 2
|
| 546 |
+
or path.parts[0] != "docs"
|
| 547 |
+
or path.suffix != ".md"
|
| 548 |
+
or path.as_posix() != raw_path
|
| 549 |
+
):
|
| 550 |
+
raise RegistryError(
|
| 551 |
+
f"{context}.documentation must reference a normalized Markdown file under docs/."
|
| 552 |
+
)
|
| 553 |
+
if separator and _DOCUMENTATION_FRAGMENT_RE.fullmatch(fragment) is None:
|
| 554 |
+
raise RegistryError(f"{context}.documentation has an invalid heading fragment.")
|
| 555 |
+
return value
|
| 556 |
+
|
| 557 |
+
|
| 558 |
+
def _require_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple[str, ...]:
|
| 559 |
+
value = table.get(key)
|
| 560 |
+
if not isinstance(value, list) or not value or any(not isinstance(item, str) for item in value):
|
| 561 |
+
raise RegistryError(f"{context}.{key} must be a non-empty string array.")
|
| 562 |
+
result = tuple(value)
|
| 563 |
+
if len(set(result)) != len(result):
|
| 564 |
+
raise RegistryError(f"{context}.{key} contains duplicate values.")
|
| 565 |
+
return result
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
def _optional_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple[str, ...]:
|
| 569 |
+
value = table.get(key, [])
|
| 570 |
+
if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
|
| 571 |
+
raise RegistryError(f"{context}.{key} must be a string array.")
|
| 572 |
+
result = tuple(value)
|
| 573 |
+
if len(set(result)) != len(result):
|
| 574 |
+
raise RegistryError(f"{context}.{key} contains duplicate values.")
|
| 575 |
+
return result
|
| 576 |
+
|
| 577 |
+
|
| 578 |
+
def _optional_str(table: Mapping[str, Any], key: str, context: str) -> str | None:
|
| 579 |
+
value = table.get(key)
|
| 580 |
+
if value is None:
|
| 581 |
+
return None
|
| 582 |
+
if (
|
| 583 |
+
not isinstance(value, str)
|
| 584 |
+
or not value.strip()
|
| 585 |
+
or value != value.strip()
|
| 586 |
+
or "\n" in value
|
| 587 |
+
or "\r" in value
|
| 588 |
+
):
|
| 589 |
+
raise RegistryError(f"{context}.{key} must be a non-empty single-line string.")
|
| 590 |
+
return value
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
def _parse_hub_license(
|
| 594 |
+
table: Mapping[str, Any],
|
| 595 |
+
*,
|
| 596 |
+
checkpoint_license: str,
|
| 597 |
+
context: str,
|
| 598 |
+
) -> tuple[str, str | None, str | None]:
|
| 599 |
+
expected_fields = {"hub_license", "hub_license_name", "hub_license_link"}
|
| 600 |
+
unknown_fields = sorted(
|
| 601 |
+
key for key in table if key.startswith("hub_") and key not in expected_fields
|
| 602 |
+
)
|
| 603 |
+
if unknown_fields:
|
| 604 |
+
raise RegistryError(f"{context} contains unsupported Hub license fields: {unknown_fields}.")
|
| 605 |
+
identifier = _require_str(table, "hub_license", context)
|
| 606 |
+
if identifier not in HUB_LICENSE_IDENTIFIERS:
|
| 607 |
+
raise RegistryError(
|
| 608 |
+
f"{context}.hub_license must be a supported Hugging Face license identifier."
|
| 609 |
+
)
|
| 610 |
+
expected_identifier: str | None = None
|
| 611 |
+
for prefix, candidate in (
|
| 612 |
+
("MIT", "mit"),
|
| 613 |
+
("Apache-2.0", "apache-2.0"),
|
| 614 |
+
("CC-BY-NC-SA-4.0", "cc-by-nc-sa-4.0"),
|
| 615 |
+
("Profluent-E1-Agreement", "other"),
|
| 616 |
+
("Unresolved", "other"),
|
| 617 |
+
):
|
| 618 |
+
if checkpoint_license.startswith(prefix):
|
| 619 |
+
expected_identifier = candidate
|
| 620 |
+
break
|
| 621 |
+
if expected_identifier is None:
|
| 622 |
+
raise RegistryError(
|
| 623 |
+
f"{context}.checkpoint_license has no declared Hugging Face identifier mapping."
|
| 624 |
+
)
|
| 625 |
+
if identifier != expected_identifier:
|
| 626 |
+
raise RegistryError(
|
| 627 |
+
f"{context}.hub_license must be {expected_identifier!r} for "
|
| 628 |
+
f"checkpoint terms {checkpoint_license!r}."
|
| 629 |
+
)
|
| 630 |
+
|
| 631 |
+
name = _optional_str(table, "hub_license_name", context)
|
| 632 |
+
link = _optional_str(table, "hub_license_link", context)
|
| 633 |
+
if identifier != "other":
|
| 634 |
+
if name is not None or link is not None:
|
| 635 |
+
raise RegistryError(
|
| 636 |
+
f"{context} may define hub_license_name and hub_license_link only "
|
| 637 |
+
"when hub_license='other'."
|
| 638 |
+
)
|
| 639 |
+
return identifier, None, None
|
| 640 |
+
if name is None or link is None:
|
| 641 |
+
raise RegistryError(
|
| 642 |
+
f"{context} must define hub_license_name and hub_license_link when hub_license='other'."
|
| 643 |
+
)
|
| 644 |
+
parsed_link = urlparse(link)
|
| 645 |
+
if (
|
| 646 |
+
parsed_link.scheme != "https"
|
| 647 |
+
or not parsed_link.netloc
|
| 648 |
+
or not parsed_link.path
|
| 649 |
+
or parsed_link.username is not None
|
| 650 |
+
or parsed_link.password is not None
|
| 651 |
+
):
|
| 652 |
+
raise RegistryError(f"{context}.hub_license_link must be an absolute HTTPS URL.")
|
| 653 |
+
return identifier, name, link
|
| 654 |
+
|
| 655 |
+
|
| 656 |
+
def _require_digest_list(
|
| 657 |
+
table: Mapping[str, Any], key: str, context: str
|
| 658 |
+
) -> tuple[FileDigest, ...]:
|
| 659 |
+
encoded = _require_str_list(table, key, context)
|
| 660 |
+
result = tuple(FileDigest.parse(value) for value in encoded)
|
| 661 |
+
paths = [item.path for item in result]
|
| 662 |
+
if len(paths) != len(set(paths)):
|
| 663 |
+
raise RegistryError(f"{context}.{key} contains duplicate paths.")
|
| 664 |
+
return result
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
def _validate_revision(revision: str, context: str) -> None:
|
| 668 |
+
if len(revision) != 40 or _HEX_RE.fullmatch(revision) is None:
|
| 669 |
+
raise RegistryError(f"{context} must be an immutable 40-character commit revision.")
|
| 670 |
+
|
| 671 |
+
|
| 672 |
+
def _parse_checkpoint(table: Mapping[str, Any], prefix: str, context: str) -> CheckpointSource:
|
| 673 |
+
repo_id = _require_str(table, f"{prefix}_repo", context)
|
| 674 |
+
if _REPOSITORY_ID_RE.fullmatch(repo_id) is None:
|
| 675 |
+
raise RegistryError(f"{context}.{prefix}_repo must be a Hugging Face repository ID.")
|
| 676 |
+
revision = _require_str(table, f"{prefix}_revision", context)
|
| 677 |
+
_validate_revision(revision, f"{context}.{prefix}_revision")
|
| 678 |
+
encoded_files = _require_str_list(table, f"{prefix}_files", context)
|
| 679 |
+
files = tuple(FileDigest.parse(value) for value in encoded_files)
|
| 680 |
+
paths = [item.path for item in files]
|
| 681 |
+
if len(paths) != len(set(paths)):
|
| 682 |
+
raise RegistryError(f"{context}.{prefix}_files contains duplicate paths.")
|
| 683 |
+
if not any(item.path.endswith(_WEIGHT_SUFFIXES) for item in files):
|
| 684 |
+
raise RegistryError(f"{context}.{prefix}_files does not identify a weight file.")
|
| 685 |
+
unresolved_files = _optional_str_list(table, f"{prefix}_unresolved_files", context)
|
| 686 |
+
for unresolved_path in unresolved_files:
|
| 687 |
+
_portable_relative_path(unresolved_path, "Unresolved checkpoint path")
|
| 688 |
+
if unresolved_path in paths:
|
| 689 |
+
raise RegistryError(
|
| 690 |
+
f"{context}.{prefix} marks {unresolved_path!r} both resolved and unresolved."
|
| 691 |
+
)
|
| 692 |
+
return CheckpointSource(
|
| 693 |
+
repo_id=repo_id,
|
| 694 |
+
revision=revision,
|
| 695 |
+
files=files,
|
| 696 |
+
unresolved_files=unresolved_files,
|
| 697 |
+
)
|
| 698 |
+
|
| 699 |
+
|
| 700 |
+
def _parse_oracle_assets(table: Mapping[str, Any], context: str) -> tuple[OracleAsset, ...]:
|
| 701 |
+
raw = table.get("oracle_assets", [])
|
| 702 |
+
if not isinstance(raw, list):
|
| 703 |
+
raise RegistryError(f"{context}.oracle_assets must be an array of tables.")
|
| 704 |
+
result: list[OracleAsset] = []
|
| 705 |
+
for index, value in enumerate(raw):
|
| 706 |
+
asset_context = f"{context}.oracle_assets[{index}]"
|
| 707 |
+
if not isinstance(value, dict):
|
| 708 |
+
raise RegistryError(f"{asset_context} must be a table.")
|
| 709 |
+
expected_fields = {"role", "path", "url", "sha256", "size"}
|
| 710 |
+
if set(value) != expected_fields:
|
| 711 |
+
raise RegistryError(f"{asset_context} must contain exactly {sorted(expected_fields)}.")
|
| 712 |
+
role = _require_str(value, "role", asset_context)
|
| 713 |
+
if role not in _ALLOWED_ORACLE_ASSET_ROLES:
|
| 714 |
+
raise RegistryError(f"Unsupported oracle asset role: {role!r}.")
|
| 715 |
+
path = _require_str(value, "path", asset_context)
|
| 716 |
+
try:
|
| 717 |
+
normalized_path = _portable_relative_path(path, "Oracle asset path")
|
| 718 |
+
except RegistryError as error:
|
| 719 |
+
raise RegistryError(f"Invalid oracle asset path: {path!r}.") from error
|
| 720 |
+
if normalized_path.suffix != ".pt":
|
| 721 |
+
raise RegistryError(f"Invalid oracle asset path: {path!r}.")
|
| 722 |
+
url = _require_str(value, "url", asset_context)
|
| 723 |
+
parsed_url = urlparse(url)
|
| 724 |
+
if (
|
| 725 |
+
parsed_url.scheme != "https"
|
| 726 |
+
or parsed_url.hostname != _FAIR_ESM_ASSET_HOST
|
| 727 |
+
or parsed_url.path != f"/fair-esm/{path}"
|
| 728 |
+
or parsed_url.params
|
| 729 |
+
or parsed_url.query
|
| 730 |
+
or parsed_url.fragment
|
| 731 |
+
):
|
| 732 |
+
raise RegistryError(f"Invalid fair-esm oracle asset URL: {url!r}.")
|
| 733 |
+
sha256 = _require_str(value, "sha256", asset_context)
|
| 734 |
+
if len(sha256) != 64 or _HEX_RE.fullmatch(sha256) is None:
|
| 735 |
+
raise RegistryError(f"Invalid oracle asset SHA-256 for {path!r}.")
|
| 736 |
+
size = value.get("size")
|
| 737 |
+
if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
|
| 738 |
+
raise RegistryError(f"{asset_context}.size must be a positive byte count.")
|
| 739 |
+
result.append(
|
| 740 |
+
OracleAsset(
|
| 741 |
+
role=role,
|
| 742 |
+
path=path,
|
| 743 |
+
url=url,
|
| 744 |
+
sha256=sha256,
|
| 745 |
+
size=size,
|
| 746 |
+
)
|
| 747 |
+
)
|
| 748 |
+
roles = [asset.role for asset in result]
|
| 749 |
+
paths = [asset.path for asset in result]
|
| 750 |
+
urls = [asset.url for asset in result]
|
| 751 |
+
if (
|
| 752 |
+
len(roles) != len(set(roles))
|
| 753 |
+
or len(paths) != len(set(paths))
|
| 754 |
+
or len(urls) != len(set(urls))
|
| 755 |
+
):
|
| 756 |
+
raise RegistryError(f"{context}.oracle_assets contains duplicate identities.")
|
| 757 |
+
return tuple(result)
|
| 758 |
+
|
| 759 |
+
|
| 760 |
+
def _parse_official_golden(
|
| 761 |
+
table: Mapping[str, Any],
|
| 762 |
+
model_id: str,
|
| 763 |
+
context: str,
|
| 764 |
+
) -> OfficialGolden | None:
|
| 765 |
+
raw = table.get("official_golden")
|
| 766 |
+
if raw is None:
|
| 767 |
+
return None
|
| 768 |
+
if not isinstance(raw, dict) or set(raw) != {"metadata", "tensors"}:
|
| 769 |
+
raise RegistryError(
|
| 770 |
+
f"{context}.official_golden must contain exactly 'metadata' and 'tensors'."
|
| 771 |
+
)
|
| 772 |
+
parsed: dict[str, FileDigest] = {}
|
| 773 |
+
for role in ("metadata", "tensors"):
|
| 774 |
+
value = raw[role]
|
| 775 |
+
if not isinstance(value, str):
|
| 776 |
+
raise RegistryError(f"{context}.official_golden.{role} must be a file digest.")
|
| 777 |
+
digest = FileDigest.parse(value)
|
| 778 |
+
if digest.algorithm != "sha256":
|
| 779 |
+
raise RegistryError(
|
| 780 |
+
f"{context}.official_golden.{role} must use an immutable SHA-256 digest."
|
| 781 |
+
)
|
| 782 |
+
expected = f"tests/goldens/{model_id}.{'json' if role == 'metadata' else 'safetensors'}"
|
| 783 |
+
if digest.path != expected:
|
| 784 |
+
raise RegistryError(f"{context}.official_golden.{role} must use path {expected!r}.")
|
| 785 |
+
parsed[role] = digest
|
| 786 |
+
return OfficialGolden(metadata=parsed["metadata"], tensors=parsed["tensors"])
|
| 787 |
+
|
| 788 |
+
|
| 789 |
+
def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
|
| 790 |
+
if not isinstance(raw, list) or not raw:
|
| 791 |
+
raise RegistryError("The manifest must contain [[attention_kernels]] entries.")
|
| 792 |
+
result: dict[str, AttentionKernelSpec] = {}
|
| 793 |
+
expected_variants = {
|
| 794 |
+
"flash_attention_2": "flash_attn2",
|
| 795 |
+
"flash_attention_3": "flash_attn3",
|
| 796 |
+
}
|
| 797 |
+
for index, value in enumerate(raw):
|
| 798 |
+
context = f"attention_kernels[{index}]"
|
| 799 |
+
if not isinstance(value, dict):
|
| 800 |
+
raise RegistryError(f"{context} must be a table.")
|
| 801 |
+
expected_fields = frozenset(
|
| 802 |
+
{
|
| 803 |
+
"implementation",
|
| 804 |
+
"repository",
|
| 805 |
+
"revision",
|
| 806 |
+
"version",
|
| 807 |
+
"expected_variant",
|
| 808 |
+
"dtypes",
|
| 809 |
+
}
|
| 810 |
+
)
|
| 811 |
+
_reject_unknown_fields(value, expected_fields, context)
|
| 812 |
+
implementation = _require_str(value, "implementation", context)
|
| 813 |
+
if implementation not in expected_variants:
|
| 814 |
+
raise RegistryError(f"Unsupported attention kernel {implementation!r}.")
|
| 815 |
+
if implementation in result:
|
| 816 |
+
raise RegistryError(f"Duplicate attention kernel {implementation!r}.")
|
| 817 |
+
repository = _require_str(value, "repository", context)
|
| 818 |
+
if _REPOSITORY_ID_RE.fullmatch(repository) is None:
|
| 819 |
+
raise RegistryError(f"Invalid attention-kernel repository {repository!r}.")
|
| 820 |
+
revision = _require_str(value, "revision", context)
|
| 821 |
+
_validate_revision(revision, f"{context}.revision")
|
| 822 |
+
kernel_version = value.get("version")
|
| 823 |
+
if (
|
| 824 |
+
isinstance(kernel_version, bool)
|
| 825 |
+
or not isinstance(kernel_version, int)
|
| 826 |
+
or kernel_version <= 0
|
| 827 |
+
):
|
| 828 |
+
raise RegistryError(f"{context}.version must be a positive integer.")
|
| 829 |
+
expected_variant = _require_str(value, "expected_variant", context)
|
| 830 |
+
if expected_variant != expected_variants[implementation]:
|
| 831 |
+
raise RegistryError(
|
| 832 |
+
f"{context}.expected_variant must be {expected_variants[implementation]!r}."
|
| 833 |
+
)
|
| 834 |
+
dtypes = _require_str_list(value, "dtypes", context)
|
| 835 |
+
if not set(dtypes).issubset(_ALLOWED_DTYPES):
|
| 836 |
+
raise RegistryError(f"{context}.dtypes contains unsupported dtypes.")
|
| 837 |
+
result[implementation] = AttentionKernelSpec(
|
| 838 |
+
implementation=implementation,
|
| 839 |
+
repository=repository,
|
| 840 |
+
revision=revision,
|
| 841 |
+
version=kernel_version,
|
| 842 |
+
expected_variant=expected_variant,
|
| 843 |
+
dtypes=cast(tuple[DtypeName, ...], dtypes),
|
| 844 |
+
)
|
| 845 |
+
if set(result) != set(expected_variants):
|
| 846 |
+
raise RegistryError("The manifest must pin both FlashAttention kernel versions.")
|
| 847 |
+
return result
|
| 848 |
+
|
| 849 |
+
|
| 850 |
+
def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
|
| 851 |
+
if not isinstance(raw, list) or not raw:
|
| 852 |
+
raise RegistryError("The manifest must contain at least one [[upstreams]] entry.")
|
| 853 |
+
result: dict[str, UpstreamSource] = {}
|
| 854 |
+
paths: set[str] = set()
|
| 855 |
+
for index, value in enumerate(raw):
|
| 856 |
+
context = f"upstreams[{index}]"
|
| 857 |
+
if not isinstance(value, dict):
|
| 858 |
+
raise RegistryError(f"{context} must be a table.")
|
| 859 |
+
_reject_unknown_fields(value, _UPSTREAM_FIELDS, context)
|
| 860 |
+
source_id = _require_str(value, "id", context)
|
| 861 |
+
if _IDENTIFIER_RE.fullmatch(source_id) is None:
|
| 862 |
+
raise RegistryError(f"Invalid upstream ID: {source_id!r}")
|
| 863 |
+
if source_id in result:
|
| 864 |
+
raise RegistryError(f"Duplicate upstream ID: {source_id!r}")
|
| 865 |
+
revision = _require_str(value, "revision", context)
|
| 866 |
+
_validate_revision(revision, f"{context}.revision")
|
| 867 |
+
path = _require_str(value, "path", context)
|
| 868 |
+
try:
|
| 869 |
+
normalized_path = _portable_relative_path(path, f"{context}.path")
|
| 870 |
+
except RegistryError as error:
|
| 871 |
+
raise RegistryError(
|
| 872 |
+
f"{context}.path must be a normalized directory directly under "
|
| 873 |
+
"'vendor/upstream/'."
|
| 874 |
+
) from error
|
| 875 |
+
if (
|
| 876 |
+
normalized_path.parts[:2] != ("vendor", "upstream")
|
| 877 |
+
or len(normalized_path.parts) != 3
|
| 878 |
+
):
|
| 879 |
+
raise RegistryError(
|
| 880 |
+
f"{context}.path must be a normalized directory directly under "
|
| 881 |
+
"'vendor/upstream/'."
|
| 882 |
+
)
|
| 883 |
+
if path in paths:
|
| 884 |
+
raise RegistryError(f"Duplicate upstream path: {path!r}")
|
| 885 |
+
paths.add(path)
|
| 886 |
+
url = _require_str(value, "url", context)
|
| 887 |
+
if not url.startswith("https://github.com/") or not url.endswith(".git"):
|
| 888 |
+
raise RegistryError(f"{context}.url must be an HTTPS GitHub clone URL.")
|
| 889 |
+
license_files = _require_str_list(value, "license_files", context)
|
| 890 |
+
license_digests = _require_digest_list(value, "license_digests", context)
|
| 891 |
+
if tuple(item.path for item in license_digests) != license_files:
|
| 892 |
+
raise RegistryError(
|
| 893 |
+
f"{context}.license_digests must cover license_files in the same order."
|
| 894 |
+
)
|
| 895 |
+
distribution_files = _require_digest_list(value, "distribution_files", context)
|
| 896 |
+
distribution_map = {item.path: item for item in distribution_files}
|
| 897 |
+
for canonical in license_digests:
|
| 898 |
+
distributed = distribution_map.get(canonical.path)
|
| 899 |
+
if distributed is None or distributed.encoded != canonical.encoded:
|
| 900 |
+
raise RegistryError(
|
| 901 |
+
f"{context}.distribution_files must include an exact copy of "
|
| 902 |
+
f"{canonical.path!r}."
|
| 903 |
+
)
|
| 904 |
+
if source_id == "e1":
|
| 905 |
+
required_e1 = {
|
| 906 |
+
"LICENSE",
|
| 907 |
+
"ATTRIBUTION",
|
| 908 |
+
"NOTICE",
|
| 909 |
+
"Apache-2.0.txt",
|
| 910 |
+
"BSD-3-Clause.txt",
|
| 911 |
+
"MODIFICATIONS.md",
|
| 912 |
+
}
|
| 913 |
+
missing_e1 = sorted(required_e1.difference(distribution_map))
|
| 914 |
+
if missing_e1:
|
| 915 |
+
raise RegistryError(f"{context} is missing E1 legal files: {missing_e1}")
|
| 916 |
+
result[source_id] = UpstreamSource(
|
| 917 |
+
id=source_id,
|
| 918 |
+
path=path,
|
| 919 |
+
url=url,
|
| 920 |
+
revision=revision,
|
| 921 |
+
license_expression=_require_str(value, "license", context),
|
| 922 |
+
license_files=license_files,
|
| 923 |
+
license_digests=license_digests,
|
| 924 |
+
distribution_files=distribution_files,
|
| 925 |
+
)
|
| 926 |
+
return result
|
| 927 |
+
|
| 928 |
+
|
| 929 |
+
def _parse_families(
|
| 930 |
+
raw: object,
|
| 931 |
+
upstreams: Mapping[str, UpstreamSource],
|
| 932 |
+
) -> dict[str, ModelFamily]:
|
| 933 |
+
if not isinstance(raw, dict) or not raw:
|
| 934 |
+
raise RegistryError("The manifest must contain [families.<id>] tables.")
|
| 935 |
+
result: dict[str, ModelFamily] = {}
|
| 936 |
+
for family_id, value in raw.items():
|
| 937 |
+
context = f"families.{family_id}"
|
| 938 |
+
if _IDENTIFIER_RE.fullmatch(family_id) is None or not isinstance(value, dict):
|
| 939 |
+
raise RegistryError(f"Invalid family table: {family_id!r}")
|
| 940 |
+
checkpoint_license = _require_str(value, "checkpoint_license", context)
|
| 941 |
+
hub_license, hub_license_name, hub_license_link = _parse_hub_license(
|
| 942 |
+
value,
|
| 943 |
+
checkpoint_license=checkpoint_license,
|
| 944 |
+
context=context,
|
| 945 |
+
)
|
| 946 |
+
_reject_unknown_fields(value, _FAMILY_FIELDS, context)
|
| 947 |
+
source_ids = _require_str_list(value, "upstreams", context)
|
| 948 |
+
unknown_sources = sorted(set(source_ids).difference(upstreams))
|
| 949 |
+
if unknown_sources:
|
| 950 |
+
raise RegistryError(f"{context} references unknown upstreams: {unknown_sources}")
|
| 951 |
+
tokenizer_mode = _require_str(value, "tokenizer_mode", context)
|
| 952 |
+
if tokenizer_mode not in _ALLOWED_TOKENIZER_MODES:
|
| 953 |
+
raise RegistryError(f"Unsupported tokenizer mode in {context}: {tokenizer_mode!r}")
|
| 954 |
+
public_input = _require_str(value, "public_input", context)
|
| 955 |
+
attention = _require_str_list(value, "attention", context)
|
| 956 |
+
if not set(attention).issubset(_ALLOWED_ATTENTION):
|
| 957 |
+
raise RegistryError(f"Unsupported attention implementation in {context}.")
|
| 958 |
+
dtypes = _require_str_list(value, "dtypes", context)
|
| 959 |
+
if not set(dtypes).issubset(_ALLOWED_DTYPES):
|
| 960 |
+
raise RegistryError(f"Unsupported dtype in {context}.")
|
| 961 |
+
bf16_execution = cast(
|
| 962 |
+
Bf16Execution,
|
| 963 |
+
_require_enum(
|
| 964 |
+
value,
|
| 965 |
+
"bf16_execution",
|
| 966 |
+
context,
|
| 967 |
+
_ALLOWED_BF16_EXECUTIONS,
|
| 968 |
+
),
|
| 969 |
+
)
|
| 970 |
+
precisions = _require_str_list(value, "precisions", context)
|
| 971 |
+
if not set(precisions).issubset(_ALLOWED_PRECISIONS):
|
| 972 |
+
raise RegistryError(f"Unsupported precision policy in {context}.")
|
| 973 |
+
experimental_precisions = _optional_str_list(
|
| 974 |
+
value,
|
| 975 |
+
"experimental_precisions",
|
| 976 |
+
context,
|
| 977 |
+
)
|
| 978 |
+
unknown_experimental_precisions = sorted(
|
| 979 |
+
set(experimental_precisions).difference(precisions)
|
| 980 |
+
)
|
| 981 |
+
if unknown_experimental_precisions:
|
| 982 |
+
raise RegistryError(
|
| 983 |
+
f"{context}.experimental_precisions must be a subset of precisions; "
|
| 984 |
+
f"unknown values: {unknown_experimental_precisions}."
|
| 985 |
+
)
|
| 986 |
+
extra = cast(RuntimeExtra, _require_enum(value, "extra", context, _ALLOWED_EXTRAS))
|
| 987 |
+
vram_tier = cast(
|
| 988 |
+
VramTier,
|
| 989 |
+
_require_enum(value, "vram_tier", context, _ALLOWED_VRAM_TIERS),
|
| 990 |
+
)
|
| 991 |
+
test_tiers_raw = _require_str_list(value, "test_tiers", context)
|
| 992 |
+
unknown_test_tiers = sorted(set(test_tiers_raw).difference(_ALLOWED_TEST_TIERS))
|
| 993 |
+
if unknown_test_tiers:
|
| 994 |
+
raise RegistryError(
|
| 995 |
+
f"{context}.test_tiers contains unsupported tiers: {unknown_test_tiers}."
|
| 996 |
+
)
|
| 997 |
+
test_tiers = cast(tuple[TestTier, ...], test_tiers_raw)
|
| 998 |
+
reference_container = _parse_reference_container(value, context)
|
| 999 |
+
reference_adapter = _parse_reference_adapter(value, context)
|
| 1000 |
+
documentation = _parse_documentation_path(value, context)
|
| 1001 |
+
runtime_paths = _require_str_list(value, "runtime_paths", context)
|
| 1002 |
+
if len(runtime_paths) != len(set(runtime_paths)):
|
| 1003 |
+
raise RegistryError(f"{context}.runtime_paths must not contain duplicates.")
|
| 1004 |
+
for runtime_path in runtime_paths:
|
| 1005 |
+
try:
|
| 1006 |
+
_portable_relative_path(runtime_path, f"{context}.runtime_paths entry")
|
| 1007 |
+
except RegistryError as error:
|
| 1008 |
+
raise RegistryError(
|
| 1009 |
+
f"Unsafe runtime path in {context}: {runtime_path!r}"
|
| 1010 |
+
) from error
|
| 1011 |
+
if runtime_path.startswith("vendor/"):
|
| 1012 |
+
raise RegistryError(f"Unsafe runtime path in {context}: {runtime_path!r}")
|
| 1013 |
+
requires_complete_weight_publication = value.get(
|
| 1014 |
+
"requires_complete_weight_publication",
|
| 1015 |
+
False,
|
| 1016 |
+
)
|
| 1017 |
+
if not isinstance(requires_complete_weight_publication, bool):
|
| 1018 |
+
raise RegistryError(
|
| 1019 |
+
f"{context}.requires_complete_weight_publication must be a boolean."
|
| 1020 |
+
)
|
| 1021 |
+
if "weights_publication_allowed" not in value:
|
| 1022 |
+
raise RegistryError(
|
| 1023 |
+
f"{context}.weights_publication_allowed must be declared explicitly."
|
| 1024 |
+
)
|
| 1025 |
+
weights_publication_allowed = value["weights_publication_allowed"]
|
| 1026 |
+
if not isinstance(weights_publication_allowed, bool):
|
| 1027 |
+
raise RegistryError(f"{context}.weights_publication_allowed must be a boolean.")
|
| 1028 |
+
raw_auto_map = value.get("auto_map")
|
| 1029 |
+
if not isinstance(raw_auto_map, dict) or not raw_auto_map:
|
| 1030 |
+
raise RegistryError(f"{context}.auto_map must be a non-empty table.")
|
| 1031 |
+
auto_map: list[tuple[str, str]] = []
|
| 1032 |
+
for auto_class, class_path in raw_auto_map.items():
|
| 1033 |
+
if auto_class not in _ALLOWED_AUTO_CLASSES or not isinstance(class_path, str):
|
| 1034 |
+
raise RegistryError(f"Invalid AutoClass mapping in {context}: {auto_class!r}")
|
| 1035 |
+
if not class_path.startswith("fastplms.") or class_path.count(".") < 2:
|
| 1036 |
+
raise RegistryError(f"Invalid Python class path in {context}: {class_path!r}")
|
| 1037 |
+
auto_map.append((auto_class, class_path))
|
| 1038 |
+
tokenizer_class = value.get("tokenizer_class")
|
| 1039 |
+
if tokenizer_class is not None:
|
| 1040 |
+
if tokenizer_mode != "tokenizer":
|
| 1041 |
+
raise RegistryError(
|
| 1042 |
+
f"{context}.tokenizer_class requires tokenizer_mode='tokenizer'."
|
| 1043 |
+
)
|
| 1044 |
+
if (
|
| 1045 |
+
not isinstance(tokenizer_class, str)
|
| 1046 |
+
or not tokenizer_class.startswith("fastplms.")
|
| 1047 |
+
or tokenizer_class.count(".") < 2
|
| 1048 |
+
):
|
| 1049 |
+
raise RegistryError(
|
| 1050 |
+
f"Invalid tokenizer class path in {context}: {tokenizer_class!r}"
|
| 1051 |
+
)
|
| 1052 |
+
backbone_model = value.get("backbone_model")
|
| 1053 |
+
if backbone_model is not None and (
|
| 1054 |
+
not isinstance(backbone_model, str)
|
| 1055 |
+
or _IDENTIFIER_RE.fullmatch(backbone_model) is None
|
| 1056 |
+
):
|
| 1057 |
+
raise RegistryError(
|
| 1058 |
+
f"{context}.backbone_model must be a valid manifest model ID."
|
| 1059 |
+
)
|
| 1060 |
+
state_transform = _require_str(value, "state_transform", context)
|
| 1061 |
+
conversion_provenance = _require_str(value, "conversion_provenance", context)
|
| 1062 |
+
required_sections = ("Input:", "Transformation:", "Output:", "Validation:", "Limitation:")
|
| 1063 |
+
missing_sections = [
|
| 1064 |
+
section for section in required_sections if section not in conversion_provenance
|
| 1065 |
+
]
|
| 1066 |
+
if missing_sections or state_transform not in conversion_provenance:
|
| 1067 |
+
raise RegistryError(
|
| 1068 |
+
f"{context}.conversion_provenance must identify {state_transform!r} and "
|
| 1069 |
+
f"contain mechanism-first sections; missing {missing_sections}."
|
| 1070 |
+
)
|
| 1071 |
+
result[family_id] = ModelFamily(
|
| 1072 |
+
id=family_id,
|
| 1073 |
+
architecture=_require_str(value, "architecture", context),
|
| 1074 |
+
upstreams=source_ids,
|
| 1075 |
+
tokenizer_mode=tokenizer_mode,
|
| 1076 |
+
public_input=public_input,
|
| 1077 |
+
extra=extra,
|
| 1078 |
+
reference_container=reference_container,
|
| 1079 |
+
reference_adapter=reference_adapter,
|
| 1080 |
+
attention=attention,
|
| 1081 |
+
dtypes=cast(tuple[DtypeName, ...], dtypes),
|
| 1082 |
+
bf16_execution=bf16_execution,
|
| 1083 |
+
precisions=precisions,
|
| 1084 |
+
experimental_precisions=experimental_precisions,
|
| 1085 |
+
vram_tier=vram_tier,
|
| 1086 |
+
checkpoint_license=checkpoint_license,
|
| 1087 |
+
hub_license=hub_license,
|
| 1088 |
+
state_transform=state_transform,
|
| 1089 |
+
representative=_require_str(value, "representative", context),
|
| 1090 |
+
documentation=documentation,
|
| 1091 |
+
test_tiers=test_tiers,
|
| 1092 |
+
runtime_paths=runtime_paths,
|
| 1093 |
+
auto_map_items=tuple(auto_map),
|
| 1094 |
+
requires_complete_weight_publication=requires_complete_weight_publication,
|
| 1095 |
+
weights_publication_allowed=weights_publication_allowed,
|
| 1096 |
+
tokenizer_class=tokenizer_class,
|
| 1097 |
+
hub_license_name=hub_license_name,
|
| 1098 |
+
hub_license_link=hub_license_link,
|
| 1099 |
+
conversion_provenance=conversion_provenance,
|
| 1100 |
+
backbone_model=backbone_model,
|
| 1101 |
+
)
|
| 1102 |
+
return result
|
| 1103 |
+
|
| 1104 |
+
|
| 1105 |
+
def _parse_runtime_assets(
|
| 1106 |
+
raw: object,
|
| 1107 |
+
families: Mapping[str, ModelFamily],
|
| 1108 |
+
) -> dict[str, RuntimeAsset]:
|
| 1109 |
+
if not isinstance(raw, list) or not raw:
|
| 1110 |
+
raise RegistryError("The manifest must contain at least one [[runtime_assets]] entry.")
|
| 1111 |
+
result: dict[str, RuntimeAsset] = {}
|
| 1112 |
+
identities: set[tuple[str, str, str]] = set()
|
| 1113 |
+
for index, value in enumerate(raw):
|
| 1114 |
+
context = f"runtime_assets[{index}]"
|
| 1115 |
+
if not isinstance(value, dict):
|
| 1116 |
+
raise RegistryError(f"{context} must be a table.")
|
| 1117 |
+
_reject_unknown_fields(value, _RUNTIME_ASSET_FIELDS, context)
|
| 1118 |
+
asset_id = _require_str(value, "id", context)
|
| 1119 |
+
if _IDENTIFIER_RE.fullmatch(asset_id) is None:
|
| 1120 |
+
raise RegistryError(f"Invalid runtime asset ID: {asset_id!r}")
|
| 1121 |
+
if asset_id in result:
|
| 1122 |
+
raise RegistryError(f"Duplicate runtime asset ID: {asset_id!r}")
|
| 1123 |
+
repository = _require_str(value, "repository", context)
|
| 1124 |
+
if _REPOSITORY_ID_RE.fullmatch(repository) is None:
|
| 1125 |
+
raise RegistryError(f"{context}.repository must be a Hugging Face repository ID.")
|
| 1126 |
+
revision = _require_str(value, "revision", context)
|
| 1127 |
+
_validate_revision(revision, f"{context}.revision")
|
| 1128 |
+
path = _require_str(value, "path", context)
|
| 1129 |
+
try:
|
| 1130 |
+
normalized_path = _portable_relative_path(path, "Runtime asset path")
|
| 1131 |
+
except RegistryError as error:
|
| 1132 |
+
raise RegistryError(f"Runtime asset path is not portable: {path!r}") from error
|
| 1133 |
+
sha256 = _require_str(value, "sha256", context)
|
| 1134 |
+
if len(sha256) != 64 or _HEX_RE.fullmatch(sha256) is None:
|
| 1135 |
+
raise RegistryError(f"Invalid runtime asset SHA-256 for {path!r}.")
|
| 1136 |
+
size = value.get("size")
|
| 1137 |
+
if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
|
| 1138 |
+
raise RegistryError(f"{context}.size must be a positive byte count.")
|
| 1139 |
+
consumer_family = _require_str(value, "consumer_family", context)
|
| 1140 |
+
if consumer_family not in families:
|
| 1141 |
+
raise RegistryError(
|
| 1142 |
+
f"{context}.consumer_family references unknown family {consumer_family!r}."
|
| 1143 |
+
)
|
| 1144 |
+
trust_kind = cast(
|
| 1145 |
+
RuntimeAssetTrustKind,
|
| 1146 |
+
_require_enum(
|
| 1147 |
+
value,
|
| 1148 |
+
"trust_kind",
|
| 1149 |
+
context,
|
| 1150 |
+
_ALLOWED_RUNTIME_ASSET_TRUST_KINDS,
|
| 1151 |
+
),
|
| 1152 |
+
)
|
| 1153 |
+
license_expression = _require_str(value, "license", context)
|
| 1154 |
+
offline_behavior = _require_str(value, "offline_behavior", context)
|
| 1155 |
+
if offline_behavior not in _ALLOWED_RUNTIME_ASSET_OFFLINE_BEHAVIORS:
|
| 1156 |
+
raise RegistryError(
|
| 1157 |
+
f"{context}.offline_behavior is unsupported: {offline_behavior!r}."
|
| 1158 |
+
)
|
| 1159 |
+
if trust_kind == "hash_pinned_pickle" and normalized_path.suffix != ".pkl":
|
| 1160 |
+
raise RegistryError(
|
| 1161 |
+
f"{context}.path must end in '.pkl' for trust_kind='hash_pinned_pickle'."
|
| 1162 |
+
)
|
| 1163 |
+
identity = (repository, revision, path)
|
| 1164 |
+
if identity in identities:
|
| 1165 |
+
raise RegistryError(f"Duplicate runtime asset identity: {identity!r}")
|
| 1166 |
+
identities.add(identity)
|
| 1167 |
+
result[asset_id] = RuntimeAsset(
|
| 1168 |
+
id=asset_id,
|
| 1169 |
+
repository=repository,
|
| 1170 |
+
revision=revision,
|
| 1171 |
+
path=path,
|
| 1172 |
+
sha256=sha256,
|
| 1173 |
+
size=size,
|
| 1174 |
+
consumer_family=consumer_family,
|
| 1175 |
+
trust_kind=trust_kind,
|
| 1176 |
+
license_expression=license_expression,
|
| 1177 |
+
offline_behavior=offline_behavior,
|
| 1178 |
+
)
|
| 1179 |
+
return result
|
| 1180 |
+
|
| 1181 |
+
|
| 1182 |
+
def _parse_models(
|
| 1183 |
+
raw: object,
|
| 1184 |
+
families: Mapping[str, ModelFamily],
|
| 1185 |
+
) -> dict[str, ModelSpec]:
|
| 1186 |
+
if not isinstance(raw, list) or not raw:
|
| 1187 |
+
raise RegistryError("The manifest must contain at least one [[models]] entry.")
|
| 1188 |
+
result: dict[str, ModelSpec] = {}
|
| 1189 |
+
fast_repositories: set[str] = set()
|
| 1190 |
+
for index, value in enumerate(raw):
|
| 1191 |
+
context = f"models[{index}]"
|
| 1192 |
+
if not isinstance(value, dict):
|
| 1193 |
+
raise RegistryError(f"{context} must be a table.")
|
| 1194 |
+
_reject_unknown_fields(value, _MODEL_FIELDS, context)
|
| 1195 |
+
model_id = _require_str(value, "id", context)
|
| 1196 |
+
if _IDENTIFIER_RE.fullmatch(model_id) is None:
|
| 1197 |
+
raise RegistryError(f"Invalid model ID: {model_id!r}")
|
| 1198 |
+
if model_id in result:
|
| 1199 |
+
raise RegistryError(f"Duplicate model ID: {model_id!r}")
|
| 1200 |
+
family_id = _require_str(value, "family", context)
|
| 1201 |
+
if family_id not in families:
|
| 1202 |
+
raise RegistryError(f"{context} references unknown family {family_id!r}.")
|
| 1203 |
+
fast = _parse_checkpoint(value, "fast", context)
|
| 1204 |
+
official = _parse_checkpoint(value, "official", context)
|
| 1205 |
+
if fast.repo_id in fast_repositories:
|
| 1206 |
+
raise RegistryError(f"Duplicate FastPLMs repository ID: {fast.repo_id!r}")
|
| 1207 |
+
fast_repositories.add(fast.repo_id)
|
| 1208 |
+
family = families[family_id]
|
| 1209 |
+
oracle_assets = _parse_oracle_assets(value, context)
|
| 1210 |
+
official_golden = _parse_official_golden(value, model_id, context)
|
| 1211 |
+
size_category = _require_str(value, "size_category", context)
|
| 1212 |
+
if size_category not in _ALLOWED_SIZE_CATEGORIES:
|
| 1213 |
+
raise RegistryError(f"Unsupported size category in {context}: {size_category!r}")
|
| 1214 |
+
generation_contract = cast(
|
| 1215 |
+
GenerationContract,
|
| 1216 |
+
_require_enum(
|
| 1217 |
+
value,
|
| 1218 |
+
"generation_contract",
|
| 1219 |
+
context,
|
| 1220 |
+
_ALLOWED_GENERATION_CONTRACTS,
|
| 1221 |
+
),
|
| 1222 |
+
)
|
| 1223 |
+
if family.tokenizer_mode == "structure" and size_category != "structure":
|
| 1224 |
+
raise RegistryError(
|
| 1225 |
+
f"Structure checkpoint {model_id!r} must use size_category='structure'."
|
| 1226 |
+
)
|
| 1227 |
+
artifact_source = value.get("artifact_source", "fast")
|
| 1228 |
+
if artifact_source not in {"fast", "official"}:
|
| 1229 |
+
raise RegistryError(f"{context}.artifact_source must be 'fast' or 'official'.")
|
| 1230 |
+
canonical_state_sha256 = value.get("canonical_state_sha256")
|
| 1231 |
+
if artifact_source == "official":
|
| 1232 |
+
if (
|
| 1233 |
+
not isinstance(canonical_state_sha256, str)
|
| 1234 |
+
or len(canonical_state_sha256) != 64
|
| 1235 |
+
or _HEX_RE.fullmatch(canonical_state_sha256) is None
|
| 1236 |
+
):
|
| 1237 |
+
raise RegistryError(
|
| 1238 |
+
f"{context}.canonical_state_sha256 must be a SHA-256 commitment "
|
| 1239 |
+
"for an official-source artifact."
|
| 1240 |
+
)
|
| 1241 |
+
elif canonical_state_sha256 is not None:
|
| 1242 |
+
raise RegistryError(
|
| 1243 |
+
f"{context}.canonical_state_sha256 is restricted to official-source artifacts."
|
| 1244 |
+
)
|
| 1245 |
+
if family.tokenizer_mode == "tokenizer" and not any(
|
| 1246 |
+
"tokenizer" in item.path or "vocab" in item.path for item in fast.files
|
| 1247 |
+
):
|
| 1248 |
+
raise RegistryError(f"{context} does not pin a tokenizer asset.")
|
| 1249 |
+
tokenizer_source_id = value.get("tokenizer_source")
|
| 1250 |
+
if tokenizer_source_id is not None and (
|
| 1251 |
+
family.tokenizer_mode != "tokenizer"
|
| 1252 |
+
or not isinstance(tokenizer_source_id, str)
|
| 1253 |
+
or _IDENTIFIER_RE.fullmatch(tokenizer_source_id) is None
|
| 1254 |
+
):
|
| 1255 |
+
raise RegistryError(f"{context}.tokenizer_source is invalid.")
|
| 1256 |
+
notes = value.get("notes", "")
|
| 1257 |
+
if not isinstance(notes, str):
|
| 1258 |
+
raise RegistryError(f"{context}.notes must be a string.")
|
| 1259 |
+
msa_conditioning = value.get("msa_conditioning")
|
| 1260 |
+
if family_id == "esmfold2":
|
| 1261 |
+
if not isinstance(msa_conditioning, bool):
|
| 1262 |
+
raise RegistryError(
|
| 1263 |
+
f"{context}.msa_conditioning must be an explicit boolean for "
|
| 1264 |
+
"ESMFold2 checkpoints."
|
| 1265 |
+
)
|
| 1266 |
+
elif "msa_conditioning" in value:
|
| 1267 |
+
raise RegistryError(
|
| 1268 |
+
f"{context}.msa_conditioning is only valid for ESMFold2 checkpoints."
|
| 1269 |
+
)
|
| 1270 |
+
raw_auto_map = value.get("auto_map")
|
| 1271 |
+
auto_map: list[tuple[str, str]] = []
|
| 1272 |
+
if raw_auto_map is not None:
|
| 1273 |
+
if not isinstance(raw_auto_map, dict) or not raw_auto_map:
|
| 1274 |
+
raise RegistryError(f"{context}.auto_map must be a non-empty table.")
|
| 1275 |
+
for auto_class, class_path in raw_auto_map.items():
|
| 1276 |
+
if auto_class not in _ALLOWED_AUTO_CLASSES or not isinstance(class_path, str):
|
| 1277 |
+
raise RegistryError(f"Invalid AutoClass mapping in {context}: {auto_class!r}")
|
| 1278 |
+
if not class_path.startswith("fastplms.") or class_path.count(".") < 2:
|
| 1279 |
+
raise RegistryError(f"Invalid Python class path in {context}: {class_path!r}")
|
| 1280 |
+
auto_map.append((auto_class, class_path))
|
| 1281 |
+
result[model_id] = ModelSpec(
|
| 1282 |
+
id=model_id,
|
| 1283 |
+
family=family,
|
| 1284 |
+
fast=fast,
|
| 1285 |
+
official=official,
|
| 1286 |
+
size_category=size_category,
|
| 1287 |
+
generation_contract=generation_contract,
|
| 1288 |
+
oracle_assets=oracle_assets,
|
| 1289 |
+
official_golden=official_golden,
|
| 1290 |
+
artifact_source=artifact_source,
|
| 1291 |
+
canonical_state_sha256=canonical_state_sha256,
|
| 1292 |
+
tokenizer_source_id=tokenizer_source_id,
|
| 1293 |
+
auto_map_items=tuple(auto_map),
|
| 1294 |
+
notes=notes,
|
| 1295 |
+
msa_conditioning=msa_conditioning,
|
| 1296 |
+
)
|
| 1297 |
+
return result
|
| 1298 |
+
|
| 1299 |
+
|
| 1300 |
+
def _validate_registry(
|
| 1301 |
+
upstreams: Mapping[str, UpstreamSource],
|
| 1302 |
+
attention_kernels: Mapping[str, AttentionKernelSpec],
|
| 1303 |
+
families: Mapping[str, ModelFamily],
|
| 1304 |
+
models: Mapping[str, ModelSpec],
|
| 1305 |
+
) -> None:
|
| 1306 |
+
for spec in models.values():
|
| 1307 |
+
if spec.tokenizer_source_id is None:
|
| 1308 |
+
continue
|
| 1309 |
+
source = models.get(spec.tokenizer_source_id)
|
| 1310 |
+
if source is None:
|
| 1311 |
+
raise RegistryError(
|
| 1312 |
+
f"Model {spec.id!r} references unknown tokenizer source "
|
| 1313 |
+
f"{spec.tokenizer_source_id!r}."
|
| 1314 |
+
)
|
| 1315 |
+
if not any(
|
| 1316 |
+
PurePosixPath(item.path).name
|
| 1317 |
+
in {
|
| 1318 |
+
"added_tokens.json",
|
| 1319 |
+
"merges.txt",
|
| 1320 |
+
"sentencepiece.bpe.model",
|
| 1321 |
+
"special_tokens_map.json",
|
| 1322 |
+
"spiece.model",
|
| 1323 |
+
"tokenizer.json",
|
| 1324 |
+
"tokenizer_config.json",
|
| 1325 |
+
"vocab.json",
|
| 1326 |
+
"vocab.txt",
|
| 1327 |
+
}
|
| 1328 |
+
for item in source.official.files
|
| 1329 |
+
):
|
| 1330 |
+
raise RegistryError(
|
| 1331 |
+
f"Tokenizer source {source.id!r} has no official tokenizer assets."
|
| 1332 |
+
)
|
| 1333 |
+
expected_esmfold2 = {
|
| 1334 |
+
"esmfold2": ("Synthyra/ESMFold2", "biohub/ESMFold2"),
|
| 1335 |
+
"esmfold2_fast": ("Synthyra/ESMFold2-Fast", "biohub/ESMFold2-Fast"),
|
| 1336 |
+
"esmfold2_experimental_cutoff2025": (
|
| 1337 |
+
"Synthyra/ESMFold2-Experimental-Cutoff2025",
|
| 1338 |
+
"biohub/ESMFold2-Experimental-Cutoff2025",
|
| 1339 |
+
),
|
| 1340 |
+
"esmfold2_experimental_fast_cutoff2025": (
|
| 1341 |
+
"Synthyra/ESMFold2-Experimental-Fast-Cutoff2025",
|
| 1342 |
+
"biohub/ESMFold2-Experimental-Fast-Cutoff2025",
|
| 1343 |
+
),
|
| 1344 |
+
}
|
| 1345 |
+
actual_esmfold2 = {
|
| 1346 |
+
model.id: (model.fast.repo_id, model.official.repo_id)
|
| 1347 |
+
for model in models.values()
|
| 1348 |
+
if model.family.id == "esmfold2"
|
| 1349 |
+
}
|
| 1350 |
+
if actual_esmfold2 != expected_esmfold2:
|
| 1351 |
+
raise RegistryError(
|
| 1352 |
+
"ESMFold2 support must contain exactly the four approved model IDs and "
|
| 1353 |
+
"official/Synthyra repositories."
|
| 1354 |
+
)
|
| 1355 |
+
|
| 1356 |
+
golden_paths: list[str] = []
|
| 1357 |
+
for model in models.values():
|
| 1358 |
+
if model.official_golden is not None:
|
| 1359 |
+
golden_paths.extend(
|
| 1360 |
+
(
|
| 1361 |
+
model.official_golden.metadata.path,
|
| 1362 |
+
model.official_golden.tensors.path,
|
| 1363 |
+
)
|
| 1364 |
+
)
|
| 1365 |
+
if len(golden_paths) != len(set(golden_paths)):
|
| 1366 |
+
raise RegistryError("Official golden paths must be unique across model declarations.")
|
| 1367 |
+
unused_upstreams = sorted(
|
| 1368 |
+
set(upstreams).difference(
|
| 1369 |
+
source for family in families.values() for source in family.upstreams
|
| 1370 |
+
)
|
| 1371 |
+
)
|
| 1372 |
+
if unused_upstreams:
|
| 1373 |
+
raise RegistryError(
|
| 1374 |
+
f"Upstream sources are not connected to a model family: {unused_upstreams}"
|
| 1375 |
+
)
|
| 1376 |
+
advertised_flash = {
|
| 1377 |
+
implementation
|
| 1378 |
+
for family in families.values()
|
| 1379 |
+
for implementation in family.attention
|
| 1380 |
+
if implementation.startswith("flash_attention_")
|
| 1381 |
+
}
|
| 1382 |
+
missing_kernels = sorted(advertised_flash.difference(attention_kernels))
|
| 1383 |
+
if missing_kernels:
|
| 1384 |
+
raise RegistryError(
|
| 1385 |
+
f"Advertised FlashAttention backends lack kernel specs: {missing_kernels}."
|
| 1386 |
+
)
|
| 1387 |
+
for family in families.values():
|
| 1388 |
+
for implementation in family.attention:
|
| 1389 |
+
kernel = attention_kernels.get(implementation)
|
| 1390 |
+
if kernel is not None and not set(family.dtypes).intersection(kernel.dtypes):
|
| 1391 |
+
raise RegistryError(
|
| 1392 |
+
f"Family {family.id!r} and attention kernel {implementation!r} "
|
| 1393 |
+
"have no supported dtype in common."
|
| 1394 |
+
)
|
| 1395 |
+
family_models = [model for model in models.values() if model.family.id == family.id]
|
| 1396 |
+
if not family_models:
|
| 1397 |
+
raise RegistryError(f"Family {family.id!r} has no checkpoints.")
|
| 1398 |
+
representative = models.get(family.representative)
|
| 1399 |
+
if representative is None or representative.family.id != family.id:
|
| 1400 |
+
raise RegistryError(
|
| 1401 |
+
f"Family {family.id!r} has invalid representative {family.representative!r}."
|
| 1402 |
+
)
|
| 1403 |
+
if family.backbone_model is not None and family.backbone_model not in models:
|
| 1404 |
+
raise RegistryError(
|
| 1405 |
+
f"Family {family.id!r} references unknown backbone model "
|
| 1406 |
+
f"{family.backbone_model!r}."
|
| 1407 |
+
)
|
| 1408 |
+
|
| 1409 |
+
|
| 1410 |
+
def _load_manifest_bytes(raw_bytes: bytes) -> ModelRegistry:
|
| 1411 |
+
try:
|
| 1412 |
+
data = tomllib.loads(raw_bytes.decode("utf-8"))
|
| 1413 |
+
except (UnicodeDecodeError, tomllib.TOMLDecodeError) as error:
|
| 1414 |
+
raise RegistryError(f"Unable to parse model manifest: {error}") from error
|
| 1415 |
+
_reject_unknown_fields(data, _ROOT_FIELDS, "manifest")
|
| 1416 |
+
if data.get("schema_version") != 1:
|
| 1417 |
+
raise RegistryError("Unsupported model manifest schema_version; expected 1.")
|
| 1418 |
+
legal_files = _require_digest_list(data, "legal_files", "manifest")
|
| 1419 |
+
required_legal_paths = {"LICENSE", "THIRD_PARTY_NOTICES.md"}
|
| 1420 |
+
if {item.path for item in legal_files} != required_legal_paths:
|
| 1421 |
+
raise RegistryError("manifest.legal_files must contain LICENSE and THIRD_PARTY_NOTICES.md.")
|
| 1422 |
+
attention_kernels = _parse_attention_kernels(data.get("attention_kernels"))
|
| 1423 |
+
upstreams = _parse_upstreams(data.get("upstreams"))
|
| 1424 |
+
families = _parse_families(data.get("families"), upstreams)
|
| 1425 |
+
runtime_assets = _parse_runtime_assets(data.get("runtime_assets"), families)
|
| 1426 |
+
models = _parse_models(data.get("models"), families)
|
| 1427 |
+
_validate_registry(upstreams, attention_kernels, families, models)
|
| 1428 |
+
return ModelRegistry(
|
| 1429 |
+
schema_version=1,
|
| 1430 |
+
upstreams=upstreams,
|
| 1431 |
+
attention_kernels=attention_kernels,
|
| 1432 |
+
families=families,
|
| 1433 |
+
models=models,
|
| 1434 |
+
runtime_assets=runtime_assets,
|
| 1435 |
+
legal_files=legal_files,
|
| 1436 |
+
)
|
| 1437 |
+
|
| 1438 |
+
|
| 1439 |
+
def load_model_registry(path: str | Path | None = None) -> ModelRegistry:
|
| 1440 |
+
"""Load and validate a model manifest without importing model code."""
|
| 1441 |
+
|
| 1442 |
+
if path is None:
|
| 1443 |
+
manifest = resources.files("fastplms").joinpath("models.toml")
|
| 1444 |
+
return _load_manifest_bytes(manifest.read_bytes())
|
| 1445 |
+
return _load_manifest_bytes(Path(path).read_bytes())
|
| 1446 |
+
|
| 1447 |
+
|
| 1448 |
+
@lru_cache(maxsize=1)
|
| 1449 |
+
def get_model_registry() -> ModelRegistry:
|
| 1450 |
+
"""Return the validated package registry, cached after its first read."""
|
| 1451 |
+
|
| 1452 |
+
return load_model_registry()
|
| 1453 |
+
|
| 1454 |
+
|
| 1455 |
+
def get_model_spec(model_id: str) -> ModelSpec:
|
| 1456 |
+
"""Return one model specification by its stable manifest ID."""
|
| 1457 |
+
|
| 1458 |
+
try:
|
| 1459 |
+
return get_model_registry()[model_id]
|
| 1460 |
+
except KeyError as error:
|
| 1461 |
+
supported = ", ".join(get_model_registry())
|
| 1462 |
+
raise KeyError(
|
| 1463 |
+
f"Unknown FastPLMs model ID {model_id!r}. Supported IDs: {supported}"
|
| 1464 |
+
) from error
|
| 1465 |
+
|
| 1466 |
+
|
| 1467 |
+
__all__ = [
|
| 1468 |
+
"HUB_LICENSE_IDENTIFIERS",
|
| 1469 |
+
"CheckpointSource",
|
| 1470 |
+
"FileDigest",
|
| 1471 |
+
"GenerationContract",
|
| 1472 |
+
"ModelFamily",
|
| 1473 |
+
"ModelRegistry",
|
| 1474 |
+
"ModelSpec",
|
| 1475 |
+
"OracleAsset",
|
| 1476 |
+
"RegistryError",
|
| 1477 |
+
"RuntimeAsset",
|
| 1478 |
+
"RuntimeAssetTrustKind",
|
| 1479 |
+
"RuntimeExtra",
|
| 1480 |
+
"TestTier",
|
| 1481 |
+
"UpstreamSource",
|
| 1482 |
+
"VramTier",
|
| 1483 |
+
"get_model_registry",
|
| 1484 |
+
"get_model_spec",
|
| 1485 |
+
"load_model_registry",
|
| 1486 |
+
]
|
fastplms/runtime.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Explicit, reversible Torch runtime configuration.
|
| 2 |
+
|
| 3 |
+
Importing FastPLMs does not change global Torch settings. Callers that want a
|
| 4 |
+
runtime profile opt in with :func:`runtime_profile` and receive their previous
|
| 5 |
+
settings back when the context exits.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
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 |
+
|
| 17 |
+
|
| 18 |
+
MatmulPrecision = Literal["highest", "high", "medium"]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass(frozen=True, slots=True)
|
| 22 |
+
class RuntimeProfile:
|
| 23 |
+
"""Requested Torch settings for a bounded inference or training block."""
|
| 24 |
+
|
| 25 |
+
float32_matmul_precision: MatmulPrecision = "highest"
|
| 26 |
+
allow_tf32: bool | None = None
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@contextmanager
|
| 30 |
+
def runtime_profile(profile: RuntimeProfile | None = None) -> Iterator[None]:
|
| 31 |
+
"""Apply a Torch runtime profile and restore the previous global settings.
|
| 32 |
+
|
| 33 |
+
The default profile requests the highest float32 matrix-multiplication
|
| 34 |
+
precision and leaves TF32 policy unchanged. Torch is imported only when the
|
| 35 |
+
context is entered.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
import torch
|
| 39 |
+
|
| 40 |
+
selected = profile or RuntimeProfile()
|
| 41 |
+
previous_matmul_precision = torch.get_float32_matmul_precision()
|
| 42 |
+
matmul_backend = getattr(getattr(torch.backends, "cuda", None), "matmul", None)
|
| 43 |
+
cudnn_backend = getattr(torch.backends, "cudnn", None)
|
| 44 |
+
previous_matmul_tf32 = (
|
| 45 |
+
getattr(matmul_backend, "allow_tf32", None) if matmul_backend is not None else None
|
| 46 |
+
)
|
| 47 |
+
previous_cudnn_tf32 = (
|
| 48 |
+
getattr(cudnn_backend, "allow_tf32", None) if cudnn_backend is not None else None
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
torch.set_float32_matmul_precision(selected.float32_matmul_precision)
|
| 52 |
+
if selected.allow_tf32 is not None:
|
| 53 |
+
if matmul_backend is not None and hasattr(matmul_backend, "allow_tf32"):
|
| 54 |
+
matmul_backend.allow_tf32 = selected.allow_tf32
|
| 55 |
+
if cudnn_backend is not None and hasattr(cudnn_backend, "allow_tf32"):
|
| 56 |
+
cudnn_backend.allow_tf32 = selected.allow_tf32
|
| 57 |
+
try:
|
| 58 |
+
yield
|
| 59 |
+
finally:
|
| 60 |
+
torch.set_float32_matmul_precision(previous_matmul_precision)
|
| 61 |
+
if selected.allow_tf32 is not None:
|
| 62 |
+
if matmul_backend is not None and previous_matmul_tf32 is not None:
|
| 63 |
+
matmul_backend.allow_tf32 = previous_matmul_tf32
|
| 64 |
+
if cudnn_backend is not None and previous_cudnn_tf32 is not None:
|
| 65 |
+
cudnn_backend.allow_tf32 = previous_cudnn_tf32
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
__all__ = ["MatmulPrecision", "RuntimeProfile", "runtime_profile"]
|
fastplms_bundle.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
modeling_fastplms.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generated bridge to the unchanged FastPLMs package sources."""
|
| 2 |
+
|
| 3 |
+
import base64
|
| 4 |
+
import hashlib
|
| 5 |
+
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 != "3ca371517961576b5af3c51ca4f07e486d91a11c8da3b0d7916d0bbc8f76be72":
|
| 17 |
+
raise RuntimeError("FastPLMs runtime identity differs from the bridge.")
|
| 18 |
+
|
| 19 |
+
_RUNTIME_TEMPORARIES = []
|
| 20 |
+
|
| 21 |
+
def _archive_runtime_hashes(payload):
|
| 22 |
+
result = {}
|
| 23 |
+
with ZipFile(BytesIO(payload)) as archive:
|
| 24 |
+
for member in archive.infolist():
|
| 25 |
+
name = member.filename
|
| 26 |
+
parts = Path(name).parts
|
| 27 |
+
if (
|
| 28 |
+
member.is_dir()
|
| 29 |
+
or "\\" in name
|
| 30 |
+
or not parts
|
| 31 |
+
or parts[0] != "fastplms"
|
| 32 |
+
or len(parts) < 2
|
| 33 |
+
or any(part in {"", ".", ".."} for part in parts)
|
| 34 |
+
or Path(name).suffix in {".pyc", ".pyo"}
|
| 35 |
+
or member.flag_bits & 0x1
|
| 36 |
+
or member.compress_type != ZIP_DEFLATED
|
| 37 |
+
or member.external_attr >> 16 != 0o100644
|
| 38 |
+
):
|
| 39 |
+
raise RuntimeError("Embedded FastPLMs archive has an unsafe path.")
|
| 40 |
+
relative = Path(*parts[1:]).as_posix()
|
| 41 |
+
if relative in result:
|
| 42 |
+
raise RuntimeError("Embedded FastPLMs archive repeats a path.")
|
| 43 |
+
result[relative] = hashlib.sha256(archive.read(member)).hexdigest()
|
| 44 |
+
return result
|
| 45 |
+
|
| 46 |
+
def _ensure_runtime():
|
| 47 |
+
payload = base64.b85decode("".join(RUNTIME_DATA))
|
| 48 |
+
if hashlib.sha256(payload).hexdigest() != RUNTIME_HASH:
|
| 49 |
+
raise RuntimeError("Embedded FastPLMs runtime hash mismatch.")
|
| 50 |
+
expected = _archive_runtime_hashes(payload)
|
| 51 |
+
temporary = tempfile.TemporaryDirectory(prefix="fastplms-artifact-runtime-")
|
| 52 |
+
try:
|
| 53 |
+
runtime_root = Path(temporary.name)
|
| 54 |
+
with ZipFile(BytesIO(payload)) as archive:
|
| 55 |
+
for member in archive.infolist():
|
| 56 |
+
target = runtime_root.joinpath(*Path(member.filename).parts)
|
| 57 |
+
target.parent.mkdir(parents=True, exist_ok=True)
|
| 58 |
+
with target.open("xb") as handle:
|
| 59 |
+
handle.write(archive.read(member))
|
| 60 |
+
package_root = runtime_root / "fastplms"
|
| 61 |
+
if _runtime_file_hashes(package_root) != expected:
|
| 62 |
+
raise RuntimeError(
|
| 63 |
+
"Private FastPLMs runtime differs from the embedded archive."
|
| 64 |
+
)
|
| 65 |
+
except BaseException:
|
| 66 |
+
temporary.cleanup()
|
| 67 |
+
raise
|
| 68 |
+
_RUNTIME_TEMPORARIES.append(temporary)
|
| 69 |
+
return package_root
|
| 70 |
+
|
| 71 |
+
def _runtime_file_hashes(package_root):
|
| 72 |
+
result = {}
|
| 73 |
+
for path in sorted(package_root.rglob("*")):
|
| 74 |
+
relative = path.relative_to(package_root)
|
| 75 |
+
if path.is_symlink():
|
| 76 |
+
raise RuntimeError("Private FastPLMs runtime contains a symlink.")
|
| 77 |
+
if path.is_dir():
|
| 78 |
+
continue
|
| 79 |
+
if path.suffix in {".pyc", ".pyo"}:
|
| 80 |
+
raise RuntimeError("Private FastPLMs runtime contains bytecode.")
|
| 81 |
+
if not path.is_file():
|
| 82 |
+
raise RuntimeError("Private FastPLMs runtime contains a non-file entry.")
|
| 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."):
|
| 107 |
+
continue
|
| 108 |
+
paths = getattr(module, "__path__", None)
|
| 109 |
+
if paths is None:
|
| 110 |
+
continue
|
| 111 |
+
relative = name.split(".")[1:]
|
| 112 |
+
candidate = package_root.joinpath(*relative)
|
| 113 |
+
candidate_text = str(candidate)
|
| 114 |
+
if candidate.is_dir() and candidate_text not in paths:
|
| 115 |
+
paths.append(candidate_text)
|
| 116 |
+
|
| 117 |
+
def _merge_runtime(installed, package_root):
|
| 118 |
+
incoming = _runtime_file_hashes(package_root)
|
| 119 |
+
known = dict(getattr(installed, "__fastplms_artifact_runtime_files__", {}))
|
| 120 |
+
installed_root_text = getattr(
|
| 121 |
+
installed, "__fastplms_artifact_installed_root__", None
|
| 122 |
+
)
|
| 123 |
+
if not known:
|
| 124 |
+
installed_file = getattr(installed, "__file__", None)
|
| 125 |
+
if installed_file is None:
|
| 126 |
+
raise RuntimeError(
|
| 127 |
+
"The loaded fastplms package has no source path and cannot be verified "
|
| 128 |
+
"against the embedded artifact runtime."
|
| 129 |
+
)
|
| 130 |
+
installed_root = Path(installed_file).resolve().parent
|
| 131 |
+
for relative, digest in incoming.items():
|
| 132 |
+
if _installed_runtime_digest(installed_root, relative) != digest:
|
| 133 |
+
raise RuntimeError(
|
| 134 |
+
"The installed FastPLMs runtime differs from this artifact at "
|
| 135 |
+
f"{relative!r}. Install the artifact's matching FastPLMs release "
|
| 136 |
+
"or use a separate Python process."
|
| 137 |
+
)
|
| 138 |
+
installed_root_text = str(installed_root)
|
| 139 |
+
installed.__fastplms_artifact_installed_root__ = installed_root_text
|
| 140 |
+
conflicts = sorted(
|
| 141 |
+
relative
|
| 142 |
+
for relative, digest in incoming.items()
|
| 143 |
+
if relative in known and known[relative] != digest
|
| 144 |
+
)
|
| 145 |
+
if conflicts:
|
| 146 |
+
raise RuntimeError(
|
| 147 |
+
"FastPLMs artifacts contain incompatible runtime sources at "
|
| 148 |
+
+ ", ".join(repr(path) for path in conflicts[:5])
|
| 149 |
+
+ ". Load incompatible releases in separate Python processes."
|
| 150 |
+
)
|
| 151 |
+
if installed_root_text is not None:
|
| 152 |
+
installed_root = Path(installed_root_text)
|
| 153 |
+
for relative, digest in incoming.items():
|
| 154 |
+
if relative in known:
|
| 155 |
+
continue
|
| 156 |
+
if _installed_runtime_digest(installed_root, relative) != digest:
|
| 157 |
+
raise RuntimeError(
|
| 158 |
+
"The installed FastPLMs runtime differs from this artifact at "
|
| 159 |
+
f"{relative!r}. Install the artifact's matching FastPLMs release "
|
| 160 |
+
"or use a separate Python process."
|
| 161 |
+
)
|
| 162 |
+
known.update(incoming)
|
| 163 |
+
installed.__fastplms_artifact_runtime_files__ = known
|
| 164 |
+
roots = list(getattr(installed, "__fastplms_artifact_runtime_roots__", ()))
|
| 165 |
+
if str(package_root) not in roots:
|
| 166 |
+
roots.append(str(package_root))
|
| 167 |
+
installed.__fastplms_artifact_runtime_roots__ = tuple(roots)
|
| 168 |
+
temporaries = list(
|
| 169 |
+
getattr(installed, "__fastplms_artifact_runtime_temporaries__", ())
|
| 170 |
+
)
|
| 171 |
+
for temporary in _RUNTIME_TEMPORARIES:
|
| 172 |
+
if temporary not in temporaries:
|
| 173 |
+
temporaries.append(temporary)
|
| 174 |
+
installed.__fastplms_artifact_runtime_temporaries__ = tuple(temporaries)
|
| 175 |
+
hashes = set(getattr(installed, "__fastplms_artifact_runtime_hashes__", ()))
|
| 176 |
+
hashes.add(RUNTIME_HASH)
|
| 177 |
+
installed.__fastplms_artifact_runtime_hashes__ = frozenset(hashes)
|
| 178 |
+
_extend_loaded_package_paths(package_root)
|
| 179 |
+
return installed
|
| 180 |
+
|
| 181 |
+
def _import_without_bytecode(module_name):
|
| 182 |
+
previous = sys.dont_write_bytecode
|
| 183 |
+
sys.dont_write_bytecode = True
|
| 184 |
+
try:
|
| 185 |
+
return importlib.import_module(module_name)
|
| 186 |
+
finally:
|
| 187 |
+
sys.dont_write_bytecode = previous
|
| 188 |
+
|
| 189 |
+
def _install_runtime():
|
| 190 |
+
installed = sys.modules.get("fastplms")
|
| 191 |
+
hashes = getattr(installed, "__fastplms_artifact_runtime_hashes__", ())
|
| 192 |
+
if RUNTIME_HASH in hashes:
|
| 193 |
+
return installed
|
| 194 |
+
package_root = _ensure_runtime()
|
| 195 |
+
if installed is not None:
|
| 196 |
+
return _merge_runtime(installed, package_root)
|
| 197 |
+
spec = importlib.util.spec_from_file_location(
|
| 198 |
+
"fastplms",
|
| 199 |
+
package_root / "__init__.py",
|
| 200 |
+
submodule_search_locations=[str(package_root)],
|
| 201 |
+
)
|
| 202 |
+
if spec is None or spec.loader is None:
|
| 203 |
+
raise ImportError("Unable to load the embedded FastPLMs runtime.")
|
| 204 |
+
package = importlib.util.module_from_spec(spec)
|
| 205 |
+
package.__fastplms_artifact_runtime_hash__ = RUNTIME_HASH
|
| 206 |
+
package.__fastplms_artifact_runtime_hashes__ = frozenset({RUNTIME_HASH})
|
| 207 |
+
package.__fastplms_artifact_runtime_files__ = _runtime_file_hashes(package_root)
|
| 208 |
+
package.__fastplms_artifact_runtime_roots__ = (str(package_root),)
|
| 209 |
+
package.__fastplms_artifact_runtime_temporaries__ = tuple(
|
| 210 |
+
_RUNTIME_TEMPORARIES
|
| 211 |
+
)
|
| 212 |
+
sys.modules["fastplms"] = package
|
| 213 |
+
previous = sys.dont_write_bytecode
|
| 214 |
+
sys.dont_write_bytecode = True
|
| 215 |
+
try:
|
| 216 |
+
try:
|
| 217 |
+
spec.loader.exec_module(package)
|
| 218 |
+
except BaseException:
|
| 219 |
+
sys.modules.pop("fastplms", None)
|
| 220 |
+
raise
|
| 221 |
+
finally:
|
| 222 |
+
sys.dont_write_bytecode = previous
|
| 223 |
+
return package
|
| 224 |
+
|
| 225 |
+
_install_runtime()
|
| 226 |
+
_module_225 = _import_without_bytecode("fastplms.models.dplm2.modeling_dplm2")
|
| 227 |
+
DPLM2Config = _module_225.DPLM2Config
|
| 228 |
+
DPLM2Config.__module__ = __name__
|
| 229 |
+
DPLM2ForMaskedLM = _module_225.DPLM2ForMaskedLM
|
| 230 |
+
DPLM2ForMaskedLM.__module__ = __name__
|
| 231 |
+
DPLM2ForSequenceClassification = _module_225.DPLM2ForSequenceClassification
|
| 232 |
+
DPLM2ForSequenceClassification.__module__ = __name__
|
| 233 |
+
DPLM2ForTokenClassification = _module_225.DPLM2ForTokenClassification
|
| 234 |
+
DPLM2ForTokenClassification.__module__ = __name__
|
| 235 |
+
DPLM2Model = _module_225.DPLM2Model
|
| 236 |
+
DPLM2Model.__module__ = __name__
|
| 237 |
+
_module_236 = _import_without_bytecode("fastplms.models.dplm2.tokenization_dplm2")
|
| 238 |
+
DPLM2Tokenizer = _module_236.DPLM2Tokenizer
|
| 239 |
+
DPLM2Tokenizer.__module__ = __name__
|
runtime-attestation.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": {
|
| 3 |
+
"LICENSES/FastPLMs-Apache-2.0.txt": "sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
|
| 4 |
+
"LICENSES/dplm/LICENSE": "sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
|
| 5 |
+
"LICENSES/dplm/PROVENANCE.md": "sha256:a659f74be9073cf1ad2d2f7071531ca56959b421f111152cf4c41184ace5970e",
|
| 6 |
+
"README.md": "sha256:5f2a653497b6807eebe5d83681babe4629a5a4636545e9652e77aaf8b3595170",
|
| 7 |
+
"THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
|
| 8 |
+
"config.json": "sha256:50a24887d94862f1505c3f629794b69667d84c0c4aef7edeb43a8095179a6dfc",
|
| 9 |
+
"fastplms/__init__.py": "sha256:4fb3196022ca8ec699d59d09bdbc5f0184195552b773698ab9b061fe3cd7df12",
|
| 10 |
+
"fastplms/attention/__init__.py": "sha256:f60b9fecfb4bcb37a4e7c26dc2f752b9035f9cbad627b4a84213f3a92ec88f7d",
|
| 11 |
+
"fastplms/attention/_core.py": "sha256:8f7ec5b65bd8b6c6fa4951d50d1c0e499abf03ae00914794b51fc410201e3e33",
|
| 12 |
+
"fastplms/attention/_kernel_lock.py": "sha256:85d8521a2af5f94fad3948af3814db0c866c414ee4d43df797b9bd6f980e947b",
|
| 13 |
+
"fastplms/attention/interfaces.py": "sha256:1c6f06a8e411e0f9bf6d230522205c93ae46ea58864006fbb892aa05e5ca5749",
|
| 14 |
+
"fastplms/embeddings/__init__.py": "sha256:47ff8cdf682d44037dd9edab133e2e60675d60786bd5cf0bffd1998f31985555",
|
| 15 |
+
"fastplms/embeddings/pooling.py": "sha256:a140266ed6b1cc344c8507edc5c6c4f2dce464c3db70ba4b16c7ac2ba2fad96e",
|
| 16 |
+
"fastplms/embeddings/runner.py": "sha256:23ee4727a918d6d331f7a0f89b823d149f1a791f0c5586e3496d7b6eb2ce97e0",
|
| 17 |
+
"fastplms/embeddings/storage.py": "sha256:3fbe2bab75092e5a4cadf4d27e4752181d597469a65a55db085ceef808ed418e",
|
| 18 |
+
"fastplms/embeddings/types.py": "sha256:119718a20989d1ae5a60fabc0f5e98bdc172c5163b04db3d4554ac3956b30e52",
|
| 19 |
+
"fastplms/models.toml": "sha256:05a8399f084a5babb5f0916cee7e564c4030767f3ff0230c4f46e539209847d1",
|
| 20 |
+
"fastplms/models/__init__.py": "sha256:5e48c2cb3877aa6f42f3b5411d53b16bba2e32827bbde634f47f174c5cb36f86",
|
| 21 |
+
"fastplms/models/_diffusion_generation.py": "sha256:00a644adf604a1d7760b11f75be5a823bcbfbf894bc7a02b0640b96245b5ae5b",
|
| 22 |
+
"fastplms/models/_esm_rotary.py": "sha256:4dcee8af74adf2cfcabeeaadf4915f1da5fb6c0672d583bf3ee5478ac1290805",
|
| 23 |
+
"fastplms/models/dplm2/__init__.py": "sha256:1d823d50970e7dc9ad2e7e06b6532c70612ce533dca2bdc49c687d4707c41232",
|
| 24 |
+
"fastplms/models/dplm2/modeling_dplm2.py": "sha256:c3af6614cd85ac290c9c310b553c213755ffb05f2c6067cf3bddfaaf19e20d6b",
|
| 25 |
+
"fastplms/models/dplm2/tokenization_dplm2.py": "sha256:ce4ce471f751255dbd44e2ebbfaffe5875a99cea28dfc9d30582aa977cfa96d5",
|
| 26 |
+
"fastplms/models/ttt.py": "sha256:a0df4e98b02120d423e3c7ca9b866a8d0e3748b9076042a0102a838a11aed046",
|
| 27 |
+
"fastplms/registry.py": "sha256:afca271911b651a882345b74a58366494a1d784e4a48c8683a87f5508f4ba16e",
|
| 28 |
+
"fastplms/runtime.py": "sha256:110018646d6f248cedab140a030c3065e1b062b61f6aff659c231e538614bc01",
|
| 29 |
+
"fastplms_bundle.py": "sha256:e5a7998e7dcb788abcffc57eb3697fd1a445cbb9c00e8d7d33e0577d23172f3e",
|
| 30 |
+
"modeling_fastplms.py": "sha256:99e57753e146b9568fc5df47bf9c5d6a0bf5a3028a11c1acf3d4ebb1e4c1f1a2",
|
| 31 |
+
"special_tokens_map.json": "sha256:10d6448302e43bd18bc2b648a750ade73a1464d77340f44fc6360151f89bd593",
|
| 32 |
+
"tokenizer_config.json": "sha256:cdfc691425b5a32516704396ebc4bc1f604f2e18de354fbd608558f5b6097dc3",
|
| 33 |
+
"vocab.txt": "sha256:d6bd0a2b1a0bd0f8e10568cfd75443e62a4bcf794907e93f41a5f945baf3b900"
|
| 34 |
+
},
|
| 35 |
+
"model_id": "dplm2_150m",
|
| 36 |
+
"redistributable": true,
|
| 37 |
+
"release_tool_revision": "1b9ce023f1e06571cf3e6324be0610ffa53e0a4a",
|
| 38 |
+
"release_tool_sha256": "1459b5d7d13d9b07bd97b3eee764f2ce73623e15e32d07ddf6825c2a9509afb9",
|
| 39 |
+
"runtime_bundle_sha256": "3ca371517961576b5af3c51ca4f07e486d91a11c8da3b0d7916d0bbc8f76be72",
|
| 40 |
+
"runtime_revision": "1b9ce023f1e06571cf3e6324be0610ffa53e0a4a",
|
| 41 |
+
"schema_version": 2,
|
| 42 |
+
"scope": "runtime-only",
|
| 43 |
+
"source_tree_sha256": "5de51d1c5c03b023e3db0af2d6a8849140af8011164caf84722faa356428efe1",
|
| 44 |
+
"weights": {
|
| 45 |
+
"repo_id": "Synthyra/DPLM2-150M",
|
| 46 |
+
"revision": "182745b8dc5661f898481a4fa60a7af9d53385c4"
|
| 47 |
+
},
|
| 48 |
+
"weights_license_status": "resolved"
|
| 49 |
+
}
|
special_tokens_map.json
CHANGED
|
@@ -1,13 +1,11 @@
|
|
| 1 |
{
|
| 2 |
-
"
|
| 3 |
-
"
|
| 4 |
-
"
|
| 5 |
-
"
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
},
|
| 12 |
-
"unk_token": "<unk>"
|
| 13 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"aa_cls_token": "<cls_aa>",
|
| 3 |
+
"aa_eos_token": "<eos_aa>",
|
| 4 |
+
"aa_mask_token": "<mask_aa>",
|
| 5 |
+
"aa_unk_token": "<unk_aa>",
|
| 6 |
+
"struct_cls_token": "<cls_struct>",
|
| 7 |
+
"struct_eos_token": "<eos_struct>",
|
| 8 |
+
"struct_mask_token": "<mask_struct>",
|
| 9 |
+
"struct_unk_token": "<unk_struct>",
|
| 10 |
+
"pad_token": "<pad>"
|
|
|
|
|
|
|
| 11 |
}
|
tokenizer_config.json
CHANGED
|
@@ -69,31 +69,7 @@
|
|
| 69 |
"special": true
|
| 70 |
},
|
| 71 |
"8229": {
|
| 72 |
-
"content": "<
|
| 73 |
-
"lstrip": false,
|
| 74 |
-
"normalized": false,
|
| 75 |
-
"rstrip": false,
|
| 76 |
-
"single_word": false,
|
| 77 |
-
"special": true
|
| 78 |
-
},
|
| 79 |
-
"8230": {
|
| 80 |
-
"content": "<unk>",
|
| 81 |
-
"lstrip": false,
|
| 82 |
-
"normalized": false,
|
| 83 |
-
"rstrip": false,
|
| 84 |
-
"single_word": false,
|
| 85 |
-
"special": true
|
| 86 |
-
},
|
| 87 |
-
"8231": {
|
| 88 |
-
"content": "<cls>",
|
| 89 |
-
"lstrip": false,
|
| 90 |
-
"normalized": false,
|
| 91 |
-
"rstrip": false,
|
| 92 |
-
"single_word": false,
|
| 93 |
-
"special": true
|
| 94 |
-
},
|
| 95 |
-
"8232": {
|
| 96 |
-
"content": "<mask>",
|
| 97 |
"lstrip": false,
|
| 98 |
"normalized": false,
|
| 99 |
"rstrip": false,
|
|
@@ -101,17 +77,18 @@
|
|
| 101 |
"special": true
|
| 102 |
}
|
| 103 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
"clean_up_tokenization_spaces": true,
|
| 105 |
-
"cls_token": "<cls>",
|
| 106 |
-
"eos_token": "<eos>",
|
| 107 |
-
"extra_special_tokens": {},
|
| 108 |
-
"mask_token": "<mask>",
|
| 109 |
"model_max_length": 1000000000000000019884624838656,
|
| 110 |
"pad_token": "<pad>",
|
| 111 |
"struct_cls_token": "<cls_struct>",
|
| 112 |
"struct_eos_token": "<eos_struct>",
|
| 113 |
"struct_mask_token": "<mask_struct>",
|
| 114 |
"struct_unk_token": "<unk_struct>",
|
| 115 |
-
"tokenizer_class": "
|
| 116 |
-
"unk_token": "<unk>"
|
| 117 |
}
|
|
|
|
| 69 |
"special": true
|
| 70 |
},
|
| 71 |
"8229": {
|
| 72 |
+
"content": "<mask_struct>",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
"lstrip": false,
|
| 74 |
"normalized": false,
|
| 75 |
"rstrip": false,
|
|
|
|
| 77 |
"special": true
|
| 78 |
}
|
| 79 |
},
|
| 80 |
+
"auto_map": {
|
| 81 |
+
"AutoTokenizer": [
|
| 82 |
+
"modeling_fastplms.DPLM2Tokenizer",
|
| 83 |
+
null
|
| 84 |
+
]
|
| 85 |
+
},
|
| 86 |
"clean_up_tokenization_spaces": true,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
"model_max_length": 1000000000000000019884624838656,
|
| 88 |
"pad_token": "<pad>",
|
| 89 |
"struct_cls_token": "<cls_struct>",
|
| 90 |
"struct_eos_token": "<eos_struct>",
|
| 91 |
"struct_mask_token": "<mask_struct>",
|
| 92 |
"struct_unk_token": "<unk_struct>",
|
| 93 |
+
"tokenizer_class": "DPLM2Tokenizer"
|
|
|
|
| 94 |
}
|
vocab.txt
CHANGED
|
@@ -8226,4 +8226,4 @@ O
|
|
| 8226 |
8189
|
| 8227 |
8190
|
| 8228 |
8191
|
| 8229 |
-
<mask_struct>
|
|
|
|
| 8226 |
8189
|
| 8227 |
8190
|
| 8228 |
8191
|
| 8229 |
+
<mask_struct>
|