Instructions to use Synthyra/Profluent-E1-300M with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Synthyra/Profluent-E1-300M with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("fill-mask", model="Synthyra/Profluent-E1-300M", trust_remote_code=True)# Load model directly from transformers import AutoModelForMaskedLM model = AutoModelForMaskedLM.from_pretrained("Synthyra/Profluent-E1-300M", 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/e1/ATTRIBUTION +34 -0
- LICENSES/e1/Apache-2.0.txt +202 -0
- LICENSES/e1/BSD-3-Clause.txt +29 -0
- LICENSES/e1/LICENSE +86 -0
- LICENSES/e1/MODIFICATIONS.md +27 -0
- LICENSES/e1/NOTICE +14 -0
- README.md +169 -255
- THIRD_PARTY_NOTICES.md +99 -0
- config.json +15 -5
- 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/e1/__init__.py +0 -0
- fastplms/models/e1/attention.py +441 -0
- fastplms/models/e1/cache.py +229 -0
- fastplms/models/e1/modeling_e1.py +2319 -0
- fastplms/models/e1/preparation.py +267 -0
- fastplms/models/e1/retrieval.py +1783 -0
- fastplms/models/e1/tokenizer.json +134 -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 +236 -0
- runtime-attestation.json +52 -0
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/e1/ATTRIBUTION
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Profluent-E1 Attribution Guidelines
|
| 2 |
+
-----------------------------------
|
| 3 |
+
|
| 4 |
+
These Profluent-E1 Attribution Guidelines (the “Guidelines”) set forth certain terms, conditions and restrictions relating to how You can use, share and distribute Profluent-E1 and any Derivative Works thereof in a manner that ensures transparent acknowledgement of Profluent-E1. The terms, conditions and restrictions set forth in these Guidelines are in addition to those set forth in the Profluent-E1 Clickthrough License Agreement, available at https://github.com/Profluent-AI/E1/blob/main/LICENSE, as may be updated or amended from time to time (the “Agreement”) and in the event of any conflict between any provision of these Guidelines and any provision of the Agreement, the provision that is more protective of Profluent Bio, Inc. (“Profluent”) and Profluent-E1 shall control. Any capitalized terms used but not defined herein shall have the meanings set forth in the Agreement. Profluent may update, modify or amend these Guidelines from time to time and Profluent will use reasonable efforts to provide You with notice of any material changes that may negatively impact Your use of Profluent-E1 by posting an updated version of these Guidelines, for example. Any violation of these Guidelines may result in the suspension or termination of Your access to and use of Profluent-E1.
|
| 5 |
+
|
| 6 |
+
1. Attribution Requirements. You may use and redistribute Profluent-E1 and Derivative Works in Source and Object forms, with or without modification, in accordance with the terms of the Agreement provided that the following conditions are met:
|
| 7 |
+
|
| 8 |
+
1.1 You must include a prominently displayed attribution to “Profluent-E1” in any distribution, documentation or public use of Profluent-E1 or any Derivative Works built, developed, improved, or modified through the use of Profluent-E1 by any Commercial Entity, including, without limitation, in any publication, report, white paper, article, product documentation, slide deck or other media that incorporates Profluent-E1 or any Derivative Works; and
|
| 9 |
+
|
| 10 |
+
1.2 You must identify any drug, drug candidate, medication or pharmaceutical of any kind and any molecular or biological target, hit or lead identification that is created, identified, discovered, developed, modified or improved using Profluent-E1 as “Built with Profluent-E1,” including, without limitation, in public disclosures to the United States Food and Drug Administration and similar regulatory authorities.
|
| 11 |
+
|
| 12 |
+
The term “Commercial Entity” as used in this Section 1 shall mean any entity engaged in any activity intended for or directed toward commercial advantage or monetary compensation, including, without limitation, the development of any product or service intended to be sold or made available for a fee, expressly excluding any university, non-profit organization, research institute, educational or government body and any not-for-profit Legal Entity.
|
| 13 |
+
|
| 14 |
+
2. No Endorsement. You may not use or allow the use of the Profluent name nor any Profluent trade name, trademark, service mark or product name to endorse or promote, or imply that Profluent endorses or promotes, any products or other offerings derived from Profluent-E1 without Profluent's specific prior written permission.
|
| 15 |
+
|
| 16 |
+
3. Format of Derivative Works. All attribution notices should be easy to find and easy to understand. The attribution requirements set forth in these Guidelines applies to both textual and visual materials where Profluent-E1 contributes materially to the resulting product or output. Any uses, distributions or documentation in Source form must incorporate a prominent display of text that reads “Profluent-E1” in verifiable form, while redistributions in Object form must be accompanied by a prominent display of “Profluent-E1” text in any documentation and each time the resulting executable program or a program dependent thereon is launched, You must also launch a prominent display (e.g., splash screen or banner text) of such text.
|
| 17 |
+
|
| 18 |
+
4. Placement and Prominence. any entity engaged in any activity intended for or directed toward commercial advantage or monetary compensation, including, without limitation, the development of any product or service intended to be sold or made available for a fee, expressly excluding any university, non-profit organization, research institute, educational or government body and any not-for-profit Legal Entity. The attribution must appear within the main body of any publication, report, white paper or article. For product documentation or software tool or interface references, the attribution must be listed on the title page or in the introductory section, near other third-party license notices. In slide decks or conference presentations, the attribution must appear on a title slide, or a slide within the first three slides of the presentation. For webpages, software interfaces, or interactive demos, attribution must be visible on-screen in a footer, “About” section, or a prominently accessible credits area without requiring extra actions like scrolling or menu navigation.
|
| 19 |
+
|
| 20 |
+
5. Formatting. You must use the exact “Profluent-E1” model name as provided by Profluent, and the font size must be at least equal to the main body text or at least 75% of the largest text on the page where the attribution is incorporated (whichever is greater). The text must be visually legible and in a color that contrasts sufficiently with its background. Graphic or visual logos provided by the Profluent may be used in place of plain-text attribution if approved, but they must remain clearly readable at normal viewing sizes.
|
| 21 |
+
|
| 22 |
+
6. Illustrative Examples. The following are examples of how attribution to Profluent-E1 may be implemented in accordance with the requirements of these guidelines:
|
| 23 |
+
|
| 24 |
+
6.1 Example 1: A research article that incorporates data derived from outputs of Profluent-E1 includes reference to “Profluent-E1” in or beneath the paragraph of text first introducing these outputs.
|
| 25 |
+
|
| 26 |
+
6.2 Example 2: A company releases a model fine-tuned from Profluent-E1 and the website's footer includes “Built with Profluent-E1.”
|
| 27 |
+
|
| 28 |
+
6.3 Example 3: A presentation at a conference includes “This work incorporates works derived from use of Profluent-E1” on the title slide.
|
| 29 |
+
|
| 30 |
+
6.4 Example 4: An open-source repository “README” file includes a visible statement near the top: “This project builds upon Profluent-E1 under the terms of the Profluent-E1 Clickthrough License Agreement.”
|
| 31 |
+
|
| 32 |
+
7. Non-Obscuration. The attribution must not be hidden or disguised by other visual or textual elements. It should remain readable at standard display resolutions and must not be abbreviated, truncated or replaced with unofficial branding.
|
| 33 |
+
|
| 34 |
+
8. Exceptions. Written exceptions to these attribution requirements may be granted by the Licensor upon request, provided that alternative acknowledgment conveys equivalent visibility and transparency.
|
LICENSES/e1/Apache-2.0.txt
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/e1/BSD-3-Clause.txt
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BSD 3-Clause License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2022, Tri Dao.
|
| 4 |
+
All rights reserved.
|
| 5 |
+
|
| 6 |
+
Redistribution and use in source and binary forms, with or without
|
| 7 |
+
modification, are permitted provided that the following conditions are met:
|
| 8 |
+
|
| 9 |
+
1. Redistributions of source code must retain the above copyright notice,
|
| 10 |
+
this list of conditions and the following disclaimer.
|
| 11 |
+
|
| 12 |
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
| 13 |
+
this list of conditions and the following disclaimer in the documentation
|
| 14 |
+
and/or other materials provided with the distribution.
|
| 15 |
+
|
| 16 |
+
3. Neither the name of the copyright holder nor the names of its
|
| 17 |
+
contributors may be used to endorse or promote products derived from this
|
| 18 |
+
software without specific prior written permission.
|
| 19 |
+
|
| 20 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
| 21 |
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
| 22 |
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
| 23 |
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
| 24 |
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
| 25 |
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
| 26 |
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
| 27 |
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
| 28 |
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
| 29 |
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
LICENSES/e1/LICENSE
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Profluent-E1 Clickthrough License Agreement
|
| 2 |
+
-------------------------------------------
|
| 3 |
+
|
| 4 |
+
Please read this Profluent-E1 Clickthrough License Agreement (the “Agreement”) carefully before using Profluent-E1 (as defined below), which is offered by Profluent Bio Inc. (“Profluent”).
|
| 5 |
+
|
| 6 |
+
By downloading Profluent-E1, or otherwise using Profluent-E1 in any manner, You agree that You have read and agree to be bound by the terms of this Agreement. If You are accessing Profluent-E1 on behalf of an organization or entity, You represent and warrant that You are authorized to enter into this Agreement on that organization's or entity's behalf and bind them to the terms of this Agreement (in which case, the references to “You” and “Your” in this Agreement, except for in this sentence, refer to that organization or entity). Use of Profluent-E1 and all other Profluent-E1 IP is expressly conditioned upon Your assent to all terms of this Agreement, including the Attribution Guidelines incorporated herein, to the exclusion of all other terms.
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
1.1 “AAA Rules” shall mean the Commercial Arbitration Rules and Mediation Procedures of the American Arbitration Association (“AAA”).
|
| 11 |
+
|
| 12 |
+
1.2 “Claim” shall mean any claim (including any tort claim), cause of action, and/or dispute under, arising out of, or relating to this Agreement.
|
| 13 |
+
|
| 14 |
+
1.3 “Contribution” shall mean any work of authorship, including the original version of Profluent-E1 and any modifications or additions to that Profluent-E1 or Derivative Works thereof, that is intentionally submitted to Profluent for inclusion in Profluent-E1 by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Profluent or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Profluent for the purpose of discussing and improving Profluent-E1, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
| 15 |
+
|
| 16 |
+
1.4 “Contributor” shall mean Profluent and any individual or Legal Entity on behalf of whom a Contribution has been received by Profluent and subsequently incorporated within Profluent-E1.
|
| 17 |
+
|
| 18 |
+
1.5 “Derivative Works” shall mean any work, whether in Source or Object form, that is based on (or derived from) Profluent-E1 and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this Agreement, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, Profluent-E1 and Derivative Works thereof.
|
| 19 |
+
|
| 20 |
+
1.6 “GitHub Page” shall mean the page made available at https://github.com/Profluent-AI/E1, as may be updated and amended from time to time.
|
| 21 |
+
|
| 22 |
+
1.7 “Hugging Face Pages” shall mean the pages made available at https://huggingface.co/Profluent-Bio/E1-600m, https://huggingface.co/Profluent-Bio/E1-300m and https://huggingface.co/Profluent-Bio/E1-150m, each as may be updated and amended from time to time.
|
| 23 |
+
|
| 24 |
+
1.8 “Legal Entity” shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, “control” means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
| 25 |
+
|
| 26 |
+
1.9 “Object” form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
| 27 |
+
|
| 28 |
+
1.10 “Profluent-E1” shall mean the Profluent-E1 Model Code, Profluent-E1 Model Weights and all software, algorithms, machine-learning model code, inference-enabling code, training-enabling code, fine-tuning enabling code and other elements of the foregoing distributed or made available to You by Profluent on the GitHub Page, each as may be updated and amended from time to time, whether in Source or Object form.
|
| 29 |
+
|
| 30 |
+
1.11 “Profluent-E1 Model Code” shall mean the code and data for Profluent-E1 made available to You at https://github.com/Profluent-AI/E1, as may be updated and amended from time to time.
|
| 31 |
+
|
| 32 |
+
1.12 “Profluent-E1 Model Weights” shall mean the trained model weights for Profluent-E1 made available to You on one or more of the Hugging Face Pages, as may be updated and amended from time to time, including all model weights that are directly or indirectly accessed or copied from the Hugging Face Pages by You or any third party.
|
| 33 |
+
|
| 34 |
+
1.13 “Source” form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
| 35 |
+
|
| 36 |
+
1.14 “You” or “Your” shall mean an individual entering into this Agreement or the organization or Legal Entity on whose behalf such individual is entering into this Agreement.
|
| 37 |
+
|
| 38 |
+
2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute Profluent-E1 and such Derivative Works in Source or Object form.
|
| 39 |
+
|
| 40 |
+
3. Grant of Patent License. Subject to the terms and conditions of this Agreement, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer Profluent-E1, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with Profluent-E1 to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that Profluent-E1 or a Contribution incorporated within Profluent-E1 constitutes direct or contributory patent infringement, then any patent licenses granted to You under this Agreement for Profluent-E1 shall terminate as of the date such litigation is filed.
|
| 41 |
+
|
| 42 |
+
4. Use of Profluent-E1 Model Code. Your access to and use of the Profluent-E1 Model Code separate and apart from Profluent-E1 and the Profluent-E1 Model Weights is subject to the Apache License, Version 2.0 made available at https://www.apache.org/licenses/LICENSE-2.0, as may be updated and amended from time to time.
|
| 43 |
+
|
| 44 |
+
5. Redistribution. You may reproduce and distribute copies of Profluent-E1 or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
| 45 |
+
|
| 46 |
+
(a) You must give any other recipients of Profluent-E1 or Derivative Works a copy of this Agreement; and
|
| 47 |
+
|
| 48 |
+
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
| 49 |
+
|
| 50 |
+
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of Profluent-E1, excluding those notices that do not pertain to any part of the Derivative Works; and
|
| 51 |
+
|
| 52 |
+
(d) If Profluent-E1 includes a "NOTICE" text file as part of its distribution or there is otherwise a “NOTICE” text file available on the GitHub Page, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify this Agreement. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from Profluent-E1, provided that such additional attribution notices cannot be construed as modifying this Agreement; and
|
| 53 |
+
|
| 54 |
+
(e) You must comply with the Attribution Guidelines and provide attribution to Profluent-E1 in accordance therewith.
|
| 55 |
+
|
| 56 |
+
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of Profluent-E1 otherwise complies with the conditions stated in this License.
|
| 57 |
+
|
| 58 |
+
6. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in Profluent-E1 by You to Profluent shall be under the terms and conditions of this Agreement, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Profluent regarding such Contributions.
|
| 59 |
+
|
| 60 |
+
7. Trademarks. This Agreement does not grant permission to use any Profluent trade names, trademarks, service marks, or product names, except as required for reasonable and customary use in describing the origin of Profluent-E1, reproducing the content of the NOTICE file and complying with the Attribution Guidelines.
|
| 61 |
+
|
| 62 |
+
8. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Profluent provides Profluent-E1 (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing Profluent-E1 and assume any risks associated with Your exercise of permissions under this Agreement. FOR THE AVOIDANCE OF DOUBT AND NOTWITHSTANDING ANYTHING TO THE CONTRARY, YOU ACKNOWLEDGE AND AGREE THAT PROFLUENT IS NOT RESPONSIBLE OR LIABLE FOR ANYTHING YOU BUILD, CREATE, DEVELOP OR DERIVE FROM YOUR USE OF PROFLUENT-E1, INCLUDING ANY DERIVATIVE WORKS.
|
| 63 |
+
|
| 64 |
+
9. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this Agreement or out of the use or inability to use Profluent-E1 (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
| 65 |
+
|
| 66 |
+
10. Accepting Warranty or Additional Liability. While redistributing Profluent-E1 or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this Agreement. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
| 67 |
+
|
| 68 |
+
11. Term and Termination. The term of this Agreement will commence upon Your acceptance of this Agreement or access to Profluent-E1 and will continue in full force and effect until terminated in accordance with the terms and conditions herein. Profluent may terminate this Agreement if You are in breach of any term or condition of this Agreement. Upon termination of this Agreement, you shall delete and cease use of Profluent-E1. All provisions of this Agreement which by their nature should survive termination shall survive termination, including, without limitation, ownership provisions, warranty disclaimers, indemnity obligations, limitations of liability and provisions regarding dispute resolution.
|
| 69 |
+
|
| 70 |
+
12. General.
|
| 71 |
+
|
| 72 |
+
12.1 This Agreement constitutes the entire agreement between You and Profluent relating to the subject matter hereof and supersedes all proposals, understandings, or discussions, whether written or oral, relating to the subject matter of this Agreement and all past dealing or industry custom. The failure of either party to enforce its rights under this Agreement at any time for any period shall not be construed as a waiver of such rights.
|
| 73 |
+
|
| 74 |
+
12.2 Profluent may amend or modify this Agreement from time to time and will use reasonable efforts to provide You with notice of any material changes that may negatively impact Your use of Profluent-E1 through the GitHub Page or through another means made available to You. No other changes, modifications or waivers to this Agreement will be effective unless in writing and signed by both parties.
|
| 75 |
+
|
| 76 |
+
12.3 You and Profluent are independent contractors, and nothing herein shall be deemed to constitute either party as the agent or representative of the other or both parties as joint venturers or partners for any purpose.
|
| 77 |
+
|
| 78 |
+
12.4 You shall comply with the U.S. Foreign Corrupt Practices Act and all applicable export laws, restrictions and regulations of the U.S. Department of Commerce, and any other applicable U.S. and foreign authority.
|
| 79 |
+
|
| 80 |
+
12.5 This Agreement and the rights and obligations herein may not be assigned or transferred, in whole or in part, by You without the prior written consent of Profluent. Any assignment in violation of this provision is void. Profluent may freely assign or transfer this Agreement, in whole or in part. This Agreement shall be binding upon, and inure to the benefit of, the successors and permitted assigns of the parties.
|
| 81 |
+
|
| 82 |
+
12.6 This Agreement shall be governed by and construed under the Federal Arbitration Act, applicable federal law, and the laws of the State of California and the United States without regard to conflicts of laws provisions thereof, and without regard to the Uniform Computer Information Transactions Act.
|
| 83 |
+
|
| 84 |
+
12.7 If You have a Claim, You agree to provide Profluent with at least sixty (60) days' written notice so that the parties may attempt to resolve the Claim internally before requesting arbitration. In case of any Claims or disputes between You and Profluent that cannot be resolved through informal internal discussions, You agree that, subject to the provisions of this section, any and all Claims will be settled by final and binding arbitration in accordance with the AAA Rules by an arbitrator selected pursuant to the AAA Rules; provided, however, that Claims arising out of or relating to Your violations of Profluent's intellectual property rights, including copyright infringement, patent infringement, trademark infringement, or efforts to use Profluent-E1 in unauthorized ways will not be subject to such obligation for settlement by final and binding arbitration, and such claims will instead be brought in the state and federal courts of Alameda County, California. The arbitrator's decision and award will be non-appealable and may be entered in, and will be enforceable in, any court of competent jurisdiction. The arbitration will take place in Alameda County, California. Each party will bear its own costs of arbitration unless the arbitrator directs otherwise. The arbitrator may not award relief or damages in excess of or contrary to what this Agreement provides, or order consolidation or arbitration on a class-wide or representative basis.
|
| 85 |
+
|
| 86 |
+
12.8 If any provision of this Agreement is held to be invalid, illegal or unenforceable in any respect, that provision shall be limited or eliminated to the minimum extent necessary so that this Agreement otherwise remains in full force and effect and enforceable.
|
LICENSES/e1/MODIFICATIONS.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Profluent-E1 modified-file notice
|
| 2 |
+
|
| 3 |
+
FastPLMs implements Profluent-E1 behavior against the pinned official source at
|
| 4 |
+
revision `bfd2620a602248499f3d2583d85a7ecddf0b6e02`. The FastPLMs files listed
|
| 5 |
+
below are modified or independently reorganized implementations of the
|
| 6 |
+
corresponding E1 interfaces. They are not byte-for-byte copies of the upstream
|
| 7 |
+
files.
|
| 8 |
+
|
| 9 |
+
| FastPLMs file | Modification notice |
|
| 10 |
+
|---|---|
|
| 11 |
+
| `src/fastplms/models/e1/modeling_e1.py` | Reorganized for Transformers AutoClasses, shared attention selection, sequence and RAG preparation, task heads, and checkpoint-compatible loading. |
|
| 12 |
+
| `src/fastplms/models/e1/attention.py` | Isolated the declared SDPA and Flex Attention paths and their masking contracts. |
|
| 13 |
+
| `src/fastplms/models/e1/preparation.py` | Reorganized raw-sequence, boundary-token, and retrieval-context preparation. |
|
| 14 |
+
| `src/fastplms/models/e1/cache.py` | Adapted the cache interface used by the reorganized Transformers implementation. |
|
| 15 |
+
| `src/fastplms/models/e1/retrieval.py` | Adapted retrieval helpers and their FastPLMs model outputs. |
|
| 16 |
+
| `src/fastplms/models/e1/__init__.py` | Added FastPLMs package exports. |
|
| 17 |
+
| `tools/conversion/state_transforms.py` | Added the deterministic `e1_to_fastplms_v1` checkpoint mapping. |
|
| 18 |
+
|
| 19 |
+
These changes were present in the FastPLMs 1.0 repository as reviewed on
|
| 20 |
+
2026-07-20. The conversion identifier is `e1_to_fastplms_v1`. Recipients must
|
| 21 |
+
retain the Profluent-E1 agreement, `ATTRIBUTION`, `NOTICE`, this modified-file
|
| 22 |
+
notice, and the applicable Apache-2.0 and BSD-3-Clause texts.
|
| 23 |
+
|
| 24 |
+
The BSD-3-Clause component is the padding utility identified by the official E1
|
| 25 |
+
repository as adapted from Dao-AILab FlashAttention. FastPLMs does not copy that
|
| 26 |
+
official utility into its production package, but preserves the notice because
|
| 27 |
+
the official source is the parity oracle for the E1 behavior contract.
|
LICENSES/e1/NOTICE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Profluent-E1 Notice File
|
| 2 |
+
------------------------
|
| 3 |
+
|
| 4 |
+
Copyright 2025 Profluent Bio Inc.
|
| 5 |
+
|
| 6 |
+
Licensed under the Profluent-E1 Clickthrough License Agreement (the “Agreement”); you may not use
|
| 7 |
+
this file except in compliance with the Agreement. Unless required by applicable law or agreed to
|
| 8 |
+
in writing, software distributed under the Agreement is distributed on an "AS IS" BASIS, WITHOUT
|
| 9 |
+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Agreement for the specific
|
| 10 |
+
language governing permissions and limitations under the Agreement and the Attribution Guidelines
|
| 11 |
+
for more information about attribution requirements for use of Profluent-E1. You may obtain a copy
|
| 12 |
+
of the Agreement at https://github.com/Profluent-AI/E1/blob/main/LICENSE and a copy of the Attribution
|
| 13 |
+
Guidelines at https://github.com/Profluent-AI/E1/blob/main/ATTRIBUTION, each as may be updated or
|
| 14 |
+
amended from time to time.
|
README.md
CHANGED
|
@@ -1,255 +1,169 @@
|
|
| 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 |
-
from
|
| 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 |
-
```python
|
| 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 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
``
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
```
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
pooling_types=["mean"],
|
| 171 |
-
)
|
| 172 |
-
```
|
| 173 |
-
|
| 174 |
-
The MSA parsing and context sampling follow Profluent's official E1 `msa_sampling` behavior, including A3M insertion stripping, neighbor reweighting, query-similarity filtering, seeded sampling, and context token budgets.
|
| 175 |
-
|
| 176 |
-
`score_ppll()` is intentionally different from Profluent's official `E1Scorer`. The official scorer computes mutant scores against a parent sequence with wildtype or masked marginal log-probability deltas. FastPLMs uses a PPLL-style mean correct-token probability over each scored sequence, then optionally averages over sampled contexts. We prefer this API because it is much cheaper while remaining comparable for our use cases.
|
| 177 |
-
|
| 178 |
-
For dataset embeddings with precomputed MSAs:
|
| 179 |
-
|
| 180 |
-
```python
|
| 181 |
-
embedding_dict = model.embed_dataset_with_msa(
|
| 182 |
-
sequences=["MALWMRLLPLLALLALWGPDPAAA"],
|
| 183 |
-
msa_dir="msas",
|
| 184 |
-
batch_size=2,
|
| 185 |
-
pooling_types=["mean"],
|
| 186 |
-
)
|
| 187 |
-
```
|
| 188 |
-
|
| 189 |
-
The standard `embed()` and `embed_dataset()` paths are unchanged. Use `embed_with_msa()` or `embed_dataset_with_msa()` when you want retrieval context included.
|
| 190 |
-
|
| 191 |
-
## Fine-tuning with 🤗 peft
|
| 192 |
-
```python
|
| 193 |
-
model = AutoModelForSequenceClassification.from_pretrained('Synthyra/Profluent-E1-150M', num_labels=2, trust_remote_code=True)
|
| 194 |
-
# these modules handle E1 attention layers
|
| 195 |
-
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj"]
|
| 196 |
-
|
| 197 |
-
lora_config = LoraConfig(
|
| 198 |
-
r=8, # choose lora parameters to your liking
|
| 199 |
-
lora_alpha=16,
|
| 200 |
-
lora_dropout=0.01,
|
| 201 |
-
bias="none",
|
| 202 |
-
target_modules=target_modules,
|
| 203 |
-
)
|
| 204 |
-
|
| 205 |
-
# Apply LoRA to the model
|
| 206 |
-
model = get_peft_model(model, lora_config)
|
| 207 |
-
|
| 208 |
-
# Unfreeze the classifier head
|
| 209 |
-
for param in model.classifier.parameters():
|
| 210 |
-
param.requires_grad = True
|
| 211 |
-
```
|
| 212 |
-
|
| 213 |
-
For a more thourough example of fine-tuning, check out our example script [here](https://github.com/Synthyra/FastPLMs/blob/main/fine_tuning_example.py).
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
### Citations
|
| 217 |
-
|
| 218 |
-
```bibtex
|
| 219 |
-
@misc{FastPLMs,
|
| 220 |
-
author={Hallee, Logan and Bichara, David and Gleghorn, Jason P.},
|
| 221 |
-
title={FastPLMs: Fast, efficient, protein language model inference from Huggingface AutoModel.},
|
| 222 |
-
year={2024},
|
| 223 |
-
url={https://huggingface.co/Synthyra/ESMplusplus_small},
|
| 224 |
-
DOI={10.57967/hf/3726},
|
| 225 |
-
publisher={Hugging Face}
|
| 226 |
-
}
|
| 227 |
-
```
|
| 228 |
-
|
| 229 |
-
```bibtex
|
| 230 |
-
@article{jain2025e1,
|
| 231 |
-
title={E1: Retrieval-Augmented Protein Encoder Models},
|
| 232 |
-
author={Jain, Sarthak and Beazer, Joel and Ruffolo, Jeffrey A and Bhatnagar, Aadyot and Madani, Ali},
|
| 233 |
-
journal={bioRxiv},
|
| 234 |
-
DOI={10.1101/2025.11.12.688125},
|
| 235 |
-
year={2025}
|
| 236 |
-
}
|
| 237 |
-
```
|
| 238 |
-
|
| 239 |
-
```bibtex
|
| 240 |
-
@article{dong2024flexattention,
|
| 241 |
-
title={Flex Attention: A Programming Model for Generating Optimized Attention Kernels},
|
| 242 |
-
author={Dong, Juechu and Feng, Boyuan and Guessous, Driss and Liang, Yanbo and He, Horace},
|
| 243 |
-
journal={arXiv preprint arXiv:2412.05496},
|
| 244 |
-
year={2024}
|
| 245 |
-
}
|
| 246 |
-
```
|
| 247 |
-
|
| 248 |
-
```bibtex
|
| 249 |
-
@inproceedings{paszke2019pytorch,
|
| 250 |
-
title={PyTorch: An Imperative Style, High-Performance Deep Learning Library},
|
| 251 |
-
author={Paszke, Adam and Gross, Sam and Massa, Francisco and Lerer, Adam and Bradbury, James and Chanan, Gregory and Killeen, Trevor and Lin, Zeming and Gimelshein, Natalia and Antiga, Luca and Desmaison, Alban and K{\"o}pf, Andreas and Yang, Edward and DeVito, Zach and Raison, Martin and Tejani, Alykhan and Chilamkurthy, Sasank and Steiner, Benoit and Fang, Lu and Bai, Junjie and Chintala, Soumith},
|
| 252 |
-
booktitle={Advances in Neural Information Processing Systems 32},
|
| 253 |
-
year={2019}
|
| 254 |
-
}
|
| 255 |
-
```
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: transformers
|
| 3 |
+
license: "other"
|
| 4 |
+
license_name: "profluent-e1-clickthrough-license-agreement"
|
| 5 |
+
license_link: "https://github.com/Profluent-AI/E1/blob/bfd2620a602248499f3d2583d85a7ecddf0b6e02/LICENSE"
|
| 6 |
+
tags:
|
| 7 |
+
- protein-language-model
|
| 8 |
+
- fastplms
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
<!-- Generated from src/fastplms/models.toml. Do not edit. -->
|
| 12 |
+
|
| 13 |
+
# Synthyra/Profluent-E1-300M
|
| 14 |
+
|
| 15 |
+
This checkpoint packages the FastPLMs `E1` implementation.
|
| 16 |
+
|
| 17 |
+
Accepted inputs are raw amino-acid sequences prepared by the native E1 adapter.
|
| 18 |
+
Supported Transformers entry points are `AutoConfig`, `AutoModel`,
|
| 19 |
+
`AutoModelForMaskedLM`, `AutoModelForSequenceClassification`,
|
| 20 |
+
`AutoModelForTokenClassification`.
|
| 21 |
+
|
| 22 |
+
## Install and platform requirements
|
| 23 |
+
|
| 24 |
+
Install FastPLMs from the exact source revision paired with this model card:
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
python -m pip install \
|
| 28 |
+
"fastplms @ git+https://github.com/Synthyra/FastPLMs.git@1b9ce023f1e06571cf3e6324be0610ffa53e0a4a"
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
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
|
| 32 |
+
access on first download. For an air-gapped run, first build the manifest-pinned
|
| 33 |
+
local artifact and use the offline form shown in the example.
|
| 34 |
+
|
| 35 |
+
## Quick start
|
| 36 |
+
|
| 37 |
+
```python
|
| 38 |
+
from transformers import AutoModel
|
| 39 |
+
|
| 40 |
+
model_id = "Synthyra/Profluent-E1-300M"
|
| 41 |
+
model = AutoModel.from_pretrained(
|
| 42 |
+
model_id,
|
| 43 |
+
trust_remote_code=True,
|
| 44 |
+
).eval()
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
This example uses the published Hub repository. For offline validation, build
|
| 48 |
+
the manifest-pinned artifact and replace `model_id` with its local
|
| 49 |
+
`dist/hub/Profluent-E1-300M` path, then pass `local_files_only=True`.
|
| 50 |
+
|
| 51 |
+
Leave attention unspecified for the Transformers default. Supported explicit
|
| 52 |
+
choices are `sdpa`, `flex_attention`.
|
| 53 |
+
Pass the selected name through `attn_implementation`.
|
| 54 |
+
When an optimized backend cannot return full attention tensors,
|
| 55 |
+
`output_attentions=True` emits one explicit runtime warning and uses a correctly
|
| 56 |
+
masked eager implementation for that call only. The warning identifies the
|
| 57 |
+
configured backend, effective backend, and reason. Configuration and later
|
| 58 |
+
calls are unchanged.
|
| 59 |
+
For BF16 execution, this family uses parameters loaded directly in BF16.
|
| 60 |
+
|
| 61 |
+
## Dataset embeddings
|
| 62 |
+
|
| 63 |
+
The shared embedding API accepts sequences, `(id, sequence)` pairs,
|
| 64 |
+
`EmbeddingInput` records, insertion-ordered `{id: sequence}` mappings, or a
|
| 65 |
+
FASTA path. Results preserve order and duplicate identifiers:
|
| 66 |
+
|
| 67 |
+
```python
|
| 68 |
+
result = model.embed_dataset(
|
| 69 |
+
["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"],
|
| 70 |
+
batch_size=2,
|
| 71 |
+
pooling=("mean", "std"),
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
for record in result:
|
| 75 |
+
print(record.id, record.sequence, record.tensor.shape)
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
Set `full_embeddings=True` for one residue tensor with shape `(l, d)` per
|
| 79 |
+
sequence. Set `output` to a directory for bounded-memory, transactional
|
| 80 |
+
safetensors with ordered-prefix resume, or choose `format="sqlite"` for
|
| 81 |
+
batch-level database commits and exact resume. Pooling excludes boundary,
|
| 82 |
+
padding, and other non-biological positions.
|
| 83 |
+
|
| 84 |
+
For a long FASTA run, stream completed batches into SQLite:
|
| 85 |
+
|
| 86 |
+
```python
|
| 87 |
+
persisted = model.embed_dataset(
|
| 88 |
+
"proteins.fasta",
|
| 89 |
+
batch_size=64,
|
| 90 |
+
pooling=("mean",),
|
| 91 |
+
output="protein-embeddings.sqlite",
|
| 92 |
+
format="sqlite",
|
| 93 |
+
resume=True,
|
| 94 |
+
)
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
Resume verifies the input order, model state, tokenizer policy, backend, dtype,
|
| 98 |
+
and pooling configuration. It never appends incompatible records to an
|
| 99 |
+
existing run.
|
| 100 |
+
|
| 101 |
+
## Tokenizer-free E1 input
|
| 102 |
+
|
| 103 |
+
E1 has no tokenizer. The model retains native raw-sequence preparation,
|
| 104 |
+
boundary tokens, sequence positions, and retrieval-augmented context behavior.
|
| 105 |
+
The ordinary representation path accepts sequences directly:
|
| 106 |
+
|
| 107 |
+
```python
|
| 108 |
+
result = model.embed_dataset(
|
| 109 |
+
["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"],
|
| 110 |
+
batch_size=2,
|
| 111 |
+
pooling=("mean",),
|
| 112 |
+
)
|
| 113 |
+
print(result[0].tensor.shape)
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
Lower-level masked-language-model calls must use the E1 batch preparer rather
|
| 117 |
+
than an `AutoTokenizer`. E1 launch messages and distributed legal files retain
|
| 118 |
+
the attribution required by the upstream agreement.
|
| 119 |
+
|
| 120 |
+
## Runtime contract
|
| 121 |
+
|
| 122 |
+
- Public input: Raw amino-acid sequences prepared by the native E1 adapter
|
| 123 |
+
- Advertised AutoClasses: `AutoConfig`, `AutoModel`, `AutoModelForMaskedLM`, `AutoModelForSequenceClassification`, `AutoModelForTokenClassification`
|
| 124 |
+
- AutoClass weight status: `AutoConfig` = `FastPLMs extension`, `AutoModel` = `pretrained`, `AutoModelForMaskedLM` = `pretrained`, `AutoModelForSequenceClassification` = `base weights + untrained task head`, `AutoModelForTokenClassification` = `base weights + untrained task head`
|
| 125 |
+
- Attention implementations: `sdpa`, `flex_attention`
|
| 126 |
+
- Precision policies: `default`
|
| 127 |
+
- BF16 execution: `static_parameters`
|
| 128 |
+
- Generation contract: `not_applicable`
|
| 129 |
+
- Optional dependency group: `core`
|
| 130 |
+
- Weight publication allowed: `true`
|
| 131 |
+
- Weight license status: `resolved`
|
| 132 |
+
- Redistributable: `true`
|
| 133 |
+
- Complete weight publication required: `false`
|
| 134 |
+
|
| 135 |
+
## Provenance
|
| 136 |
+
|
| 137 |
+
- FastPLMs weights: `Synthyra/Profluent-E1-300M@5ef52c0ad2ae2578f40622696b763523810e8e26`
|
| 138 |
+
- Runtime revision: `1b9ce023f1e06571cf3e6324be0610ffa53e0a4a`
|
| 139 |
+
- Runtime source-tree SHA-256: `cec0500dd5f6b238474600a6e80b8d3ccd4544ef7daec90441f14273f1b658d2`
|
| 140 |
+
- Runtime bundle SHA-256: `c99634d724e168524f43f56ad1d22af47c75db0cbe657a2a55a237104fe7b833`
|
| 141 |
+
- Generator/schema version and complete/runtime-only attestations: recorded in `provenance.json`
|
| 142 |
+
- Official checkpoint: `Profluent-Bio/E1-300m@5a2871c587eadbcc9237bc686ea45e5b4d28dfb3`
|
| 143 |
+
- Artifact source: `fast`
|
| 144 |
+
- State transform: `e1_to_fastplms_v1`
|
| 145 |
+
- BF16 execution: `static_parameters`
|
| 146 |
+
- Pinned upstreams: `e1`
|
| 147 |
+
- Reference container: `reference-e1`
|
| 148 |
+
- Release tiers: `check`, `compliance`, `feature`, `artifact`, `benchmark`
|
| 149 |
+
- Unresolved required file identities: `0`
|
| 150 |
+
|
| 151 |
+
The local artifact records exact file identities, conversion provenance, source
|
| 152 |
+
revisions, and legal texts in `provenance.json`. A nonzero unresolved count is a
|
| 153 |
+
release blocker.
|
| 154 |
+
|
| 155 |
+
## Validation boundary
|
| 156 |
+
|
| 157 |
+
For tiers declared by the manifest, the release contract compares applicable
|
| 158 |
+
semantic configuration, tokenizer behavior, state keys, shapes, dtypes,
|
| 159 |
+
values, aliases, and representative inference with the pinned official
|
| 160 |
+
implementation. This metadata does not by itself claim that a particular build
|
| 161 |
+
passed, that one backend is faster, or that an output has biological or
|
| 162 |
+
therapeutic validity.
|
| 163 |
+
|
| 164 |
+
## License
|
| 165 |
+
|
| 166 |
+
Checkpoint terms: [Profluent-E1 Clickthrough License Agreement](https://github.com/Profluent-AI/E1/blob/bfd2620a602248499f3d2583d85a7ecddf0b6e02/LICENSE). The Hub model-card identifier is
|
| 167 |
+
`other`. Applicable source licenses, notices, attribution,
|
| 168 |
+
and conversion records are distributed with the local artifact. Review them
|
| 169 |
+
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
|
@@ -4,16 +4,26 @@
|
|
| 4 |
],
|
| 5 |
"attn_backend": "sdpa",
|
| 6 |
"auto_map": {
|
| 7 |
-
"AutoConfig": "
|
| 8 |
-
"AutoModel": "
|
| 9 |
-
"AutoModelForMaskedLM": "
|
| 10 |
-
"AutoModelForSequenceClassification": "
|
| 11 |
-
"AutoModelForTokenClassification": "
|
| 12 |
},
|
| 13 |
"bos_token_id": 1,
|
| 14 |
"clip_qkv": 8,
|
| 15 |
"dtype": "bfloat16",
|
| 16 |
"eos_token_id": 2,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
"gated_mlp": true,
|
| 18 |
"global_attention_every_n_layers": 3,
|
| 19 |
"gradient_checkpointing": false,
|
|
|
|
| 4 |
],
|
| 5 |
"attn_backend": "sdpa",
|
| 6 |
"auto_map": {
|
| 7 |
+
"AutoConfig": "modeling_fastplms.E1Config",
|
| 8 |
+
"AutoModel": "modeling_fastplms.E1Model",
|
| 9 |
+
"AutoModelForMaskedLM": "modeling_fastplms.E1ForMaskedLM",
|
| 10 |
+
"AutoModelForSequenceClassification": "modeling_fastplms.E1ForSequenceClassification",
|
| 11 |
+
"AutoModelForTokenClassification": "modeling_fastplms.E1ForTokenClassification"
|
| 12 |
},
|
| 13 |
"bos_token_id": 1,
|
| 14 |
"clip_qkv": 8,
|
| 15 |
"dtype": "bfloat16",
|
| 16 |
"eos_token_id": 2,
|
| 17 |
+
"fastplms_checkpoint_hash": "c29bdfd0241a0cd9685ed347bdd627018ecacff451baf0cd20674b64f44483f9",
|
| 18 |
+
"fastplms_checkpoint_repo_id": "Synthyra/Profluent-E1-300M",
|
| 19 |
+
"fastplms_checkpoint_revision": "5ef52c0ad2ae2578f40622696b763523810e8e26",
|
| 20 |
+
"fastplms_model_id": "e1_300m",
|
| 21 |
+
"fastplms_release_tool_revision": "1b9ce023f1e06571cf3e6324be0610ffa53e0a4a",
|
| 22 |
+
"fastplms_release_tool_sha256": "1459b5d7d13d9b07bd97b3eee764f2ce73623e15e32d07ddf6825c2a9509afb9",
|
| 23 |
+
"fastplms_runtime_bundle_sha256": "c99634d724e168524f43f56ad1d22af47c75db0cbe657a2a55a237104fe7b833",
|
| 24 |
+
"fastplms_runtime_revision": "1b9ce023f1e06571cf3e6324be0610ffa53e0a4a",
|
| 25 |
+
"fastplms_source_tree_sha256": "cec0500dd5f6b238474600a6e80b8d3ccd4544ef7daec90441f14273f1b658d2",
|
| 26 |
+
"fastplms_weights_revision": "5ef52c0ad2ae2578f40622696b763523810e8e26",
|
| 27 |
"gated_mlp": true,
|
| 28 |
"global_attention_every_n_layers": 3,
|
| 29 |
"gradient_checkpointing": false,
|
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/e1/__init__.py
ADDED
|
File without changes
|
fastplms/models/e1/attention.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""E1 attention mask, unpadding, FlexAttention, and kernel adapters."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from collections.abc import Callable
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from torch.nn.attention.flex_attention import _create_sparse_block_from_block_mask
|
| 10 |
+
|
| 11 |
+
from fastplms.attention import (
|
| 12 |
+
BlockMask,
|
| 13 |
+
_ensure_flash_kernels_loaded,
|
| 14 |
+
_get_flex_attention_fn,
|
| 15 |
+
_kernels_flash_forward,
|
| 16 |
+
_kernels_flash_varlen_forward,
|
| 17 |
+
create_block_mask,
|
| 18 |
+
flex_attention,
|
| 19 |
+
index_first_axis,
|
| 20 |
+
pad_input,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@torch.compiler.disable
|
| 25 |
+
def create_block_causal_mask_optimized(sequence_ids: torch.Tensor) -> BlockMask:
|
| 26 |
+
if create_block_mask is None:
|
| 27 |
+
raise RuntimeError("Flex Attention block-mask creation is unavailable in this environment.")
|
| 28 |
+
# Assumes sequence_ids is sorted in increasing order for each batch item, except for
|
| 29 |
+
# the -1 values, which are used to indicate the padding tokens.
|
| 30 |
+
def document_mask(b, h, q_idx, kv_idx): # type: ignore[no-untyped-def]
|
| 31 |
+
return (
|
| 32 |
+
(sequence_ids[b, q_idx] >= sequence_ids[b, kv_idx])
|
| 33 |
+
& (sequence_ids[b, q_idx] != -1)
|
| 34 |
+
& (sequence_ids[b, kv_idx] != -1)
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
batch_size, seqlen = sequence_ids.shape
|
| 38 |
+
return create_block_mask(
|
| 39 |
+
document_mask, batch_size, 1, seqlen, seqlen, device=sequence_ids.device
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@torch.compiler.disable
|
| 44 |
+
def create_within_seq_block_mask(sequence_ids: torch.Tensor) -> BlockMask:
|
| 45 |
+
if create_block_mask is None:
|
| 46 |
+
raise RuntimeError("Flex Attention block-mask creation is unavailable in this environment.")
|
| 47 |
+
def document_mask(b, h, q_idx, kv_idx): # type: ignore[no-untyped-def]
|
| 48 |
+
return (
|
| 49 |
+
(sequence_ids[b, q_idx] == sequence_ids[b, kv_idx])
|
| 50 |
+
& (sequence_ids[b, q_idx] != -1)
|
| 51 |
+
& (sequence_ids[b, kv_idx] != -1)
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
batch_size, seqlen = sequence_ids.shape
|
| 55 |
+
return create_block_mask(
|
| 56 |
+
document_mask, batch_size, 1, seqlen, seqlen, device=sequence_ids.device
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def build_within_seq_mask_4d(sequence_ids: torch.Tensor) -> torch.Tensor:
|
| 61 |
+
not_pad = sequence_ids != -1
|
| 62 |
+
same_seq = sequence_ids.unsqueeze(-1) == sequence_ids.unsqueeze(-2)
|
| 63 |
+
valid = not_pad.unsqueeze(-1) & not_pad.unsqueeze(-2)
|
| 64 |
+
return (same_seq & valid).unsqueeze(1)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def build_block_causal_mask_4d(sequence_ids: torch.Tensor) -> torch.Tensor:
|
| 68 |
+
not_pad = sequence_ids != -1
|
| 69 |
+
causal = sequence_ids.unsqueeze(-1) >= sequence_ids.unsqueeze(-2)
|
| 70 |
+
valid = not_pad.unsqueeze(-1) & not_pad.unsqueeze(-2)
|
| 71 |
+
return (causal & valid).unsqueeze(1)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def flex_attention_func(
|
| 75 |
+
query_states: torch.Tensor, # Q has shape (b, l, h, d).
|
| 76 |
+
key_states: torch.Tensor, # K has shape (b, l, h_kv, d).
|
| 77 |
+
value_states: torch.Tensor, # V has shape (b, l, h_kv, d).
|
| 78 |
+
score_mod: Callable | None = None,
|
| 79 |
+
block_mask: BlockMask | None = None,
|
| 80 |
+
sequence_lengths: tuple[int, ...] | None = None,
|
| 81 |
+
mask_semantics: str = "within_sequence",
|
| 82 |
+
) -> torch.Tensor:
|
| 83 |
+
if flex_attention is None:
|
| 84 |
+
raise RuntimeError("Flex Attention is not available in this environment.")
|
| 85 |
+
if score_mod is not None:
|
| 86 |
+
raise NotImplementedError("E1 Flex Attention does not support score_mod.")
|
| 87 |
+
query_states = query_states.transpose(1, 2).contiguous() # (bs, nh, seqlen, hs)
|
| 88 |
+
key_states = key_states.transpose(1, 2).contiguous() # (bs, nkv, seqlen, hs)
|
| 89 |
+
value_states = value_states.transpose(1, 2).contiguous() # (bs, nkv, seqlen, hs)
|
| 90 |
+
|
| 91 |
+
fn = _get_flex_attention_fn(
|
| 92 |
+
device=query_states.device,
|
| 93 |
+
dtype=query_states.dtype,
|
| 94 |
+
shape=tuple(query_states.shape),
|
| 95 |
+
sequence_lengths=sequence_lengths,
|
| 96 |
+
mask_semantics=mask_semantics,
|
| 97 |
+
)
|
| 98 |
+
if fn is None:
|
| 99 |
+
raise RuntimeError("Flex Attention is not available in this environment.")
|
| 100 |
+
outputs = fn(
|
| 101 |
+
query_states,
|
| 102 |
+
key_states,
|
| 103 |
+
value_states,
|
| 104 |
+
block_mask=block_mask,
|
| 105 |
+
score_mod=score_mod,
|
| 106 |
+
enable_gqa=query_states.shape[1] != key_states.shape[1], # if nkv != nh
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
outputs = outputs.transpose(1, 2) # (bs, seqlen, nh, hs)
|
| 110 |
+
return outputs
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def kernels_flash_attention_func(
|
| 114 |
+
query_states: torch.Tensor, # (bs, seqlen, nh, hs)
|
| 115 |
+
key_states: torch.Tensor, # (bs, seqlen, nkv, hs)
|
| 116 |
+
value_states: torch.Tensor, # (bs, seqlen, nkv, hs)
|
| 117 |
+
q_sequence_ids: torch.Tensor,
|
| 118 |
+
k_sequence_ids: torch.Tensor,
|
| 119 |
+
causal: bool = False,
|
| 120 |
+
implementation: str = "flash_attention_3",
|
| 121 |
+
) -> torch.Tensor: # (bs, seqlen, nh, hs)
|
| 122 |
+
_ensure_flash_kernels_loaded(implementation)
|
| 123 |
+
|
| 124 |
+
if not causal:
|
| 125 |
+
batch_size, q_len = query_states.shape[0], query_states.shape[1]
|
| 126 |
+
(
|
| 127 |
+
query_states,
|
| 128 |
+
key_states,
|
| 129 |
+
value_states,
|
| 130 |
+
indices_q,
|
| 131 |
+
(cu_seqlens_q, cu_seqlens_k),
|
| 132 |
+
(max_seqlen_in_batch_q, max_seqlen_in_batch_k),
|
| 133 |
+
) = _unpad_input(query_states, key_states, value_states, q_sequence_ids, k_sequence_ids)
|
| 134 |
+
|
| 135 |
+
attn_output_unpad = _kernels_flash_varlen_forward(
|
| 136 |
+
query_states,
|
| 137 |
+
key_states,
|
| 138 |
+
value_states,
|
| 139 |
+
cu_seqlens_q=cu_seqlens_q,
|
| 140 |
+
cu_seqlens_k=cu_seqlens_k,
|
| 141 |
+
max_seqlen_in_batch_q=max_seqlen_in_batch_q,
|
| 142 |
+
max_seqlen_in_batch_k=max_seqlen_in_batch_k,
|
| 143 |
+
causal=False,
|
| 144 |
+
implementation=implementation,
|
| 145 |
+
)
|
| 146 |
+
attn_output = pad_input(attn_output_unpad, indices_q, batch_size, q_len)
|
| 147 |
+
|
| 148 |
+
else:
|
| 149 |
+
attn_output = _kernels_flash_forward(
|
| 150 |
+
query_states, key_states, value_states, causal=True, implementation=implementation
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
return attn_output
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def block_min_max_seq_ids(
|
| 157 |
+
sequence_lengths: torch.Tensor,
|
| 158 |
+
block_size: int = 128,
|
| 159 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 160 |
+
"""Map each physical attention block to its first and last sequence."""
|
| 161 |
+
|
| 162 |
+
total_tokens = sequence_lengths.sum()
|
| 163 |
+
block_count = int(
|
| 164 |
+
torch.div(
|
| 165 |
+
total_tokens + block_size - 1,
|
| 166 |
+
block_size,
|
| 167 |
+
rounding_mode="floor",
|
| 168 |
+
).item()
|
| 169 |
+
)
|
| 170 |
+
padded_tokens = block_count * block_size - total_tokens
|
| 171 |
+
lengths_with_tail = torch.cat(
|
| 172 |
+
(sequence_lengths, padded_tokens.to(sequence_lengths).reshape(1)),
|
| 173 |
+
)
|
| 174 |
+
sequence_ends = lengths_with_tail.to(torch.long).cumsum(dim=0)
|
| 175 |
+
block_starts = torch.arange(
|
| 176 |
+
start=0,
|
| 177 |
+
end=block_count * block_size,
|
| 178 |
+
step=block_size,
|
| 179 |
+
dtype=torch.long,
|
| 180 |
+
device=sequence_lengths.device,
|
| 181 |
+
)
|
| 182 |
+
block_last_tokens = block_starts + block_size - 1
|
| 183 |
+
first_sequence = torch.searchsorted(sequence_ends, block_starts, right=True)
|
| 184 |
+
last_sequence = torch.searchsorted(sequence_ends, block_last_tokens, right=True)
|
| 185 |
+
return first_sequence, last_sequence
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def get_overlapping_blocks(
|
| 189 |
+
q_lengths: torch.Tensor,
|
| 190 |
+
k_lengths: torch.Tensor,
|
| 191 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 192 |
+
"""Classify query/key block pairs as full, partial, or disjoint."""
|
| 193 |
+
|
| 194 |
+
q_first, q_last = block_min_max_seq_ids(q_lengths)
|
| 195 |
+
k_first, k_last = block_min_max_seq_ids(k_lengths)
|
| 196 |
+
intersection_start = torch.maximum(q_first[:, None], k_first[None, :])
|
| 197 |
+
intersection_end = torch.minimum(q_last[:, None], k_last[None, :])
|
| 198 |
+
intersects = intersection_start <= intersection_end
|
| 199 |
+
both_blocks_are_single_sequence = (q_first == q_last)[:, None] & (k_first == k_last)[None, :]
|
| 200 |
+
full_blocks = intersects & both_blocks_are_single_sequence
|
| 201 |
+
return full_blocks, intersects & ~both_blocks_are_single_sequence
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def _document_ids(sequence_lengths: torch.Tensor) -> torch.Tensor:
|
| 205 |
+
sequence_numbers = torch.arange(
|
| 206 |
+
sequence_lengths.numel(),
|
| 207 |
+
device=sequence_lengths.device,
|
| 208 |
+
dtype=torch.long,
|
| 209 |
+
)
|
| 210 |
+
return sequence_numbers.repeat_interleave(sequence_lengths.to(torch.long))
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
@torch.compiler.disable
|
| 214 |
+
def direct_block_mask(q_lengths: torch.Tensor, k_lengths: torch.Tensor) -> BlockMask:
|
| 215 |
+
"""Build a packed-sequence mask from preclassified sparse blocks."""
|
| 216 |
+
|
| 217 |
+
full, partial = get_overlapping_blocks(q_lengths, k_lengths)
|
| 218 |
+
q_document = _document_ids(q_lengths)
|
| 219 |
+
k_document = _document_ids(k_lengths)
|
| 220 |
+
|
| 221 |
+
def same_document(
|
| 222 |
+
_batch: torch.Tensor,
|
| 223 |
+
_head: torch.Tensor,
|
| 224 |
+
q_index: torch.Tensor,
|
| 225 |
+
k_index: torch.Tensor,
|
| 226 |
+
) -> torch.Tensor:
|
| 227 |
+
return q_document[q_index].eq(k_document[k_index])
|
| 228 |
+
|
| 229 |
+
return _create_sparse_block_from_block_mask(
|
| 230 |
+
(partial[None, None], full[None, None]),
|
| 231 |
+
same_document,
|
| 232 |
+
seq_lengths=(q_document.numel(), k_document.numel()),
|
| 233 |
+
Q_BLOCK_SIZE=128,
|
| 234 |
+
KV_BLOCK_SIZE=128,
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
@torch.compiler.disable
|
| 239 |
+
def doc_id_mask(q_lengths: torch.Tensor, k_lengths: torch.Tensor) -> BlockMask:
|
| 240 |
+
if create_block_mask is None:
|
| 241 |
+
raise RuntimeError("Flex Attention block-mask creation is unavailable in this environment.")
|
| 242 |
+
q_document = _document_ids(q_lengths)
|
| 243 |
+
k_document = _document_ids(k_lengths)
|
| 244 |
+
|
| 245 |
+
def same_document(
|
| 246 |
+
_batch: torch.Tensor,
|
| 247 |
+
_head: torch.Tensor,
|
| 248 |
+
q_index: torch.Tensor,
|
| 249 |
+
k_index: torch.Tensor,
|
| 250 |
+
) -> torch.Tensor:
|
| 251 |
+
return q_document[q_index].eq(k_document[k_index])
|
| 252 |
+
|
| 253 |
+
return create_block_mask(
|
| 254 |
+
same_document,
|
| 255 |
+
1,
|
| 256 |
+
1,
|
| 257 |
+
q_document.numel(),
|
| 258 |
+
k_document.numel(),
|
| 259 |
+
BLOCK_SIZE=128,
|
| 260 |
+
device=q_lengths.device,
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def varlen_flex_attention_func(
|
| 265 |
+
query_states: torch.Tensor,
|
| 266 |
+
key_states: torch.Tensor,
|
| 267 |
+
value_states: torch.Tensor,
|
| 268 |
+
q_sequence_ids: torch.Tensor,
|
| 269 |
+
k_sequence_ids: torch.Tensor,
|
| 270 |
+
) -> torch.Tensor:
|
| 271 |
+
if flex_attention is None:
|
| 272 |
+
raise RuntimeError("Flex Attention is not available in this environment.")
|
| 273 |
+
batch_size, q_len = query_states.shape[0], query_states.shape[1]
|
| 274 |
+
(
|
| 275 |
+
query_states,
|
| 276 |
+
key_states,
|
| 277 |
+
value_states,
|
| 278 |
+
indices_q,
|
| 279 |
+
(cu_seqlens_q, cu_seqlens_k),
|
| 280 |
+
(_max_seqlen_in_batch_q, _max_seqlen_in_batch_k),
|
| 281 |
+
) = _unpad_input(query_states, key_states, value_states, q_sequence_ids, k_sequence_ids)
|
| 282 |
+
|
| 283 |
+
query_states = query_states.unsqueeze(0).transpose(1, 2).contiguous()
|
| 284 |
+
key_states = key_states.unsqueeze(0).transpose(1, 2).contiguous()
|
| 285 |
+
value_states = value_states.unsqueeze(0).transpose(1, 2).contiguous()
|
| 286 |
+
|
| 287 |
+
seqlens_q = cu_seqlens_q[1:] - cu_seqlens_q[:-1]
|
| 288 |
+
seqlens_k = cu_seqlens_k[1:] - cu_seqlens_k[:-1]
|
| 289 |
+
block_mask = block_mask_creator(seqlens_q, seqlens_k)
|
| 290 |
+
|
| 291 |
+
packed_lengths = (
|
| 292 |
+
*(int(length) for length in seqlens_q.tolist()),
|
| 293 |
+
-1,
|
| 294 |
+
*(int(length) for length in seqlens_k.tolist()),
|
| 295 |
+
)
|
| 296 |
+
fn = _get_flex_attention_fn(
|
| 297 |
+
device=query_states.device,
|
| 298 |
+
dtype=query_states.dtype,
|
| 299 |
+
shape=tuple(query_states.shape) + tuple(key_states.shape),
|
| 300 |
+
sequence_lengths=packed_lengths,
|
| 301 |
+
mask_semantics="packed_document_equality",
|
| 302 |
+
)
|
| 303 |
+
if fn is None:
|
| 304 |
+
raise RuntimeError("Flex Attention is not available in this environment.")
|
| 305 |
+
attn_output_unpad = fn(
|
| 306 |
+
query_states,
|
| 307 |
+
key_states,
|
| 308 |
+
value_states,
|
| 309 |
+
block_mask=block_mask,
|
| 310 |
+
enable_gqa=query_states.shape[1] != key_states.shape[1],
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
attn_output = pad_input(
|
| 314 |
+
attn_output_unpad.transpose(1, 2).squeeze(0), indices_q, batch_size, q_len
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
return attn_output
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def _get_unpad_data(sequence_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, int]:
|
| 321 |
+
"""Return packed indices and run lengths for the non-padding sequence IDs."""
|
| 322 |
+
|
| 323 |
+
flat_ids = sequence_ids.reshape(-1)
|
| 324 |
+
non_pad_indices = torch.where(flat_ids.ne(-1))[0]
|
| 325 |
+
if non_pad_indices.numel() == 0:
|
| 326 |
+
raise ValueError("Packed attention requires at least one non-padding token.")
|
| 327 |
+
|
| 328 |
+
valid_ids = flat_ids.index_select(0, non_pad_indices)
|
| 329 |
+
row_ids = torch.div(
|
| 330 |
+
non_pad_indices,
|
| 331 |
+
sequence_ids.shape[1],
|
| 332 |
+
rounding_mode="floor",
|
| 333 |
+
)
|
| 334 |
+
run_starts = torch.ones_like(valid_ids, dtype=torch.bool)
|
| 335 |
+
run_starts[1:] = (valid_ids[1:] != valid_ids[:-1]) | (row_ids[1:] != row_ids[:-1])
|
| 336 |
+
start_indices = torch.where(run_starts)[0]
|
| 337 |
+
end_indices = torch.cat((start_indices[1:], start_indices.new_tensor([valid_ids.numel()])))
|
| 338 |
+
sequence_lengths = end_indices - start_indices
|
| 339 |
+
cumulative_lengths = torch.cat(
|
| 340 |
+
(
|
| 341 |
+
torch.zeros(1, dtype=torch.int32, device=sequence_ids.device),
|
| 342 |
+
sequence_lengths.cumsum(dim=0, dtype=torch.int32),
|
| 343 |
+
),
|
| 344 |
+
)
|
| 345 |
+
return non_pad_indices, cumulative_lengths, int(sequence_lengths.max().item())
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def _unpad_input(
|
| 349 |
+
query_layer: torch.Tensor,
|
| 350 |
+
key_layer: torch.Tensor,
|
| 351 |
+
value_layer: torch.Tensor,
|
| 352 |
+
q_sequence_ids: torch.Tensor,
|
| 353 |
+
k_sequence_ids: torch.Tensor,
|
| 354 |
+
) -> tuple[
|
| 355 |
+
torch.Tensor,
|
| 356 |
+
torch.Tensor,
|
| 357 |
+
torch.Tensor,
|
| 358 |
+
torch.Tensor,
|
| 359 |
+
tuple[torch.Tensor, torch.Tensor],
|
| 360 |
+
tuple[int, int],
|
| 361 |
+
]:
|
| 362 |
+
for name, layer in (
|
| 363 |
+
("query_layer", query_layer),
|
| 364 |
+
("key_layer", key_layer),
|
| 365 |
+
("value_layer", value_layer),
|
| 366 |
+
):
|
| 367 |
+
if layer.ndim != 4:
|
| 368 |
+
raise ValueError(
|
| 369 |
+
f"{name} must have shape (batch, sequence, heads, head_dim); "
|
| 370 |
+
f"got {tuple(layer.shape)}."
|
| 371 |
+
)
|
| 372 |
+
if value_layer.shape != key_layer.shape:
|
| 373 |
+
raise ValueError(
|
| 374 |
+
"key_layer and value_layer must have identical shapes; "
|
| 375 |
+
f"got {tuple(key_layer.shape)} and {tuple(value_layer.shape)}."
|
| 376 |
+
)
|
| 377 |
+
if query_layer.shape[0] != key_layer.shape[0]:
|
| 378 |
+
raise ValueError(
|
| 379 |
+
"Query and KV batch sizes must match; "
|
| 380 |
+
f"got {query_layer.shape[0]} and {key_layer.shape[0]}."
|
| 381 |
+
)
|
| 382 |
+
if query_layer.shape[-1] != key_layer.shape[-1]:
|
| 383 |
+
raise ValueError(
|
| 384 |
+
"Query and KV head dimensions must match; "
|
| 385 |
+
f"got {query_layer.shape[-1]} and {key_layer.shape[-1]}."
|
| 386 |
+
)
|
| 387 |
+
batch_size, kv_seq_len, num_heads, head_dim = key_layer.shape
|
| 388 |
+
query_length, num_q_heads = query_layer.shape[1], query_layer.shape[2]
|
| 389 |
+
if query_layer.shape[:2] != q_sequence_ids.shape:
|
| 390 |
+
raise ValueError(
|
| 391 |
+
"Shape mismatch between query layer and query sequence ids: "
|
| 392 |
+
f"{query_layer.shape[:2]} != {q_sequence_ids.shape}"
|
| 393 |
+
)
|
| 394 |
+
if key_layer.shape[:2] != k_sequence_ids.shape:
|
| 395 |
+
raise ValueError(
|
| 396 |
+
"Shape mismatch between key layer and key sequence ids: "
|
| 397 |
+
f"{key_layer.shape[:2]} != {k_sequence_ids.shape}"
|
| 398 |
+
)
|
| 399 |
+
if query_length > kv_seq_len:
|
| 400 |
+
raise ValueError(
|
| 401 |
+
"Query length must be less than or equal to KV sequence length: "
|
| 402 |
+
f"{query_length} > {kv_seq_len}"
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
indices_k, cu_seqlens_k, max_seqlen_in_batch_k = _get_unpad_data(k_sequence_ids)
|
| 406 |
+
|
| 407 |
+
key_layer = index_first_axis(
|
| 408 |
+
key_layer.reshape(batch_size * kv_seq_len, num_heads, head_dim), indices_k
|
| 409 |
+
)
|
| 410 |
+
value_layer = index_first_axis(
|
| 411 |
+
value_layer.reshape(batch_size * kv_seq_len, num_heads, head_dim), indices_k
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
if torch.equal(q_sequence_ids, k_sequence_ids):
|
| 415 |
+
indices_q = indices_k
|
| 416 |
+
cu_seqlens_q = cu_seqlens_k
|
| 417 |
+
max_seqlen_in_batch_q = max_seqlen_in_batch_k
|
| 418 |
+
else:
|
| 419 |
+
indices_q, cu_seqlens_q, max_seqlen_in_batch_q = _get_unpad_data(q_sequence_ids)
|
| 420 |
+
|
| 421 |
+
query_layer = index_first_axis(
|
| 422 |
+
query_layer.reshape(batch_size * query_length, num_q_heads, head_dim), indices_q
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
if cu_seqlens_q.shape != cu_seqlens_k.shape:
|
| 426 |
+
raise ValueError(
|
| 427 |
+
"Query and KV must have the same number of sequences: "
|
| 428 |
+
f"{cu_seqlens_q.shape} != {cu_seqlens_k.shape}"
|
| 429 |
+
)
|
| 430 |
+
|
| 431 |
+
return (
|
| 432 |
+
query_layer,
|
| 433 |
+
key_layer,
|
| 434 |
+
value_layer,
|
| 435 |
+
indices_q,
|
| 436 |
+
(cu_seqlens_q, cu_seqlens_k),
|
| 437 |
+
(max_seqlen_in_batch_q, max_seqlen_in_batch_k),
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
block_mask_creator = direct_block_mask if os.getenv("FAST_BLOCK_MASK", "1") == "1" else doc_id_mask
|
fastplms/models/e1/cache.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Key-value cache implementations used by E1 inference."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
from transformers.modeling_outputs import ModelOutput
|
| 9 |
+
from transformers.utils import logging
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _get_logger():
|
| 13 |
+
"""Resolve the Transformers logger only when a cache path emits a message."""
|
| 14 |
+
|
| 15 |
+
return logging.get_logger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class DynamicCache:
|
| 19 |
+
"""A cache that grows K and V along their sequence dimension.
|
| 20 |
+
|
| 21 |
+
Each cached tensor has shape (b, l, h, d).
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
key_cache (`list[torch.Tensor]`): The list of key states.
|
| 25 |
+
value_cache (`list[torch.Tensor]`): The list of value states.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
def __init__(self) -> None:
|
| 29 |
+
self.key_cache: list[torch.Tensor] = []
|
| 30 |
+
self.value_cache: list[torch.Tensor] = []
|
| 31 |
+
|
| 32 |
+
def update(
|
| 33 |
+
self, key_states: torch.Tensor, value_states: torch.Tensor, layer_idx: int
|
| 34 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 35 |
+
"""
|
| 36 |
+
Update the key and value caches in-place, and return the necessary keys and value states.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
key_states (`torch.Tensor`): K to cache with shape (b, l, h, d).
|
| 40 |
+
value_states (`torch.Tensor`): V to cache with shape (b, l, h, d).
|
| 41 |
+
layer_idx (`int`): The index of the layer to update.
|
| 42 |
+
|
| 43 |
+
Returns:
|
| 44 |
+
tuple[`torch.Tensor`, `torch.Tensor`]: Cached K and V, each with shape
|
| 45 |
+
(b, l, h, d).
|
| 46 |
+
"""
|
| 47 |
+
# Lazy initialization
|
| 48 |
+
if len(self.key_cache) <= layer_idx:
|
| 49 |
+
# There may be skipped layers, fill them with empty lists
|
| 50 |
+
for _ in range(len(self.key_cache), layer_idx):
|
| 51 |
+
self.key_cache.append(torch.tensor([]))
|
| 52 |
+
self.value_cache.append(torch.tensor([]))
|
| 53 |
+
self.key_cache.append(key_states)
|
| 54 |
+
self.value_cache.append(value_states)
|
| 55 |
+
elif (
|
| 56 |
+
not self.key_cache[
|
| 57 |
+
layer_idx
|
| 58 |
+
].numel() # prefers not t.numel() to len(t) == 0 to export the model
|
| 59 |
+
): # fills previously skipped layers; checking for tensor causes errors
|
| 60 |
+
self.key_cache[layer_idx] = key_states
|
| 61 |
+
self.value_cache[layer_idx] = value_states
|
| 62 |
+
else:
|
| 63 |
+
self.key_cache[layer_idx] = torch.cat([self.key_cache[layer_idx], key_states], dim=1)
|
| 64 |
+
self.value_cache[layer_idx] = torch.cat(
|
| 65 |
+
[self.value_cache[layer_idx], value_states], dim=1
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
return self.key_cache[layer_idx], self.value_cache[layer_idx]
|
| 69 |
+
|
| 70 |
+
def get_seq_length(self, layer_idx: int = 0) -> int:
|
| 71 |
+
"""Return the cached sequence length for one layer."""
|
| 72 |
+
is_empty_layer = (
|
| 73 |
+
len(self.key_cache) == 0 # no cache in any layer
|
| 74 |
+
or len(self.key_cache)
|
| 75 |
+
<= layer_idx # skipped `layer_idx` and hasn't run a layer with cache after it
|
| 76 |
+
or not self.key_cache[layer_idx].numel() # the layer has no cache
|
| 77 |
+
)
|
| 78 |
+
layer_seq_length = self.key_cache[layer_idx].shape[1] if not is_empty_layer else 0
|
| 79 |
+
return layer_seq_length
|
| 80 |
+
|
| 81 |
+
def crop(self, max_length: int) -> None:
|
| 82 |
+
"""Crop every cached K and V tensor to ``max_length`` tokens."""
|
| 83 |
+
if max_length <= 0:
|
| 84 |
+
raise ValueError("max_length must be positive")
|
| 85 |
+
|
| 86 |
+
if self.get_seq_length() <= max_length:
|
| 87 |
+
return
|
| 88 |
+
|
| 89 |
+
for layer_idx in range(len(self.key_cache)):
|
| 90 |
+
if self.key_cache[layer_idx].numel():
|
| 91 |
+
self.key_cache[layer_idx] = self.key_cache[layer_idx][:, :max_length, ...]
|
| 92 |
+
self.value_cache[layer_idx] = self.value_cache[layer_idx][:, :max_length, ...]
|
| 93 |
+
|
| 94 |
+
def batch_repeat_interleave(self, repeats: int) -> None:
|
| 95 |
+
"""Repeat the cache `repeats` times in the batch dimension. Used in contrastive search."""
|
| 96 |
+
for layer_idx in range(len(self.key_cache)):
|
| 97 |
+
if self.key_cache[layer_idx].numel():
|
| 98 |
+
self.key_cache[layer_idx] = self.key_cache[layer_idx].repeat_interleave(
|
| 99 |
+
repeats, dim=0
|
| 100 |
+
)
|
| 101 |
+
self.value_cache[layer_idx] = self.value_cache[layer_idx].repeat_interleave(
|
| 102 |
+
repeats, dim=0
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
def batch_select_indices(self, indices: torch.Tensor) -> None:
|
| 106 |
+
"""Keep selected rows of the cache batch dimension."""
|
| 107 |
+
for layer_idx in range(len(self.key_cache)):
|
| 108 |
+
if self.key_cache[layer_idx].numel():
|
| 109 |
+
self.key_cache[layer_idx] = self.key_cache[layer_idx][indices, ...]
|
| 110 |
+
self.value_cache[layer_idx] = self.value_cache[layer_idx][indices, ...]
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class KVCache:
|
| 114 |
+
def __init__(self, cache_size: int = 4) -> None:
|
| 115 |
+
self.cache_size = cache_size
|
| 116 |
+
self.tensor_input_field_names = [
|
| 117 |
+
"input_ids",
|
| 118 |
+
"within_seq_position_ids",
|
| 119 |
+
"global_position_ids",
|
| 120 |
+
"sequence_ids",
|
| 121 |
+
"labels",
|
| 122 |
+
]
|
| 123 |
+
# Upstream E1 called the encoder output ``embeddings``. FastPLMs uses
|
| 124 |
+
# the standard Transformers ``last_hidden_state`` name, while keeping
|
| 125 |
+
# the aliases here makes the cache safe for either output contract.
|
| 126 |
+
self.tensor_output_field_names = [
|
| 127 |
+
"logits",
|
| 128 |
+
"last_hidden_state",
|
| 129 |
+
"embeddings",
|
| 130 |
+
"token_embeddings",
|
| 131 |
+
]
|
| 132 |
+
self.cache_dict: dict[str, DynamicCache] = {}
|
| 133 |
+
self.cache_queue: list[str] = []
|
| 134 |
+
|
| 135 |
+
def reset(self) -> None:
|
| 136 |
+
for k in list(self.cache_dict.keys()):
|
| 137 |
+
del self.cache_dict[k]
|
| 138 |
+
del self.cache_dict
|
| 139 |
+
self.cache_dict = {}
|
| 140 |
+
self.cache_queue = []
|
| 141 |
+
|
| 142 |
+
torch.cuda.empty_cache()
|
| 143 |
+
|
| 144 |
+
def before_forward(self, batch: dict[str, torch.Tensor]) -> None:
|
| 145 |
+
contexts: list[str] | None = batch.get("context")
|
| 146 |
+
if contexts is None or "context_len" not in batch:
|
| 147 |
+
_get_logger().warning_once(
|
| 148 |
+
"KVCache requires both `context` and `context_len`; cache setup was skipped."
|
| 149 |
+
)
|
| 150 |
+
return
|
| 151 |
+
|
| 152 |
+
context_lens: list[int] = list(set(batch["context_len"]))
|
| 153 |
+
contexts: list[str] = list(set(contexts)) # type: ignore[no-redef]
|
| 154 |
+
if len(contexts) != 1 or len(context_lens) != 1:
|
| 155 |
+
_get_logger().warning(
|
| 156 |
+
"SingleContextKVCache requires a single context and context length. "
|
| 157 |
+
"Multiple contexts or context lengths found in a single batch. Skipping."
|
| 158 |
+
)
|
| 159 |
+
return
|
| 160 |
+
|
| 161 |
+
batch_size = batch["input_ids"].shape[0]
|
| 162 |
+
|
| 163 |
+
unique_context = contexts[0]
|
| 164 |
+
unique_context_len = context_lens[0]
|
| 165 |
+
batch["use_cache"] = True
|
| 166 |
+
|
| 167 |
+
if unique_context not in self.cache_dict:
|
| 168 |
+
return
|
| 169 |
+
|
| 170 |
+
self.cache_dict[unique_context].batch_repeat_interleave(batch_size)
|
| 171 |
+
past_key_values = self.cache_dict[unique_context]
|
| 172 |
+
batch["past_key_values"] = past_key_values
|
| 173 |
+
|
| 174 |
+
# Remove context from the input fields
|
| 175 |
+
for field_name in self.tensor_input_field_names:
|
| 176 |
+
if batch.get(field_name) is not None:
|
| 177 |
+
batch[field_name] = batch[field_name][:, unique_context_len:]
|
| 178 |
+
|
| 179 |
+
def after_forward(self, batch: dict[str, Any], outputs: ModelOutput) -> None:
|
| 180 |
+
contexts = batch.get("context")
|
| 181 |
+
context_lens = batch.get("context_len", [])
|
| 182 |
+
if (
|
| 183 |
+
contexts is None
|
| 184 |
+
or len(set(contexts)) != 1
|
| 185 |
+
or len(set(context_lens)) != 1
|
| 186 |
+
or context_lens[0] == 0
|
| 187 |
+
):
|
| 188 |
+
return
|
| 189 |
+
|
| 190 |
+
if not batch.get("use_cache", False):
|
| 191 |
+
raise ValueError("E1 retrieval cache updates require use_cache=True.")
|
| 192 |
+
unique_context = contexts[0]
|
| 193 |
+
unique_context_len = context_lens[0]
|
| 194 |
+
|
| 195 |
+
past_key_values = getattr(outputs, "past_key_values", None)
|
| 196 |
+
if not isinstance(past_key_values, DynamicCache):
|
| 197 |
+
_get_logger().warning_once(
|
| 198 |
+
"KVCache is incompatible with models that don't return a DynamicCache. Skipping."
|
| 199 |
+
)
|
| 200 |
+
return
|
| 201 |
+
|
| 202 |
+
if "past_key_values" not in batch:
|
| 203 |
+
if len(self.cache_queue) == self.cache_size:
|
| 204 |
+
last_context = self.cache_queue.pop(0)
|
| 205 |
+
if last_context not in self.cache_queue:
|
| 206 |
+
del self.cache_dict[last_context]
|
| 207 |
+
torch.cuda.empty_cache()
|
| 208 |
+
|
| 209 |
+
self.cache_dict[unique_context] = past_key_values
|
| 210 |
+
self.cache_queue.append(unique_context)
|
| 211 |
+
|
| 212 |
+
# Remove context from the input fields
|
| 213 |
+
for field_name in self.tensor_input_field_names:
|
| 214 |
+
if field_name in batch and batch[field_name] is not None:
|
| 215 |
+
batch[field_name] = batch[field_name][:, unique_context_len:]
|
| 216 |
+
|
| 217 |
+
# Remove context from the output fields
|
| 218 |
+
for field_name in self.tensor_output_field_names:
|
| 219 |
+
if field_name in outputs and outputs[field_name] is not None:
|
| 220 |
+
outputs[field_name] = outputs[field_name][:, unique_context_len:]
|
| 221 |
+
if "hidden_states" in outputs and outputs["hidden_states"] is not None:
|
| 222 |
+
hidden_states = outputs["hidden_states"]
|
| 223 |
+
sliced_hidden_states = tuple(
|
| 224 |
+
hidden_state[:, unique_context_len:] for hidden_state in hidden_states
|
| 225 |
+
)
|
| 226 |
+
outputs["hidden_states"] = sliced_hidden_states
|
| 227 |
+
|
| 228 |
+
self.cache_dict[unique_context].crop(unique_context_len)
|
| 229 |
+
self.cache_dict[unique_context].batch_select_indices([0])
|
fastplms/models/e1/modeling_e1.py
ADDED
|
@@ -0,0 +1,2319 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from collections import defaultdict
|
| 7 |
+
from contextvars import ContextVar
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from enum import Enum
|
| 10 |
+
from typing import Any, ClassVar, TypedDict
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
import torch.nn as nn
|
| 14 |
+
import torch.nn.functional as F
|
| 15 |
+
from tqdm.auto import tqdm
|
| 16 |
+
from transformers import PretrainedConfig, PreTrainedModel
|
| 17 |
+
from transformers.activations import ACT2FN
|
| 18 |
+
from transformers.modeling_outputs import ModelOutput
|
| 19 |
+
from transformers.utils import logging
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
from fastplms.attention import (
|
| 23 |
+
AttentionBackend,
|
| 24 |
+
BlockMask,
|
| 25 |
+
FastPLMsAttentionMixin,
|
| 26 |
+
resolve_attention_backend,
|
| 27 |
+
resolve_attention_backend_for_call,
|
| 28 |
+
)
|
| 29 |
+
from fastplms.embeddings import (
|
| 30 |
+
EmbeddingBatch,
|
| 31 |
+
EmbeddingMixin,
|
| 32 |
+
EmbeddingResult,
|
| 33 |
+
Pooler,
|
| 34 |
+
embed_dataset,
|
| 35 |
+
select_hidden_state_embeddings,
|
| 36 |
+
)
|
| 37 |
+
from fastplms.models.ttt import FastPLMTestTimeTrainingMixin
|
| 38 |
+
except ModuleNotFoundError as error:
|
| 39 |
+
_COMPOSITE_REQUIRED_NAMES = (
|
| 40 |
+
"AttentionBackend",
|
| 41 |
+
"BlockMask",
|
| 42 |
+
"EmbeddingBatch",
|
| 43 |
+
"EmbeddingMixin",
|
| 44 |
+
"EmbeddingResult",
|
| 45 |
+
"FastPLMsAttentionMixin",
|
| 46 |
+
"FastPLMTestTimeTrainingMixin",
|
| 47 |
+
"Pooler",
|
| 48 |
+
"embed_dataset",
|
| 49 |
+
"resolve_attention_backend",
|
| 50 |
+
"resolve_attention_backend_for_call",
|
| 51 |
+
"select_hidden_state_embeddings",
|
| 52 |
+
)
|
| 53 |
+
if error.name != "fastplms" or any(
|
| 54 |
+
name not in globals() for name in _COMPOSITE_REQUIRED_NAMES
|
| 55 |
+
):
|
| 56 |
+
raise
|
| 57 |
+
# Legacy flat Hub composites define every shared symbol above this block.
|
| 58 |
+
|
| 59 |
+
from .attention import ( # noqa: F401
|
| 60 |
+
_document_ids,
|
| 61 |
+
_get_unpad_data,
|
| 62 |
+
_unpad_input,
|
| 63 |
+
block_mask_creator,
|
| 64 |
+
block_min_max_seq_ids,
|
| 65 |
+
build_block_causal_mask_4d,
|
| 66 |
+
build_within_seq_mask_4d,
|
| 67 |
+
create_block_causal_mask_optimized,
|
| 68 |
+
create_within_seq_block_mask,
|
| 69 |
+
direct_block_mask,
|
| 70 |
+
doc_id_mask,
|
| 71 |
+
flex_attention_func,
|
| 72 |
+
get_overlapping_blocks,
|
| 73 |
+
kernels_flash_attention_func,
|
| 74 |
+
varlen_flex_attention_func,
|
| 75 |
+
)
|
| 76 |
+
from .cache import DynamicCache, KVCache # noqa: F401
|
| 77 |
+
from .preparation import ( # noqa: F401
|
| 78 |
+
BOS_TOKEN_ID,
|
| 79 |
+
E1_TOKENIZER_REPO_ID,
|
| 80 |
+
E1_VOCAB_SIZE,
|
| 81 |
+
EOS_TOKEN_ID,
|
| 82 |
+
PAD_TOKEN_ID,
|
| 83 |
+
DataPrepConfig,
|
| 84 |
+
E1BatchPreparer,
|
| 85 |
+
_load_tokenizer_file,
|
| 86 |
+
get_context,
|
| 87 |
+
get_tokenizer,
|
| 88 |
+
)
|
| 89 |
+
from .retrieval import ( # noqa: F401
|
| 90 |
+
COLABFOLD_HOST,
|
| 91 |
+
DEFAULT_EMBED_MAX_TOKENS,
|
| 92 |
+
DEFAULT_EMBED_SIMILARITY,
|
| 93 |
+
DEFAULT_MAX_CONTEXT_TOKENS,
|
| 94 |
+
DEFAULT_SIMILARITY_THRESHOLDS,
|
| 95 |
+
DOCKER_IMAGE,
|
| 96 |
+
E1_MSA_SAMPLING_SOURCE_REVISION,
|
| 97 |
+
LOWERCASE_CHARS,
|
| 98 |
+
ColabFoldSearcher,
|
| 99 |
+
ContextCache,
|
| 100 |
+
ContextSpecification,
|
| 101 |
+
E1Prediction,
|
| 102 |
+
HomologueSearcher,
|
| 103 |
+
IdSequence,
|
| 104 |
+
IndexedSequence,
|
| 105 |
+
_ColabFoldResponse,
|
| 106 |
+
_E1ContextPredictor,
|
| 107 |
+
_forward_for_embedding,
|
| 108 |
+
_make_homologue_searcher,
|
| 109 |
+
_pool_hidden_states,
|
| 110 |
+
_safe_extract_tar,
|
| 111 |
+
_sequence_output_dir,
|
| 112 |
+
_strip_a3m_insertions,
|
| 113 |
+
build_context_specifications,
|
| 114 |
+
compute_ppll,
|
| 115 |
+
convert_to_tensor,
|
| 116 |
+
get_context_id,
|
| 117 |
+
get_msa_for_sequence,
|
| 118 |
+
get_num_neighbors,
|
| 119 |
+
get_query_from_a3m,
|
| 120 |
+
get_similarity_to_query,
|
| 121 |
+
load_msa_dir,
|
| 122 |
+
load_msa_from_hf,
|
| 123 |
+
parse_msa,
|
| 124 |
+
read_fasta_sequences,
|
| 125 |
+
sample_context,
|
| 126 |
+
sample_contexts_for_msa,
|
| 127 |
+
sample_multiple_contexts,
|
| 128 |
+
write_fasta_sequences,
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _get_logger():
|
| 133 |
+
"""Resolve the Transformers logger only when a runtime path emits a message."""
|
| 134 |
+
|
| 135 |
+
return logging.get_logger(__name__)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
_TOKENIZER_LOAD_CONTEXT: ContextVar[dict[str, Any] | None] = ContextVar(
|
| 139 |
+
"fastplms_e1_tokenizer_load_context",
|
| 140 |
+
default=None,
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class E1Config(PretrainedConfig):
|
| 145 |
+
model_type = "E1"
|
| 146 |
+
keys_to_ignore_at_inference: ClassVar[list[str]] = ["past_key_values"]
|
| 147 |
+
|
| 148 |
+
def __init__( # type: ignore
|
| 149 |
+
self,
|
| 150 |
+
# Model architecture/initialization
|
| 151 |
+
vocab_size=None,
|
| 152 |
+
hidden_size=4096,
|
| 153 |
+
intermediate_size=16384,
|
| 154 |
+
gated_mlp=False,
|
| 155 |
+
num_hidden_layers=40,
|
| 156 |
+
num_attention_heads=32,
|
| 157 |
+
num_key_value_heads=8,
|
| 158 |
+
hidden_act="silu",
|
| 159 |
+
rms_norm_eps=1e-5,
|
| 160 |
+
initializer_range=0.02,
|
| 161 |
+
dtype="bfloat16",
|
| 162 |
+
gradient_checkpointing=False,
|
| 163 |
+
no_ffn_gradient_checkpointing=False,
|
| 164 |
+
use_cache=False,
|
| 165 |
+
# Tokenization
|
| 166 |
+
pad_token_id=None,
|
| 167 |
+
bos_token_id=None,
|
| 168 |
+
eos_token_id=None,
|
| 169 |
+
tie_word_embeddings=False,
|
| 170 |
+
# Attention implementation & rotary positional embeddings
|
| 171 |
+
global_attention_every_n_layers=0,
|
| 172 |
+
max_num_sequences=512,
|
| 173 |
+
max_num_positions_within_seq=8192,
|
| 174 |
+
max_num_positions_global=1024 * 128,
|
| 175 |
+
rope_theta_within_seq=10000.0,
|
| 176 |
+
rope_theta_global=100000.0,
|
| 177 |
+
clip_qkv=None,
|
| 178 |
+
attn_backend=None,
|
| 179 |
+
**kwargs,
|
| 180 |
+
) -> None:
|
| 181 |
+
super().__init__(
|
| 182 |
+
pad_token_id=PAD_TOKEN_ID,
|
| 183 |
+
bos_token_id=BOS_TOKEN_ID,
|
| 184 |
+
eos_token_id=EOS_TOKEN_ID,
|
| 185 |
+
tie_word_embeddings=tie_word_embeddings,
|
| 186 |
+
dtype=dtype,
|
| 187 |
+
**kwargs,
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
self.hidden_size = hidden_size
|
| 191 |
+
if intermediate_size is None:
|
| 192 |
+
intermediate_size = 3 * hidden_size if gated_mlp else 4 * hidden_size
|
| 193 |
+
self.intermediate_size = intermediate_size
|
| 194 |
+
self.gated_mlp = gated_mlp
|
| 195 |
+
self.num_hidden_layers = num_hidden_layers
|
| 196 |
+
self.num_attention_heads = num_attention_heads
|
| 197 |
+
self.max_num_positions_within_seq = max_num_positions_within_seq
|
| 198 |
+
self.max_num_positions_global = max_num_positions_global
|
| 199 |
+
|
| 200 |
+
# for backward compatibility
|
| 201 |
+
if num_key_value_heads is None:
|
| 202 |
+
num_key_value_heads = num_attention_heads
|
| 203 |
+
|
| 204 |
+
self.num_key_value_heads = num_key_value_heads
|
| 205 |
+
self.hidden_act = hidden_act
|
| 206 |
+
self.initializer_range = initializer_range
|
| 207 |
+
self.rms_norm_eps = rms_norm_eps
|
| 208 |
+
self.rope_theta_within_seq = rope_theta_within_seq
|
| 209 |
+
self.rope_theta_global = rope_theta_global
|
| 210 |
+
self.max_num_sequences = max_num_sequences
|
| 211 |
+
if clip_qkv is not None and clip_qkv <= 0:
|
| 212 |
+
raise ValueError(f"clip_qkv must be positive when provided, got {clip_qkv}.")
|
| 213 |
+
self.clip_qkv = clip_qkv
|
| 214 |
+
self.global_attention_every_n_layers = global_attention_every_n_layers
|
| 215 |
+
|
| 216 |
+
self.vocab_size = E1_VOCAB_SIZE
|
| 217 |
+
self.gradient_checkpointing = gradient_checkpointing
|
| 218 |
+
self.no_ffn_gradient_checkpointing = no_ffn_gradient_checkpointing
|
| 219 |
+
if not isinstance(use_cache, bool):
|
| 220 |
+
raise TypeError("use_cache must be a boolean.")
|
| 221 |
+
self.use_cache = use_cache
|
| 222 |
+
self.attn_backend = attn_backend
|
| 223 |
+
|
| 224 |
+
if vocab_size is not None:
|
| 225 |
+
if vocab_size < self.vocab_size:
|
| 226 |
+
_get_logger().warning(
|
| 227 |
+
f"Using vocab_size {vocab_size} smaller than {self.vocab_size} "
|
| 228 |
+
"from the tokenizer contract."
|
| 229 |
+
)
|
| 230 |
+
self.vocab_size = vocab_size
|
| 231 |
+
elif vocab_size > self.vocab_size:
|
| 232 |
+
_get_logger().warning(
|
| 233 |
+
f"Using vocab_size {vocab_size} instead of smaller {self.vocab_size} "
|
| 234 |
+
"from E1 tokenizer contract."
|
| 235 |
+
)
|
| 236 |
+
self.vocab_size = vocab_size
|
| 237 |
+
if pad_token_id is not None and pad_token_id != self.pad_token_id:
|
| 238 |
+
_get_logger().warning(
|
| 239 |
+
f"Ignoring pad_token_id. Using {self.pad_token_id} from E1 tokenizer contract"
|
| 240 |
+
)
|
| 241 |
+
if bos_token_id is not None and bos_token_id != self.bos_token_id:
|
| 242 |
+
_get_logger().warning(
|
| 243 |
+
f"Ignoring bos_token_id. Using {self.bos_token_id} from E1 tokenizer contract"
|
| 244 |
+
)
|
| 245 |
+
if eos_token_id is not None and eos_token_id != self.eos_token_id:
|
| 246 |
+
_get_logger().warning(
|
| 247 |
+
f"Ignoring eos_token_id. Using {self.eos_token_id} from E1 tokenizer contract"
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
class AttentionLayerType(Enum):
|
| 252 |
+
WITHIN_SEQ = "within_seq"
|
| 253 |
+
GLOBAL = "global"
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
class AttentionArgs(TypedDict, total=False):
|
| 257 |
+
within_seq_block_mask: BlockMask | None
|
| 258 |
+
block_causal_block_mask: BlockMask | None
|
| 259 |
+
within_seq_mask_4d: torch.Tensor | None
|
| 260 |
+
block_causal_mask_4d: torch.Tensor | None
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
|
| 264 |
+
"""This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep).
|
| 265 |
+
|
| 266 |
+
The hidden states go from (batch, num_key_value_heads, seqlen, head_dim) to (batch,
|
| 267 |
+
num_attention_heads, seqlen, head_dim)
|
| 268 |
+
"""
|
| 269 |
+
batch, num_key_value_heads, slen, head_dim = hidden_states.shape
|
| 270 |
+
if n_rep == 1:
|
| 271 |
+
return hidden_states
|
| 272 |
+
hidden_states = hidden_states[:, :, None, :, :].expand(
|
| 273 |
+
batch, num_key_value_heads, n_rep, slen, head_dim
|
| 274 |
+
)
|
| 275 |
+
return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
class RotaryPositionalEmbedding(nn.Module):
|
| 279 |
+
def __init__(
|
| 280 |
+
self,
|
| 281 |
+
dim: int,
|
| 282 |
+
max_position_embeddings: int = 2048,
|
| 283 |
+
base: int = 10000,
|
| 284 |
+
device: torch.device | None = None,
|
| 285 |
+
):
|
| 286 |
+
super().__init__()
|
| 287 |
+
|
| 288 |
+
self.dim = dim
|
| 289 |
+
self.base = base
|
| 290 |
+
self.max_position_embeddings = max_position_embeddings
|
| 291 |
+
# Transformers may instantiate modules on the meta device while loading
|
| 292 |
+
# a checkpoint. Precomputed non-persistent buffers would then be
|
| 293 |
+
# materialized without values. Empty buffers make initialization lazy
|
| 294 |
+
# and deterministic on the first real-device forward.
|
| 295 |
+
empty = torch.empty(0, dtype=torch.float32, device=device)
|
| 296 |
+
self.register_buffer("inv_freq", empty, persistent=False)
|
| 297 |
+
self.register_buffer("cos_cached", empty.clone(), persistent=False)
|
| 298 |
+
self.register_buffer("sin_cached", empty.clone(), persistent=False)
|
| 299 |
+
self.max_seq_len_cached = 0
|
| 300 |
+
|
| 301 |
+
@staticmethod
|
| 302 |
+
def rotate_half(x: torch.Tensor) -> torch.Tensor:
|
| 303 |
+
"""Rotates half the hidden dims of the input."""
|
| 304 |
+
x1 = x[..., : x.shape[-1] // 2]
|
| 305 |
+
x2 = x[..., x.shape[-1] // 2 :]
|
| 306 |
+
return torch.cat((-x2, x1), dim=-1)
|
| 307 |
+
|
| 308 |
+
def _set_sin_cos_cache(self, seq_len: int, device: torch.device) -> None:
|
| 309 |
+
# Compute angles in FP32, matching the official cache constructed before
|
| 310 |
+
# the model is converted to its inference dtype.
|
| 311 |
+
self.max_seq_len_cached = seq_len
|
| 312 |
+
inv_freq = self.base ** -(
|
| 313 |
+
torch.arange(0, self.dim, 2, dtype=torch.float32, device=device) / self.dim
|
| 314 |
+
)
|
| 315 |
+
self.inv_freq = inv_freq
|
| 316 |
+
t = torch.arange(seq_len, device=device, dtype=torch.float32)
|
| 317 |
+
angles = torch.outer(t, inv_freq)
|
| 318 |
+
angles = torch.cat((angles, angles), dim=1)
|
| 319 |
+
self.cos_cached = angles.cos()
|
| 320 |
+
self.sin_cached = angles.sin()
|
| 321 |
+
|
| 322 |
+
def forward(
|
| 323 |
+
self,
|
| 324 |
+
q: torch.Tensor,
|
| 325 |
+
k: torch.Tensor,
|
| 326 |
+
position_ids: torch.LongTensor,
|
| 327 |
+
seq_len: int | None = None,
|
| 328 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 329 |
+
# Q and K have shape (b, l, h, d).
|
| 330 |
+
device, dtype = q.device, q.dtype
|
| 331 |
+
seq_len = position_ids.max().item() + 1 if seq_len is None else seq_len
|
| 332 |
+
|
| 333 |
+
if seq_len > self.max_seq_len_cached:
|
| 334 |
+
self._set_sin_cos_cache(seq_len=seq_len, device=device)
|
| 335 |
+
|
| 336 |
+
# Selecting by position gives C and S shape (b, l, d). Insert a head
|
| 337 |
+
# axis so they broadcast over Q and K with shape (b, l, h, d).
|
| 338 |
+
idxs = position_ids.to(device)
|
| 339 |
+
cos = self.cos_cached.to(device=device, dtype=dtype).unsqueeze(-2)[idxs]
|
| 340 |
+
sin = self.sin_cached.to(device=device, dtype=dtype).unsqueeze(-2)[idxs]
|
| 341 |
+
|
| 342 |
+
# Apply the real and imaginary parts of the rotary transform to Q and K.
|
| 343 |
+
# Both halves reuse C and S, so rotate_half supplies the cross terms.
|
| 344 |
+
q_embed = (q * cos) + (self.rotate_half(q) * sin)
|
| 345 |
+
k_embed = (k * cos) + (self.rotate_half(k) * sin)
|
| 346 |
+
return q_embed, k_embed
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
class Attention(nn.Module):
|
| 350 |
+
"""Multi-headed attention from 'Attention Is All You Need' paper."""
|
| 351 |
+
|
| 352 |
+
def __init__(self, config: E1Config, layer_idx: int):
|
| 353 |
+
super().__init__()
|
| 354 |
+
self.config = config
|
| 355 |
+
self.layer_idx = layer_idx
|
| 356 |
+
|
| 357 |
+
self.hidden_size = config.hidden_size
|
| 358 |
+
self.num_heads = config.num_attention_heads
|
| 359 |
+
self.head_dim = self.hidden_size // self.num_heads
|
| 360 |
+
self.num_kv_heads = config.num_key_value_heads
|
| 361 |
+
self.num_key_value_groups = self.num_heads // self.num_kv_heads
|
| 362 |
+
self.max_num_seqs = config.max_num_sequences
|
| 363 |
+
self.clip_qkv = config.clip_qkv
|
| 364 |
+
|
| 365 |
+
if (self.head_dim * self.num_heads) != self.hidden_size:
|
| 366 |
+
raise ValueError(
|
| 367 |
+
f"hidden_size must be divisible by num_heads (got `hidden_size`: {self.hidden_size}"
|
| 368 |
+
f" and `num_heads`: {self.num_heads})."
|
| 369 |
+
)
|
| 370 |
+
self.q_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False)
|
| 371 |
+
self.k_proj = nn.Linear(self.hidden_size, self.num_kv_heads * self.head_dim, bias=False)
|
| 372 |
+
self.v_proj = nn.Linear(self.hidden_size, self.num_kv_heads * self.head_dim, bias=False)
|
| 373 |
+
self.o_proj = nn.Linear(self.num_heads * self.head_dim, self.hidden_size, bias=False)
|
| 374 |
+
|
| 375 |
+
if self.config.global_attention_every_n_layers > 0:
|
| 376 |
+
self.layer_type = (
|
| 377 |
+
AttentionLayerType.GLOBAL
|
| 378 |
+
if (self.layer_idx + 1) % self.config.global_attention_every_n_layers == 0
|
| 379 |
+
else AttentionLayerType.WITHIN_SEQ
|
| 380 |
+
)
|
| 381 |
+
else:
|
| 382 |
+
self.layer_type = AttentionLayerType.WITHIN_SEQ
|
| 383 |
+
|
| 384 |
+
self.rope_theta = (
|
| 385 |
+
config.rope_theta_within_seq
|
| 386 |
+
if self.layer_type == AttentionLayerType.WITHIN_SEQ
|
| 387 |
+
else config.rope_theta_global
|
| 388 |
+
)
|
| 389 |
+
self.max_position_embeddings = (
|
| 390 |
+
config.max_num_positions_within_seq
|
| 391 |
+
if self.layer_type == AttentionLayerType.WITHIN_SEQ
|
| 392 |
+
else config.max_num_positions_global
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
self.rotary_emb = RotaryPositionalEmbedding(
|
| 396 |
+
self.head_dim,
|
| 397 |
+
max_position_embeddings=self.max_position_embeddings,
|
| 398 |
+
base=self.rope_theta,
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
self.attn_backend = resolve_attention_backend(config.attn_backend)
|
| 402 |
+
|
| 403 |
+
def prepare_qkv(
|
| 404 |
+
self,
|
| 405 |
+
hidden_states: torch.Tensor,
|
| 406 |
+
position_ids: torch.LongTensor,
|
| 407 |
+
past_key_value: DynamicCache | None = None,
|
| 408 |
+
use_cache: bool = False,
|
| 409 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 410 |
+
bsz, q_len, _ = hidden_states.size()
|
| 411 |
+
query_states: torch.Tensor = self.q_proj(hidden_states)
|
| 412 |
+
key_states: torch.Tensor = self.k_proj(hidden_states)
|
| 413 |
+
val_states: torch.Tensor = self.v_proj(hidden_states)
|
| 414 |
+
|
| 415 |
+
query_states = query_states.view(bsz, q_len, self.num_heads, self.head_dim)
|
| 416 |
+
key_states = key_states.view(bsz, q_len, self.num_kv_heads, self.head_dim)
|
| 417 |
+
val_states = val_states.view(bsz, q_len, self.num_kv_heads, self.head_dim)
|
| 418 |
+
|
| 419 |
+
if self.clip_qkv is not None:
|
| 420 |
+
query_states = query_states.clamp(-self.clip_qkv, self.clip_qkv)
|
| 421 |
+
key_states = key_states.clamp(-self.clip_qkv, self.clip_qkv)
|
| 422 |
+
val_states = val_states.clamp(-self.clip_qkv, self.clip_qkv)
|
| 423 |
+
|
| 424 |
+
query_states, key_states = self.rotary_emb(query_states, key_states, position_ids)
|
| 425 |
+
|
| 426 |
+
if use_cache and past_key_value is not None:
|
| 427 |
+
key_states, val_states = past_key_value.update(key_states, val_states, self.layer_idx)
|
| 428 |
+
|
| 429 |
+
input_dtype = query_states.dtype
|
| 430 |
+
if torch.is_autocast_enabled():
|
| 431 |
+
target_dtype = torch.get_autocast_dtype("cuda")
|
| 432 |
+
else:
|
| 433 |
+
target_dtype = self.q_proj.weight.dtype
|
| 434 |
+
if input_dtype != target_dtype:
|
| 435 |
+
_get_logger().warning_once(
|
| 436 |
+
f"The input hidden states seems to be silently casted in {input_dtype}. "
|
| 437 |
+
f"This might be because you have upcasted embedding or layer norm layers "
|
| 438 |
+
f"in {input_dtype}. We will cast back the input in {target_dtype}."
|
| 439 |
+
)
|
| 440 |
+
query_states = query_states.to(target_dtype)
|
| 441 |
+
key_states = key_states.to(target_dtype)
|
| 442 |
+
val_states = val_states.to(target_dtype)
|
| 443 |
+
|
| 444 |
+
return query_states, key_states, val_states
|
| 445 |
+
|
| 446 |
+
def forward(
|
| 447 |
+
self,
|
| 448 |
+
hidden_states: torch.Tensor,
|
| 449 |
+
within_seq_position_ids: torch.LongTensor,
|
| 450 |
+
global_position_ids: torch.LongTensor,
|
| 451 |
+
sequence_ids: torch.LongTensor,
|
| 452 |
+
attention_args: AttentionArgs | None = None,
|
| 453 |
+
past_key_value: DynamicCache | None = None,
|
| 454 |
+
output_attentions: bool = False,
|
| 455 |
+
output_s_max: bool = False,
|
| 456 |
+
use_cache: bool = False,
|
| 457 |
+
effective_backend: AttentionBackend | None = None,
|
| 458 |
+
) -> tuple[torch.Tensor, torch.Tensor | None, DynamicCache | None, list[torch.Tensor] | None]:
|
| 459 |
+
is_cache_prefilled = (
|
| 460 |
+
use_cache
|
| 461 |
+
and past_key_value is not None
|
| 462 |
+
and past_key_value.get_seq_length(self.layer_idx) > 0
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
query_states, key_states, val_states = self.prepare_qkv(
|
| 466 |
+
hidden_states=hidden_states,
|
| 467 |
+
position_ids=within_seq_position_ids
|
| 468 |
+
if self.layer_type == AttentionLayerType.WITHIN_SEQ
|
| 469 |
+
else global_position_ids,
|
| 470 |
+
past_key_value=past_key_value,
|
| 471 |
+
use_cache=use_cache,
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
attn_output, attn_weights, s_max = self._attn(
|
| 475 |
+
query_states=query_states,
|
| 476 |
+
key_states=key_states,
|
| 477 |
+
val_states=val_states,
|
| 478 |
+
sequence_ids=sequence_ids,
|
| 479 |
+
attention_args=attention_args,
|
| 480 |
+
output_attentions=output_attentions,
|
| 481 |
+
output_s_max=output_s_max,
|
| 482 |
+
is_cache_prefilled=is_cache_prefilled,
|
| 483 |
+
effective_backend=effective_backend,
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
attn_output = self.o_proj(attn_output)
|
| 487 |
+
return attn_output, attn_weights, past_key_value, s_max
|
| 488 |
+
|
| 489 |
+
def _attn(
|
| 490 |
+
self,
|
| 491 |
+
query_states: torch.Tensor,
|
| 492 |
+
key_states: torch.Tensor,
|
| 493 |
+
val_states: torch.Tensor,
|
| 494 |
+
sequence_ids: torch.Tensor,
|
| 495 |
+
attention_args: AttentionArgs | None = None,
|
| 496 |
+
output_attentions: bool = False,
|
| 497 |
+
output_s_max: bool = False,
|
| 498 |
+
is_cache_prefilled: bool = False,
|
| 499 |
+
effective_backend: AttentionBackend | None = None,
|
| 500 |
+
) -> tuple[torch.Tensor, torch.Tensor | None, list[torch.Tensor] | None]:
|
| 501 |
+
# A filled cache changes the implementation shape, not the layer's
|
| 502 |
+
# biological attention contract. Global layers must retain the cached
|
| 503 |
+
# context, while within-sequence layers consume only the newly appended
|
| 504 |
+
# sequence. This matches the pinned E1 inference implementation.
|
| 505 |
+
effective_layer_type = self.layer_type
|
| 506 |
+
|
| 507 |
+
if effective_backend is None:
|
| 508 |
+
effective_backend = resolve_attention_backend_for_call(
|
| 509 |
+
self.attn_backend,
|
| 510 |
+
output_attentions=output_attentions,
|
| 511 |
+
)
|
| 512 |
+
if output_attentions:
|
| 513 |
+
return self._manual_attn(
|
| 514 |
+
query_states,
|
| 515 |
+
key_states,
|
| 516 |
+
val_states,
|
| 517 |
+
sequence_ids=sequence_ids,
|
| 518 |
+
attention_args=attention_args,
|
| 519 |
+
effective_layer_type=effective_layer_type,
|
| 520 |
+
output_s_max=output_s_max,
|
| 521 |
+
is_cache_prefilled=is_cache_prefilled,
|
| 522 |
+
)
|
| 523 |
+
|
| 524 |
+
if effective_backend == AttentionBackend.EAGER:
|
| 525 |
+
attn_output, _, s_max = self._manual_attn(
|
| 526 |
+
query_states,
|
| 527 |
+
key_states,
|
| 528 |
+
val_states,
|
| 529 |
+
sequence_ids=sequence_ids,
|
| 530 |
+
attention_args=attention_args,
|
| 531 |
+
effective_layer_type=effective_layer_type,
|
| 532 |
+
output_s_max=output_s_max,
|
| 533 |
+
is_cache_prefilled=is_cache_prefilled,
|
| 534 |
+
)
|
| 535 |
+
return attn_output, None, s_max
|
| 536 |
+
if effective_backend.is_flash:
|
| 537 |
+
if effective_layer_type == AttentionLayerType.WITHIN_SEQ:
|
| 538 |
+
attn_output, attn_weights = self._kernels_flash_attn(
|
| 539 |
+
query_states,
|
| 540 |
+
key_states,
|
| 541 |
+
val_states,
|
| 542 |
+
sequence_ids=sequence_ids,
|
| 543 |
+
is_cache_prefilled=is_cache_prefilled,
|
| 544 |
+
)
|
| 545 |
+
else:
|
| 546 |
+
raise ValueError(
|
| 547 |
+
"E1 global attention does not support a kernels Flash backend; "
|
| 548 |
+
"use eager, sdpa, or flex_attention."
|
| 549 |
+
)
|
| 550 |
+
elif effective_backend == AttentionBackend.FLEX:
|
| 551 |
+
attn_output, attn_weights = self._flex_attn(
|
| 552 |
+
query_states,
|
| 553 |
+
key_states,
|
| 554 |
+
val_states,
|
| 555 |
+
sequence_ids=sequence_ids,
|
| 556 |
+
attention_args=attention_args,
|
| 557 |
+
effective_layer_type=effective_layer_type,
|
| 558 |
+
is_cache_prefilled=is_cache_prefilled,
|
| 559 |
+
)
|
| 560 |
+
elif effective_backend == AttentionBackend.SDPA:
|
| 561 |
+
attn_output, attn_weights = self._sdpa_attn(
|
| 562 |
+
query_states,
|
| 563 |
+
key_states,
|
| 564 |
+
val_states,
|
| 565 |
+
sequence_ids=sequence_ids,
|
| 566 |
+
attention_args=attention_args,
|
| 567 |
+
effective_layer_type=effective_layer_type,
|
| 568 |
+
is_cache_prefilled=is_cache_prefilled,
|
| 569 |
+
)
|
| 570 |
+
else:
|
| 571 |
+
raise AssertionError(f"Unsupported resolved backend: {effective_backend}")
|
| 572 |
+
|
| 573 |
+
s_max_key_states = key_states
|
| 574 |
+
if (
|
| 575 |
+
is_cache_prefilled
|
| 576 |
+
and effective_layer_type == AttentionLayerType.WITHIN_SEQ
|
| 577 |
+
and query_states.shape[1] < key_states.shape[1]
|
| 578 |
+
):
|
| 579 |
+
s_max_key_states = key_states[:, -query_states.shape[1] :]
|
| 580 |
+
s_max = self._compute_s_max(query_states, s_max_key_states) if output_s_max else None
|
| 581 |
+
return attn_output, attn_weights, s_max
|
| 582 |
+
|
| 583 |
+
@torch.no_grad()
|
| 584 |
+
def _compute_s_max(
|
| 585 |
+
self,
|
| 586 |
+
query_states: torch.Tensor, # Q has shape (b, l, h, d).
|
| 587 |
+
key_states: torch.Tensor, # K has shape (b, l, h_kv, d).
|
| 588 |
+
) -> list[torch.Tensor]:
|
| 589 |
+
query_heads = query_states.transpose(1, 2).contiguous()
|
| 590 |
+
key_heads = key_states.transpose(1, 2).contiguous()
|
| 591 |
+
key_heads = repeat_kv(key_heads, self.num_key_value_groups)
|
| 592 |
+
scale = 1.0 / (self.head_dim**0.5)
|
| 593 |
+
q_norm = torch.linalg.vector_norm(query_heads, dim=-1)
|
| 594 |
+
k_norm = torch.linalg.vector_norm(key_heads, dim=-1)
|
| 595 |
+
s_max_bound = (q_norm.max(dim=-1).values * k_norm.max(dim=-1).values).max(
|
| 596 |
+
dim=0
|
| 597 |
+
).values * scale
|
| 598 |
+
return [s_max_bound[h] for h in range(self.num_heads)]
|
| 599 |
+
|
| 600 |
+
def _kernels_flash_attn(
|
| 601 |
+
self,
|
| 602 |
+
query_states: torch.Tensor,
|
| 603 |
+
key_states: torch.Tensor,
|
| 604 |
+
val_states: torch.Tensor,
|
| 605 |
+
sequence_ids: torch.Tensor,
|
| 606 |
+
is_cache_prefilled: bool = False,
|
| 607 |
+
) -> tuple[torch.Tensor, None]:
|
| 608 |
+
bsz, q_len = query_states.shape[0], query_states.shape[1]
|
| 609 |
+
_, kv_len = key_states.shape[0], key_states.shape[1]
|
| 610 |
+
|
| 611 |
+
if self.layer_type == AttentionLayerType.GLOBAL:
|
| 612 |
+
q_sequence_ids = sequence_ids
|
| 613 |
+
if q_len < kv_len:
|
| 614 |
+
first_token_id = sequence_ids[:, 0].unsqueeze(1)
|
| 615 |
+
k_sequence_ids = torch.cat(
|
| 616 |
+
[first_token_id.expand(bsz, kv_len - q_len), sequence_ids], dim=-1
|
| 617 |
+
)
|
| 618 |
+
else:
|
| 619 |
+
k_sequence_ids = sequence_ids
|
| 620 |
+
else:
|
| 621 |
+
if q_len < kv_len:
|
| 622 |
+
key_states = key_states[:, -q_len:]
|
| 623 |
+
val_states = val_states[:, -q_len:]
|
| 624 |
+
q_sequence_ids = k_sequence_ids = sequence_ids
|
| 625 |
+
|
| 626 |
+
attn_output = kernels_flash_attention_func(
|
| 627 |
+
query_states,
|
| 628 |
+
key_states,
|
| 629 |
+
val_states,
|
| 630 |
+
q_sequence_ids=q_sequence_ids,
|
| 631 |
+
k_sequence_ids=k_sequence_ids,
|
| 632 |
+
causal=False,
|
| 633 |
+
implementation=self.attn_backend.value,
|
| 634 |
+
)
|
| 635 |
+
attn_output = attn_output.reshape(bsz, q_len, self.hidden_size).contiguous()
|
| 636 |
+
return attn_output, None
|
| 637 |
+
|
| 638 |
+
def _flex_attn(
|
| 639 |
+
self,
|
| 640 |
+
query_states: torch.Tensor,
|
| 641 |
+
key_states: torch.Tensor,
|
| 642 |
+
val_states: torch.Tensor,
|
| 643 |
+
sequence_ids: torch.Tensor,
|
| 644 |
+
attention_args: AttentionArgs | None = None,
|
| 645 |
+
effective_layer_type: AttentionLayerType = AttentionLayerType.WITHIN_SEQ,
|
| 646 |
+
is_cache_prefilled: bool = False,
|
| 647 |
+
) -> tuple[torch.Tensor, None]:
|
| 648 |
+
bsz, q_len = query_states.shape[0], query_states.shape[1]
|
| 649 |
+
kv_len = key_states.shape[1]
|
| 650 |
+
if is_cache_prefilled and q_len < kv_len:
|
| 651 |
+
if effective_layer_type == AttentionLayerType.WITHIN_SEQ:
|
| 652 |
+
key_states = key_states[:, -q_len:]
|
| 653 |
+
val_states = val_states[:, -q_len:]
|
| 654 |
+
block_mask = create_within_seq_block_mask(sequence_ids)
|
| 655 |
+
outputs = flex_attention_func(
|
| 656 |
+
query_states,
|
| 657 |
+
key_states,
|
| 658 |
+
val_states,
|
| 659 |
+
block_mask=block_mask,
|
| 660 |
+
mask_semantics=effective_layer_type.value,
|
| 661 |
+
)
|
| 662 |
+
else:
|
| 663 |
+
q_sequence_ids, k_sequence_ids = self._cached_global_sequence_ids(
|
| 664 |
+
sequence_ids,
|
| 665 |
+
kv_len,
|
| 666 |
+
)
|
| 667 |
+
outputs = varlen_flex_attention_func(
|
| 668 |
+
query_states,
|
| 669 |
+
key_states,
|
| 670 |
+
val_states,
|
| 671 |
+
q_sequence_ids=q_sequence_ids,
|
| 672 |
+
k_sequence_ids=k_sequence_ids,
|
| 673 |
+
)
|
| 674 |
+
outputs = outputs.reshape(bsz, q_len, self.hidden_size).contiguous()
|
| 675 |
+
return outputs, None
|
| 676 |
+
|
| 677 |
+
if effective_layer_type == AttentionLayerType.WITHIN_SEQ:
|
| 678 |
+
block_mask = (
|
| 679 |
+
attention_args["within_seq_block_mask"] if attention_args is not None else None
|
| 680 |
+
)
|
| 681 |
+
else:
|
| 682 |
+
block_mask = (
|
| 683 |
+
attention_args["block_causal_block_mask"] if attention_args is not None else None
|
| 684 |
+
)
|
| 685 |
+
outputs = flex_attention_func(
|
| 686 |
+
query_states,
|
| 687 |
+
key_states,
|
| 688 |
+
val_states,
|
| 689 |
+
block_mask=block_mask,
|
| 690 |
+
mask_semantics=effective_layer_type.value,
|
| 691 |
+
)
|
| 692 |
+
outputs = outputs.reshape(bsz, q_len, self.hidden_size).contiguous()
|
| 693 |
+
return outputs, None
|
| 694 |
+
|
| 695 |
+
@staticmethod
|
| 696 |
+
def _cached_global_sequence_ids(
|
| 697 |
+
query_sequence_ids: torch.Tensor,
|
| 698 |
+
kv_len: int,
|
| 699 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 700 |
+
"""Assign cached context to the incoming query sequence.
|
| 701 |
+
|
| 702 |
+
E1 retrieval cache hits contain one incoming sequence. The pinned
|
| 703 |
+
implementation relabels the cached prefix with that sequence ID so its
|
| 704 |
+
valid query tokens attend the complete cached context, while padding is
|
| 705 |
+
excluded by the equality mask or packed Flex path.
|
| 706 |
+
"""
|
| 707 |
+
|
| 708 |
+
q_len = query_sequence_ids.shape[1]
|
| 709 |
+
cached_len = kv_len - q_len
|
| 710 |
+
if cached_len < 0:
|
| 711 |
+
raise ValueError(f"E1 cached KV length {kv_len} is shorter than query length {q_len}.")
|
| 712 |
+
first_sequence_id = query_sequence_ids[:, :1]
|
| 713 |
+
if bool(first_sequence_id.eq(-1).any()):
|
| 714 |
+
raise ValueError("E1 cached queries must start with a non-padding sequence token.")
|
| 715 |
+
cached_sequence_ids = first_sequence_id.expand(-1, cached_len)
|
| 716 |
+
key_sequence_ids = torch.cat((cached_sequence_ids, query_sequence_ids), dim=-1)
|
| 717 |
+
return query_sequence_ids, key_sequence_ids
|
| 718 |
+
|
| 719 |
+
def _cached_attention_mask_4d(
|
| 720 |
+
self,
|
| 721 |
+
sequence_ids: torch.Tensor,
|
| 722 |
+
kv_len: int,
|
| 723 |
+
effective_layer_type: AttentionLayerType,
|
| 724 |
+
) -> torch.Tensor:
|
| 725 |
+
if effective_layer_type == AttentionLayerType.WITHIN_SEQ:
|
| 726 |
+
return build_within_seq_mask_4d(sequence_ids)
|
| 727 |
+
query_sequence_ids, key_sequence_ids = self._cached_global_sequence_ids(
|
| 728 |
+
sequence_ids,
|
| 729 |
+
kv_len,
|
| 730 |
+
)
|
| 731 |
+
query_valid = query_sequence_ids.ne(-1)
|
| 732 |
+
key_valid = key_sequence_ids.ne(-1)
|
| 733 |
+
same_sequence = query_sequence_ids.unsqueeze(-1).eq(key_sequence_ids.unsqueeze(-2))
|
| 734 |
+
return (same_sequence & query_valid.unsqueeze(-1) & key_valid.unsqueeze(-2)).unsqueeze(1)
|
| 735 |
+
|
| 736 |
+
def _sdpa_attn(
|
| 737 |
+
self,
|
| 738 |
+
query_states: torch.Tensor, # Q has shape (b, l, h, d).
|
| 739 |
+
key_states: torch.Tensor, # K has shape (b, l, h_kv, d).
|
| 740 |
+
val_states: torch.Tensor, # V has shape (b, l, h_kv, d).
|
| 741 |
+
sequence_ids: torch.Tensor,
|
| 742 |
+
attention_args: AttentionArgs | None = None,
|
| 743 |
+
effective_layer_type: AttentionLayerType = AttentionLayerType.WITHIN_SEQ,
|
| 744 |
+
is_cache_prefilled: bool = False,
|
| 745 |
+
) -> tuple[torch.Tensor, None]:
|
| 746 |
+
bsz, q_len = query_states.shape[:2]
|
| 747 |
+
kv_len = key_states.shape[1]
|
| 748 |
+
|
| 749 |
+
if is_cache_prefilled and q_len < kv_len:
|
| 750 |
+
if effective_layer_type == AttentionLayerType.WITHIN_SEQ:
|
| 751 |
+
key_states = key_states[:, -q_len:]
|
| 752 |
+
val_states = val_states[:, -q_len:]
|
| 753 |
+
attention_mask_4d = self._cached_attention_mask_4d(
|
| 754 |
+
sequence_ids,
|
| 755 |
+
kv_len,
|
| 756 |
+
effective_layer_type,
|
| 757 |
+
)
|
| 758 |
+
elif attention_args is not None:
|
| 759 |
+
if effective_layer_type == AttentionLayerType.WITHIN_SEQ:
|
| 760 |
+
attention_mask_4d = attention_args["within_seq_mask_4d"]
|
| 761 |
+
else:
|
| 762 |
+
attention_mask_4d = attention_args["block_causal_mask_4d"]
|
| 763 |
+
else:
|
| 764 |
+
attention_mask_4d = None
|
| 765 |
+
|
| 766 |
+
query_heads = query_states.transpose(1, 2).contiguous()
|
| 767 |
+
key_heads = key_states.transpose(1, 2).contiguous()
|
| 768 |
+
value_heads = val_states.transpose(1, 2).contiguous()
|
| 769 |
+
key_heads = repeat_kv(key_heads, self.num_key_value_groups)
|
| 770 |
+
value_heads = repeat_kv(value_heads, self.num_key_value_groups)
|
| 771 |
+
context_heads = F.scaled_dot_product_attention(
|
| 772 |
+
query_heads, key_heads, value_heads, attn_mask=attention_mask_4d
|
| 773 |
+
)
|
| 774 |
+
attn_output = (
|
| 775 |
+
context_heads.transpose(1, 2).reshape(bsz, q_len, self.hidden_size).contiguous()
|
| 776 |
+
)
|
| 777 |
+
return attn_output, None
|
| 778 |
+
|
| 779 |
+
def _manual_attn(
|
| 780 |
+
self,
|
| 781 |
+
query_states: torch.Tensor, # Q has shape (b, l, h, d).
|
| 782 |
+
key_states: torch.Tensor, # K has shape (b, l, h_kv, d).
|
| 783 |
+
val_states: torch.Tensor, # V has shape (b, l, h_kv, d).
|
| 784 |
+
sequence_ids: torch.Tensor,
|
| 785 |
+
attention_args: AttentionArgs | None = None,
|
| 786 |
+
effective_layer_type: AttentionLayerType = AttentionLayerType.WITHIN_SEQ,
|
| 787 |
+
output_s_max: bool = False,
|
| 788 |
+
is_cache_prefilled: bool = False,
|
| 789 |
+
) -> tuple[torch.Tensor, torch.Tensor, list[torch.Tensor] | None]:
|
| 790 |
+
bsz, q_len = query_states.shape[:2]
|
| 791 |
+
kv_len = key_states.shape[1]
|
| 792 |
+
|
| 793 |
+
if is_cache_prefilled and q_len < kv_len:
|
| 794 |
+
if effective_layer_type == AttentionLayerType.WITHIN_SEQ:
|
| 795 |
+
key_states = key_states[:, -q_len:]
|
| 796 |
+
val_states = val_states[:, -q_len:]
|
| 797 |
+
attention_mask_4d = self._cached_attention_mask_4d(
|
| 798 |
+
sequence_ids,
|
| 799 |
+
kv_len,
|
| 800 |
+
effective_layer_type,
|
| 801 |
+
)
|
| 802 |
+
elif attention_args is not None:
|
| 803 |
+
if effective_layer_type == AttentionLayerType.WITHIN_SEQ:
|
| 804 |
+
attention_mask_4d = attention_args["within_seq_mask_4d"]
|
| 805 |
+
else:
|
| 806 |
+
attention_mask_4d = attention_args["block_causal_mask_4d"]
|
| 807 |
+
else:
|
| 808 |
+
attention_mask_4d = None
|
| 809 |
+
|
| 810 |
+
query_heads = query_states.transpose(1, 2).contiguous()
|
| 811 |
+
key_heads = key_states.transpose(1, 2).contiguous()
|
| 812 |
+
value_heads = val_states.transpose(1, 2).contiguous()
|
| 813 |
+
key_heads = repeat_kv(key_heads, self.num_key_value_groups)
|
| 814 |
+
value_heads = repeat_kv(value_heads, self.num_key_value_groups)
|
| 815 |
+
scale = 1.0 / (self.head_dim**0.5)
|
| 816 |
+
attn_weights = torch.matmul(query_heads, key_heads.transpose(-2, -1)) * scale
|
| 817 |
+
if attention_mask_4d is not None:
|
| 818 |
+
attention_mask_4d = attention_mask_4d.to(dtype=torch.bool)
|
| 819 |
+
attn_weights = attn_weights.masked_fill(
|
| 820 |
+
attention_mask_4d.logical_not(),
|
| 821 |
+
torch.finfo(attn_weights.dtype).min,
|
| 822 |
+
)
|
| 823 |
+
attn_weights = F.softmax(attn_weights, dim=-1)
|
| 824 |
+
if attention_mask_4d is not None:
|
| 825 |
+
attn_weights = attn_weights.masked_fill(attention_mask_4d.logical_not(), 0.0)
|
| 826 |
+
context_heads = torch.matmul(attn_weights, value_heads)
|
| 827 |
+
attn_output = (
|
| 828 |
+
context_heads.transpose(1, 2).reshape(bsz, q_len, self.hidden_size).contiguous()
|
| 829 |
+
)
|
| 830 |
+
s_max = self._compute_s_max(query_states, key_states) if output_s_max else None
|
| 831 |
+
return attn_output, attn_weights, s_max
|
| 832 |
+
|
| 833 |
+
|
| 834 |
+
class MLP(nn.Module):
|
| 835 |
+
def __init__(self, config: E1Config):
|
| 836 |
+
super().__init__()
|
| 837 |
+
self.ffn_dim = config.intermediate_size
|
| 838 |
+
self.hidden_dim = config.hidden_size
|
| 839 |
+
self.w1 = nn.Linear(self.hidden_dim, self.ffn_dim, bias=False)
|
| 840 |
+
self.w2 = nn.Linear(self.ffn_dim, self.hidden_dim, bias=False)
|
| 841 |
+
self.act_fn = ACT2FN[config.hidden_act]
|
| 842 |
+
|
| 843 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 844 |
+
return self.w2(self.act_fn(self.w1(hidden_states)))
|
| 845 |
+
|
| 846 |
+
|
| 847 |
+
class GLUMLP(nn.Module):
|
| 848 |
+
def __init__(self, config: E1Config):
|
| 849 |
+
super().__init__()
|
| 850 |
+
self.ffn_dim = config.intermediate_size
|
| 851 |
+
self.hidden_dim = config.hidden_size
|
| 852 |
+
self.w1 = nn.Linear(self.hidden_dim, self.ffn_dim, bias=False)
|
| 853 |
+
self.w2 = nn.Linear(self.ffn_dim, self.hidden_dim, bias=False)
|
| 854 |
+
self.w3 = nn.Linear(self.hidden_dim, self.ffn_dim, bias=False)
|
| 855 |
+
self.act_fn = ACT2FN[config.hidden_act]
|
| 856 |
+
|
| 857 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 858 |
+
hidden_states = self.act_fn(self.w1(hidden_states)) * self.w3(hidden_states)
|
| 859 |
+
hidden_states = self.w2(hidden_states)
|
| 860 |
+
return hidden_states
|
| 861 |
+
|
| 862 |
+
|
| 863 |
+
class FFN(nn.Module):
|
| 864 |
+
def __init__(self, config: E1Config):
|
| 865 |
+
super().__init__()
|
| 866 |
+
mlp_cls = GLUMLP if config.gated_mlp else MLP
|
| 867 |
+
self.mlp = mlp_cls(config)
|
| 868 |
+
|
| 869 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 870 |
+
return self.mlp(hidden_states)
|
| 871 |
+
|
| 872 |
+
|
| 873 |
+
@dataclass
|
| 874 |
+
class E1ModelOutputWithPast(ModelOutput):
|
| 875 |
+
"""E1 encoder outputs.
|
| 876 |
+
|
| 877 |
+
``last_hidden_state`` is H with shape (b, l, d). Optional hidden states use
|
| 878 |
+
the same shape per layer, while attention tensors have shape (b, h, l, l).
|
| 879 |
+
``past_key_values`` stores the reusable K and V tensors for cached decoding.
|
| 880 |
+
"""
|
| 881 |
+
|
| 882 |
+
last_hidden_state: torch.FloatTensor | None = None
|
| 883 |
+
past_key_values: DynamicCache | None = None
|
| 884 |
+
hidden_states: tuple[torch.FloatTensor, ...] | None = None
|
| 885 |
+
attentions: tuple[torch.FloatTensor, ...] | None = None
|
| 886 |
+
s_max: tuple[list[torch.Tensor], ...] | None = None
|
| 887 |
+
|
| 888 |
+
|
| 889 |
+
@dataclass
|
| 890 |
+
class E1MaskedLMOutputWithPast(ModelOutput):
|
| 891 |
+
"""Masked-LM output with the standard HF fields first, then E1 diagnostics."""
|
| 892 |
+
|
| 893 |
+
loss: torch.FloatTensor | None = None
|
| 894 |
+
logits: torch.FloatTensor | None = None
|
| 895 |
+
hidden_states: tuple[torch.FloatTensor, ...] | None = None
|
| 896 |
+
attentions: tuple[torch.FloatTensor, ...] | None = None
|
| 897 |
+
mlm_loss: torch.FloatTensor | None = None
|
| 898 |
+
last_hidden_state: torch.FloatTensor | None = None
|
| 899 |
+
past_key_values: DynamicCache | None = None
|
| 900 |
+
s_max: tuple[list[torch.Tensor], ...] | None = None
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
@dataclass
|
| 904 |
+
class E1ClassificationOutputWithPast(ModelOutput):
|
| 905 |
+
"""Sequence-classifier output matching HF ``SequenceClassifierOutputWithPast``."""
|
| 906 |
+
|
| 907 |
+
loss: torch.FloatTensor | None = None
|
| 908 |
+
logits: torch.FloatTensor | None = None
|
| 909 |
+
past_key_values: DynamicCache | None = None
|
| 910 |
+
hidden_states: tuple[torch.FloatTensor, ...] | None = None
|
| 911 |
+
attentions: tuple[torch.FloatTensor, ...] | None = None
|
| 912 |
+
last_hidden_state: torch.FloatTensor | None = None
|
| 913 |
+
s_max: tuple[list[torch.Tensor], ...] | None = None
|
| 914 |
+
|
| 915 |
+
|
| 916 |
+
@dataclass
|
| 917 |
+
class E1TokenClassificationOutputWithPast(ModelOutput):
|
| 918 |
+
"""Token-classifier output with the standard HF fields before E1 extensions."""
|
| 919 |
+
|
| 920 |
+
loss: torch.FloatTensor | None = None
|
| 921 |
+
logits: torch.FloatTensor | None = None
|
| 922 |
+
hidden_states: tuple[torch.FloatTensor, ...] | None = None
|
| 923 |
+
attentions: tuple[torch.FloatTensor, ...] | None = None
|
| 924 |
+
last_hidden_state: torch.FloatTensor | None = None
|
| 925 |
+
past_key_values: DynamicCache | None = None
|
| 926 |
+
s_max: tuple[list[torch.Tensor], ...] | None = None
|
| 927 |
+
|
| 928 |
+
|
| 929 |
+
class RMSNorm(nn.Module):
|
| 930 |
+
def __init__(self, hidden_size: int, eps: float = 1e-6):
|
| 931 |
+
super().__init__()
|
| 932 |
+
self.weight = nn.Parameter(torch.ones(hidden_size))
|
| 933 |
+
self.variance_epsilon = eps
|
| 934 |
+
self.hidden_size = hidden_size
|
| 935 |
+
|
| 936 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 937 |
+
input_dtype = hidden_states.dtype
|
| 938 |
+
return torch.nn.functional.rms_norm(
|
| 939 |
+
hidden_states, (self.hidden_size,), self.weight, self.variance_epsilon
|
| 940 |
+
).to(input_dtype)
|
| 941 |
+
|
| 942 |
+
|
| 943 |
+
class NormAttentionNorm(nn.Module):
|
| 944 |
+
def __init__(self, config: E1Config, layer_idx: int):
|
| 945 |
+
super().__init__()
|
| 946 |
+
self.self_attn = Attention(config, layer_idx)
|
| 947 |
+
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 948 |
+
self.post_attention_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 949 |
+
|
| 950 |
+
def forward(
|
| 951 |
+
self,
|
| 952 |
+
hidden_states: torch.Tensor,
|
| 953 |
+
within_seq_position_ids: torch.LongTensor,
|
| 954 |
+
global_position_ids: torch.LongTensor,
|
| 955 |
+
sequence_ids: torch.LongTensor,
|
| 956 |
+
attention_args: AttentionArgs | None = None,
|
| 957 |
+
past_key_value: DynamicCache | None = None,
|
| 958 |
+
output_attentions: bool = False,
|
| 959 |
+
output_s_max: bool = False,
|
| 960 |
+
use_cache: bool = False,
|
| 961 |
+
effective_backend: AttentionBackend | None = None,
|
| 962 |
+
) -> tuple[
|
| 963 |
+
torch.Tensor,
|
| 964 |
+
torch.Tensor,
|
| 965 |
+
torch.Tensor | None,
|
| 966 |
+
DynamicCache | None,
|
| 967 |
+
list[torch.Tensor] | None,
|
| 968 |
+
]:
|
| 969 |
+
residual = hidden_states
|
| 970 |
+
hidden_states = self.input_layernorm(hidden_states)
|
| 971 |
+
hidden_states, self_attn_weights, present_key_value, s_max = self.self_attn(
|
| 972 |
+
hidden_states=hidden_states,
|
| 973 |
+
within_seq_position_ids=within_seq_position_ids,
|
| 974 |
+
global_position_ids=global_position_ids,
|
| 975 |
+
sequence_ids=sequence_ids,
|
| 976 |
+
attention_args=attention_args,
|
| 977 |
+
past_key_value=past_key_value,
|
| 978 |
+
output_attentions=output_attentions,
|
| 979 |
+
output_s_max=output_s_max,
|
| 980 |
+
use_cache=use_cache,
|
| 981 |
+
effective_backend=effective_backend,
|
| 982 |
+
)
|
| 983 |
+
hidden_states = residual + hidden_states
|
| 984 |
+
|
| 985 |
+
residual = hidden_states
|
| 986 |
+
hidden_states = self.post_attention_layernorm(hidden_states)
|
| 987 |
+
return hidden_states, residual, self_attn_weights, present_key_value, s_max
|
| 988 |
+
|
| 989 |
+
|
| 990 |
+
class DecoderLayer(nn.Module):
|
| 991 |
+
def __init__(self, config: E1Config, layer_idx: int):
|
| 992 |
+
super().__init__()
|
| 993 |
+
self.initializer_range = config.initializer_range
|
| 994 |
+
self.hidden_size = config.hidden_size
|
| 995 |
+
self.norm_attn_norm = NormAttentionNorm(config, layer_idx)
|
| 996 |
+
self.ffn = FFN(config)
|
| 997 |
+
|
| 998 |
+
def forward(
|
| 999 |
+
self,
|
| 1000 |
+
hidden_states: torch.Tensor,
|
| 1001 |
+
within_seq_position_ids: torch.LongTensor,
|
| 1002 |
+
global_position_ids: torch.LongTensor,
|
| 1003 |
+
sequence_ids: torch.LongTensor,
|
| 1004 |
+
attention_args: AttentionArgs | None = None,
|
| 1005 |
+
past_key_value: DynamicCache | None = None,
|
| 1006 |
+
output_attentions: bool = False,
|
| 1007 |
+
output_s_max: bool = False,
|
| 1008 |
+
use_cache: bool = False,
|
| 1009 |
+
effective_backend: AttentionBackend | None = None,
|
| 1010 |
+
) -> tuple[torch.Tensor, torch.Tensor | None, DynamicCache | None, list[torch.Tensor] | None]:
|
| 1011 |
+
hidden_states, residual, self_attn_weights, present_key_value, s_max = self.norm_attn_norm(
|
| 1012 |
+
hidden_states=hidden_states,
|
| 1013 |
+
within_seq_position_ids=within_seq_position_ids,
|
| 1014 |
+
global_position_ids=global_position_ids,
|
| 1015 |
+
sequence_ids=sequence_ids,
|
| 1016 |
+
attention_args=attention_args,
|
| 1017 |
+
past_key_value=past_key_value,
|
| 1018 |
+
output_attentions=output_attentions,
|
| 1019 |
+
output_s_max=output_s_max,
|
| 1020 |
+
use_cache=use_cache,
|
| 1021 |
+
effective_backend=effective_backend,
|
| 1022 |
+
)
|
| 1023 |
+
|
| 1024 |
+
# Fully Connected
|
| 1025 |
+
hidden_states = self.ffn(hidden_states)
|
| 1026 |
+
hidden_states = residual + hidden_states
|
| 1027 |
+
|
| 1028 |
+
return hidden_states, self_attn_weights, present_key_value, s_max
|
| 1029 |
+
|
| 1030 |
+
|
| 1031 |
+
class E1PreTrainedModel(FastPLMsAttentionMixin, PreTrainedModel):
|
| 1032 |
+
config_class = E1Config
|
| 1033 |
+
embedding_unsupported_pooling = ("cls", "parti")
|
| 1034 |
+
config: E1Config
|
| 1035 |
+
base_model_prefix = "model"
|
| 1036 |
+
supports_gradient_checkpointing = True
|
| 1037 |
+
_no_split_modules: ClassVar[list[str]] = ["DecoderLayer"]
|
| 1038 |
+
_transformer_layer_cls: ClassVar[list[type[nn.Module]]] = [DecoderLayer]
|
| 1039 |
+
_skip_keys_device_placement = "past_key_values"
|
| 1040 |
+
all_tied_weights_keys: ClassVar[dict[str, str]] = {}
|
| 1041 |
+
_supports_flash_attn_2 = False
|
| 1042 |
+
_supports_flash_attn_3 = False
|
| 1043 |
+
_fastplms_attention_implementations = ("sdpa", "flex_attention")
|
| 1044 |
+
_is_internal_encoder = False
|
| 1045 |
+
|
| 1046 |
+
def __init__(self, config: E1Config, *args: Any, **kwargs: Any) -> None:
|
| 1047 |
+
super().__init__(config, *args, **kwargs)
|
| 1048 |
+
# The E1 agreement requires this exact attribution when an E1 model is
|
| 1049 |
+
# launched. Internal encoder construction is excluded so each public
|
| 1050 |
+
# model launch displays the attribution exactly once.
|
| 1051 |
+
if not self._is_internal_encoder:
|
| 1052 |
+
print("Profluent-E1", file=sys.stderr, flush=True)
|
| 1053 |
+
|
| 1054 |
+
@classmethod
|
| 1055 |
+
def from_pretrained( # type: ignore[override]
|
| 1056 |
+
cls,
|
| 1057 |
+
pretrained_model_name_or_path: str | os.PathLike,
|
| 1058 |
+
*model_args: Any,
|
| 1059 |
+
**kwargs: Any,
|
| 1060 |
+
) -> E1PreTrainedModel:
|
| 1061 |
+
tokenizer_token = None
|
| 1062 |
+
if "token" in kwargs:
|
| 1063 |
+
tokenizer_token = kwargs["token"]
|
| 1064 |
+
elif "use_auth_token" in kwargs:
|
| 1065 |
+
tokenizer_token = kwargs["use_auth_token"]
|
| 1066 |
+
load_context_token = _TOKENIZER_LOAD_CONTEXT.set(
|
| 1067 |
+
{
|
| 1068 |
+
"tokenizer_source": pretrained_model_name_or_path,
|
| 1069 |
+
"local_files_only": bool(kwargs.get("local_files_only", False)),
|
| 1070 |
+
"cache_dir": kwargs.get("cache_dir"),
|
| 1071 |
+
"revision": kwargs.get("revision"),
|
| 1072 |
+
"token": tokenizer_token,
|
| 1073 |
+
}
|
| 1074 |
+
)
|
| 1075 |
+
try:
|
| 1076 |
+
return super().from_pretrained(pretrained_model_name_or_path, *model_args, **kwargs)
|
| 1077 |
+
finally:
|
| 1078 |
+
_TOKENIZER_LOAD_CONTEXT.reset(load_context_token)
|
| 1079 |
+
|
| 1080 |
+
@staticmethod
|
| 1081 |
+
def _tokenizer_kwargs_from_config(config: E1Config) -> dict[str, Any]:
|
| 1082 |
+
load_context = _TOKENIZER_LOAD_CONTEXT.get()
|
| 1083 |
+
resolved_revision = getattr(config, "_commit_hash", None)
|
| 1084 |
+
if not isinstance(resolved_revision, str) or not resolved_revision.strip():
|
| 1085 |
+
resolved_revision = None
|
| 1086 |
+
if load_context is not None:
|
| 1087 |
+
tokenizer_kwargs = dict(load_context)
|
| 1088 |
+
if resolved_revision is not None:
|
| 1089 |
+
tokenizer_kwargs["revision"] = resolved_revision
|
| 1090 |
+
return tokenizer_kwargs
|
| 1091 |
+
|
| 1092 |
+
tokenizer_source = None
|
| 1093 |
+
if isinstance(config._name_or_path, str) and len(config._name_or_path) > 0:
|
| 1094 |
+
tokenizer_source = config._name_or_path
|
| 1095 |
+
return {
|
| 1096 |
+
"tokenizer_source": tokenizer_source,
|
| 1097 |
+
"local_files_only": False,
|
| 1098 |
+
"cache_dir": None,
|
| 1099 |
+
"revision": resolved_revision,
|
| 1100 |
+
"token": None,
|
| 1101 |
+
}
|
| 1102 |
+
|
| 1103 |
+
@property
|
| 1104 |
+
def prep_tokens(self) -> E1BatchPreparer:
|
| 1105 |
+
"""Create E1's raw-sequence preparer only when a sequence API uses it."""
|
| 1106 |
+
|
| 1107 |
+
preparer = self.__dict__.get("_fastplms_prep_tokens")
|
| 1108 |
+
if preparer is not None:
|
| 1109 |
+
return preparer
|
| 1110 |
+
encoder = self._modules.get("model")
|
| 1111 |
+
if encoder is not None and encoder is not self:
|
| 1112 |
+
return encoder.prep_tokens
|
| 1113 |
+
tokenizer_kwargs = self.__dict__.get("_fastplms_tokenizer_kwargs")
|
| 1114 |
+
if tokenizer_kwargs is None:
|
| 1115 |
+
raise RuntimeError("E1 tokenizer settings were not initialized.")
|
| 1116 |
+
preparer = E1BatchPreparer(
|
| 1117 |
+
data_prep_config=DataPrepConfig(
|
| 1118 |
+
max_num_sequences=self.config.max_num_sequences,
|
| 1119 |
+
max_num_positions_within_seq=self.config.max_num_positions_within_seq,
|
| 1120 |
+
),
|
| 1121 |
+
**tokenizer_kwargs,
|
| 1122 |
+
)
|
| 1123 |
+
self.__dict__["_fastplms_prep_tokens"] = preparer
|
| 1124 |
+
return preparer
|
| 1125 |
+
|
| 1126 |
+
@prep_tokens.setter
|
| 1127 |
+
def prep_tokens(self, value: E1BatchPreparer | None) -> None:
|
| 1128 |
+
self.__dict__["_fastplms_prep_tokens"] = value
|
| 1129 |
+
|
| 1130 |
+
def _init_weights(self, module: nn.Module) -> None:
|
| 1131 |
+
if isinstance(module, RMSNorm):
|
| 1132 |
+
nn.init.ones_(module.weight)
|
| 1133 |
+
return
|
| 1134 |
+
if not isinstance(module, (nn.Linear, nn.Embedding)):
|
| 1135 |
+
return
|
| 1136 |
+
|
| 1137 |
+
nn.init.normal_(module.weight, mean=0.0, std=self.config.initializer_range)
|
| 1138 |
+
if isinstance(module, nn.Linear) and module.bias is not None:
|
| 1139 |
+
nn.init.zeros_(module.bias)
|
| 1140 |
+
if isinstance(module, nn.Embedding) and module.padding_idx is not None:
|
| 1141 |
+
with torch.no_grad():
|
| 1142 |
+
module.weight[module.padding_idx].zero_()
|
| 1143 |
+
|
| 1144 |
+
def _backward_compatibility_gradient_checkpointing(self) -> None:
|
| 1145 |
+
if self.supports_gradient_checkpointing and getattr(
|
| 1146 |
+
self.config, "gradient_checkpointing", False
|
| 1147 |
+
):
|
| 1148 |
+
self.gradient_checkpointing_enable(dict(use_reentrant=False))
|
| 1149 |
+
|
| 1150 |
+
def post_init(self) -> None:
|
| 1151 |
+
super().post_init()
|
| 1152 |
+
|
| 1153 |
+
@property
|
| 1154 |
+
def _device(self) -> torch.device:
|
| 1155 |
+
return next(self.parameters()).device
|
| 1156 |
+
|
| 1157 |
+
@property
|
| 1158 |
+
def attn_backend(self) -> str:
|
| 1159 |
+
return self.config.attn_backend
|
| 1160 |
+
|
| 1161 |
+
@attn_backend.setter
|
| 1162 |
+
def attn_backend(self, backend: str) -> None:
|
| 1163 |
+
if backend not in self._fastplms_attention_implementations:
|
| 1164 |
+
raise ValueError(
|
| 1165 |
+
f"E1 does not support {backend!r}; expected one of "
|
| 1166 |
+
f"{self._fastplms_attention_implementations}."
|
| 1167 |
+
)
|
| 1168 |
+
self.config.attn_backend = backend
|
| 1169 |
+
resolved = resolve_attention_backend(backend)
|
| 1170 |
+
for module in self.modules():
|
| 1171 |
+
if isinstance(module, FAST_E1_ENCODER):
|
| 1172 |
+
module._attn_backend = resolved
|
| 1173 |
+
elif isinstance(module, Attention):
|
| 1174 |
+
module.attn_backend = resolved
|
| 1175 |
+
|
| 1176 |
+
|
| 1177 |
+
class FAST_E1_ENCODER(E1PreTrainedModel, EmbeddingMixin):
|
| 1178 |
+
config: E1Config
|
| 1179 |
+
config_class = E1Config
|
| 1180 |
+
_is_internal_encoder = True
|
| 1181 |
+
|
| 1182 |
+
def __init__(self, config: E1Config, **kwargs):
|
| 1183 |
+
E1PreTrainedModel.__init__(self, config, **kwargs)
|
| 1184 |
+
self.padding_idx = config.pad_token_id
|
| 1185 |
+
self.vocab_size = config.vocab_size
|
| 1186 |
+
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
|
| 1187 |
+
self.embed_seq_id = nn.Embedding(config.max_num_sequences, config.hidden_size)
|
| 1188 |
+
self.layers = nn.ModuleList(
|
| 1189 |
+
[DecoderLayer(config, i) for i in range(config.num_hidden_layers)]
|
| 1190 |
+
)
|
| 1191 |
+
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
| 1192 |
+
self.gradient_checkpointing = config.gradient_checkpointing
|
| 1193 |
+
self.__dict__["_fastplms_tokenizer_kwargs"] = (
|
| 1194 |
+
E1PreTrainedModel._tokenizer_kwargs_from_config(config)
|
| 1195 |
+
)
|
| 1196 |
+
self.__dict__["_fastplms_prep_tokens"] = None
|
| 1197 |
+
self._attn_backend = resolve_attention_backend(config.attn_backend)
|
| 1198 |
+
self.post_init()
|
| 1199 |
+
|
| 1200 |
+
def get_input_embeddings(self) -> nn.Embedding:
|
| 1201 |
+
return self.embed_tokens
|
| 1202 |
+
|
| 1203 |
+
def set_input_embeddings(self, value: nn.Embedding) -> None:
|
| 1204 |
+
self.embed_tokens = value
|
| 1205 |
+
|
| 1206 |
+
def _embed(
|
| 1207 |
+
self,
|
| 1208 |
+
sequences: list[str],
|
| 1209 |
+
return_attention_mask: bool = False,
|
| 1210 |
+
hidden_state_index: int = -1,
|
| 1211 |
+
store_all_hidden_states: bool = False,
|
| 1212 |
+
**kwargs,
|
| 1213 |
+
) -> torch.Tensor:
|
| 1214 |
+
batch = self.prep_tokens.get_batch_kwargs(sequences, device=self._device)
|
| 1215 |
+
# The native preparer also returns training labels plus retrieval
|
| 1216 |
+
# descriptors. The encoder accepts only its aligned model inputs.
|
| 1217 |
+
encoder_batch: dict[str, torch.Tensor] = {}
|
| 1218 |
+
for name in (
|
| 1219 |
+
"input_ids",
|
| 1220 |
+
"within_seq_position_ids",
|
| 1221 |
+
"global_position_ids",
|
| 1222 |
+
"sequence_ids",
|
| 1223 |
+
):
|
| 1224 |
+
value = batch[name]
|
| 1225 |
+
if not isinstance(value, torch.Tensor):
|
| 1226 |
+
raise TypeError(f"Prepared E1 field {name!r} must be a tensor.")
|
| 1227 |
+
encoder_batch[name] = value
|
| 1228 |
+
output_hidden_states = store_all_hidden_states or hidden_state_index != -1
|
| 1229 |
+
output = self.forward(
|
| 1230 |
+
**encoder_batch,
|
| 1231 |
+
output_hidden_states=output_hidden_states,
|
| 1232 |
+
output_attentions=False,
|
| 1233 |
+
return_dict=True,
|
| 1234 |
+
)
|
| 1235 |
+
embeddings = select_hidden_state_embeddings(
|
| 1236 |
+
output.last_hidden_state,
|
| 1237 |
+
output.hidden_states,
|
| 1238 |
+
hidden_state_index=hidden_state_index,
|
| 1239 |
+
store_all_hidden_states=store_all_hidden_states,
|
| 1240 |
+
)
|
| 1241 |
+
if return_attention_mask:
|
| 1242 |
+
attention_mask = (encoder_batch["sequence_ids"] != -1).long()
|
| 1243 |
+
return embeddings, attention_mask
|
| 1244 |
+
else:
|
| 1245 |
+
return embeddings
|
| 1246 |
+
|
| 1247 |
+
def _prepare_hidden_states(
|
| 1248 |
+
self,
|
| 1249 |
+
input_ids: torch.LongTensor | None,
|
| 1250 |
+
inputs_embeds: torch.FloatTensor | None,
|
| 1251 |
+
within_seq_position_ids: torch.LongTensor | None,
|
| 1252 |
+
global_position_ids: torch.LongTensor | None,
|
| 1253 |
+
sequence_ids: torch.LongTensor | None,
|
| 1254 |
+
) -> tuple[torch.Tensor, torch.LongTensor, torch.LongTensor, torch.LongTensor]:
|
| 1255 |
+
if (input_ids is None) == (inputs_embeds is None):
|
| 1256 |
+
message = (
|
| 1257 |
+
"Must specify either input_ids or inputs_embeds"
|
| 1258 |
+
if input_ids is None
|
| 1259 |
+
else "Cannot specify both input_ids and inputs_embeds"
|
| 1260 |
+
)
|
| 1261 |
+
raise ValueError(message)
|
| 1262 |
+
|
| 1263 |
+
source = input_ids if input_ids is not None else inputs_embeds
|
| 1264 |
+
if source is None:
|
| 1265 |
+
raise RuntimeError("E1 input validation did not resolve an input tensor.")
|
| 1266 |
+
expected_rank = 2 if input_ids is not None else 3
|
| 1267 |
+
if source.ndim != expected_rank:
|
| 1268 |
+
source_name = "input_ids" if input_ids is not None else "inputs_embeds"
|
| 1269 |
+
raise ValueError(
|
| 1270 |
+
f"{source_name} must have rank {expected_rank}; got shape {tuple(source.shape)}."
|
| 1271 |
+
)
|
| 1272 |
+
batch_size, sequence_length = source.shape[:2]
|
| 1273 |
+
if sequence_length == 0:
|
| 1274 |
+
raise ValueError("E1 inputs must contain at least one token.")
|
| 1275 |
+
if inputs_embeds is not None and inputs_embeds.shape[-1] != self.config.hidden_size:
|
| 1276 |
+
raise ValueError(
|
| 1277 |
+
"inputs_embeds hidden dimension must match config.hidden_size; "
|
| 1278 |
+
f"got {inputs_embeds.shape[-1]} and {self.config.hidden_size}."
|
| 1279 |
+
)
|
| 1280 |
+
if inputs_embeds is not None:
|
| 1281 |
+
default_positions = torch.arange(sequence_length, device=source.device).expand(
|
| 1282 |
+
batch_size,
|
| 1283 |
+
-1,
|
| 1284 |
+
)
|
| 1285 |
+
if within_seq_position_ids is None:
|
| 1286 |
+
within_seq_position_ids = default_positions
|
| 1287 |
+
if global_position_ids is None:
|
| 1288 |
+
global_position_ids = default_positions
|
| 1289 |
+
if sequence_ids is None:
|
| 1290 |
+
sequence_ids = torch.zeros_like(default_positions)
|
| 1291 |
+
|
| 1292 |
+
if within_seq_position_ids is None or global_position_ids is None or sequence_ids is None:
|
| 1293 |
+
raise ValueError("Position and sequence IDs are required when input_ids are provided.")
|
| 1294 |
+
expected_shape = (batch_size, sequence_length)
|
| 1295 |
+
aligned_inputs = {
|
| 1296 |
+
"within_seq_position_ids": within_seq_position_ids,
|
| 1297 |
+
"global_position_ids": global_position_ids,
|
| 1298 |
+
"sequence_ids": sequence_ids,
|
| 1299 |
+
}
|
| 1300 |
+
for name, value in aligned_inputs.items():
|
| 1301 |
+
if tuple(value.shape) != expected_shape:
|
| 1302 |
+
raise ValueError(
|
| 1303 |
+
f"{name} must have shape {expected_shape}; got {tuple(value.shape)}."
|
| 1304 |
+
)
|
| 1305 |
+
within_positions = within_seq_position_ids.long()
|
| 1306 |
+
global_positions = global_position_ids.long()
|
| 1307 |
+
sequence_numbers = sequence_ids.long()
|
| 1308 |
+
lowest_position, highest_position = torch.aminmax(within_positions)
|
| 1309 |
+
if (
|
| 1310 |
+
lowest_position.item() < -1
|
| 1311 |
+
or highest_position.item() >= self.config.max_num_positions_within_seq
|
| 1312 |
+
):
|
| 1313 |
+
raise ValueError(
|
| 1314 |
+
"Position ids must be in the range "
|
| 1315 |
+
f"[-1, {self.config.max_num_positions_within_seq}); got max "
|
| 1316 |
+
f"{highest_position.item()} and min {lowest_position.item()}"
|
| 1317 |
+
)
|
| 1318 |
+
lowest_global, highest_global = torch.aminmax(global_positions)
|
| 1319 |
+
if (
|
| 1320 |
+
lowest_global.item() < -1
|
| 1321 |
+
or highest_global.item() >= self.config.max_num_positions_global
|
| 1322 |
+
):
|
| 1323 |
+
raise ValueError(
|
| 1324 |
+
"Global position ids must be in the range "
|
| 1325 |
+
f"[-1, {self.config.max_num_positions_global}); got max "
|
| 1326 |
+
f"{highest_global.item()} and min {lowest_global.item()}"
|
| 1327 |
+
)
|
| 1328 |
+
lowest_sequence, highest_sequence = torch.aminmax(sequence_numbers)
|
| 1329 |
+
if lowest_sequence.item() < -1 or highest_sequence.item() >= self.config.max_num_sequences:
|
| 1330 |
+
raise ValueError(
|
| 1331 |
+
"Sequence ids must be in the range "
|
| 1332 |
+
f"[-1, {self.config.max_num_sequences}); got max "
|
| 1333 |
+
f"{highest_sequence.item()} and min {lowest_sequence.item()}"
|
| 1334 |
+
)
|
| 1335 |
+
|
| 1336 |
+
if inputs_embeds is None:
|
| 1337 |
+
if input_ids is None:
|
| 1338 |
+
raise RuntimeError("E1 input validation lost the token ID tensor.")
|
| 1339 |
+
token_embeddings = self.embed_tokens(input_ids)
|
| 1340 |
+
inputs_embeds = token_embeddings + self.embed_seq_id(sequence_numbers.clamp_min(0))
|
| 1341 |
+
layer_dtype = self.layers[0].norm_attn_norm.self_attn.q_proj.weight.dtype
|
| 1342 |
+
target_dtype = (
|
| 1343 |
+
torch.get_autocast_dtype("cuda") if torch.is_autocast_enabled() else layer_dtype
|
| 1344 |
+
)
|
| 1345 |
+
return (
|
| 1346 |
+
inputs_embeds.to(target_dtype),
|
| 1347 |
+
within_positions,
|
| 1348 |
+
global_positions,
|
| 1349 |
+
sequence_numbers,
|
| 1350 |
+
)
|
| 1351 |
+
|
| 1352 |
+
def _resolve_forward_cache(
|
| 1353 |
+
self,
|
| 1354 |
+
past_key_values: DynamicCache | None,
|
| 1355 |
+
use_cache: bool,
|
| 1356 |
+
) -> tuple[DynamicCache | None, bool]:
|
| 1357 |
+
checkpointing = self.gradient_checkpointing and self.training and torch.is_grad_enabled()
|
| 1358 |
+
if checkpointing and use_cache:
|
| 1359 |
+
_get_logger().warning_once(
|
| 1360 |
+
"`use_cache=True` is incompatible with gradient checkpointing; "
|
| 1361 |
+
"setting `use_cache=False`."
|
| 1362 |
+
)
|
| 1363 |
+
use_cache = False
|
| 1364 |
+
if not use_cache:
|
| 1365 |
+
return None, False
|
| 1366 |
+
return past_key_values if past_key_values is not None else DynamicCache(), True
|
| 1367 |
+
|
| 1368 |
+
def _build_forward_attention_args(
|
| 1369 |
+
self,
|
| 1370 |
+
sequence_ids: torch.LongTensor,
|
| 1371 |
+
past_key_values: DynamicCache | None,
|
| 1372 |
+
effective_backend: AttentionBackend,
|
| 1373 |
+
) -> AttentionArgs | None:
|
| 1374 |
+
if past_key_values is not None and past_key_values.get_seq_length() != 0:
|
| 1375 |
+
return None
|
| 1376 |
+
|
| 1377 |
+
use_flex = effective_backend == AttentionBackend.FLEX
|
| 1378 |
+
use_dense_mask = effective_backend in {
|
| 1379 |
+
AttentionBackend.EAGER,
|
| 1380 |
+
AttentionBackend.SDPA,
|
| 1381 |
+
}
|
| 1382 |
+
return AttentionArgs(
|
| 1383 |
+
block_causal_block_mask=(
|
| 1384 |
+
create_block_causal_mask_optimized(sequence_ids)
|
| 1385 |
+
if use_flex and self.config.global_attention_every_n_layers > 0
|
| 1386 |
+
else None
|
| 1387 |
+
),
|
| 1388 |
+
within_seq_block_mask=(
|
| 1389 |
+
create_within_seq_block_mask(sequence_ids) if use_flex else None
|
| 1390 |
+
),
|
| 1391 |
+
within_seq_mask_4d=(build_within_seq_mask_4d(sequence_ids) if use_dense_mask else None),
|
| 1392 |
+
block_causal_mask_4d=(
|
| 1393 |
+
build_block_causal_mask_4d(sequence_ids) if use_dense_mask else None
|
| 1394 |
+
),
|
| 1395 |
+
)
|
| 1396 |
+
|
| 1397 |
+
def _run_decoder_layers(
|
| 1398 |
+
self,
|
| 1399 |
+
hidden_states: torch.Tensor,
|
| 1400 |
+
within_seq_position_ids: torch.LongTensor,
|
| 1401 |
+
global_position_ids: torch.LongTensor,
|
| 1402 |
+
sequence_ids: torch.LongTensor,
|
| 1403 |
+
attention_args: AttentionArgs | None,
|
| 1404 |
+
past_key_values: DynamicCache | None,
|
| 1405 |
+
use_cache: bool,
|
| 1406 |
+
output_attentions: bool,
|
| 1407 |
+
output_hidden_states: bool,
|
| 1408 |
+
output_s_max: bool,
|
| 1409 |
+
effective_backend: AttentionBackend,
|
| 1410 |
+
) -> E1ModelOutputWithPast:
|
| 1411 |
+
hidden_history: list[torch.Tensor] | None = [] if output_hidden_states else None
|
| 1412 |
+
attention_history: list[torch.Tensor] | None = [] if output_attentions else None
|
| 1413 |
+
s_max_history: list[list[torch.Tensor]] | None = [] if output_s_max else None
|
| 1414 |
+
next_cache: DynamicCache | None = None
|
| 1415 |
+
|
| 1416 |
+
for layer in self.layers:
|
| 1417 |
+
if hidden_history is not None:
|
| 1418 |
+
hidden_history.append(hidden_states)
|
| 1419 |
+
if self.gradient_checkpointing and self.training and torch.is_grad_enabled():
|
| 1420 |
+
layer_output = self._gradient_checkpointing_func(
|
| 1421 |
+
layer.__call__,
|
| 1422 |
+
hidden_states,
|
| 1423 |
+
within_seq_position_ids,
|
| 1424 |
+
global_position_ids,
|
| 1425 |
+
sequence_ids,
|
| 1426 |
+
attention_args,
|
| 1427 |
+
past_key_values,
|
| 1428 |
+
output_attentions,
|
| 1429 |
+
output_s_max,
|
| 1430 |
+
use_cache,
|
| 1431 |
+
effective_backend,
|
| 1432 |
+
)
|
| 1433 |
+
else:
|
| 1434 |
+
layer_output = layer(
|
| 1435 |
+
hidden_states,
|
| 1436 |
+
within_seq_position_ids=within_seq_position_ids,
|
| 1437 |
+
global_position_ids=global_position_ids,
|
| 1438 |
+
sequence_ids=sequence_ids,
|
| 1439 |
+
attention_args=attention_args,
|
| 1440 |
+
past_key_value=past_key_values,
|
| 1441 |
+
output_attentions=output_attentions,
|
| 1442 |
+
output_s_max=output_s_max,
|
| 1443 |
+
use_cache=use_cache,
|
| 1444 |
+
effective_backend=effective_backend,
|
| 1445 |
+
)
|
| 1446 |
+
hidden_states, attention, layer_cache, s_max = layer_output
|
| 1447 |
+
if use_cache:
|
| 1448 |
+
past_key_values = layer_cache
|
| 1449 |
+
next_cache = layer_cache
|
| 1450 |
+
if attention_history is not None:
|
| 1451 |
+
if attention is None:
|
| 1452 |
+
raise RuntimeError(
|
| 1453 |
+
"An E1 layer did not return attention tensors when requested."
|
| 1454 |
+
)
|
| 1455 |
+
attention_history.append(attention)
|
| 1456 |
+
if s_max_history is not None:
|
| 1457 |
+
if s_max is None:
|
| 1458 |
+
raise RuntimeError(
|
| 1459 |
+
"An E1 layer did not return s_max diagnostics when requested."
|
| 1460 |
+
)
|
| 1461 |
+
s_max_history.append(s_max)
|
| 1462 |
+
|
| 1463 |
+
hidden_states = self.norm(hidden_states)
|
| 1464 |
+
if hidden_history is not None:
|
| 1465 |
+
hidden_history.append(hidden_states)
|
| 1466 |
+
return E1ModelOutputWithPast(
|
| 1467 |
+
last_hidden_state=hidden_states,
|
| 1468 |
+
past_key_values=next_cache,
|
| 1469 |
+
hidden_states=tuple(hidden_history) if hidden_history is not None else None,
|
| 1470 |
+
attentions=tuple(attention_history) if attention_history is not None else None,
|
| 1471 |
+
s_max=tuple(s_max_history) if s_max_history is not None else None,
|
| 1472 |
+
)
|
| 1473 |
+
|
| 1474 |
+
def forward(
|
| 1475 |
+
self,
|
| 1476 |
+
input_ids: torch.LongTensor | None = None,
|
| 1477 |
+
within_seq_position_ids: torch.LongTensor | None = None,
|
| 1478 |
+
global_position_ids: torch.LongTensor | None = None,
|
| 1479 |
+
sequence_ids: torch.LongTensor | None = None,
|
| 1480 |
+
inputs_embeds: torch.FloatTensor | None = None,
|
| 1481 |
+
past_key_values: DynamicCache | None = None,
|
| 1482 |
+
use_cache: bool | None = None,
|
| 1483 |
+
output_attentions: bool | None = None,
|
| 1484 |
+
output_hidden_states: bool | None = None,
|
| 1485 |
+
output_s_max: bool = False,
|
| 1486 |
+
return_dict: bool | None = None,
|
| 1487 |
+
) -> E1ModelOutputWithPast | tuple[Any, ...]:
|
| 1488 |
+
"""Transform token or soft embeddings H with shape (b, l, d)."""
|
| 1489 |
+
|
| 1490 |
+
use_cache = (
|
| 1491 |
+
use_cache if use_cache is not None else bool(getattr(self.config, "use_cache", False))
|
| 1492 |
+
)
|
| 1493 |
+
output_attentions = (
|
| 1494 |
+
output_attentions if output_attentions is not None else self.config.output_attentions
|
| 1495 |
+
)
|
| 1496 |
+
output_hidden_states = (
|
| 1497 |
+
output_hidden_states
|
| 1498 |
+
if output_hidden_states is not None
|
| 1499 |
+
else self.config.output_hidden_states
|
| 1500 |
+
)
|
| 1501 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 1502 |
+
hidden_states, within_positions, global_positions, sequence_numbers = (
|
| 1503 |
+
self._prepare_hidden_states(
|
| 1504 |
+
input_ids,
|
| 1505 |
+
inputs_embeds,
|
| 1506 |
+
within_seq_position_ids,
|
| 1507 |
+
global_position_ids,
|
| 1508 |
+
sequence_ids,
|
| 1509 |
+
)
|
| 1510 |
+
)
|
| 1511 |
+
cache, use_cache = self._resolve_forward_cache(past_key_values, use_cache)
|
| 1512 |
+
effective_backend = resolve_attention_backend_for_call(
|
| 1513 |
+
self._attn_backend,
|
| 1514 |
+
output_attentions=bool(output_attentions),
|
| 1515 |
+
)
|
| 1516 |
+
attention_args = self._build_forward_attention_args(
|
| 1517 |
+
sequence_numbers,
|
| 1518 |
+
cache,
|
| 1519 |
+
effective_backend,
|
| 1520 |
+
)
|
| 1521 |
+
result = self._run_decoder_layers(
|
| 1522 |
+
hidden_states,
|
| 1523 |
+
within_positions,
|
| 1524 |
+
global_positions,
|
| 1525 |
+
sequence_numbers,
|
| 1526 |
+
attention_args,
|
| 1527 |
+
cache,
|
| 1528 |
+
use_cache,
|
| 1529 |
+
output_attentions,
|
| 1530 |
+
output_hidden_states,
|
| 1531 |
+
output_s_max,
|
| 1532 |
+
effective_backend,
|
| 1533 |
+
)
|
| 1534 |
+
if not return_dict:
|
| 1535 |
+
return result.to_tuple()
|
| 1536 |
+
return result
|
| 1537 |
+
|
| 1538 |
+
|
| 1539 |
+
class E1Model(E1PreTrainedModel, EmbeddingMixin):
|
| 1540 |
+
config: E1Config
|
| 1541 |
+
config_class = E1Config
|
| 1542 |
+
|
| 1543 |
+
def __init__(self, config: E1Config, **kwargs):
|
| 1544 |
+
E1PreTrainedModel.__init__(self, config, **kwargs)
|
| 1545 |
+
self.model: FAST_E1_ENCODER = FAST_E1_ENCODER(config, **kwargs)
|
| 1546 |
+
self.post_init()
|
| 1547 |
+
|
| 1548 |
+
def get_input_embeddings(self) -> nn.Embedding:
|
| 1549 |
+
return self.model.get_input_embeddings()
|
| 1550 |
+
|
| 1551 |
+
def set_input_embeddings(self, value: nn.Embedding) -> None:
|
| 1552 |
+
self.model.set_input_embeddings(value)
|
| 1553 |
+
|
| 1554 |
+
def _embed(
|
| 1555 |
+
self, sequences: list[str], return_attention_mask: bool = False, **kwargs
|
| 1556 |
+
) -> torch.Tensor:
|
| 1557 |
+
return self.model._embed(sequences, return_attention_mask=return_attention_mask, **kwargs)
|
| 1558 |
+
|
| 1559 |
+
def forward(
|
| 1560 |
+
self,
|
| 1561 |
+
input_ids: torch.LongTensor | None = None,
|
| 1562 |
+
within_seq_position_ids: torch.LongTensor | None = None,
|
| 1563 |
+
global_position_ids: torch.LongTensor | None = None,
|
| 1564 |
+
sequence_ids: torch.LongTensor | None = None,
|
| 1565 |
+
inputs_embeds: torch.FloatTensor | None = None,
|
| 1566 |
+
past_key_values: DynamicCache | None = None,
|
| 1567 |
+
use_cache: bool | None = None,
|
| 1568 |
+
output_attentions: bool | None = None,
|
| 1569 |
+
output_hidden_states: bool | None = None,
|
| 1570 |
+
output_s_max: bool = False,
|
| 1571 |
+
return_dict: bool | None = None,
|
| 1572 |
+
) -> E1ModelOutputWithPast | tuple[Any, ...]:
|
| 1573 |
+
return self.model(
|
| 1574 |
+
input_ids=input_ids,
|
| 1575 |
+
within_seq_position_ids=within_seq_position_ids,
|
| 1576 |
+
global_position_ids=global_position_ids,
|
| 1577 |
+
sequence_ids=sequence_ids,
|
| 1578 |
+
inputs_embeds=inputs_embeds,
|
| 1579 |
+
past_key_values=past_key_values,
|
| 1580 |
+
use_cache=use_cache,
|
| 1581 |
+
output_attentions=output_attentions,
|
| 1582 |
+
output_hidden_states=output_hidden_states,
|
| 1583 |
+
output_s_max=output_s_max,
|
| 1584 |
+
return_dict=return_dict,
|
| 1585 |
+
)
|
| 1586 |
+
|
| 1587 |
+
|
| 1588 |
+
class E1ForMaskedLM(FastPLMTestTimeTrainingMixin, E1PreTrainedModel, EmbeddingMixin):
|
| 1589 |
+
config: E1Config
|
| 1590 |
+
config_class = E1Config
|
| 1591 |
+
|
| 1592 |
+
def __init__(self, config: E1Config, **kwargs):
|
| 1593 |
+
E1PreTrainedModel.__init__(self, config, **kwargs)
|
| 1594 |
+
self.model: FAST_E1_ENCODER = FAST_E1_ENCODER(config, **kwargs)
|
| 1595 |
+
self.vocab_size = config.vocab_size
|
| 1596 |
+
self.mlm_head = torch.nn.Sequential(
|
| 1597 |
+
nn.Linear(config.hidden_size, config.hidden_size, bias=True),
|
| 1598 |
+
nn.GELU(),
|
| 1599 |
+
nn.LayerNorm(config.hidden_size, eps=config.rms_norm_eps),
|
| 1600 |
+
nn.Linear(config.hidden_size, config.vocab_size, bias=True),
|
| 1601 |
+
)
|
| 1602 |
+
self.gradient_checkpointing = config.gradient_checkpointing
|
| 1603 |
+
self.post_init()
|
| 1604 |
+
self.init_ttt({"lora_target_replace_module": "Attention"})
|
| 1605 |
+
|
| 1606 |
+
@property
|
| 1607 |
+
def device_mesh(self) -> torch.distributed.device_mesh.DeviceMesh:
|
| 1608 |
+
return self.model.device_mesh
|
| 1609 |
+
|
| 1610 |
+
def get_input_embeddings(self) -> nn.Embedding:
|
| 1611 |
+
return self.model.get_input_embeddings()
|
| 1612 |
+
|
| 1613 |
+
def set_input_embeddings(self, value: nn.Embedding) -> None:
|
| 1614 |
+
self.model.set_input_embeddings(value)
|
| 1615 |
+
|
| 1616 |
+
def _embed(
|
| 1617 |
+
self, sequences: list[str], return_attention_mask: bool = False, **kwargs
|
| 1618 |
+
) -> torch.Tensor:
|
| 1619 |
+
return self.model._embed(sequences, return_attention_mask=return_attention_mask, **kwargs)
|
| 1620 |
+
|
| 1621 |
+
def get_output_embeddings(self) -> nn.Linear:
|
| 1622 |
+
return self.mlm_head[-1]
|
| 1623 |
+
|
| 1624 |
+
def set_output_embeddings(self, value: nn.Linear) -> None:
|
| 1625 |
+
self.mlm_head[-1] = value
|
| 1626 |
+
|
| 1627 |
+
def _ttt_get_trainable_modules(self) -> list[nn.Module]:
|
| 1628 |
+
return [self.model]
|
| 1629 |
+
|
| 1630 |
+
def _ttt_tokenize(
|
| 1631 |
+
self,
|
| 1632 |
+
seq: str | list[str] | None = None,
|
| 1633 |
+
input_ids: torch.Tensor | None = None,
|
| 1634 |
+
**kwargs,
|
| 1635 |
+
) -> dict[str, torch.Tensor]:
|
| 1636 |
+
if input_ids is not None:
|
| 1637 |
+
return {
|
| 1638 |
+
"input_ids": input_ids,
|
| 1639 |
+
"within_seq_position_ids": kwargs["within_seq_position_ids"],
|
| 1640 |
+
"global_position_ids": kwargs["global_position_ids"],
|
| 1641 |
+
"sequence_ids": kwargs["sequence_ids"],
|
| 1642 |
+
}
|
| 1643 |
+
if seq is None:
|
| 1644 |
+
raise ValueError("Pass either seq or E1 token tensors for TTT.")
|
| 1645 |
+
sequences = [seq] if isinstance(seq, str) else seq
|
| 1646 |
+
batch = self.prep_tokens.get_batch_kwargs(sequences, device=torch.device("cpu"))
|
| 1647 |
+
return {
|
| 1648 |
+
"input_ids": batch["input_ids"],
|
| 1649 |
+
"within_seq_position_ids": batch["within_seq_position_ids"],
|
| 1650 |
+
"global_position_ids": batch["global_position_ids"],
|
| 1651 |
+
"sequence_ids": batch["sequence_ids"],
|
| 1652 |
+
}
|
| 1653 |
+
|
| 1654 |
+
def _ttt_mask_token(self) -> int:
|
| 1655 |
+
return int(self.prep_tokens.mask_token_id)
|
| 1656 |
+
|
| 1657 |
+
def _ttt_padding_token(self) -> int:
|
| 1658 |
+
return int(self.prep_tokens.pad_token_id)
|
| 1659 |
+
|
| 1660 |
+
def _ttt_replacement_tokens(self, input_ids: torch.Tensor) -> torch.Tensor:
|
| 1661 |
+
amino_acids = "ACDEFGHIKLMNPQRSTVWY"
|
| 1662 |
+
ids = [self.prep_tokens.vocab[aa] for aa in amino_acids]
|
| 1663 |
+
return torch.tensor(ids, device=input_ids.device, dtype=input_ids.dtype)
|
| 1664 |
+
|
| 1665 |
+
def _ttt_non_special_mask(self, input_ids: torch.Tensor) -> torch.Tensor:
|
| 1666 |
+
return ~self.prep_tokens.get_boundary_token_mask(input_ids)
|
| 1667 |
+
|
| 1668 |
+
def _ttt_predict_logits(
|
| 1669 |
+
self,
|
| 1670 |
+
batch: torch.Tensor | dict[str, torch.Tensor],
|
| 1671 |
+
**kwargs,
|
| 1672 |
+
) -> torch.Tensor:
|
| 1673 |
+
del kwargs
|
| 1674 |
+
if not isinstance(batch, dict):
|
| 1675 |
+
raise TypeError("E1 TTT expects a tensor dictionary.")
|
| 1676 |
+
output = self(
|
| 1677 |
+
input_ids=batch["input_ids"],
|
| 1678 |
+
within_seq_position_ids=batch["within_seq_position_ids"],
|
| 1679 |
+
global_position_ids=batch["global_position_ids"],
|
| 1680 |
+
sequence_ids=batch["sequence_ids"],
|
| 1681 |
+
return_dict=True,
|
| 1682 |
+
)
|
| 1683 |
+
return output.logits
|
| 1684 |
+
|
| 1685 |
+
def search_homologues(
|
| 1686 |
+
self,
|
| 1687 |
+
sequence: str,
|
| 1688 |
+
output_dir: str,
|
| 1689 |
+
provider: str = "colabfold",
|
| 1690 |
+
target_db: str | None = None,
|
| 1691 |
+
seq_id: str | None = None,
|
| 1692 |
+
**kwargs,
|
| 1693 |
+
) -> str:
|
| 1694 |
+
searcher = _make_homologue_searcher(provider=provider, target_db=target_db, **kwargs)
|
| 1695 |
+
return searcher.search(sequence=sequence, output_dir=output_dir, seq_id=seq_id)
|
| 1696 |
+
|
| 1697 |
+
def batch_search_homologues(
|
| 1698 |
+
self,
|
| 1699 |
+
sequences: list[str],
|
| 1700 |
+
output_dir: str,
|
| 1701 |
+
provider: str = "colabfold",
|
| 1702 |
+
target_db: str | None = None,
|
| 1703 |
+
seq_ids: list[str] | None = None,
|
| 1704 |
+
continue_on_error: bool = True,
|
| 1705 |
+
**kwargs,
|
| 1706 |
+
) -> dict[str, str]:
|
| 1707 |
+
searcher = _make_homologue_searcher(provider=provider, target_db=target_db, **kwargs)
|
| 1708 |
+
return searcher.batch_search(
|
| 1709 |
+
sequences=sequences,
|
| 1710 |
+
output_dir=output_dir,
|
| 1711 |
+
seq_ids=seq_ids,
|
| 1712 |
+
continue_on_error=continue_on_error,
|
| 1713 |
+
)
|
| 1714 |
+
|
| 1715 |
+
def sample_msa_contexts(
|
| 1716 |
+
self,
|
| 1717 |
+
a3m_path: str,
|
| 1718 |
+
seed: int = 42,
|
| 1719 |
+
max_context_tokens: list[int] | None = None,
|
| 1720 |
+
similarity_thresholds: list[float] | None = None,
|
| 1721 |
+
min_query_similarity: float = 0.3,
|
| 1722 |
+
context_cache_dir: str | None = None,
|
| 1723 |
+
) -> dict[str, str]:
|
| 1724 |
+
context_specs = build_context_specifications(
|
| 1725 |
+
max_context_tokens=max_context_tokens,
|
| 1726 |
+
similarity_thresholds=similarity_thresholds,
|
| 1727 |
+
min_query_similarity=min_query_similarity,
|
| 1728 |
+
)
|
| 1729 |
+
cache = None
|
| 1730 |
+
if context_cache_dir is not None:
|
| 1731 |
+
key = repr((max_context_tokens, similarity_thresholds, min_query_similarity))
|
| 1732 |
+
specs_hash = hashlib.md5(key.encode()).hexdigest()[:8]
|
| 1733 |
+
cache = ContextCache(context_cache_dir, specs_hash, seed)
|
| 1734 |
+
cached = cache.load(a3m_path)
|
| 1735 |
+
if cached is not None:
|
| 1736 |
+
return cached
|
| 1737 |
+
contexts = sample_contexts_for_msa(a3m_path, context_specs, seed=seed)
|
| 1738 |
+
if cache is not None:
|
| 1739 |
+
cache.store(a3m_path, contexts)
|
| 1740 |
+
return contexts
|
| 1741 |
+
|
| 1742 |
+
@torch.inference_mode()
|
| 1743 |
+
def score_ppll(
|
| 1744 |
+
self,
|
| 1745 |
+
sequences: list[str],
|
| 1746 |
+
a3m_path: str,
|
| 1747 |
+
ensemble: bool = True,
|
| 1748 |
+
seed: int = 42,
|
| 1749 |
+
max_context_tokens: list[int] | None = None,
|
| 1750 |
+
similarity_thresholds: list[float] | None = None,
|
| 1751 |
+
min_query_similarity: float = 0.3,
|
| 1752 |
+
max_batch_tokens: int = 131072,
|
| 1753 |
+
cache_size: int = 1,
|
| 1754 |
+
context_cache_dir: str | None = None,
|
| 1755 |
+
progress: bool = True,
|
| 1756 |
+
) -> list[float] | list[list[float]]:
|
| 1757 |
+
"""Score sequences with FastPLMs PPLL reduction over sampled E1 MSA contexts.
|
| 1758 |
+
|
| 1759 |
+
This intentionally differs from Profluent's official E1Scorer, which scores
|
| 1760 |
+
mutants against a parent sequence with wildtype or masked marginal log-prob
|
| 1761 |
+
deltas. Here each sequence is scored by mean correct-token probability and
|
| 1762 |
+
optionally averaged across sampled contexts.
|
| 1763 |
+
"""
|
| 1764 |
+
contexts = self.sample_msa_contexts(
|
| 1765 |
+
a3m_path=a3m_path,
|
| 1766 |
+
seed=seed,
|
| 1767 |
+
max_context_tokens=max_context_tokens,
|
| 1768 |
+
similarity_thresholds=similarity_thresholds,
|
| 1769 |
+
min_query_similarity=min_query_similarity,
|
| 1770 |
+
context_cache_dir=context_cache_dir,
|
| 1771 |
+
)
|
| 1772 |
+
if not contexts:
|
| 1773 |
+
raise ValueError("At least one sampled MSA context is required for PPLL scoring.")
|
| 1774 |
+
|
| 1775 |
+
predictor = _E1ContextPredictor(
|
| 1776 |
+
model=self,
|
| 1777 |
+
data_prep_config=DataPrepConfig(remove_X_tokens=True),
|
| 1778 |
+
max_batch_tokens=max_batch_tokens,
|
| 1779 |
+
fields_to_save=["logits"],
|
| 1780 |
+
save_masked_positions_only=False,
|
| 1781 |
+
keep_predictions_in_gpu=False,
|
| 1782 |
+
use_cache=True,
|
| 1783 |
+
cache_size=cache_size,
|
| 1784 |
+
progress=progress,
|
| 1785 |
+
)
|
| 1786 |
+
vocab = predictor.batch_preparer.vocab
|
| 1787 |
+
seq_token_ids = [
|
| 1788 |
+
torch.tensor([vocab[aa] for aa in seq if aa != "X"], device=self.device)
|
| 1789 |
+
for seq in sequences
|
| 1790 |
+
]
|
| 1791 |
+
context_ids = list(contexts.keys())
|
| 1792 |
+
all_scores = torch.zeros(len(sequences), len(context_ids), device=self.device)
|
| 1793 |
+
|
| 1794 |
+
iterator = tqdm(context_ids, desc="Scoring with contexts", disable=not progress)
|
| 1795 |
+
for ctx_idx, ctx_id in enumerate(iterator):
|
| 1796 |
+
predictions = list(
|
| 1797 |
+
predictor.predict(
|
| 1798 |
+
sequences=sequences,
|
| 1799 |
+
sequence_ids=list(range(len(sequences))),
|
| 1800 |
+
context_seqs={ctx_id: contexts[ctx_id]},
|
| 1801 |
+
)
|
| 1802 |
+
)
|
| 1803 |
+
for prediction in predictions:
|
| 1804 |
+
seq_idx = prediction["id"]
|
| 1805 |
+
if not isinstance(seq_idx, int):
|
| 1806 |
+
raise TypeError("Expected integer sequence ids for score aggregation.")
|
| 1807 |
+
all_scores[seq_idx, ctx_idx] = compute_ppll(
|
| 1808 |
+
prediction["logits"], seq_token_ids[seq_idx]
|
| 1809 |
+
)
|
| 1810 |
+
if predictor.kv_cache is not None:
|
| 1811 |
+
predictor.kv_cache.reset()
|
| 1812 |
+
|
| 1813 |
+
if ensemble:
|
| 1814 |
+
return all_scores.mean(dim=1).tolist()
|
| 1815 |
+
return all_scores.tolist()
|
| 1816 |
+
|
| 1817 |
+
@torch.inference_mode()
|
| 1818 |
+
def embed_with_msa(
|
| 1819 |
+
self,
|
| 1820 |
+
sequences: list[str],
|
| 1821 |
+
a3m_path: str | None = None,
|
| 1822 |
+
context: str | None = None,
|
| 1823 |
+
pooling_types: list[str] | None = None,
|
| 1824 |
+
pooling: str = "mean",
|
| 1825 |
+
matrix_embed: bool = False,
|
| 1826 |
+
seed: int = 42,
|
| 1827 |
+
max_batch_tokens: int = 131072,
|
| 1828 |
+
embed_max_tokens: int = DEFAULT_EMBED_MAX_TOKENS,
|
| 1829 |
+
embed_similarity: float = DEFAULT_EMBED_SIMILARITY,
|
| 1830 |
+
min_query_similarity: float = 0.3,
|
| 1831 |
+
progress: bool = True,
|
| 1832 |
+
) -> torch.Tensor | list[torch.Tensor]:
|
| 1833 |
+
if a3m_path is not None and context is None:
|
| 1834 |
+
spec = ContextSpecification(
|
| 1835 |
+
max_num_samples=511,
|
| 1836 |
+
max_token_length=embed_max_tokens,
|
| 1837 |
+
max_query_similarity=embed_similarity,
|
| 1838 |
+
min_query_similarity=min_query_similarity,
|
| 1839 |
+
)
|
| 1840 |
+
contexts, _ = sample_multiple_contexts(
|
| 1841 |
+
msa_path=a3m_path,
|
| 1842 |
+
context_specifications=[spec],
|
| 1843 |
+
seed=seed,
|
| 1844 |
+
)
|
| 1845 |
+
context = contexts[0] if contexts else None
|
| 1846 |
+
|
| 1847 |
+
hidden_list = _forward_for_embedding(
|
| 1848 |
+
model=self,
|
| 1849 |
+
sequences=sequences,
|
| 1850 |
+
context=context,
|
| 1851 |
+
max_batch_tokens=max_batch_tokens,
|
| 1852 |
+
progress=progress,
|
| 1853 |
+
)
|
| 1854 |
+
if matrix_embed:
|
| 1855 |
+
return hidden_list
|
| 1856 |
+
if pooling_types is not None:
|
| 1857 |
+
return _pool_hidden_states(hidden_list, pooling_types, self.device)
|
| 1858 |
+
if pooling not in ("mean", "cls"):
|
| 1859 |
+
raise ValueError("pooling must be 'mean' or 'cls' when pooling_types is not provided")
|
| 1860 |
+
embeddings = [
|
| 1861 |
+
hidden.mean(dim=0) if pooling == "mean" else hidden[0] for hidden in hidden_list
|
| 1862 |
+
]
|
| 1863 |
+
return torch.stack(embeddings)
|
| 1864 |
+
|
| 1865 |
+
@torch.inference_mode()
|
| 1866 |
+
def embed_dataset_with_msa(
|
| 1867 |
+
self,
|
| 1868 |
+
sequences: list[str],
|
| 1869 |
+
msa_lookup: dict[str, str] | None = None,
|
| 1870 |
+
msa_dir: str | None = None,
|
| 1871 |
+
msa_hf_path: str | None = None,
|
| 1872 |
+
batch_size: int = 2,
|
| 1873 |
+
max_len: int = 2048,
|
| 1874 |
+
pooling_types: list[str] | None = None,
|
| 1875 |
+
pooling: str = "mean",
|
| 1876 |
+
matrix_embed: bool = False,
|
| 1877 |
+
embed_dtype: torch.dtype = torch.bfloat16,
|
| 1878 |
+
embed_max_tokens: int = DEFAULT_EMBED_MAX_TOKENS,
|
| 1879 |
+
embed_similarity: float = DEFAULT_EMBED_SIMILARITY,
|
| 1880 |
+
min_query_similarity: float = 0.3,
|
| 1881 |
+
seed: int = 42,
|
| 1882 |
+
progress: bool = True,
|
| 1883 |
+
max_batch_tokens: int = 131072,
|
| 1884 |
+
batch_window_size: int | None = None,
|
| 1885 |
+
max_tokens_per_batch: int | None = None,
|
| 1886 |
+
output: str | os.PathLike[str] | None = None,
|
| 1887 |
+
format: str = "safetensors",
|
| 1888 |
+
resume: bool = True,
|
| 1889 |
+
shard_size: int = 2 * 1024**3,
|
| 1890 |
+
model_state_fingerprint: str | None = None,
|
| 1891 |
+
) -> EmbeddingResult:
|
| 1892 |
+
"""Embed an ordered sequence dataset with optional sampled MSA context.
|
| 1893 |
+
|
| 1894 |
+
Unlike the legacy dictionary return, the result preserves duplicate
|
| 1895 |
+
sequences and input order. ``output`` uses the same transactional,
|
| 1896 |
+
resumable SQLite or safetensors persistence as :meth:`embed_dataset`.
|
| 1897 |
+
``max_len`` counts biological residues.
|
| 1898 |
+
"""
|
| 1899 |
+
|
| 1900 |
+
if not sequences:
|
| 1901 |
+
raise ValueError("sequences must contain at least one protein sequence.")
|
| 1902 |
+
if any(not isinstance(sequence, str) or not sequence for sequence in sequences):
|
| 1903 |
+
raise ValueError("sequences must contain non-empty strings.")
|
| 1904 |
+
if max_len <= 0:
|
| 1905 |
+
raise ValueError("max_len must be positive.")
|
| 1906 |
+
if msa_lookup is None:
|
| 1907 |
+
if msa_dir is not None:
|
| 1908 |
+
msa_lookup = load_msa_dir(msa_dir)
|
| 1909 |
+
elif msa_hf_path is not None:
|
| 1910 |
+
msa_lookup = load_msa_from_hf(msa_hf_path)
|
| 1911 |
+
else:
|
| 1912 |
+
msa_lookup = {}
|
| 1913 |
+
|
| 1914 |
+
truncated_sequences = [sequence[:max_len] for sequence in sequences]
|
| 1915 |
+
unique_seqs = sorted(set(truncated_sequences), key=lambda value: (-len(value), value))
|
| 1916 |
+
context_map: dict[str, str | None] = {}
|
| 1917 |
+
spec = ContextSpecification(
|
| 1918 |
+
max_num_samples=511,
|
| 1919 |
+
max_token_length=embed_max_tokens,
|
| 1920 |
+
max_query_similarity=embed_similarity,
|
| 1921 |
+
min_query_similarity=min_query_similarity,
|
| 1922 |
+
)
|
| 1923 |
+
for seq in unique_seqs:
|
| 1924 |
+
a3m_path = get_msa_for_sequence(seq, msa_lookup)
|
| 1925 |
+
if a3m_path is None:
|
| 1926 |
+
context_map[seq] = None
|
| 1927 |
+
continue
|
| 1928 |
+
contexts, _ = sample_multiple_contexts(
|
| 1929 |
+
msa_path=a3m_path,
|
| 1930 |
+
context_specifications=[spec],
|
| 1931 |
+
seed=seed,
|
| 1932 |
+
)
|
| 1933 |
+
context_map[seq] = contexts[0] if contexts else None
|
| 1934 |
+
|
| 1935 |
+
context_digest = hashlib.sha256()
|
| 1936 |
+
for sequence in unique_seqs:
|
| 1937 |
+
for value in (sequence, context_map[sequence] or ""):
|
| 1938 |
+
encoded = value.encode("utf-8")
|
| 1939 |
+
context_digest.update(len(encoded).to_bytes(8, "big"))
|
| 1940 |
+
context_digest.update(encoded)
|
| 1941 |
+
|
| 1942 |
+
def embed_msa_batch(batch_sequences: list[str]) -> EmbeddingBatch:
|
| 1943 |
+
grouped_positions: dict[str | None, list[int]] = defaultdict(list)
|
| 1944 |
+
for position, sequence in enumerate(batch_sequences):
|
| 1945 |
+
grouped_positions[context_map[sequence]].append(position)
|
| 1946 |
+
hidden_by_position: list[torch.Tensor | None] = [None] * len(batch_sequences)
|
| 1947 |
+
for context, positions in grouped_positions.items():
|
| 1948 |
+
context_sequences = [batch_sequences[position] for position in positions]
|
| 1949 |
+
hidden_states = _forward_for_embedding(
|
| 1950 |
+
model=self,
|
| 1951 |
+
sequences=context_sequences,
|
| 1952 |
+
context=context,
|
| 1953 |
+
max_batch_tokens=max_batch_tokens,
|
| 1954 |
+
progress=progress,
|
| 1955 |
+
)
|
| 1956 |
+
for position, hidden in zip(positions, hidden_states, strict=True):
|
| 1957 |
+
hidden_by_position[position] = hidden
|
| 1958 |
+
resolved = [hidden for hidden in hidden_by_position if hidden is not None]
|
| 1959 |
+
if len(resolved) != len(batch_sequences):
|
| 1960 |
+
raise RuntimeError("E1 MSA embedding did not return every requested sequence.")
|
| 1961 |
+
max_residues = max(hidden.shape[0] for hidden in resolved)
|
| 1962 |
+
hidden_size = resolved[0].shape[-1]
|
| 1963 |
+
X = resolved[0].new_zeros((len(resolved), max_residues, hidden_size))
|
| 1964 |
+
residue_mask = torch.zeros(
|
| 1965 |
+
(len(resolved), max_residues),
|
| 1966 |
+
dtype=torch.bool,
|
| 1967 |
+
device=X.device,
|
| 1968 |
+
)
|
| 1969 |
+
for position, hidden in enumerate(resolved):
|
| 1970 |
+
residue_count = hidden.shape[0]
|
| 1971 |
+
X[position, :residue_count] = hidden
|
| 1972 |
+
residue_mask[position, :residue_count] = True
|
| 1973 |
+
return EmbeddingBatch(X=X, residue_mask=residue_mask)
|
| 1974 |
+
|
| 1975 |
+
resolved_pooling: str | list[str] | None = (
|
| 1976 |
+
None if matrix_embed else pooling_types if pooling_types is not None else pooling
|
| 1977 |
+
)
|
| 1978 |
+
adapter_identity = {
|
| 1979 |
+
"kind": "e1-msa-v1",
|
| 1980 |
+
"sampling_source_revision": E1_MSA_SAMPLING_SOURCE_REVISION,
|
| 1981 |
+
"context_sha256": context_digest.hexdigest(),
|
| 1982 |
+
"context_count": sum(context is not None for context in context_map.values()),
|
| 1983 |
+
"seed": seed,
|
| 1984 |
+
"embed_max_tokens": embed_max_tokens,
|
| 1985 |
+
"embed_similarity": embed_similarity,
|
| 1986 |
+
"min_query_similarity": min_query_similarity,
|
| 1987 |
+
"max_batch_tokens": max_batch_tokens,
|
| 1988 |
+
}
|
| 1989 |
+
return embed_dataset(
|
| 1990 |
+
self,
|
| 1991 |
+
[(str(position), sequence) for position, sequence in enumerate(sequences)],
|
| 1992 |
+
batch_size=batch_size,
|
| 1993 |
+
pooling=resolved_pooling,
|
| 1994 |
+
full_embeddings=matrix_embed,
|
| 1995 |
+
output=output,
|
| 1996 |
+
format=format,
|
| 1997 |
+
resume=resume,
|
| 1998 |
+
max_length=max_len,
|
| 1999 |
+
truncate=True,
|
| 2000 |
+
dtype=embed_dtype,
|
| 2001 |
+
shard_size=shard_size,
|
| 2002 |
+
model_state_fingerprint=model_state_fingerprint,
|
| 2003 |
+
batch_window_size=batch_window_size,
|
| 2004 |
+
max_tokens_per_batch=max_tokens_per_batch,
|
| 2005 |
+
_embedding_batch_fn=embed_msa_batch,
|
| 2006 |
+
_embedding_batch_identity=adapter_identity,
|
| 2007 |
+
_allowed_unsupported_pooling=("cls",),
|
| 2008 |
+
)
|
| 2009 |
+
|
| 2010 |
+
def forward(
|
| 2011 |
+
self,
|
| 2012 |
+
input_ids: torch.LongTensor | None = None,
|
| 2013 |
+
within_seq_position_ids: torch.LongTensor | None = None,
|
| 2014 |
+
global_position_ids: torch.LongTensor | None = None,
|
| 2015 |
+
sequence_ids: torch.LongTensor | None = None,
|
| 2016 |
+
inputs_embeds: torch.FloatTensor | None = None,
|
| 2017 |
+
labels: torch.LongTensor | None = None,
|
| 2018 |
+
past_key_values: DynamicCache | None = None,
|
| 2019 |
+
use_cache: bool | None = None,
|
| 2020 |
+
output_attentions: bool | None = None,
|
| 2021 |
+
output_hidden_states: bool | None = None,
|
| 2022 |
+
output_s_max: bool = False,
|
| 2023 |
+
return_dict: bool | None = None,
|
| 2024 |
+
) -> E1MaskedLMOutputWithPast | tuple[Any, ...]:
|
| 2025 |
+
"""Return hidden states and masked-token logits for E1 inputs.
|
| 2026 |
+
|
| 2027 |
+
Token, position, sequence, and label tensors have shape (b, l).
|
| 2028 |
+
Callers may instead provide precomputed H with shape (b, l, d).
|
| 2029 |
+
"""
|
| 2030 |
+
use_cache = (
|
| 2031 |
+
use_cache if use_cache is not None else bool(getattr(self.config, "use_cache", False))
|
| 2032 |
+
)
|
| 2033 |
+
output_attentions = (
|
| 2034 |
+
output_attentions if output_attentions is not None else self.config.output_attentions
|
| 2035 |
+
)
|
| 2036 |
+
output_hidden_states = (
|
| 2037 |
+
output_hidden_states
|
| 2038 |
+
if output_hidden_states is not None
|
| 2039 |
+
else self.config.output_hidden_states
|
| 2040 |
+
)
|
| 2041 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 2042 |
+
outputs: E1ModelOutputWithPast = self.model(
|
| 2043 |
+
input_ids=input_ids,
|
| 2044 |
+
within_seq_position_ids=within_seq_position_ids,
|
| 2045 |
+
global_position_ids=global_position_ids,
|
| 2046 |
+
sequence_ids=sequence_ids,
|
| 2047 |
+
inputs_embeds=inputs_embeds,
|
| 2048 |
+
past_key_values=past_key_values,
|
| 2049 |
+
use_cache=use_cache,
|
| 2050 |
+
output_attentions=output_attentions,
|
| 2051 |
+
output_hidden_states=output_hidden_states,
|
| 2052 |
+
output_s_max=output_s_max,
|
| 2053 |
+
return_dict=True,
|
| 2054 |
+
)
|
| 2055 |
+
|
| 2056 |
+
last_hidden_state = outputs.last_hidden_state
|
| 2057 |
+
loss = None
|
| 2058 |
+
|
| 2059 |
+
mlm_logits = self.mlm_head(last_hidden_state).float()
|
| 2060 |
+
mlm_loss = None
|
| 2061 |
+
if labels is not None:
|
| 2062 |
+
mlm_logits_flat = mlm_logits.contiguous().view(-1, self.config.vocab_size)
|
| 2063 |
+
mlm_labels_flat = labels.to(mlm_logits_flat.device).contiguous().view(-1)
|
| 2064 |
+
mlm_loss = F.cross_entropy(
|
| 2065 |
+
mlm_logits_flat,
|
| 2066 |
+
mlm_labels_flat,
|
| 2067 |
+
ignore_index=-100,
|
| 2068 |
+
reduction="none",
|
| 2069 |
+
)
|
| 2070 |
+
mask = mlm_labels_flat.ne(-100) & mlm_labels_flat.ne(self.model.padding_idx)
|
| 2071 |
+
n_mlm = mask.sum().clamp_min(1)
|
| 2072 |
+
mlm_loss = (mlm_loss * mask.to(mlm_loss)).sum() / n_mlm
|
| 2073 |
+
loss = 0.0
|
| 2074 |
+
loss += mlm_loss
|
| 2075 |
+
|
| 2076 |
+
result = E1MaskedLMOutputWithPast(
|
| 2077 |
+
loss=loss,
|
| 2078 |
+
logits=mlm_logits,
|
| 2079 |
+
hidden_states=outputs.hidden_states,
|
| 2080 |
+
attentions=outputs.attentions,
|
| 2081 |
+
mlm_loss=mlm_loss,
|
| 2082 |
+
last_hidden_state=last_hidden_state,
|
| 2083 |
+
past_key_values=outputs.past_key_values,
|
| 2084 |
+
s_max=outputs.s_max,
|
| 2085 |
+
)
|
| 2086 |
+
if not return_dict:
|
| 2087 |
+
return result.to_tuple()
|
| 2088 |
+
return result
|
| 2089 |
+
|
| 2090 |
+
|
| 2091 |
+
class E1ForSequenceClassification(E1PreTrainedModel, EmbeddingMixin):
|
| 2092 |
+
config: E1Config
|
| 2093 |
+
config_class = E1Config
|
| 2094 |
+
|
| 2095 |
+
def __init__(self, config: E1Config, **kwargs):
|
| 2096 |
+
pooling_types = kwargs.pop("pooling_types", None)
|
| 2097 |
+
if pooling_types is None:
|
| 2098 |
+
pooling_types = ["mean", "var"]
|
| 2099 |
+
elif not isinstance(pooling_types, list):
|
| 2100 |
+
raise TypeError("pooling_types must be a non-empty list of pooling names")
|
| 2101 |
+
elif not pooling_types or not all(isinstance(name, str) for name in pooling_types):
|
| 2102 |
+
raise ValueError("pooling_types must be a non-empty list of pooling names")
|
| 2103 |
+
|
| 2104 |
+
E1PreTrainedModel.__init__(self, config, **kwargs)
|
| 2105 |
+
self.model: FAST_E1_ENCODER = FAST_E1_ENCODER(config, **kwargs)
|
| 2106 |
+
self.vocab_size = config.vocab_size
|
| 2107 |
+
self.num_labels = config.num_labels
|
| 2108 |
+
self.pooler = Pooler(pooling_types)
|
| 2109 |
+
self.classifier = nn.Sequential(
|
| 2110 |
+
nn.Linear(config.hidden_size * len(pooling_types), config.hidden_size * 4),
|
| 2111 |
+
nn.GELU(),
|
| 2112 |
+
nn.LayerNorm(config.hidden_size * 4),
|
| 2113 |
+
nn.Linear(config.hidden_size * 4, config.num_labels),
|
| 2114 |
+
)
|
| 2115 |
+
self.mse = nn.MSELoss()
|
| 2116 |
+
self.ce = nn.CrossEntropyLoss()
|
| 2117 |
+
self.bce = nn.BCEWithLogitsLoss()
|
| 2118 |
+
self.gradient_checkpointing = config.gradient_checkpointing
|
| 2119 |
+
self.post_init()
|
| 2120 |
+
|
| 2121 |
+
@property
|
| 2122 |
+
def device_mesh(self) -> torch.distributed.device_mesh.DeviceMesh:
|
| 2123 |
+
return self.model.device_mesh
|
| 2124 |
+
|
| 2125 |
+
def get_input_embeddings(self) -> nn.Embedding:
|
| 2126 |
+
return self.model.get_input_embeddings()
|
| 2127 |
+
|
| 2128 |
+
def set_input_embeddings(self, value: nn.Embedding) -> None:
|
| 2129 |
+
self.model.set_input_embeddings(value)
|
| 2130 |
+
|
| 2131 |
+
def _embed(
|
| 2132 |
+
self, sequences: list[str], return_attention_mask: bool = False, **kwargs
|
| 2133 |
+
) -> torch.Tensor:
|
| 2134 |
+
return self.model._embed(sequences, return_attention_mask=return_attention_mask, **kwargs)
|
| 2135 |
+
|
| 2136 |
+
def forward(
|
| 2137 |
+
self,
|
| 2138 |
+
input_ids: torch.LongTensor | None = None,
|
| 2139 |
+
within_seq_position_ids: torch.LongTensor | None = None,
|
| 2140 |
+
global_position_ids: torch.LongTensor | None = None,
|
| 2141 |
+
sequence_ids: torch.LongTensor | None = None,
|
| 2142 |
+
inputs_embeds: torch.FloatTensor | None = None,
|
| 2143 |
+
labels: torch.LongTensor | None = None,
|
| 2144 |
+
past_key_values: DynamicCache | None = None,
|
| 2145 |
+
use_cache: bool | None = None,
|
| 2146 |
+
output_attentions: bool | None = None,
|
| 2147 |
+
output_hidden_states: bool | None = None,
|
| 2148 |
+
output_s_max: bool = False,
|
| 2149 |
+
return_dict: bool | None = None,
|
| 2150 |
+
) -> E1ClassificationOutputWithPast | tuple[Any, ...]:
|
| 2151 |
+
use_cache = (
|
| 2152 |
+
use_cache if use_cache is not None else bool(getattr(self.config, "use_cache", False))
|
| 2153 |
+
)
|
| 2154 |
+
output_attentions = (
|
| 2155 |
+
output_attentions if output_attentions is not None else self.config.output_attentions
|
| 2156 |
+
)
|
| 2157 |
+
output_hidden_states = (
|
| 2158 |
+
output_hidden_states
|
| 2159 |
+
if output_hidden_states is not None
|
| 2160 |
+
else self.config.output_hidden_states
|
| 2161 |
+
)
|
| 2162 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 2163 |
+
outputs: E1ModelOutputWithPast = self.model(
|
| 2164 |
+
input_ids=input_ids,
|
| 2165 |
+
within_seq_position_ids=within_seq_position_ids,
|
| 2166 |
+
global_position_ids=global_position_ids,
|
| 2167 |
+
sequence_ids=sequence_ids,
|
| 2168 |
+
inputs_embeds=inputs_embeds,
|
| 2169 |
+
past_key_values=past_key_values,
|
| 2170 |
+
use_cache=use_cache,
|
| 2171 |
+
output_attentions=output_attentions,
|
| 2172 |
+
output_hidden_states=output_hidden_states,
|
| 2173 |
+
output_s_max=output_s_max,
|
| 2174 |
+
return_dict=True,
|
| 2175 |
+
)
|
| 2176 |
+
|
| 2177 |
+
attention_mask = (
|
| 2178 |
+
(sequence_ids != -1).long()
|
| 2179 |
+
if sequence_ids is not None
|
| 2180 |
+
else torch.ones(
|
| 2181 |
+
outputs.last_hidden_state.shape[:2],
|
| 2182 |
+
device=outputs.last_hidden_state.device,
|
| 2183 |
+
dtype=torch.long,
|
| 2184 |
+
)
|
| 2185 |
+
)
|
| 2186 |
+
x = outputs.last_hidden_state
|
| 2187 |
+
features = self.pooler(x, attention_mask)
|
| 2188 |
+
logits = self.classifier(features)
|
| 2189 |
+
loss = None
|
| 2190 |
+
if labels is not None:
|
| 2191 |
+
labels = labels.to(logits.device)
|
| 2192 |
+
if self.config.problem_type is None:
|
| 2193 |
+
if self.num_labels == 1:
|
| 2194 |
+
self.config.problem_type = "regression"
|
| 2195 |
+
elif self.num_labels > 1 and (
|
| 2196 |
+
labels.dtype == torch.long or labels.dtype == torch.int
|
| 2197 |
+
):
|
| 2198 |
+
self.config.problem_type = "single_label_classification"
|
| 2199 |
+
else:
|
| 2200 |
+
self.config.problem_type = "multi_label_classification"
|
| 2201 |
+
|
| 2202 |
+
if self.config.problem_type == "regression":
|
| 2203 |
+
if self.num_labels == 1:
|
| 2204 |
+
loss = self.mse(logits.flatten(), labels.flatten())
|
| 2205 |
+
else:
|
| 2206 |
+
loss = self.mse(logits, labels)
|
| 2207 |
+
elif self.config.problem_type == "single_label_classification":
|
| 2208 |
+
loss = self.ce(logits.view(-1, self.num_labels), labels.view(-1))
|
| 2209 |
+
elif self.config.problem_type == "multi_label_classification":
|
| 2210 |
+
loss = self.bce(logits, labels)
|
| 2211 |
+
|
| 2212 |
+
result = E1ClassificationOutputWithPast(
|
| 2213 |
+
loss=loss,
|
| 2214 |
+
logits=logits,
|
| 2215 |
+
past_key_values=outputs.past_key_values,
|
| 2216 |
+
hidden_states=outputs.hidden_states,
|
| 2217 |
+
attentions=outputs.attentions,
|
| 2218 |
+
last_hidden_state=x,
|
| 2219 |
+
s_max=outputs.s_max,
|
| 2220 |
+
)
|
| 2221 |
+
if not return_dict:
|
| 2222 |
+
return result.to_tuple()
|
| 2223 |
+
return result
|
| 2224 |
+
|
| 2225 |
+
|
| 2226 |
+
class E1ForTokenClassification(E1PreTrainedModel, EmbeddingMixin):
|
| 2227 |
+
config: E1Config
|
| 2228 |
+
config_class = E1Config
|
| 2229 |
+
|
| 2230 |
+
def __init__(self, config: E1Config, **kwargs):
|
| 2231 |
+
E1PreTrainedModel.__init__(self, config, **kwargs)
|
| 2232 |
+
self.model: FAST_E1_ENCODER = FAST_E1_ENCODER(config, **kwargs)
|
| 2233 |
+
self.vocab_size = config.vocab_size
|
| 2234 |
+
self.num_labels = config.num_labels
|
| 2235 |
+
self.classifier = nn.Sequential(
|
| 2236 |
+
nn.Linear(config.hidden_size, config.hidden_size * 4),
|
| 2237 |
+
nn.GELU(),
|
| 2238 |
+
nn.LayerNorm(config.hidden_size * 4),
|
| 2239 |
+
nn.Linear(config.hidden_size * 4, config.num_labels),
|
| 2240 |
+
)
|
| 2241 |
+
self.loss_fct = nn.CrossEntropyLoss()
|
| 2242 |
+
self.gradient_checkpointing = config.gradient_checkpointing
|
| 2243 |
+
self.post_init()
|
| 2244 |
+
|
| 2245 |
+
@property
|
| 2246 |
+
def device_mesh(self) -> torch.distributed.device_mesh.DeviceMesh:
|
| 2247 |
+
return self.model.device_mesh
|
| 2248 |
+
|
| 2249 |
+
def get_input_embeddings(self) -> nn.Embedding:
|
| 2250 |
+
return self.model.get_input_embeddings()
|
| 2251 |
+
|
| 2252 |
+
def set_input_embeddings(self, value: nn.Embedding) -> None:
|
| 2253 |
+
self.model.set_input_embeddings(value)
|
| 2254 |
+
|
| 2255 |
+
def _embed(
|
| 2256 |
+
self, sequences: list[str], return_attention_mask: bool = False, **kwargs
|
| 2257 |
+
) -> torch.Tensor:
|
| 2258 |
+
return self.model._embed(sequences, return_attention_mask=return_attention_mask, **kwargs)
|
| 2259 |
+
|
| 2260 |
+
def forward(
|
| 2261 |
+
self,
|
| 2262 |
+
input_ids: torch.LongTensor | None = None,
|
| 2263 |
+
within_seq_position_ids: torch.LongTensor | None = None,
|
| 2264 |
+
global_position_ids: torch.LongTensor | None = None,
|
| 2265 |
+
sequence_ids: torch.LongTensor | None = None,
|
| 2266 |
+
inputs_embeds: torch.FloatTensor | None = None,
|
| 2267 |
+
labels: torch.LongTensor | None = None,
|
| 2268 |
+
past_key_values: DynamicCache | None = None,
|
| 2269 |
+
use_cache: bool | None = None,
|
| 2270 |
+
output_attentions: bool | None = None,
|
| 2271 |
+
output_hidden_states: bool | None = None,
|
| 2272 |
+
output_s_max: bool = False,
|
| 2273 |
+
return_dict: bool | None = None,
|
| 2274 |
+
) -> E1TokenClassificationOutputWithPast | tuple[Any, ...]:
|
| 2275 |
+
use_cache = (
|
| 2276 |
+
use_cache if use_cache is not None else bool(getattr(self.config, "use_cache", False))
|
| 2277 |
+
)
|
| 2278 |
+
output_attentions = (
|
| 2279 |
+
output_attentions if output_attentions is not None else self.config.output_attentions
|
| 2280 |
+
)
|
| 2281 |
+
output_hidden_states = (
|
| 2282 |
+
output_hidden_states
|
| 2283 |
+
if output_hidden_states is not None
|
| 2284 |
+
else self.config.output_hidden_states
|
| 2285 |
+
)
|
| 2286 |
+
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
|
| 2287 |
+
outputs: E1ModelOutputWithPast = self.model(
|
| 2288 |
+
input_ids=input_ids,
|
| 2289 |
+
within_seq_position_ids=within_seq_position_ids,
|
| 2290 |
+
global_position_ids=global_position_ids,
|
| 2291 |
+
sequence_ids=sequence_ids,
|
| 2292 |
+
inputs_embeds=inputs_embeds,
|
| 2293 |
+
past_key_values=past_key_values,
|
| 2294 |
+
use_cache=use_cache,
|
| 2295 |
+
output_attentions=output_attentions,
|
| 2296 |
+
output_hidden_states=output_hidden_states,
|
| 2297 |
+
output_s_max=output_s_max,
|
| 2298 |
+
return_dict=True,
|
| 2299 |
+
)
|
| 2300 |
+
|
| 2301 |
+
x = outputs.last_hidden_state
|
| 2302 |
+
logits = self.classifier(x)
|
| 2303 |
+
loss = None
|
| 2304 |
+
if labels is not None:
|
| 2305 |
+
labels = labels.to(logits.device)
|
| 2306 |
+
loss = self.loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
|
| 2307 |
+
|
| 2308 |
+
result = E1TokenClassificationOutputWithPast(
|
| 2309 |
+
loss=loss,
|
| 2310 |
+
logits=logits,
|
| 2311 |
+
hidden_states=outputs.hidden_states,
|
| 2312 |
+
attentions=outputs.attentions,
|
| 2313 |
+
last_hidden_state=x,
|
| 2314 |
+
past_key_values=outputs.past_key_values,
|
| 2315 |
+
s_max=outputs.s_max,
|
| 2316 |
+
)
|
| 2317 |
+
if not return_dict:
|
| 2318 |
+
return result.to_tuple()
|
| 2319 |
+
return result
|
fastplms/models/e1/preparation.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tokenizer loading and raw-sequence batch preparation for E1."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import itertools
|
| 6 |
+
import os
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from tokenizers import Tokenizer
|
| 11 |
+
from torch.nn.utils.rnn import pad_sequence
|
| 12 |
+
|
| 13 |
+
PAD_TOKEN_ID = 0
|
| 14 |
+
BOS_TOKEN_ID = 1
|
| 15 |
+
EOS_TOKEN_ID = 2
|
| 16 |
+
E1_VOCAB_SIZE = 34
|
| 17 |
+
E1_TOKENIZER_REPO_ID = "Synthyra/Profluent-E1-150M"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _load_tokenizer_file(fname: str) -> Tokenizer:
|
| 21 |
+
tokenizer: Tokenizer = Tokenizer.from_file(fname)
|
| 22 |
+
padding = tokenizer.padding
|
| 23 |
+
actual_pad_id = None if padding is None else padding.get("pad_id")
|
| 24 |
+
if actual_pad_id != PAD_TOKEN_ID:
|
| 25 |
+
raise ValueError(
|
| 26 |
+
f"Padding token id must be {PAD_TOKEN_ID}, but got {actual_pad_id}"
|
| 27 |
+
)
|
| 28 |
+
return tokenizer
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def get_tokenizer(
|
| 32 |
+
pretrained_model_name_or_path: str | os.PathLike | None = None,
|
| 33 |
+
*,
|
| 34 |
+
local_files_only: bool = False,
|
| 35 |
+
cache_dir: str | os.PathLike | None = None,
|
| 36 |
+
revision: str | None = None,
|
| 37 |
+
token: str | bool | None = None,
|
| 38 |
+
) -> Tokenizer:
|
| 39 |
+
source_path = None
|
| 40 |
+
checked_local_source = False
|
| 41 |
+
if pretrained_model_name_or_path is not None:
|
| 42 |
+
source_path = os.fspath(pretrained_model_name_or_path)
|
| 43 |
+
if os.path.isdir(source_path):
|
| 44 |
+
checked_local_source = True
|
| 45 |
+
fname = os.path.join(source_path, "tokenizer.json")
|
| 46 |
+
if os.path.isfile(fname):
|
| 47 |
+
return _load_tokenizer_file(fname)
|
| 48 |
+
|
| 49 |
+
fname = os.path.join(os.path.dirname(__file__), "tokenizer.json")
|
| 50 |
+
if os.path.isfile(fname):
|
| 51 |
+
return _load_tokenizer_file(fname)
|
| 52 |
+
|
| 53 |
+
if local_files_only and checked_local_source:
|
| 54 |
+
raise FileNotFoundError(
|
| 55 |
+
f"E1 tokenizer.json was not found in {source_path} or next to {__file__}."
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
from huggingface_hub import hf_hub_download
|
| 59 |
+
|
| 60 |
+
repo_id = E1_TOKENIZER_REPO_ID
|
| 61 |
+
if source_path is not None and not checked_local_source:
|
| 62 |
+
repo_id = source_path
|
| 63 |
+
try:
|
| 64 |
+
fname = hf_hub_download(
|
| 65 |
+
repo_id=repo_id,
|
| 66 |
+
filename="tokenizer.json",
|
| 67 |
+
cache_dir=os.fspath(cache_dir) if cache_dir is not None else None,
|
| 68 |
+
revision=revision,
|
| 69 |
+
token=token,
|
| 70 |
+
local_files_only=local_files_only,
|
| 71 |
+
)
|
| 72 |
+
except Exception as error:
|
| 73 |
+
raise FileNotFoundError(
|
| 74 |
+
f"E1 tokenizer.json was not found locally and could not be loaded from {repo_id}."
|
| 75 |
+
) from error
|
| 76 |
+
return _load_tokenizer_file(fname)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@dataclass
|
| 80 |
+
class DataPrepConfig:
|
| 81 |
+
max_num_sequences: int = 512
|
| 82 |
+
max_num_positions_within_seq: int = 8192
|
| 83 |
+
remove_X_tokens: bool = False
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def get_context(sequence: str) -> str | None:
|
| 87 |
+
if "," in sequence:
|
| 88 |
+
return sequence.rsplit(",", 1)[0]
|
| 89 |
+
return None
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class E1BatchPreparer:
|
| 93 |
+
def __init__(
|
| 94 |
+
self,
|
| 95 |
+
data_prep_config: DataPrepConfig | None = None,
|
| 96 |
+
tokenizer: Tokenizer | None = None,
|
| 97 |
+
tokenizer_source: str | os.PathLike | None = None,
|
| 98 |
+
local_files_only: bool = False,
|
| 99 |
+
cache_dir: str | os.PathLike | None = None,
|
| 100 |
+
revision: str | None = None,
|
| 101 |
+
token: str | bool | None = None,
|
| 102 |
+
preserve_context_labels: bool = False,
|
| 103 |
+
):
|
| 104 |
+
self.tokenizer = tokenizer or get_tokenizer(
|
| 105 |
+
tokenizer_source,
|
| 106 |
+
local_files_only=local_files_only,
|
| 107 |
+
cache_dir=cache_dir,
|
| 108 |
+
revision=revision,
|
| 109 |
+
token=token,
|
| 110 |
+
)
|
| 111 |
+
self.data_prep_config = data_prep_config or DataPrepConfig()
|
| 112 |
+
self.pad_token_id = self.tokenizer.token_to_id("<pad>")
|
| 113 |
+
self.preserve_context_labels = preserve_context_labels
|
| 114 |
+
self.boundary_token_ids = torch.tensor(
|
| 115 |
+
[self.tokenizer.token_to_id(token) for token in ["<bos>", "<eos>", "1", "2", "<pad>"]]
|
| 116 |
+
).long()
|
| 117 |
+
self.mask_token = "?" # nosec
|
| 118 |
+
self.mask_token_id = self.tokenizer.token_to_id(self.mask_token)
|
| 119 |
+
self.X_token_id = self.tokenizer.token_to_id("X")
|
| 120 |
+
self.vocab = self.tokenizer.get_vocab()
|
| 121 |
+
|
| 122 |
+
def get_batch_kwargs( # type: ignore[override]
|
| 123 |
+
self,
|
| 124 |
+
sequences: list[str],
|
| 125 |
+
device: torch.device | None = None,
|
| 126 |
+
non_blocking: bool = False,
|
| 127 |
+
) -> dict[str, torch.Tensor | list[str] | list[int]]:
|
| 128 |
+
device = torch.device("cpu") if device is None else device
|
| 129 |
+
sequence_encodings = [self.prepare_multiseq(sequence) for sequence in sequences]
|
| 130 |
+
return self.pad_encodings(sequence_encodings, device, non_blocking)
|
| 131 |
+
|
| 132 |
+
def pad_encodings(
|
| 133 |
+
self,
|
| 134 |
+
sequence_encodings: list[dict[str, torch.Tensor]],
|
| 135 |
+
device: torch.device | None = None,
|
| 136 |
+
non_blocking: bool = False,
|
| 137 |
+
) -> dict[str, torch.Tensor | list[str] | list[int]]:
|
| 138 |
+
device = torch.device("cpu") if device is None else device
|
| 139 |
+
non_blocking = non_blocking and device.type == "cuda"
|
| 140 |
+
padded_encodings = {}
|
| 141 |
+
# Note: We use -1 as the padding value for sequence and position ids because the 0 value
|
| 142 |
+
# is a valid value for sequence and position ids. -1 is then used to distinguish valid
|
| 143 |
+
# tokens from padding tokens, for example, when doing padding/unpadding for flash attention.
|
| 144 |
+
for key, padding_value in {
|
| 145 |
+
"input_ids": self.pad_token_id,
|
| 146 |
+
"sequence_ids": -1,
|
| 147 |
+
"within_seq_position_ids": -1,
|
| 148 |
+
"global_position_ids": -1,
|
| 149 |
+
"labels": self.pad_token_id,
|
| 150 |
+
}.items():
|
| 151 |
+
padded_encodings[key] = pad_sequence(
|
| 152 |
+
[enc[key] for enc in sequence_encodings],
|
| 153 |
+
batch_first=True,
|
| 154 |
+
padding_value=padding_value,
|
| 155 |
+
).to(device=device, dtype=torch.long, non_blocking=non_blocking)
|
| 156 |
+
|
| 157 |
+
padded_encodings["context"] = [enc["context"] for enc in sequence_encodings]
|
| 158 |
+
padded_encodings["context_len"] = [enc["context_len"] for enc in sequence_encodings]
|
| 159 |
+
|
| 160 |
+
return padded_encodings
|
| 161 |
+
|
| 162 |
+
def prepare_multiseq(self, sequence: str) -> dict[str, torch.Tensor | str | int]:
|
| 163 |
+
sequences = sequence.split(",")
|
| 164 |
+
if len(sequences) > self.data_prep_config.max_num_sequences:
|
| 165 |
+
raise ValueError(
|
| 166 |
+
f"Number of sequences {len(sequences)} exceeds max number of sequences "
|
| 167 |
+
f"{self.data_prep_config.max_num_sequences} in the provided multi-sequence "
|
| 168 |
+
"instance. Please remove some homologous sequences before trying again."
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
encodings = tuple(self.prepare_singleseq(item) for item in sequences)
|
| 172 |
+
token_counts = torch.tensor(
|
| 173 |
+
[encoding["input_ids"].numel() for encoding in encodings],
|
| 174 |
+
dtype=torch.long,
|
| 175 |
+
)
|
| 176 |
+
input_ids = torch.cat(tuple(encoding["input_ids"] for encoding in encodings))
|
| 177 |
+
labels = torch.cat(tuple(encoding["labels"] for encoding in encodings))
|
| 178 |
+
positions = tuple(encoding["position_ids"] for encoding in encodings)
|
| 179 |
+
within_seq_position_ids = torch.cat(positions)
|
| 180 |
+
|
| 181 |
+
# Offsets preserve gaps left by optional X-token removal.
|
| 182 |
+
position_spans = torch.tensor(
|
| 183 |
+
[int(position_ids[-1].item()) + 1 for position_ids in positions],
|
| 184 |
+
dtype=torch.long,
|
| 185 |
+
)
|
| 186 |
+
position_offsets = torch.cat(
|
| 187 |
+
(torch.zeros(1, dtype=torch.long), position_spans[:-1]),
|
| 188 |
+
).cumsum(dim=0)
|
| 189 |
+
global_position_ids = torch.cat(
|
| 190 |
+
tuple(
|
| 191 |
+
position_ids + offset
|
| 192 |
+
for position_ids, offset in zip(positions, position_offsets, strict=True)
|
| 193 |
+
)
|
| 194 |
+
)
|
| 195 |
+
sequence_ids = torch.arange(len(encodings), dtype=torch.long).repeat_interleave(
|
| 196 |
+
token_counts
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
context_len = int(token_counts[:-1].sum().item())
|
| 200 |
+
context = self.tokenizer.decode(input_ids[:context_len].tolist(), skip_special_tokens=False)
|
| 201 |
+
if not self.preserve_context_labels:
|
| 202 |
+
labels[:context_len] = self.pad_token_id
|
| 203 |
+
|
| 204 |
+
aligned_tensors = (
|
| 205 |
+
sequence_ids,
|
| 206 |
+
within_seq_position_ids,
|
| 207 |
+
global_position_ids,
|
| 208 |
+
labels,
|
| 209 |
+
)
|
| 210 |
+
if any(tensor.shape != input_ids.shape for tensor in aligned_tensors):
|
| 211 |
+
raise AssertionError(
|
| 212 |
+
"Input ids, sequence ids, within seq position ids, global position ids, "
|
| 213 |
+
"and labels must have the same shape"
|
| 214 |
+
)
|
| 215 |
+
if input_ids.numel() < context_len:
|
| 216 |
+
raise AssertionError(
|
| 217 |
+
"Input ids must have at least as many tokens as the context length"
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
return {
|
| 221 |
+
"input_ids": input_ids,
|
| 222 |
+
"sequence_ids": sequence_ids,
|
| 223 |
+
"within_seq_position_ids": within_seq_position_ids,
|
| 224 |
+
"global_position_ids": global_position_ids,
|
| 225 |
+
"labels": labels,
|
| 226 |
+
"context": context,
|
| 227 |
+
"context_len": context_len,
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
def prepare_singleseq(self, sequence: str) -> dict[str, torch.Tensor]:
|
| 231 |
+
if not self.validate_sequence(sequence):
|
| 232 |
+
raise ValueError(
|
| 233 |
+
f"Invalid sequence: {sequence}; Input sequence should contain "
|
| 234 |
+
"[A-Z] or ? characters only"
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
if len(sequence) > self.data_prep_config.max_num_positions_within_seq:
|
| 238 |
+
raise ValueError(
|
| 239 |
+
f"Sequence length {len(sequence)} exceeds max length "
|
| 240 |
+
f"{self.data_prep_config.max_num_positions_within_seq}"
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
symbols = itertools.chain(("<bos>", "1"), sequence, ("2", "<eos>"))
|
| 244 |
+
tokens = torch.tensor(
|
| 245 |
+
[self.vocab[symbol] for symbol in symbols],
|
| 246 |
+
dtype=torch.long,
|
| 247 |
+
)
|
| 248 |
+
position_ids = torch.arange(tokens.numel(), dtype=torch.long)
|
| 249 |
+
|
| 250 |
+
if self.data_prep_config.remove_X_tokens:
|
| 251 |
+
keep = tokens.ne(self.X_token_id)
|
| 252 |
+
tokens = tokens[keep]
|
| 253 |
+
position_ids = position_ids[keep]
|
| 254 |
+
|
| 255 |
+
return {"input_ids": tokens, "labels": tokens, "position_ids": position_ids}
|
| 256 |
+
|
| 257 |
+
def get_boundary_token_mask(self, tokens: torch.Tensor) -> torch.BoolTensor:
|
| 258 |
+
return torch.isin(tokens, self.boundary_token_ids.to(tokens.device))
|
| 259 |
+
|
| 260 |
+
def get_mask_positions_mask(self, tokens: torch.Tensor) -> torch.BoolTensor:
|
| 261 |
+
return tokens == self.mask_token_id
|
| 262 |
+
|
| 263 |
+
def validate_sequence(self, sequence: str) -> bool:
|
| 264 |
+
if not isinstance(sequence, str):
|
| 265 |
+
raise TypeError("Sequence must be a string.")
|
| 266 |
+
sequence = sequence.replace(self.mask_token, "")
|
| 267 |
+
return sequence.isalpha() and sequence.isupper()
|
fastplms/models/e1/retrieval.py
ADDED
|
@@ -0,0 +1,1783 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FASTA, MSA, context sampling, and homologue-search utilities for E1."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
import itertools
|
| 7 |
+
import json
|
| 8 |
+
import math
|
| 9 |
+
import numbers
|
| 10 |
+
import os
|
| 11 |
+
import platform
|
| 12 |
+
import random
|
| 13 |
+
import re
|
| 14 |
+
import shutil
|
| 15 |
+
import subprocess
|
| 16 |
+
import tarfile
|
| 17 |
+
import tempfile
|
| 18 |
+
import time
|
| 19 |
+
import urllib.error
|
| 20 |
+
import urllib.parse
|
| 21 |
+
import urllib.request
|
| 22 |
+
from collections import defaultdict, namedtuple
|
| 23 |
+
from collections.abc import Iterator, Sequence
|
| 24 |
+
from dataclasses import dataclass
|
| 25 |
+
from datetime import UTC, datetime
|
| 26 |
+
from email.utils import parsedate_to_datetime
|
| 27 |
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
| 28 |
+
from typing import TYPE_CHECKING, Any, TypedDict
|
| 29 |
+
|
| 30 |
+
import numpy as np
|
| 31 |
+
import torch
|
| 32 |
+
from tqdm.auto import tqdm
|
| 33 |
+
from transformers import PreTrainedModel
|
| 34 |
+
from transformers.utils import logging
|
| 35 |
+
|
| 36 |
+
from fastplms.embeddings import Pooler
|
| 37 |
+
|
| 38 |
+
from .cache import KVCache
|
| 39 |
+
from .preparation import DataPrepConfig, E1BatchPreparer, get_context
|
| 40 |
+
|
| 41 |
+
if TYPE_CHECKING:
|
| 42 |
+
from .modeling_e1 import E1MaskedLMOutputWithPast
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _get_logger():
|
| 46 |
+
"""Resolve the Transformers logger only when a retrieval path emits a message."""
|
| 47 |
+
|
| 48 |
+
return logging.get_logger(__name__)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
MMSEQS2_IMAGE_REPOSITORY = "ghcr.io/soedinglab/mmseqs2"
|
| 52 |
+
MMSEQS2_VERSION = "18-8cc5c"
|
| 53 |
+
MMSEQS2_CPU_MANIFEST_DIGEST = (
|
| 54 |
+
"sha256:41b12b0d5f41432fa1b9976123da6e2e06e7fab49a34964f3b54ec038e5845d9"
|
| 55 |
+
)
|
| 56 |
+
MMSEQS2_CPU_ARM64_CHILD_DIGEST = (
|
| 57 |
+
"sha256:8bec048845f8f20749c2e2ad067a27d67eef839d2bb068e9d6e957113e9a7fba"
|
| 58 |
+
)
|
| 59 |
+
DOCKER_IMAGE = (
|
| 60 |
+
f"{MMSEQS2_IMAGE_REPOSITORY}:{MMSEQS2_VERSION}@{MMSEQS2_CPU_MANIFEST_DIGEST}"
|
| 61 |
+
)
|
| 62 |
+
DEFAULT_MMSEQS2_PHASE_TIMEOUT = 1800.0
|
| 63 |
+
COLABFOLD_HOST = "https://api.colabfold.com"
|
| 64 |
+
LOWERCASE_CHARS = b"abcdefghijklmnopqrstuvwxyz"
|
| 65 |
+
DEFAULT_MAX_CONTEXT_TOKENS = [6144, 12288, 24576]
|
| 66 |
+
DEFAULT_SIMILARITY_THRESHOLDS = [1.0, 0.95, 0.9, 0.7, 0.5]
|
| 67 |
+
DEFAULT_EMBED_MAX_TOKENS = 8192
|
| 68 |
+
DEFAULT_EMBED_SIMILARITY = 0.95
|
| 69 |
+
E1_MSA_SAMPLING_SOURCE_REVISION = "bfd2620a602248499f3d2583d85a7ecddf0b6e02"
|
| 70 |
+
|
| 71 |
+
IdSequence = namedtuple("IdSequence", ["id", "sequence"])
|
| 72 |
+
IndexedSequence = tuple[int, str]
|
| 73 |
+
|
| 74 |
+
_SHA256_DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
| 75 |
+
_IMAGE_VERSION_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
| 76 |
+
_SAFE_SEQUENCE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
| 77 |
+
_SAFE_QUERY_SEQUENCE_RE = re.compile(r"^[A-Za-z*.-]+$")
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@dataclass(frozen=True, slots=True)
|
| 81 |
+
class _PinnedImageReference:
|
| 82 |
+
repository: str
|
| 83 |
+
version: str
|
| 84 |
+
digest: str
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
@dataclass(frozen=True, slots=True)
|
| 88 |
+
class _DockerImageIdentity:
|
| 89 |
+
reference: str
|
| 90 |
+
repository: str
|
| 91 |
+
version: str
|
| 92 |
+
manifest_digest: str
|
| 93 |
+
image_id: str
|
| 94 |
+
os: str
|
| 95 |
+
architecture: str
|
| 96 |
+
|
| 97 |
+
def to_dict(self) -> dict[str, str]:
|
| 98 |
+
return {
|
| 99 |
+
"reference": self.reference,
|
| 100 |
+
"repository": self.repository,
|
| 101 |
+
"version": self.version,
|
| 102 |
+
"manifest_digest": self.manifest_digest,
|
| 103 |
+
"image_id": self.image_id,
|
| 104 |
+
"os": self.os,
|
| 105 |
+
"architecture": self.architecture,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _parse_pinned_image_reference(reference: str) -> _PinnedImageReference:
|
| 110 |
+
"""Parse ``repository:version@sha256:digest`` and reject mutable images."""
|
| 111 |
+
|
| 112 |
+
if not isinstance(reference, str) or not reference or any(char.isspace() for char in reference):
|
| 113 |
+
raise ValueError(
|
| 114 |
+
"docker_image must be an immutable repository:version@sha256:digest reference"
|
| 115 |
+
)
|
| 116 |
+
try:
|
| 117 |
+
name_and_version, digest = reference.rsplit("@", maxsplit=1)
|
| 118 |
+
except ValueError as error:
|
| 119 |
+
raise ValueError(
|
| 120 |
+
"docker_image must include an immutable @sha256 digest; mutable tags are rejected"
|
| 121 |
+
) from error
|
| 122 |
+
last_slash = name_and_version.rfind("/")
|
| 123 |
+
last_colon = name_and_version.rfind(":")
|
| 124 |
+
if last_colon <= last_slash:
|
| 125 |
+
raise ValueError("docker_image must include an explicit version tag before its digest")
|
| 126 |
+
repository = name_and_version[:last_colon]
|
| 127 |
+
version = name_and_version[last_colon + 1 :]
|
| 128 |
+
if (
|
| 129 |
+
not repository
|
| 130 |
+
or repository.endswith("/")
|
| 131 |
+
or "@" in repository
|
| 132 |
+
or _IMAGE_VERSION_RE.fullmatch(version) is None
|
| 133 |
+
):
|
| 134 |
+
raise ValueError("docker_image contains an invalid repository or version tag")
|
| 135 |
+
if _SHA256_DIGEST_RE.fullmatch(digest) is None:
|
| 136 |
+
raise ValueError("docker_image must include a lowercase sha256 digest")
|
| 137 |
+
return _PinnedImageReference(repository=repository, version=version, digest=digest)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def _docker_architecture() -> str:
|
| 141 |
+
machine = platform.machine().lower()
|
| 142 |
+
aliases = {
|
| 143 |
+
"aarch64": "arm64",
|
| 144 |
+
"arm64": "arm64",
|
| 145 |
+
"amd64": "amd64",
|
| 146 |
+
"x86_64": "amd64",
|
| 147 |
+
}
|
| 148 |
+
try:
|
| 149 |
+
return aliases[machine]
|
| 150 |
+
except KeyError as error:
|
| 151 |
+
raise RuntimeError(f"Unsupported Docker host architecture: {machine!r}") from error
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def _json_sha256(payload: Any) -> str:
|
| 155 |
+
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
| 156 |
+
return hashlib.sha256(encoded).hexdigest()
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def _file_sha256(path: str) -> str:
|
| 160 |
+
hasher = hashlib.sha256()
|
| 161 |
+
with open(path, "rb") as handle:
|
| 162 |
+
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
| 163 |
+
hasher.update(block)
|
| 164 |
+
return hasher.hexdigest()
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def _sequence_output_dir(output_dir: str, seq_id: str) -> str:
|
| 168 |
+
"""Return the per-sequence directory after enforcing path containment."""
|
| 169 |
+
|
| 170 |
+
if not isinstance(seq_id, str) or _SAFE_SEQUENCE_ID_RE.fullmatch(seq_id) is None:
|
| 171 |
+
raise ValueError(
|
| 172 |
+
"seq_id must use only ASCII letters, digits, dot, underscore, and hyphen"
|
| 173 |
+
)
|
| 174 |
+
if (
|
| 175 |
+
seq_id in {".", ".."}
|
| 176 |
+
or PurePosixPath(seq_id).name != seq_id
|
| 177 |
+
or PureWindowsPath(seq_id).name != seq_id
|
| 178 |
+
or PureWindowsPath(seq_id).is_absolute()
|
| 179 |
+
):
|
| 180 |
+
raise ValueError(f"seq_id must be a single relative filename component: {seq_id!r}")
|
| 181 |
+
|
| 182 |
+
output_root = Path(output_dir).resolve()
|
| 183 |
+
sequence_dir = Path(output_dir) / seq_id
|
| 184 |
+
resolved_sequence_dir = sequence_dir.resolve()
|
| 185 |
+
if resolved_sequence_dir.parent != output_root:
|
| 186 |
+
raise ValueError(f"seq_id resolves outside output_dir: {seq_id!r}")
|
| 187 |
+
return os.fspath(sequence_dir)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
@dataclass
|
| 191 |
+
class ContextSpecification:
|
| 192 |
+
max_num_samples: int = 511
|
| 193 |
+
max_token_length: int = 32768
|
| 194 |
+
max_query_similarity: float = 1.0
|
| 195 |
+
min_query_similarity: float = 0.0
|
| 196 |
+
neighbor_similarity_lower_bound: float = 0.8
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
class E1Prediction(TypedDict, total=False):
|
| 200 |
+
id: str | int
|
| 201 |
+
context_id: str | int | None
|
| 202 |
+
logits: torch.Tensor
|
| 203 |
+
token_embeddings: torch.Tensor
|
| 204 |
+
mean_token_embeddings: torch.Tensor
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def read_fasta_sequences(path: str) -> dict[str, str]:
|
| 208 |
+
sequences: dict[str, str] = {}
|
| 209 |
+
header: str | None = None
|
| 210 |
+
parts: list[str] = []
|
| 211 |
+
with open(path, encoding="utf-8") as handle:
|
| 212 |
+
for raw_line in handle:
|
| 213 |
+
line = raw_line.strip()
|
| 214 |
+
if not line:
|
| 215 |
+
continue
|
| 216 |
+
if line.startswith(">"):
|
| 217 |
+
if header is not None:
|
| 218 |
+
sequences[header] = "".join(parts)
|
| 219 |
+
header = line[1:].strip()
|
| 220 |
+
parts = []
|
| 221 |
+
else:
|
| 222 |
+
if header is None:
|
| 223 |
+
raise ValueError(f"FASTA sequence found before header in {path}")
|
| 224 |
+
parts.append(line)
|
| 225 |
+
if header is not None:
|
| 226 |
+
sequences[header] = "".join(parts)
|
| 227 |
+
return sequences
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def write_fasta_sequences(path: str, sequences: dict[str, str]) -> None:
|
| 231 |
+
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
| 232 |
+
with open(path, "w", encoding="utf-8") as handle:
|
| 233 |
+
for header, sequence in sequences.items():
|
| 234 |
+
handle.write(f">{header}\n{sequence}\n")
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def parse_msa(path: str) -> list[IdSequence]:
|
| 238 |
+
records = read_fasta_sequences(path)
|
| 239 |
+
sequences = []
|
| 240 |
+
for record_id, record_seq in records.items():
|
| 241 |
+
sequence = str(record_seq).replace("\x00", "").replace(".", "-")
|
| 242 |
+
sequences.append(IdSequence(record_id, sequence))
|
| 243 |
+
if not sequences:
|
| 244 |
+
raise ValueError(f"No sequences found in MSA file: {path}")
|
| 245 |
+
return sequences
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def convert_to_tensor(
|
| 249 |
+
sequences: list[IdSequence], device: torch.device | None = None
|
| 250 |
+
) -> torch.ByteTensor:
|
| 251 |
+
if device is None:
|
| 252 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 253 |
+
byte_sequences = [
|
| 254 |
+
sequence.sequence.encode("ascii").translate(None, LOWERCASE_CHARS) for sequence in sequences
|
| 255 |
+
]
|
| 256 |
+
lengths = {len(byte_sequence) for byte_sequence in byte_sequences}
|
| 257 |
+
if len(lengths) != 1:
|
| 258 |
+
raise ValueError(
|
| 259 |
+
"MSA rows must have equal aligned lengths after removing insertions: "
|
| 260 |
+
f"{sorted(lengths)}"
|
| 261 |
+
)
|
| 262 |
+
array = np.vstack(
|
| 263 |
+
[np.frombuffer(byte_sequence, dtype=np.uint8) for byte_sequence in byte_sequences]
|
| 264 |
+
)
|
| 265 |
+
return torch.from_numpy(array).to(device)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def get_num_neighbors(byte_seqs: torch.ByteTensor, sim_threshold: float = 0.8) -> list[int]:
|
| 269 |
+
gap_token_id = np.frombuffer(b"-", np.uint8)[0].item()
|
| 270 |
+
seq_lens = (byte_seqs != gap_token_id).sum(dim=1)
|
| 271 |
+
num_neighbors: list[int] = []
|
| 272 |
+
for i in range(byte_seqs.shape[0]):
|
| 273 |
+
query_non_gaps = byte_seqs[i] != gap_token_id
|
| 274 |
+
seqs_sim = (byte_seqs[:, query_non_gaps] == byte_seqs[i, query_non_gaps]).sum(
|
| 275 |
+
dim=1
|
| 276 |
+
) / seq_lens
|
| 277 |
+
num_neighbors.append(int((seqs_sim >= sim_threshold).sum().item()))
|
| 278 |
+
return num_neighbors
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def get_similarity_to_query(byte_seqs: torch.ByteTensor) -> torch.FloatTensor:
|
| 282 |
+
return (byte_seqs == byte_seqs[0, :]).sum(dim=1) / byte_seqs.shape[1]
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def sample_context(
|
| 286 |
+
msa_path: str,
|
| 287 |
+
max_num_samples: int,
|
| 288 |
+
max_token_length: int,
|
| 289 |
+
max_query_similarity: float = 1.0,
|
| 290 |
+
min_query_similarity: float = 0.0,
|
| 291 |
+
neighbor_similarity_lower_bound: float = 0.8,
|
| 292 |
+
use_full_sequences_in_context: bool = False,
|
| 293 |
+
full_sequences_path: str | None = None,
|
| 294 |
+
seed: int = 0,
|
| 295 |
+
device: torch.device | None = None,
|
| 296 |
+
cache_num_neighbors_path: str | None = None,
|
| 297 |
+
) -> tuple[str, list[str]]:
|
| 298 |
+
msa_sequences = parse_msa(msa_path)
|
| 299 |
+
msa_as_byte_tensor = convert_to_tensor(msa_sequences, device)
|
| 300 |
+
if cache_num_neighbors_path is not None and os.path.exists(cache_num_neighbors_path):
|
| 301 |
+
num_neighbors = np.load(cache_num_neighbors_path)
|
| 302 |
+
else:
|
| 303 |
+
num_neighbors = np.array(
|
| 304 |
+
get_num_neighbors(msa_as_byte_tensor, neighbor_similarity_lower_bound)
|
| 305 |
+
)
|
| 306 |
+
if cache_num_neighbors_path is not None:
|
| 307 |
+
np.save(cache_num_neighbors_path, num_neighbors)
|
| 308 |
+
|
| 309 |
+
sampling_weights = 1.0 / num_neighbors
|
| 310 |
+
query_similarity = get_similarity_to_query(msa_as_byte_tensor)
|
| 311 |
+
filtered_mask = (query_similarity <= max_query_similarity) & (
|
| 312 |
+
query_similarity >= min_query_similarity
|
| 313 |
+
)
|
| 314 |
+
if int(filtered_mask.sum()) < 1:
|
| 315 |
+
raise ValueError(
|
| 316 |
+
"No sequences found with similarity to query within range "
|
| 317 |
+
f"{min_query_similarity} <= query_similarity <= {max_query_similarity}."
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
filtered_weights = np.where(filtered_mask.cpu().numpy(), sampling_weights, 0.0)
|
| 321 |
+
sampled_indices = np.random.default_rng(seed).choice(
|
| 322 |
+
len(filtered_weights),
|
| 323 |
+
size=min(max_num_samples, int(filtered_mask.sum())),
|
| 324 |
+
p=filtered_weights / filtered_weights.sum(),
|
| 325 |
+
replace=False,
|
| 326 |
+
shuffle=True,
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
if use_full_sequences_in_context:
|
| 330 |
+
if full_sequences_path is None:
|
| 331 |
+
raise ValueError(
|
| 332 |
+
"full_sequences_path is required when use_full_sequences_in_context=True"
|
| 333 |
+
)
|
| 334 |
+
full_sequences = parse_msa(full_sequences_path)
|
| 335 |
+
if len(full_sequences) != len(msa_sequences):
|
| 336 |
+
raise ValueError("Number of full sequences must match number of MSA sequences")
|
| 337 |
+
for i, (full_seq, msa_seq) in enumerate(zip(full_sequences, msa_sequences, strict=True)):
|
| 338 |
+
if full_seq.id != msa_seq.id:
|
| 339 |
+
raise ValueError(
|
| 340 |
+
"Full sequences and MSA sequences must be in the same order and have the "
|
| 341 |
+
f"same ids. Found differing id for sample {i}: "
|
| 342 |
+
f"{full_seq.id} != {msa_seq.id}"
|
| 343 |
+
)
|
| 344 |
+
sampled_sequences = [full_sequences[int(i)] for i in sampled_indices]
|
| 345 |
+
else:
|
| 346 |
+
sampled_sequences = [msa_sequences[int(i)] for i in sampled_indices]
|
| 347 |
+
|
| 348 |
+
context_sequences: list[str] = []
|
| 349 |
+
context_ids: list[str] = []
|
| 350 |
+
context_length = 0
|
| 351 |
+
for seq in sampled_sequences:
|
| 352 |
+
seq_str = seq.sequence.upper().encode("ascii").translate(None, b"-").decode("ascii")
|
| 353 |
+
if context_length + len(seq_str) > max_token_length:
|
| 354 |
+
break
|
| 355 |
+
context_sequences.append(seq_str)
|
| 356 |
+
context_ids.append(seq.id)
|
| 357 |
+
context_length += len(seq_str)
|
| 358 |
+
return ",".join(context_sequences), context_ids
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def sample_multiple_contexts(
|
| 362 |
+
msa_path: str,
|
| 363 |
+
context_specifications: list[ContextSpecification],
|
| 364 |
+
use_full_sequences_in_context: bool = False,
|
| 365 |
+
full_sequences_path: str | None = None,
|
| 366 |
+
seed: int = 0,
|
| 367 |
+
device: torch.device | None = None,
|
| 368 |
+
cache_num_neighbors_path: str | None = None,
|
| 369 |
+
) -> tuple[list[str], list[list[str]]]:
|
| 370 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 371 |
+
if cache_num_neighbors_path is None:
|
| 372 |
+
cache_num_neighbors_path = os.path.join(temp_dir, "num_neighbors.npy")
|
| 373 |
+
|
| 374 |
+
contexts: list[str] = []
|
| 375 |
+
context_ids: list[list[str]] = []
|
| 376 |
+
for i, context_specification in enumerate(context_specifications):
|
| 377 |
+
context, ids = sample_context(
|
| 378 |
+
msa_path=msa_path,
|
| 379 |
+
max_num_samples=context_specification.max_num_samples,
|
| 380 |
+
max_token_length=context_specification.max_token_length,
|
| 381 |
+
max_query_similarity=context_specification.max_query_similarity,
|
| 382 |
+
min_query_similarity=context_specification.min_query_similarity,
|
| 383 |
+
neighbor_similarity_lower_bound=context_specification.neighbor_similarity_lower_bound,
|
| 384 |
+
use_full_sequences_in_context=use_full_sequences_in_context,
|
| 385 |
+
full_sequences_path=full_sequences_path,
|
| 386 |
+
seed=seed + i,
|
| 387 |
+
device=device,
|
| 388 |
+
cache_num_neighbors_path=cache_num_neighbors_path,
|
| 389 |
+
)
|
| 390 |
+
contexts.append(context)
|
| 391 |
+
context_ids.append(ids)
|
| 392 |
+
return contexts, context_ids
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
def get_context_id(max_tokens: int, sim_threshold: float) -> str:
|
| 396 |
+
return f"identity_{sim_threshold}_tokens_{max_tokens}"
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def build_context_specifications(
|
| 400 |
+
max_context_tokens: list[int] | None = None,
|
| 401 |
+
similarity_thresholds: list[float] | None = None,
|
| 402 |
+
min_query_similarity: float = 0.3,
|
| 403 |
+
) -> list[tuple[ContextSpecification, str]]:
|
| 404 |
+
if max_context_tokens is None:
|
| 405 |
+
max_context_tokens = DEFAULT_MAX_CONTEXT_TOKENS
|
| 406 |
+
if similarity_thresholds is None:
|
| 407 |
+
similarity_thresholds = DEFAULT_SIMILARITY_THRESHOLDS
|
| 408 |
+
|
| 409 |
+
specs = []
|
| 410 |
+
for max_tokens in max_context_tokens:
|
| 411 |
+
for sim_threshold in similarity_thresholds:
|
| 412 |
+
spec = ContextSpecification(
|
| 413 |
+
max_num_samples=511,
|
| 414 |
+
max_token_length=max_tokens,
|
| 415 |
+
max_query_similarity=sim_threshold,
|
| 416 |
+
min_query_similarity=min_query_similarity,
|
| 417 |
+
neighbor_similarity_lower_bound=0.8,
|
| 418 |
+
)
|
| 419 |
+
specs.append((spec, get_context_id(max_tokens, sim_threshold)))
|
| 420 |
+
return specs
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def sample_contexts_for_msa(
|
| 424 |
+
a3m_path: str,
|
| 425 |
+
context_specs: list[tuple[ContextSpecification, str]],
|
| 426 |
+
seed: int = 42,
|
| 427 |
+
) -> dict[str, str]:
|
| 428 |
+
specs_only = [spec for spec, _ in context_specs]
|
| 429 |
+
context_ids = [context_id for _, context_id in context_specs]
|
| 430 |
+
contexts, _ = sample_multiple_contexts(
|
| 431 |
+
msa_path=a3m_path,
|
| 432 |
+
context_specifications=specs_only,
|
| 433 |
+
seed=seed,
|
| 434 |
+
)
|
| 435 |
+
return dict(zip(context_ids, contexts, strict=True))
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
def _strip_a3m_insertions(sequence: str) -> str:
|
| 439 |
+
uppercase_or_gap = [char for char in sequence if char.isupper() or char in "-."]
|
| 440 |
+
return "".join(uppercase_or_gap).replace("-", "").replace(".", "")
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
def get_query_from_a3m(path: str) -> str:
|
| 444 |
+
header_found = False
|
| 445 |
+
seq_parts: list[str] = []
|
| 446 |
+
with open(path, encoding="utf-8") as handle:
|
| 447 |
+
for raw_line in handle:
|
| 448 |
+
line = raw_line.strip()
|
| 449 |
+
if not line:
|
| 450 |
+
continue
|
| 451 |
+
if line.startswith(">"):
|
| 452 |
+
if header_found:
|
| 453 |
+
break
|
| 454 |
+
header_found = True
|
| 455 |
+
continue
|
| 456 |
+
if header_found:
|
| 457 |
+
seq_parts.append(line)
|
| 458 |
+
if not header_found:
|
| 459 |
+
raise ValueError(f"No FASTA header found in A3M file: {path}")
|
| 460 |
+
return _strip_a3m_insertions("".join(seq_parts))
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
def load_msa_dir(msa_dir: str) -> dict[str, str]:
|
| 464 |
+
msa_lookup: dict[str, str] = {}
|
| 465 |
+
a3m_files = list(Path(msa_dir).rglob("*.a3m"))
|
| 466 |
+
if not a3m_files:
|
| 467 |
+
raise FileNotFoundError(f"No .a3m files found in {msa_dir}")
|
| 468 |
+
for a3m_path in tqdm(a3m_files, desc="Loading MSAs"):
|
| 469 |
+
query_seq = get_query_from_a3m(str(a3m_path))
|
| 470 |
+
msa_lookup[query_seq] = str(a3m_path)
|
| 471 |
+
_get_logger().info("Loaded %d MSAs from %s", len(msa_lookup), msa_dir)
|
| 472 |
+
return msa_lookup
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
def _safe_extract_tar(tar: tarfile.TarFile, output_dir: str) -> None:
|
| 476 |
+
output_root = Path(output_dir).resolve()
|
| 477 |
+
for member in tar.getmembers():
|
| 478 |
+
if member.issym() or member.islnk():
|
| 479 |
+
raise ValueError(f"Tar links are not allowed: {member.name}")
|
| 480 |
+
if member.isdev():
|
| 481 |
+
raise ValueError(f"Tar device entries are not allowed: {member.name}")
|
| 482 |
+
target = (output_root / member.name).resolve()
|
| 483 |
+
if output_root != target and output_root not in target.parents:
|
| 484 |
+
raise ValueError(f"Unsafe tar member path: {member.name}")
|
| 485 |
+
tar.extractall(output_root, filter="data")
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
def load_msa_from_hf(
|
| 489 |
+
hf_path: str,
|
| 490 |
+
cache_dir: str | None = None,
|
| 491 |
+
token: str | None = None,
|
| 492 |
+
) -> dict[str, str]:
|
| 493 |
+
from huggingface_hub import snapshot_download
|
| 494 |
+
|
| 495 |
+
if cache_dir is None:
|
| 496 |
+
cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "fastplms_msa")
|
| 497 |
+
os.makedirs(cache_dir, exist_ok=True)
|
| 498 |
+
local_dir = os.path.join(cache_dir, hf_path.replace("/", "_"))
|
| 499 |
+
if not os.path.exists(local_dir) or not any(Path(local_dir).rglob("*.a3m")):
|
| 500 |
+
local_dir = snapshot_download(
|
| 501 |
+
repo_id=hf_path,
|
| 502 |
+
repo_type="dataset",
|
| 503 |
+
local_dir=local_dir,
|
| 504 |
+
token=token,
|
| 505 |
+
)
|
| 506 |
+
for tar_path in Path(local_dir).rglob("*.tar.gz"):
|
| 507 |
+
with tarfile.open(tar_path) as tar:
|
| 508 |
+
_safe_extract_tar(tar, str(tar_path.parent))
|
| 509 |
+
return load_msa_dir(local_dir)
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
def get_msa_for_sequence(
|
| 513 |
+
sequence: str, msa_lookup: dict[str, str], min_identity: float = 0.95
|
| 514 |
+
) -> str | None:
|
| 515 |
+
if sequence in msa_lookup:
|
| 516 |
+
return msa_lookup[sequence]
|
| 517 |
+
|
| 518 |
+
best_match_path: str | None = None
|
| 519 |
+
best_identity = 0.0
|
| 520 |
+
for query_seq, a3m_path in msa_lookup.items():
|
| 521 |
+
if abs(len(query_seq) - len(sequence)) > 10:
|
| 522 |
+
continue
|
| 523 |
+
min_len = min(len(query_seq), len(sequence))
|
| 524 |
+
if min_len == 0:
|
| 525 |
+
continue
|
| 526 |
+
matches = sum(a == b for a, b in zip(query_seq[:min_len], sequence[:min_len], strict=True))
|
| 527 |
+
identity = matches / min_len
|
| 528 |
+
if identity > best_identity:
|
| 529 |
+
best_identity = identity
|
| 530 |
+
best_match_path = a3m_path
|
| 531 |
+
|
| 532 |
+
if best_identity >= min_identity:
|
| 533 |
+
return best_match_path
|
| 534 |
+
return None
|
| 535 |
+
|
| 536 |
+
|
| 537 |
+
class ContextCache:
|
| 538 |
+
"""Content-addressed JSON cache for deterministic E1 MSA contexts."""
|
| 539 |
+
|
| 540 |
+
_SCHEMA_VERSION = 1
|
| 541 |
+
|
| 542 |
+
def __init__(
|
| 543 |
+
self,
|
| 544 |
+
cache_dir: str,
|
| 545 |
+
specs_hash: str,
|
| 546 |
+
seed: int,
|
| 547 |
+
source_revision: str = E1_MSA_SAMPLING_SOURCE_REVISION,
|
| 548 |
+
) -> None:
|
| 549 |
+
self.cache_dir = cache_dir
|
| 550 |
+
self.specs_hash = specs_hash
|
| 551 |
+
self.seed = seed
|
| 552 |
+
self.source_revision = source_revision
|
| 553 |
+
os.makedirs(cache_dir, exist_ok=True)
|
| 554 |
+
|
| 555 |
+
def _cache_path(self, key: str) -> str:
|
| 556 |
+
safe_key = hashlib.sha256(key.encode("utf-8")).hexdigest()[:16]
|
| 557 |
+
return os.path.join(self.cache_dir, f"{safe_key}_seed{self.seed}_{self.specs_hash}.json")
|
| 558 |
+
|
| 559 |
+
def _input_fingerprint(self, key: str) -> str:
|
| 560 |
+
descriptor: dict[str, Any] = {
|
| 561 |
+
"key": os.path.abspath(key) if os.path.isfile(key) else key,
|
| 562 |
+
"seed": self.seed,
|
| 563 |
+
"source_revision": self.source_revision,
|
| 564 |
+
"specs_hash": self.specs_hash,
|
| 565 |
+
}
|
| 566 |
+
if os.path.isfile(key):
|
| 567 |
+
hasher = hashlib.sha256()
|
| 568 |
+
with open(key, "rb") as handle:
|
| 569 |
+
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
| 570 |
+
hasher.update(block)
|
| 571 |
+
descriptor["content_sha256"] = hasher.hexdigest()
|
| 572 |
+
else:
|
| 573 |
+
descriptor["literal_key"] = key
|
| 574 |
+
payload = json.dumps(descriptor, sort_keys=True, separators=(",", ":"))
|
| 575 |
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
| 576 |
+
|
| 577 |
+
def load(self, key: str) -> dict[str, str] | None:
|
| 578 |
+
path = self._cache_path(key)
|
| 579 |
+
if not os.path.exists(path):
|
| 580 |
+
return None
|
| 581 |
+
try:
|
| 582 |
+
with open(path, encoding="utf-8") as handle:
|
| 583 |
+
payload = json.load(handle)
|
| 584 |
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
| 585 |
+
return None
|
| 586 |
+
if not isinstance(payload, dict):
|
| 587 |
+
return None
|
| 588 |
+
if payload.get("schema_version") != self._SCHEMA_VERSION:
|
| 589 |
+
return None
|
| 590 |
+
if payload.get("input_fingerprint") != self._input_fingerprint(key):
|
| 591 |
+
return None
|
| 592 |
+
if payload.get("source_revision") != self.source_revision:
|
| 593 |
+
return None
|
| 594 |
+
contexts = payload.get("contexts")
|
| 595 |
+
if not isinstance(contexts, dict) or not all(
|
| 596 |
+
isinstance(name, str) and isinstance(context, str) for name, context in contexts.items()
|
| 597 |
+
):
|
| 598 |
+
return None
|
| 599 |
+
return contexts
|
| 600 |
+
|
| 601 |
+
def store(self, key: str, contexts: dict[str, str]) -> None:
|
| 602 |
+
if not all(
|
| 603 |
+
isinstance(name, str) and isinstance(context, str) for name, context in contexts.items()
|
| 604 |
+
):
|
| 605 |
+
raise TypeError("contexts must map string identifiers to string contexts")
|
| 606 |
+
path = self._cache_path(key)
|
| 607 |
+
payload = {
|
| 608 |
+
"schema_version": self._SCHEMA_VERSION,
|
| 609 |
+
"source_revision": self.source_revision,
|
| 610 |
+
"input_fingerprint": self._input_fingerprint(key),
|
| 611 |
+
"contexts": contexts,
|
| 612 |
+
}
|
| 613 |
+
temp_path: str | None = None
|
| 614 |
+
try:
|
| 615 |
+
with tempfile.NamedTemporaryFile(
|
| 616 |
+
mode="w",
|
| 617 |
+
encoding="utf-8",
|
| 618 |
+
dir=self.cache_dir,
|
| 619 |
+
prefix=".context-",
|
| 620 |
+
suffix=".tmp",
|
| 621 |
+
delete=False,
|
| 622 |
+
) as handle:
|
| 623 |
+
temp_path = handle.name
|
| 624 |
+
json.dump(payload, handle, sort_keys=True, separators=(",", ":"))
|
| 625 |
+
handle.write("\n")
|
| 626 |
+
handle.flush()
|
| 627 |
+
os.fsync(handle.fileno())
|
| 628 |
+
os.replace(temp_path, path)
|
| 629 |
+
temp_path = None
|
| 630 |
+
finally:
|
| 631 |
+
if temp_path is not None:
|
| 632 |
+
Path(temp_path).unlink(missing_ok=True)
|
| 633 |
+
|
| 634 |
+
|
| 635 |
+
def compute_ppll(logits: torch.Tensor, token_ids: torch.Tensor) -> float:
|
| 636 |
+
if token_ids.numel() == 0:
|
| 637 |
+
raise ValueError("Cannot score an empty token sequence")
|
| 638 |
+
if token_ids.device != logits.device:
|
| 639 |
+
token_ids = token_ids.to(logits.device)
|
| 640 |
+
if logits.shape[0] != token_ids.shape[0]:
|
| 641 |
+
raise ValueError(
|
| 642 |
+
f"Logits length {logits.shape[0]} != token_ids length {token_ids.shape[0]}"
|
| 643 |
+
)
|
| 644 |
+
probs = logits.softmax(dim=-1)
|
| 645 |
+
token_probs = probs.gather(dim=1, index=token_ids.unsqueeze(1)).squeeze(1)
|
| 646 |
+
return float(token_probs.mean().item())
|
| 647 |
+
|
| 648 |
+
|
| 649 |
+
class _E1ContextPredictor:
|
| 650 |
+
def __init__(
|
| 651 |
+
self,
|
| 652 |
+
model: PreTrainedModel,
|
| 653 |
+
data_prep_config: DataPrepConfig | None = None,
|
| 654 |
+
max_batch_tokens: int = 65536,
|
| 655 |
+
use_cache: bool = True,
|
| 656 |
+
cache_size: int = 4,
|
| 657 |
+
save_masked_positions_only: bool = False,
|
| 658 |
+
fields_to_save: list[str] | None = None,
|
| 659 |
+
keep_predictions_in_gpu: bool = False,
|
| 660 |
+
progress: bool = True,
|
| 661 |
+
) -> None:
|
| 662 |
+
self.model = model
|
| 663 |
+
self.max_batch_tokens = max_batch_tokens
|
| 664 |
+
self.batch_preparer = E1BatchPreparer(data_prep_config=data_prep_config)
|
| 665 |
+
self.model.eval()
|
| 666 |
+
self.kv_cache = KVCache(cache_size=cache_size) if use_cache else None
|
| 667 |
+
self.fields_to_save = fields_to_save or [
|
| 668 |
+
"logits",
|
| 669 |
+
"token_embeddings",
|
| 670 |
+
"mean_token_embeddings",
|
| 671 |
+
]
|
| 672 |
+
self.save_masked_positions_only = save_masked_positions_only
|
| 673 |
+
self.keep_predictions_in_gpu = keep_predictions_in_gpu
|
| 674 |
+
self.progress = progress
|
| 675 |
+
|
| 676 |
+
@property
|
| 677 |
+
def device(self) -> torch.device:
|
| 678 |
+
return next(self.model.parameters()).device
|
| 679 |
+
|
| 680 |
+
def group_by_length(
|
| 681 |
+
self, indexed_sequences: list[IndexedSequence]
|
| 682 |
+
) -> list[list[IndexedSequence]]:
|
| 683 |
+
batches: list[list[IndexedSequence]] = [[]]
|
| 684 |
+
for idx, seq in sorted(
|
| 685 |
+
indexed_sequences, key=lambda idx_seq: (len(idx_seq[1]), idx_seq[0])
|
| 686 |
+
):
|
| 687 |
+
if len(batches[-1]) > 0 and len(seq) * (len(batches[-1]) + 1) > self.max_batch_tokens:
|
| 688 |
+
batches.append([])
|
| 689 |
+
batches[-1].append((idx, seq))
|
| 690 |
+
return batches
|
| 691 |
+
|
| 692 |
+
def group_by_context(
|
| 693 |
+
self, indexed_sequences: list[IndexedSequence]
|
| 694 |
+
) -> list[list[IndexedSequence]]:
|
| 695 |
+
batches: dict[str | None, list[IndexedSequence]] = defaultdict(list)
|
| 696 |
+
for idx, seq in indexed_sequences:
|
| 697 |
+
batches[get_context(seq)].append((idx, seq))
|
| 698 |
+
return list(batches.values())
|
| 699 |
+
|
| 700 |
+
def batch_sequences(self, sequences: list[str]) -> list[list[int]]:
|
| 701 |
+
indexed_sequences: list[IndexedSequence] = list(enumerate(sequences))
|
| 702 |
+
indexed_batches = self.group_by_context(indexed_sequences)
|
| 703 |
+
indexed_batches = list(
|
| 704 |
+
itertools.chain.from_iterable(
|
| 705 |
+
[self.group_by_length(batch) for batch in indexed_batches]
|
| 706 |
+
)
|
| 707 |
+
)
|
| 708 |
+
batches = [[item[0] for item in batch] for batch in indexed_batches]
|
| 709 |
+
flattened_indices = list(itertools.chain.from_iterable(batches))
|
| 710 |
+
if sorted(flattened_indices) != list(range(len(sequences))):
|
| 711 |
+
raise RuntimeError("Batches must contain all indices with no repetition")
|
| 712 |
+
return batches
|
| 713 |
+
|
| 714 |
+
@torch.no_grad()
|
| 715 |
+
def predict_batch(
|
| 716 |
+
self, sequences: list[str], sequence_metadata: list[dict[str, str | int]]
|
| 717 |
+
) -> list[E1Prediction]:
|
| 718 |
+
outputs = self.predict_batch_padded(sequences)
|
| 719 |
+
outputs["logits"] = outputs["logits"].float()
|
| 720 |
+
outputs["embeddings"] = outputs["embeddings"].float()
|
| 721 |
+
|
| 722 |
+
token_mask = outputs["non_boundary_token_mask"] & outputs["last_sequence_mask"]
|
| 723 |
+
if self.save_masked_positions_only:
|
| 724 |
+
token_mask = token_mask & outputs["mask_positions_mask"]
|
| 725 |
+
|
| 726 |
+
predictions: list[E1Prediction] = []
|
| 727 |
+
for i in range(len(sequences)):
|
| 728 |
+
pred: E1Prediction = {"id": sequence_metadata[i]["id"]}
|
| 729 |
+
if "context_id" in sequence_metadata[i]:
|
| 730 |
+
pred["context_id"] = sequence_metadata[i]["context_id"]
|
| 731 |
+
if "logits" in self.fields_to_save:
|
| 732 |
+
pred["logits"] = outputs["logits"][i, token_mask[i]]
|
| 733 |
+
if not self.keep_predictions_in_gpu:
|
| 734 |
+
pred["logits"] = pred["logits"].to("cpu")
|
| 735 |
+
if "token_embeddings" in self.fields_to_save:
|
| 736 |
+
pred["token_embeddings"] = outputs["embeddings"][i, token_mask[i]]
|
| 737 |
+
if not self.keep_predictions_in_gpu:
|
| 738 |
+
pred["token_embeddings"] = pred["token_embeddings"].to("cpu")
|
| 739 |
+
if "mean_token_embeddings" in self.fields_to_save:
|
| 740 |
+
pred["mean_token_embeddings"] = outputs["embeddings"][i, token_mask[i]].mean(dim=0)
|
| 741 |
+
if not self.keep_predictions_in_gpu:
|
| 742 |
+
pred["mean_token_embeddings"] = pred["mean_token_embeddings"].to("cpu")
|
| 743 |
+
predictions.append(pred)
|
| 744 |
+
return predictions
|
| 745 |
+
|
| 746 |
+
@torch.no_grad()
|
| 747 |
+
def predict_batch_padded(self, sequences: list[str]) -> dict[str, torch.Tensor]:
|
| 748 |
+
device = self.device
|
| 749 |
+
autocast_enabled = device.type == "cuda"
|
| 750 |
+
with torch.autocast(device.type, torch.bfloat16, enabled=autocast_enabled):
|
| 751 |
+
batch = self.batch_preparer.get_batch_kwargs(sequences, device=device)
|
| 752 |
+
if self.kv_cache is not None:
|
| 753 |
+
self.kv_cache.before_forward(batch)
|
| 754 |
+
|
| 755 |
+
past_key_values = batch.get("past_key_values")
|
| 756 |
+
use_cache = bool(batch["use_cache"]) if "use_cache" in batch else False
|
| 757 |
+
output: E1MaskedLMOutputWithPast = self.model(
|
| 758 |
+
input_ids=batch["input_ids"],
|
| 759 |
+
within_seq_position_ids=batch["within_seq_position_ids"],
|
| 760 |
+
global_position_ids=batch["global_position_ids"],
|
| 761 |
+
sequence_ids=batch["sequence_ids"],
|
| 762 |
+
past_key_values=past_key_values,
|
| 763 |
+
use_cache=use_cache,
|
| 764 |
+
output_attentions=False,
|
| 765 |
+
output_hidden_states=False,
|
| 766 |
+
)
|
| 767 |
+
if self.kv_cache is not None:
|
| 768 |
+
self.kv_cache.after_forward(batch, output)
|
| 769 |
+
|
| 770 |
+
padding_mask = batch["input_ids"] == self.batch_preparer.pad_token_id
|
| 771 |
+
last_sequence_mask = (
|
| 772 |
+
batch["sequence_ids"] == batch["sequence_ids"].max(dim=1).values[:, None]
|
| 773 |
+
)
|
| 774 |
+
boundary_token_mask = self.batch_preparer.get_boundary_token_mask(batch["input_ids"])
|
| 775 |
+
mask_positions_mask = self.batch_preparer.get_mask_positions_mask(batch["input_ids"])
|
| 776 |
+
return {
|
| 777 |
+
"logits": output.logits,
|
| 778 |
+
"embeddings": output.last_hidden_state,
|
| 779 |
+
"last_sequence_mask": last_sequence_mask,
|
| 780 |
+
"non_boundary_token_mask": ~boundary_token_mask,
|
| 781 |
+
"mask_positions_mask": mask_positions_mask,
|
| 782 |
+
"valid_token_mask": ~padding_mask,
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
@torch.no_grad()
|
| 786 |
+
def predict(
|
| 787 |
+
self,
|
| 788 |
+
sequences: Sequence[str],
|
| 789 |
+
sequence_ids: Sequence[int | str] | None = None,
|
| 790 |
+
context_seqs: dict[str, str] | None = None,
|
| 791 |
+
) -> Iterator[E1Prediction]:
|
| 792 |
+
if sequence_ids is None:
|
| 793 |
+
sequence_ids = list(range(len(sequences)))
|
| 794 |
+
if context_seqs:
|
| 795 |
+
sequences_with_context = [
|
| 796 |
+
(ctx + "," + seq, {"context_id": ctx_id, "id": sequence_id})
|
| 797 |
+
for ctx_id, ctx in context_seqs.items()
|
| 798 |
+
for seq, sequence_id in zip(sequences, sequence_ids, strict=True)
|
| 799 |
+
]
|
| 800 |
+
else:
|
| 801 |
+
sequences_with_context = [
|
| 802 |
+
(seq, {"id": sequence_id})
|
| 803 |
+
for seq, sequence_id in zip(sequences, sequence_ids, strict=True)
|
| 804 |
+
]
|
| 805 |
+
|
| 806 |
+
batched_sequences, sequence_metadata = tuple(zip(*sequences_with_context, strict=True))
|
| 807 |
+
batches = self.batch_sequences(list(batched_sequences))
|
| 808 |
+
iterator = tqdm(batches, desc="Predicting batches", disable=not self.progress)
|
| 809 |
+
for indices in iterator:
|
| 810 |
+
sequence_batch = [batched_sequences[i] for i in indices]
|
| 811 |
+
sequence_batch_metadata = [sequence_metadata[i] for i in indices]
|
| 812 |
+
yield from self.predict_batch(sequence_batch, sequence_batch_metadata)
|
| 813 |
+
|
| 814 |
+
|
| 815 |
+
def _pool_hidden_states(
|
| 816 |
+
hidden_list: list[torch.Tensor],
|
| 817 |
+
pooling_types: list[str],
|
| 818 |
+
device: torch.device,
|
| 819 |
+
) -> torch.Tensor:
|
| 820 |
+
pooler = Pooler(pooling_types)
|
| 821 |
+
max_len = max(hidden.shape[0] for hidden in hidden_list)
|
| 822 |
+
hidden_dim = hidden_list[0].shape[1]
|
| 823 |
+
batch_size = len(hidden_list)
|
| 824 |
+
padded = torch.zeros(batch_size, max_len, hidden_dim, device=device)
|
| 825 |
+
attention_mask = torch.zeros(batch_size, max_len, device=device)
|
| 826 |
+
for i, hidden in enumerate(hidden_list):
|
| 827 |
+
seq_len = hidden.shape[0]
|
| 828 |
+
padded[i, :seq_len] = hidden
|
| 829 |
+
attention_mask[i, :seq_len] = 1.0
|
| 830 |
+
return pooler(padded, attention_mask)
|
| 831 |
+
|
| 832 |
+
|
| 833 |
+
def _forward_for_embedding(
|
| 834 |
+
model: PreTrainedModel,
|
| 835 |
+
sequences: list[str],
|
| 836 |
+
context: str | None,
|
| 837 |
+
max_batch_tokens: int,
|
| 838 |
+
progress: bool,
|
| 839 |
+
) -> list[torch.Tensor]:
|
| 840 |
+
predictor = _E1ContextPredictor(
|
| 841 |
+
model=model,
|
| 842 |
+
data_prep_config=DataPrepConfig(remove_X_tokens=True),
|
| 843 |
+
max_batch_tokens=max_batch_tokens,
|
| 844 |
+
fields_to_save=["token_embeddings"],
|
| 845 |
+
keep_predictions_in_gpu=True,
|
| 846 |
+
use_cache=False,
|
| 847 |
+
cache_size=1,
|
| 848 |
+
progress=progress,
|
| 849 |
+
)
|
| 850 |
+
context_seqs = {"embed_ctx": context} if context else None
|
| 851 |
+
predictions = list(
|
| 852 |
+
predictor.predict(
|
| 853 |
+
sequences=sequences,
|
| 854 |
+
sequence_ids=list(range(len(sequences))),
|
| 855 |
+
context_seqs=context_seqs,
|
| 856 |
+
)
|
| 857 |
+
)
|
| 858 |
+
predictions.sort(key=lambda prediction: prediction["id"])
|
| 859 |
+
return [prediction["token_embeddings"] for prediction in predictions]
|
| 860 |
+
|
| 861 |
+
|
| 862 |
+
class HomologueSearcher:
|
| 863 |
+
"""Run local MMseqs2 searches through one verified, digest-pinned image.
|
| 864 |
+
|
| 865 |
+
The default CPU image is multi-architecture and immutable. Pulling and
|
| 866 |
+
container networking are separate explicit opt-ins. GPU execution requires
|
| 867 |
+
a caller-supplied digest-pinned GPU image because the official CUDA image is
|
| 868 |
+
not portable to every supported host architecture.
|
| 869 |
+
"""
|
| 870 |
+
|
| 871 |
+
_PROVENANCE_SCHEMA_VERSION = 1
|
| 872 |
+
_PROVENANCE_FILENAME = "search-provenance.json"
|
| 873 |
+
|
| 874 |
+
def __init__(
|
| 875 |
+
self,
|
| 876 |
+
target_db: str,
|
| 877 |
+
docker_image: str = DOCKER_IMAGE,
|
| 878 |
+
sensitivity: float = 7.5,
|
| 879 |
+
max_seqs: int = 1000,
|
| 880 |
+
min_seq_id: float = 0.0,
|
| 881 |
+
coverage: float = 0.8,
|
| 882 |
+
split_memory_limit: str | None = None,
|
| 883 |
+
use_gpu: bool = False,
|
| 884 |
+
allow_pull: bool = False,
|
| 885 |
+
allow_network: bool = False,
|
| 886 |
+
phase_timeout: float = DEFAULT_MMSEQS2_PHASE_TIMEOUT,
|
| 887 |
+
target_db_identity: str | None = None,
|
| 888 |
+
) -> None:
|
| 889 |
+
image_reference = _parse_pinned_image_reference(docker_image)
|
| 890 |
+
if not isinstance(target_db, str) or not target_db or "\x00" in target_db:
|
| 891 |
+
raise ValueError("target_db must be a non-empty path without null bytes")
|
| 892 |
+
numeric_values = {
|
| 893 |
+
"sensitivity": sensitivity,
|
| 894 |
+
"min_seq_id": min_seq_id,
|
| 895 |
+
"coverage": coverage,
|
| 896 |
+
}
|
| 897 |
+
for name, value in numeric_values.items():
|
| 898 |
+
if (
|
| 899 |
+
isinstance(value, bool)
|
| 900 |
+
or not isinstance(value, numbers.Real)
|
| 901 |
+
or not math.isfinite(float(value))
|
| 902 |
+
):
|
| 903 |
+
raise ValueError(f"{name} must be a finite real number")
|
| 904 |
+
if sensitivity <= 0:
|
| 905 |
+
raise ValueError("sensitivity must be positive")
|
| 906 |
+
if not 0.0 <= min_seq_id <= 1.0:
|
| 907 |
+
raise ValueError("min_seq_id must be in [0, 1]")
|
| 908 |
+
if not 0.0 <= coverage <= 1.0:
|
| 909 |
+
raise ValueError("coverage must be in [0, 1]")
|
| 910 |
+
if isinstance(max_seqs, bool) or not isinstance(max_seqs, int) or max_seqs < 1:
|
| 911 |
+
raise ValueError("max_seqs must be an integer >= 1")
|
| 912 |
+
if split_memory_limit is not None and (
|
| 913 |
+
not isinstance(split_memory_limit, str)
|
| 914 |
+
or not split_memory_limit.strip()
|
| 915 |
+
or "\x00" in split_memory_limit
|
| 916 |
+
):
|
| 917 |
+
raise ValueError("split_memory_limit must be None or a non-empty string")
|
| 918 |
+
if type(use_gpu) is not bool:
|
| 919 |
+
raise TypeError("use_gpu must be a boolean")
|
| 920 |
+
if type(allow_pull) is not bool:
|
| 921 |
+
raise TypeError("allow_pull must be a boolean")
|
| 922 |
+
if type(allow_network) is not bool:
|
| 923 |
+
raise TypeError("allow_network must be a boolean")
|
| 924 |
+
if (
|
| 925 |
+
isinstance(phase_timeout, bool)
|
| 926 |
+
or not isinstance(phase_timeout, (int, float))
|
| 927 |
+
or not math.isfinite(float(phase_timeout))
|
| 928 |
+
or phase_timeout <= 0
|
| 929 |
+
):
|
| 930 |
+
raise ValueError("phase_timeout must be a finite positive number")
|
| 931 |
+
if target_db_identity is not None and (
|
| 932 |
+
not isinstance(target_db_identity, str) or not target_db_identity.strip()
|
| 933 |
+
):
|
| 934 |
+
raise ValueError("target_db_identity must be None or a non-empty string")
|
| 935 |
+
if (
|
| 936 |
+
use_gpu
|
| 937 |
+
and image_reference.repository == MMSEQS2_IMAGE_REPOSITORY
|
| 938 |
+
and image_reference.digest == MMSEQS2_CPU_MANIFEST_DIGEST
|
| 939 |
+
):
|
| 940 |
+
raise ValueError(
|
| 941 |
+
"The default MMseqs2 image is CPU-only. GPU search requires an explicit "
|
| 942 |
+
"digest-pinned image compatible with the host architecture."
|
| 943 |
+
)
|
| 944 |
+
self.target_db = target_db
|
| 945 |
+
self.docker_image = docker_image
|
| 946 |
+
self._image_reference = image_reference
|
| 947 |
+
self.sensitivity = float(sensitivity)
|
| 948 |
+
self.max_seqs = max_seqs
|
| 949 |
+
self.min_seq_id = float(min_seq_id)
|
| 950 |
+
self.coverage = float(coverage)
|
| 951 |
+
self.split_memory_limit = (
|
| 952 |
+
split_memory_limit.strip() if split_memory_limit is not None else None
|
| 953 |
+
)
|
| 954 |
+
self.use_gpu = use_gpu
|
| 955 |
+
self.allow_pull = allow_pull
|
| 956 |
+
self.allow_network = allow_network
|
| 957 |
+
self.phase_timeout = float(phase_timeout)
|
| 958 |
+
self.target_db_identity = (
|
| 959 |
+
target_db_identity.strip() if target_db_identity is not None else None
|
| 960 |
+
)
|
| 961 |
+
self._verified_image_identity: _DockerImageIdentity | None = None
|
| 962 |
+
|
| 963 |
+
@staticmethod
|
| 964 |
+
def _seq_hash(sequence: str) -> str:
|
| 965 |
+
return hashlib.md5(sequence.encode()).hexdigest()[:12]
|
| 966 |
+
|
| 967 |
+
def _run_docker_command(
|
| 968 |
+
self,
|
| 969 |
+
cmd: list[str],
|
| 970 |
+
*,
|
| 971 |
+
phase: str = "docker command",
|
| 972 |
+
**kwargs,
|
| 973 |
+
) -> subprocess.CompletedProcess:
|
| 974 |
+
kwargs["timeout"] = self.phase_timeout
|
| 975 |
+
try:
|
| 976 |
+
return subprocess.run(cmd, **kwargs)
|
| 977 |
+
except subprocess.TimeoutExpired as error:
|
| 978 |
+
raise TimeoutError(
|
| 979 |
+
f"MMseqs2 phase {phase!r} exceeded {self.phase_timeout:g} seconds"
|
| 980 |
+
) from error
|
| 981 |
+
|
| 982 |
+
@staticmethod
|
| 983 |
+
def _working_root() -> Path:
|
| 984 |
+
return Path.cwd().resolve(strict=True)
|
| 985 |
+
|
| 986 |
+
def _resolve_path_under_cwd(self, path: str, *, must_exist: bool = False) -> Path:
|
| 987 |
+
root = self._working_root()
|
| 988 |
+
candidate = Path(path)
|
| 989 |
+
if not candidate.is_absolute():
|
| 990 |
+
candidate = root / candidate
|
| 991 |
+
try:
|
| 992 |
+
resolved = candidate.resolve(strict=must_exist)
|
| 993 |
+
except OSError as error:
|
| 994 |
+
raise ValueError(f"Path cannot be resolved safely: {path!r}") from error
|
| 995 |
+
if resolved != root and root not in resolved.parents:
|
| 996 |
+
raise ValueError(
|
| 997 |
+
"Path must resolve under the current working directory for the Docker mount. "
|
| 998 |
+
f"cwd={os.fspath(root)!r}, path={os.fspath(resolved)!r}"
|
| 999 |
+
)
|
| 1000 |
+
return resolved
|
| 1001 |
+
|
| 1002 |
+
def _validate_paths_under_cwd(self, *paths: str) -> None:
|
| 1003 |
+
for path in paths:
|
| 1004 |
+
self._resolve_path_under_cwd(path)
|
| 1005 |
+
|
| 1006 |
+
def _path_in_container(self, local_path: str) -> str:
|
| 1007 |
+
resolved = self._resolve_path_under_cwd(local_path)
|
| 1008 |
+
relative = resolved.relative_to(self._working_root())
|
| 1009 |
+
return relative.as_posix() or "."
|
| 1010 |
+
|
| 1011 |
+
def _docker_base_cmd(self) -> list[str]:
|
| 1012 |
+
root = self._working_root()
|
| 1013 |
+
cmd = ["docker", "run", "--rm"]
|
| 1014 |
+
if not self.allow_network:
|
| 1015 |
+
cmd.extend(["--network", "none"])
|
| 1016 |
+
cmd.extend(["-v", f"{os.fspath(root)}:/app", "-w", "/app"])
|
| 1017 |
+
if self.use_gpu:
|
| 1018 |
+
if not torch.cuda.is_available():
|
| 1019 |
+
raise RuntimeError(
|
| 1020 |
+
"use_gpu=True requires CUDA to be available in the FastPLMs host process"
|
| 1021 |
+
)
|
| 1022 |
+
cmd.extend(["--gpus", "all"])
|
| 1023 |
+
cmd.append(self.docker_image)
|
| 1024 |
+
return cmd
|
| 1025 |
+
|
| 1026 |
+
def _inspect_docker_image(self, *, check: bool) -> _DockerImageIdentity | None:
|
| 1027 |
+
inspect = self._run_docker_command(
|
| 1028 |
+
["docker", "image", "inspect", self.docker_image],
|
| 1029 |
+
phase="image inspection",
|
| 1030 |
+
capture_output=True,
|
| 1031 |
+
text=True,
|
| 1032 |
+
check=check,
|
| 1033 |
+
)
|
| 1034 |
+
if inspect.returncode != 0:
|
| 1035 |
+
stderr = inspect.stderr if isinstance(inspect.stderr, str) else ""
|
| 1036 |
+
if "no such image" in stderr.lower() or "not found" in stderr.lower():
|
| 1037 |
+
return None
|
| 1038 |
+
raise subprocess.CalledProcessError(
|
| 1039 |
+
inspect.returncode,
|
| 1040 |
+
inspect.args,
|
| 1041 |
+
output=inspect.stdout,
|
| 1042 |
+
stderr=inspect.stderr,
|
| 1043 |
+
)
|
| 1044 |
+
try:
|
| 1045 |
+
payload = json.loads(inspect.stdout)
|
| 1046 |
+
if (
|
| 1047 |
+
not isinstance(payload, list)
|
| 1048 |
+
or len(payload) != 1
|
| 1049 |
+
or not isinstance(payload[0], dict)
|
| 1050 |
+
):
|
| 1051 |
+
raise ValueError("Docker inspect must return exactly one image object")
|
| 1052 |
+
image = payload[0]
|
| 1053 |
+
repo_digests = image.get("RepoDigests")
|
| 1054 |
+
image_id = image.get("Id")
|
| 1055 |
+
image_os = image.get("Os")
|
| 1056 |
+
architecture = image.get("Architecture")
|
| 1057 |
+
if not isinstance(repo_digests, list) or not all(
|
| 1058 |
+
isinstance(value, str) for value in repo_digests
|
| 1059 |
+
):
|
| 1060 |
+
raise ValueError("Docker inspect did not return RepoDigests")
|
| 1061 |
+
expected_repo_digest = (
|
| 1062 |
+
f"{self._image_reference.repository}@{self._image_reference.digest}"
|
| 1063 |
+
)
|
| 1064 |
+
if expected_repo_digest not in repo_digests:
|
| 1065 |
+
raise ValueError(
|
| 1066 |
+
"Docker image RepoDigests do not contain the requested repository "
|
| 1067 |
+
"and manifest digest"
|
| 1068 |
+
)
|
| 1069 |
+
if not isinstance(image_id, str) or _SHA256_DIGEST_RE.fullmatch(image_id) is None:
|
| 1070 |
+
raise ValueError("Docker inspect returned an invalid image ID")
|
| 1071 |
+
if image_os != "linux":
|
| 1072 |
+
raise ValueError(f"MMseqs2 image OS must be 'linux', got {image_os!r}")
|
| 1073 |
+
expected_architecture = _docker_architecture()
|
| 1074 |
+
if architecture != expected_architecture:
|
| 1075 |
+
raise ValueError(
|
| 1076 |
+
"MMseqs2 image architecture does not match the host: "
|
| 1077 |
+
f"expected {expected_architecture!r}, got {architecture!r}"
|
| 1078 |
+
)
|
| 1079 |
+
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as error:
|
| 1080 |
+
raise RuntimeError(
|
| 1081 |
+
f"Docker image identity verification failed for {self.docker_image!r}"
|
| 1082 |
+
) from error
|
| 1083 |
+
return _DockerImageIdentity(
|
| 1084 |
+
reference=self.docker_image,
|
| 1085 |
+
repository=self._image_reference.repository,
|
| 1086 |
+
version=self._image_reference.version,
|
| 1087 |
+
manifest_digest=self._image_reference.digest,
|
| 1088 |
+
image_id=image_id,
|
| 1089 |
+
os=image_os,
|
| 1090 |
+
architecture=architecture,
|
| 1091 |
+
)
|
| 1092 |
+
|
| 1093 |
+
def _ensure_docker_image(self) -> _DockerImageIdentity:
|
| 1094 |
+
if self._verified_image_identity is not None:
|
| 1095 |
+
return self._verified_image_identity
|
| 1096 |
+
self._run_docker_command(
|
| 1097 |
+
["docker", "version"],
|
| 1098 |
+
phase="Docker availability check",
|
| 1099 |
+
capture_output=True,
|
| 1100 |
+
text=True,
|
| 1101 |
+
check=True,
|
| 1102 |
+
)
|
| 1103 |
+
identity = self._inspect_docker_image(check=False)
|
| 1104 |
+
if identity is None:
|
| 1105 |
+
if not self.allow_pull:
|
| 1106 |
+
raise RuntimeError(
|
| 1107 |
+
"The pinned MMseqs2 image is not present locally and allow_pull=False. "
|
| 1108 |
+
"Preload the exact image out of band or opt in with allow_pull=True."
|
| 1109 |
+
)
|
| 1110 |
+
self._run_docker_command(
|
| 1111 |
+
["docker", "pull", self.docker_image],
|
| 1112 |
+
phase="image pull",
|
| 1113 |
+
check=True,
|
| 1114 |
+
capture_output=True,
|
| 1115 |
+
text=True,
|
| 1116 |
+
)
|
| 1117 |
+
identity = self._inspect_docker_image(check=True)
|
| 1118 |
+
if identity is None:
|
| 1119 |
+
raise RuntimeError("Docker image inspection succeeded without a verified identity")
|
| 1120 |
+
self._verified_image_identity = identity
|
| 1121 |
+
return identity
|
| 1122 |
+
|
| 1123 |
+
def _target_db_descriptor(self) -> dict[str, Any]:
|
| 1124 |
+
prefix = self._resolve_path_under_cwd(self.target_db)
|
| 1125 |
+
files: list[dict[str, Any]] = []
|
| 1126 |
+
for candidate in sorted(prefix.parent.glob(f"{prefix.name}*")):
|
| 1127 |
+
resolved = self._resolve_path_under_cwd(os.fspath(candidate), must_exist=True)
|
| 1128 |
+
if not resolved.is_file():
|
| 1129 |
+
continue
|
| 1130 |
+
stat_result = resolved.stat()
|
| 1131 |
+
files.append(
|
| 1132 |
+
{
|
| 1133 |
+
"path": resolved.relative_to(self._working_root()).as_posix(),
|
| 1134 |
+
"size": stat_result.st_size,
|
| 1135 |
+
"mtime_ns": stat_result.st_mtime_ns,
|
| 1136 |
+
}
|
| 1137 |
+
)
|
| 1138 |
+
if not files:
|
| 1139 |
+
raise FileNotFoundError(
|
| 1140 |
+
f"No MMseqs2 database files found for target_db prefix {self.target_db!r}"
|
| 1141 |
+
)
|
| 1142 |
+
derived_identity = _json_sha256(files)
|
| 1143 |
+
return {
|
| 1144 |
+
"prefix": prefix.relative_to(self._working_root()).as_posix(),
|
| 1145 |
+
"identity": self.target_db_identity or derived_identity,
|
| 1146 |
+
"identity_kind": "explicit" if self.target_db_identity is not None else "file-metadata",
|
| 1147 |
+
"files": files,
|
| 1148 |
+
}
|
| 1149 |
+
|
| 1150 |
+
def _request_provenance(self, sequence: str) -> dict[str, Any]:
|
| 1151 |
+
return {
|
| 1152 |
+
"provider": "mmseqs2",
|
| 1153 |
+
"sequence_sha256": hashlib.sha256(sequence.encode("utf-8")).hexdigest(),
|
| 1154 |
+
"image": {
|
| 1155 |
+
"reference": self.docker_image,
|
| 1156 |
+
"repository": self._image_reference.repository,
|
| 1157 |
+
"version": self._image_reference.version,
|
| 1158 |
+
"manifest_digest": self._image_reference.digest,
|
| 1159 |
+
},
|
| 1160 |
+
"platform": {"os": "linux", "architecture": _docker_architecture()},
|
| 1161 |
+
"target_db": self._target_db_descriptor(),
|
| 1162 |
+
"parameters": {
|
| 1163 |
+
"sensitivity": self.sensitivity,
|
| 1164 |
+
"max_seqs": self.max_seqs,
|
| 1165 |
+
"min_seq_id": self.min_seq_id,
|
| 1166 |
+
"coverage": self.coverage,
|
| 1167 |
+
"split_memory_limit": self.split_memory_limit,
|
| 1168 |
+
"use_gpu": self.use_gpu,
|
| 1169 |
+
"allow_network": self.allow_network,
|
| 1170 |
+
},
|
| 1171 |
+
}
|
| 1172 |
+
|
| 1173 |
+
def _load_cached_result(
|
| 1174 |
+
self,
|
| 1175 |
+
a3m_output: str,
|
| 1176 |
+
provenance_path: str,
|
| 1177 |
+
request_provenance: dict[str, Any],
|
| 1178 |
+
) -> bool:
|
| 1179 |
+
if not Path(a3m_output).is_file() or not Path(provenance_path).is_file():
|
| 1180 |
+
return False
|
| 1181 |
+
try:
|
| 1182 |
+
with open(provenance_path, encoding="utf-8") as handle:
|
| 1183 |
+
payload = json.load(handle)
|
| 1184 |
+
if not isinstance(payload, dict):
|
| 1185 |
+
return False
|
| 1186 |
+
if payload.get("schema_version") != self._PROVENANCE_SCHEMA_VERSION:
|
| 1187 |
+
return False
|
| 1188 |
+
if payload.get("request") != request_provenance:
|
| 1189 |
+
return False
|
| 1190 |
+
request_identity = _json_sha256(request_provenance)
|
| 1191 |
+
if payload.get("request_identity_sha256") != request_identity:
|
| 1192 |
+
return False
|
| 1193 |
+
runtime = payload.get("runtime")
|
| 1194 |
+
if not isinstance(runtime, dict):
|
| 1195 |
+
return False
|
| 1196 |
+
if runtime.get("reference") != self.docker_image:
|
| 1197 |
+
return False
|
| 1198 |
+
if runtime.get("repository") != self._image_reference.repository:
|
| 1199 |
+
return False
|
| 1200 |
+
if runtime.get("version") != self._image_reference.version:
|
| 1201 |
+
return False
|
| 1202 |
+
if runtime.get("manifest_digest") != self._image_reference.digest:
|
| 1203 |
+
return False
|
| 1204 |
+
if runtime.get("os") != "linux":
|
| 1205 |
+
return False
|
| 1206 |
+
if runtime.get("architecture") != _docker_architecture():
|
| 1207 |
+
return False
|
| 1208 |
+
image_id = runtime.get("image_id")
|
| 1209 |
+
if not isinstance(image_id, str) or _SHA256_DIGEST_RE.fullmatch(image_id) is None:
|
| 1210 |
+
return False
|
| 1211 |
+
cache_identity = _json_sha256(
|
| 1212 |
+
{"request_identity_sha256": request_identity, "runtime": runtime}
|
| 1213 |
+
)
|
| 1214 |
+
if payload.get("cache_identity_sha256") != cache_identity:
|
| 1215 |
+
return False
|
| 1216 |
+
result = payload.get("result")
|
| 1217 |
+
if not isinstance(result, dict):
|
| 1218 |
+
return False
|
| 1219 |
+
if result.get("path") != Path(a3m_output).name:
|
| 1220 |
+
return False
|
| 1221 |
+
if result.get("size") != Path(a3m_output).stat().st_size:
|
| 1222 |
+
return False
|
| 1223 |
+
return result.get("sha256") == _file_sha256(a3m_output)
|
| 1224 |
+
except (OSError, UnicodeError, json.JSONDecodeError, TypeError, ValueError):
|
| 1225 |
+
return False
|
| 1226 |
+
|
| 1227 |
+
def _store_result_provenance(
|
| 1228 |
+
self,
|
| 1229 |
+
provenance_path: str,
|
| 1230 |
+
a3m_output: str,
|
| 1231 |
+
request_provenance: dict[str, Any],
|
| 1232 |
+
identity: _DockerImageIdentity,
|
| 1233 |
+
) -> None:
|
| 1234 |
+
request_identity = _json_sha256(request_provenance)
|
| 1235 |
+
runtime = identity.to_dict()
|
| 1236 |
+
payload = {
|
| 1237 |
+
"schema_version": self._PROVENANCE_SCHEMA_VERSION,
|
| 1238 |
+
"request": request_provenance,
|
| 1239 |
+
"request_identity_sha256": request_identity,
|
| 1240 |
+
"runtime": runtime,
|
| 1241 |
+
"cache_identity_sha256": _json_sha256(
|
| 1242 |
+
{"request_identity_sha256": request_identity, "runtime": runtime}
|
| 1243 |
+
),
|
| 1244 |
+
"result": {
|
| 1245 |
+
"path": Path(a3m_output).name,
|
| 1246 |
+
"size": Path(a3m_output).stat().st_size,
|
| 1247 |
+
"sha256": _file_sha256(a3m_output),
|
| 1248 |
+
},
|
| 1249 |
+
}
|
| 1250 |
+
output_dir = os.path.dirname(provenance_path) or "."
|
| 1251 |
+
temporary_path: str | None = None
|
| 1252 |
+
try:
|
| 1253 |
+
with tempfile.NamedTemporaryFile(
|
| 1254 |
+
mode="w",
|
| 1255 |
+
encoding="utf-8",
|
| 1256 |
+
dir=output_dir,
|
| 1257 |
+
prefix=".mmseqs2-provenance-",
|
| 1258 |
+
suffix=".tmp",
|
| 1259 |
+
delete=False,
|
| 1260 |
+
) as handle:
|
| 1261 |
+
temporary_path = handle.name
|
| 1262 |
+
json.dump(payload, handle, indent=2, sort_keys=True)
|
| 1263 |
+
handle.write("\n")
|
| 1264 |
+
handle.flush()
|
| 1265 |
+
os.fsync(handle.fileno())
|
| 1266 |
+
os.replace(temporary_path, provenance_path)
|
| 1267 |
+
temporary_path = None
|
| 1268 |
+
finally:
|
| 1269 |
+
if temporary_path is not None:
|
| 1270 |
+
Path(temporary_path).unlink(missing_ok=True)
|
| 1271 |
+
|
| 1272 |
+
def create_db(self, fasta_path: str, db_path: str) -> str:
|
| 1273 |
+
self._validate_paths_under_cwd(fasta_path, db_path)
|
| 1274 |
+
os.makedirs(os.path.dirname(db_path) or ".", exist_ok=True)
|
| 1275 |
+
if os.path.exists(f"{db_path}.dbtype"):
|
| 1276 |
+
return db_path
|
| 1277 |
+
self._ensure_docker_image()
|
| 1278 |
+
self._run_docker_command(
|
| 1279 |
+
[
|
| 1280 |
+
*self._docker_base_cmd(),
|
| 1281 |
+
"createdb",
|
| 1282 |
+
self._path_in_container(fasta_path),
|
| 1283 |
+
self._path_in_container(db_path),
|
| 1284 |
+
],
|
| 1285 |
+
phase="createdb",
|
| 1286 |
+
check=True,
|
| 1287 |
+
capture_output=True,
|
| 1288 |
+
text=True,
|
| 1289 |
+
)
|
| 1290 |
+
return db_path
|
| 1291 |
+
|
| 1292 |
+
def create_index(self, db_path: str, tmp_dir: str | None = None) -> None:
|
| 1293 |
+
if tmp_dir is None:
|
| 1294 |
+
tmp_dir = os.path.join(os.path.dirname(db_path), "tmp_index")
|
| 1295 |
+
self._validate_paths_under_cwd(db_path, tmp_dir)
|
| 1296 |
+
os.makedirs(tmp_dir, exist_ok=True)
|
| 1297 |
+
self._ensure_docker_image()
|
| 1298 |
+
self._run_docker_command(
|
| 1299 |
+
[
|
| 1300 |
+
*self._docker_base_cmd(),
|
| 1301 |
+
"createindex",
|
| 1302 |
+
self._path_in_container(db_path),
|
| 1303 |
+
self._path_in_container(tmp_dir),
|
| 1304 |
+
],
|
| 1305 |
+
phase="createindex",
|
| 1306 |
+
check=True,
|
| 1307 |
+
capture_output=True,
|
| 1308 |
+
text=True,
|
| 1309 |
+
)
|
| 1310 |
+
|
| 1311 |
+
def search(self, sequence: str, output_dir: str, seq_id: str | None = None) -> str:
|
| 1312 |
+
if (
|
| 1313 |
+
not isinstance(sequence, str)
|
| 1314 |
+
or _SAFE_QUERY_SEQUENCE_RE.fullmatch(sequence) is None
|
| 1315 |
+
):
|
| 1316 |
+
raise ValueError(
|
| 1317 |
+
"sequence must be a non-empty unaligned ASCII protein sequence"
|
| 1318 |
+
)
|
| 1319 |
+
if seq_id is None:
|
| 1320 |
+
seq_id = self._seq_hash(sequence)
|
| 1321 |
+
seq_output_dir = _sequence_output_dir(output_dir, seq_id)
|
| 1322 |
+
a3m_output = os.path.join(seq_output_dir, f"{seq_id}.a3m")
|
| 1323 |
+
provenance_path = os.path.join(seq_output_dir, self._PROVENANCE_FILENAME)
|
| 1324 |
+
self._validate_paths_under_cwd(seq_output_dir, self.target_db)
|
| 1325 |
+
request_provenance = self._request_provenance(sequence)
|
| 1326 |
+
if self._load_cached_result(a3m_output, provenance_path, request_provenance):
|
| 1327 |
+
return a3m_output
|
| 1328 |
+
|
| 1329 |
+
identity = self._ensure_docker_image()
|
| 1330 |
+
os.makedirs(seq_output_dir, exist_ok=True)
|
| 1331 |
+
Path(a3m_output).unlink(missing_ok=True)
|
| 1332 |
+
Path(provenance_path).unlink(missing_ok=True)
|
| 1333 |
+
query_fasta = os.path.join(seq_output_dir, "query.fasta")
|
| 1334 |
+
write_fasta_sequences(query_fasta, {seq_id: sequence})
|
| 1335 |
+
query_db = os.path.join(seq_output_dir, "queryDB")
|
| 1336 |
+
result_db = os.path.join(seq_output_dir, "resultDB")
|
| 1337 |
+
tmp_dir = os.path.join(seq_output_dir, "tmp")
|
| 1338 |
+
os.makedirs(tmp_dir, exist_ok=True)
|
| 1339 |
+
self._validate_paths_under_cwd(
|
| 1340 |
+
query_fasta, query_db, self.target_db, seq_output_dir, result_db, tmp_dir
|
| 1341 |
+
)
|
| 1342 |
+
|
| 1343 |
+
docker_base = self._docker_base_cmd()
|
| 1344 |
+
self._run_docker_command(
|
| 1345 |
+
[
|
| 1346 |
+
*docker_base,
|
| 1347 |
+
"createdb",
|
| 1348 |
+
self._path_in_container(query_fasta),
|
| 1349 |
+
self._path_in_container(query_db),
|
| 1350 |
+
],
|
| 1351 |
+
phase="query createdb",
|
| 1352 |
+
check=True,
|
| 1353 |
+
capture_output=True,
|
| 1354 |
+
text=True,
|
| 1355 |
+
)
|
| 1356 |
+
search_cmd = [
|
| 1357 |
+
*docker_base,
|
| 1358 |
+
"search",
|
| 1359 |
+
self._path_in_container(query_db),
|
| 1360 |
+
self._path_in_container(self.target_db),
|
| 1361 |
+
self._path_in_container(result_db),
|
| 1362 |
+
self._path_in_container(tmp_dir),
|
| 1363 |
+
"-s",
|
| 1364 |
+
str(self.sensitivity),
|
| 1365 |
+
"--max-seqs",
|
| 1366 |
+
str(self.max_seqs),
|
| 1367 |
+
"--min-seq-id",
|
| 1368 |
+
str(self.min_seq_id),
|
| 1369 |
+
"-c",
|
| 1370 |
+
str(self.coverage),
|
| 1371 |
+
]
|
| 1372 |
+
if self.split_memory_limit is not None:
|
| 1373 |
+
search_cmd.extend(["--split-memory-limit", self.split_memory_limit])
|
| 1374 |
+
if self.use_gpu and torch.cuda.is_available():
|
| 1375 |
+
search_cmd.extend(["--gpu", "1"])
|
| 1376 |
+
self._run_docker_command(
|
| 1377 |
+
search_cmd,
|
| 1378 |
+
phase="search",
|
| 1379 |
+
check=True,
|
| 1380 |
+
capture_output=True,
|
| 1381 |
+
text=True,
|
| 1382 |
+
)
|
| 1383 |
+
self._run_docker_command(
|
| 1384 |
+
[
|
| 1385 |
+
*docker_base,
|
| 1386 |
+
"result2msa",
|
| 1387 |
+
self._path_in_container(query_db),
|
| 1388 |
+
self._path_in_container(self.target_db),
|
| 1389 |
+
self._path_in_container(result_db),
|
| 1390 |
+
self._path_in_container(a3m_output),
|
| 1391 |
+
"--msa-format-mode",
|
| 1392 |
+
"6",
|
| 1393 |
+
],
|
| 1394 |
+
phase="result2msa",
|
| 1395 |
+
check=True,
|
| 1396 |
+
capture_output=True,
|
| 1397 |
+
text=True,
|
| 1398 |
+
)
|
| 1399 |
+
if not Path(a3m_output).is_file():
|
| 1400 |
+
raise RuntimeError("MMseqs2 result2msa did not create a regular A3M file")
|
| 1401 |
+
resolved_a3m = self._resolve_path_under_cwd(a3m_output, must_exist=True)
|
| 1402 |
+
if not resolved_a3m.is_file():
|
| 1403 |
+
raise RuntimeError("MMseqs2 result2msa did not create a regular A3M file")
|
| 1404 |
+
self._store_result_provenance(
|
| 1405 |
+
provenance_path,
|
| 1406 |
+
a3m_output,
|
| 1407 |
+
request_provenance,
|
| 1408 |
+
identity,
|
| 1409 |
+
)
|
| 1410 |
+
for pattern in ["queryDB*", "resultDB*"]:
|
| 1411 |
+
for path in Path(seq_output_dir).glob(pattern):
|
| 1412 |
+
path.unlink(missing_ok=True)
|
| 1413 |
+
tmp_path = Path(tmp_dir)
|
| 1414 |
+
if tmp_path.exists():
|
| 1415 |
+
shutil.rmtree(tmp_path, ignore_errors=True)
|
| 1416 |
+
return a3m_output
|
| 1417 |
+
|
| 1418 |
+
def batch_search(
|
| 1419 |
+
self,
|
| 1420 |
+
sequences: list[str],
|
| 1421 |
+
output_dir: str,
|
| 1422 |
+
seq_ids: list[str] | None = None,
|
| 1423 |
+
continue_on_error: bool = True,
|
| 1424 |
+
) -> dict[str, str]:
|
| 1425 |
+
if seq_ids is None:
|
| 1426 |
+
seq_ids = [self._seq_hash(seq) for seq in sequences]
|
| 1427 |
+
if len(seq_ids) != len(sequences):
|
| 1428 |
+
raise ValueError("seq_ids must contain exactly one identifier per sequence")
|
| 1429 |
+
self._validate_paths_under_cwd(output_dir)
|
| 1430 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 1431 |
+
results: dict[str, str] = {}
|
| 1432 |
+
for seq, sid in tqdm(
|
| 1433 |
+
list(zip(sequences, seq_ids, strict=True)),
|
| 1434 |
+
desc="Searching homologues",
|
| 1435 |
+
):
|
| 1436 |
+
try:
|
| 1437 |
+
results[seq] = self.search(seq, output_dir, sid)
|
| 1438 |
+
except Exception as error:
|
| 1439 |
+
if not continue_on_error:
|
| 1440 |
+
raise
|
| 1441 |
+
_get_logger().warning(
|
| 1442 |
+
"Homologue search failed and was skipped: "
|
| 1443 |
+
"provider=mmseqs2 seq_id=%s error_type=%s",
|
| 1444 |
+
sid,
|
| 1445 |
+
type(error).__name__,
|
| 1446 |
+
)
|
| 1447 |
+
return results
|
| 1448 |
+
|
| 1449 |
+
|
| 1450 |
+
@dataclass(frozen=True)
|
| 1451 |
+
class _ColabFoldResponse:
|
| 1452 |
+
"""Minimal response surface required by the ColabFold API client."""
|
| 1453 |
+
|
| 1454 |
+
status_code: int
|
| 1455 |
+
headers: dict[str, str]
|
| 1456 |
+
content: bytes
|
| 1457 |
+
|
| 1458 |
+
def json(self) -> dict[str, Any]:
|
| 1459 |
+
value = json.loads(self.content.decode("utf-8"))
|
| 1460 |
+
if not isinstance(value, dict):
|
| 1461 |
+
raise ValueError("ColabFold returned a non-object JSON response")
|
| 1462 |
+
return value
|
| 1463 |
+
|
| 1464 |
+
|
| 1465 |
+
class ColabFoldSearcher:
|
| 1466 |
+
def __init__(
|
| 1467 |
+
self,
|
| 1468 |
+
host_url: str = COLABFOLD_HOST,
|
| 1469 |
+
user_agent: str = "",
|
| 1470 |
+
mode: str = "env",
|
| 1471 |
+
timeout: float = 30.0,
|
| 1472 |
+
max_retries: int = 10,
|
| 1473 |
+
base_delay: float = 1.0,
|
| 1474 |
+
max_delay: float = 60.0,
|
| 1475 |
+
inter_request_delay: tuple[float, float] = (1.0, 3.0),
|
| 1476 |
+
max_wait_time: int = 600,
|
| 1477 |
+
) -> None:
|
| 1478 |
+
self.host_url = host_url.rstrip("/")
|
| 1479 |
+
self.mode = mode
|
| 1480 |
+
self.timeout = timeout
|
| 1481 |
+
self.max_retries = max_retries
|
| 1482 |
+
self.base_delay = base_delay
|
| 1483 |
+
self.max_delay = max_delay
|
| 1484 |
+
self.inter_request_delay = inter_request_delay
|
| 1485 |
+
self.max_wait_time = max_wait_time
|
| 1486 |
+
self.headers = {"User-Agent": user_agent} if user_agent else {}
|
| 1487 |
+
|
| 1488 |
+
@staticmethod
|
| 1489 |
+
def _seq_hash(sequence: str) -> str:
|
| 1490 |
+
return hashlib.md5(sequence.encode()).hexdigest()[:12]
|
| 1491 |
+
|
| 1492 |
+
def _backoff_delay(self, attempt: int) -> float:
|
| 1493 |
+
delay = min(self.base_delay * (2**attempt), self.max_delay)
|
| 1494 |
+
return min(delay + random.uniform(0, delay * 0.5), self.max_delay)
|
| 1495 |
+
|
| 1496 |
+
def _retry_after_delay(self, headers: dict[str, str], attempt: int) -> float:
|
| 1497 |
+
raw_value = next(
|
| 1498 |
+
(value for name, value in headers.items() if name.lower() == "retry-after"),
|
| 1499 |
+
None,
|
| 1500 |
+
)
|
| 1501 |
+
if raw_value is None:
|
| 1502 |
+
return self._backoff_delay(attempt)
|
| 1503 |
+
try:
|
| 1504 |
+
delay = float(raw_value)
|
| 1505 |
+
except (TypeError, ValueError):
|
| 1506 |
+
try:
|
| 1507 |
+
retry_at = parsedate_to_datetime(raw_value)
|
| 1508 |
+
if retry_at.tzinfo is None:
|
| 1509 |
+
retry_at = retry_at.replace(tzinfo=UTC)
|
| 1510 |
+
delay = (retry_at - datetime.now(UTC)).total_seconds()
|
| 1511 |
+
except (TypeError, ValueError, OverflowError):
|
| 1512 |
+
return self._backoff_delay(attempt)
|
| 1513 |
+
if not math.isfinite(delay):
|
| 1514 |
+
return self._backoff_delay(attempt)
|
| 1515 |
+
return min(max(0.0, delay), self.max_delay)
|
| 1516 |
+
|
| 1517 |
+
def _remaining_timeout(self, deadline: float | None, context: str) -> float:
|
| 1518 |
+
if deadline is None:
|
| 1519 |
+
return self.timeout
|
| 1520 |
+
remaining = deadline - time.monotonic()
|
| 1521 |
+
if remaining <= 0:
|
| 1522 |
+
raise TimeoutError(f"{context} exceeded the {self.max_wait_time}s deadline")
|
| 1523 |
+
return min(self.timeout, remaining)
|
| 1524 |
+
|
| 1525 |
+
def _sleep_with_deadline(
|
| 1526 |
+
self,
|
| 1527 |
+
delay: float,
|
| 1528 |
+
deadline: float | None,
|
| 1529 |
+
context: str,
|
| 1530 |
+
) -> None:
|
| 1531 |
+
delay = max(0.0, delay)
|
| 1532 |
+
if deadline is None:
|
| 1533 |
+
time.sleep(delay)
|
| 1534 |
+
return
|
| 1535 |
+
remaining = deadline - time.monotonic()
|
| 1536 |
+
if remaining <= 0:
|
| 1537 |
+
raise TimeoutError(f"{context} exceeded the {self.max_wait_time}s deadline")
|
| 1538 |
+
if delay >= remaining:
|
| 1539 |
+
time.sleep(remaining)
|
| 1540 |
+
raise TimeoutError(f"{context} exceeded the {self.max_wait_time}s deadline")
|
| 1541 |
+
time.sleep(delay)
|
| 1542 |
+
|
| 1543 |
+
@staticmethod
|
| 1544 |
+
def _http_error(response: _ColabFoldResponse, url: str) -> RuntimeError:
|
| 1545 |
+
return RuntimeError(f"ColabFold request to {url} returned HTTP {response.status_code}")
|
| 1546 |
+
|
| 1547 |
+
def _request_with_retries(
|
| 1548 |
+
self,
|
| 1549 |
+
method: str,
|
| 1550 |
+
url: str,
|
| 1551 |
+
*,
|
| 1552 |
+
deadline: float | None = None,
|
| 1553 |
+
**kwargs: Any,
|
| 1554 |
+
) -> _ColabFoldResponse:
|
| 1555 |
+
payload = kwargs.pop("data", None)
|
| 1556 |
+
if kwargs:
|
| 1557 |
+
unexpected = ", ".join(sorted(kwargs))
|
| 1558 |
+
raise TypeError(f"Unsupported HTTP request options: {unexpected}")
|
| 1559 |
+
|
| 1560 |
+
encoded_payload = None
|
| 1561 |
+
headers = dict(self.headers)
|
| 1562 |
+
if payload is not None:
|
| 1563 |
+
encoded_payload = urllib.parse.urlencode(payload).encode("utf-8")
|
| 1564 |
+
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
| 1565 |
+
|
| 1566 |
+
last_error: BaseException | None = None
|
| 1567 |
+
for attempt in range(self.max_retries):
|
| 1568 |
+
try:
|
| 1569 |
+
request = urllib.request.Request(
|
| 1570 |
+
url,
|
| 1571 |
+
data=encoded_payload,
|
| 1572 |
+
headers=headers,
|
| 1573 |
+
method=method.upper(),
|
| 1574 |
+
)
|
| 1575 |
+
try:
|
| 1576 |
+
timeout = self._remaining_timeout(deadline, f"Request to {url}")
|
| 1577 |
+
with urllib.request.urlopen(request, timeout=timeout) as stream:
|
| 1578 |
+
response = _ColabFoldResponse(
|
| 1579 |
+
status_code=int(stream.status),
|
| 1580 |
+
headers={name.lower(): value for name, value in stream.headers.items()},
|
| 1581 |
+
content=stream.read(),
|
| 1582 |
+
)
|
| 1583 |
+
except urllib.error.HTTPError as error:
|
| 1584 |
+
response = _ColabFoldResponse(
|
| 1585 |
+
status_code=int(error.code),
|
| 1586 |
+
headers={name.lower(): value for name, value in error.headers.items()},
|
| 1587 |
+
content=error.read(),
|
| 1588 |
+
)
|
| 1589 |
+
if response.status_code == 429:
|
| 1590 |
+
last_error = self._http_error(response, url)
|
| 1591 |
+
if attempt + 1 >= self.max_retries:
|
| 1592 |
+
break
|
| 1593 |
+
self._sleep_with_deadline(
|
| 1594 |
+
self._retry_after_delay(response.headers, attempt),
|
| 1595 |
+
deadline,
|
| 1596 |
+
f"Request to {url}",
|
| 1597 |
+
)
|
| 1598 |
+
continue
|
| 1599 |
+
if response.status_code >= 500:
|
| 1600 |
+
last_error = self._http_error(response, url)
|
| 1601 |
+
if attempt + 1 >= self.max_retries:
|
| 1602 |
+
break
|
| 1603 |
+
self._sleep_with_deadline(
|
| 1604 |
+
self._backoff_delay(attempt),
|
| 1605 |
+
deadline,
|
| 1606 |
+
f"Request to {url}",
|
| 1607 |
+
)
|
| 1608 |
+
continue
|
| 1609 |
+
if not 200 <= response.status_code < 300:
|
| 1610 |
+
raise self._http_error(response, url)
|
| 1611 |
+
return response
|
| 1612 |
+
except (TimeoutError, urllib.error.URLError) as error:
|
| 1613 |
+
last_error = error
|
| 1614 |
+
if deadline is not None and time.monotonic() >= deadline:
|
| 1615 |
+
raise TimeoutError(
|
| 1616 |
+
f"Request to {url} exceeded the {self.max_wait_time}s deadline"
|
| 1617 |
+
) from error
|
| 1618 |
+
if attempt + 1 >= self.max_retries:
|
| 1619 |
+
break
|
| 1620 |
+
self._sleep_with_deadline(
|
| 1621 |
+
self._backoff_delay(attempt),
|
| 1622 |
+
deadline,
|
| 1623 |
+
f"Request to {url}",
|
| 1624 |
+
)
|
| 1625 |
+
raise RuntimeError(
|
| 1626 |
+
f"Request to {url} failed after {self.max_retries} attempts"
|
| 1627 |
+
) from last_error
|
| 1628 |
+
|
| 1629 |
+
def _submit(
|
| 1630 |
+
self,
|
| 1631 |
+
sequence: str,
|
| 1632 |
+
mode: str | None = None,
|
| 1633 |
+
deadline: float | None = None,
|
| 1634 |
+
) -> dict[str, Any]:
|
| 1635 |
+
mode = mode or self.mode
|
| 1636 |
+
query = f">101\n{sequence}\n"
|
| 1637 |
+
for attempt in range(self.max_retries):
|
| 1638 |
+
response = self._request_with_retries(
|
| 1639 |
+
"POST",
|
| 1640 |
+
f"{self.host_url}/ticket/msa",
|
| 1641 |
+
data={"q": query, "mode": mode},
|
| 1642 |
+
deadline=deadline,
|
| 1643 |
+
)
|
| 1644 |
+
data = response.json()
|
| 1645 |
+
status = data.get("status", "UNKNOWN")
|
| 1646 |
+
if status in ("RATELIMIT", "UNKNOWN"):
|
| 1647 |
+
if attempt + 1 >= self.max_retries:
|
| 1648 |
+
break
|
| 1649 |
+
self._sleep_with_deadline(
|
| 1650 |
+
self._backoff_delay(attempt),
|
| 1651 |
+
deadline,
|
| 1652 |
+
"ColabFold job submission",
|
| 1653 |
+
)
|
| 1654 |
+
continue
|
| 1655 |
+
return data
|
| 1656 |
+
raise RuntimeError(f"Failed to submit sequence after {self.max_retries} attempts")
|
| 1657 |
+
|
| 1658 |
+
def _poll(self, ticket_id: str, deadline: float | None = None) -> dict[str, Any]:
|
| 1659 |
+
if deadline is None:
|
| 1660 |
+
deadline = time.monotonic() + self.max_wait_time
|
| 1661 |
+
poll_interval = 1.0
|
| 1662 |
+
while True:
|
| 1663 |
+
response = self._request_with_retries(
|
| 1664 |
+
"GET",
|
| 1665 |
+
f"{self.host_url}/ticket/{ticket_id}",
|
| 1666 |
+
deadline=deadline,
|
| 1667 |
+
)
|
| 1668 |
+
data = response.json()
|
| 1669 |
+
status = data.get("status", "ERROR")
|
| 1670 |
+
if status in ("COMPLETE", "ERROR"):
|
| 1671 |
+
return data
|
| 1672 |
+
if status not in ("RUNNING", "PENDING", "UNKNOWN"):
|
| 1673 |
+
return data
|
| 1674 |
+
wait = min(poll_interval + random.uniform(0, 0.5), 5.0)
|
| 1675 |
+
self._sleep_with_deadline(wait, deadline, f"Job {ticket_id}")
|
| 1676 |
+
poll_interval = min(poll_interval + 1.0, 5.0)
|
| 1677 |
+
|
| 1678 |
+
def _download(
|
| 1679 |
+
self,
|
| 1680 |
+
ticket_id: str,
|
| 1681 |
+
output_path: str,
|
| 1682 |
+
deadline: float | None = None,
|
| 1683 |
+
) -> None:
|
| 1684 |
+
response = self._request_with_retries(
|
| 1685 |
+
"GET",
|
| 1686 |
+
f"{self.host_url}/result/download/{ticket_id}",
|
| 1687 |
+
deadline=deadline,
|
| 1688 |
+
)
|
| 1689 |
+
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
| 1690 |
+
with open(output_path, "wb") as handle:
|
| 1691 |
+
handle.write(response.content)
|
| 1692 |
+
|
| 1693 |
+
def _extract_a3m(self, tar_path: str, output_dir: str, seq_id: str) -> str:
|
| 1694 |
+
with tarfile.open(tar_path) as tar:
|
| 1695 |
+
_safe_extract_tar(tar, output_dir)
|
| 1696 |
+
|
| 1697 |
+
uniref_a3m = os.path.join(output_dir, "uniref.a3m")
|
| 1698 |
+
env_a3m = os.path.join(output_dir, "bfd.mgnify30.metaeuk30.smag30.a3m")
|
| 1699 |
+
a3m_files: list[str] = []
|
| 1700 |
+
if os.path.exists(uniref_a3m):
|
| 1701 |
+
a3m_files.append(uniref_a3m)
|
| 1702 |
+
if "env" in self.mode and os.path.exists(env_a3m):
|
| 1703 |
+
a3m_files.append(env_a3m)
|
| 1704 |
+
combined_path = os.path.join(output_dir, f"{seq_id}.a3m")
|
| 1705 |
+
if len(a3m_files) == 1:
|
| 1706 |
+
os.replace(a3m_files[0], combined_path)
|
| 1707 |
+
elif len(a3m_files) > 1:
|
| 1708 |
+
with open(combined_path, "w", encoding="utf-8") as out_handle:
|
| 1709 |
+
for a3m_file in a3m_files:
|
| 1710 |
+
with open(a3m_file, encoding="utf-8") as in_handle:
|
| 1711 |
+
out_handle.write(in_handle.read())
|
| 1712 |
+
else:
|
| 1713 |
+
raise RuntimeError("No .a3m files found in downloaded archive")
|
| 1714 |
+
if os.path.exists(tar_path):
|
| 1715 |
+
os.remove(tar_path)
|
| 1716 |
+
for a3m_file in a3m_files:
|
| 1717 |
+
if os.path.exists(a3m_file) and a3m_file != combined_path:
|
| 1718 |
+
os.remove(a3m_file)
|
| 1719 |
+
return combined_path
|
| 1720 |
+
|
| 1721 |
+
def search(self, sequence: str, output_dir: str, seq_id: str | None = None) -> str:
|
| 1722 |
+
if seq_id is None:
|
| 1723 |
+
seq_id = self._seq_hash(sequence)
|
| 1724 |
+
seq_output_dir = _sequence_output_dir(output_dir, seq_id)
|
| 1725 |
+
a3m_output = os.path.join(seq_output_dir, f"{seq_id}.a3m")
|
| 1726 |
+
if os.path.exists(a3m_output):
|
| 1727 |
+
return a3m_output
|
| 1728 |
+
os.makedirs(seq_output_dir, exist_ok=True)
|
| 1729 |
+
deadline = time.monotonic() + self.max_wait_time
|
| 1730 |
+
result = self._submit(sequence, deadline=deadline)
|
| 1731 |
+
status = result.get("status", "UNKNOWN")
|
| 1732 |
+
if status == "ERROR":
|
| 1733 |
+
raise RuntimeError(f"ColabFold API error for {seq_id}")
|
| 1734 |
+
if status == "MAINTENANCE":
|
| 1735 |
+
raise RuntimeError("ColabFold API is under maintenance")
|
| 1736 |
+
ticket_id = result["id"]
|
| 1737 |
+
result = self._poll(ticket_id, deadline=deadline)
|
| 1738 |
+
status = result.get("status", "UNKNOWN")
|
| 1739 |
+
if status != "COMPLETE":
|
| 1740 |
+
raise RuntimeError(f"Job failed for {seq_id}: {status}")
|
| 1741 |
+
tar_path = os.path.join(seq_output_dir, f"{seq_id}.tar.gz")
|
| 1742 |
+
self._download(ticket_id, tar_path, deadline=deadline)
|
| 1743 |
+
return self._extract_a3m(tar_path, seq_output_dir, seq_id)
|
| 1744 |
+
|
| 1745 |
+
def batch_search(
|
| 1746 |
+
self,
|
| 1747 |
+
sequences: list[str],
|
| 1748 |
+
output_dir: str,
|
| 1749 |
+
seq_ids: list[str] | None = None,
|
| 1750 |
+
continue_on_error: bool = True,
|
| 1751 |
+
) -> dict[str, str]:
|
| 1752 |
+
if seq_ids is None:
|
| 1753 |
+
seq_ids = [self._seq_hash(seq) for seq in sequences]
|
| 1754 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 1755 |
+
results: dict[str, str] = {}
|
| 1756 |
+
pairs = list(zip(sequences, seq_ids, strict=True))
|
| 1757 |
+
for i, (seq, sid) in enumerate(tqdm(pairs, desc="ColabFold search")):
|
| 1758 |
+
try:
|
| 1759 |
+
results[seq] = self.search(seq, output_dir, sid)
|
| 1760 |
+
except Exception as error:
|
| 1761 |
+
if not continue_on_error:
|
| 1762 |
+
raise
|
| 1763 |
+
_get_logger().warning(
|
| 1764 |
+
"Homologue search failed and was skipped: "
|
| 1765 |
+
"provider=colabfold seq_id=%s error_type=%s",
|
| 1766 |
+
sid,
|
| 1767 |
+
type(error).__name__,
|
| 1768 |
+
)
|
| 1769 |
+
if i < len(pairs) - 1:
|
| 1770 |
+
time.sleep(random.uniform(*self.inter_request_delay))
|
| 1771 |
+
return results
|
| 1772 |
+
|
| 1773 |
+
|
| 1774 |
+
def _make_homologue_searcher(
|
| 1775 |
+
provider: str, target_db: str | None, **kwargs
|
| 1776 |
+
) -> HomologueSearcher | ColabFoldSearcher:
|
| 1777 |
+
if provider == "mmseqs2":
|
| 1778 |
+
if target_db is None:
|
| 1779 |
+
raise ValueError("target_db is required for MMseqs2 homologue search")
|
| 1780 |
+
return HomologueSearcher(target_db=target_db, **kwargs)
|
| 1781 |
+
if provider == "colabfold":
|
| 1782 |
+
return ColabFoldSearcher(**kwargs)
|
| 1783 |
+
raise ValueError(f"Unknown homologue search provider: {provider}")
|
fastplms/models/e1/tokenizer.json
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "1.0",
|
| 3 |
+
"truncation": null,
|
| 4 |
+
"padding": {
|
| 5 |
+
"strategy": "BatchLongest",
|
| 6 |
+
"direction": "Right",
|
| 7 |
+
"pad_to_multiple_of": null,
|
| 8 |
+
"pad_id": 0,
|
| 9 |
+
"pad_type_id": 0,
|
| 10 |
+
"pad_token": "<pad>"
|
| 11 |
+
},
|
| 12 |
+
"added_tokens": [
|
| 13 |
+
{
|
| 14 |
+
"id": 0,
|
| 15 |
+
"content": "<pad>",
|
| 16 |
+
"single_word": false,
|
| 17 |
+
"lstrip": false,
|
| 18 |
+
"rstrip": false,
|
| 19 |
+
"normalized": false,
|
| 20 |
+
"special": true
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"id": 1,
|
| 24 |
+
"content": "<bos>",
|
| 25 |
+
"single_word": false,
|
| 26 |
+
"lstrip": false,
|
| 27 |
+
"rstrip": false,
|
| 28 |
+
"normalized": false,
|
| 29 |
+
"special": true
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"id": 2,
|
| 33 |
+
"content": "<eos>",
|
| 34 |
+
"single_word": false,
|
| 35 |
+
"lstrip": false,
|
| 36 |
+
"rstrip": false,
|
| 37 |
+
"normalized": false,
|
| 38 |
+
"special": true
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": 3,
|
| 42 |
+
"content": "<bos_glm>",
|
| 43 |
+
"single_word": false,
|
| 44 |
+
"lstrip": false,
|
| 45 |
+
"rstrip": false,
|
| 46 |
+
"normalized": false,
|
| 47 |
+
"special": true
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"id": 4,
|
| 51 |
+
"content": "<eos_span>",
|
| 52 |
+
"single_word": false,
|
| 53 |
+
"lstrip": false,
|
| 54 |
+
"rstrip": false,
|
| 55 |
+
"normalized": false,
|
| 56 |
+
"special": true
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"id": 5,
|
| 60 |
+
"content": "?",
|
| 61 |
+
"single_word": false,
|
| 62 |
+
"lstrip": false,
|
| 63 |
+
"rstrip": false,
|
| 64 |
+
"normalized": false,
|
| 65 |
+
"special": true
|
| 66 |
+
}
|
| 67 |
+
],
|
| 68 |
+
"normalizer": null,
|
| 69 |
+
"pre_tokenizer": {
|
| 70 |
+
"type": "ByteLevel",
|
| 71 |
+
"add_prefix_space": false,
|
| 72 |
+
"trim_offsets": true,
|
| 73 |
+
"use_regex": true
|
| 74 |
+
},
|
| 75 |
+
"post_processor": {
|
| 76 |
+
"type": "ByteLevel",
|
| 77 |
+
"add_prefix_space": true,
|
| 78 |
+
"trim_offsets": true,
|
| 79 |
+
"use_regex": true
|
| 80 |
+
},
|
| 81 |
+
"decoder": {
|
| 82 |
+
"type": "ByteLevel",
|
| 83 |
+
"add_prefix_space": true,
|
| 84 |
+
"trim_offsets": true,
|
| 85 |
+
"use_regex": true
|
| 86 |
+
},
|
| 87 |
+
"model": {
|
| 88 |
+
"type": "BPE",
|
| 89 |
+
"dropout": null,
|
| 90 |
+
"unk_token": "X",
|
| 91 |
+
"continuing_subword_prefix": null,
|
| 92 |
+
"end_of_word_suffix": null,
|
| 93 |
+
"fuse_unk": false,
|
| 94 |
+
"byte_fallback": false,
|
| 95 |
+
"ignore_merges": false,
|
| 96 |
+
"vocab": {
|
| 97 |
+
"<pad>": 0,
|
| 98 |
+
"<bos>": 1,
|
| 99 |
+
"<eos>": 2,
|
| 100 |
+
"<bos_glm>": 3,
|
| 101 |
+
"<eos_span>": 4,
|
| 102 |
+
"?": 5,
|
| 103 |
+
"1": 6,
|
| 104 |
+
"2": 7,
|
| 105 |
+
"A": 8,
|
| 106 |
+
"B": 9,
|
| 107 |
+
"C": 10,
|
| 108 |
+
"D": 11,
|
| 109 |
+
"E": 12,
|
| 110 |
+
"F": 13,
|
| 111 |
+
"G": 14,
|
| 112 |
+
"H": 15,
|
| 113 |
+
"I": 16,
|
| 114 |
+
"J": 17,
|
| 115 |
+
"K": 18,
|
| 116 |
+
"L": 19,
|
| 117 |
+
"M": 20,
|
| 118 |
+
"N": 21,
|
| 119 |
+
"O": 22,
|
| 120 |
+
"P": 23,
|
| 121 |
+
"Q": 24,
|
| 122 |
+
"R": 25,
|
| 123 |
+
"S": 26,
|
| 124 |
+
"T": 27,
|
| 125 |
+
"U": 28,
|
| 126 |
+
"V": 29,
|
| 127 |
+
"W": 30,
|
| 128 |
+
"X": 31,
|
| 129 |
+
"Y": 32,
|
| 130 |
+
"Z": 33
|
| 131 |
+
},
|
| 132 |
+
"merges": []
|
| 133 |
+
}
|
| 134 |
+
}
|
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,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 != "c99634d724e168524f43f56ad1d22af47c75db0cbe657a2a55a237104fe7b833":
|
| 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.e1.modeling_e1")
|
| 227 |
+
E1Config = _module_225.E1Config
|
| 228 |
+
E1Config.__module__ = __name__
|
| 229 |
+
E1ForMaskedLM = _module_225.E1ForMaskedLM
|
| 230 |
+
E1ForMaskedLM.__module__ = __name__
|
| 231 |
+
E1ForSequenceClassification = _module_225.E1ForSequenceClassification
|
| 232 |
+
E1ForSequenceClassification.__module__ = __name__
|
| 233 |
+
E1ForTokenClassification = _module_225.E1ForTokenClassification
|
| 234 |
+
E1ForTokenClassification.__module__ = __name__
|
| 235 |
+
E1Model = _module_225.E1Model
|
| 236 |
+
E1Model.__module__ = __name__
|
runtime-attestation.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": {
|
| 3 |
+
"LICENSES/FastPLMs-Apache-2.0.txt": "sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
|
| 4 |
+
"LICENSES/e1/ATTRIBUTION": "sha256:deb22b250f6491b649eda5c63e080dd56486b8d2736cea6a52ef875436214367",
|
| 5 |
+
"LICENSES/e1/Apache-2.0.txt": "sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
|
| 6 |
+
"LICENSES/e1/BSD-3-Clause.txt": "sha256:36e1987f2f17db7f8ad36cd7a37dbb7aeaaf0ab68b97ab4b9d3556f3a7a76ae8",
|
| 7 |
+
"LICENSES/e1/LICENSE": "sha256:8ef1dd556091544db3044164a8015424a3dcb3450fb3765a81b88463551bbe81",
|
| 8 |
+
"LICENSES/e1/MODIFICATIONS.md": "sha256:2506f47c0f5475af8e8ff2cff13eb8b79e8e25a08a054cdd617bf336536750ca",
|
| 9 |
+
"LICENSES/e1/NOTICE": "sha256:6de9db0320b4ee82f665c0951d8fd4cd53701a659c9dbce9bc3e3ea6afc4c6b3",
|
| 10 |
+
"README.md": "sha256:9f2d8fecae3f233b7546666d2487fca985627e25e8d9bb5277267d5de5a9ec10",
|
| 11 |
+
"THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
|
| 12 |
+
"config.json": "sha256:5a6ea49bbfefd19984b92448121277dfbecae387bb2fb360ba9cec29a508ed7a",
|
| 13 |
+
"fastplms/__init__.py": "sha256:4fb3196022ca8ec699d59d09bdbc5f0184195552b773698ab9b061fe3cd7df12",
|
| 14 |
+
"fastplms/attention/__init__.py": "sha256:f60b9fecfb4bcb37a4e7c26dc2f752b9035f9cbad627b4a84213f3a92ec88f7d",
|
| 15 |
+
"fastplms/attention/_core.py": "sha256:8f7ec5b65bd8b6c6fa4951d50d1c0e499abf03ae00914794b51fc410201e3e33",
|
| 16 |
+
"fastplms/attention/_kernel_lock.py": "sha256:85d8521a2af5f94fad3948af3814db0c866c414ee4d43df797b9bd6f980e947b",
|
| 17 |
+
"fastplms/attention/interfaces.py": "sha256:1c6f06a8e411e0f9bf6d230522205c93ae46ea58864006fbb892aa05e5ca5749",
|
| 18 |
+
"fastplms/embeddings/__init__.py": "sha256:47ff8cdf682d44037dd9edab133e2e60675d60786bd5cf0bffd1998f31985555",
|
| 19 |
+
"fastplms/embeddings/pooling.py": "sha256:a140266ed6b1cc344c8507edc5c6c4f2dce464c3db70ba4b16c7ac2ba2fad96e",
|
| 20 |
+
"fastplms/embeddings/runner.py": "sha256:23ee4727a918d6d331f7a0f89b823d149f1a791f0c5586e3496d7b6eb2ce97e0",
|
| 21 |
+
"fastplms/embeddings/storage.py": "sha256:3fbe2bab75092e5a4cadf4d27e4752181d597469a65a55db085ceef808ed418e",
|
| 22 |
+
"fastplms/embeddings/types.py": "sha256:119718a20989d1ae5a60fabc0f5e98bdc172c5163b04db3d4554ac3956b30e52",
|
| 23 |
+
"fastplms/models.toml": "sha256:05a8399f084a5babb5f0916cee7e564c4030767f3ff0230c4f46e539209847d1",
|
| 24 |
+
"fastplms/models/__init__.py": "sha256:5e48c2cb3877aa6f42f3b5411d53b16bba2e32827bbde634f47f174c5cb36f86",
|
| 25 |
+
"fastplms/models/e1/__init__.py": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
| 26 |
+
"fastplms/models/e1/attention.py": "sha256:8449b2bafba9573dfcfe4ad907e1fce24c55a4f279f1f72cd52227e48dcecf67",
|
| 27 |
+
"fastplms/models/e1/cache.py": "sha256:b17a0a743505f567b387d1edb8550f028a8770db1ba56ad8ad2ff0d3fce19c46",
|
| 28 |
+
"fastplms/models/e1/modeling_e1.py": "sha256:96545247491395291060e2cbb7b6b082ed6073e0fbf12d4656e490010aae5ae4",
|
| 29 |
+
"fastplms/models/e1/preparation.py": "sha256:3c1cc96d36cbfa74dc0f847cc5f44c6edd527d8e223d0a564d4891a041761395",
|
| 30 |
+
"fastplms/models/e1/retrieval.py": "sha256:d077ef10100cf4f1482fbc7c8a944c241e61061a94374699156388d9e8e5c4db",
|
| 31 |
+
"fastplms/models/e1/tokenizer.json": "sha256:65d4345dbe908d3deb554891dc01b94e215fb17cd9e0bd0535e062e256737415",
|
| 32 |
+
"fastplms/models/ttt.py": "sha256:a0df4e98b02120d423e3c7ca9b866a8d0e3748b9076042a0102a838a11aed046",
|
| 33 |
+
"fastplms/registry.py": "sha256:afca271911b651a882345b74a58366494a1d784e4a48c8683a87f5508f4ba16e",
|
| 34 |
+
"fastplms/runtime.py": "sha256:110018646d6f248cedab140a030c3065e1b062b61f6aff659c231e538614bc01",
|
| 35 |
+
"fastplms_bundle.py": "sha256:bbe671e5653b3d3028100278166f0e8a91f72eaeaf3c2ed3b7902cd833020840",
|
| 36 |
+
"modeling_fastplms.py": "sha256:8fe92d7a9b0b1916575448c4fbba67005fcad75e456f68bba38aff2054d43c1f"
|
| 37 |
+
},
|
| 38 |
+
"model_id": "e1_300m",
|
| 39 |
+
"redistributable": true,
|
| 40 |
+
"release_tool_revision": "1b9ce023f1e06571cf3e6324be0610ffa53e0a4a",
|
| 41 |
+
"release_tool_sha256": "1459b5d7d13d9b07bd97b3eee764f2ce73623e15e32d07ddf6825c2a9509afb9",
|
| 42 |
+
"runtime_bundle_sha256": "c99634d724e168524f43f56ad1d22af47c75db0cbe657a2a55a237104fe7b833",
|
| 43 |
+
"runtime_revision": "1b9ce023f1e06571cf3e6324be0610ffa53e0a4a",
|
| 44 |
+
"schema_version": 2,
|
| 45 |
+
"scope": "runtime-only",
|
| 46 |
+
"source_tree_sha256": "cec0500dd5f6b238474600a6e80b8d3ccd4544ef7daec90441f14273f1b658d2",
|
| 47 |
+
"weights": {
|
| 48 |
+
"repo_id": "Synthyra/Profluent-E1-300M",
|
| 49 |
+
"revision": "5ef52c0ad2ae2578f40622696b763523810e8e26"
|
| 50 |
+
},
|
| 51 |
+
"weights_license_status": "resolved"
|
| 52 |
+
}
|