Aleksei Ustimenko commited on
Commit ·
5ccb4fd
0
Parent(s):
Initial HamiltonZero release
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +38 -0
- .gitignore +13 -0
- LICENSE +202 -0
- NOTICE +5 -0
- README.md +304 -0
- THIRD_PARTY_NOTICES.md +13 -0
- datasets/README.md +38 -0
- datasets/eval/j1j2_45x45_obc.json +3 -0
- datasets/eval/j1j2_64x64_obc.json +3 -0
- datasets/eval/j1j2_90x90_obc.json +3 -0
- datasets/eval/ppp_ohno_n1024.json +3 -0
- datasets/eval/ppp_ohno_n256.json +3 -0
- datasets/eval/ppp_ohno_n512.json +3 -0
- datasets/eval/rudy_n1024.json +3 -0
- datasets/eval/rudy_n256.json +3 -0
- datasets/eval/rudy_n512.json +3 -0
- datasets/eval/triangular_42x42_pbc.json +3 -0
- datasets/train/foundation_5000.jsonl +3 -0
- examples/compiled_inference.py +71 -0
- examples/eval.json +5 -0
- examples/eval_large_n.json +5 -0
- examples/finetune.json +6 -0
- examples/j1j2_4x4_route.ipynb +205 -0
- examples/networkx_system.py +16 -0
- examples/train.json +6 -0
- pyproject.toml +47 -0
- src/hamiltonzero/__init__.py +40 -0
- src/hamiltonzero/checkpoint.py +65 -0
- src/hamiltonzero/cli.py +69 -0
- src/hamiltonzero/compiled/__init__.py +2 -0
- src/hamiltonzero/compiled/api.py +34 -0
- src/hamiltonzero/compiled/execute.py +118 -0
- src/hamiltonzero/compiled/model.py +456 -0
- src/hamiltonzero/compiled/tree.py +582 -0
- src/hamiltonzero/compiled/trunk.py +129 -0
- src/hamiltonzero/compiled/types.py +169 -0
- src/hamiltonzero/config.py +301 -0
- src/hamiltonzero/data/__init__.py +22 -0
- src/hamiltonzero/data/systems.py +264 -0
- src/hamiltonzero/energy/__init__.py +22 -0
- src/hamiltonzero/energy/compiled.py +57 -0
- src/hamiltonzero/energy/custom_lap.py +1491 -0
- src/hamiltonzero/energy/frame.py +152 -0
- src/hamiltonzero/energy/kernel.py +196 -0
- src/hamiltonzero/evaluation/__init__.py +42 -0
- src/hamiltonzero/evaluation/backend.py +148 -0
- src/hamiltonzero/evaluation/greedy_router.py +125 -0
- src/hamiltonzero/evaluation/large_n.py +371 -0
- src/hamiltonzero/evaluation/pallas_mha.py +164 -0
- src/hamiltonzero/evaluation/runner.py +509 -0
.gitattributes
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
datasets/train/*.jsonl filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
datasets/eval/*.json filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
weights/*.eqx filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*.egg-info/
|
| 4 |
+
.venv/
|
| 5 |
+
build/
|
| 6 |
+
dist/
|
| 7 |
+
outputs/
|
| 8 |
+
weights/
|
| 9 |
+
*.eqx
|
| 10 |
+
*.hzc
|
| 11 |
+
*.zst
|
| 12 |
+
!weights/
|
| 13 |
+
!weights/hamiltonzero_v1.eqx
|
LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|
NOTICE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
HamiltonZero
|
| 2 |
+
Copyright 2026 Simulacra Research Inc.
|
| 3 |
+
|
| 4 |
+
This product includes third-party software. See THIRD_PARTY_NOTICES.md for
|
| 5 |
+
attribution and license information.
|
README.md
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
# HamiltonZero
|
| 6 |
+
|
| 7 |
+
HamiltonZero is Simulacra Research's research release for compiled neural
|
| 8 |
+
wavefunctions of quantum spin Hamiltonians. It exposes three workflows:
|
| 9 |
+
|
| 10 |
+
- learned-router multisystem training;
|
| 11 |
+
- compiled single-system fine-tuning;
|
| 12 |
+
- compiled single-system evaluation, with optional router contest or large-N
|
| 13 |
+
execution.
|
| 14 |
+
|
| 15 |
+
## Installation
|
| 16 |
+
|
| 17 |
+
HamiltonZero requires Python 3.12 and JAX-compatible accelerator drivers.
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
python -m pip install .
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
The package pins the Python `jax` package to a
|
| 24 |
+
[`TakeOver/jax` commit](https://github.com/TakeOver/jax/commit/79f82535b15a444516d4a5e2beb71d283665b2ff),
|
| 25 |
+
also published as `hamiltonzero-jax-v0.11.0-spin.1`, and pins
|
| 26 |
+
`jaxlib==0.11.0`. The fork contains the symbolic-zero JVP support used by the
|
| 27 |
+
tuned Pallas attention kernel; stock Python JAX 0.11.0 is not sufficient for
|
| 28 |
+
that pathway. Install the accelerator plugin appropriate for the host using
|
| 29 |
+
the standard JAX instructions.
|
| 30 |
+
|
| 31 |
+
Learned-router training uses eight visible accelerators and requires an MCMC
|
| 32 |
+
batch size divisible by eight. Fine-tuning uses all visible accelerators and
|
| 33 |
+
requires its MCMC batch size to be divisible by their count. Evaluation chooses
|
| 34 |
+
a visible-device subset compatible with its walker batch.
|
| 35 |
+
|
| 36 |
+
## Foundation checkpoint
|
| 37 |
+
|
| 38 |
+
This Hugging Face repository stores the directly loadable HamiltonZero v1
|
| 39 |
+
foundation checkpoint at `weights/hamiltonzero_v1.eqx`. To download the
|
| 40 |
+
checkpoint without cloning the repository:
|
| 41 |
+
|
| 42 |
+
```bash
|
| 43 |
+
hf download simulacra-research/HamiltonZero \
|
| 44 |
+
weights/hamiltonzero_v1.eqx \
|
| 45 |
+
--local-dir .
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
The checkpoint contains the complete foundation wavefunction and its learned
|
| 49 |
+
router. `router` is the checkpoint kind, not a router-only parameter file.
|
| 50 |
+
|
| 51 |
+
To load the model directly, construct an architecture template and deserialize
|
| 52 |
+
its array leaves:
|
| 53 |
+
|
| 54 |
+
```python
|
| 55 |
+
import jax
|
| 56 |
+
|
| 57 |
+
from hamiltonzero.checkpoint import load_model
|
| 58 |
+
from hamiltonzero.config import ModelConfig
|
| 59 |
+
from hamiltonzero.model import build_model
|
| 60 |
+
|
| 61 |
+
template = build_model(
|
| 62 |
+
ModelConfig(),
|
| 63 |
+
jax.random.PRNGKey(0),
|
| 64 |
+
n_max=64,
|
| 65 |
+
)
|
| 66 |
+
model = load_model("weights/hamiltonzero_v1.eqx", template)
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
The template key initializes placeholder values only; deserialization replaces
|
| 70 |
+
all serialized array leaves. Set `n_max` to the padded width of the system when
|
| 71 |
+
constructing a template for direct model use. The command-line evaluation path
|
| 72 |
+
does this from the input system automatically.
|
| 73 |
+
|
| 74 |
+
## Hamiltonians and NetworkX
|
| 75 |
+
|
| 76 |
+
The public API follows the textbook convention
|
| 77 |
+
|
| 78 |
+
\[
|
| 79 |
+
H = \sum_{i<j} S_i^T J_{ij} S_j + \sum_i h_i^T S_i,
|
| 80 |
+
\qquad S=\sigma/2.
|
| 81 |
+
\]
|
| 82 |
+
|
| 83 |
+
Construct and save a system from a simple undirected NetworkX graph:
|
| 84 |
+
|
| 85 |
+
```python
|
| 86 |
+
from pathlib import Path
|
| 87 |
+
|
| 88 |
+
import networkx as nx
|
| 89 |
+
|
| 90 |
+
from hamiltonzero import SpinHamiltonian
|
| 91 |
+
from hamiltonzero.data import save_system
|
| 92 |
+
|
| 93 |
+
graph = nx.path_graph(8)
|
| 94 |
+
nx.set_edge_attributes(graph, 1.0, "J")
|
| 95 |
+
nx.set_node_attributes(graph, 0.0, "h")
|
| 96 |
+
|
| 97 |
+
system = SpinHamiltonian.from_networkx(graph)
|
| 98 |
+
save_system(Path("outputs/systems/chain_8.json"), system)
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
The same example is runnable as `python examples/networkx_system.py`. An edge
|
| 102 |
+
`J` may be an isotropic scalar, a length-three diagonal, or a 3-by-3 exchange
|
| 103 |
+
matrix. A node `h` may be a scalar z-field or a length-three field vector.
|
| 104 |
+
`SpinHamiltonian.from_arrays` accepts dense arrays instead.
|
| 105 |
+
|
| 106 |
+
HamiltonZero converts public inputs to the model's internal `-J/2` and `-h`
|
| 107 |
+
representation. The `SpinHamiltonian.J` and `SpinHamiltonian.h` properties
|
| 108 |
+
return the public textbook values. If `mu` is omitted, a conservative value is
|
| 109 |
+
computed from the Hamiltonian.
|
| 110 |
+
|
| 111 |
+
## Standalone compiled inference
|
| 112 |
+
|
| 113 |
+
The compact inference API loads the foundation checkpoint, runs the
|
| 114 |
+
beam-8 router, permutes the Hamiltonian, and compiles the selected physical
|
| 115 |
+
wavefunction in one call:
|
| 116 |
+
|
| 117 |
+
```python
|
| 118 |
+
import jax
|
| 119 |
+
import networkx as nx
|
| 120 |
+
|
| 121 |
+
from hamiltonzero import SpinHamiltonian, burn_in, energy, prepare, spin, step
|
| 122 |
+
|
| 123 |
+
graph = nx.path_graph(8)
|
| 124 |
+
nx.set_edge_attributes(graph, 1.0, "J")
|
| 125 |
+
system = SpinHamiltonian.from_networkx(graph)
|
| 126 |
+
|
| 127 |
+
route_key, mcmc_key = jax.random.split(jax.random.PRNGKey(0))
|
| 128 |
+
compiled, order = prepare(
|
| 129 |
+
system,
|
| 130 |
+
"weights/hamiltonzero_v1.eqx",
|
| 131 |
+
route_key,
|
| 132 |
+
)
|
| 133 |
+
state, q = burn_in(
|
| 134 |
+
compiled,
|
| 135 |
+
mcmc_key,
|
| 136 |
+
batch_size=256,
|
| 137 |
+
replicas=8,
|
| 138 |
+
burn_in=1024,
|
| 139 |
+
walker_chunk_size=16,
|
| 140 |
+
)
|
| 141 |
+
local_energy = energy(compiled, q)
|
| 142 |
+
local_spin = spin(compiled, q)
|
| 143 |
+
state, q = step(compiled, state, steps=24, walker_chunk_size=16)
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
`state` is the complete replica-exchange MCMC state and `q` is its cold-chain
|
| 147 |
+
population. `energy` returns named `total`, `exchange`, `casimir`, and `field`
|
| 148 |
+
local-energy samples. `spin` returns the complex local spin estimator in the
|
| 149 |
+
routed `(site, x/y/z)` order; contracting it with the routed public field
|
| 150 |
+
reproduces the energy field channel. The selected padded-site permutation is
|
| 151 |
+
returned as `order`. `order.leaf_to_input[leaf]` is the public input-site index
|
| 152 |
+
assigned to a compiled tree leaf; `order.input_to_leaf[site]` is its inverse.
|
| 153 |
+
The first mapping is also available as `compiled.route`. The public NetworkX
|
| 154 |
+
path starts in exactly the supplied `system.nodes` order, applies this route
|
| 155 |
+
once to the context and walkers, and then compiles an identity-routed tree.
|
| 156 |
+
There is no additional bit reversal: applying one would corrupt the mapping.
|
| 157 |
+
Both arrays include padded virtual leaves when the model width exceeds the
|
| 158 |
+
physical site count. A complete runnable version that prints sample means and
|
| 159 |
+
standard deviations is in
|
| 160 |
+
[`examples/compiled_inference.py`](examples/compiled_inference.py).
|
| 161 |
+
[`examples/j1j2_4x4_route.ipynb`](examples/j1j2_4x4_route.ipynb) constructs a
|
| 162 |
+
periodic 4-by-4 J1-J2 model from NetworkX and visualizes the returned order as
|
| 163 |
+
the successive cells of the compiled binary merge tree. Install its plotting
|
| 164 |
+
dependencies with `python -m pip install '.[notebooks]'`.
|
| 165 |
+
|
| 166 |
+
For a normalized pure state, the full-state `Tr(|psi><psi|)` is exactly one.
|
| 167 |
+
The nontrivial purity observable is the subsystem second Renyi value
|
| 168 |
+
`Tr(rho_A^2)`. It uses two independent computational-basis chains and a
|
| 169 |
+
two-replica SWAP estimator:
|
| 170 |
+
|
| 171 |
+
```python
|
| 172 |
+
import jax
|
| 173 |
+
|
| 174 |
+
from hamiltonzero import burn_in_basis, measure_renyi2
|
| 175 |
+
|
| 176 |
+
x_key, y_key = jax.random.split(jax.random.PRNGKey(1))
|
| 177 |
+
x_state, x = burn_in_basis(compiled, x_key, batch_size=256, burn_in=1024)
|
| 178 |
+
y_state, y = burn_in_basis(compiled, y_key, batch_size=256, burn_in=1024)
|
| 179 |
+
x_state, y_state, result = measure_renyi2(
|
| 180 |
+
compiled,
|
| 181 |
+
x_state,
|
| 182 |
+
y_state,
|
| 183 |
+
subsystem=range(4),
|
| 184 |
+
blocks=16,
|
| 185 |
+
steps_between=24,
|
| 186 |
+
)
|
| 187 |
+
print(result.purity, result.standard_error, result.renyi2_nats)
|
| 188 |
+
print(result.resolved, result.failure_reasons)
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
`subsystem` accepts public site indices or a boolean mask. The result also
|
| 192 |
+
retains each SWAP sample in stable log-polar form. Entropy is reported only
|
| 193 |
+
when the block-count, effective-sample-size, autocorrelation,
|
| 194 |
+
heavy-tail, imaginary-null, and physical-bound checks resolve the estimate;
|
| 195 |
+
otherwise `renyi2_nats` is `None` and `failure_reasons` says why.
|
| 196 |
+
`burn_in_basis` samples the computational basis required by this estimator.
|
| 197 |
+
The SU(2)-quaternion walkers returned by `burn_in` cannot be substituted for
|
| 198 |
+
those samples.
|
| 199 |
+
|
| 200 |
+
## Datasets
|
| 201 |
+
|
| 202 |
+
The repository includes the exact 5,000-system foundation training panel and
|
| 203 |
+
the evaluation systems with at least 256 physical spins. Every file uses the
|
| 204 |
+
public textbook units above.
|
| 205 |
+
|
| 206 |
+
- `datasets/train/foundation_5000.jsonl` contains systems from 2 through 64
|
| 207 |
+
spins, fixed WL1/FWL2 dispatch, and available exact-diagonalization energies.
|
| 208 |
+
- `datasets/eval/` contains the PPP-Ohno, RUDY, square-lattice J1-J2, and
|
| 209 |
+
triangular-Heisenberg evaluation systems from 256 through 8,100 physical
|
| 210 |
+
spins.
|
| 211 |
+
|
| 212 |
+
Large-N files store physical sites only; the loader reconstructs power-of-two
|
| 213 |
+
padding in memory. See [`datasets/README.md`](datasets/README.md) for the full
|
| 214 |
+
inventory and sparse exchange encoding.
|
| 215 |
+
|
| 216 |
+
## Train
|
| 217 |
+
|
| 218 |
+
The training command starts a new learned-router multisystem run and writes one
|
| 219 |
+
final full foundation-model checkpoint, including its learned router, plus a
|
| 220 |
+
metadata sidecar:
|
| 221 |
+
|
| 222 |
+
```bash
|
| 223 |
+
hamiltonzero train examples/train.json
|
| 224 |
+
```
|
| 225 |
+
|
| 226 |
+
The example uses `datasets/train/foundation_5000.jsonl`, writes
|
| 227 |
+
`outputs/foundation.eqx`, and exposes model, MCMC, KFAC, router, and energy
|
| 228 |
+
parameters through JSON. The command writes the trained model at the end of
|
| 229 |
+
the run.
|
| 230 |
+
|
| 231 |
+
To skip burn-in using compatible post-burn-in sampler states:
|
| 232 |
+
|
| 233 |
+
```bash
|
| 234 |
+
hamiltonzero train examples/train.json --reuse-mcmc path/to/mcmc-states
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
For multisystem training, the path is a directory containing
|
| 238 |
+
`<system-index>.eqx` files. For a one-system training panel it may be a single
|
| 239 |
+
file.
|
| 240 |
+
|
| 241 |
+
## Fine-tune
|
| 242 |
+
|
| 243 |
+
Fine-tuning selects and freezes a route from a router checkpoint, compiles the
|
| 244 |
+
single-system wavefunction, and optimizes that compiled model:
|
| 245 |
+
|
| 246 |
+
```bash
|
| 247 |
+
hamiltonzero finetune examples/finetune.json
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
The example fine-tunes on the 256-spin PPP-Ohno system and writes
|
| 251 |
+
`outputs/ppp_ohno_n256.eqx`. A neighboring `.eqx.json` sidecar records the
|
| 252 |
+
compiled-fine-tune kind, frozen model width, and configured ranks. A compatible
|
| 253 |
+
single post-burn-in state can also be supplied:
|
| 254 |
+
|
| 255 |
+
```bash
|
| 256 |
+
hamiltonzero finetune examples/finetune.json --reuse-mcmc path/to/state.eqx
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
## Evaluate
|
| 260 |
+
|
| 261 |
+
Compiled evaluation uses the route selected by a router checkpoint, or the
|
| 262 |
+
embedded frozen route in a compiled fine-tune checkpoint:
|
| 263 |
+
|
| 264 |
+
```bash
|
| 265 |
+
hamiltonzero eval examples/eval.json
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
Use router contest to compare candidate routes before evaluating the winner:
|
| 269 |
+
|
| 270 |
+
```bash
|
| 271 |
+
hamiltonzero eval examples/eval.json --contest
|
| 272 |
+
```
|
| 273 |
+
|
| 274 |
+
Use the sequence-sharded large-N implementation for the large systems:
|
| 275 |
+
|
| 276 |
+
```bash
|
| 277 |
+
hamiltonzero eval examples/eval_large_n.json --large-n
|
| 278 |
+
```
|
| 279 |
+
|
| 280 |
+
Each evaluation writes `eval.json` and `eval.metrics.jsonl` inside its
|
| 281 |
+
configured output directory.
|
| 282 |
+
|
| 283 |
+
Training and fine-tuning metrics are written beside the final checkpoint as
|
| 284 |
+
`<checkpoint>.metrics.jsonl`. Evaluation writes the same per-measurement fields
|
| 285 |
+
to `eval.metrics.jsonl`. These JSONL rows contain step, energy, energy standard
|
| 286 |
+
deviation, step wall time, and total wall time. Final `eval.json` additionally
|
| 287 |
+
reports exchange/field channels and lag-one autocorrelation when available.
|
| 288 |
+
|
| 289 |
+
## Configuration
|
| 290 |
+
|
| 291 |
+
Every command accepts one JSON configuration. The files in `examples/` are
|
| 292 |
+
minimal runnable configurations; omitted parameters use the defaults in
|
| 293 |
+
`hamiltonzero.config`.
|
| 294 |
+
|
| 295 |
+
The KFAC-JAX fork is vendored under `src/kfac_jax`.
|
| 296 |
+
|
| 297 |
+
## License
|
| 298 |
+
|
| 299 |
+
HamiltonZero first-party source, datasets, and released model weights are
|
| 300 |
+
licensed under Apache-2.0, copyright Simulacra Research Inc. The vendored
|
| 301 |
+
KFAC-JAX fork and JAX-derived large-N attention kernel remain under
|
| 302 |
+
Apache-2.0. The Microsoft-Folx-derived attention forward and reverse-mode
|
| 303 |
+
kernels remain under MIT. See
|
| 304 |
+
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md).
|
THIRD_PARTY_NOTICES.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Third-party notices
|
| 2 |
+
|
| 3 |
+
The KFAC-JAX fork under `src/kfac_jax`, including modifications copyright
|
| 4 |
+
2026 Simulacra Research Inc., is licensed under Apache-2.0. Its license is in
|
| 5 |
+
`third_party/kfac_jax/LICENSE`.
|
| 6 |
+
|
| 7 |
+
The attention forward and reverse-mode kernels derived from Microsoft Folx,
|
| 8 |
+
with modifications copyright 2026 Simulacra Research Inc., are licensed
|
| 9 |
+
under MIT. Its license is in `third_party/folx/LICENSE`.
|
| 10 |
+
|
| 11 |
+
The large-N Pallas attention kernel derived from JAX, with modifications
|
| 12 |
+
copyright 2026 Simulacra Research Inc., is licensed under Apache-2.0. Its
|
| 13 |
+
license is in `third_party/jax/LICENSE`.
|
datasets/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Datasets
|
| 2 |
+
|
| 3 |
+
All files use the public textbook convention
|
| 4 |
+
|
| 5 |
+
\[
|
| 6 |
+
H=\sum_{i<j}S_i^T J_{ij}S_j+\sum_i h_i^T S_i,\qquad S=\sigma/2.
|
| 7 |
+
\]
|
| 8 |
+
|
| 9 |
+
Sparse exchange terms are encoded as `[i, j, J]`, where `J` is a scalar,
|
| 10 |
+
a length-three diagonal, or a 3-by-3 matrix. Missing fields are zero.
|
| 11 |
+
HamiltonZero expands this representation when loading a system.
|
| 12 |
+
|
| 13 |
+
`train/foundation_5000.jsonl` contains the 5,000 systems used by the released
|
| 14 |
+
foundation checkpoint, with sizes from 2 to 64 spins. Each row includes the
|
| 15 |
+
fixed WL1/FWL2 dispatch and, where available, `e_ed` in textbook energy units.
|
| 16 |
+
There are 3,169 non-null exact-diagonalization references.
|
| 17 |
+
|
| 18 |
+
The N≥256 evaluation systems are:
|
| 19 |
+
|
| 20 |
+
| File | Physical spins | Family |
|
| 21 |
+
|---|---:|---|
|
| 22 |
+
| `eval/ppp_ohno_n256.json` | 256 | PPP–Ohno C128H130 |
|
| 23 |
+
| `eval/ppp_ohno_n512.json` | 512 | PPP–Ohno C256H258 |
|
| 24 |
+
| `eval/ppp_ohno_n1024.json` | 1,024 | PPP–Ohno C512H514 |
|
| 25 |
+
| `eval/rudy_n256.json` | 256 | RUDY-12 MaxCut |
|
| 26 |
+
| `eval/rudy_n512.json` | 512 | RUDY-12 MaxCut |
|
| 27 |
+
| `eval/rudy_n1024.json` | 1,024 | RUDY-12 MaxCut |
|
| 28 |
+
| `eval/j1j2_45x45_obc.json` | 2,025 | square-lattice J1–J2 |
|
| 29 |
+
| `eval/triangular_42x42_pbc.json` | 1,764 | triangular Heisenberg |
|
| 30 |
+
| `eval/j1j2_64x64_obc.json` | 4,096 | square-lattice J1–J2 |
|
| 31 |
+
| `eval/j1j2_90x90_obc.json` | 8,100 | square-lattice J1–J2 |
|
| 32 |
+
|
| 33 |
+
Each evaluation record stores its fixed WL1 routing dispatch.
|
| 34 |
+
|
| 35 |
+
All stored couplings and reference energies use the textbook convention above.
|
| 36 |
+
Padding is reconstructed in memory and is not stored as zero-valued sites.
|
| 37 |
+
|
| 38 |
+
These datasets are distributed under the repository Apache-2.0 license.
|
datasets/eval/j1j2_45x45_obc.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:524a4ab53dea1f05fab99b827f4e8aec8d991ac160c58a59b10d07a094ec2730
|
| 3 |
+
size 117093
|
datasets/eval/j1j2_64x64_obc.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b5177badfdd22af24182fc3a8ff358c4639457f0e6ed4bc2c332e818adb602f6
|
| 3 |
+
size 247879
|
datasets/eval/j1j2_90x90_obc.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:33ea4692401413b9afbf8caf5a02efceae1f2a9e16d3d38df4137d0913cf9315
|
| 3 |
+
size 501771
|
datasets/eval/ppp_ohno_n1024.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3c5934d263227de2520b7fe9843de59aa493528f4666c2bf996eb97d39743e9f
|
| 3 |
+
size 21848168
|
datasets/eval/ppp_ohno_n256.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:07e3dc5b262c7429bfa8cc7d08d561d7aa32a6031a8b68b5944fda84a124362e
|
| 3 |
+
size 1322178
|
datasets/eval/ppp_ohno_n512.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c346bbf5ed672d18a782f6cc2753416f10a141fbf5bb38d0977c67bca818d2e9
|
| 3 |
+
size 5385846
|
datasets/eval/rudy_n1024.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5bff99762c67a1ffda145f96da213475ae3f8280ae89d5b2022ed86a34182918
|
| 3 |
+
size 1623829
|
datasets/eval/rudy_n256.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6dae5999b8f02b7e513c581cec5469549606bc3cfee95dce732ab7c06d33d9a7
|
| 3 |
+
size 98782
|
datasets/eval/rudy_n512.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0416837f2d2bcb7421b2c8f483bcf9413365cc0f0c27c6d5c11288bad656b2ad
|
| 3 |
+
size 401666
|
datasets/eval/triangular_42x42_pbc.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:347f7dc3d215ef5c6f422aafb3bfe1dac7a5c9c01b954903ad6b7bcfecad85be
|
| 3 |
+
size 78234
|
datasets/train/foundation_5000.jsonl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ba07575057cc04fe69edebbf1c2083a25e6920991e6e7c606bbc994200744e48
|
| 3 |
+
size 17788002
|
examples/compiled_inference.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import jax
|
| 7 |
+
import jax.numpy as jnp
|
| 8 |
+
import networkx as nx
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
from hamiltonzero import (
|
| 12 |
+
SpinHamiltonian,
|
| 13 |
+
burn_in,
|
| 14 |
+
burn_in_basis,
|
| 15 |
+
energy,
|
| 16 |
+
measure_renyi2,
|
| 17 |
+
prepare,
|
| 18 |
+
spin,
|
| 19 |
+
step,
|
| 20 |
+
step_basis,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
graph = nx.path_graph(8)
|
| 25 |
+
nx.set_edge_attributes(graph, 1.0, "J")
|
| 26 |
+
system = SpinHamiltonian.from_networkx(graph)
|
| 27 |
+
|
| 28 |
+
route_key, mcmc_key, basis_x_key, basis_y_key = jax.random.split(
|
| 29 |
+
jax.random.PRNGKey(0), 4
|
| 30 |
+
)
|
| 31 |
+
compiled, order = prepare(
|
| 32 |
+
system,
|
| 33 |
+
Path("weights/hamiltonzero_v1.eqx"),
|
| 34 |
+
route_key,
|
| 35 |
+
)
|
| 36 |
+
state, q = burn_in(
|
| 37 |
+
compiled,
|
| 38 |
+
mcmc_key,
|
| 39 |
+
batch_size=256,
|
| 40 |
+
replicas=8,
|
| 41 |
+
burn_in=1024,
|
| 42 |
+
walker_chunk_size=16,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
local_energy = energy(compiled, q)
|
| 46 |
+
local_spin = spin(compiled, q)
|
| 47 |
+
print("leaf_to_input", np.asarray(order.leaf_to_input))
|
| 48 |
+
print("input_to_leaf", np.asarray(order.input_to_leaf))
|
| 49 |
+
print("energy", float(jnp.mean(local_energy.total.real)))
|
| 50 |
+
print("energy_std", float(jnp.std(local_energy.total.real)))
|
| 51 |
+
print("spin", np.asarray(jnp.mean(local_spin.real, axis=0)))
|
| 52 |
+
|
| 53 |
+
state, q = step(compiled, state, steps=24, walker_chunk_size=16)
|
| 54 |
+
|
| 55 |
+
basis_x, bits_x = burn_in_basis(compiled, basis_x_key, batch_size=256, burn_in=1024)
|
| 56 |
+
basis_y, bits_y = burn_in_basis(compiled, basis_y_key, batch_size=256, burn_in=1024)
|
| 57 |
+
basis_x, bits_x = step_basis(compiled, basis_x, steps=24)
|
| 58 |
+
basis_y, bits_y = step_basis(compiled, basis_y, steps=24)
|
| 59 |
+
basis_x, basis_y, purity = measure_renyi2(
|
| 60 |
+
compiled,
|
| 61 |
+
basis_x,
|
| 62 |
+
basis_y,
|
| 63 |
+
subsystem=range(4),
|
| 64 |
+
blocks=16,
|
| 65 |
+
samples_per_block=1,
|
| 66 |
+
steps_between=24,
|
| 67 |
+
)
|
| 68 |
+
print("purity", purity.purity)
|
| 69 |
+
print("purity_standard_error", purity.standard_error)
|
| 70 |
+
print("purity_resolved", purity.resolved, purity.failure_reasons)
|
| 71 |
+
print("renyi2_nats", purity.renyi2_nats)
|
examples/eval.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"system": "datasets/eval/ppp_ohno_n256.json",
|
| 3 |
+
"checkpoint": "weights/hamiltonzero_v1.eqx",
|
| 4 |
+
"output": "outputs/ppp_ohno_n256_eval"
|
| 5 |
+
}
|
examples/eval_large_n.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"system": "datasets/eval/j1j2_45x45_obc.json",
|
| 3 |
+
"checkpoint": "weights/hamiltonzero_v1.eqx",
|
| 4 |
+
"output": "outputs/j1j2_45x45_eval"
|
| 5 |
+
}
|
examples/finetune.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"system": "datasets/eval/ppp_ohno_n256.json",
|
| 3 |
+
"checkpoint": "weights/hamiltonzero_v1.eqx",
|
| 4 |
+
"output": "outputs/ppp_ohno_n256.eqx",
|
| 5 |
+
"steps": 1000
|
| 6 |
+
}
|
examples/j1j2_4x4_route.ipynb
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "code",
|
| 5 |
+
"execution_count": 1,
|
| 6 |
+
"id": "license",
|
| 7 |
+
"metadata": {},
|
| 8 |
+
"outputs": [],
|
| 9 |
+
"source": [
|
| 10 |
+
"# Copyright (c) 2026 Simulacra Research Inc.\n",
|
| 11 |
+
"# SPDX-License-Identifier: Apache-2.0"
|
| 12 |
+
]
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"cell_type": "markdown",
|
| 16 |
+
"id": "title",
|
| 17 |
+
"metadata": {},
|
| 18 |
+
"source": [
|
| 19 |
+
"# Learned merge-tree order for a 4×4 J1–J2 torus\n",
|
| 20 |
+
"\n",
|
| 21 |
+
"The public Hamiltonian is built in textbook units. `prepare` loads HamiltonZero v1, selects the beam-8 route, compiles the wavefunction, and returns both directions of the mapping between public input sites and binary-tree leaves."
|
| 22 |
+
]
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"cell_type": "code",
|
| 26 |
+
"execution_count": 2,
|
| 27 |
+
"id": "imports",
|
| 28 |
+
"metadata": {},
|
| 29 |
+
"outputs": [],
|
| 30 |
+
"source": [
|
| 31 |
+
"from pathlib import Path\n",
|
| 32 |
+
"\n",
|
| 33 |
+
"import jax\n",
|
| 34 |
+
"import matplotlib.pyplot as plt\n",
|
| 35 |
+
"from matplotlib.colors import ListedColormap\n",
|
| 36 |
+
"import networkx as nx\n",
|
| 37 |
+
"import numpy as np\n",
|
| 38 |
+
"\n",
|
| 39 |
+
"from hamiltonzero import SpinHamiltonian, prepare"
|
| 40 |
+
]
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"cell_type": "code",
|
| 44 |
+
"execution_count": 3,
|
| 45 |
+
"id": "hamiltonian",
|
| 46 |
+
"metadata": {},
|
| 47 |
+
"outputs": [],
|
| 48 |
+
"source": [
|
| 49 |
+
"L = 4\n",
|
| 50 |
+
"J1 = 1.0\n",
|
| 51 |
+
"J2 = 0.3\n",
|
| 52 |
+
"nodes = tuple((row, column) for row in range(L) for column in range(L))\n",
|
| 53 |
+
"graph = nx.Graph()\n",
|
| 54 |
+
"graph.add_nodes_from(nodes)\n",
|
| 55 |
+
"for row, column in nodes:\n",
|
| 56 |
+
" graph.add_edge((row, column), (row, (column + 1) % L), J=J1)\n",
|
| 57 |
+
" graph.add_edge((row, column), ((row + 1) % L, column), J=J1)\n",
|
| 58 |
+
" graph.add_edge((row, column), ((row + 1) % L, (column + 1) % L), J=J2)\n",
|
| 59 |
+
" graph.add_edge((row, column), ((row + 1) % L, (column - 1) % L), J=J2)\n",
|
| 60 |
+
"graph.graph[\"needs_fwl2\"] = False\n",
|
| 61 |
+
"system = SpinHamiltonian.from_networkx(graph, nodes=nodes)"
|
| 62 |
+
]
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"cell_type": "code",
|
| 66 |
+
"execution_count": 4,
|
| 67 |
+
"id": "prepare",
|
| 68 |
+
"metadata": {},
|
| 69 |
+
"outputs": [
|
| 70 |
+
{
|
| 71 |
+
"name": "stdout",
|
| 72 |
+
"output_type": "stream",
|
| 73 |
+
"text": [
|
| 74 |
+
"leaf input lattice site\n",
|
| 75 |
+
" 0 0 (0, 0)\n",
|
| 76 |
+
" 1 1 (0, 1)\n",
|
| 77 |
+
" 2 2 (0, 2)\n",
|
| 78 |
+
" 3 3 (0, 3)\n",
|
| 79 |
+
" 4 7 (1, 3)\n",
|
| 80 |
+
" 5 6 (1, 2)\n",
|
| 81 |
+
" 6 5 (1, 1)\n",
|
| 82 |
+
" 7 4 (1, 0)\n",
|
| 83 |
+
" 8 8 (2, 0)\n",
|
| 84 |
+
" 9 9 (2, 1)\n",
|
| 85 |
+
" 10 10 (2, 2)\n",
|
| 86 |
+
" 11 11 (2, 3)\n",
|
| 87 |
+
" 12 15 (3, 3)\n",
|
| 88 |
+
" 13 14 (3, 2)\n",
|
| 89 |
+
" 14 13 (3, 1)\n",
|
| 90 |
+
" 15 12 (3, 0)\n"
|
| 91 |
+
]
|
| 92 |
+
}
|
| 93 |
+
],
|
| 94 |
+
"source": [
|
| 95 |
+
"route_key = jax.random.PRNGKey(0)\n",
|
| 96 |
+
"compiled, order = prepare(\n",
|
| 97 |
+
" system,\n",
|
| 98 |
+
" Path(\"weights/hamiltonzero_v1.eqx\"),\n",
|
| 99 |
+
" route_key,\n",
|
| 100 |
+
")\n",
|
| 101 |
+
"leaf_to_input = np.asarray(order.leaf_to_input, dtype=np.int32)\n",
|
| 102 |
+
"input_to_leaf = np.asarray(order.input_to_leaf, dtype=np.int32)\n",
|
| 103 |
+
"print(\"leaf input lattice site\")\n",
|
| 104 |
+
"for leaf, site in enumerate(leaf_to_input):\n",
|
| 105 |
+
" print(f\"{leaf:>4} {site:>5} {system.nodes[site]}\")"
|
| 106 |
+
]
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"cell_type": "markdown",
|
| 110 |
+
"id": "permutation-convention",
|
| 111 |
+
"metadata": {},
|
| 112 |
+
"source": [
|
| 113 |
+
"## Permutation convention\n",
|
| 114 |
+
"\n",
|
| 115 |
+
"`leaf_to_input[leaf]` gives the index in `system.nodes` placed at that compiled leaf. `input_to_leaf[site]` is the inverse used below to color each public lattice site by its merge-tree cell. The NetworkX loader preserves the supplied node order. HamiltonZero applies the learned permutation once to the context and walkers, then compiles an identity-routed tree, so **no extra bit reversal is needed**. For padded systems the arrays also contain virtual leaves, which must remain when determining merge groups even though they are not drawn as physical sites."
|
| 116 |
+
]
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"cell_type": "code",
|
| 120 |
+
"execution_count": 5,
|
| 121 |
+
"id": "visualize",
|
| 122 |
+
"metadata": {},
|
| 123 |
+
"outputs": [
|
| 124 |
+
{
|
| 125 |
+
"data": {
|
| 126 |
+
"image/png": "iVBORw0KGgoAAAANSUhEUgAAB0QAAAGYCAYAAADSo1HRAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAASdAAAEnQB3mYfeAAAhqxJREFUeJzs3Xd0VOX69vErnRZKgFASem8aQIqgotKlK1WpUgQsCCqi6EEEURQVRTgqKCqiHJBmpEiRDhJACAJSJZAAgQABQhJSn/cP38zPIYUJBHZm+H7Wcp3M3s/sueZhnTs7+97FzRhjBNwFzpw5oypVqig2Nlb33HOP+vTpo3Llyuny5cs6duyYfvnlFyUlJengwYN273Nzc1P9+vW1c+dOi5JnbtasWRo8eLDy5Mmjl19+Wffee69CQ0M1ZcoUXbt2Td9884369etndUwAuCWNGjVSSEiI/P39NXDgQNWqVUuSdPz4cW3YsEHr1q1TWFiYAgMDbe9p0aKF1q5dq+joaBUuXNii5BmbNWuWhg0bpvvvv19t27bVhx9+qAsXLuTKrABws8aOHatJkybJx8dHTz75pB544AH5+vrq5MmT2r17t5YuXapp06apf//+tve89957eu2117RgwQJ17drVuvAZeOCBB3TmzBl17NhRVatWVbFixXTgwAFNmzZNFy5cUJ06dRQaGio3NzerowLATVu3bp0effRRSf/sT3fp0kUlSpTQ2bNndfDgQS1evFjNmjXT999/b3vPzp071aBBAz3zzDP6/PPPrYqeocjISJUvX15t27ZVo0aNVLFiRcXGxmrt2rX64YcfZIzR3Llz9eSTT1odFQBu2qVLl1SpUiVdvHhR1apV04ABA1ShQgXFxsbq2LFjWrFihU6fPq0zZ87Yva9w4cIqVqyYjh49alFyxxw7dky1a9dWgwYNtGnTJrVu3VorV660OhaclKfVAYA7ZeHChYqNjVWjRo20ceNGeXt7261/9913FRYWZk24m/TOO+9Ikr755hv16NFDktS1a1fVqlVLvXr10sSJE2mIAnBqBw8eVEhIiHx9fRUSEqJy5crZrX/jjTcUGRmpIkWKWJQw+9q3b69u3bqpUKFCkqTp06dbnAgAct63334rSfruu+/UvXv3dOvj4uJ09erVOx3rpn3//fcqX758uuU9e/ZUvXr19Oeff2rPnj2qW7funQ8HADkkrXYPHjxYX375Zbr1n3zyicLDw+90rJvm5+eniIgIFStWzG75gAEDVL58eb3zzjv6+eefaYgCcGq//PKLLl68qNq1a2v79u3Kly+f3fqJEyc63THvf3v++edVokQJvfHGG2rdurXVceDkaIjirnH27FlJUrt27dI1Q9P8+yDHli1b9O6770qSjh49qvbt29vWPfnkk3Y7zCkpKVq8eLHWrl2rs2fPqlChQnrooYf05JNPysfHxzbu2rVrtobl5MmTtXjxYv3yyy+6cuWKatasqSFDhiggIMCh7xMaGqqwsDBVrFgx3UGmHj166PXXX9fRo0e1f/9+29VUAOBs0mp33bp10zVD05QsWdL2c2xsrHr06KHQ0FBJ/xyo9vT8Z3cnrfb+29atW7VkyRIdP35cnp6eCgoKUr9+/ey2KUkvv/yy7az4P//8U999950iIiJUunRp9e7dWw0bNnT4O12/bQBwRWfPnpWbm5s6d+6c4fp8+fLZHawZOXKk7Uzv9957T998841t3eLFi+Xl5WV7HRYWph9++EH79+9XYmKiKlasqJ49e6ZrRn7//feaN2+exo0bp8DAQM2cOVP79u1T/vz59dhjj6lr164OX9GZUTNUkqpVq6bGjRvrt99+U2RkpEPbAoDcKm3fu0uXLhmud3NzU9myZW2vv/32W82aNUuStGLFCrvjJuPGjVODBg1sr69evaoffvhB27Zt0+XLl1WyZEm1adNGHTp0sKvFf/75p1577TV16tRJffv21TfffKPNmzcrKSlJjRo10pAhQ5Q/f36Hvo+3t3e6Zmia++67T5Lk4eHh0LYAILdKq92tWrVK1wxN8+992dDQUI0dO1axsbFKSkqyq92dOnXS4MGDba+NMfr555+1evVqnTlzRgUKFFCTJk3Up0+fdJ/VqVMnBQYGavr06VqxYoUWL15su2p10KBBqlChQra/26JFi7RixQotWbIk0+8GZIsB7hJff/21kWTatGnj0PgFCxYYSRn+N27cONu48PBwExQUlOG46tWrm5MnT9rGxsTEGEmmadOmZuDAgenGFyxY0GzZssWhfLNnzzaSTP/+/TNc369fPyPJzJ0716HtAUBudPLkSSPJFC9e3Fy8ePGG46OjozOt3U2bNrWNS0xMNH369MlwnK+vr/n111/tttuoUSMjycycOdN4eHjYjXdzczMfffTRTX/HgIAAI8lER0ff9DYAILepWrWqkWTWr1/v0Pj69etnWr/j4+Nt46ZPn268vb3TjXFzczNvv/223TbHjh1rJJnJkyeb4sWLp3tPly5dTFJS0i1/13r16hlJ5siRI7e8LQCw0vDhw40k88Ybbzg0/tVXX820dgcHB9vGbdu2zZQsWTLDca1bt7ar8+vWrTOSzDPPPGPbB//3f5UrVzbh4eG39D0vX75sWrRoYSSZefPm3dK2AMBqixYtMpJMkyZNTEpKyg3Hr169OtPaPWLECNu4s2fPmiZNmmQ4rkKFCubw4cN22/Xw8DC1atUyo0aNSjc+X758ZtWqVdn6XlevXjVlypQx7du3N8YYs2nTJtvvDeBmcYUo7hpdunTR2LFjtXLlSjVu3FjdunVTw4YNFRQUJF9f33TjH3jgAQUHB6tDhw6qXLmyPv74Y9u6qlWrSvrnytCOHTtqz549atmypTp37qwSJUro/PnzmjdvntavX68+ffpo/fr1dtvetWuXtmzZov79+6t58+a6dOmSZs+erT/++EPdu3fXoUOHbnjGY0REhCRlenZN2pk/znQ7GwC4XpkyZdShQwcFBwcrKChIvXv3VpMmTVS/fv0Mr7QsUKCAgoOD9cYbbyg0NFTz5s2z1VM/Pz/buFdffVVz5sxRjRo11LdvX1WsWFHXrl3T+vXrbbd3PHbsmIoWLWq3/eeff15NmzZVr169lDdvXi1btkwLFizQyy+/rCZNmqhRo0a3d0IAwEk8++yzGjFihNq1a6ennnpKDz/8sOrXr68qVapkeFXm1KlTNWvWLH377bcaM2aMmjZtaluXdneX5cuX69lnn1XRokXVv39/BQUFydvbW6Ghofr888/1n//8Rw0aNFCbNm3stj1u3DgVL15cH3zwgQIDA7Vr1y59+umnWrx4saZMmaIxY8bc9Pf87bff9Mcff+jRRx9V5cqVb3o7AJAbDBkyRDNnztQ777yj0NBQtWvXTvfdd5/q1KmT4Z22+vfvr5IlS2rkyJFq27athg8fbluXdgeVM2fOqH379rp8+bKeeuopPfLIIypcuLBOnjypmTNn6tdff9Ubb7yhKVOm2G077fa9o0ePVt26dXXy5El98sknOnr0qPr37681a9Y4/L1SU1PVsWNHSVJ0dLRCQ0OVnJyscePG2R4/BADOqk2bNqpUqZK2bt2q+vXrq2fPnmrYsKHq1atne1TPvwUFBSk4OFg9e/ZUwYIF7W6RXrFiRdvP3bp109atW/XQQw+pa9euKl26tKKjo7Vw4UKtXLlSPXr00K5du+z27dPuVtizZ0+1adNGcXFx+v7777V161b16tVLhw4dSnecJTNvv/22zp8/r2nTpt3C7ADXsbojC9xJu3fvNvfdd5/dGSoeHh6mQYMG5osvvjDJycnp3iPJ1K9fP8PtzZ8/30gyL774Yrp1qamp5rHHHjOSzF9//WWM+b8rRCWZKVOm2I1PSEgwDRo0MJLMrFmzbvhd0s7E/PDDDzNcP2XKlGyd2QkAudWFCxdMjx49jLu7u139rlixonn99dfNhQsX0r2nefPmmV51GRkZaby9vU2DBg3szkZP8+GHHxpJZtq0abZlaWenP/bYY+nOuHzppZeMJNOzZ8+b+n5cIQrAFaWkpJjx48eb/Pnz29VuPz8/07dvX7N///5073n33XeNJLNgwYIMt1mvXj2TL18+8/fff6dbFxoaaiSZTp062ZalXSHq7++f7nfFihUrbOscOZM+IydOnDAlS5Y0vr6+XB0KwGUsWbLElC1b1q5258mTx7Rq1cruqs80O3bssF3RmZHRo0cbSeb7779Pty4mJsZUqFDBFCxY0HY8Ju0KUUlm3bp1duPPnj1r/Pz8jCSzZ88eh79TUlJSuquVOnfubEJDQx3eBgDkZgcPHjRNmza1q3Pu7u6mbt265tNPPzUJCQnp3lOoUCFTqVKlDLe3atUqI8kMGDAgw/W9evUyksy2bdtsy9LupnX9sejk5GTbMZrrj4dnZv/+/cbLy8tMnDjRtowrRJETuEIUd5WgoCDt2LFDe/bssZ3NvXnzZu3YsUM7duzQokWLFBwcbPeMoqz8+uuvkv654rNz584yxkiSjDEyxuj48eOSpH379ql69eq29/n6+uqFF16w25a3t7dGjx6tbt26aePGjRo4cGCWn532TLzk5OQM1yclJdmNAwBn5efnp3nz5mnKlClatWqVQkJCFBISoj179mjSpEmaM2eONm3alOkzRq+3bt06JSYmKiYmRj179pT0f3Vbki5duiTpn9p9vddff13u7u52y8aOHasPP/xQGzduvIVvCQCuxd3dXf/5z380atQorVq1Stu2bdMff/yhrVu36rvvvtP//vc/LViwQB06dHBoe+fPn9cff/yhokWLauTIkZLsa7cxRl5eXhnW7uHDh9vdJUD650z6+vXra9euXTp06JBq1KiRre8XGRmpVq1aKTo6WsHBwVwdCsBldOrUSe3bt9fmzZu1ceNG23GTVatWadWqVRo9erQmT57s8PbSjpuk1f3rj5tcu3ZNV65cUXh4uN0z7h544AE9/PDDdtvy9/fX4MGDNXnyZG3cuFH33nuvQxk8PDwUHBwsY4zOnj2rDRs2aN68efr111+1YsUKNWvWzOHvAwC5UbVq1bR582bt379fa9eutd2dcPfu3dq9e7f+97//afXq1cqbN69D20ur3X/99Ve6Y96SFBYWJumf4yaNGze2vc/DwyPd3VfSlq1du1YbN27USy+9dMPPf/bZZ1WhQgW98sorDuUFHEWnBHeloKAgBQUF2V5v3LhR3bt316+//qpvv/1WgwYNcmg7abet3bRpU5bjYmNj7V5XqlQpw6ZrWtP0zJkzN/zstFseXLhwIcP1Fy9elCQVLlz4htsCAGcQGBiop59+Wk8//bSkf3bA+/Tpo82bN+vVV1/VvHnzHNpOWu0+ePCgDh48mOm462u3JLuTW9IUKVJEJUqUUGRkpEOfDwB3kwIFCujxxx/X448/Lkm6evWqXnvtNX322WcaPHiwIiIiHDqBL612X7hwQUuXLs10nKO1O235rl27dObMmWw1RE+fPq1HH31Uf//9txYtWqSWLVs6/F4AcAYeHh5q1qyZrVFojNGcOXM0cOBAvf/+++rZs6fq1q3r0LbS6ndwcHCW466v31nVbsmx4yZp3Nzc1L59e9vrQYMGqU2bNurdu7deeukl7dy50+FtAUBuVqtWLdWqVcv2OiQkRN27d9eWLVs0ffp0vfzyyw5tJ612//7771mOu752lylTJsPHwGWndn///fdav369Vq9eneHt2oFbQUMUkPTQQw/plVde0csvv6w1a9Y43BBNO3gzZcoUVatWLdNx/26+SlJMTEyG49KW+/j43PCz055jun///gzXpy2vUqXKDbcFAM6ofPny+uyzzxQUFJStZwil1e4+ffqoe/fumY4LDAxMtywmJibD511cvXrV4bsLAMDdrECBAvr00081f/58nT17Vvv27Uu3r5yRtNpdp04dTZo0KdNxefLkSbcsJ/a905w8eVLNmzfXiRMnNH/+fLsD7ADgqtzc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7dry8vJSp06dNHjwYEnS1KlTNXLkSElSsWLFVLVqVXl5eSksLEzh4eFKTU1VfHy87ZEVnp6e8vf3V2JiouLj43XvvfcqLi5O+/fvV3Jyspo3b67Vq1fLzc3tpr7n5s2b9eCDD6p169ZauXLlrUwZ7mYGuEuEhoaaQYMGmRIlShhJdv95eXmZrl27mrCwsHTv27VrlyldurTd+HHjxtmN+eabb0z58uXTbbdq1armvffes42LiYkxkkzTpk3NmjVrTMmSJW1j3dzczJNPPmni4uKy9b02b95sqlSpku5zt27delPzBAC5yYULF8xrr71matSoka7GSjINGzY0v/32W7r3Xb582TRp0sRubNOmTe3GbN++3TRt2jTdNv38/Mxzzz1noqKibGMbNWpkJJlDhw6Zhg0b2o2vX7++OXz4sMPfKTw8PMPv8u//VqxYcfOTBgC5wKeffmqaNWtmPDw80tW4smXLms8++yzD9z3//PPG3d3dbnx8fLxtfWRkpHnyySeNp6en3Rhvb2/z+OOPm127dtnGjh071kgyCxcuNP3797fbbokSJczPP//s8PepVKnSDWv3zJkzb37CACAXWLlypenataspUKBAuhpXsGBB8/zzz5srV66ke9+8efNM/vz57cYHBwfb1iclJZm33nrLFClSJN12GzdubP73v//Zxq5bt85IMiNGjDDTp083+fLls6v11x+Pycr27dtNp06djJeXl91nuru7mxYtWpg9e/bc0nwBQG5w8OBBM2zYMBMQEJCuxnp4eJj27dubQ4cOpXvfX3/9le549ogRI+zGLFiwwFStWjXdditUqGDGjRtnUlJSbGM9PDxMrVq1zNatW03ZsmXtxnfu3Nlcvnz5lr7npk2bjCTTunXrW9oO7m5cIYq7jjFGERERCg8P19WrV+Xn56caNWoof/78mb4nKSlJe/fu1blz55SSkqKqVatmeM/zQ4cOKTw8XPny5VO5cuUUEBBgtz7tCtG0sx+Tk5P1xx9/6MqVK6pRo0a68dn5Tn/++afOnj2rkiVLqk6dOje1HQDIzc6fP6+TJ08qKipK+fPnV7Vq1VS8ePEs33Po0CGdOHFCiYmJ8vPzy/AqoMjISB06dEipqakqW7asypUrZ7tSM03aFaJpZz8eOHBAp06dUunSpW3P1nDUtWvXMrx11781atToht8NAJxBbGys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a/v27apQoYLmzp1rdSynZIzR+vXrtWjRIh06dEgFChTQvffeq+eee05Fixa1Op7TSU1N1YoVK/Tzzz/r6NGjKlSokIKCgvTMM8+oRIkSVsfDTZo7d64WLlyoixcvqmrVqnr22Wd17733Wh3LKf3999+aP3++Vq5cqWvXrmn58uXy8/OzOpZTOnTokL7//nvt3btXiYmJqlKlioYMGaLatWtbHc0p7d27V3PmzNG+ffvk5uamypUra+DAgfx/3Ylt27ZNX3zxhY4fP64SJUroySefVOfOna2O5ZSio6O1ZMkSLVq0SFFRURo/frxat25tdSyndOHCBX3//ffavn27zp49q3LlyqlLly7q0KGD1dGc0tmzZ/XNN99ox44dio6OVtmyZfX444+rffv2cnNzszoebkJERIQ+/vhj7d69W3ny5FHLli317LPPytvb2+poTicpKUmrV6/W/PnzdfDgQbVr105vvvmm1bGcUmJiopYsWaJVq1bp+PHj8vf3V9OmTTV48GD5+PhYHc/pXLt2TfPmzdO6det08uRJlShRQg8++KAGDBigfPnyWR0PN+HatWuaPn261qxZo8TERNWtW1cvvfSSSpUqZXU0p7Rr1y7Nnz9fmzdvVtGiRfXzzz9bHclpbdmyRT/99JP++usv+fj4qHbt2nr++edVsmRJq6M5pTVr1mjRokU6fPiwChQooDp16uiZZ55RYGCg1dGyhStE74CYmBg1adJE33//vYYMGaJRo0Zpw4YNql+/vs6cOWN1PKfTsmVLjRo1SmXLltWpU6cUGhpqdSSn1adPH7Vq1UrGGL388svq1auXli9frkqVKikkJMTqeE6nb9++mjNnjho1aqQ33nhDnTt31oIFC1SlShXt2rXL6ni4CYMHD9awYcPUokULjRs3TsYYNWjQQKtXr7Y6mtOZNGmSWrVqpUuXLsnX11fbt29XYmKi1bGc0uzZs1W9enXt3LlT/fr10wsvvKDLly/r3nvv1bRp06yO53Rmzpyp4cOHKyAgQKNGjdIzzzyjkydPKigoSDNmzLA6Hm7Cjz/+qAceeEB+fn4aP368GjdurJ49e3Ig+CZs2bJF1atX18aNG1W9enVt375dUVFRVsdySseOHVNAQIC++OILPfLII3r99ddVoUIF9ejRQ127dpUxxuqITuXQoUNq1qyZ4uPj1bdvX7366qsq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3NysjgcHcdwEzmTSpElavHix+vfvr/bt21sdxymVKFFCU6dO1bVr17R3715NnjxZffv21fLly1WyZEmr48FB1G44k02bNumll15S9erVNXHiRKvjOK3x48crLi5OYWFhmjZtmoYOHaoSJUrowQcftDqaw2iI3maFChWSlPF98GNiYuzGAFbauHGjnnjiCVWtWlXLly/nrOpbkPZc1gceeECPP/64atSooaefflqhoaEWJ0N2FCpUiNqNXO/s2bNq0aKFLl26pJUrV6pq1apWR3Ja/567xx9/XE888YSeffZZtW3bNt1zo5B7/Xvfu2DBgnbrYmJiVLhwYQtSAfZSUlLUu3dv/frrr3r//ffVu3dvqyM5rRIlSthuWdmmTRuVK1dOzz77rFq3bq1evXpZnA6O4rgJnMV///tfjR07Vu3bt9eXX35pdRynlSdPHjVu3FiS9PDDD6tVq1aqVauWxo4dq6+++sridHAUtRvOYteuXWrfvr1Kly6tVatWZfg8TDimbt26kqSmTZvqiSee0L333qs+ffooLCzM2mDZwLXBt1n58uXl7e2tv//+O926tGXVqlW707EAO7///rvatWunsmXL6rfffuMM9Rzk6+urpk2bau/evUpISLA6DrKhWrVq1G7kaufPn1fz5s0VERGhZcuW6YEHHrA6kktp06aNEhMTOZnFyaTV5uvrd1JSkiIiIqjdsFxqaqoGDBig+fPna9KkSXrllVesjuRS0h5TsWPHDouTIDsyq91py/z9/TmhBZabPXu27WS5n376SV5eXlZHchnVq1dX+fLlqd1Opnjx4vLz8+O4CXK1vXv3qlWrVipSpIh+++03lSlTxupILiNPnjx6+OGHdeLECZ07d87qOA6jIXqbeXl5qUWLFlq/fr2Sk5Pt1q1atUq1a9fm/4iw1K5du9SmTRuVLl1a69at4/Ykt8GJEyfk5+fHVbdOpm3btrpw4YJ2795tt3zVqlVyd3dX69atLUoGSJcuXVKrVq10/Phx/fLLL2rWrJnVkVzOiRMnJEmlSpWyOAmyI60Zsnr1arvlGzZsUEJCgh577DErYgGSJGOMhg4dqjlz5mjixIk8ouI2oHY7p6CgIJUsWTJd7b569aq2bt1K7YblfvjhBw0aNEitW7fW4sWL+ds+hyUkJCgyMpLa7YTatGmjrVu3Ki4uzm75qlWrVKZM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kZQux19//WUkmdmzZ9uWLV682EgyGzduNMOGDTNFihQx7u7uplmzZiYqKsokJiaaoUOH2pYHBQWZo0eP2m03bRubN282o0aNMkWKFDGenp6mQoUKZt26dTecCwDOz8PDw7z66qt2y+677z5bzS1cuLDx9vY27du3NydOnDDGGHP8+HETEBBgJBlfX1/b2CFDhmS5LqvPvHbtmnnttddMiRIljLe3t/H19TVNmzY1v//+u92YMWPGmGLFipl8+fIZLy8vU7duXbNhw4Ybfk9Htn/hwgXTp08fky9fPuPr62s8PT1Ny5YtzV9//ZXlnGVUo9MEBASYp556yvY6JibGSDITJkwwX375pSlZsqTx8fExFSpUMNu2bTPGGPP555+bkiVLmjx58hh/f3/z888/223z39uYM2eOKV26tMmXL58pWLCg+eijj244FwCcX+vWrU39+vXtlo0cOdJWc/39/Y2Hh4epW7eu+e2332xjGjdubLy9vY23t7dtbM2aNW+4LrPPNMaY7777ztSsWdO4u7ubQoUKmYoVK5pvvvnGbsy3335rqlWrZjw9PU2BAgVMqVKlzPTp0x36rjfafkpKipk4caLx9/c3+fLlM56enqZmzZpm6dKlN5yz62t0mn79+pkSJUrYLWvUqJFp1qyZ2b59u6levbopUKCAKVCggPnvf/9rjDFm69atpmbNmsbX19fkyZPHvPnmm+m2m7aNPXv2mDp16piCBQsaT09P07t3bxMfH+/QfABwXh988IGRZKKjo23LFi1aZKu5pUuXNl5eXiYwMNBMnTrVNuaVV14xBQsWNJJM6dKlbeNDQ0OzXJfZZxpjzPbt282jjz5qPD09TaFChYyfn58ZM2aMiY2NtRvz8MMP28bkz5/fDBs2zMTFxd3wuzqy/SVLlphatWoZDw8Pky9fPuPv728mTpxoUlJSspyzjGq0Mf8cX5Jk/vzzT9uysWPHGknmzJkzpm3btqZgwYLG3d3dPP300yYpKcmcO3fOtGnTxhQsWNC4ubmZDh06mJiYGLvtpm0jKirKdOnSxRQqVMi4ubmZhg0bmmPHjt1wLgA4t927dxtJZsGCBbZl586dszvmnXaMeMiQIebq1avGGGOWLl1qihcvbiSZokWL2sZOnz49y3WZfaYxxkRGRpq+ffsaX19f22c+8cQT5vjx47YxZ8+eNb179zb5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",
|
| 127 |
+
"text/plain": [
|
| 128 |
+
"<Figure size 1860x408 with 5 Axes>"
|
| 129 |
+
]
|
| 130 |
+
},
|
| 131 |
+
"metadata": {},
|
| 132 |
+
"output_type": "display_data"
|
| 133 |
+
}
|
| 134 |
+
],
|
| 135 |
+
"source": [
|
| 136 |
+
"palette = ListedColormap([\n",
|
| 137 |
+
" \"#1696d2\", \"#0077b6\", \"#00a676\", \"#f59f00\",\n",
|
| 138 |
+
" \"#7b2cbf\", \"#e76f51\", \"#577590\", \"#90be6d\",\n",
|
| 139 |
+
"])\n",
|
| 140 |
+
"\n",
|
| 141 |
+
"def color_regions(labels):\n",
|
| 142 |
+
" regions = sorted(np.unique(labels))\n",
|
| 143 |
+
" neighbors = {region: set() for region in regions}\n",
|
| 144 |
+
" for row in range(L):\n",
|
| 145 |
+
" for column in range(L):\n",
|
| 146 |
+
" here = labels[row, column]\n",
|
| 147 |
+
" if row + 1 < L:\n",
|
| 148 |
+
" there = labels[row + 1, column]\n",
|
| 149 |
+
" if here != there:\n",
|
| 150 |
+
" neighbors[here].add(there)\n",
|
| 151 |
+
" neighbors[there].add(here)\n",
|
| 152 |
+
" if column + 1 < L:\n",
|
| 153 |
+
" there = labels[row, column + 1]\n",
|
| 154 |
+
" if here != there:\n",
|
| 155 |
+
" neighbors[here].add(there)\n",
|
| 156 |
+
" neighbors[there].add(here)\n",
|
| 157 |
+
" assigned = {}\n",
|
| 158 |
+
" for region in regions:\n",
|
| 159 |
+
" used = {assigned[value] for value in neighbors[region] if value in assigned}\n",
|
| 160 |
+
" assigned[region] = next(color for color in range(palette.N) if color not in used)\n",
|
| 161 |
+
" return np.vectorize(assigned.__getitem__)(labels)\n",
|
| 162 |
+
"\n",
|
| 163 |
+
"if leaf_to_input.shape != (L * L,) or not np.array_equal(np.sort(leaf_to_input), np.arange(L * L)):\n",
|
| 164 |
+
" raise ValueError(\"the returned order is not a permutation of the 4x4 public sites\")\n",
|
| 165 |
+
"leaf_position = input_to_leaf\n",
|
| 166 |
+
"levels = int(np.log2(leaf_to_input.size)) + 1\n",
|
| 167 |
+
"fig, axes = plt.subplots(1, levels, figsize=(3.1 * levels, 3.4), constrained_layout=True)\n",
|
| 168 |
+
"for level, axis in enumerate(axes):\n",
|
| 169 |
+
" labels = (leaf_position // (2**level)).reshape(L, L)\n",
|
| 170 |
+
" colors = np.zeros_like(labels) if level == 0 else color_regions(labels)\n",
|
| 171 |
+
" axis.imshow(colors, cmap=palette, vmin=0, vmax=palette.N - 1, origin=\"upper\")\n",
|
| 172 |
+
" for boundary in range(L + 1):\n",
|
| 173 |
+
" axis.plot([-0.5, L - 0.5], [boundary - 0.5, boundary - 0.5], color=\"black\", linewidth=1.0) if boundary in (0, L) else None\n",
|
| 174 |
+
" axis.plot([boundary - 0.5, boundary - 0.5], [-0.5, L - 0.5], color=\"black\", linewidth=1.0) if boundary in (0, L) else None\n",
|
| 175 |
+
" for row in range(L):\n",
|
| 176 |
+
" for column in range(L - 1):\n",
|
| 177 |
+
" if labels[row, column] != labels[row, column + 1]:\n",
|
| 178 |
+
" axis.plot([column + 0.5, column + 0.5], [row - 0.5, row + 0.5], color=\"black\", linewidth=1.0)\n",
|
| 179 |
+
" for row in range(L - 1):\n",
|
| 180 |
+
" for column in range(L):\n",
|
| 181 |
+
" if labels[row, column] != labels[row + 1, column]:\n",
|
| 182 |
+
" axis.plot([column - 0.5, column + 0.5], [row + 0.5, row + 0.5], color=\"black\", linewidth=1.0)\n",
|
| 183 |
+
" axis.set_title(f\"Step {level}\")\n",
|
| 184 |
+
" axis.set_xticks(range(L))\n",
|
| 185 |
+
" axis.set_yticks(range(L))\n",
|
| 186 |
+
" axis.set_xlabel(\"lattice column\")\n",
|
| 187 |
+
" axis.set_ylabel(\"lattice row\" if level == 0 else \"\")\n",
|
| 188 |
+
"plt.show()"
|
| 189 |
+
]
|
| 190 |
+
}
|
| 191 |
+
],
|
| 192 |
+
"metadata": {
|
| 193 |
+
"kernelspec": {
|
| 194 |
+
"display_name": "Python 3",
|
| 195 |
+
"language": "python",
|
| 196 |
+
"name": "python3"
|
| 197 |
+
},
|
| 198 |
+
"language_info": {
|
| 199 |
+
"name": "python",
|
| 200 |
+
"version": "3.12"
|
| 201 |
+
}
|
| 202 |
+
},
|
| 203 |
+
"nbformat": 4,
|
| 204 |
+
"nbformat_minor": 5
|
| 205 |
+
}
|
examples/networkx_system.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
|
| 8 |
+
from hamiltonzero import SpinHamiltonian
|
| 9 |
+
from hamiltonzero.data import save_system
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
graph = nx.path_graph(8)
|
| 13 |
+
nx.set_edge_attributes(graph, 1.0, "J")
|
| 14 |
+
nx.set_node_attributes(graph, 0.0, "h")
|
| 15 |
+
system = SpinHamiltonian.from_networkx(graph)
|
| 16 |
+
save_system(Path("outputs/systems/chain_8.json"), system)
|
examples/train.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"systems": "datasets/train/foundation_5000.jsonl",
|
| 3 |
+
"output": "outputs/foundation.eqx",
|
| 4 |
+
"steps": 1000,
|
| 5 |
+
"n_max": 64
|
| 6 |
+
}
|
pyproject.toml
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=77", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "hamiltonzero"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "Compiled neural wavefunctions for quantum spin systems"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.12"
|
| 11 |
+
license = "Apache-2.0 AND MIT"
|
| 12 |
+
license-files = [
|
| 13 |
+
"LICENSE",
|
| 14 |
+
"NOTICE",
|
| 15 |
+
"THIRD_PARTY_NOTICES.md",
|
| 16 |
+
"third_party/folx/LICENSE",
|
| 17 |
+
"third_party/jax/LICENSE",
|
| 18 |
+
"third_party/kfac_jax/LICENSE",
|
| 19 |
+
]
|
| 20 |
+
dependencies = [
|
| 21 |
+
"absl-py>=2.5",
|
| 22 |
+
"distrax>=0.1.9",
|
| 23 |
+
"dm-tree>=0.1.10",
|
| 24 |
+
"equinox==0.13.6",
|
| 25 |
+
"immutabledict>=4.3",
|
| 26 |
+
"jax @ git+https://github.com/TakeOver/jax.git@79f82535b15a444516d4a5e2beb71d283665b2ff",
|
| 27 |
+
"jaxlib==0.11.0",
|
| 28 |
+
"jaxtyping==0.3.9",
|
| 29 |
+
"networkx>=3.5",
|
| 30 |
+
"numpy>=2.3",
|
| 31 |
+
"optax>=0.2.8",
|
| 32 |
+
"packaging>=26",
|
| 33 |
+
"typing-extensions>=4.15",
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
[project.optional-dependencies]
|
| 37 |
+
notebooks = [
|
| 38 |
+
"jupyterlab>=4.4",
|
| 39 |
+
"matplotlib>=3.10",
|
| 40 |
+
]
|
| 41 |
+
|
| 42 |
+
[project.scripts]
|
| 43 |
+
hamiltonzero = "hamiltonzero.cli:main"
|
| 44 |
+
|
| 45 |
+
[tool.setuptools.packages.find]
|
| 46 |
+
where = ["src"]
|
| 47 |
+
include = ["hamiltonzero*", "kfac_jax*"]
|
src/hamiltonzero/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from .hamiltonian import SpinHamiltonian
|
| 5 |
+
from .inference import (
|
| 6 |
+
CompiledOrder,
|
| 7 |
+
EnergySamples,
|
| 8 |
+
PreparedInference,
|
| 9 |
+
burn_in,
|
| 10 |
+
energy,
|
| 11 |
+
prepare,
|
| 12 |
+
spin,
|
| 13 |
+
step,
|
| 14 |
+
)
|
| 15 |
+
from .renyi2 import (
|
| 16 |
+
BasisSamplerState,
|
| 17 |
+
Renyi2Result,
|
| 18 |
+
burn_in_basis,
|
| 19 |
+
measure_renyi2,
|
| 20 |
+
renyi2_purity,
|
| 21 |
+
step_basis,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
__all__ = [
|
| 25 |
+
"BasisSamplerState",
|
| 26 |
+
"CompiledOrder",
|
| 27 |
+
"EnergySamples",
|
| 28 |
+
"PreparedInference",
|
| 29 |
+
"Renyi2Result",
|
| 30 |
+
"SpinHamiltonian",
|
| 31 |
+
"burn_in",
|
| 32 |
+
"burn_in_basis",
|
| 33 |
+
"energy",
|
| 34 |
+
"measure_renyi2",
|
| 35 |
+
"prepare",
|
| 36 |
+
"renyi2_purity",
|
| 37 |
+
"spin",
|
| 38 |
+
"step",
|
| 39 |
+
"step_basis",
|
| 40 |
+
]
|
src/hamiltonzero/checkpoint.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any, Literal, TypeVar
|
| 9 |
+
|
| 10 |
+
import equinox as eqx
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
T = TypeVar("T")
|
| 14 |
+
CheckpointKind = Literal["router", "compiled_finetune"]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _metadata_path(path: str | Path) -> Path:
|
| 18 |
+
source = Path(path)
|
| 19 |
+
return source.with_name(source.name + ".json")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def save_model(
|
| 23 |
+
path: str | Path,
|
| 24 |
+
model: object,
|
| 25 |
+
*,
|
| 26 |
+
kind: CheckpointKind | None = None,
|
| 27 |
+
metadata: dict[str, Any] | None = None,
|
| 28 |
+
) -> None:
|
| 29 |
+
destination = Path(path)
|
| 30 |
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
| 31 |
+
eqx.tree_serialise_leaves(destination, model)
|
| 32 |
+
if kind is not None:
|
| 33 |
+
payload = {"kind": kind, **(metadata or {})}
|
| 34 |
+
_metadata_path(destination).write_text(
|
| 35 |
+
json.dumps(payload, indent=2, sort_keys=True) + "\n"
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def load_model(path: str | Path, template: T) -> T:
|
| 40 |
+
return eqx.tree_deserialise_leaves(Path(path), template)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def load_model_metadata(path: str | Path) -> dict[str, Any] | None:
|
| 44 |
+
source = _metadata_path(path)
|
| 45 |
+
return json.loads(source.read_text()) if source.exists() else None
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def save_mcmc(path: str | Path, state: object) -> None:
|
| 49 |
+
destination = Path(path)
|
| 50 |
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
| 51 |
+
eqx.tree_serialise_leaves(destination, state)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def load_mcmc(path: str | Path, template: T) -> T:
|
| 55 |
+
return eqx.tree_deserialise_leaves(Path(path), template)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
__all__ = [
|
| 59 |
+
"CheckpointKind",
|
| 60 |
+
"load_mcmc",
|
| 61 |
+
"load_model",
|
| 62 |
+
"load_model_metadata",
|
| 63 |
+
"save_mcmc",
|
| 64 |
+
"save_model",
|
| 65 |
+
]
|
src/hamiltonzero/cli.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import dataclasses
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
from .config import load_config
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _write_metric(path: Path, metric) -> None:
|
| 15 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 16 |
+
with path.open("a", encoding="utf-8") as stream:
|
| 17 |
+
stream.write(
|
| 18 |
+
json.dumps(dataclasses.asdict(metric), separators=(",", ":")) + "\n"
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _parser() -> argparse.ArgumentParser:
|
| 23 |
+
parser = argparse.ArgumentParser(prog="hamiltonzero")
|
| 24 |
+
commands = parser.add_subparsers(dest="mode", required=True)
|
| 25 |
+
for mode in ("train", "finetune"):
|
| 26 |
+
command = commands.add_parser(mode)
|
| 27 |
+
command.add_argument("config", type=Path)
|
| 28 |
+
command.add_argument("--reuse-mcmc", type=Path)
|
| 29 |
+
evaluate = commands.add_parser("eval")
|
| 30 |
+
evaluate.add_argument("config", type=Path)
|
| 31 |
+
pathway = evaluate.add_mutually_exclusive_group()
|
| 32 |
+
pathway.add_argument("--contest", action="store_true")
|
| 33 |
+
pathway.add_argument("--large-n", action="store_true")
|
| 34 |
+
return parser
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def main(argv: list[str] | None = None) -> None:
|
| 38 |
+
args = _parser().parse_args(argv)
|
| 39 |
+
config = load_config(args.config, args.mode)
|
| 40 |
+
if args.mode == "eval":
|
| 41 |
+
from .modes.eval import run
|
| 42 |
+
|
| 43 |
+
if args.contest or args.large_n:
|
| 44 |
+
config = dataclasses.replace(
|
| 45 |
+
config,
|
| 46 |
+
contest=bool(args.contest),
|
| 47 |
+
large_n=bool(args.large_n),
|
| 48 |
+
)
|
| 49 |
+
run(config)
|
| 50 |
+
return
|
| 51 |
+
if args.reuse_mcmc is not None:
|
| 52 |
+
config = dataclasses.replace(
|
| 53 |
+
config,
|
| 54 |
+
mcmc=dataclasses.replace(config.mcmc, reuse_mcmc=args.reuse_mcmc),
|
| 55 |
+
)
|
| 56 |
+
metrics_path = config.output.with_name(config.output.name + ".metrics.jsonl")
|
| 57 |
+
sink = lambda metric: _write_metric(metrics_path, metric)
|
| 58 |
+
if args.mode == "train":
|
| 59 |
+
from .modes.train import run_train
|
| 60 |
+
|
| 61 |
+
run_train(config, metric_sink=sink)
|
| 62 |
+
else:
|
| 63 |
+
from .modes.finetune import run_finetune
|
| 64 |
+
|
| 65 |
+
run_finetune(config, metric_sink=sink)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
if __name__ == "__main__":
|
| 69 |
+
main()
|
src/hamiltonzero/compiled/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
src/hamiltonzero/compiled/api.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import jax
|
| 7 |
+
import jax.numpy as jnp
|
| 8 |
+
|
| 9 |
+
from .tree import compile_physical_tree_reference
|
| 10 |
+
from .trunk import bind_shared_kernel, compile_shared_trunk
|
| 11 |
+
from .types import CompiledWaveFunction, CompiledWaveFunctions
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def compile_wavefunction(model, context, perm) -> CompiledWaveFunction:
|
| 15 |
+
trunk = compile_shared_trunk(model, context)
|
| 16 |
+
tree = compile_physical_tree_reference(model, trunk, perm)
|
| 17 |
+
return CompiledWaveFunction(kernel=bind_shared_kernel(model), tree=tree)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def compile_wavefunctions(model, context, perms) -> CompiledWaveFunctions:
|
| 21 |
+
trunk = compile_shared_trunk(model, context)
|
| 22 |
+
perms = jnp.asarray(perms, dtype=jnp.int32)
|
| 23 |
+
trees = jax.vmap(lambda perm: compile_physical_tree_reference(model, trunk, perm))(
|
| 24 |
+
perms
|
| 25 |
+
)
|
| 26 |
+
return CompiledWaveFunctions(kernel=bind_shared_kernel(model), trees=trees)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def select_compiled_wavefunction(
|
| 30 |
+
candidates: CompiledWaveFunctions,
|
| 31 |
+
winner,
|
| 32 |
+
) -> CompiledWaveFunction:
|
| 33 |
+
tree = jax.tree_util.tree_map(lambda value: value[winner], candidates.trees)
|
| 34 |
+
return CompiledWaveFunction(kernel=candidates.kernel, tree=tree)
|
src/hamiltonzero/compiled/execute.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import jax
|
| 9 |
+
import jax.numpy as jnp
|
| 10 |
+
|
| 11 |
+
from hamiltonzero.model import normalize_leaf_carriers
|
| 12 |
+
|
| 13 |
+
from .types import CARRY_LEFT, CARRY_RIGHT, EMPTY, MERGE
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _single(values: tuple[Any, ...], name: str) -> Any:
|
| 17 |
+
if len(values) != 1:
|
| 18 |
+
raise ValueError(
|
| 19 |
+
f"compiled HT executor requires exactly one {name}; got {len(values)}"
|
| 20 |
+
)
|
| 21 |
+
return values[0]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _factorized_apply(factor: Any, h: jax.Array, x: jax.Array) -> jax.Array:
|
| 25 |
+
odd_dtype = jnp.float32
|
| 26 |
+
x_compute = x if x.dtype == odd_dtype else x.astype(odd_dtype)
|
| 27 |
+
V = factor.V if factor.V.dtype == odd_dtype else factor.V.astype(odd_dtype)
|
| 28 |
+
U = factor.U if factor.U.dtype == odd_dtype else factor.U.astype(odd_dtype)
|
| 29 |
+
mixed = (x_compute @ V) * h
|
| 30 |
+
mixed = mixed if mixed.dtype == odd_dtype else mixed.astype(odd_dtype)
|
| 31 |
+
return mixed @ U
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _compiled_quadrilinear_merge(
|
| 35 |
+
T: jax.Array,
|
| 36 |
+
u_a: jax.Array,
|
| 37 |
+
u_b: jax.Array,
|
| 38 |
+
) -> jax.Array:
|
| 39 |
+
from hamiltonzero.energy import custom_lap_active, quadrilinear_merge_p
|
| 40 |
+
|
| 41 |
+
odd_dtype = jnp.float32
|
| 42 |
+
T = T if T.dtype == odd_dtype else T.astype(odd_dtype)
|
| 43 |
+
u_a = u_a if u_a.dtype == odd_dtype else u_a.astype(odd_dtype)
|
| 44 |
+
u_b = u_b if u_b.dtype == odd_dtype else u_b.astype(odd_dtype)
|
| 45 |
+
if custom_lap_active():
|
| 46 |
+
return quadrilinear_merge_p.bind(T, u_a, u_b)
|
| 47 |
+
G, d_r, _, _ = T.shape
|
| 48 |
+
leading = u_a.shape[:-1]
|
| 49 |
+
u_a_flat = u_a.reshape((-1, G, d_r))
|
| 50 |
+
u_b_flat = u_b.reshape((-1, G, d_r))
|
| 51 |
+
out_flat = jnp.einsum("ijkl,Bik,Bil->Bij", T, u_a_flat, u_b_flat)
|
| 52 |
+
return out_flat.reshape((*leading, G * d_r))
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _opcode_gates(opcodes: jax.Array, dtype: jnp.dtype) -> tuple[jax.Array, ...]:
|
| 56 |
+
both = (opcodes == MERGE).astype(dtype)
|
| 57 |
+
left = (opcodes == CARRY_LEFT).astype(dtype)
|
| 58 |
+
right = (opcodes == CARRY_RIGHT).astype(dtype)
|
| 59 |
+
return both, left, right
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _gate_reference(
|
| 63 |
+
candidate: jax.Array,
|
| 64 |
+
left_value: jax.Array,
|
| 65 |
+
right_value: jax.Array,
|
| 66 |
+
opcodes: jax.Array,
|
| 67 |
+
*,
|
| 68 |
+
feature_axis: bool,
|
| 69 |
+
) -> jax.Array:
|
| 70 |
+
both, left, right = _opcode_gates(opcodes, candidate.dtype)
|
| 71 |
+
if feature_axis:
|
| 72 |
+
both, left, right = both[..., None], left[..., None], right[..., None]
|
| 73 |
+
pad = candidate.ndim - both.ndim
|
| 74 |
+
shape = (1,) * pad + both.shape
|
| 75 |
+
both, left, right = both.reshape(shape), left.reshape(shape), right.reshape(shape)
|
| 76 |
+
return both * candidate + left * left_value + right * right_value
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def execute_wavefunction(kernel: Any, tree: Any, q_routed: jax.Array):
|
| 80 |
+
if len(tree.leaf_combiner_h) != 0 or len(tree.readout_combiner_h) != 0:
|
| 81 |
+
raise ValueError("single-head compiled HT executor does not accept combiners")
|
| 82 |
+
if len(tree.merge_h) != len(tree.opcodes):
|
| 83 |
+
raise ValueError("merge_h and opcodes must have one entry per tree level")
|
| 84 |
+
q_weight = kernel.q_to_odd.weight
|
| 85 |
+
leaf_factor = _single(kernel.leaf_factors, "leaf factor")
|
| 86 |
+
merge_factor = _single(kernel.merge_factors, "merge factor")
|
| 87 |
+
readout_factor = _single(kernel.readout_factors, "readout factor")
|
| 88 |
+
leaf_h = _single(tree.leaf_h, "leaf conditioner")
|
| 89 |
+
readout_h = _single(tree.readout_h, "readout conditioner")
|
| 90 |
+
odd_dtype = jnp.float32
|
| 91 |
+
q_compute = q_routed if q_routed.dtype == odd_dtype else q_routed.astype(odd_dtype)
|
| 92 |
+
q_weight = q_weight if q_weight.dtype == odd_dtype else q_weight.astype(odd_dtype)
|
| 93 |
+
z = q_compute @ q_weight
|
| 94 |
+
u_raw = _factorized_apply(leaf_factor, leaf_h, z)
|
| 95 |
+
u, log_rms = normalize_leaf_carriers(u_raw)
|
| 96 |
+
s = jnp.zeros(u.shape[:-1], dtype=odd_dtype)
|
| 97 |
+
s = s + log_rms.astype(s.dtype)
|
| 98 |
+
for h_level, opcodes in zip(tree.merge_h, tree.opcodes, strict=True):
|
| 99 |
+
if u.shape[-2] != 2 * h_level.shape[-2]:
|
| 100 |
+
raise ValueError("compiled merge level has incompatible shrinking shape")
|
| 101 |
+
u_left, u_right = u[..., 0::2, :], u[..., 1::2, :]
|
| 102 |
+
s_left, s_right = s[..., 0::2], s[..., 1::2]
|
| 103 |
+
raw = _compiled_quadrilinear_merge(kernel.merge_T, u_left, u_right)
|
| 104 |
+
out = raw + _factorized_apply(merge_factor, h_level, raw)
|
| 105 |
+
scale = jnp.sqrt(jnp.mean(out * out, axis=-1) + kernel.merge_eps)
|
| 106 |
+
candidate_u = out / scale[..., None]
|
| 107 |
+
candidate_s = s_left + s_right + jnp.log(scale)
|
| 108 |
+
u = _gate_reference(candidate_u, u_left, u_right, opcodes, feature_axis=True)
|
| 109 |
+
s = _gate_reference(candidate_s, s_left, s_right, opcodes, feature_axis=False)
|
| 110 |
+
if u.shape[-2] != 1:
|
| 111 |
+
raise ValueError("compiled tree did not reduce to one root")
|
| 112 |
+
u_root = u[..., 0, :]
|
| 113 |
+
s_root = s[..., 0]
|
| 114 |
+
psi = _factorized_apply(readout_factor, readout_h, u_root)
|
| 115 |
+
psi_re, psi_im = psi[..., 0], psi[..., 1]
|
| 116 |
+
log_abs = 0.5 * jnp.log(psi_re * psi_re + psi_im * psi_im) + s_root
|
| 117 |
+
phase = jnp.arctan2(psi_im, psi_re)
|
| 118 |
+
return log_abs, phase
|
src/hamiltonzero/compiled/model.py
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import equinox as eqx
|
| 9 |
+
import jax
|
| 10 |
+
import jax.numpy as jnp
|
| 11 |
+
from jaxtyping import Array
|
| 12 |
+
|
| 13 |
+
from hamiltonzero.model import (
|
| 14 |
+
normalize_leaf_carriers,
|
| 15 |
+
quadrilinear_merge,
|
| 16 |
+
tagged_dense_no_bias,
|
| 17 |
+
)
|
| 18 |
+
from hamiltonzero.optim import register_scale_and_shift
|
| 19 |
+
|
| 20 |
+
from .execute import (
|
| 21 |
+
_compiled_quadrilinear_merge,
|
| 22 |
+
_factorized_apply,
|
| 23 |
+
_gate_reference,
|
| 24 |
+
_single,
|
| 25 |
+
)
|
| 26 |
+
from .types import EMPTY, MERGE, OPCODE_DTYPE, CompiledTree, SharedKernel, level_widths
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _scale_tag(y_flat, x_flat, scale_param, *, tag_id: str):
|
| 30 |
+
return register_scale_and_shift(
|
| 31 |
+
y_flat,
|
| 32 |
+
x_flat,
|
| 33 |
+
scale=scale_param,
|
| 34 |
+
tag_id=tag_id,
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class CompiledFinetuneWaveFunction(eqx.Module):
|
| 39 |
+
kernel: SharedKernel
|
| 40 |
+
leaf_h: Array
|
| 41 |
+
merge_h: Array
|
| 42 |
+
readout_h: Array
|
| 43 |
+
perm: Array
|
| 44 |
+
inv_perm: Array
|
| 45 |
+
leaf_real: Array
|
| 46 |
+
opcodes: Array
|
| 47 |
+
n_sites: int = eqx.field(static=True)
|
| 48 |
+
r_leaf: int = eqx.field(static=True)
|
| 49 |
+
r_merge: int = eqx.field(static=True)
|
| 50 |
+
|
| 51 |
+
def leaf_h_rows(self) -> Array:
|
| 52 |
+
return self.leaf_h.reshape(self.n_sites, self.r_leaf)
|
| 53 |
+
|
| 54 |
+
def merge_h_level(self, level: int, width: int) -> Array:
|
| 55 |
+
return self.merge_h[level, : width * self.r_merge].reshape(width, self.r_merge)
|
| 56 |
+
|
| 57 |
+
@classmethod
|
| 58 |
+
def from_compiled(cls, kernel: SharedKernel, tree: CompiledTree):
|
| 59 |
+
leaf_h = _single(tree.leaf_h, "leaf conditioner")
|
| 60 |
+
readout_h = _single(tree.readout_h, "readout conditioner")
|
| 61 |
+
n_sites = int(tree.perm.shape[-1])
|
| 62 |
+
widths = level_widths(n_sites)
|
| 63 |
+
w_max = widths[0]
|
| 64 |
+
r_leaf = int(leaf_h.shape[-1])
|
| 65 |
+
r_merge = int(tree.merge_h[0].shape[-1])
|
| 66 |
+
merge_rows = []
|
| 67 |
+
opcode_rows = []
|
| 68 |
+
for width, h_l, ops_l in zip(widths, tree.merge_h, tree.opcodes, strict=True):
|
| 69 |
+
if h_l.shape[-2] != width or ops_l.shape[-1] != width:
|
| 70 |
+
raise ValueError(
|
| 71 |
+
f"level width mismatch: expected {width}, got "
|
| 72 |
+
f"{h_l.shape[-2]}/{ops_l.shape[-1]}"
|
| 73 |
+
)
|
| 74 |
+
pad = w_max - width
|
| 75 |
+
merge_rows.append(
|
| 76 |
+
jnp.pad(h_l, ((0, pad), (0, 0))).reshape(-1) if pad else h_l.reshape(-1)
|
| 77 |
+
)
|
| 78 |
+
opcode_rows.append(
|
| 79 |
+
jnp.pad(ops_l, (0, pad), constant_values=EMPTY) if pad else ops_l
|
| 80 |
+
)
|
| 81 |
+
return cls(
|
| 82 |
+
kernel=kernel,
|
| 83 |
+
leaf_h=leaf_h.reshape(-1),
|
| 84 |
+
merge_h=jnp.stack(merge_rows),
|
| 85 |
+
readout_h=readout_h,
|
| 86 |
+
perm=tree.perm,
|
| 87 |
+
inv_perm=tree.inv_perm,
|
| 88 |
+
leaf_real=tree.leaf_real,
|
| 89 |
+
opcodes=jnp.stack(opcode_rows).astype(OPCODE_DTYPE),
|
| 90 |
+
n_sites=n_sites,
|
| 91 |
+
r_leaf=r_leaf,
|
| 92 |
+
r_merge=r_merge,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
def as_compiled_tree(self) -> CompiledTree:
|
| 96 |
+
widths = level_widths(self.n_sites)
|
| 97 |
+
return CompiledTree(
|
| 98 |
+
perm=self.perm,
|
| 99 |
+
inv_perm=self.inv_perm,
|
| 100 |
+
leaf_real=self.leaf_real,
|
| 101 |
+
leaf_h=(self.leaf_h_rows(),),
|
| 102 |
+
leaf_combiner_h=(),
|
| 103 |
+
merge_h=tuple(
|
| 104 |
+
self.merge_h_level(i, width) for i, width in enumerate(widths)
|
| 105 |
+
),
|
| 106 |
+
opcodes=tuple(self.opcodes[i, :width] for i, width in enumerate(widths)),
|
| 107 |
+
readout_h=(self.readout_h,),
|
| 108 |
+
readout_combiner_h=(),
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
def route_q(self, q: Array) -> Array:
|
| 112 |
+
return jnp.take(q, self.perm, axis=-2)
|
| 113 |
+
|
| 114 |
+
def param_counts(self) -> dict:
|
| 115 |
+
kernel_leaves = {
|
| 116 |
+
"q_to_odd": self.kernel.q_to_odd.weight.size,
|
| 117 |
+
"leaf_V": self.kernel.leaf_factors[0].V.size,
|
| 118 |
+
"leaf_U": self.kernel.leaf_factors[0].U.size,
|
| 119 |
+
"merge_T": self.kernel.merge_T.size,
|
| 120 |
+
"merge_V": self.kernel.merge_factors[0].V.size,
|
| 121 |
+
"merge_U": self.kernel.merge_factors[0].U.size,
|
| 122 |
+
"readout_V": self.kernel.readout_factors[0].V.size,
|
| 123 |
+
"readout_U": self.kernel.readout_factors[0].U.size,
|
| 124 |
+
}
|
| 125 |
+
tree_leaves = {
|
| 126 |
+
"leaf_h": self.leaf_h.size,
|
| 127 |
+
"merge_h": self.merge_h.size,
|
| 128 |
+
"readout_h": self.readout_h.size,
|
| 129 |
+
}
|
| 130 |
+
return {
|
| 131 |
+
"kernel": kernel_leaves,
|
| 132 |
+
"tree": tree_leaves,
|
| 133 |
+
"kernel_total": sum(kernel_leaves.values()),
|
| 134 |
+
"tree_total": sum(tree_leaves.values()),
|
| 135 |
+
"total": sum(kernel_leaves.values()) + sum(tree_leaves.values()),
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
def __call__(self, q: Array, ctx: Any = None, t: Any = 0.0):
|
| 139 |
+
del ctx, t
|
| 140 |
+
return self._forward_plain(q)
|
| 141 |
+
|
| 142 |
+
def _forward_plain(self, q: Array):
|
| 143 |
+
kernel = self.kernel
|
| 144 |
+
odd_dtype = jnp.float32
|
| 145 |
+
q_c = q if q.dtype == odd_dtype else q.astype(odd_dtype)
|
| 146 |
+
weight = kernel.q_to_odd.weight
|
| 147 |
+
weight = weight if weight.dtype == odd_dtype else weight.astype(odd_dtype)
|
| 148 |
+
z = q_c @ weight
|
| 149 |
+
u_raw = _factorized_apply(kernel.leaf_factors[0], self.leaf_h_rows(), z)
|
| 150 |
+
u, log_rms = normalize_leaf_carriers(u_raw)
|
| 151 |
+
s = jnp.zeros(u.shape[:-1], dtype=odd_dtype) + log_rms.astype(odd_dtype)
|
| 152 |
+
widths = level_widths(self.n_sites)
|
| 153 |
+
for level, width in enumerate(widths):
|
| 154 |
+
h_level = self.merge_h_level(level, width)
|
| 155 |
+
opcodes = self.opcodes[level, :width]
|
| 156 |
+
u_left, u_right = u[..., 0::2, :], u[..., 1::2, :]
|
| 157 |
+
s_left, s_right = s[..., 0::2], s[..., 1::2]
|
| 158 |
+
raw = _compiled_quadrilinear_merge(kernel.merge_T, u_left, u_right)
|
| 159 |
+
out = raw + _factorized_apply(kernel.merge_factors[0], h_level, raw)
|
| 160 |
+
scale = jnp.sqrt(jnp.mean(out * out, axis=-1) + kernel.merge_eps)
|
| 161 |
+
candidate_u = out / scale[..., None]
|
| 162 |
+
candidate_s = s_left + s_right + jnp.log(scale)
|
| 163 |
+
u = _gate_reference(
|
| 164 |
+
candidate_u, u_left, u_right, opcodes, feature_axis=True
|
| 165 |
+
)
|
| 166 |
+
s = _gate_reference(
|
| 167 |
+
candidate_s, s_left, s_right, opcodes, feature_axis=False
|
| 168 |
+
)
|
| 169 |
+
return self._readout(
|
| 170 |
+
_factorized_apply(kernel.readout_factors[0], self.readout_h, u[..., 0, :]),
|
| 171 |
+
s[..., 0],
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
def _readout(self, psi: Array, s_root: Array):
|
| 175 |
+
psi_re, psi_im = psi[..., 0], psi[..., 1]
|
| 176 |
+
log_abs = 0.5 * jnp.log(psi_re * psi_re + psi_im * psi_im) + s_root
|
| 177 |
+
return log_abs, jnp.arctan2(psi_im, psi_re)
|
| 178 |
+
|
| 179 |
+
def call_tagged(self, q: Array, ctx: Any = None, t: Any = 0.0):
|
| 180 |
+
del ctx, t
|
| 181 |
+
if q.ndim != 2:
|
| 182 |
+
raise ValueError(
|
| 183 |
+
f"call_tagged is per-walker: expected q [P, 4], got {q.shape}"
|
| 184 |
+
)
|
| 185 |
+
kernel = self.kernel
|
| 186 |
+
odd_dtype = jnp.float32
|
| 187 |
+
q_c = q if q.dtype == odd_dtype else q.astype(odd_dtype)
|
| 188 |
+
real = self.leaf_real
|
| 189 |
+
z = tagged_dense_no_bias(
|
| 190 |
+
kernel.q_to_odd.weight,
|
| 191 |
+
q_c,
|
| 192 |
+
tag_id="compiled.q_to_odd",
|
| 193 |
+
pathway="odd",
|
| 194 |
+
kfac_structural_mask=real,
|
| 195 |
+
kfac_scan_shared=False,
|
| 196 |
+
kfac_repeat_ndim=1,
|
| 197 |
+
)
|
| 198 |
+
leaf = kernel.leaf_factors[0]
|
| 199 |
+
vz = tagged_dense_no_bias(
|
| 200 |
+
leaf.V,
|
| 201 |
+
z,
|
| 202 |
+
tag_id="compiled.leaf.V",
|
| 203 |
+
pathway="odd",
|
| 204 |
+
kfac_structural_mask=real,
|
| 205 |
+
kfac_scan_shared=False,
|
| 206 |
+
kfac_repeat_ndim=1,
|
| 207 |
+
)
|
| 208 |
+
vz_flat = vz.reshape(-1)
|
| 209 |
+
mixed = _scale_tag(
|
| 210 |
+
vz_flat * self.leaf_h,
|
| 211 |
+
vz_flat,
|
| 212 |
+
self.leaf_h,
|
| 213 |
+
tag_id="compiled.leaf.h",
|
| 214 |
+
).reshape(vz.shape)
|
| 215 |
+
u_raw = tagged_dense_no_bias(
|
| 216 |
+
leaf.U,
|
| 217 |
+
mixed,
|
| 218 |
+
tag_id="compiled.leaf.U",
|
| 219 |
+
pathway="odd",
|
| 220 |
+
kfac_structural_mask=real,
|
| 221 |
+
kfac_scan_shared=False,
|
| 222 |
+
kfac_repeat_ndim=1,
|
| 223 |
+
)
|
| 224 |
+
u, log_rms = normalize_leaf_carriers(u_raw)
|
| 225 |
+
s = jnp.zeros(u.shape[:-1], dtype=odd_dtype) + log_rms.astype(odd_dtype)
|
| 226 |
+
merge = kernel.merge_factors[0]
|
| 227 |
+
merge_T = kernel.merge_T
|
| 228 |
+
merge_eps = kernel.merge_eps
|
| 229 |
+
|
| 230 |
+
def level_body(carry, xs):
|
| 231 |
+
u_buffer, s_buffer = carry
|
| 232 |
+
h_level, opcodes = xs
|
| 233 |
+
u_left, u_right = u_buffer[0::2], u_buffer[1::2]
|
| 234 |
+
s_left, s_right = s_buffer[0::2], s_buffer[1::2]
|
| 235 |
+
merge_rows = opcodes == MERGE
|
| 236 |
+
raw = quadrilinear_merge(
|
| 237 |
+
merge_T,
|
| 238 |
+
u_left,
|
| 239 |
+
u_right,
|
| 240 |
+
tag_id="compiled.merge.T",
|
| 241 |
+
pathway="odd",
|
| 242 |
+
kfac_structural_mask=merge_rows,
|
| 243 |
+
kfac_scan_shared=True,
|
| 244 |
+
kfac_repeat_ndim=1,
|
| 245 |
+
)
|
| 246 |
+
vx = tagged_dense_no_bias(
|
| 247 |
+
merge.V,
|
| 248 |
+
raw,
|
| 249 |
+
tag_id="compiled.merge.V",
|
| 250 |
+
pathway="odd",
|
| 251 |
+
kfac_structural_mask=merge_rows,
|
| 252 |
+
kfac_scan_shared=True,
|
| 253 |
+
kfac_repeat_ndim=1,
|
| 254 |
+
)
|
| 255 |
+
vx_flat = vx.reshape(-1)
|
| 256 |
+
mixed_level = _scale_tag(
|
| 257 |
+
vx_flat * h_level,
|
| 258 |
+
vx_flat,
|
| 259 |
+
h_level,
|
| 260 |
+
tag_id="compiled.merge.h",
|
| 261 |
+
).reshape(vx.shape)
|
| 262 |
+
correction = tagged_dense_no_bias(
|
| 263 |
+
merge.U,
|
| 264 |
+
mixed_level,
|
| 265 |
+
tag_id="compiled.merge.U",
|
| 266 |
+
pathway="odd",
|
| 267 |
+
kfac_structural_mask=merge_rows,
|
| 268 |
+
kfac_scan_shared=True,
|
| 269 |
+
kfac_repeat_ndim=1,
|
| 270 |
+
)
|
| 271 |
+
out = raw + correction
|
| 272 |
+
scale = jnp.sqrt(jnp.mean(out * out, axis=-1) + merge_eps)
|
| 273 |
+
candidate_u = out / scale[..., None]
|
| 274 |
+
candidate_s = s_left + s_right + jnp.log(scale)
|
| 275 |
+
u_next = _gate_reference(
|
| 276 |
+
candidate_u, u_left, u_right, opcodes, feature_axis=True
|
| 277 |
+
)
|
| 278 |
+
s_next = _gate_reference(
|
| 279 |
+
candidate_s, s_left, s_right, opcodes, feature_axis=False
|
| 280 |
+
)
|
| 281 |
+
return (
|
| 282 |
+
jnp.concatenate([u_next, jnp.zeros_like(u_next)], axis=0),
|
| 283 |
+
jnp.concatenate([s_next, jnp.zeros_like(s_next)], axis=0),
|
| 284 |
+
), None
|
| 285 |
+
|
| 286 |
+
(u_buffer, s_buffer), _ = jax.lax.scan(
|
| 287 |
+
level_body,
|
| 288 |
+
(u, s),
|
| 289 |
+
(self.merge_h, self.opcodes),
|
| 290 |
+
)
|
| 291 |
+
readout = kernel.readout_factors[0]
|
| 292 |
+
vr = tagged_dense_no_bias(
|
| 293 |
+
readout.V,
|
| 294 |
+
u_buffer[0],
|
| 295 |
+
tag_id="compiled.readout.V",
|
| 296 |
+
pathway="odd",
|
| 297 |
+
)
|
| 298 |
+
mixed_readout = _scale_tag(
|
| 299 |
+
vr * self.readout_h,
|
| 300 |
+
vr,
|
| 301 |
+
self.readout_h,
|
| 302 |
+
tag_id="compiled.readout.h",
|
| 303 |
+
)
|
| 304 |
+
psi = tagged_dense_no_bias(
|
| 305 |
+
readout.U,
|
| 306 |
+
mixed_readout,
|
| 307 |
+
tag_id="compiled.readout.U",
|
| 308 |
+
pathway="odd",
|
| 309 |
+
)
|
| 310 |
+
return self._readout(psi, s_buffer[0])
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def _expand_stage(V, U, h_2d, new_rank: int, key):
|
| 314 |
+
old_rank = V.shape[-1]
|
| 315 |
+
if new_rank < old_rank:
|
| 316 |
+
raise ValueError(f"cannot shrink rank {old_rank} -> {new_rank}")
|
| 317 |
+
if new_rank == old_rank:
|
| 318 |
+
return V, U, h_2d
|
| 319 |
+
extra = new_rank - old_rank
|
| 320 |
+
key_u, key_h = jax.random.split(key)
|
| 321 |
+
v_new = jnp.zeros((*V.shape[:-1], extra), dtype=V.dtype)
|
| 322 |
+
u_new = jnp.std(U) * jax.random.normal(key_u, (extra, *U.shape[1:]), dtype=U.dtype)
|
| 323 |
+
h_new = jnp.std(h_2d) * jax.random.normal(
|
| 324 |
+
key_h, (*h_2d.shape[:-1], extra), dtype=h_2d.dtype
|
| 325 |
+
)
|
| 326 |
+
return (
|
| 327 |
+
jnp.concatenate([V, v_new], axis=-1),
|
| 328 |
+
jnp.concatenate([U, u_new], axis=0),
|
| 329 |
+
jnp.concatenate([h_2d, h_new], axis=-1),
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def expand_rank(
|
| 334 |
+
model: CompiledFinetuneWaveFunction,
|
| 335 |
+
*,
|
| 336 |
+
leaf_rank: int,
|
| 337 |
+
merge_rank: int,
|
| 338 |
+
key,
|
| 339 |
+
) -> CompiledFinetuneWaveFunction:
|
| 340 |
+
key_leaf, key_merge = jax.random.split(jnp.asarray(key), 3)[:2]
|
| 341 |
+
kernel = model.kernel
|
| 342 |
+
leaf = kernel.leaf_factors[0]
|
| 343 |
+
merge = kernel.merge_factors[0]
|
| 344 |
+
n_sites = model.n_sites
|
| 345 |
+
max_width = n_sites // 2
|
| 346 |
+
n_levels = model.merge_h.shape[0]
|
| 347 |
+
leaf_h = model.leaf_h.reshape(n_sites, model.r_leaf)
|
| 348 |
+
merge_h = model.merge_h.reshape(n_levels, max_width, model.r_merge)
|
| 349 |
+
readout_h = model.readout_h
|
| 350 |
+
r_leaf, r_merge = model.r_leaf, model.r_merge
|
| 351 |
+
V, U, leaf_h = _expand_stage(leaf.V, leaf.U, leaf_h, int(leaf_rank), key_leaf)
|
| 352 |
+
leaf = eqx.tree_at(lambda factor: (factor.V, factor.U), leaf, (V, U))
|
| 353 |
+
r_leaf = int(leaf_rank)
|
| 354 |
+
V, U, merge_h = _expand_stage(merge.V, merge.U, merge_h, int(merge_rank), key_merge)
|
| 355 |
+
merge = eqx.tree_at(lambda factor: (factor.V, factor.U), merge, (V, U))
|
| 356 |
+
r_merge = int(merge_rank)
|
| 357 |
+
kernel = eqx.tree_at(
|
| 358 |
+
lambda value: (
|
| 359 |
+
value.leaf_factors,
|
| 360 |
+
value.merge_factors,
|
| 361 |
+
),
|
| 362 |
+
kernel,
|
| 363 |
+
((leaf,), (merge,)),
|
| 364 |
+
)
|
| 365 |
+
return CompiledFinetuneWaveFunction(
|
| 366 |
+
kernel=kernel,
|
| 367 |
+
leaf_h=leaf_h.reshape(-1),
|
| 368 |
+
merge_h=merge_h.reshape(n_levels, -1),
|
| 369 |
+
readout_h=readout_h,
|
| 370 |
+
perm=model.perm,
|
| 371 |
+
inv_perm=model.inv_perm,
|
| 372 |
+
leaf_real=model.leaf_real,
|
| 373 |
+
opcodes=model.opcodes,
|
| 374 |
+
n_sites=n_sites,
|
| 375 |
+
r_leaf=r_leaf,
|
| 376 |
+
r_merge=r_merge,
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def compile_finetune_model(
|
| 381 |
+
eager_model,
|
| 382 |
+
ctx_row,
|
| 383 |
+
*,
|
| 384 |
+
leaf_rank: int,
|
| 385 |
+
merge_rank: int,
|
| 386 |
+
physical_perm,
|
| 387 |
+
key,
|
| 388 |
+
) -> CompiledFinetuneWaveFunction:
|
| 389 |
+
from .tree import compile_physical_tree_reference
|
| 390 |
+
from .trunk import bind_shared_kernel, compile_shared_trunk
|
| 391 |
+
|
| 392 |
+
shared_trunk = compile_shared_trunk(eager_model, ctx_row)
|
| 393 |
+
n_sites = int(shared_trunk.real_mask.shape[-1])
|
| 394 |
+
identity = jnp.arange(n_sites, dtype=jnp.int32)
|
| 395 |
+
tree = compile_physical_tree_reference(eager_model, shared_trunk, identity)
|
| 396 |
+
model = CompiledFinetuneWaveFunction.from_compiled(
|
| 397 |
+
bind_shared_kernel(eager_model), tree
|
| 398 |
+
)
|
| 399 |
+
model = expand_rank(
|
| 400 |
+
model,
|
| 401 |
+
leaf_rank=leaf_rank,
|
| 402 |
+
merge_rank=merge_rank,
|
| 403 |
+
key=key,
|
| 404 |
+
)
|
| 405 |
+
physical_perm = jnp.asarray(physical_perm, dtype=jnp.int32)
|
| 406 |
+
if physical_perm.shape != (n_sites,):
|
| 407 |
+
raise ValueError(
|
| 408 |
+
f"physical_perm must have shape {(n_sites,)}, got {physical_perm.shape}"
|
| 409 |
+
)
|
| 410 |
+
model = eqx.tree_at(
|
| 411 |
+
lambda value: (value.perm, value.inv_perm),
|
| 412 |
+
model,
|
| 413 |
+
(
|
| 414 |
+
physical_perm,
|
| 415 |
+
jnp.argsort(physical_perm).astype(jnp.int32),
|
| 416 |
+
),
|
| 417 |
+
)
|
| 418 |
+
return model
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def build_finetune_template_model(
|
| 422 |
+
eager_model,
|
| 423 |
+
n_sites: int,
|
| 424 |
+
*,
|
| 425 |
+
leaf_rank: int,
|
| 426 |
+
merge_rank: int,
|
| 427 |
+
) -> CompiledFinetuneWaveFunction:
|
| 428 |
+
from .trunk import bind_shared_kernel
|
| 429 |
+
|
| 430 |
+
kernel = bind_shared_kernel(eager_model)
|
| 431 |
+
widths = level_widths(int(n_sites))
|
| 432 |
+
r_leaf = int(kernel.leaf_factors[0].V.shape[-1])
|
| 433 |
+
r_merge = int(kernel.merge_factors[0].V.shape[-1])
|
| 434 |
+
r_readout = int(kernel.readout_factors[0].V.shape[-1])
|
| 435 |
+
tree = CompiledTree(
|
| 436 |
+
perm=jnp.arange(n_sites, dtype=jnp.int32),
|
| 437 |
+
inv_perm=jnp.arange(n_sites, dtype=jnp.int32),
|
| 438 |
+
leaf_real=jnp.ones((n_sites,), dtype=jnp.bool_),
|
| 439 |
+
leaf_h=(jnp.ones((n_sites, r_leaf), dtype=jnp.float32),),
|
| 440 |
+
leaf_combiner_h=(),
|
| 441 |
+
merge_h=tuple(
|
| 442 |
+
jnp.ones((width, r_merge), dtype=jnp.float32) for width in widths
|
| 443 |
+
),
|
| 444 |
+
opcodes=tuple(
|
| 445 |
+
jnp.full((width,), MERGE, dtype=OPCODE_DTYPE) for width in widths
|
| 446 |
+
),
|
| 447 |
+
readout_h=(jnp.ones((r_readout,), dtype=jnp.float32),),
|
| 448 |
+
readout_combiner_h=(),
|
| 449 |
+
)
|
| 450 |
+
model = CompiledFinetuneWaveFunction.from_compiled(kernel, tree)
|
| 451 |
+
return expand_rank(
|
| 452 |
+
model,
|
| 453 |
+
leaf_rank=leaf_rank,
|
| 454 |
+
merge_rank=merge_rank,
|
| 455 |
+
key=jax.random.PRNGKey(0),
|
| 456 |
+
)
|
src/hamiltonzero/compiled/tree.py
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import equinox as eqx
|
| 9 |
+
import jax
|
| 10 |
+
import jax.numpy as jnp
|
| 11 |
+
|
| 12 |
+
from hamiltonzero.model import (
|
| 13 |
+
edge_merge_masked,
|
| 14 |
+
tagged_dense,
|
| 15 |
+
tagged_rms_eqx_style,
|
| 16 |
+
tree_active_clock_depth,
|
| 17 |
+
tree_depth_count_features,
|
| 18 |
+
tree_sphere,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
from .types import (
|
| 22 |
+
CARRY_LEFT,
|
| 23 |
+
CARRY_RIGHT,
|
| 24 |
+
EMPTY,
|
| 25 |
+
MERGE,
|
| 26 |
+
CompiledTree,
|
| 27 |
+
LadderProjectionKernel,
|
| 28 |
+
PhysicalCompilerKernel,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def bind_physical_compiler_kernel(model: Any) -> PhysicalCompilerKernel:
|
| 33 |
+
leaf = eqx.tree_at(lambda x: (x.P_u.V, x.P_u.U), model.leaf, (None, None))
|
| 34 |
+
merge = eqx.tree_at(
|
| 35 |
+
lambda x: (x.T, x.output_hypernet.V, x.output_hypernet.U),
|
| 36 |
+
model.merge,
|
| 37 |
+
(None, None, None),
|
| 38 |
+
)
|
| 39 |
+
readout = eqx.tree_at(
|
| 40 |
+
lambda x: (x.output_hypernet.V, x.output_hypernet.U),
|
| 41 |
+
model.readout,
|
| 42 |
+
(None, None),
|
| 43 |
+
)
|
| 44 |
+
return PhysicalCompilerKernel(
|
| 45 |
+
contextualizer=model.readout_leaf_context,
|
| 46 |
+
global_fork=model.gladder_fork_phys,
|
| 47 |
+
leaf=leaf,
|
| 48 |
+
merge=merge,
|
| 49 |
+
readout=readout,
|
| 50 |
+
leaf_projection=LadderProjectionKernel(
|
| 51 |
+
model.gladder_to_gemb_w,
|
| 52 |
+
model.gladder_to_gemb_b,
|
| 53 |
+
model.gladder_gemb_ln_s,
|
| 54 |
+
),
|
| 55 |
+
tree_pool=model.gladder_tree_pool,
|
| 56 |
+
tree_update=model.gladder_tree_update,
|
| 57 |
+
tree_projection_weight=model.gladder_tree_proj_w,
|
| 58 |
+
tree_projection_bias=model.gladder_tree_proj_b,
|
| 59 |
+
root_projection=LadderProjectionKernel(
|
| 60 |
+
model.gladder_root_proj_w,
|
| 61 |
+
model.gladder_root_proj_b,
|
| 62 |
+
model.gladder_root_ln_s,
|
| 63 |
+
),
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _project_global(
|
| 68 |
+
projection: LadderProjectionKernel,
|
| 69 |
+
value,
|
| 70 |
+
*,
|
| 71 |
+
dense_tag: str,
|
| 72 |
+
norm_tag: str,
|
| 73 |
+
):
|
| 74 |
+
structural_active = jnp.asarray(True)
|
| 75 |
+
out = tagged_dense(
|
| 76 |
+
projection.weight,
|
| 77 |
+
projection.bias,
|
| 78 |
+
value,
|
| 79 |
+
tag_id=dense_tag,
|
| 80 |
+
pathway="even",
|
| 81 |
+
kfac_structural_mask=structural_active,
|
| 82 |
+
kfac_scan_shared=False,
|
| 83 |
+
kfac_repeat_ndim=0,
|
| 84 |
+
)
|
| 85 |
+
return tagged_rms_eqx_style(
|
| 86 |
+
projection.norm_scale,
|
| 87 |
+
out,
|
| 88 |
+
tag_id=norm_tag,
|
| 89 |
+
pathway="even",
|
| 90 |
+
kfac_structural_mask=structural_active,
|
| 91 |
+
kfac_scan_shared=False,
|
| 92 |
+
kfac_repeat_ndim=0,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def compile_context_only_reduction(
|
| 97 |
+
*,
|
| 98 |
+
merge,
|
| 99 |
+
c_leaf,
|
| 100 |
+
leaf_real,
|
| 101 |
+
g_emb,
|
| 102 |
+
edges,
|
| 103 |
+
structural_mask,
|
| 104 |
+
gladder,
|
| 105 |
+
n_total=None,
|
| 106 |
+
clock_depth=None,
|
| 107 |
+
initial_counts=None,
|
| 108 |
+
level_offset: int = 0,
|
| 109 |
+
feature_n_levels=None,
|
| 110 |
+
):
|
| 111 |
+
c = jnp.asarray(c_leaf)
|
| 112 |
+
m = jnp.asarray(leaf_real, dtype=c.dtype)
|
| 113 |
+
if c.shape[0] != m.shape[0]:
|
| 114 |
+
raise ValueError("c_leaf and leaf_real widths differ")
|
| 115 |
+
n = c.shape[0]
|
| 116 |
+
if n == 0 or n & (n - 1):
|
| 117 |
+
raise ValueError(f"context-only width must be a power of two, got {n}")
|
| 118 |
+
k = jnp.asarray(structural_mask, dtype=c.dtype)
|
| 119 |
+
if k.shape != m.shape:
|
| 120 |
+
raise ValueError("structural_mask and leaf_real widths differ")
|
| 121 |
+
n_total = jnp.sum(m) if n_total is None else n_total
|
| 122 |
+
feature_n_levels = (
|
| 123 |
+
tree_active_clock_depth(m) if feature_n_levels is None else feature_n_levels
|
| 124 |
+
)
|
| 125 |
+
clock_depth = tree_active_clock_depth(k) if clock_depth is None else clock_depth
|
| 126 |
+
counts = m if initial_counts is None else jnp.asarray(initial_counts, dtype=m.dtype)
|
| 127 |
+
if counts.shape != m.shape:
|
| 128 |
+
raise ValueError("initial_counts and leaf_real widths differ")
|
| 129 |
+
candidates = []
|
| 130 |
+
carried = []
|
| 131 |
+
depth_levels = []
|
| 132 |
+
opcode_levels = []
|
| 133 |
+
g_curr = g_emb
|
| 134 |
+
e_curr = edges
|
| 135 |
+
e_curr = tree_sphere(e_curr)
|
| 136 |
+
level = int(level_offset)
|
| 137 |
+
while c.shape[0] > 1:
|
| 138 |
+
c_a, c_b = c[0::2], c[1::2]
|
| 139 |
+
m_a, m_b = m[0::2], m[1::2]
|
| 140 |
+
k_a, k_b = k[0::2], k[1::2]
|
| 141 |
+
pair_count = c_a.shape[0]
|
| 142 |
+
pair_idx = jnp.arange(pair_count, dtype=jnp.int32)
|
| 143 |
+
both_struct = k_a * k_b
|
| 144 |
+
pair_base = jnp.maximum(
|
| 145 |
+
jnp.sum((k_a + k_b - k_a * k_b).astype(jnp.int32)),
|
| 146 |
+
jnp.asarray(2, dtype=jnp.int32),
|
| 147 |
+
)
|
| 148 |
+
cnt_a, cnt_b = counts[0::2], counts[1::2]
|
| 149 |
+
depth = tree_depth_count_features(
|
| 150 |
+
cnt_a, cnt_b, n_total, level, feature_n_levels, c.dtype
|
| 151 |
+
)
|
| 152 |
+
counts = cnt_a + cnt_b
|
| 153 |
+
depth_levels.append(depth)
|
| 154 |
+
d_edge = e_curr.shape[-1]
|
| 155 |
+
e_pairs = e_curr.reshape(pair_count, 2, pair_count, 2, d_edge)
|
| 156 |
+
sibling_lr = e_pairs[pair_idx, 0, pair_idx, 1]
|
| 157 |
+
sibling_rl = e_pairs[pair_idx, 1, pair_idx, 0]
|
| 158 |
+
level_active = jnp.any(both_struct.astype(bool))
|
| 159 |
+
g_level = g_curr
|
| 160 |
+
g_level = tagged_dense(
|
| 161 |
+
gladder[2],
|
| 162 |
+
gladder[3],
|
| 163 |
+
g_curr,
|
| 164 |
+
tag_id="gladder.tree.proj",
|
| 165 |
+
pathway="even",
|
| 166 |
+
kfac_structural_mask=level_active,
|
| 167 |
+
kfac_scan_shared=False,
|
| 168 |
+
kfac_repeat_ndim=0,
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
def candidate_one(ca, cb, elr, erl, dep, pidx, struct_active):
|
| 172 |
+
return merge.context_candidate(
|
| 173 |
+
ca,
|
| 174 |
+
cb,
|
| 175 |
+
g_level,
|
| 176 |
+
sibling_edge_lr=elr,
|
| 177 |
+
sibling_edge_rl=erl,
|
| 178 |
+
level_idx=jnp.int32(level),
|
| 179 |
+
pair_idx=pidx,
|
| 180 |
+
pair_base=pair_base,
|
| 181 |
+
clock_depth=clock_depth,
|
| 182 |
+
depth_feats=dep,
|
| 183 |
+
kfac_structural_mask=struct_active,
|
| 184 |
+
kfac_g_structural_mask=level_active,
|
| 185 |
+
kfac_scan_shared=False,
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
candidate = jax.vmap(candidate_one)(
|
| 189 |
+
c_a,
|
| 190 |
+
c_b,
|
| 191 |
+
sibling_lr,
|
| 192 |
+
sibling_rl,
|
| 193 |
+
depth,
|
| 194 |
+
pair_idx,
|
| 195 |
+
both_struct,
|
| 196 |
+
)
|
| 197 |
+
candidates.append(candidate)
|
| 198 |
+
gate_m_a, gate_m_b = k_a, k_b
|
| 199 |
+
gate_both = gate_m_a * gate_m_b
|
| 200 |
+
gate_a = gate_m_a * (1.0 - gate_m_b)
|
| 201 |
+
gate_b = (1.0 - gate_m_a) * gate_m_b
|
| 202 |
+
c = (
|
| 203 |
+
gate_both[:, None] * candidate
|
| 204 |
+
+ gate_a[:, None] * c_a
|
| 205 |
+
+ gate_b[:, None] * c_b
|
| 206 |
+
)
|
| 207 |
+
m = m_a + m_b - m_a * m_b
|
| 208 |
+
k = k_a + k_b - k_a * k_b
|
| 209 |
+
opcode_levels.append(
|
| 210 |
+
jnp.where(
|
| 211 |
+
m_a.astype(jnp.bool_),
|
| 212 |
+
jnp.where(m_b.astype(jnp.bool_), MERGE, CARRY_LEFT),
|
| 213 |
+
jnp.where(m_b.astype(jnp.bool_), CARRY_RIGHT, EMPTY),
|
| 214 |
+
).astype(jnp.uint8)
|
| 215 |
+
)
|
| 216 |
+
attn_mask = both_struct
|
| 217 |
+
d_edge = e_curr.shape[-1]
|
| 218 |
+
e_blocks = e_curr.reshape(pair_count, 2, pair_count, 2, d_edge)
|
| 219 |
+
e00, e01 = e_blocks[:, 0, :, 0], e_blocks[:, 0, :, 1]
|
| 220 |
+
e10, e11 = e_blocks[:, 1, :, 0], e_blocks[:, 1, :, 1]
|
| 221 |
+
|
| 222 |
+
def edge_row(e0, e1, e2, e3, ma, mb, ka, kb, ca, cb):
|
| 223 |
+
return jax.vmap(
|
| 224 |
+
lambda x0, x1, x2, x3, mqa, mqb, kqa, kqb, cqa, cqb: edge_merge_masked(
|
| 225 |
+
x0,
|
| 226 |
+
x1,
|
| 227 |
+
x2,
|
| 228 |
+
x3,
|
| 229 |
+
ma,
|
| 230 |
+
mb,
|
| 231 |
+
mqa,
|
| 232 |
+
mqb,
|
| 233 |
+
ca,
|
| 234 |
+
cb,
|
| 235 |
+
cqa,
|
| 236 |
+
cqb,
|
| 237 |
+
merge.edge_merge,
|
| 238 |
+
k_2i=ka,
|
| 239 |
+
k_2i1=kb,
|
| 240 |
+
k_2j=kqa,
|
| 241 |
+
k_2j1=kqb,
|
| 242 |
+
kfac_scan_shared=False,
|
| 243 |
+
)[0]
|
| 244 |
+
)(e0, e1, e2, e3, m_a, m_b, k_a, k_b, c_a, c_b)
|
| 245 |
+
|
| 246 |
+
e_new = jax.vmap(edge_row)(e00, e01, e10, e11, m_a, m_b, k_a, k_b, c_a, c_b)
|
| 247 |
+
e_new = merge.tree_edge_fwl.apply_residual(
|
| 248 |
+
e_new, c, attn_mask, kfac_scan_shared=False
|
| 249 |
+
)
|
| 250 |
+
edge_keep = (both_struct[:, None] * both_struct[None, :]).astype(bool)
|
| 251 |
+
e_curr = jnp.where(edge_keep[..., None], e_new, e00)
|
| 252 |
+
e_curr = jnp.where(edge_keep[..., None], tree_sphere(e_curr), e00)
|
| 253 |
+
c_skip = c
|
| 254 |
+
c = merge.level_edge_attn(
|
| 255 |
+
c,
|
| 256 |
+
e_curr,
|
| 257 |
+
attn_mask,
|
| 258 |
+
level_idx=jnp.int32(level),
|
| 259 |
+
kfac_scan_shared=False,
|
| 260 |
+
)
|
| 261 |
+
c = jnp.where(attn_mask.astype(bool)[:, None], tree_sphere(c), c_skip)
|
| 262 |
+
carried.append(c)
|
| 263 |
+
level_mask = k
|
| 264 |
+
update_active = jnp.any(attn_mask.astype(bool))
|
| 265 |
+
pool_structural_mask = level_mask.astype(c.dtype) * update_active.astype(
|
| 266 |
+
c.dtype
|
| 267 |
+
)
|
| 268 |
+
pooled = gladder[0](
|
| 269 |
+
g_curr,
|
| 270 |
+
c,
|
| 271 |
+
level_mask.astype(c.dtype),
|
| 272 |
+
kfac_structural_mask=pool_structural_mask,
|
| 273 |
+
kfac_update_mask=update_active,
|
| 274 |
+
kfac_scan_shared=False,
|
| 275 |
+
kfac_repeat_ndim=1,
|
| 276 |
+
)
|
| 277 |
+
g_curr = gladder[1](
|
| 278 |
+
g_curr,
|
| 279 |
+
pooled,
|
| 280 |
+
update_mask=update_active,
|
| 281 |
+
kfac_structural_mask=update_active,
|
| 282 |
+
kfac_scan_shared=False,
|
| 283 |
+
)
|
| 284 |
+
level += 1
|
| 285 |
+
e_root = e_curr[0, 0]
|
| 286 |
+
merge_h = tuple(
|
| 287 |
+
compile_merge_h(
|
| 288 |
+
merge,
|
| 289 |
+
candidate,
|
| 290 |
+
depth_levels[i],
|
| 291 |
+
)
|
| 292 |
+
for i, candidate in enumerate(candidates)
|
| 293 |
+
)
|
| 294 |
+
return {
|
| 295 |
+
"c_candidate": tuple(candidates),
|
| 296 |
+
"c_carried": tuple(carried),
|
| 297 |
+
"depth_features": tuple(depth_levels),
|
| 298 |
+
"merge_h": merge_h,
|
| 299 |
+
"opcodes": tuple(opcode_levels),
|
| 300 |
+
"c_root": c[0],
|
| 301 |
+
"e_root": e_root,
|
| 302 |
+
"g_final": g_curr,
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def project_conditioner(context, hypernet):
|
| 307 |
+
return jnp.matmul(context, hypernet.W_h)
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def leaf_context(leaf_builder, e_leaf, g_emb):
|
| 311 |
+
g_broadcast = jnp.broadcast_to(g_emb, e_leaf.shape[:-1] + g_emb.shape)
|
| 312 |
+
return jnp.concatenate((e_leaf, g_broadcast), axis=-1)
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def compile_target_leaf_h(leaf_builder, e_leaf, g_emb):
|
| 316 |
+
context = leaf_context(leaf_builder, e_leaf, g_emb)
|
| 317 |
+
return (project_conditioner(context, leaf_builder.P_u),)
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def merge_context(merge, c_p_candidate, depth_features):
|
| 321 |
+
return jnp.concatenate(
|
| 322 |
+
(c_p_candidate, depth_features.astype(c_p_candidate.dtype)), axis=-1
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def compile_merge_h(merge, c_p_candidate, depth_features):
|
| 327 |
+
return project_conditioner(
|
| 328 |
+
merge_context(merge, c_p_candidate, depth_features), merge.output_hypernet
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def readout_context(readout, e_root, c_root, g_emb):
|
| 333 |
+
e_norm = readout.ln_e(e_root, pathway="even")
|
| 334 |
+
return jnp.concatenate(
|
| 335 |
+
(e_norm, c_root.astype(e_norm.dtype), g_emb.astype(e_norm.dtype)), axis=-1
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
def compile_target_readout_h(readout, e_root, c_root, g_emb):
|
| 340 |
+
context = readout_context(readout, e_root, c_root, g_emb)
|
| 341 |
+
return (project_conditioner(context, readout.output_hypernet),)
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def classify_merge_opcodes(leaf_real):
|
| 345 |
+
active = jnp.asarray(leaf_real, dtype=jnp.bool_)
|
| 346 |
+
n = active.shape[0]
|
| 347 |
+
if n == 0 or n & (n - 1):
|
| 348 |
+
raise ValueError(f"leaf_real width must be a nonzero power of two, got {n}")
|
| 349 |
+
levels = []
|
| 350 |
+
while active.shape[0] > 1:
|
| 351 |
+
left = active[0::2]
|
| 352 |
+
right = active[1::2]
|
| 353 |
+
opcode = jnp.where(
|
| 354 |
+
left,
|
| 355 |
+
jnp.where(right, MERGE, CARRY_LEFT),
|
| 356 |
+
jnp.where(right, CARRY_RIGHT, EMPTY),
|
| 357 |
+
).astype(jnp.uint8)
|
| 358 |
+
levels.append(opcode)
|
| 359 |
+
active = left | right
|
| 360 |
+
return tuple(levels)
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def assemble_compiled_tree(*, perm, leaf_real, boundaries) -> CompiledTree:
|
| 364 |
+
perm = jnp.asarray(perm, dtype=jnp.int32)
|
| 365 |
+
if perm.ndim != 1:
|
| 366 |
+
raise ValueError(f"perm must be rank one, got shape {perm.shape}")
|
| 367 |
+
leaf_real = jnp.asarray(leaf_real, dtype=jnp.bool_)
|
| 368 |
+
if leaf_real.shape != perm.shape:
|
| 369 |
+
raise ValueError(
|
| 370 |
+
f"leaf_real shape {leaf_real.shape} must match perm {perm.shape}"
|
| 371 |
+
)
|
| 372 |
+
inv_perm = jnp.argsort(perm).astype(jnp.int32)
|
| 373 |
+
return CompiledTree(
|
| 374 |
+
perm=perm,
|
| 375 |
+
inv_perm=inv_perm,
|
| 376 |
+
leaf_real=leaf_real,
|
| 377 |
+
leaf_h=tuple(boundaries["leaf_h"]),
|
| 378 |
+
leaf_combiner_h=tuple(boundaries["leaf_combiner_h"]),
|
| 379 |
+
merge_h=tuple(boundaries["merge_h"]),
|
| 380 |
+
opcodes=tuple(boundaries["opcodes"]),
|
| 381 |
+
readout_h=tuple(boundaries["readout_h"]),
|
| 382 |
+
readout_combiner_h=tuple(boundaries["readout_combiner_h"]),
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def compile_physical_tree_from_reduced_state(
|
| 387 |
+
kernel: PhysicalCompilerKernel,
|
| 388 |
+
*,
|
| 389 |
+
perm,
|
| 390 |
+
leaf_real,
|
| 391 |
+
leaf_h,
|
| 392 |
+
c_reduced,
|
| 393 |
+
edge_reduced,
|
| 394 |
+
real_reduced,
|
| 395 |
+
structural_reduced,
|
| 396 |
+
counts_reduced,
|
| 397 |
+
g_reduced,
|
| 398 |
+
early_merge_h=(),
|
| 399 |
+
early_opcodes=(),
|
| 400 |
+
full_structural_mask=None,
|
| 401 |
+
) -> CompiledTree:
|
| 402 |
+
perm = jnp.asarray(perm, dtype=jnp.int32)
|
| 403 |
+
leaf_real = jnp.asarray(leaf_real)
|
| 404 |
+
if perm.ndim != 1 or leaf_real.shape != perm.shape:
|
| 405 |
+
raise ValueError("perm and leaf_real must be matching rank-one arrays")
|
| 406 |
+
early_merge_h = tuple(early_merge_h)
|
| 407 |
+
early_opcodes = tuple(early_opcodes)
|
| 408 |
+
if len(early_merge_h) != len(early_opcodes):
|
| 409 |
+
raise ValueError("early merge_h/opcode level counts differ")
|
| 410 |
+
if full_structural_mask is None:
|
| 411 |
+
full_structural_mask = leaf_real
|
| 412 |
+
level_offset = len(early_merge_h)
|
| 413 |
+
reduced = compile_context_only_reduction(
|
| 414 |
+
merge=kernel.merge,
|
| 415 |
+
c_leaf=c_reduced,
|
| 416 |
+
leaf_real=real_reduced,
|
| 417 |
+
g_emb=g_reduced,
|
| 418 |
+
edges=edge_reduced,
|
| 419 |
+
structural_mask=structural_reduced,
|
| 420 |
+
n_total=jnp.sum(leaf_real),
|
| 421 |
+
clock_depth=tree_active_clock_depth(jnp.asarray(full_structural_mask)),
|
| 422 |
+
gladder=(
|
| 423 |
+
kernel.tree_pool,
|
| 424 |
+
kernel.tree_update,
|
| 425 |
+
kernel.tree_projection_weight,
|
| 426 |
+
kernel.tree_projection_bias,
|
| 427 |
+
),
|
| 428 |
+
initial_counts=counts_reduced,
|
| 429 |
+
level_offset=level_offset,
|
| 430 |
+
feature_n_levels=tree_active_clock_depth(leaf_real),
|
| 431 |
+
)
|
| 432 |
+
readout_g_emb = _project_global(
|
| 433 |
+
kernel.root_projection,
|
| 434 |
+
reduced["g_final"],
|
| 435 |
+
dense_tag="gladder.root_proj",
|
| 436 |
+
norm_tag="gladder.root_ln",
|
| 437 |
+
)
|
| 438 |
+
boundaries = {
|
| 439 |
+
"leaf_h": tuple(leaf_h),
|
| 440 |
+
"leaf_combiner_h": (),
|
| 441 |
+
"merge_h": early_merge_h + tuple(reduced["merge_h"]),
|
| 442 |
+
"opcodes": early_opcodes + tuple(reduced["opcodes"]),
|
| 443 |
+
"readout_h": compile_target_readout_h(
|
| 444 |
+
kernel.readout,
|
| 445 |
+
reduced["e_root"],
|
| 446 |
+
reduced["c_root"],
|
| 447 |
+
readout_g_emb,
|
| 448 |
+
),
|
| 449 |
+
"readout_combiner_h": (),
|
| 450 |
+
}
|
| 451 |
+
return assemble_compiled_tree(
|
| 452 |
+
perm=perm,
|
| 453 |
+
leaf_real=leaf_real,
|
| 454 |
+
boundaries=boundaries,
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def compile_physical_tree_from_shared_trunk(
|
| 459 |
+
kernel: PhysicalCompilerKernel,
|
| 460 |
+
shared_trunk,
|
| 461 |
+
perm,
|
| 462 |
+
) -> CompiledTree:
|
| 463 |
+
perm = jnp.asarray(perm, dtype=jnp.int32)
|
| 464 |
+
if perm.ndim != 1 or perm.shape != shared_trunk.real_mask.shape:
|
| 465 |
+
raise ValueError("perm must be rank one and match the shared trunk site width")
|
| 466 |
+
node = shared_trunk.node_raw[perm]
|
| 467 |
+
edge = shared_trunk.edge_raw[perm][:, perm]
|
| 468 |
+
leaf_real = shared_trunk.real_mask[perm]
|
| 469 |
+
structural_mask = shared_trunk.balanced_mask
|
| 470 |
+
e_leaf, edge_leaf, g_stream = kernel.contextualizer.with_edge(
|
| 471 |
+
node,
|
| 472 |
+
edge,
|
| 473 |
+
leaf_real,
|
| 474 |
+
structural_mask,
|
| 475 |
+
g=shared_trunk.global_stream,
|
| 476 |
+
)
|
| 477 |
+
g_stream = kernel.global_fork(g_stream, edge_leaf, structural_mask)
|
| 478 |
+
leaf_g_emb = _project_global(
|
| 479 |
+
kernel.leaf_projection,
|
| 480 |
+
g_stream,
|
| 481 |
+
dense_tag="gladder.to_gemb",
|
| 482 |
+
norm_tag="gladder.gemb_ln",
|
| 483 |
+
)
|
| 484 |
+
c_leaf = tree_sphere(kernel.leaf.P_c(e_leaf, pathway="even"))
|
| 485 |
+
reduced = compile_context_only_reduction(
|
| 486 |
+
merge=kernel.merge,
|
| 487 |
+
c_leaf=c_leaf,
|
| 488 |
+
leaf_real=leaf_real,
|
| 489 |
+
g_emb=g_stream,
|
| 490 |
+
edges=edge_leaf,
|
| 491 |
+
structural_mask=structural_mask,
|
| 492 |
+
gladder=(
|
| 493 |
+
kernel.tree_pool,
|
| 494 |
+
kernel.tree_update,
|
| 495 |
+
kernel.tree_projection_weight,
|
| 496 |
+
kernel.tree_projection_bias,
|
| 497 |
+
),
|
| 498 |
+
)
|
| 499 |
+
readout_g_emb = _project_global(
|
| 500 |
+
kernel.root_projection,
|
| 501 |
+
reduced["g_final"],
|
| 502 |
+
dense_tag="gladder.root_proj",
|
| 503 |
+
norm_tag="gladder.root_ln",
|
| 504 |
+
)
|
| 505 |
+
boundaries = {
|
| 506 |
+
"leaf_h": compile_target_leaf_h(
|
| 507 |
+
kernel.leaf,
|
| 508 |
+
e_leaf,
|
| 509 |
+
leaf_g_emb,
|
| 510 |
+
),
|
| 511 |
+
"leaf_combiner_h": (),
|
| 512 |
+
"merge_h": reduced["merge_h"],
|
| 513 |
+
"opcodes": reduced["opcodes"],
|
| 514 |
+
"readout_h": compile_target_readout_h(
|
| 515 |
+
kernel.readout,
|
| 516 |
+
reduced["e_root"],
|
| 517 |
+
reduced["c_root"],
|
| 518 |
+
readout_g_emb,
|
| 519 |
+
),
|
| 520 |
+
"readout_combiner_h": (),
|
| 521 |
+
}
|
| 522 |
+
return assemble_compiled_tree(
|
| 523 |
+
perm=perm,
|
| 524 |
+
leaf_real=leaf_real,
|
| 525 |
+
boundaries=boundaries,
|
| 526 |
+
)
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
def compile_physical_tree_reference(
|
| 530 |
+
model,
|
| 531 |
+
shared_trunk,
|
| 532 |
+
perm,
|
| 533 |
+
) -> CompiledTree:
|
| 534 |
+
if model.gladder_post is None or model.gladder_fork_phys is None:
|
| 535 |
+
raise ValueError(
|
| 536 |
+
"reference physical compiler requires the target global ladder"
|
| 537 |
+
)
|
| 538 |
+
perm = jnp.asarray(perm, dtype=jnp.int32)
|
| 539 |
+
if perm.ndim != 1 or perm.shape != shared_trunk.real_mask.shape:
|
| 540 |
+
raise ValueError("perm must be rank one and match the shared trunk site width")
|
| 541 |
+
node = shared_trunk.node_raw[perm]
|
| 542 |
+
edge = shared_trunk.edge_raw[perm][:, perm]
|
| 543 |
+
leaf_real = shared_trunk.real_mask[perm]
|
| 544 |
+
structural_mask = shared_trunk.balanced_mask
|
| 545 |
+
e_leaf, edge_leaf, g_stream = model._contextualize_leaf_even_with_edge_g(
|
| 546 |
+
node,
|
| 547 |
+
edge,
|
| 548 |
+
leaf_real,
|
| 549 |
+
structural_mask,
|
| 550 |
+
shared_trunk.global_stream,
|
| 551 |
+
)
|
| 552 |
+
g_stream = model.gladder_fork_phys(g_stream, edge_leaf, structural_mask)
|
| 553 |
+
leaf_g_emb = model._gladder_project(g_stream)
|
| 554 |
+
c_leaf = tree_sphere(model.leaf.P_c(e_leaf, pathway="even"))
|
| 555 |
+
reduced = compile_context_only_reduction(
|
| 556 |
+
merge=model.merge,
|
| 557 |
+
c_leaf=c_leaf,
|
| 558 |
+
leaf_real=leaf_real,
|
| 559 |
+
g_emb=g_stream,
|
| 560 |
+
edges=edge_leaf,
|
| 561 |
+
structural_mask=structural_mask,
|
| 562 |
+
gladder=model._gladder_tree_refs(),
|
| 563 |
+
)
|
| 564 |
+
readout_g_emb = model._gladder_root_project(reduced["g_final"])
|
| 565 |
+
boundaries = {
|
| 566 |
+
"leaf_h": compile_target_leaf_h(model.leaf, e_leaf, leaf_g_emb),
|
| 567 |
+
"leaf_combiner_h": (),
|
| 568 |
+
"merge_h": reduced["merge_h"],
|
| 569 |
+
"opcodes": reduced["opcodes"],
|
| 570 |
+
"readout_h": compile_target_readout_h(
|
| 571 |
+
model.readout,
|
| 572 |
+
reduced["e_root"],
|
| 573 |
+
reduced["c_root"],
|
| 574 |
+
readout_g_emb,
|
| 575 |
+
),
|
| 576 |
+
"readout_combiner_h": (),
|
| 577 |
+
}
|
| 578 |
+
return assemble_compiled_tree(
|
| 579 |
+
perm=perm,
|
| 580 |
+
leaf_real=leaf_real,
|
| 581 |
+
boundaries=boundaries,
|
| 582 |
+
)
|
src/hamiltonzero/compiled/trunk.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import equinox as eqx
|
| 7 |
+
import jax
|
| 8 |
+
import jax.numpy as jnp
|
| 9 |
+
|
| 10 |
+
from hamiltonzero.model import tree_sphere
|
| 11 |
+
|
| 12 |
+
from .types import (
|
| 13 |
+
CanonicalHamiltonian,
|
| 14 |
+
FactorizedQSide,
|
| 15 |
+
QSideLeafInput,
|
| 16 |
+
SharedKernel,
|
| 17 |
+
SharedTrunk,
|
| 18 |
+
TrunkCompilerKernel,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _qside(hypernet) -> FactorizedQSide:
|
| 23 |
+
return FactorizedQSide(V=hypernet.V, U=hypernet.U)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def bind_shared_kernel(model) -> SharedKernel:
|
| 27 |
+
return SharedKernel(
|
| 28 |
+
q_to_odd=QSideLeafInput(weight=model.q_to_odd.weight),
|
| 29 |
+
leaf_factors=(_qside(model.leaf.P_u),),
|
| 30 |
+
leaf_combiner_factors=(),
|
| 31 |
+
merge_T=model.merge.T,
|
| 32 |
+
merge_factors=(_qside(model.merge.output_hypernet),),
|
| 33 |
+
readout_factors=(_qside(model.readout.output_hypernet),),
|
| 34 |
+
merge_eps=float(model.merge.eps),
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def bind_trunk_compiler_kernel(model) -> TrunkCompilerKernel:
|
| 39 |
+
return TrunkCompilerKernel(
|
| 40 |
+
featurizer=model.featurizer,
|
| 41 |
+
trunk=model.trunk,
|
| 42 |
+
shared_global=model.gladder_post,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class _TrunkMaskContext(eqx.Module):
|
| 47 |
+
mask: jax.Array
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def compile_canonical_shared_trunk(
|
| 51 |
+
kernel: TrunkCompilerKernel,
|
| 52 |
+
canonical: CanonicalHamiltonian,
|
| 53 |
+
) -> SharedTrunk:
|
| 54 |
+
if canonical.node_mask.ndim != 2 or canonical.node_mask.shape[0] != 1:
|
| 55 |
+
raise ValueError("compiled shared trunk requires exact physical P=1")
|
| 56 |
+
if canonical.balanced_mask.shape != canonical.node_mask.shape:
|
| 57 |
+
raise ValueError("balanced_mask must match node_mask shape")
|
| 58 |
+
graph = canonical.graph_inputs
|
| 59 |
+
if graph.node.shape[:2] != canonical.node_mask.shape:
|
| 60 |
+
raise ValueError("canonical graph node width must match node_mask")
|
| 61 |
+
if graph.edge.shape[:3] != (
|
| 62 |
+
1,
|
| 63 |
+
canonical.node_mask.shape[1],
|
| 64 |
+
canonical.node_mask.shape[1],
|
| 65 |
+
):
|
| 66 |
+
raise ValueError("canonical graph edge width must match node_mask")
|
| 67 |
+
|
| 68 |
+
def one(edge_input, node_input, real_mask, balanced_mask):
|
| 69 |
+
edge_feat, local_feat, global_feat = kernel.featurizer(
|
| 70 |
+
edge_input,
|
| 71 |
+
real_mask,
|
| 72 |
+
node_input,
|
| 73 |
+
)
|
| 74 |
+
g_seed = tree_sphere(global_feat.astype(local_feat.dtype))
|
| 75 |
+
node_raw, edge_raw, g_seed = kernel.trunk(
|
| 76 |
+
_TrunkMaskContext(real_mask),
|
| 77 |
+
edge_feat,
|
| 78 |
+
local_feat,
|
| 79 |
+
g_seed,
|
| 80 |
+
)
|
| 81 |
+
global_stream = kernel.shared_global(
|
| 82 |
+
g_seed.astype(edge_raw.dtype), edge_raw, real_mask
|
| 83 |
+
)
|
| 84 |
+
return SharedTrunk(
|
| 85 |
+
node_raw=node_raw,
|
| 86 |
+
edge_raw=edge_raw,
|
| 87 |
+
global_raw=global_feat,
|
| 88 |
+
global_stream=global_stream,
|
| 89 |
+
real_mask=real_mask,
|
| 90 |
+
balanced_mask=balanced_mask,
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
return jax.vmap(one)(
|
| 94 |
+
graph.edge,
|
| 95 |
+
graph.node,
|
| 96 |
+
canonical.node_mask,
|
| 97 |
+
canonical.balanced_mask,
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def select_single_physical_trunk(trunk: SharedTrunk) -> SharedTrunk:
|
| 102 |
+
leaves = jax.tree_util.tree_leaves(trunk)
|
| 103 |
+
if not leaves or any(x.ndim < 1 or x.shape[0] != 1 for x in leaves):
|
| 104 |
+
raise ValueError("production SharedTrunk must have exact leading P=1")
|
| 105 |
+
return jax.tree_util.tree_map(lambda x: x[0], trunk)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def compile_shared_trunk(model, ctx) -> SharedTrunk:
|
| 109 |
+
edge_feat, local_feat, global_feat = model.featurizer(
|
| 110 |
+
ctx.J_double_prime,
|
| 111 |
+
ctx.mask,
|
| 112 |
+
ctx.h_prime,
|
| 113 |
+
)
|
| 114 |
+
g_seed = tree_sphere(global_feat.astype(local_feat.dtype))
|
| 115 |
+
node_raw, edge_raw, g_seed = model.trunk(
|
| 116 |
+
ctx,
|
| 117 |
+
edge_feat,
|
| 118 |
+
local_feat,
|
| 119 |
+
g_seed,
|
| 120 |
+
)
|
| 121 |
+
global_stream = model._gladder_g_stream(edge_raw, ctx.mask, g_seed)
|
| 122 |
+
return SharedTrunk(
|
| 123 |
+
node_raw=node_raw,
|
| 124 |
+
edge_raw=edge_raw,
|
| 125 |
+
global_raw=global_feat,
|
| 126 |
+
global_stream=global_stream,
|
| 127 |
+
real_mask=ctx.mask,
|
| 128 |
+
balanced_mask=ctx.bmask,
|
| 129 |
+
)
|
src/hamiltonzero/compiled/types.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
from enum import IntEnum
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import equinox as eqx
|
| 10 |
+
import jax
|
| 11 |
+
import jax.numpy as jnp
|
| 12 |
+
from jaxtyping import Array
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class MergeOpcode(IntEnum):
|
| 16 |
+
MERGE = 0
|
| 17 |
+
CARRY_LEFT = 1
|
| 18 |
+
CARRY_RIGHT = 2
|
| 19 |
+
EMPTY = 3
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
MERGE = int(MergeOpcode.MERGE)
|
| 23 |
+
CARRY_LEFT = int(MergeOpcode.CARRY_LEFT)
|
| 24 |
+
CARRY_RIGHT = int(MergeOpcode.CARRY_RIGHT)
|
| 25 |
+
EMPTY = int(MergeOpcode.EMPTY)
|
| 26 |
+
OPCODE_DTYPE = jnp.uint8
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class QSideLeafInput(eqx.Module):
|
| 30 |
+
weight: Array
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class FactorizedQSide(eqx.Module):
|
| 34 |
+
V: Array
|
| 35 |
+
U: Array
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class SharedKernel(eqx.Module):
|
| 39 |
+
q_to_odd: QSideLeafInput
|
| 40 |
+
leaf_factors: tuple[FactorizedQSide, ...]
|
| 41 |
+
leaf_combiner_factors: tuple[FactorizedQSide, ...]
|
| 42 |
+
merge_T: Array
|
| 43 |
+
merge_factors: tuple[FactorizedQSide, ...]
|
| 44 |
+
readout_factors: tuple[FactorizedQSide, ...]
|
| 45 |
+
merge_eps: float = eqx.field(static=True)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class TrunkCompilerKernel(eqx.Module):
|
| 49 |
+
featurizer: Any
|
| 50 |
+
trunk: Any
|
| 51 |
+
shared_global: Any
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class LadderProjectionKernel(eqx.Module):
|
| 55 |
+
weight: Array
|
| 56 |
+
bias: Array
|
| 57 |
+
norm_scale: Array
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class PhysicalCompilerKernel(eqx.Module):
|
| 61 |
+
contextualizer: Any
|
| 62 |
+
global_fork: Any
|
| 63 |
+
leaf: Any
|
| 64 |
+
merge: Any
|
| 65 |
+
readout: Any
|
| 66 |
+
leaf_projection: LadderProjectionKernel
|
| 67 |
+
tree_pool: Any
|
| 68 |
+
tree_update: Any
|
| 69 |
+
tree_projection_weight: Array
|
| 70 |
+
tree_projection_bias: Array
|
| 71 |
+
root_projection: LadderProjectionKernel
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class ModelHamiltonianArrays(eqx.Module):
|
| 75 |
+
coupling: Array
|
| 76 |
+
full_coupling: Array
|
| 77 |
+
field: Array
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class GraphInputs(eqx.Module):
|
| 81 |
+
node: Array
|
| 82 |
+
edge: Array
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class QuotientInputs(eqx.Module):
|
| 86 |
+
node_key: Array
|
| 87 |
+
edge_key: Array
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class EnergyInputs(eqx.Module):
|
| 91 |
+
custom_lap_J_eff: Array
|
| 92 |
+
custom_lap_radial_const: Array
|
| 93 |
+
one_body_fields: tuple[Array, ...]
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class EnergyMasks(eqx.Module):
|
| 97 |
+
real: Array
|
| 98 |
+
balanced: Array
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
class EnergyFrame(eqx.Module):
|
| 102 |
+
custom_lap_J_eff: Array
|
| 103 |
+
w_levels: tuple[Array, ...]
|
| 104 |
+
custom_lap_radial_const: Array
|
| 105 |
+
one_body_fields: tuple[Array, ...]
|
| 106 |
+
masks: EnergyMasks
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
EnergyFrameBatch = EnergyFrame
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class CanonicalHamiltonian(eqx.Module):
|
| 113 |
+
model_coupling_fields: ModelHamiltonianArrays
|
| 114 |
+
node_mask: Array
|
| 115 |
+
balanced_mask: Array
|
| 116 |
+
graph_inputs: GraphInputs
|
| 117 |
+
quotient_inputs: QuotientInputs
|
| 118 |
+
energy_inputs: EnergyInputs
|
| 119 |
+
system_identity: Array
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class SharedTrunk(eqx.Module):
|
| 123 |
+
node_raw: Array
|
| 124 |
+
edge_raw: Array
|
| 125 |
+
global_raw: Array
|
| 126 |
+
global_stream: Array
|
| 127 |
+
real_mask: Array
|
| 128 |
+
balanced_mask: Array
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
class CompiledTree(eqx.Module):
|
| 132 |
+
perm: Array
|
| 133 |
+
inv_perm: Array
|
| 134 |
+
leaf_real: Array
|
| 135 |
+
leaf_h: tuple[Array, ...]
|
| 136 |
+
leaf_combiner_h: tuple[Array, ...]
|
| 137 |
+
merge_h: tuple[Array, ...]
|
| 138 |
+
opcodes: tuple[Array, ...]
|
| 139 |
+
readout_h: tuple[Array, ...]
|
| 140 |
+
readout_combiner_h: tuple[Array, ...]
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
CompiledTreeBatch = CompiledTree
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
class CompiledWaveFunction(eqx.Module):
|
| 147 |
+
kernel: SharedKernel
|
| 148 |
+
tree: CompiledTree
|
| 149 |
+
|
| 150 |
+
def __call__(self, q_routed, _ctx=None, _t=0.0):
|
| 151 |
+
from .execute import execute_wavefunction
|
| 152 |
+
|
| 153 |
+
return execute_wavefunction(self.kernel, self.tree, q_routed)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class CompiledWaveFunctions(eqx.Module):
|
| 157 |
+
kernel: SharedKernel
|
| 158 |
+
trees: CompiledTreeBatch
|
| 159 |
+
|
| 160 |
+
def __call__(self, q_routed, _ctx=None, _t=0.0):
|
| 161 |
+
from .execute import execute_wavefunction
|
| 162 |
+
|
| 163 |
+
return execute_wavefunction(self.kernel, self.trees, q_routed)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def level_widths(n_sites: int) -> tuple[int, ...]:
|
| 167 |
+
if n_sites <= 0 or n_sites & (n_sites - 1):
|
| 168 |
+
raise ValueError(f"n_sites must be a positive power of two, got {n_sites}")
|
| 169 |
+
return tuple(n_sites >> level for level in range(1, n_sites.bit_length()))
|
src/hamiltonzero/config.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import dataclasses
|
| 7 |
+
import json
|
| 8 |
+
from dataclasses import dataclass, field
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Any, Literal, TypeVar
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
AttentionImplementation = Literal["tuned", "einsum"]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass(frozen=True, slots=True)
|
| 17 |
+
class ModelConfig:
|
| 18 |
+
d_e: int = 512
|
| 19 |
+
d_o: int = 32
|
| 20 |
+
d_c: int = 512
|
| 21 |
+
d_r: int = 32
|
| 22 |
+
n_heads: int = 16
|
| 23 |
+
n_layers: int = 8
|
| 24 |
+
rank: int = 32
|
| 25 |
+
edge_channels: int = 96
|
| 26 |
+
attention_qk_dim: int = 256
|
| 27 |
+
attention_v_dim: int = 256
|
| 28 |
+
merge_dim: int = 1536
|
| 29 |
+
trunk_edge_node_context_dim: int = 256
|
| 30 |
+
trunk_edge_hidden_dim: int = 128
|
| 31 |
+
trunk_attention_bias_hidden_dim: int = 64
|
| 32 |
+
trunk_ffn_hidden_dim: int = 2048
|
| 33 |
+
trunk_two_hop_hidden_dim: int = 128
|
| 34 |
+
tree_edge_node_context_dim: int = 256
|
| 35 |
+
global_dim: int = 256
|
| 36 |
+
merge_hypernet_rank: int = 256
|
| 37 |
+
featurizer_bond_dim: int = 128
|
| 38 |
+
featurizer_heads: int = 8
|
| 39 |
+
featurizer_head_dim: int = 64
|
| 40 |
+
featurizer_global_queries: int = 4
|
| 41 |
+
featurizer_edge_hidden_dim: int = 192
|
| 42 |
+
featurizer_zeeman_hidden_dim: int = 2048
|
| 43 |
+
featurizer_global_hidden_dim: int = 16384
|
| 44 |
+
featurizer_combine_hidden_dim: int = 12288
|
| 45 |
+
featurizer_token_initial_scale: float = 0.02
|
| 46 |
+
polar_group_norm_tau: float = 0.001
|
| 47 |
+
polar_bond_hidden_dim: int = 256
|
| 48 |
+
polar_bond_groups: int = 16
|
| 49 |
+
polar_bond_group_dim: int = 16
|
| 50 |
+
polar_zeeman_groups: int = 16
|
| 51 |
+
polar_zeeman_group_dim: int = 16
|
| 52 |
+
router_max_n: int = 128
|
| 53 |
+
router_model_dim: int = 512
|
| 54 |
+
router_heads: int = 16
|
| 55 |
+
router_attention_dim: int = 128
|
| 56 |
+
router_score_dim: int = 512
|
| 57 |
+
router_candidate_dim: int = 1024
|
| 58 |
+
router_summary_dim: int = 1024
|
| 59 |
+
router_ffn_dim: int = 1024
|
| 60 |
+
router_score_initial_scale: float = 0.0
|
| 61 |
+
router_rope_base: float = 10000.0
|
| 62 |
+
router_rope_scaling: float = 1.0
|
| 63 |
+
router_tree_prefix_layers: int = 4
|
| 64 |
+
router_tree_candidate_layers: int = 2
|
| 65 |
+
router_tree_merge_dim: int = 1024
|
| 66 |
+
router_tree_post_layers: int = 4
|
| 67 |
+
router_context_layers: int = 2
|
| 68 |
+
router_context_heads: int = 4
|
| 69 |
+
router_context_attention_dim: int = 256
|
| 70 |
+
router_context_edge_node_dim: int = 256
|
| 71 |
+
level_edge_heads: int = 8
|
| 72 |
+
level_edge_mlp_dim: int = 384
|
| 73 |
+
level_edge_mlp_blocks: int = 3
|
| 74 |
+
level_edge_ffn_dim: int = 1024
|
| 75 |
+
level_edge_rope_base: float = 10000.0
|
| 76 |
+
level_edge_rope_scaling: float = 1.0
|
| 77 |
+
root_readout_edge_rank: int = 128
|
| 78 |
+
ngpt_alpha_initial: float = 0.25
|
| 79 |
+
ngpt_alpha_initial_fraction: float = 0.25
|
| 80 |
+
ngpt_alpha_maximum: float = 0.8
|
| 81 |
+
global_ladder_tap_dim: int = 256
|
| 82 |
+
level_edge_bias_mlp_dim: int = 128
|
| 83 |
+
level_edge_bias_mlp_blocks: int = 1
|
| 84 |
+
merge_context_mlp_dim: int = 1024
|
| 85 |
+
readout_context_layers: int = 2
|
| 86 |
+
readout_context_heads: int = 8
|
| 87 |
+
readout_context_attention_dim: int = 128
|
| 88 |
+
readout_context_edge_node_dim: int = 256
|
| 89 |
+
readout_context_summary_dim: int = 1024
|
| 90 |
+
readout_context_mlp_dim: int = 2048
|
| 91 |
+
readout_context_bias_dim: int = 32
|
| 92 |
+
readout_context_edge_ffn_dim: int = 384
|
| 93 |
+
readout_context_rope_base: float = 10000.0
|
| 94 |
+
readout_context_rope_scaling: float = 1.0
|
| 95 |
+
two_hop_channels: int = 64
|
| 96 |
+
tree_fwl_channels: int = 128
|
| 97 |
+
attention: AttentionImplementation = "tuned"
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@dataclass(frozen=True, slots=True)
|
| 101 |
+
class MCMCConfig:
|
| 102 |
+
batch_size: int = 512
|
| 103 |
+
replicas: int = 8
|
| 104 |
+
steps: int = 32
|
| 105 |
+
burn_in: int = 256
|
| 106 |
+
burn_in_replica_steps: int = 2
|
| 107 |
+
walker_chunk_size: int | None = None
|
| 108 |
+
initial_sigma: float = 0.3
|
| 109 |
+
initial_haar_sites: int = 1
|
| 110 |
+
sigma_scale: float = 1.1
|
| 111 |
+
langevin_target_acceptance: float = 0.574
|
| 112 |
+
haar_target_acceptance: float = 0.234
|
| 113 |
+
beta_history_weight: float = 0.9
|
| 114 |
+
adapt_every: int = 1
|
| 115 |
+
reuse_mcmc: Path | None = None
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
@dataclass(frozen=True, slots=True)
|
| 119 |
+
class KFACConfig:
|
| 120 |
+
learning_rate_numerator: float = 0.05
|
| 121 |
+
learning_rate_offset: float = 5.0
|
| 122 |
+
learning_rate_decay_steps: float = 5000.0
|
| 123 |
+
curvature_ema: float = 0.995
|
| 124 |
+
curvature_update_period: int = 2
|
| 125 |
+
inverse_update_period: int = 2
|
| 126 |
+
damping: float = 0.001
|
| 127 |
+
minimum_damping: float = 0.0001
|
| 128 |
+
norm_constraint: float = 0.001
|
| 129 |
+
mad_clip_width: float = 5.0
|
| 130 |
+
momentum: float = 0.0
|
| 131 |
+
l2_regularization: float = 0.0
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
@dataclass(frozen=True, slots=True)
|
| 135 |
+
class RouterConfig:
|
| 136 |
+
temperature: float = 1.0
|
| 137 |
+
loss_weight: float = 1.0
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@dataclass(frozen=True, slots=True)
|
| 141 |
+
class EnergyConfig:
|
| 142 |
+
mu: float | None = None
|
| 143 |
+
eps: float = 0.1
|
| 144 |
+
chunk_size: int = 512
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
@dataclass(frozen=True, slots=True)
|
| 148 |
+
class TrainConfig:
|
| 149 |
+
systems: Path
|
| 150 |
+
output: Path
|
| 151 |
+
steps: int
|
| 152 |
+
seed: int = 777
|
| 153 |
+
n_max: int = 64
|
| 154 |
+
model: ModelConfig = field(default_factory=ModelConfig)
|
| 155 |
+
router: RouterConfig = field(default_factory=RouterConfig)
|
| 156 |
+
mcmc: MCMCConfig = field(default_factory=MCMCConfig)
|
| 157 |
+
kfac: KFACConfig = field(default_factory=KFACConfig)
|
| 158 |
+
energy: EnergyConfig = field(default_factory=EnergyConfig)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _finetune_mcmc() -> MCMCConfig:
|
| 162 |
+
return MCMCConfig(batch_size=256, replicas=8, steps=2, burn_in=256)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def _finetune_kfac() -> KFACConfig:
|
| 166 |
+
return KFACConfig(
|
| 167 |
+
learning_rate_numerator=0.002,
|
| 168 |
+
learning_rate_offset=1.0,
|
| 169 |
+
learning_rate_decay_steps=10000.0,
|
| 170 |
+
curvature_ema=0.99,
|
| 171 |
+
curvature_update_period=2,
|
| 172 |
+
inverse_update_period=4,
|
| 173 |
+
damping=0.001,
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def _finetune_energy() -> EnergyConfig:
|
| 178 |
+
return EnergyConfig(mu=2.86)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
@dataclass(frozen=True, slots=True)
|
| 182 |
+
class FineTuneConfig:
|
| 183 |
+
system: Path
|
| 184 |
+
checkpoint: Path
|
| 185 |
+
output: Path
|
| 186 |
+
steps: int = 10000
|
| 187 |
+
seed: int = 777
|
| 188 |
+
leaf_rank: int = 1536
|
| 189 |
+
merge_rank: int = 1024
|
| 190 |
+
route_temperature: float = 1.0
|
| 191 |
+
model: ModelConfig = field(default_factory=lambda: ModelConfig(attention="einsum"))
|
| 192 |
+
mcmc: MCMCConfig = field(default_factory=_finetune_mcmc)
|
| 193 |
+
kfac: KFACConfig = field(default_factory=_finetune_kfac)
|
| 194 |
+
energy: EnergyConfig = field(default_factory=_finetune_energy)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def _eval_mcmc() -> EvalMCMCConfig:
|
| 198 |
+
return EvalMCMCConfig(
|
| 199 |
+
batch_size=256,
|
| 200 |
+
replicas=8,
|
| 201 |
+
steps=24,
|
| 202 |
+
burn_in=1024,
|
| 203 |
+
walker_chunk_size=16,
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
@dataclass(frozen=True, slots=True)
|
| 208 |
+
class EvalMCMCConfig:
|
| 209 |
+
batch_size: int = 256
|
| 210 |
+
replicas: int = 8
|
| 211 |
+
steps: int = 24
|
| 212 |
+
burn_in: int = 1024
|
| 213 |
+
burn_in_replica_steps: int = 2
|
| 214 |
+
walker_chunk_size: int = 16
|
| 215 |
+
initial_sigma: float = 0.3
|
| 216 |
+
initial_haar_sites: int = 1
|
| 217 |
+
sigma_scale: float = 1.1
|
| 218 |
+
langevin_target_acceptance: float = 0.574
|
| 219 |
+
haar_target_acceptance: float = 0.234
|
| 220 |
+
beta_history_weight: float = 0.9
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
@dataclass(frozen=True, slots=True)
|
| 224 |
+
class EvalConfig:
|
| 225 |
+
system: Path
|
| 226 |
+
checkpoint: Path
|
| 227 |
+
output: Path
|
| 228 |
+
seed: int = 777
|
| 229 |
+
contest: bool = False
|
| 230 |
+
large_n: bool = False
|
| 231 |
+
measurements: int = 256
|
| 232 |
+
contest_candidates: int = 8
|
| 233 |
+
contest_beam_width: int = 8
|
| 234 |
+
contest_preburn: int = 128
|
| 235 |
+
contest_measurements: int = 128
|
| 236 |
+
contest_se_multiplier: float = 2.0
|
| 237 |
+
route_temperature: float = 4.0
|
| 238 |
+
large_n_sequence_shards: int = 0
|
| 239 |
+
large_n_pair_tile_size: int = 128
|
| 240 |
+
contextualizer_attention: AttentionImplementation | None = None
|
| 241 |
+
model: ModelConfig = field(default_factory=ModelConfig)
|
| 242 |
+
mcmc: EvalMCMCConfig = field(default_factory=_eval_mcmc)
|
| 243 |
+
energy: EnergyConfig = field(default_factory=EnergyConfig)
|
| 244 |
+
|
| 245 |
+
def __post_init__(self) -> None:
|
| 246 |
+
if self.contest and self.large_n:
|
| 247 |
+
raise ValueError("contest and large_n are mutually exclusive")
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
Config = TrainConfig | FineTuneConfig | EvalConfig
|
| 251 |
+
T = TypeVar("T")
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def _coerce(cls: type[T], values: dict[str, Any]) -> T:
|
| 255 |
+
nested = {
|
| 256 |
+
"model": ModelConfig,
|
| 257 |
+
"router": RouterConfig,
|
| 258 |
+
"mcmc": MCMCConfig,
|
| 259 |
+
"kfac": KFACConfig,
|
| 260 |
+
"energy": EnergyConfig,
|
| 261 |
+
}
|
| 262 |
+
if cls is EvalConfig:
|
| 263 |
+
nested["mcmc"] = EvalMCMCConfig
|
| 264 |
+
data = dict(values)
|
| 265 |
+
fields_by_name = {item.name: item for item in dataclasses.fields(cls)}
|
| 266 |
+
for name, nested_cls in nested.items():
|
| 267 |
+
if name in data and isinstance(data[name], dict):
|
| 268 |
+
item = fields_by_name.get(name)
|
| 269 |
+
defaults: dict[str, Any] = {}
|
| 270 |
+
if item is not None and item.default_factory is not dataclasses.MISSING:
|
| 271 |
+
defaults = dataclasses.asdict(item.default_factory())
|
| 272 |
+
nested_values = {**defaults, **data[name]}
|
| 273 |
+
if name == "mcmc" and nested_values.get("reuse_mcmc") is not None:
|
| 274 |
+
nested_values["reuse_mcmc"] = Path(nested_values["reuse_mcmc"])
|
| 275 |
+
data[name] = nested_cls(**nested_values)
|
| 276 |
+
path_fields = {"systems", "system", "checkpoint", "output"}
|
| 277 |
+
for item in dataclasses.fields(cls):
|
| 278 |
+
if item.name in path_fields and item.name in data:
|
| 279 |
+
data[item.name] = Path(data[item.name])
|
| 280 |
+
return cls(**data)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def load_config(path: str | Path, mode: Literal["train", "finetune", "eval"]) -> Config:
|
| 284 |
+
values = json.loads(Path(path).read_text())
|
| 285 |
+
cls = {"train": TrainConfig, "finetune": FineTuneConfig, "eval": EvalConfig}[mode]
|
| 286 |
+
return _coerce(cls, values)
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
__all__ = [
|
| 290 |
+
"AttentionImplementation",
|
| 291 |
+
"EnergyConfig",
|
| 292 |
+
"EvalConfig",
|
| 293 |
+
"EvalMCMCConfig",
|
| 294 |
+
"FineTuneConfig",
|
| 295 |
+
"KFACConfig",
|
| 296 |
+
"MCMCConfig",
|
| 297 |
+
"ModelConfig",
|
| 298 |
+
"RouterConfig",
|
| 299 |
+
"TrainConfig",
|
| 300 |
+
"load_config",
|
| 301 |
+
]
|
src/hamiltonzero/data/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from .systems import (
|
| 5 |
+
build_context,
|
| 6 |
+
build_context_and_energy,
|
| 7 |
+
build_multi_context,
|
| 8 |
+
load_system,
|
| 9 |
+
load_systems,
|
| 10 |
+
padded_model_arrays,
|
| 11 |
+
save_system,
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
__all__ = [
|
| 15 |
+
"build_context",
|
| 16 |
+
"build_context_and_energy",
|
| 17 |
+
"build_multi_context",
|
| 18 |
+
"load_system",
|
| 19 |
+
"load_systems",
|
| 20 |
+
"padded_model_arrays",
|
| 21 |
+
"save_system",
|
| 22 |
+
]
|
src/hamiltonzero/data/systems.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
from dataclasses import replace
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any, Iterable
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
from hamiltonzero.hamiltonian import SpinHamiltonian, _exchange_matrix
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _load_payload(path: str | Path) -> dict[str, Any]:
|
| 17 |
+
source = Path(path)
|
| 18 |
+
if source.suffix == ".jsonl":
|
| 19 |
+
records = []
|
| 20 |
+
with source.open(encoding="utf-8") as stream:
|
| 21 |
+
for line_number, line in enumerate(stream, 1):
|
| 22 |
+
if not line.strip():
|
| 23 |
+
continue
|
| 24 |
+
record = json.loads(line)
|
| 25 |
+
if not isinstance(record, dict):
|
| 26 |
+
raise ValueError(f"JSONL record {line_number} must be an object")
|
| 27 |
+
records.append(record)
|
| 28 |
+
return {"systems": records}
|
| 29 |
+
payload = json.loads(source.read_text())
|
| 30 |
+
if not isinstance(payload, dict):
|
| 31 |
+
raise ValueError("Hamiltonian dataset must be a JSON object")
|
| 32 |
+
return payload
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _from_sparse(spec: dict[str, Any]) -> SpinHamiltonian:
|
| 36 |
+
n_sites = int(spec.get("n_sites", spec.get("n_spins", 0)))
|
| 37 |
+
if n_sites <= 0:
|
| 38 |
+
raise ValueError("sparse Hamiltonian requires a positive n_sites")
|
| 39 |
+
exchange = np.zeros((n_sites, n_sites, 3, 3), dtype=np.float32)
|
| 40 |
+
coupling = np.zeros((n_sites, n_sites), dtype=np.float32)
|
| 41 |
+
seen: set[tuple[int, int]] = set()
|
| 42 |
+
for offset, term in enumerate(spec["exchange"]):
|
| 43 |
+
if not isinstance(term, list) or len(term) != 3:
|
| 44 |
+
raise ValueError(f"exchange term {offset} must be [i,j,J]")
|
| 45 |
+
left, right, value = term
|
| 46 |
+
if (
|
| 47 |
+
not isinstance(left, int)
|
| 48 |
+
or isinstance(left, bool)
|
| 49 |
+
or not isinstance(right, int)
|
| 50 |
+
or isinstance(right, bool)
|
| 51 |
+
or not 0 <= left < right < n_sites
|
| 52 |
+
):
|
| 53 |
+
raise ValueError(
|
| 54 |
+
f"exchange term {offset} must satisfy 0 <= i < j < n_sites"
|
| 55 |
+
)
|
| 56 |
+
pair = (left, right)
|
| 57 |
+
if pair in seen:
|
| 58 |
+
raise ValueError(f"duplicate exchange term for sites {pair}")
|
| 59 |
+
seen.add(pair)
|
| 60 |
+
matrix = _exchange_matrix(value)
|
| 61 |
+
exchange[left, right] = matrix
|
| 62 |
+
exchange[right, left] = matrix.T
|
| 63 |
+
coupling[left, right] = coupling[right, left] = 1.0
|
| 64 |
+
field = spec.get("field", spec.get("h", spec.get("h_field", 0.0)))
|
| 65 |
+
return SpinHamiltonian.from_arrays(
|
| 66 |
+
exchange,
|
| 67 |
+
field,
|
| 68 |
+
coupling=coupling,
|
| 69 |
+
nodes=spec.get("nodes"),
|
| 70 |
+
mu=spec.get("mu"),
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _next_power_of_two(value: int) -> int:
|
| 75 |
+
return 1 if value <= 1 else 1 << (value - 1).bit_length()
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _from_record(
|
| 79 |
+
record: dict[str, Any],
|
| 80 |
+
*,
|
| 81 |
+
needs_fwl2: bool | None = None,
|
| 82 |
+
) -> SpinHamiltonian:
|
| 83 |
+
outer = record
|
| 84 |
+
spec = record.get("spec", record)
|
| 85 |
+
convention = spec.get("convention", "textbook")
|
| 86 |
+
if convention != "textbook":
|
| 87 |
+
raise ValueError("public Hamiltonian JSON must use convention='textbook'")
|
| 88 |
+
if "exchange" in spec:
|
| 89 |
+
system = _from_sparse(spec)
|
| 90 |
+
else:
|
| 91 |
+
system = SpinHamiltonian.from_arrays(
|
| 92 |
+
spec["J"],
|
| 93 |
+
spec.get("h", spec.get("h_field", 0.0)),
|
| 94 |
+
coupling=spec.get("coupling"),
|
| 95 |
+
nodes=spec.get("nodes"),
|
| 96 |
+
mu=spec.get("mu"),
|
| 97 |
+
)
|
| 98 |
+
if needs_fwl2 is None:
|
| 99 |
+
needs_fwl2 = outer.get("needs_fwl2", spec.get("needs_fwl2"))
|
| 100 |
+
metadata = {
|
| 101 |
+
name: outer.get(name, spec.get(name))
|
| 102 |
+
for name in ("category", "tag", "topology_class", "j_class")
|
| 103 |
+
}
|
| 104 |
+
return replace(
|
| 105 |
+
system,
|
| 106 |
+
_needs_fwl2=needs_fwl2,
|
| 107 |
+
_category=metadata["category"],
|
| 108 |
+
_tag=metadata["tag"],
|
| 109 |
+
_topology_class=metadata["topology_class"],
|
| 110 |
+
_j_class=metadata["j_class"],
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def load_system(path: str | Path) -> SpinHamiltonian:
|
| 115 |
+
payload = _load_payload(path)
|
| 116 |
+
if "systems" in payload:
|
| 117 |
+
systems = payload["systems"]
|
| 118 |
+
if len(systems) != 1:
|
| 119 |
+
raise ValueError("load_system requires exactly one system")
|
| 120 |
+
dispatch = payload.get("needs_fwl2", payload.get("dataset_needs_fwl2"))
|
| 121 |
+
if dispatch is not None:
|
| 122 |
+
if not isinstance(dispatch, list) or len(dispatch) != 1:
|
| 123 |
+
raise ValueError("needs_fwl2 sidecar must align with systems")
|
| 124 |
+
return _from_record(systems[0], needs_fwl2=bool(dispatch[0]))
|
| 125 |
+
return _from_record(systems[0])
|
| 126 |
+
return _from_record(payload)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def load_systems(path: str | Path) -> list[SpinHamiltonian]:
|
| 130 |
+
payload = _load_payload(path)
|
| 131 |
+
records = payload.get("systems", [payload])
|
| 132 |
+
dispatch = (
|
| 133 |
+
payload.get("needs_fwl2", payload.get("dataset_needs_fwl2"))
|
| 134 |
+
if "systems" in payload
|
| 135 |
+
else None
|
| 136 |
+
)
|
| 137 |
+
if dispatch is None and isinstance(payload.get("per_system"), list):
|
| 138 |
+
derived = payload["per_system"]
|
| 139 |
+
if len(derived) == len(records) and all(
|
| 140 |
+
isinstance(value, dict) and "needs_fwl2" in value for value in derived
|
| 141 |
+
):
|
| 142 |
+
dispatch = [value["needs_fwl2"] for value in derived]
|
| 143 |
+
if dispatch is not None:
|
| 144 |
+
if not isinstance(dispatch, list) or len(dispatch) != len(records):
|
| 145 |
+
raise ValueError("needs_fwl2 sidecar must align with systems")
|
| 146 |
+
return [
|
| 147 |
+
_from_record(record, needs_fwl2=bool(value))
|
| 148 |
+
for record, value in zip(records, dispatch, strict=True)
|
| 149 |
+
]
|
| 150 |
+
return [_from_record(record) for record in records]
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def save_system(path: str | Path, system: SpinHamiltonian) -> None:
|
| 154 |
+
destination = Path(path)
|
| 155 |
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
| 156 |
+
destination.write_text(json.dumps(system.to_dict(), indent=2) + "\n")
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def padded_model_arrays(
|
| 160 |
+
system: SpinHamiltonian,
|
| 161 |
+
n_max: int | None = None,
|
| 162 |
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
| 163 |
+
width = _next_power_of_two(system.n_spins) if n_max is None else int(n_max)
|
| 164 |
+
if width < system.n_spins:
|
| 165 |
+
raise ValueError("n_max cannot be smaller than the system")
|
| 166 |
+
if width <= 0 or width & (width - 1):
|
| 167 |
+
raise ValueError("n_max must be a positive power of two")
|
| 168 |
+
coupling, exchange, field = system.model_arrays()
|
| 169 |
+
padding = width - system.n_spins
|
| 170 |
+
coupling = np.pad(coupling, ((0, padding), (0, padding)))
|
| 171 |
+
exchange = np.pad(exchange, ((0, padding), (0, padding), (0, 0), (0, 0)))
|
| 172 |
+
field = np.pad(field, ((0, padding), (0, 0)))
|
| 173 |
+
mask = np.zeros((width,), dtype=np.int32)
|
| 174 |
+
mask[: system.n_spins] = 1
|
| 175 |
+
return coupling, exchange, field, mask
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def _context_arrays(system: SpinHamiltonian, n_max: int | None):
|
| 179 |
+
import jax.numpy as jnp
|
| 180 |
+
|
| 181 |
+
from hamiltonzero.model.route_quotient import system_needs_fwl2
|
| 182 |
+
|
| 183 |
+
_coupling, exchange, field, mask = padded_model_arrays(system, n_max)
|
| 184 |
+
needs_fwl2 = system._needs_fwl2
|
| 185 |
+
if needs_fwl2 is None:
|
| 186 |
+
_, physical_exchange, physical_field = system.model_arrays()
|
| 187 |
+
needs_fwl2 = system_needs_fwl2(
|
| 188 |
+
physical_exchange,
|
| 189 |
+
physical_field,
|
| 190 |
+
system.n_spins,
|
| 191 |
+
category=system._category,
|
| 192 |
+
tag=system._tag,
|
| 193 |
+
topology_class=system._topology_class,
|
| 194 |
+
j_class=system._j_class,
|
| 195 |
+
)
|
| 196 |
+
return (
|
| 197 |
+
jnp.asarray(exchange),
|
| 198 |
+
jnp.asarray(field),
|
| 199 |
+
jnp.asarray(mask),
|
| 200 |
+
needs_fwl2,
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def build_context(
|
| 205 |
+
system: SpinHamiltonian,
|
| 206 |
+
n_max: int | None = None,
|
| 207 |
+
):
|
| 208 |
+
from hamiltonzero.model import SpinContext
|
| 209 |
+
|
| 210 |
+
exchange, field, mask, needs_fwl2 = _context_arrays(system, n_max)
|
| 211 |
+
return SpinContext(
|
| 212 |
+
J_full=exchange,
|
| 213 |
+
h=field,
|
| 214 |
+
mask=mask,
|
| 215 |
+
needs_fwl2=needs_fwl2,
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def build_context_and_energy(
|
| 220 |
+
system: SpinHamiltonian,
|
| 221 |
+
n_max: int | None = None,
|
| 222 |
+
mu: float | None = None,
|
| 223 |
+
eps: float = 0.1,
|
| 224 |
+
):
|
| 225 |
+
from hamiltonzero.energy.frame import build_energy_inputs
|
| 226 |
+
from hamiltonzero.model import SpinContext
|
| 227 |
+
|
| 228 |
+
exchange, field, mask, needs_fwl2 = _context_arrays(system, n_max)
|
| 229 |
+
context = SpinContext(
|
| 230 |
+
J_full=exchange,
|
| 231 |
+
h=field,
|
| 232 |
+
mask=mask,
|
| 233 |
+
needs_fwl2=needs_fwl2,
|
| 234 |
+
)
|
| 235 |
+
energy_inputs = build_energy_inputs(
|
| 236 |
+
exchange,
|
| 237 |
+
field,
|
| 238 |
+
mask,
|
| 239 |
+
system.mu if mu is None else mu,
|
| 240 |
+
eps,
|
| 241 |
+
)
|
| 242 |
+
return context, energy_inputs
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def build_multi_context(
|
| 246 |
+
systems: Iterable[SpinHamiltonian],
|
| 247 |
+
n_max: int,
|
| 248 |
+
):
|
| 249 |
+
from hamiltonzero.model import MultiSystemContext
|
| 250 |
+
|
| 251 |
+
system_list = list(systems)
|
| 252 |
+
contexts = [build_context(system, n_max=n_max) for system in system_list]
|
| 253 |
+
return MultiSystemContext.stack(contexts)
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
__all__ = [
|
| 257 |
+
"build_context",
|
| 258 |
+
"build_context_and_energy",
|
| 259 |
+
"build_multi_context",
|
| 260 |
+
"load_system",
|
| 261 |
+
"load_systems",
|
| 262 |
+
"padded_model_arrays",
|
| 263 |
+
"save_system",
|
| 264 |
+
]
|
src/hamiltonzero/energy/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
from hamiltonzero.model._custom_lap_primitives import (
|
| 7 |
+
custom_lap_active,
|
| 8 |
+
quadrilinear_merge_p,
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
from .compiled import (
|
| 12 |
+
vmc_energy_custom_lap_compiled,
|
| 13 |
+
vmc_energy_custom_lap_finetune,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
__all__ = [
|
| 18 |
+
"custom_lap_active",
|
| 19 |
+
"quadrilinear_merge_p",
|
| 20 |
+
"vmc_energy_custom_lap_compiled",
|
| 21 |
+
"vmc_energy_custom_lap_finetune",
|
| 22 |
+
]
|
src/hamiltonzero/energy/compiled.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import jax
|
| 10 |
+
|
| 11 |
+
from hamiltonzero.energy.kernel import (
|
| 12 |
+
_vmc_energy_custom_lap_finetune,
|
| 13 |
+
_vmc_energy_custom_lap_prebuilt,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def vmc_energy_custom_lap_compiled(
|
| 18 |
+
kernel: Any,
|
| 19 |
+
tree: Any,
|
| 20 |
+
energy_frame: Any,
|
| 21 |
+
q_routed: jax.Array,
|
| 22 |
+
*,
|
| 23 |
+
chunk_size: int | None = 512,
|
| 24 |
+
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]:
|
| 25 |
+
return _vmc_energy_custom_lap_prebuilt(
|
| 26 |
+
kernel,
|
| 27 |
+
tree,
|
| 28 |
+
energy_frame,
|
| 29 |
+
q_routed,
|
| 30 |
+
chunk_size=chunk_size,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def vmc_energy_custom_lap_finetune(
|
| 35 |
+
model: Any,
|
| 36 |
+
energy_frame: Any,
|
| 37 |
+
q_routed: jax.Array,
|
| 38 |
+
*,
|
| 39 |
+
chunk_size: int | None = 512,
|
| 40 |
+
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]:
|
| 41 |
+
from hamiltonzero.compiled.model import CompiledFinetuneWaveFunction
|
| 42 |
+
|
| 43 |
+
if not isinstance(model, CompiledFinetuneWaveFunction):
|
| 44 |
+
raise TypeError("fine-tune energy requires CompiledFinetuneWaveFunction")
|
| 45 |
+
return _vmc_energy_custom_lap_finetune(
|
| 46 |
+
model,
|
| 47 |
+
energy_frame,
|
| 48 |
+
q_routed,
|
| 49 |
+
0.0,
|
| 50 |
+
chunk_size=chunk_size,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
__all__ = [
|
| 55 |
+
"vmc_energy_custom_lap_compiled",
|
| 56 |
+
"vmc_energy_custom_lap_finetune",
|
| 57 |
+
]
|
src/hamiltonzero/energy/custom_lap.py
ADDED
|
@@ -0,0 +1,1491 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
from typing import Any, Callable, NamedTuple
|
| 8 |
+
|
| 9 |
+
import jax
|
| 10 |
+
import jax.numpy as jnp
|
| 11 |
+
from jax import lax
|
| 12 |
+
from jax.extend import core
|
| 13 |
+
from jax.extend.core import Literal
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class JLP(NamedTuple):
|
| 17 |
+
value: Any
|
| 18 |
+
jac: Any
|
| 19 |
+
lap: Any
|
| 20 |
+
level: int
|
| 21 |
+
has_chunk_axis: bool
|
| 22 |
+
chunk_idx: int = -1
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _is_jlp(x) -> bool:
|
| 26 |
+
return isinstance(x, JLP)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
from hamiltonzero.model._custom_lap_primitives import (
|
| 30 |
+
custom_lap_active,
|
| 31 |
+
enter_custom_lap,
|
| 32 |
+
restore_custom_lap,
|
| 33 |
+
quadrilinear_merge_p,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class use_custom_lap:
|
| 38 |
+
def __enter__(self):
|
| 39 |
+
self._cla_token = enter_custom_lap()
|
| 40 |
+
return self
|
| 41 |
+
|
| 42 |
+
def __exit__(self, *exc):
|
| 43 |
+
restore_custom_lap(self._cla_token)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def build_W_levels(W_full, N: int) -> list:
|
| 47 |
+
|
| 48 |
+
assert W_full.shape == (3 * N, 3 * N), (
|
| 49 |
+
f"W_full must be [3N, 3N]; got {W_full.shape}"
|
| 50 |
+
)
|
| 51 |
+
assert (N & (N - 1)) == 0, f"N must be a power of 2; got {N}"
|
| 52 |
+
levels = []
|
| 53 |
+
k = 1
|
| 54 |
+
while k <= N:
|
| 55 |
+
n_chunks = N // k
|
| 56 |
+
W_reshaped = W_full.reshape(n_chunks, 3 * k, n_chunks, 3 * k)
|
| 57 |
+
idx = jnp.arange(n_chunks)
|
| 58 |
+
W_k = W_reshaped[idx, :, idx, :]
|
| 59 |
+
levels.append(W_k)
|
| 60 |
+
k *= 2
|
| 61 |
+
return levels
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _level_idx(k: int) -> int:
|
| 65 |
+
|
| 66 |
+
assert k > 0 and (k & (k - 1)) == 0, f"k must be a power of 2; got {k}"
|
| 67 |
+
return k.bit_length() - 1
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _W_at_level(W_levels, k: int):
|
| 71 |
+
return W_levels[_level_idx(k)]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
_RULE_REGISTRY: dict[core.Primitive, Callable] = {}
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _params_except(params, *drop, **defaults):
|
| 78 |
+
|
| 79 |
+
out = {k: params[k] for k in params if k not in drop}
|
| 80 |
+
for k, v in defaults.items():
|
| 81 |
+
out.setdefault(k, v)
|
| 82 |
+
return out
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _shape_bind_params(params, *drop):
|
| 86 |
+
|
| 87 |
+
out = {k: params[k] for k in params if k not in drop and k != "out_sharding"}
|
| 88 |
+
out.setdefault("sharding", params.get("out_sharding", None))
|
| 89 |
+
return out
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _select_W_for_jlp(
|
| 93 |
+
level: int, chunk_idx: int, has_chunk_axis: bool, W_levels, M_jac: int
|
| 94 |
+
):
|
| 95 |
+
|
| 96 |
+
W_k = _W_at_level(W_levels, level)
|
| 97 |
+
if has_chunk_axis:
|
| 98 |
+
assert W_k.shape[0] == M_jac, (
|
| 99 |
+
f"has_chunk_axis: W_k chunks {W_k.shape[0]} must equal jac M {M_jac}"
|
| 100 |
+
)
|
| 101 |
+
return W_k
|
| 102 |
+
if chunk_idx >= 0:
|
| 103 |
+
assert M_jac == 1
|
| 104 |
+
return W_k[chunk_idx : chunk_idx + 1]
|
| 105 |
+
|
| 106 |
+
assert W_k.shape[0] == M_jac, (
|
| 107 |
+
f"multi-chunk: W_k chunks {W_k.shape[0]} must equal jac M {M_jac}"
|
| 108 |
+
)
|
| 109 |
+
return W_k
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _jac_self_quad_form(
|
| 113 |
+
jac, level: int, chunk_idx: int, has_chunk_axis: bool, W_levels
|
| 114 |
+
):
|
| 115 |
+
|
| 116 |
+
M = jac.shape[1]
|
| 117 |
+
W_used = _select_W_for_jlp(level, chunk_idx, has_chunk_axis, W_levels, M)
|
| 118 |
+
n_trailing = jac.ndim - 2
|
| 119 |
+
if n_trailing == 0:
|
| 120 |
+
out = jnp.einsum("mc,cmn,nc->c", jac, W_used, jac)
|
| 121 |
+
elif n_trailing == 1:
|
| 122 |
+
out = jnp.einsum("mca,cmn,nca->ca", jac, W_used, jac)
|
| 123 |
+
elif n_trailing == 2:
|
| 124 |
+
out = jnp.einsum("mcab,cmn,ncab->cab", jac, W_used, jac)
|
| 125 |
+
elif n_trailing == 3:
|
| 126 |
+
out = jnp.einsum("mcabd,cmn,ncabd->cabd", jac, W_used, jac)
|
| 127 |
+
else:
|
| 128 |
+
raise NotImplementedError(
|
| 129 |
+
f"_jac_self_quad_form: trailing rank {n_trailing} not supported"
|
| 130 |
+
)
|
| 131 |
+
if not has_chunk_axis:
|
| 132 |
+
out = out.sum(axis=0)
|
| 133 |
+
return out
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _make_unary_rule(prim, f_prime_fn, f_dprime_fn):
|
| 137 |
+
|
| 138 |
+
def rule(invals, params, W_levels):
|
| 139 |
+
[x] = invals
|
| 140 |
+
assert _is_jlp(x)
|
| 141 |
+
v_out = prim.bind(x.value, **params)
|
| 142 |
+
fp = f_prime_fn(x.value)
|
| 143 |
+
jac_out = fp * x.jac
|
| 144 |
+
fpp = f_dprime_fn(x.value)
|
| 145 |
+
cross = _jac_self_quad_form(
|
| 146 |
+
x.jac, x.level, x.chunk_idx, x.has_chunk_axis, W_levels
|
| 147 |
+
)
|
| 148 |
+
lap_out = fp * x.lap + fpp * cross
|
| 149 |
+
return JLP(
|
| 150 |
+
value=v_out,
|
| 151 |
+
jac=jac_out,
|
| 152 |
+
lap=lap_out,
|
| 153 |
+
level=x.level,
|
| 154 |
+
has_chunk_axis=x.has_chunk_axis,
|
| 155 |
+
chunk_idx=x.chunk_idx,
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
_RULE_REGISTRY[prim] = rule
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
_make_unary_rule(lax.sin_p, jnp.cos, lambda x: -jnp.sin(x))
|
| 162 |
+
_make_unary_rule(lax.cos_p, lambda x: -jnp.sin(x), lambda x: -jnp.cos(x))
|
| 163 |
+
_make_unary_rule(
|
| 164 |
+
lax.tanh_p,
|
| 165 |
+
lambda x: 1.0 - jnp.tanh(x) ** 2,
|
| 166 |
+
lambda x: -2.0 * jnp.tanh(x) * (1.0 - jnp.tanh(x) ** 2),
|
| 167 |
+
)
|
| 168 |
+
_make_unary_rule(lax.exp_p, jnp.exp, jnp.exp)
|
| 169 |
+
_make_unary_rule(lax.log_p, lambda x: 1.0 / x, lambda x: -1.0 / (x * x))
|
| 170 |
+
_make_unary_rule(lax.neg_p, lambda x: -jnp.ones_like(x), lambda x: jnp.zeros_like(x))
|
| 171 |
+
_make_unary_rule(lax.abs_p, lambda x: jnp.sign(x), lambda x: jnp.zeros_like(x))
|
| 172 |
+
_make_unary_rule(
|
| 173 |
+
lax.sqrt_p, lambda x: 0.5 / jnp.sqrt(x), lambda x: -0.25 / (x * jnp.sqrt(x))
|
| 174 |
+
)
|
| 175 |
+
_make_unary_rule(
|
| 176 |
+
lax.rsqrt_p,
|
| 177 |
+
lambda x: -0.5 / (x * jnp.sqrt(x)),
|
| 178 |
+
lambda x: 0.75 / (x * x * jnp.sqrt(x)),
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def _logistic_prime(x):
|
| 183 |
+
s = jax.nn.sigmoid(x)
|
| 184 |
+
return s * (1.0 - s)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def _logistic_dprime(x):
|
| 188 |
+
s = jax.nn.sigmoid(x)
|
| 189 |
+
return s * (1.0 - s) * (1.0 - 2.0 * s)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
_logistic_p = lax.logistic_p
|
| 193 |
+
_make_unary_rule(_logistic_p, _logistic_prime, _logistic_dprime)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def _integer_pow_rule(invals, params, W_levels):
|
| 197 |
+
[x] = invals
|
| 198 |
+
y = params["y"]
|
| 199 |
+
assert _is_jlp(x)
|
| 200 |
+
v_out = lax.integer_pow_p.bind(x.value, **params)
|
| 201 |
+
if y == 0:
|
| 202 |
+
return JLP(
|
| 203 |
+
value=jnp.ones_like(x.value),
|
| 204 |
+
jac=jnp.zeros_like(x.jac),
|
| 205 |
+
lap=jnp.zeros_like(x.lap),
|
| 206 |
+
level=x.level,
|
| 207 |
+
has_chunk_axis=x.has_chunk_axis,
|
| 208 |
+
chunk_idx=x.chunk_idx,
|
| 209 |
+
)
|
| 210 |
+
if y == 1:
|
| 211 |
+
return x
|
| 212 |
+
fp = float(y) * lax.integer_pow_p.bind(x.value, y=y - 1)
|
| 213 |
+
jac_out = fp * x.jac
|
| 214 |
+
fpp = float(y * (y - 1)) * lax.integer_pow_p.bind(x.value, y=max(y - 2, 0))
|
| 215 |
+
cross = _jac_self_quad_form(x.jac, x.level, x.chunk_idx, x.has_chunk_axis, W_levels)
|
| 216 |
+
lap_out = fp * x.lap + fpp * cross
|
| 217 |
+
return JLP(
|
| 218 |
+
value=v_out,
|
| 219 |
+
jac=jac_out,
|
| 220 |
+
lap=lap_out,
|
| 221 |
+
level=x.level,
|
| 222 |
+
has_chunk_axis=x.has_chunk_axis,
|
| 223 |
+
chunk_idx=x.chunk_idx,
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
_RULE_REGISTRY[lax.integer_pow_p] = _integer_pow_rule
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def _convert_rule(invals, params, W_levels):
|
| 231 |
+
[x] = invals
|
| 232 |
+
assert _is_jlp(x)
|
| 233 |
+
new_dtype = params["new_dtype"]
|
| 234 |
+
return JLP(
|
| 235 |
+
value=x.value.astype(new_dtype),
|
| 236 |
+
jac=x.jac.astype(new_dtype),
|
| 237 |
+
lap=x.lap.astype(new_dtype),
|
| 238 |
+
level=x.level,
|
| 239 |
+
has_chunk_axis=x.has_chunk_axis,
|
| 240 |
+
chunk_idx=x.chunk_idx,
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
_RULE_REGISTRY[lax.convert_element_type_p] = _convert_rule
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def _broadcast_jac_lap_for_op(x, target_shape):
|
| 248 |
+
|
| 249 |
+
lap_out = jnp.broadcast_to(x.lap, target_shape)
|
| 250 |
+
jac_out = _broadcast_jac_to_value_shape(x.jac, target_shape, x.has_chunk_axis)
|
| 251 |
+
return jac_out, lap_out
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def _broadcast_jac_to_value_shape(jac, target_value_shape, has_chunk_axis: bool):
|
| 255 |
+
|
| 256 |
+
if has_chunk_axis:
|
| 257 |
+
new_shape = (jac.shape[0], target_value_shape[0]) + tuple(
|
| 258 |
+
target_value_shape[1:]
|
| 259 |
+
)
|
| 260 |
+
else:
|
| 261 |
+
new_shape = (jac.shape[0], jac.shape[1]) + tuple(target_value_shape)
|
| 262 |
+
while jac.ndim < len(new_shape):
|
| 263 |
+
jac = jnp.expand_dims(jac, axis=jac.ndim)
|
| 264 |
+
return jnp.broadcast_to(jac, new_shape)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def _promote_jlp_one_level(x: JLP) -> JLP:
|
| 268 |
+
|
| 269 |
+
k = x.level
|
| 270 |
+
new_k = 2 * k
|
| 271 |
+
if x.has_chunk_axis:
|
| 272 |
+
assert x.chunk_idx in (0, 1), (
|
| 273 |
+
f"_promote_jlp_one_level (chunked): chunk_idx must be 0 or 1; got {x}"
|
| 274 |
+
)
|
| 275 |
+
is_left = x.chunk_idx == 0
|
| 276 |
+
zero_shape = (3 * k,) + x.jac.shape[1:]
|
| 277 |
+
zeros = jnp.zeros(zero_shape, dtype=x.jac.dtype)
|
| 278 |
+
if is_left:
|
| 279 |
+
new_jac = jnp.concatenate([x.jac, zeros], axis=0)
|
| 280 |
+
else:
|
| 281 |
+
new_jac = jnp.concatenate([zeros, x.jac], axis=0)
|
| 282 |
+
return JLP(
|
| 283 |
+
value=x.value,
|
| 284 |
+
jac=new_jac,
|
| 285 |
+
lap=x.lap,
|
| 286 |
+
level=new_k,
|
| 287 |
+
has_chunk_axis=True,
|
| 288 |
+
chunk_idx=-1,
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
assert x.chunk_idx >= 0, (
|
| 292 |
+
f"_promote_jlp_one_level: per-node requires chunk_idx>=0; got {x}"
|
| 293 |
+
)
|
| 294 |
+
is_left = x.chunk_idx % 2 == 0
|
| 295 |
+
new_chunk_idx = x.chunk_idx // 2
|
| 296 |
+
zero_shape = (3 * k,) + x.jac.shape[1:]
|
| 297 |
+
zeros = jnp.zeros(zero_shape, dtype=x.jac.dtype)
|
| 298 |
+
if is_left:
|
| 299 |
+
new_jac = jnp.concatenate([x.jac, zeros], axis=0)
|
| 300 |
+
else:
|
| 301 |
+
new_jac = jnp.concatenate([zeros, x.jac], axis=0)
|
| 302 |
+
return JLP(
|
| 303 |
+
value=x.value,
|
| 304 |
+
jac=new_jac,
|
| 305 |
+
lap=x.lap,
|
| 306 |
+
level=new_k,
|
| 307 |
+
has_chunk_axis=False,
|
| 308 |
+
chunk_idx=new_chunk_idx,
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def _align_jlp_levels(a: JLP, b: JLP):
|
| 313 |
+
|
| 314 |
+
while a.level < b.level:
|
| 315 |
+
a = _promote_jlp_one_level(a)
|
| 316 |
+
while b.level < a.level:
|
| 317 |
+
b = _promote_jlp_one_level(b)
|
| 318 |
+
assert a.has_chunk_axis == b.has_chunk_axis, (
|
| 319 |
+
"_align_jlp_levels: chunk-axis mismatch"
|
| 320 |
+
)
|
| 321 |
+
if a.has_chunk_axis:
|
| 322 |
+
if (
|
| 323 |
+
a.chunk_idx in (0, 1)
|
| 324 |
+
and b.chunk_idx in (0, 1)
|
| 325 |
+
and a.chunk_idx != b.chunk_idx
|
| 326 |
+
):
|
| 327 |
+
a = _promote_jlp_one_level(a)
|
| 328 |
+
b = _promote_jlp_one_level(b)
|
| 329 |
+
return a, b
|
| 330 |
+
|
| 331 |
+
while a.chunk_idx != b.chunk_idx:
|
| 332 |
+
a = _promote_jlp_one_level(a)
|
| 333 |
+
b = _promote_jlp_one_level(b)
|
| 334 |
+
return a, b
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def _add_or_sub_rule(sign: float):
|
| 338 |
+
def rule(invals, params, W_levels):
|
| 339 |
+
a, b = invals
|
| 340 |
+
if _is_jlp(a) and _is_jlp(b):
|
| 341 |
+
need_promote = (
|
| 342 |
+
(a.level != b.level)
|
| 343 |
+
or (
|
| 344 |
+
not a.has_chunk_axis
|
| 345 |
+
and not b.has_chunk_axis
|
| 346 |
+
and a.chunk_idx != b.chunk_idx
|
| 347 |
+
)
|
| 348 |
+
or (
|
| 349 |
+
a.has_chunk_axis
|
| 350 |
+
and b.has_chunk_axis
|
| 351 |
+
and a.chunk_idx in (0, 1)
|
| 352 |
+
and b.chunk_idx in (0, 1)
|
| 353 |
+
and a.chunk_idx != b.chunk_idx
|
| 354 |
+
)
|
| 355 |
+
)
|
| 356 |
+
if need_promote:
|
| 357 |
+
a, b = _align_jlp_levels(a, b)
|
| 358 |
+
assert a.has_chunk_axis == b.has_chunk_axis, "add/sub: chunk-axis mismatch"
|
| 359 |
+
v_out = a.value + sign * b.value
|
| 360 |
+
a_jac_b = _broadcast_jac_to_value_shape(
|
| 361 |
+
a.jac, v_out.shape, a.has_chunk_axis
|
| 362 |
+
)
|
| 363 |
+
b_jac_b = _broadcast_jac_to_value_shape(
|
| 364 |
+
b.jac, v_out.shape, b.has_chunk_axis
|
| 365 |
+
)
|
| 366 |
+
jac_out = a_jac_b + sign * b_jac_b
|
| 367 |
+
a_lap_b = jnp.broadcast_to(a.lap, v_out.shape)
|
| 368 |
+
b_lap_b = jnp.broadcast_to(b.lap, v_out.shape)
|
| 369 |
+
lap_out = a_lap_b + sign * b_lap_b
|
| 370 |
+
return JLP(
|
| 371 |
+
value=v_out,
|
| 372 |
+
jac=jac_out,
|
| 373 |
+
lap=lap_out,
|
| 374 |
+
level=a.level,
|
| 375 |
+
has_chunk_axis=a.has_chunk_axis,
|
| 376 |
+
chunk_idx=a.chunk_idx,
|
| 377 |
+
)
|
| 378 |
+
if _is_jlp(a):
|
| 379 |
+
v_out = a.value + sign * b
|
| 380 |
+
jac_out, lap_out = _broadcast_jac_lap_for_op(a, v_out.shape)
|
| 381 |
+
return JLP(
|
| 382 |
+
value=v_out,
|
| 383 |
+
jac=jac_out,
|
| 384 |
+
lap=lap_out,
|
| 385 |
+
level=a.level,
|
| 386 |
+
has_chunk_axis=a.has_chunk_axis,
|
| 387 |
+
chunk_idx=a.chunk_idx,
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
v_out = a + sign * b.value
|
| 391 |
+
jac_out, lap_out = _broadcast_jac_lap_for_op(b, v_out.shape)
|
| 392 |
+
return JLP(
|
| 393 |
+
value=v_out,
|
| 394 |
+
jac=sign * jac_out,
|
| 395 |
+
lap=sign * lap_out,
|
| 396 |
+
level=b.level,
|
| 397 |
+
has_chunk_axis=b.has_chunk_axis,
|
| 398 |
+
chunk_idx=b.chunk_idx,
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
return rule
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
_RULE_REGISTRY[lax.add_p] = _add_or_sub_rule(+1.0)
|
| 405 |
+
_RULE_REGISTRY[lax.sub_p] = _add_or_sub_rule(-1.0)
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
def _mul_rule(invals, params, W_levels):
|
| 409 |
+
a, b = invals
|
| 410 |
+
if _is_jlp(a) and _is_jlp(b):
|
| 411 |
+
need_promote = (a.level != b.level) or (
|
| 412 |
+
not a.has_chunk_axis and not b.has_chunk_axis and a.chunk_idx != b.chunk_idx
|
| 413 |
+
)
|
| 414 |
+
if need_promote:
|
| 415 |
+
a, b = _align_jlp_levels(a, b)
|
| 416 |
+
assert a.has_chunk_axis == b.has_chunk_axis
|
| 417 |
+
|
| 418 |
+
v_out = a.value * b.value
|
| 419 |
+
a_jac_b = _broadcast_jac_to_value_shape(a.jac, v_out.shape, a.has_chunk_axis)
|
| 420 |
+
b_jac_b = _broadcast_jac_to_value_shape(b.jac, v_out.shape, b.has_chunk_axis)
|
| 421 |
+
a_val_b = jnp.broadcast_to(a.value, v_out.shape)
|
| 422 |
+
b_val_b = jnp.broadcast_to(b.value, v_out.shape)
|
| 423 |
+
jac_out = a_val_b * b_jac_b + b_val_b * a_jac_b
|
| 424 |
+
a_lap_b = jnp.broadcast_to(a.lap, v_out.shape)
|
| 425 |
+
b_lap_b = jnp.broadcast_to(b.lap, v_out.shape)
|
| 426 |
+
cross = _jac_cross_quad_form(
|
| 427 |
+
a_jac_b, b_jac_b, a.level, a.chunk_idx, a.has_chunk_axis, W_levels
|
| 428 |
+
)
|
| 429 |
+
lap_out = a_val_b * b_lap_b + b_val_b * a_lap_b + 2.0 * cross
|
| 430 |
+
return JLP(
|
| 431 |
+
value=v_out,
|
| 432 |
+
jac=jac_out,
|
| 433 |
+
lap=lap_out,
|
| 434 |
+
level=a.level,
|
| 435 |
+
has_chunk_axis=a.has_chunk_axis,
|
| 436 |
+
chunk_idx=a.chunk_idx,
|
| 437 |
+
)
|
| 438 |
+
if _is_jlp(a):
|
| 439 |
+
v_out = a.value * b
|
| 440 |
+
jac_b, lap_b = _broadcast_jac_lap_for_op(a, v_out.shape)
|
| 441 |
+
return JLP(
|
| 442 |
+
value=v_out,
|
| 443 |
+
jac=b * jac_b,
|
| 444 |
+
lap=b * lap_b,
|
| 445 |
+
level=a.level,
|
| 446 |
+
has_chunk_axis=a.has_chunk_axis,
|
| 447 |
+
chunk_idx=a.chunk_idx,
|
| 448 |
+
)
|
| 449 |
+
|
| 450 |
+
v_out = a * b.value
|
| 451 |
+
jac_b, lap_b = _broadcast_jac_lap_for_op(b, v_out.shape)
|
| 452 |
+
return JLP(
|
| 453 |
+
value=v_out,
|
| 454 |
+
jac=a * jac_b,
|
| 455 |
+
lap=a * lap_b,
|
| 456 |
+
level=b.level,
|
| 457 |
+
has_chunk_axis=b.has_chunk_axis,
|
| 458 |
+
chunk_idx=b.chunk_idx,
|
| 459 |
+
)
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
def _jac_cross_quad_form(
|
| 463 |
+
jac_a, jac_b, level: int, chunk_idx: int, has_chunk_axis: bool, W_levels
|
| 464 |
+
):
|
| 465 |
+
|
| 466 |
+
M = jac_a.shape[1]
|
| 467 |
+
W_used = _select_W_for_jlp(level, chunk_idx, has_chunk_axis, W_levels, M)
|
| 468 |
+
n_trailing = jac_a.ndim - 2
|
| 469 |
+
if n_trailing == 0:
|
| 470 |
+
out = jnp.einsum("mc,cmn,nc->c", jac_a, W_used, jac_b)
|
| 471 |
+
elif n_trailing == 1:
|
| 472 |
+
out = jnp.einsum("mca,cmn,nca->ca", jac_a, W_used, jac_b)
|
| 473 |
+
elif n_trailing == 2:
|
| 474 |
+
out = jnp.einsum("mcab,cmn,ncab->cab", jac_a, W_used, jac_b)
|
| 475 |
+
elif n_trailing == 3:
|
| 476 |
+
out = jnp.einsum("mcabd,cmn,ncabd->cabd", jac_a, W_used, jac_b)
|
| 477 |
+
else:
|
| 478 |
+
raise NotImplementedError(
|
| 479 |
+
f"_jac_cross_quad_form: trailing rank {n_trailing} not supported"
|
| 480 |
+
)
|
| 481 |
+
if not has_chunk_axis:
|
| 482 |
+
out = out.sum(axis=0)
|
| 483 |
+
return out
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
_RULE_REGISTRY[lax.mul_p] = _mul_rule
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
def _div_rule(invals, params, W_levels):
|
| 490 |
+
a, b = invals
|
| 491 |
+
if _is_jlp(a) and _is_jlp(b):
|
| 492 |
+
need_promote = (a.level != b.level) or (
|
| 493 |
+
not a.has_chunk_axis and not b.has_chunk_axis and a.chunk_idx != b.chunk_idx
|
| 494 |
+
)
|
| 495 |
+
if need_promote:
|
| 496 |
+
a, b = _align_jlp_levels(a, b)
|
| 497 |
+
assert a.has_chunk_axis == b.has_chunk_axis
|
| 498 |
+
v_out = a.value / b.value
|
| 499 |
+
a_val_b = jnp.broadcast_to(a.value, v_out.shape)
|
| 500 |
+
b_val_b = jnp.broadcast_to(b.value, v_out.shape)
|
| 501 |
+
a_jac_b = _broadcast_jac_to_value_shape(a.jac, v_out.shape, a.has_chunk_axis)
|
| 502 |
+
b_jac_b = _broadcast_jac_to_value_shape(b.jac, v_out.shape, b.has_chunk_axis)
|
| 503 |
+
inv_b = 1.0 / b_val_b
|
| 504 |
+
jac_out = inv_b * a_jac_b - (a_val_b * inv_b * inv_b) * b_jac_b
|
| 505 |
+
a_lap_b = jnp.broadcast_to(a.lap, v_out.shape)
|
| 506 |
+
b_lap_b = jnp.broadcast_to(b.lap, v_out.shape)
|
| 507 |
+
lap_first = inv_b * a_lap_b - (a_val_b * inv_b * inv_b) * b_lap_b
|
| 508 |
+
cross_bb = _jac_self_quad_form(
|
| 509 |
+
b_jac_b, b.level, b.chunk_idx, b.has_chunk_axis, W_levels
|
| 510 |
+
)
|
| 511 |
+
cross_ab = _jac_cross_quad_form(
|
| 512 |
+
a_jac_b, b_jac_b, a.level, a.chunk_idx, a.has_chunk_axis, W_levels
|
| 513 |
+
)
|
| 514 |
+
lap_second = (2.0 * a_val_b * inv_b**3) * cross_bb + (
|
| 515 |
+
-2.0 * inv_b * inv_b
|
| 516 |
+
) * cross_ab
|
| 517 |
+
lap_out = lap_first + lap_second
|
| 518 |
+
return JLP(
|
| 519 |
+
value=v_out,
|
| 520 |
+
jac=jac_out,
|
| 521 |
+
lap=lap_out,
|
| 522 |
+
level=a.level,
|
| 523 |
+
has_chunk_axis=a.has_chunk_axis,
|
| 524 |
+
chunk_idx=a.chunk_idx,
|
| 525 |
+
)
|
| 526 |
+
if _is_jlp(a):
|
| 527 |
+
inv_b = 1.0 / b
|
| 528 |
+
v_out = a.value * inv_b
|
| 529 |
+
jac_b, lap_b = _broadcast_jac_lap_for_op(a, v_out.shape)
|
| 530 |
+
return JLP(
|
| 531 |
+
value=v_out,
|
| 532 |
+
jac=inv_b * jac_b,
|
| 533 |
+
lap=inv_b * lap_b,
|
| 534 |
+
level=a.level,
|
| 535 |
+
has_chunk_axis=a.has_chunk_axis,
|
| 536 |
+
chunk_idx=a.chunk_idx,
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
v_out = a / b.value
|
| 540 |
+
inv_b = 1.0 / b.value
|
| 541 |
+
fp = -a * inv_b * inv_b
|
| 542 |
+
jac_out = fp * b.jac
|
| 543 |
+
fpp = 2.0 * a * inv_b**3
|
| 544 |
+
cross = _jac_self_quad_form(b.jac, b.level, b.chunk_idx, b.has_chunk_axis, W_levels)
|
| 545 |
+
lap_out = fp * b.lap + fpp * cross
|
| 546 |
+
return JLP(
|
| 547 |
+
value=v_out,
|
| 548 |
+
jac=jac_out,
|
| 549 |
+
lap=lap_out,
|
| 550 |
+
level=b.level,
|
| 551 |
+
has_chunk_axis=b.has_chunk_axis,
|
| 552 |
+
chunk_idx=b.chunk_idx,
|
| 553 |
+
)
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
_RULE_REGISTRY[lax.div_p] = _div_rule
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
def _atan2_rule(invals, params, W_levels):
|
| 560 |
+
a_arg, b_arg = invals
|
| 561 |
+
if _is_jlp(a_arg) and _is_jlp(b_arg):
|
| 562 |
+
need_promote = (a_arg.level != b_arg.level) or (
|
| 563 |
+
not a_arg.has_chunk_axis
|
| 564 |
+
and not b_arg.has_chunk_axis
|
| 565 |
+
and a_arg.chunk_idx != b_arg.chunk_idx
|
| 566 |
+
)
|
| 567 |
+
if need_promote:
|
| 568 |
+
a_arg, b_arg = _align_jlp_levels(a_arg, b_arg)
|
| 569 |
+
assert a_arg.has_chunk_axis == b_arg.has_chunk_axis
|
| 570 |
+
a, b = a_arg, b_arg
|
| 571 |
+
elif _is_jlp(a_arg):
|
| 572 |
+
a = a_arg
|
| 573 |
+
b = _trivial_jlp_like(b_arg, a_arg)
|
| 574 |
+
else:
|
| 575 |
+
b = b_arg
|
| 576 |
+
a = _trivial_jlp_like(a_arg, b_arg)
|
| 577 |
+
v_out = jnp.arctan2(a.value, b.value)
|
| 578 |
+
r_sq = a.value**2 + b.value**2
|
| 579 |
+
inv_r2 = 1.0 / r_sq
|
| 580 |
+
fa = b.value * inv_r2
|
| 581 |
+
fb = -a.value * inv_r2
|
| 582 |
+
aj = _broadcast_jac_to_value_shape(a.jac, v_out.shape, a.has_chunk_axis)
|
| 583 |
+
bj = _broadcast_jac_to_value_shape(b.jac, v_out.shape, b.has_chunk_axis)
|
| 584 |
+
jac_out = fa * aj + fb * bj
|
| 585 |
+
faa = -2.0 * a.value * b.value * inv_r2 * inv_r2
|
| 586 |
+
fbb = 2.0 * a.value * b.value * inv_r2 * inv_r2
|
| 587 |
+
fab = (a.value**2 - b.value**2) * inv_r2 * inv_r2
|
| 588 |
+
al = jnp.broadcast_to(a.lap, v_out.shape)
|
| 589 |
+
bl = jnp.broadcast_to(b.lap, v_out.shape)
|
| 590 |
+
cross_aa = _jac_self_quad_form(aj, a.level, a.chunk_idx, a.has_chunk_axis, W_levels)
|
| 591 |
+
cross_bb = _jac_self_quad_form(bj, b.level, b.chunk_idx, b.has_chunk_axis, W_levels)
|
| 592 |
+
cross_ab = _jac_cross_quad_form(
|
| 593 |
+
aj, bj, a.level, a.chunk_idx, a.has_chunk_axis, W_levels
|
| 594 |
+
)
|
| 595 |
+
lap_out = fa * al + fb * bl + faa * cross_aa + fbb * cross_bb + 2.0 * fab * cross_ab
|
| 596 |
+
return JLP(
|
| 597 |
+
value=v_out,
|
| 598 |
+
jac=jac_out,
|
| 599 |
+
lap=lap_out,
|
| 600 |
+
level=a.level,
|
| 601 |
+
has_chunk_axis=a.has_chunk_axis,
|
| 602 |
+
chunk_idx=a.chunk_idx,
|
| 603 |
+
)
|
| 604 |
+
|
| 605 |
+
|
| 606 |
+
def _trivial_jlp_like(plain_val, template_jlp: JLP) -> JLP:
|
| 607 |
+
|
| 608 |
+
val = jnp.asarray(plain_val)
|
| 609 |
+
if template_jlp.has_chunk_axis:
|
| 610 |
+
jac_shape = (template_jlp.jac.shape[0], template_jlp.jac.shape[1]) + tuple(
|
| 611 |
+
val.shape[1:]
|
| 612 |
+
)
|
| 613 |
+
else:
|
| 614 |
+
jac_shape = (template_jlp.jac.shape[0], template_jlp.jac.shape[1]) + tuple(
|
| 615 |
+
val.shape
|
| 616 |
+
)
|
| 617 |
+
return JLP(
|
| 618 |
+
value=val,
|
| 619 |
+
jac=jnp.zeros(jac_shape, dtype=val.dtype),
|
| 620 |
+
lap=jnp.zeros_like(val),
|
| 621 |
+
level=template_jlp.level,
|
| 622 |
+
has_chunk_axis=template_jlp.has_chunk_axis,
|
| 623 |
+
chunk_idx=template_jlp.chunk_idx,
|
| 624 |
+
)
|
| 625 |
+
|
| 626 |
+
|
| 627 |
+
_RULE_REGISTRY[lax.atan2_p] = _atan2_rule
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
def _dot_general_rule(invals, params, W_levels):
|
| 631 |
+
lhs, rhs = invals
|
| 632 |
+
dimension_numbers = params["dimension_numbers"]
|
| 633 |
+
(lhs_contract, rhs_contract), (lhs_batch, rhs_batch) = dimension_numbers
|
| 634 |
+
|
| 635 |
+
if _is_jlp(lhs) and _is_jlp(rhs):
|
| 636 |
+
raise NotImplementedError(
|
| 637 |
+
"dot_general: JLP × JLP outside quadrilinear_merge_p is not supported. "
|
| 638 |
+
"If a model op needs same-level bilinear, route it through the merge "
|
| 639 |
+
"primitive or rewrite as elementwise mul + reduce_sum."
|
| 640 |
+
)
|
| 641 |
+
if _is_jlp(lhs):
|
| 642 |
+
return _dot_general_jlp_plain(
|
| 643 |
+
lhs, rhs, dimension_numbers, params, jlp_is_lhs=True
|
| 644 |
+
)
|
| 645 |
+
return _dot_general_jlp_plain(rhs, lhs, dimension_numbers, params, jlp_is_lhs=False)
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
def _dot_general_jlp_plain(
|
| 649 |
+
jlp_arg, plain_arg, dimension_numbers, params, jlp_is_lhs: bool
|
| 650 |
+
):
|
| 651 |
+
|
| 652 |
+
(lhs_contract, rhs_contract), (lhs_batch, rhs_batch) = dimension_numbers
|
| 653 |
+
dot_kw = {
|
| 654 |
+
"precision": params.get("precision", None),
|
| 655 |
+
"preferred_element_type": params.get("preferred_element_type", None),
|
| 656 |
+
"out_sharding": params.get("out_sharding", None),
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
if jlp_is_lhs:
|
| 660 |
+
v_out = lax.dot_general(jlp_arg.value, plain_arg, dimension_numbers, **dot_kw)
|
| 661 |
+
lap_out = lax.dot_general(jlp_arg.lap, plain_arg, dimension_numbers, **dot_kw)
|
| 662 |
+
else:
|
| 663 |
+
v_out = lax.dot_general(plain_arg, jlp_arg.value, dimension_numbers, **dot_kw)
|
| 664 |
+
lap_out = lax.dot_general(plain_arg, jlp_arg.lap, dimension_numbers, **dot_kw)
|
| 665 |
+
|
| 666 |
+
jac = jlp_arg.jac
|
| 667 |
+
leading_3k = jac.shape[0]
|
| 668 |
+
|
| 669 |
+
if jlp_arg.has_chunk_axis:
|
| 670 |
+
jac_for_dot = jac
|
| 671 |
+
shift = 1
|
| 672 |
+
else:
|
| 673 |
+
jac_for_dot = jac.reshape((leading_3k,) + tuple(jlp_arg.value.shape))
|
| 674 |
+
shift = 1
|
| 675 |
+
|
| 676 |
+
if jlp_is_lhs:
|
| 677 |
+
new_lhs_contract = tuple(a + shift for a in lhs_contract)
|
| 678 |
+
new_rhs_contract = tuple(rhs_contract)
|
| 679 |
+
new_lhs_batch = tuple(a + shift for a in lhs_batch)
|
| 680 |
+
new_rhs_batch = tuple(rhs_batch)
|
| 681 |
+
new_dim_nums = (
|
| 682 |
+
(new_lhs_contract, new_rhs_contract),
|
| 683 |
+
(new_lhs_batch, new_rhs_batch),
|
| 684 |
+
)
|
| 685 |
+
jac_out_raw = lax.dot_general(jac_for_dot, plain_arg, new_dim_nums, **dot_kw)
|
| 686 |
+
|
| 687 |
+
n_batch = len(new_lhs_batch)
|
| 688 |
+
pos_3k = n_batch
|
| 689 |
+
else:
|
| 690 |
+
new_lhs_contract = tuple(lhs_contract)
|
| 691 |
+
new_rhs_contract = tuple(a + shift for a in rhs_contract)
|
| 692 |
+
new_lhs_batch = tuple(lhs_batch)
|
| 693 |
+
new_rhs_batch = tuple(a + shift for a in rhs_batch)
|
| 694 |
+
new_dim_nums = (
|
| 695 |
+
(new_lhs_contract, new_rhs_contract),
|
| 696 |
+
(new_lhs_batch, new_rhs_batch),
|
| 697 |
+
)
|
| 698 |
+
jac_out_raw = lax.dot_general(plain_arg, jac_for_dot, new_dim_nums, **dot_kw)
|
| 699 |
+
|
| 700 |
+
n_batch = len(new_lhs_batch)
|
| 701 |
+
lhs_ndim = jnp.asarray(plain_arg).ndim
|
| 702 |
+
n_lhs_nonbatch = lhs_ndim - n_batch - len(new_lhs_contract)
|
| 703 |
+
pos_3k = n_batch + n_lhs_nonbatch
|
| 704 |
+
|
| 705 |
+
if pos_3k != 0:
|
| 706 |
+
jac_out = jnp.moveaxis(jac_out_raw, pos_3k, 0)
|
| 707 |
+
else:
|
| 708 |
+
jac_out = jac_out_raw
|
| 709 |
+
|
| 710 |
+
if jlp_arg.has_chunk_axis:
|
| 711 |
+
new_has_chunk_axis = True
|
| 712 |
+
new_chunk_idx = -1
|
| 713 |
+
else:
|
| 714 |
+
jac_out = jac_out[:, None]
|
| 715 |
+
new_has_chunk_axis = False
|
| 716 |
+
new_chunk_idx = jlp_arg.chunk_idx
|
| 717 |
+
|
| 718 |
+
return JLP(
|
| 719 |
+
value=v_out,
|
| 720 |
+
jac=jac_out,
|
| 721 |
+
lap=lap_out,
|
| 722 |
+
level=jlp_arg.level,
|
| 723 |
+
has_chunk_axis=new_has_chunk_axis,
|
| 724 |
+
chunk_idx=new_chunk_idx,
|
| 725 |
+
)
|
| 726 |
+
|
| 727 |
+
|
| 728 |
+
_RULE_REGISTRY[lax.dot_general_p] = _dot_general_rule
|
| 729 |
+
|
| 730 |
+
|
| 731 |
+
def _broadcast_in_dim_rule(invals, params, W_levels):
|
| 732 |
+
[x] = invals
|
| 733 |
+
assert _is_jlp(x)
|
| 734 |
+
shape = params["shape"]
|
| 735 |
+
broadcast_dimensions = params["broadcast_dimensions"]
|
| 736 |
+
extra = _shape_bind_params(params, "shape", "broadcast_dimensions")
|
| 737 |
+
|
| 738 |
+
v_out = lax.broadcast_in_dim_p.bind(
|
| 739 |
+
x.value, shape=shape, broadcast_dimensions=broadcast_dimensions, **extra
|
| 740 |
+
)
|
| 741 |
+
lap_out = lax.broadcast_in_dim_p.bind(
|
| 742 |
+
x.lap, shape=shape, broadcast_dimensions=broadcast_dimensions, **extra
|
| 743 |
+
)
|
| 744 |
+
|
| 745 |
+
if x.has_chunk_axis:
|
| 746 |
+
new_shape = (x.jac.shape[0],) + tuple(shape)
|
| 747 |
+
new_bd = (0,) + tuple(d + 1 for d in broadcast_dimensions)
|
| 748 |
+
jac_out = lax.broadcast_in_dim_p.bind(
|
| 749 |
+
x.jac.reshape((x.jac.shape[0],) + x.value.shape),
|
| 750 |
+
shape=new_shape,
|
| 751 |
+
broadcast_dimensions=new_bd,
|
| 752 |
+
**extra,
|
| 753 |
+
)
|
| 754 |
+
|
| 755 |
+
new_has_chunk_axis = True
|
| 756 |
+
new_chunk_idx = -1
|
| 757 |
+
else:
|
| 758 |
+
new_shape = (x.jac.shape[0], 1) + tuple(shape)
|
| 759 |
+
new_bd = (0, 1) + tuple(d + 2 for d in broadcast_dimensions)
|
| 760 |
+
jac_out = lax.broadcast_in_dim_p.bind(
|
| 761 |
+
x.jac,
|
| 762 |
+
shape=new_shape,
|
| 763 |
+
broadcast_dimensions=new_bd,
|
| 764 |
+
**extra,
|
| 765 |
+
)
|
| 766 |
+
new_has_chunk_axis = False
|
| 767 |
+
new_chunk_idx = x.chunk_idx
|
| 768 |
+
|
| 769 |
+
return JLP(
|
| 770 |
+
value=v_out,
|
| 771 |
+
jac=jac_out,
|
| 772 |
+
lap=lap_out,
|
| 773 |
+
level=x.level,
|
| 774 |
+
has_chunk_axis=new_has_chunk_axis,
|
| 775 |
+
chunk_idx=new_chunk_idx,
|
| 776 |
+
)
|
| 777 |
+
|
| 778 |
+
|
| 779 |
+
_RULE_REGISTRY[lax.broadcast_in_dim_p] = _broadcast_in_dim_rule
|
| 780 |
+
|
| 781 |
+
|
| 782 |
+
def _reduce_sum_rule(invals, params, W_levels):
|
| 783 |
+
[x] = invals
|
| 784 |
+
assert _is_jlp(x)
|
| 785 |
+
axes = tuple(params["axes"])
|
| 786 |
+
extra = _params_except(params, "axes", out_sharding=None)
|
| 787 |
+
v_out = lax.reduce_sum_p.bind(x.value, axes=axes, **extra)
|
| 788 |
+
lap_out = lax.reduce_sum_p.bind(x.lap, axes=axes, **extra)
|
| 789 |
+
if x.has_chunk_axis:
|
| 790 |
+
if 0 in axes:
|
| 791 |
+
non_chunk_axes = tuple(a for a in axes if a != 0)
|
| 792 |
+
jac_reduce_axes = tuple(a + 1 for a in non_chunk_axes)
|
| 793 |
+
if jac_reduce_axes:
|
| 794 |
+
jac_out = lax.reduce_sum_p.bind(x.jac, axes=jac_reduce_axes, **extra)
|
| 795 |
+
else:
|
| 796 |
+
jac_out = x.jac
|
| 797 |
+
new_has_chunk_axis = False
|
| 798 |
+
new_chunk_idx = -1
|
| 799 |
+
else:
|
| 800 |
+
jac_reduce_axes = tuple(a + 1 for a in axes)
|
| 801 |
+
jac_out = lax.reduce_sum_p.bind(x.jac, axes=jac_reduce_axes, **extra)
|
| 802 |
+
new_has_chunk_axis = True
|
| 803 |
+
new_chunk_idx = -1
|
| 804 |
+
else:
|
| 805 |
+
jac_reduce_axes = tuple(a + 2 for a in axes)
|
| 806 |
+
jac_out = lax.reduce_sum_p.bind(x.jac, axes=jac_reduce_axes, **extra)
|
| 807 |
+
new_has_chunk_axis = False
|
| 808 |
+
new_chunk_idx = x.chunk_idx
|
| 809 |
+
return JLP(
|
| 810 |
+
value=v_out,
|
| 811 |
+
jac=jac_out,
|
| 812 |
+
lap=lap_out,
|
| 813 |
+
level=x.level,
|
| 814 |
+
has_chunk_axis=new_has_chunk_axis,
|
| 815 |
+
chunk_idx=new_chunk_idx,
|
| 816 |
+
)
|
| 817 |
+
|
| 818 |
+
|
| 819 |
+
_RULE_REGISTRY[lax.reduce_sum_p] = _reduce_sum_rule
|
| 820 |
+
|
| 821 |
+
|
| 822 |
+
def _reduce_max_rule(invals, params, W_levels):
|
| 823 |
+
[x] = invals
|
| 824 |
+
assert _is_jlp(x)
|
| 825 |
+
axes = tuple(params["axes"])
|
| 826 |
+
extra = _params_except(params, "axes", out_sharding=None)
|
| 827 |
+
v_out = lax.reduce_max_p.bind(x.value, axes=axes, **extra)
|
| 828 |
+
|
| 829 |
+
keep_shape = list(x.value.shape)
|
| 830 |
+
for a in axes:
|
| 831 |
+
keep_shape[a] = 1
|
| 832 |
+
v_max_kept = lax.reduce_max_p.bind(x.value, axes=axes, **extra).reshape(
|
| 833 |
+
tuple(keep_shape)
|
| 834 |
+
)
|
| 835 |
+
mask = (x.value == v_max_kept).astype(x.value.dtype)
|
| 836 |
+
|
| 837 |
+
mask_sum_axes = lax.reduce_sum_p.bind(
|
| 838 |
+
mask,
|
| 839 |
+
axes=axes,
|
| 840 |
+
**extra,
|
| 841 |
+
).reshape(tuple(keep_shape))
|
| 842 |
+
mask = mask / (mask_sum_axes + 1e-30)
|
| 843 |
+
|
| 844 |
+
def _gated_reduce(jac_arr, jac_axes):
|
| 845 |
+
|
| 846 |
+
n_lead = jac_arr.ndim - x.value.ndim
|
| 847 |
+
mask_b = mask.reshape((1,) * n_lead + mask.shape)
|
| 848 |
+
|
| 849 |
+
return lax.reduce_sum_p.bind(
|
| 850 |
+
jac_arr * mask_b,
|
| 851 |
+
axes=jac_axes,
|
| 852 |
+
**extra,
|
| 853 |
+
)
|
| 854 |
+
|
| 855 |
+
if x.has_chunk_axis:
|
| 856 |
+
if 0 in axes:
|
| 857 |
+
non_chunk_axes = tuple(a for a in axes if a != 0)
|
| 858 |
+
jac_reduce_axes = tuple(a + 1 for a in non_chunk_axes) + (1,)
|
| 859 |
+
jac_out = _gated_reduce(x.jac, jac_reduce_axes)
|
| 860 |
+
new_has_chunk_axis = False
|
| 861 |
+
new_chunk_idx = -1
|
| 862 |
+
else:
|
| 863 |
+
jac_reduce_axes = tuple(a + 1 for a in axes)
|
| 864 |
+
jac_out = _gated_reduce(x.jac, jac_reduce_axes)
|
| 865 |
+
new_has_chunk_axis = True
|
| 866 |
+
new_chunk_idx = -1
|
| 867 |
+
else:
|
| 868 |
+
jac_reduce_axes = tuple(a + 2 for a in axes)
|
| 869 |
+
jac_out = _gated_reduce(x.jac, jac_reduce_axes)
|
| 870 |
+
new_has_chunk_axis = False
|
| 871 |
+
new_chunk_idx = x.chunk_idx
|
| 872 |
+
lap_out = jnp.zeros_like(v_out)
|
| 873 |
+
return JLP(
|
| 874 |
+
value=v_out,
|
| 875 |
+
jac=jac_out,
|
| 876 |
+
lap=lap_out,
|
| 877 |
+
level=x.level,
|
| 878 |
+
has_chunk_axis=new_has_chunk_axis,
|
| 879 |
+
chunk_idx=new_chunk_idx,
|
| 880 |
+
)
|
| 881 |
+
|
| 882 |
+
|
| 883 |
+
_RULE_REGISTRY[lax.reduce_max_p] = _reduce_max_rule
|
| 884 |
+
|
| 885 |
+
|
| 886 |
+
def _make_minmax_rule(sign_for_first):
|
| 887 |
+
|
| 888 |
+
def rule(invals, params, W_levels):
|
| 889 |
+
a, b = invals
|
| 890 |
+
if _is_jlp(a) and _is_jlp(b):
|
| 891 |
+
need_promote = (
|
| 892 |
+
(a.level != b.level)
|
| 893 |
+
or (
|
| 894 |
+
not a.has_chunk_axis
|
| 895 |
+
and not b.has_chunk_axis
|
| 896 |
+
and a.chunk_idx != b.chunk_idx
|
| 897 |
+
)
|
| 898 |
+
or (
|
| 899 |
+
a.has_chunk_axis
|
| 900 |
+
and b.has_chunk_axis
|
| 901 |
+
and a.chunk_idx in (0, 1)
|
| 902 |
+
and b.chunk_idx in (0, 1)
|
| 903 |
+
and a.chunk_idx != b.chunk_idx
|
| 904 |
+
)
|
| 905 |
+
)
|
| 906 |
+
if need_promote:
|
| 907 |
+
a, b = _align_jlp_levels(a, b)
|
| 908 |
+
cmp = (a.value - b.value) * sign_for_first
|
| 909 |
+
mask_a = (cmp > 0).astype(a.value.dtype)
|
| 910 |
+
mask_b = 1.0 - mask_a
|
| 911 |
+
v_out = mask_a * a.value + mask_b * b.value
|
| 912 |
+
|
| 913 |
+
jac_a_b = _broadcast_jac_to_value_shape(
|
| 914 |
+
a.jac, v_out.shape, a.has_chunk_axis
|
| 915 |
+
)
|
| 916 |
+
jac_b_b = _broadcast_jac_to_value_shape(
|
| 917 |
+
b.jac, v_out.shape, b.has_chunk_axis
|
| 918 |
+
)
|
| 919 |
+
n_lead_a = jac_a_b.ndim - mask_a.ndim
|
| 920 |
+
n_lead_b = jac_b_b.ndim - mask_b.ndim
|
| 921 |
+
mask_a_lead = mask_a.reshape((1,) * n_lead_a + mask_a.shape)
|
| 922 |
+
mask_b_lead = mask_b.reshape((1,) * n_lead_b + mask_b.shape)
|
| 923 |
+
jac_out = mask_a_lead * jac_a_b + mask_b_lead * jac_b_b
|
| 924 |
+
lap_out = mask_a * jnp.broadcast_to(
|
| 925 |
+
a.lap, v_out.shape
|
| 926 |
+
) + mask_b * jnp.broadcast_to(b.lap, v_out.shape)
|
| 927 |
+
return JLP(
|
| 928 |
+
value=v_out,
|
| 929 |
+
jac=jac_out,
|
| 930 |
+
lap=lap_out,
|
| 931 |
+
level=a.level,
|
| 932 |
+
has_chunk_axis=a.has_chunk_axis,
|
| 933 |
+
chunk_idx=a.chunk_idx,
|
| 934 |
+
)
|
| 935 |
+
if _is_jlp(a):
|
| 936 |
+
cmp = (a.value - b) * sign_for_first
|
| 937 |
+
mask_a = (cmp > 0).astype(a.value.dtype)
|
| 938 |
+
v_out = mask_a * a.value + (1 - mask_a) * b
|
| 939 |
+
n_lead = a.jac.ndim - mask_a.ndim
|
| 940 |
+
mask_a_lead = mask_a.reshape((1,) * n_lead + mask_a.shape)
|
| 941 |
+
jac_out = mask_a_lead * a.jac
|
| 942 |
+
lap_out = mask_a * a.lap
|
| 943 |
+
return JLP(
|
| 944 |
+
value=v_out,
|
| 945 |
+
jac=jac_out,
|
| 946 |
+
lap=lap_out,
|
| 947 |
+
level=a.level,
|
| 948 |
+
has_chunk_axis=a.has_chunk_axis,
|
| 949 |
+
chunk_idx=a.chunk_idx,
|
| 950 |
+
)
|
| 951 |
+
|
| 952 |
+
cmp = (a - b.value) * sign_for_first
|
| 953 |
+
mask_a = (cmp > 0).astype(b.value.dtype)
|
| 954 |
+
v_out = mask_a * a + (1 - mask_a) * b.value
|
| 955 |
+
mask_b = 1 - mask_a
|
| 956 |
+
n_lead = b.jac.ndim - mask_b.ndim
|
| 957 |
+
mask_b_lead = mask_b.reshape((1,) * n_lead + mask_b.shape)
|
| 958 |
+
jac_out = mask_b_lead * b.jac
|
| 959 |
+
lap_out = mask_b * b.lap
|
| 960 |
+
return JLP(
|
| 961 |
+
value=v_out,
|
| 962 |
+
jac=jac_out,
|
| 963 |
+
lap=lap_out,
|
| 964 |
+
level=b.level,
|
| 965 |
+
has_chunk_axis=b.has_chunk_axis,
|
| 966 |
+
chunk_idx=b.chunk_idx,
|
| 967 |
+
)
|
| 968 |
+
|
| 969 |
+
return rule
|
| 970 |
+
|
| 971 |
+
|
| 972 |
+
_RULE_REGISTRY[lax.max_p] = _make_minmax_rule(+1.0)
|
| 973 |
+
_RULE_REGISTRY[lax.min_p] = _make_minmax_rule(-1.0)
|
| 974 |
+
|
| 975 |
+
|
| 976 |
+
def _reshape_jac_for_value(x: JLP, new_value_shape: tuple, new_dimensions):
|
| 977 |
+
|
| 978 |
+
leading_3k = x.jac.shape[0]
|
| 979 |
+
if x.has_chunk_axis:
|
| 980 |
+
M = x.value.shape[0]
|
| 981 |
+
old_trailing = x.value.shape[1:]
|
| 982 |
+
|
| 983 |
+
if len(new_value_shape) >= 1 and new_value_shape[0] == M:
|
| 984 |
+
jac_axes_perm = None
|
| 985 |
+
if new_dimensions is not None:
|
| 986 |
+
jac_axes_perm = (0,) + tuple(d + 1 for d in new_dimensions)
|
| 987 |
+
jac_pre = jnp.transpose(x.jac, jac_axes_perm)
|
| 988 |
+
else:
|
| 989 |
+
jac_pre = x.jac
|
| 990 |
+
new_jac_shape = (leading_3k,) + tuple(new_value_shape)
|
| 991 |
+
jac_new = jnp.reshape(jac_pre, new_jac_shape)
|
| 992 |
+
|
| 993 |
+
return jac_new, True, -1
|
| 994 |
+
|
| 995 |
+
if M == 1:
|
| 996 |
+
assert new_dimensions is None, (
|
| 997 |
+
"reshape with M=1-squeeze + transpose not yet supported"
|
| 998 |
+
)
|
| 999 |
+
|
| 1000 |
+
new_jac_shape = (leading_3k, 1) + tuple(new_value_shape)
|
| 1001 |
+
|
| 1002 |
+
jac_new = jnp.reshape(x.jac, new_jac_shape)
|
| 1003 |
+
return jac_new, False, 0
|
| 1004 |
+
raise NotImplementedError(
|
| 1005 |
+
f"reshape: cannot reshape JLP value {x.value.shape} (has_chunk_axis, M={M}) "
|
| 1006 |
+
f"to {new_value_shape} — chunk axis would be fused/lost."
|
| 1007 |
+
)
|
| 1008 |
+
|
| 1009 |
+
assert new_dimensions is None or all(d >= 0 for d in new_dimensions)
|
| 1010 |
+
jac_pre = x.jac
|
| 1011 |
+
if new_dimensions is not None:
|
| 1012 |
+
jac_axes_perm = (0, 1) + tuple(d + 2 for d in new_dimensions)
|
| 1013 |
+
jac_pre = jnp.transpose(jac_pre, jac_axes_perm)
|
| 1014 |
+
new_jac_shape = (leading_3k, 1) + tuple(new_value_shape)
|
| 1015 |
+
jac_new = jnp.reshape(jac_pre, new_jac_shape)
|
| 1016 |
+
return jac_new, False, x.chunk_idx
|
| 1017 |
+
|
| 1018 |
+
|
| 1019 |
+
def _reshape_rule(invals, params, W_levels):
|
| 1020 |
+
[x] = invals
|
| 1021 |
+
assert _is_jlp(x)
|
| 1022 |
+
new_sizes = tuple(params["new_sizes"])
|
| 1023 |
+
dimensions = params.get("dimensions")
|
| 1024 |
+
|
| 1025 |
+
extra = _shape_bind_params(params, "new_sizes", "dimensions")
|
| 1026 |
+
v_out = lax.reshape_p.bind(
|
| 1027 |
+
x.value, new_sizes=new_sizes, dimensions=dimensions, **extra
|
| 1028 |
+
)
|
| 1029 |
+
lap_out = lax.reshape_p.bind(
|
| 1030 |
+
x.lap, new_sizes=new_sizes, dimensions=dimensions, **extra
|
| 1031 |
+
)
|
| 1032 |
+
jac_new, new_has_chunk, new_idx = _reshape_jac_for_value(x, new_sizes, dimensions)
|
| 1033 |
+
return JLP(
|
| 1034 |
+
value=v_out,
|
| 1035 |
+
jac=jac_new,
|
| 1036 |
+
lap=lap_out,
|
| 1037 |
+
level=x.level,
|
| 1038 |
+
has_chunk_axis=new_has_chunk,
|
| 1039 |
+
chunk_idx=new_idx,
|
| 1040 |
+
)
|
| 1041 |
+
|
| 1042 |
+
|
| 1043 |
+
_RULE_REGISTRY[lax.reshape_p] = _reshape_rule
|
| 1044 |
+
|
| 1045 |
+
|
| 1046 |
+
def _transpose_rule(invals, params, W_levels):
|
| 1047 |
+
[x] = invals
|
| 1048 |
+
assert _is_jlp(x)
|
| 1049 |
+
perm = tuple(params["permutation"])
|
| 1050 |
+
extra = {k: params[k] for k in params if k != "permutation"}
|
| 1051 |
+
v_out = lax.transpose_p.bind(x.value, permutation=perm, **extra)
|
| 1052 |
+
lap_out = lax.transpose_p.bind(x.lap, permutation=perm, **extra)
|
| 1053 |
+
|
| 1054 |
+
if x.has_chunk_axis:
|
| 1055 |
+
if perm[0] != 0:
|
| 1056 |
+
raise NotImplementedError(
|
| 1057 |
+
"transpose: chunk axis (value axis 0) must remain at position 0; "
|
| 1058 |
+
f"got permutation {perm}."
|
| 1059 |
+
)
|
| 1060 |
+
|
| 1061 |
+
jac_perm = (0, 1) + tuple(p + 1 for p in perm[1:])
|
| 1062 |
+
else:
|
| 1063 |
+
jac_perm = (0, 1) + tuple(p + 2 for p in perm)
|
| 1064 |
+
jac_new = lax.transpose_p.bind(x.jac, permutation=jac_perm, **extra)
|
| 1065 |
+
return JLP(
|
| 1066 |
+
value=v_out,
|
| 1067 |
+
jac=jac_new,
|
| 1068 |
+
lap=lap_out,
|
| 1069 |
+
level=x.level,
|
| 1070 |
+
has_chunk_axis=x.has_chunk_axis,
|
| 1071 |
+
chunk_idx=x.chunk_idx,
|
| 1072 |
+
)
|
| 1073 |
+
|
| 1074 |
+
|
| 1075 |
+
_RULE_REGISTRY[lax.transpose_p] = _transpose_rule
|
| 1076 |
+
|
| 1077 |
+
|
| 1078 |
+
def _slice_rule(invals, params, W_levels):
|
| 1079 |
+
[x] = invals
|
| 1080 |
+
assert _is_jlp(x)
|
| 1081 |
+
start = tuple(params["start_indices"])
|
| 1082 |
+
limit = tuple(params["limit_indices"])
|
| 1083 |
+
strides = params.get("strides")
|
| 1084 |
+
extra = {
|
| 1085 |
+
k: params[k]
|
| 1086 |
+
for k in params
|
| 1087 |
+
if k not in ("start_indices", "limit_indices", "strides")
|
| 1088 |
+
}
|
| 1089 |
+
v_out = lax.slice_p.bind(
|
| 1090 |
+
x.value, start_indices=start, limit_indices=limit, strides=strides, **extra
|
| 1091 |
+
)
|
| 1092 |
+
lap_out = lax.slice_p.bind(
|
| 1093 |
+
x.lap, start_indices=start, limit_indices=limit, strides=strides, **extra
|
| 1094 |
+
)
|
| 1095 |
+
|
| 1096 |
+
if x.has_chunk_axis:
|
| 1097 |
+
new_chunk_idx = x.chunk_idx
|
| 1098 |
+
old_M = x.value.shape[0]
|
| 1099 |
+
stride0 = strides[0] if strides is not None else 1
|
| 1100 |
+
v_out_M = v_out.shape[0]
|
| 1101 |
+
chunk_axis_touched = start[0] != 0 or limit[0] != old_M or stride0 != 1
|
| 1102 |
+
if chunk_axis_touched:
|
| 1103 |
+
if v_out_M == 1:
|
| 1104 |
+
new_chunk_idx = start[0]
|
| 1105 |
+
elif stride0 > 1 and v_out_M * stride0 == old_M and start[0] in (0, 1):
|
| 1106 |
+
new_chunk_idx = start[0]
|
| 1107 |
+
elif v_out_M != old_M:
|
| 1108 |
+
raise NotImplementedError(
|
| 1109 |
+
f"slice on chunk axis: unsupported sub-range "
|
| 1110 |
+
f"start={start[0]}, limit={limit[0]}, stride={stride0}, "
|
| 1111 |
+
f"old_M={old_M}, v_out_M={v_out_M}"
|
| 1112 |
+
)
|
| 1113 |
+
jac_start = (0, start[0]) + tuple(start[1:])
|
| 1114 |
+
jac_limit = (x.jac.shape[0], limit[0]) + tuple(limit[1:])
|
| 1115 |
+
jac_strides = None if strides is None else (1, strides[0]) + tuple(strides[1:])
|
| 1116 |
+
jac_out = lax.slice_p.bind(
|
| 1117 |
+
x.jac,
|
| 1118 |
+
start_indices=jac_start,
|
| 1119 |
+
limit_indices=jac_limit,
|
| 1120 |
+
strides=jac_strides,
|
| 1121 |
+
**extra,
|
| 1122 |
+
)
|
| 1123 |
+
return JLP(
|
| 1124 |
+
value=v_out,
|
| 1125 |
+
jac=jac_out,
|
| 1126 |
+
lap=lap_out,
|
| 1127 |
+
level=x.level,
|
| 1128 |
+
has_chunk_axis=True,
|
| 1129 |
+
chunk_idx=new_chunk_idx,
|
| 1130 |
+
)
|
| 1131 |
+
|
| 1132 |
+
jac_start = (0, 0) + tuple(start)
|
| 1133 |
+
jac_limit = (x.jac.shape[0], x.jac.shape[1]) + tuple(limit)
|
| 1134 |
+
jac_strides = None if strides is None else (1, 1) + tuple(strides)
|
| 1135 |
+
jac_out = lax.slice_p.bind(
|
| 1136 |
+
x.jac,
|
| 1137 |
+
start_indices=jac_start,
|
| 1138 |
+
limit_indices=jac_limit,
|
| 1139 |
+
strides=jac_strides,
|
| 1140 |
+
**extra,
|
| 1141 |
+
)
|
| 1142 |
+
return JLP(
|
| 1143 |
+
value=v_out,
|
| 1144 |
+
jac=jac_out,
|
| 1145 |
+
lap=lap_out,
|
| 1146 |
+
level=x.level,
|
| 1147 |
+
has_chunk_axis=False,
|
| 1148 |
+
chunk_idx=x.chunk_idx,
|
| 1149 |
+
)
|
| 1150 |
+
|
| 1151 |
+
|
| 1152 |
+
_RULE_REGISTRY[lax.slice_p] = _slice_rule
|
| 1153 |
+
|
| 1154 |
+
|
| 1155 |
+
def _squeeze_rule(invals, params, W_levels):
|
| 1156 |
+
[x] = invals
|
| 1157 |
+
assert _is_jlp(x)
|
| 1158 |
+
dims = tuple(params["dimensions"])
|
| 1159 |
+
extra = {k: params[k] for k in params if k != "dimensions"}
|
| 1160 |
+
v_out = lax.squeeze_p.bind(x.value, dimensions=dims, **extra)
|
| 1161 |
+
lap_out = lax.squeeze_p.bind(x.lap, dimensions=dims, **extra)
|
| 1162 |
+
if x.has_chunk_axis and 0 in dims:
|
| 1163 |
+
non_chunk_dims = tuple(d for d in dims if d != 0)
|
| 1164 |
+
jac_dims = tuple(d + 1 for d in non_chunk_dims)
|
| 1165 |
+
|
| 1166 |
+
if jac_dims:
|
| 1167 |
+
jac_out = lax.squeeze_p.bind(x.jac, dimensions=jac_dims, **extra)
|
| 1168 |
+
else:
|
| 1169 |
+
jac_out = x.jac
|
| 1170 |
+
return JLP(
|
| 1171 |
+
value=v_out,
|
| 1172 |
+
jac=jac_out,
|
| 1173 |
+
lap=lap_out,
|
| 1174 |
+
level=x.level,
|
| 1175 |
+
has_chunk_axis=False,
|
| 1176 |
+
chunk_idx=x.chunk_idx,
|
| 1177 |
+
)
|
| 1178 |
+
if x.has_chunk_axis:
|
| 1179 |
+
jac_dims = tuple(d + 1 for d in dims)
|
| 1180 |
+
else:
|
| 1181 |
+
jac_dims = tuple(d + 2 for d in dims)
|
| 1182 |
+
if jac_dims:
|
| 1183 |
+
jac_out = lax.squeeze_p.bind(x.jac, dimensions=jac_dims, **extra)
|
| 1184 |
+
else:
|
| 1185 |
+
jac_out = x.jac
|
| 1186 |
+
return JLP(
|
| 1187 |
+
value=v_out,
|
| 1188 |
+
jac=jac_out,
|
| 1189 |
+
lap=lap_out,
|
| 1190 |
+
level=x.level,
|
| 1191 |
+
has_chunk_axis=x.has_chunk_axis,
|
| 1192 |
+
chunk_idx=x.chunk_idx,
|
| 1193 |
+
)
|
| 1194 |
+
|
| 1195 |
+
|
| 1196 |
+
_RULE_REGISTRY[lax.squeeze_p] = _squeeze_rule
|
| 1197 |
+
|
| 1198 |
+
|
| 1199 |
+
_stack_p = lax.stack_p
|
| 1200 |
+
|
| 1201 |
+
|
| 1202 |
+
def _stack_rule(invals, params, W_levels):
|
| 1203 |
+
del W_levels
|
| 1204 |
+
axis = int(params["axis"])
|
| 1205 |
+
extra = {k: params[k] for k in params if k != "axis"}
|
| 1206 |
+
|
| 1207 |
+
jlp_inputs = [v for v in invals if _is_jlp(v)]
|
| 1208 |
+
levels = {v.level for v in jlp_inputs}
|
| 1209 |
+
assert len(levels) == 1, f"stack: mixed JLP levels {levels}"
|
| 1210 |
+
level = next(iter(levels))
|
| 1211 |
+
has_chunk = jlp_inputs[0].has_chunk_axis
|
| 1212 |
+
chunk_idx = jlp_inputs[0].chunk_idx
|
| 1213 |
+
for v in jlp_inputs[1:]:
|
| 1214 |
+
assert v.has_chunk_axis == has_chunk and v.chunk_idx == chunk_idx, (
|
| 1215 |
+
"stack: inconsistent chunk metadata"
|
| 1216 |
+
)
|
| 1217 |
+
|
| 1218 |
+
if has_chunk and axis == 0:
|
| 1219 |
+
raise NotImplementedError(
|
| 1220 |
+
"stack: inserting an axis before the JLP chunk axis is not supported"
|
| 1221 |
+
)
|
| 1222 |
+
|
| 1223 |
+
values = [v.value if _is_jlp(v) else v for v in invals]
|
| 1224 |
+
value_out = _stack_p.bind(*values, axis=axis, **extra)
|
| 1225 |
+
|
| 1226 |
+
laps = [v.lap if _is_jlp(v) else jnp.zeros_like(v) for v in invals]
|
| 1227 |
+
lap_out = _stack_p.bind(*laps, axis=axis, **extra)
|
| 1228 |
+
|
| 1229 |
+
jac_axis = axis + (1 if has_chunk else 2)
|
| 1230 |
+
ref_jac = jlp_inputs[0].jac
|
| 1231 |
+
jacs = [v.jac if _is_jlp(v) else jnp.zeros_like(ref_jac) for v in invals]
|
| 1232 |
+
jac_out = _stack_p.bind(*jacs, axis=jac_axis, **extra)
|
| 1233 |
+
return JLP(
|
| 1234 |
+
value=value_out,
|
| 1235 |
+
jac=jac_out,
|
| 1236 |
+
lap=lap_out,
|
| 1237 |
+
level=level,
|
| 1238 |
+
has_chunk_axis=has_chunk,
|
| 1239 |
+
chunk_idx=chunk_idx,
|
| 1240 |
+
)
|
| 1241 |
+
|
| 1242 |
+
|
| 1243 |
+
_RULE_REGISTRY[_stack_p] = _stack_rule
|
| 1244 |
+
|
| 1245 |
+
|
| 1246 |
+
def _concatenate_rule(invals, params, W_levels):
|
| 1247 |
+
dim = params["dimension"]
|
| 1248 |
+
extra = {k: params[k] for k in params if k != "dimension"}
|
| 1249 |
+
|
| 1250 |
+
jlp_inputs = [v for v in invals if _is_jlp(v)]
|
| 1251 |
+
levels = set(v.level for v in jlp_inputs)
|
| 1252 |
+
assert len(levels) == 1, f"concatenate: mixed JLP levels {levels}"
|
| 1253 |
+
level = next(iter(levels))
|
| 1254 |
+
has_chunk = jlp_inputs[0].has_chunk_axis
|
| 1255 |
+
chunk_idx = jlp_inputs[0].chunk_idx
|
| 1256 |
+
for v in jlp_inputs[1:]:
|
| 1257 |
+
assert v.has_chunk_axis == has_chunk and v.chunk_idx == chunk_idx, (
|
| 1258 |
+
"concatenate: inconsistent chunk metadata"
|
| 1259 |
+
)
|
| 1260 |
+
if has_chunk and dim == 0:
|
| 1261 |
+
raise NotImplementedError("concatenate on chunk axis not supported")
|
| 1262 |
+
|
| 1263 |
+
values = [v.value if _is_jlp(v) else v for v in invals]
|
| 1264 |
+
v_out = lax.concatenate_p.bind(*values, dimension=dim, **extra)
|
| 1265 |
+
laps = [v.lap if _is_jlp(v) else jnp.zeros_like(v) for v in invals]
|
| 1266 |
+
lap_out = lax.concatenate_p.bind(*laps, dimension=dim, **extra)
|
| 1267 |
+
|
| 1268 |
+
jac_dim = dim + 2 if not has_chunk else dim + 1
|
| 1269 |
+
jacs = []
|
| 1270 |
+
for v in invals:
|
| 1271 |
+
if _is_jlp(v):
|
| 1272 |
+
jacs.append(v.jac)
|
| 1273 |
+
else:
|
| 1274 |
+
shape_v = v.shape if hasattr(v, "shape") else jnp.asarray(v).shape
|
| 1275 |
+
if has_chunk:
|
| 1276 |
+
M = jlp_inputs[0].jac.shape[1]
|
| 1277 |
+
jac_shape = (jlp_inputs[0].jac.shape[0], M) + tuple(shape_v[1:])
|
| 1278 |
+
else:
|
| 1279 |
+
M = jlp_inputs[0].jac.shape[1]
|
| 1280 |
+
jac_shape = (jlp_inputs[0].jac.shape[0], M) + tuple(shape_v)
|
| 1281 |
+
jacs.append(jnp.zeros(jac_shape, dtype=jlp_inputs[0].jac.dtype))
|
| 1282 |
+
jac_out = lax.concatenate_p.bind(*jacs, dimension=jac_dim, **extra)
|
| 1283 |
+
return JLP(
|
| 1284 |
+
value=v_out,
|
| 1285 |
+
jac=jac_out,
|
| 1286 |
+
lap=lap_out,
|
| 1287 |
+
level=level,
|
| 1288 |
+
has_chunk_axis=has_chunk,
|
| 1289 |
+
chunk_idx=chunk_idx,
|
| 1290 |
+
)
|
| 1291 |
+
|
| 1292 |
+
|
| 1293 |
+
_RULE_REGISTRY[lax.concatenate_p] = _concatenate_rule
|
| 1294 |
+
|
| 1295 |
+
|
| 1296 |
+
def _jit_p_rule(invals, params, W_levels):
|
| 1297 |
+
|
| 1298 |
+
inner_jaxpr = params["jaxpr"]
|
| 1299 |
+
j = inner_jaxpr.jaxpr
|
| 1300 |
+
consts = inner_jaxpr.consts
|
| 1301 |
+
env: dict = {}
|
| 1302 |
+
for cv, c in zip(j.constvars, consts):
|
| 1303 |
+
env[cv] = c
|
| 1304 |
+
for iv, x in zip(j.invars, invals):
|
| 1305 |
+
env[iv] = x
|
| 1306 |
+
for eqn in j.eqns:
|
| 1307 |
+
outvals = _eval_eqn(eqn, env, W_levels)
|
| 1308 |
+
for ov, ov_val in zip(eqn.outvars, outvals):
|
| 1309 |
+
env[ov] = ov_val
|
| 1310 |
+
return [env[ov] for ov in j.outvars]
|
| 1311 |
+
|
| 1312 |
+
|
| 1313 |
+
from jax._src import pjit as _pjit_module
|
| 1314 |
+
|
| 1315 |
+
_RULE_REGISTRY[_pjit_module.jit_p] = _jit_p_rule
|
| 1316 |
+
|
| 1317 |
+
|
| 1318 |
+
def _quadrilinear_merge_rule(invals, params, W_levels):
|
| 1319 |
+
|
| 1320 |
+
T, u_a, u_b = invals
|
| 1321 |
+
assert not _is_jlp(T), "quadrilinear_merge: T must be plain"
|
| 1322 |
+
assert _is_jlp(u_a) and _is_jlp(u_b), "quadrilinear_merge: u_a, u_b must be JLPs"
|
| 1323 |
+
assert u_a.level == u_b.level, (
|
| 1324 |
+
f"quadrilinear_merge: leg levels differ {u_a.level} vs {u_b.level}"
|
| 1325 |
+
)
|
| 1326 |
+
assert u_a.has_chunk_axis and u_b.has_chunk_axis, (
|
| 1327 |
+
"quadrilinear_merge requires the compiled chunk axis"
|
| 1328 |
+
)
|
| 1329 |
+
|
| 1330 |
+
k = u_a.level
|
| 1331 |
+
new_k = 2 * k
|
| 1332 |
+
G, d_r, _, _ = T.shape
|
| 1333 |
+
d_m_eff = G * d_r
|
| 1334 |
+
M = u_a.value.shape[0]
|
| 1335 |
+
assert u_b.value.shape[0] == M, "quadrilinear_merge: chunked legs must agree on M"
|
| 1336 |
+
u_a_2d = u_a.value.reshape(M, G, d_r)
|
| 1337 |
+
u_b_2d = u_b.value.reshape(M, G, d_r)
|
| 1338 |
+
raw_value_2d = jnp.einsum("ijkl,mik,mil->mij", T, u_a_2d, u_b_2d)
|
| 1339 |
+
raw_value = raw_value_2d.reshape(M, d_m_eff)
|
| 1340 |
+
ua_jac_2d = u_a.jac.reshape(u_a.jac.shape[0], M, G, d_r)
|
| 1341 |
+
ub_jac_2d = u_b.jac.reshape(u_b.jac.shape[0], M, G, d_r)
|
| 1342 |
+
jac_upper_2d = jnp.einsum(
|
| 1343 |
+
"ijkl,Amik,mil->Amij",
|
| 1344 |
+
T,
|
| 1345 |
+
ua_jac_2d,
|
| 1346 |
+
u_b_2d,
|
| 1347 |
+
)
|
| 1348 |
+
jac_lower_2d = jnp.einsum(
|
| 1349 |
+
"ijkl,mik,Bmil->Bmij",
|
| 1350 |
+
T,
|
| 1351 |
+
u_a_2d,
|
| 1352 |
+
ub_jac_2d,
|
| 1353 |
+
)
|
| 1354 |
+
jac_upper = jac_upper_2d.reshape(jac_upper_2d.shape[0], M, d_m_eff)
|
| 1355 |
+
jac_lower = jac_lower_2d.reshape(jac_lower_2d.shape[0], M, d_m_eff)
|
| 1356 |
+
jac_out = jnp.concatenate([jac_upper, jac_lower], axis=0)
|
| 1357 |
+
ua_lap_2d = u_a.lap.reshape(M, G, d_r)
|
| 1358 |
+
ub_lap_2d = u_b.lap.reshape(M, G, d_r)
|
| 1359 |
+
term1_2d = jnp.einsum(
|
| 1360 |
+
"ijkl,mik,mil->mij",
|
| 1361 |
+
T,
|
| 1362 |
+
ua_lap_2d,
|
| 1363 |
+
u_b_2d,
|
| 1364 |
+
)
|
| 1365 |
+
term2_2d = jnp.einsum(
|
| 1366 |
+
"ijkl,mik,mil->mij",
|
| 1367 |
+
T,
|
| 1368 |
+
u_a_2d,
|
| 1369 |
+
ub_lap_2d,
|
| 1370 |
+
)
|
| 1371 |
+
W_2k = _W_at_level(W_levels, new_k)
|
| 1372 |
+
W_off = W_2k[:, : 3 * k, 3 * k :]
|
| 1373 |
+
cross_2d = jnp.einsum(
|
| 1374 |
+
"ijkl,Amik,Bmil,mAB->mij",
|
| 1375 |
+
T,
|
| 1376 |
+
ua_jac_2d,
|
| 1377 |
+
ub_jac_2d,
|
| 1378 |
+
W_off,
|
| 1379 |
+
)
|
| 1380 |
+
lap_out_2d = term1_2d + term2_2d + 2.0 * cross_2d
|
| 1381 |
+
lap_out = lap_out_2d.reshape(M, d_m_eff)
|
| 1382 |
+
return JLP(
|
| 1383 |
+
value=raw_value,
|
| 1384 |
+
jac=jac_out,
|
| 1385 |
+
lap=lap_out,
|
| 1386 |
+
level=new_k,
|
| 1387 |
+
has_chunk_axis=True,
|
| 1388 |
+
chunk_idx=-1,
|
| 1389 |
+
)
|
| 1390 |
+
|
| 1391 |
+
|
| 1392 |
+
_RULE_REGISTRY[quadrilinear_merge_p] = _quadrilinear_merge_rule
|
| 1393 |
+
|
| 1394 |
+
|
| 1395 |
+
def _eval_eqn(eqn, env, W_levels):
|
| 1396 |
+
invals = []
|
| 1397 |
+
for v in eqn.invars:
|
| 1398 |
+
if isinstance(v, Literal):
|
| 1399 |
+
invals.append(v.val)
|
| 1400 |
+
else:
|
| 1401 |
+
invals.append(env[v])
|
| 1402 |
+
has_jlp = any(_is_jlp(x) for x in invals)
|
| 1403 |
+
if not has_jlp:
|
| 1404 |
+
bind_params = eqn.primitive.get_bind_params(eqn.params)
|
| 1405 |
+
outvals = eqn.primitive.bind(*invals, **bind_params)
|
| 1406 |
+
if not eqn.primitive.multiple_results:
|
| 1407 |
+
outvals = [outvals]
|
| 1408 |
+
return outvals
|
| 1409 |
+
rule = _RULE_REGISTRY.get(eqn.primitive)
|
| 1410 |
+
if rule is None:
|
| 1411 |
+
raise NotImplementedError(
|
| 1412 |
+
f"custom_lap: no rule for primitive {eqn.primitive.name!r}. "
|
| 1413 |
+
f"eqn = {eqn}. Register a rule in energy/custom_lap.py."
|
| 1414 |
+
)
|
| 1415 |
+
outvals = rule(invals, eqn.params, W_levels)
|
| 1416 |
+
if not eqn.primitive.multiple_results:
|
| 1417 |
+
outvals = [outvals]
|
| 1418 |
+
return outvals
|
| 1419 |
+
|
| 1420 |
+
|
| 1421 |
+
def _trace_z(fn, z, N, W_levels):
|
| 1422 |
+
|
| 1423 |
+
z = jnp.asarray(z)
|
| 1424 |
+
assert z.shape == (N, 3), f"z must be [N={N}, 3]; got {z.shape}"
|
| 1425 |
+
eye3 = jnp.eye(3, dtype=z.dtype)
|
| 1426 |
+
z_jac = jnp.broadcast_to(eye3[:, None, :], (3, N, 3))
|
| 1427 |
+
z_lap = jnp.zeros((N, 3), dtype=z.dtype)
|
| 1428 |
+
z_jlp = JLP(
|
| 1429 |
+
value=z,
|
| 1430 |
+
jac=z_jac,
|
| 1431 |
+
lap=z_lap,
|
| 1432 |
+
level=1,
|
| 1433 |
+
has_chunk_axis=True,
|
| 1434 |
+
chunk_idx=-1,
|
| 1435 |
+
)
|
| 1436 |
+
closed = jax.make_jaxpr(fn)(z)
|
| 1437 |
+
jaxpr = closed.jaxpr
|
| 1438 |
+
consts = closed.consts
|
| 1439 |
+
env: dict = {}
|
| 1440 |
+
for cv, c in zip(jaxpr.constvars, consts):
|
| 1441 |
+
env[cv] = c
|
| 1442 |
+
env[jaxpr.invars[0]] = z_jlp
|
| 1443 |
+
for eqn in jaxpr.eqns:
|
| 1444 |
+
outvals = _eval_eqn(eqn, env, W_levels)
|
| 1445 |
+
for ov, ov_val in zip(eqn.outvars, outvals):
|
| 1446 |
+
env[ov] = ov_val
|
| 1447 |
+
return [env[ov] for ov in jaxpr.outvars]
|
| 1448 |
+
|
| 1449 |
+
|
| 1450 |
+
def _canonical_jac(jlp: JLP):
|
| 1451 |
+
|
| 1452 |
+
if jlp.jac.shape[1] != 1:
|
| 1453 |
+
jac = jnp.swapaxes(jlp.jac, 0, 1)
|
| 1454 |
+
return jac.reshape((jlp.jac.shape[0] * jlp.jac.shape[1],) + jlp.jac.shape[2:])
|
| 1455 |
+
return jlp.jac[:, 0]
|
| 1456 |
+
|
| 1457 |
+
|
| 1458 |
+
def custom_forward_laplacian_with_jac(fn: Callable, W_levels: list, N: int) -> Callable:
|
| 1459 |
+
|
| 1460 |
+
def lap_jac_fn(z):
|
| 1461 |
+
outs = _trace_z(fn, z, N, W_levels)
|
| 1462 |
+
if len(outs) == 1:
|
| 1463 |
+
out = outs[0]
|
| 1464 |
+
if _is_jlp(out):
|
| 1465 |
+
return out.value, _canonical_jac(out), out.lap
|
| 1466 |
+
value = out
|
| 1467 |
+
return (
|
| 1468 |
+
value,
|
| 1469 |
+
jnp.zeros((3 * N,) + value.shape, dtype=value.dtype),
|
| 1470 |
+
jnp.zeros_like(value),
|
| 1471 |
+
)
|
| 1472 |
+
values = tuple(o.value if _is_jlp(o) else o for o in outs)
|
| 1473 |
+
jacs = tuple(
|
| 1474 |
+
_canonical_jac(o)
|
| 1475 |
+
if _is_jlp(o)
|
| 1476 |
+
else jnp.zeros((3 * N,) + o.shape, dtype=o.dtype)
|
| 1477 |
+
for o in outs
|
| 1478 |
+
)
|
| 1479 |
+
laps = tuple(o.lap if _is_jlp(o) else jnp.zeros_like(o) for o in outs)
|
| 1480 |
+
return values, jacs, laps
|
| 1481 |
+
|
| 1482 |
+
return lap_jac_fn
|
| 1483 |
+
|
| 1484 |
+
|
| 1485 |
+
__all__ = [
|
| 1486 |
+
"JLP",
|
| 1487 |
+
"build_W_levels",
|
| 1488 |
+
"custom_forward_laplacian_with_jac",
|
| 1489 |
+
"custom_lap_active",
|
| 1490 |
+
"use_custom_lap",
|
| 1491 |
+
]
|
src/hamiltonzero/energy/frame.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
from collections.abc import Sequence
|
| 8 |
+
|
| 9 |
+
import jax
|
| 10 |
+
import jax.numpy as jnp
|
| 11 |
+
import numpy as np
|
| 12 |
+
from jaxtyping import Array
|
| 13 |
+
|
| 14 |
+
from hamiltonzero.compiled.types import EnergyFrame, EnergyInputs, EnergyMasks
|
| 15 |
+
from hamiltonzero.energy.custom_lap import build_W_levels
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _real_dtype(dtype):
|
| 19 |
+
return jnp.real(jnp.zeros((), dtype)).dtype
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _host_eigvalsh(x):
|
| 23 |
+
out_dtype = _real_dtype(x.dtype)
|
| 24 |
+
out_shape = jax.ShapeDtypeStruct(x.shape[:-1], out_dtype)
|
| 25 |
+
|
| 26 |
+
def callback(a):
|
| 27 |
+
values = np.linalg.eigvalsh(np.asarray(a))
|
| 28 |
+
return values.astype(np.dtype(out_dtype))
|
| 29 |
+
|
| 30 |
+
return jax.pure_callback(
|
| 31 |
+
callback,
|
| 32 |
+
out_shape,
|
| 33 |
+
x,
|
| 34 |
+
vmap_method="sequential",
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _compute_custom_lap_views_batched(J_full, mask, mu, eps):
|
| 39 |
+
n = J_full.shape[1]
|
| 40 |
+
n_systems = J_full.shape[0]
|
| 41 |
+
dtype = J_full.dtype
|
| 42 |
+
J_matrix = jnp.transpose(J_full, (0, 1, 3, 2, 4)).reshape(n_systems, 3 * n, 3 * n)
|
| 43 |
+
J_matrix_symmetric = 0.5 * (J_matrix + jnp.swapaxes(J_matrix, -1, -2))
|
| 44 |
+
mask_3 = jnp.repeat(mask.astype(dtype), 3, axis=-1)
|
| 45 |
+
eye_3n = jnp.eye(3 * n, dtype=dtype)
|
| 46 |
+
masked_identity = eye_3n[None] * mask_3[:, None, :]
|
| 47 |
+
J_eff = (J_matrix_symmetric - (mu + eps)[:, None, None] * masked_identity) / 4.0
|
| 48 |
+
J_eff = 0.5 * (J_eff + jnp.swapaxes(J_eff, -1, -2))
|
| 49 |
+
|
| 50 |
+
eigh_epsilon = jnp.asarray(1e-6, dtype=_real_dtype(dtype))
|
| 51 |
+
eigenvalues = _host_eigvalsh(J_eff + eigh_epsilon * eye_3n[None]) - eigh_epsilon
|
| 52 |
+
lam_max = jnp.max(eigenvalues, axis=-1)
|
| 53 |
+
delta_mu = jax.nn.relu(4.0 * (lam_max + eps))
|
| 54 |
+
shift = delta_mu / 4.0
|
| 55 |
+
J_eff = J_eff - shift[:, None, None] * eye_3n[None]
|
| 56 |
+
mu_eff = mu + delta_mu
|
| 57 |
+
casimir_per_site = (mu_eff + eps) * jnp.asarray(0.75, dtype=dtype)
|
| 58 |
+
radial_const = -casimir_per_site * mask.astype(dtype).sum(axis=-1)
|
| 59 |
+
return J_eff, radial_const
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _compute_custom_lap_views(J_full, mask, mu, eps):
|
| 63 |
+
values = _compute_custom_lap_views_batched(
|
| 64 |
+
J_full[None],
|
| 65 |
+
mask[None],
|
| 66 |
+
jnp.asarray(mu)[None],
|
| 67 |
+
jnp.asarray(eps)[None],
|
| 68 |
+
)
|
| 69 |
+
return tuple(value[0] for value in values)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def build_energy_inputs(J_full, h, mask, mu, eps) -> EnergyInputs:
|
| 73 |
+
dtype = J_full.dtype
|
| 74 |
+
mu_array = jnp.asarray(mu, dtype=dtype)
|
| 75 |
+
eps_array = jnp.asarray(eps, dtype=dtype)
|
| 76 |
+
J_eff, radial_const = _compute_custom_lap_views(
|
| 77 |
+
J_full,
|
| 78 |
+
mask,
|
| 79 |
+
mu_array,
|
| 80 |
+
eps_array,
|
| 81 |
+
)
|
| 82 |
+
return EnergyInputs(
|
| 83 |
+
custom_lap_J_eff=J_eff,
|
| 84 |
+
custom_lap_radial_const=radial_const,
|
| 85 |
+
one_body_fields=(h,),
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def block_permutation3(perm: Array) -> Array:
|
| 90 |
+
|
| 91 |
+
components = jnp.arange(3, dtype=perm.dtype)
|
| 92 |
+
return (perm[:, None] * 3 + components[None, :]).reshape(-1)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def route_one_body_field(field: Array, perm: Array) -> Array:
|
| 96 |
+
|
| 97 |
+
return jnp.take(field, perm, axis=0)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def route_one_body_fields(
|
| 101 |
+
fields: Sequence[Array],
|
| 102 |
+
perm: Array,
|
| 103 |
+
) -> tuple[Array, ...]:
|
| 104 |
+
|
| 105 |
+
return tuple(route_one_body_field(field, perm) for field in fields)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def route_J_eff(J_eff: Array, perm: Array) -> Array:
|
| 109 |
+
|
| 110 |
+
block_perm = block_permutation3(perm)
|
| 111 |
+
return jnp.take(jnp.take(J_eff, block_perm, axis=0), block_perm, axis=1)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def route_energy_inputs(energy_inputs: EnergyInputs, perm: Array) -> EnergyInputs:
|
| 115 |
+
return EnergyInputs(
|
| 116 |
+
custom_lap_J_eff=route_J_eff(energy_inputs.custom_lap_J_eff, perm),
|
| 117 |
+
custom_lap_radial_const=energy_inputs.custom_lap_radial_const,
|
| 118 |
+
one_body_fields=route_one_body_fields(energy_inputs.one_body_fields, perm),
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def compile_energy_frame(
|
| 123 |
+
energy_inputs: EnergyInputs,
|
| 124 |
+
real_mask: Array,
|
| 125 |
+
balanced_mask: Array,
|
| 126 |
+
perm: Array,
|
| 127 |
+
) -> EnergyFrame:
|
| 128 |
+
|
| 129 |
+
J_eff = route_J_eff(energy_inputs.custom_lap_J_eff, perm)
|
| 130 |
+
n_sites = int(perm.shape[0])
|
| 131 |
+
frame = EnergyFrame(
|
| 132 |
+
custom_lap_J_eff=J_eff,
|
| 133 |
+
w_levels=tuple(build_W_levels(J_eff, n_sites)),
|
| 134 |
+
custom_lap_radial_const=energy_inputs.custom_lap_radial_const,
|
| 135 |
+
one_body_fields=route_one_body_fields(energy_inputs.one_body_fields, perm),
|
| 136 |
+
masks=EnergyMasks(
|
| 137 |
+
real=route_one_body_field(real_mask, perm),
|
| 138 |
+
balanced=balanced_mask,
|
| 139 |
+
),
|
| 140 |
+
)
|
| 141 |
+
return frame
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
__all__ = [
|
| 145 |
+
"block_permutation3",
|
| 146 |
+
"build_energy_inputs",
|
| 147 |
+
"compile_energy_frame",
|
| 148 |
+
"route_energy_inputs",
|
| 149 |
+
"route_J_eff",
|
| 150 |
+
"route_one_body_field",
|
| 151 |
+
"route_one_body_fields",
|
| 152 |
+
]
|
src/hamiltonzero/energy/kernel.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
from typing import Any, Callable
|
| 8 |
+
|
| 9 |
+
import jax
|
| 10 |
+
import jax.numpy as jnp
|
| 11 |
+
from jaxtyping import Array, Complex, Float
|
| 12 |
+
|
| 13 |
+
from hamiltonzero.compiled.execute import execute_wavefunction
|
| 14 |
+
from hamiltonzero.energy.custom_lap import (
|
| 15 |
+
custom_forward_laplacian_with_jac,
|
| 16 |
+
use_custom_lap,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _right_su2_chart_jet(
|
| 21 |
+
q: Float[Array, "N 4"],
|
| 22 |
+
z: Float[Array, "N 3"],
|
| 23 |
+
) -> Float[Array, "N 4"]:
|
| 24 |
+
|
| 25 |
+
q0, q1, q2, q3 = q[:, 0], q[:, 1], q[:, 2], q[:, 3]
|
| 26 |
+
zx, zy, zz = z[:, 0], z[:, 1], z[:, 2]
|
| 27 |
+
u0 = 1.0 - 0.5 * (zx * zx + zy * zy + zz * zz)
|
| 28 |
+
u1, u2, u3 = zz, zy, zx
|
| 29 |
+
return jnp.stack(
|
| 30 |
+
[
|
| 31 |
+
q0 * u0 - q1 * u1 - q2 * u2 - q3 * u3,
|
| 32 |
+
q0 * u1 + q1 * u0 + q2 * u3 - q3 * u2,
|
| 33 |
+
q0 * u2 - q1 * u3 + q2 * u0 + q3 * u1,
|
| 34 |
+
q0 * u3 + q1 * u2 - q2 * u1 + q3 * u0,
|
| 35 |
+
],
|
| 36 |
+
axis=-1,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _custom_lap_single_finetune(
|
| 41 |
+
model: Callable,
|
| 42 |
+
q: Float[Array, "N 4"],
|
| 43 |
+
t,
|
| 44 |
+
energy_frame: Any,
|
| 45 |
+
):
|
| 46 |
+
if len(energy_frame.one_body_fields) != 1:
|
| 47 |
+
raise ValueError("custom-Laplacian energy requires exactly one one-body field")
|
| 48 |
+
return _custom_lap_single_from_frame(
|
| 49 |
+
model,
|
| 50 |
+
None,
|
| 51 |
+
q,
|
| 52 |
+
t,
|
| 53 |
+
J_eff=energy_frame.custom_lap_J_eff,
|
| 54 |
+
W_levels=energy_frame.w_levels,
|
| 55 |
+
field_xyz=energy_frame.one_body_fields[0],
|
| 56 |
+
radial_const=energy_frame.custom_lap_radial_const,
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _custom_lap_single_prebuilt(
|
| 61 |
+
model: Callable,
|
| 62 |
+
q: Float[Array, "N 4"],
|
| 63 |
+
t,
|
| 64 |
+
frame: Any,
|
| 65 |
+
):
|
| 66 |
+
if len(frame.one_body_fields) != 1:
|
| 67 |
+
raise ValueError("custom-Laplacian energy requires exactly one one-body field")
|
| 68 |
+
return _custom_lap_single_from_frame(
|
| 69 |
+
model,
|
| 70 |
+
None,
|
| 71 |
+
q,
|
| 72 |
+
t,
|
| 73 |
+
J_eff=frame.custom_lap_J_eff,
|
| 74 |
+
W_levels=frame.w_levels,
|
| 75 |
+
field_xyz=frame.one_body_fields[0],
|
| 76 |
+
radial_const=frame.custom_lap_radial_const,
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _custom_lap_single_from_frame(
|
| 81 |
+
model: Callable,
|
| 82 |
+
ctx: Any,
|
| 83 |
+
q: Float[Array, "N 4"],
|
| 84 |
+
t,
|
| 85 |
+
*,
|
| 86 |
+
J_eff,
|
| 87 |
+
W_levels,
|
| 88 |
+
field_xyz,
|
| 89 |
+
radial_const,
|
| 90 |
+
):
|
| 91 |
+
N = q.shape[0]
|
| 92 |
+
|
| 93 |
+
def f_entry(z):
|
| 94 |
+
q_pert = _right_su2_chart_jet(q, z)
|
| 95 |
+
re, im = model(q_pert, ctx, t)
|
| 96 |
+
return jnp.stack([re, im])
|
| 97 |
+
|
| 98 |
+
with use_custom_lap():
|
| 99 |
+
_value, jac_pair, lap_pair = custom_forward_laplacian_with_jac(
|
| 100 |
+
f_entry,
|
| 101 |
+
W_levels,
|
| 102 |
+
N,
|
| 103 |
+
)(jnp.zeros((N, 3), dtype=q.dtype))
|
| 104 |
+
|
| 105 |
+
tr_total = lap_pair[0] + 1j * lap_pair[1]
|
| 106 |
+
|
| 107 |
+
g_lie = jac_pair[:, 0] + 1j * jac_pair[:, 1]
|
| 108 |
+
quad_total = jnp.einsum("a,ab,b->", g_lie, J_eff.astype(g_lie.dtype), g_lie)
|
| 109 |
+
|
| 110 |
+
la_xyz = 0.5 * g_lie.reshape(N, 3)
|
| 111 |
+
field = (1j * jnp.einsum("ic,ic->", field_xyz.astype(g_lie.dtype), la_xyz)).astype(
|
| 112 |
+
g_lie.dtype
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
total = tr_total + quad_total + radial_const.astype(g_lie.dtype) + field
|
| 116 |
+
|
| 117 |
+
zero = jnp.zeros_like(total)
|
| 118 |
+
exchange = total - field
|
| 119 |
+
return total, exchange, zero, field
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _vmc_energy_custom_lap_finetune(
|
| 123 |
+
model: Callable,
|
| 124 |
+
energy_frame: Any,
|
| 125 |
+
q: Float[Array, "... N 4"],
|
| 126 |
+
t=0.0,
|
| 127 |
+
*,
|
| 128 |
+
chunk_size: int | None = None,
|
| 129 |
+
) -> tuple[
|
| 130 |
+
Complex[Array, "..."],
|
| 131 |
+
Complex[Array, "..."],
|
| 132 |
+
Complex[Array, "..."],
|
| 133 |
+
Complex[Array, "..."],
|
| 134 |
+
]:
|
| 135 |
+
|
| 136 |
+
def single(qq, tt):
|
| 137 |
+
return _custom_lap_single_finetune(model, qq, tt, energy_frame)
|
| 138 |
+
|
| 139 |
+
return _run_custom_lap_batch(single, q, t, chunk_size)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def _vmc_energy_custom_lap_prebuilt(
|
| 143 |
+
kernel: Any,
|
| 144 |
+
tree: Any,
|
| 145 |
+
energy_frame: Any,
|
| 146 |
+
q: Float[Array, "... N 4"],
|
| 147 |
+
*,
|
| 148 |
+
chunk_size: int | None = None,
|
| 149 |
+
) -> tuple[
|
| 150 |
+
Complex[Array, "..."],
|
| 151 |
+
Complex[Array, "..."],
|
| 152 |
+
Complex[Array, "..."],
|
| 153 |
+
Complex[Array, "..."],
|
| 154 |
+
]:
|
| 155 |
+
def model(q_pert, _ctx, _t):
|
| 156 |
+
return execute_wavefunction(kernel, tree, q_pert)
|
| 157 |
+
|
| 158 |
+
def single(qq, tt):
|
| 159 |
+
return _custom_lap_single_prebuilt(model, qq, tt, energy_frame)
|
| 160 |
+
|
| 161 |
+
return _run_custom_lap_batch(single, q, 0.0, chunk_size)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def _run_custom_lap_batch(single, q, t, chunk_size):
|
| 165 |
+
n_sites, n_dims = q.shape[-2], q.shape[-1]
|
| 166 |
+
assert n_dims == 4, f"expected quaternion last dim 4, got {n_dims}"
|
| 167 |
+
lead = q.shape[:-2]
|
| 168 |
+
n_items = 1
|
| 169 |
+
for d in lead:
|
| 170 |
+
n_items *= d
|
| 171 |
+
|
| 172 |
+
q_flat = q.reshape(n_items, n_sites, 4)
|
| 173 |
+
t_arr = jnp.asarray(t, dtype=q.dtype)
|
| 174 |
+
t_bcast = jnp.broadcast_to(t_arr, lead if lead else ())
|
| 175 |
+
t_flat = t_bcast.reshape(n_items) if lead else jnp.broadcast_to(t_arr, (n_items,))
|
| 176 |
+
|
| 177 |
+
with jax.default_matmul_precision("highest"):
|
| 178 |
+
if chunk_size is None or chunk_size >= n_items:
|
| 179 |
+
total, exchange, casimir, field = jax.vmap(single)(q_flat, t_flat)
|
| 180 |
+
else:
|
| 181 |
+
total, exchange, casimir, field = jax.lax.map(
|
| 182 |
+
lambda x: single(x[0], x[1]),
|
| 183 |
+
(q_flat, t_flat),
|
| 184 |
+
batch_size=chunk_size,
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
out_shape = lead if lead else ()
|
| 188 |
+
return (
|
| 189 |
+
total.reshape(out_shape),
|
| 190 |
+
exchange.reshape(out_shape),
|
| 191 |
+
casimir.reshape(out_shape),
|
| 192 |
+
field.reshape(out_shape),
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
__all__ = []
|
src/hamiltonzero/evaluation/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from .backend import (
|
| 5 |
+
BeamCandidates,
|
| 6 |
+
CanonicalContext,
|
| 7 |
+
EvalBackend,
|
| 8 |
+
LargeNCompilation,
|
| 9 |
+
MCMCPopulation,
|
| 10 |
+
)
|
| 11 |
+
from .runner import evaluate
|
| 12 |
+
from .runtime import DefaultEvalBackend, build_eval_backend
|
| 13 |
+
from .statistics import (
|
| 14 |
+
ChannelMetrics,
|
| 15 |
+
EnergyWindow,
|
| 16 |
+
P01_TWO_SIDED,
|
| 17 |
+
select_winner_per_physical,
|
| 18 |
+
standard_error_from_tailstd,
|
| 19 |
+
welch_band_walkermean,
|
| 20 |
+
)
|
| 21 |
+
from .types import ContestCandidate, ContestResult, EvalMetric, EvalResult
|
| 22 |
+
|
| 23 |
+
__all__ = [
|
| 24 |
+
"BeamCandidates",
|
| 25 |
+
"CanonicalContext",
|
| 26 |
+
"ChannelMetrics",
|
| 27 |
+
"ContestCandidate",
|
| 28 |
+
"ContestResult",
|
| 29 |
+
"DefaultEvalBackend",
|
| 30 |
+
"EnergyWindow",
|
| 31 |
+
"EvalBackend",
|
| 32 |
+
"EvalMetric",
|
| 33 |
+
"EvalResult",
|
| 34 |
+
"LargeNCompilation",
|
| 35 |
+
"MCMCPopulation",
|
| 36 |
+
"P01_TWO_SIDED",
|
| 37 |
+
"evaluate",
|
| 38 |
+
"build_eval_backend",
|
| 39 |
+
"select_winner_per_physical",
|
| 40 |
+
"standard_error_from_tailstd",
|
| 41 |
+
"welch_band_walkermean",
|
| 42 |
+
]
|
src/hamiltonzero/evaluation/backend.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any, Protocol
|
| 9 |
+
|
| 10 |
+
from hamiltonzero.config import EnergyConfig, EvalMCMCConfig, ModelConfig
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@dataclass(frozen=True, slots=True)
|
| 14 |
+
class CanonicalContext:
|
| 15 |
+
context: Any
|
| 16 |
+
old_inverse: Any
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@dataclass(frozen=True, slots=True)
|
| 20 |
+
class BeamCandidates:
|
| 21 |
+
permutations: Any
|
| 22 |
+
log_probabilities: Any
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass(frozen=True, slots=True)
|
| 26 |
+
class LargeNCompilation:
|
| 27 |
+
wavefunction: Any
|
| 28 |
+
permutation: Any
|
| 29 |
+
log_probability: Any
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass(frozen=True, slots=True)
|
| 33 |
+
class MCMCPopulation:
|
| 34 |
+
q: Any
|
| 35 |
+
sigma: Any
|
| 36 |
+
beta: Any
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class EvalBackend(Protocol):
|
| 40 |
+
def load_system(self, path: Path, energy: EnergyConfig) -> Any: ...
|
| 41 |
+
|
| 42 |
+
def load_model(
|
| 43 |
+
self,
|
| 44 |
+
checkpoint: Path,
|
| 45 |
+
config: ModelConfig,
|
| 46 |
+
key: Any,
|
| 47 |
+
context: Any,
|
| 48 |
+
*,
|
| 49 |
+
contextualizer_attention: str | None,
|
| 50 |
+
) -> Any: ...
|
| 51 |
+
|
| 52 |
+
def canonicalize_context(self, context: Any) -> CanonicalContext: ...
|
| 53 |
+
|
| 54 |
+
def embedded_route(self, model: Any) -> Any | None: ...
|
| 55 |
+
|
| 56 |
+
def route_context(
|
| 57 |
+
self,
|
| 58 |
+
context: Any,
|
| 59 |
+
permutation: Any,
|
| 60 |
+
*,
|
| 61 |
+
compact_custom_lap: bool,
|
| 62 |
+
) -> Any: ...
|
| 63 |
+
|
| 64 |
+
def release_context(self, context: Any) -> None: ...
|
| 65 |
+
|
| 66 |
+
def virtual_context(self, context: Any, permutations: Any) -> Any: ...
|
| 67 |
+
|
| 68 |
+
def beam_candidates(
|
| 69 |
+
self,
|
| 70 |
+
model: Any,
|
| 71 |
+
context: Any,
|
| 72 |
+
*,
|
| 73 |
+
beam_width: int,
|
| 74 |
+
top_k: int,
|
| 75 |
+
temperature: float,
|
| 76 |
+
) -> BeamCandidates: ...
|
| 77 |
+
|
| 78 |
+
def compile_single(self, model: Any, routed_context: Any) -> Any: ...
|
| 79 |
+
|
| 80 |
+
def compile_embedded(self, model: Any) -> Any: ...
|
| 81 |
+
|
| 82 |
+
def compile_candidates(
|
| 83 |
+
self,
|
| 84 |
+
model: Any,
|
| 85 |
+
canonical_context: Any,
|
| 86 |
+
permutations: Any,
|
| 87 |
+
) -> Any: ...
|
| 88 |
+
|
| 89 |
+
def select_candidate(self, wavefunctions: Any, winner: int) -> Any: ...
|
| 90 |
+
|
| 91 |
+
def compile_large_n(
|
| 92 |
+
self,
|
| 93 |
+
model: Any,
|
| 94 |
+
canonical_context: Any,
|
| 95 |
+
*,
|
| 96 |
+
sequence_shards: int,
|
| 97 |
+
pair_tile_size: int,
|
| 98 |
+
temperature: float,
|
| 99 |
+
) -> LargeNCompilation: ...
|
| 100 |
+
|
| 101 |
+
def prepare_singular(
|
| 102 |
+
self,
|
| 103 |
+
model: Any,
|
| 104 |
+
context: Any,
|
| 105 |
+
state: Any,
|
| 106 |
+
) -> tuple[Any, Any, Any]: ...
|
| 107 |
+
|
| 108 |
+
def initialize_mcmc(
|
| 109 |
+
self,
|
| 110 |
+
key: Any,
|
| 111 |
+
model: Any,
|
| 112 |
+
context: Any,
|
| 113 |
+
config: EvalMCMCConfig,
|
| 114 |
+
) -> Any: ...
|
| 115 |
+
|
| 116 |
+
def step_mcmc(
|
| 117 |
+
self,
|
| 118 |
+
state: Any,
|
| 119 |
+
model: Any,
|
| 120 |
+
context: Any,
|
| 121 |
+
*,
|
| 122 |
+
replica_steps: int,
|
| 123 |
+
walker_chunk_size: int,
|
| 124 |
+
) -> Any: ...
|
| 125 |
+
|
| 126 |
+
def adapt_mcmc(self, state: Any, config: EvalMCMCConfig) -> Any: ...
|
| 127 |
+
|
| 128 |
+
def route_mcmc(self, state: Any, permutation: Any) -> Any: ...
|
| 129 |
+
|
| 130 |
+
def mcmc_population(self, state: Any) -> MCMCPopulation: ...
|
| 131 |
+
|
| 132 |
+
def replace_mcmc_population(
|
| 133 |
+
self,
|
| 134 |
+
state: Any,
|
| 135 |
+
population: MCMCPopulation,
|
| 136 |
+
) -> Any: ...
|
| 137 |
+
|
| 138 |
+
def cold_walkers(self, state: Any) -> Any: ...
|
| 139 |
+
|
| 140 |
+
def custom_lap_energy(
|
| 141 |
+
self,
|
| 142 |
+
model: Any,
|
| 143 |
+
context: Any,
|
| 144 |
+
q: Any,
|
| 145 |
+
config: EnergyConfig,
|
| 146 |
+
) -> tuple[Any, Any, Any, Any]: ...
|
| 147 |
+
|
| 148 |
+
def block_until_ready(self, value: Any) -> None: ...
|
src/hamiltonzero/evaluation/greedy_router.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
from collections.abc import Callable
|
| 8 |
+
from functools import partial
|
| 9 |
+
|
| 10 |
+
import jax
|
| 11 |
+
from jax.sharding import Mesh, NamedSharding, PartitionSpec as P
|
| 12 |
+
|
| 13 |
+
from hamiltonzero.model.route_pointer import TreePrefixPointerMHSEA
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _ring_permute_rows_local(values, route_ids, *, axis_size: int):
|
| 17 |
+
|
| 18 |
+
local_rows = values.shape[0]
|
| 19 |
+
lane = jax.lax.axis_index("seq").astype(jax.numpy.int32)
|
| 20 |
+
local_ids = jax.lax.dynamic_slice_in_dim(
|
| 21 |
+
route_ids,
|
| 22 |
+
lane * local_rows,
|
| 23 |
+
local_rows,
|
| 24 |
+
axis=0,
|
| 25 |
+
)
|
| 26 |
+
owner = local_ids // local_rows
|
| 27 |
+
within_owner = local_ids % local_rows
|
| 28 |
+
output0 = jax.numpy.zeros_like(values)
|
| 29 |
+
|
| 30 |
+
def take_from(panel, origin, output):
|
| 31 |
+
selected = panel[within_owner]
|
| 32 |
+
mask = owner == origin
|
| 33 |
+
while mask.ndim < selected.ndim:
|
| 34 |
+
mask = mask[..., None]
|
| 35 |
+
return jax.numpy.where(mask, selected, output)
|
| 36 |
+
|
| 37 |
+
origin0 = lane
|
| 38 |
+
output0 = take_from(values, origin0, output0)
|
| 39 |
+
permutation = [(i, (i + 1) % axis_size) for i in range(axis_size)]
|
| 40 |
+
|
| 41 |
+
def step(carry, _):
|
| 42 |
+
panel, origin, output = carry
|
| 43 |
+
panel = jax.lax.ppermute(panel, "seq", permutation)
|
| 44 |
+
origin = (origin - jax.numpy.asarray(1, jax.numpy.int32)) % axis_size
|
| 45 |
+
output = take_from(panel, origin, output)
|
| 46 |
+
return (panel, origin, output), None
|
| 47 |
+
|
| 48 |
+
(_, _, output), _ = jax.lax.scan(
|
| 49 |
+
step,
|
| 50 |
+
(values, origin0, output0),
|
| 51 |
+
xs=None,
|
| 52 |
+
length=axis_size - 1,
|
| 53 |
+
)
|
| 54 |
+
return output
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _make_row_permute(mesh: Mesh) -> Callable:
|
| 58 |
+
axis_size = int(mesh.shape["seq"])
|
| 59 |
+
spec = P("seq", None, None)
|
| 60 |
+
if axis_size == 1:
|
| 61 |
+
return lambda values, route_ids: values[route_ids]
|
| 62 |
+
return jax.shard_map(
|
| 63 |
+
partial(_ring_permute_rows_local, axis_size=axis_size),
|
| 64 |
+
mesh=mesh,
|
| 65 |
+
in_specs=(spec, P()),
|
| 66 |
+
out_specs=spec,
|
| 67 |
+
check_vma=False,
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def build_compact_greedy_router(
|
| 72 |
+
*,
|
| 73 |
+
mesh: Mesh,
|
| 74 |
+
decoder_template: TreePrefixPointerMHSEA,
|
| 75 |
+
pair_tile_size: int = 128,
|
| 76 |
+
) -> Callable:
|
| 77 |
+
|
| 78 |
+
if tuple(mesh.axis_names) != ("seq",):
|
| 79 |
+
raise ValueError("compact greedy router requires a one-dimensional 'seq' mesh")
|
| 80 |
+
if int(mesh.shape["seq"]) < 1:
|
| 81 |
+
raise ValueError("compact greedy router requires at least one seq lane")
|
| 82 |
+
if not isinstance(decoder_template, TreePrefixPointerMHSEA):
|
| 83 |
+
raise TypeError("decoder_template must be TreePrefixPointerMHSEA")
|
| 84 |
+
if isinstance(pair_tile_size, bool) or int(pair_tile_size) < 1:
|
| 85 |
+
raise ValueError("pair_tile_size must be a positive integer")
|
| 86 |
+
|
| 87 |
+
rep = NamedSharding(mesh, P())
|
| 88 |
+
seq_vec = NamedSharding(mesh, P("seq", None))
|
| 89 |
+
seq_edge = NamedSharding(mesh, P("seq", None, None))
|
| 90 |
+
decoder_rep = jax.tree_util.tree_map(lambda _leaf: rep, decoder_template)
|
| 91 |
+
row_permute = _make_row_permute(mesh)
|
| 92 |
+
|
| 93 |
+
def decode(decoder, h, edge, mask, global_feat, tau, real_mask):
|
| 94 |
+
|
| 95 |
+
h = jax.lax.with_sharding_constraint(h, seq_vec)
|
| 96 |
+
edge = jax.lax.with_sharding_constraint(edge, seq_edge)
|
| 97 |
+
perm, logp = decoder._decode_greedy_compact(
|
| 98 |
+
h,
|
| 99 |
+
edge,
|
| 100 |
+
mask,
|
| 101 |
+
global_feat=global_feat,
|
| 102 |
+
tau=tau,
|
| 103 |
+
real_mask=real_mask,
|
| 104 |
+
sequence_mesh=mesh,
|
| 105 |
+
pair_tile_size=int(pair_tile_size),
|
| 106 |
+
row_permute_fn=row_permute,
|
| 107 |
+
)
|
| 108 |
+
return perm, logp
|
| 109 |
+
|
| 110 |
+
return jax.jit(
|
| 111 |
+
decode,
|
| 112 |
+
in_shardings=(
|
| 113 |
+
decoder_rep,
|
| 114 |
+
seq_vec,
|
| 115 |
+
seq_edge,
|
| 116 |
+
rep,
|
| 117 |
+
rep,
|
| 118 |
+
rep,
|
| 119 |
+
rep,
|
| 120 |
+
),
|
| 121 |
+
out_shardings=(rep, rep),
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
__all__ = ["build_compact_greedy_router"]
|
src/hamiltonzero/evaluation/large_n.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import gc
|
| 8 |
+
from typing import NamedTuple
|
| 9 |
+
|
| 10 |
+
import jax
|
| 11 |
+
import jax.numpy as jnp
|
| 12 |
+
import numpy as np
|
| 13 |
+
from jax.sharding import Mesh, NamedSharding, PartitionSpec as P
|
| 14 |
+
|
| 15 |
+
from hamiltonzero.compiled.tree import bind_physical_compiler_kernel
|
| 16 |
+
from hamiltonzero.compiled.trunk import (
|
| 17 |
+
bind_shared_kernel,
|
| 18 |
+
bind_trunk_compiler_kernel,
|
| 19 |
+
)
|
| 20 |
+
from hamiltonzero.compiled.types import CompiledWaveFunction
|
| 21 |
+
from hamiltonzero.model.route_pointer import TreePrefixPointerMHSEA
|
| 22 |
+
from .sequence_trunk import (
|
| 23 |
+
build_sequence_pair_permute,
|
| 24 |
+
build_sequence_parallel_contextualizer,
|
| 25 |
+
build_sequence_parallel_edge_global_update,
|
| 26 |
+
build_sequence_parallel_physical_leaf,
|
| 27 |
+
build_sequence_parallel_physical_reducer,
|
| 28 |
+
build_sequence_parallel_shared_trunk,
|
| 29 |
+
)
|
| 30 |
+
from .greedy_router import build_compact_greedy_router
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class LargeNCompiledEvalResult(NamedTuple):
|
| 34 |
+
wavefunction: CompiledWaveFunction
|
| 35 |
+
perm: jax.Array
|
| 36 |
+
logp: jax.Array
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _sequence_mesh(n: int, requested_shards: int) -> Mesh:
|
| 40 |
+
devices = tuple(jax.devices())
|
| 41 |
+
shards = len(devices) if requested_shards == 0 else requested_shards
|
| 42 |
+
if shards < 1 or shards > len(devices):
|
| 43 |
+
raise ValueError(
|
| 44 |
+
f"compiled eval requested {shards} seq shards, but JAX exposes "
|
| 45 |
+
f"{len(devices)} devices"
|
| 46 |
+
)
|
| 47 |
+
if n % shards:
|
| 48 |
+
raise ValueError(f"N={n} must be divisible by seq_shards={shards}")
|
| 49 |
+
local_rows = n // shards
|
| 50 |
+
if n & (n - 1) or local_rows & (local_rows - 1):
|
| 51 |
+
raise ValueError(
|
| 52 |
+
"large-N physical compilation requires a power-of-two padded "
|
| 53 |
+
f"width and power-of-two rows per lane; got N={n}, "
|
| 54 |
+
f"local_rows={local_rows}"
|
| 55 |
+
)
|
| 56 |
+
return Mesh(np.asarray(devices[:shards], dtype=object), ("seq",))
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _replicate(tree, sharding: NamedSharding):
|
| 60 |
+
return jax.device_put(tree, jax.tree_util.tree_map(lambda _leaf: sharding, tree))
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class _LargeNXlaTreePrefixPointer(TreePrefixPointerMHSEA):
|
| 64 |
+
def _resolve_heavy_attn_impl(self, n: int):
|
| 65 |
+
del n
|
| 66 |
+
return None
|
| 67 |
+
|
| 68 |
+
def _resolve_tree_attn_impl(self):
|
| 69 |
+
return None
|
| 70 |
+
|
| 71 |
+
def _route_attention(
|
| 72 |
+
self,
|
| 73 |
+
q,
|
| 74 |
+
k,
|
| 75 |
+
v,
|
| 76 |
+
edge_bias,
|
| 77 |
+
key_mask,
|
| 78 |
+
*,
|
| 79 |
+
impl,
|
| 80 |
+
key_mask_only=False,
|
| 81 |
+
attention_mask=None,
|
| 82 |
+
sequence_axis_name=None,
|
| 83 |
+
sequence_mesh=None,
|
| 84 |
+
):
|
| 85 |
+
del impl, key_mask_only
|
| 86 |
+
dtype = q.dtype
|
| 87 |
+
valid = key_mask.astype(bool)[:, None, :]
|
| 88 |
+
if attention_mask is not None:
|
| 89 |
+
valid = valid & attention_mask.astype(bool)
|
| 90 |
+
has_key = jnp.any(valid, axis=-1)
|
| 91 |
+
q_c = q.astype(jnp.float32)
|
| 92 |
+
k_c = k.astype(jnp.float32)
|
| 93 |
+
v_c = v.astype(jnp.float32)
|
| 94 |
+
bias_c = edge_bias.astype(jnp.float32)
|
| 95 |
+
if sequence_axis_name is not None:
|
| 96 |
+
|
| 97 |
+
def sharding(*axes):
|
| 98 |
+
spec = P(*axes)
|
| 99 |
+
return (
|
| 100 |
+
NamedSharding(sequence_mesh, spec)
|
| 101 |
+
if sequence_mesh is not None
|
| 102 |
+
else spec
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
q_c = jax.lax.with_sharding_constraint(
|
| 106 |
+
q_c, sharding(None, sequence_axis_name, None, None)
|
| 107 |
+
)
|
| 108 |
+
k_c = jax.lax.with_sharding_constraint(
|
| 109 |
+
k_c, sharding(None, None, None, None)
|
| 110 |
+
)
|
| 111 |
+
v_c = jax.lax.with_sharding_constraint(
|
| 112 |
+
v_c, sharding(None, None, None, None)
|
| 113 |
+
)
|
| 114 |
+
bias_c = jax.lax.with_sharding_constraint(
|
| 115 |
+
bias_c, sharding(None, sequence_axis_name, None, None)
|
| 116 |
+
)
|
| 117 |
+
logits = jnp.einsum("bihd,bjhd->bhij", q_c, k_c)
|
| 118 |
+
logits = logits / jnp.sqrt(jnp.asarray(self.d_head, dtype=jnp.float32))
|
| 119 |
+
logits = logits + jnp.transpose(bias_c, (0, 3, 1, 2))
|
| 120 |
+
if sequence_axis_name is not None:
|
| 121 |
+
logits = jax.lax.with_sharding_constraint(
|
| 122 |
+
logits, sharding(None, None, sequence_axis_name, None)
|
| 123 |
+
)
|
| 124 |
+
logits = jnp.where(
|
| 125 |
+
valid[:, None, :, :],
|
| 126 |
+
logits,
|
| 127 |
+
jnp.asarray(-1.0e30, dtype=jnp.float32),
|
| 128 |
+
)
|
| 129 |
+
if sequence_axis_name is not None:
|
| 130 |
+
logits = jax.lax.with_sharding_constraint(
|
| 131 |
+
logits, sharding(None, None, sequence_axis_name, None)
|
| 132 |
+
)
|
| 133 |
+
alpha = jax.nn.softmax(logits, axis=-1)
|
| 134 |
+
if sequence_axis_name is not None:
|
| 135 |
+
alpha = jax.lax.with_sharding_constraint(
|
| 136 |
+
alpha, sharding(None, None, sequence_axis_name, None)
|
| 137 |
+
)
|
| 138 |
+
out = jnp.einsum("bhij,bjhd->bihd", alpha, v_c)
|
| 139 |
+
if sequence_axis_name is not None:
|
| 140 |
+
out = jax.lax.with_sharding_constraint(
|
| 141 |
+
out, sharding(None, sequence_axis_name, None, None)
|
| 142 |
+
)
|
| 143 |
+
out = out.astype(dtype)
|
| 144 |
+
return jnp.where(has_key[..., None, None], out, jnp.zeros_like(out))
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _large_n_xla_decoder_view(decoder: TreePrefixPointerMHSEA):
|
| 148 |
+
compiled = object.__new__(_LargeNXlaTreePrefixPointer)
|
| 149 |
+
compiled.__dict__.update(decoder.__dict__)
|
| 150 |
+
return compiled
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def _validate_large_n_model(model) -> None:
|
| 154 |
+
decoder = getattr(model, "route_decoder", None)
|
| 155 |
+
failures: list[str] = []
|
| 156 |
+
if not isinstance(decoder, TreePrefixPointerMHSEA):
|
| 157 |
+
failures.append("route decoder must be TreePrefixPointerMHSEA")
|
| 158 |
+
if getattr(model, "route_contextualizer", None) is None:
|
| 159 |
+
failures.append("route contextualizer must be enabled")
|
| 160 |
+
if getattr(model, "gladder_fork_route", None) is None:
|
| 161 |
+
failures.append("route global fork must be enabled")
|
| 162 |
+
if getattr(model, "readout_leaf_context", None) is None:
|
| 163 |
+
failures.append("physical contextualizer must be enabled")
|
| 164 |
+
if failures:
|
| 165 |
+
raise ValueError(
|
| 166 |
+
"unsupported large-N eval compiler model: " + "; ".join(failures)
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def _validate_context(ctx) -> int:
|
| 171 |
+
jdp = jnp.asarray(ctx.J_double_prime)
|
| 172 |
+
mask = jnp.asarray(ctx.mask)
|
| 173 |
+
bmask = jnp.asarray(ctx.bmask)
|
| 174 |
+
h_prime = jnp.asarray(ctx.h_prime)
|
| 175 |
+
if jdp.ndim != 3 or jdp.shape[-1] != 10:
|
| 176 |
+
raise ValueError("ctx.J_double_prime must have shape [N,N,10]")
|
| 177 |
+
n = int(jdp.shape[0])
|
| 178 |
+
if jdp.shape[1] != n:
|
| 179 |
+
raise ValueError("ctx.J_double_prime pair axes must be square")
|
| 180 |
+
if mask.shape != (n,) or bmask.shape != (n,):
|
| 181 |
+
raise ValueError("ctx.mask and ctx.bmask must both have shape [N]")
|
| 182 |
+
if h_prime.shape != (n, 3):
|
| 183 |
+
raise ValueError("ctx.h_prime must have shape [N,3]")
|
| 184 |
+
if jdp.dtype != jnp.float32 or h_prime.dtype != jnp.float32:
|
| 185 |
+
raise TypeError(
|
| 186 |
+
"large-N eval keeps streamed pair/frontier arithmetic in fp32; "
|
| 187 |
+
f"got J={jdp.dtype}, h={h_prime.dtype}"
|
| 188 |
+
)
|
| 189 |
+
return n
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def compile_large_n_eval_wavefunction(
|
| 193 |
+
model,
|
| 194 |
+
ctx,
|
| 195 |
+
*,
|
| 196 |
+
seq_shards: int = 0,
|
| 197 |
+
pair_tile_size: int = 128,
|
| 198 |
+
tau: float = 1.0,
|
| 199 |
+
) -> LargeNCompiledEvalResult:
|
| 200 |
+
|
| 201 |
+
_validate_large_n_model(model)
|
| 202 |
+
n = _validate_context(ctx)
|
| 203 |
+
if pair_tile_size < 1:
|
| 204 |
+
raise ValueError("pair_tile_size must be positive")
|
| 205 |
+
mesh = _sequence_mesh(n, int(seq_shards))
|
| 206 |
+
rep = NamedSharding(mesh, P())
|
| 207 |
+
seq_edge = NamedSharding(mesh, P("seq", None, None))
|
| 208 |
+
|
| 209 |
+
trunk_kernel = _replicate(bind_trunk_compiler_kernel(model), rep)
|
| 210 |
+
route_contextualizer = _replicate(model.route_contextualizer, rep)
|
| 211 |
+
route_global_fork = _replicate(model.gladder_fork_route, rep)
|
| 212 |
+
decoder = _replicate(_large_n_xla_decoder_view(model.route_decoder), rep)
|
| 213 |
+
physical_kernel = _replicate(bind_physical_compiler_kernel(model), rep)
|
| 214 |
+
|
| 215 |
+
jdp = jax.device_put(jnp.asarray(ctx.J_double_prime), seq_edge)
|
| 216 |
+
h_prime, real_mask, structural_mask = jax.device_put(
|
| 217 |
+
(
|
| 218 |
+
jnp.asarray(ctx.h_prime),
|
| 219 |
+
jnp.asarray(ctx.mask),
|
| 220 |
+
jnp.asarray(ctx.bmask),
|
| 221 |
+
),
|
| 222 |
+
rep,
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
trunk_entry = build_sequence_parallel_shared_trunk(
|
| 226 |
+
mesh=mesh,
|
| 227 |
+
kernel_template=trunk_kernel,
|
| 228 |
+
featurizer_tile_size=int(pair_tile_size),
|
| 229 |
+
)
|
| 230 |
+
trunk = trunk_entry(trunk_kernel, jdp, h_prime, real_mask, structural_mask)
|
| 231 |
+
|
| 232 |
+
route_context_entry = build_sequence_parallel_contextualizer(
|
| 233 |
+
mesh=mesh,
|
| 234 |
+
contextualizer_template=route_contextualizer,
|
| 235 |
+
g_template=trunk.global_stream,
|
| 236 |
+
tile_size=int(pair_tile_size),
|
| 237 |
+
)
|
| 238 |
+
route_node, route_edge, route_g = route_context_entry(
|
| 239 |
+
route_contextualizer,
|
| 240 |
+
trunk.node_raw,
|
| 241 |
+
trunk.edge_raw,
|
| 242 |
+
real_mask,
|
| 243 |
+
structural_mask,
|
| 244 |
+
trunk.global_stream,
|
| 245 |
+
)
|
| 246 |
+
route_global_entry = build_sequence_parallel_edge_global_update(
|
| 247 |
+
mesh=mesh,
|
| 248 |
+
module_template=route_global_fork,
|
| 249 |
+
tile_size=int(pair_tile_size),
|
| 250 |
+
)
|
| 251 |
+
route_global = route_global_entry(
|
| 252 |
+
route_global_fork, route_g, route_edge, structural_mask
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
route_entry = build_compact_greedy_router(
|
| 256 |
+
mesh=mesh,
|
| 257 |
+
decoder_template=decoder,
|
| 258 |
+
pair_tile_size=int(pair_tile_size),
|
| 259 |
+
)
|
| 260 |
+
perm, logp = route_entry(
|
| 261 |
+
decoder,
|
| 262 |
+
route_node,
|
| 263 |
+
route_edge,
|
| 264 |
+
structural_mask,
|
| 265 |
+
route_global,
|
| 266 |
+
jnp.asarray(tau, dtype=jnp.float32),
|
| 267 |
+
real_mask,
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
jax.block_until_ready((perm, logp))
|
| 271 |
+
del route_node, route_edge, route_g, route_global
|
| 272 |
+
|
| 273 |
+
pair_permute = build_sequence_pair_permute(mesh=mesh)
|
| 274 |
+
routed_node, routed_edge = pair_permute(trunk.node_raw, trunk.edge_raw, perm)
|
| 275 |
+
leaf_real = real_mask[perm]
|
| 276 |
+
global_stream = trunk.global_stream
|
| 277 |
+
|
| 278 |
+
jax.block_until_ready((routed_node, routed_edge, leaf_real, global_stream))
|
| 279 |
+
del (
|
| 280 |
+
trunk,
|
| 281 |
+
trunk_kernel,
|
| 282 |
+
route_contextualizer,
|
| 283 |
+
route_global_fork,
|
| 284 |
+
decoder,
|
| 285 |
+
jdp,
|
| 286 |
+
h_prime,
|
| 287 |
+
real_mask,
|
| 288 |
+
trunk_entry,
|
| 289 |
+
route_context_entry,
|
| 290 |
+
route_global_entry,
|
| 291 |
+
route_entry,
|
| 292 |
+
pair_permute,
|
| 293 |
+
)
|
| 294 |
+
gc.collect()
|
| 295 |
+
|
| 296 |
+
physical_context_entry = build_sequence_parallel_contextualizer(
|
| 297 |
+
mesh=mesh,
|
| 298 |
+
contextualizer_template=physical_kernel.contextualizer,
|
| 299 |
+
g_template=global_stream,
|
| 300 |
+
tile_size=int(pair_tile_size),
|
| 301 |
+
)
|
| 302 |
+
physical_node, physical_edge, physical_context_g = physical_context_entry(
|
| 303 |
+
physical_kernel.contextualizer,
|
| 304 |
+
routed_node,
|
| 305 |
+
routed_edge,
|
| 306 |
+
leaf_real,
|
| 307 |
+
structural_mask,
|
| 308 |
+
global_stream,
|
| 309 |
+
)
|
| 310 |
+
jax.block_until_ready((physical_node, physical_edge, physical_context_g))
|
| 311 |
+
del routed_node, routed_edge, global_stream, physical_context_entry
|
| 312 |
+
gc.collect()
|
| 313 |
+
|
| 314 |
+
physical_global_entry = build_sequence_parallel_edge_global_update(
|
| 315 |
+
mesh=mesh,
|
| 316 |
+
module_template=physical_kernel.global_fork,
|
| 317 |
+
tile_size=int(pair_tile_size),
|
| 318 |
+
)
|
| 319 |
+
physical_global = physical_global_entry(
|
| 320 |
+
physical_kernel.global_fork,
|
| 321 |
+
physical_context_g,
|
| 322 |
+
physical_edge,
|
| 323 |
+
structural_mask,
|
| 324 |
+
)
|
| 325 |
+
jax.block_until_ready(physical_global)
|
| 326 |
+
del physical_context_g, physical_global_entry
|
| 327 |
+
gc.collect()
|
| 328 |
+
|
| 329 |
+
physical_leaf_entry = build_sequence_parallel_physical_leaf(
|
| 330 |
+
mesh=mesh,
|
| 331 |
+
kernel_template=physical_kernel,
|
| 332 |
+
)
|
| 333 |
+
leaf_h, c_rows = physical_leaf_entry(
|
| 334 |
+
physical_kernel,
|
| 335 |
+
physical_node,
|
| 336 |
+
physical_global,
|
| 337 |
+
)
|
| 338 |
+
jax.block_until_ready((leaf_h, c_rows))
|
| 339 |
+
del physical_node, physical_leaf_entry
|
| 340 |
+
gc.collect()
|
| 341 |
+
|
| 342 |
+
physical_reducer_entry = build_sequence_parallel_physical_reducer(
|
| 343 |
+
mesh=mesh,
|
| 344 |
+
kernel_template=physical_kernel,
|
| 345 |
+
edge_template=physical_edge,
|
| 346 |
+
replicate_threshold=min(512, n),
|
| 347 |
+
contextualizer_tile_size=int(pair_tile_size),
|
| 348 |
+
)
|
| 349 |
+
tree = physical_reducer_entry(
|
| 350 |
+
physical_kernel,
|
| 351 |
+
physical_edge,
|
| 352 |
+
leaf_h,
|
| 353 |
+
c_rows,
|
| 354 |
+
leaf_real,
|
| 355 |
+
structural_mask,
|
| 356 |
+
physical_global,
|
| 357 |
+
perm,
|
| 358 |
+
)
|
| 359 |
+
jax.block_until_ready(tree)
|
| 360 |
+
|
| 361 |
+
return LargeNCompiledEvalResult(
|
| 362 |
+
wavefunction=CompiledWaveFunction(bind_shared_kernel(model), tree),
|
| 363 |
+
perm=perm,
|
| 364 |
+
logp=logp,
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
__all__ = [
|
| 369 |
+
"LargeNCompiledEvalResult",
|
| 370 |
+
"compile_large_n_eval_wavefunction",
|
| 371 |
+
]
|
src/hamiltonzero/evaluation/pallas_mha.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
|
| 3 |
+
# Copyright 2023 The JAX Authors.
|
| 4 |
+
# Modifications copyright (c) 2026 Simulacra Research Inc.
|
| 5 |
+
#
|
| 6 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 7 |
+
# you may not use this file except in compliance with the License.
|
| 8 |
+
# You may obtain a copy of the License at
|
| 9 |
+
#
|
| 10 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 11 |
+
#
|
| 12 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 13 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 14 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 15 |
+
# See the License for the specific language governing permissions and
|
| 16 |
+
# limitations under the License.
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import dataclasses
|
| 22 |
+
import functools
|
| 23 |
+
import math
|
| 24 |
+
from typing import Any
|
| 25 |
+
|
| 26 |
+
import jax
|
| 27 |
+
import jax.numpy as jnp
|
| 28 |
+
from jax import lax
|
| 29 |
+
from jax.experimental import pallas as pl
|
| 30 |
+
from jax.experimental.pallas import triton as plgpu
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@dataclasses.dataclass(frozen=True, slots=True)
|
| 34 |
+
class BlockSizes:
|
| 35 |
+
block_q: int
|
| 36 |
+
block_k: int
|
| 37 |
+
|
| 38 |
+
@classmethod
|
| 39 |
+
def get_default(cls):
|
| 40 |
+
return cls(block_q=128, block_k=128)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def noncausal_bias_mha_forward_kernel(
|
| 44 |
+
q_ref,
|
| 45 |
+
k_ref,
|
| 46 |
+
v_ref,
|
| 47 |
+
bias_ref,
|
| 48 |
+
o_ref: Any,
|
| 49 |
+
*,
|
| 50 |
+
block_q: int,
|
| 51 |
+
block_k: int,
|
| 52 |
+
head_dim: int,
|
| 53 |
+
):
|
| 54 |
+
seq_len = k_ref.shape[0]
|
| 55 |
+
start_q = pl.program_id(0)
|
| 56 |
+
head_dim_padded = q_ref.shape[-1]
|
| 57 |
+
m_i = jnp.zeros(block_q, dtype=jnp.float32) - float("inf")
|
| 58 |
+
l_i = jnp.zeros(block_q, dtype=jnp.float32)
|
| 59 |
+
o = jnp.zeros((block_q, head_dim_padded), dtype=jnp.float32)
|
| 60 |
+
curr_q_slice = pl.dslice(start_q * block_q, block_q)
|
| 61 |
+
head_mask = (jnp.arange(head_dim_padded) < head_dim)[None, :]
|
| 62 |
+
q = plgpu.load(q_ref, mask=head_mask, other=0.0)
|
| 63 |
+
|
| 64 |
+
def body(start_k, carry):
|
| 65 |
+
o_prev, m_prev, l_prev = carry
|
| 66 |
+
curr_k_slice = pl.dslice(start_k * block_k, block_k)
|
| 67 |
+
k = plgpu.load(k_ref.at[curr_k_slice, :], mask=head_mask, other=0.0)
|
| 68 |
+
qk = pl.dot(q, k.T)
|
| 69 |
+
bias = plgpu.load(bias_ref.at[curr_q_slice, curr_k_slice])
|
| 70 |
+
qk += bias
|
| 71 |
+
qk *= math.log2(math.e)
|
| 72 |
+
qk = qk.astype(q_ref.dtype)
|
| 73 |
+
m_curr = jnp.max(qk, axis=-1)
|
| 74 |
+
m_next = jnp.maximum(m_prev, m_curr)
|
| 75 |
+
correction = jnp.exp2(m_prev - m_next)
|
| 76 |
+
l_prev_corr = correction * l_prev
|
| 77 |
+
s_curr = jnp.exp2(qk - m_next[:, None])
|
| 78 |
+
l_curr = s_curr.sum(axis=-1)
|
| 79 |
+
l_next = l_prev_corr + l_curr
|
| 80 |
+
o_prev_corr = correction[:, None] * o_prev
|
| 81 |
+
v = plgpu.load(v_ref.at[curr_k_slice, :], mask=head_mask)
|
| 82 |
+
o_curr = pl.dot(s_curr.astype(v.dtype), v)
|
| 83 |
+
o_next = o_prev_corr + o_curr
|
| 84 |
+
return o_next, m_next, l_next
|
| 85 |
+
|
| 86 |
+
upper_bound = pl.cdiv(seq_len, block_k)
|
| 87 |
+
o, _m_i, l_i = lax.fori_loop(0, upper_bound, body, (o, m_i, l_i))
|
| 88 |
+
o /= l_i[:, None]
|
| 89 |
+
plgpu.store(o_ref.at[:, : o.shape[-1]], o.astype(o_ref.dtype), mask=head_mask)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@functools.partial(jax.jit, static_argnames=["block_sizes"])
|
| 93 |
+
def noncausal_bias_mha(
|
| 94 |
+
q,
|
| 95 |
+
k,
|
| 96 |
+
v,
|
| 97 |
+
bias,
|
| 98 |
+
*,
|
| 99 |
+
block_sizes: BlockSizes = BlockSizes.get_default(),
|
| 100 |
+
):
|
| 101 |
+
batch_size, q_seq_len, num_heads, head_dim = q.shape
|
| 102 |
+
kv_seq_len = k.shape[1]
|
| 103 |
+
block_q = min(block_sizes.block_q, q_seq_len)
|
| 104 |
+
block_k = min(block_sizes.block_k, kv_seq_len)
|
| 105 |
+
head_dim_padded = pl.next_power_of_2(head_dim)
|
| 106 |
+
if (q.shape[-1] != k.shape[-1]) or (q.shape[-1] != v.shape[-1]):
|
| 107 |
+
raise ValueError(
|
| 108 |
+
"This kernel expects q, k, and v to have the same head dimension, "
|
| 109 |
+
f"but found {q.shape=}, {k.shape=}, {v.shape=}."
|
| 110 |
+
)
|
| 111 |
+
if bias.shape != (batch_size, q_seq_len, kv_seq_len, num_heads):
|
| 112 |
+
raise ValueError(
|
| 113 |
+
f"bias must have shape [batch, query, key, heads]; got {bias.shape}"
|
| 114 |
+
)
|
| 115 |
+
if q_seq_len % block_q != 0:
|
| 116 |
+
raise ValueError(f"{q_seq_len=} must be a multiple of {block_q=}")
|
| 117 |
+
if kv_seq_len % block_k != 0:
|
| 118 |
+
raise ValueError(f"{kv_seq_len=} must be a multiple of {block_k=}")
|
| 119 |
+
grid = (pl.cdiv(q_seq_len, block_q), batch_size, num_heads)
|
| 120 |
+
num_warps = 4 if head_dim <= 64 else 8
|
| 121 |
+
kernel = functools.partial(
|
| 122 |
+
noncausal_bias_mha_forward_kernel,
|
| 123 |
+
block_q=block_q,
|
| 124 |
+
block_k=block_k,
|
| 125 |
+
head_dim=head_dim,
|
| 126 |
+
)
|
| 127 |
+
in_specs = [
|
| 128 |
+
pl.BlockSpec(
|
| 129 |
+
(None, block_q, None, head_dim_padded),
|
| 130 |
+
lambda i, j, k: (j, i, k, 0),
|
| 131 |
+
),
|
| 132 |
+
pl.BlockSpec(
|
| 133 |
+
(None, kv_seq_len, None, head_dim_padded),
|
| 134 |
+
lambda i, j, k: (j, 0, k, 0),
|
| 135 |
+
),
|
| 136 |
+
pl.BlockSpec(
|
| 137 |
+
(None, kv_seq_len, None, head_dim_padded),
|
| 138 |
+
lambda i, j, k: (j, 0, k, 0),
|
| 139 |
+
),
|
| 140 |
+
pl.BlockSpec(
|
| 141 |
+
(None, block_q, kv_seq_len, None),
|
| 142 |
+
lambda i, j, k: (j, i, 0, k),
|
| 143 |
+
),
|
| 144 |
+
]
|
| 145 |
+
out_shape = [q]
|
| 146 |
+
out_specs = [
|
| 147 |
+
pl.BlockSpec(
|
| 148 |
+
(None, block_q, None, head_dim_padded),
|
| 149 |
+
lambda i, j, k: (j, i, k, 0),
|
| 150 |
+
)
|
| 151 |
+
]
|
| 152 |
+
out = pl.pallas_call(
|
| 153 |
+
kernel,
|
| 154 |
+
grid=grid,
|
| 155 |
+
in_specs=in_specs,
|
| 156 |
+
out_specs=out_specs,
|
| 157 |
+
compiler_params=plgpu.CompilerParams(num_warps=num_warps, num_stages=2),
|
| 158 |
+
out_shape=out_shape,
|
| 159 |
+
name="mha_forward",
|
| 160 |
+
)(q, k, v, bias)
|
| 161 |
+
return out[0]
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
__all__ = ["BlockSizes", "noncausal_bias_mha"]
|
src/hamiltonzero/evaluation/runner.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2026 Simulacra Research Inc.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import time
|
| 7 |
+
from dataclasses import replace
|
| 8 |
+
from typing import Any, Callable
|
| 9 |
+
|
| 10 |
+
import jax
|
| 11 |
+
import jax.numpy as jnp
|
| 12 |
+
import numpy as np
|
| 13 |
+
|
| 14 |
+
from hamiltonzero.config import EvalConfig
|
| 15 |
+
|
| 16 |
+
from .backend import EvalBackend, MCMCPopulation
|
| 17 |
+
from .statistics import EnergyWindow, P01_TWO_SIDED, select_winner_per_physical
|
| 18 |
+
from .types import ContestCandidate, ContestResult, EvalMetric, EvalResult
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _permute_q_prefix(q, permutations):
|
| 22 |
+
index = jnp.broadcast_to(permutations[:, None, None, :, None], q.shape)
|
| 23 |
+
return jnp.take_along_axis(q, index, axis=3)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _collapse_to_winner(q_virtual, permutations, winner_idx, *, P, K):
|
| 27 |
+
winner_idx = jnp.asarray(winner_idx, dtype=jnp.int32)
|
| 28 |
+
inverse = jnp.argsort(permutations, axis=-1)
|
| 29 |
+
q_canonical = _permute_q_prefix(q_virtual, inverse)
|
| 30 |
+
batch_per_candidate = q_canonical.shape[1]
|
| 31 |
+
tail = q_canonical.shape[2:]
|
| 32 |
+
q_physical = q_canonical.reshape((P, K, batch_per_candidate) + tail).reshape(
|
| 33 |
+
(P, K * batch_per_candidate) + tail
|
| 34 |
+
)
|
| 35 |
+
permutations_pk = permutations.reshape((P, K, -1))
|
| 36 |
+
route = jnp.take_along_axis(
|
| 37 |
+
permutations_pk,
|
| 38 |
+
winner_idx[:, None, None],
|
| 39 |
+
axis=1,
|
| 40 |
+
)[:, 0]
|
| 41 |
+
return _permute_q_prefix(q_physical, route), route
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _gather_winner_ladder(values, winner_idx, *, P, K):
|
| 45 |
+
winner_idx = jnp.asarray(winner_idx, dtype=jnp.int32)
|
| 46 |
+
values_pk = values.reshape((P, K) + values.shape[1:])
|
| 47 |
+
index = winner_idx[(slice(None), None) + (None,) * (values_pk.ndim - 2)]
|
| 48 |
+
return jnp.take_along_axis(values_pk, index, axis=1)[:, 0]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _as_batched_route(permutation):
|
| 52 |
+
value = jnp.asarray(permutation, dtype=jnp.int32)
|
| 53 |
+
if value.ndim == 1:
|
| 54 |
+
value = value[None, :]
|
| 55 |
+
if value.ndim != 2 or value.shape[0] != 1:
|
| 56 |
+
raise ValueError("single-system evaluation requires a route with shape [1, N]")
|
| 57 |
+
return value
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _compose_walker_route(old_inverse, route):
|
| 61 |
+
return jnp.take_along_axis(
|
| 62 |
+
jnp.asarray(old_inverse, dtype=jnp.int32),
|
| 63 |
+
route,
|
| 64 |
+
axis=-1,
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _adapt(backend: EvalBackend, state: Any, config: EvalConfig):
|
| 69 |
+
return backend.adapt_mcmc(state, config.mcmc)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _burn_in(
|
| 73 |
+
backend: EvalBackend,
|
| 74 |
+
state: Any,
|
| 75 |
+
model: Any,
|
| 76 |
+
context: Any,
|
| 77 |
+
config: EvalConfig,
|
| 78 |
+
*,
|
| 79 |
+
iterations: int,
|
| 80 |
+
replica_steps: int,
|
| 81 |
+
):
|
| 82 |
+
for _ in range(int(iterations)):
|
| 83 |
+
state = backend.step_mcmc(
|
| 84 |
+
state,
|
| 85 |
+
model,
|
| 86 |
+
context,
|
| 87 |
+
replica_steps=int(replica_steps),
|
| 88 |
+
walker_chunk_size=int(config.mcmc.walker_chunk_size),
|
| 89 |
+
)
|
| 90 |
+
backend.block_until_ready(backend.cold_walkers(state))
|
| 91 |
+
state = _adapt(backend, state, config)
|
| 92 |
+
return state
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _measure(
|
| 96 |
+
backend: EvalBackend,
|
| 97 |
+
state: Any,
|
| 98 |
+
model: Any,
|
| 99 |
+
context: Any,
|
| 100 |
+
config: EvalConfig,
|
| 101 |
+
*,
|
| 102 |
+
started: float,
|
| 103 |
+
metric_sink: Callable[[EvalMetric], None] | None,
|
| 104 |
+
):
|
| 105 |
+
window = EnergyWindow(
|
| 106 |
+
config.measurements,
|
| 107 |
+
systems=1,
|
| 108 |
+
batch_size=config.mcmc.batch_size,
|
| 109 |
+
)
|
| 110 |
+
for step in range(config.measurements):
|
| 111 |
+
step_started = time.perf_counter()
|
| 112 |
+
state = backend.step_mcmc(
|
| 113 |
+
state,
|
| 114 |
+
model,
|
| 115 |
+
context,
|
| 116 |
+
replica_steps=int(config.mcmc.steps),
|
| 117 |
+
walker_chunk_size=int(config.mcmc.walker_chunk_size),
|
| 118 |
+
)
|
| 119 |
+
q_cold = backend.cold_walkers(state)
|
| 120 |
+
total, exchange, _casimir, field = backend.custom_lap_energy(
|
| 121 |
+
model,
|
| 122 |
+
context,
|
| 123 |
+
q_cold,
|
| 124 |
+
config.energy,
|
| 125 |
+
)
|
| 126 |
+
backend.block_until_ready(total)
|
| 127 |
+
window.push(total, exchange, field)
|
| 128 |
+
state = _adapt(backend, state, config)
|
| 129 |
+
if metric_sink is not None:
|
| 130 |
+
energy = np.asarray(total).real
|
| 131 |
+
metric_sink(
|
| 132 |
+
EvalMetric(
|
| 133 |
+
step=step,
|
| 134 |
+
energy=float(np.mean(energy)),
|
| 135 |
+
energy_std=float(np.std(energy)),
|
| 136 |
+
step_walltime=float(time.perf_counter() - step_started),
|
| 137 |
+
walltime=float(time.perf_counter() - started),
|
| 138 |
+
)
|
| 139 |
+
)
|
| 140 |
+
return state, window
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def _ordinary(
|
| 144 |
+
backend: EvalBackend,
|
| 145 |
+
model: Any,
|
| 146 |
+
context: Any,
|
| 147 |
+
canonical,
|
| 148 |
+
mcmc_key,
|
| 149 |
+
config: EvalConfig,
|
| 150 |
+
):
|
| 151 |
+
state = backend.initialize_mcmc(mcmc_key, model, context, config.mcmc)
|
| 152 |
+
candidates = backend.beam_candidates(
|
| 153 |
+
model,
|
| 154 |
+
canonical.context,
|
| 155 |
+
beam_width=int(config.contest_beam_width),
|
| 156 |
+
top_k=1,
|
| 157 |
+
temperature=float(config.route_temperature),
|
| 158 |
+
)
|
| 159 |
+
permutations = jnp.asarray(candidates.permutations, dtype=jnp.int32)
|
| 160 |
+
if permutations.ndim != 3 or permutations.shape[:2] != (1, 1):
|
| 161 |
+
raise ValueError("ordinary eval router must return shape [1, 1, N]")
|
| 162 |
+
route = permutations[:, 0]
|
| 163 |
+
walker_route = _compose_walker_route(canonical.old_inverse, route)
|
| 164 |
+
routed_context = backend.route_context(
|
| 165 |
+
canonical.context,
|
| 166 |
+
route,
|
| 167 |
+
compact_custom_lap=False,
|
| 168 |
+
)
|
| 169 |
+
state = backend.route_mcmc(state, walker_route)
|
| 170 |
+
wavefunction = backend.compile_single(model, routed_context)
|
| 171 |
+
backend.block_until_ready(wavefunction)
|
| 172 |
+
logp = float(np.asarray(candidates.log_probabilities)[0, 0])
|
| 173 |
+
return wavefunction, routed_context, state, route, logp, None
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def _compiled_finetune_ordinary(
|
| 177 |
+
backend: EvalBackend,
|
| 178 |
+
model: Any,
|
| 179 |
+
context: Any,
|
| 180 |
+
canonical,
|
| 181 |
+
embedded_route,
|
| 182 |
+
mcmc_key,
|
| 183 |
+
config: EvalConfig,
|
| 184 |
+
):
|
| 185 |
+
state = backend.initialize_mcmc(mcmc_key, model, context, config.mcmc)
|
| 186 |
+
route = _as_batched_route(embedded_route)
|
| 187 |
+
if route.shape[-1] != canonical.context.mask.shape[-1]:
|
| 188 |
+
raise ValueError(
|
| 189 |
+
"compiled fine-tune route width does not match the evaluation system"
|
| 190 |
+
)
|
| 191 |
+
walker_route = _compose_walker_route(canonical.old_inverse, route)
|
| 192 |
+
routed_context = backend.route_context(
|
| 193 |
+
canonical.context,
|
| 194 |
+
route,
|
| 195 |
+
compact_custom_lap=False,
|
| 196 |
+
)
|
| 197 |
+
state = backend.route_mcmc(state, walker_route)
|
| 198 |
+
wavefunction = backend.compile_embedded(model)
|
| 199 |
+
backend.block_until_ready(wavefunction)
|
| 200 |
+
return wavefunction, routed_context, state, route, None, None
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def _contest(
|
| 204 |
+
backend: EvalBackend,
|
| 205 |
+
model: Any,
|
| 206 |
+
canonical,
|
| 207 |
+
root_key,
|
| 208 |
+
config: EvalConfig,
|
| 209 |
+
):
|
| 210 |
+
K = int(config.contest_candidates)
|
| 211 |
+
batch_per_candidate = int(config.mcmc.batch_size) // K
|
| 212 |
+
candidates = backend.beam_candidates(
|
| 213 |
+
model,
|
| 214 |
+
canonical.context,
|
| 215 |
+
beam_width=int(config.contest_beam_width),
|
| 216 |
+
top_k=K,
|
| 217 |
+
temperature=float(config.route_temperature),
|
| 218 |
+
)
|
| 219 |
+
beam_permutations = jnp.asarray(candidates.permutations, dtype=jnp.int32)
|
| 220 |
+
if beam_permutations.ndim != 3 or beam_permutations.shape[:2] != (1, K):
|
| 221 |
+
raise ValueError(f"contest router must return shape [1, {K}, N]")
|
| 222 |
+
n_sites = int(beam_permutations.shape[-1])
|
| 223 |
+
permutations = beam_permutations.reshape((K, n_sites))
|
| 224 |
+
virtual_context = backend.virtual_context(canonical.context, permutations)
|
| 225 |
+
race_mcmc = replace(config.mcmc, batch_size=batch_per_candidate)
|
| 226 |
+
state = backend.initialize_mcmc(
|
| 227 |
+
jax.random.fold_in(root_key, 7411),
|
| 228 |
+
model,
|
| 229 |
+
virtual_context,
|
| 230 |
+
race_mcmc,
|
| 231 |
+
)
|
| 232 |
+
wavefunctions = backend.compile_candidates(
|
| 233 |
+
model,
|
| 234 |
+
canonical.context,
|
| 235 |
+
permutations,
|
| 236 |
+
)
|
| 237 |
+
backend.block_until_ready(wavefunctions)
|
| 238 |
+
for _ in range(int(config.contest_preburn)):
|
| 239 |
+
state = backend.step_mcmc(
|
| 240 |
+
state,
|
| 241 |
+
wavefunctions,
|
| 242 |
+
virtual_context,
|
| 243 |
+
replica_steps=int(config.mcmc.burn_in_replica_steps),
|
| 244 |
+
walker_chunk_size=int(config.mcmc.walker_chunk_size),
|
| 245 |
+
)
|
| 246 |
+
backend.block_until_ready(backend.cold_walkers(state))
|
| 247 |
+
state = backend.adapt_mcmc(state, race_mcmc)
|
| 248 |
+
race_window = EnergyWindow(
|
| 249 |
+
config.contest_measurements,
|
| 250 |
+
systems=K,
|
| 251 |
+
batch_size=batch_per_candidate,
|
| 252 |
+
)
|
| 253 |
+
for _ in range(int(config.contest_measurements)):
|
| 254 |
+
state = backend.step_mcmc(
|
| 255 |
+
state,
|
| 256 |
+
wavefunctions,
|
| 257 |
+
virtual_context,
|
| 258 |
+
replica_steps=int(config.mcmc.steps),
|
| 259 |
+
walker_chunk_size=int(config.mcmc.walker_chunk_size),
|
| 260 |
+
)
|
| 261 |
+
state = backend.adapt_mcmc(state, race_mcmc)
|
| 262 |
+
q_cold = backend.cold_walkers(state)
|
| 263 |
+
total, exchange, _casimir, field = backend.custom_lap_energy(
|
| 264 |
+
wavefunctions,
|
| 265 |
+
virtual_context,
|
| 266 |
+
q_cold,
|
| 267 |
+
config.energy,
|
| 268 |
+
)
|
| 269 |
+
backend.block_until_ready(total)
|
| 270 |
+
race_window.push(total, exchange, field)
|
| 271 |
+
energies = np.asarray(
|
| 272 |
+
[[race_window.tail_mean("total", candidate) for candidate in range(K)]]
|
| 273 |
+
)
|
| 274 |
+
tailstd = np.asarray(
|
| 275 |
+
[[race_window.tail_std("total", candidate) for candidate in range(K)]]
|
| 276 |
+
)
|
| 277 |
+
beam_logp = np.asarray(candidates.log_probabilities, dtype=float)
|
| 278 |
+
winners, ties, reasons, _bands, standard_errors = select_winner_per_physical(
|
| 279 |
+
energies,
|
| 280 |
+
tailstd,
|
| 281 |
+
beam_logp,
|
| 282 |
+
batch_per_candidate,
|
| 283 |
+
z=P01_TWO_SIDED,
|
| 284 |
+
ucb_z=float(config.contest_se_multiplier),
|
| 285 |
+
)
|
| 286 |
+
winner = int(winners[0])
|
| 287 |
+
wavefunction = backend.select_candidate(wavefunctions, winner)
|
| 288 |
+
population = backend.mcmc_population(state)
|
| 289 |
+
q_final, route = _collapse_to_winner(
|
| 290 |
+
population.q,
|
| 291 |
+
permutations,
|
| 292 |
+
winners,
|
| 293 |
+
P=1,
|
| 294 |
+
K=K,
|
| 295 |
+
)
|
| 296 |
+
sigma = _gather_winner_ladder(population.sigma, winners, P=1, K=K)
|
| 297 |
+
beta = _gather_winner_ladder(population.beta, winners, P=1, K=K)
|
| 298 |
+
routed_context = backend.route_context(
|
| 299 |
+
canonical.context,
|
| 300 |
+
route,
|
| 301 |
+
compact_custom_lap=False,
|
| 302 |
+
)
|
| 303 |
+
final_state = backend.initialize_mcmc(
|
| 304 |
+
jax.random.fold_in(root_key, 7919),
|
| 305 |
+
wavefunction,
|
| 306 |
+
routed_context,
|
| 307 |
+
config.mcmc,
|
| 308 |
+
)
|
| 309 |
+
final_state = backend.replace_mcmc_population(
|
| 310 |
+
final_state,
|
| 311 |
+
MCMCPopulation(q=q_final, sigma=sigma, beta=beta),
|
| 312 |
+
)
|
| 313 |
+
backend.block_until_ready(backend.cold_walkers(final_state))
|
| 314 |
+
contest_candidates = tuple(
|
| 315 |
+
ContestCandidate(
|
| 316 |
+
index=index,
|
| 317 |
+
route_log_probability=float(beam_logp[0, index]),
|
| 318 |
+
energy=float(energies[0, index]),
|
| 319 |
+
standard_error=float(standard_errors[0, index]),
|
| 320 |
+
walker_tail_std=float(tailstd[0, index]),
|
| 321 |
+
in_tie_set=bool(ties[0, index]),
|
| 322 |
+
)
|
| 323 |
+
for index in range(K)
|
| 324 |
+
)
|
| 325 |
+
contest = ContestResult(
|
| 326 |
+
winner=winner,
|
| 327 |
+
reason=reasons[0],
|
| 328 |
+
candidates=contest_candidates,
|
| 329 |
+
)
|
| 330 |
+
backend.release_context(virtual_context)
|
| 331 |
+
return (
|
| 332 |
+
wavefunction,
|
| 333 |
+
routed_context,
|
| 334 |
+
final_state,
|
| 335 |
+
route,
|
| 336 |
+
float(beam_logp[0, winner]),
|
| 337 |
+
contest,
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def _large_n(
|
| 342 |
+
backend: EvalBackend,
|
| 343 |
+
model: Any,
|
| 344 |
+
context: Any,
|
| 345 |
+
canonical,
|
| 346 |
+
mcmc_key,
|
| 347 |
+
config: EvalConfig,
|
| 348 |
+
):
|
| 349 |
+
state = backend.initialize_mcmc(mcmc_key, model, context, config.mcmc)
|
| 350 |
+
compiled = backend.compile_large_n(
|
| 351 |
+
model,
|
| 352 |
+
canonical.context,
|
| 353 |
+
sequence_shards=int(config.large_n_sequence_shards),
|
| 354 |
+
pair_tile_size=int(config.large_n_pair_tile_size),
|
| 355 |
+
temperature=float(config.route_temperature),
|
| 356 |
+
)
|
| 357 |
+
route = _as_batched_route(compiled.permutation)
|
| 358 |
+
walker_route = _compose_walker_route(canonical.old_inverse, route)
|
| 359 |
+
routed_context = backend.route_context(
|
| 360 |
+
canonical.context,
|
| 361 |
+
route,
|
| 362 |
+
compact_custom_lap=True,
|
| 363 |
+
)
|
| 364 |
+
state = backend.route_mcmc(state, walker_route)
|
| 365 |
+
backend.block_until_ready(compiled.wavefunction)
|
| 366 |
+
logp = float(np.asarray(compiled.log_probability))
|
| 367 |
+
return compiled.wavefunction, routed_context, state, route, logp, None
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def _validate(config: EvalConfig) -> None:
|
| 371 |
+
if config.contest and config.large_n:
|
| 372 |
+
raise ValueError("contest and large_n are mutually exclusive")
|
| 373 |
+
if int(config.measurements) < 1:
|
| 374 |
+
raise ValueError("measurements must be positive")
|
| 375 |
+
if int(config.mcmc.batch_size) < 1:
|
| 376 |
+
raise ValueError("MCMC batch size must be positive")
|
| 377 |
+
if int(config.mcmc.replicas) < 2:
|
| 378 |
+
raise ValueError("MCMC requires at least two replicas")
|
| 379 |
+
if int(config.mcmc.steps) < 1:
|
| 380 |
+
raise ValueError("MCMC replica steps must be positive")
|
| 381 |
+
if int(config.mcmc.burn_in_replica_steps) < 1:
|
| 382 |
+
raise ValueError("burn-in replica steps must be positive")
|
| 383 |
+
if int(config.mcmc.walker_chunk_size) < 1:
|
| 384 |
+
raise ValueError("walker chunk size must be positive")
|
| 385 |
+
if config.contest:
|
| 386 |
+
K = int(config.contest_candidates)
|
| 387 |
+
W = int(config.contest_beam_width)
|
| 388 |
+
if K < 2 or W < K:
|
| 389 |
+
raise ValueError("contest requires beam_width >= candidates >= 2")
|
| 390 |
+
if int(config.mcmc.batch_size) % K:
|
| 391 |
+
raise ValueError("MCMC batch size must be divisible by candidates")
|
| 392 |
+
if int(config.mcmc.batch_size) // K < 32:
|
| 393 |
+
raise ValueError("contest requires at least 32 walkers per candidate")
|
| 394 |
+
if int(config.contest_preburn) < 0:
|
| 395 |
+
raise ValueError("contest preburn must be non-negative")
|
| 396 |
+
if int(config.contest_measurements) < 1:
|
| 397 |
+
raise ValueError("contest measurements must be positive")
|
| 398 |
+
if int(config.large_n_sequence_shards) < 0:
|
| 399 |
+
raise ValueError("large-N sequence shards must be non-negative")
|
| 400 |
+
if int(config.large_n_pair_tile_size) < 1:
|
| 401 |
+
raise ValueError("large-N pair tile size must be positive")
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def evaluate(
|
| 405 |
+
config: EvalConfig,
|
| 406 |
+
backend: EvalBackend,
|
| 407 |
+
*,
|
| 408 |
+
metric_sink: Callable[[EvalMetric], None] | None = None,
|
| 409 |
+
) -> EvalResult:
|
| 410 |
+
_validate(config)
|
| 411 |
+
started = time.perf_counter()
|
| 412 |
+
root_key = jax.random.PRNGKey(int(config.seed))
|
| 413 |
+
model_key, mcmc_key = jax.random.split(root_key)
|
| 414 |
+
context = backend.load_system(config.system, config.energy)
|
| 415 |
+
model = backend.load_model(
|
| 416 |
+
config.checkpoint,
|
| 417 |
+
config.model,
|
| 418 |
+
model_key,
|
| 419 |
+
context,
|
| 420 |
+
contextualizer_attention=config.contextualizer_attention,
|
| 421 |
+
)
|
| 422 |
+
canonical = backend.canonicalize_context(context)
|
| 423 |
+
embedded_route = backend.embedded_route(model)
|
| 424 |
+
if embedded_route is not None and (config.contest or config.large_n):
|
| 425 |
+
raise ValueError(
|
| 426 |
+
"compiled fine-tune checkpoints support ordinary eval only; "
|
| 427 |
+
"contest and large_n require a router checkpoint"
|
| 428 |
+
)
|
| 429 |
+
if embedded_route is not None:
|
| 430 |
+
prepared = _compiled_finetune_ordinary(
|
| 431 |
+
backend,
|
| 432 |
+
model,
|
| 433 |
+
context,
|
| 434 |
+
canonical,
|
| 435 |
+
embedded_route,
|
| 436 |
+
mcmc_key,
|
| 437 |
+
config,
|
| 438 |
+
)
|
| 439 |
+
path = "ordinary"
|
| 440 |
+
elif config.contest:
|
| 441 |
+
prepared = _contest(
|
| 442 |
+
backend,
|
| 443 |
+
model,
|
| 444 |
+
canonical,
|
| 445 |
+
root_key,
|
| 446 |
+
config,
|
| 447 |
+
)
|
| 448 |
+
path = "contest"
|
| 449 |
+
elif config.large_n:
|
| 450 |
+
prepared = _large_n(
|
| 451 |
+
backend,
|
| 452 |
+
model,
|
| 453 |
+
context,
|
| 454 |
+
canonical,
|
| 455 |
+
mcmc_key,
|
| 456 |
+
config,
|
| 457 |
+
)
|
| 458 |
+
path = "large_n"
|
| 459 |
+
else:
|
| 460 |
+
prepared = _ordinary(
|
| 461 |
+
backend,
|
| 462 |
+
model,
|
| 463 |
+
context,
|
| 464 |
+
canonical,
|
| 465 |
+
mcmc_key,
|
| 466 |
+
config,
|
| 467 |
+
)
|
| 468 |
+
path = "ordinary"
|
| 469 |
+
wavefunction, routed_context, state, route, route_logp, contest = prepared
|
| 470 |
+
del model, context, canonical, embedded_route, prepared
|
| 471 |
+
wavefunction, routed_context, state = backend.prepare_singular(
|
| 472 |
+
wavefunction,
|
| 473 |
+
routed_context,
|
| 474 |
+
state,
|
| 475 |
+
)
|
| 476 |
+
state = _burn_in(
|
| 477 |
+
backend,
|
| 478 |
+
state,
|
| 479 |
+
wavefunction,
|
| 480 |
+
routed_context,
|
| 481 |
+
config,
|
| 482 |
+
iterations=int(config.mcmc.burn_in),
|
| 483 |
+
replica_steps=int(config.mcmc.burn_in_replica_steps),
|
| 484 |
+
)
|
| 485 |
+
_state, window = _measure(
|
| 486 |
+
backend,
|
| 487 |
+
state,
|
| 488 |
+
wavefunction,
|
| 489 |
+
routed_context,
|
| 490 |
+
config,
|
| 491 |
+
started=started,
|
| 492 |
+
metric_sink=metric_sink,
|
| 493 |
+
)
|
| 494 |
+
route_host = np.asarray(route, dtype=np.int32)
|
| 495 |
+
if route_host.shape[0] != 1:
|
| 496 |
+
raise ValueError("single-system eval produced more than one route")
|
| 497 |
+
return EvalResult(
|
| 498 |
+
path=path,
|
| 499 |
+
route=tuple(int(value) for value in route_host[0]),
|
| 500 |
+
route_log_probability=(None if route_logp is None else float(route_logp)),
|
| 501 |
+
measurements=int(window.count),
|
| 502 |
+
walltime_seconds=float(time.perf_counter() - started),
|
| 503 |
+
energy=window.metrics("total"),
|
| 504 |
+
channels={
|
| 505 |
+
"exchange": window.metrics("exchange"),
|
| 506 |
+
"field": window.metrics("field"),
|
| 507 |
+
},
|
| 508 |
+
contest=contest,
|
| 509 |
+
)
|