Decision-2.0-Eos-0.8B Core ML: shared prefix + chunked packed questions, fp16 package, runtime, parity reports
Browse files- .gitattributes +1 -0
- Decision2EosPacked.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- Decision2EosPacked.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- Decision2EosPacked.mlpackage/Manifest.json +18 -0
- LICENSE +202 -0
- README.md +101 -0
- conversion/cases.py +19 -0
- conversion/check_release.py +40 -0
- conversion/combine.py +11 -0
- conversion/convert_eos.py +37 -0
- conversion/eos_graph.py +351 -0
- conversion/kev_stages.py +661 -0
- conversion/pack.py +121 -0
- conversion/parity_chunked_torch.py +26 -0
- conversion/q64ctx.py +20 -0
- conversion/qwen35_export.py +333 -0
- conversion/time_eos.py +15 -0
- conversion/typed_ref.py +13 -0
- coreml_config.json +24 -0
- decision2_coreml.py +277 -0
- decision_config.json +58 -0
- example.py +17 -0
- reports/typed-decisions-test.txt +6 -0
- tokenizer.json +3 -0
- tokenizer_config.json +32 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* 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
|
|
|
|
|
|
| 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 |
+
tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
Decision2EosPacked.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e8a434580763e03408199536441dcfabbb3e58f479e2ef9e823e69d8b497e443
|
| 3 |
+
size 21981050
|
Decision2EosPacked.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:680030e081b927dc5902d522e0df4f24bdf3a13910c228625d7fd98ac324e939
|
| 3 |
+
size 1515655104
|
Decision2EosPacked.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"4DDB3568-C852-4A8A-8E49-47C9F5FCB612": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"7323AD02-87D1-4987-95F3-AC1D30199D66": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "4DDB3568-C852-4A8A-8E49-47C9F5FCB612"
|
| 18 |
+
}
|
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.
|
README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model: vllm-sr/Decision-2.0-Eos-0.8B
|
| 4 |
+
tags:
|
| 5 |
+
- coreml
|
| 6 |
+
- decision-making
|
| 7 |
+
- on-device
|
| 8 |
+
- system-one
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Decision-2.0-Eos-0.8B · Core ML
|
| 12 |
+
|
| 13 |
+
[vllm-sr/Decision-2.0-Eos-0.8B](https://huggingface.co/vllm-sr/Decision-2.0-Eos-0.8B) (vLLM Semantic Router, Apache-2.0)
|
| 14 |
+
converted to Core ML for on-device use on Apple silicon. Decision 2.0 answers Choice, Yes/No and Score questions
|
| 15 |
+
about one input, with a probability for every option and no text generation.
|
| 16 |
+
|
| 17 |
+
This port answers **every question of a request in one model call**. The text all the questions share (the context) is
|
| 18 |
+
read once, and each question then continues from it as if it were alone. Eos uses a Qwen3.5 hybrid backbone (Gated
|
| 19 |
+
DeltaNet + attention), so "continues from it" covers the DeltaNet recurrent state and conv history as well as the
|
| 20 |
+
attention keys. See [How it works](#how-it-works).
|
| 21 |
+
|
| 22 |
+
| | |
|
| 23 |
+
|---|---|
|
| 24 |
+
| Source | `vllm-sr/Decision-2.0-Eos-0.8B` @ `3594047d` (Qwen3.5-0.8B-Base backbone + shared decision head) |
|
| 25 |
+
| Package | `Decision2EosPacked.mlpackage`, fp16, 1.4 GB, five functions sharing one copy of the weights |
|
| 26 |
+
| Functions | `S128_C256_N32`, `S256_C512_N64`, `S512_C768_N96`, `S512_C1024_N128`, `S1024_C1024_N128` (shared-prefix tokens / packed question tokens / option slots) |
|
| 27 |
+
| Requires | macOS 15+ / iOS 18+; run on the GPU (`CPU_AND_GPU`) |
|
| 28 |
+
|
| 29 |
+
Kai 0.6B (Qwen3 backbone) is at [FluidInference/decision-2.0-kai-coreml](https://huggingface.co/FluidInference/decision-2.0-kai-coreml),
|
| 30 |
+
with the same Python runtime.
|
| 31 |
+
|
| 32 |
+
## Quickstart
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
pip install coremltools tokenizers numpy
|
| 36 |
+
hf download FluidInference/decision-2.0-eos-coreml --local-dir decision-2.0-eos-coreml
|
| 37 |
+
cd decision-2.0-eos-coreml && python example.py
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
`decision2_coreml.py` mirrors the upstream `system_one()` output (`choice`/`noul`/`score`, `probabilities`,
|
| 41 |
+
`confidence`, `legend`). It picks the smallest function that fits a request. A request that fits none is split into
|
| 42 |
+
chunks, each repeating the shared prefix, on whichever function needs the fewest milliseconds.
|
| 43 |
+
|
| 44 |
+
## Fidelity
|
| 45 |
+
|
| 46 |
+
Against the upstream runtime (fp32, Transformers 5.18, MPS) on the **typed-decisions TEST** split
|
| 47 |
+
(`LocalLLaMA/typed-decisions`, 400 requests, 2,000 decisions):
|
| 48 |
+
|
| 49 |
+
| | Upstream | Core ML fp16 |
|
| 50 |
+
|---|---:|---:|
|
| 51 |
+
| Choice accuracy (600) | 0.430 | 0.433 |
|
| 52 |
+
| Yes/No accuracy (600) | 0.598 | 0.598 |
|
| 53 |
+
| Score accuracy (800) | 0.379 | 0.376 |
|
| 54 |
+
| Answers that differ from upstream | — | 4 / 2,000 (all near-ties: upstream top-2 margin ≤ 0.003) |
|
| 55 |
+
| Max probability difference | — | 0.0077 |
|
| 56 |
+
|
| 57 |
+
Tokenization (via `tokenizers`) matches the upstream encoder on all 2,000 questions. The fp32 PyTorch version of the
|
| 58 |
+
graph (`conversion/eos_graph.py`, `EosChunked`) matches upstream to 2e-6. The accuracies are only for the comparison
|
| 59 |
+
with upstream: they are not the model card's benchmark protocol.
|
| 60 |
+
|
| 61 |
+
## Speed (MacBook Pro M5 Pro, 24 GB, macOS 27)
|
| 62 |
+
|
| 63 |
+
| Request | Upstream PyTorch, MPS | Core ML (GPU) |
|
| 64 |
+
|---|---:|---:|
|
| 65 |
+
| Card quickstart, 3 questions | 306 ms | **34 ms** |
|
| 66 |
+
| typed-decisions request, 5 questions (median) | 1,595 ms | **110 ms** |
|
| 67 |
+
| One email, 64 questions | 15,647 ms | **874 ms** (13 calls) |
|
| 68 |
+
|
| 69 |
+
Times are full requests (tokenize, pack, predict, answer) and exclude model loading. Per call: S128_C256 34 ms,
|
| 70 |
+
S256_C512 64 ms, S512_C768 109 ms, S512_C1024 131 ms, S1024_C1024 176 ms. On a Mac the upstream runtime has none of its
|
| 71 |
+
GPU kernels (flash-linear-attention, causal-conv1d), so the speedup is specific to this machine. The Neural Engine was
|
| 72 |
+
not used: on Kev-0.8B, which has the same backbone, it was about 24× slower than the GPU.
|
| 73 |
+
|
| 74 |
+
## How it works
|
| 75 |
+
|
| 76 |
+
One call takes the shared prefix (right-padded to S) and the questions' suffixes packed end to end (C tokens):
|
| 77 |
+
|
| 78 |
+
- **Attention layers:** one mask. Prefix tokens are causal; a suffix token sees the real prefix tokens and earlier
|
| 79 |
+
tokens of its own question.
|
| 80 |
+
- **Gated DeltaNet layers:** the prefix runs the chunked delta rule (chunk 64; padded positions get β = g = 0, exact
|
| 81 |
+
no-ops), which yields its final recurrent state S₀. The packed questions also run in chunks of 64. A token whose
|
| 82 |
+
question began in an earlier chunk continues from the carried state; a token whose question began in this chunk
|
| 83 |
+
starts from S₀ (`cont`). Decays and the WY inverse are masked to the question within each chunk (`seg_chunks`), and
|
| 84 |
+
only the question of a chunk's last token carries its state forward (`last_seg`). A question's first three
|
| 85 |
+
convolution lags read the prefix's last three inputs (`lag_keep`, `lag_tail`).
|
| 86 |
+
- **Readout:** Decision 2.0's head (bilinear + MLP over LayerNorm'd hidden states) at each option's last token and the
|
| 87 |
+
question's final token (`cand_idx`, `query_idx`).
|
| 88 |
+
|
| 89 |
+
So every question sees exactly its own upstream row (`Context … Question … Options … Decision:`), and the delta-rule
|
| 90 |
+
cost stays linear in packed tokens. Full input list: `coreml_config.json`.
|
| 91 |
+
|
| 92 |
+
## Conversion
|
| 93 |
+
|
| 94 |
+
`conversion/`: `eos_graph.py` (Decision 2.0 graph on top of `kev_stages.py` / `qwen35_export.py`, the Qwen3.5 export
|
| 95 |
+
from [Kev-0.8B Core ML](https://huggingface.co/FluidInference/kev-0.8b-coreml)), `convert_eos.py S cP N` (torch 2.7.0,
|
| 96 |
+
coremltools 9.0), `combine.py`, parity and timing scripts.
|
| 97 |
+
|
| 98 |
+
## License
|
| 99 |
+
|
| 100 |
+
Apache-2.0, as the source model. `LICENSE`, `tokenizer*.json` and `decision_config.json` are copied unchanged from the
|
| 101 |
+
source repository.
|
conversion/cases.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LEVELS5 = ["None", "Low", "Medium", "High", "Critical"]
|
| 2 |
+
CASES = [
|
| 3 |
+
("The order arrived damaged yesterday. The customer has a receipt and asks for a replacement today.",
|
| 4 |
+
{"route": {"type":"choice","instructions":"Which team should handle this request?",
|
| 5 |
+
"criteria":{"returns":"Refunds, replacements and damaged deliveries","billing":"Payments, invoices and charges","technical":"Product setup and faults"}},
|
| 6 |
+
"receipt": {"type":"noul","instructions":"Does the customer have a receipt?"},
|
| 7 |
+
"urgency": {"type":"score","instructions":"How urgent is this request?","criteria":["Routine","Soon","Today"]}}),
|
| 8 |
+
("Hi team, our production database went down at 3am and customers cannot log in. We need someone on this immediately. Invoice #4521 is also overdue but that can wait.",
|
| 9 |
+
{"severity": {"type":"score","instructions":"How severe is the incident?","criteria":LEVELS5},
|
| 10 |
+
"outage": {"type":"noul","instructions":"Is there an active outage?"},
|
| 11 |
+
"billing": {"type":"noul","instructions":"Is a billing topic mentioned?"},
|
| 12 |
+
"dept": {"type":"choice","instructions":"Which department?","criteria":{"sre":"Site reliability and on-call","finance":"Accounts receivable","sales":"New business","hr":"People operations"}},
|
| 13 |
+
"sentiment": {"type":"choice","instructions":"Customer sentiment?","criteria":{"pos":"Positive","neu":"Neutral","neg":"Negative"}}}),
|
| 14 |
+
({"user":"What's the capital of Australia?","history":[]},
|
| 15 |
+
{"route": {"type":"choice","instructions":"Which model should answer?",
|
| 16 |
+
"criteria":{"small":"A small fast model for simple factual lookups","large":"A large reasoning model for complex multi-step problems","code":"A coding model"}},
|
| 17 |
+
"jailbreak": {"type":"noul","instructions":"Is this a jailbreak attempt?"},
|
| 18 |
+
"difficulty": {"type":"score","instructions":"How hard is this query?","criteria":LEVELS5}}),
|
| 19 |
+
]
|
conversion/check_release.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json, sys, time
|
| 2 |
+
import numpy as np, pandas as pd
|
| 3 |
+
sys.path.insert(0, sys.argv[3])
|
| 4 |
+
from decision2_coreml import Decision2CoreML, encode as r_encode, question_to_row as r_row
|
| 5 |
+
from transformers import AutoTokenizer
|
| 6 |
+
import pack, q64ctx
|
| 7 |
+
ref_root, refpath, rel = sys.argv[1], sys.argv[2], sys.argv[3]
|
| 8 |
+
tok = AutoTokenizer.from_pretrained(ref_root)
|
| 9 |
+
m = Decision2CoreML(rel)
|
| 10 |
+
d = pd.read_parquet("data/typed/all/test-00000-of-00001.parquet")
|
| 11 |
+
ref = {j["id"]: j["answers"] for j in map(json.loads, open(refpath))}
|
| 12 |
+
def lab(a): return ("true" if a["noul"] >= .5 else "false") if a["type"] == "noul" else max(a["probabilities"], key=a["probabilities"].get)
|
| 13 |
+
from collections import defaultdict
|
| 14 |
+
acc = defaultdict(lambda: [0, 0, 0]); margins = []
|
| 15 |
+
tokdiff = flips = n = 0; worst = 0; ts = []
|
| 16 |
+
for r in d.itertuples():
|
| 17 |
+
st, qs = json.loads(r.state), json.loads(r.questions)
|
| 18 |
+
for qid, q in qs.items(): # tokenization vs upstream encoder
|
| 19 |
+
up = pack.rows(tok, st, {qid: q})[0][2]; mine = r_encode(r_row(st, q), m.tokenizer)
|
| 20 |
+
tokdiff += up["ids"] != mine["ids"] or up["candidate_positions"] != mine["candidate_positions"]
|
| 21 |
+
t = time.time(); a = m.system_one(state=st, questions=qs)["answers"]; ts.append((time.time() - t) * 1000)
|
| 22 |
+
for q in qs:
|
| 23 |
+
rq, aq = ref[r.id][q], a[q]; n += 1; flips += lab(rq) != lab(aq)
|
| 24 |
+
pr = rq.get("probabilities", {"true": rq.get("noul")}); pa = aq.get("probabilities", {"true": aq.get("noul")})
|
| 25 |
+
g = json.loads(r.gold)[q]["label"]; s_ = acc[rq["type"]]; s_[0] += 1; s_[1] += lab(rq) == g; s_[2] += lab(aq) == g
|
| 26 |
+
if lab(rq) != lab(aq):
|
| 27 |
+
ps = sorted(pr.values(), reverse=True) if len(pr) > 1 else sorted([pr["true"], 1 - pr["true"]], reverse=True)
|
| 28 |
+
margins.append(round(ps[0] - ps[1], 4))
|
| 29 |
+
worst = max(worst, max(abs(pr[k] - pa[k]) for k in pr))
|
| 30 |
+
for f in ("confidence", "score"):
|
| 31 |
+
if f in rq: assert abs(rq[f] - aq[f]) < 0.05, (r.id, q, f)
|
| 32 |
+
assert set(rq) == set(aq), (set(rq) ^ set(aq))
|
| 33 |
+
print("flip margins (upstream top-2)", margins)
|
| 34 |
+
for k, (c, h1, h2) in sorted(acc.items()): print(f" {k:6s} n={c} acc upstream {h1/c:.3f} coreml {h2/c:.3f}")
|
| 35 |
+
print(f"typed: token mismatches {tokdiff}, {n} decisions, flips {flips}, max|dp| {worst:.4f}, request p50 {np.median(ts):.1f} ms")
|
| 36 |
+
t = time.time(); a = m.system_one(state=q64ctx.state, questions=q64ctx.Q); m.system_one(state=q64ctx.state, questions=q64ctx.Q)
|
| 37 |
+
ts = []
|
| 38 |
+
for _ in range(5):
|
| 39 |
+
t = time.time(); m.system_one(state=q64ctx.state, questions=q64ctx.Q); ts.append((time.time() - t) * 1000)
|
| 40 |
+
print(f"64 questions: {len(m._calls([r_encode(r_row(q64ctx.state, q), m.tokenizer) for q in q64ctx.Q.values()]))} calls, p50 {np.median(ts):.0f} ms end to end")
|
conversion/combine.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Merge single-bucket packages into one multifunction package (weights stored once; macOS 15+)."""
|
| 2 |
+
import sys
|
| 3 |
+
import coremltools as ct
|
| 4 |
+
out, specs = sys.argv[1], sys.argv[2:] # name=path ...
|
| 5 |
+
desc = ct.utils.MultiFunctionDescriptor()
|
| 6 |
+
for s in specs:
|
| 7 |
+
name, path = s.split("=")
|
| 8 |
+
desc.add_function(path, src_function_name="main", target_function_name=name)
|
| 9 |
+
desc.default_function_name = specs[1].split("=")[0] if len(specs) > 1 else specs[0].split("=")[0]
|
| 10 |
+
ct.utils.save_multifunction(desc, out)
|
| 11 |
+
print("saved", out)
|
conversion/convert_eos.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys, time
|
| 2 |
+
import numpy as np, torch, coremltools as ct
|
| 3 |
+
from eos_graph import load_chunked, load_eos, load_rows
|
| 4 |
+
# packed: convert_eos.py S P N | rows: convert_eos.py S BxQ N
|
| 5 |
+
S, N = int(sys.argv[1]), int(sys.argv[3])
|
| 6 |
+
if "x" in sys.argv[2]:
|
| 7 |
+
B, Q = map(int, sys.argv[2].split("x")); P = B * Q
|
| 8 |
+
g = load_rows("eos", S, B, Q); tag = f"S{S}_R{B}x{Q}_N{N}"
|
| 9 |
+
elif sys.argv[2].startswith("c"):
|
| 10 |
+
P = int(sys.argv[2][1:]); g = load_chunked("eos", S, P); tag = f"S{S}_C{P}_N{N}"
|
| 11 |
+
else:
|
| 12 |
+
P = int(sys.argv[2]); g = load_eos("eos", S, P); tag = f"S{S}_P{P}_N{N}"
|
| 13 |
+
chunked = sys.argv[2].startswith("c")
|
| 14 |
+
out = sys.argv[4] if len(sys.argv) > 4 else f"coreml/eos_{tag}.mlpackage"
|
| 15 |
+
T, R = S + P, g.cfg.rotary_dim
|
| 16 |
+
ex = (torch.zeros(1, T, dtype=torch.int32), torch.zeros(T, R), torch.zeros(T, R), torch.ones(S), torch.zeros(3, S),
|
| 17 |
+
torch.eye(P), torch.zeros(3, P), torch.zeros(3, P, 3))
|
| 18 |
+
if chunked:
|
| 19 |
+
M = P // 64
|
| 20 |
+
ex += (torch.eye(64).expand(M, 64, 64).contiguous(), torch.zeros(P), torch.zeros(M, 64))
|
| 21 |
+
ex += (torch.zeros(N, dtype=torch.int32), torch.zeros(N, dtype=torch.int32))
|
| 22 |
+
t = time.time()
|
| 23 |
+
with torch.no_grad():
|
| 24 |
+
tr = torch.jit.trace(g, ex, check_trace=False)
|
| 25 |
+
f16 = np.float16
|
| 26 |
+
m = ct.convert(tr, convert_to="mlprogram", minimum_deployment_target=ct.target.macOS15,
|
| 27 |
+
inputs=[ct.TensorType(name="input_ids", shape=(1, T), dtype=np.int32),
|
| 28 |
+
ct.TensorType(name="cos", shape=(T, R), dtype=f16), ct.TensorType(name="sin", shape=(T, R), dtype=f16),
|
| 29 |
+
ct.TensorType(name="valid", shape=(S,), dtype=f16), ct.TensorType(name="tail_onehot", shape=(3, S), dtype=f16),
|
| 30 |
+
ct.TensorType(name="segment", shape=(P, P), dtype=f16), ct.TensorType(name="lag_keep", shape=(3, P), dtype=f16),
|
| 31 |
+
ct.TensorType(name="lag_tail", shape=(3, P, 3), dtype=f16)]
|
| 32 |
+
+ ([ct.TensorType(name="seg_chunks", shape=(P // 64, 64, 64), dtype=f16), ct.TensorType(name="cont", shape=(P,), dtype=f16),
|
| 33 |
+
ct.TensorType(name="last_seg", shape=(P // 64, 64), dtype=f16)] if chunked else [])
|
| 34 |
+
+ [ct.TensorType(name="cand_idx", shape=(N,), dtype=np.int32), ct.TensorType(name="query_idx", shape=(N,), dtype=np.int32)],
|
| 35 |
+
outputs=[ct.TensorType(name="logits", dtype=np.float32)], compute_precision=ct.precision.FLOAT16)
|
| 36 |
+
m.short_description = "Decision-2.0-Eos-0.8B (vllm-sr): shared prefix + packed questions, one call"
|
| 37 |
+
m.save(out); print("saved", out, f"{time.time()-t:.0f}s")
|
conversion/eos_graph.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Decision-2.0-Eos-0.8B (Qwen3.5 hybrid) as one fused Core ML graph: shared prefix + packed questions.
|
| 2 |
+
|
| 3 |
+
Built on the Kev-0.8B FusedPass (kev_stages.py): the shared prefix is the "state" (S tokens, right-padded; padded
|
| 4 |
+
positions are exact no-ops in the delta rule and masked in attention), the questions' suffixes are packed in P
|
| 5 |
+
tokens. Each suffix restarts from the prefix's recurrent state and conv tail (segment mask), and attends to the
|
| 6 |
+
prefix and itself. The readout is Decision 2.0's shared bilinear + MLP head at each option's last token and the
|
| 7 |
+
question's final token (gather indices into the packed region).
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn.functional as F
|
| 15 |
+
from safetensors.torch import load_file
|
| 16 |
+
from torch import nn
|
| 17 |
+
|
| 18 |
+
import kev_stages
|
| 19 |
+
from kev_stages import MASK_VALUE, FusedPass
|
| 20 |
+
from qwen35_export import TextConfig, rope_cos_sin
|
| 21 |
+
|
| 22 |
+
LAGS = 3
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class EosFused(FusedPass):
|
| 26 |
+
def __init__(self, cfg: TextConfig, state_len: int, packed_len: int, lane: int | None = None, chunk: int = 64):
|
| 27 |
+
# lane = None: questions may span the whole packed region (inverse over all P)
|
| 28 |
+
super().__init__(cfg, state_len, packed_len, batch=1, max_options=2, chunk_size=chunk,
|
| 29 |
+
lane=lane or packed_len)
|
| 30 |
+
h = cfg.hidden_size
|
| 31 |
+
del self.q, self.k, self.pointer_scale, self.inverse_temperature
|
| 32 |
+
self.embed_tokens = nn.Embedding(1, h) # resized on load
|
| 33 |
+
self.candidate_norm = nn.LayerNorm(h)
|
| 34 |
+
self.query_norm = nn.LayerNorm(h)
|
| 35 |
+
self.key = nn.Linear(h, 256, bias=False)
|
| 36 |
+
self.query = nn.Linear(h, 256, bias=False)
|
| 37 |
+
self.candidate_mlp = nn.Linear(h, 256)
|
| 38 |
+
self.query_mlp = nn.Linear(h, 256, bias=False)
|
| 39 |
+
self.scalar = nn.Linear(256, 1, bias=False)
|
| 40 |
+
|
| 41 |
+
def forward(self, input_ids, cos, sin, valid, tail_onehot, segment, lag_keep, lag_tail, cand_idx, query_idx):
|
| 42 |
+
"""input_ids [1, S+P]; cos/sin [S+P, R]; valid [S]; tail_onehot [3, S]; segment [P, P]; lag_keep [3, P];
|
| 43 |
+
lag_tail [3, P, 3]; cand_idx/query_idx [N] (indices into the packed region) -> logits [N]."""
|
| 44 |
+
S, P = self.S, self.P
|
| 45 |
+
bias = torch.cat([
|
| 46 |
+
torch.cat([self.state_causal, self.state_block], dim=1),
|
| 47 |
+
torch.cat([((1.0 - valid) * MASK_VALUE)[None, :].expand(P, S), (1.0 - segment) * MASK_VALUE], dim=1),
|
| 48 |
+
], dim=0)
|
| 49 |
+
x = self.embed_tokens(input_ids)
|
| 50 |
+
for layer in self.decoder.layers:
|
| 51 |
+
h = layer.input_layernorm(x)
|
| 52 |
+
if layer.is_linear:
|
| 53 |
+
out = self._delta(layer.linear_attn, h, valid, tail_onehot, segment, lag_keep, lag_tail)
|
| 54 |
+
else:
|
| 55 |
+
out = self._attention(layer.self_attn, h, cos, sin, bias)
|
| 56 |
+
x = x + out
|
| 57 |
+
x = x + layer.mlp(layer.post_attention_layernorm(x))
|
| 58 |
+
xq = self.norm(x[0, S:])
|
| 59 |
+
cand = self.candidate_norm(xq[cand_idx].float())
|
| 60 |
+
qry = self.query_norm(xq[query_idx].float())
|
| 61 |
+
bilinear = (self.key(cand) * self.query(qry)).sum(-1) / 16.0
|
| 62 |
+
nonlinear = self.scalar(F.gelu(self.candidate_mlp(cand) + self.query_mlp(qry))).squeeze(-1)
|
| 63 |
+
return bilinear + nonlinear
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def load_eos(root: str, state_len: int, packed_len: int, lane: int | None = None) -> EosFused:
|
| 67 |
+
cfg = TextConfig(json.load(open(f"{root}/backbone/config.json")))
|
| 68 |
+
state = {k: v.float() for k, v in load_file(f"{root}/backbone/model.safetensors").items()}
|
| 69 |
+
model = EosFused(cfg, state_len, packed_len, lane)
|
| 70 |
+
model.decoder.load_merged(state)
|
| 71 |
+
model.norm.load_state_dict({"weight": state["norm.weight"]})
|
| 72 |
+
model.embed_tokens = nn.Embedding.from_pretrained(state["embed_tokens.weight"], freeze=True)
|
| 73 |
+
head = {k: v.float() for k, v in load_file(f"{root}/decision_head.safetensors").items()}
|
| 74 |
+
missing, unexpected = model.load_state_dict(head, strict=False)
|
| 75 |
+
assert not unexpected and not [m for m in missing if m.split(".")[0] in
|
| 76 |
+
("candidate_norm", "query_norm", "key", "query", "candidate_mlp",
|
| 77 |
+
"query_mlp", "scalar")], (missing, unexpected)
|
| 78 |
+
return model.eval()
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def prefix_len(encoded):
|
| 82 |
+
seqs = [e["ids"] for e in encoded]
|
| 83 |
+
limit = min(min(e["candidate_positions"][0] for e in encoded), min(len(s) for s in seqs) - 1)
|
| 84 |
+
for i in range(limit):
|
| 85 |
+
if any(s[i] != seqs[0][i] for s in seqs):
|
| 86 |
+
return i
|
| 87 |
+
return limit
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def eos_inputs(cfg: TextConfig, encoded, S: int, P: int, pad_id: int, lane: int | None = None,
|
| 91 |
+
row_len: int | None = None):
|
| 92 |
+
"""Fused inputs for a group of encoded rows (vendored `encode` output) sharing their common token prefix.
|
| 93 |
+
With `lane`, a question that would cross a lane boundary starts at the next lane; with `row_len`, question j
|
| 94 |
+
starts at j * row_len (EosRows)."""
|
| 95 |
+
pre = prefix_len(encoded)
|
| 96 |
+
if not 1 <= pre <= S:
|
| 97 |
+
raise ValueError(f"prefix {pre} tokens does not fit S{S}")
|
| 98 |
+
ids = np.full(S + P, pad_id, dtype=np.int64)
|
| 99 |
+
pos = np.zeros(S + P, dtype=np.int64)
|
| 100 |
+
ids[:pre] = encoded[0]["ids"][:pre]
|
| 101 |
+
pos[:pre] = np.arange(pre)
|
| 102 |
+
pos[pre:S] = pre - 1
|
| 103 |
+
valid = np.zeros(S, dtype=np.float32)
|
| 104 |
+
valid[:pre] = 1
|
| 105 |
+
tail = np.zeros((LAGS, S), dtype=np.float32)
|
| 106 |
+
for i in range(LAGS): # tail row i = pre-conv input at position pre - LAGS + i (zero before the start)
|
| 107 |
+
p = pre - LAGS + i
|
| 108 |
+
if p >= 0:
|
| 109 |
+
tail[i, p] = 1
|
| 110 |
+
segment = np.eye(P, dtype=np.float32)
|
| 111 |
+
keep = np.zeros((LAGS, P), dtype=np.float32)
|
| 112 |
+
lag_tail = np.zeros((LAGS, P, LAGS), dtype=np.float32)
|
| 113 |
+
pos[S:] = pre
|
| 114 |
+
cand, qry, owner = [], [], []
|
| 115 |
+
start = 0
|
| 116 |
+
for j, e in enumerate(encoded):
|
| 117 |
+
suffix = e["ids"][pre:]
|
| 118 |
+
n = len(suffix)
|
| 119 |
+
if row_len:
|
| 120 |
+
start = j * row_len
|
| 121 |
+
if n > row_len:
|
| 122 |
+
raise ValueError(f"question of {n} tokens > row {row_len}")
|
| 123 |
+
elif lane and start // lane != (start + n - 1) // lane:
|
| 124 |
+
start = (start // lane + 1) * lane
|
| 125 |
+
if start + n > P:
|
| 126 |
+
raise ValueError(f"questions exceed P{P}")
|
| 127 |
+
ids[S + start : S + start + n] = suffix
|
| 128 |
+
pos[S + start : S + start + n] = np.arange(pre, pre + n)
|
| 129 |
+
segment[start : start + n, start : start + n] = np.tril(np.ones((n, n), dtype=np.float32))
|
| 130 |
+
for p in range(n):
|
| 131 |
+
for s in range(1, LAGS + 1):
|
| 132 |
+
if p >= s:
|
| 133 |
+
keep[s - 1, start + p] = 1
|
| 134 |
+
else:
|
| 135 |
+
lag_tail[s - 1, start + p, LAGS + p - s] = 1
|
| 136 |
+
for c in e["candidate_positions"]:
|
| 137 |
+
cand.append(start + c - pre)
|
| 138 |
+
qry.append(start + e["query_position"] - pre)
|
| 139 |
+
owner.append(j)
|
| 140 |
+
start += n
|
| 141 |
+
cos, sin = rope_cos_sin(cfg, torch.from_numpy(pos)[None].expand(3, -1))
|
| 142 |
+
return {
|
| 143 |
+
"input_ids": ids[None].astype(np.int32), "cos": cos.numpy(), "sin": sin.numpy(), "valid": valid,
|
| 144 |
+
"tail_onehot": tail, "segment": segment, "lag_keep": keep, "lag_tail": lag_tail,
|
| 145 |
+
}, cand, qry, owner, start
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def with_slots(x, cand, qry, N):
|
| 149 |
+
if len(cand) > N:
|
| 150 |
+
raise ValueError(f"{len(cand)} options > N{N}")
|
| 151 |
+
pad = N - len(cand)
|
| 152 |
+
return {**x, "cand_idx": np.array(cand + [0] * pad, dtype=np.int32),
|
| 153 |
+
"query_idx": np.array(qry + [0] * pad, dtype=np.int32)}
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class EosRows(EosFused):
|
| 157 |
+
"""Questions in B rows of Q tokens (one question per row, right-padded): the question delta rule runs chunked
|
| 158 |
+
(chunk 64) from the prefix's final state, batched over rows, instead of one P x P segment inverse."""
|
| 159 |
+
|
| 160 |
+
def __init__(self, cfg: TextConfig, state_len: int, rows: int, row_len: int, chunk: int = 64):
|
| 161 |
+
assert state_len % chunk == 0 and row_len % chunk == 0
|
| 162 |
+
super().__init__(cfg, state_len, rows * row_len, lane=row_len, chunk=chunk)
|
| 163 |
+
self.B, self.Q = rows, row_len
|
| 164 |
+
|
| 165 |
+
def _delta(self, net, x, valid, tail_onehot, segment, lag_keep, lag_tail):
|
| 166 |
+
cfg, S, P, B, Q = self.cfg, self.S, self.P, self.B, self.Q
|
| 167 |
+
T = S + P
|
| 168 |
+
H, Dk, Dv, kh = cfg.lin_v_heads, cfg.lin_k_dim, cfg.lin_v_dim, cfg.lin_k_heads
|
| 169 |
+
rep = H // kh
|
| 170 |
+
pre = net.in_proj_qkv(x)[0]
|
| 171 |
+
pre_s, pre_p = pre[:S], pre[S:]
|
| 172 |
+
tail = torch.matmul(tail_onehot, pre_s)
|
| 173 |
+
weight = net.conv1d.weight[:, 0]
|
| 174 |
+
lags = weight.shape[1] - 1
|
| 175 |
+
conv_s, conv_p = pre_s * weight[:, lags], pre_p * weight[:, lags]
|
| 176 |
+
for s in range(1, lags + 1):
|
| 177 |
+
conv_s = conv_s + F.pad(pre_s[: S - s], (0, 0, s, 0)) * weight[:, lags - s]
|
| 178 |
+
shifted = F.pad(pre_p[: P - s], (0, 0, s, 0)) * lag_keep[s - 1][:, None] + torch.matmul(lag_tail[s - 1], tail)
|
| 179 |
+
conv_p = conv_p + shifted * weight[:, lags - s]
|
| 180 |
+
qkv = F.silu(torch.cat([conv_s, conv_p], dim=0))
|
| 181 |
+
q = qkv[:, : net.key_dim].reshape(T, kh, Dk)
|
| 182 |
+
k = qkv[:, net.key_dim : 2 * net.key_dim].reshape(T, kh, Dk)
|
| 183 |
+
v = qkv[:, 2 * net.key_dim :].reshape(T, H, Dv).transpose(0, 1)
|
| 184 |
+
z = net.in_proj_z(x).reshape(1, T, H, Dv)
|
| 185 |
+
beta = torch.sigmoid(net.in_proj_b(x))[0].transpose(0, 1)
|
| 186 |
+
g = (-torch.exp(net.A_log) * F.softplus(net.in_proj_a(x) + net.dt_bias))[0].transpose(0, 1)
|
| 187 |
+
q = q * torch.rsqrt((q * q).sum(-1, keepdim=True) + 1e-6)
|
| 188 |
+
k = k * torch.rsqrt((k * k).sum(-1, keepdim=True) + 1e-6)
|
| 189 |
+
q = (q[:, :, None].expand(T, kh, rep, Dk).reshape(T, H, Dk) * (Dk**-0.5)).transpose(0, 1)
|
| 190 |
+
k = k[:, :, None].expand(T, kh, rep, Dk).reshape(T, H, Dk).transpose(0, 1)
|
| 191 |
+
|
| 192 |
+
C, N = net.chunk, net.num_chunks
|
| 193 |
+
new_v, k_cumdecay, q_dec, k_dec, attn_intra, chunk_decay = kev_stages._delta_chunk_terms(
|
| 194 |
+
net, q[:, :S].reshape(H, N, C, Dk), k[:, :S].reshape(H, N, C, Dk), v[:, :S].reshape(H, N, C, Dv),
|
| 195 |
+
(beta[:, :S] * valid).reshape(H, N, C, 1), (g[:, :S] * valid).reshape(H, N, C))
|
| 196 |
+
state = torch.zeros(H, Dk, Dv, dtype=x.dtype)
|
| 197 |
+
outs = []
|
| 198 |
+
for i in range(N):
|
| 199 |
+
v_new = new_v[:, i] - torch.matmul(k_cumdecay[:, i], state)
|
| 200 |
+
outs.append(torch.matmul(q_dec[:, i], state) + torch.matmul(attn_intra[:, i], v_new))
|
| 201 |
+
state = state * chunk_decay[:, i] + torch.matmul(k_dec[:, i].transpose(-1, -2), v_new)
|
| 202 |
+
|
| 203 |
+
G, M = H * B, Q // C
|
| 204 |
+
new_v, k_cumdecay, q_dec, k_dec, attn_intra, chunk_decay = kev_stages._delta_chunk_terms(
|
| 205 |
+
net, q[:, S:].reshape(G, M, C, Dk), k[:, S:].reshape(G, M, C, Dk), v[:, S:].reshape(G, M, C, Dv),
|
| 206 |
+
beta[:, S:].reshape(G, M, C, 1), g[:, S:].reshape(G, M, C))
|
| 207 |
+
st = state[:, None].expand(H, B, Dk, Dv).reshape(G, Dk, Dv)
|
| 208 |
+
rows = []
|
| 209 |
+
for i in range(M):
|
| 210 |
+
v_new = new_v[:, i] - torch.matmul(k_cumdecay[:, i], st)
|
| 211 |
+
rows.append(torch.matmul(q_dec[:, i], st) + torch.matmul(attn_intra[:, i], v_new))
|
| 212 |
+
if i + 1 < M:
|
| 213 |
+
st = st * chunk_decay[:, i] + torch.matmul(k_dec[:, i].transpose(-1, -2), v_new)
|
| 214 |
+
outs.append(torch.stack(rows, dim=1).reshape(H, P, Dv))
|
| 215 |
+
|
| 216 |
+
core = torch.cat(outs, dim=1).transpose(0, 1)[None]
|
| 217 |
+
core = net.norm(core, z)
|
| 218 |
+
return net.out_proj(core.reshape(1, T, H * Dv))
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def load_rows(root: str, state_len: int, rows: int, row_len: int) -> EosRows:
|
| 222 |
+
cfg = TextConfig(json.load(open(f"{root}/backbone/config.json")))
|
| 223 |
+
state = {k: v.float() for k, v in load_file(f"{root}/backbone/model.safetensors").items()}
|
| 224 |
+
model = EosRows(cfg, state_len, rows, row_len)
|
| 225 |
+
model.decoder.load_merged(state)
|
| 226 |
+
model.norm.load_state_dict({"weight": state["norm.weight"]})
|
| 227 |
+
model.embed_tokens = nn.Embedding.from_pretrained(state["embed_tokens.weight"], freeze=True)
|
| 228 |
+
head = {k: v.float() for k, v in load_file(f"{root}/decision_head.safetensors").items()}
|
| 229 |
+
model.load_state_dict(head, strict=False)
|
| 230 |
+
return model.eval()
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
class EosChunked(EosFused):
|
| 234 |
+
"""Packed questions with a chunked delta rule (chunk C) instead of one P x P segment inverse.
|
| 235 |
+
|
| 236 |
+
Per chunk, a token starts either from the carried recurrent state (its question began in an earlier chunk,
|
| 237 |
+
`cont` = 1) or from the prefix's final state S0 (its question began in this chunk). `seg_chunks` [M, C, C] is the
|
| 238 |
+
segment mask restricted to each chunk (j <= t, same question); `last_seg` [M, C] marks the tokens in the question
|
| 239 |
+
of the chunk's last token, the only one whose state is carried to the next chunk."""
|
| 240 |
+
|
| 241 |
+
def __init__(self, cfg: TextConfig, state_len: int, packed_len: int, chunk: int = 64):
|
| 242 |
+
assert state_len % chunk == 0 and packed_len % chunk == 0
|
| 243 |
+
super().__init__(cfg, state_len, packed_len, lane=packed_len, chunk=chunk)
|
| 244 |
+
self.C, self.M = chunk, packed_len // chunk
|
| 245 |
+
self.register_buffer("eye_c", torch.eye(chunk), persistent=False)
|
| 246 |
+
|
| 247 |
+
def forward(self, input_ids, cos, sin, valid, tail_onehot, segment, lag_keep, lag_tail, seg_chunks, cont,
|
| 248 |
+
last_seg, cand_idx, query_idx):
|
| 249 |
+
self._extra = (seg_chunks, cont, last_seg)
|
| 250 |
+
return super().forward(input_ids, cos, sin, valid, tail_onehot, segment, lag_keep, lag_tail, cand_idx, query_idx)
|
| 251 |
+
|
| 252 |
+
def _delta(self, net, x, valid, tail_onehot, segment, lag_keep, lag_tail):
|
| 253 |
+
seg_chunks, cont, last_seg = self._extra
|
| 254 |
+
cfg, S, P, C, M = self.cfg, self.S, self.P, self.C, self.M
|
| 255 |
+
T = S + P
|
| 256 |
+
H, Dk, Dv, kh = cfg.lin_v_heads, cfg.lin_k_dim, cfg.lin_v_dim, cfg.lin_k_heads
|
| 257 |
+
rep = H // kh
|
| 258 |
+
pre = net.in_proj_qkv(x)[0]
|
| 259 |
+
pre_s, pre_p = pre[:S], pre[S:]
|
| 260 |
+
tail = torch.matmul(tail_onehot, pre_s)
|
| 261 |
+
weight = net.conv1d.weight[:, 0]
|
| 262 |
+
lags = weight.shape[1] - 1
|
| 263 |
+
conv_s, conv_p = pre_s * weight[:, lags], pre_p * weight[:, lags]
|
| 264 |
+
for s in range(1, lags + 1):
|
| 265 |
+
conv_s = conv_s + F.pad(pre_s[: S - s], (0, 0, s, 0)) * weight[:, lags - s]
|
| 266 |
+
shifted = F.pad(pre_p[: P - s], (0, 0, s, 0)) * lag_keep[s - 1][:, None] + torch.matmul(lag_tail[s - 1], tail)
|
| 267 |
+
conv_p = conv_p + shifted * weight[:, lags - s]
|
| 268 |
+
qkv = F.silu(torch.cat([conv_s, conv_p], dim=0))
|
| 269 |
+
q = qkv[:, : net.key_dim].reshape(T, kh, Dk)
|
| 270 |
+
k = qkv[:, net.key_dim : 2 * net.key_dim].reshape(T, kh, Dk)
|
| 271 |
+
v = qkv[:, 2 * net.key_dim :].reshape(T, H, Dv).transpose(0, 1)
|
| 272 |
+
z = net.in_proj_z(x).reshape(1, T, H, Dv)
|
| 273 |
+
beta = torch.sigmoid(net.in_proj_b(x))[0].transpose(0, 1)
|
| 274 |
+
g = (-torch.exp(net.A_log) * F.softplus(net.in_proj_a(x) + net.dt_bias))[0].transpose(0, 1)
|
| 275 |
+
q = q * torch.rsqrt((q * q).sum(-1, keepdim=True) + 1e-6)
|
| 276 |
+
k = k * torch.rsqrt((k * k).sum(-1, keepdim=True) + 1e-6)
|
| 277 |
+
q = (q[:, :, None].expand(T, kh, rep, Dk).reshape(T, H, Dk) * (Dk**-0.5)).transpose(0, 1)
|
| 278 |
+
k = k[:, :, None].expand(T, kh, rep, Dk).reshape(T, H, Dk).transpose(0, 1)
|
| 279 |
+
|
| 280 |
+
N = net.num_chunks
|
| 281 |
+
new_v, k_cumdecay, q_dec, k_dec, attn_intra, chunk_decay = kev_stages._delta_chunk_terms(
|
| 282 |
+
net, q[:, :S].reshape(H, N, net.chunk, Dk), k[:, :S].reshape(H, N, net.chunk, Dk),
|
| 283 |
+
v[:, :S].reshape(H, N, net.chunk, Dv), (beta[:, :S] * valid).reshape(H, N, net.chunk, 1),
|
| 284 |
+
(g[:, :S] * valid).reshape(H, N, net.chunk))
|
| 285 |
+
state = torch.zeros(H, Dk, Dv, dtype=x.dtype)
|
| 286 |
+
outs = []
|
| 287 |
+
for i in range(N):
|
| 288 |
+
v_new = new_v[:, i] - torch.matmul(k_cumdecay[:, i], state)
|
| 289 |
+
outs.append(torch.matmul(q_dec[:, i], state) + torch.matmul(attn_intra[:, i], v_new))
|
| 290 |
+
state = state * chunk_decay[:, i] + torch.matmul(k_dec[:, i].transpose(-1, -2), v_new)
|
| 291 |
+
s0 = state
|
| 292 |
+
|
| 293 |
+
# questions: packed, chunked, segment-aware
|
| 294 |
+
qp = q[:, S:].reshape(H, M, C, Dk)
|
| 295 |
+
kp = k[:, S:].reshape(H, M, C, Dk)
|
| 296 |
+
vp = v[:, S:].reshape(H, M, C, Dv)
|
| 297 |
+
bp = beta[:, S:].reshape(H, M, C, 1)
|
| 298 |
+
gp = g[:, S:].reshape(H, M, C, 1)
|
| 299 |
+
cum = torch.matmul(seg_chunks, gp).squeeze(-1) # [H, M, C]: decay since max(chunk start, question start)
|
| 300 |
+
pair_decay = torch.exp((cum[..., :, None] - cum[..., None, :]) * seg_chunks + (seg_chunks - 1.0) * 1e4)
|
| 301 |
+
k_beta = kp * bp
|
| 302 |
+
kkt = torch.matmul(k_beta, kp.transpose(-1, -2)) * pair_decay
|
| 303 |
+
attn = torch.matmul(qp, kp.transpose(-1, -2)) * pair_decay
|
| 304 |
+
inv = kev_stages._unit_lower_inverse((self.eye_c + kkt * (seg_chunks - self.eye_c)).reshape(H * M, C, C), C)
|
| 305 |
+
inv = inv.reshape(H, M, C, C)
|
| 306 |
+
decay = torch.exp(cum)[..., None] # [H, M, C, 1]
|
| 307 |
+
new_v = torch.matmul(inv, vp * bp)
|
| 308 |
+
k_cd = torch.matmul(inv, k_beta * decay)
|
| 309 |
+
q_dec = qp * decay
|
| 310 |
+
ls = last_seg[None, :, :]
|
| 311 |
+
to_last = (torch.exp((cum[..., -1:] - cum) * ls) * ls)[..., None] # [H, M, C, 1]
|
| 312 |
+
k_last = kp * to_last
|
| 313 |
+
last_decay = torch.exp(cum[..., -1:])[..., None] # [H, M, 1, 1]
|
| 314 |
+
cont_c = cont.reshape(M, C, 1)
|
| 315 |
+
carried = s0
|
| 316 |
+
for i in range(M):
|
| 317 |
+
c = cont_c[i]
|
| 318 |
+
v_new = new_v[:, i] - c * torch.matmul(k_cd[:, i], carried) - (1.0 - c) * torch.matmul(k_cd[:, i], s0)
|
| 319 |
+
outs.append(c * torch.matmul(q_dec[:, i], carried) + (1.0 - c) * torch.matmul(q_dec[:, i], s0)
|
| 320 |
+
+ torch.matmul(attn[:, i], v_new))
|
| 321 |
+
if i + 1 < M:
|
| 322 |
+
cl = cont_c[i, -1:]
|
| 323 |
+
init = cl * carried + (1.0 - cl) * s0
|
| 324 |
+
carried = init * last_decay[:, i] + torch.matmul(k_last[:, i].transpose(-1, -2), v_new)
|
| 325 |
+
|
| 326 |
+
core = torch.cat(outs, dim=1).transpose(0, 1)[None]
|
| 327 |
+
core = net.norm(core, z)
|
| 328 |
+
return net.out_proj(core.reshape(1, T, H * Dv))
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
def load_chunked(root: str, state_len: int, packed_len: int) -> EosChunked:
|
| 332 |
+
cfg = TextConfig(json.load(open(f"{root}/backbone/config.json")))
|
| 333 |
+
state = {k: v.float() for k, v in load_file(f"{root}/backbone/model.safetensors").items()}
|
| 334 |
+
model = EosChunked(cfg, state_len, packed_len)
|
| 335 |
+
model.decoder.load_merged(state)
|
| 336 |
+
model.norm.load_state_dict({"weight": state["norm.weight"]})
|
| 337 |
+
model.embed_tokens = nn.Embedding.from_pretrained(state["embed_tokens.weight"], freeze=True)
|
| 338 |
+
model.load_state_dict({k: v.float() for k, v in load_file(f"{root}/decision_head.safetensors").items()},
|
| 339 |
+
strict=False)
|
| 340 |
+
return model.eval()
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def chunk_inputs(segment: np.ndarray, C: int = 64):
|
| 344 |
+
"""seg_chunks [M, C, C], cont [P], last_seg [M, C] from the packed segment mask [P, P]."""
|
| 345 |
+
P = segment.shape[0]
|
| 346 |
+
M = P // C
|
| 347 |
+
seg_chunks = np.stack([segment[i * C : (i + 1) * C, i * C : (i + 1) * C] for i in range(M)])
|
| 348 |
+
starts = np.argmax(segment > 0, axis=1) # first position of each token's question (segment row's first 1)
|
| 349 |
+
cont = (starts < (np.arange(P) // C) * C).astype(np.float32)
|
| 350 |
+
last_seg = np.stack([segment[(i + 1) * C - 1, i * C : (i + 1) * C] for i in range(M)])
|
| 351 |
+
return seg_chunks.astype(np.float32), cont, last_seg.astype(np.float32)
|
conversion/kev_stages.py
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Two-stage Kev export: read the state once, then answer every question from its cache.
|
| 2 |
+
|
| 3 |
+
Kev's server does this with a KV + recurrent-state cache; a row-per-question export re-reads the state for every
|
| 4 |
+
question. Stage A (`StatePass`) runs the state tokens and returns, per layer, what a continuation needs:
|
| 5 |
+
|
| 6 |
+
- full-attention layers: keys (after norm and RoPE) and values of the state positions;
|
| 7 |
+
- Gated DeltaNet layers: the recurrent state after the last real token, and the last `conv_kernel - 1` pre-convolution
|
| 8 |
+
inputs (the causal conv's history).
|
| 9 |
+
|
| 10 |
+
The state is right-padded to a bucket. Padded positions get beta = 0 and g = 0 in the delta rule, which makes them
|
| 11 |
+
exact no-ops on the recurrent state (the WY inverse row of a beta = 0 position is the identity row), so the state
|
| 12 |
+
comes out as of the last real token. Attention keys of padded positions are masked in stage B.
|
| 13 |
+
|
| 14 |
+
Stage B (`QuestionPass`) runs B question branches at once, each right-padded to Q tokens and positioned right after
|
| 15 |
+
the state, on top of that cache, and applies Kev's pointer head at <decide> and each </opt>.
|
| 16 |
+
"""
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
import torch.nn.functional as F
|
| 21 |
+
from torch import nn
|
| 22 |
+
|
| 23 |
+
from qwen35_export import DecoderChunk, RMSNorm, TextConfig, rotate_half
|
| 24 |
+
|
| 25 |
+
MASK_VALUE = -1e4
|
| 26 |
+
MASKED_INVERSE = True
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _rope(x, cos, sin, r):
|
| 30 |
+
return torch.cat([x[..., :r] * cos + rotate_half(x[..., :r]) * sin, x[..., r:]], dim=-1)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _delta_chunk_terms(net, q, k, v, beta, g):
|
| 34 |
+
"""Chunked delta-rule terms for tensors shaped [G, N, C, D] (beta [G, N, C, 1], g [G, N, C])."""
|
| 35 |
+
g_col = g.unsqueeze(-1)
|
| 36 |
+
cum = torch.matmul(net.prefix_sum_t, g_col).squeeze(-1)
|
| 37 |
+
to_end = torch.matmul(net.suffix_sum_t, g_col).squeeze(-1)
|
| 38 |
+
lead = g.shape[:2]
|
| 39 |
+
pair = torch.matmul(net.pair_sum_t, g_col).reshape(*lead, net.chunk, net.chunk)
|
| 40 |
+
pair_decay = torch.exp(pair) * net.lower_incl
|
| 41 |
+
k_beta, v_beta = k * beta, v * beta
|
| 42 |
+
kkt = torch.matmul(k_beta, k.transpose(-1, -2)) * pair_decay
|
| 43 |
+
attn_intra = torch.matmul(q, k.transpose(-1, -2)) * pair_decay
|
| 44 |
+
inv = _unit_lower_inverse(net.eye + kkt * net.strict_lower, net.chunk) if MASKED_INVERSE else \
|
| 45 |
+
net.unit_lower_inverse(net.eye + kkt * net.strict_lower)
|
| 46 |
+
new_v = torch.matmul(inv, v_beta)
|
| 47 |
+
k_cumdecay = torch.matmul(inv, k_beta * torch.exp(cum)[..., None])
|
| 48 |
+
q_dec = q * torch.exp(cum)[..., None]
|
| 49 |
+
k_dec = k * torch.exp(to_end)[..., None]
|
| 50 |
+
chunk_decay = torch.exp(cum[..., -1:])[..., None]
|
| 51 |
+
return new_v, k_cumdecay, q_dec, k_dec, attn_intra, chunk_decay
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _delta_inputs(net, x, conv_input_prefix=None):
|
| 55 |
+
"""Projections of a DeltaNet layer for x [B, L, D]. Returns q, k, v (heads expanded), z, beta, g, and the
|
| 56 |
+
pre-convolution qkv [B, L, conv]."""
|
| 57 |
+
cfg = net.cfg
|
| 58 |
+
B, L = x.shape[0], x.shape[1]
|
| 59 |
+
H, Dk, Dv, kh = cfg.lin_v_heads, cfg.lin_k_dim, cfg.lin_v_dim, cfg.lin_k_heads
|
| 60 |
+
rep = H // kh
|
| 61 |
+
pre = net.in_proj_qkv(x) # [B, L, conv]
|
| 62 |
+
seq = pre.transpose(1, 2) # [B, conv, L]
|
| 63 |
+
if conv_input_prefix is None:
|
| 64 |
+
seq = F.pad(seq, (cfg.conv_kernel - 1, 0))
|
| 65 |
+
else:
|
| 66 |
+
seq = torch.cat([conv_input_prefix, seq], dim=-1)
|
| 67 |
+
qkv = F.silu(net.conv1d(seq)).transpose(1, 2) # [B, L, conv]
|
| 68 |
+
q = qkv[..., : net.key_dim].reshape(B, L, kh, Dk)
|
| 69 |
+
k = qkv[..., net.key_dim : 2 * net.key_dim].reshape(B, L, kh, Dk)
|
| 70 |
+
v = qkv[..., 2 * net.key_dim :].reshape(B, L, H, Dv)
|
| 71 |
+
z = net.in_proj_z(x).reshape(B, L, H, Dv)
|
| 72 |
+
beta = torch.sigmoid(net.in_proj_b(x)) # [B, L, H]
|
| 73 |
+
g = -torch.exp(net.A_log) * F.softplus(net.in_proj_a(x) + net.dt_bias) # [B, L, H]
|
| 74 |
+
q = q * torch.rsqrt((q * q).sum(-1, keepdim=True) + 1e-6)
|
| 75 |
+
k = k * torch.rsqrt((k * k).sum(-1, keepdim=True) + 1e-6)
|
| 76 |
+
q = q[:, :, :, None].expand(B, L, kh, rep, Dk).reshape(B, L, H, Dk) * (Dk**-0.5)
|
| 77 |
+
k = k[:, :, :, None].expand(B, L, kh, rep, Dk).reshape(B, L, H, Dk)
|
| 78 |
+
return q, k, v, z, beta, g, pre
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class StatePass(nn.Module):
|
| 82 |
+
def __init__(self, cfg: TextConfig, seq_len: int, chunk_size: int = 128):
|
| 83 |
+
super().__init__()
|
| 84 |
+
self.cfg, self.S = cfg, seq_len
|
| 85 |
+
self.decoder = DecoderChunk(cfg, 0, cfg.num_layers, seq_len, chunk_size=chunk_size, with_head=False)
|
| 86 |
+
|
| 87 |
+
def _attention(self, attn, x, cos, sin):
|
| 88 |
+
cfg, S = self.cfg, self.S
|
| 89 |
+
h, kv, d, r = cfg.num_heads, cfg.num_kv_heads, cfg.head_dim, cfg.rotary_dim
|
| 90 |
+
qg = attn.q_proj(x).view(1, S, h, 2 * d)
|
| 91 |
+
q, gate = qg[..., :d], qg[..., d:].reshape(1, S, h * d)
|
| 92 |
+
q = attn.q_norm(q).transpose(1, 2)
|
| 93 |
+
k = attn.k_norm(attn.k_proj(x).view(1, S, kv, d)).transpose(1, 2)
|
| 94 |
+
v = attn.v_proj(x).view(1, S, kv, d).transpose(1, 2)
|
| 95 |
+
c, s = cos[None, None], sin[None, None]
|
| 96 |
+
q, k = _rope(q, c, s, r), _rope(k, c, s, r)
|
| 97 |
+
rep = h // kv
|
| 98 |
+
kr = k[:, :, None].expand(1, kv, rep, S, d).reshape(1, h, S, d)
|
| 99 |
+
vr = v[:, :, None].expand(1, kv, rep, S, d).reshape(1, h, S, d)
|
| 100 |
+
scores = torch.matmul(q, kr.transpose(-1, -2)) * (d**-0.5) + self.decoder.mask
|
| 101 |
+
out = torch.matmul(torch.softmax(scores, dim=-1), vr).transpose(1, 2).reshape(1, S, h * d)
|
| 102 |
+
return attn.o_proj(out * torch.sigmoid(gate)), k[0], v[0]
|
| 103 |
+
|
| 104 |
+
def _delta(self, net, x, valid, tail_onehot):
|
| 105 |
+
cfg = self.cfg
|
| 106 |
+
L, C, N = self.S, net.chunk, net.num_chunks
|
| 107 |
+
H, Dk, Dv = cfg.lin_v_heads, cfg.lin_k_dim, cfg.lin_v_dim
|
| 108 |
+
q, k, v, z, beta, g, pre = _delta_inputs(net, x)
|
| 109 |
+
tail = torch.matmul(tail_onehot, pre[0]).transpose(0, 1) # [conv, kernel-1]
|
| 110 |
+
mask = valid[None, :, None]
|
| 111 |
+
beta, g = beta * mask, g * mask # padded positions: no write, no decay
|
| 112 |
+
q = q[0].permute(1, 0, 2).reshape(H, N, C, Dk)
|
| 113 |
+
k = k[0].permute(1, 0, 2).reshape(H, N, C, Dk)
|
| 114 |
+
v = v[0].permute(1, 0, 2).reshape(H, N, C, Dv)
|
| 115 |
+
beta = beta[0].T.reshape(H, N, C, 1)
|
| 116 |
+
g = g[0].T.reshape(H, N, C)
|
| 117 |
+
new_v, k_cumdecay, q_dec, k_dec, attn_intra, chunk_decay = _delta_chunk_terms(net, q, k, v, beta, g)
|
| 118 |
+
state = torch.zeros(H, Dk, Dv, dtype=x.dtype)
|
| 119 |
+
outs = []
|
| 120 |
+
for i in range(N):
|
| 121 |
+
v_new = new_v[:, i] - torch.matmul(k_cumdecay[:, i], state)
|
| 122 |
+
outs.append(torch.matmul(q_dec[:, i], state) + torch.matmul(attn_intra[:, i], v_new))
|
| 123 |
+
state = state * chunk_decay[:, i] + torch.matmul(k_dec[:, i].transpose(-1, -2), v_new)
|
| 124 |
+
core = torch.stack(outs, dim=1).reshape(H, L, Dv).permute(1, 0, 2)
|
| 125 |
+
core = net.norm(core, z[0])
|
| 126 |
+
return net.out_proj(core.reshape(1, L, H * Dv)), state, tail
|
| 127 |
+
|
| 128 |
+
def forward(self, hidden, cos, sin, valid, tail_onehot):
|
| 129 |
+
"""hidden [1, S, D], cos/sin [S, R], valid [S] (1 = real token), tail_onehot [kernel-1, S] selecting the last
|
| 130 |
+
real positions in order -> keys, values [A, kv, S, d]; delta states [Ld, H, Dk, Dv]; conv tails [Ld, conv, k-1]."""
|
| 131 |
+
keys, values, states, tails = [], [], [], []
|
| 132 |
+
x = hidden
|
| 133 |
+
for layer in self.decoder.layers:
|
| 134 |
+
h = layer.input_layernorm(x)
|
| 135 |
+
if layer.is_linear:
|
| 136 |
+
out, state, tail = self._delta(layer.linear_attn, h, valid, tail_onehot)
|
| 137 |
+
states.append(state)
|
| 138 |
+
tails.append(tail)
|
| 139 |
+
else:
|
| 140 |
+
out, key, value = self._attention(layer.self_attn, h, cos, sin)
|
| 141 |
+
keys.append(key)
|
| 142 |
+
values.append(value)
|
| 143 |
+
x = x + out
|
| 144 |
+
x = x + layer.mlp(layer.post_attention_layernorm(x))
|
| 145 |
+
return torch.stack(keys), torch.stack(values), torch.stack(states), torch.stack(tails)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
class QuestionPass(nn.Module):
|
| 149 |
+
def __init__(self, cfg: TextConfig, state_len: int, question_len: int, batch: int, max_options: int,
|
| 150 |
+
pointer_dim: int = 256):
|
| 151 |
+
super().__init__()
|
| 152 |
+
self.cfg, self.S, self.Q, self.B, self.K = cfg, state_len, question_len, batch, max_options
|
| 153 |
+
# chunk = the whole question: one delta-rule chunk on top of the cached state
|
| 154 |
+
self.decoder = DecoderChunk(cfg, 0, cfg.num_layers, question_len, chunk_size=question_len, with_head=False)
|
| 155 |
+
self.norm = RMSNorm(cfg.hidden_size, cfg.eps)
|
| 156 |
+
self.q = nn.Linear(cfg.hidden_size, pointer_dim)
|
| 157 |
+
self.k = nn.Linear(cfg.hidden_size, pointer_dim)
|
| 158 |
+
self.register_buffer("pointer_scale", torch.tensor([pointer_dim**-0.5]))
|
| 159 |
+
self.register_buffer("inverse_temperature", torch.tensor([1.0]))
|
| 160 |
+
causal = torch.full((question_len, question_len), MASK_VALUE).triu(1)
|
| 161 |
+
self.register_buffer("causal", causal)
|
| 162 |
+
|
| 163 |
+
def _attention(self, attn, x, cos, sin, key_cache, value_cache, cache_bias):
|
| 164 |
+
cfg, B, Q, S = self.cfg, self.B, self.Q, self.S
|
| 165 |
+
h, kv, d, r = cfg.num_heads, cfg.num_kv_heads, cfg.head_dim, cfg.rotary_dim
|
| 166 |
+
qg = attn.q_proj(x).view(B, Q, h, 2 * d)
|
| 167 |
+
q, gate = qg[..., :d], qg[..., d:].reshape(B, Q, h * d)
|
| 168 |
+
q = attn.q_norm(q).transpose(1, 2) # [B, h, Q, d]
|
| 169 |
+
k = attn.k_norm(attn.k_proj(x).view(B, Q, kv, d)).transpose(1, 2)
|
| 170 |
+
v = attn.v_proj(x).view(B, Q, kv, d).transpose(1, 2)
|
| 171 |
+
c, s = cos[None, None], sin[None, None]
|
| 172 |
+
q, k = _rope(q, c, s, r), _rope(k, c, s, r)
|
| 173 |
+
k_all = torch.cat([key_cache[None].expand(B, kv, S, d), k], dim=2) # [B, kv, S+Q, d]
|
| 174 |
+
v_all = torch.cat([value_cache[None].expand(B, kv, S, d), v], dim=2)
|
| 175 |
+
rep, T = h // kv, S + Q
|
| 176 |
+
k_all = k_all[:, :, None].expand(B, kv, rep, T, d).reshape(B, h, T, d)
|
| 177 |
+
v_all = v_all[:, :, None].expand(B, kv, rep, T, d).reshape(B, h, T, d)
|
| 178 |
+
bias = torch.cat([cache_bias[None, :].expand(Q, S), self.causal], dim=1) # [Q, S+Q]
|
| 179 |
+
scores = torch.matmul(q, k_all.transpose(-1, -2)) * (d**-0.5) + bias
|
| 180 |
+
out = torch.matmul(torch.softmax(scores, dim=-1), v_all).transpose(1, 2).reshape(B, Q, h * d)
|
| 181 |
+
return attn.o_proj(out * torch.sigmoid(gate))
|
| 182 |
+
|
| 183 |
+
def _delta(self, net, x, state, tail):
|
| 184 |
+
cfg, B, Q = self.cfg, self.B, self.Q
|
| 185 |
+
H, Dk, Dv = cfg.lin_v_heads, cfg.lin_k_dim, cfg.lin_v_dim
|
| 186 |
+
q, k, v, z, beta, g, _ = _delta_inputs(net, x, conv_input_prefix=tail[None].expand(B, -1, -1))
|
| 187 |
+
G = B * H
|
| 188 |
+
q = q.permute(0, 2, 1, 3).reshape(G, 1, Q, Dk)
|
| 189 |
+
k = k.permute(0, 2, 1, 3).reshape(G, 1, Q, Dk)
|
| 190 |
+
v = v.permute(0, 2, 1, 3).reshape(G, 1, Q, Dv)
|
| 191 |
+
beta = beta.permute(0, 2, 1).reshape(G, 1, Q, 1)
|
| 192 |
+
g = g.permute(0, 2, 1).reshape(G, 1, Q)
|
| 193 |
+
new_v, k_cumdecay, q_dec, _, attn_intra, _ = _delta_chunk_terms(net, q, k, v, beta, g)
|
| 194 |
+
s0 = state[None].expand(B, H, Dk, Dv).reshape(G, Dk, Dv)
|
| 195 |
+
v_new = new_v[:, 0] - torch.matmul(k_cumdecay[:, 0], s0)
|
| 196 |
+
core = torch.matmul(q_dec[:, 0], s0) + torch.matmul(attn_intra[:, 0], v_new) # [G, Q, Dv]
|
| 197 |
+
core = core.reshape(B, H, Q, Dv).permute(0, 2, 1, 3) # [B, Q, H, Dv]
|
| 198 |
+
core = net.norm(core, z)
|
| 199 |
+
return net.out_proj(core.reshape(B, Q, H * Dv))
|
| 200 |
+
|
| 201 |
+
def forward(self, hidden, cos, sin, keys, values, cache_valid, states, tails, decide_onehot, option_onehot,
|
| 202 |
+
option_mask):
|
| 203 |
+
"""hidden [B, Q, D]; cos/sin [Q, R] (positions n .. n+Q-1); keys/values [A, kv, S, d]; cache_valid [S];
|
| 204 |
+
states [Ld, H, Dk, Dv]; tails [Ld, conv, k-1]; decide_onehot [B, Q]; option_onehot [B, K, Q];
|
| 205 |
+
option_mask [B, K] -> logits [B, K] (temperature applied), probabilities [B, K]."""
|
| 206 |
+
cache_bias = (1.0 - cache_valid) * MASK_VALUE
|
| 207 |
+
x = hidden
|
| 208 |
+
a = d = 0
|
| 209 |
+
for layer in self.decoder.layers:
|
| 210 |
+
h = layer.input_layernorm(x)
|
| 211 |
+
if layer.is_linear:
|
| 212 |
+
out = self._delta(layer.linear_attn, h, states[d], tails[d])
|
| 213 |
+
d += 1
|
| 214 |
+
else:
|
| 215 |
+
out = self._attention(layer.self_attn, h, cos, sin, keys[a], values[a], cache_bias)
|
| 216 |
+
a += 1
|
| 217 |
+
x = x + out
|
| 218 |
+
x = x + layer.mlp(layer.post_attention_layernorm(x))
|
| 219 |
+
decide = self.norm(torch.matmul(decide_onehot[:, None, :], x)[:, 0]) # [B, D]
|
| 220 |
+
options = self.norm(torch.matmul(option_onehot, x)) # [B, K, D]
|
| 221 |
+
logits = (self.k(options) * self.q(decide)[:, None, :]).sum(-1) * self.pointer_scale * self.inverse_temperature
|
| 222 |
+
logits = torch.where(option_mask > 0.5, logits, torch.full_like(logits, MASK_VALUE))
|
| 223 |
+
return logits, torch.softmax(logits, dim=-1)
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def load_stages(merged, state_len: int, question_len: int, batch: int, max_options: int, chunk_size: int = 128):
|
| 227 |
+
import json
|
| 228 |
+
from pathlib import Path
|
| 229 |
+
|
| 230 |
+
from safetensors.torch import load_file
|
| 231 |
+
|
| 232 |
+
merged = Path(merged)
|
| 233 |
+
meta = json.loads((merged / "meta.json").read_text())
|
| 234 |
+
cfg = TextConfig(meta["text_config"])
|
| 235 |
+
state = load_file(str(merged / "text.safetensors"))
|
| 236 |
+
head = load_file(str(merged / "head.safetensors"))
|
| 237 |
+
first = StatePass(cfg, state_len, chunk_size)
|
| 238 |
+
first.decoder.load_merged(state)
|
| 239 |
+
second = QuestionPass(cfg, state_len, question_len, batch, max_options)
|
| 240 |
+
second.decoder.load_merged(state)
|
| 241 |
+
second.norm.load_state_dict({"weight": state["norm.weight"]})
|
| 242 |
+
second.q.load_state_dict({"weight": head["q.weight"], "bias": head["q.bias"]})
|
| 243 |
+
second.k.load_state_dict({"weight": head["k.weight"], "bias": head["k.bias"]})
|
| 244 |
+
second.pointer_scale.fill_(meta["pointer_scale"])
|
| 245 |
+
second.inverse_temperature.fill_(1.0 / meta["temperature"])
|
| 246 |
+
return first.eval(), second.eval(), cfg, meta, state["embed_tokens.weight"]
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def stage_inputs(cfg, embed, state_ids, question_rows, state_len, question_len, batch, max_options, pad_id):
|
| 250 |
+
"""question_rows: [(branch ids, decide offset, option offsets)] with offsets inside the branch."""
|
| 251 |
+
from qwen35_export import rope_cos_sin
|
| 252 |
+
|
| 253 |
+
n = len(state_ids)
|
| 254 |
+
if n > state_len or len(question_rows) > batch:
|
| 255 |
+
raise ValueError(f"state {n} > {state_len} or {len(question_rows)} questions > {batch}")
|
| 256 |
+
ids = torch.full((state_len,), pad_id, dtype=torch.long)
|
| 257 |
+
ids[:n] = torch.tensor(state_ids)
|
| 258 |
+
positions = torch.arange(state_len)
|
| 259 |
+
cos, sin = rope_cos_sin(cfg, positions.unsqueeze(0).expand(3, -1))
|
| 260 |
+
valid = torch.zeros(state_len)
|
| 261 |
+
valid[:n] = 1
|
| 262 |
+
kernel = cfg.conv_kernel - 1
|
| 263 |
+
tail = torch.zeros(kernel, state_len)
|
| 264 |
+
for j in range(kernel):
|
| 265 |
+
p = n - kernel + j
|
| 266 |
+
if p >= 0:
|
| 267 |
+
tail[j, p] = 1
|
| 268 |
+
state_in = (embed[ids].unsqueeze(0), cos, sin, valid, tail)
|
| 269 |
+
|
| 270 |
+
q_ids = torch.full((batch, question_len), pad_id, dtype=torch.long)
|
| 271 |
+
decide = torch.zeros(batch, question_len)
|
| 272 |
+
options = torch.zeros(batch, max_options, question_len)
|
| 273 |
+
mask = torch.zeros(batch, max_options)
|
| 274 |
+
for b, (branch, d, opts) in enumerate(question_rows):
|
| 275 |
+
if len(branch) > question_len or len(opts) > max_options:
|
| 276 |
+
raise ValueError("question exceeds its bucket")
|
| 277 |
+
q_ids[b, : len(branch)] = torch.tensor(branch)
|
| 278 |
+
decide[b, d] = 1
|
| 279 |
+
for i, o in enumerate(opts):
|
| 280 |
+
options[b, i, o] = 1
|
| 281 |
+
mask[b, i] = 1
|
| 282 |
+
q_pos = torch.arange(n, n + question_len)
|
| 283 |
+
q_cos, q_sin = rope_cos_sin(cfg, q_pos.unsqueeze(0).expand(3, -1))
|
| 284 |
+
return state_in, (embed[q_ids], q_cos, q_sin, valid, decide, options, mask)
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
class PackedQuestionPass(nn.Module):
|
| 288 |
+
"""Stage B with the questions packed end to end in one sequence of P tokens instead of B padded rows.
|
| 289 |
+
|
| 290 |
+
`segment` [P, P] is 1 where j <= i in the same question (padding positions are their own one-token segments):
|
| 291 |
+
it is the attention mask over the question tokens, the segment-local cumulative-decay operator of the delta rule
|
| 292 |
+
(every question restarts from the cached state), and its strict part is the WY triangle. A question's first
|
| 293 |
+
conv_kernel - 1 positions read the state's conv tail instead of the previous question: `lag_keep` [k-1, P] keeps
|
| 294 |
+
in-segment lags and `lag_tail` [k-1, P, k-1] selects tail entries for the rest.
|
| 295 |
+
"""
|
| 296 |
+
|
| 297 |
+
def __init__(self, cfg: TextConfig, state_len: int, packed_len: int, batch: int, max_options: int,
|
| 298 |
+
pointer_dim: int = 256):
|
| 299 |
+
super().__init__()
|
| 300 |
+
self.cfg, self.S, self.P, self.B, self.K = cfg, state_len, packed_len, batch, max_options
|
| 301 |
+
self.decoder = DecoderChunk(cfg, 0, cfg.num_layers, packed_len, chunk_size=packed_len, with_head=False)
|
| 302 |
+
self.norm = RMSNorm(cfg.hidden_size, cfg.eps)
|
| 303 |
+
self.q = nn.Linear(cfg.hidden_size, pointer_dim)
|
| 304 |
+
self.k = nn.Linear(cfg.hidden_size, pointer_dim)
|
| 305 |
+
self.register_buffer("pointer_scale", torch.tensor([pointer_dim**-0.5]))
|
| 306 |
+
self.register_buffer("inverse_temperature", torch.tensor([1.0]))
|
| 307 |
+
self.register_buffer("eye", torch.eye(packed_len))
|
| 308 |
+
|
| 309 |
+
def _attention(self, attn, x, cos, sin, key_cache, value_cache, bias):
|
| 310 |
+
cfg, P, S = self.cfg, self.P, self.S
|
| 311 |
+
h, kv, d, r = cfg.num_heads, cfg.num_kv_heads, cfg.head_dim, cfg.rotary_dim
|
| 312 |
+
qg = attn.q_proj(x).view(1, P, h, 2 * d)
|
| 313 |
+
q, gate = qg[..., :d], qg[..., d:].reshape(1, P, h * d)
|
| 314 |
+
q = attn.q_norm(q).transpose(1, 2)
|
| 315 |
+
k = attn.k_norm(attn.k_proj(x).view(1, P, kv, d)).transpose(1, 2)
|
| 316 |
+
v = attn.v_proj(x).view(1, P, kv, d).transpose(1, 2)
|
| 317 |
+
c, s = cos[None, None], sin[None, None]
|
| 318 |
+
q, k = _rope(q, c, s, r), _rope(k, c, s, r)
|
| 319 |
+
k_all = torch.cat([key_cache[None], k], dim=2) # [1, kv, S+P, d]
|
| 320 |
+
v_all = torch.cat([value_cache[None], v], dim=2)
|
| 321 |
+
rep, T = h // kv, S + P
|
| 322 |
+
k_all = k_all[:, :, None].expand(1, kv, rep, T, d).reshape(1, h, T, d)
|
| 323 |
+
v_all = v_all[:, :, None].expand(1, kv, rep, T, d).reshape(1, h, T, d)
|
| 324 |
+
scores = torch.matmul(q, k_all.transpose(-1, -2)) * (d**-0.5) + bias
|
| 325 |
+
out = torch.matmul(torch.softmax(scores, dim=-1), v_all).transpose(1, 2).reshape(1, P, h * d)
|
| 326 |
+
return attn.o_proj(out * torch.sigmoid(gate))
|
| 327 |
+
|
| 328 |
+
def _delta(self, net, x, state, tail, segment, lag_keep, lag_tail):
|
| 329 |
+
cfg, P = self.cfg, self.P
|
| 330 |
+
H, Dk, Dv, kh = cfg.lin_v_heads, cfg.lin_k_dim, cfg.lin_v_dim, cfg.lin_k_heads
|
| 331 |
+
rep = H // kh
|
| 332 |
+
pre = net.in_proj_qkv(x)[0] # [P, conv]
|
| 333 |
+
weight = net.conv1d.weight[:, 0] # [conv, kernel]
|
| 334 |
+
lags = weight.shape[1] - 1
|
| 335 |
+
conv = pre * weight[:, lags]
|
| 336 |
+
for s in range(1, lags + 1):
|
| 337 |
+
shifted = F.pad(pre[: P - s], (0, 0, s, 0)) * lag_keep[s - 1][:, None]
|
| 338 |
+
shifted = shifted + torch.matmul(lag_tail[s - 1], tail.transpose(0, 1))
|
| 339 |
+
conv = conv + shifted * weight[:, lags - s]
|
| 340 |
+
qkv = F.silu(conv)
|
| 341 |
+
q = qkv[:, : net.key_dim].reshape(P, kh, Dk)
|
| 342 |
+
k = qkv[:, net.key_dim : 2 * net.key_dim].reshape(P, kh, Dk)
|
| 343 |
+
v = qkv[:, 2 * net.key_dim :].reshape(P, H, Dv).transpose(0, 1) # [H, P, Dv]
|
| 344 |
+
z = net.in_proj_z(x).reshape(1, P, H, Dv)
|
| 345 |
+
beta = torch.sigmoid(net.in_proj_b(x))[0].transpose(0, 1)[..., None] # [H, P, 1]
|
| 346 |
+
g = (-torch.exp(net.A_log) * F.softplus(net.in_proj_a(x) + net.dt_bias))[0].transpose(0, 1) # [H, P]
|
| 347 |
+
q = q * torch.rsqrt((q * q).sum(-1, keepdim=True) + 1e-6)
|
| 348 |
+
k = k * torch.rsqrt((k * k).sum(-1, keepdim=True) + 1e-6)
|
| 349 |
+
q = (q[:, :, None].expand(P, kh, rep, Dk).reshape(P, H, Dk) * (Dk**-0.5)).transpose(0, 1) # [H, P, Dk]
|
| 350 |
+
k = k[:, :, None].expand(P, kh, rep, Dk).reshape(P, H, Dk).transpose(0, 1)
|
| 351 |
+
cum = torch.matmul(g, segment.transpose(0, 1)) # [H, P]: decay since the segment start
|
| 352 |
+
pair = (cum[:, :, None] - cum[:, None, :]) * segment + (segment - 1.0) * 1e4
|
| 353 |
+
pair_decay = torch.exp(pair)
|
| 354 |
+
k_beta, v_beta = k * beta, v * beta
|
| 355 |
+
kkt = torch.matmul(k_beta, k.transpose(-1, -2)) * pair_decay
|
| 356 |
+
attn_intra = torch.matmul(q, k.transpose(-1, -2)) * pair_decay
|
| 357 |
+
inv = _unit_lower_inverse(self.eye + kkt * (segment - self.eye), P)
|
| 358 |
+
decay = torch.exp(cum)[..., None]
|
| 359 |
+
v_new = torch.matmul(inv, v_beta) - torch.matmul(torch.matmul(inv, k_beta * decay), state)
|
| 360 |
+
core = torch.matmul(q * decay, state) + torch.matmul(attn_intra, v_new) # [H, P, Dv]
|
| 361 |
+
core = net.norm(core.transpose(0, 1)[None], z)
|
| 362 |
+
return net.out_proj(core.reshape(1, P, H * Dv))
|
| 363 |
+
|
| 364 |
+
def forward(self, hidden, cos, sin, keys, values, cache_valid, states, tails, segment, lag_keep, lag_tail,
|
| 365 |
+
decide_onehot, option_onehot, option_mask):
|
| 366 |
+
"""hidden [1, P, D]; cos/sin [P, R] (each question at positions n, n+1, ...); keys/values [A, kv, S, d];
|
| 367 |
+
cache_valid [S]; states [Ld, H, Dk, Dv]; tails [Ld, conv, k-1]; segment [P, P]; lag_keep [k-1, P];
|
| 368 |
+
lag_tail [k-1, P, k-1]; decide_onehot [B, P]; option_onehot [B, K, P]; option_mask [B, K]
|
| 369 |
+
-> logits [B, K] (temperature applied), probabilities [B, K]."""
|
| 370 |
+
bias = torch.cat([((1.0 - cache_valid) * MASK_VALUE)[None, :].expand(self.P, self.S),
|
| 371 |
+
(1.0 - segment) * MASK_VALUE], dim=1)
|
| 372 |
+
x = hidden
|
| 373 |
+
a = d = 0
|
| 374 |
+
for layer in self.decoder.layers:
|
| 375 |
+
h = layer.input_layernorm(x)
|
| 376 |
+
if layer.is_linear:
|
| 377 |
+
out = self._delta(layer.linear_attn, h, states[d], tails[d], segment, lag_keep, lag_tail)
|
| 378 |
+
d += 1
|
| 379 |
+
else:
|
| 380 |
+
out = self._attention(layer.self_attn, h, cos, sin, keys[a], values[a], bias)
|
| 381 |
+
a += 1
|
| 382 |
+
x = x + out
|
| 383 |
+
x = x + layer.mlp(layer.post_attention_layernorm(x))
|
| 384 |
+
decide = self.norm(torch.matmul(decide_onehot, x[0])) # [B, D]
|
| 385 |
+
options = self.norm(torch.matmul(option_onehot, x[0])) # [B, K, D]
|
| 386 |
+
logits = (self.k(options) * self.q(decide)[:, None, :]).sum(-1) * self.pointer_scale * self.inverse_temperature
|
| 387 |
+
logits = torch.where(option_mask > 0.5, logits, torch.full_like(logits, MASK_VALUE))
|
| 388 |
+
return logits, torch.softmax(logits, dim=-1)
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
def load_packed(merged, state_len: int, packed_len: int, batch: int, max_options: int):
|
| 392 |
+
import json
|
| 393 |
+
from pathlib import Path
|
| 394 |
+
|
| 395 |
+
from safetensors.torch import load_file
|
| 396 |
+
|
| 397 |
+
merged = Path(merged)
|
| 398 |
+
meta = json.loads((merged / "meta.json").read_text())
|
| 399 |
+
cfg = TextConfig(meta["text_config"])
|
| 400 |
+
state = load_file(str(merged / "text.safetensors"))
|
| 401 |
+
head = load_file(str(merged / "head.safetensors"))
|
| 402 |
+
model = PackedQuestionPass(cfg, state_len, packed_len, batch, max_options)
|
| 403 |
+
model.decoder.load_merged(state)
|
| 404 |
+
model.norm.load_state_dict({"weight": state["norm.weight"]})
|
| 405 |
+
model.q.load_state_dict({"weight": head["q.weight"], "bias": head["q.bias"]})
|
| 406 |
+
model.k.load_state_dict({"weight": head["k.weight"], "bias": head["k.bias"]})
|
| 407 |
+
model.pointer_scale.fill_(meta["pointer_scale"])
|
| 408 |
+
model.inverse_temperature.fill_(1.0 / meta["temperature"])
|
| 409 |
+
return model.eval()
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def packed_inputs(cfg, embed, state_count, question_rows, packed_len, batch, max_options, pad_id, lags=3, lane=None):
|
| 413 |
+
"""question_rows: [(branch ids, decide offset, option offsets)] -> PackedQuestionPass inputs after the cache.
|
| 414 |
+
With `lane`, a question that would cross a lane boundary starts at the next lane."""
|
| 415 |
+
from qwen35_export import rope_cos_sin
|
| 416 |
+
|
| 417 |
+
P = packed_len
|
| 418 |
+
ids = torch.full((P,), pad_id, dtype=torch.long)
|
| 419 |
+
positions = torch.full((P,), state_count, dtype=torch.long)
|
| 420 |
+
segment = torch.eye(P)
|
| 421 |
+
lag_keep = torch.zeros(lags, P)
|
| 422 |
+
lag_tail = torch.zeros(lags, P, lags)
|
| 423 |
+
decide = torch.zeros(batch, P)
|
| 424 |
+
options = torch.zeros(batch, max_options, P)
|
| 425 |
+
mask = torch.zeros(batch, max_options)
|
| 426 |
+
start = 0
|
| 427 |
+
for b, (branch, d, opts) in enumerate(question_rows):
|
| 428 |
+
n = len(branch)
|
| 429 |
+
if lane and start // lane != (start + n - 1) // lane:
|
| 430 |
+
start = (start // lane + 1) * lane
|
| 431 |
+
if start + n > P or b >= batch or len(opts) > max_options:
|
| 432 |
+
raise ValueError("questions exceed the packed bucket")
|
| 433 |
+
ids[start : start + n] = torch.tensor(branch)
|
| 434 |
+
positions[start : start + n] = torch.arange(state_count, state_count + n)
|
| 435 |
+
segment[start : start + n, start : start + n] = torch.ones(n, n).tril()
|
| 436 |
+
for p in range(n):
|
| 437 |
+
for s in range(1, lags + 1):
|
| 438 |
+
if p >= s:
|
| 439 |
+
lag_keep[s - 1, start + p] = 1
|
| 440 |
+
else:
|
| 441 |
+
lag_tail[s - 1, start + p, lags + p - s] = 1
|
| 442 |
+
decide[b, start + d] = 1
|
| 443 |
+
for i, o in enumerate(opts):
|
| 444 |
+
options[b, i, start + o] = 1
|
| 445 |
+
mask[b, i] = 1
|
| 446 |
+
start += n
|
| 447 |
+
cos, sin = rope_cos_sin(cfg, positions.unsqueeze(0).expand(3, -1))
|
| 448 |
+
return embed[ids].unsqueeze(0), cos, sin, segment, lag_keep, lag_tail, decide, options, mask
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
def _masked_block_inverse(t, C, block=None):
|
| 452 |
+
"""Same blocking as GatedDeltaNet.unit_lower_inverse, kept as one [.., C, C] matrix: with D the inverse of t's
|
| 453 |
+
diagonal s-blocks and L = t's lower-left s-blocks inside each diagonal 2s-block, the 2s-block inverse is
|
| 454 |
+
D - D L D (L D L = 0). Four ops per level instead of block extraction, slicing and concatenation."""
|
| 455 |
+
idx = torch.arange(C)
|
| 456 |
+
inv = None
|
| 457 |
+
s = 1
|
| 458 |
+
while s < min(C, block or C):
|
| 459 |
+
same_pair = (idx[:, None] // (2 * s)) == (idx[None, :] // (2 * s))
|
| 460 |
+
lower_left = same_pair & ((idx[:, None] // s) % 2 == 1) & ((idx[None, :] // s) % 2 == 0)
|
| 461 |
+
mask = lower_left.to(t.dtype)
|
| 462 |
+
if inv is None:
|
| 463 |
+
inv = torch.eye(C, dtype=t.dtype) - t * mask
|
| 464 |
+
else:
|
| 465 |
+
inv = inv - torch.matmul(torch.matmul(inv, t * mask), inv)
|
| 466 |
+
s *= 2
|
| 467 |
+
return inv
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
def _unit_lower_inverse(t, C, block=None):
|
| 471 |
+
"""GatedDeltaNet.unit_lower_inverse for a [G, C, C] unit-lower-triangular batch of any power-of-two C. `block`:
|
| 472 |
+
t is known block-diagonal in blocks of that size (packed questions never cross a lane)."""
|
| 473 |
+
if MASKED_INVERSE:
|
| 474 |
+
return _masked_block_inverse(t, C, block)
|
| 475 |
+
b = t.shape[0]
|
| 476 |
+
inv = torch.ones(b, C, 1, 1, dtype=t.dtype)
|
| 477 |
+
s = 1
|
| 478 |
+
while s < C:
|
| 479 |
+
n2 = C // (2 * s)
|
| 480 |
+
blocks = (t.reshape(b, n2, 2 * s, n2, 2 * s) * torch.eye(n2, dtype=t.dtype)[:, None, :, None]).sum(-2)
|
| 481 |
+
x = blocks[..., s:, :s]
|
| 482 |
+
pair = inv.reshape(b, n2, 2, s, s)
|
| 483 |
+
a_inv, b_inv = pair[..., 0, :, :], pair[..., 1, :, :]
|
| 484 |
+
lower = -torch.matmul(b_inv, torch.matmul(x, a_inv))
|
| 485 |
+
inv = torch.cat([torch.cat([a_inv, torch.zeros_like(a_inv)], dim=-1), torch.cat([lower, b_inv], dim=-1)], dim=-2)
|
| 486 |
+
s *= 2
|
| 487 |
+
return inv.reshape(b, C, C)
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
class FusedPass(nn.Module):
|
| 491 |
+
"""State pass and packed question pass in one call: sequence = S state tokens (right-padded) + P packed question
|
| 492 |
+
tokens. Projections, norms, MLPs and attention run once over all S + P positions (attention under one combined
|
| 493 |
+
mask); only the delta rule splits: chunked over the state, then one segment-masked chunk for the questions on top
|
| 494 |
+
of the state's final recurrent state and conv tail."""
|
| 495 |
+
|
| 496 |
+
def __init__(self, cfg: TextConfig, state_len: int, packed_len: int, batch: int, max_options: int,
|
| 497 |
+
chunk_size: int = 64, pointer_dim: int = 256, lane: int = 128):
|
| 498 |
+
super().__init__()
|
| 499 |
+
self.cfg, self.S, self.P, self.B, self.K = cfg, state_len, packed_len, batch, max_options
|
| 500 |
+
self.lane = min(lane, packed_len)
|
| 501 |
+
self.decoder = DecoderChunk(cfg, 0, cfg.num_layers, state_len, chunk_size=min(chunk_size, state_len),
|
| 502 |
+
with_head=False)
|
| 503 |
+
self.norm = RMSNorm(cfg.hidden_size, cfg.eps)
|
| 504 |
+
self.q = nn.Linear(cfg.hidden_size, pointer_dim)
|
| 505 |
+
self.k = nn.Linear(cfg.hidden_size, pointer_dim)
|
| 506 |
+
self.register_buffer("pointer_scale", torch.tensor([pointer_dim**-0.5]))
|
| 507 |
+
self.register_buffer("inverse_temperature", torch.tensor([1.0]))
|
| 508 |
+
self.register_buffer("eye_p", torch.eye(packed_len))
|
| 509 |
+
self.register_buffer("state_causal", torch.full((state_len, state_len), MASK_VALUE).triu(1))
|
| 510 |
+
self.register_buffer("state_block", torch.full((state_len, packed_len), MASK_VALUE))
|
| 511 |
+
|
| 512 |
+
def _attention(self, attn, x, cos, sin, bias):
|
| 513 |
+
cfg, T = self.cfg, self.S + self.P
|
| 514 |
+
h, kv, d, r = cfg.num_heads, cfg.num_kv_heads, cfg.head_dim, cfg.rotary_dim
|
| 515 |
+
qg = attn.q_proj(x).view(1, T, h, 2 * d)
|
| 516 |
+
q, gate = qg[..., :d], qg[..., d:].reshape(1, T, h * d)
|
| 517 |
+
q = attn.q_norm(q).transpose(1, 2)
|
| 518 |
+
k = attn.k_norm(attn.k_proj(x).view(1, T, kv, d)).transpose(1, 2)
|
| 519 |
+
v = attn.v_proj(x).view(1, T, kv, d).transpose(1, 2)
|
| 520 |
+
c, s = cos[None, None], sin[None, None]
|
| 521 |
+
q, k = _rope(q, c, s, r), _rope(k, c, s, r)
|
| 522 |
+
rep = h // kv
|
| 523 |
+
k = k[:, :, None].expand(1, kv, rep, T, d).reshape(1, h, T, d)
|
| 524 |
+
v = v[:, :, None].expand(1, kv, rep, T, d).reshape(1, h, T, d)
|
| 525 |
+
scores = torch.matmul(q, k.transpose(-1, -2)) * (d**-0.5) + bias
|
| 526 |
+
out = torch.matmul(torch.softmax(scores, dim=-1), v).transpose(1, 2).reshape(1, T, h * d)
|
| 527 |
+
return attn.o_proj(out * torch.sigmoid(gate))
|
| 528 |
+
|
| 529 |
+
def _delta(self, net, x, valid, tail_onehot, segment, lag_keep, lag_tail):
|
| 530 |
+
cfg, S, P = self.cfg, self.S, self.P
|
| 531 |
+
T = S + P
|
| 532 |
+
H, Dk, Dv, kh = cfg.lin_v_heads, cfg.lin_k_dim, cfg.lin_v_dim, cfg.lin_k_heads
|
| 533 |
+
rep = H // kh
|
| 534 |
+
pre = net.in_proj_qkv(x)[0] # [T, conv]
|
| 535 |
+
pre_s, pre_p = pre[:S], pre[S:]
|
| 536 |
+
tail = torch.matmul(tail_onehot, pre_s) # [k-1, conv]
|
| 537 |
+
weight = net.conv1d.weight[:, 0] # [conv, kernel]
|
| 538 |
+
lags = weight.shape[1] - 1
|
| 539 |
+
conv_s, conv_p = pre_s * weight[:, lags], pre_p * weight[:, lags]
|
| 540 |
+
for s in range(1, lags + 1):
|
| 541 |
+
conv_s = conv_s + F.pad(pre_s[: S - s], (0, 0, s, 0)) * weight[:, lags - s]
|
| 542 |
+
shifted = F.pad(pre_p[: P - s], (0, 0, s, 0)) * lag_keep[s - 1][:, None] + torch.matmul(lag_tail[s - 1], tail)
|
| 543 |
+
conv_p = conv_p + shifted * weight[:, lags - s]
|
| 544 |
+
qkv = F.silu(torch.cat([conv_s, conv_p], dim=0)) # [T, conv]
|
| 545 |
+
q = qkv[:, : net.key_dim].reshape(T, kh, Dk)
|
| 546 |
+
k = qkv[:, net.key_dim : 2 * net.key_dim].reshape(T, kh, Dk)
|
| 547 |
+
v = qkv[:, 2 * net.key_dim :].reshape(T, H, Dv).transpose(0, 1) # [H, T, Dv]
|
| 548 |
+
z = net.in_proj_z(x).reshape(1, T, H, Dv)
|
| 549 |
+
beta = torch.sigmoid(net.in_proj_b(x))[0].transpose(0, 1) # [H, T]
|
| 550 |
+
g = (-torch.exp(net.A_log) * F.softplus(net.in_proj_a(x) + net.dt_bias))[0].transpose(0, 1) # [H, T]
|
| 551 |
+
q = q * torch.rsqrt((q * q).sum(-1, keepdim=True) + 1e-6)
|
| 552 |
+
k = k * torch.rsqrt((k * k).sum(-1, keepdim=True) + 1e-6)
|
| 553 |
+
q = (q[:, :, None].expand(T, kh, rep, Dk).reshape(T, H, Dk) * (Dk**-0.5)).transpose(0, 1) # [H, T, Dk]
|
| 554 |
+
k = k[:, :, None].expand(T, kh, rep, Dk).reshape(T, H, Dk).transpose(0, 1)
|
| 555 |
+
|
| 556 |
+
# state: chunked, padded positions are no-ops
|
| 557 |
+
C, N = net.chunk, net.num_chunks
|
| 558 |
+
beta_s, g_s = beta[:, :S] * valid, g[:, :S] * valid
|
| 559 |
+
new_v, k_cumdecay, q_dec, k_dec, attn_intra, chunk_decay = _delta_chunk_terms(
|
| 560 |
+
net, q[:, :S].reshape(H, N, C, Dk), k[:, :S].reshape(H, N, C, Dk), v[:, :S].reshape(H, N, C, Dv),
|
| 561 |
+
beta_s.reshape(H, N, C, 1), g_s.reshape(H, N, C))
|
| 562 |
+
state = torch.zeros(H, Dk, Dv, dtype=x.dtype)
|
| 563 |
+
outs = []
|
| 564 |
+
for i in range(N):
|
| 565 |
+
v_new = new_v[:, i] - torch.matmul(k_cumdecay[:, i], state)
|
| 566 |
+
outs.append(torch.matmul(q_dec[:, i], state) + torch.matmul(attn_intra[:, i], v_new))
|
| 567 |
+
state = state * chunk_decay[:, i] + torch.matmul(k_dec[:, i].transpose(-1, -2), v_new)
|
| 568 |
+
|
| 569 |
+
# questions: one segment-masked chunk from the final state
|
| 570 |
+
qp, kp, vp = q[:, S:], k[:, S:], v[:, S:]
|
| 571 |
+
beta_p = beta[:, S:][..., None]
|
| 572 |
+
cum = torch.matmul(g[:, S:], segment.transpose(0, 1)) # [H, P]
|
| 573 |
+
pair_decay = torch.exp((cum[:, :, None] - cum[:, None, :]) * segment + (segment - 1.0) * 1e4)
|
| 574 |
+
k_beta = kp * beta_p
|
| 575 |
+
kkt = torch.matmul(k_beta, kp.transpose(-1, -2)) * pair_decay
|
| 576 |
+
attn_p = torch.matmul(qp, kp.transpose(-1, -2)) * pair_decay
|
| 577 |
+
inv = _unit_lower_inverse(self.eye_p + kkt * (segment - self.eye_p), P, self.lane)
|
| 578 |
+
decay = torch.exp(cum)[..., None]
|
| 579 |
+
v_new = torch.matmul(inv, vp * beta_p) - torch.matmul(torch.matmul(inv, k_beta * decay), state)
|
| 580 |
+
outs.append(torch.matmul(qp * decay, state) + torch.matmul(attn_p, v_new))
|
| 581 |
+
|
| 582 |
+
core = torch.cat(outs, dim=1).transpose(0, 1)[None] # [1, T, H, Dv]
|
| 583 |
+
core = net.norm(core, z)
|
| 584 |
+
return net.out_proj(core.reshape(1, T, H * Dv))
|
| 585 |
+
|
| 586 |
+
def forward(self, hidden, cos, sin, valid, tail_onehot, segment, lag_keep, lag_tail, decide_onehot, option_onehot,
|
| 587 |
+
option_mask):
|
| 588 |
+
"""hidden [1, S+P, D]; cos/sin [S+P, R]; valid [S]; tail_onehot [k-1, S]; segment [P, P]; lag_keep [k-1, P];
|
| 589 |
+
lag_tail [k-1, P, k-1]; decide_onehot [B, P]; option_onehot [B, K, P]; option_mask [B, K]
|
| 590 |
+
-> logits [B, K] (temperature applied), probabilities [B, K]."""
|
| 591 |
+
S, P = self.S, self.P
|
| 592 |
+
bias = torch.cat([
|
| 593 |
+
torch.cat([self.state_causal, self.state_block], dim=1),
|
| 594 |
+
torch.cat([((1.0 - valid) * MASK_VALUE)[None, :].expand(P, S), (1.0 - segment) * MASK_VALUE], dim=1),
|
| 595 |
+
], dim=0)
|
| 596 |
+
x = hidden
|
| 597 |
+
for layer in self.decoder.layers:
|
| 598 |
+
h = layer.input_layernorm(x)
|
| 599 |
+
if layer.is_linear:
|
| 600 |
+
out = self._delta(layer.linear_attn, h, valid, tail_onehot, segment, lag_keep, lag_tail)
|
| 601 |
+
else:
|
| 602 |
+
out = self._attention(layer.self_attn, h, cos, sin, bias)
|
| 603 |
+
x = x + out
|
| 604 |
+
x = x + layer.mlp(layer.post_attention_layernorm(x))
|
| 605 |
+
xq = x[0, S:]
|
| 606 |
+
decide = self.norm(torch.matmul(decide_onehot, xq))
|
| 607 |
+
options = self.norm(torch.matmul(option_onehot, xq))
|
| 608 |
+
logits = (self.k(options) * self.q(decide)[:, None, :]).sum(-1) * self.pointer_scale * self.inverse_temperature
|
| 609 |
+
logits = torch.where(option_mask > 0.5, logits, torch.full_like(logits, MASK_VALUE))
|
| 610 |
+
return logits, torch.softmax(logits, dim=-1)
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
def load_fused(merged, state_len: int, packed_len: int, batch: int, max_options: int, chunk_size: int = 64,
|
| 614 |
+
lane: int = 128):
|
| 615 |
+
import json
|
| 616 |
+
from pathlib import Path
|
| 617 |
+
|
| 618 |
+
from safetensors.torch import load_file
|
| 619 |
+
|
| 620 |
+
merged = Path(merged)
|
| 621 |
+
meta = json.loads((merged / "meta.json").read_text())
|
| 622 |
+
cfg = TextConfig(meta["text_config"])
|
| 623 |
+
state = load_file(str(merged / "text.safetensors"))
|
| 624 |
+
head = load_file(str(merged / "head.safetensors"))
|
| 625 |
+
model = FusedPass(cfg, state_len, packed_len, batch, max_options, chunk_size, lane=lane)
|
| 626 |
+
model.decoder.load_merged(state)
|
| 627 |
+
model.norm.load_state_dict({"weight": state["norm.weight"]})
|
| 628 |
+
model.q.load_state_dict({"weight": head["q.weight"], "bias": head["q.bias"]})
|
| 629 |
+
model.k.load_state_dict({"weight": head["k.weight"], "bias": head["k.bias"]})
|
| 630 |
+
model.pointer_scale.fill_(meta["pointer_scale"])
|
| 631 |
+
model.inverse_temperature.fill_(1.0 / meta["temperature"])
|
| 632 |
+
return model.eval()
|
| 633 |
+
|
| 634 |
+
|
| 635 |
+
def fused_inputs(cfg, embed, state_ids, question_rows, state_len, packed_len, batch, max_options, pad_id, lane=128):
|
| 636 |
+
state_in, _ = stage_inputs(cfg, embed, state_ids, [], state_len, 32, 2, max_options, pad_id)
|
| 637 |
+
s_hidden, s_cos, s_sin, valid, tail = state_in
|
| 638 |
+
p_hidden, p_cos, p_sin, segment, keep, lag_tail, decide, options, mask = packed_inputs(
|
| 639 |
+
cfg, embed, len(state_ids), question_rows, packed_len, batch, max_options, pad_id, lane=min(lane, packed_len))
|
| 640 |
+
return (torch.cat([s_hidden, p_hidden], dim=1), torch.cat([s_cos, p_cos]), torch.cat([s_sin, p_sin]), valid, tail,
|
| 641 |
+
segment, keep, lag_tail, decide, options, mask)
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
def pack_groups(lengths, packed_len, batch, lane=None):
|
| 645 |
+
"""Greedy packing (the order is kept) of question lengths into calls of `packed_len` tokens and `batch` readouts,
|
| 646 |
+
no question crossing a lane. Returns lists of question indices."""
|
| 647 |
+
groups, current, start = [], [], 0
|
| 648 |
+
for i, n in enumerate(lengths):
|
| 649 |
+
if n > (lane or packed_len):
|
| 650 |
+
raise ValueError(f"question of {n} tokens exceeds the lane")
|
| 651 |
+
placed = start
|
| 652 |
+
if lane and placed // lane != (placed + n - 1) // lane:
|
| 653 |
+
placed = (placed // lane + 1) * lane
|
| 654 |
+
if current and (placed + n > packed_len or len(current) == batch):
|
| 655 |
+
groups.append(current)
|
| 656 |
+
current, placed = [], 0
|
| 657 |
+
current.append(i)
|
| 658 |
+
start = placed + n
|
| 659 |
+
if current:
|
| 660 |
+
groups.append(current)
|
| 661 |
+
return groups
|
conversion/pack.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Host side: System One request -> packed tree inputs, and logits -> answers (vendored code)."""
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
sys.path.insert(0, "kai")
|
| 8 |
+
from decision2._vendor.dev2model.decision_model import encode # noqa: E402
|
| 9 |
+
from decision2._vendor.dev2model.infer import product_answer, question_to_row # noqa: E402
|
| 10 |
+
from decision2._vendor.dev2model.score_bias import apply as apply_score_bias # noqa: E402
|
| 11 |
+
|
| 12 |
+
NEG = -1e4
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def rows(tokenizer, state, questions, cap=8192):
|
| 16 |
+
item = {"id": "request", "state": state}
|
| 17 |
+
out = []
|
| 18 |
+
for qid, q in questions.items():
|
| 19 |
+
row = question_to_row(item, qid, q)
|
| 20 |
+
out.append((qid, row, encode(row, tokenizer, cap)))
|
| 21 |
+
return out
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def pack(jobs, L, N, pad_id):
|
| 25 |
+
"""Shared prefix once, then each suffix; suffix tokens see prefix + own suffix (causal)."""
|
| 26 |
+
seqs = [e["ids"] for _, _, e in jobs]
|
| 27 |
+
P = min(min(e["candidate_positions"][0] for _, _, e in jobs), min(len(s) for s in seqs) - 1)
|
| 28 |
+
for i in range(P):
|
| 29 |
+
if any(s[i] != seqs[0][i] for s in seqs):
|
| 30 |
+
P = i
|
| 31 |
+
break
|
| 32 |
+
ids, pos, seg = list(seqs[0][:P]), list(range(P)), [-1] * P
|
| 33 |
+
starts = []
|
| 34 |
+
for j, s in enumerate(seqs):
|
| 35 |
+
starts.append(len(ids) - P)
|
| 36 |
+
ids += s[P:]
|
| 37 |
+
pos += range(P, len(s))
|
| 38 |
+
seg += [j] * (len(s) - P)
|
| 39 |
+
T = len(ids)
|
| 40 |
+
if T > L:
|
| 41 |
+
raise ValueError(f"packed length {T} > {L}")
|
| 42 |
+
cand, qry, owner = [], [], []
|
| 43 |
+
for j, (_, _, e) in enumerate(jobs):
|
| 44 |
+
for c in e["candidate_positions"]:
|
| 45 |
+
cand.append(c if c < P else c + starts[j])
|
| 46 |
+
qry.append(e["query_position"] + starts[j])
|
| 47 |
+
owner.append(j)
|
| 48 |
+
if len(cand) > N:
|
| 49 |
+
raise ValueError(f"{len(cand)} candidates > {N}")
|
| 50 |
+
seg = np.array(seg + [-2] * (L - T))
|
| 51 |
+
p = np.array(pos + [0] * (L - T))
|
| 52 |
+
i = np.arange(L)
|
| 53 |
+
causal = i[None, :] <= i[:, None]
|
| 54 |
+
same = (seg[None, :] == seg[:, None]) | (seg[None, :] == -1)
|
| 55 |
+
allow = causal & same & (seg[None, :] != -2)
|
| 56 |
+
allow[np.arange(L), np.arange(L)] = True # padding rows attend to themselves
|
| 57 |
+
mask = np.where(allow, 0.0, NEG).astype(np.float32)[None, None]
|
| 58 |
+
pad = N - len(cand)
|
| 59 |
+
return {
|
| 60 |
+
"input_ids": np.array([ids + [pad_id] * (L - T)], dtype=np.int32),
|
| 61 |
+
"position_ids": p[None].astype(np.int32),
|
| 62 |
+
"mask": mask,
|
| 63 |
+
"cand_idx": np.array(cand + [0] * pad, dtype=np.int32),
|
| 64 |
+
"query_idx": np.array(qry + [0] * pad, dtype=np.int32),
|
| 65 |
+
}, owner, T
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def answers(jobs, logits, owner, score_bias=None, temps=None):
|
| 69 |
+
per = [[] for _ in jobs]
|
| 70 |
+
for j, v in zip(owner, logits[: len(owner)]):
|
| 71 |
+
per[j].append(float(v))
|
| 72 |
+
out = {}
|
| 73 |
+
for (qid, row, e), values in zip(jobs, per):
|
| 74 |
+
if score_bias is not None and row["task_type"] == "score":
|
| 75 |
+
values = apply_score_bias(score_bias, values, len(row["options"]))
|
| 76 |
+
out[qid] = product_answer(
|
| 77 |
+
row["task_type"], e["keys"], values, (temps or {}).get(row["task_type"], 1.0),
|
| 78 |
+
[o["description"] for o in row["options"]],
|
| 79 |
+
)
|
| 80 |
+
return out
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def size(jobs):
|
| 84 |
+
"""(packed tokens, candidates) of one packed call, without building it."""
|
| 85 |
+
seqs = [e["ids"] for _, _, e in jobs]
|
| 86 |
+
P = min(min(e["candidate_positions"][0] for _, _, e in jobs), min(len(s) for s in seqs) - 1)
|
| 87 |
+
for i in range(P):
|
| 88 |
+
if any(s[i] != seqs[0][i] for s in seqs):
|
| 89 |
+
P = i
|
| 90 |
+
break
|
| 91 |
+
return P + sum(len(s) - P for s in seqs), sum(len(e["keys"]) for _, _, e in jobs)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def fits(jobs, L, N):
|
| 95 |
+
T, C = size(jobs)
|
| 96 |
+
return T <= L and C <= N
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def chunks(jobs, L, N):
|
| 100 |
+
"""Greedy split of a request's questions into groups that each pack into one L/N call."""
|
| 101 |
+
groups, cur = [], []
|
| 102 |
+
for j in range(len(jobs)):
|
| 103 |
+
if not cur or fits([jobs[i] for i in cur + [j]], L, N):
|
| 104 |
+
cur.append(j)
|
| 105 |
+
else:
|
| 106 |
+
groups.append(cur)
|
| 107 |
+
cur = [j]
|
| 108 |
+
return groups + [cur]
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def run(model, jobs, L, N, pad_id):
|
| 112 |
+
"""Logits per job (list of lists), over as many packed calls as needed."""
|
| 113 |
+
per = [None] * len(jobs)
|
| 114 |
+
for group in chunks(jobs, L, N):
|
| 115 |
+
sub = [jobs[i] for i in group]
|
| 116 |
+
x, owner, _ = pack(sub, L, N, pad_id)
|
| 117 |
+
x["mask"] = x["mask"].astype("float16")
|
| 118 |
+
out = model.predict(x)["logits"]
|
| 119 |
+
for k, i in enumerate(group):
|
| 120 |
+
per[i] = [float(v) for v, o in zip(out, owner) if o == k]
|
| 121 |
+
return per
|
conversion/parity_chunked_torch.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys, torch
|
| 2 |
+
from transformers import AutoModel, AutoTokenizer
|
| 3 |
+
from cases import CASES
|
| 4 |
+
import pack, q64ctx
|
| 5 |
+
from eos_graph import load_chunked, eos_inputs, with_slots, chunk_inputs
|
| 6 |
+
S, P = int(sys.argv[1]), int(sys.argv[2])
|
| 7 |
+
ref = AutoModel.from_pretrained("eos", trust_remote_code=True, device="mps")
|
| 8 |
+
tok = AutoTokenizer.from_pretrained("eos"); pad = tok.pad_token_id if tok.pad_token_id is not None else tok.eos_token_id
|
| 9 |
+
g = load_chunked("eos", S, P); worst = 0
|
| 10 |
+
cases = CASES + [(q64ctx.state, dict(list(q64ctx.Q.items())[:9]))]
|
| 11 |
+
for state, qs in cases:
|
| 12 |
+
r = ref.system_one(state=state, questions=qs)["answers"]
|
| 13 |
+
jobs = pack.rows(tok, state, qs)
|
| 14 |
+
x, cand, qry, owner, used = eos_inputs(g.cfg, [e for _, _, e in jobs], S, P, pad)
|
| 15 |
+
x = with_slots(x, cand, qry, 64)
|
| 16 |
+
x["seg_chunks"], x["cont"], x["last_seg"] = chunk_inputs(x["segment"])
|
| 17 |
+
names = ("input_ids", "cos", "sin", "valid", "tail_onehot", "segment", "lag_keep", "lag_tail", "seg_chunks", "cont", "last_seg", "cand_idx", "query_idx")
|
| 18 |
+
with torch.no_grad():
|
| 19 |
+
lg = g(*[torch.from_numpy(x[k]) for k in names])
|
| 20 |
+
a = pack.answers(jobs, lg.tolist(), owner, None)
|
| 21 |
+
d = 0
|
| 22 |
+
for q in qs:
|
| 23 |
+
pr = r[q].get("probabilities", {"true": r[q].get("noul")}); pa = a[q].get("probabilities", {"true": a[q].get("noul")})
|
| 24 |
+
d = max(d, max(abs(pr[k] - pa[k]) for k in pr))
|
| 25 |
+
worst = max(worst, d); print(f"{len(qs)} questions, packed {used}, max dp {d:.2e}")
|
| 26 |
+
print("WORST", worst)
|
conversion/q64ctx.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json, sys, time
|
| 2 |
+
import numpy as np, coremltools as ct
|
| 3 |
+
from transformers import AutoModel, AutoTokenizer
|
| 4 |
+
import pack
|
| 5 |
+
state = ("Subject: URGENT - payment failed for order #88213\n\nHi, I tried to pay for my annual Pro plan renewal "
|
| 6 |
+
"with my Visa ending 4421 but the charge was declined twice, and now my team of 12 is locked out of the dashboard. "
|
| 7 |
+
"We have a board demo tomorrow at 9am PST. I already contacted my bank and they say the card is fine. Can you "
|
| 8 |
+
"please extend access for 48 hours and send me an invoice I can pay by wire instead? Also, the export to CSV "
|
| 9 |
+
"feature has been timing out since last week. Thanks, Dana (Head of Ops, Brightline Logistics)")
|
| 10 |
+
yn = ["Is this about a failed payment?", "Is the customer locked out?", "Is there a deadline mentioned?", "Does the customer ask for an invoice?",
|
| 11 |
+
"Is a bug reported?", "Is the customer angry?", "Is this spam?", "Is this a phishing attempt?", "Is the sender an existing customer?",
|
| 12 |
+
"Does the message mention a team size?", "Does the customer request a refund?", "Does the customer want to cancel?",
|
| 13 |
+
"Is wire transfer mentioned?", "Is a credit card mentioned?", "Is personal data shared?", "Does it need a reply today?"]
|
| 14 |
+
Q = {}
|
| 15 |
+
for i in range(64):
|
| 16 |
+
k = i % 4
|
| 17 |
+
if k == 0: Q[f"q{i}"] = {"type": "noul", "instructions": yn[(i // 4) % 16] + ("" if i < 32 else " (strict)")}
|
| 18 |
+
elif k == 1: Q[f"q{i}"] = {"type": "choice", "instructions": f"Which team should own item {i}?", "criteria": {"billing": "Payments and invoices", "support": "Product bugs", "sales": "Plans and upgrades", "security": "Fraud and access"}}
|
| 19 |
+
elif k == 2: Q[f"q{i}"] = {"type": "score", "instructions": f"Urgency rating {i}?", "criteria": ["None", "Low", "Medium", "High", "Critical"]}
|
| 20 |
+
else: Q[f"q{i}"] = {"type": "choice", "instructions": f"Overall sentiment {i}?", "criteria": {"pos": "Positive", "neu": "Neutral", "neg": "Negative"}}
|
conversion/qwen35_export.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Trace-friendly, prefill-only Qwen3.5 text decoder (copied from models/computer-use/cua-s1-4b/coreml, mobius PR #104).
|
| 2 |
+
|
| 3 |
+
Cua-S1-4B reads one forward pass: the logits of the answer letters A..Z at the
|
| 4 |
+
last prompt position. Nothing is generated, so this graph has no KV cache, no
|
| 5 |
+
recurrent-state I/O and no full vocabulary head:
|
| 6 |
+
|
| 7 |
+
- inputs are `inputs_embeds` (host-side embedding gather, so a multimodal
|
| 8 |
+
caller can splice vision features in) plus host-computed M-RoPE `cos`/`sin`;
|
| 9 |
+
- prompts are right-padded to a fixed bucket; the model is causal and the gated
|
| 10 |
+
delta rule is a forward scan, so padding after the last real token cannot
|
| 11 |
+
change anything before it;
|
| 12 |
+
- the last chunk gathers the hidden state at `last_index` (one-hot matmul),
|
| 13 |
+
applies the final norm and multiplies by the 26 tied-embedding rows of the
|
| 14 |
+
letter tokens.
|
| 15 |
+
|
| 16 |
+
The gated delta rule uses the chunked (WY/UT) form. The unit-lower-triangular
|
| 17 |
+
solve is a recursive 2x2 block inverse (log2(chunk) batched matmul levels). The
|
| 18 |
+
nilpotent power series (I + N)(I + N^2)... is exact algebraically but its terms
|
| 19 |
+
grow combinatorially before cancelling and overflow on real Qwen3.5 layers.
|
| 20 |
+
Within-chunk decay differences are computed as masked sums of the per-step
|
| 21 |
+
decays instead of differences of cumulative sums, which keeps them accurate in
|
| 22 |
+
fp16 when the cumulative decay is large.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import math
|
| 28 |
+
|
| 29 |
+
import torch
|
| 30 |
+
import torch.nn.functional as F
|
| 31 |
+
from torch import nn
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class TextConfig:
|
| 35 |
+
def __init__(self, cfg: dict):
|
| 36 |
+
self.hidden_size = cfg["hidden_size"]
|
| 37 |
+
self.intermediate_size = cfg["intermediate_size"]
|
| 38 |
+
self.num_layers = cfg["num_hidden_layers"]
|
| 39 |
+
self.layer_types = cfg["layer_types"]
|
| 40 |
+
self.eps = cfg["rms_norm_eps"]
|
| 41 |
+
self.num_heads = cfg["num_attention_heads"]
|
| 42 |
+
self.num_kv_heads = cfg["num_key_value_heads"]
|
| 43 |
+
self.head_dim = cfg["head_dim"]
|
| 44 |
+
self.lin_k_heads = cfg["linear_num_key_heads"]
|
| 45 |
+
self.lin_v_heads = cfg["linear_num_value_heads"]
|
| 46 |
+
self.lin_k_dim = cfg["linear_key_head_dim"]
|
| 47 |
+
self.lin_v_dim = cfg["linear_value_head_dim"]
|
| 48 |
+
self.conv_kernel = cfg["linear_conv_kernel_dim"]
|
| 49 |
+
rope = cfg["rope_parameters"]
|
| 50 |
+
self.rope_theta = rope["rope_theta"]
|
| 51 |
+
self.rotary_dim = int(self.head_dim * rope.get("partial_rotary_factor", 1.0))
|
| 52 |
+
self.mrope_section = rope.get("mrope_section", [11, 11, 10])
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def rope_cos_sin(cfg: TextConfig, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 56 |
+
"""Interleaved M-RoPE tables. `position_ids` is [3, L] (t, h, w); returns [L, rotary_dim] fp32 each.
|
| 57 |
+
|
| 58 |
+
Mirrors `Qwen3_5TextRotaryEmbedding` (computed in float64 then cast, host side)."""
|
| 59 |
+
dim = cfg.rotary_dim
|
| 60 |
+
inv_freq = 1.0 / (cfg.rope_theta ** (torch.arange(0, dim, 2, dtype=torch.float64) / dim))
|
| 61 |
+
freqs = position_ids.to(torch.float64)[:, :, None] * inv_freq[None, None, :] # [3, L, dim/2]
|
| 62 |
+
thw = freqs[0].clone()
|
| 63 |
+
for axis, offset in ((1, 1), (2, 2)):
|
| 64 |
+
length = cfg.mrope_section[axis] * 3
|
| 65 |
+
thw[:, offset:length:3] = freqs[axis][:, offset:length:3]
|
| 66 |
+
emb = torch.cat([thw, thw], dim=-1)
|
| 67 |
+
return emb.cos().float(), emb.sin().float()
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class RMSNorm(nn.Module):
|
| 71 |
+
"""Qwen3.5 zero-centred RMSNorm: x * rsqrt(mean(x^2)+eps) * (1 + w)."""
|
| 72 |
+
|
| 73 |
+
def __init__(self, dim: int, eps: float):
|
| 74 |
+
super().__init__()
|
| 75 |
+
self.eps = eps
|
| 76 |
+
self.weight = nn.Parameter(torch.zeros(dim))
|
| 77 |
+
|
| 78 |
+
def forward(self, x):
|
| 79 |
+
x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
|
| 80 |
+
return x * (1.0 + self.weight)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class MLP(nn.Module):
|
| 84 |
+
def __init__(self, cfg: TextConfig):
|
| 85 |
+
super().__init__()
|
| 86 |
+
self.gate_proj = nn.Linear(cfg.hidden_size, cfg.intermediate_size, bias=False)
|
| 87 |
+
self.up_proj = nn.Linear(cfg.hidden_size, cfg.intermediate_size, bias=False)
|
| 88 |
+
self.down_proj = nn.Linear(cfg.intermediate_size, cfg.hidden_size, bias=False)
|
| 89 |
+
|
| 90 |
+
def forward(self, x):
|
| 91 |
+
return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def rotate_half(x):
|
| 95 |
+
half = x.shape[-1] // 2
|
| 96 |
+
return torch.cat([-x[..., half:], x[..., :half]], dim=-1)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class FullAttention(nn.Module):
|
| 100 |
+
def __init__(self, cfg: TextConfig):
|
| 101 |
+
super().__init__()
|
| 102 |
+
self.cfg = cfg
|
| 103 |
+
h, kv, d = cfg.num_heads, cfg.num_kv_heads, cfg.head_dim
|
| 104 |
+
self.q_proj = nn.Linear(cfg.hidden_size, h * d * 2, bias=False)
|
| 105 |
+
self.k_proj = nn.Linear(cfg.hidden_size, kv * d, bias=False)
|
| 106 |
+
self.v_proj = nn.Linear(cfg.hidden_size, kv * d, bias=False)
|
| 107 |
+
self.o_proj = nn.Linear(h * d, cfg.hidden_size, bias=False)
|
| 108 |
+
self.q_norm = RMSNorm(d, cfg.eps)
|
| 109 |
+
self.k_norm = RMSNorm(d, cfg.eps)
|
| 110 |
+
|
| 111 |
+
def forward(self, x, cos, sin, mask):
|
| 112 |
+
cfg = self.cfg
|
| 113 |
+
L = x.shape[1]
|
| 114 |
+
h, kv, d, r = cfg.num_heads, cfg.num_kv_heads, cfg.head_dim, cfg.rotary_dim
|
| 115 |
+
qg = self.q_proj(x).view(1, L, h, 2 * d)
|
| 116 |
+
q, gate = qg[..., :d], qg[..., d:]
|
| 117 |
+
gate = gate.reshape(1, L, h * d)
|
| 118 |
+
q = self.q_norm(q).transpose(1, 2) # [1, h, L, d]
|
| 119 |
+
k = self.k_norm(self.k_proj(x).view(1, L, kv, d)).transpose(1, 2)
|
| 120 |
+
v = self.v_proj(x).view(1, L, kv, d).transpose(1, 2)
|
| 121 |
+
|
| 122 |
+
c, s = cos[None, None], sin[None, None] # [1, 1, L, r]
|
| 123 |
+
q = torch.cat([q[..., :r] * c + rotate_half(q[..., :r]) * s, q[..., r:]], dim=-1)
|
| 124 |
+
k = torch.cat([k[..., :r] * c + rotate_half(k[..., :r]) * s, k[..., r:]], dim=-1)
|
| 125 |
+
|
| 126 |
+
rep = h // kv
|
| 127 |
+
k = k[:, :, None].expand(1, kv, rep, L, d).reshape(1, h, L, d)
|
| 128 |
+
v = v[:, :, None].expand(1, kv, rep, L, d).reshape(1, h, L, d)
|
| 129 |
+
scores = torch.matmul(q, k.transpose(-1, -2)) * (d**-0.5) + mask
|
| 130 |
+
out = torch.matmul(torch.softmax(scores, dim=-1), v) # [1, h, L, d]
|
| 131 |
+
out = out.transpose(1, 2).reshape(1, L, h * d)
|
| 132 |
+
return self.o_proj(out * torch.sigmoid(gate))
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class GatedRMSNorm(nn.Module):
|
| 136 |
+
def __init__(self, dim: int, eps: float):
|
| 137 |
+
super().__init__()
|
| 138 |
+
self.eps = eps
|
| 139 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 140 |
+
|
| 141 |
+
def forward(self, x, gate):
|
| 142 |
+
x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
|
| 143 |
+
return self.weight * x * F.silu(gate)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
class GatedDeltaNet(nn.Module):
|
| 147 |
+
def __init__(self, cfg: TextConfig, seq_len: int, chunk_size: int):
|
| 148 |
+
super().__init__()
|
| 149 |
+
self.cfg = cfg
|
| 150 |
+
self.chunk = chunk_size
|
| 151 |
+
assert seq_len % chunk_size == 0, "bucket length must be a multiple of the delta-rule chunk size"
|
| 152 |
+
self.num_chunks = seq_len // chunk_size
|
| 153 |
+
kd = cfg.lin_k_heads * cfg.lin_k_dim
|
| 154 |
+
vd = cfg.lin_v_heads * cfg.lin_v_dim
|
| 155 |
+
self.key_dim, self.value_dim = kd, vd
|
| 156 |
+
self.conv_dim = 2 * kd + vd
|
| 157 |
+
self.in_proj_qkv = nn.Linear(cfg.hidden_size, self.conv_dim, bias=False)
|
| 158 |
+
self.in_proj_z = nn.Linear(cfg.hidden_size, vd, bias=False)
|
| 159 |
+
self.in_proj_b = nn.Linear(cfg.hidden_size, cfg.lin_v_heads, bias=False)
|
| 160 |
+
self.in_proj_a = nn.Linear(cfg.hidden_size, cfg.lin_v_heads, bias=False)
|
| 161 |
+
self.conv1d = nn.Conv1d(self.conv_dim, self.conv_dim, cfg.conv_kernel, groups=self.conv_dim, bias=False)
|
| 162 |
+
self.dt_bias = nn.Parameter(torch.ones(cfg.lin_v_heads))
|
| 163 |
+
self.A_log = nn.Parameter(torch.zeros(cfg.lin_v_heads))
|
| 164 |
+
self.norm = GatedRMSNorm(cfg.lin_v_dim, cfg.eps)
|
| 165 |
+
self.out_proj = nn.Linear(vd, cfg.hidden_size, bias=False)
|
| 166 |
+
|
| 167 |
+
C = chunk_size
|
| 168 |
+
idx = torch.arange(C)
|
| 169 |
+
# incl[i, j, k] = 1 if j < k <= i : sum_k g_k = cum_i - cum_j (lower triangle incl. diagonal is 0 on i == j)
|
| 170 |
+
incl = ((idx[None, None, :] > idx[None, :, None]) & (idx[None, None, :] <= idx[:, None, None])).float()
|
| 171 |
+
self.register_buffer("pair_sum_t", incl.reshape(C * C, C), persistent=False) # [C*C, C]
|
| 172 |
+
self.register_buffer("prefix_sum_t", (idx[None, :] <= idx[:, None]).float(), persistent=False) # [C, C]
|
| 173 |
+
self.register_buffer("suffix_sum_t", (idx[None, :] > idx[:, None]).float(), persistent=False)
|
| 174 |
+
self.register_buffer("lower_incl", (idx[None, :] <= idx[:, None]).float(), persistent=False)
|
| 175 |
+
self.register_buffer("strict_lower", (idx[None, :] < idx[:, None]).float(), persistent=False)
|
| 176 |
+
self.register_buffer("eye", torch.eye(C), persistent=False)
|
| 177 |
+
assert C & (C - 1) == 0, "delta-rule chunk size must be a power of two"
|
| 178 |
+
for n in (C // (2 * s) for s in (2**i for i in range(int(math.log2(C))))):
|
| 179 |
+
self.register_buffer(f"block_eye_{n}", torch.eye(n), persistent=False)
|
| 180 |
+
|
| 181 |
+
def unit_lower_inverse(self, t: torch.Tensor) -> torch.Tensor:
|
| 182 |
+
"""Inverse of unit-lower-triangular [..., C, C] by recursive 2x2 blocking:
|
| 183 |
+
[[A, 0], [X, B]]^-1 = [[A^-1, 0], [-B^-1 X A^-1, B^-1]].
|
| 184 |
+
|
| 185 |
+
Stable like forward substitution (it only forms the true inverse's sub-blocks),
|
| 186 |
+
but log2(C) batched levels instead of C sequential row updates."""
|
| 187 |
+
C = self.chunk
|
| 188 |
+
lead = t.shape[:-2]
|
| 189 |
+
t = t.reshape(-1, C, C) # Core ML tensors are rank <= 5
|
| 190 |
+
b = t.shape[0]
|
| 191 |
+
inv = torch.ones(b, C, 1, 1, dtype=t.dtype, device=t.device) # 1x1 diagonal blocks of a unit triangle
|
| 192 |
+
s = 1
|
| 193 |
+
while s < C:
|
| 194 |
+
n2 = C // (2 * s)
|
| 195 |
+
# diagonal 2s x 2s blocks of t, then their lower-left s x s part
|
| 196 |
+
blocks = (t.reshape(b, n2, 2 * s, n2, 2 * s) * getattr(self, f"block_eye_{n2}")[:, None, :, None]).sum(-2)
|
| 197 |
+
x = blocks[..., s:, :s] # [b, n2, s, s]
|
| 198 |
+
pair = inv.reshape(b, n2, 2, s, s)
|
| 199 |
+
a_inv, b_inv = pair[..., 0, :, :], pair[..., 1, :, :]
|
| 200 |
+
lower = -torch.matmul(b_inv, torch.matmul(x, a_inv))
|
| 201 |
+
top = torch.cat([a_inv, torch.zeros_like(a_inv)], dim=-1)
|
| 202 |
+
bottom = torch.cat([lower, b_inv], dim=-1)
|
| 203 |
+
inv = torch.cat([top, bottom], dim=-2) # [b, n2, 2s, 2s]
|
| 204 |
+
s *= 2
|
| 205 |
+
return inv.reshape(*lead, C, C)
|
| 206 |
+
|
| 207 |
+
def forward(self, x):
|
| 208 |
+
cfg = self.cfg
|
| 209 |
+
L = x.shape[1]
|
| 210 |
+
C, N = self.chunk, self.num_chunks
|
| 211 |
+
H, Dk, Dv = cfg.lin_v_heads, cfg.lin_k_dim, cfg.lin_v_dim
|
| 212 |
+
rep = cfg.lin_v_heads // cfg.lin_k_heads
|
| 213 |
+
|
| 214 |
+
qkv = self.in_proj_qkv(x).transpose(1, 2) # [1, conv_dim, L]
|
| 215 |
+
qkv = F.silu(self.conv1d(F.pad(qkv, (cfg.conv_kernel - 1, 0)))).transpose(1, 2)
|
| 216 |
+
q = qkv[..., : self.key_dim].reshape(1, L, cfg.lin_k_heads, Dk)
|
| 217 |
+
k = qkv[..., self.key_dim : 2 * self.key_dim].reshape(1, L, cfg.lin_k_heads, Dk)
|
| 218 |
+
v = qkv[..., 2 * self.key_dim :].reshape(1, L, H, Dv)
|
| 219 |
+
z = self.in_proj_z(x).reshape(1, L, H, Dv)
|
| 220 |
+
beta = torch.sigmoid(self.in_proj_b(x)) # [1, L, H]
|
| 221 |
+
g = -torch.exp(self.A_log) * F.softplus(self.in_proj_a(x) + self.dt_bias) # [1, L, H], <= 0
|
| 222 |
+
|
| 223 |
+
# l2norm (FLA convention) then repeat k heads up to v heads
|
| 224 |
+
q = q * torch.rsqrt((q * q).sum(-1, keepdim=True) + 1e-6)
|
| 225 |
+
k = k * torch.rsqrt((k * k).sum(-1, keepdim=True) + 1e-6)
|
| 226 |
+
q = q[:, :, :, None].expand(1, L, cfg.lin_k_heads, rep, Dk).reshape(1, L, H, Dk)
|
| 227 |
+
k = k[:, :, :, None].expand(1, L, cfg.lin_k_heads, rep, Dk).reshape(1, L, H, Dk)
|
| 228 |
+
q = q * (Dk**-0.5)
|
| 229 |
+
|
| 230 |
+
# -> [H, N, C, D]
|
| 231 |
+
q = q[0].permute(1, 0, 2).reshape(H, N, C, Dk)
|
| 232 |
+
k = k[0].permute(1, 0, 2).reshape(H, N, C, Dk)
|
| 233 |
+
v = v[0].permute(1, 0, 2).reshape(H, N, C, Dv)
|
| 234 |
+
beta = beta[0].T.reshape(H, N, C, 1)
|
| 235 |
+
g = g[0].T.reshape(H, N, C)
|
| 236 |
+
|
| 237 |
+
# constant-first matmuls: `x @ const` would lower to Core ML `linear` and be picked up by weight compression
|
| 238 |
+
g_col = g.unsqueeze(-1) # [H, N, C, 1]
|
| 239 |
+
cum = torch.matmul(self.prefix_sum_t, g_col).squeeze(-1) # cum_i = sum_{k<=i} g_k
|
| 240 |
+
to_end = torch.matmul(self.suffix_sum_t, g_col).squeeze(-1) # sum_{k>i} g_k = cum_last - cum_i
|
| 241 |
+
pair = torch.matmul(self.pair_sum_t, g_col).reshape(H, N, C, C) # cum_i - cum_j (0 above diagonal)
|
| 242 |
+
pair_decay = torch.exp(pair) * self.lower_incl
|
| 243 |
+
|
| 244 |
+
k_beta = k * beta
|
| 245 |
+
v_beta = v * beta
|
| 246 |
+
kkt = torch.matmul(k_beta, k.transpose(-1, -2)) * pair_decay
|
| 247 |
+
attn_intra = torch.matmul(q, k.transpose(-1, -2)) * pair_decay
|
| 248 |
+
|
| 249 |
+
inv = self.unit_lower_inverse(self.eye + kkt * self.strict_lower)
|
| 250 |
+
new_v = torch.matmul(inv, v_beta) # [H, N, C, Dv]
|
| 251 |
+
k_cumdecay = torch.matmul(inv, k_beta * torch.exp(cum)[..., None]) # [H, N, C, Dk]
|
| 252 |
+
|
| 253 |
+
q_dec = q * torch.exp(cum)[..., None]
|
| 254 |
+
k_dec = k * torch.exp(to_end)[..., None]
|
| 255 |
+
chunk_decay = torch.exp(cum[..., -1:])[..., None] # [H, N, 1, 1]
|
| 256 |
+
|
| 257 |
+
state = torch.zeros(H, Dk, Dv, dtype=x.dtype, device=x.device)
|
| 258 |
+
outs = []
|
| 259 |
+
for i in range(N):
|
| 260 |
+
v_new = new_v[:, i] - torch.matmul(k_cumdecay[:, i], state)
|
| 261 |
+
outs.append(torch.matmul(q_dec[:, i], state) + torch.matmul(attn_intra[:, i], v_new))
|
| 262 |
+
if i + 1 < N:
|
| 263 |
+
state = state * chunk_decay[:, i] + torch.matmul(k_dec[:, i].transpose(-1, -2), v_new)
|
| 264 |
+
core = torch.stack(outs, dim=1).reshape(H, L, Dv).permute(1, 0, 2) # [L, H, Dv]
|
| 265 |
+
|
| 266 |
+
core = self.norm(core, z[0])
|
| 267 |
+
return self.out_proj(core.reshape(1, L, H * Dv))
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
class DecoderLayer(nn.Module):
|
| 271 |
+
def __init__(self, cfg: TextConfig, layer_idx: int, seq_len: int, chunk_size: int):
|
| 272 |
+
super().__init__()
|
| 273 |
+
self.is_linear = cfg.layer_types[layer_idx] == "linear_attention"
|
| 274 |
+
if self.is_linear:
|
| 275 |
+
self.linear_attn = GatedDeltaNet(cfg, seq_len, chunk_size)
|
| 276 |
+
else:
|
| 277 |
+
self.self_attn = FullAttention(cfg)
|
| 278 |
+
self.mlp = MLP(cfg)
|
| 279 |
+
self.input_layernorm = RMSNorm(cfg.hidden_size, cfg.eps)
|
| 280 |
+
self.post_attention_layernorm = RMSNorm(cfg.hidden_size, cfg.eps)
|
| 281 |
+
|
| 282 |
+
def forward(self, x, cos, sin, mask):
|
| 283 |
+
h = self.input_layernorm(x)
|
| 284 |
+
h = self.linear_attn(h) if self.is_linear else self.self_attn(h, cos, sin, mask)
|
| 285 |
+
x = x + h
|
| 286 |
+
return x + self.mlp(self.post_attention_layernorm(x))
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
class DecoderChunk(nn.Module):
|
| 290 |
+
"""Layers [start, end). The last chunk (with_head) returns letter logits [1, n_letters]."""
|
| 291 |
+
|
| 292 |
+
def __init__(
|
| 293 |
+
self,
|
| 294 |
+
cfg: TextConfig,
|
| 295 |
+
start: int,
|
| 296 |
+
end: int,
|
| 297 |
+
seq_len: int,
|
| 298 |
+
chunk_size: int = 64,
|
| 299 |
+
with_head: bool = False,
|
| 300 |
+
n_letters: int = 26,
|
| 301 |
+
):
|
| 302 |
+
super().__init__()
|
| 303 |
+
self.start, self.end, self.with_head = start, end, with_head
|
| 304 |
+
self.layers = nn.ModuleList([DecoderLayer(cfg, i, seq_len, chunk_size) for i in range(start, end)])
|
| 305 |
+
mask = torch.full((seq_len, seq_len), -1e4).triu(1)
|
| 306 |
+
self.register_buffer("mask", mask[None, None], persistent=False)
|
| 307 |
+
if with_head:
|
| 308 |
+
self.norm = RMSNorm(cfg.hidden_size, cfg.eps)
|
| 309 |
+
self.letter_head = nn.Linear(cfg.hidden_size, n_letters, bias=False)
|
| 310 |
+
|
| 311 |
+
def forward(self, hidden, cos, sin, last_onehot=None):
|
| 312 |
+
for layer in self.layers:
|
| 313 |
+
hidden = layer(hidden, cos, sin, self.mask)
|
| 314 |
+
if not self.with_head:
|
| 315 |
+
return hidden
|
| 316 |
+
last = torch.matmul(last_onehot, hidden[0]) # [1, hidden]
|
| 317 |
+
return self.letter_head(self.norm(last))
|
| 318 |
+
|
| 319 |
+
def load_merged(self, state: dict[str, torch.Tensor], letter_rows: torch.Tensor | None = None):
|
| 320 |
+
"""`state` uses Qwen3_5TextModel names relative to the language model (`layers.N...`, `norm.weight`)."""
|
| 321 |
+
own = {}
|
| 322 |
+
for local, global_idx in enumerate(range(self.start, self.end)):
|
| 323 |
+
prefix = f"layers.{global_idx}."
|
| 324 |
+
for key, value in state.items():
|
| 325 |
+
if key.startswith(prefix):
|
| 326 |
+
own[f"layers.{local}.{key[len(prefix):]}"] = value
|
| 327 |
+
if self.with_head:
|
| 328 |
+
own["norm.weight"] = state["norm.weight"]
|
| 329 |
+
own["letter_head.weight"] = letter_rows
|
| 330 |
+
missing, unexpected = self.load_state_dict({k: v.float() for k, v in own.items()}, strict=False)
|
| 331 |
+
missing = [m for m in missing if not m.endswith(("mask",))]
|
| 332 |
+
if missing or unexpected:
|
| 333 |
+
raise RuntimeError(f"weight mismatch: missing={missing[:8]} unexpected={unexpected[:8]}")
|
conversion/time_eos.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys, time
|
| 2 |
+
import numpy as np, coremltools as ct
|
| 3 |
+
for path in sys.argv[1:]:
|
| 4 |
+
m = ct.models.MLModel(path, compute_units=ct.ComputeUnit.CPU_AND_GPU)
|
| 5 |
+
spec = {i.name: tuple(i.type.multiArrayType.shape) for i in m.get_spec().description.input}
|
| 6 |
+
x = {}
|
| 7 |
+
for k, shp in spec.items():
|
| 8 |
+
dt = np.int32 if k in ("input_ids", "cand_idx", "query_idx") else np.float16
|
| 9 |
+
x[k] = np.zeros(shp, dtype=dt)
|
| 10 |
+
x["valid"][:] = 1; x["segment"][:] = np.eye(spec["segment"][0])
|
| 11 |
+
if "seg_chunks" in x: x["seg_chunks"][:] = np.eye(64)
|
| 12 |
+
m.predict(x); ts = []
|
| 13 |
+
for _ in range(10):
|
| 14 |
+
t = time.time(); m.predict(x); ts.append((time.time() - t) * 1000)
|
| 15 |
+
print(f"{path.split('/')[-1]:40s} p50 {np.median(ts):6.1f} ms")
|
conversion/typed_ref.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Upstream answers for typed-decisions TEST (400 requests, 2,000 decisions), cached as JSONL."""
|
| 2 |
+
import json, sys, time
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from transformers import AutoModel
|
| 5 |
+
root, out = sys.argv[1], sys.argv[2]
|
| 6 |
+
d = pd.read_parquet("data/typed/all/test-00000-of-00001.parquet")
|
| 7 |
+
m = AutoModel.from_pretrained(root, trust_remote_code=True, device="mps")
|
| 8 |
+
t = time.time()
|
| 9 |
+
with open(out, "w") as f:
|
| 10 |
+
for r in d.itertuples():
|
| 11 |
+
a = m.system_one(state=json.loads(r.state), questions=json.loads(r.questions))
|
| 12 |
+
f.write(json.dumps({"id": r.id, "answers": a["answers"], "tokens": a["usage"]["input_tokens"]}) + "\n")
|
| 13 |
+
print(f"{len(d)} requests in {time.time()-t:.0f}s")
|
coreml_config.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "Decision-2.0-Eos-0.8B",
|
| 3 |
+
"source": "vllm-sr/Decision-2.0-Eos-0.8B",
|
| 4 |
+
"source_revision": "3594047d69f476f1d01cf84c593e213fc3a4dfe0",
|
| 5 |
+
"backbone": "qwen3_5",
|
| 6 |
+
"package": "Decision2EosPacked.mlpackage",
|
| 7 |
+
"functions": {"S128_C256_N32": 34.3, "S256_C512_N64": 64.1, "S512_C768_N96": 108.6, "S512_C1024_N128": 131.0, "S1024_C1024_N128": 176.0},
|
| 8 |
+
"functions_note": "function name -> measured predict p50 ms on M5 Pro GPU, used to choose chunking. S = shared-prefix tokens, C = packed question tokens (multiple of 64), N = option slots",
|
| 9 |
+
"pad_token_id": 248044,
|
| 10 |
+
"rope": {"rotary_dim": 64, "rope_theta": 10000000},
|
| 11 |
+
"inputs": {
|
| 12 |
+
"input_ids": "[1, S+C] int32: shared prefix right-padded to S, then every question's suffix, then padding",
|
| 13 |
+
"cos, sin": "[S+C, 64] float16: RoPE tables (prefix positions 0.., each suffix continues from the prefix length)",
|
| 14 |
+
"valid": "[S] float16: 1 for real prefix tokens",
|
| 15 |
+
"tail_onehot": "[3, S] float16: selects the prefix's last 3 pre-convolution inputs (conv history)",
|
| 16 |
+
"segment": "[C, C] float16: 1 where j <= i within the same question (padding = own segment)",
|
| 17 |
+
"lag_keep, lag_tail": "[3, C], [3, C, 3] float16: a question's first 3 conv lags read the prefix tail",
|
| 18 |
+
"seg_chunks": "[C/64, 64, 64] float16: segment restricted to each 64-token chunk",
|
| 19 |
+
"cont": "[C] float16: 1 if the token's question started in an earlier chunk",
|
| 20 |
+
"last_seg": "[C/64, 64] float16: tokens in the question of each chunk's last token",
|
| 21 |
+
"cand_idx, query_idx": "[N] int32: packed-region index of each option's last token and of its question's final token"
|
| 22 |
+
},
|
| 23 |
+
"outputs": {"logits": "[N] float32: one raw logit per option (before softmax)"}
|
| 24 |
+
}
|
decision2_coreml.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Decision 2.0 System One on Core ML: every question of a request in one packed call.
|
| 2 |
+
|
| 3 |
+
Supports the Qwen3 package (Kai, `L*_N*` functions) and the Qwen3.5 hybrid package (Eos, `S*_C*_N*` functions).
|
| 4 |
+
|
| 5 |
+
Needs only numpy, tokenizers and coremltools. Prompt rendering, option endpoints, Score
|
| 6 |
+
offsets and answer normalization follow the upstream runtime (vllm-sr Decision 2.0,
|
| 7 |
+
Apache-2.0: decision2/_vendor/dev2model/{decision_model,infer,score_bias}.py).
|
| 8 |
+
|
| 9 |
+
from decision2_coreml import Decision2CoreML
|
| 10 |
+
model = Decision2CoreML(".") # this repo's folder
|
| 11 |
+
model.system_one(state="...", questions={...})
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import json
|
| 17 |
+
import math
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from typing import Any
|
| 20 |
+
|
| 21 |
+
import coremltools as ct
|
| 22 |
+
import numpy as np
|
| 23 |
+
from tokenizers import Tokenizer
|
| 24 |
+
|
| 25 |
+
MAX_OPTIONS = 255
|
| 26 |
+
NEG = -1e4
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def canonical(value: Any) -> str:
|
| 30 |
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _payload(value: Any) -> str:
|
| 34 |
+
return value if isinstance(value, str) else canonical(value)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def question_to_row(state: Any, question: dict[str, Any]) -> dict[str, Any]:
|
| 38 |
+
kind = question.get("type")
|
| 39 |
+
if kind not in ("choice", "noul", "score"):
|
| 40 |
+
raise ValueError("unsupported question type")
|
| 41 |
+
if not question.get("instructions"):
|
| 42 |
+
raise ValueError("missing question instructions")
|
| 43 |
+
criteria = question.get("criteria")
|
| 44 |
+
if kind == "score":
|
| 45 |
+
if not isinstance(criteria, list) or not 2 <= len(criteria) <= 10:
|
| 46 |
+
raise ValueError("score criteria must be an ordered list of 2..10 levels")
|
| 47 |
+
options = [{"key": str(i), "description": d} for i, d in enumerate(criteria)]
|
| 48 |
+
else:
|
| 49 |
+
if kind == "noul":
|
| 50 |
+
criteria = criteria or {}
|
| 51 |
+
if not isinstance(criteria, dict) or set(criteria) - {"false", "true"}:
|
| 52 |
+
raise ValueError("noul requires only false and true criteria")
|
| 53 |
+
if len(criteria) < 2:
|
| 54 |
+
criteria = {"false": criteria.get("false", "No"), "true": criteria.get("true", "Yes")}
|
| 55 |
+
if not isinstance(criteria, dict) or not 2 <= len(criteria) <= MAX_OPTIONS:
|
| 56 |
+
raise ValueError("choice criteria must be an object with 2..255 options")
|
| 57 |
+
options = [{"key": k, "description": d} for k, d in criteria.items()]
|
| 58 |
+
return {"state": state, "instructions": question["instructions"], "options": options, "task_type": kind}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def encode(row: dict[str, Any], tokenizer: Tokenizer) -> dict[str, Any]:
|
| 62 |
+
prefix = (
|
| 63 |
+
f"Context:\n{_payload(row['state'])}\n\n"
|
| 64 |
+
f"Task type: {row['task_type']}\nQuestion:\n{_payload(row['instructions'])}\nOptions:"
|
| 65 |
+
)
|
| 66 |
+
ids = tokenizer.encode(prefix, add_special_tokens=False).ids
|
| 67 |
+
endpoints = []
|
| 68 |
+
for option in row["options"]:
|
| 69 |
+
text = "\n<option>\n" + canonical({"key": option["key"], "description": option["description"]}) + "\n</option>"
|
| 70 |
+
ids.extend(tokenizer.encode(text, add_special_tokens=False).ids)
|
| 71 |
+
endpoints.append(len(ids) - 1)
|
| 72 |
+
suffix = "\n\nSelect the single option best supported by the context and instructions.\nDecision:"
|
| 73 |
+
ids.extend(tokenizer.encode(suffix, add_special_tokens=False).ids)
|
| 74 |
+
return {"ids": ids, "candidate_positions": endpoints, "query_position": len(ids) - 1,
|
| 75 |
+
"keys": [o["key"] for o in row["options"]]}
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _prefix(encoded: list[dict[str, Any]]) -> int:
|
| 79 |
+
seqs = [e["ids"] for e in encoded]
|
| 80 |
+
limit = min(min(e["candidate_positions"][0] for e in encoded), min(len(s) for s in seqs) - 1)
|
| 81 |
+
for i in range(limit):
|
| 82 |
+
if any(s[i] != seqs[0][i] for s in seqs):
|
| 83 |
+
return i
|
| 84 |
+
return limit
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def packed_size(encoded: list[dict[str, Any]]) -> tuple[int, int]:
|
| 88 |
+
P = _prefix(encoded)
|
| 89 |
+
return P + sum(len(e["ids"]) - P for e in encoded), sum(len(e["keys"]) for e in encoded)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def pack(encoded: list[dict[str, Any]], L: int, N: int, pad_id: int) -> tuple[dict[str, np.ndarray], list[int]]:
|
| 93 |
+
"""Shared prefix once, then each question's suffix; a suffix sees the prefix and itself (causal)."""
|
| 94 |
+
P = _prefix(encoded)
|
| 95 |
+
ids, pos, seg = list(encoded[0]["ids"][:P]), list(range(P)), [-1] * P
|
| 96 |
+
cand, qry, owner = [], [], []
|
| 97 |
+
for j, e in enumerate(encoded):
|
| 98 |
+
shift = len(ids) - P
|
| 99 |
+
ids += e["ids"][P:]
|
| 100 |
+
pos += range(P, len(e["ids"]))
|
| 101 |
+
seg += [j] * (len(e["ids"]) - P)
|
| 102 |
+
for c in e["candidate_positions"]:
|
| 103 |
+
cand.append(c if c < P else c + shift)
|
| 104 |
+
qry.append(e["query_position"] + shift)
|
| 105 |
+
owner.append(j)
|
| 106 |
+
T = len(ids)
|
| 107 |
+
if T > L or len(cand) > N:
|
| 108 |
+
raise ValueError(f"{T} tokens / {len(cand)} options exceed L{L}_N{N}")
|
| 109 |
+
s = np.array(seg + [-2] * (L - T))
|
| 110 |
+
i = np.arange(L)
|
| 111 |
+
allow = (i[None, :] <= i[:, None]) & ((s[None, :] == s[:, None]) | (s[None, :] == -1)) & (s[None, :] != -2)
|
| 112 |
+
allow[i, i] = True
|
| 113 |
+
return {
|
| 114 |
+
"input_ids": np.array([ids + [pad_id] * (L - T)], dtype=np.int32),
|
| 115 |
+
"position_ids": np.array([pos + [0] * (L - T)], dtype=np.int32),
|
| 116 |
+
"mask": np.where(allow, 0.0, NEG).astype(np.float16)[None, None],
|
| 117 |
+
"cand_idx": np.array(cand + [0] * (N - len(cand)), dtype=np.int32),
|
| 118 |
+
"query_idx": np.array(qry + [0] * (N - len(cand)), dtype=np.int32),
|
| 119 |
+
}, owner
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def pack_hybrid(encoded: list[dict[str, Any]], S: int, P: int, N: int, pad_id: int, rope: dict[str, Any],
|
| 123 |
+
lags: int = 3, chunk: int = 64) -> tuple[dict[str, np.ndarray], list[int]]:
|
| 124 |
+
"""Qwen3.5 (Gated DeltaNet hybrid) graph inputs: the shared prefix right-padded to S, the questions' suffixes
|
| 125 |
+
packed in P, plus the masks that make every suffix restart from the prefix's state (see README)."""
|
| 126 |
+
pre = _prefix(encoded)
|
| 127 |
+
total = sum(len(e["ids"]) - pre for e in encoded)
|
| 128 |
+
options = sum(len(e["keys"]) for e in encoded)
|
| 129 |
+
if not 1 <= pre <= S or total > P or options > N:
|
| 130 |
+
raise ValueError(f"prefix {pre} / {total} question tokens / {options} options exceed S{S}_C{P}_N{N}")
|
| 131 |
+
ids = np.full(S + P, pad_id, dtype=np.int32)
|
| 132 |
+
pos = np.zeros(S + P, dtype=np.float64)
|
| 133 |
+
ids[:pre] = encoded[0]["ids"][:pre]
|
| 134 |
+
pos[:pre] = np.arange(pre)
|
| 135 |
+
pos[pre:] = pre
|
| 136 |
+
valid = np.zeros(S, dtype=np.float16)
|
| 137 |
+
valid[:pre] = 1
|
| 138 |
+
tail = np.zeros((lags, S), dtype=np.float16)
|
| 139 |
+
for i in range(lags):
|
| 140 |
+
if pre - lags + i >= 0:
|
| 141 |
+
tail[i, pre - lags + i] = 1
|
| 142 |
+
segment = np.eye(P, dtype=np.float16)
|
| 143 |
+
keep = np.zeros((lags, P), dtype=np.float16)
|
| 144 |
+
lag_tail = np.zeros((lags, P, lags), dtype=np.float16)
|
| 145 |
+
cand, qry, owner = [], [], []
|
| 146 |
+
start = 0
|
| 147 |
+
for j, e in enumerate(encoded):
|
| 148 |
+
n = len(e["ids"]) - pre
|
| 149 |
+
ids[S + start : S + start + n] = e["ids"][pre:]
|
| 150 |
+
pos[S + start : S + start + n] = np.arange(pre, pre + n)
|
| 151 |
+
segment[start : start + n, start : start + n] = np.tril(np.ones((n, n)))
|
| 152 |
+
for p in range(n):
|
| 153 |
+
for s in range(1, lags + 1):
|
| 154 |
+
if p >= s:
|
| 155 |
+
keep[s - 1, start + p] = 1
|
| 156 |
+
else:
|
| 157 |
+
lag_tail[s - 1, start + p, lags + p - s] = 1
|
| 158 |
+
for c in e["candidate_positions"]:
|
| 159 |
+
cand.append(start + c - pre)
|
| 160 |
+
qry.append(start + e["query_position"] - pre)
|
| 161 |
+
owner.append(j)
|
| 162 |
+
start += n
|
| 163 |
+
dim = rope["rotary_dim"]
|
| 164 |
+
inv_freq = 1.0 / (rope["rope_theta"] ** (np.arange(0, dim, 2, dtype=np.float64) / dim))
|
| 165 |
+
freqs = pos[:, None] * inv_freq[None, :] # text-only M-RoPE: all three axes share the position
|
| 166 |
+
emb = np.concatenate([freqs, freqs], axis=-1)
|
| 167 |
+
M = P // chunk
|
| 168 |
+
starts = np.argmax(segment > 0, axis=1)
|
| 169 |
+
pad = N - len(cand)
|
| 170 |
+
return {
|
| 171 |
+
"input_ids": ids[None], "cos": np.cos(emb).astype(np.float16), "sin": np.sin(emb).astype(np.float16),
|
| 172 |
+
"valid": valid, "tail_onehot": tail, "segment": segment, "lag_keep": keep, "lag_tail": lag_tail,
|
| 173 |
+
"seg_chunks": np.stack([segment[i * chunk : (i + 1) * chunk, i * chunk : (i + 1) * chunk] for i in range(M)]),
|
| 174 |
+
"cont": (starts < (np.arange(P) // chunk) * chunk).astype(np.float16),
|
| 175 |
+
"last_seg": np.stack([segment[(i + 1) * chunk - 1, i * chunk : (i + 1) * chunk] for i in range(M)]),
|
| 176 |
+
"cand_idx": np.array(cand + [0] * pad, dtype=np.int32),
|
| 177 |
+
"query_idx": np.array(qry + [0] * pad, dtype=np.int32),
|
| 178 |
+
}, owner
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def answer(kind: str, keys: list[str], logits: list[float], descriptions: list[Any]) -> dict[str, Any]:
|
| 182 |
+
top = max(logits)
|
| 183 |
+
exps = [math.exp(v - top) for v in logits]
|
| 184 |
+
total = sum(exps)
|
| 185 |
+
probs = [v / total for v in exps]
|
| 186 |
+
pmap = dict(zip(keys, probs))
|
| 187 |
+
if kind == "noul":
|
| 188 |
+
return {"type": "noul", "noul": pmap["true"]}
|
| 189 |
+
entropy = -sum(p * math.log(p) for p in probs if p > 0)
|
| 190 |
+
confidence = max(0.0, min(1.0, 1.0 - entropy / math.log(len(keys))))
|
| 191 |
+
if kind == "score":
|
| 192 |
+
return {"type": "score", "score": sum(int(k) * pmap[k] for k in keys), "probabilities": pmap,
|
| 193 |
+
"confidence": confidence,
|
| 194 |
+
"legend": {k: d if isinstance(d, str) else canonical(d) for k, d in zip(keys, descriptions)}}
|
| 195 |
+
return {"type": "choice", "choice": keys[probs.index(max(probs))], "probabilities": pmap, "confidence": confidence}
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def _dims(name: str) -> dict[str, int]:
|
| 199 |
+
return {part[0]: int(part[1:]) for part in name.split("_")}
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
class Decision2CoreML:
|
| 203 |
+
def __init__(self, root: str | Path = ".", package: str | None = None,
|
| 204 |
+
compute_units: ct.ComputeUnit = ct.ComputeUnit.CPU_AND_GPU):
|
| 205 |
+
root = Path(root)
|
| 206 |
+
self.config = json.loads((root / "coreml_config.json").read_text())
|
| 207 |
+
self.tokenizer = Tokenizer.from_file(str(root / "tokenizer.json"))
|
| 208 |
+
self.pad_id = self.config["pad_token_id"]
|
| 209 |
+
self.hybrid = self.config.get("backbone", "qwen3") == "qwen3_5"
|
| 210 |
+
bias = root / "score_bias.json"
|
| 211 |
+
offsets = json.loads(bias.read_text())["offsets"] if bias.exists() else {}
|
| 212 |
+
self.score_bias = {int(k): v for k, v in offsets.items()}
|
| 213 |
+
path = str(root / (package or self.config["package"]))
|
| 214 |
+
self.cost_ms = self.config["functions"]
|
| 215 |
+
self.buckets = [(name, _dims(name), ct.models.MLModel(path, function_name=name, compute_units=compute_units))
|
| 216 |
+
for name in self.config["functions"]] # smallest first
|
| 217 |
+
|
| 218 |
+
def _fits(self, d: dict[str, int], encoded: list[dict[str, Any]]) -> bool:
|
| 219 |
+
options = sum(len(e["keys"]) for e in encoded)
|
| 220 |
+
if self.hybrid:
|
| 221 |
+
pre = _prefix(encoded)
|
| 222 |
+
return pre <= d["S"] and sum(len(e["ids"]) - pre for e in encoded) <= d["C"] and options <= d["N"]
|
| 223 |
+
return packed_size(encoded)[0] <= d["L"] and options <= d["N"]
|
| 224 |
+
|
| 225 |
+
def _inputs(self, d: dict[str, int], encoded: list[dict[str, Any]]):
|
| 226 |
+
if self.hybrid:
|
| 227 |
+
return pack_hybrid(encoded, d["S"], d["C"], d["N"], self.pad_id, self.config["rope"])
|
| 228 |
+
return pack(encoded, d["L"], d["N"], self.pad_id)
|
| 229 |
+
|
| 230 |
+
def _calls(self, encoded: list[dict[str, Any]]) -> list[tuple[Any, dict[str, int], list[int]]]:
|
| 231 |
+
"""Fewest estimated milliseconds: one call in the smallest bucket that fits, or greedy chunks."""
|
| 232 |
+
best = None
|
| 233 |
+
for name, d, m in self.buckets:
|
| 234 |
+
groups, group = [], []
|
| 235 |
+
for j in range(len(encoded)):
|
| 236 |
+
if not self._fits(d, [encoded[i] for i in group + [j]]):
|
| 237 |
+
if not group or not self._fits(d, [encoded[j]]):
|
| 238 |
+
break # this question alone does not fit this bucket
|
| 239 |
+
groups.append(group)
|
| 240 |
+
group = []
|
| 241 |
+
group.append(j)
|
| 242 |
+
else:
|
| 243 |
+
groups.append(group)
|
| 244 |
+
cost = len(groups) * self.cost_ms[name]
|
| 245 |
+
if best is None or cost < best[0]:
|
| 246 |
+
best = (cost, [(m, d, g) for g in groups])
|
| 247 |
+
if best is None:
|
| 248 |
+
raise ValueError(f"a question exceeds the largest function {self.buckets[-1][0]}")
|
| 249 |
+
return best[1]
|
| 250 |
+
|
| 251 |
+
def logits(self, encoded: list[dict[str, Any]]) -> list[list[float]]:
|
| 252 |
+
per: list[list[float]] = [[] for _ in encoded]
|
| 253 |
+
for m, d, group in self._calls(encoded):
|
| 254 |
+
x, owner = self._inputs(d, [encoded[i] for i in group])
|
| 255 |
+
out = m.predict(x)["logits"]
|
| 256 |
+
for k, o in enumerate(owner):
|
| 257 |
+
per[group[o]].append(float(out[k]))
|
| 258 |
+
return per
|
| 259 |
+
def system_one(self, state: Any, questions: dict[str, dict[str, Any]]) -> dict[str, Any]:
|
| 260 |
+
rows, encoded, answers = {}, {}, {}
|
| 261 |
+
for qid, q in questions.items():
|
| 262 |
+
try:
|
| 263 |
+
rows[qid] = question_to_row(state, q)
|
| 264 |
+
encoded[qid] = encode(rows[qid], self.tokenizer)
|
| 265 |
+
except ValueError:
|
| 266 |
+
answers[qid] = {"type": q.get("type") if isinstance(q, dict) else None, "error": "invalid_question"}
|
| 267 |
+
qids = list(encoded)
|
| 268 |
+
if qids:
|
| 269 |
+
for qid, values in zip(qids, self.logits([encoded[q] for q in qids])):
|
| 270 |
+
row = rows[qid]
|
| 271 |
+
offsets = self.score_bias.get(len(values)) if row["task_type"] == "score" else None
|
| 272 |
+
if offsets:
|
| 273 |
+
values = [v + b for v, b in zip(values, offsets)]
|
| 274 |
+
answers[qid] = answer(row["task_type"], encoded[qid]["keys"], values,
|
| 275 |
+
[o["description"] for o in row["options"]])
|
| 276 |
+
return {"model": self.config["model_name"], "answers": {q: answers[q] for q in questions},
|
| 277 |
+
"usage": {"input_tokens": sum(len(e["ids"]) for e in encoded.values()), "output_tokens": 0}}
|
decision_config.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architecture": "qwen3.5-text-endpoints-global-query-shared-bilinear-mlp",
|
| 3 |
+
"prompt_version": "decision2-segmented-options-global-query-v1",
|
| 4 |
+
"base_revision": "2fc06364715b967f1860aea9cf38778875588b17",
|
| 5 |
+
"source_prompt_version": "structured-segmented-candidate-endpoints-global-query-v2",
|
| 6 |
+
"head_dim": 256,
|
| 7 |
+
"max_options": 255,
|
| 8 |
+
"text_parameter_count": 752393024,
|
| 9 |
+
"parameter_dtype": "float32",
|
| 10 |
+
"autocast_dtype": "bfloat16",
|
| 11 |
+
"head_compute_dtype": "float32",
|
| 12 |
+
"attention": "sdpa",
|
| 13 |
+
"checkpoint_format": "full",
|
| 14 |
+
"full_training_source": {
|
| 15 |
+
"kind": "decision1",
|
| 16 |
+
"revision": null,
|
| 17 |
+
"source_fingerprint": {
|
| 18 |
+
"source_name": "363c4a5e56afc115b1c78c837633956d0bbb63ab",
|
| 19 |
+
"files_sha256": {
|
| 20 |
+
"backbone/config.json": "5cece452e606f8ffca1790b79018fed5b0fde7d1a0791d375ca09492e0d1809b",
|
| 21 |
+
"backbone/model.safetensors": "613f491da2794c5d22f68e81bd2d41000c5675bc4538180148e6e453e8198abd",
|
| 22 |
+
"config.json": "37c16ffad2439cc06c7a40de037e59341cb57ae4e948cbaf9b1950c5d38d0d37",
|
| 23 |
+
"decision_config.json": "71fde3c2ea6f4bf4e781028d7f8f39da73599f9d868979efbfbd8b7f43dd68c3",
|
| 24 |
+
"decision_head.safetensors": "75f4ee8b512aea8f74fdcb42849b4cd37cfb37ff17c5d0b5fda1bb7b4f6d72f6",
|
| 25 |
+
"tokenizer.json": "06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523",
|
| 26 |
+
"tokenizer_config.json": "bee8eba30f0eb4af73c0fe2cd06d0f89b657d7819941c438157ec42f7c80ea87"
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
"training_mode": "full",
|
| 31 |
+
"loss_version": "decision2-valid-k-ce-plus-optional-brier-and-replay-kl-v1",
|
| 32 |
+
"initialization": "linear-interpolation-of-full-checkpoints",
|
| 33 |
+
"interpolation": {
|
| 34 |
+
"alpha": 0.75,
|
| 35 |
+
"formula": "W = (1 - alpha) * release + alpha * arm, per tensor in FP32 (backbone and decision head)",
|
| 36 |
+
"release": {
|
| 37 |
+
"path": "/runs/m14/inputs/refs/DEV2.0-0.8B/bede7938a8c209c09f27400b79eed57948d6b75e",
|
| 38 |
+
"model_sha256": "3f02f0e5fc68a1fd985ce48bf3a3bb2d4ed16d1a1705c2258317b78a4d6514f4"
|
| 39 |
+
},
|
| 40 |
+
"arm": {
|
| 41 |
+
"path": "/runs/m16/inputs/arms/08b-RA",
|
| 42 |
+
"model_sha256": "f25beedd6b9b80fd8ce92857bfd6a14d77d73624e4fd410cc535357c0a1b41fb",
|
| 43 |
+
"soup": {
|
| 44 |
+
"method": "uniform weight average (FP32)",
|
| 45 |
+
"members": [
|
| 46 |
+
{
|
| 47 |
+
"path": "/runs/m12/arms/full/m12-08b-RA-s1/checkpoint-0004115",
|
| 48 |
+
"model_sha256": "576022450d7e284c33f46f680d1ddaeb419b093353b21f0753efe6f73b120f47"
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"path": "/runs/m12/arms/full/m12-08b-RA-s2/checkpoint-0004109",
|
| 52 |
+
"model_sha256": "6f254ab5458ef0038ee0528f84d7aaea57335004912d9d23f8a432673311898f"
|
| 53 |
+
}
|
| 54 |
+
]
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
}
|
example.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from decision2_coreml import Decision2CoreML
|
| 3 |
+
|
| 4 |
+
model = Decision2CoreML(".")
|
| 5 |
+
result = model.system_one(
|
| 6 |
+
state="The order arrived damaged yesterday. The customer has a receipt and asks for a replacement today.",
|
| 7 |
+
questions={
|
| 8 |
+
"route": {"type": "choice", "instructions": "Which team should handle this request?",
|
| 9 |
+
"criteria": {"returns": "Refunds, replacements and damaged deliveries",
|
| 10 |
+
"billing": "Payments, invoices and charges",
|
| 11 |
+
"technical": "Product setup and faults"}},
|
| 12 |
+
"receipt": {"type": "noul", "instructions": "Does the customer have a receipt?"},
|
| 13 |
+
"urgency": {"type": "score", "instructions": "How urgent is this request?",
|
| 14 |
+
"criteria": ["Routine", "Soon", "Today"]},
|
| 15 |
+
},
|
| 16 |
+
)
|
| 17 |
+
print(json.dumps(result["answers"], indent=2))
|
reports/typed-decisions-test.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flip margins (upstream top-2) [0.0001, 0.0005, 0.0007, 0.0026]
|
| 2 |
+
choice n=600 acc upstream 0.430 coreml 0.433
|
| 3 |
+
noul n=600 acc upstream 0.598 coreml 0.598
|
| 4 |
+
score n=800 acc upstream 0.379 coreml 0.376
|
| 5 |
+
typed: token mismatches 0, 2000 decisions, flips 4, max|dp| 0.0077, request p50 110.1 ms
|
| 6 |
+
64 questions: 13 calls, p50 874 ms end to end
|
tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523
|
| 3 |
+
size 19989325
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": false,
|
| 3 |
+
"audio_bos_token": "<|audio_start|>",
|
| 4 |
+
"audio_eos_token": "<|audio_end|>",
|
| 5 |
+
"audio_token": "<|audio_pad|>",
|
| 6 |
+
"backend": "tokenizers",
|
| 7 |
+
"bos_token": null,
|
| 8 |
+
"clean_up_tokenization_spaces": false,
|
| 9 |
+
"eos_token": "<|im_end|>",
|
| 10 |
+
"errors": "replace",
|
| 11 |
+
"image_token": "<|image_pad|>",
|
| 12 |
+
"is_local": true,
|
| 13 |
+
"local_files_only": true,
|
| 14 |
+
"model_max_length": 262144,
|
| 15 |
+
"model_specific_special_tokens": {
|
| 16 |
+
"audio_bos_token": "<|audio_start|>",
|
| 17 |
+
"audio_eos_token": "<|audio_end|>",
|
| 18 |
+
"audio_token": "<|audio_pad|>",
|
| 19 |
+
"image_token": "<|image_pad|>",
|
| 20 |
+
"video_token": "<|video_pad|>",
|
| 21 |
+
"vision_bos_token": "<|vision_start|>",
|
| 22 |
+
"vision_eos_token": "<|vision_end|>"
|
| 23 |
+
},
|
| 24 |
+
"pad_token": "<|endoftext|>",
|
| 25 |
+
"pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
|
| 26 |
+
"split_special_tokens": false,
|
| 27 |
+
"tokenizer_class": "Qwen2Tokenizer",
|
| 28 |
+
"unk_token": null,
|
| 29 |
+
"video_token": "<|video_pad|>",
|
| 30 |
+
"vision_bos_token": "<|vision_start|>",
|
| 31 |
+
"vision_eos_token": "<|vision_end|>"
|
| 32 |
+
}
|