lhallee commited on
Commit
57d0cd3
·
verified ·
1 Parent(s): 7ddb5a7

Update FastPLMs runtime files

Browse files

Add-only FastPLMs files-only publication. Checkpoint weights and complete-artifact attestations are unchanged.

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/biohub-esm/LICENSE.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ **License (MIT)**
2
+
3
+ Copyright 2026 Chan Zuckerberg Biohub, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
LICENSES/biohub-esm/THIRD_PARTY_NOTICE.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The code in this repository depends on the following third-party libraries:
2
+
3
+ | Library | License | Link |
4
+ |----------|----------|----------|
5
+ | flash-attn | BSD | https://github.com/Dao-AILab/flash-attention/blob/main/LICENSE |
6
+ | PyTorch | BSD | https://github.com/pytorch/pytorch/blob/main/LICENSE |
7
+ | xformers | BSD | https://github.com/facebookresearch/xformers/blob/main/LICENSE |
8
+ | jaxtyping | MIT | https://github.com/patrick-kidger/jaxtyping/blob/main/LICENSE |
9
+ | einops | MIT | https://github.com/arogozhnikov/einops/blob/main/LICENSE |
10
+ | omegaconf | BSD | https://github.com/omry/omegaconf/blob/master/LICENSE |
11
+ | attrs | MIT | https://github.com/python-attrs/attrs/blob/main/LICENSE |
12
+ | scipy | BSD-3-Clause | https://github.com/scipy/scipy/blob/main/LICENSE.txt<br>https://github.com/scipy/scipy/blob/main/LICENSES_bundled.txt |
13
+ | lightning / torchmetrics | Apache 2.0 | https://github.com/Lightning-AI/torchmetrics/blob/master/LICENSE |
LICENSES/biohub-transformers/LICENSE ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright 2018- The Hugging Face team. All rights reserved.
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.
README.md CHANGED
@@ -1,176 +1,191 @@
1
- ---
2
- library_name: transformers
3
- license: mit
4
- tags:
5
- - biology
6
- - protein-language-model
7
- - esm3
8
- - multimodal-protein-model
9
- ---
10
-
11
- # FastPLMs ESM3 Small
12
-
13
- FastPLMs ESM3 Small is a Hugging Face compatible implementation of Biohub's open ESM3 small model. It loads through `AutoModel`, supports sequence-only inference by default, and exposes ESM3's additional tensor tracks directly through normal keyword arguments.
14
-
15
- This repository includes the Biohub ESM MIT license in `LICENSE`.
16
-
17
- ## Use With Transformers
18
-
19
- ```python
20
- import torch
21
- from transformers import AutoModel
22
-
23
- model = AutoModel.from_pretrained(
24
- "Synthyra/ESM3_small",
25
- trust_remote_code=True,
26
- dtype=torch.bfloat16,
27
- device_map="cuda",
28
- ).eval()
29
-
30
- sequences = ["MKTAYIAKQRQISFVKSHFSRQDILDLWIYHTQGYFP"]
31
- tokens = model.tokenize_sequences(sequences, device=model.device)
32
-
33
- with torch.inference_mode():
34
- output = model(**tokens)
35
-
36
- print(output.logits.shape) # sequence logits, (batch_size, seq_len, 64)
37
- print(output.last_hidden_state.shape) # ESM3 embeddings, (batch_size, seq_len, hidden_size)
38
- print(output.function_logits.shape) # function logits, (batch_size, seq_len, 8, 260)
39
- ```
40
-
41
- You can also call sequence inference directly:
42
-
43
- ```python
44
- output = model.forward_sequence(["MKTAYIAKQRQISFVKSHFSRQDILDLWIYHTQGYFP"])
45
- ```
46
-
47
- ## Experimental Test-Time Training
48
-
49
- TTT is disabled by default. No LoRA adapters are injected during normal
50
- `forward_sequence`, `forward`, or `embed_dataset` calls. Calling `model.ttt(...)`
51
- opts in to experimental masked-LM adaptation of the ESM3 sequence track through
52
- local LoRA weights. It can improve some difficult proteins, but it adds
53
- test-time compute and can degrade already confident predictions.
54
-
55
- ```python
56
- metrics = model.ttt(
57
- seq="MKTAYIAKQRQISFVKSHFSRQDILDLWIYHTQGYFP",
58
- ttt_config={"steps": 3, "ags": 1, "batch_size": 1},
59
- )
60
- model.ttt_reset()
61
- print(metrics["losses"])
62
- ```
63
-
64
- Switch between SDPA and Flex Attention after loading:
65
-
66
- ```python
67
- model.attn_backend = "flex"
68
- output = model.forward_sequence(["MKTAYIAKQRQISFVKSHFSRQDILDLWIYHTQGYFP"])
69
- model.attn_backend = "sdpa"
70
- ```
71
-
72
- ## Embed Entire Datasets
73
-
74
- To embed a list of protein sequences, call `embed_dataset`. Sequences are deduplicated, sorted by length, optionally truncated, and embedded in batches.
75
-
76
- ```python
77
- embedding_dict = model.embed_dataset(
78
- sequences=[
79
- "MALWMRLLPLLALLALWGPDPAAA",
80
- "MKTAYIAKQRQISFVKSHFSRQDILDLWIYHTQGYFP",
81
- ],
82
- batch_size=2,
83
- max_len=512,
84
- full_embeddings=False,
85
- embed_dtype=torch.float32,
86
- pooling_types=["mean", "cls"],
87
- save=True,
88
- save_path="esm3_embeddings.pth",
89
- )
90
-
91
- # embedding_dict maps sequence strings to pooled tensors.
92
- print(embedding_dict["MALWMRLLPLLALLALWGPDPAAA"].shape)
93
- ```
94
-
95
- Residue-wise embeddings are available by setting `full_embeddings=True`:
96
-
97
- ```python
98
- residue_embeddings = model.embed_dataset(
99
- sequences=["MKTAYIAKQRQISFVKSHFSRQDILDLWIYHTQGYFP"],
100
- batch_size=1,
101
- max_len=512,
102
- full_embeddings=True,
103
- save=False,
104
- )
105
-
106
- print(residue_embeddings["MKTAYIAKQRQISFVKSHFSRQDILDLWIYHTQGYFP"].shape)
107
- ```
108
-
109
- FASTA input is also supported:
110
-
111
- ```python
112
- embedding_dict = model.embed_dataset(
113
- fasta_path="proteins.fasta",
114
- batch_size=4,
115
- pooling_types=["mean"],
116
- save_path="esm3_fasta_embeddings.pth",
117
- )
118
- ```
119
-
120
- `embed_dataset` currently supports pooled `mean`, `cls`, and `max` embeddings, plus unpooled residue embeddings. It supports `.pth` saves; SQLite streaming is not enabled for the ESM3 wrapper yet.
121
-
122
- ## Multimodal Track Arguments
123
-
124
- The default path is amino acid sequence inference. Additional ESM3 tracks can be supplied directly using the same tensor shapes as Biohub ESM3:
125
-
126
- ```python
127
- tokens = model.tokenize_sequences(
128
- ["MKTAYIAKQRQISFVKSHFSRQDILDLWIYHTQGYFP"],
129
- device=model.device,
130
- )
131
-
132
- function_tokens = tokens["input_ids"].new_zeros((*tokens["input_ids"].shape, 8))
133
-
134
- with torch.inference_mode():
135
- output = model(
136
- **tokens,
137
- function_tokens=function_tokens,
138
- )
139
-
140
- print(output.sequence_logits.shape)
141
- print(output.function_logits.shape)
142
- ```
143
-
144
- Accepted track arguments include `sequence_tokens`, `structure_tokens`, `ss8_tokens`, `sasa_tokens`, `function_tokens`, `residue_annotation_tokens`, `average_plddt`, `per_res_plddt`, `structure_coords`, `chain_id`, and `sequence_id`. `input_ids` aliases `sequence_tokens`, and `attention_mask` is converted into `sequence_id` if no explicit `sequence_id` is provided.
145
-
146
- ## Loading Biohub Checkpoints Locally
147
-
148
- You can build the FastPLMs wrapper from the Biohub checkpoint directly:
149
-
150
- ```python
151
- from fastplms.esm3.modeling_esm3 import FastESM3Model
152
-
153
- model = FastESM3Model.from_pretrained_esm("esm3-sm-open-v1", device="cuda")
154
- ```
155
-
156
- This requires Hugging Face access to the gated `biohub/esm3-sm-open-v1` source repo.
157
-
158
- ## Biohub SDK Compatibility
159
-
160
- The core forward path is self-contained. Higher-level Biohub SDK workflows are delegated lazily to the official `esm` submodule when available:
161
-
162
- ```python
163
- # These methods use Biohub SDK dataclasses and generation configs.
164
- encoded = model.encode(esm_protein)
165
- decoded = model.decode(encoded)
166
- generated = model.generate(esm_protein, generation_config)
167
- ```
168
-
169
- Available delegated methods include `encode`, `decode`, `generate`, `batch_generate`, `logits`, and `forward_and_sample`.
170
-
171
- ## Source
172
-
173
- - Biohub ESM repository: https://github.com/Biohub/esm
174
- - Biohub ESM license: https://github.com/Biohub/esm/blob/main/LICENSE.md
175
- - Paper: https://biohub.ai/papers/esm_protein.pdf
176
- - Official model source: https://huggingface.co/biohub/esm3-sm-open-v1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: transformers
3
+ license: "mit"
4
+ tags:
5
+ - protein-language-model
6
+ - fastplms
7
+ ---
8
+
9
+ <!-- Generated from src/fastplms/models.toml. Do not edit. -->
10
+
11
+ # Synthyra/ESM3_small
12
+
13
+ This checkpoint packages the FastPLMs `ESM3` implementation.
14
+
15
+ Accepted inputs are sequence, structure, and function tracks prepared through
16
+ the multimodal helpers.
17
+ Supported Transformers entry points are `AutoConfig`, `AutoModel`.
18
+
19
+ ## Install and platform requirements
20
+
21
+ Install FastPLMs from the exact source revision paired with this model card:
22
+
23
+ ```bash
24
+ python -m pip install \
25
+ "fastplms @ git+https://github.com/Synthyra/FastPLMs.git@73e4525b91fde6ca88e33f9b292fe4ba2400c472"
26
+ ```
27
+
28
+ 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
29
+ access on first download. For an air-gapped run, first build the manifest-pinned
30
+ local artifact and use the offline form shown in the example.
31
+
32
+ ## Quick start
33
+
34
+ ```python
35
+ from transformers import AutoModel
36
+
37
+ model_id = "Synthyra/ESM3_small"
38
+ model = AutoModel.from_pretrained(
39
+ model_id,
40
+ trust_remote_code=True,
41
+ ).eval()
42
+ ```
43
+
44
+ This example uses the published Hub repository. For offline validation, build
45
+ the manifest-pinned artifact and replace `model_id` with its local
46
+ `dist/hub/ESM3_small` path, then pass `local_files_only=True`.
47
+
48
+ Leave attention unspecified for the Transformers default. Supported explicit
49
+ choices are `eager`, `sdpa`, `flex_attention`.
50
+ Pass the selected name through `attn_implementation`.
51
+ When an optimized backend cannot return full attention tensors,
52
+ `output_attentions=True` emits one explicit runtime warning and uses a correctly
53
+ masked eager implementation for that call only. The warning identifies the
54
+ configured backend, effective backend, and reason. Configuration and later
55
+ calls are unchanged.
56
+ For BF16 execution, this family uses FP32 parameters with CUDA BF16 autocast.
57
+
58
+ ## Dataset embeddings
59
+
60
+ The shared embedding API accepts sequences, `(id, sequence)` pairs,
61
+ `EmbeddingInput` records, insertion-ordered `{id: sequence}` mappings, or a
62
+ FASTA path. Results preserve order and duplicate identifiers:
63
+
64
+ ```python
65
+ result = model.embed_dataset(
66
+ ["MSTNPKPQRKTKRNT", "MKTIIALSYIFCLVFA"],
67
+ batch_size=2,
68
+ pooling=("mean", "std"),
69
+ )
70
+
71
+ for record in result:
72
+ print(record.id, record.sequence, record.tensor.shape)
73
+ ```
74
+
75
+ Set `full_embeddings=True` for one residue tensor with shape `(l, d)` per
76
+ sequence. Set `output` to a directory for bounded-memory, transactional
77
+ safetensors with ordered-prefix resume, or choose `format="sqlite"` for
78
+ batch-level database commits and exact resume. Pooling excludes boundary,
79
+ padding, and other non-biological positions.
80
+
81
+ For a long FASTA run, stream completed batches into SQLite:
82
+
83
+ ```python
84
+ persisted = model.embed_dataset(
85
+ "proteins.fasta",
86
+ batch_size=64,
87
+ pooling=("mean",),
88
+ output="protein-embeddings.sqlite",
89
+ format="sqlite",
90
+ resume=True,
91
+ )
92
+ ```
93
+
94
+ Resume verifies the input order, model state, tokenizer policy, backend, dtype,
95
+ and pooling configuration. It never appends incompatible records to an
96
+ existing run.
97
+
98
+ ## Sequence inference and masked-sequence generation
99
+
100
+ ESM3 owns its sequence preparation. This example exercises the sequence track;
101
+ the public input contract also supports structure and function tracks through
102
+ the multimodal helpers:
103
+
104
+ ```python
105
+ import torch
106
+
107
+ batch = model.tokenize_sequences(
108
+ ["MKTAYIAKQ", "GGGG"],
109
+ device=model.device,
110
+ )
111
+ with torch.inference_mode():
112
+ output = model(**batch)
113
+
114
+ print(output.last_hidden_state.shape)
115
+ print(output.logits.shape)
116
+ print(output.structure_logits.shape)
117
+ print(output.function_logits.shape)
118
+ ```
119
+
120
+ When `return_dict=False`, ESM3 follows the standard base-model tuple prefix:
121
+ `last_hidden_state`, then requested `hidden_states` and `attentions`. Multimodal
122
+ logits and extensions follow that prefix. Prefer named fields for individual
123
+ tracks.
124
+
125
+ Generate masked sequence positions with an explicit seed:
126
+
127
+ ```python
128
+ from fastplms.models.esm3.modeling_esm3 import FastESM3GenerationConfig
129
+
130
+ config = FastESM3GenerationConfig(
131
+ num_steps=8,
132
+ temperature=1.0,
133
+ seed=7,
134
+ )
135
+ generated = model.generate("MK____A", config)
136
+ print(generated)
137
+ ```
138
+
139
+ Underscores mark positions to generate. Model outputs are predictions over
140
+ tracks, not experimental measurements of structure or function.
141
+
142
+ ## Runtime contract
143
+
144
+ - Public input: Sequence, structure, and function tracks prepared through the multimodal helpers
145
+ - Advertised AutoClasses: `AutoConfig`, `AutoModel`
146
+ - AutoClass weight status: `AutoConfig` = `FastPLMs extension`, `AutoModel` = `pretrained`
147
+ - Attention implementations: `eager`, `sdpa`, `flex_attention`
148
+ - Precision policies: `default`
149
+ - BF16 execution: `fp32_parameters_autocast`
150
+ - Generation contract: `not_applicable`
151
+ - Optional dependency group: `core`
152
+ - Weight publication allowed: `true`
153
+ - Weight license status: `resolved`
154
+ - Redistributable: `true`
155
+ - Complete weight publication required: `false`
156
+
157
+ ## Provenance
158
+
159
+ - FastPLMs weights: `Synthyra/ESM3_small@7ddb5a740f9e5f93933eb6410c0ee8684bc63ec1`
160
+ - Runtime revision: `73e4525b91fde6ca88e33f9b292fe4ba2400c472`
161
+ - Runtime source-tree SHA-256: `4b83474b9a9381edcfa2b308d9d23af91de3ca8e22d8b5b5f56bab317f152630`
162
+ - Runtime bundle SHA-256: `b0a12aa8be05f693e35ffc16025c2a546bd37682744b9686ece6e6bef9c9be92`
163
+ - Generator/schema version and complete/runtime-only attestations: recorded in `provenance.json`
164
+ - Official checkpoint: `biohub/esm3-sm-open-v1@47f0545b2b6daf26a93439a3cd610f4f7f3d5478`
165
+ - Artifact source: `fast`
166
+ - State transform: `esm3_to_fastplms_v1`
167
+ - BF16 execution: `fp32_parameters_autocast`
168
+ - Pinned upstreams: `biohub-esm`, `biohub-transformers`
169
+ - Reference container: `reference-biohub-esm`
170
+ - Release tiers: `check`, `compliance`, `feature`, `artifact`, `benchmark`
171
+ - Unresolved required file identities: `0`
172
+
173
+ The local artifact records exact file identities, conversion provenance, source
174
+ revisions, and legal texts in `provenance.json`. A nonzero unresolved count is a
175
+ release blocker.
176
+
177
+ ## Validation boundary
178
+
179
+ For tiers declared by the manifest, the release contract compares applicable
180
+ semantic configuration, tokenizer behavior, state keys, shapes, dtypes,
181
+ values, aliases, and representative inference with the pinned official
182
+ implementation. This metadata does not by itself claim that a particular build
183
+ passed, that one backend is faster, or that an output has biological or
184
+ therapeutic validity.
185
+
186
+ ## License
187
+
188
+ Checkpoint terms: MIT. The Hub model-card identifier is
189
+ `mit`. Applicable source licenses, notices, attribution,
190
+ and conversion records are distributed with the local artifact. Review them
191
+ 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,10 +4,19 @@
4
  ],
5
  "attn_backend": "sdpa",
6
  "auto_map": {
7
- "AutoConfig": "modeling_esm3.FastESM3Config",
8
- "AutoModel": "modeling_esm3.FastESM3Model",
9
- "AutoModelForMaskedLM": "modeling_esm3.FastESM3Model"
10
  },
 
 
 
 
 
 
 
 
 
 
11
  "hidden_size": 1536,
12
  "initializer_range": 0.02,
13
  "model_name": "esm3_sm_open_v1",
 
4
  ],
5
  "attn_backend": "sdpa",
6
  "auto_map": {
7
+ "AutoConfig": "modeling_fastplms.FastESM3Config",
8
+ "AutoModel": "modeling_fastplms.FastESM3Model"
 
9
  },
10
+ "fastplms_checkpoint_hash": "53c7d39ae7fa322e5cb8fd60e6a718f9f18da5de87689a9eee7c6b9a509d0cf2",
11
+ "fastplms_checkpoint_repo_id": "Synthyra/ESM3_small",
12
+ "fastplms_checkpoint_revision": "7ddb5a740f9e5f93933eb6410c0ee8684bc63ec1",
13
+ "fastplms_model_id": "esm3_small",
14
+ "fastplms_release_tool_revision": "73e4525b91fde6ca88e33f9b292fe4ba2400c472",
15
+ "fastplms_release_tool_sha256": "baaa0230677d8e61331e2926cf2cad9d5f67f2ad9191e8c88658a70b1e69a944",
16
+ "fastplms_runtime_bundle_sha256": "b0a12aa8be05f693e35ffc16025c2a546bd37682744b9686ece6e6bef9c9be92",
17
+ "fastplms_runtime_revision": "73e4525b91fde6ca88e33f9b292fe4ba2400c472",
18
+ "fastplms_source_tree_sha256": "4b83474b9a9381edcfa2b308d9d23af91de3ca8e22d8b5b5f56bab317f152630",
19
+ "fastplms_weights_revision": "7ddb5a740f9e5f93933eb6410c0ee8684bc63ec1",
20
  "hidden_size": 1536,
21
  "initializer_range": 0.02,
22
  "model_name": "esm3_sm_open_v1",
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/esm3/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from fastplms.models.esm3.modeling_esm3 import FastESM3Config, FastESM3Model
2
+
3
+ __all__ = ["FastESM3Config", "FastESM3Model"]
fastplms/models/esm3/modeling_esm3.py ADDED
@@ -0,0 +1,2515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face-compatible ESM3 implementation.
2
+
3
+ The production module is self-contained. The pinned Biohub repository is used
4
+ only by the reference adapter in the parity suite.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import functools
11
+ import hashlib
12
+ import io
13
+ import json
14
+ import math
15
+ import os
16
+ import shutil
17
+ import stat
18
+ import tempfile
19
+ from contextlib import contextmanager
20
+ from dataclasses import dataclass
21
+ from pathlib import Path, PurePosixPath
22
+ from typing import ClassVar
23
+ from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
24
+
25
+ import einops
26
+ import torch
27
+ import torch.nn as nn
28
+ import torch.nn.functional as F
29
+ from einops import rearrange
30
+ from tokenizers import Tokenizer
31
+ from tokenizers.models import BPE
32
+ from tokenizers.processors import TemplateProcessing
33
+ from transformers import PretrainedConfig, PreTrainedModel, PreTrainedTokenizerFast
34
+ from transformers.modeling_outputs import ModelOutput
35
+
36
+ try:
37
+ from fastplms.attention import (
38
+ AttentionBackend,
39
+ BlockMask,
40
+ FastPLMsAttentionMixin,
41
+ _get_flex_attention_fn,
42
+ create_block_mask,
43
+ resolve_attention_backend,
44
+ resolve_attention_backend_for_call,
45
+ )
46
+ from fastplms.embeddings import EmbeddingMixin
47
+ from fastplms.models.ttt import FastPLMTestTimeTrainingMixin
48
+ except ModuleNotFoundError as error:
49
+ _COMPOSITE_REQUIRED_NAMES = (
50
+ "AttentionBackend",
51
+ "BlockMask",
52
+ "EmbeddingMixin",
53
+ "FastPLMsAttentionMixin",
54
+ "FastPLMTestTimeTrainingMixin",
55
+ "_get_flex_attention_fn",
56
+ "create_block_mask",
57
+ "resolve_attention_backend",
58
+ "resolve_attention_backend_for_call",
59
+ )
60
+ if error.name != "fastplms" or any(
61
+ name not in globals() for name in _COMPOSITE_REQUIRED_NAMES
62
+ ):
63
+ raise
64
+ # Legacy flat Hub composites define every shared symbol above this block.
65
+
66
+
67
+ _SAVED_RUNTIME_SCHEMA_VERSION = 1
68
+ _SAVED_RUNTIME_FILES = (
69
+ "__init__.py",
70
+ "attention/__init__.py",
71
+ "attention/_core.py",
72
+ "attention/_kernel_lock.py",
73
+ "attention/interfaces.py",
74
+ "embeddings/__init__.py",
75
+ "embeddings/pooling.py",
76
+ "embeddings/runner.py",
77
+ "embeddings/storage.py",
78
+ "embeddings/types.py",
79
+ "models/__init__.py",
80
+ "models/esm3/__init__.py",
81
+ "models/esm3/modeling_esm3.py",
82
+ "models/ttt.py",
83
+ "models.toml",
84
+ "registry.py",
85
+ "runtime.py",
86
+ )
87
+ _MAX_SAVED_RUNTIME_FILE_BYTES = 1024 * 1024
88
+ _MAX_SAVED_RUNTIME_TOTAL_BYTES = 4 * 1024 * 1024
89
+ _MAX_SAVED_RUNTIME_ARCHIVE_BYTES = 2 * 1024 * 1024
90
+
91
+
92
+ @contextmanager
93
+ def _temporary_eval(model: nn.Module):
94
+ """Temporarily disable training behavior without flattening mixed module states."""
95
+ training_states = tuple((module, module.training) for module in model.modules())
96
+ model.eval()
97
+ try:
98
+ yield
99
+ finally:
100
+ for module, training in training_states:
101
+ module.training = training
102
+
103
+
104
+ def _validate_saved_runtime_relative_path(value: str) -> PurePosixPath:
105
+ """Return one canonical, fixed-inventory runtime source path."""
106
+
107
+ relative = PurePosixPath(value)
108
+ if (
109
+ not value
110
+ or "\\" in value
111
+ or relative.is_absolute()
112
+ or relative.as_posix() != value
113
+ or any(part in {"", ".", ".."} or ":" in part or "\0" in part for part in relative.parts)
114
+ ):
115
+ raise RuntimeError(f"Saved ESM3 runtime path is unsafe: {value!r}.")
116
+ return relative
117
+
118
+
119
+ def _read_saved_runtime_file(package_root: Path, relative: PurePosixPath) -> bytes:
120
+ """Read one allowlisted regular file without following a symlink."""
121
+
122
+ current = package_root
123
+ for index, part in enumerate(relative.parts):
124
+ current = current / part
125
+ try:
126
+ metadata = current.lstat()
127
+ except OSError as error:
128
+ raise RuntimeError(
129
+ f"Saved ESM3 runtime file is missing: {relative.as_posix()!r}."
130
+ ) from error
131
+ if stat.S_ISLNK(metadata.st_mode):
132
+ raise RuntimeError(
133
+ f"Saved ESM3 runtime path must not contain a symlink: {relative.as_posix()!r}."
134
+ )
135
+ if index < len(relative.parts) - 1:
136
+ if not stat.S_ISDIR(metadata.st_mode):
137
+ raise RuntimeError(
138
+ f"Saved ESM3 runtime parent is not a directory: {relative.as_posix()!r}."
139
+ )
140
+ continue
141
+ if not stat.S_ISREG(metadata.st_mode):
142
+ raise RuntimeError(
143
+ f"Saved ESM3 runtime entry is not a regular file: {relative.as_posix()!r}."
144
+ )
145
+ if metadata.st_size > _MAX_SAVED_RUNTIME_FILE_BYTES:
146
+ raise RuntimeError(
147
+ f"Saved ESM3 runtime file exceeds its size limit: {relative.as_posix()!r}."
148
+ )
149
+ before = metadata
150
+
151
+ try:
152
+ with current.open("rb") as handle:
153
+ payload = handle.read(_MAX_SAVED_RUNTIME_FILE_BYTES + 1)
154
+ after = current.lstat()
155
+ except OSError as error:
156
+ raise RuntimeError(
157
+ f"Unable to read saved ESM3 runtime file: {relative.as_posix()!r}."
158
+ ) from error
159
+ identity_before = (
160
+ before.st_dev,
161
+ before.st_ino,
162
+ before.st_size,
163
+ before.st_mtime_ns,
164
+ before.st_ctime_ns,
165
+ )
166
+ identity_after = (
167
+ after.st_dev,
168
+ after.st_ino,
169
+ after.st_size,
170
+ after.st_mtime_ns,
171
+ after.st_ctime_ns,
172
+ )
173
+ if (
174
+ stat.S_ISLNK(after.st_mode)
175
+ or not stat.S_ISREG(after.st_mode)
176
+ or identity_before != identity_after
177
+ or len(payload) != before.st_size
178
+ or len(payload) > _MAX_SAVED_RUNTIME_FILE_BYTES
179
+ ):
180
+ raise RuntimeError(
181
+ f"Saved ESM3 runtime file changed while it was validated: {relative.as_posix()!r}."
182
+ )
183
+ return payload
184
+
185
+
186
+ def _saved_runtime_files(package_root: Path) -> dict[str, bytes]:
187
+ """Read exactly the fixed ESM3 runtime inventory into validated bytes."""
188
+
189
+ try:
190
+ root_metadata = package_root.lstat()
191
+ except OSError as error:
192
+ raise RuntimeError(f"Saved ESM3 runtime package is unavailable: {package_root}.") from error
193
+ if stat.S_ISLNK(root_metadata.st_mode) or not stat.S_ISDIR(root_metadata.st_mode):
194
+ raise RuntimeError("Saved ESM3 runtime package root must be a non-symlink directory.")
195
+
196
+ files: dict[str, bytes] = {}
197
+ total_size = 0
198
+ for value in _SAVED_RUNTIME_FILES:
199
+ relative = _validate_saved_runtime_relative_path(value)
200
+ payload = _read_saved_runtime_file(package_root, relative)
201
+ total_size += len(payload)
202
+ if total_size > _MAX_SAVED_RUNTIME_TOTAL_BYTES:
203
+ raise RuntimeError("Saved ESM3 runtime exceeds its total expanded size limit.")
204
+ files[relative.as_posix()] = payload
205
+ if len(files) != len(_SAVED_RUNTIME_FILES):
206
+ raise RuntimeError("Saved ESM3 runtime allowlist contains duplicate paths.")
207
+ return files
208
+
209
+
210
+ def _saved_runtime_manifest(files: dict[str, bytes]) -> dict[str, object]:
211
+ records = {
212
+ relative: {
213
+ "sha256": hashlib.sha256(payload).hexdigest(),
214
+ "size": len(payload),
215
+ }
216
+ for relative, payload in sorted(files.items())
217
+ }
218
+ return {
219
+ "schema_version": _SAVED_RUNTIME_SCHEMA_VERSION,
220
+ "files": records,
221
+ "total_size": sum(record["size"] for record in records.values()),
222
+ }
223
+
224
+
225
+ def _saved_runtime_tree_hash(manifest: dict[str, object]) -> str:
226
+ files = manifest["files"]
227
+ if not isinstance(files, dict):
228
+ raise RuntimeError("Saved ESM3 runtime manifest files are invalid.")
229
+ digest = hashlib.sha256()
230
+ for relative, raw_record in sorted(files.items()):
231
+ if not isinstance(relative, str) or not isinstance(raw_record, dict):
232
+ raise RuntimeError("Saved ESM3 runtime manifest record is invalid.")
233
+ digest.update(relative.encode("utf-8"))
234
+ digest.update(b"\0")
235
+ digest.update(str(raw_record["size"]).encode("ascii"))
236
+ digest.update(b"\0")
237
+ digest.update(str(raw_record["sha256"]).encode("ascii"))
238
+ digest.update(b"\n")
239
+ return digest.hexdigest()
240
+
241
+
242
+ def _build_saved_runtime_archive(
243
+ package_root: Path,
244
+ ) -> tuple[bytes, dict[str, object], str]:
245
+ """Build a deterministic archive directly from validated runtime bytes."""
246
+
247
+ files = _saved_runtime_files(package_root)
248
+ manifest = _saved_runtime_manifest(files)
249
+ tree_hash = _saved_runtime_tree_hash(manifest)
250
+
251
+ buffer = io.BytesIO()
252
+ with ZipFile(buffer, mode="w", compression=ZIP_DEFLATED, compresslevel=9) as archive:
253
+ for relative, contents in sorted(files.items()):
254
+ archive_path = (PurePosixPath("fastplms") / relative).as_posix()
255
+ info = ZipInfo(archive_path, date_time=(1980, 1, 1, 0, 0, 0))
256
+ info.create_system = 3
257
+ info.compress_type = ZIP_DEFLATED
258
+ info.external_attr = 0o100644 << 16
259
+ archive.writestr(info, contents, compress_type=ZIP_DEFLATED, compresslevel=9)
260
+ payload = buffer.getvalue()
261
+ if len(payload) > _MAX_SAVED_RUNTIME_ARCHIVE_BYTES:
262
+ raise RuntimeError("Saved ESM3 runtime archive exceeds its compressed size limit.")
263
+ return payload, manifest, tree_hash
264
+
265
+
266
+ def _render_saved_runtime_bundle(
267
+ archive: bytes,
268
+ manifest: dict[str, object],
269
+ tree_hash: str,
270
+ ) -> tuple[str, bytes]:
271
+ archive_hash = hashlib.sha256(archive).hexdigest()
272
+ encoded = base64.b85encode(archive).decode("ascii")
273
+ chunks = (encoded[index : index + 100] for index in range(0, len(encoded), 100))
274
+ manifest_source = json.dumps(manifest, indent=2, sort_keys=True, ensure_ascii=True)
275
+ lines = [
276
+ '"""Deterministic embedded FastPLMs runtime for one saved ESM3 model."""',
277
+ "",
278
+ f'RUNTIME_HASH = "{archive_hash}"',
279
+ f'RUNTIME_TREE_HASH = "{tree_hash}"',
280
+ f"RUNTIME_MANIFEST = {manifest_source}",
281
+ "RUNTIME_DATA = (",
282
+ *(f" {chunk!r}," for chunk in chunks),
283
+ ")",
284
+ "",
285
+ ]
286
+ return archive_hash, "\n".join(lines).encode("utf-8")
287
+
288
+
289
+ def _render_saved_runtime_bridge(archive_hash: str, tree_hash: str) -> str:
290
+ """Render the fail-closed Transformers bridge for one runtime identity."""
291
+
292
+ lines = [
293
+ '"""Bridge to the bundled FastPLMs ESM3 runtime."""',
294
+ "",
295
+ "import atexit",
296
+ "import base64",
297
+ "import hashlib",
298
+ "import importlib",
299
+ "import importlib.util",
300
+ "import stat",
301
+ "import sys",
302
+ "import tempfile",
303
+ "from io import BytesIO",
304
+ "from pathlib import Path, PurePosixPath",
305
+ "from zipfile import BadZipFile, ZIP_DEFLATED, ZipFile",
306
+ "",
307
+ "from .fastplms_bundle import (",
308
+ " RUNTIME_DATA,",
309
+ " RUNTIME_HASH,",
310
+ " RUNTIME_MANIFEST,",
311
+ " RUNTIME_TREE_HASH,",
312
+ ")",
313
+ "",
314
+ f'if RUNTIME_HASH != "{archive_hash}" or RUNTIME_TREE_HASH != "{tree_hash}":',
315
+ ' raise RuntimeError("FastPLMs runtime identity differs from the saved ESM3 bridge.")',
316
+ "",
317
+ f"_MAX_RUNTIME_FILE_BYTES = {_MAX_SAVED_RUNTIME_FILE_BYTES}",
318
+ f"_MAX_RUNTIME_TOTAL_BYTES = {_MAX_SAVED_RUNTIME_TOTAL_BYTES}",
319
+ f"_MAX_RUNTIME_ARCHIVE_BYTES = {_MAX_SAVED_RUNTIME_ARCHIVE_BYTES}",
320
+ "_MAX_RUNTIME_ENCODED_BYTES = (_MAX_RUNTIME_ARCHIVE_BYTES * 5 + 3) // 4",
321
+ "_EXPECTED_RUNTIME_FILES = (",
322
+ *(f" {relative!r}," for relative in _SAVED_RUNTIME_FILES),
323
+ ")",
324
+ "_RUNTIME_TEMPORARIES = []",
325
+ "",
326
+ "def _runtime_tree_hash(files):",
327
+ " digest = hashlib.sha256()",
328
+ " for relative, record in sorted(files.items()):",
329
+ ' digest.update(relative.encode("utf-8"))',
330
+ ' digest.update(b"\\0")',
331
+ ' digest.update(str(record["size"]).encode("ascii"))',
332
+ ' digest.update(b"\\0")',
333
+ ' digest.update(record["sha256"].encode("ascii"))',
334
+ ' digest.update(b"\\n")',
335
+ " return digest.hexdigest()",
336
+ "",
337
+ "def _validated_manifest():",
338
+ " if not isinstance(RUNTIME_MANIFEST, dict) or set(RUNTIME_MANIFEST) != {",
339
+ ' "schema_version",',
340
+ ' "files",',
341
+ ' "total_size",',
342
+ " }:",
343
+ ' raise RuntimeError("Embedded FastPLMs runtime manifest is invalid.")',
344
+ f' if RUNTIME_MANIFEST["schema_version"] != {_SAVED_RUNTIME_SCHEMA_VERSION}:',
345
+ ' raise RuntimeError("Embedded FastPLMs runtime manifest schema is unsupported.")',
346
+ ' raw_files = RUNTIME_MANIFEST["files"]',
347
+ " if not isinstance(raw_files, dict) or set(raw_files) != set(_EXPECTED_RUNTIME_FILES):",
348
+ ' raise RuntimeError("Embedded FastPLMs runtime inventory is invalid.")',
349
+ " files = {}",
350
+ " total_size = 0",
351
+ " for relative in _EXPECTED_RUNTIME_FILES:",
352
+ " record = raw_files[relative]",
353
+ ' if not isinstance(record, dict) or set(record) != {"sha256", "size"}:',
354
+ ' raise RuntimeError("Embedded FastPLMs runtime manifest record is invalid.")',
355
+ ' size = record["size"]',
356
+ ' file_hash = record["sha256"]',
357
+ " if (",
358
+ " isinstance(size, bool)",
359
+ " or not isinstance(size, int)",
360
+ " or size < 0",
361
+ " or size > _MAX_RUNTIME_FILE_BYTES",
362
+ " or not isinstance(file_hash, str)",
363
+ " or len(file_hash) != 64",
364
+ ' or any(character not in "0123456789abcdef" for character in file_hash)',
365
+ " ):",
366
+ ' raise RuntimeError("Embedded FastPLMs runtime manifest record is invalid.")',
367
+ ' files[relative] = {"sha256": file_hash, "size": size}',
368
+ " total_size += size",
369
+ " if total_size > _MAX_RUNTIME_TOTAL_BYTES:",
370
+ ' raise RuntimeError("Embedded FastPLMs runtime exceeds its size limit.")',
371
+ " if (",
372
+ ' isinstance(RUNTIME_MANIFEST["total_size"], bool)',
373
+ ' or RUNTIME_MANIFEST["total_size"] != total_size',
374
+ " ):",
375
+ ' raise RuntimeError("Embedded FastPLMs runtime total size is invalid.")',
376
+ " if _runtime_tree_hash(files) != RUNTIME_TREE_HASH:",
377
+ ' raise RuntimeError("Embedded FastPLMs runtime tree hash mismatch.")',
378
+ " return files",
379
+ "",
380
+ "_EXPECTED_MANIFEST = _validated_manifest()",
381
+ "",
382
+ "def _archive_relative_path(member):",
383
+ " name = member.filename",
384
+ " relative_archive = PurePosixPath(name)",
385
+ " parts = relative_archive.parts",
386
+ " if (",
387
+ ' not name or "\\\\" in name',
388
+ " or relative_archive.is_absolute()",
389
+ " or relative_archive.as_posix() != name",
390
+ " or len(parts) < 2",
391
+ ' or parts[0] != "fastplms"',
392
+ ' or any(part in {"", ".", ".."} or ":" in part or "\\0" in part for part in parts)',
393
+ " ):",
394
+ ' raise RuntimeError("Embedded FastPLMs archive has an unsafe path.")',
395
+ " relative = PurePosixPath(*parts[1:]).as_posix()",
396
+ " if relative not in _EXPECTED_MANIFEST:",
397
+ ' raise RuntimeError("Embedded FastPLMs archive inventory is unexpected.")',
398
+ " return relative",
399
+ "",
400
+ "def _validated_archive_files(payload):",
401
+ " if len(payload) > _MAX_RUNTIME_ARCHIVE_BYTES:",
402
+ (
403
+ ' raise RuntimeError("Embedded FastPLMs archive exceeds its compressed '
404
+ 'size limit.")'
405
+ ),
406
+ " try:",
407
+ " with ZipFile(BytesIO(payload)) as archive:",
408
+ " members = archive.infolist()",
409
+ " if archive.comment or len(members) != len(_EXPECTED_MANIFEST):",
410
+ ' raise RuntimeError("Embedded FastPLMs archive inventory is invalid.")',
411
+ " files = {}",
412
+ " total_size = 0",
413
+ " for member in members:",
414
+ " relative = _archive_relative_path(member)",
415
+ " if relative in files:",
416
+ ' raise RuntimeError("Embedded FastPLMs archive repeats a path.")',
417
+ " record = _EXPECTED_MANIFEST[relative]",
418
+ " if (",
419
+ " member.is_dir()",
420
+ " or member.flag_bits & 0x1",
421
+ " or member.compress_type != ZIP_DEFLATED",
422
+ " or member.create_system != 3",
423
+ " or member.external_attr >> 16 != 0o100644",
424
+ " or member.date_time != (1980, 1, 1, 0, 0, 0)",
425
+ " or member.extra",
426
+ " or member.comment",
427
+ ' or member.filename != f"fastplms/{relative}"',
428
+ ' or member.file_size != record["size"]',
429
+ " or member.file_size > _MAX_RUNTIME_FILE_BYTES",
430
+ " or member.compress_size > _MAX_RUNTIME_ARCHIVE_BYTES",
431
+ " ):",
432
+ (
433
+ ' raise RuntimeError("Embedded FastPLMs archive member is not '
434
+ 'canonical.")'
435
+ ),
436
+ ' with archive.open(member, mode="r") as handle:',
437
+ ' contents = handle.read(record["size"] + 1)',
438
+ " if (",
439
+ ' len(contents) != record["size"]',
440
+ ' or hashlib.sha256(contents).hexdigest() != record["sha256"]',
441
+ " ):",
442
+ ' raise RuntimeError("Embedded FastPLMs archive member hash mismatch.")',
443
+ " total_size += len(contents)",
444
+ " if total_size > _MAX_RUNTIME_TOTAL_BYTES:",
445
+ (
446
+ ' raise RuntimeError("Embedded FastPLMs archive exceeds its size '
447
+ 'limit.")'
448
+ ),
449
+ " files[relative] = contents",
450
+ " except RuntimeError:",
451
+ " raise",
452
+ " except (BadZipFile, KeyError, OSError, ValueError) as error:",
453
+ ' raise RuntimeError("Embedded FastPLMs archive is invalid.") from error',
454
+ " if set(files) != set(_EXPECTED_MANIFEST):",
455
+ ' raise RuntimeError("Embedded FastPLMs archive inventory is incomplete.")',
456
+ " return files",
457
+ "",
458
+ "def _read_runtime_file(package_root, relative):",
459
+ " current = package_root",
460
+ " parts = PurePosixPath(relative).parts",
461
+ " for index, part in enumerate(parts):",
462
+ " current = current / part",
463
+ " try:",
464
+ " metadata = current.lstat()",
465
+ " except OSError as error:",
466
+ ' raise RuntimeError(f"Runtime file is missing: {relative!r}.") from error',
467
+ " if stat.S_ISLNK(metadata.st_mode):",
468
+ ' raise RuntimeError(f"Runtime path contains a symlink: {relative!r}.")',
469
+ " if index < len(parts) - 1:",
470
+ " if not stat.S_ISDIR(metadata.st_mode):",
471
+ ' raise RuntimeError(f"Runtime parent is not a directory: {relative!r}.")',
472
+ " continue",
473
+ " if not stat.S_ISREG(metadata.st_mode):",
474
+ ' raise RuntimeError(f"Runtime entry is not a regular file: {relative!r}.")',
475
+ " if metadata.st_size > _MAX_RUNTIME_FILE_BYTES:",
476
+ ' raise RuntimeError(f"Runtime file exceeds its size limit: {relative!r}.")',
477
+ " before = metadata",
478
+ " try:",
479
+ ' with current.open("rb") as handle:',
480
+ " contents = handle.read(_MAX_RUNTIME_FILE_BYTES + 1)",
481
+ " after = current.lstat()",
482
+ " except OSError as error:",
483
+ ' raise RuntimeError(f"Unable to read runtime file: {relative!r}.") from error',
484
+ " before_identity = (",
485
+ " before.st_dev,",
486
+ " before.st_ino,",
487
+ " before.st_size,",
488
+ " before.st_mtime_ns,",
489
+ " before.st_ctime_ns,",
490
+ " )",
491
+ " after_identity = (",
492
+ " after.st_dev,",
493
+ " after.st_ino,",
494
+ " after.st_size,",
495
+ " after.st_mtime_ns,",
496
+ " after.st_ctime_ns,",
497
+ " )",
498
+ " if (",
499
+ " stat.S_ISLNK(after.st_mode)",
500
+ " or not stat.S_ISREG(after.st_mode)",
501
+ " or before_identity != after_identity",
502
+ " or len(contents) != before.st_size",
503
+ " or len(contents) > _MAX_RUNTIME_FILE_BYTES",
504
+ " ):",
505
+ ' raise RuntimeError(f"Runtime file changed while validated: {relative!r}.")',
506
+ " return contents",
507
+ "",
508
+ "def _runtime_file_manifest(package_root):",
509
+ " try:",
510
+ " root_metadata = package_root.lstat()",
511
+ " except OSError as error:",
512
+ ' raise RuntimeError("Runtime package root is unavailable.") from error',
513
+ " if stat.S_ISLNK(root_metadata.st_mode) or not stat.S_ISDIR(root_metadata.st_mode):",
514
+ ' raise RuntimeError("Runtime package root must be a non-symlink directory.")',
515
+ " files = {}",
516
+ " total_size = 0",
517
+ " for relative in _EXPECTED_RUNTIME_FILES:",
518
+ " contents = _read_runtime_file(package_root, relative)",
519
+ " files[relative] = {",
520
+ ' "sha256": hashlib.sha256(contents).hexdigest(),',
521
+ ' "size": len(contents),',
522
+ " }",
523
+ " total_size += len(contents)",
524
+ " if total_size > _MAX_RUNTIME_TOTAL_BYTES:",
525
+ ' raise RuntimeError("Runtime package exceeds its total size limit.")',
526
+ " return files",
527
+ "",
528
+ "def _cleanup_runtime_temporaries():",
529
+ " while _RUNTIME_TEMPORARIES:",
530
+ " _RUNTIME_TEMPORARIES.pop().cleanup()",
531
+ "",
532
+ "atexit.register(_cleanup_runtime_temporaries)",
533
+ "",
534
+ "def _ensure_runtime():",
535
+ " if (",
536
+ " not isinstance(RUNTIME_DATA, tuple)",
537
+ " or not RUNTIME_DATA",
538
+ " or any(not isinstance(chunk, str) for chunk in RUNTIME_DATA)",
539
+ " ):",
540
+ ' raise RuntimeError("Embedded FastPLMs runtime data is invalid.")',
541
+ ' encoded = "".join(RUNTIME_DATA)',
542
+ " if len(encoded) > _MAX_RUNTIME_ENCODED_BYTES:",
543
+ ' raise RuntimeError("Embedded FastPLMs runtime data exceeds its size limit.")',
544
+ " try:",
545
+ ' payload = base64.b85decode(encoded.encode("ascii"))',
546
+ " except (UnicodeEncodeError, ValueError) as error:",
547
+ ' raise RuntimeError("Embedded FastPLMs runtime data is invalid.") from error',
548
+ " if hashlib.sha256(payload).hexdigest() != RUNTIME_HASH:",
549
+ ' raise RuntimeError("Embedded FastPLMs runtime hash mismatch.")',
550
+ " files = _validated_archive_files(payload)",
551
+ ' temporary = tempfile.TemporaryDirectory(prefix="fastplms-esm3-runtime-")',
552
+ " try:",
553
+ " runtime_root = Path(temporary.name).resolve()",
554
+ " module_root = Path(__file__).resolve().parent",
555
+ " if runtime_root == module_root or module_root in runtime_root.parents:",
556
+ (
557
+ ' raise RuntimeError("FastPLMs runtime temporary must be outside the saved '
558
+ 'model.")'
559
+ ),
560
+ ' package_root = runtime_root / "fastplms"',
561
+ " for relative in _EXPECTED_RUNTIME_FILES:",
562
+ " target = package_root.joinpath(*PurePosixPath(relative).parts)",
563
+ " target.parent.mkdir(parents=True, exist_ok=True)",
564
+ ' with target.open("xb") as handle:',
565
+ " handle.write(files[relative])",
566
+ " actual = _runtime_file_manifest(package_root)",
567
+ " if (",
568
+ " actual != _EXPECTED_MANIFEST",
569
+ " or _runtime_tree_hash(actual) != RUNTIME_TREE_HASH",
570
+ " ):",
571
+ ' raise RuntimeError("Extracted FastPLMs runtime identity mismatch.")',
572
+ " except BaseException:",
573
+ " temporary.cleanup()",
574
+ " raise",
575
+ " return package_root, temporary",
576
+ "",
577
+ "def _verify_loaded_runtime(package):",
578
+ ' package_file = getattr(package, "__file__", None)',
579
+ " if not isinstance(package_file, str) or not package_file:",
580
+ " raise RuntimeError(",
581
+ ' "Loaded FastPLMs version/runtime mismatch: source path is unavailable."',
582
+ " )",
583
+ " package_root = Path(package_file).absolute().parent",
584
+ " try:",
585
+ " actual = _runtime_file_manifest(package_root)",
586
+ " except RuntimeError as error:",
587
+ " raise RuntimeError(",
588
+ ' "Loaded FastPLMs version/runtime mismatch: sources cannot be verified."',
589
+ " ) from error",
590
+ " if actual != _EXPECTED_MANIFEST or _runtime_tree_hash(actual) != RUNTIME_TREE_HASH:",
591
+ " mismatch = next(",
592
+ " (",
593
+ " relative",
594
+ " for relative in _EXPECTED_RUNTIME_FILES",
595
+ " if actual.get(relative) != _EXPECTED_MANIFEST[relative]",
596
+ " ),",
597
+ ' "unknown",',
598
+ " )",
599
+ " raise RuntimeError(",
600
+ ' f"Loaded FastPLMs version/runtime mismatch at {mismatch!r}. "',
601
+ ' "Install the matching FastPLMs release or use a separate Python process."',
602
+ " )",
603
+ " package.__fastplms_saved_runtime_tree_hash__ = RUNTIME_TREE_HASH",
604
+ " package.__fastplms_saved_runtime_manifest__ = _EXPECTED_MANIFEST",
605
+ " return package",
606
+ "",
607
+ "def _install_runtime():",
608
+ ' installed = sys.modules.get("fastplms")',
609
+ " if installed is not None:",
610
+ " return _verify_loaded_runtime(installed)",
611
+ ' stale = sorted(name for name in sys.modules if name.startswith("fastplms."))',
612
+ " if stale:",
613
+ " raise RuntimeError(",
614
+ ' "Loaded FastPLMs version/runtime mismatch: orphaned submodules exist."',
615
+ " )",
616
+ " package_root, temporary = _ensure_runtime()",
617
+ " spec = importlib.util.spec_from_file_location(",
618
+ ' "fastplms",',
619
+ ' package_root / "__init__.py",',
620
+ " submodule_search_locations=[str(package_root)],",
621
+ " )",
622
+ " if spec is None or spec.loader is None:",
623
+ " temporary.cleanup()",
624
+ ' raise ImportError("Unable to load the embedded FastPLMs runtime.")',
625
+ " package = importlib.util.module_from_spec(spec)",
626
+ ' sys.modules["fastplms"] = package',
627
+ " previous = sys.dont_write_bytecode",
628
+ " sys.dont_write_bytecode = True",
629
+ " try:",
630
+ " spec.loader.exec_module(package)",
631
+ " except BaseException:",
632
+ ' sys.modules.pop("fastplms", None)',
633
+ " temporary.cleanup()",
634
+ " raise",
635
+ " finally:",
636
+ " sys.dont_write_bytecode = previous",
637
+ " _RUNTIME_TEMPORARIES.append(temporary)",
638
+ " package.__fastplms_saved_runtime_tree_hash__ = RUNTIME_TREE_HASH",
639
+ " package.__fastplms_saved_runtime_manifest__ = _EXPECTED_MANIFEST",
640
+ " package.__fastplms_saved_runtime_temporary__ = temporary",
641
+ " return package",
642
+ "",
643
+ "def _import_without_bytecode(module_name):",
644
+ " previous = sys.dont_write_bytecode",
645
+ " sys.dont_write_bytecode = True",
646
+ " try:",
647
+ " return importlib.import_module(module_name)",
648
+ " finally:",
649
+ " sys.dont_write_bytecode = previous",
650
+ "",
651
+ "_install_runtime()",
652
+ '_modeling = _import_without_bytecode("fastplms.models.esm3.modeling_esm3")',
653
+ "FastESM3Config = _modeling.FastESM3Config",
654
+ "FastESM3Model = _modeling.FastESM3Model",
655
+ "",
656
+ ]
657
+ return "\n".join(lines)
658
+
659
+
660
+ def _replace_saved_runtime_file(path: Path, payload: bytes) -> None:
661
+ """Atomically replace one generated runtime file without following a symlink."""
662
+
663
+ path.parent.mkdir(parents=True, exist_ok=True)
664
+ descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
665
+ temporary = Path(temporary_name)
666
+ try:
667
+ with os.fdopen(descriptor, "wb") as handle:
668
+ handle.write(payload)
669
+ handle.flush()
670
+ os.fsync(handle.fileno())
671
+ os.replace(temporary, path)
672
+ finally:
673
+ if temporary.exists():
674
+ temporary.unlink()
675
+
676
+
677
+ def _remove_old_saved_runtime_path(path: Path) -> None:
678
+ try:
679
+ metadata = path.lstat()
680
+ except FileNotFoundError:
681
+ return
682
+ if stat.S_ISDIR(metadata.st_mode) and not stat.S_ISLNK(metadata.st_mode):
683
+ shutil.rmtree(path)
684
+ return
685
+ path.unlink()
686
+
687
+
688
+ def _clean_old_saved_runtime(save_directory: Path) -> None:
689
+ _remove_old_saved_runtime_path(save_directory / "fastplms")
690
+ for pattern in ("_fastplms_runtime_*", "._fastplms_runtime_*"):
691
+ for candidate in save_directory.glob(pattern):
692
+ _remove_old_saved_runtime_path(candidate)
693
+
694
+
695
+ def _validate_saved_runtime_destination(save_directory: Path) -> None:
696
+ if save_directory.is_symlink():
697
+ raise ValueError("ESM3 save directory must not be a symlink.")
698
+ package_source = Path(__file__).resolve().parents[2]
699
+ destination = save_directory.resolve(strict=False)
700
+ if destination == package_source or package_source in destination.parents:
701
+ raise ValueError("ESM3 save directory must be outside the FastPLMs source package.")
702
+ for name in ("config.json", "fastplms_bundle.py", "modeling_fastplms.py"):
703
+ if (save_directory / name).is_symlink():
704
+ raise ValueError(f"ESM3 generated save path must not be a symlink: {name!r}.")
705
+
706
+
707
+ def _write_saved_runtime(
708
+ save_directory: Path,
709
+ prepared_runtime: tuple[bytes, dict[str, object], str] | None = None,
710
+ ) -> None:
711
+ """Make one ESM3 ``save_pretrained`` directory independently loadable."""
712
+
713
+ _validate_saved_runtime_destination(save_directory)
714
+ if prepared_runtime is None:
715
+ package_source = Path(__file__).resolve().parents[2]
716
+ prepared_runtime = _build_saved_runtime_archive(package_source)
717
+ archive, manifest, tree_hash = prepared_runtime
718
+ archive_hash, bundle = _render_saved_runtime_bundle(archive, manifest, tree_hash)
719
+ bridge = _render_saved_runtime_bridge(archive_hash, tree_hash).encode("utf-8")
720
+
721
+ _clean_old_saved_runtime(save_directory)
722
+ _replace_saved_runtime_file(save_directory / "fastplms_bundle.py", bundle)
723
+ _replace_saved_runtime_file(save_directory / "modeling_fastplms.py", bridge)
724
+
725
+ config_path = save_directory / "config.json"
726
+ try:
727
+ config = json.loads(config_path.read_text(encoding="utf-8"))
728
+ except (OSError, json.JSONDecodeError) as error:
729
+ raise RuntimeError("Saved ESM3 config.json is missing or invalid.") from error
730
+ if not isinstance(config, dict):
731
+ raise RuntimeError("Saved ESM3 config.json must contain a JSON object.")
732
+ config["auto_map"] = {
733
+ "AutoConfig": "modeling_fastplms.FastESM3Config",
734
+ "AutoModel": "modeling_fastplms.FastESM3Model",
735
+ }
736
+ config_payload = (json.dumps(config, indent=2, sort_keys=True) + "\n").encode("utf-8")
737
+ _replace_saved_runtime_file(config_path, config_payload)
738
+
739
+
740
+ ESM3_OPEN_SMALL = "esm3_sm_open_v1"
741
+ ESM3_OPEN_SMALL_ALIASES = {
742
+ "ESM3_small",
743
+ "esm3_small",
744
+ "esm3_sm_open_v1",
745
+ "esm3-open-2024-03",
746
+ "esm3-sm-open-v1",
747
+ "esm3-open",
748
+ }
749
+
750
+ SEQUENCE_BOS_TOKEN = 0
751
+ SEQUENCE_PAD_TOKEN = 1
752
+ SEQUENCE_EOS_TOKEN = 2
753
+ SEQUENCE_CHAINBREAK_TOKEN = 31
754
+ SEQUENCE_MASK_TOKEN = 32
755
+
756
+ VQVAE_CODEBOOK_SIZE = 4096
757
+ STRUCTURE_MASK_TOKEN = VQVAE_CODEBOOK_SIZE
758
+ STRUCTURE_EOS_TOKEN = VQVAE_CODEBOOK_SIZE + 1
759
+ STRUCTURE_BOS_TOKEN = VQVAE_CODEBOOK_SIZE + 2
760
+ STRUCTURE_PAD_TOKEN = VQVAE_CODEBOOK_SIZE + 3
761
+ STRUCTURE_CHAINBREAK_TOKEN = VQVAE_CODEBOOK_SIZE + 4
762
+
763
+ SASA_PAD_TOKEN = 0
764
+ SS8_PAD_TOKEN = 0
765
+ INTERPRO_PAD_TOKEN = 0
766
+ RESIDUE_PAD_TOKEN = 0
767
+ MAX_RESIDUE_ANNOTATIONS = 16
768
+ FUNCTION_TOKENS_DEPTH = 8
769
+
770
+ SEQUENCE_VOCAB = [
771
+ "<cls>",
772
+ "<pad>",
773
+ "<eos>",
774
+ "<unk>",
775
+ "L",
776
+ "A",
777
+ "G",
778
+ "V",
779
+ "S",
780
+ "E",
781
+ "R",
782
+ "T",
783
+ "I",
784
+ "D",
785
+ "P",
786
+ "K",
787
+ "Q",
788
+ "N",
789
+ "F",
790
+ "Y",
791
+ "M",
792
+ "H",
793
+ "W",
794
+ "C",
795
+ "X",
796
+ "B",
797
+ "U",
798
+ "Z",
799
+ "O",
800
+ ".",
801
+ "-",
802
+ "|",
803
+ "<mask>",
804
+ ]
805
+
806
+ _SUPPORTED_ATTENTION_BACKENDS = ("eager", "sdpa", "flex_attention")
807
+
808
+
809
+ class FastESM3Config(PretrainedConfig):
810
+ model_type = "fast_esm3"
811
+
812
+ def __init__(
813
+ self,
814
+ vocab_size: int = 64,
815
+ hidden_size: int = 1536,
816
+ num_attention_heads: int = 24,
817
+ num_vector_heads: int = 256,
818
+ num_hidden_layers: int = 48,
819
+ initializer_range: float = 0.02,
820
+ attn_backend: str | None = None,
821
+ model_name: str = ESM3_OPEN_SMALL,
822
+ **kwargs,
823
+ ):
824
+ super().__init__(**kwargs)
825
+ if hidden_size <= 0:
826
+ raise ValueError(f"hidden_size must be positive, got {hidden_size}.")
827
+ if num_attention_heads <= 0:
828
+ raise ValueError(f"num_attention_heads must be positive, got {num_attention_heads}.")
829
+ if hidden_size % FUNCTION_TOKENS_DEPTH != 0:
830
+ raise ValueError(
831
+ f"hidden_size must be divisible by {FUNCTION_TOKENS_DEPTH}, got {hidden_size}."
832
+ )
833
+ if hidden_size % num_attention_heads != 0:
834
+ raise ValueError(
835
+ "hidden_size must be divisible by num_attention_heads, "
836
+ f"got hidden_size={hidden_size} and num_attention_heads={num_attention_heads}."
837
+ )
838
+ self.vocab_size = vocab_size
839
+ self.hidden_size = hidden_size
840
+ self.num_attention_heads = num_attention_heads
841
+ self.num_vector_heads = num_vector_heads
842
+ self.num_hidden_layers = num_hidden_layers
843
+ self.initializer_range = initializer_range
844
+ self.attn_backend = attn_backend
845
+ self.model_name = _resolve_esm3_checkpoint_key(model_name)
846
+ self.tie_word_embeddings = False
847
+
848
+
849
+ @dataclass
850
+ class FastESM3Output(ModelOutput):
851
+ loss: torch.Tensor | None = None
852
+ last_hidden_state: torch.Tensor | None = None
853
+ hidden_states: tuple[torch.Tensor, ...] | None = None
854
+ attentions: tuple[torch.Tensor, ...] | None = None
855
+ logits: torch.Tensor | None = None
856
+ sequence_logits: torch.Tensor | None = None
857
+ structure_logits: torch.Tensor | None = None
858
+ secondary_structure_logits: torch.Tensor | None = None
859
+ sasa_logits: torch.Tensor | None = None
860
+ function_logits: torch.Tensor | None = None
861
+ residue_logits: torch.Tensor | None = None
862
+ embeddings: torch.Tensor | None = None
863
+
864
+
865
+ @dataclass(frozen=True)
866
+ class FastESM3GenerationConfig:
867
+ """Sequence-track sampling controls for the local ESM3 generation API."""
868
+
869
+ num_steps: int | None = None
870
+ temperature: float = 1.0
871
+ seed: int | None = None
872
+
873
+
874
+ class EsmSequenceTokenizer(PreTrainedTokenizerFast):
875
+ model_input_names: ClassVar[list[str]] = ["input_ids", "attention_mask"]
876
+
877
+ def __init__(
878
+ self,
879
+ unk_token: str = "<unk>",
880
+ cls_token: str = "<cls>",
881
+ pad_token: str = "<pad>",
882
+ mask_token: str = "<mask>",
883
+ eos_token: str = "<eos>",
884
+ chain_break_token: str = "|",
885
+ **kwargs,
886
+ ):
887
+ token_to_id = {token: index for index, token in enumerate(SEQUENCE_VOCAB)}
888
+ bpe = BPE(token_to_id, merges=[], unk_token=unk_token)
889
+ tokenizer = Tokenizer(bpe)
890
+ special_tokens = [
891
+ cls_token,
892
+ pad_token,
893
+ mask_token,
894
+ eos_token,
895
+ chain_break_token,
896
+ ]
897
+ self.cb_token = chain_break_token
898
+ tokenizer.add_special_tokens(special_tokens)
899
+ tokenizer.post_processor = TemplateProcessing(
900
+ single="<cls> $A <eos>",
901
+ pair="<cls>:0 $A:0 <eos>:0 $B:1 <eos>:1",
902
+ special_tokens=[
903
+ ("<cls>", tokenizer.token_to_id("<cls>")),
904
+ ("<eos>", tokenizer.token_to_id("<eos>")),
905
+ ],
906
+ )
907
+ super().__init__(
908
+ tokenizer_object=tokenizer,
909
+ unk_token=unk_token,
910
+ cls_token=cls_token,
911
+ pad_token=pad_token,
912
+ mask_token=mask_token,
913
+ eos_token=eos_token,
914
+ additional_special_tokens=[chain_break_token],
915
+ **kwargs,
916
+ )
917
+
918
+ @property
919
+ def bos_token(self) -> str:
920
+ return self.cls_token
921
+
922
+ @property
923
+ def bos_token_id(self) -> int:
924
+ return self.cls_token_id
925
+
926
+ @property
927
+ def chain_break_token(self) -> str:
928
+ return self.cb_token
929
+
930
+ @property
931
+ def chain_break_token_id(self) -> int:
932
+ token_id = self.convert_tokens_to_ids(self.chain_break_token)
933
+ if not isinstance(token_id, int):
934
+ raise RuntimeError("ESM3 chain-break token did not resolve to one token id.")
935
+ return token_id
936
+
937
+ @property
938
+ def all_token_ids(self) -> list[int]:
939
+ return list(range(self.vocab_size))
940
+
941
+ @property
942
+ def special_token_ids(self) -> list[int]:
943
+ return self.all_special_ids
944
+
945
+
946
+ def rbf(values: torch.Tensor, v_min: float, v_max: float, n_bins: int = 16) -> torch.Tensor:
947
+ centers = torch.linspace(
948
+ v_min,
949
+ v_max,
950
+ n_bins,
951
+ device=values.device,
952
+ dtype=values.dtype,
953
+ )
954
+ centers = centers.view([1] * len(values.shape) + [-1])
955
+ std = (v_max - v_min) / n_bins
956
+ z = (values.unsqueeze(-1) - centers) / std
957
+ return torch.exp(-(z**2))
958
+
959
+
960
+ def RegressionHead(
961
+ d_model: int,
962
+ output_dim: int,
963
+ hidden_dim: int | None = None,
964
+ ) -> nn.Module:
965
+ hidden_dim = hidden_dim if hidden_dim is not None else d_model
966
+ return nn.Sequential(
967
+ nn.Linear(d_model, hidden_dim),
968
+ nn.GELU(),
969
+ nn.LayerNorm(hidden_dim),
970
+ nn.Linear(hidden_dim, output_dim),
971
+ )
972
+
973
+
974
+ def rotate_half(x: torch.Tensor, interleaved: bool = False) -> torch.Tensor:
975
+ if not interleaved:
976
+ x1, x2 = x.chunk(2, dim=-1)
977
+ return torch.cat((-x2, x1), dim=-1)
978
+ x1, x2 = x[..., ::2], x[..., 1::2]
979
+ return rearrange(
980
+ torch.stack((-x2, x1), dim=-1),
981
+ "... d two -> ... (d two)",
982
+ two=2,
983
+ )
984
+
985
+
986
+ def apply_rotary_emb_torch(
987
+ x: torch.Tensor,
988
+ cos: torch.Tensor,
989
+ sin: torch.Tensor,
990
+ interleaved: bool = False,
991
+ ) -> torch.Tensor:
992
+ ro_dim = cos.shape[-1] * 2
993
+ if ro_dim > x.shape[-1]:
994
+ raise ValueError(
995
+ "Rotary embedding width cannot exceed the input head dimension; "
996
+ f"got rotary width {ro_dim} and head dimension {x.shape[-1]}."
997
+ )
998
+ seqlen = x.size(1)
999
+ cos = cos[:seqlen]
1000
+ sin = sin[:seqlen]
1001
+ cos = einops.repeat(cos, "s d -> s 1 (2 d)")
1002
+ sin = einops.repeat(sin, "s d -> s 1 (2 d)")
1003
+ return torch.cat(
1004
+ [
1005
+ x[..., :ro_dim] * cos + rotate_half(x[..., :ro_dim], interleaved) * sin,
1006
+ x[..., ro_dim:],
1007
+ ],
1008
+ dim=-1,
1009
+ )
1010
+
1011
+
1012
+ class RotaryEmbedding(nn.Module):
1013
+ def __init__(
1014
+ self,
1015
+ dim: int,
1016
+ base: float = 10000.0,
1017
+ interleaved: bool = False,
1018
+ scale_base: float | None = None,
1019
+ scaling_factor: float = 1.0,
1020
+ pos_idx_in_fp32: bool = True,
1021
+ device: torch.device | None = None,
1022
+ ):
1023
+ super().__init__()
1024
+ self.dim = dim
1025
+ self.base = float(base)
1026
+ self.pos_idx_in_fp32 = pos_idx_in_fp32
1027
+ self.interleaved = interleaved
1028
+ self.scale_base = scale_base
1029
+ self.scaling_factor = scaling_factor
1030
+ self.device = device
1031
+ self._seq_len_cached = 0
1032
+ self._cos_cached = None
1033
+ self._sin_cached = None
1034
+ self.reset_parameters()
1035
+
1036
+ def reset_parameters(self) -> None:
1037
+ inv_freq = self._compute_inv_freq(self.device)
1038
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
1039
+ arange = torch.arange(0, self.dim, 2, device=self.device, dtype=torch.float32)
1040
+ scale = (
1041
+ (arange + 0.4 * self.dim) / (1.4 * self.dim) if self.scale_base is not None else None
1042
+ )
1043
+ self.register_buffer("scale", scale)
1044
+
1045
+ def _compute_inv_freq(self, device: torch.device | None = None) -> torch.Tensor:
1046
+ return 1 / (
1047
+ self.base
1048
+ ** (torch.arange(0, self.dim, 2, device=device, dtype=torch.float32) / self.dim)
1049
+ )
1050
+
1051
+ def _update_cos_sin_cache(
1052
+ self,
1053
+ seqlen: int,
1054
+ device: torch.device | None = None,
1055
+ dtype: torch.dtype | None = None,
1056
+ ) -> None:
1057
+ if (
1058
+ seqlen > self._seq_len_cached
1059
+ or self._cos_cached is None
1060
+ or self._cos_cached.device != device
1061
+ or self._cos_cached.dtype != dtype
1062
+ or (self.training and self._cos_cached.is_inference())
1063
+ ):
1064
+ self._seq_len_cached = seqlen
1065
+ # ``inv_freq`` is non-persistent and may have been materialized
1066
+ # without values after Transformers constructs this module on the
1067
+ # meta device. Recreate it deterministically on the first forward.
1068
+ self.inv_freq = self._compute_inv_freq(device)
1069
+ if self.pos_idx_in_fp32:
1070
+ t = torch.arange(seqlen, device=device, dtype=torch.float32)
1071
+ t /= self.scaling_factor
1072
+ inv_freq = self.inv_freq
1073
+ else:
1074
+ t = torch.arange(seqlen, device=device, dtype=self.inv_freq.dtype)
1075
+ t /= self.scaling_factor
1076
+ inv_freq = self.inv_freq
1077
+ freqs = torch.outer(t, inv_freq)
1078
+
1079
+ if self.scale is None:
1080
+ self._cos_cached = torch.cos(freqs).to(dtype)
1081
+ self._sin_cached = torch.sin(freqs).to(dtype)
1082
+ else:
1083
+ raise NotImplementedError("Scaled rotary embeddings are not used by ESM3.")
1084
+
1085
+ def forward(
1086
+ self,
1087
+ q: torch.Tensor,
1088
+ k: torch.Tensor,
1089
+ seqlen_offset: int = 0,
1090
+ ) -> tuple[torch.Tensor, torch.Tensor]:
1091
+ self._update_cos_sin_cache(
1092
+ q.shape[1] + seqlen_offset,
1093
+ device=q.device,
1094
+ dtype=q.dtype,
1095
+ )
1096
+ if self._cos_cached is None or self._sin_cached is None:
1097
+ raise RuntimeError(
1098
+ "ESM3 rotary cache initialization did not produce sine/cosine tables."
1099
+ )
1100
+ return (
1101
+ apply_rotary_emb_torch(
1102
+ q,
1103
+ self._cos_cached[seqlen_offset:],
1104
+ self._sin_cached[seqlen_offset:],
1105
+ self.interleaved,
1106
+ ),
1107
+ apply_rotary_emb_torch(
1108
+ k,
1109
+ self._cos_cached[seqlen_offset:],
1110
+ self._sin_cached[seqlen_offset:],
1111
+ self.interleaved,
1112
+ ),
1113
+ )
1114
+
1115
+
1116
+ def fp32_autocast_context(device_type: str):
1117
+ if device_type == "cuda":
1118
+ return torch.autocast(device_type="cuda", enabled=False)
1119
+ return torch.autocast(device_type=device_type, enabled=False)
1120
+
1121
+
1122
+ class RotationMatrix:
1123
+ def __init__(self, rots: torch.Tensor):
1124
+ if rots.ndim >= 1 and rots.shape[-1] == 9:
1125
+ rots = rots.unflatten(-1, (3, 3))
1126
+ if rots.ndim < 2 or tuple(rots.shape[-2:]) != (3, 3):
1127
+ raise ValueError(
1128
+ "Rotation matrices must have trailing shape (3, 3) or flattened "
1129
+ f"shape (9,); got {tuple(rots.shape)}."
1130
+ )
1131
+ self._rots = rots.to(torch.float32)
1132
+
1133
+ @classmethod
1134
+ def identity(cls, shape: tuple[int, ...], **tensor_kwargs) -> RotationMatrix:
1135
+ rots = torch.eye(3, **tensor_kwargs)
1136
+ rots = rots.view(*[1 for _ in range(len(shape))], 3, 3)
1137
+ rots = rots.expand(*shape, -1, -1)
1138
+ return cls(rots)
1139
+
1140
+ def __getitem__(self, idx) -> RotationMatrix:
1141
+ indices = (idx,) if isinstance(idx, int) or idx is None else tuple(idx)
1142
+ return RotationMatrix(self._rots[(*indices, slice(None), slice(None))])
1143
+
1144
+ @property
1145
+ def shape(self) -> torch.Size:
1146
+ return self._rots.shape[:-2]
1147
+
1148
+ @property
1149
+ def tensor(self) -> torch.Tensor:
1150
+ return self._rots.flatten(-2)
1151
+
1152
+ @property
1153
+ def device(self) -> torch.device:
1154
+ return self._rots.device
1155
+
1156
+ def as_matrix(self) -> RotationMatrix:
1157
+ return self
1158
+
1159
+ def apply(self, p: torch.Tensor) -> torch.Tensor:
1160
+ with fp32_autocast_context(self.device.type):
1161
+ p = p.to(self._rots.dtype)
1162
+ if self._rots.shape[-3] == 1:
1163
+ return p @ self._rots.transpose(-1, -2).squeeze(-3)
1164
+ return torch.einsum("...ij,...j", self._rots, p)
1165
+
1166
+ def invert(self) -> RotationMatrix:
1167
+ return RotationMatrix(self._rots.transpose(-1, -2))
1168
+
1169
+ @staticmethod
1170
+ def from_graham_schmidt(
1171
+ x_axis: torch.Tensor,
1172
+ xy_plane: torch.Tensor,
1173
+ eps: float = 1e-12,
1174
+ ) -> RotationMatrix:
1175
+ with fp32_autocast_context(x_axis.device.type):
1176
+ e1 = xy_plane
1177
+ denom = torch.sqrt((x_axis**2).sum(dim=-1, keepdim=True) + eps)
1178
+ x_axis = x_axis / denom
1179
+ dot = (x_axis * e1).sum(dim=-1, keepdim=True)
1180
+ e1 = e1 - x_axis * dot
1181
+ denom = torch.sqrt((e1**2).sum(dim=-1, keepdim=True) + eps)
1182
+ e1 = e1 / denom
1183
+ e2 = torch.cross(x_axis, e1, dim=-1)
1184
+ return RotationMatrix(torch.stack([x_axis, e1, e2], dim=-1))
1185
+
1186
+
1187
+ @dataclass(frozen=True)
1188
+ class Affine3D:
1189
+ trans: torch.Tensor
1190
+ rot: RotationMatrix
1191
+
1192
+ def __post_init__(self) -> None:
1193
+ if self.trans.ndim < 1 or self.trans.shape[-1] != 3:
1194
+ raise ValueError(
1195
+ "Affine translations must have trailing dimension 3; "
1196
+ f"got {tuple(self.trans.shape)}."
1197
+ )
1198
+ if self.trans.shape[:-1] != self.rot.shape:
1199
+ raise ValueError(
1200
+ "Affine translation and rotation batch shapes must match; "
1201
+ f"got {tuple(self.trans.shape[:-1])} and {tuple(self.rot.shape)}."
1202
+ )
1203
+
1204
+ def __getitem__(self, idx) -> Affine3D:
1205
+ indices = (idx,) if isinstance(idx, int) or idx is None else tuple(idx)
1206
+ return Affine3D(
1207
+ trans=self.trans[(*indices, slice(None))],
1208
+ rot=self.rot[idx],
1209
+ )
1210
+
1211
+ @property
1212
+ def shape(self) -> torch.Size:
1213
+ return self.trans.shape[:-1]
1214
+
1215
+ @property
1216
+ def dtype(self) -> torch.dtype:
1217
+ return self.trans.dtype
1218
+
1219
+ @property
1220
+ def device(self) -> torch.device:
1221
+ return self.trans.device
1222
+
1223
+ @property
1224
+ def tensor(self) -> torch.Tensor:
1225
+ return torch.cat([self.rot.tensor, self.trans], dim=-1)
1226
+
1227
+ def as_matrix(self) -> Affine3D:
1228
+ return Affine3D(trans=self.trans, rot=self.rot.as_matrix())
1229
+
1230
+ def apply(self, p: torch.Tensor) -> torch.Tensor:
1231
+ return self.rot.apply(p) + self.trans
1232
+
1233
+ @staticmethod
1234
+ def from_tensor(t: torch.Tensor) -> Affine3D:
1235
+ match t.shape[-1]:
1236
+ case 12:
1237
+ trans = t[..., -3:]
1238
+ rot = RotationMatrix(t[..., :-3].unflatten(-1, (3, 3)))
1239
+ case _:
1240
+ raise RuntimeError(
1241
+ f"Cannot detect rotation format from {t.shape[-1] - 3}-d flat vector"
1242
+ )
1243
+ return Affine3D(trans, rot)
1244
+
1245
+ @staticmethod
1246
+ def from_graham_schmidt(
1247
+ neg_x_axis: torch.Tensor,
1248
+ origin: torch.Tensor,
1249
+ xy_plane: torch.Tensor,
1250
+ eps: float = 1e-10,
1251
+ ) -> Affine3D:
1252
+ x_axis = origin - neg_x_axis
1253
+ xy_plane = xy_plane - origin
1254
+ return Affine3D(
1255
+ trans=origin,
1256
+ rot=RotationMatrix.from_graham_schmidt(x_axis, xy_plane, eps),
1257
+ )
1258
+
1259
+
1260
+ def build_affine3d_from_coordinates(coords: torch.Tensor) -> tuple[Affine3D, torch.Tensor]:
1261
+ max_supported_distance = 1e6
1262
+ coord_mask = torch.all(
1263
+ torch.all(torch.isfinite(coords) & (coords < max_supported_distance), dim=-1),
1264
+ dim=-1,
1265
+ )
1266
+
1267
+ def atom3_to_backbone_affine(bb_positions: torch.Tensor) -> Affine3D:
1268
+ n_atom, ca_atom, c_atom = bb_positions.unbind(dim=-2)
1269
+ return Affine3D.from_graham_schmidt(c_atom, ca_atom, n_atom)
1270
+
1271
+ coords = coords.clone().float()
1272
+ coords[~coord_mask] = 0
1273
+ average_per_n_ca_c = coords.masked_fill(~coord_mask[..., None, None], 0).sum(1) / (
1274
+ coord_mask.sum(-1)[..., None, None] + 1e-8
1275
+ )
1276
+ affine_from_average = atom3_to_backbone_affine(average_per_n_ca_c.float()).as_matrix()
1277
+
1278
+ batch_size, seq_len, _, _ = coords.shape
1279
+ affine_rot_mats = affine_from_average.rot.tensor[..., None, :].expand(
1280
+ batch_size,
1281
+ seq_len,
1282
+ 9,
1283
+ )
1284
+ affine_trans = affine_from_average.trans[..., None, :].expand(batch_size, seq_len, 3)
1285
+ identity_rot = RotationMatrix.identity(
1286
+ (batch_size, seq_len),
1287
+ dtype=torch.float32,
1288
+ device=coords.device,
1289
+ requires_grad=False,
1290
+ )
1291
+ affine_rot_mats = affine_rot_mats.where(
1292
+ coord_mask.any(-1)[..., None, None],
1293
+ identity_rot.tensor,
1294
+ )
1295
+ black_hole_affine = Affine3D(affine_trans, RotationMatrix(affine_rot_mats))
1296
+
1297
+ affine = atom3_to_backbone_affine(coords.float())
1298
+ affine = Affine3D.from_tensor(
1299
+ affine.tensor.where(coord_mask[..., None], black_hole_affine.tensor)
1300
+ )
1301
+ return affine, coord_mask
1302
+
1303
+
1304
+ class MultiHeadAttention(nn.Module):
1305
+ def __init__(
1306
+ self,
1307
+ d_model: int,
1308
+ n_heads: int,
1309
+ bias: bool = False,
1310
+ qk_layernorm: bool = True,
1311
+ attn_backend: str = "sdpa",
1312
+ ):
1313
+ super().__init__()
1314
+ self.d_model = d_model
1315
+ self.n_heads = n_heads
1316
+ self.d_head = self.d_model // self.n_heads
1317
+ self.scale = self.d_head**-0.5
1318
+ self.attn_backend = resolve_attention_backend(attn_backend)
1319
+ self.layernorm_qkv = nn.Sequential(
1320
+ nn.LayerNorm(d_model),
1321
+ nn.Linear(d_model, d_model * 3, bias=bias),
1322
+ )
1323
+ self.out_proj = nn.Linear(d_model, d_model, bias=bias)
1324
+ if qk_layernorm:
1325
+ self.q_ln = nn.LayerNorm(d_model, bias=bias)
1326
+ self.k_ln = nn.LayerNorm(d_model, bias=bias)
1327
+ else:
1328
+ self.q_ln = nn.Identity()
1329
+ self.k_ln = nn.Identity()
1330
+ self.rotary = RotaryEmbedding(d_model // n_heads)
1331
+
1332
+ def _apply_rotary(
1333
+ self,
1334
+ q: torch.Tensor,
1335
+ k: torch.Tensor,
1336
+ ) -> tuple[torch.Tensor, torch.Tensor]:
1337
+ q = q.unflatten(-1, (self.n_heads, self.d_head))
1338
+ k = k.unflatten(-1, (self.n_heads, self.d_head))
1339
+ q, k = self.rotary(q, k)
1340
+ q = q.flatten(-2, -1)
1341
+ k = k.flatten(-2, -1)
1342
+ return q, k
1343
+
1344
+ def forward(
1345
+ self,
1346
+ x: torch.Tensor,
1347
+ seq_id: torch.Tensor | None,
1348
+ attention_mask: torch.Tensor | None = None,
1349
+ output_attentions: bool = False,
1350
+ effective_backend: AttentionBackend | None = None,
1351
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
1352
+ qkv = self.layernorm_qkv(x)
1353
+ query, key, value = torch.chunk(qkv, 3, dim=-1)
1354
+ query = self.q_ln(query).to(query.dtype)
1355
+ key = self.k_ln(key).to(query.dtype)
1356
+ query, key = self._apply_rotary(query, key)
1357
+
1358
+ reshaper = functools.partial(
1359
+ einops.rearrange,
1360
+ pattern="b s (h d) -> b h s d",
1361
+ h=self.n_heads,
1362
+ )
1363
+ query, key, value = map(reshaper, (query, key, value))
1364
+
1365
+ mask = None
1366
+ if seq_id is not None:
1367
+ mask = (seq_id.unsqueeze(-1) == seq_id.unsqueeze(-2)).unsqueeze(1)
1368
+ if attention_mask is not None:
1369
+ key_padding_mask = attention_mask[:, None, None, :]
1370
+ mask = key_padding_mask if mask is None else mask & key_padding_mask
1371
+
1372
+ if effective_backend is None:
1373
+ effective_backend = resolve_attention_backend_for_call(
1374
+ self.attn_backend,
1375
+ output_attentions=output_attentions,
1376
+ )
1377
+ if output_attentions or effective_backend == AttentionBackend.EAGER:
1378
+ attn_scores = torch.einsum("bhld,bhsd->bhls", query, key) * self.scale
1379
+ if mask is not None:
1380
+ attn_scores = attn_scores.masked_fill(
1381
+ ~mask,
1382
+ torch.finfo(attn_scores.dtype).min,
1383
+ )
1384
+ attn_weights = torch.softmax(attn_scores, dim=-1)
1385
+ if mask is not None:
1386
+ attn_weights = attn_weights.masked_fill(~mask, 0.0)
1387
+ context = torch.einsum("bhls,bhsd->bhld", attn_weights, value)
1388
+ if not output_attentions:
1389
+ attn_weights = None
1390
+ else:
1391
+ attn_weights = None
1392
+ if effective_backend == AttentionBackend.FLEX:
1393
+ block_mask = self._create_flex_block_mask(seq_id, attention_mask, query)
1394
+ if seq_id is not None and attention_mask is not None:
1395
+ mask_semantics = "sequence_id_and_padding"
1396
+ elif seq_id is not None:
1397
+ mask_semantics = "sequence_id_equality"
1398
+ elif attention_mask is not None:
1399
+ mask_semantics = "padding"
1400
+ else:
1401
+ mask_semantics = "dense"
1402
+ fn = _get_flex_attention_fn(
1403
+ device=query.device,
1404
+ dtype=query.dtype,
1405
+ shape=tuple(query.shape),
1406
+ sequence_lengths=None,
1407
+ mask_semantics=mask_semantics,
1408
+ )
1409
+ if fn is None:
1410
+ raise RuntimeError("Flex Attention is not available in this environment.")
1411
+ context = fn(
1412
+ query,
1413
+ key,
1414
+ value,
1415
+ block_mask=block_mask,
1416
+ scale=self.scale,
1417
+ )
1418
+ elif effective_backend == AttentionBackend.SDPA:
1419
+ context = F.scaled_dot_product_attention(
1420
+ query,
1421
+ key,
1422
+ value,
1423
+ attn_mask=mask,
1424
+ scale=self.scale,
1425
+ )
1426
+ else:
1427
+ raise RuntimeError(f"Unsupported resolved ESM3 backend: {effective_backend}")
1428
+
1429
+ if mask is not None:
1430
+ context = context.masked_fill(~mask.any(dim=-1, keepdim=True), 0.0)
1431
+ context = einops.rearrange(context, "b h s d -> b s (h d)")
1432
+ return self.out_proj(context), attn_weights
1433
+
1434
+ @staticmethod
1435
+ def _create_flex_block_mask(
1436
+ seq_id: torch.Tensor | None,
1437
+ attention_mask: torch.Tensor | None,
1438
+ query: torch.Tensor,
1439
+ ) -> BlockMask | None:
1440
+ if seq_id is None and attention_mask is None:
1441
+ return None
1442
+ if create_block_mask is None:
1443
+ raise RuntimeError(
1444
+ "Flex Attention requested but torch.create_block_mask is unavailable."
1445
+ )
1446
+ batch_size, _, seq_len, _ = query.shape
1447
+
1448
+ def mask_mod(batch_idx, _head_idx, q_idx, kv_idx):
1449
+ if seq_id is None:
1450
+ return attention_mask[batch_idx, kv_idx]
1451
+ allowed = seq_id[batch_idx, q_idx] == seq_id[batch_idx, kv_idx]
1452
+ if attention_mask is not None:
1453
+ allowed = allowed & attention_mask[batch_idx, kv_idx]
1454
+ return allowed
1455
+
1456
+ return create_block_mask(
1457
+ mask_mod,
1458
+ batch_size,
1459
+ 1,
1460
+ seq_len,
1461
+ seq_len,
1462
+ device=query.device,
1463
+ )
1464
+
1465
+
1466
+ class GeometricReasoningOriginalImpl(nn.Module):
1467
+ def __init__(
1468
+ self,
1469
+ c_s: int,
1470
+ v_heads: int,
1471
+ num_vector_messages: int = 1,
1472
+ mask_and_zero_frameless: bool = True,
1473
+ bias: bool = False,
1474
+ ):
1475
+ super().__init__()
1476
+ self.c_s = c_s
1477
+ self.v_heads = v_heads
1478
+ self.num_vector_messages = num_vector_messages
1479
+ self.mask_and_zero_frameless = mask_and_zero_frameless
1480
+
1481
+ coordinate_width = 3
1482
+ vector_channels = coordinate_width * v_heads
1483
+ projection_width = vector_channels * (4 + num_vector_messages)
1484
+ output_width = vector_channels * num_vector_messages
1485
+ self.s_norm = nn.LayerNorm(c_s, bias=bias)
1486
+ self.proj = nn.Linear(c_s, projection_width, bias=bias)
1487
+ self.out_proj = nn.Linear(output_width, c_s, bias=bias)
1488
+ self.distance_scale_per_head = nn.Parameter(torch.zeros(v_heads))
1489
+ self.rotation_scale_per_head = nn.Parameter(torch.zeros(v_heads))
1490
+
1491
+ def forward(
1492
+ self,
1493
+ s: torch.Tensor,
1494
+ affine: Affine3D,
1495
+ affine_mask: torch.Tensor,
1496
+ sequence_id: torch.Tensor | None,
1497
+ chain_id: torch.Tensor,
1498
+ ) -> torch.Tensor:
1499
+ if sequence_id is None:
1500
+ sequence_id = torch.zeros_like(s[..., 0], dtype=torch.int64)
1501
+ attn_bias = sequence_id.unsqueeze(-1) == sequence_id.unsqueeze(-2)
1502
+ attn_bias = attn_bias.unsqueeze(1).float()
1503
+ attn_bias = attn_bias.masked_fill(
1504
+ ~affine_mask[:, None, None, :],
1505
+ torch.finfo(attn_bias.dtype).min,
1506
+ )
1507
+ chain_id_mask = chain_id.unsqueeze(1) != chain_id.unsqueeze(2)
1508
+ attn_bias = attn_bias.masked_fill(
1509
+ chain_id_mask.unsqueeze(1),
1510
+ torch.finfo(s.dtype).min,
1511
+ )
1512
+
1513
+ ns = self.s_norm(s)
1514
+ vec_rot, vec_dist = self.proj(ns).split(
1515
+ [
1516
+ self.v_heads * 2 * 3 + self.v_heads * 3 * self.num_vector_messages,
1517
+ self.v_heads * 2 * 3,
1518
+ ],
1519
+ dim=-1,
1520
+ )
1521
+
1522
+ query_rot, key_rot, value = (
1523
+ affine.rot[..., None]
1524
+ .apply(rearrange(vec_rot, "... (h c) -> ... h c", c=3))
1525
+ .split(
1526
+ [self.v_heads, self.v_heads, self.v_heads * self.num_vector_messages],
1527
+ dim=-2,
1528
+ )
1529
+ )
1530
+ query_dist, key_dist = (
1531
+ affine[..., None]
1532
+ .apply(rearrange(vec_dist, "... (h c) -> ... h c", c=3))
1533
+ .chunk(2, dim=-2)
1534
+ )
1535
+
1536
+ query_dist = rearrange(query_dist, "b s h d -> b h s 1 d")
1537
+ key_dist = rearrange(key_dist, "b s h d -> b h 1 s d")
1538
+ query_rot = rearrange(query_rot, "b s h d -> b h s d")
1539
+ key_rot = rearrange(key_rot, "b s h d -> b h d s")
1540
+ value = rearrange(
1541
+ value,
1542
+ "b s (h m) d -> b h s (m d)",
1543
+ m=self.num_vector_messages,
1544
+ )
1545
+
1546
+ distance_term = (query_dist - key_dist).norm(dim=-1) / math.sqrt(3)
1547
+ rotation_term = query_rot.matmul(key_rot) / math.sqrt(3)
1548
+ distance_term_weight = rearrange(
1549
+ F.softplus(self.distance_scale_per_head),
1550
+ "h -> h 1 1",
1551
+ )
1552
+ rotation_term_weight = rearrange(
1553
+ F.softplus(self.rotation_scale_per_head),
1554
+ "h -> h 1 1",
1555
+ )
1556
+ attn_weight = rotation_term * rotation_term_weight - distance_term * distance_term_weight
1557
+
1558
+ s_q = attn_weight.size(2)
1559
+ s_k = attn_weight.size(3)
1560
+ offset_q = max(0, attn_bias.size(2) - s_q)
1561
+ offset_k = max(0, attn_bias.size(3) - s_k)
1562
+ attn_bias = attn_bias[:, :, offset_q:, offset_k:]
1563
+ attn_weight = torch.softmax(attn_weight + attn_bias, dim=-1)
1564
+
1565
+ attn_out = attn_weight.matmul(value)
1566
+ attn_out = (
1567
+ affine.rot[..., None]
1568
+ .invert()
1569
+ .apply(
1570
+ rearrange(
1571
+ attn_out,
1572
+ "b h s (m d) -> b s (h m) d",
1573
+ m=self.num_vector_messages,
1574
+ )
1575
+ )
1576
+ )
1577
+ attn_out = rearrange(
1578
+ attn_out,
1579
+ "b s (h m) d -> b s (h m d)",
1580
+ m=self.num_vector_messages,
1581
+ )
1582
+ if self.mask_and_zero_frameless:
1583
+ attn_out = attn_out.masked_fill(~affine_mask[..., None], 0.0)
1584
+ attn_out = attn_out.to(self.out_proj.weight.dtype)
1585
+ return self.out_proj(attn_out)
1586
+
1587
+
1588
+ def swiglu_correction_fn(expansion_ratio: float, d_model: int) -> int:
1589
+ return int(((expansion_ratio * d_model) + 255) // 256 * 256)
1590
+
1591
+
1592
+ class SwiGLU(nn.Module):
1593
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
1594
+ x1, x2 = x.chunk(2, dim=-1)
1595
+ return F.silu(x1) * x2
1596
+
1597
+
1598
+ def swiglu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Module:
1599
+ return nn.Sequential(
1600
+ nn.LayerNorm(d_model),
1601
+ nn.Linear(
1602
+ d_model,
1603
+ swiglu_correction_fn(expansion_ratio, d_model) * 2,
1604
+ bias=bias,
1605
+ ),
1606
+ SwiGLU(),
1607
+ nn.Linear(swiglu_correction_fn(expansion_ratio, d_model), d_model, bias=bias),
1608
+ )
1609
+
1610
+
1611
+ def gelu_ln_ffn(d_model: int, expansion_ratio: float, bias: bool) -> nn.Module:
1612
+ hidden_dim = int(expansion_ratio * d_model)
1613
+ return nn.Sequential(
1614
+ nn.LayerNorm(d_model),
1615
+ nn.Linear(d_model, hidden_dim, bias=bias),
1616
+ nn.GELU(),
1617
+ nn.Linear(hidden_dim, d_model, bias=bias),
1618
+ )
1619
+
1620
+
1621
+ class UnifiedTransformerBlock(nn.Module):
1622
+ def __init__(
1623
+ self,
1624
+ d_model: int,
1625
+ n_heads: int,
1626
+ use_geom_attn: bool = False,
1627
+ use_plain_attn: bool = True,
1628
+ v_heads: int | None = None,
1629
+ bias: bool = False,
1630
+ expansion_ratio: float = 4.0,
1631
+ residue_scaling_factor: float = 1.0,
1632
+ mask_and_zero_frameless: bool = False,
1633
+ qk_layernorm: bool = True,
1634
+ ffn_type: str = "swiglu",
1635
+ attn_backend: str = "sdpa",
1636
+ ):
1637
+ super().__init__()
1638
+ self.use_plain_attn = use_plain_attn
1639
+ if self.use_plain_attn:
1640
+ self.attn = MultiHeadAttention(
1641
+ d_model,
1642
+ n_heads,
1643
+ bias,
1644
+ qk_layernorm=qk_layernorm,
1645
+ attn_backend=attn_backend,
1646
+ )
1647
+ self.use_geom_attn = use_geom_attn
1648
+ if self.use_geom_attn:
1649
+ if v_heads is None:
1650
+ raise ValueError("v_heads is required when geometric attention is enabled.")
1651
+ self.geom_attn = GeometricReasoningOriginalImpl(
1652
+ c_s=d_model,
1653
+ v_heads=v_heads,
1654
+ bias=bias,
1655
+ mask_and_zero_frameless=mask_and_zero_frameless,
1656
+ )
1657
+ if ffn_type == "swiglu":
1658
+ self.ffn = swiglu_ln_ffn(d_model, expansion_ratio, bias)
1659
+ elif ffn_type == "gelu":
1660
+ self.ffn = gelu_ln_ffn(d_model, expansion_ratio, bias)
1661
+ else:
1662
+ raise ValueError(f"Unknown ffn_type: {ffn_type}")
1663
+ self.scaling_factor = residue_scaling_factor
1664
+
1665
+ def _add_scaled_residual(
1666
+ self, hidden_states: torch.Tensor, residual: torch.Tensor
1667
+ ) -> torch.Tensor:
1668
+ return hidden_states + residual / self.scaling_factor
1669
+
1670
+ def forward(
1671
+ self,
1672
+ x: torch.Tensor,
1673
+ sequence_id: torch.Tensor | None,
1674
+ attention_mask: torch.Tensor | None,
1675
+ frames: Affine3D,
1676
+ frames_mask: torch.Tensor,
1677
+ chain_id: torch.Tensor,
1678
+ output_attentions: bool = False,
1679
+ effective_backend: AttentionBackend | None = None,
1680
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
1681
+ attn_weights: torch.Tensor | None = None
1682
+ if self.use_plain_attn:
1683
+ plain_residual, attn_weights = self.attn(
1684
+ x,
1685
+ sequence_id,
1686
+ attention_mask,
1687
+ output_attentions=output_attentions,
1688
+ effective_backend=effective_backend,
1689
+ )
1690
+ x = self._add_scaled_residual(x, plain_residual)
1691
+
1692
+ if self.use_geom_attn:
1693
+ geometric_residual = self.geom_attn(
1694
+ x,
1695
+ frames,
1696
+ frames_mask,
1697
+ sequence_id,
1698
+ chain_id,
1699
+ )
1700
+ x = self._add_scaled_residual(x, geometric_residual)
1701
+
1702
+ return self._add_scaled_residual(x, self.ffn(x)), attn_weights
1703
+
1704
+
1705
+ class TransformerStack(nn.Module):
1706
+ def __init__(
1707
+ self,
1708
+ d_model: int,
1709
+ n_heads: int,
1710
+ v_heads: int | None,
1711
+ n_layers: int,
1712
+ n_layers_geom: int = 1,
1713
+ scale_residue: bool = True,
1714
+ mask_and_zero_frameless: bool = False,
1715
+ bias: bool = False,
1716
+ qk_layernorm: bool = True,
1717
+ ffn_type: str = "swiglu",
1718
+ expansion_ratio: float = 8 / 3,
1719
+ attn_backend: str = "sdpa",
1720
+ ):
1721
+ super().__init__()
1722
+ self.blocks = nn.ModuleList(
1723
+ [
1724
+ UnifiedTransformerBlock(
1725
+ d_model,
1726
+ n_heads,
1727
+ v_heads=v_heads,
1728
+ use_geom_attn=index < n_layers_geom,
1729
+ residue_scaling_factor=(math.sqrt(n_layers / 36) if scale_residue else 1.0),
1730
+ expansion_ratio=expansion_ratio,
1731
+ mask_and_zero_frameless=mask_and_zero_frameless,
1732
+ bias=bias,
1733
+ qk_layernorm=qk_layernorm,
1734
+ ffn_type=ffn_type,
1735
+ attn_backend=attn_backend,
1736
+ )
1737
+ for index in range(n_layers)
1738
+ ]
1739
+ )
1740
+ self.attention_backend = resolve_attention_backend(attn_backend)
1741
+ self.norm = nn.LayerNorm(d_model, bias=False)
1742
+
1743
+ def forward(
1744
+ self,
1745
+ x: torch.Tensor,
1746
+ sequence_id: torch.Tensor | None = None,
1747
+ attention_mask: torch.Tensor | None = None,
1748
+ affine: Affine3D | None = None,
1749
+ affine_mask: torch.Tensor | None = None,
1750
+ chain_id: torch.Tensor | None = None,
1751
+ output_attentions: bool = False,
1752
+ output_hidden_states: bool = False,
1753
+ ) -> tuple[
1754
+ torch.Tensor,
1755
+ torch.Tensor,
1756
+ tuple[torch.Tensor, ...] | None,
1757
+ tuple[torch.Tensor, ...] | None,
1758
+ ]:
1759
+ *batch_dims, _ = x.shape
1760
+ if chain_id is None:
1761
+ chain_id = torch.ones(size=batch_dims, dtype=torch.int64, device=x.device)
1762
+ if affine is None or affine_mask is None:
1763
+ raise ValueError("affine and affine_mask are required for ESM3 transformer calls.")
1764
+ effective_backend = resolve_attention_backend_for_call(
1765
+ self.attention_backend,
1766
+ output_attentions=output_attentions,
1767
+ )
1768
+ all_hidden_states = [] if output_hidden_states else None
1769
+ all_attentions = []
1770
+ for block in self.blocks:
1771
+ x, attn_weights = block(
1772
+ x,
1773
+ sequence_id,
1774
+ attention_mask,
1775
+ affine,
1776
+ affine_mask,
1777
+ chain_id,
1778
+ output_attentions=output_attentions,
1779
+ effective_backend=effective_backend,
1780
+ )
1781
+ if all_hidden_states is not None:
1782
+ all_hidden_states.append(x)
1783
+ if output_attentions and attn_weights is not None:
1784
+ all_attentions.append(attn_weights)
1785
+ hidden_states = tuple(all_hidden_states) if all_hidden_states is not None else None
1786
+ attentions = tuple(all_attentions) if output_attentions else None
1787
+ return self.norm(x), x, hidden_states, attentions
1788
+
1789
+
1790
+ class EncodeInputs(nn.Module):
1791
+ def __init__(self, d_model: int, sequence_vocab_size: int = 64):
1792
+ super().__init__()
1793
+
1794
+ discrete_tracks = (
1795
+ ("sequence_embed", sequence_vocab_size),
1796
+ ("structure_tokens_embed", 4101),
1797
+ ("ss8_embed", 11),
1798
+ ("sasa_embed", 19),
1799
+ )
1800
+ for attribute, vocabulary_size in discrete_tracks:
1801
+ setattr(self, attribute, nn.Embedding(vocabulary_size, d_model))
1802
+
1803
+ self.plddt_projection, self.structure_per_res_plddt_projection = (
1804
+ nn.Linear(16, d_model),
1805
+ nn.Linear(16, d_model),
1806
+ )
1807
+ function_width = d_model // 8
1808
+ self.function_embed = nn.ModuleList(
1809
+ nn.Embedding(260, function_width, padding_idx=0) for _ in range(8)
1810
+ )
1811
+ self.residue_embed = nn.EmbeddingBag(1478, d_model, mode="sum", padding_idx=0)
1812
+
1813
+ def forward(
1814
+ self,
1815
+ sequence_tokens: torch.Tensor,
1816
+ structure_tokens: torch.Tensor,
1817
+ average_plddt: torch.Tensor,
1818
+ per_res_plddt: torch.Tensor,
1819
+ ss8_tokens: torch.Tensor,
1820
+ sasa_tokens: torch.Tensor,
1821
+ function_tokens: torch.Tensor,
1822
+ residue_annotation_tokens: torch.Tensor,
1823
+ ) -> torch.Tensor:
1824
+ sequence_embed = self.sequence_embed(sequence_tokens)
1825
+ rbf_16_fn = functools.partial(rbf, v_min=0.0, v_max=1.0, n_bins=16)
1826
+ plddt_embed = self.plddt_projection(
1827
+ rbf_16_fn(average_plddt).to(self.plddt_projection.weight.dtype)
1828
+ )
1829
+ structure_per_res_plddt = self.structure_per_res_plddt_projection(
1830
+ rbf_16_fn(per_res_plddt).to(self.structure_per_res_plddt_projection.weight.dtype)
1831
+ )
1832
+ structure_embed = self.structure_tokens_embed(structure_tokens)
1833
+ ss8_embed = self.ss8_embed(ss8_tokens)
1834
+ sasa_embed = self.sasa_embed(sasa_tokens)
1835
+ function_embed = torch.cat(
1836
+ [
1837
+ embed_fn(funcs)
1838
+ for embed_fn, funcs in zip(
1839
+ self.function_embed,
1840
+ function_tokens.unbind(-1),
1841
+ strict=True,
1842
+ )
1843
+ ],
1844
+ -1,
1845
+ )
1846
+
1847
+ batch_size, seq_len, num_annotations = residue_annotation_tokens.shape
1848
+ residue_embed = self.residue_embed(
1849
+ rearrange(
1850
+ residue_annotation_tokens,
1851
+ "b l n -> (b l) n",
1852
+ b=batch_size,
1853
+ l=seq_len,
1854
+ n=num_annotations,
1855
+ )
1856
+ )
1857
+ residue_embed = rearrange(
1858
+ residue_embed,
1859
+ "(b l) d -> b l d",
1860
+ b=batch_size,
1861
+ l=seq_len,
1862
+ )
1863
+
1864
+ return (
1865
+ sequence_embed
1866
+ + plddt_embed
1867
+ + structure_per_res_plddt
1868
+ + structure_embed
1869
+ + ss8_embed
1870
+ + sasa_embed
1871
+ + function_embed
1872
+ + residue_embed
1873
+ )
1874
+
1875
+
1876
+ @dataclass
1877
+ class ESM3CoreOutput:
1878
+ sequence_logits: torch.Tensor
1879
+ structure_logits: torch.Tensor
1880
+ secondary_structure_logits: torch.Tensor
1881
+ sasa_logits: torch.Tensor
1882
+ function_logits: torch.Tensor
1883
+ residue_logits: torch.Tensor
1884
+ embeddings: torch.Tensor
1885
+ hidden_states: tuple[torch.Tensor, ...] | None = None
1886
+ attentions: tuple[torch.Tensor, ...] | None = None
1887
+
1888
+
1889
+ class OutputHeads(nn.Module):
1890
+ def __init__(self, d_model: int, sequence_vocab_size: int = 64):
1891
+ super().__init__()
1892
+ self.sequence_head = RegressionHead(d_model, sequence_vocab_size)
1893
+ self.structure_head = RegressionHead(d_model, 4096)
1894
+ self.ss8_head = RegressionHead(d_model, 8 + 3)
1895
+ self.sasa_head = RegressionHead(d_model, 16 + 3)
1896
+ self.function_head = RegressionHead(d_model, 260 * 8)
1897
+ self.residue_head = RegressionHead(d_model, 1478)
1898
+
1899
+ def forward(
1900
+ self,
1901
+ x: torch.Tensor,
1902
+ embed: torch.Tensor,
1903
+ hidden_states: tuple[torch.Tensor, ...] | None = None,
1904
+ attentions: tuple[torch.Tensor, ...] | None = None,
1905
+ ) -> ESM3CoreOutput:
1906
+ function_logits = self.function_head(x)
1907
+ function_logits = rearrange(function_logits, "... (k v) -> ... k v", k=8)
1908
+ return ESM3CoreOutput(
1909
+ sequence_logits=self.sequence_head(x),
1910
+ structure_logits=self.structure_head(x),
1911
+ secondary_structure_logits=self.ss8_head(x),
1912
+ sasa_logits=self.sasa_head(x),
1913
+ function_logits=function_logits,
1914
+ residue_logits=self.residue_head(x),
1915
+ embeddings=embed,
1916
+ hidden_states=hidden_states,
1917
+ attentions=attentions,
1918
+ )
1919
+
1920
+
1921
+ class ESM3Core(nn.Module):
1922
+ def __init__(
1923
+ self,
1924
+ d_model: int,
1925
+ n_heads: int,
1926
+ v_heads: int,
1927
+ n_layers: int,
1928
+ attn_backend: str = "sdpa",
1929
+ sequence_vocab_size: int = 64,
1930
+ ):
1931
+ super().__init__()
1932
+ self.encoder = EncodeInputs(d_model, sequence_vocab_size)
1933
+ self.transformer = TransformerStack(
1934
+ d_model,
1935
+ n_heads,
1936
+ v_heads,
1937
+ n_layers,
1938
+ mask_and_zero_frameless=True,
1939
+ attn_backend=attn_backend,
1940
+ )
1941
+ self.output_heads = OutputHeads(d_model, sequence_vocab_size)
1942
+
1943
+ def forward(
1944
+ self,
1945
+ *,
1946
+ sequence_tokens: torch.Tensor | None = None,
1947
+ structure_tokens: torch.Tensor | None = None,
1948
+ ss8_tokens: torch.Tensor | None = None,
1949
+ sasa_tokens: torch.Tensor | None = None,
1950
+ function_tokens: torch.Tensor | None = None,
1951
+ residue_annotation_tokens: torch.Tensor | None = None,
1952
+ average_plddt: torch.Tensor | None = None,
1953
+ per_res_plddt: torch.Tensor | None = None,
1954
+ structure_coords: torch.Tensor | None = None,
1955
+ chain_id: torch.Tensor | None = None,
1956
+ sequence_id: torch.Tensor | None = None,
1957
+ attention_mask: torch.Tensor | None = None,
1958
+ output_attentions: bool | None = None,
1959
+ output_hidden_states: bool | None = None,
1960
+ ) -> ESM3CoreOutput:
1961
+ output_attentions = bool(output_attentions)
1962
+ output_hidden_states = bool(output_hidden_states)
1963
+ present_inputs = [
1964
+ sequence_tokens,
1965
+ structure_tokens,
1966
+ ss8_tokens,
1967
+ sasa_tokens,
1968
+ structure_coords,
1969
+ function_tokens,
1970
+ residue_annotation_tokens,
1971
+ ]
1972
+ try:
1973
+ seq_len, device = next((x.shape[1], x.device) for x in present_inputs if x is not None)
1974
+ except StopIteration:
1975
+ raise ValueError("At least one of the inputs must be non-None") from None
1976
+
1977
+ def defaults(x: torch.Tensor | None, token: int) -> torch.Tensor:
1978
+ if x is None:
1979
+ return torch.full(
1980
+ (1, seq_len),
1981
+ token,
1982
+ dtype=torch.long,
1983
+ device=device,
1984
+ )
1985
+ return x
1986
+
1987
+ sequence_tokens = defaults(sequence_tokens, SEQUENCE_MASK_TOKEN)
1988
+ ss8_tokens = defaults(ss8_tokens, SS8_PAD_TOKEN)
1989
+ sasa_tokens = defaults(sasa_tokens, SASA_PAD_TOKEN)
1990
+ average_plddt = defaults(average_plddt, 1).float()
1991
+ per_res_plddt = defaults(per_res_plddt, 0).float()
1992
+ chain_id = defaults(chain_id, 0)
1993
+
1994
+ if residue_annotation_tokens is None:
1995
+ residue_annotation_tokens = torch.full(
1996
+ (1, seq_len, MAX_RESIDUE_ANNOTATIONS),
1997
+ RESIDUE_PAD_TOKEN,
1998
+ dtype=torch.long,
1999
+ device=device,
2000
+ )
2001
+ if function_tokens is None:
2002
+ function_tokens = torch.full(
2003
+ (1, seq_len, FUNCTION_TOKENS_DEPTH),
2004
+ INTERPRO_PAD_TOKEN,
2005
+ dtype=torch.long,
2006
+ device=device,
2007
+ )
2008
+ if structure_coords is None:
2009
+ structure_coords = torch.full(
2010
+ (1, seq_len, 3, 3),
2011
+ float("nan"),
2012
+ dtype=torch.float,
2013
+ device=device,
2014
+ )
2015
+
2016
+ structure_coords = structure_coords[..., :3, :]
2017
+ affine, affine_mask = build_affine3d_from_coordinates(structure_coords)
2018
+
2019
+ structure_tokens = defaults(structure_tokens, STRUCTURE_MASK_TOKEN)
2020
+ structure_tokens = (
2021
+ structure_tokens.masked_fill(structure_tokens == -1, STRUCTURE_MASK_TOKEN)
2022
+ .masked_fill(sequence_tokens == SEQUENCE_BOS_TOKEN, STRUCTURE_BOS_TOKEN)
2023
+ .masked_fill(sequence_tokens == SEQUENCE_PAD_TOKEN, STRUCTURE_PAD_TOKEN)
2024
+ .masked_fill(sequence_tokens == SEQUENCE_EOS_TOKEN, STRUCTURE_EOS_TOKEN)
2025
+ .masked_fill(
2026
+ sequence_tokens == SEQUENCE_CHAINBREAK_TOKEN,
2027
+ STRUCTURE_CHAINBREAK_TOKEN,
2028
+ )
2029
+ )
2030
+
2031
+ x = self.encoder(
2032
+ sequence_tokens,
2033
+ structure_tokens,
2034
+ average_plddt,
2035
+ per_res_plddt,
2036
+ ss8_tokens,
2037
+ sasa_tokens,
2038
+ function_tokens,
2039
+ residue_annotation_tokens,
2040
+ )
2041
+ expected_mask_shape = tuple(x.shape[:2])
2042
+ if sequence_id is not None and tuple(sequence_id.shape) != expected_mask_shape:
2043
+ raise ValueError(
2044
+ "sequence_id must have shape (batch, sequence); "
2045
+ f"expected {expected_mask_shape}, received {tuple(sequence_id.shape)}."
2046
+ )
2047
+ if attention_mask is not None:
2048
+ if tuple(attention_mask.shape) != expected_mask_shape:
2049
+ raise ValueError(
2050
+ "attention_mask must have shape (batch, sequence); "
2051
+ f"expected {expected_mask_shape}, received {tuple(attention_mask.shape)}."
2052
+ )
2053
+ if attention_mask.dtype != torch.bool and not bool(
2054
+ torch.logical_or(attention_mask == 0, attention_mask == 1).all()
2055
+ ):
2056
+ raise ValueError("attention_mask must contain only boolean or 0/1 values.")
2057
+ attention_mask = attention_mask.to(device=x.device, dtype=torch.bool)
2058
+ if not bool(attention_mask.any(dim=-1).all()):
2059
+ raise ValueError("attention_mask must keep at least one valid key per batch row.")
2060
+ affine_mask = affine_mask & attention_mask
2061
+ x, embedding, hidden_states, attentions = self.transformer(
2062
+ x,
2063
+ sequence_id,
2064
+ attention_mask,
2065
+ affine,
2066
+ affine_mask,
2067
+ chain_id,
2068
+ output_attentions=output_attentions,
2069
+ output_hidden_states=output_hidden_states,
2070
+ )
2071
+ return self.output_heads(
2072
+ x,
2073
+ embedding,
2074
+ hidden_states=hidden_states,
2075
+ attentions=attentions,
2076
+ )
2077
+
2078
+
2079
+ def _resolve_esm3_checkpoint_key(model_name: str) -> str:
2080
+ if model_name in ESM3_OPEN_SMALL_ALIASES:
2081
+ return ESM3_OPEN_SMALL
2082
+ raise ValueError(
2083
+ f"Unsupported ESM3 checkpoint {model_name}. "
2084
+ f"Supported names: {sorted(ESM3_OPEN_SMALL_ALIASES)}"
2085
+ )
2086
+
2087
+
2088
+ def _build_esm3_core(config: FastESM3Config) -> nn.Module:
2089
+ return ESM3Core(
2090
+ d_model=config.hidden_size,
2091
+ n_heads=config.num_attention_heads,
2092
+ v_heads=config.num_vector_heads,
2093
+ n_layers=config.num_hidden_layers,
2094
+ attn_backend=config.attn_backend,
2095
+ sequence_vocab_size=config.vocab_size,
2096
+ )
2097
+
2098
+
2099
+ class FastESM3PreTrainedModel(FastPLMsAttentionMixin, PreTrainedModel):
2100
+ config_class = FastESM3Config
2101
+ base_model_prefix = "esm3"
2102
+ main_input_name = "input_ids"
2103
+ supports_gradient_checkpointing = False
2104
+ all_tied_weights_keys: ClassVar[dict[str, str]] = {}
2105
+ _supports_flash_attn_2 = False
2106
+ _supports_flash_attn_3 = False
2107
+ _fastplms_attention_implementations = _SUPPORTED_ATTENTION_BACKENDS
2108
+
2109
+ @property
2110
+ def tokenizer(self) -> EsmSequenceTokenizer:
2111
+ """Construct the sequence tokenizer only when a raw-sequence API needs it."""
2112
+
2113
+ tokenizer = self.__dict__.get("_fastplms_tokenizer")
2114
+ if tokenizer is None:
2115
+ tokenizer = EsmSequenceTokenizer()
2116
+ self.__dict__["_fastplms_tokenizer"] = tokenizer
2117
+ return tokenizer
2118
+
2119
+ @tokenizer.setter
2120
+ def tokenizer(self, value: EsmSequenceTokenizer | None) -> None:
2121
+ self.__dict__["_fastplms_tokenizer"] = value
2122
+
2123
+ def _init_weights(self, module: nn.Module) -> None:
2124
+ for parameter in module.parameters(recurse=False):
2125
+ if parameter.__dict__.get("_is_hf_initialized"):
2126
+ return
2127
+
2128
+ if isinstance(module, nn.Linear):
2129
+ nn.init.normal_(module.weight, mean=0.0, std=self.config.initializer_range)
2130
+ if module.bias is not None:
2131
+ nn.init.zeros_(module.bias)
2132
+ elif isinstance(module, nn.Embedding):
2133
+ nn.init.normal_(module.weight, mean=0.0, std=self.config.initializer_range)
2134
+ if module.padding_idx is not None:
2135
+ with torch.no_grad():
2136
+ module.weight[module.padding_idx].zero_()
2137
+ elif isinstance(module, nn.LayerNorm):
2138
+ if module.bias is not None:
2139
+ nn.init.zeros_(module.bias)
2140
+ nn.init.ones_(module.weight)
2141
+
2142
+ @property
2143
+ def attn_backend(self) -> str:
2144
+ return self.config.attn_backend
2145
+
2146
+ @attn_backend.setter
2147
+ def attn_backend(self, backend: str) -> None:
2148
+ if backend not in _SUPPORTED_ATTENTION_BACKENDS:
2149
+ raise ValueError(
2150
+ f"ESM3 currently supports only {_SUPPORTED_ATTENTION_BACKENDS}; got {backend}."
2151
+ )
2152
+ self.set_attn_implementation(backend)
2153
+
2154
+
2155
+ class FastESM3Model(FastPLMTestTimeTrainingMixin, FastESM3PreTrainedModel, EmbeddingMixin):
2156
+ config_class = FastESM3Config
2157
+ # Direct ESM3 saves intentionally package an independently loadable remote
2158
+ # runtime. Register the concrete advertised class explicitly so
2159
+ # Transformers writes a real AutoModel key instead of a null auto_map key.
2160
+ _auto_class = "AutoModel"
2161
+
2162
+ def __init__(self, config: FastESM3Config, **kwargs):
2163
+ super().__init__(config, **kwargs)
2164
+ self.esm3 = _build_esm3_core(config)
2165
+ self.post_init()
2166
+ self.init_ttt({"lora_target_replace_module": "MultiHeadAttention"})
2167
+
2168
+ @property
2169
+ def device(self) -> torch.device:
2170
+ return next(self.parameters()).device
2171
+
2172
+ @property
2173
+ def raw_model(self) -> nn.Module:
2174
+ return self.esm3
2175
+
2176
+ def get_input_embeddings(self) -> nn.Module:
2177
+ return self.esm3.encoder.sequence_embed
2178
+
2179
+ def set_input_embeddings(self, value: nn.Module) -> None:
2180
+ self.esm3.encoder.sequence_embed = value
2181
+
2182
+ def get_output_embeddings(self) -> nn.Module:
2183
+ return self.esm3.output_heads.sequence_head[-1]
2184
+
2185
+ def set_output_embeddings(self, value: nn.Module) -> None:
2186
+ self.esm3.output_heads.sequence_head[-1] = value
2187
+
2188
+ def save_pretrained(self, save_directory, *args, **kwargs) -> None:
2189
+ """Save weights plus the unchanged sources needed for an isolated reload."""
2190
+
2191
+ save_path = Path(save_directory)
2192
+ _validate_saved_runtime_destination(save_path)
2193
+ prepared_runtime = _build_saved_runtime_archive(Path(__file__).resolve().parents[2])
2194
+ super().save_pretrained(save_directory, *args, **kwargs)
2195
+ _write_saved_runtime(save_path, prepared_runtime)
2196
+
2197
+ def tokenize_sequences(
2198
+ self,
2199
+ sequences: str | list[str],
2200
+ padding: bool = True,
2201
+ return_tensors: str = "pt",
2202
+ device: torch.device | str | None = None,
2203
+ add_special_tokens: bool = True,
2204
+ ) -> dict[str, torch.Tensor]:
2205
+ tokenized = self.tokenizer(
2206
+ sequences,
2207
+ padding=padding,
2208
+ return_tensors=return_tensors,
2209
+ add_special_tokens=add_special_tokens,
2210
+ )
2211
+ if device is None:
2212
+ return tokenized
2213
+ return {name: tensor.to(device) for name, tensor in tokenized.items()}
2214
+
2215
+ def forward_sequence(
2216
+ self,
2217
+ sequences: str | list[str],
2218
+ device: torch.device | str | None = None,
2219
+ **kwargs,
2220
+ ) -> FastESM3Output:
2221
+ if device is None:
2222
+ device = self.device
2223
+ tokenized = self.tokenize_sequences(sequences, device=device)
2224
+ return self(**tokenized, **kwargs)
2225
+
2226
+ def _embed(
2227
+ self,
2228
+ input_ids: torch.Tensor,
2229
+ attention_mask: torch.Tensor | None = None,
2230
+ hidden_state_index: int = -1,
2231
+ store_all_hidden_states: bool = False,
2232
+ **kwargs,
2233
+ ) -> torch.Tensor:
2234
+ output_hidden_states = store_all_hidden_states or hidden_state_index != -1
2235
+ output = self(
2236
+ input_ids=input_ids,
2237
+ attention_mask=attention_mask,
2238
+ output_hidden_states=output_hidden_states,
2239
+ return_dict=True,
2240
+ **kwargs,
2241
+ )
2242
+ if store_all_hidden_states:
2243
+ if output.hidden_states is None:
2244
+ raise RuntimeError("store_all_hidden_states requires hidden states.")
2245
+ return torch.stack(tuple(output.hidden_states), dim=1)
2246
+ if hidden_state_index == -1:
2247
+ return output.last_hidden_state
2248
+ if output.hidden_states is None:
2249
+ raise RuntimeError("hidden_state_index selection requires hidden states.")
2250
+ return output.hidden_states[hidden_state_index]
2251
+
2252
+ def encode(
2253
+ self,
2254
+ inputs: str | list[str],
2255
+ *,
2256
+ device: torch.device | str | None = None,
2257
+ ) -> dict[str, torch.Tensor]:
2258
+ """Tokenize raw sequences without importing the Biohub SDK."""
2259
+ if isinstance(inputs, str):
2260
+ inputs = inputs.replace("_", self.tokenizer.mask_token)
2261
+ else:
2262
+ inputs = [sequence.replace("_", self.tokenizer.mask_token) for sequence in inputs]
2263
+ return self.tokenize_sequences(inputs, device=device or self.device)
2264
+
2265
+ def decode(self, inputs: torch.Tensor | dict[str, torch.Tensor]) -> str | list[str]:
2266
+ """Decode sequence tokens while removing model special tokens."""
2267
+ token_ids = inputs["input_ids"] if isinstance(inputs, dict) else inputs
2268
+ single = token_ids.ndim == 1
2269
+ if single:
2270
+ token_ids = token_ids.unsqueeze(0)
2271
+ sequences = self.tokenizer.batch_decode(token_ids, skip_special_tokens=True)
2272
+ sequences = [sequence.replace(" ", "") for sequence in sequences]
2273
+ return sequences[0] if single else sequences
2274
+
2275
+ @torch.inference_mode()
2276
+ def generate(
2277
+ self,
2278
+ inputs: str | list[str] | torch.Tensor | dict[str, torch.Tensor],
2279
+ config: FastESM3GenerationConfig | None = None,
2280
+ ) -> str | list[str] | torch.Tensor:
2281
+ """Fill sequence-track mask tokens with iterative categorical sampling.
2282
+
2283
+ Raw strings use ``_`` for masked residues. Tensor inputs use token ID 32.
2284
+ The method samples only amino-acid token IDs and preserves every
2285
+ unmasked input token.
2286
+ """
2287
+ config = config or FastESM3GenerationConfig()
2288
+ if config.temperature <= 0:
2289
+ raise ValueError("temperature must be greater than zero")
2290
+ if config.num_steps is not None:
2291
+ if isinstance(config.num_steps, bool) or not isinstance(config.num_steps, int):
2292
+ raise TypeError("num_steps must be an integer or None")
2293
+ if config.num_steps <= 0:
2294
+ raise ValueError("num_steps must be positive")
2295
+
2296
+ return_strings = isinstance(inputs, (str, list))
2297
+ single_string = isinstance(inputs, str)
2298
+ if return_strings:
2299
+ encoded = self.encode(inputs)
2300
+ token_ids = encoded["input_ids"]
2301
+ conditioning = {"attention_mask": encoded["attention_mask"]}
2302
+ elif isinstance(inputs, dict):
2303
+ supported_inputs = {
2304
+ "input_ids",
2305
+ "attention_mask",
2306
+ "sequence_tokens",
2307
+ "structure_tokens",
2308
+ "ss8_tokens",
2309
+ "sasa_tokens",
2310
+ "function_tokens",
2311
+ "residue_annotation_tokens",
2312
+ "average_plddt",
2313
+ "per_res_plddt",
2314
+ "structure_coords",
2315
+ "chain_id",
2316
+ "sequence_id",
2317
+ }
2318
+ unsupported = sorted(set(inputs) - supported_inputs)
2319
+ if unsupported:
2320
+ names = ", ".join(unsupported)
2321
+ raise TypeError(f"Unsupported ESM3 generation inputs: {names}")
2322
+ if "input_ids" in inputs and "sequence_tokens" in inputs:
2323
+ raise ValueError("Pass only one of input_ids or sequence_tokens to generate().")
2324
+ sequence_key = "input_ids" if "input_ids" in inputs else "sequence_tokens"
2325
+ if sequence_key not in inputs:
2326
+ raise ValueError("ESM3 generation requires input_ids or sequence_tokens.")
2327
+ token_ids = inputs[sequence_key].to(self.device)
2328
+ conditioning = {
2329
+ name: value.to(self.device)
2330
+ for name, value in inputs.items()
2331
+ if name != sequence_key
2332
+ }
2333
+ else:
2334
+ token_ids = inputs.to(self.device)
2335
+ conditioning = {}
2336
+
2337
+ single_tensor = token_ids.ndim == 1
2338
+ if single_tensor:
2339
+ sequence_length = token_ids.shape[0]
2340
+ token_ids = token_ids.unsqueeze(0)
2341
+ conditioning = {
2342
+ name: (
2343
+ value.unsqueeze(0)
2344
+ if value.ndim > 0 and value.shape[0] == sequence_length
2345
+ else value
2346
+ )
2347
+ for name, value in conditioning.items()
2348
+ }
2349
+ sampled_ids = token_ids.clone()
2350
+ initial_mask = sampled_ids.eq(SEQUENCE_MASK_TOKEN)
2351
+ n_masked = int(initial_mask.sum().item())
2352
+ if n_masked == 0:
2353
+ result = sampled_ids.squeeze(0) if single_tensor else sampled_ids
2354
+ if return_strings:
2355
+ decoded = self.decode(result)
2356
+ return decoded[0] if single_string and isinstance(decoded, list) else decoded
2357
+ return result
2358
+
2359
+ n_steps = n_masked if config.num_steps is None else config.num_steps
2360
+ generator = None
2361
+ if config.seed is not None:
2362
+ generator = torch.Generator(device=sampled_ids.device)
2363
+ generator.manual_seed(config.seed)
2364
+
2365
+ for step in range(n_steps):
2366
+ remaining = sampled_ids.eq(SEQUENCE_MASK_TOKEN)
2367
+ if not bool(remaining.any()):
2368
+ break
2369
+ with _temporary_eval(self):
2370
+ output = self(
2371
+ sequence_tokens=sampled_ids,
2372
+ output_attentions=False,
2373
+ output_hidden_states=False,
2374
+ return_dict=True,
2375
+ **conditioning,
2376
+ )
2377
+ amino_acid_logits = output.sequence_logits[..., 4:29] / config.temperature
2378
+ probabilities = amino_acid_logits.softmax(dim=-1)
2379
+ sampled = (
2380
+ torch.multinomial(
2381
+ probabilities.reshape(-1, probabilities.shape[-1]),
2382
+ num_samples=1,
2383
+ generator=generator,
2384
+ ).reshape_as(sampled_ids)
2385
+ + 4
2386
+ )
2387
+
2388
+ remaining_count = int(remaining.sum().item())
2389
+ steps_left = n_steps - step
2390
+ fill_count = max(1, (remaining_count + steps_left - 1) // steps_left)
2391
+ confidence = probabilities.max(dim=-1).values.masked_fill(~remaining, -1.0)
2392
+ selected = torch.zeros_like(remaining)
2393
+ flat_selected = selected.reshape(-1)
2394
+ chosen = confidence.reshape(-1).topk(min(fill_count, remaining_count)).indices
2395
+ flat_selected[chosen] = True
2396
+ sampled_ids[selected] = sampled[selected]
2397
+
2398
+ if bool(sampled_ids.eq(SEQUENCE_MASK_TOKEN).any()):
2399
+ raise RuntimeError("generation ended before all sequence masks were filled")
2400
+ result = sampled_ids.squeeze(0) if single_tensor else sampled_ids
2401
+ if return_strings:
2402
+ decoded = self.decode(result)
2403
+ return decoded[0] if single_string and isinstance(decoded, list) else decoded
2404
+ return result
2405
+
2406
+ def batch_generate(
2407
+ self,
2408
+ inputs: list[str | torch.Tensor],
2409
+ configs: list[FastESM3GenerationConfig],
2410
+ ) -> list[str | torch.Tensor]:
2411
+ if len(inputs) != len(configs):
2412
+ raise ValueError("inputs and configs must have equal lengths")
2413
+ return [self.generate(value, config) for value, config in zip(inputs, configs, strict=True)]
2414
+
2415
+ def _ttt_get_trainable_modules(self) -> list[nn.Module]:
2416
+ return [self.esm3]
2417
+
2418
+ def forward_and_sample(
2419
+ self,
2420
+ inputs: str | list[str] | torch.Tensor | dict[str, torch.Tensor],
2421
+ sampling_configuration: FastESM3GenerationConfig | None = None,
2422
+ ) -> str | list[str] | torch.Tensor:
2423
+ return self.generate(inputs, sampling_configuration)
2424
+
2425
+ def logits(self, inputs=None, **kwargs) -> FastESM3Output:
2426
+ if inputs is None:
2427
+ return self.forward(**kwargs)
2428
+ if isinstance(inputs, (str, list)):
2429
+ return self.forward(**self.encode(inputs), **kwargs)
2430
+ if isinstance(inputs, dict):
2431
+ return self.forward(**inputs, **kwargs)
2432
+ if isinstance(inputs, torch.Tensor):
2433
+ return self.forward(sequence_tokens=inputs, **kwargs)
2434
+ raise TypeError("inputs must be raw sequences, sequence tokens, or a token mapping")
2435
+
2436
+ def forward(
2437
+ self,
2438
+ input_ids: torch.Tensor | None = None,
2439
+ attention_mask: torch.Tensor | None = None,
2440
+ sequence_tokens: torch.Tensor | None = None,
2441
+ structure_tokens: torch.Tensor | None = None,
2442
+ ss8_tokens: torch.Tensor | None = None,
2443
+ sasa_tokens: torch.Tensor | None = None,
2444
+ function_tokens: torch.Tensor | None = None,
2445
+ residue_annotation_tokens: torch.Tensor | None = None,
2446
+ average_plddt: torch.Tensor | None = None,
2447
+ per_res_plddt: torch.Tensor | None = None,
2448
+ structure_coords: torch.Tensor | None = None,
2449
+ chain_id: torch.Tensor | None = None,
2450
+ sequence_id: torch.Tensor | None = None,
2451
+ labels: torch.Tensor | None = None,
2452
+ output_attentions: bool | None = None,
2453
+ output_hidden_states: bool | None = None,
2454
+ return_dict: bool | None = None,
2455
+ **kwargs,
2456
+ ) -> FastESM3Output | tuple[torch.Tensor, ...]:
2457
+ if kwargs:
2458
+ names = ", ".join(sorted(kwargs))
2459
+ raise TypeError(f"Unexpected ESM3 forward arguments: {names}")
2460
+ output_attentions = (
2461
+ output_attentions if output_attentions is not None else self.config.output_attentions
2462
+ )
2463
+ output_hidden_states = (
2464
+ output_hidden_states
2465
+ if output_hidden_states is not None
2466
+ else self.config.output_hidden_states
2467
+ )
2468
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
2469
+ if input_ids is not None and sequence_tokens is not None:
2470
+ raise ValueError("Pass only one of input_ids or sequence_tokens.")
2471
+ if sequence_tokens is None:
2472
+ sequence_tokens = input_ids
2473
+ output = self.esm3(
2474
+ sequence_tokens=sequence_tokens,
2475
+ structure_tokens=structure_tokens,
2476
+ ss8_tokens=ss8_tokens,
2477
+ sasa_tokens=sasa_tokens,
2478
+ function_tokens=function_tokens,
2479
+ residue_annotation_tokens=residue_annotation_tokens,
2480
+ average_plddt=average_plddt,
2481
+ per_res_plddt=per_res_plddt,
2482
+ structure_coords=structure_coords,
2483
+ chain_id=chain_id,
2484
+ sequence_id=sequence_id,
2485
+ attention_mask=attention_mask,
2486
+ output_attentions=output_attentions,
2487
+ output_hidden_states=output_hidden_states,
2488
+ )
2489
+
2490
+ loss = None
2491
+ if labels is not None:
2492
+ labels = labels.to(output.sequence_logits.device)
2493
+ loss = F.cross_entropy(
2494
+ output.sequence_logits.view(-1, output.sequence_logits.shape[-1]),
2495
+ labels.view(-1),
2496
+ ignore_index=-100,
2497
+ )
2498
+
2499
+ result = FastESM3Output(
2500
+ last_hidden_state=output.embeddings,
2501
+ hidden_states=output.hidden_states,
2502
+ attentions=output.attentions,
2503
+ logits=output.sequence_logits,
2504
+ sequence_logits=output.sequence_logits,
2505
+ structure_logits=output.structure_logits,
2506
+ secondary_structure_logits=output.secondary_structure_logits,
2507
+ sasa_logits=output.sasa_logits,
2508
+ function_logits=output.function_logits,
2509
+ residue_logits=output.residue_logits,
2510
+ embeddings=output.embeddings,
2511
+ loss=loss,
2512
+ )
2513
+ if not return_dict:
2514
+ return result.to_tuple()
2515
+ return result
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,1479 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ _WINDOWS_INVALID_PATH_CHARACTERS = frozenset('<>:"|?*')
24
+ _WINDOWS_RESERVED_PATH_NAMES = frozenset(
25
+ {"AUX", "CON", "NUL", "PRN"}
26
+ | {f"COM{index}" for index in range(1, 10)}
27
+ | {f"LPT{index}" for index in range(1, 10)}
28
+ )
29
+ _REPOSITORY_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*$")
30
+ _REFERENCE_CONTAINER_RE = re.compile(r"^reference-[a-z0-9]+(?:-[a-z0-9]+)*$")
31
+ _REFERENCE_ADAPTER_RE = re.compile(
32
+ r"^tests\.parity\.support\.reference_adapters\.[a-z_][a-z0-9_]*$"
33
+ )
34
+ _DOCUMENTATION_FRAGMENT_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
35
+ _ALLOWED_ATTENTION = frozenset(
36
+ {"eager", "sdpa", "flex_attention", "flash_attention_2", "flash_attention_3"}
37
+ )
38
+ _ALLOWED_DTYPES = frozenset({"float32", "bfloat16"})
39
+ _ALLOWED_PRECISIONS = frozenset({"default", "auto", "fp32", "bf16", "fp8"})
40
+ _ALLOWED_BF16_EXECUTIONS = frozenset({"static_parameters", "fp32_parameters_autocast"})
41
+ HUB_LICENSE_IDENTIFIERS = frozenset({"mit", "apache-2.0", "cc-by-nc-sa-4.0", "other"})
42
+ _ALLOWED_TOKENIZER_MODES = frozenset({"tokenizer", "sequence", "structure"})
43
+ _ALLOWED_SIZE_CATEGORIES = frozenset({"small", "medium", "large", "xlarge", "structure"})
44
+ RuntimeExtra = Literal["core", "structure"]
45
+ TestTier = Literal["check", "compliance", "structure", "feature", "artifact", "benchmark"]
46
+ VramTier = Literal["sequence", "large-sequence", "structure", "structure-6b"]
47
+ GenerationContract = Literal["not_applicable", "required", "official_unavailable"]
48
+ RuntimeAssetTrustKind = Literal["hash_pinned_pickle"]
49
+ Bf16Execution = Literal["static_parameters", "fp32_parameters_autocast"]
50
+ DtypeName = Literal["float32", "bfloat16"]
51
+ _ALLOWED_EXTRAS = frozenset({"core", "structure"})
52
+ _ALLOWED_TEST_TIERS = frozenset(
53
+ {"check", "compliance", "structure", "feature", "artifact", "benchmark"}
54
+ )
55
+ _ALLOWED_VRAM_TIERS = frozenset({"sequence", "large-sequence", "structure", "structure-6b"})
56
+ _ALLOWED_GENERATION_CONTRACTS = frozenset({"not_applicable", "required", "official_unavailable"})
57
+ _ALLOWED_RUNTIME_ASSET_TRUST_KINDS = frozenset({"hash_pinned_pickle"})
58
+ _ALLOWED_RUNTIME_ASSET_OFFLINE_BEHAVIORS = frozenset({"requires_cached_verified_file"})
59
+ _ALLOWED_AUTO_CLASSES = frozenset(
60
+ {
61
+ "AutoConfig",
62
+ "AutoModel",
63
+ "AutoModelForMaskedLM",
64
+ "AutoModelForProteinFolding",
65
+ "AutoModelForSequenceClassification",
66
+ "AutoModelForSeq2SeqLM",
67
+ "AutoModelForTokenClassification",
68
+ }
69
+ )
70
+ _WEIGHT_SUFFIXES = (".bin", ".ckpt", ".pt", ".pth", ".safetensors")
71
+ _ALLOWED_ORACLE_ASSET_ROLES = frozenset({"weights", "contact_regression"})
72
+ _FAIR_ESM_ASSET_HOST = "dl.fbaipublicfiles.com"
73
+ _ROOT_FIELDS = frozenset(
74
+ {
75
+ "schema_version",
76
+ "legal_files",
77
+ "attention_kernels",
78
+ "upstreams",
79
+ "families",
80
+ "models",
81
+ "runtime_assets",
82
+ }
83
+ )
84
+ _UPSTREAM_FIELDS = frozenset(
85
+ {
86
+ "id",
87
+ "path",
88
+ "url",
89
+ "revision",
90
+ "license",
91
+ "license_files",
92
+ "license_digests",
93
+ "distribution_files",
94
+ }
95
+ )
96
+ _FAMILY_FIELDS = frozenset(
97
+ {
98
+ "architecture",
99
+ "upstreams",
100
+ "tokenizer_mode",
101
+ "public_input",
102
+ "extra",
103
+ "reference_container",
104
+ "reference_adapter",
105
+ "attention",
106
+ "dtypes",
107
+ "bf16_execution",
108
+ "precisions",
109
+ "experimental_precisions",
110
+ "vram_tier",
111
+ "checkpoint_license",
112
+ "hub_license",
113
+ "hub_license_name",
114
+ "hub_license_link",
115
+ "state_transform",
116
+ "conversion_provenance",
117
+ "representative",
118
+ "documentation",
119
+ "test_tiers",
120
+ "runtime_paths",
121
+ "requires_complete_weight_publication",
122
+ "weights_publication_allowed",
123
+ "auto_map",
124
+ "tokenizer_class",
125
+ "backbone_model",
126
+ }
127
+ )
128
+ _MODEL_FIELDS = frozenset(
129
+ {
130
+ "id",
131
+ "family",
132
+ "size_category",
133
+ "generation_contract",
134
+ "fast_repo",
135
+ "fast_revision",
136
+ "fast_files",
137
+ "fast_unresolved_files",
138
+ "official_repo",
139
+ "official_revision",
140
+ "official_files",
141
+ "official_unresolved_files",
142
+ "oracle_assets",
143
+ "official_golden",
144
+ "artifact_source",
145
+ "canonical_state_sha256",
146
+ "tokenizer_source",
147
+ "auto_map",
148
+ "notes",
149
+ "msa_conditioning",
150
+ }
151
+ )
152
+ _RUNTIME_ASSET_FIELDS = frozenset(
153
+ {
154
+ "id",
155
+ "repository",
156
+ "revision",
157
+ "path",
158
+ "sha256",
159
+ "size",
160
+ "consumer_family",
161
+ "trust_kind",
162
+ "license",
163
+ "offline_behavior",
164
+ }
165
+ )
166
+
167
+
168
+ class RegistryError(ValueError):
169
+ """Raised when the model manifest is incomplete or internally inconsistent."""
170
+
171
+
172
+ def _portable_relative_path(value: str, context: str) -> PurePosixPath:
173
+ """Return one normalized cross-platform relative path or fail closed."""
174
+
175
+ posix = PurePosixPath(value)
176
+ windows = PureWindowsPath(value)
177
+ unsafe_windows_part = any(
178
+ part.rstrip(" .") != part
179
+ or part.split(".", maxsplit=1)[0].upper() in _WINDOWS_RESERVED_PATH_NAMES
180
+ or any(
181
+ ord(character) < 32 or character in _WINDOWS_INVALID_PATH_CHARACTERS
182
+ for character in part
183
+ )
184
+ for part in posix.parts
185
+ )
186
+ if (
187
+ not value
188
+ or not posix.parts
189
+ or posix == PurePosixPath(".")
190
+ or posix.is_absolute()
191
+ or windows.is_absolute()
192
+ or windows.drive
193
+ or "\\" in value
194
+ or "." in posix.parts
195
+ or ".." in posix.parts
196
+ or value != posix.as_posix()
197
+ or any(
198
+ part.lower() in {".git", ".cache", "__pycache__"}
199
+ for part in posix.parts
200
+ )
201
+ or unsafe_windows_part
202
+ ):
203
+ raise RegistryError(f"{context} is not portable: {value!r}")
204
+ return posix
205
+
206
+
207
+ @dataclass(frozen=True, slots=True)
208
+ class FileDigest:
209
+ """Expected content identity for one pinned file."""
210
+
211
+ path: str
212
+ algorithm: str
213
+ digest: str
214
+
215
+ @classmethod
216
+ def parse(cls, value: str) -> FileDigest:
217
+ try:
218
+ path, encoded_digest = value.split("=", maxsplit=1)
219
+ algorithm, digest = encoded_digest.split(":", maxsplit=1)
220
+ except ValueError as error:
221
+ raise RegistryError("File digests must use '<path>=<algorithm>:<digest>'.") from error
222
+
223
+ _portable_relative_path(path, "Checkpoint file path")
224
+
225
+ expected_length = {"git-sha1": 40, "sha256": 64}.get(algorithm)
226
+ if expected_length is None:
227
+ raise RegistryError(f"Unsupported file digest algorithm: {algorithm!r}")
228
+ if len(digest) != expected_length or _HEX_RE.fullmatch(digest) is None:
229
+ raise RegistryError(f"Invalid {algorithm} digest for {path!r}: {digest!r}")
230
+ return cls(path=path, algorithm=algorithm, digest=digest)
231
+
232
+ @property
233
+ def encoded(self) -> str:
234
+ return f"{self.algorithm}:{self.digest}"
235
+
236
+
237
+ @dataclass(frozen=True, slots=True)
238
+ class CheckpointSource:
239
+ """One immutable Hugging Face repository snapshot."""
240
+
241
+ repo_id: str
242
+ revision: str
243
+ files: tuple[FileDigest, ...]
244
+ unresolved_files: tuple[str, ...] = ()
245
+
246
+ @property
247
+ def file_map(self) -> Mapping[str, FileDigest]:
248
+ return MappingProxyType({item.path: item for item in self.files})
249
+
250
+
251
+ @dataclass(frozen=True, slots=True)
252
+ class OracleAsset:
253
+ """Hash-pinned external file required by a native parity oracle."""
254
+
255
+ role: str
256
+ path: str
257
+ url: str
258
+ sha256: str
259
+ size: int
260
+
261
+
262
+ @dataclass(frozen=True, slots=True)
263
+ class RuntimeAsset:
264
+ """Immutable runtime data with an explicit deserialization trust boundary."""
265
+
266
+ id: str
267
+ repository: str
268
+ revision: str
269
+ path: str
270
+ sha256: str
271
+ size: int
272
+ consumer_family: str
273
+ trust_kind: RuntimeAssetTrustKind
274
+ license_expression: str
275
+ offline_behavior: str
276
+
277
+
278
+ @dataclass(frozen=True, slots=True)
279
+ class OfficialGolden:
280
+ """Hash-pinned official output bundle required by the check tier."""
281
+
282
+ metadata: FileDigest
283
+ tensors: FileDigest
284
+
285
+
286
+ @dataclass(frozen=True, slots=True)
287
+ class UpstreamSource:
288
+ """Pinned official implementation used as a parity oracle."""
289
+
290
+ id: str
291
+ path: str
292
+ url: str
293
+ revision: str
294
+ license_expression: str
295
+ license_files: tuple[str, ...]
296
+ license_digests: tuple[FileDigest, ...] = ()
297
+ distribution_files: tuple[FileDigest, ...] = ()
298
+
299
+
300
+ @dataclass(frozen=True, slots=True)
301
+ class AttentionKernelSpec:
302
+ """Immutable Hugging Face kernel used by one attention backend."""
303
+
304
+ implementation: str
305
+ repository: str
306
+ revision: str
307
+ version: int
308
+ expected_variant: str
309
+ dtypes: tuple[DtypeName, ...]
310
+
311
+
312
+ @dataclass(frozen=True, slots=True)
313
+ class ModelFamily:
314
+ """Shared runtime and compliance contract for one architecture family."""
315
+
316
+ id: str
317
+ architecture: str
318
+ upstreams: tuple[str, ...]
319
+ tokenizer_mode: str
320
+ public_input: str
321
+ extra: RuntimeExtra
322
+ reference_container: str
323
+ reference_adapter: str
324
+ attention: tuple[str, ...]
325
+ dtypes: tuple[DtypeName, ...]
326
+ bf16_execution: Bf16Execution
327
+ precisions: tuple[str, ...]
328
+ vram_tier: VramTier
329
+ checkpoint_license: str
330
+ hub_license: str
331
+ state_transform: str
332
+ representative: str
333
+ documentation: str
334
+ test_tiers: tuple[TestTier, ...]
335
+ runtime_paths: tuple[str, ...]
336
+ auto_map_items: tuple[tuple[str, str], ...]
337
+ requires_complete_weight_publication: bool = False
338
+ weights_publication_allowed: bool = False
339
+ experimental_precisions: tuple[str, ...] = ()
340
+ tokenizer_class: str | None = None
341
+ hub_license_name: str | None = None
342
+ hub_license_link: str | None = None
343
+ conversion_provenance: str = ""
344
+ backbone_model: str | None = None
345
+
346
+ @property
347
+ def auto_map(self) -> Mapping[str, str]:
348
+ return MappingProxyType(dict(self.auto_map_items))
349
+
350
+ @property
351
+ def hub_license_metadata(self) -> Mapping[str, str]:
352
+ """Return valid Hugging Face model-card license fields."""
353
+
354
+ metadata = {"license": self.hub_license}
355
+ if self.hub_license_name is not None:
356
+ metadata["license_name"] = self.hub_license_name
357
+ if self.hub_license_link is not None:
358
+ metadata["license_link"] = self.hub_license_link
359
+ return MappingProxyType(metadata)
360
+
361
+ @property
362
+ def stable_precisions(self) -> tuple[str, ...]:
363
+ """Return precision policies covered by the release contract."""
364
+
365
+ experimental = set(self.experimental_precisions)
366
+ return tuple(precision for precision in self.precisions if precision not in experimental)
367
+
368
+
369
+ @dataclass(frozen=True, slots=True)
370
+ class ModelSpec:
371
+ """Complete immutable source and runtime contract for one checkpoint."""
372
+
373
+ id: str
374
+ family: ModelFamily
375
+ fast: CheckpointSource
376
+ official: CheckpointSource
377
+ size_category: str
378
+ generation_contract: GenerationContract = "not_applicable"
379
+ oracle_assets: tuple[OracleAsset, ...] = ()
380
+ official_golden: OfficialGolden | None = None
381
+ artifact_source: str = "fast"
382
+ canonical_state_sha256: str | None = None
383
+ tokenizer_source_id: str | None = None
384
+ auto_map_items: tuple[tuple[str, str], ...] = ()
385
+ notes: str = ""
386
+ msa_conditioning: bool | None = None
387
+
388
+ @property
389
+ def is_deep_reference(self) -> bool:
390
+ return self.id == self.family.representative
391
+
392
+ @property
393
+ def auto_map(self) -> Mapping[str, str]:
394
+ if self.auto_map_items:
395
+ return MappingProxyType(dict(self.auto_map_items))
396
+ return self.family.auto_map
397
+
398
+ @property
399
+ def artifact_checkpoint(self) -> CheckpointSource:
400
+ """Return the checkpoint selected for local artifact construction."""
401
+
402
+ return self.fast if self.artifact_source == "fast" else self.official
403
+
404
+ @property
405
+ def oracle_asset_map(self) -> Mapping[str, OracleAsset]:
406
+ """Return native oracle assets keyed by their declared role."""
407
+
408
+ return MappingProxyType({asset.role: asset for asset in self.oracle_assets})
409
+
410
+
411
+ class ModelRegistry(Mapping[str, ModelSpec]):
412
+ """Validated mapping of model IDs to typed model specifications."""
413
+
414
+ def __init__(
415
+ self,
416
+ *,
417
+ schema_version: int,
418
+ upstreams: Mapping[str, UpstreamSource],
419
+ families: Mapping[str, ModelFamily],
420
+ models: Mapping[str, ModelSpec],
421
+ runtime_assets: Mapping[str, RuntimeAsset] = MappingProxyType({}),
422
+ attention_kernels: Mapping[str, AttentionKernelSpec] = MappingProxyType({}),
423
+ legal_files: tuple[FileDigest, ...] = (),
424
+ ) -> None:
425
+ self.schema_version = schema_version
426
+ self.upstreams = MappingProxyType(dict(upstreams))
427
+ self.attention_kernels = MappingProxyType(dict(attention_kernels))
428
+ self.families = MappingProxyType(dict(families))
429
+ self._models = MappingProxyType(dict(models))
430
+ self.runtime_assets = MappingProxyType(dict(runtime_assets))
431
+ self.legal_files = legal_files
432
+
433
+ def __getitem__(self, key: str) -> ModelSpec:
434
+ return self._models[key]
435
+
436
+ def __iter__(self) -> Iterator[str]:
437
+ return iter(self._models)
438
+
439
+ def __len__(self) -> int:
440
+ return len(self._models)
441
+
442
+ def by_family(self, family_id: str) -> tuple[ModelSpec, ...]:
443
+ if family_id not in self.families:
444
+ raise KeyError(family_id)
445
+ return tuple(model for model in self._models.values() if model.family.id == family_id)
446
+
447
+ def supported_attention_dtypes(
448
+ self,
449
+ family_id: str,
450
+ implementation: str,
451
+ ) -> tuple[DtypeName, ...]:
452
+ """Return manifest-supported dtypes for one family/backend pair."""
453
+
454
+ family = self.families[family_id]
455
+ if implementation not in family.attention:
456
+ raise KeyError(
457
+ f"Family {family_id!r} does not advertise attention backend "
458
+ f"{implementation!r}."
459
+ )
460
+ kernel = self.attention_kernels.get(implementation)
461
+ if kernel is None:
462
+ return family.dtypes
463
+ return tuple(dtype for dtype in family.dtypes if dtype in kernel.dtypes)
464
+
465
+ def require_resolved(self, model_id: str | None = None) -> None:
466
+ """Fail release validation when required file identities remain unresolved."""
467
+
468
+ selected = self._models.values() if model_id is None else (self._models[model_id],)
469
+ unresolved: list[str] = []
470
+ for model in selected:
471
+ for label, checkpoint in (("fast", model.fast), ("official", model.official)):
472
+ for path in checkpoint.unresolved_files:
473
+ unresolved.append(f"{model.id}.{label}:{path}")
474
+ if unresolved:
475
+ detail = ", ".join(unresolved)
476
+ raise RegistryError(f"Release provenance is unresolved: {detail}")
477
+
478
+
479
+ def _reject_unknown_fields(
480
+ table: Mapping[str, Any],
481
+ allowed: frozenset[str],
482
+ context: str,
483
+ ) -> None:
484
+ unknown = sorted(set(table).difference(allowed))
485
+ if unknown:
486
+ raise RegistryError(f"{context} contains unknown fields: {unknown}.")
487
+
488
+
489
+ def _require_str(table: Mapping[str, Any], key: str, context: str) -> str:
490
+ value = table.get(key)
491
+ if not isinstance(value, str) or not value.strip():
492
+ raise RegistryError(f"{context}.{key} must be a non-empty string.")
493
+ return value
494
+
495
+
496
+ def _require_enum(
497
+ table: Mapping[str, Any],
498
+ key: str,
499
+ context: str,
500
+ allowed: frozenset[str],
501
+ ) -> str:
502
+ value = _require_str(table, key, context)
503
+ if value not in allowed:
504
+ raise RegistryError(
505
+ f"{context}.{key} must be one of {sorted(allowed)}; received {value!r}."
506
+ )
507
+ return value
508
+
509
+
510
+ def _parse_reference_container(table: Mapping[str, Any], context: str) -> str:
511
+ value = _require_str(table, "reference_container", context)
512
+ if _REFERENCE_CONTAINER_RE.fullmatch(value) is None:
513
+ raise RegistryError(
514
+ f"{context}.reference_container must be a portable 'reference-<name>' target."
515
+ )
516
+ return value
517
+
518
+
519
+ def _parse_reference_adapter(table: Mapping[str, Any], context: str) -> str:
520
+ value = _require_str(table, "reference_adapter", context)
521
+ if _REFERENCE_ADAPTER_RE.fullmatch(value) is None:
522
+ raise RegistryError(
523
+ f"{context}.reference_adapter must name one module under "
524
+ "tests.parity.support.reference_adapters."
525
+ )
526
+ return value
527
+
528
+
529
+ def _parse_documentation_path(table: Mapping[str, Any], context: str) -> str:
530
+ value = _require_str(table, "documentation", context)
531
+ if value.count("#") > 1 or "\\" in value:
532
+ raise RegistryError(f"{context}.documentation must be a portable documentation path.")
533
+ raw_path, separator, fragment = value.partition("#")
534
+ path = PurePosixPath(raw_path)
535
+ if (
536
+ path.is_absolute()
537
+ or ".." in path.parts
538
+ or len(path.parts) < 2
539
+ or path.parts[0] != "docs"
540
+ or path.suffix != ".md"
541
+ or path.as_posix() != raw_path
542
+ ):
543
+ raise RegistryError(
544
+ f"{context}.documentation must reference a normalized Markdown file under docs/."
545
+ )
546
+ if separator and _DOCUMENTATION_FRAGMENT_RE.fullmatch(fragment) is None:
547
+ raise RegistryError(f"{context}.documentation has an invalid heading fragment.")
548
+ return value
549
+
550
+
551
+ def _require_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple[str, ...]:
552
+ value = table.get(key)
553
+ if not isinstance(value, list) or not value or any(not isinstance(item, str) for item in value):
554
+ raise RegistryError(f"{context}.{key} must be a non-empty string array.")
555
+ result = tuple(value)
556
+ if len(set(result)) != len(result):
557
+ raise RegistryError(f"{context}.{key} contains duplicate values.")
558
+ return result
559
+
560
+
561
+ def _optional_str_list(table: Mapping[str, Any], key: str, context: str) -> tuple[str, ...]:
562
+ value = table.get(key, [])
563
+ if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
564
+ raise RegistryError(f"{context}.{key} must be a string array.")
565
+ result = tuple(value)
566
+ if len(set(result)) != len(result):
567
+ raise RegistryError(f"{context}.{key} contains duplicate values.")
568
+ return result
569
+
570
+
571
+ def _optional_str(table: Mapping[str, Any], key: str, context: str) -> str | None:
572
+ value = table.get(key)
573
+ if value is None:
574
+ return None
575
+ if (
576
+ not isinstance(value, str)
577
+ or not value.strip()
578
+ or value != value.strip()
579
+ or "\n" in value
580
+ or "\r" in value
581
+ ):
582
+ raise RegistryError(f"{context}.{key} must be a non-empty single-line string.")
583
+ return value
584
+
585
+
586
+ def _parse_hub_license(
587
+ table: Mapping[str, Any],
588
+ *,
589
+ checkpoint_license: str,
590
+ context: str,
591
+ ) -> tuple[str, str | None, str | None]:
592
+ expected_fields = {"hub_license", "hub_license_name", "hub_license_link"}
593
+ unknown_fields = sorted(
594
+ key for key in table if key.startswith("hub_") and key not in expected_fields
595
+ )
596
+ if unknown_fields:
597
+ raise RegistryError(f"{context} contains unsupported Hub license fields: {unknown_fields}.")
598
+ identifier = _require_str(table, "hub_license", context)
599
+ if identifier not in HUB_LICENSE_IDENTIFIERS:
600
+ raise RegistryError(
601
+ f"{context}.hub_license must be a supported Hugging Face license identifier."
602
+ )
603
+ expected_identifier: str | None = None
604
+ for prefix, candidate in (
605
+ ("MIT", "mit"),
606
+ ("Apache-2.0", "apache-2.0"),
607
+ ("CC-BY-NC-SA-4.0", "cc-by-nc-sa-4.0"),
608
+ ("Profluent-E1-Agreement", "other"),
609
+ ("Unresolved", "other"),
610
+ ):
611
+ if checkpoint_license.startswith(prefix):
612
+ expected_identifier = candidate
613
+ break
614
+ if expected_identifier is None:
615
+ raise RegistryError(
616
+ f"{context}.checkpoint_license has no declared Hugging Face identifier mapping."
617
+ )
618
+ if identifier != expected_identifier:
619
+ raise RegistryError(
620
+ f"{context}.hub_license must be {expected_identifier!r} for "
621
+ f"checkpoint terms {checkpoint_license!r}."
622
+ )
623
+
624
+ name = _optional_str(table, "hub_license_name", context)
625
+ link = _optional_str(table, "hub_license_link", context)
626
+ if identifier != "other":
627
+ if name is not None or link is not None:
628
+ raise RegistryError(
629
+ f"{context} may define hub_license_name and hub_license_link only "
630
+ "when hub_license='other'."
631
+ )
632
+ return identifier, None, None
633
+ if name is None or link is None:
634
+ raise RegistryError(
635
+ f"{context} must define hub_license_name and hub_license_link when hub_license='other'."
636
+ )
637
+ parsed_link = urlparse(link)
638
+ if (
639
+ parsed_link.scheme != "https"
640
+ or not parsed_link.netloc
641
+ or not parsed_link.path
642
+ or parsed_link.username is not None
643
+ or parsed_link.password is not None
644
+ ):
645
+ raise RegistryError(f"{context}.hub_license_link must be an absolute HTTPS URL.")
646
+ return identifier, name, link
647
+
648
+
649
+ def _require_digest_list(
650
+ table: Mapping[str, Any], key: str, context: str
651
+ ) -> tuple[FileDigest, ...]:
652
+ encoded = _require_str_list(table, key, context)
653
+ result = tuple(FileDigest.parse(value) for value in encoded)
654
+ paths = [item.path for item in result]
655
+ if len(paths) != len(set(paths)):
656
+ raise RegistryError(f"{context}.{key} contains duplicate paths.")
657
+ return result
658
+
659
+
660
+ def _validate_revision(revision: str, context: str) -> None:
661
+ if len(revision) != 40 or _HEX_RE.fullmatch(revision) is None:
662
+ raise RegistryError(f"{context} must be an immutable 40-character commit revision.")
663
+
664
+
665
+ def _parse_checkpoint(table: Mapping[str, Any], prefix: str, context: str) -> CheckpointSource:
666
+ repo_id = _require_str(table, f"{prefix}_repo", context)
667
+ if _REPOSITORY_ID_RE.fullmatch(repo_id) is None:
668
+ raise RegistryError(f"{context}.{prefix}_repo must be a Hugging Face repository ID.")
669
+ revision = _require_str(table, f"{prefix}_revision", context)
670
+ _validate_revision(revision, f"{context}.{prefix}_revision")
671
+ encoded_files = _require_str_list(table, f"{prefix}_files", context)
672
+ files = tuple(FileDigest.parse(value) for value in encoded_files)
673
+ paths = [item.path for item in files]
674
+ if len(paths) != len(set(paths)):
675
+ raise RegistryError(f"{context}.{prefix}_files contains duplicate paths.")
676
+ if not any(item.path.endswith(_WEIGHT_SUFFIXES) for item in files):
677
+ raise RegistryError(f"{context}.{prefix}_files does not identify a weight file.")
678
+ unresolved_files = _optional_str_list(table, f"{prefix}_unresolved_files", context)
679
+ for unresolved_path in unresolved_files:
680
+ _portable_relative_path(unresolved_path, "Unresolved checkpoint path")
681
+ if unresolved_path in paths:
682
+ raise RegistryError(
683
+ f"{context}.{prefix} marks {unresolved_path!r} both resolved and unresolved."
684
+ )
685
+ return CheckpointSource(
686
+ repo_id=repo_id,
687
+ revision=revision,
688
+ files=files,
689
+ unresolved_files=unresolved_files,
690
+ )
691
+
692
+
693
+ def _parse_oracle_assets(table: Mapping[str, Any], context: str) -> tuple[OracleAsset, ...]:
694
+ raw = table.get("oracle_assets", [])
695
+ if not isinstance(raw, list):
696
+ raise RegistryError(f"{context}.oracle_assets must be an array of tables.")
697
+ result: list[OracleAsset] = []
698
+ for index, value in enumerate(raw):
699
+ asset_context = f"{context}.oracle_assets[{index}]"
700
+ if not isinstance(value, dict):
701
+ raise RegistryError(f"{asset_context} must be a table.")
702
+ expected_fields = {"role", "path", "url", "sha256", "size"}
703
+ if set(value) != expected_fields:
704
+ raise RegistryError(f"{asset_context} must contain exactly {sorted(expected_fields)}.")
705
+ role = _require_str(value, "role", asset_context)
706
+ if role not in _ALLOWED_ORACLE_ASSET_ROLES:
707
+ raise RegistryError(f"Unsupported oracle asset role: {role!r}.")
708
+ path = _require_str(value, "path", asset_context)
709
+ try:
710
+ normalized_path = _portable_relative_path(path, "Oracle asset path")
711
+ except RegistryError as error:
712
+ raise RegistryError(f"Invalid oracle asset path: {path!r}.") from error
713
+ if normalized_path.suffix != ".pt":
714
+ raise RegistryError(f"Invalid oracle asset path: {path!r}.")
715
+ url = _require_str(value, "url", asset_context)
716
+ parsed_url = urlparse(url)
717
+ if (
718
+ parsed_url.scheme != "https"
719
+ or parsed_url.hostname != _FAIR_ESM_ASSET_HOST
720
+ or parsed_url.path != f"/fair-esm/{path}"
721
+ or parsed_url.params
722
+ or parsed_url.query
723
+ or parsed_url.fragment
724
+ ):
725
+ raise RegistryError(f"Invalid fair-esm oracle asset URL: {url!r}.")
726
+ sha256 = _require_str(value, "sha256", asset_context)
727
+ if len(sha256) != 64 or _HEX_RE.fullmatch(sha256) is None:
728
+ raise RegistryError(f"Invalid oracle asset SHA-256 for {path!r}.")
729
+ size = value.get("size")
730
+ if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
731
+ raise RegistryError(f"{asset_context}.size must be a positive byte count.")
732
+ result.append(
733
+ OracleAsset(
734
+ role=role,
735
+ path=path,
736
+ url=url,
737
+ sha256=sha256,
738
+ size=size,
739
+ )
740
+ )
741
+ roles = [asset.role for asset in result]
742
+ paths = [asset.path for asset in result]
743
+ urls = [asset.url for asset in result]
744
+ if (
745
+ len(roles) != len(set(roles))
746
+ or len(paths) != len(set(paths))
747
+ or len(urls) != len(set(urls))
748
+ ):
749
+ raise RegistryError(f"{context}.oracle_assets contains duplicate identities.")
750
+ return tuple(result)
751
+
752
+
753
+ def _parse_official_golden(
754
+ table: Mapping[str, Any],
755
+ model_id: str,
756
+ context: str,
757
+ ) -> OfficialGolden | None:
758
+ raw = table.get("official_golden")
759
+ if raw is None:
760
+ return None
761
+ if not isinstance(raw, dict) or set(raw) != {"metadata", "tensors"}:
762
+ raise RegistryError(
763
+ f"{context}.official_golden must contain exactly 'metadata' and 'tensors'."
764
+ )
765
+ parsed: dict[str, FileDigest] = {}
766
+ for role in ("metadata", "tensors"):
767
+ value = raw[role]
768
+ if not isinstance(value, str):
769
+ raise RegistryError(f"{context}.official_golden.{role} must be a file digest.")
770
+ digest = FileDigest.parse(value)
771
+ if digest.algorithm != "sha256":
772
+ raise RegistryError(
773
+ f"{context}.official_golden.{role} must use an immutable SHA-256 digest."
774
+ )
775
+ expected = f"tests/goldens/{model_id}.{'json' if role == 'metadata' else 'safetensors'}"
776
+ if digest.path != expected:
777
+ raise RegistryError(f"{context}.official_golden.{role} must use path {expected!r}.")
778
+ parsed[role] = digest
779
+ return OfficialGolden(metadata=parsed["metadata"], tensors=parsed["tensors"])
780
+
781
+
782
+ def _parse_attention_kernels(raw: object) -> dict[str, AttentionKernelSpec]:
783
+ if not isinstance(raw, list) or not raw:
784
+ raise RegistryError("The manifest must contain [[attention_kernels]] entries.")
785
+ result: dict[str, AttentionKernelSpec] = {}
786
+ expected_variants = {
787
+ "flash_attention_2": "flash_attn2",
788
+ "flash_attention_3": "flash_attn3",
789
+ }
790
+ for index, value in enumerate(raw):
791
+ context = f"attention_kernels[{index}]"
792
+ if not isinstance(value, dict):
793
+ raise RegistryError(f"{context} must be a table.")
794
+ expected_fields = frozenset(
795
+ {
796
+ "implementation",
797
+ "repository",
798
+ "revision",
799
+ "version",
800
+ "expected_variant",
801
+ "dtypes",
802
+ }
803
+ )
804
+ _reject_unknown_fields(value, expected_fields, context)
805
+ implementation = _require_str(value, "implementation", context)
806
+ if implementation not in expected_variants:
807
+ raise RegistryError(f"Unsupported attention kernel {implementation!r}.")
808
+ if implementation in result:
809
+ raise RegistryError(f"Duplicate attention kernel {implementation!r}.")
810
+ repository = _require_str(value, "repository", context)
811
+ if _REPOSITORY_ID_RE.fullmatch(repository) is None:
812
+ raise RegistryError(f"Invalid attention-kernel repository {repository!r}.")
813
+ revision = _require_str(value, "revision", context)
814
+ _validate_revision(revision, f"{context}.revision")
815
+ kernel_version = value.get("version")
816
+ if (
817
+ isinstance(kernel_version, bool)
818
+ or not isinstance(kernel_version, int)
819
+ or kernel_version <= 0
820
+ ):
821
+ raise RegistryError(f"{context}.version must be a positive integer.")
822
+ expected_variant = _require_str(value, "expected_variant", context)
823
+ if expected_variant != expected_variants[implementation]:
824
+ raise RegistryError(
825
+ f"{context}.expected_variant must be {expected_variants[implementation]!r}."
826
+ )
827
+ dtypes = _require_str_list(value, "dtypes", context)
828
+ if not set(dtypes).issubset(_ALLOWED_DTYPES):
829
+ raise RegistryError(f"{context}.dtypes contains unsupported dtypes.")
830
+ result[implementation] = AttentionKernelSpec(
831
+ implementation=implementation,
832
+ repository=repository,
833
+ revision=revision,
834
+ version=kernel_version,
835
+ expected_variant=expected_variant,
836
+ dtypes=cast(tuple[DtypeName, ...], dtypes),
837
+ )
838
+ if set(result) != set(expected_variants):
839
+ raise RegistryError("The manifest must pin both FlashAttention kernel versions.")
840
+ return result
841
+
842
+
843
+ def _parse_upstreams(raw: object) -> dict[str, UpstreamSource]:
844
+ if not isinstance(raw, list) or not raw:
845
+ raise RegistryError("The manifest must contain at least one [[upstreams]] entry.")
846
+ result: dict[str, UpstreamSource] = {}
847
+ paths: set[str] = set()
848
+ for index, value in enumerate(raw):
849
+ context = f"upstreams[{index}]"
850
+ if not isinstance(value, dict):
851
+ raise RegistryError(f"{context} must be a table.")
852
+ _reject_unknown_fields(value, _UPSTREAM_FIELDS, context)
853
+ source_id = _require_str(value, "id", context)
854
+ if _IDENTIFIER_RE.fullmatch(source_id) is None:
855
+ raise RegistryError(f"Invalid upstream ID: {source_id!r}")
856
+ if source_id in result:
857
+ raise RegistryError(f"Duplicate upstream ID: {source_id!r}")
858
+ revision = _require_str(value, "revision", context)
859
+ _validate_revision(revision, f"{context}.revision")
860
+ path = _require_str(value, "path", context)
861
+ try:
862
+ normalized_path = _portable_relative_path(path, f"{context}.path")
863
+ except RegistryError as error:
864
+ raise RegistryError(
865
+ f"{context}.path must be a normalized directory directly under "
866
+ "'vendor/upstream/'."
867
+ ) from error
868
+ if (
869
+ normalized_path.parts[:2] != ("vendor", "upstream")
870
+ or len(normalized_path.parts) != 3
871
+ ):
872
+ raise RegistryError(
873
+ f"{context}.path must be a normalized directory directly under "
874
+ "'vendor/upstream/'."
875
+ )
876
+ if path in paths:
877
+ raise RegistryError(f"Duplicate upstream path: {path!r}")
878
+ paths.add(path)
879
+ url = _require_str(value, "url", context)
880
+ if not url.startswith("https://github.com/") or not url.endswith(".git"):
881
+ raise RegistryError(f"{context}.url must be an HTTPS GitHub clone URL.")
882
+ license_files = _require_str_list(value, "license_files", context)
883
+ license_digests = _require_digest_list(value, "license_digests", context)
884
+ if tuple(item.path for item in license_digests) != license_files:
885
+ raise RegistryError(
886
+ f"{context}.license_digests must cover license_files in the same order."
887
+ )
888
+ distribution_files = _require_digest_list(value, "distribution_files", context)
889
+ distribution_map = {item.path: item for item in distribution_files}
890
+ for canonical in license_digests:
891
+ distributed = distribution_map.get(canonical.path)
892
+ if distributed is None or distributed.encoded != canonical.encoded:
893
+ raise RegistryError(
894
+ f"{context}.distribution_files must include an exact copy of "
895
+ f"{canonical.path!r}."
896
+ )
897
+ if source_id == "e1":
898
+ required_e1 = {
899
+ "LICENSE",
900
+ "ATTRIBUTION",
901
+ "NOTICE",
902
+ "Apache-2.0.txt",
903
+ "BSD-3-Clause.txt",
904
+ "MODIFICATIONS.md",
905
+ }
906
+ missing_e1 = sorted(required_e1.difference(distribution_map))
907
+ if missing_e1:
908
+ raise RegistryError(f"{context} is missing E1 legal files: {missing_e1}")
909
+ result[source_id] = UpstreamSource(
910
+ id=source_id,
911
+ path=path,
912
+ url=url,
913
+ revision=revision,
914
+ license_expression=_require_str(value, "license", context),
915
+ license_files=license_files,
916
+ license_digests=license_digests,
917
+ distribution_files=distribution_files,
918
+ )
919
+ return result
920
+
921
+
922
+ def _parse_families(
923
+ raw: object,
924
+ upstreams: Mapping[str, UpstreamSource],
925
+ ) -> dict[str, ModelFamily]:
926
+ if not isinstance(raw, dict) or not raw:
927
+ raise RegistryError("The manifest must contain [families.<id>] tables.")
928
+ result: dict[str, ModelFamily] = {}
929
+ for family_id, value in raw.items():
930
+ context = f"families.{family_id}"
931
+ if _IDENTIFIER_RE.fullmatch(family_id) is None or not isinstance(value, dict):
932
+ raise RegistryError(f"Invalid family table: {family_id!r}")
933
+ checkpoint_license = _require_str(value, "checkpoint_license", context)
934
+ hub_license, hub_license_name, hub_license_link = _parse_hub_license(
935
+ value,
936
+ checkpoint_license=checkpoint_license,
937
+ context=context,
938
+ )
939
+ _reject_unknown_fields(value, _FAMILY_FIELDS, context)
940
+ source_ids = _require_str_list(value, "upstreams", context)
941
+ unknown_sources = sorted(set(source_ids).difference(upstreams))
942
+ if unknown_sources:
943
+ raise RegistryError(f"{context} references unknown upstreams: {unknown_sources}")
944
+ tokenizer_mode = _require_str(value, "tokenizer_mode", context)
945
+ if tokenizer_mode not in _ALLOWED_TOKENIZER_MODES:
946
+ raise RegistryError(f"Unsupported tokenizer mode in {context}: {tokenizer_mode!r}")
947
+ public_input = _require_str(value, "public_input", context)
948
+ attention = _require_str_list(value, "attention", context)
949
+ if not set(attention).issubset(_ALLOWED_ATTENTION):
950
+ raise RegistryError(f"Unsupported attention implementation in {context}.")
951
+ dtypes = _require_str_list(value, "dtypes", context)
952
+ if not set(dtypes).issubset(_ALLOWED_DTYPES):
953
+ raise RegistryError(f"Unsupported dtype in {context}.")
954
+ bf16_execution = cast(
955
+ Bf16Execution,
956
+ _require_enum(
957
+ value,
958
+ "bf16_execution",
959
+ context,
960
+ _ALLOWED_BF16_EXECUTIONS,
961
+ ),
962
+ )
963
+ precisions = _require_str_list(value, "precisions", context)
964
+ if not set(precisions).issubset(_ALLOWED_PRECISIONS):
965
+ raise RegistryError(f"Unsupported precision policy in {context}.")
966
+ experimental_precisions = _optional_str_list(
967
+ value,
968
+ "experimental_precisions",
969
+ context,
970
+ )
971
+ unknown_experimental_precisions = sorted(
972
+ set(experimental_precisions).difference(precisions)
973
+ )
974
+ if unknown_experimental_precisions:
975
+ raise RegistryError(
976
+ f"{context}.experimental_precisions must be a subset of precisions; "
977
+ f"unknown values: {unknown_experimental_precisions}."
978
+ )
979
+ extra = cast(RuntimeExtra, _require_enum(value, "extra", context, _ALLOWED_EXTRAS))
980
+ vram_tier = cast(
981
+ VramTier,
982
+ _require_enum(value, "vram_tier", context, _ALLOWED_VRAM_TIERS),
983
+ )
984
+ test_tiers_raw = _require_str_list(value, "test_tiers", context)
985
+ unknown_test_tiers = sorted(set(test_tiers_raw).difference(_ALLOWED_TEST_TIERS))
986
+ if unknown_test_tiers:
987
+ raise RegistryError(
988
+ f"{context}.test_tiers contains unsupported tiers: {unknown_test_tiers}."
989
+ )
990
+ test_tiers = cast(tuple[TestTier, ...], test_tiers_raw)
991
+ reference_container = _parse_reference_container(value, context)
992
+ reference_adapter = _parse_reference_adapter(value, context)
993
+ documentation = _parse_documentation_path(value, context)
994
+ runtime_paths = _require_str_list(value, "runtime_paths", context)
995
+ if len(runtime_paths) != len(set(runtime_paths)):
996
+ raise RegistryError(f"{context}.runtime_paths must not contain duplicates.")
997
+ for runtime_path in runtime_paths:
998
+ try:
999
+ _portable_relative_path(runtime_path, f"{context}.runtime_paths entry")
1000
+ except RegistryError as error:
1001
+ raise RegistryError(
1002
+ f"Unsafe runtime path in {context}: {runtime_path!r}"
1003
+ ) from error
1004
+ if runtime_path.startswith("vendor/"):
1005
+ raise RegistryError(f"Unsafe runtime path in {context}: {runtime_path!r}")
1006
+ requires_complete_weight_publication = value.get(
1007
+ "requires_complete_weight_publication",
1008
+ False,
1009
+ )
1010
+ if not isinstance(requires_complete_weight_publication, bool):
1011
+ raise RegistryError(
1012
+ f"{context}.requires_complete_weight_publication must be a boolean."
1013
+ )
1014
+ if "weights_publication_allowed" not in value:
1015
+ raise RegistryError(
1016
+ f"{context}.weights_publication_allowed must be declared explicitly."
1017
+ )
1018
+ weights_publication_allowed = value["weights_publication_allowed"]
1019
+ if not isinstance(weights_publication_allowed, bool):
1020
+ raise RegistryError(f"{context}.weights_publication_allowed must be a boolean.")
1021
+ raw_auto_map = value.get("auto_map")
1022
+ if not isinstance(raw_auto_map, dict) or not raw_auto_map:
1023
+ raise RegistryError(f"{context}.auto_map must be a non-empty table.")
1024
+ auto_map: list[tuple[str, str]] = []
1025
+ for auto_class, class_path in raw_auto_map.items():
1026
+ if auto_class not in _ALLOWED_AUTO_CLASSES or not isinstance(class_path, str):
1027
+ raise RegistryError(f"Invalid AutoClass mapping in {context}: {auto_class!r}")
1028
+ if not class_path.startswith("fastplms.") or class_path.count(".") < 2:
1029
+ raise RegistryError(f"Invalid Python class path in {context}: {class_path!r}")
1030
+ auto_map.append((auto_class, class_path))
1031
+ tokenizer_class = value.get("tokenizer_class")
1032
+ if tokenizer_class is not None:
1033
+ if tokenizer_mode != "tokenizer":
1034
+ raise RegistryError(
1035
+ f"{context}.tokenizer_class requires tokenizer_mode='tokenizer'."
1036
+ )
1037
+ if (
1038
+ not isinstance(tokenizer_class, str)
1039
+ or not tokenizer_class.startswith("fastplms.")
1040
+ or tokenizer_class.count(".") < 2
1041
+ ):
1042
+ raise RegistryError(
1043
+ f"Invalid tokenizer class path in {context}: {tokenizer_class!r}"
1044
+ )
1045
+ backbone_model = value.get("backbone_model")
1046
+ if backbone_model is not None and (
1047
+ not isinstance(backbone_model, str)
1048
+ or _IDENTIFIER_RE.fullmatch(backbone_model) is None
1049
+ ):
1050
+ raise RegistryError(
1051
+ f"{context}.backbone_model must be a valid manifest model ID."
1052
+ )
1053
+ state_transform = _require_str(value, "state_transform", context)
1054
+ conversion_provenance = _require_str(value, "conversion_provenance", context)
1055
+ required_sections = ("Input:", "Transformation:", "Output:", "Validation:", "Limitation:")
1056
+ missing_sections = [
1057
+ section for section in required_sections if section not in conversion_provenance
1058
+ ]
1059
+ if missing_sections or state_transform not in conversion_provenance:
1060
+ raise RegistryError(
1061
+ f"{context}.conversion_provenance must identify {state_transform!r} and "
1062
+ f"contain mechanism-first sections; missing {missing_sections}."
1063
+ )
1064
+ result[family_id] = ModelFamily(
1065
+ id=family_id,
1066
+ architecture=_require_str(value, "architecture", context),
1067
+ upstreams=source_ids,
1068
+ tokenizer_mode=tokenizer_mode,
1069
+ public_input=public_input,
1070
+ extra=extra,
1071
+ reference_container=reference_container,
1072
+ reference_adapter=reference_adapter,
1073
+ attention=attention,
1074
+ dtypes=cast(tuple[DtypeName, ...], dtypes),
1075
+ bf16_execution=bf16_execution,
1076
+ precisions=precisions,
1077
+ experimental_precisions=experimental_precisions,
1078
+ vram_tier=vram_tier,
1079
+ checkpoint_license=checkpoint_license,
1080
+ hub_license=hub_license,
1081
+ state_transform=state_transform,
1082
+ representative=_require_str(value, "representative", context),
1083
+ documentation=documentation,
1084
+ test_tiers=test_tiers,
1085
+ runtime_paths=runtime_paths,
1086
+ auto_map_items=tuple(auto_map),
1087
+ requires_complete_weight_publication=requires_complete_weight_publication,
1088
+ weights_publication_allowed=weights_publication_allowed,
1089
+ tokenizer_class=tokenizer_class,
1090
+ hub_license_name=hub_license_name,
1091
+ hub_license_link=hub_license_link,
1092
+ conversion_provenance=conversion_provenance,
1093
+ backbone_model=backbone_model,
1094
+ )
1095
+ return result
1096
+
1097
+
1098
+ def _parse_runtime_assets(
1099
+ raw: object,
1100
+ families: Mapping[str, ModelFamily],
1101
+ ) -> dict[str, RuntimeAsset]:
1102
+ if not isinstance(raw, list) or not raw:
1103
+ raise RegistryError("The manifest must contain at least one [[runtime_assets]] entry.")
1104
+ result: dict[str, RuntimeAsset] = {}
1105
+ identities: set[tuple[str, str, str]] = set()
1106
+ for index, value in enumerate(raw):
1107
+ context = f"runtime_assets[{index}]"
1108
+ if not isinstance(value, dict):
1109
+ raise RegistryError(f"{context} must be a table.")
1110
+ _reject_unknown_fields(value, _RUNTIME_ASSET_FIELDS, context)
1111
+ asset_id = _require_str(value, "id", context)
1112
+ if _IDENTIFIER_RE.fullmatch(asset_id) is None:
1113
+ raise RegistryError(f"Invalid runtime asset ID: {asset_id!r}")
1114
+ if asset_id in result:
1115
+ raise RegistryError(f"Duplicate runtime asset ID: {asset_id!r}")
1116
+ repository = _require_str(value, "repository", context)
1117
+ if _REPOSITORY_ID_RE.fullmatch(repository) is None:
1118
+ raise RegistryError(f"{context}.repository must be a Hugging Face repository ID.")
1119
+ revision = _require_str(value, "revision", context)
1120
+ _validate_revision(revision, f"{context}.revision")
1121
+ path = _require_str(value, "path", context)
1122
+ try:
1123
+ normalized_path = _portable_relative_path(path, "Runtime asset path")
1124
+ except RegistryError as error:
1125
+ raise RegistryError(f"Runtime asset path is not portable: {path!r}") from error
1126
+ sha256 = _require_str(value, "sha256", context)
1127
+ if len(sha256) != 64 or _HEX_RE.fullmatch(sha256) is None:
1128
+ raise RegistryError(f"Invalid runtime asset SHA-256 for {path!r}.")
1129
+ size = value.get("size")
1130
+ if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
1131
+ raise RegistryError(f"{context}.size must be a positive byte count.")
1132
+ consumer_family = _require_str(value, "consumer_family", context)
1133
+ if consumer_family not in families:
1134
+ raise RegistryError(
1135
+ f"{context}.consumer_family references unknown family {consumer_family!r}."
1136
+ )
1137
+ trust_kind = cast(
1138
+ RuntimeAssetTrustKind,
1139
+ _require_enum(
1140
+ value,
1141
+ "trust_kind",
1142
+ context,
1143
+ _ALLOWED_RUNTIME_ASSET_TRUST_KINDS,
1144
+ ),
1145
+ )
1146
+ license_expression = _require_str(value, "license", context)
1147
+ offline_behavior = _require_str(value, "offline_behavior", context)
1148
+ if offline_behavior not in _ALLOWED_RUNTIME_ASSET_OFFLINE_BEHAVIORS:
1149
+ raise RegistryError(
1150
+ f"{context}.offline_behavior is unsupported: {offline_behavior!r}."
1151
+ )
1152
+ if trust_kind == "hash_pinned_pickle" and normalized_path.suffix != ".pkl":
1153
+ raise RegistryError(
1154
+ f"{context}.path must end in '.pkl' for trust_kind='hash_pinned_pickle'."
1155
+ )
1156
+ identity = (repository, revision, path)
1157
+ if identity in identities:
1158
+ raise RegistryError(f"Duplicate runtime asset identity: {identity!r}")
1159
+ identities.add(identity)
1160
+ result[asset_id] = RuntimeAsset(
1161
+ id=asset_id,
1162
+ repository=repository,
1163
+ revision=revision,
1164
+ path=path,
1165
+ sha256=sha256,
1166
+ size=size,
1167
+ consumer_family=consumer_family,
1168
+ trust_kind=trust_kind,
1169
+ license_expression=license_expression,
1170
+ offline_behavior=offline_behavior,
1171
+ )
1172
+ return result
1173
+
1174
+
1175
+ def _parse_models(
1176
+ raw: object,
1177
+ families: Mapping[str, ModelFamily],
1178
+ ) -> dict[str, ModelSpec]:
1179
+ if not isinstance(raw, list) or not raw:
1180
+ raise RegistryError("The manifest must contain at least one [[models]] entry.")
1181
+ result: dict[str, ModelSpec] = {}
1182
+ fast_repositories: set[str] = set()
1183
+ for index, value in enumerate(raw):
1184
+ context = f"models[{index}]"
1185
+ if not isinstance(value, dict):
1186
+ raise RegistryError(f"{context} must be a table.")
1187
+ _reject_unknown_fields(value, _MODEL_FIELDS, context)
1188
+ model_id = _require_str(value, "id", context)
1189
+ if _IDENTIFIER_RE.fullmatch(model_id) is None:
1190
+ raise RegistryError(f"Invalid model ID: {model_id!r}")
1191
+ if model_id in result:
1192
+ raise RegistryError(f"Duplicate model ID: {model_id!r}")
1193
+ family_id = _require_str(value, "family", context)
1194
+ if family_id not in families:
1195
+ raise RegistryError(f"{context} references unknown family {family_id!r}.")
1196
+ fast = _parse_checkpoint(value, "fast", context)
1197
+ official = _parse_checkpoint(value, "official", context)
1198
+ if fast.repo_id in fast_repositories:
1199
+ raise RegistryError(f"Duplicate FastPLMs repository ID: {fast.repo_id!r}")
1200
+ fast_repositories.add(fast.repo_id)
1201
+ family = families[family_id]
1202
+ oracle_assets = _parse_oracle_assets(value, context)
1203
+ official_golden = _parse_official_golden(value, model_id, context)
1204
+ size_category = _require_str(value, "size_category", context)
1205
+ if size_category not in _ALLOWED_SIZE_CATEGORIES:
1206
+ raise RegistryError(f"Unsupported size category in {context}: {size_category!r}")
1207
+ generation_contract = cast(
1208
+ GenerationContract,
1209
+ _require_enum(
1210
+ value,
1211
+ "generation_contract",
1212
+ context,
1213
+ _ALLOWED_GENERATION_CONTRACTS,
1214
+ ),
1215
+ )
1216
+ if family.tokenizer_mode == "structure" and size_category != "structure":
1217
+ raise RegistryError(
1218
+ f"Structure checkpoint {model_id!r} must use size_category='structure'."
1219
+ )
1220
+ artifact_source = value.get("artifact_source", "fast")
1221
+ if artifact_source not in {"fast", "official"}:
1222
+ raise RegistryError(f"{context}.artifact_source must be 'fast' or 'official'.")
1223
+ canonical_state_sha256 = value.get("canonical_state_sha256")
1224
+ if artifact_source == "official":
1225
+ if (
1226
+ not isinstance(canonical_state_sha256, str)
1227
+ or len(canonical_state_sha256) != 64
1228
+ or _HEX_RE.fullmatch(canonical_state_sha256) is None
1229
+ ):
1230
+ raise RegistryError(
1231
+ f"{context}.canonical_state_sha256 must be a SHA-256 commitment "
1232
+ "for an official-source artifact."
1233
+ )
1234
+ elif canonical_state_sha256 is not None:
1235
+ raise RegistryError(
1236
+ f"{context}.canonical_state_sha256 is restricted to official-source artifacts."
1237
+ )
1238
+ if family.tokenizer_mode == "tokenizer" and not any(
1239
+ "tokenizer" in item.path or "vocab" in item.path for item in fast.files
1240
+ ):
1241
+ raise RegistryError(f"{context} does not pin a tokenizer asset.")
1242
+ tokenizer_source_id = value.get("tokenizer_source")
1243
+ if tokenizer_source_id is not None and (
1244
+ family.tokenizer_mode != "tokenizer"
1245
+ or not isinstance(tokenizer_source_id, str)
1246
+ or _IDENTIFIER_RE.fullmatch(tokenizer_source_id) is None
1247
+ ):
1248
+ raise RegistryError(f"{context}.tokenizer_source is invalid.")
1249
+ notes = value.get("notes", "")
1250
+ if not isinstance(notes, str):
1251
+ raise RegistryError(f"{context}.notes must be a string.")
1252
+ msa_conditioning = value.get("msa_conditioning")
1253
+ if family_id == "esmfold2":
1254
+ if not isinstance(msa_conditioning, bool):
1255
+ raise RegistryError(
1256
+ f"{context}.msa_conditioning must be an explicit boolean for "
1257
+ "ESMFold2 checkpoints."
1258
+ )
1259
+ elif "msa_conditioning" in value:
1260
+ raise RegistryError(
1261
+ f"{context}.msa_conditioning is only valid for ESMFold2 checkpoints."
1262
+ )
1263
+ raw_auto_map = value.get("auto_map")
1264
+ auto_map: list[tuple[str, str]] = []
1265
+ if raw_auto_map is not None:
1266
+ if not isinstance(raw_auto_map, dict) or not raw_auto_map:
1267
+ raise RegistryError(f"{context}.auto_map must be a non-empty table.")
1268
+ for auto_class, class_path in raw_auto_map.items():
1269
+ if auto_class not in _ALLOWED_AUTO_CLASSES or not isinstance(class_path, str):
1270
+ raise RegistryError(f"Invalid AutoClass mapping in {context}: {auto_class!r}")
1271
+ if not class_path.startswith("fastplms.") or class_path.count(".") < 2:
1272
+ raise RegistryError(f"Invalid Python class path in {context}: {class_path!r}")
1273
+ auto_map.append((auto_class, class_path))
1274
+ result[model_id] = ModelSpec(
1275
+ id=model_id,
1276
+ family=family,
1277
+ fast=fast,
1278
+ official=official,
1279
+ size_category=size_category,
1280
+ generation_contract=generation_contract,
1281
+ oracle_assets=oracle_assets,
1282
+ official_golden=official_golden,
1283
+ artifact_source=artifact_source,
1284
+ canonical_state_sha256=canonical_state_sha256,
1285
+ tokenizer_source_id=tokenizer_source_id,
1286
+ auto_map_items=tuple(auto_map),
1287
+ notes=notes,
1288
+ msa_conditioning=msa_conditioning,
1289
+ )
1290
+ return result
1291
+
1292
+
1293
+ def _validate_registry(
1294
+ upstreams: Mapping[str, UpstreamSource],
1295
+ attention_kernels: Mapping[str, AttentionKernelSpec],
1296
+ families: Mapping[str, ModelFamily],
1297
+ models: Mapping[str, ModelSpec],
1298
+ ) -> None:
1299
+ for spec in models.values():
1300
+ if spec.tokenizer_source_id is None:
1301
+ continue
1302
+ source = models.get(spec.tokenizer_source_id)
1303
+ if source is None:
1304
+ raise RegistryError(
1305
+ f"Model {spec.id!r} references unknown tokenizer source "
1306
+ f"{spec.tokenizer_source_id!r}."
1307
+ )
1308
+ if not any(
1309
+ PurePosixPath(item.path).name
1310
+ in {
1311
+ "added_tokens.json",
1312
+ "merges.txt",
1313
+ "sentencepiece.bpe.model",
1314
+ "special_tokens_map.json",
1315
+ "spiece.model",
1316
+ "tokenizer.json",
1317
+ "tokenizer_config.json",
1318
+ "vocab.json",
1319
+ "vocab.txt",
1320
+ }
1321
+ for item in source.official.files
1322
+ ):
1323
+ raise RegistryError(
1324
+ f"Tokenizer source {source.id!r} has no official tokenizer assets."
1325
+ )
1326
+ expected_esmfold2 = {
1327
+ "esmfold2": ("Synthyra/ESMFold2", "biohub/ESMFold2"),
1328
+ "esmfold2_fast": ("Synthyra/ESMFold2-Fast", "biohub/ESMFold2-Fast"),
1329
+ "esmfold2_experimental_cutoff2025": (
1330
+ "Synthyra/ESMFold2-Experimental-Cutoff2025",
1331
+ "biohub/ESMFold2-Experimental-Cutoff2025",
1332
+ ),
1333
+ "esmfold2_experimental_fast_cutoff2025": (
1334
+ "Synthyra/ESMFold2-Experimental-Fast-Cutoff2025",
1335
+ "biohub/ESMFold2-Experimental-Fast-Cutoff2025",
1336
+ ),
1337
+ }
1338
+ actual_esmfold2 = {
1339
+ model.id: (model.fast.repo_id, model.official.repo_id)
1340
+ for model in models.values()
1341
+ if model.family.id == "esmfold2"
1342
+ }
1343
+ if actual_esmfold2 != expected_esmfold2:
1344
+ raise RegistryError(
1345
+ "ESMFold2 support must contain exactly the four approved model IDs and "
1346
+ "official/Synthyra repositories."
1347
+ )
1348
+
1349
+ golden_paths: list[str] = []
1350
+ for model in models.values():
1351
+ if model.official_golden is not None:
1352
+ golden_paths.extend(
1353
+ (
1354
+ model.official_golden.metadata.path,
1355
+ model.official_golden.tensors.path,
1356
+ )
1357
+ )
1358
+ if len(golden_paths) != len(set(golden_paths)):
1359
+ raise RegistryError("Official golden paths must be unique across model declarations.")
1360
+ unused_upstreams = sorted(
1361
+ set(upstreams).difference(
1362
+ source for family in families.values() for source in family.upstreams
1363
+ )
1364
+ )
1365
+ if unused_upstreams:
1366
+ raise RegistryError(
1367
+ f"Upstream sources are not connected to a model family: {unused_upstreams}"
1368
+ )
1369
+ advertised_flash = {
1370
+ implementation
1371
+ for family in families.values()
1372
+ for implementation in family.attention
1373
+ if implementation.startswith("flash_attention_")
1374
+ }
1375
+ missing_kernels = sorted(advertised_flash.difference(attention_kernels))
1376
+ if missing_kernels:
1377
+ raise RegistryError(
1378
+ f"Advertised FlashAttention backends lack kernel specs: {missing_kernels}."
1379
+ )
1380
+ for family in families.values():
1381
+ for implementation in family.attention:
1382
+ kernel = attention_kernels.get(implementation)
1383
+ if kernel is not None and not set(family.dtypes).intersection(kernel.dtypes):
1384
+ raise RegistryError(
1385
+ f"Family {family.id!r} and attention kernel {implementation!r} "
1386
+ "have no supported dtype in common."
1387
+ )
1388
+ family_models = [model for model in models.values() if model.family.id == family.id]
1389
+ if not family_models:
1390
+ raise RegistryError(f"Family {family.id!r} has no checkpoints.")
1391
+ representative = models.get(family.representative)
1392
+ if representative is None or representative.family.id != family.id:
1393
+ raise RegistryError(
1394
+ f"Family {family.id!r} has invalid representative {family.representative!r}."
1395
+ )
1396
+ if family.backbone_model is not None and family.backbone_model not in models:
1397
+ raise RegistryError(
1398
+ f"Family {family.id!r} references unknown backbone model "
1399
+ f"{family.backbone_model!r}."
1400
+ )
1401
+
1402
+
1403
+ def _load_manifest_bytes(raw_bytes: bytes) -> ModelRegistry:
1404
+ try:
1405
+ data = tomllib.loads(raw_bytes.decode("utf-8"))
1406
+ except (UnicodeDecodeError, tomllib.TOMLDecodeError) as error:
1407
+ raise RegistryError(f"Unable to parse model manifest: {error}") from error
1408
+ _reject_unknown_fields(data, _ROOT_FIELDS, "manifest")
1409
+ if data.get("schema_version") != 1:
1410
+ raise RegistryError("Unsupported model manifest schema_version; expected 1.")
1411
+ legal_files = _require_digest_list(data, "legal_files", "manifest")
1412
+ required_legal_paths = {"LICENSE", "THIRD_PARTY_NOTICES.md"}
1413
+ if {item.path for item in legal_files} != required_legal_paths:
1414
+ raise RegistryError("manifest.legal_files must contain LICENSE and THIRD_PARTY_NOTICES.md.")
1415
+ attention_kernels = _parse_attention_kernels(data.get("attention_kernels"))
1416
+ upstreams = _parse_upstreams(data.get("upstreams"))
1417
+ families = _parse_families(data.get("families"), upstreams)
1418
+ runtime_assets = _parse_runtime_assets(data.get("runtime_assets"), families)
1419
+ models = _parse_models(data.get("models"), families)
1420
+ _validate_registry(upstreams, attention_kernels, families, models)
1421
+ return ModelRegistry(
1422
+ schema_version=1,
1423
+ upstreams=upstreams,
1424
+ attention_kernels=attention_kernels,
1425
+ families=families,
1426
+ models=models,
1427
+ runtime_assets=runtime_assets,
1428
+ legal_files=legal_files,
1429
+ )
1430
+
1431
+
1432
+ def load_model_registry(path: str | Path | None = None) -> ModelRegistry:
1433
+ """Load and validate a model manifest without importing model code."""
1434
+
1435
+ if path is None:
1436
+ manifest = resources.files("fastplms").joinpath("models.toml")
1437
+ return _load_manifest_bytes(manifest.read_bytes())
1438
+ return _load_manifest_bytes(Path(path).read_bytes())
1439
+
1440
+
1441
+ @lru_cache(maxsize=1)
1442
+ def get_model_registry() -> ModelRegistry:
1443
+ """Return the validated package registry, cached after its first read."""
1444
+
1445
+ return load_model_registry()
1446
+
1447
+
1448
+ def get_model_spec(model_id: str) -> ModelSpec:
1449
+ """Return one model specification by its stable manifest ID."""
1450
+
1451
+ try:
1452
+ return get_model_registry()[model_id]
1453
+ except KeyError as error:
1454
+ supported = ", ".join(get_model_registry())
1455
+ raise KeyError(
1456
+ f"Unknown FastPLMs model ID {model_id!r}. Supported IDs: {supported}"
1457
+ ) from error
1458
+
1459
+
1460
+ __all__ = [
1461
+ "HUB_LICENSE_IDENTIFIERS",
1462
+ "CheckpointSource",
1463
+ "FileDigest",
1464
+ "GenerationContract",
1465
+ "ModelFamily",
1466
+ "ModelRegistry",
1467
+ "ModelSpec",
1468
+ "OracleAsset",
1469
+ "RegistryError",
1470
+ "RuntimeAsset",
1471
+ "RuntimeAssetTrustKind",
1472
+ "RuntimeExtra",
1473
+ "TestTier",
1474
+ "UpstreamSource",
1475
+ "VramTier",
1476
+ "get_model_registry",
1477
+ "get_model_spec",
1478
+ "load_model_registry",
1479
+ ]
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,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 != "b0a12aa8be05f693e35ffc16025c2a546bd37682744b9686ece6e6bef9c9be92":
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.esm3.modeling_esm3")
227
+ FastESM3Config = _module_225.FastESM3Config
228
+ FastESM3Config.__module__ = __name__
229
+ FastESM3Model = _module_225.FastESM3Model
230
+ FastESM3Model.__module__ = __name__
runtime-attestation.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": {
3
+ "LICENSES/FastPLMs-Apache-2.0.txt": "sha256:2d2b50c7b1414bff1189a1db1f0cfb92e3e064b50f4c2b1019827b683e1b629a",
4
+ "LICENSES/biohub-esm/LICENSE.md": "sha256:b63df9ca1dd96b3b21eec226b51b236d0bd152ac20eafc43aad46bf832b48d8a",
5
+ "LICENSES/biohub-esm/THIRD_PARTY_NOTICE.md": "sha256:5bff8515ba4e0f53abdc43714c180b79c5b606160497d98de741a369cb9b6a23",
6
+ "LICENSES/biohub-transformers/LICENSE": "sha256:77fd4710def9ec3c0f6225800e0235f15a425abd4a8b03559127fcd782612049",
7
+ "README.md": "sha256:ef23b8485cab4f082d2820a3b253b42774d7b1bf604a34787353ce73b0dd30cc",
8
+ "THIRD_PARTY_NOTICES.md": "sha256:25704b3c76404696cae52e7fca13088d329f70f412687340351259e86cd62baa",
9
+ "config.json": "sha256:ac8744b969625da611337a17d63fdd100c51434781d958715731dd43cb5c2136",
10
+ "fastplms/__init__.py": "sha256:4fb3196022ca8ec699d59d09bdbc5f0184195552b773698ab9b061fe3cd7df12",
11
+ "fastplms/attention/__init__.py": "sha256:f60b9fecfb4bcb37a4e7c26dc2f752b9035f9cbad627b4a84213f3a92ec88f7d",
12
+ "fastplms/attention/_core.py": "sha256:8f7ec5b65bd8b6c6fa4951d50d1c0e499abf03ae00914794b51fc410201e3e33",
13
+ "fastplms/attention/_kernel_lock.py": "sha256:85d8521a2af5f94fad3948af3814db0c866c414ee4d43df797b9bd6f980e947b",
14
+ "fastplms/attention/interfaces.py": "sha256:1c6f06a8e411e0f9bf6d230522205c93ae46ea58864006fbb892aa05e5ca5749",
15
+ "fastplms/embeddings/__init__.py": "sha256:47ff8cdf682d44037dd9edab133e2e60675d60786bd5cf0bffd1998f31985555",
16
+ "fastplms/embeddings/pooling.py": "sha256:a140266ed6b1cc344c8507edc5c6c4f2dce464c3db70ba4b16c7ac2ba2fad96e",
17
+ "fastplms/embeddings/runner.py": "sha256:23ee4727a918d6d331f7a0f89b823d149f1a791f0c5586e3496d7b6eb2ce97e0",
18
+ "fastplms/embeddings/storage.py": "sha256:3fbe2bab75092e5a4cadf4d27e4752181d597469a65a55db085ceef808ed418e",
19
+ "fastplms/embeddings/types.py": "sha256:119718a20989d1ae5a60fabc0f5e98bdc172c5163b04db3d4554ac3956b30e52",
20
+ "fastplms/models.toml": "sha256:05a8399f084a5babb5f0916cee7e564c4030767f3ff0230c4f46e539209847d1",
21
+ "fastplms/models/__init__.py": "sha256:5e48c2cb3877aa6f42f3b5411d53b16bba2e32827bbde634f47f174c5cb36f86",
22
+ "fastplms/models/esm3/__init__.py": "sha256:2da984da22b0b6067e2ea7c3943b62b0b31f39c6bc3f33677b435adf853a422c",
23
+ "fastplms/models/esm3/modeling_esm3.py": "sha256:f5fcba4f97895d6232cb38fdd503374ee976bc89769e93f4a308295d9b9f986c",
24
+ "fastplms/models/ttt.py": "sha256:a0df4e98b02120d423e3c7ca9b866a8d0e3748b9076042a0102a838a11aed046",
25
+ "fastplms/registry.py": "sha256:d2c3c36434bdddc58539861413273207916085a19fa26c4cda79190adcf8f2cc",
26
+ "fastplms/runtime.py": "sha256:110018646d6f248cedab140a030c3065e1b062b61f6aff659c231e538614bc01",
27
+ "fastplms_bundle.py": "sha256:bdbebfc7a6f7132d4f144a74ad1d73d4d0ac2b60c514e52e365f487c493581cb",
28
+ "modeling_fastplms.py": "sha256:0e32e778d494515fbc1d77e76a5d55798854b6f0ae459d2c79b4e5908246c015",
29
+ "special_tokens_map.json": "sha256:0b7245ec86c8c3aeaf61523ba70dfa79be137e6283f127bd651adc30b4f15c74",
30
+ "tokenizer.json": "sha256:8d3447b278176e65fb3ef0224472927bf5fee3be46ea2bd77fad0111423cee1f",
31
+ "tokenizer_config.json": "sha256:e8d8e40c9f92b334f0272e80bb65ed4043cb9836523cbae899e9859e8cbb8833"
32
+ },
33
+ "model_id": "esm3_small",
34
+ "redistributable": true,
35
+ "release_tool_revision": "73e4525b91fde6ca88e33f9b292fe4ba2400c472",
36
+ "release_tool_sha256": "baaa0230677d8e61331e2926cf2cad9d5f67f2ad9191e8c88658a70b1e69a944",
37
+ "runtime_bundle_sha256": "b0a12aa8be05f693e35ffc16025c2a546bd37682744b9686ece6e6bef9c9be92",
38
+ "runtime_revision": "73e4525b91fde6ca88e33f9b292fe4ba2400c472",
39
+ "schema_version": 2,
40
+ "scope": "runtime-only",
41
+ "source_tree_sha256": "4b83474b9a9381edcfa2b308d9d23af91de3ca8e22d8b5b5f56bab317f152630",
42
+ "weights": {
43
+ "repo_id": "Synthyra/ESM3_small",
44
+ "revision": "7ddb5a740f9e5f93933eb6410c0ee8684bc63ec1"
45
+ },
46
+ "weights_license_status": "resolved"
47
+ }
tokenizer.json CHANGED
@@ -83,35 +83,17 @@
83
  }
84
  ],
85
  "pair": [
86
- {
87
- "SpecialToken": {
88
- "id": "<cls>",
89
- "type_id": 0
90
- }
91
- },
92
  {
93
  "Sequence": {
94
  "id": "A",
95
  "type_id": 0
96
  }
97
  },
98
- {
99
- "SpecialToken": {
100
- "id": "<eos>",
101
- "type_id": 0
102
- }
103
- },
104
  {
105
  "Sequence": {
106
  "id": "B",
107
  "type_id": 1
108
  }
109
- },
110
- {
111
- "SpecialToken": {
112
- "id": "<eos>",
113
- "type_id": 1
114
- }
115
  }
116
  ],
117
  "special_tokens": {
 
83
  }
84
  ],
85
  "pair": [
 
 
 
 
 
 
86
  {
87
  "Sequence": {
88
  "id": "A",
89
  "type_id": 0
90
  }
91
  },
 
 
 
 
 
 
92
  {
93
  "Sequence": {
94
  "id": "B",
95
  "type_id": 1
96
  }
 
 
 
 
 
 
97
  }
98
  ],
99
  "special_tokens": {
tokenizer_config.json CHANGED
@@ -60,6 +60,6 @@
60
  "mask_token": "<mask>",
61
  "model_max_length": 1000000000000000019884624838656,
62
  "pad_token": "<pad>",
63
- "tokenizer_class": "EsmSequenceTokenizer",
64
  "unk_token": "<unk>"
65
  }
 
60
  "mask_token": "<mask>",
61
  "model_max_length": 1000000000000000019884624838656,
62
  "pad_token": "<pad>",
63
+ "tokenizer_class": "ESMCTokenizer",
64
  "unk_token": "<unk>"
65
  }