Publish validated AMOS Core AI conversion with source provenance and runtime adapter
Browse files- .gitattributes +4 -0
- LICENSE +202 -0
- README.md +45 -0
- SHA256SUMS +44 -0
- SOURCE.json +179 -0
- bf16-attempt.json +298 -0
- bf16-comparison.json +282 -0
- calibration.json +7 -0
- config.json +1 -0
- convert_decoder.py +46 -0
- coreai_decoder.py +85 -0
- heads.aimodel/main.hash +2 -0
- heads.aimodel/main.mlirb +3 -0
- heads.aimodel/metadata.json +5 -0
- manifest.json +23 -0
- model/chat_template.jinja +154 -0
- model/config.json +110 -0
- model/generation_config.json +6 -0
- model/preprocessor_config.json +26 -0
- model/tokenizer.json +3 -0
- model/tokenizer_config.json +35 -0
- model/video_preprocessor_config.json +21 -0
- qwen_export_helpers.py +175 -0
- runtime-smoke.json +12 -0
- serve/README.md +46 -0
- serve/amos/__init__.py +1 -0
- serve/amos/calibrate.py +49 -0
- serve/amos/decoder.py +727 -0
- serve/amos/encoder.py +161 -0
- serve/amos/facts.py +32 -0
- serve/amos/model.py +321 -0
- serve/amos/packing.py +224 -0
- serve/amos/py.typed +0 -0
- serve/amos/serve.py +728 -0
- serve/amos/vision.py +133 -0
- serve/pyproject.toml +25 -0
- serve/sources.json +59 -0
- smoke_decoder.py +13 -0
- text.aimodel/main.hash +1 -0
- text.aimodel/main.mlirb +3 -0
- text.aimodel/metadata.json +5 -0
- validate_decoder.py +57 -0
- validation.json +333 -0
- vision.aimodel/main.hash +1 -0
- vision.aimodel/main.mlirb +3 -0
- vision.aimodel/metadata.json +5 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,7 @@ 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 |
+
heads.aimodel/main.mlirb filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
model/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
text.aimodel/main.mlirb filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
vision.aimodel/main.mlirb filter=lfs diff=lfs merge=lfs -text
|
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,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model: mukaddamzaid/amos-1-small
|
| 4 |
+
tags: [coreai, core-ai, apple, aimodel, decisions, vision]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# amos-1-small — Apple Core AI
|
| 8 |
+
|
| 9 |
+
Independent AMOS-1 conversion with **bidirectional full attention** preserved. Text backbone, 255-code readout, vision tower, span head, region head and trained box head execute through Core AI. Original prompt formatting, temperatures and quote decoding are retained. This is a decision model, not a text generator.
|
| 10 |
+
|
| 11 |
+
## Use
|
| 12 |
+
|
| 13 |
+
Requires macOS 27 and the Core AI Python runtime. Tested with `coreai-torch==0.4.3`, PyTorch 2.14.0, Transformers 5.17 development, NumPy and Pillow. The converter warns that this PyTorch version exceeds its validated range. Host preprocessing/decoding uses PyTorch tensors; it loads no PyTorch neural weights.
|
| 14 |
+
|
| 15 |
+
```python
|
| 16 |
+
import asyncio
|
| 17 |
+
from coreai_decoder import AmosDecoder
|
| 18 |
+
|
| 19 |
+
async def main():
|
| 20 |
+
m = await AmosDecoder.load(".")
|
| 21 |
+
print(await m.request({
|
| 22 |
+
"state": "Please refund the duplicate charge.",
|
| 23 |
+
"questions": {"refund": {
|
| 24 |
+
"type": "noul", "instructions": "Is a refund requested?"
|
| 25 |
+
}}
|
| 26 |
+
}))
|
| 27 |
+
|
| 28 |
+
asyncio.run(main())
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
`predict(state, questions, images=())` takes upstream internal questions (`text`, `options`) and PIL images, including image options. `request()` takes base64 images in the original AMOS format.
|
| 32 |
+
|
| 33 |
+
## Verified scope and limits
|
| 34 |
+
|
| 35 |
+
Batch one, **512 combined tokens**, 224×224 images (49 merged tokens), float32 backbone/vision weights with FP32 readout and heads. Requests over capacity raise an error. Original longer contexts, variable image resolution, wide-choice two-pass routing, rotations, thinking, hosted server and explanation endpoints are not implemented. Up to 255 option codes are available subject to token capacity. No iPhone, Neural Engine placement, throughput or upstream benchmark accuracy claim has been verified. Core AI uses default specialization.
|
| 36 |
+
|
| 37 |
+
Nine smoke/parity cases cover all six typed question kinds, quote extraction, Unicode, 24 options, synthetic state images and image options. Tested discrete decisions matched upstream. Maximum measured probability/box-coordinate error: **0.00001019**. Full results are in `validation.json`; these smoke cases do not establish benchmark accuracy. `runtime-smoke.json` verifies inference with PyTorch neural execution disabled.
|
| 38 |
+
|
| 39 |
+
The primary parity reference executes the pinned checkpoint in FP32. Compared with the original BF16 serving path in 8 additional cases, maximum probability/box error was 0.034142. Region confidence can change discontinuously when a box crosses a grid boundary. See `bf16-comparison.json`; this is not bit-identical BF16 serving.
|
| 40 |
+
|
| 41 |
+
## Attribution and reproducibility
|
| 42 |
+
|
| 43 |
+
Original model and serving code: Zaid Mukaddam, [mukaddamzaid/amos-1-small](https://huggingface.co/mukaddamzaid/amos-1-small). Weights derive from Qwen3.5. Apache-2.0 applies; see `LICENSE` and `SOURCE.json` for the pinned AMOS revision and source hashes. Conversion adds no restrictions. Upstream serving sources are included under `serve/`.
|
| 44 |
+
|
| 45 |
+
From the original workspace layout, run `convert_decoder.py amos-1-small`, `validate_decoder.py amos-1-small`, then `smoke_decoder.py amos-1-small`. Release integrity is recorded in `SHA256SUMS`.
|
SHA256SUMS
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 LICENSE
|
| 2 |
+
917c1c22fba4cc984a5e95b01cc5e1a386e013fb599782b055b909c91647c06d README.md
|
| 3 |
+
9f25e439543c4493b66f609c2a4c57a3d97c11194ac247c3c043f4657725911e SOURCE.json
|
| 4 |
+
19dd2e174b2f7468407243220912947feb04a9260f347223021f880b532439a1 bf16-attempt.json
|
| 5 |
+
5865684cabaa8e1a23243db7d9d32569a57c7b5011bf47ed95814b9813af57dd bf16-comparison.json
|
| 6 |
+
f9ca221eceeea6dc52774885a8c50ad727bb8c269a7ff4fbb439131d2f5be0f2 calibration.json
|
| 7 |
+
2636cc2d838bc7395655ebb3d2ba933b186a43b13ce134c9a441ee8ba45a9d0d config.json
|
| 8 |
+
650d3c6c51a4fead342fba413ed8339f6e04d1f48569225e2215e14a837d62c0 convert_decoder.py
|
| 9 |
+
ef80ea9244fb5d713b91e077dde578ebd1e1455ea0788ecec4b3691de1084e41 coreai_decoder.py
|
| 10 |
+
3708c801947fd75c057ff534edc404aacb4cc0acc9e653c12f09207e0d848d37 heads.aimodel/main.hash
|
| 11 |
+
132384c42c3a5c6269a79862d6540a5a6834b532674ba847d83a5b181f85a860 heads.aimodel/main.mlirb
|
| 12 |
+
3284600a3ad7c8e378bbe228ce3eb967d5da56bb40959aba7e56b177a3fc928a heads.aimodel/metadata.json
|
| 13 |
+
cc3b3cabf8162d51af8764eb9f645fa21c45cde4eba25a31d73caf1534511a29 manifest.json
|
| 14 |
+
273d8e0e683b885071fb17e08d71e5f2a5ddfb5309756181681de4f5a1822d80 model/chat_template.jinja
|
| 15 |
+
90c4a5672369ff2b3010f057c5dd1abafafcd29c9890ad61e7a893f6a1fa2b5e model/config.json
|
| 16 |
+
62153eb6c69f2e1f426beaa8002b7186437e949c7588167085df14e10e9c0a73 model/generation_config.json
|
| 17 |
+
3a159dfec9978a186a72ba085e0ad6a050f3968d8b364218d7bd13f5c89381f2 model/preprocessor_config.json
|
| 18 |
+
aa7582a6b90dc3711049d293d985e549f2b22c0c4e5ad11b9272a03c6294886e model/tokenizer.json
|
| 19 |
+
8c54bd45ef2fc821a711502da04af1bff020fe02c2207117c7ffdbf1866e6e25 model/tokenizer_config.json
|
| 20 |
+
7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13 model/video_preprocessor_config.json
|
| 21 |
+
d7910a3390f8073d95908f3301c0ea0e2df4551699546430003bc8226373c2bc qwen_export_helpers.py
|
| 22 |
+
8d39d10513cef09276fb806d7d16b33c3c9b7d30a212bdd87f0eb16ad8796d2e runtime-smoke.json
|
| 23 |
+
9e17531efd289cf720a36688d943197f9baf9db7c4f743aa4951f6b6f7dbafda serve/README.md
|
| 24 |
+
0579eb845ee7af463ec88cf627d99dd636466f2c12d840a7cbe183d8f17044fd serve/amos/__init__.py
|
| 25 |
+
ba094d9eaedd229a99b2bdcd35a371ffdeb9aaee258a342524cc19e2f85ec421 serve/amos/calibrate.py
|
| 26 |
+
0dcacb27a05d31781f8310cbe6123b0ee351c62456cc6b5f8576b40b8ddad080 serve/amos/decoder.py
|
| 27 |
+
b4a5a606a2dee99eedf86cab50be759eca0908a318e4f3037ba92560d837bd67 serve/amos/encoder.py
|
| 28 |
+
f2efd7ef25453f7f2dc204dd247c86733a55697ec48714f30560df8ed2da1f23 serve/amos/facts.py
|
| 29 |
+
d4f4354f3e4d3b475b4c1f832a4a9741da757a368a1d2e0e74544d06009c2c27 serve/amos/model.py
|
| 30 |
+
14f027c9e15c456abb0f264c07dae06afb7ace7b64270f2374e97eabeb04f993 serve/amos/packing.py
|
| 31 |
+
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 serve/amos/py.typed
|
| 32 |
+
b0523591c320a074a0ec0751b68ebc205aeedd090ff5e7ca10bda3fb94a07ef8 serve/amos/serve.py
|
| 33 |
+
133d1e29cc64e371f5df1f0cb0328fd77b8b9093d8465319580cf5ea4af5d081 serve/amos/vision.py
|
| 34 |
+
bea3f98882a5d12c891aae4a5d75eda8c5a5436e88889d94b6a7d3d648d126f2 serve/pyproject.toml
|
| 35 |
+
0d43aa4e58fe6131f4ed1f4bbcbcaa4a0815f8b1fe5331c7e96489700681136b serve/sources.json
|
| 36 |
+
c5b0aa4de369327de6d03ba19d322557568e9d9c312b901ab42478dc679199d2 smoke_decoder.py
|
| 37 |
+
bfe4ef23e8e7bb5041f9e10a769b5c2169b26225ffdd7016e9062c689e593932 text.aimodel/main.hash
|
| 38 |
+
1c469415a143a1d9d75486ed48dd9b32c1033f1854a6bfc394ae9eb08caa8c72 text.aimodel/main.mlirb
|
| 39 |
+
a678dc0dcb7ff0a16708ee3e534460d0ba3c0679c9f43fac4c003b4d68d17b35 text.aimodel/metadata.json
|
| 40 |
+
94dc6f950a92250e5711be9af9c1b7893109e1dd207f97f9ae00e5584b71ae0e validate_decoder.py
|
| 41 |
+
41865477766f6b49dcfa43354734bb6b2158b8395e80f6d9b711a13f1dcae541 validation.json
|
| 42 |
+
7631b43ef5f505089f17d48a3a71ec71bb5fc210c933189ab779c176da39e051 vision.aimodel/main.hash
|
| 43 |
+
1f646ae2677fb9ba2793ec942d2b9ed82690199d7a75685ecdf4a84ea791f38f vision.aimodel/main.mlirb
|
| 44 |
+
ffd58384957fd6de58cf8bb5ac50261aba88af370aa53499e065b0a635b903f8 vision.aimodel/metadata.json
|
SOURCE.json
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"repo": "mukaddamzaid/amos-1-small",
|
| 3 |
+
"revision": "09ba3e9d8f506cfcb96ea130845d5d76d9d92028",
|
| 4 |
+
"bytes": 4555951728,
|
| 5 |
+
"files": [
|
| 6 |
+
{
|
| 7 |
+
"path": ".gitattributes",
|
| 8 |
+
"size": 1636,
|
| 9 |
+
"lfs": null
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"path": "LICENSE",
|
| 13 |
+
"size": 11358,
|
| 14 |
+
"lfs": null
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"path": "README.md",
|
| 18 |
+
"size": 4347,
|
| 19 |
+
"lfs": null
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"path": "assets/amos-1-small.png",
|
| 23 |
+
"size": 116696,
|
| 24 |
+
"lfs": {
|
| 25 |
+
"size": 116696,
|
| 26 |
+
"sha256": "fa3963e990c3e0e6f1f8f1296027b44ca080a1adab4855c7cae515a122d79089",
|
| 27 |
+
"pointer_size": 131
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"path": "calibration.json",
|
| 32 |
+
"size": 147,
|
| 33 |
+
"lfs": null
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"path": "config.json",
|
| 37 |
+
"size": 28,
|
| 38 |
+
"lfs": null
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"path": "model/chat_template.jinja",
|
| 42 |
+
"size": 7755,
|
| 43 |
+
"lfs": null
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"path": "model/config.json",
|
| 47 |
+
"size": 2855,
|
| 48 |
+
"lfs": null
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"path": "model/generation_config.json",
|
| 52 |
+
"size": 116,
|
| 53 |
+
"lfs": null
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"path": "model/heads.pt",
|
| 57 |
+
"size": 109123355,
|
| 58 |
+
"lfs": {
|
| 59 |
+
"size": 109123355,
|
| 60 |
+
"sha256": "30ba05e777e4a07e8aec5139eb6700d99b4d294d86ad67f312fc14e3e23575c1",
|
| 61 |
+
"pointer_size": 134
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"path": "model/model.safetensors",
|
| 66 |
+
"size": 4426558864,
|
| 67 |
+
"lfs": {
|
| 68 |
+
"size": 4426558864,
|
| 69 |
+
"sha256": "790c9700fe4a0be2b79498919c04d74a3cbfaf01e451d4c589db601608f4f557",
|
| 70 |
+
"pointer_size": 135
|
| 71 |
+
}
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"path": "model/preprocessor_config.json",
|
| 75 |
+
"size": 443,
|
| 76 |
+
"lfs": null
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"path": "model/tokenizer.json",
|
| 80 |
+
"size": 19989510,
|
| 81 |
+
"lfs": {
|
| 82 |
+
"size": 19989510,
|
| 83 |
+
"sha256": "aa7582a6b90dc3711049d293d985e549f2b22c0c4e5ad11b9272a03c6294886e",
|
| 84 |
+
"pointer_size": 133
|
| 85 |
+
}
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"path": "model/tokenizer_config.json",
|
| 89 |
+
"size": 1172,
|
| 90 |
+
"lfs": null
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"path": "model/video_preprocessor_config.json",
|
| 94 |
+
"size": 385,
|
| 95 |
+
"lfs": null
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"path": "serve/README.md",
|
| 99 |
+
"size": 2505,
|
| 100 |
+
"lfs": null
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"path": "serve/amos/__init__.py",
|
| 104 |
+
"size": 42,
|
| 105 |
+
"lfs": null
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"path": "serve/amos/calibrate.py",
|
| 109 |
+
"size": 1712,
|
| 110 |
+
"lfs": null
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"path": "serve/amos/decoder.py",
|
| 114 |
+
"size": 43439,
|
| 115 |
+
"lfs": null
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"path": "serve/amos/encoder.py",
|
| 119 |
+
"size": 7816,
|
| 120 |
+
"lfs": null
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"path": "serve/amos/facts.py",
|
| 124 |
+
"size": 1356,
|
| 125 |
+
"lfs": null
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"path": "serve/amos/model.py",
|
| 129 |
+
"size": 15106,
|
| 130 |
+
"lfs": null
|
| 131 |
+
},
|
| 132 |
+
{
|
| 133 |
+
"path": "serve/amos/packing.py",
|
| 134 |
+
"size": 10835,
|
| 135 |
+
"lfs": null
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"path": "serve/amos/py.typed",
|
| 139 |
+
"size": 0,
|
| 140 |
+
"lfs": null
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"path": "serve/amos/serve.py",
|
| 144 |
+
"size": 39697,
|
| 145 |
+
"lfs": null
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"path": "serve/amos/vision.py",
|
| 149 |
+
"size": 6633,
|
| 150 |
+
"lfs": null
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"path": "serve/pyproject.toml",
|
| 154 |
+
"size": 842,
|
| 155 |
+
"lfs": null
|
| 156 |
+
},
|
| 157 |
+
{
|
| 158 |
+
"path": "serve/sources.json",
|
| 159 |
+
"size": 1723,
|
| 160 |
+
"lfs": null
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"path": "typed_decisions.json",
|
| 164 |
+
"size": 1355,
|
| 165 |
+
"lfs": null
|
| 166 |
+
}
|
| 167 |
+
],
|
| 168 |
+
"local_source_hashes": {
|
| 169 |
+
"model/model.safetensors": "790c9700fe4a0be2b79498919c04d74a3cbfaf01e451d4c589db601608f4f557",
|
| 170 |
+
"model/tokenizer_config.json": "8c54bd45ef2fc821a711502da04af1bff020fe02c2207117c7ffdbf1866e6e25",
|
| 171 |
+
"model/config.json": "90c4a5672369ff2b3010f057c5dd1abafafcd29c9890ad61e7a893f6a1fa2b5e",
|
| 172 |
+
"model/tokenizer.json": "aa7582a6b90dc3711049d293d985e549f2b22c0c4e5ad11b9272a03c6294886e",
|
| 173 |
+
"model/heads.pt": "30ba05e777e4a07e8aec5139eb6700d99b4d294d86ad67f312fc14e3e23575c1",
|
| 174 |
+
"model/generation_config.json": "62153eb6c69f2e1f426beaa8002b7186437e949c7588167085df14e10e9c0a73",
|
| 175 |
+
"model/chat_template.jinja": "273d8e0e683b885071fb17e08d71e5f2a5ddfb5309756181681de4f5a1822d80",
|
| 176 |
+
"model/video_preprocessor_config.json": "7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13",
|
| 177 |
+
"model/preprocessor_config.json": "3a159dfec9978a186a72ba085e0ad6a050f3968d8b364218d7bd13f5c89381f2"
|
| 178 |
+
}
|
| 179 |
+
}
|
bf16-attempt.json
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"case": "refund",
|
| 4 |
+
"tokens": 122,
|
| 5 |
+
"max_probability_or_box_error": 0.0008294582366943359,
|
| 6 |
+
"decisions_match": true,
|
| 7 |
+
"upstream": [
|
| 8 |
+
{
|
| 9 |
+
"type": "noul",
|
| 10 |
+
"p_yes": 0.9724245667457581
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"type": "choice",
|
| 14 |
+
"probs": [
|
| 15 |
+
0.9623249769210815,
|
| 16 |
+
0.01487948652356863,
|
| 17 |
+
0.02279551699757576
|
| 18 |
+
],
|
| 19 |
+
"choice": 0
|
| 20 |
+
}
|
| 21 |
+
],
|
| 22 |
+
"coreai": [
|
| 23 |
+
{
|
| 24 |
+
"type": "noul",
|
| 25 |
+
"p_yes": 0.9723380208015442
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"type": "choice",
|
| 29 |
+
"probs": [
|
| 30 |
+
0.9614955186843872,
|
| 31 |
+
0.014982474967837334,
|
| 32 |
+
0.023522058501839638
|
| 33 |
+
],
|
| 34 |
+
"choice": 0
|
| 35 |
+
}
|
| 36 |
+
]
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"case": "span",
|
| 40 |
+
"tokens": 123,
|
| 41 |
+
"max_probability_or_box_error": 0,
|
| 42 |
+
"decisions_match": true,
|
| 43 |
+
"upstream": [
|
| 44 |
+
{
|
| 45 |
+
"type": "span",
|
| 46 |
+
"start": 55,
|
| 47 |
+
"end": 63,
|
| 48 |
+
"text": "INV-2231",
|
| 49 |
+
"p": 1.0
|
| 50 |
+
}
|
| 51 |
+
],
|
| 52 |
+
"coreai": [
|
| 53 |
+
{
|
| 54 |
+
"type": "span",
|
| 55 |
+
"start": 55,
|
| 56 |
+
"end": 63,
|
| 57 |
+
"text": "INV-2231",
|
| 58 |
+
"p": 1.0
|
| 59 |
+
}
|
| 60 |
+
]
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"case": "score",
|
| 64 |
+
"tokens": 98,
|
| 65 |
+
"max_probability_or_box_error": 0.001256406307220459,
|
| 66 |
+
"decisions_match": true,
|
| 67 |
+
"upstream": [
|
| 68 |
+
{
|
| 69 |
+
"type": "score",
|
| 70 |
+
"probs": [
|
| 71 |
+
0.011553974822163582,
|
| 72 |
+
0.012325819581747055,
|
| 73 |
+
0.05834781378507614,
|
| 74 |
+
0.9177724123001099
|
| 75 |
+
],
|
| 76 |
+
"choice": 3,
|
| 77 |
+
"expected": 2.882338684052229
|
| 78 |
+
}
|
| 79 |
+
],
|
| 80 |
+
"coreai": [
|
| 81 |
+
{
|
| 82 |
+
"type": "score",
|
| 83 |
+
"probs": [
|
| 84 |
+
0.011898641474545002,
|
| 85 |
+
0.012468610890209675,
|
| 86 |
+
0.059116706252098083,
|
| 87 |
+
0.9165160059928894
|
| 88 |
+
],
|
| 89 |
+
"choice": 3,
|
| 90 |
+
"expected": 2.880250041373074
|
| 91 |
+
}
|
| 92 |
+
]
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"case": "multi",
|
| 96 |
+
"tokens": 144,
|
| 97 |
+
"max_probability_or_box_error": 0.0004277229309082031,
|
| 98 |
+
"decisions_match": true,
|
| 99 |
+
"upstream": [
|
| 100 |
+
{
|
| 101 |
+
"type": "multi",
|
| 102 |
+
"probs": [
|
| 103 |
+
0.9700461626052856,
|
| 104 |
+
0.9614417552947998,
|
| 105 |
+
0.022039415314793587
|
| 106 |
+
]
|
| 107 |
+
}
|
| 108 |
+
],
|
| 109 |
+
"coreai": [
|
| 110 |
+
{
|
| 111 |
+
"type": "multi",
|
| 112 |
+
"probs": [
|
| 113 |
+
0.9697818160057068,
|
| 114 |
+
0.961869478225708,
|
| 115 |
+
0.02190219797194004
|
| 116 |
+
]
|
| 117 |
+
}
|
| 118 |
+
]
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
"case": "negative",
|
| 122 |
+
"tokens": 83,
|
| 123 |
+
"max_probability_or_box_error": 0.0006557293236255646,
|
| 124 |
+
"decisions_match": true,
|
| 125 |
+
"upstream": [
|
| 126 |
+
{
|
| 127 |
+
"type": "noul",
|
| 128 |
+
"p_yes": 0.049154266715049744
|
| 129 |
+
}
|
| 130 |
+
],
|
| 131 |
+
"coreai": [
|
| 132 |
+
{
|
| 133 |
+
"type": "noul",
|
| 134 |
+
"p_yes": 0.04980999603867531
|
| 135 |
+
}
|
| 136 |
+
]
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"case": "unicode",
|
| 140 |
+
"tokens": 128,
|
| 141 |
+
"max_probability_or_box_error": 0.0008890628814697266,
|
| 142 |
+
"decisions_match": true,
|
| 143 |
+
"upstream": [
|
| 144 |
+
{
|
| 145 |
+
"type": "span",
|
| 146 |
+
"start": 10,
|
| 147 |
+
"end": 17,
|
| 148 |
+
"text": "\u20ac123.45",
|
| 149 |
+
"p": 0.9967826008796692
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
"type": "choice",
|
| 153 |
+
"probs": [
|
| 154 |
+
0.9754987955093384,
|
| 155 |
+
0.013173527084290981,
|
| 156 |
+
0.011327654123306274
|
| 157 |
+
],
|
| 158 |
+
"choice": 0
|
| 159 |
+
}
|
| 160 |
+
],
|
| 161 |
+
"coreai": [
|
| 162 |
+
{
|
| 163 |
+
"type": "span",
|
| 164 |
+
"start": 10,
|
| 165 |
+
"end": 17,
|
| 166 |
+
"text": "\u20ac123.45",
|
| 167 |
+
"p": 0.9976716637611389
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"type": "choice",
|
| 171 |
+
"probs": [
|
| 172 |
+
0.9759361147880554,
|
| 173 |
+
0.01289563812315464,
|
| 174 |
+
0.011168276891112328
|
| 175 |
+
],
|
| 176 |
+
"choice": 0
|
| 177 |
+
}
|
| 178 |
+
]
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
"case": "options",
|
| 182 |
+
"tokens": 230,
|
| 183 |
+
"max_probability_or_box_error": 0.0005109310150146484,
|
| 184 |
+
"decisions_match": true,
|
| 185 |
+
"upstream": [
|
| 186 |
+
{
|
| 187 |
+
"type": "choice",
|
| 188 |
+
"probs": [
|
| 189 |
+
0.003980829380452633,
|
| 190 |
+
0.003826719941571355,
|
| 191 |
+
0.004242099355906248,
|
| 192 |
+
0.0034746285527944565,
|
| 193 |
+
0.003326982958242297,
|
| 194 |
+
0.0027517760172486305,
|
| 195 |
+
0.0033690459094941616,
|
| 196 |
+
0.0029055955819785595,
|
| 197 |
+
0.002698659896850586,
|
| 198 |
+
0.002124715130776167,
|
| 199 |
+
0.00298517057672143,
|
| 200 |
+
0.0031158491037786007,
|
| 201 |
+
0.003178240265697241,
|
| 202 |
+
0.00276554049924016,
|
| 203 |
+
0.0025187907740473747,
|
| 204 |
+
0.0038957030046731234,
|
| 205 |
+
0.00339102977886796,
|
| 206 |
+
0.9302572011947632,
|
| 207 |
+
0.0037882667966187,
|
| 208 |
+
0.0032934194896370173,
|
| 209 |
+
0.0014340636553242803,
|
| 210 |
+
0.0015281529631465673,
|
| 211 |
+
0.0019327420741319656,
|
| 212 |
+
0.0032147751189768314
|
| 213 |
+
],
|
| 214 |
+
"choice": 17
|
| 215 |
+
}
|
| 216 |
+
],
|
| 217 |
+
"coreai": [
|
| 218 |
+
{
|
| 219 |
+
"type": "choice",
|
| 220 |
+
"probs": [
|
| 221 |
+
0.004058705177158117,
|
| 222 |
+
0.003873375477269292,
|
| 223 |
+
0.004297157749533653,
|
| 224 |
+
0.0035347286611795425,
|
| 225 |
+
0.003358113579452038,
|
| 226 |
+
0.0027418085373938084,
|
| 227 |
+
0.0034121156204491854,
|
| 228 |
+
0.002912522992119193,
|
| 229 |
+
0.0026929345913231373,
|
| 230 |
+
0.002130611566826701,
|
| 231 |
+
0.0030158632434904575,
|
| 232 |
+
0.003093816339969635,
|
| 233 |
+
0.0032024341635406017,
|
| 234 |
+
0.002781065646559,
|
| 235 |
+
0.002530055120587349,
|
| 236 |
+
0.003932687919586897,
|
| 237 |
+
0.0034593497402966022,
|
| 238 |
+
0.9297462701797485,
|
| 239 |
+
0.003791194409132004,
|
| 240 |
+
0.003320459509268403,
|
| 241 |
+
0.0014135388191789389,
|
| 242 |
+
0.0015202258946374059,
|
| 243 |
+
0.0019439932657405734,
|
| 244 |
+
0.0032370148692280054
|
| 245 |
+
],
|
| 246 |
+
"choice": 17
|
| 247 |
+
}
|
| 248 |
+
]
|
| 249 |
+
},
|
| 250 |
+
{
|
| 251 |
+
"case": "vision",
|
| 252 |
+
"tokens": 155,
|
| 253 |
+
"max_probability_or_box_error": 0.047274649143218994,
|
| 254 |
+
"decisions_match": true,
|
| 255 |
+
"upstream": [
|
| 256 |
+
{
|
| 257 |
+
"type": "region",
|
| 258 |
+
"cell": 9,
|
| 259 |
+
"box": [
|
| 260 |
+
0.2031,
|
| 261 |
+
0.0,
|
| 262 |
+
0.5674,
|
| 263 |
+
0.86
|
| 264 |
+
],
|
| 265 |
+
"p": 0.5970777273178101
|
| 266 |
+
},
|
| 267 |
+
{
|
| 268 |
+
"type": "choice",
|
| 269 |
+
"probs": [
|
| 270 |
+
0.9851310849189758,
|
| 271 |
+
0.014868861995637417
|
| 272 |
+
],
|
| 273 |
+
"choice": 0
|
| 274 |
+
}
|
| 275 |
+
],
|
| 276 |
+
"coreai": [
|
| 277 |
+
{
|
| 278 |
+
"type": "region",
|
| 279 |
+
"cell": 9,
|
| 280 |
+
"box": [
|
| 281 |
+
0.2005,
|
| 282 |
+
0.0,
|
| 283 |
+
0.5598,
|
| 284 |
+
0.8525
|
| 285 |
+
],
|
| 286 |
+
"p": 0.5498030781745911
|
| 287 |
+
},
|
| 288 |
+
{
|
| 289 |
+
"type": "choice",
|
| 290 |
+
"probs": [
|
| 291 |
+
0.9848902821540833,
|
| 292 |
+
0.015109767206013203
|
| 293 |
+
],
|
| 294 |
+
"choice": 0
|
| 295 |
+
}
|
| 296 |
+
]
|
| 297 |
+
}
|
| 298 |
+
]
|
bf16-comparison.json
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"case": "refund",
|
| 4 |
+
"max_probability_or_box_error": 0.0006063971668481827,
|
| 5 |
+
"upstream_bf16": [
|
| 6 |
+
{
|
| 7 |
+
"type": "noul",
|
| 8 |
+
"p_yes": 0.9724245667457581
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"type": "choice",
|
| 12 |
+
"probs": [
|
| 13 |
+
0.9623249769210815,
|
| 14 |
+
0.01487948652356863,
|
| 15 |
+
0.02279551699757576
|
| 16 |
+
],
|
| 17 |
+
"choice": 0
|
| 18 |
+
}
|
| 19 |
+
],
|
| 20 |
+
"coreai_float32": [
|
| 21 |
+
{
|
| 22 |
+
"type": "noul",
|
| 23 |
+
"p_yes": 0.9725583791732788
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"type": "choice",
|
| 27 |
+
"probs": [
|
| 28 |
+
0.9617581963539124,
|
| 29 |
+
0.014839922077953815,
|
| 30 |
+
0.023401914164423943
|
| 31 |
+
],
|
| 32 |
+
"choice": 0
|
| 33 |
+
}
|
| 34 |
+
]
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"case": "span",
|
| 38 |
+
"max_probability_or_box_error": 0,
|
| 39 |
+
"upstream_bf16": [
|
| 40 |
+
{
|
| 41 |
+
"type": "span",
|
| 42 |
+
"start": 55,
|
| 43 |
+
"end": 63,
|
| 44 |
+
"text": "INV-2231",
|
| 45 |
+
"p": 1.0
|
| 46 |
+
}
|
| 47 |
+
],
|
| 48 |
+
"coreai_float32": [
|
| 49 |
+
{
|
| 50 |
+
"type": "span",
|
| 51 |
+
"start": 55,
|
| 52 |
+
"end": 63,
|
| 53 |
+
"text": "INV-2231",
|
| 54 |
+
"p": 1.0
|
| 55 |
+
}
|
| 56 |
+
]
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"case": "score",
|
| 60 |
+
"max_probability_or_box_error": 0.0008858442306518555,
|
| 61 |
+
"upstream_bf16": [
|
| 62 |
+
{
|
| 63 |
+
"type": "score",
|
| 64 |
+
"probs": [
|
| 65 |
+
0.011553974822163582,
|
| 66 |
+
0.012325819581747055,
|
| 67 |
+
0.05834781378507614,
|
| 68 |
+
0.9177724123001099
|
| 69 |
+
],
|
| 70 |
+
"choice": 3,
|
| 71 |
+
"expected": 2.882338684052229
|
| 72 |
+
}
|
| 73 |
+
],
|
| 74 |
+
"coreai_float32": [
|
| 75 |
+
{
|
| 76 |
+
"type": "score",
|
| 77 |
+
"probs": [
|
| 78 |
+
0.01181461475789547,
|
| 79 |
+
0.012464579194784164,
|
| 80 |
+
0.058834243565797806,
|
| 81 |
+
0.916886568069458
|
| 82 |
+
],
|
| 83 |
+
"choice": 3,
|
| 84 |
+
"expected": 2.880792770534754
|
| 85 |
+
}
|
| 86 |
+
]
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"case": "multi",
|
| 90 |
+
"max_probability_or_box_error": 0.00027555227279663086,
|
| 91 |
+
"upstream_bf16": [
|
| 92 |
+
{
|
| 93 |
+
"type": "multi",
|
| 94 |
+
"probs": [
|
| 95 |
+
0.9700461626052856,
|
| 96 |
+
0.9614417552947998,
|
| 97 |
+
0.022039415314793587
|
| 98 |
+
]
|
| 99 |
+
}
|
| 100 |
+
],
|
| 101 |
+
"coreai_float32": [
|
| 102 |
+
{
|
| 103 |
+
"type": "multi",
|
| 104 |
+
"probs": [
|
| 105 |
+
0.9698116779327393,
|
| 106 |
+
0.9617173075675964,
|
| 107 |
+
0.021801499649882317
|
| 108 |
+
]
|
| 109 |
+
}
|
| 110 |
+
]
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"case": "negative",
|
| 114 |
+
"max_probability_or_box_error": 0.0009536556899547577,
|
| 115 |
+
"upstream_bf16": [
|
| 116 |
+
{
|
| 117 |
+
"type": "noul",
|
| 118 |
+
"p_yes": 0.049154266715049744
|
| 119 |
+
}
|
| 120 |
+
],
|
| 121 |
+
"coreai_float32": [
|
| 122 |
+
{
|
| 123 |
+
"type": "noul",
|
| 124 |
+
"p_yes": 0.0501079224050045
|
| 125 |
+
}
|
| 126 |
+
]
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"case": "unicode",
|
| 130 |
+
"max_probability_or_box_error": 0.0007423162460327148,
|
| 131 |
+
"upstream_bf16": [
|
| 132 |
+
{
|
| 133 |
+
"type": "span",
|
| 134 |
+
"start": 10,
|
| 135 |
+
"end": 17,
|
| 136 |
+
"text": "\u20ac123.45",
|
| 137 |
+
"p": 0.9967826008796692
|
| 138 |
+
},
|
| 139 |
+
{
|
| 140 |
+
"type": "choice",
|
| 141 |
+
"probs": [
|
| 142 |
+
0.9754987955093384,
|
| 143 |
+
0.013173527084290981,
|
| 144 |
+
0.011327654123306274
|
| 145 |
+
],
|
| 146 |
+
"choice": 0
|
| 147 |
+
}
|
| 148 |
+
],
|
| 149 |
+
"coreai_float32": [
|
| 150 |
+
{
|
| 151 |
+
"type": "span",
|
| 152 |
+
"start": 10,
|
| 153 |
+
"end": 17,
|
| 154 |
+
"text": "\u20ac123.45",
|
| 155 |
+
"p": 0.9975249171257019
|
| 156 |
+
},
|
| 157 |
+
{
|
| 158 |
+
"type": "choice",
|
| 159 |
+
"probs": [
|
| 160 |
+
0.9758961200714111,
|
| 161 |
+
0.012953538447618484,
|
| 162 |
+
0.011150323785841465
|
| 163 |
+
],
|
| 164 |
+
"choice": 0
|
| 165 |
+
}
|
| 166 |
+
]
|
| 167 |
+
},
|
| 168 |
+
{
|
| 169 |
+
"case": "options",
|
| 170 |
+
"max_probability_or_box_error": 0.000368654727935791,
|
| 171 |
+
"upstream_bf16": [
|
| 172 |
+
{
|
| 173 |
+
"type": "choice",
|
| 174 |
+
"probs": [
|
| 175 |
+
0.003980829380452633,
|
| 176 |
+
0.003826719941571355,
|
| 177 |
+
0.004242099355906248,
|
| 178 |
+
0.0034746285527944565,
|
| 179 |
+
0.003326982958242297,
|
| 180 |
+
0.0027517760172486305,
|
| 181 |
+
0.0033690459094941616,
|
| 182 |
+
0.0029055955819785595,
|
| 183 |
+
0.002698659896850586,
|
| 184 |
+
0.002124715130776167,
|
| 185 |
+
0.00298517057672143,
|
| 186 |
+
0.0031158491037786007,
|
| 187 |
+
0.003178240265697241,
|
| 188 |
+
0.00276554049924016,
|
| 189 |
+
0.0025187907740473747,
|
| 190 |
+
0.0038957030046731234,
|
| 191 |
+
0.00339102977886796,
|
| 192 |
+
0.9302572011947632,
|
| 193 |
+
0.0037882667966187,
|
| 194 |
+
0.0032934194896370173,
|
| 195 |
+
0.0014340636553242803,
|
| 196 |
+
0.0015281529631465673,
|
| 197 |
+
0.0019327420741319656,
|
| 198 |
+
0.0032147751189768314
|
| 199 |
+
],
|
| 200 |
+
"choice": 17
|
| 201 |
+
}
|
| 202 |
+
],
|
| 203 |
+
"coreai_float32": [
|
| 204 |
+
{
|
| 205 |
+
"type": "choice",
|
| 206 |
+
"probs": [
|
| 207 |
+
0.004064517095685005,
|
| 208 |
+
0.00385798211209476,
|
| 209 |
+
0.004268613178282976,
|
| 210 |
+
0.0035152393393218517,
|
| 211 |
+
0.0033423572313040495,
|
| 212 |
+
0.0027638806495815516,
|
| 213 |
+
0.0034003155305981636,
|
| 214 |
+
0.0028926795348525047,
|
| 215 |
+
0.0026991423219442368,
|
| 216 |
+
0.00212881900370121,
|
| 217 |
+
0.0030107346829026937,
|
| 218 |
+
0.00310140079818666,
|
| 219 |
+
0.0031818472780287266,
|
| 220 |
+
0.002777676796540618,
|
| 221 |
+
0.002535800449550152,
|
| 222 |
+
0.003921480383723974,
|
| 223 |
+
0.003430022159591317,
|
| 224 |
+
0.9298885464668274,
|
| 225 |
+
0.003782423911616206,
|
| 226 |
+
0.0032975096255540848,
|
| 227 |
+
0.0014300619950518012,
|
| 228 |
+
0.0015298567013815045,
|
| 229 |
+
0.0019381335005164146,
|
| 230 |
+
0.0032409762497991323
|
| 231 |
+
],
|
| 232 |
+
"choice": 17
|
| 233 |
+
}
|
| 234 |
+
]
|
| 235 |
+
},
|
| 236 |
+
{
|
| 237 |
+
"case": "vision",
|
| 238 |
+
"max_probability_or_box_error": 0.03414243459701538,
|
| 239 |
+
"upstream_bf16": [
|
| 240 |
+
{
|
| 241 |
+
"type": "region",
|
| 242 |
+
"cell": 9,
|
| 243 |
+
"box": [
|
| 244 |
+
0.2031,
|
| 245 |
+
0.0,
|
| 246 |
+
0.5674,
|
| 247 |
+
0.86
|
| 248 |
+
],
|
| 249 |
+
"p": 0.5970777273178101
|
| 250 |
+
},
|
| 251 |
+
{
|
| 252 |
+
"type": "choice",
|
| 253 |
+
"probs": [
|
| 254 |
+
0.9851310849189758,
|
| 255 |
+
0.014868861995637417
|
| 256 |
+
],
|
| 257 |
+
"choice": 0
|
| 258 |
+
}
|
| 259 |
+
],
|
| 260 |
+
"coreai_float32": [
|
| 261 |
+
{
|
| 262 |
+
"type": "region",
|
| 263 |
+
"cell": 9,
|
| 264 |
+
"box": [
|
| 265 |
+
0.1936,
|
| 266 |
+
0.0,
|
| 267 |
+
0.5538,
|
| 268 |
+
0.8461
|
| 269 |
+
],
|
| 270 |
+
"p": 0.5629352927207947
|
| 271 |
+
},
|
| 272 |
+
{
|
| 273 |
+
"type": "choice",
|
| 274 |
+
"probs": [
|
| 275 |
+
0.9847150444984436,
|
| 276 |
+
0.015284995548427105
|
| 277 |
+
],
|
| 278 |
+
"choice": 0
|
| 279 |
+
}
|
| 280 |
+
]
|
| 281 |
+
}
|
| 282 |
+
]
|
calibration.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"span": {
|
| 3 |
+
"cap": 30,
|
| 4 |
+
"penalty": 0.2,
|
| 5 |
+
"fitted_on": "held-out quotes: the quote-amounts slice and longquotes clean.jsonl (fit halves)"
|
| 6 |
+
}
|
| 7 |
+
}
|
config.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"base": "Qwen/Qwen3.5-2B"}
|
convert_decoder.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Export AMOS Qwen3.5 bidirectional decisions and trained quote/box heads."""
|
| 2 |
+
import argparse,json,math,sys
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import torch
|
| 5 |
+
from coreai_torch import TorchConverter,get_decomp_table
|
| 6 |
+
from qwen_export_helpers import DecisionVision
|
| 7 |
+
|
| 8 |
+
class Text(torch.nn.Module):
|
| 9 |
+
def __init__(self,model,letters):
|
| 10 |
+
super().__init__();self.language_model=model.model.language_model
|
| 11 |
+
self.register_buffer('readout',model.get_output_embeddings().weight[letters].detach().float().clone())
|
| 12 |
+
def forward(self,input_ids,attention_mask,position_ids,visual_embeddings,visual_mask):
|
| 13 |
+
emb=self.language_model.embed_tokens(input_ids)
|
| 14 |
+
emb=torch.where(visual_mask,visual_embeddings.to(emb.dtype),emb)
|
| 15 |
+
h=self.language_model(inputs_embeds=emb,attention_mask={'full_attention':attention_mask[:,None,None,:].bool(),'linear_attention':None},position_ids=position_ids,use_cache=False).last_hidden_state
|
| 16 |
+
return h.float(),h.float()@self.readout.T
|
| 17 |
+
|
| 18 |
+
class Heads(torch.nn.Module):
|
| 19 |
+
def __init__(self,heads):super().__init__();self.heads=heads
|
| 20 |
+
def forward(self,question_hidden,state_hidden,image_hidden):
|
| 21 |
+
h=self.heads;d=question_hidden.shape[0];dtype=h.span_q.weight.dtype
|
| 22 |
+
q=question_hidden.to(dtype);s=state_hidden.to(dtype);im=image_hidden.to(dtype)
|
| 23 |
+
span=torch.einsum('cd,lcd->cl',h.span_q(q).view(2,d),h.span_k(s).view(-1,2,d))/math.sqrt(d)
|
| 24 |
+
region=h.region_k(im)@h.region_q(q)/math.sqrt(d)
|
| 25 |
+
box=h.box(q,im,region,7,7) if h.box is not None else torch.zeros(4)
|
| 26 |
+
return span.float(),region.float(),box.float()
|
| 27 |
+
|
| 28 |
+
def save(m,args,path):
|
| 29 |
+
with torch.no_grad():ep=torch.export.export(m,args,strict=False).run_decompositions(get_decomp_table())
|
| 30 |
+
program=TorchConverter().add_exported_program(ep).to_coreai();program.save_asset(path)
|
| 31 |
+
return list(ep.graph_signature.user_outputs)
|
| 32 |
+
|
| 33 |
+
def main():
|
| 34 |
+
p=argparse.ArgumentParser();p.add_argument('name');p.add_argument('--length',type=int,default=512);p.add_argument('--precision',default='bfloat16');a=p.parse_args()
|
| 35 |
+
source=Path('upstream')/a.name;out=Path('bundles')/(a.name+'-CoreAI');out.mkdir(parents=True,exist_ok=True)
|
| 36 |
+
sys.path.insert(0,str((source/'serve').resolve()))
|
| 37 |
+
from amos.decoder import load_model,load_heads
|
| 38 |
+
torch.set_num_threads(4);model,codec=load_model(str(source/'model'),'cpu',dtype=getattr(torch,a.precision));model.eval();heads=load_heads(source/'model',model,codec)
|
| 39 |
+
d=model.config.text_config.hidden_size;L=a.length
|
| 40 |
+
inputs=(torch.full((1,L),codec.pad,dtype=torch.int32),torch.ones(1,L,dtype=torch.int32),torch.arange(L,dtype=torch.int32).view(1,1,-1).expand(4,1,-1),torch.zeros(1,L,d),torch.zeros(1,L,1,dtype=torch.bool))
|
| 41 |
+
print('Exporting text',flush=True);outputs=save(Text(model,codec.letters).eval(),inputs,out/'text.aimodel')
|
| 42 |
+
print('Exporting vision',flush=True);visual=model.model.visual;vo=save(DecisionVision(visual).eval(),(torch.zeros(196,1536),),out/'vision.aimodel')
|
| 43 |
+
print('Exporting heads',flush=True);ho=save(Heads(heads).eval(),(torch.zeros(d),torch.zeros(L,d),torch.zeros(49,d)),out/'heads.aimodel')
|
| 44 |
+
(out/'manifest.json').write_text(json.dumps({'sequence_length':L,'hidden_size':d,'outputs':outputs,'vision_output':vo[0],'head_outputs':ho,'precision':a.precision,'echo':codec.echo,'kinds':heads.kinds,'keep':heads.keep,'has_box':heads.box is not None,'image_side':224},indent=2))
|
| 45 |
+
print('Saved',flush=True)
|
| 46 |
+
if __name__=='__main__':main()
|
coreai_decoder.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""AMOS Qwen3.5 Core AI adapter. No PyTorch neural model is loaded."""
|
| 2 |
+
import json,sys
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
from coreai.runtime import AIModel,NDArray,SpecializationOptions
|
| 7 |
+
from transformers import AutoTokenizer,Qwen2VLImageProcessorPil
|
| 8 |
+
|
| 9 |
+
class AmosDecoder:
|
| 10 |
+
@classmethod
|
| 11 |
+
async def load(cls,bundle):
|
| 12 |
+
self=cls();self.root=Path(bundle).resolve();sys.path.insert(0,str(self.root/'serve'))
|
| 13 |
+
import amos.decoder as decoder
|
| 14 |
+
decoder.IMAGE_PIXELS=224*224
|
| 15 |
+
self.manifest=json.loads((self.root/'manifest.json').read_text())
|
| 16 |
+
self.codec=decoder.Codec(AutoTokenizer.from_pretrained(self.root/'model'),Qwen2VLImageProcessorPil.from_pretrained(self.root/'model',size={'shortest_edge':224*224,'longest_edge':224*224}),255)
|
| 17 |
+
self.codec.echo=self.manifest['echo']
|
| 18 |
+
self.temperature=decoder.calibration(self.root) or {};self.span=decoder.span_decoding(self.root)
|
| 19 |
+
self.models=[];self.functions={}
|
| 20 |
+
for name in ['text','vision','heads']:
|
| 21 |
+
m=await AIModel.load(self.root/(name+'.aimodel'),SpecializationOptions.default());self.models.append(m);self.functions[name]=m.load_function('main')
|
| 22 |
+
return self
|
| 23 |
+
|
| 24 |
+
async def raw(self,state,questions,images=()):
|
| 25 |
+
from amos.decoder import expand
|
| 26 |
+
flat,owner=expand(questions)
|
| 27 |
+
ids,at,extra=self.codec.encode_case(state,flat,images)
|
| 28 |
+
n=len(ids);L=self.manifest['sequence_length'];d=self.manifest['hidden_size']
|
| 29 |
+
if n>L:raise ValueError(f'Request needs {n} tokens; Core AI capacity is {L}.')
|
| 30 |
+
args={'input_ids':np.full((1,L),self.codec.pad,np.int32),'attention_mask':np.zeros((1,L),np.int32),'position_ids':np.zeros((4,1,L),np.int32),'visual_embeddings':np.zeros((1,L,d),np.float32),'visual_mask':np.zeros((1,L,1),bool)}
|
| 31 |
+
args['input_ids'][0,:n]=ids;args['attention_mask'][0,:n]=1;args['position_ids'][0,0,:n]=np.arange(n)
|
| 32 |
+
image_at={s:(rows,cols) for s,rows,cols in extra.get('images',[])}
|
| 33 |
+
features=[]
|
| 34 |
+
if image_at:
|
| 35 |
+
pv=extra['pixel_values'].numpy()
|
| 36 |
+
for start in range(0,len(pv),196):
|
| 37 |
+
out=await self.functions['vision']({'pixel_values':NDArray(pv[start:start+196].astype(np.float32))});features.append(out[self.manifest['vision_output']].numpy())
|
| 38 |
+
cursor=0;offset=0;fi=0
|
| 39 |
+
while cursor<n:
|
| 40 |
+
if cursor in image_at:
|
| 41 |
+
rows,cols=image_at[cursor]
|
| 42 |
+
if (rows,cols)!=(7,7):raise ValueError('Only 224x224 image grids are supported')
|
| 43 |
+
end=cursor+49;args['position_ids'][1,0,cursor:end]=offset;args['position_ids'][2,0,cursor:end]=offset+np.repeat(np.arange(7),7);args['position_ids'][3,0,cursor:end]=offset+np.tile(np.arange(7),7)
|
| 44 |
+
args['visual_embeddings'][0,cursor:end]=features[fi];args['visual_mask'][0,cursor:end]=True;offset+=7;cursor=end;fi+=1
|
| 45 |
+
else:
|
| 46 |
+
args['position_ids'][1:,0,cursor]=offset;offset+=1;cursor+=1
|
| 47 |
+
output=await self.functions['text']({k:NDArray(v) for k,v in args.items()})
|
| 48 |
+
hidden,logits=[output[k].numpy() for k in self.manifest['outputs']]
|
| 49 |
+
head_results={}
|
| 50 |
+
for i,(q,pos) in enumerate(zip(flat,at)):
|
| 51 |
+
if q['type'] not in ['span','region']:continue
|
| 52 |
+
first,offsets=extra['state'];state_hidden=np.zeros((L,d),np.float32);state_hidden[:len(offsets)]=hidden[0,first:first+len(offsets)]
|
| 53 |
+
cells=np.zeros((49,d),np.float32)
|
| 54 |
+
if extra.get('images'):
|
| 55 |
+
start,rows,cols=extra['images'][0];cells=hidden[0,start:start+49].copy()
|
| 56 |
+
if q['type']=='region' and not extra.get('images'):raise ValueError('Region requires a state image')
|
| 57 |
+
ho=await self.functions['heads']({'question_hidden':NDArray(hidden[0,pos].copy()),'state_hidden':NDArray(state_hidden),'image_hidden':NDArray(cells)})
|
| 58 |
+
span,region,box=[torch.from_numpy(ho[k].numpy().copy()) for k in self.manifest['head_outputs']]
|
| 59 |
+
head_results[i]=span[:,:len(offsets)] if q['type']=='span' else torch.cat([region,box]) if self.manifest['has_box'] else region
|
| 60 |
+
return {'flat':flat,'owner':owner,'at':at,'extra':extra,'logits':logits[0],'heads':head_results,'tokens':n,'hidden':hidden[0]}
|
| 61 |
+
|
| 62 |
+
async def predict(self,state,questions,images=()):
|
| 63 |
+
from amos.decoder import options,decode_head
|
| 64 |
+
raw=await self.raw(state,questions,images);out=[None]*len(questions)
|
| 65 |
+
for j,(q,k,pos) in enumerate(zip(raw['flat'],raw['owner'],raw['at'])):
|
| 66 |
+
typ=questions[k]['type']
|
| 67 |
+
if q['type'] in ['span','region']:
|
| 68 |
+
out[k]=decode_head(q,raw['heads'][j],raw['extra'],state,self.manifest['keep'],self.span);continue
|
| 69 |
+
logits=torch.tensor(raw['logits'][pos,:len(options(q))]);p=(logits/self.temperature.get(q['type'],1)).softmax(-1).tolist()
|
| 70 |
+
if typ=='multi':out[k]=out[k] or {'type':typ,'probs':[]};out[k]['probs'].append(p[0])
|
| 71 |
+
elif typ=='noul':out[k]={'type':typ,'p_yes':p[0]}
|
| 72 |
+
else:
|
| 73 |
+
out[k]={'type':typ,'probs':p,'choice':int(np.argmax(p))}
|
| 74 |
+
if typ=='score':out[k]['expected']=sum(i*v for i,v in enumerate(p))
|
| 75 |
+
return out
|
| 76 |
+
|
| 77 |
+
async def request(self,req):
|
| 78 |
+
from amos.serve import parse,answer
|
| 79 |
+
from PIL import Image
|
| 80 |
+
import base64,io
|
| 81 |
+
def image(s):return Image.open(io.BytesIO(base64.b64decode(s.split(',')[-1]))).convert('RGB')
|
| 82 |
+
state,ids,qs=parse(req);ims=[image(s) for s in req.get('images',[])]
|
| 83 |
+
qs=[{**q,'options':[image(o['image']) if isinstance(o,dict) else o for o in q['options']]} if 'options' in q else q for q in qs]
|
| 84 |
+
rs=await self.predict(state,qs,ims)
|
| 85 |
+
return {'answers':{i:answer(q,r) for i,q,r in zip(ids,qs,rs)}}
|
heads.aimodel/main.hash
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#��,:\bi��b�T
|
| 2 |
+
Zh4�2gK�G�:[��`
|
heads.aimodel/main.mlirb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:132384c42c3a5c6269a79862d6540a5a6834b532674ba847d83a5b181f85a860
|
| 3 |
+
size 109141184
|
heads.aimodel/metadata.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"creationDate" : "20261014T012237Z",
|
| 3 |
+
"assetVersion" : "2.0",
|
| 4 |
+
"producer" : "coreai-core 1.0.0b3"
|
| 5 |
+
}
|
manifest.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"sequence_length": 512,
|
| 3 |
+
"hidden_size": 2048,
|
| 4 |
+
"outputs": [
|
| 5 |
+
"mul_1744",
|
| 6 |
+
"view_3923"
|
| 7 |
+
],
|
| 8 |
+
"vision_output": "addmm_98",
|
| 9 |
+
"head_outputs": [
|
| 10 |
+
"div",
|
| 11 |
+
"div_1",
|
| 12 |
+
"clamp_2"
|
| 13 |
+
],
|
| 14 |
+
"precision": "float32",
|
| 15 |
+
"echo": true,
|
| 16 |
+
"kinds": [
|
| 17 |
+
"span",
|
| 18 |
+
"region"
|
| 19 |
+
],
|
| 20 |
+
"keep": 0.25,
|
| 21 |
+
"has_box": true,
|
| 22 |
+
"image_side": 224
|
| 23 |
+
}
|
model/chat_template.jinja
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{%- set image_count = namespace(value=0) %}
|
| 2 |
+
{%- set video_count = namespace(value=0) %}
|
| 3 |
+
{%- macro render_content(content, do_vision_count, is_system_content=false) %}
|
| 4 |
+
{%- if content is string %}
|
| 5 |
+
{{- content }}
|
| 6 |
+
{%- elif content is iterable and content is not mapping %}
|
| 7 |
+
{%- for item in content %}
|
| 8 |
+
{%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
|
| 9 |
+
{%- if is_system_content %}
|
| 10 |
+
{{- raise_exception('System message cannot contain images.') }}
|
| 11 |
+
{%- endif %}
|
| 12 |
+
{%- if do_vision_count %}
|
| 13 |
+
{%- set image_count.value = image_count.value + 1 %}
|
| 14 |
+
{%- endif %}
|
| 15 |
+
{%- if add_vision_id %}
|
| 16 |
+
{{- 'Picture ' ~ image_count.value ~ ': ' }}
|
| 17 |
+
{%- endif %}
|
| 18 |
+
{{- '<|vision_start|><|image_pad|><|vision_end|>' }}
|
| 19 |
+
{%- elif 'video' in item or item.type == 'video' %}
|
| 20 |
+
{%- if is_system_content %}
|
| 21 |
+
{{- raise_exception('System message cannot contain videos.') }}
|
| 22 |
+
{%- endif %}
|
| 23 |
+
{%- if do_vision_count %}
|
| 24 |
+
{%- set video_count.value = video_count.value + 1 %}
|
| 25 |
+
{%- endif %}
|
| 26 |
+
{%- if add_vision_id %}
|
| 27 |
+
{{- 'Video ' ~ video_count.value ~ ': ' }}
|
| 28 |
+
{%- endif %}
|
| 29 |
+
{{- '<|vision_start|><|video_pad|><|vision_end|>' }}
|
| 30 |
+
{%- elif 'text' in item %}
|
| 31 |
+
{{- item.text }}
|
| 32 |
+
{%- else %}
|
| 33 |
+
{{- raise_exception('Unexpected item type in content.') }}
|
| 34 |
+
{%- endif %}
|
| 35 |
+
{%- endfor %}
|
| 36 |
+
{%- elif content is none or content is undefined %}
|
| 37 |
+
{{- '' }}
|
| 38 |
+
{%- else %}
|
| 39 |
+
{{- raise_exception('Unexpected content type.') }}
|
| 40 |
+
{%- endif %}
|
| 41 |
+
{%- endmacro %}
|
| 42 |
+
{%- if not messages %}
|
| 43 |
+
{{- raise_exception('No messages provided.') }}
|
| 44 |
+
{%- endif %}
|
| 45 |
+
{%- if tools and tools is iterable and tools is not mapping %}
|
| 46 |
+
{{- '<|im_start|>system\n' }}
|
| 47 |
+
{{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
|
| 48 |
+
{%- for tool in tools %}
|
| 49 |
+
{{- "\n" }}
|
| 50 |
+
{{- tool | tojson }}
|
| 51 |
+
{%- endfor %}
|
| 52 |
+
{{- "\n</tools>" }}
|
| 53 |
+
{{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
|
| 54 |
+
{%- if messages[0].role == 'system' %}
|
| 55 |
+
{%- set content = render_content(messages[0].content, false, true)|trim %}
|
| 56 |
+
{%- if content %}
|
| 57 |
+
{{- '\n\n' + content }}
|
| 58 |
+
{%- endif %}
|
| 59 |
+
{%- endif %}
|
| 60 |
+
{{- '<|im_end|>\n' }}
|
| 61 |
+
{%- else %}
|
| 62 |
+
{%- if messages[0].role == 'system' %}
|
| 63 |
+
{%- set content = render_content(messages[0].content, false, true)|trim %}
|
| 64 |
+
{{- '<|im_start|>system\n' + content + '<|im_end|>\n' }}
|
| 65 |
+
{%- endif %}
|
| 66 |
+
{%- endif %}
|
| 67 |
+
{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
|
| 68 |
+
{%- for message in messages[::-1] %}
|
| 69 |
+
{%- set index = (messages|length - 1) - loop.index0 %}
|
| 70 |
+
{%- if ns.multi_step_tool and message.role == "user" %}
|
| 71 |
+
{%- set content = render_content(message.content, false)|trim %}
|
| 72 |
+
{%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
|
| 73 |
+
{%- set ns.multi_step_tool = false %}
|
| 74 |
+
{%- set ns.last_query_index = index %}
|
| 75 |
+
{%- endif %}
|
| 76 |
+
{%- endif %}
|
| 77 |
+
{%- endfor %}
|
| 78 |
+
{%- if ns.multi_step_tool %}
|
| 79 |
+
{{- raise_exception('No user query found in messages.') }}
|
| 80 |
+
{%- endif %}
|
| 81 |
+
{%- for message in messages %}
|
| 82 |
+
{%- set content = render_content(message.content, true)|trim %}
|
| 83 |
+
{%- if message.role == "system" %}
|
| 84 |
+
{%- if not loop.first %}
|
| 85 |
+
{{- raise_exception('System message must be at the beginning.') }}
|
| 86 |
+
{%- endif %}
|
| 87 |
+
{%- elif message.role == "user" %}
|
| 88 |
+
{{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
|
| 89 |
+
{%- elif message.role == "assistant" %}
|
| 90 |
+
{%- set reasoning_content = '' %}
|
| 91 |
+
{%- if message.reasoning_content is string %}
|
| 92 |
+
{%- set reasoning_content = message.reasoning_content %}
|
| 93 |
+
{%- else %}
|
| 94 |
+
{%- if '</think>' in content %}
|
| 95 |
+
{%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
|
| 96 |
+
{%- set content = content.split('</think>')[-1].lstrip('\n') %}
|
| 97 |
+
{%- endif %}
|
| 98 |
+
{%- endif %}
|
| 99 |
+
{%- set reasoning_content = reasoning_content|trim %}
|
| 100 |
+
{%- if loop.index0 > ns.last_query_index %}
|
| 101 |
+
{{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
|
| 102 |
+
{%- else %}
|
| 103 |
+
{{- '<|im_start|>' + message.role + '\n' + content }}
|
| 104 |
+
{%- endif %}
|
| 105 |
+
{%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
|
| 106 |
+
{%- for tool_call in message.tool_calls %}
|
| 107 |
+
{%- if tool_call.function is defined %}
|
| 108 |
+
{%- set tool_call = tool_call.function %}
|
| 109 |
+
{%- endif %}
|
| 110 |
+
{%- if loop.first %}
|
| 111 |
+
{%- if content|trim %}
|
| 112 |
+
{{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 113 |
+
{%- else %}
|
| 114 |
+
{{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 115 |
+
{%- endif %}
|
| 116 |
+
{%- else %}
|
| 117 |
+
{{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 118 |
+
{%- endif %}
|
| 119 |
+
{%- if tool_call.arguments is defined %}
|
| 120 |
+
{%- for args_name, args_value in tool_call.arguments|items %}
|
| 121 |
+
{{- '<parameter=' + args_name + '>\n' }}
|
| 122 |
+
{%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
|
| 123 |
+
{{- args_value }}
|
| 124 |
+
{{- '\n</parameter>\n' }}
|
| 125 |
+
{%- endfor %}
|
| 126 |
+
{%- endif %}
|
| 127 |
+
{{- '</function>\n</tool_call>' }}
|
| 128 |
+
{%- endfor %}
|
| 129 |
+
{%- endif %}
|
| 130 |
+
{{- '<|im_end|>\n' }}
|
| 131 |
+
{%- elif message.role == "tool" %}
|
| 132 |
+
{%- if loop.previtem and loop.previtem.role != "tool" %}
|
| 133 |
+
{{- '<|im_start|>user' }}
|
| 134 |
+
{%- endif %}
|
| 135 |
+
{{- '\n<tool_response>\n' }}
|
| 136 |
+
{{- content }}
|
| 137 |
+
{{- '\n</tool_response>' }}
|
| 138 |
+
{%- if not loop.last and loop.nextitem.role != "tool" %}
|
| 139 |
+
{{- '<|im_end|>\n' }}
|
| 140 |
+
{%- elif loop.last %}
|
| 141 |
+
{{- '<|im_end|>\n' }}
|
| 142 |
+
{%- endif %}
|
| 143 |
+
{%- else %}
|
| 144 |
+
{{- raise_exception('Unexpected message role.') }}
|
| 145 |
+
{%- endif %}
|
| 146 |
+
{%- endfor %}
|
| 147 |
+
{%- if add_generation_prompt %}
|
| 148 |
+
{{- '<|im_start|>assistant\n' }}
|
| 149 |
+
{%- if enable_thinking is defined and enable_thinking is true %}
|
| 150 |
+
{{- '<think>\n' }}
|
| 151 |
+
{%- else %}
|
| 152 |
+
{{- '<think>\n\n</think>\n\n' }}
|
| 153 |
+
{%- endif %}
|
| 154 |
+
{%- endif %}
|
model/config.json
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"amos_export_dtype": "bfloat16",
|
| 3 |
+
"architectures": [
|
| 4 |
+
"Qwen3_5ForConditionalGeneration"
|
| 5 |
+
],
|
| 6 |
+
"dtype": "bfloat16",
|
| 7 |
+
"image_token_id": 248056,
|
| 8 |
+
"model_type": "qwen3_5",
|
| 9 |
+
"text_config": {
|
| 10 |
+
"attention_bias": false,
|
| 11 |
+
"attention_dropout": 0.0,
|
| 12 |
+
"attn_output_gate": true,
|
| 13 |
+
"bos_token_id": null,
|
| 14 |
+
"dtype": "bfloat16",
|
| 15 |
+
"eos_token_id": 248044,
|
| 16 |
+
"full_attention_interval": 4,
|
| 17 |
+
"head_dim": 256,
|
| 18 |
+
"hidden_act": "silu",
|
| 19 |
+
"hidden_size": 2048,
|
| 20 |
+
"initializer_range": 0.02,
|
| 21 |
+
"intermediate_size": 6144,
|
| 22 |
+
"layer_types": [
|
| 23 |
+
"linear_attention",
|
| 24 |
+
"linear_attention",
|
| 25 |
+
"linear_attention",
|
| 26 |
+
"full_attention",
|
| 27 |
+
"linear_attention",
|
| 28 |
+
"linear_attention",
|
| 29 |
+
"linear_attention",
|
| 30 |
+
"full_attention",
|
| 31 |
+
"linear_attention",
|
| 32 |
+
"linear_attention",
|
| 33 |
+
"linear_attention",
|
| 34 |
+
"full_attention",
|
| 35 |
+
"linear_attention",
|
| 36 |
+
"linear_attention",
|
| 37 |
+
"linear_attention",
|
| 38 |
+
"full_attention",
|
| 39 |
+
"linear_attention",
|
| 40 |
+
"linear_attention",
|
| 41 |
+
"linear_attention",
|
| 42 |
+
"full_attention",
|
| 43 |
+
"linear_attention",
|
| 44 |
+
"linear_attention",
|
| 45 |
+
"linear_attention",
|
| 46 |
+
"full_attention"
|
| 47 |
+
],
|
| 48 |
+
"linear_conv_kernel_dim": 4,
|
| 49 |
+
"linear_key_head_dim": 128,
|
| 50 |
+
"linear_num_key_heads": 16,
|
| 51 |
+
"linear_num_value_heads": 16,
|
| 52 |
+
"linear_value_head_dim": 128,
|
| 53 |
+
"mamba_ssm_dtype": "float32",
|
| 54 |
+
"max_position_embeddings": 262144,
|
| 55 |
+
"mlp_only_layers": [],
|
| 56 |
+
"model_type": "qwen3_5_text",
|
| 57 |
+
"mtp_num_hidden_layers": 1,
|
| 58 |
+
"mtp_use_dedicated_embeddings": false,
|
| 59 |
+
"num_attention_heads": 8,
|
| 60 |
+
"num_hidden_layers": 24,
|
| 61 |
+
"num_key_value_heads": 2,
|
| 62 |
+
"pad_token_id": null,
|
| 63 |
+
"partial_rotary_factor": 0.25,
|
| 64 |
+
"rms_norm_eps": 1e-06,
|
| 65 |
+
"rope_parameters": {
|
| 66 |
+
"mrope_interleaved": true,
|
| 67 |
+
"mrope_section": [
|
| 68 |
+
11,
|
| 69 |
+
11,
|
| 70 |
+
10
|
| 71 |
+
],
|
| 72 |
+
"partial_rotary_factor": 0.25,
|
| 73 |
+
"rope_theta": 10000000,
|
| 74 |
+
"rope_type": "default"
|
| 75 |
+
},
|
| 76 |
+
"tie_word_embeddings": true,
|
| 77 |
+
"use_cache": false,
|
| 78 |
+
"vocab_size": 248320,
|
| 79 |
+
"transformers_version": "5.17.0"
|
| 80 |
+
},
|
| 81 |
+
"tie_word_embeddings": true,
|
| 82 |
+
"transformers_version": "5.17.0",
|
| 83 |
+
"use_cache": false,
|
| 84 |
+
"video_token_id": 248057,
|
| 85 |
+
"vision_config": {
|
| 86 |
+
"deepstack_visual_indexes": [],
|
| 87 |
+
"depth": 24,
|
| 88 |
+
"dtype": "bfloat16",
|
| 89 |
+
"hidden_act": "gelu_pytorch_tanh",
|
| 90 |
+
"hidden_size": 1024,
|
| 91 |
+
"in_channels": 3,
|
| 92 |
+
"initializer_range": 0.02,
|
| 93 |
+
"intermediate_size": 4096,
|
| 94 |
+
"model_type": "qwen3_5_vision",
|
| 95 |
+
"num_heads": 16,
|
| 96 |
+
"num_position_embeddings": 2304,
|
| 97 |
+
"out_hidden_size": 2048,
|
| 98 |
+
"patch_size": 16,
|
| 99 |
+
"rope_parameters": {
|
| 100 |
+
"rope_theta": 10000.0,
|
| 101 |
+
"rope_type": "axial"
|
| 102 |
+
},
|
| 103 |
+
"spatial_merge_size": 2,
|
| 104 |
+
"temporal_patch_size": 2
|
| 105 |
+
},
|
| 106 |
+
"vision_end_token_id": 248054,
|
| 107 |
+
"vision_start_token_id": 248053,
|
| 108 |
+
"amos_codes": 255,
|
| 109 |
+
"amos_noncausal": true
|
| 110 |
+
}
|
model/generation_config.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_from_model_config": true,
|
| 3 |
+
"eos_token_id": 248044,
|
| 4 |
+
"transformers_version": "5.17.0",
|
| 5 |
+
"use_cache": true
|
| 6 |
+
}
|
model/preprocessor_config.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"do_convert_rgb": true,
|
| 3 |
+
"do_normalize": true,
|
| 4 |
+
"do_rescale": true,
|
| 5 |
+
"do_resize": true,
|
| 6 |
+
"image_mean": [
|
| 7 |
+
0.5,
|
| 8 |
+
0.5,
|
| 9 |
+
0.5
|
| 10 |
+
],
|
| 11 |
+
"image_processor_type": "Qwen2VLImageProcessor",
|
| 12 |
+
"image_std": [
|
| 13 |
+
0.5,
|
| 14 |
+
0.5,
|
| 15 |
+
0.5
|
| 16 |
+
],
|
| 17 |
+
"merge_size": 2,
|
| 18 |
+
"patch_size": 16,
|
| 19 |
+
"resample": 3,
|
| 20 |
+
"rescale_factor": 0.00392156862745098,
|
| 21 |
+
"size": {
|
| 22 |
+
"longest_edge": 16777216,
|
| 23 |
+
"shortest_edge": 65536
|
| 24 |
+
},
|
| 25 |
+
"temporal_patch_size": 2
|
| 26 |
+
}
|
model/tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:aa7582a6b90dc3711049d293d985e549f2b22c0c4e5ad11b9272a03c6294886e
|
| 3 |
+
size 19989510
|
model/tokenizer_config.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"extra_special_tokens": [
|
| 12 |
+
"<answer>"
|
| 13 |
+
],
|
| 14 |
+
"image_token": "<|image_pad|>",
|
| 15 |
+
"is_local": true,
|
| 16 |
+
"local_files_only": false,
|
| 17 |
+
"model_max_length": 262144,
|
| 18 |
+
"model_specific_special_tokens": {
|
| 19 |
+
"audio_bos_token": "<|audio_start|>",
|
| 20 |
+
"audio_eos_token": "<|audio_end|>",
|
| 21 |
+
"audio_token": "<|audio_pad|>",
|
| 22 |
+
"image_token": "<|image_pad|>",
|
| 23 |
+
"video_token": "<|video_pad|>",
|
| 24 |
+
"vision_bos_token": "<|vision_start|>",
|
| 25 |
+
"vision_eos_token": "<|vision_end|>"
|
| 26 |
+
},
|
| 27 |
+
"pad_token": "<|endoftext|>",
|
| 28 |
+
"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+",
|
| 29 |
+
"split_special_tokens": false,
|
| 30 |
+
"tokenizer_class": "Qwen2Tokenizer",
|
| 31 |
+
"unk_token": null,
|
| 32 |
+
"video_token": "<|video_pad|>",
|
| 33 |
+
"vision_bos_token": "<|vision_start|>",
|
| 34 |
+
"vision_eos_token": "<|vision_end|>"
|
| 35 |
+
}
|
model/video_preprocessor_config.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"size": {
|
| 3 |
+
"longest_edge": 25165824,
|
| 4 |
+
"shortest_edge": 4096
|
| 5 |
+
},
|
| 6 |
+
"patch_size": 16,
|
| 7 |
+
"temporal_patch_size": 2,
|
| 8 |
+
"merge_size": 2,
|
| 9 |
+
"image_mean": [
|
| 10 |
+
0.5,
|
| 11 |
+
0.5,
|
| 12 |
+
0.5
|
| 13 |
+
],
|
| 14 |
+
"image_std": [
|
| 15 |
+
0.5,
|
| 16 |
+
0.5,
|
| 17 |
+
0.5
|
| 18 |
+
],
|
| 19 |
+
"processor_class": "Qwen3VLProcessor",
|
| 20 |
+
"video_processor_type": "Qwen3VLVideoProcessor"
|
| 21 |
+
}
|
qwen_export_helpers.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Export pinned Decision 3.0 text inference; vision export is separate."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
from coreai_torch import TorchConverter, get_decomp_table
|
| 10 |
+
from safetensors.torch import load_file
|
| 11 |
+
from transformers import Qwen3_5Model
|
| 12 |
+
from transformers.models.qwen3_5.modeling_qwen3_5 import apply_rotary_pos_emb_vision
|
| 13 |
+
from transformers.vision_utils import (
|
| 14 |
+
get_vision_interpolation_indices_and_weights,
|
| 15 |
+
get_vision_position_ids,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class DecisionText(torch.nn.Module):
|
| 20 |
+
def __init__(self, backbone, readout):
|
| 21 |
+
super().__init__()
|
| 22 |
+
self.language_model = backbone.language_model
|
| 23 |
+
self.register_buffer("readout", readout.float())
|
| 24 |
+
|
| 25 |
+
def forward(
|
| 26 |
+
self, input_ids, attention_mask, position_ids, visual_embeddings, visual_mask
|
| 27 |
+
):
|
| 28 |
+
embeddings = self.language_model.embed_tokens(input_ids)
|
| 29 |
+
embeddings = torch.where(
|
| 30 |
+
visual_mask, visual_embeddings.to(embeddings.dtype), embeddings
|
| 31 |
+
)
|
| 32 |
+
hidden = self.language_model(
|
| 33 |
+
inputs_embeds=embeddings,
|
| 34 |
+
attention_mask={
|
| 35 |
+
"full_attention": attention_mask[:, None, None, :].bool(),
|
| 36 |
+
"linear_attention": attention_mask,
|
| 37 |
+
},
|
| 38 |
+
position_ids=position_ids,
|
| 39 |
+
use_cache=False,
|
| 40 |
+
).last_hidden_state
|
| 41 |
+
return hidden[:, -1].float() @ self.readout.T
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class DecisionVision(torch.nn.Module):
|
| 45 |
+
"""The unchanged vision weights at a fixed one-image 224px grid."""
|
| 46 |
+
|
| 47 |
+
def __init__(self, visual, side=224):
|
| 48 |
+
super().__init__()
|
| 49 |
+
self.visual = visual
|
| 50 |
+
grid = torch.tensor(
|
| 51 |
+
[[1, side // visual.config.patch_size, side // visual.config.patch_size]]
|
| 52 |
+
)
|
| 53 |
+
indices, weights = get_vision_interpolation_indices_and_weights(
|
| 54 |
+
grid,
|
| 55 |
+
num_grid_per_side=visual.num_grid_per_side,
|
| 56 |
+
mode=visual.interpolation_mode,
|
| 57 |
+
align_corners=visual.interpolation_align_corners,
|
| 58 |
+
spatial_merge_size=visual.config.spatial_merge_size,
|
| 59 |
+
kwargs={},
|
| 60 |
+
)
|
| 61 |
+
self.register_buffer("indices", indices)
|
| 62 |
+
self.register_buffer("weights", weights)
|
| 63 |
+
self.register_buffer(
|
| 64 |
+
"positions",
|
| 65 |
+
get_vision_position_ids(grid, visual.spatial_merge_size, kwargs={}),
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
def forward(self, pixel_values):
|
| 69 |
+
v = self.visual
|
| 70 |
+
hidden = F.linear(
|
| 71 |
+
pixel_values.to(v.patch_embed.proj.weight.dtype),
|
| 72 |
+
v.patch_embed.proj.weight.flatten(1),
|
| 73 |
+
v.patch_embed.proj.bias,
|
| 74 |
+
)
|
| 75 |
+
hidden = hidden + (v.pos_embed(self.indices) * self.weights[:, :, None]).sum(
|
| 76 |
+
1
|
| 77 |
+
).to(hidden.dtype)
|
| 78 |
+
cos, sin = v.rotary_pos_emb(hidden, self.positions)
|
| 79 |
+
for block in v.blocks:
|
| 80 |
+
x = block.norm1(hidden)
|
| 81 |
+
attn = block.attn
|
| 82 |
+
q, k, value = (
|
| 83 |
+
attn.qkv(x)
|
| 84 |
+
.reshape(x.shape[0], 3, attn.num_heads, -1)
|
| 85 |
+
.permute(1, 0, 2, 3)
|
| 86 |
+
.unbind(0)
|
| 87 |
+
)
|
| 88 |
+
q, k = apply_rotary_pos_emb_vision(q, k, cos, sin)
|
| 89 |
+
y = F.scaled_dot_product_attention(
|
| 90 |
+
q.transpose(0, 1).unsqueeze(0),
|
| 91 |
+
k.transpose(0, 1).unsqueeze(0),
|
| 92 |
+
value.transpose(0, 1).unsqueeze(0),
|
| 93 |
+
scale=attn.scaling,
|
| 94 |
+
)
|
| 95 |
+
hidden = hidden + attn.proj(
|
| 96 |
+
y.squeeze(0).transpose(0, 1).reshape(x.shape[0], -1)
|
| 97 |
+
)
|
| 98 |
+
hidden = hidden + block.mlp(block.norm2(hidden))
|
| 99 |
+
return v.merger(hidden).float()
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def save(model, sample, path):
|
| 103 |
+
with torch.no_grad():
|
| 104 |
+
exported = torch.export.export(model, sample, strict=False)
|
| 105 |
+
print("Decomposing", flush=True)
|
| 106 |
+
exported = exported.run_decompositions(get_decomp_table())
|
| 107 |
+
print("Converting", flush=True)
|
| 108 |
+
program = TorchConverter().add_exported_program(exported).to_coreai()
|
| 109 |
+
program.save_asset(path)
|
| 110 |
+
return exported.graph_signature.user_outputs[0]
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def main():
|
| 114 |
+
p = argparse.ArgumentParser()
|
| 115 |
+
p.add_argument("--source", default="upstream/d3-edge")
|
| 116 |
+
p.add_argument("--output", default="bundles/d3-edge-CoreAI")
|
| 117 |
+
p.add_argument("--length", type=int, default=512)
|
| 118 |
+
p.add_argument("--precision", choices=["bfloat16", "float32"], default="bfloat16")
|
| 119 |
+
a = p.parse_args()
|
| 120 |
+
torch.set_num_threads(4)
|
| 121 |
+
source = Path(a.source)
|
| 122 |
+
output = Path(a.output)
|
| 123 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 124 |
+
print("Loading backbone", flush=True)
|
| 125 |
+
backbone = Qwen3_5Model.from_pretrained(
|
| 126 |
+
source, dtype=getattr(torch, a.precision), attn_implementation="sdpa"
|
| 127 |
+
).eval()
|
| 128 |
+
model = DecisionText(
|
| 129 |
+
backbone, load_file(str(source / "readout.safetensors"))["weight"]
|
| 130 |
+
).eval()
|
| 131 |
+
hidden = backbone.config.text_config.hidden_size
|
| 132 |
+
sample = (
|
| 133 |
+
torch.ones(1, a.length, dtype=torch.int32),
|
| 134 |
+
torch.ones(1, a.length, dtype=torch.int32),
|
| 135 |
+
torch.arange(a.length, dtype=torch.int32).view(1, 1, -1).expand(4, 1, -1),
|
| 136 |
+
torch.zeros(1, a.length, hidden),
|
| 137 |
+
torch.zeros(1, a.length, 1, dtype=torch.bool),
|
| 138 |
+
)
|
| 139 |
+
print("Exporting", flush=True)
|
| 140 |
+
out = save(model, sample, output / "decision.aimodel")
|
| 141 |
+
vision = DecisionVision(backbone.visual).eval()
|
| 142 |
+
patches = (224 // backbone.config.vision_config.patch_size) ** 2
|
| 143 |
+
width = (
|
| 144 |
+
3
|
| 145 |
+
* backbone.config.vision_config.temporal_patch_size
|
| 146 |
+
* backbone.config.vision_config.patch_size**2
|
| 147 |
+
)
|
| 148 |
+
vision_out = save(vision, (torch.zeros(patches, width),), output / "vision.aimodel")
|
| 149 |
+
(output / "manifest.json").write_text(
|
| 150 |
+
json.dumps(
|
| 151 |
+
{
|
| 152 |
+
"source": source.name,
|
| 153 |
+
"sequence_length": a.length,
|
| 154 |
+
"hidden_size": hidden,
|
| 155 |
+
"inputs": [
|
| 156 |
+
"input_ids",
|
| 157 |
+
"attention_mask",
|
| 158 |
+
"position_ids",
|
| 159 |
+
"visual_embeddings",
|
| 160 |
+
"visual_mask",
|
| 161 |
+
],
|
| 162 |
+
"output": out,
|
| 163 |
+
"vision_output": vision_out,
|
| 164 |
+
"image_side": 224,
|
| 165 |
+
"precision": a.precision,
|
| 166 |
+
},
|
| 167 |
+
indent=2,
|
| 168 |
+
)
|
| 169 |
+
+ "\n"
|
| 170 |
+
)
|
| 171 |
+
print("Saved", flush=True)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
if __name__ == "__main__":
|
| 175 |
+
main()
|
runtime-smoke.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"passed": true,
|
| 3 |
+
"pytorch_neural_execution_disabled": true,
|
| 4 |
+
"result": {
|
| 5 |
+
"answers": {
|
| 6 |
+
"refund": {
|
| 7 |
+
"type": "noul",
|
| 8 |
+
"noul": 0.9714967012405396
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
}
|
| 12 |
+
}
|
serve/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# amos-serve
|
| 2 |
+
|
| 3 |
+
The serving code of the Amos 1 decision models (`amos-1-large`, `amos-1-medium`, `amos-1-small`, `amos-1-mini`): one
|
| 4 |
+
HTTP server that takes a case and typed questions and answers every question from one forward pass, each with a
|
| 5 |
+
probability. No text is generated. This is the code behind api.amos.scira.ai and the code the Decision Index runs.
|
| 6 |
+
|
| 7 |
+
Install into a fresh environment (Python 3.12 or newer, an NVIDIA GPU):
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
pip install torch==2.14.0 --index-url https://download.pytorch.org/whl/cu130
|
| 11 |
+
pip install .
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
The dependencies are pinned (torch 2.14.0, transformers 5.17.0, flash-linear-attention 0.5.2) because they are the
|
| 15 |
+
versions every published number was measured with: with them the package answers byte for byte like our own servers.
|
| 16 |
+
Other versions run, but probabilities can move by up to 0.02.
|
| 17 |
+
|
| 18 |
+
Serve a model directory (a Hugging Face checkout with `model/`, `config.json` and `calibration.json`):
|
| 19 |
+
|
| 20 |
+
```bash
|
| 21 |
+
python -m amos.serve amos-1-large --device cuda --port 8000 --rotations 1 --max-len 131072 --strict --no-state-cache
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
- `--strict` keeps question and option text exactly as sent and refuses requests over `--max-len` tokens instead of
|
| 25 |
+
truncating them.
|
| 26 |
+
- `--rotations N` averages each choice question over N option orders (1 is what the Index latency rule allows).
|
| 27 |
+
- `--no-state-cache` encodes every request from scratch.
|
| 28 |
+
- `GET /v1/info` reports the model name; `POST /v1/systemone` takes the System One request shape:
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
curl -s localhost:8000/v1/systemone -H 'content-type: application/json' -d '{
|
| 32 |
+
"state": "Invoice INV-2231 for 306.00 was charged twice this morning. Please refund one of them today.",
|
| 33 |
+
"questions": {
|
| 34 |
+
"refund": {"type": "noul", "instructions": "Is the customer asking for a refund?"},
|
| 35 |
+
"team": {"type": "choice", "instructions": "Which team should handle this?",
|
| 36 |
+
"criteria": {"billing": null, "technical": null, "sales": null}},
|
| 37 |
+
"urgency": {"type": "score", "instructions": "How urgent is this?", "criteria": ["Can wait", "This week", "Today"]}
|
| 38 |
+
}
|
| 39 |
+
}'
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
Choice questions take 2 to 255 options and are read in one pass. Photos go in `images` as base64 data URLs; every
|
| 43 |
+
question sees them. `span` (quote the case) and `region` (point at the photo) need the model's `heads.pt`.
|
| 44 |
+
|
| 45 |
+
`sources.json` lists every file with the repository commit and sha256 it was taken from. `decoder.py` and
|
| 46 |
+
`calibrate.py` are the repository's modules cut to the definitions the server reaches; the other modules are verbatim.
|
serve/amos/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Amos 1 serving code. See README.md."""
|
serve/amos/calibrate.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cut from amos/calibrate.py: only the definitions amos.serve reaches (see sources.json)."""
|
| 2 |
+
import argparse
|
| 3 |
+
import json
|
| 4 |
+
import math
|
| 5 |
+
import sys
|
| 6 |
+
from collections import defaultdict
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
SET_TYPES = ("noul", "choice", "score")
|
| 10 |
+
|
| 11 |
+
def _log(p):
|
| 12 |
+
return math.log(min(max(p, 1e-9), 1 - 1e-9))
|
| 13 |
+
|
| 14 |
+
def distribution(pred) -> list[float]:
|
| 15 |
+
return [1 - pred["p_yes"], pred["p_yes"]] if pred["type"] == "noul" else pred["probs"]
|
| 16 |
+
|
| 17 |
+
def rescale(probs, T):
|
| 18 |
+
"""Softmax(log p / T): temperature applied to the model's logits (log-probs up to a constant)."""
|
| 19 |
+
z = [_log(p) / T for p in probs]
|
| 20 |
+
m = max(z)
|
| 21 |
+
e = [math.exp(x - m) for x in z]
|
| 22 |
+
s = sum(e)
|
| 23 |
+
return [x / s for x in e]
|
| 24 |
+
|
| 25 |
+
def rescale_binary(p, T):
|
| 26 |
+
return 1 / (1 + math.exp(-(_log(p) - _log(1 - p)) / T))
|
| 27 |
+
|
| 28 |
+
def apply(pred: dict, cal: dict, alpha: float | None = None) -> dict:
|
| 29 |
+
"""Recalibrated copy of a decoded prediction; with alpha, adds the conformal set and abstain flag."""
|
| 30 |
+
typ, T = pred["type"], cal["temperature"].get(pred["type"], 1.0)
|
| 31 |
+
out = dict(pred)
|
| 32 |
+
if typ in SET_TYPES:
|
| 33 |
+
probs = rescale(distribution(pred), T)
|
| 34 |
+
if typ == "noul":
|
| 35 |
+
out["p_yes"] = probs[1]
|
| 36 |
+
else:
|
| 37 |
+
out["probs"] = probs
|
| 38 |
+
if typ == "score":
|
| 39 |
+
out["expected"] = sum(i * p for i, p in enumerate(probs))
|
| 40 |
+
if alpha is not None:
|
| 41 |
+
qhat = cal["qhat"][typ][str(alpha)]
|
| 42 |
+
out["set"] = [i for i, p in enumerate(probs) if p >= 1 - qhat]
|
| 43 |
+
out["abstain"] = len(out["set"]) != 1
|
| 44 |
+
elif typ == "multi":
|
| 45 |
+
out["probs"] = [rescale_binary(p, T) for p in pred["probs"]]
|
| 46 |
+
else:
|
| 47 |
+
out["p"] = rescale_binary(pred["p"], T)
|
| 48 |
+
return out
|
| 49 |
+
|
serve/amos/decoder.py
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cut from amos/decoder.py: only the definitions amos.serve reaches (see sources.json)."""
|
| 2 |
+
import argparse
|
| 3 |
+
import glob
|
| 4 |
+
import hashlib
|
| 5 |
+
import itertools
|
| 6 |
+
import json
|
| 7 |
+
import math
|
| 8 |
+
import os
|
| 9 |
+
import random
|
| 10 |
+
import shutil
|
| 11 |
+
import string
|
| 12 |
+
import time
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
import torch
|
| 15 |
+
|
| 16 |
+
ANSWER = "<answer>"
|
| 17 |
+
|
| 18 |
+
IMAGE_PAD = "<|image_pad|>"
|
| 19 |
+
|
| 20 |
+
LETTERS = string.ascii_uppercase + string.ascii_lowercase # the first 52 option codes; runs before amos_codes use these alone
|
| 21 |
+
|
| 22 |
+
KINDS = ("noul", "choice", "score") # answered by option letters
|
| 23 |
+
|
| 24 |
+
HEADED = ("span", "region") # answered by Heads
|
| 25 |
+
|
| 26 |
+
SPAN_LENGTH_PENALTY = 1.6
|
| 27 |
+
|
| 28 |
+
SPAN_CAP = 30
|
| 29 |
+
|
| 30 |
+
HINTS = {"score": " (levels from lowest to highest)", "span": " (quote the case)", "region": " (point to it in the image)"}
|
| 31 |
+
|
| 32 |
+
IMAGE_PIXELS = int(os.environ.get("AMOS_IMAGE_PIXELS", 1 << 20))
|
| 33 |
+
|
| 34 |
+
HEADS = "heads.pt"
|
| 35 |
+
|
| 36 |
+
REGION_KEEP = 0.5 # a point's box: cells joined to the peak holding this share of its probability (vision.peak_box)
|
| 37 |
+
|
| 38 |
+
SYSTEM = ("You make decisions about a case. Read the case, then answer every question with the letter of the option "
|
| 39 |
+
"that is most likely right.")
|
| 40 |
+
|
| 41 |
+
def options(q: dict) -> list[str]:
|
| 42 |
+
"""Option lines in answer order; yes/no is yes first, so its target is [P(yes), P(no)]."""
|
| 43 |
+
if q["type"] == "noul":
|
| 44 |
+
c = q.get("criteria") or {}
|
| 45 |
+
return [f"yes: {c['true']}" if c.get("true") else "yes", f"no: {c['false']}" if c.get("false") else "no"]
|
| 46 |
+
return list(q["options"])
|
| 47 |
+
|
| 48 |
+
def render(state: str, questions: list[dict], images=(), codes=LETTERS) -> list[dict]:
|
| 49 |
+
"""Chat messages for a case. Images (the state's after "Case:", image options where they are listed, a pick-any
|
| 50 |
+
option's image after its question) make the user message a list of text and image items; without any it is plain
|
| 51 |
+
text."""
|
| 52 |
+
parts = ["Case:\n", *images, f"{state}\n\nQuestions:\n"]
|
| 53 |
+
for i, q in enumerate(questions, start=1):
|
| 54 |
+
parts += ["\n" if i > 1 else "", f"q{i}. {q['text']}" + HINTS.get(q["type"], "")]
|
| 55 |
+
if "subject" in q:
|
| 56 |
+
parts += [" ", q["subject"]]
|
| 57 |
+
for j, o in enumerate(options(q) if q["type"] in KINDS else ()):
|
| 58 |
+
parts += [f"\n{codes[j]}) ", o]
|
| 59 |
+
merged = []
|
| 60 |
+
for x in parts:
|
| 61 |
+
if isinstance(x, str) and merged and isinstance(merged[-1], str):
|
| 62 |
+
merged[-1] += x
|
| 63 |
+
else:
|
| 64 |
+
merged.append(x)
|
| 65 |
+
user = merged[0] if len(merged) == 1 else [{"type": "text", "text": x} if isinstance(x, str) else {"type": "image", "image": x}
|
| 66 |
+
for x in merged if x != ""]
|
| 67 |
+
answers = "\n".join(f"q{i}: {ANSWER}" for i in range(1, len(questions) + 1))
|
| 68 |
+
reply = {"role": "assistant", "content": answers}
|
| 69 |
+
if len(questions) == 1 and questions[0].get("reasoning"): # a trace goes in the thinking block, as think() reads
|
| 70 |
+
reply["reasoning_content"] = questions[0]["reasoning"]
|
| 71 |
+
return [{"role": "system", "content": SYSTEM}, {"role": "user", "content": user}, reply]
|
| 72 |
+
|
| 73 |
+
def expand(questions: list[dict]) -> tuple[list[dict], list[int]]:
|
| 74 |
+
"""Pick-any questions become one yes/no per option ("subject") in the same prompt, each with its own answer ->
|
| 75 |
+
(questions, index of each one's original question)."""
|
| 76 |
+
flat, owner = [], []
|
| 77 |
+
for k, q in enumerate(questions):
|
| 78 |
+
if q["type"] != "multi":
|
| 79 |
+
flat.append(q)
|
| 80 |
+
owner.append(k)
|
| 81 |
+
continue
|
| 82 |
+
for j, o in enumerate(q["options"]):
|
| 83 |
+
flat.append({"type": "noul", "text": q["text"] + " Does this option apply?", "subject": o,
|
| 84 |
+
**({"target": q["target"][j]} if "target" in q else {})})
|
| 85 |
+
owner.append(k)
|
| 86 |
+
return flat, owner
|
| 87 |
+
|
| 88 |
+
PLAIN_TEMPLATE = ("{% for m in messages %}{{ m['role'] }}:\n{% if m['content'] is string %}{{ m['content'] }}{% else %}"
|
| 89 |
+
"{% for x in m['content'] %}{{ x['text'] if x['type'] == 'text' else '' }}{% endfor %}{% endif %}"
|
| 90 |
+
"\n\n{% endfor %}")
|
| 91 |
+
|
| 92 |
+
def option_codes(tokenizer, n: int) -> list[str]:
|
| 93 |
+
"""The first n option codes: the 52 letters, then two-letter codes the tokenizer keeps as one token."""
|
| 94 |
+
two = ("".join(p) for p in itertools.product(string.ascii_uppercase, repeat=2))
|
| 95 |
+
codes = list(LETTERS)[:n] + [c for c in two if len(tokenizer.encode(c, add_special_tokens=False)) == 1][:max(0, n - 52)]
|
| 96 |
+
if len(codes) != n:
|
| 97 |
+
raise ValueError(f"the tokenizer has only {len(codes)} one-token option codes, not {n}")
|
| 98 |
+
return codes
|
| 99 |
+
|
| 100 |
+
class Codec:
|
| 101 |
+
def __init__(self, tokenizer, image_processor=None, n_codes: int = len(LETTERS)):
|
| 102 |
+
self.tok, self.image_processor = tokenizer, image_processor
|
| 103 |
+
if getattr(tokenizer, "chat_template", "") is None:
|
| 104 |
+
tokenizer.chat_template = PLAIN_TEMPLATE
|
| 105 |
+
if ANSWER not in tokenizer.get_added_vocab():
|
| 106 |
+
tokenizer.add_special_tokens({"additional_special_tokens": [ANSWER]})
|
| 107 |
+
self.marker = tokenizer.convert_tokens_to_ids(ANSWER)
|
| 108 |
+
self.codes = option_codes(tokenizer, n_codes)
|
| 109 |
+
ids = [tokenizer.encode(x, add_special_tokens=False) for x in self.codes]
|
| 110 |
+
if any(len(i) != 1 for i in ids):
|
| 111 |
+
raise ValueError("every option letter must be one token")
|
| 112 |
+
self.letters = [i[0] for i in ids]
|
| 113 |
+
self.pad = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id
|
| 114 |
+
# A unified model (Gemma 4) comes with its whole processor, which turns each image placeholder into its own run
|
| 115 |
+
# of tokens; Qwen comes with an image processor alone, and the placeholder is expanded here
|
| 116 |
+
self.unified = image_processor is not None and hasattr(image_processor, "image_token")
|
| 117 |
+
pad = image_processor.image_token if self.unified else IMAGE_PAD
|
| 118 |
+
self.image_pad = tokenizer.convert_tokens_to_ids(pad) if image_processor is not None else None
|
| 119 |
+
self.echo = False # set by load_heads when the quote head was trained on an echoed case (see encode_case)
|
| 120 |
+
# the tokenizer's added tokens (the answer marker, chat roles, <think>, image placeholders) as user text would
|
| 121 |
+
# tokenize as the tokens themselves and break the prompt; defuse() swaps their "<" for a look-alike
|
| 122 |
+
self.specials = sorted((t for t in tokenizer.get_added_vocab() if t.startswith("<")), key=len, reverse=True)
|
| 123 |
+
|
| 124 |
+
def defuse(self, text):
|
| 125 |
+
"""User text with any added-token string made ordinary text; same length, so character offsets still hold."""
|
| 126 |
+
if not isinstance(text, str) or "<" not in text:
|
| 127 |
+
return text
|
| 128 |
+
for t in self.specials:
|
| 129 |
+
text = text.replace(t, "‹" + t[1:])
|
| 130 |
+
return text
|
| 131 |
+
|
| 132 |
+
def safe(self, state: str, questions: list[dict]) -> tuple[str, list[dict]]:
|
| 133 |
+
"""state and questions with every user-written string defused (images and other values pass through)."""
|
| 134 |
+
d = self.defuse
|
| 135 |
+
qs = [{**q, **{k: d(q[k]) for k in ("text", "subject") if k in q},
|
| 136 |
+
**({"options": [d(o) for o in q["options"]]} if "options" in q else {}),
|
| 137 |
+
**({"criteria": {k: d(v) for k, v in q["criteria"].items()}} if isinstance(q.get("criteria"), dict) else {})}
|
| 138 |
+
for q in questions]
|
| 139 |
+
return d(state), qs
|
| 140 |
+
|
| 141 |
+
def encode(self, state: str, questions: list[dict]) -> tuple[list[int], list[int]]:
|
| 142 |
+
"""-> (token ids, the position whose next-token logits answer each question)."""
|
| 143 |
+
state, questions = self.safe(state, questions)
|
| 144 |
+
text = self.tok.apply_chat_template(render(state, questions, codes=self.codes), tokenize=False,
|
| 145 |
+
enable_thinking=False)
|
| 146 |
+
ids = self.tok(text, add_special_tokens=False)["input_ids"]
|
| 147 |
+
at = [i - 1 for i, t in enumerate(ids) if t == self.marker]
|
| 148 |
+
if len(at) != len(questions):
|
| 149 |
+
raise ValueError("one answer marker per question")
|
| 150 |
+
return ids, at
|
| 151 |
+
|
| 152 |
+
def reason_span(self, ids: list[int]) -> tuple[int, int]:
|
| 153 |
+
"""The tokens inside the thinking block, (first, end): the trace a reasoning record teaches the model to write."""
|
| 154 |
+
start, end = (self.tok.convert_tokens_to_ids(t) for t in ("<think>", "</think>"))
|
| 155 |
+
return ids.index(start) + 1, ids.index(end)
|
| 156 |
+
|
| 157 |
+
def encode_case(self, state: str, questions: list[dict], images=()) -> tuple[list[int], list[int], dict]:
|
| 158 |
+
"""-> (token ids, answer positions, extra); without images the ids are exactly encode()'s. extra["state"] is
|
| 159 |
+
(first state token, each state token's char offsets in the state). With images extra also holds the
|
| 160 |
+
image processor's pixel_values and image_grid_thw, and "images": (first token, rows, cols) of each image's
|
| 161 |
+
merged token grid; each image placeholder is expanded to its token count, as the model's processor does."""
|
| 162 |
+
state, questions = self.safe(state, questions)
|
| 163 |
+
messages = render(state, questions, images, self.codes)
|
| 164 |
+
user = messages[1]["content"]
|
| 165 |
+
pics = [] if isinstance(user, str) else [x["image"] for x in user if x["type"] == "image"]
|
| 166 |
+
text = self.tok.apply_chat_template(messages, tokenize=False, enable_thinking=False)
|
| 167 |
+
enc = self.tok(text, add_special_tokens=False, return_offsets_mapping=True)
|
| 168 |
+
ids, spans = enc["input_ids"], enc["offset_mapping"]
|
| 169 |
+
# an empty state has no text to find (and Gemma's template trims the newlines that would surround it)
|
| 170 |
+
s0 = text.index(state + "\n\nQuestions:\n") if state else 0
|
| 171 |
+
s1 = s0 + len(state)
|
| 172 |
+
toks = [i for i, (a, b) in enumerate(spans) if a < s1 and b > s0]
|
| 173 |
+
offsets = [(max(spans[i][0], s0) - s0, min(spans[i][1], s1) - s0) for i in toks]
|
| 174 |
+
where, extra = list(range(len(ids))), {}
|
| 175 |
+
if pics and self.unified:
|
| 176 |
+
ids, where, extra = self._unified_images(text, ids, pics)
|
| 177 |
+
elif pics:
|
| 178 |
+
if self.image_processor is None:
|
| 179 |
+
raise ValueError("this model has no vision tower")
|
| 180 |
+
ip = self.image_processor
|
| 181 |
+
v = ip(images=[p.convert("RGB") for p in pics], max_pixels=IMAGE_PIXELS, return_tensors="pt")
|
| 182 |
+
grid, m = v["image_grid_thw"], ip.merge_size
|
| 183 |
+
counts, out, where, starts = iter((grid.prod(-1) // m**2).tolist()), [], [], []
|
| 184 |
+
for t in ids:
|
| 185 |
+
where.append(len(out))
|
| 186 |
+
if t == self.image_pad:
|
| 187 |
+
starts.append(len(out))
|
| 188 |
+
out += [t] * next(counts)
|
| 189 |
+
else:
|
| 190 |
+
out.append(t)
|
| 191 |
+
if len(starts) != len(pics):
|
| 192 |
+
raise ValueError("one image placeholder per image")
|
| 193 |
+
ids = out
|
| 194 |
+
extra = {"pixel_values": v["pixel_values"], "image_grid_thw": grid,
|
| 195 |
+
"images": [(s, int(g[1]) // m, int(g[2]) // m) for s, g in zip(starts, grid)]}
|
| 196 |
+
extra["state"] = (where[toks[0]] if toks else 0, offsets)
|
| 197 |
+
at = [i - 1 for i, t in enumerate(ids) if t == self.marker]
|
| 198 |
+
if len(at) != len(questions):
|
| 199 |
+
raise ValueError("one answer marker per question")
|
| 200 |
+
if self.echo and toks and any(q["type"] == "span" for q in questions):
|
| 201 |
+
# Echo: the case again after every answer marker, and quotes are read from this copy. Reading left to
|
| 202 |
+
# right, a case token cannot see what follows it ("Total due" after a line item), but every copied token
|
| 203 |
+
# has seen the whole case and each question. Attention only looks back, so no earlier answer changes.
|
| 204 |
+
sep = self.tok("\n\nThe case again:\n", add_special_tokens=False)["input_ids"]
|
| 205 |
+
copy = self.tok(state, add_special_tokens=False, return_offsets_mapping=True)
|
| 206 |
+
extra["state"] = (len(ids) + len(sep), [tuple(o) for o in copy["offset_mapping"]])
|
| 207 |
+
ids = ids + sep + copy["input_ids"]
|
| 208 |
+
return ids, at, extra
|
| 209 |
+
|
| 210 |
+
def _unified_images(self, text: str, ids: list[int], pics) -> tuple[list[int], list[int], dict]:
|
| 211 |
+
"""A unified model's processor expands each image placeholder into <|image>, its image tokens, <image|>; ->
|
| 212 |
+
(expanded ids, each original token's new position, the model's image inputs). The images have no separate
|
| 213 |
+
grid of merged tokens here, so "images" is empty and the point and box heads do not apply."""
|
| 214 |
+
ip = self.image_processor
|
| 215 |
+
v = ip(text=text, images=[p.convert("RGB") for p in pics], return_tensors="pt", add_special_tokens=False)
|
| 216 |
+
new = v["input_ids"][0].tolist()
|
| 217 |
+
run = {self.image_pad, self.tok.convert_tokens_to_ids(ip.boi_token), self.tok.convert_tokens_to_ids(ip.eoi_token)}
|
| 218 |
+
where, j = [], 0
|
| 219 |
+
for t in ids:
|
| 220 |
+
where.append(j)
|
| 221 |
+
if t == self.image_pad:
|
| 222 |
+
while j < len(new) and new[j] in run:
|
| 223 |
+
j += 1
|
| 224 |
+
elif j < len(new) and new[j] == t:
|
| 225 |
+
j += 1
|
| 226 |
+
else:
|
| 227 |
+
raise ValueError("the processor changed a text token")
|
| 228 |
+
if j != len(new):
|
| 229 |
+
raise ValueError("the processor added tokens after the text")
|
| 230 |
+
return new, where, {"pixel_values": v["pixel_values"], "image_position_ids": v["image_position_ids"],
|
| 231 |
+
"mm_token_type_ids": v["mm_token_type_ids"][0], "images": []}
|
| 232 |
+
|
| 233 |
+
def load_model(path: str, device: str, *, dtype=None, vision=None):
|
| 234 |
+
"""-> (model, codec). vision=None loads the image-text model when the checkpoint has a vision tower and an image
|
| 235 |
+
processor config beside it, else the text model alone. Qwen's image processor runs on PIL, never torchvision
|
| 236 |
+
(not installed, and the Auto classes demand it), so training and serving resize alike on every machine."""
|
| 237 |
+
from transformers import (
|
| 238 |
+
AutoConfig,
|
| 239 |
+
AutoModelForCausalLM,
|
| 240 |
+
AutoModelForImageTextToText,
|
| 241 |
+
AutoTokenizer,
|
| 242 |
+
Qwen2VLImageProcessorPil,
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
if device == "cuda": # as in amos.train: cuDNN attention fails to load in bf16 with the pip CUDA wheels
|
| 246 |
+
torch.backends.cuda.enable_cudnn_sdp(False)
|
| 247 |
+
|
| 248 |
+
tok = AutoTokenizer.from_pretrained(path)
|
| 249 |
+
config = AutoConfig.from_pretrained(path)
|
| 250 |
+
unified = config.model_type.startswith("gemma4") # images through its own processor (which needs torchvision)
|
| 251 |
+
if vision is None:
|
| 252 |
+
vision = hasattr(config, "vision_config") and (Path(path) / ("processor_config.json" if unified else
|
| 253 |
+
"preprocessor_config.json")).exists()
|
| 254 |
+
processor = None
|
| 255 |
+
if vision:
|
| 256 |
+
from transformers import AutoProcessor
|
| 257 |
+
|
| 258 |
+
processor = AutoProcessor.from_pretrained(path) if unified else Qwen2VLImageProcessorPil.from_pretrained(path)
|
| 259 |
+
codec = Codec(tok, processor, getattr(config, "amos_codes", len(LETTERS)))
|
| 260 |
+
if dtype is None:
|
| 261 |
+
dtype = torch.float32 if getattr(config, "amos_export_dtype", None) == "float32" else torch.bfloat16
|
| 262 |
+
from transformers.models.auto.modeling_auto import MODEL_FOR_CAUSAL_LM_MAPPING_NAMES
|
| 263 |
+
|
| 264 |
+
# some image-text models (Muse Glimmer) have no causal-LM class: load the whole model, its vision tower unused
|
| 265 |
+
causal = config.model_type in MODEL_FOR_CAUSAL_LM_MAPPING_NAMES
|
| 266 |
+
model = (AutoModelForImageTextToText if vision or not causal else AutoModelForCausalLM).from_pretrained(
|
| 267 |
+
path, config=config, dtype=dtype)
|
| 268 |
+
if vision and not unified:
|
| 269 |
+
patchify_as_matmul(model.model.visual.patch_embed)
|
| 270 |
+
if model.get_input_embeddings().weight.shape[0] < len(tok):
|
| 271 |
+
model.resize_token_embeddings(len(tok), mean_resizing=True)
|
| 272 |
+
return model.to(device), codec
|
| 273 |
+
|
| 274 |
+
def patchify_as_matmul(patch_embed):
|
| 275 |
+
"""Qwen's patch embedding is a conv3d whose kernel is the whole flattened patch, so it is one matmul with the same
|
| 276 |
+
weights; run it as one. cuDNN's conv3d fails to load on some hosts and PyTorch's own takes ~5 s per photo."""
|
| 277 |
+
proj = patch_embed.proj
|
| 278 |
+
patch_embed.forward = lambda x: torch.nn.functional.linear(x.to(proj.weight.dtype), proj.weight.flatten(1), proj.bias)
|
| 279 |
+
|
| 280 |
+
def noncausal(model) -> bool:
|
| 281 |
+
"""Whether the run was trained with bidirectional full attention (train --noncausal), so it must be read that way."""
|
| 282 |
+
return bool(getattr(model.config, "amos_noncausal", False))
|
| 283 |
+
|
| 284 |
+
def bidirectional_mask(lengths: list[int], width: int, device) -> torch.Tensor:
|
| 285 |
+
"""[B, 1, L, L], True where the key is one of its row's real tokens: every query sees the whole case, never pads."""
|
| 286 |
+
keys = torch.arange(width, device=device) < torch.tensor(lengths, device=device)[:, None]
|
| 287 |
+
return keys[:, None, None, :].expand(-1, 1, width, -1)
|
| 288 |
+
|
| 289 |
+
PREFILL_CHUNK = 16384
|
| 290 |
+
|
| 291 |
+
def chunked_hidden(decoder, x: torch.Tensor) -> torch.Tensor:
|
| 292 |
+
"""Last hidden states [1, L, d] for one causal text row, read PREFILL_CHUNK tokens at a time."""
|
| 293 |
+
from transformers import DynamicCache
|
| 294 |
+
|
| 295 |
+
cache, out = DynamicCache(config=decoder.config), []
|
| 296 |
+
for s in range(0, x.shape[1], PREFILL_CHUNK):
|
| 297 |
+
out.append(decoder(input_ids=x[:, s : s + PREFILL_CHUNK], past_key_values=cache, use_cache=True).last_hidden_state)
|
| 298 |
+
del cache
|
| 299 |
+
return torch.cat(out, 1)
|
| 300 |
+
|
| 301 |
+
def layerwise_hidden(decoder, x: torch.Tensor) -> torch.Tensor:
|
| 302 |
+
"""Last hidden states [1, L, d] for one noncausal text row, read a layer at a time.
|
| 303 |
+
|
| 304 |
+
A noncausal run's full-attention layers let every token see the whole case, so it cannot be read in chunks the way
|
| 305 |
+
a causal one can. Instead the whole case goes through each layer before the next, PREFILL_CHUNK tokens at a time:
|
| 306 |
+
a linear-attention layer is a forward recurrence, so its chunks carry its conv and recurrent state; a full-attention
|
| 307 |
+
layer first takes keys and values for every token, then each chunk of queries attends to all of them. The same
|
| 308 |
+
computation as one pass, holding one layer's activations for one chunk instead of every layer's for the whole case."""
|
| 309 |
+
import torch.nn.functional as F
|
| 310 |
+
from transformers import DynamicCache
|
| 311 |
+
from transformers.models.qwen3_5.modeling_qwen3_5 import apply_rotary_pos_emb
|
| 312 |
+
|
| 313 |
+
n, C = x.shape[1], PREFILL_CHUNK
|
| 314 |
+
h = decoder.embed_tokens(x)
|
| 315 |
+
pos = torch.arange(n, device=x.device).view(1, 1, -1).expand(4, 1, -1)
|
| 316 |
+
cos, sin = decoder.rotary_emb(h, pos[1:])
|
| 317 |
+
text_pos, cache = pos[0], DynamicCache(config=decoder.config)
|
| 318 |
+
for i, layer in enumerate(decoder.layers[: decoder.config.num_hidden_layers]):
|
| 319 |
+
if decoder.config.layer_types[i] == "linear_attention":
|
| 320 |
+
for s in range(0, n, C): # each chunk is overwritten after it is read; later chunks are still this layer's input
|
| 321 |
+
h[:, s : s + C] = layer(h[:, s : s + C], position_embeddings=(cos[:, s : s + C], sin[:, s : s + C]),
|
| 322 |
+
attention_mask=None, position_ids=text_pos[:, s : s + C], past_key_values=cache)
|
| 323 |
+
continue
|
| 324 |
+
at = layer.self_attn
|
| 325 |
+
keys, values = [], []
|
| 326 |
+
for s in range(0, n, C):
|
| 327 |
+
hn = layer.input_layernorm(h[:, s : s + C])
|
| 328 |
+
shape = (*hn.shape[:-1], -1, at.head_dim)
|
| 329 |
+
k = at.k_norm(at.k_proj(hn).view(shape)).transpose(1, 2)
|
| 330 |
+
_, k = apply_rotary_pos_emb(k, k, cos[:, s : s + C], sin[:, s : s + C])
|
| 331 |
+
keys.append(k)
|
| 332 |
+
values.append(at.v_proj(hn).view(shape).transpose(1, 2))
|
| 333 |
+
keys, values = torch.cat(keys, 2), torch.cat(values, 2)
|
| 334 |
+
for s in range(0, n, C):
|
| 335 |
+
res = h[:, s : s + C]
|
| 336 |
+
hn = layer.input_layernorm(res)
|
| 337 |
+
shape = hn.shape[:-1]
|
| 338 |
+
q, gate = torch.chunk(at.q_proj(hn).view(*shape, -1, at.head_dim * 2), 2, dim=-1)
|
| 339 |
+
q = at.q_norm(q).transpose(1, 2)
|
| 340 |
+
q, _ = apply_rotary_pos_emb(q, q, cos[:, s : s + C], sin[:, s : s + C])
|
| 341 |
+
o = F.scaled_dot_product_attention(q, keys, values, scale=at.scaling, enable_gqa=True)
|
| 342 |
+
o = at.o_proj(o.transpose(1, 2).reshape(*shape, -1) * torch.sigmoid(gate.reshape(*shape, -1)))
|
| 343 |
+
mid = res + o
|
| 344 |
+
h[:, s : s + C] = mid + layer.mlp(layer.post_attention_layernorm(mid))
|
| 345 |
+
del keys, values
|
| 346 |
+
del cache
|
| 347 |
+
for s in range(0, n, C):
|
| 348 |
+
h[:, s : s + C] = decoder.norm(h[:, s : s + C])
|
| 349 |
+
return h
|
| 350 |
+
|
| 351 |
+
def hidden(model, codec, batch: list[tuple]) -> torch.Tensor:
|
| 352 |
+
"""Last hidden states [B, L, d] for a batch of (ids, answer positions, questions[, extra]).
|
| 353 |
+
|
| 354 |
+
Sequences are right-padded without a mask: the model is causal (its linear-attention layers are recurrences that
|
| 355 |
+
only run forward), so padding after a sequence cannot change anything read from inside it. A noncausal run's
|
| 356 |
+
full-attention layers see the whole sequence, so they get a mask hiding the pads; the recurrences stay causal.
|
| 357 |
+
Entries with images go through the image-text model, which fills image tokens from the vision tower and gives
|
| 358 |
+
them 2-D positions."""
|
| 359 |
+
width = max(len(e[0]) for e in batch)
|
| 360 |
+
x = torch.full((len(batch), width), codec.pad, dtype=torch.long, device=model.device)
|
| 361 |
+
for b, e in enumerate(batch):
|
| 362 |
+
x[b, : len(e[0])] = torch.tensor(e[0], device=model.device)
|
| 363 |
+
vis = [e[3] for e in batch if len(e) > 3 and "pixel_values" in e[3]]
|
| 364 |
+
kw = {} if not noncausal(model) else {"attention_mask": {
|
| 365 |
+
"full_attention": bidirectional_mask([len(e[0]) for e in batch], width, x.device), "linear_attention": None}}
|
| 366 |
+
if not vis and len(batch) == 1 and width > PREFILL_CHUNK:
|
| 367 |
+
return layerwise_hidden(model.get_decoder(), x) if kw else chunked_hidden(model.get_decoder(), x)
|
| 368 |
+
if not vis:
|
| 369 |
+
return model.get_decoder()(input_ids=x, **kw).last_hidden_state
|
| 370 |
+
if codec.unified and kw:
|
| 371 |
+
raise ValueError("noncausal attention is built for Qwen3.5's layer types, not Gemma 4's")
|
| 372 |
+
if codec.unified: # Gemma 4: image patches go through the same layers, placed by their token types and positions
|
| 373 |
+
types = torch.zeros_like(x)
|
| 374 |
+
for b, e in enumerate(batch):
|
| 375 |
+
if len(e) > 3 and "mm_token_type_ids" in e[3]:
|
| 376 |
+
types[b, : len(e[3]["mm_token_type_ids"])] = e[3]["mm_token_type_ids"].to(x.device)
|
| 377 |
+
return model.model(input_ids=x, mm_token_type_ids=types,
|
| 378 |
+
pixel_values=torch.cat([v["pixel_values"] for v in vis]).to(model.device, model.dtype),
|
| 379 |
+
image_position_ids=torch.cat([v["image_position_ids"] for v in vis]).to(model.device)
|
| 380 |
+
).last_hidden_state
|
| 381 |
+
grid, types = torch.cat([v["image_grid_thw"] for v in vis]).to(model.device), (x == codec.image_pad).int()
|
| 382 |
+
if kw: # its rope index would read a dict mask as per-row padding, so the positions come unmasked, as causal
|
| 383 |
+
kw["position_ids"] = model.model.get_rope_index(x, types, image_grid_thw=grid)[0]
|
| 384 |
+
return model.model(input_ids=x, pixel_values=torch.cat([v["pixel_values"] for v in vis]).to(model.device),
|
| 385 |
+
image_grid_thw=grid, mm_token_type_ids=types, **kw).last_hidden_state
|
| 386 |
+
|
| 387 |
+
def finish_logits(model, logits: torch.Tensor) -> torch.Tensor:
|
| 388 |
+
"""The model's own last step from raw output-layer logits, as its forward would apply it: Granite divides by
|
| 389 |
+
logits_scaling, Gemma caps with final_logit_softcapping. Qwen has neither, so its logits pass unchanged."""
|
| 390 |
+
cfg = getattr(model.config, "text_config", None) or model.config
|
| 391 |
+
if scaling := getattr(cfg, "logits_scaling", None):
|
| 392 |
+
logits = logits / scaling
|
| 393 |
+
if cap := getattr(cfg, "final_logit_softcapping", None):
|
| 394 |
+
logits = cap * torch.tanh(logits / cap)
|
| 395 |
+
return logits
|
| 396 |
+
|
| 397 |
+
def distributions(model, codec, batch: list[tuple], h: torch.Tensor | None = None) -> list[torch.Tensor]:
|
| 398 |
+
"""Log-probabilities over the option letters of each yes/no, choice and score question in a batch of (ids, answer
|
| 399 |
+
positions, questions[, extra]), in order. Only the 52 letter logits are computed, from the tied output
|
| 400 |
+
embeddings."""
|
| 401 |
+
h = hidden(model, codec, batch) if h is None else h
|
| 402 |
+
picks = [(b, p, q) for b, e in enumerate(batch) for p, q in zip(e[1], e[2]) if q["type"] in KINDS]
|
| 403 |
+
if not picks:
|
| 404 |
+
return []
|
| 405 |
+
rows = torch.tensor([b for b, _, _ in picks], device=h.device)
|
| 406 |
+
cols = torch.tensor([p for _, p, _ in picks], device=h.device)
|
| 407 |
+
logits = finish_logits(model, h[rows, cols].float() @ model.get_output_embeddings().weight[codec.letters].float().T)
|
| 408 |
+
return [logits[k, : len(options(q))].log_softmax(-1) for k, (_, _, q) in enumerate(picks)]
|
| 409 |
+
|
| 410 |
+
class BoxHead(torch.nn.Module):
|
| 411 |
+
"""A point's box, predicted rather than grown from cells: reads the question's vector, the image features pooled
|
| 412 |
+
by the point head's own cell distribution, and that distribution's centre and spread, and moves the centre and
|
| 413 |
+
sets the size. Cells alone draw a box around only the few cells the point head is sure of (a car's tail light
|
| 414 |
+
rather than the car); this head is trained on overlap with the whole object."""
|
| 415 |
+
|
| 416 |
+
def __init__(self, d: int, width: int = 512):
|
| 417 |
+
super().__init__()
|
| 418 |
+
self.net = torch.nn.Sequential(torch.nn.Linear(2 * d + 4, width), torch.nn.GELU(), torch.nn.Linear(width, 4))
|
| 419 |
+
torch.nn.init.zeros_(self.net[-1].weight) # starts as a half-image box on the cells' centre
|
| 420 |
+
torch.nn.init.zeros_(self.net[-1].bias)
|
| 421 |
+
|
| 422 |
+
def forward(self, hq: torch.Tensor, cells: torch.Tensor, logits: torch.Tensor, rows: int, cols: int) -> torch.Tensor:
|
| 423 |
+
p = logits.detach().float().softmax(-1) # the point head stays as it is; only the box learns
|
| 424 |
+
ys, xs = torch.meshgrid((torch.arange(rows, device=p.device) + 0.5) / rows,
|
| 425 |
+
(torch.arange(cols, device=p.device) + 0.5) / cols, indexing="ij")
|
| 426 |
+
xs, ys = xs.flatten(), ys.flatten()
|
| 427 |
+
mx, my = (p * xs).sum(), (p * ys).sum()
|
| 428 |
+
sx, sy = (p * (xs - mx) ** 2).sum().sqrt(), (p * (ys - my) ** 2).sum().sqrt()
|
| 429 |
+
pooled = (p[:, None] * cells.float()).sum(0)
|
| 430 |
+
o = self.net(torch.cat([hq.float(), pooled, torch.stack([mx, my, sx, sy])]).to(self.net[0].weight.dtype)).float()
|
| 431 |
+
cx, cy = (mx + 0.5 * torch.tanh(o[0])).clamp(0, 1), (my + 0.5 * torch.tanh(o[1])).clamp(0, 1)
|
| 432 |
+
w, hgt = torch.sigmoid(o[2]), torch.sigmoid(o[3])
|
| 433 |
+
return torch.stack([cx - w / 2, cy - hgt / 2, cx + w / 2, cy + hgt / 2]).clamp(0, 1)
|
| 434 |
+
|
| 435 |
+
class Heads(torch.nn.Module):
|
| 436 |
+
"""Quote and point heads, as amos.model has them: a question's answer-position vector scored against the state's
|
| 437 |
+
tokens (start and end of a quote) or against the first state image's tokens (one cell each); `keep` is the
|
| 438 |
+
region readout's share (decode_head). With a BoxHead, a region's logits carry four more values: its box."""
|
| 439 |
+
|
| 440 |
+
def __init__(self, d: int, kinds=HEADED, keep: float = REGION_KEEP, box: bool = False):
|
| 441 |
+
super().__init__()
|
| 442 |
+
self.kinds, self.keep = list(kinds), keep
|
| 443 |
+
self.span = (SPAN_CAP, SPAN_LENGTH_PENALTY) # the quote decoder's cap and penalty; serving sets the run's own
|
| 444 |
+
self.span_q, self.span_k = torch.nn.Linear(d, 2 * d), torch.nn.Linear(d, 2 * d)
|
| 445 |
+
self.region_q, self.region_k = torch.nn.Linear(d, d), torch.nn.Linear(d, d)
|
| 446 |
+
for m in (self.span_q, self.span_k, self.region_q, self.region_k):
|
| 447 |
+
torch.nn.init.normal_(m.weight, std=0.02)
|
| 448 |
+
torch.nn.init.zeros_(m.bias)
|
| 449 |
+
self.box = BoxHead(d) if box else None
|
| 450 |
+
|
| 451 |
+
def forward(self, h: torch.Tensor, batch: list[tuple]) -> list[torch.Tensor]:
|
| 452 |
+
"""Logits of each span ([2, state tokens]) and region ([cells], then the 4 box edges when there is a box head)
|
| 453 |
+
question in the batch, in order."""
|
| 454 |
+
h, d, out = h.to(self.span_q.weight.dtype), h.shape[-1], []
|
| 455 |
+
for b, e in enumerate(batch):
|
| 456 |
+
for p, q in zip(e[1], e[2]):
|
| 457 |
+
if q["type"] == "span":
|
| 458 |
+
first, offsets = e[3]["state"]
|
| 459 |
+
k = self.span_k(h[b, first : first + len(offsets)]).view(-1, 2, d)
|
| 460 |
+
out.append(torch.einsum("cd,lcd->cl", self.span_q(h[b, p]).view(2, d), k) / math.sqrt(d))
|
| 461 |
+
elif q["type"] == "region":
|
| 462 |
+
if not e[3].get("images"):
|
| 463 |
+
raise ValueError("a region question needs a state image")
|
| 464 |
+
start, rows, cols = e[3]["images"][0]
|
| 465 |
+
cells = h[b, start : start + rows * cols]
|
| 466 |
+
x = self.region_k(cells) @ self.region_q(h[b, p]) / math.sqrt(d)
|
| 467 |
+
if self.box is not None:
|
| 468 |
+
x = torch.cat([x, self.box(h[b, p], cells, x, rows, cols).to(x.dtype)])
|
| 469 |
+
out.append(x)
|
| 470 |
+
return out
|
| 471 |
+
|
| 472 |
+
def decode_head(q: dict, x: torch.Tensor, extra: dict, state: str, keep: float = REGION_KEEP,
|
| 473 |
+
span: tuple[int, float] = (SPAN_CAP, SPAN_LENGTH_PENALTY)) -> dict:
|
| 474 |
+
"""A span or region result in amos.model.decode's format; a quote is always cut from the state, at most span[0]
|
| 475 |
+
tokens long, each token past the start scaled by exp(-span[1]). A point is the most likely cell and the box of the
|
| 476 |
+
cells around it holding `keep` of its probability, with the box's mass."""
|
| 477 |
+
from types import SimpleNamespace
|
| 478 |
+
|
| 479 |
+
from .model import decode_span
|
| 480 |
+
from .vision import peak_box
|
| 481 |
+
|
| 482 |
+
x = x.float().cpu()
|
| 483 |
+
if q["type"] == "span":
|
| 484 |
+
x = torch.cat([x.new_full((2, 1), -torch.inf), x], 1) # decode_span skips a leading [CLS] slot
|
| 485 |
+
a, z, _, p = decode_span(x[0], x[1], SimpleNamespace(offsets=extra["state"][1], state=state), span[1], span[0])
|
| 486 |
+
# Qwen fuses a JSON string's closing quote with the last word ('detail."', 'approves.",'), so a quote cut at
|
| 487 |
+
# token edges can end (or start) on an unmatched quote mark; drop it and what follows it
|
| 488 |
+
while state[a:z].count('"') % 2:
|
| 489 |
+
if state[z - 1] in '",}]':
|
| 490 |
+
z -= 1
|
| 491 |
+
elif state[a] in '"{[':
|
| 492 |
+
a += 1
|
| 493 |
+
else:
|
| 494 |
+
break
|
| 495 |
+
t = state[a:z]
|
| 496 |
+
a, z = a + len(t) - len(t.lstrip()), z - (len(t) - len(t.rstrip()))
|
| 497 |
+
return {"type": "span", "start": a, "end": max(a, z), "text": state[a : max(a, z)], "p": p}
|
| 498 |
+
_, rows, cols = extra["images"][0]
|
| 499 |
+
cells = x[: rows * cols].softmax(-1)
|
| 500 |
+
if len(x) > rows * cols: # a box head's box; its mass is the cells it covers
|
| 501 |
+
box = [round(v, 4) for v in x[rows * cols :].tolist()]
|
| 502 |
+
r0, r1 = int(box[1] * rows), max(int(box[1] * rows) + 1, math.ceil(box[3] * rows))
|
| 503 |
+
c0, c1 = int(box[0] * cols), max(int(box[0] * cols) + 1, math.ceil(box[2] * cols))
|
| 504 |
+
return {"type": "region", "cell": int(cells.argmax()), "box": box,
|
| 505 |
+
"p": float(cells.view(rows, cols)[r0:r1, c0:c1].sum())}
|
| 506 |
+
box, p = peak_box(cells, rows, cols, keep)
|
| 507 |
+
return {"type": "region", "cell": int(cells.argmax()), "box": box, "p": p}
|
| 508 |
+
|
| 509 |
+
def load_heads(path, model, codec: Codec | None = None) -> Heads | None:
|
| 510 |
+
"""The trained heads saved beside a checkpoint, or None. A quote head trained on echoed cases switches the codec's
|
| 511 |
+
echo on, since it reads nothing else."""
|
| 512 |
+
f = Path(path) / HEADS
|
| 513 |
+
if not f.exists():
|
| 514 |
+
return None
|
| 515 |
+
saved = torch.load(f, map_location="cpu", weights_only=True)
|
| 516 |
+
heads = Heads(model.get_input_embeddings().weight.shape[1], saved["kinds"], saved.get("keep", REGION_KEEP),
|
| 517 |
+
box=any(k.startswith("box.") for k in saved["state"]))
|
| 518 |
+
heads.load_state_dict(saved["state"])
|
| 519 |
+
if codec is not None:
|
| 520 |
+
codec.echo = bool(saved.get("echo", False))
|
| 521 |
+
return heads.to(model.device).eval()
|
| 522 |
+
|
| 523 |
+
def tempered(lp: torch.Tensor, t: float | None = None) -> list[float]:
|
| 524 |
+
"""Probabilities from log-probabilities, sharpened (t < 1) or softened (t > 1) by a fitted temperature."""
|
| 525 |
+
return (lp / t).softmax(-1).tolist() if t else lp.exp().tolist()
|
| 526 |
+
|
| 527 |
+
def rotate(q: dict, r: int) -> dict:
|
| 528 |
+
"""A choice question with its options rotated r places; anything else as it is (a scale's order is its meaning)."""
|
| 529 |
+
if q["type"] != "choice" or not r:
|
| 530 |
+
return q
|
| 531 |
+
s = r % len(q["options"])
|
| 532 |
+
return {**q, "options": q["options"][s:] + q["options"][:s]}
|
| 533 |
+
|
| 534 |
+
def read(model, codec, state: str, questions: list[dict], images=(), max_len: int | None = None,
|
| 535 |
+
temperature: dict | None = None):
|
| 536 |
+
"""-> (option distributions for letter-answered questions, input tokens), one forward pass."""
|
| 537 |
+
ids, at, extra = codec.encode_case(state, questions, images)
|
| 538 |
+
if max_len and len(ids) > max_len:
|
| 539 |
+
raise ValueError(f"maximum context length: request has {len(ids)} tokens, limit {max_len}")
|
| 540 |
+
entry = [(ids, at, questions, extra)]
|
| 541 |
+
t = temperature or {}
|
| 542 |
+
picked = [q for q in questions if q["type"] in KINDS]
|
| 543 |
+
return [tempered(lp, t.get(q["type"])) for lp, q in
|
| 544 |
+
zip(distributions(model, codec, entry, hidden(model, codec, entry)), picked)], len(ids)
|
| 545 |
+
|
| 546 |
+
def wide_choice(model, codec, state: str, q: dict, images=(), max_len: int | None = None,
|
| 547 |
+
temperature: dict | None = None):
|
| 548 |
+
"""A choice over more options than there are letters, in two passes -> (distribution, most input tokens).
|
| 549 |
+
|
| 550 |
+
First the options are split into chunks, each asked as its own question; then the leading options of every chunk
|
| 551 |
+
are asked again together. Finalists share their first-pass mass (each chunk weighted equally) in proportion to the
|
| 552 |
+
second pass; every other option keeps its first-pass share, so the distribution sums to one and its top answer is
|
| 553 |
+
the second pass's."""
|
| 554 |
+
n, per = len(q["options"]), len(codec.codes)
|
| 555 |
+
size = -(-n // -(-n // per)) # balanced chunks of at most `per`
|
| 556 |
+
chunks = [list(range(s, min(s + size, n))) for s in range(0, n, size)]
|
| 557 |
+
first, t1 = read(model, codec, state, [{**q, "options": [q["options"][i] for i in c]} for c in chunks], images, max_len,
|
| 558 |
+
temperature)
|
| 559 |
+
prior = [0.0] * n
|
| 560 |
+
for c, p in zip(chunks, first):
|
| 561 |
+
for i, x in zip(c, p):
|
| 562 |
+
prior[i] = x / len(chunks)
|
| 563 |
+
keep = per // len(chunks)
|
| 564 |
+
finalists = sorted(i for c, p in zip(chunks, first) for i, _ in sorted(zip(c, p), key=lambda t: -t[1])[:keep])
|
| 565 |
+
(second,), t2 = read(model, codec, state, [{**q, "options": [q["options"][i] for i in finalists]}], images, max_len,
|
| 566 |
+
temperature)
|
| 567 |
+
mass, out = sum(prior[i] for i in finalists), prior[:]
|
| 568 |
+
for i, x in zip(finalists, second):
|
| 569 |
+
out[i] = mass * x
|
| 570 |
+
return out, max(t1, t2)
|
| 571 |
+
|
| 572 |
+
@torch.no_grad()
|
| 573 |
+
def sureness(r: dict) -> float:
|
| 574 |
+
"""How sure a yes/no, choice or score result is: the probability of its answer."""
|
| 575 |
+
return max(r["p_yes"], 1 - r["p_yes"]) if r["type"] == "noul" else max(r["probs"])
|
| 576 |
+
|
| 577 |
+
@torch.no_grad()
|
| 578 |
+
def think_many(model, codec, state: str, qs: list[dict], max_tokens: int, temperature: dict | None = None,
|
| 579 |
+
generate=None):
|
| 580 |
+
"""think for several questions about one case at once: their thoughts are generated as one left-padded batch, so a
|
| 581 |
+
case with eight unsure questions costs about one generation instead of eight. generate(prefixes, max_tokens) ->
|
| 582 |
+
[(text, tokens)] writes the thoughts elsewhere instead (a vLLM server: serve's AMOS_THINK_URL); the letters are
|
| 583 |
+
still read here, so the answers are the same either way."""
|
| 584 |
+
if noncausal(model):
|
| 585 |
+
raise ValueError("a noncausal run cannot think: generation reads a causal model's cache")
|
| 586 |
+
state, qs = codec.safe(state, qs)
|
| 587 |
+
prefixes = [codec.tok.apply_chat_template(render(state, [q], codes=codec.codes)[:2], tokenize=False,
|
| 588 |
+
add_generation_prompt=True, enable_thinking=True) for q in qs]
|
| 589 |
+
if generate is not None:
|
| 590 |
+
thoughts = generate(prefixes, max_tokens)
|
| 591 |
+
else:
|
| 592 |
+
ids = [codec.tok(p, add_special_tokens=False)["input_ids"] for p in prefixes]
|
| 593 |
+
width, pad = max(map(len, ids)), codec.tok.pad_token_id
|
| 594 |
+
batch = torch.tensor([[pad] * (width - len(x)) + x for x in ids], device=model.device)
|
| 595 |
+
mask = torch.tensor([[0] * (width - len(x)) + [1] * len(x) for x in ids], device=model.device)
|
| 596 |
+
out = model.generate(batch, attention_mask=mask, max_new_tokens=max_tokens, do_sample=False, use_cache=True,
|
| 597 |
+
pad_token_id=pad)
|
| 598 |
+
thoughts = [(codec.tok.decode(row, skip_special_tokens=False).replace(codec.tok.pad_token or "\0", ""),
|
| 599 |
+
int((row != pad).sum())) for row in out[:, width:]]
|
| 600 |
+
results = []
|
| 601 |
+
for prefix, q, (text, n) in zip(prefixes, qs, thoughts):
|
| 602 |
+
reasoning = text.split("</think>")[0].replace("<|im_end|>", "").strip()
|
| 603 |
+
full = codec.tok(prefix + reasoning + "\n</think>\n\n" + f"q1: {ANSWER}", add_special_tokens=False)["input_ids"]
|
| 604 |
+
at = [i - 1 for i, t in enumerate(full) if t == codec.marker]
|
| 605 |
+
lp = distributions(model, codec, [(full, at, [q])])[0]
|
| 606 |
+
results.append((tempered(lp, (temperature or {}).get(q["type"])), n))
|
| 607 |
+
return results
|
| 608 |
+
|
| 609 |
+
def results(model, codec, state: str, questions: list[dict], images=(), heads: Heads | None = None,
|
| 610 |
+
max_len: int | None = None, rotations: int = 1, temperature: dict | None = None,
|
| 611 |
+
think_below: float | None = None, think_tokens: int = 512, generate=None) -> tuple[list[dict], int]:
|
| 612 |
+
"""One typed result per question in amos.model.decode's format (noul p_yes; choice/score probs, choice, expected;
|
| 613 |
+
multi independent probs; span start/end/text/p; region cell/box/p), and the number of input tokens. A choice over
|
| 614 |
+
more options than there are letters is answered on its own (wide_choice). rotations and temperature: see
|
| 615 |
+
one_pass. think_below: a yes/no, choice or score answer less sure than this is asked again after the model
|
| 616 |
+
thinks (think), as GLiDE spends compute only where a decision is uncertain; the result then carries "thought"."""
|
| 617 |
+
if think_below and noncausal(model): # on every request, not only the unsure ones
|
| 618 |
+
raise ValueError("a noncausal run cannot think: generation reads a causal model's cache")
|
| 619 |
+
out, tokens = _results(model, codec, state, questions, images, heads, max_len, rotations, temperature)
|
| 620 |
+
if think_below and not images:
|
| 621 |
+
unsure = [k for k, (q, r) in enumerate(zip(questions, out))
|
| 622 |
+
if q["type"] in KINDS and len(options(q)) <= len(codec.codes) and sureness(r) < think_below]
|
| 623 |
+
for k, (p, n) in zip(unsure, think_many(model, codec, state, [questions[k] for k in unsure], think_tokens,
|
| 624 |
+
temperature, generate) if unsure else []):
|
| 625 |
+
q = questions[k]
|
| 626 |
+
if q["type"] == "noul":
|
| 627 |
+
out[k] = {"type": "noul", "p_yes": p[0], "thought": n}
|
| 628 |
+
else:
|
| 629 |
+
out[k] = {"type": q["type"], "probs": p, "choice": max(range(len(p)), key=p.__getitem__), "thought": n}
|
| 630 |
+
if q["type"] == "score":
|
| 631 |
+
out[k]["expected"] = sum(i * x for i, x in enumerate(p))
|
| 632 |
+
return out, tokens
|
| 633 |
+
|
| 634 |
+
def _results(model, codec, state: str, questions: list[dict], images=(), heads: Heads | None = None,
|
| 635 |
+
max_len: int | None = None, rotations: int = 1, temperature: dict | None = None) -> tuple[list[dict], int]:
|
| 636 |
+
wide = {k for k, q in enumerate(questions) if q["type"] == "choice" and len(q["options"]) > len(codec.codes)}
|
| 637 |
+
out, tokens = [None] * len(questions), 0
|
| 638 |
+
for k in sorted(wide):
|
| 639 |
+
p, n = wide_choice(model, codec, state, questions[k], images, max_len, temperature)
|
| 640 |
+
out[k], tokens = {"type": "choice", "probs": p, "choice": max(range(len(p)), key=p.__getitem__)}, max(tokens, n)
|
| 641 |
+
rest = [k for k in range(len(questions)) if k not in wide]
|
| 642 |
+
if rest:
|
| 643 |
+
sub, n = one_pass(model, codec, state, [questions[k] for k in rest], images, heads, max_len, rotations,
|
| 644 |
+
temperature)
|
| 645 |
+
for k, r in zip(rest, sub):
|
| 646 |
+
out[k] = r
|
| 647 |
+
tokens = max(tokens, n)
|
| 648 |
+
return out, tokens
|
| 649 |
+
|
| 650 |
+
ROTATION_BATCH_TOKENS = 65536
|
| 651 |
+
|
| 652 |
+
def one_pass(model, codec, state: str, questions: list[dict], images=(), heads: Heads | None = None,
|
| 653 |
+
max_len: int | None = None, rotations: int = 1, temperature: dict | None = None) -> tuple[list[dict], int]:
|
| 654 |
+
"""results() for questions that fit the letters, in one forward pass.
|
| 655 |
+
|
| 656 |
+
rotations > 1 (and a choice question present): the case is also read with every choice question's options rotated,
|
| 657 |
+
each rotation one more row of the same batched forward, and choice distributions are averaged in the original order.
|
| 658 |
+
temperature: {question type: t} fitted by `decoder calibrate` (pick-any's per-option yes/no uses noul's)."""
|
| 659 |
+
flat, owner = expand(questions)
|
| 660 |
+
rotations = rotations if any(q["type"] == "choice" for q in flat) else 1
|
| 661 |
+
entries = []
|
| 662 |
+
for r in range(rotations):
|
| 663 |
+
qs = [rotate(q, r) for q in flat]
|
| 664 |
+
ids, at, extra = codec.encode_case(state, qs, images)
|
| 665 |
+
if max_len and len(ids) > max_len:
|
| 666 |
+
raise ValueError(f"maximum context length: request has {len(ids)} tokens, limit {max_len}")
|
| 667 |
+
entries.append((ids, at, qs, extra))
|
| 668 |
+
ids, extra = entries[0][0], entries[0][3]
|
| 669 |
+
if heads is None and any(q["type"] in HEADED for q in flat):
|
| 670 |
+
raise ValueError("this model has no span or region head")
|
| 671 |
+
if sum(len(e[0]) for e in entries) <= ROTATION_BATCH_TOKENS:
|
| 672 |
+
h = hidden(model, codec, entries)
|
| 673 |
+
lps = distributions(model, codec, entries, h)
|
| 674 |
+
else: # a long case: one rotation at a time, so memory stays that of a single row
|
| 675 |
+
h, lps = None, []
|
| 676 |
+
for e in entries:
|
| 677 |
+
he = hidden(model, codec, [e])
|
| 678 |
+
lps += distributions(model, codec, [e], he)
|
| 679 |
+
h = he if h is None else h
|
| 680 |
+
head = iter(heads(h[:1], entries[:1]) if heads is not None else [])
|
| 681 |
+
picked = [j for j, q in enumerate(flat) if q["type"] in KINDS]
|
| 682 |
+
temps = temperature or {}
|
| 683 |
+
mean = {}
|
| 684 |
+
for r in range(rotations):
|
| 685 |
+
for m, j in enumerate(picked):
|
| 686 |
+
p, n = tempered(lps[r * len(picked) + m], temps.get(flat[j]["type"])), len(options(flat[j]))
|
| 687 |
+
s = r % n if flat[j]["type"] == "choice" else 0
|
| 688 |
+
acc = mean.setdefault(j, [0.0] * n)
|
| 689 |
+
for i in range(n): # rotated position (i - s) mod n holds original option i
|
| 690 |
+
acc[i] += p[(i - s) % n] / rotations
|
| 691 |
+
lps = iter(mean[j] for j in picked)
|
| 692 |
+
out = [None] * len(questions)
|
| 693 |
+
for q, k in zip(flat, owner):
|
| 694 |
+
t = questions[k]["type"]
|
| 695 |
+
if q["type"] in HEADED:
|
| 696 |
+
out[k] = decode_head(q, next(head), extra, state, heads.keep, heads.span)
|
| 697 |
+
continue
|
| 698 |
+
p = next(lps)
|
| 699 |
+
if t == "multi":
|
| 700 |
+
out[k] = out[k] or {"type": t, "probs": []}
|
| 701 |
+
out[k]["probs"].append(p[0])
|
| 702 |
+
elif t == "noul":
|
| 703 |
+
out[k] = {"type": t, "p_yes": p[0]}
|
| 704 |
+
else:
|
| 705 |
+
out[k] = {"type": t, "probs": p, "choice": max(range(len(p)), key=p.__getitem__)}
|
| 706 |
+
if t == "score":
|
| 707 |
+
out[k]["expected"] = sum(i * x for i, x in enumerate(p))
|
| 708 |
+
return out, len(ids)
|
| 709 |
+
|
| 710 |
+
CALIBRATION = "calibration.json"
|
| 711 |
+
|
| 712 |
+
def calibration(run) -> dict | None:
|
| 713 |
+
"""The per-type temperatures `decoder calibrate` fitted for a run, or None."""
|
| 714 |
+
f = Path(run) / CALIBRATION
|
| 715 |
+
return json.loads(f.read_text()).get("temperature") if f.exists() else None
|
| 716 |
+
|
| 717 |
+
def span_decoding(run) -> tuple[int, float]:
|
| 718 |
+
"""The quote decoder's (cap, penalty) fitted for a run on held-out quote records, or the defaults."""
|
| 719 |
+
f = Path(run) / CALIBRATION
|
| 720 |
+
s = (json.loads(f.read_text()).get("span") or {}) if f.exists() else {}
|
| 721 |
+
return int(s.get("cap", SPAN_CAP)), float(s.get("penalty", SPAN_LENGTH_PENALTY))
|
| 722 |
+
|
| 723 |
+
def multi_cut(run) -> float | None:
|
| 724 |
+
"""The pick-any cut fitted on our own held-out pick-any questions (an option is picked at p >= cut), or None."""
|
| 725 |
+
f = Path(run) / CALIBRATION
|
| 726 |
+
return json.loads(f.read_text()).get("multi_cut") if f.exists() else None
|
| 727 |
+
|
serve/amos/encoder.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ModernBERT-architecture encoder (Ettin weights), written from scratch.
|
| 2 |
+
|
| 3 |
+
Attention is controlled by per-token vectors instead of a precomputed mask:
|
| 4 |
+
position_ids: RoPE positions (branches restart after the state).
|
| 5 |
+
segment_ids: 0 = state, k > 0 = question branch k, -1 = padding.
|
| 6 |
+
option_ids: 0 = not an option, m > 0 = option m of its question.
|
| 7 |
+
A token attends to state tokens, its question's own tokens, and (for option tokens)
|
| 8 |
+
its own option. Local layers additionally restrict attention to
|
| 9 |
+
|pos_i - pos_j| <= window // 2, measured in position ids (not sequence index) so
|
| 10 |
+
packed and separate requests match.
|
| 11 |
+
Comparison mode lets each question and its options attend to the whole question branch.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn.functional as F
|
| 19 |
+
from huggingface_hub import hf_hub_download
|
| 20 |
+
from torch import nn
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class Config:
|
| 25 |
+
vocab_size: int
|
| 26 |
+
hidden_size: int
|
| 27 |
+
intermediate_size: int
|
| 28 |
+
num_hidden_layers: int
|
| 29 |
+
num_attention_heads: int
|
| 30 |
+
global_attn_every_n_layers: int = 3
|
| 31 |
+
local_attention: int = 128
|
| 32 |
+
global_rope_theta: float = 160000.0
|
| 33 |
+
local_rope_theta: float = 10000.0
|
| 34 |
+
norm_eps: float = 1e-5
|
| 35 |
+
pad_token_id: int = 50283
|
| 36 |
+
cls_token_id: int = 50281
|
| 37 |
+
sep_token_id: int = 50282
|
| 38 |
+
model_type: str = "modernbert"
|
| 39 |
+
compare_options: bool = False
|
| 40 |
+
date_facts: bool = False # an input-format setting: facts.date_facts runs on every state (model.pack_request)
|
| 41 |
+
|
| 42 |
+
@classmethod
|
| 43 |
+
def from_hf(cls, d: dict) -> "Config":
|
| 44 |
+
"""Reads a Hugging Face config or a run's saved config, ignoring fields this class does not define."""
|
| 45 |
+
return cls(**{k: d[k] for k in cls.__dataclass_fields__ if k in d})
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def rope(x: torch.Tensor, pos: torch.Tensor, theta: float) -> torch.Tensor:
|
| 49 |
+
"""x: [B, H, L, D], pos: [B, L]. Half-split rotation (rotate_half convention)."""
|
| 50 |
+
d = x.shape[-1]
|
| 51 |
+
inv_freq = 1.0 / (theta ** (torch.arange(0, d, 2, device=x.device, dtype=torch.float32) / d))
|
| 52 |
+
freqs = pos[:, None, :, None].float() * inv_freq # [B, 1, L, D/2]
|
| 53 |
+
cos, sin = freqs.cos().to(x.dtype), freqs.sin().to(x.dtype)
|
| 54 |
+
x1, x2 = x[..., : d // 2], x[..., d // 2 :]
|
| 55 |
+
return torch.cat((x1 * cos - x2 * sin, x2 * cos + x1 * sin), dim=-1)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class Layer(nn.Module):
|
| 59 |
+
def __init__(self, c: Config, idx: int):
|
| 60 |
+
super().__init__()
|
| 61 |
+
self.is_global = idx % c.global_attn_every_n_layers == 0
|
| 62 |
+
self.theta = c.global_rope_theta if self.is_global else c.local_rope_theta
|
| 63 |
+
self.heads = c.num_attention_heads
|
| 64 |
+
self.attn_norm = nn.LayerNorm(c.hidden_size, eps=c.norm_eps, bias=False) if idx else nn.Identity()
|
| 65 |
+
self.Wqkv = nn.Linear(c.hidden_size, 3 * c.hidden_size, bias=False)
|
| 66 |
+
self.Wo = nn.Linear(c.hidden_size, c.hidden_size, bias=False)
|
| 67 |
+
self.mlp_norm = nn.LayerNorm(c.hidden_size, eps=c.norm_eps, bias=False)
|
| 68 |
+
self.Wi = nn.Linear(c.hidden_size, 2 * c.intermediate_size, bias=False)
|
| 69 |
+
self.Wo_mlp = nn.Linear(c.intermediate_size, c.hidden_size, bias=False)
|
| 70 |
+
|
| 71 |
+
def forward(self, h, pos, global_mask, local_mask, prefix=None):
|
| 72 |
+
"""prefix: cached (k, v) of earlier tokens, prepended to this call's keys and values."""
|
| 73 |
+
B, L, _ = h.shape
|
| 74 |
+
q, k, v = self.Wqkv(self.attn_norm(h)).view(B, L, 3, self.heads, -1).permute(2, 0, 3, 1, 4)
|
| 75 |
+
q, k = rope(q, pos, self.theta), rope(k, pos, self.theta)
|
| 76 |
+
kv = (k, v)
|
| 77 |
+
if prefix is not None:
|
| 78 |
+
k, v = torch.cat([prefix[0], k], 2), torch.cat([prefix[1], v], 2)
|
| 79 |
+
mask = global_mask if self.is_global else local_mask
|
| 80 |
+
a = F.scaled_dot_product_attention(q, k, v, attn_mask=mask)
|
| 81 |
+
h = h + self.Wo(a.transpose(1, 2).reshape(B, L, -1))
|
| 82 |
+
x, gate = self.Wi(self.mlp_norm(h)).chunk(2, dim=-1)
|
| 83 |
+
return h + self.Wo_mlp(F.gelu(x) * gate), kv
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class Encoder(nn.Module):
|
| 87 |
+
def __init__(self, c: Config):
|
| 88 |
+
super().__init__()
|
| 89 |
+
self.config = c
|
| 90 |
+
self.tok_embeddings = nn.Embedding(c.vocab_size, c.hidden_size)
|
| 91 |
+
self.emb_norm = nn.LayerNorm(c.hidden_size, eps=c.norm_eps, bias=False)
|
| 92 |
+
self.layers = nn.ModuleList(Layer(c, i) for i in range(c.num_hidden_layers))
|
| 93 |
+
self.final_norm = nn.LayerNorm(c.hidden_size, eps=c.norm_eps, bias=False)
|
| 94 |
+
|
| 95 |
+
@staticmethod
|
| 96 |
+
def masks(pos, seg, opt, window, pos_k=None, seg_k=None, opt_k=None, *, compare_options=False):
|
| 97 |
+
"""Boolean [B, 1, Lq, Lk] masks, True = may attend. Global and local variants.
|
| 98 |
+
|
| 99 |
+
Keys default to the queries; pass *_k to attend over a longer key sequence (cached state + queries).
|
| 100 |
+
The masks are dense, so their memory grows with Lq * Lk.
|
| 101 |
+
"""
|
| 102 |
+
pos_k, seg_k, opt_k = (x if x is not None else y for x, y in ((pos_k, pos), (seg_k, seg), (opt_k, opt)))
|
| 103 |
+
si, sj, oi, oj = seg[:, :, None], seg_k[:, None, :], opt[:, :, None], opt_k[:, None, :]
|
| 104 |
+
within = (sj == si) & ((si > 0) if compare_options else ((oj == 0) | (oj == oi)))
|
| 105 |
+
allowed = (sj != -1) & ((sj == 0) | within)
|
| 106 |
+
local = allowed & ((pos[:, :, None] - pos_k[:, None, :]).abs() <= window // 2)
|
| 107 |
+
return allowed[:, None], local[:, None]
|
| 108 |
+
|
| 109 |
+
def forward(self, input_ids, position_ids=None, segment_ids=None, option_ids=None, inputs_embeds=None):
|
| 110 |
+
B, L = input_ids.shape
|
| 111 |
+
if position_ids is None:
|
| 112 |
+
position_ids = torch.arange(L, device=input_ids.device).expand(B, L)
|
| 113 |
+
if segment_ids is None:
|
| 114 |
+
segment_ids = torch.zeros_like(input_ids)
|
| 115 |
+
if option_ids is None:
|
| 116 |
+
option_ids = torch.zeros_like(input_ids)
|
| 117 |
+
emb = self.tok_embeddings(input_ids) if inputs_embeds is None else inputs_embeds
|
| 118 |
+
return self.run(emb, position_ids, segment_ids, option_ids)[0]
|
| 119 |
+
|
| 120 |
+
def run(self, emb, pos, seg, opt, cache=None):
|
| 121 |
+
"""Encode embeddings, optionally on top of a cached prefix. Returns (hidden, cache for these tokens).
|
| 122 |
+
|
| 123 |
+
A cache holds each layer's rotated keys and values plus the pos/seg/opt ids of its tokens. The state
|
| 124 |
+
never attends to questions, so a state encoded once can serve any number of later questions.
|
| 125 |
+
"""
|
| 126 |
+
if cache is None:
|
| 127 |
+
g, l = self.masks(pos, seg, opt, self.config.local_attention, compare_options=self.config.compare_options)
|
| 128 |
+
else:
|
| 129 |
+
g, l = self.masks(pos, seg, opt, self.config.local_attention,
|
| 130 |
+
torch.cat([cache["pos"], pos], 1), torch.cat([cache["seg"], seg], 1), torch.cat([cache["opt"], opt], 1),
|
| 131 |
+
compare_options=self.config.compare_options)
|
| 132 |
+
h, kvs = self.emb_norm(emb), []
|
| 133 |
+
for i, layer in enumerate(self.layers):
|
| 134 |
+
h, kv = layer(h, pos, g, l, None if cache is None else cache["kv"][i])
|
| 135 |
+
kvs.append(kv)
|
| 136 |
+
return self.final_norm(h), {"kv": kvs, "pos": pos, "seg": seg, "opt": opt}
|
| 137 |
+
|
| 138 |
+
@classmethod
|
| 139 |
+
def from_pretrained(cls, repo: str) -> "Encoder":
|
| 140 |
+
with open(hf_hub_download(repo, "config.json")) as f:
|
| 141 |
+
model = cls(Config.from_hf(json.load(f)))
|
| 142 |
+
sd = torch.load(hf_hub_download(repo, "pytorch_model.bin"), map_location="cpu", weights_only=True)
|
| 143 |
+
rename = {
|
| 144 |
+
"model.embeddings.tok_embeddings.": "tok_embeddings.",
|
| 145 |
+
"model.embeddings.norm.": "emb_norm.",
|
| 146 |
+
"model.final_norm.": "final_norm.",
|
| 147 |
+
".attn.Wqkv.": ".Wqkv.",
|
| 148 |
+
".attn.Wo.": ".Wo.",
|
| 149 |
+
".mlp.Wi.": ".Wi.",
|
| 150 |
+
".mlp.Wo.": ".Wo_mlp.",
|
| 151 |
+
"model.layers.": "layers.",
|
| 152 |
+
}
|
| 153 |
+
out = {}
|
| 154 |
+
for k, v in sd.items():
|
| 155 |
+
if not k.startswith("model."): # MLM head, unused
|
| 156 |
+
continue
|
| 157 |
+
for a, b in rename.items():
|
| 158 |
+
k = k.replace(a, b)
|
| 159 |
+
out[k] = v
|
| 160 |
+
model.load_state_dict(out, strict=True)
|
| 161 |
+
return model.eval()
|
serve/amos/facts.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Computed facts appended to a state, so the model reads date gaps instead of doing calendar arithmetic.
|
| 2 |
+
|
| 3 |
+
The same function runs in training (train.py) and at inference (model.pack_request), switched by the run's
|
| 4 |
+
encoder config (date_facts), so a model always sees the input format it was trained on.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import re
|
| 8 |
+
from datetime import date
|
| 9 |
+
from itertools import combinations
|
| 10 |
+
|
| 11 |
+
# An ISO date, optionally the value of a JSON key and optionally followed by a time ("2026-03-18T09:00:00Z").
|
| 12 |
+
DATE = re.compile(r'(?:"([^"\n]{1,60})":\s*")?(\d{4})-(\d{2})-(\d{2})(?=[T "\s,.;)\]}]|$)')
|
| 13 |
+
MAX_DATES = 6 # 15 pairs at most, so long records gain a few lines, not a page
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def date_facts(state: str) -> str:
|
| 17 |
+
"""The state plus one line per pair of distinct dates it mentions, or the state unchanged if it has fewer than 2."""
|
| 18 |
+
seen = {}
|
| 19 |
+
for m in DATE.finditer(state):
|
| 20 |
+
try:
|
| 21 |
+
d = date(int(m[2]), int(m[3]), int(m[4]))
|
| 22 |
+
except ValueError:
|
| 23 |
+
continue
|
| 24 |
+
seen.setdefault(d, m[1] or "a date in the text")
|
| 25 |
+
if len(seen) == MAX_DATES:
|
| 26 |
+
break
|
| 27 |
+
if len(seen) < 2:
|
| 28 |
+
return state
|
| 29 |
+
lines = []
|
| 30 |
+
for (a, ka), (b, kb) in combinations(sorted(seen.items()), 2):
|
| 31 |
+
lines.append(f"- {ka} ({a}) is {(b - a).days} days before {kb} ({b}).")
|
| 32 |
+
return f"{state}\n\nDate facts:\n" + "\n".join(lines)
|
serve/amos/model.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Amos: encoder + typed decision heads. Every output is an option, a span of the state, or a P(yes).
|
| 2 |
+
|
| 3 |
+
All heads read the question's [DECIDE_t] vector:
|
| 4 |
+
noul softmax over two implicit options ["no: ...", "yes: ..."]; P(yes) is the second
|
| 5 |
+
choice/score softmax over bilinear(h_decide, mean-pooled option tokens)
|
| 6 |
+
multi independent sigmoid over the same pointer logits (+ bias)
|
| 7 |
+
span start/end softmax, bilinear(h_decide, state tokens); decoded to an exact substring
|
| 8 |
+
region softmax over the first state image's 49 cells, bilinear(h_decide, image tokens)
|
| 9 |
+
|
| 10 |
+
With a vision tower, image slots are filled with projected vision features before the first layer;
|
| 11 |
+
the tower itself stays frozen and is not part of saved checkpoints.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
import math
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
import torch.nn.functional as F
|
| 20 |
+
from torch import nn
|
| 21 |
+
|
| 22 |
+
from .encoder import Config, Encoder
|
| 23 |
+
from .facts import date_facts
|
| 24 |
+
from .packing import (
|
| 25 |
+
IMAGE_TOKENS,
|
| 26 |
+
TYPES,
|
| 27 |
+
Codec,
|
| 28 |
+
Packed,
|
| 29 |
+
char_span_to_tokens,
|
| 30 |
+
collate,
|
| 31 |
+
pack,
|
| 32 |
+
)
|
| 33 |
+
from .vision import Projector, VisionTower, box_target, cell_boxes
|
| 34 |
+
|
| 35 |
+
NOUL, CHOICE, SCORE, MULTI, SPAN, REGION = range(len(TYPES))
|
| 36 |
+
MAX_SPAN_TOKENS = 30
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _init(*mods):
|
| 40 |
+
for m in mods:
|
| 41 |
+
nn.init.normal_(m.weight, std=0.02)
|
| 42 |
+
nn.init.zeros_(m.bias)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class Amos(nn.Module):
|
| 46 |
+
def __init__(self, encoder: Encoder, vision: VisionTower | None = None):
|
| 47 |
+
super().__init__()
|
| 48 |
+
d = encoder.config.hidden_size
|
| 49 |
+
self.encoder = encoder
|
| 50 |
+
self.ptr_q, self.ptr_o = nn.Linear(d, d), nn.Linear(d, d)
|
| 51 |
+
self.multi_bias = nn.Parameter(torch.zeros(()))
|
| 52 |
+
self.span_q, self.span_k = nn.Linear(d, 2 * d), nn.Linear(d, 2 * d)
|
| 53 |
+
_init(self.ptr_q, self.ptr_o, self.span_q, self.span_k)
|
| 54 |
+
self.vision = vision
|
| 55 |
+
if vision is not None:
|
| 56 |
+
self.projector = Projector(vision.hidden_size, d)
|
| 57 |
+
self.region_q, self.region_k = nn.Linear(d, d), nn.Linear(d, d)
|
| 58 |
+
_init(self.region_q, self.region_k)
|
| 59 |
+
|
| 60 |
+
@classmethod
|
| 61 |
+
def from_encoder(cls, repo: str, codec: Codec, vision_repo: str | None = None) -> "Amos":
|
| 62 |
+
enc = Encoder.from_pretrained(repo)
|
| 63 |
+
init_markers(enc, codec)
|
| 64 |
+
return cls(enc, VisionTower.from_pretrained(vision_repo) if vision_repo else None)
|
| 65 |
+
|
| 66 |
+
def saved_state(self) -> dict:
|
| 67 |
+
"""Weights to checkpoint: everything except the frozen, separately downloadable vision tower."""
|
| 68 |
+
return {k: v for k, v in self.state_dict().items() if not k.startswith("vision.")}
|
| 69 |
+
|
| 70 |
+
def embed(self, b: dict) -> torch.Tensor:
|
| 71 |
+
emb = self.encoder.tok_embeddings(b["ids"])
|
| 72 |
+
if "images" not in b:
|
| 73 |
+
return emb
|
| 74 |
+
x = b["images"]
|
| 75 |
+
if x.dim() == 4: # pixels -> frozen tower; precomputed features skip it
|
| 76 |
+
with torch.no_grad():
|
| 77 |
+
x = self.vision(x)
|
| 78 |
+
tokens = self.projector(x.to(emb.dtype))
|
| 79 |
+
cols = b["img_at"][:, None] + torch.arange(IMAGE_TOKENS, device=emb.device)
|
| 80 |
+
rows = b["img_row"][:, None].expand_as(cols)
|
| 81 |
+
return emb.index_put((rows.reshape(-1), cols.reshape(-1)), tokens.reshape(-1, tokens.shape[-1]))
|
| 82 |
+
|
| 83 |
+
def forward(self, b: dict) -> dict:
|
| 84 |
+
return self.heads(self.encoder(b["ids"], b["pos"], b["seg"], b["opt"], inputs_embeds=self.embed(b)), b)
|
| 85 |
+
|
| 86 |
+
def forward_cached(self, b: dict, state_len: int, cache: dict | None):
|
| 87 |
+
"""One pack (B = 1) with a reusable state. Returns (outputs, state cache); same result as forward().
|
| 88 |
+
|
| 89 |
+
The state never attends to the questions, so its keys, values and hidden states in a full pass are
|
| 90 |
+
exactly what a state-only pass would produce: a miss runs one normal pass and slices the cache out
|
| 91 |
+
of it; a hit encodes only the question tokens on top of the cached state.
|
| 92 |
+
"""
|
| 93 |
+
emb, s = self.embed(b), state_len
|
| 94 |
+
if cache is None:
|
| 95 |
+
h, kv = self.encoder.run(emb, b["pos"], b["seg"], b["opt"])
|
| 96 |
+
cache = {"kv": [(k[:, :, :s], v[:, :, :s]) for k, v in kv["kv"]], "hidden": h[:, :s],
|
| 97 |
+
**{k: b[k][:, :s] for k in ("pos", "seg", "opt")}}
|
| 98 |
+
return self.heads(h, b), cache
|
| 99 |
+
h_q, _ = self.encoder.run(emb[:, s:], b["pos"][:, s:], b["seg"][:, s:], b["opt"][:, s:], cache)
|
| 100 |
+
return self.heads(torch.cat([cache["hidden"], h_q], 1), b), cache
|
| 101 |
+
|
| 102 |
+
def heads(self, h: torch.Tensor, b: dict) -> dict:
|
| 103 |
+
d = h.shape[-1]
|
| 104 |
+
hq = h[b["q_row"], b["q_tok"]]
|
| 105 |
+
out = {}
|
| 106 |
+
|
| 107 |
+
Q, O = len(hq), len(b["o_q"])
|
| 108 |
+
opt_logits = h.new_full((Q, opt_width(b)), -torch.inf)
|
| 109 |
+
if O:
|
| 110 |
+
of = b["option_of"]
|
| 111 |
+
valid = of >= 0
|
| 112 |
+
pooled = h.new_zeros(O, d).index_add_(0, of[valid], h[valid])
|
| 113 |
+
pooled = pooled / torch.bincount(of[valid], minlength=O).clamp(min=1)[:, None]
|
| 114 |
+
ptr = (self.ptr_q(hq)[b["o_q"]] * self.ptr_o(pooled)).sum(-1) / math.sqrt(d)
|
| 115 |
+
ptr = ptr + (b["q_type"][b["o_q"]] == MULTI) * self.multi_bias
|
| 116 |
+
opt_logits = opt_logits.index_put((b["o_q"], b["o_rank"]), ptr)
|
| 117 |
+
out["opt"] = opt_logits
|
| 118 |
+
|
| 119 |
+
sq = (b["q_type"] == SPAN).nonzero().squeeze(-1)
|
| 120 |
+
out["span_q"] = sq
|
| 121 |
+
if len(sq):
|
| 122 |
+
rows = b["q_row"][sq]
|
| 123 |
+
qs = self.span_q(hq[sq]).view(len(sq), 2, d)
|
| 124 |
+
ks = self.span_k(h[rows]).view(len(sq), -1, 2, d)
|
| 125 |
+
logits = torch.einsum("qcd,qlcd->qcl", qs, ks) / math.sqrt(d)
|
| 126 |
+
out["span"] = logits.masked_fill(~b["state_mask"][rows][:, None, :], -torch.inf)
|
| 127 |
+
|
| 128 |
+
rq = (b["q_type"] == REGION).nonzero().squeeze(-1)
|
| 129 |
+
out["region_q"] = rq
|
| 130 |
+
if len(rq):
|
| 131 |
+
rows = b["q_row"][rq]
|
| 132 |
+
logits = torch.einsum("qd,qld->ql", self.region_q(hq[rq]), self.region_k(h[rows])) / math.sqrt(d)
|
| 133 |
+
out["region"] = logits.masked_fill(~b["region_mask"][rows], -torch.inf)
|
| 134 |
+
return out
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def init_markers(enc: nn.Module, codec: Codec, markers=None):
|
| 138 |
+
"""Reserved markers start as copies of [CLS] (their pretrained rows were never trained)."""
|
| 139 |
+
with torch.no_grad():
|
| 140 |
+
for i in markers or (*codec.decide.values(), codec.opt, codec.img, codec.img_end, codec.patch):
|
| 141 |
+
enc.tok_embeddings.weight[i] = enc.tok_embeddings.weight[codec.cls]
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def add_targets(b: dict, packs: list[Packed], questions: list[list[dict]]) -> dict:
|
| 145 |
+
"""Attach target tensors to a collated batch. Targets are probabilities (hard labels are one-hot).
|
| 146 |
+
|
| 147 |
+
noul: float P(yes). choice/score: distribution over options. multi: independent P per option.
|
| 148 |
+
span: list of accepted [start, end) char spans in the state. region: normalized (x0, y0, x1, y1) box
|
| 149 |
+
in the first state image, spread over the 49 cells by overlap.
|
| 150 |
+
"""
|
| 151 |
+
Q, L = len(b["q_row"]), b["ids"].shape[1]
|
| 152 |
+
t_opt = torch.zeros(Q, opt_width(b))
|
| 153 |
+
t_span, t_region, has = [], [], torch.ones(Q, dtype=torch.bool)
|
| 154 |
+
qi = 0
|
| 155 |
+
for row, (p, qs) in enumerate(zip(packs, questions)):
|
| 156 |
+
ok = b["state_mask"][row]
|
| 157 |
+
for q in qs:
|
| 158 |
+
tgt, typ = q["target"], q["type"]
|
| 159 |
+
if typ == "noul": # implicit options are ["no", "yes"]
|
| 160 |
+
t_opt[qi, :2] = torch.tensor([1 - float(tgt), float(tgt)])
|
| 161 |
+
elif typ == "region":
|
| 162 |
+
dist = torch.zeros(L)
|
| 163 |
+
dist[p.image_at[0] : p.image_at[0] + IMAGE_TOKENS] = box_target(tgt)
|
| 164 |
+
t_region.append(dist)
|
| 165 |
+
elif typ == "span":
|
| 166 |
+
dist = torch.zeros(2, L)
|
| 167 |
+
for s, e in tgt:
|
| 168 |
+
tok = char_span_to_tokens(p.offsets, s, e)
|
| 169 |
+
if tok and ok[tok[0]] and ok[tok[1]]: # boundaries must be decodable (not truncated/blank)
|
| 170 |
+
dist[0, tok[0]] += 1
|
| 171 |
+
dist[1, tok[1]] += 1
|
| 172 |
+
has[qi] = bool(dist.sum())
|
| 173 |
+
t_span.append(dist / dist.sum(-1, keepdim=True).clamp(min=1))
|
| 174 |
+
else:
|
| 175 |
+
t_opt[qi, : len(tgt)] = torch.tensor(tgt, dtype=torch.float)
|
| 176 |
+
qi += 1
|
| 177 |
+
b["t_opt"], b["has_target"] = t_opt, has
|
| 178 |
+
b["t_span"] = torch.stack(t_span) if t_span else torch.zeros(0, 2, L)
|
| 179 |
+
b["t_region"] = torch.stack(t_region) if t_region else torch.zeros(0, L)
|
| 180 |
+
return b
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def opt_width(b: dict) -> int:
|
| 184 |
+
return max(1, int(b["q_nopt"].max())) if len(b["q_nopt"]) else 1
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def emd2(p: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
|
| 188 |
+
"""Squared earth mover's distance between ordinal distributions [N, K] (padded with zeros)."""
|
| 189 |
+
return (p.cumsum(-1) - t.cumsum(-1)).pow(2).sum(-1)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def soft_ce(logits: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
|
| 193 |
+
"""Cross-entropy against a (soft) target distribution; masked (-inf) logits must have zero target."""
|
| 194 |
+
logp = logits.log_softmax(-1).masked_fill(target == 0, 0)
|
| 195 |
+
return -(target * logp).sum(-1)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def brier(p: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
|
| 199 |
+
"""Half the squared distance between distributions [N, K]; padded options have p = t = 0."""
|
| 200 |
+
return 0.5 * (p - t).pow(2).sum(-1)
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def loss_fn(out: dict, b: dict, ordinal_weight: float = 1.0, brier_weight: float = 0.0,
|
| 204 |
+
type_weights: dict | None = None) -> tuple[torch.Tensor, dict]:
|
| 205 |
+
qt, has = b["q_type"], b["has_target"]
|
| 206 |
+
parts = {}
|
| 207 |
+
for typ in (NOUL, CHOICE, SCORE):
|
| 208 |
+
m = (qt == typ) & has
|
| 209 |
+
if m.any():
|
| 210 |
+
ce = soft_ce(out["opt"][m], b["t_opt"][m])
|
| 211 |
+
if brier_weight:
|
| 212 |
+
ce = ce + brier_weight * brier(out["opt"][m].softmax(-1), b["t_opt"][m])
|
| 213 |
+
if typ == SCORE:
|
| 214 |
+
ce = ce + ordinal_weight * emd2(out["opt"][m].softmax(-1), b["t_opt"][m])
|
| 215 |
+
parts[TYPES[typ]] = ce
|
| 216 |
+
m = (qt == MULTI) & has
|
| 217 |
+
if m.any():
|
| 218 |
+
logits, valid = out["opt"][m], out["opt"][m].isfinite()
|
| 219 |
+
bce = F.binary_cross_entropy_with_logits(logits.masked_fill(~valid, 0), b["t_opt"][m], reduction="none")
|
| 220 |
+
parts["multi"] = (bce * valid).sum(-1) / valid.sum(-1)
|
| 221 |
+
if len(out["span_q"]):
|
| 222 |
+
m = has[out["span_q"]]
|
| 223 |
+
if m.any():
|
| 224 |
+
parts["span"] = soft_ce(out["span"][m], b["t_span"][m]).mean(-1)
|
| 225 |
+
if len(out["region_q"]):
|
| 226 |
+
parts["region"] = soft_ce(out["region"], b["t_region"])
|
| 227 |
+
w = type_weights or {}
|
| 228 |
+
total = torch.cat([v * w.get(k, 1.0) for k, v in parts.items()]).mean()
|
| 229 |
+
return total, {k: v.mean().item() for k, v in parts.items()}
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def to_device(b: dict, device) -> dict:
|
| 233 |
+
return {k: v.to(device) if isinstance(v, torch.Tensor) else v for k, v in b.items()}
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def decode_span(start: torch.Tensor, end: torch.Tensor, p: Packed, length_penalty: float = 0.0,
|
| 237 |
+
max_tokens: int = MAX_SPAN_TOKENS):
|
| 238 |
+
"""Best valid span under independent start/end softmax. Returns (char_start, char_end, text, prob).
|
| 239 |
+
length_penalty: each token past the start scales a span's score by exp(-length_penalty). max_tokens: the longest
|
| 240 |
+
span considered.
|
| 241 |
+
|
| 242 |
+
The answer is always p.state[char_start:char_end]: it is cut from the input, never generated.
|
| 243 |
+
"""
|
| 244 |
+
n = len(p.offsets)
|
| 245 |
+
if not n or not start.isfinite().any():
|
| 246 |
+
return 0, 0, "", 0.0
|
| 247 |
+
ps, pe = start.softmax(-1)[1 : n + 1], end.softmax(-1)[1 : n + 1] # state text tokens only
|
| 248 |
+
K = min(max_tokens, n)
|
| 249 |
+
pe_pad = F.pad(pe, (0, K))
|
| 250 |
+
joint = torch.stack([ps * pe_pad[k : k + n] for k in range(K)], 1) # [start, length - 1]
|
| 251 |
+
joint = joint * torch.exp(-length_penalty * torch.arange(K, dtype=joint.dtype))
|
| 252 |
+
joint = joint / joint.sum().clamp(min=1e-12)
|
| 253 |
+
s, k = divmod(int(joint.argmax()), K)
|
| 254 |
+
prob = float(joint[s, k])
|
| 255 |
+
a, z = p.offsets[s][0], p.offsets[s + k][1]
|
| 256 |
+
raw = p.state[a:z]
|
| 257 |
+
a, z = a + len(raw) - len(raw.lstrip()), z - (len(raw) - len(raw.rstrip()))
|
| 258 |
+
return a, max(a, z), p.state[a : max(a, z)], prob
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def decode(out: dict, packs: list[Packed]) -> list[list[dict]]:
|
| 262 |
+
"""Turn a batch forward into one typed result per question, grouped per pack."""
|
| 263 |
+
opt = out["opt"].float().cpu()
|
| 264 |
+
span = out["span"].float().cpu() if "span" in out else None
|
| 265 |
+
region = out["region"].float().cpu() if "region" in out else None
|
| 266 |
+
res, qi, si, ri = [], 0, 0, 0
|
| 267 |
+
for p in packs:
|
| 268 |
+
rs = []
|
| 269 |
+
for t, n in zip(p.types, p.n_options):
|
| 270 |
+
if t == "noul":
|
| 271 |
+
rs.append({"type": t, "p_yes": opt[qi, :2].softmax(-1)[1].item()})
|
| 272 |
+
elif t in ("choice", "score"):
|
| 273 |
+
probs = opt[qi, :n].softmax(-1)
|
| 274 |
+
r = {"type": t, "probs": probs.tolist(), "choice": int(probs.argmax())}
|
| 275 |
+
if t == "score":
|
| 276 |
+
r["expected"] = float((probs * torch.arange(n)).sum())
|
| 277 |
+
rs.append(r)
|
| 278 |
+
elif t == "multi":
|
| 279 |
+
rs.append({"type": t, "probs": torch.sigmoid(opt[qi, :n]).tolist()})
|
| 280 |
+
elif t == "region":
|
| 281 |
+
cells = region[ri].softmax(-1)[p.image_at[0] : p.image_at[0] + IMAGE_TOKENS]
|
| 282 |
+
k = int(cells.argmax())
|
| 283 |
+
rs.append({"type": t, "cell": k, "box": cell_boxes()[k].tolist(), "p": float(cells[k]), "heatmap": cells.tolist()})
|
| 284 |
+
ri += 1
|
| 285 |
+
else:
|
| 286 |
+
a, z, text, prob = decode_span(span[si, 0], span[si, 1], p)
|
| 287 |
+
rs.append({"type": t, "start": a, "end": z, "text": text, "p": prob})
|
| 288 |
+
si += 1
|
| 289 |
+
qi += 1
|
| 290 |
+
res.append(rs)
|
| 291 |
+
return res
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
def load_run(run, device="cpu"):
|
| 295 |
+
"""A trained run directory (config.json + model.pt) -> (model in eval mode, its codec)."""
|
| 296 |
+
cfg = json.loads((Path(run) / "config.json").read_text())
|
| 297 |
+
enc = cfg["encoder"]
|
| 298 |
+
if cfg.get("readout", "pointer") != "pointer" or enc.get("question_reads") or enc.get("model_type", "modernbert") != "modernbert":
|
| 299 |
+
raise ValueError(f"{run}: trained with a removed mode (MLM readout, question reads or DeBERTa)")
|
| 300 |
+
codec = Codec(cfg["repo"], max_state=cfg["max_state"], digits=cfg.get("digits", False),
|
| 301 |
+
score_rank=cfg.get("score_rank", False))
|
| 302 |
+
vision = VisionTower.from_pretrained(cfg["vision"]) if cfg.get("vision") else None
|
| 303 |
+
model = Amos(Encoder(Config.from_hf(enc)), vision)
|
| 304 |
+
missing, unexpected = model.load_state_dict(torch.load(Path(run) / "model.pt", map_location="cpu", weights_only=True), strict=False)
|
| 305 |
+
if unexpected or any(not k.startswith("vision.") for k in missing):
|
| 306 |
+
raise ValueError(f"checkpoint mismatch: missing {missing}, unexpected {unexpected}")
|
| 307 |
+
return model.to(device).eval(), codec
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def pack_request(model: Amos, codec: Codec, state: str, questions: list[dict], images=(), *, strict=False) -> list[Packed]:
|
| 311 |
+
"""One pack holding every question (a list, so callers batch requests as rows)."""
|
| 312 |
+
if model.encoder.config.date_facts:
|
| 313 |
+
state = date_facts(state)
|
| 314 |
+
return [pack(codec, state, questions, images, strict=strict)]
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
@torch.no_grad()
|
| 318 |
+
def predict(model: Amos, codec: Codec, state: str, questions: list[dict], device="cpu", images=()) -> list[dict]:
|
| 319 |
+
"""Run one state with its questions; returns one typed result per question."""
|
| 320 |
+
ps = pack_request(model, codec, state, questions, images)
|
| 321 |
+
return [r for rs in decode(model(to_device(collate(ps, codec.pad), device)), ps) for r in rs]
|
serve/amos/packing.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pack one state and N typed questions into a single sequence.
|
| 2 |
+
|
| 3 |
+
Layout: [CLS] state text [IMG] image tokens [/IMG] [SEP] | [DECIDE_t] q1 [SEP] [OPT] o1 [OPT] o2 ... | ...
|
| 4 |
+
|
| 5 |
+
Attention tree (see Encoder.masks): state sees state; a question sees state + itself;
|
| 6 |
+
an option sees state + its question + itself. Positions restart after the state for
|
| 7 |
+
every question and after the question for every option, so results do not depend on
|
| 8 |
+
question order, option order, or which siblings share the pack.
|
| 9 |
+
|
| 10 |
+
Images (state images or image options) occupy IMAGE_TOKENS placeholder slots each; the model
|
| 11 |
+
fills those slots with projected vision features before the first encoder layer.
|
| 12 |
+
|
| 13 |
+
Structural tokens are inserted by id. User text is tokenized with special-token matching
|
| 14 |
+
disabled, and the reserved [unusedN] markers are made special, so no text can forge them.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import json
|
| 18 |
+
from dataclasses import dataclass, field
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
from huggingface_hub import hf_hub_download
|
| 22 |
+
from tokenizers import Tokenizer, pre_tokenizers
|
| 23 |
+
|
| 24 |
+
TYPES =("noul", "choice", "score", "multi", "span", "region")
|
| 25 |
+
OPTION_TYPES = ("choice", "score", "multi")
|
| 26 |
+
MAX_OPTIONS = 255
|
| 27 |
+
IMAGE_TOKENS = 49 # merged patch tokens per image (vision.MERGED)
|
| 28 |
+
# Fixed [unusedN] slots, so adding a marker never moves one a trained checkpoint already uses.
|
| 29 |
+
MARKERS = {"noul": 0, "choice": 1, "score": 2, "multi": 3, "span": 4, "opt": 5, "region": 6, "img": 7, "img_end": 8, "patch": 9}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class Codec:
|
| 33 |
+
def __init__(self, repo: str, max_state: int = 512, max_question: int = 128, max_option: int = 48,
|
| 34 |
+
digits: bool = False, score_rank: bool = False):
|
| 35 |
+
with open(hf_hub_download(repo, "tokenizer.json")) as f:
|
| 36 |
+
spec = json.load(f)
|
| 37 |
+
for t in spec["added_tokens"]:
|
| 38 |
+
if t["content"].startswith("[unused"):
|
| 39 |
+
t["special"] = True
|
| 40 |
+
self.tok = Tokenizer.from_str(json.dumps(spec))
|
| 41 |
+
if digits:
|
| 42 |
+
# BPE cuts numbers into arbitrary chunks (300020 -> "3000" "20"), so two numbers of the same size
|
| 43 |
+
# rarely line up. One token per digit lets the model compare them place by place.
|
| 44 |
+
self.tok.pre_tokenizer = pre_tokenizers.Sequence(
|
| 45 |
+
[pre_tokenizers.Digits(individual_digits=True), self.tok.pre_tokenizer])
|
| 46 |
+
self.digits = digits
|
| 47 |
+
self.score_rank = score_rank
|
| 48 |
+
self.tok.encode_special_tokens = True # literal "[SEP]" / "[unused0]" in text stays plain text
|
| 49 |
+
self.tok.no_truncation()
|
| 50 |
+
self.tok.no_padding()
|
| 51 |
+
tid = self.tok.token_to_id
|
| 52 |
+
self.cls, self.sep, self.pad = tid("[CLS]"), tid("[SEP]"), tid("[PAD]")
|
| 53 |
+
marker = {k: tid(f"[unused{i}]") for k, i in MARKERS.items()}
|
| 54 |
+
assert None not in (self.cls, self.sep, self.pad, *marker.values()), f"{repo}: special tokens missing"
|
| 55 |
+
self.decide = {t: marker[t] for t in TYPES}
|
| 56 |
+
self.opt, self.img, self.img_end, self.patch = marker["opt"], marker["img"], marker["img_end"], marker["patch"]
|
| 57 |
+
self.reserved = {self.cls, self.sep, self.pad, *marker.values()}
|
| 58 |
+
self.max_state, self.max_question, self.max_option = max_state, max_question, max_option
|
| 59 |
+
|
| 60 |
+
def encode(self, text: str) -> list[int]:
|
| 61 |
+
return self.tok.encode(text, add_special_tokens=False).ids
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@dataclass
|
| 65 |
+
class Packed:
|
| 66 |
+
ids: list[int] = field(default_factory=list)
|
| 67 |
+
pos: list[int] = field(default_factory=list)
|
| 68 |
+
seg: list[int] = field(default_factory=list) # 0 state, k question k
|
| 69 |
+
opt: list[int] = field(default_factory=list) # 0 not an option, m option m of its question
|
| 70 |
+
decide: list[int] = field(default_factory=list) # token index of each question's DECIDE token
|
| 71 |
+
types: list[str] = field(default_factory=list)
|
| 72 |
+
n_options: list[int] = field(default_factory=list)
|
| 73 |
+
offsets: list[tuple[int, int]] = field(default_factory=list) # char offsets of state text tokens 1..n
|
| 74 |
+
state: str = ""
|
| 75 |
+
images: list = field(default_factory=list) # pixels [3, H, W] or vision features [patches, d], in slot order
|
| 76 |
+
image_at: list[int] = field(default_factory=list) # token index of each image's first placeholder
|
| 77 |
+
|
| 78 |
+
def __len__(self):
|
| 79 |
+
return len(self.ids)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def noul_options(q: dict) -> list[str]:
|
| 83 |
+
"""noul is scored as two implicit options, carrying the optional criteria descriptions."""
|
| 84 |
+
c = q.get("criteria") or {}
|
| 85 |
+
return [f"no: {c['false']}" if c.get("false") else "no", f"yes: {c['true']}" if c.get("true") else "yes"]
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _image_tokens(codec: Codec, p: Packed, image, start: int) -> list[int]:
|
| 89 |
+
p.images.append(image)
|
| 90 |
+
p.image_at.append(start + 1)
|
| 91 |
+
return [codec.img, *[codec.patch] * IMAGE_TOKENS, codec.img_end]
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def pack(codec: Codec, state: str, questions: list[dict], images=(), *, strict: bool = False) -> Packed:
|
| 95 |
+
"""questions: [{"type": ..., "text": ..., "options": [...], "criteria": {...}}].
|
| 96 |
+
|
| 97 |
+
options: required for choice/score/multi (score: ordered low to high); an option may be an image
|
| 98 |
+
tensor instead of text. criteria: optional {"true": ..., "false": ...} for noul. span and region
|
| 99 |
+
take neither. images: state images, placed after the state text.
|
| 100 |
+
"""
|
| 101 |
+
enc = codec.tok.encode(state, add_special_tokens=False)
|
| 102 |
+
if strict and len(enc.ids) > codec.max_state:
|
| 103 |
+
raise ValueError(f"maximum context length: state has {len(enc.ids)} tokens, limit {codec.max_state}")
|
| 104 |
+
keep = min(len(enc.ids), codec.max_state)
|
| 105 |
+
p = Packed(state=state, offsets=enc.offsets[:keep])
|
| 106 |
+
p.ids = [codec.cls, *enc.ids[:keep]]
|
| 107 |
+
for im in images:
|
| 108 |
+
p.ids += _image_tokens(codec, p, im, len(p.ids))
|
| 109 |
+
p.ids.append(codec.sep)
|
| 110 |
+
n = len(p.ids)
|
| 111 |
+
p.pos, p.seg, p.opt = list(range(n)), [0] * n, [0] * n
|
| 112 |
+
for k, q in enumerate(questions, start=1):
|
| 113 |
+
t = q["type"]
|
| 114 |
+
if t not in TYPES:
|
| 115 |
+
raise ValueError(f"unknown question type {t!r}")
|
| 116 |
+
if t == "region" and not images:
|
| 117 |
+
raise ValueError("region questions need a state image")
|
| 118 |
+
options = noul_options(q) if t == "noul" else q.get("options") or []
|
| 119 |
+
if t != "noul" and ((t in OPTION_TYPES) != bool(options) or len(options) > MAX_OPTIONS):
|
| 120 |
+
raise ValueError(f"{t} question needs {'1-255' if t in OPTION_TYPES else 'no'} options")
|
| 121 |
+
text = codec.encode(q["text"])
|
| 122 |
+
if not strict:
|
| 123 |
+
text = text[: codec.max_question]
|
| 124 |
+
branch = [codec.decide[t], *text, codec.sep]
|
| 125 |
+
p.decide.append(len(p.ids))
|
| 126 |
+
p.types.append(t)
|
| 127 |
+
p.n_options.append(len(options))
|
| 128 |
+
p.ids += branch
|
| 129 |
+
p.pos += range(n, n + len(branch))
|
| 130 |
+
p.seg += [k] * len(branch)
|
| 131 |
+
p.opt += [0] * len(branch)
|
| 132 |
+
o_start = n + len(branch)
|
| 133 |
+
for m, o in enumerate(options, start=1):
|
| 134 |
+
if isinstance(o, str):
|
| 135 |
+
if t == "score" and codec.score_rank:
|
| 136 |
+
o = f"Level {m - 1} of {len(options) - 1}: {o}"
|
| 137 |
+
text = codec.encode(o)
|
| 138 |
+
toks = [codec.opt, *(text if strict else text[: codec.max_option])]
|
| 139 |
+
else:
|
| 140 |
+
toks = [codec.opt, *_image_tokens(codec, p, o, len(p.ids))]
|
| 141 |
+
p.ids += toks
|
| 142 |
+
p.pos += range(o_start, o_start + len(toks))
|
| 143 |
+
p.seg += [k] * len(toks)
|
| 144 |
+
p.opt += [m] * len(toks)
|
| 145 |
+
return p
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def char_span_to_tokens(offsets, start: int, end: int):
|
| 149 |
+
"""Map a char span [start, end) of the state to inclusive state-token indices (1-based), or None."""
|
| 150 |
+
toks = [i for i, (a, b) in enumerate(offsets, start=1) if a < end and b > start]
|
| 151 |
+
return (toks[0], toks[-1]) if toks else None
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
PAD_TO = 64 # rows are padded to a multiple of this: few distinct shapes, so few cached kernels/graphs
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def padded(n: int) -> int:
|
| 158 |
+
return -(-n // PAD_TO) * PAD_TO
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def batches(lengths: list[int], budget: int, rng=None, chunk: int = 4096) -> list[list[int]]:
|
| 162 |
+
"""Group indices into batches with rows * L <= budget and rows * L^2 <= budget * 512 (L = padded longest row).
|
| 163 |
+
|
| 164 |
+
The second bound caps attention memory, which grows with L^2. Sorting by length limits padding;
|
| 165 |
+
with an rng, sorting happens within random chunks and the batch order is shuffled, so batches
|
| 166 |
+
differ every epoch.
|
| 167 |
+
"""
|
| 168 |
+
idx = list(range(len(lengths)))
|
| 169 |
+
if rng:
|
| 170 |
+
rng.shuffle(idx)
|
| 171 |
+
else:
|
| 172 |
+
chunk = len(idx) or 1
|
| 173 |
+
|
| 174 |
+
def fits(rows, longest):
|
| 175 |
+
L = padded(longest)
|
| 176 |
+
return rows * L <= budget and rows * L * L <= budget * 512
|
| 177 |
+
|
| 178 |
+
out = []
|
| 179 |
+
for c in range(0, len(idx), chunk):
|
| 180 |
+
cur, longest = [], 0
|
| 181 |
+
for i in sorted(idx[c : c + chunk], key=lengths.__getitem__):
|
| 182 |
+
if cur and not fits(len(cur) + 1, max(longest, lengths[i])):
|
| 183 |
+
out.append(cur)
|
| 184 |
+
cur, longest = [], 0
|
| 185 |
+
cur.append(i)
|
| 186 |
+
longest = max(longest, lengths[i])
|
| 187 |
+
if cur:
|
| 188 |
+
out.append(cur)
|
| 189 |
+
if rng:
|
| 190 |
+
rng.shuffle(out)
|
| 191 |
+
return out
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def collate(packs: list[Packed], pad_id: int) -> dict:
|
| 195 |
+
"""Right-pad packs into [B, L] tensors and index every question, option, and image in the batch."""
|
| 196 |
+
B, L = len(packs), padded(max(len(p) for p in packs))
|
| 197 |
+
t = {k: torch.full((B, L), v, dtype=torch.long) for k, v in [("ids", pad_id), ("pos", 0), ("seg", -1), ("opt", 0)]}
|
| 198 |
+
t["option_of"] = torch.full((B, L), -1, dtype=torch.long) # global option index per token
|
| 199 |
+
t["state_mask"] = torch.zeros(B, L, dtype=torch.bool) # span boundary candidates: non-blank state tokens
|
| 200 |
+
t["region_mask"] = torch.zeros(B, L, dtype=torch.bool) # region candidates: first state image's tokens
|
| 201 |
+
q_row, q_tok, q_type, q_nopt, o_q, o_rank, images, img_row, img_at = [], [], [], [], [], [], [], [], []
|
| 202 |
+
for b, p in enumerate(packs):
|
| 203 |
+
n = len(p)
|
| 204 |
+
for k in ("ids", "pos", "seg", "opt"):
|
| 205 |
+
t[k][b, :n] = torch.tensor(getattr(p, k))
|
| 206 |
+
t["state_mask"][b, 1 : 1 + len(p.offsets)] = torch.tensor([bool(p.state[a:z].strip()) for a, z in p.offsets])
|
| 207 |
+
if p.image_at and p.seg[p.image_at[0]] == 0:
|
| 208 |
+
t["region_mask"][b, p.image_at[0] : p.image_at[0] + IMAGE_TOKENS] = True
|
| 209 |
+
images += p.images
|
| 210 |
+
img_row += [b] * len(p.images)
|
| 211 |
+
img_at += p.image_at
|
| 212 |
+
base = []
|
| 213 |
+
for tok, typ, n_opt in zip(p.decide, p.types, p.n_options):
|
| 214 |
+
base.append(len(o_q))
|
| 215 |
+
o_q += [len(q_row)] * n_opt
|
| 216 |
+
o_rank += range(n_opt)
|
| 217 |
+
q_row.append(b), q_tok.append(tok), q_type.append(TYPES.index(typ)), q_nopt.append(n_opt)
|
| 218 |
+
t["option_of"][b, :n] = torch.tensor([base[s - 1] + o - 1 if o else -1 for s, o in zip(p.seg, p.opt)])
|
| 219 |
+
for k, v in [("q_row", q_row), ("q_tok", q_tok), ("q_type", q_type), ("q_nopt", q_nopt), ("o_q", o_q),
|
| 220 |
+
("o_rank", o_rank), ("img_row", img_row), ("img_at", img_at)]:
|
| 221 |
+
t[k] = torch.tensor(v, dtype=torch.long)
|
| 222 |
+
if images:
|
| 223 |
+
t["images"] = torch.stack([im.float() for im in images])
|
| 224 |
+
return t
|
serve/amos/py.typed
ADDED
|
File without changes
|
serve/amos/serve.py
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Local HTTP server: POST /v1/systemone.
|
| 2 |
+
|
| 3 |
+
uv run python -m amos.serve runs/distilled --port 8000
|
| 4 |
+
uv run python -m amos.serve runs/distilled --bench
|
| 5 |
+
uv run python -m amos.serve runs/dec --device cuda # a decoder run (RUN/model/config.json) is detected
|
| 6 |
+
uv run python -m amos.serve runs/dec --also amos-1-reason=runs/reason # a second model, picked by "model"
|
| 7 |
+
|
| 8 |
+
Request:
|
| 9 |
+
{"state": str | object | array,
|
| 10 |
+
"images": ["<base64 image>", ...], # optional state images (vision runs)
|
| 11 |
+
"questions": {"<id>": {"type": "noul" | "choice" | "score" | "multi" | "span" | "region",
|
| 12 |
+
"instructions": str | object | array,
|
| 13 |
+
"criteria": ...}},
|
| 14 |
+
"abstain": 0.1} # optional: conformal sets at this alpha
|
| 15 |
+
|
| 16 |
+
criteria: noul {"true": ..., "false": ...} (optional); choice and multi {"<option>": description | null |
|
| 17 |
+
{"image": "<base64>"}}; score [level descriptions, lowest first]; span and region none. Objects and
|
| 18 |
+
arrays are rendered as JSON text. Encoded images are cached by content hash.
|
| 19 |
+
|
| 20 |
+
Response: {"model", "answers": {"<id>": {...}}, "usage": {"input_tokens", "output_tokens": 0}}
|
| 21 |
+
noul {"type", "noul": P(yes)}
|
| 22 |
+
choice {"type", "choice", "probabilities": {option: p}, "confidence"}
|
| 23 |
+
score {"type", "score": expected level, "legend": {"0": ...}, "probabilities": {"0": p}, "confidence"}
|
| 24 |
+
multi {"type", "probabilities": {option: p}}
|
| 25 |
+
span {"type", "span": text, "start", "end", "confidence"}
|
| 26 |
+
region {"type", "box": [x0, y0, x1, y1] normalized, "cell": 0-48 (a decoder: its image's row-major token grid),
|
| 27 |
+
"confidence"}
|
| 28 |
+
A decoder run answers what its checkpoint supports (/v1/info card.types) and refuses the rest, and abstain, with 422.
|
| 29 |
+
With "abstain", noul/choice/score answers add "set" (options kept at that alpha) and "abstain" (set size != 1).
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
import argparse
|
| 33 |
+
import base64
|
| 34 |
+
import hashlib
|
| 35 |
+
import io
|
| 36 |
+
import json
|
| 37 |
+
import math
|
| 38 |
+
import mimetypes
|
| 39 |
+
import os
|
| 40 |
+
import queue
|
| 41 |
+
import re
|
| 42 |
+
import shutil
|
| 43 |
+
import sys
|
| 44 |
+
import threading
|
| 45 |
+
import time
|
| 46 |
+
import traceback
|
| 47 |
+
import urllib.request
|
| 48 |
+
from collections import OrderedDict
|
| 49 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 50 |
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
| 51 |
+
from pathlib import Path
|
| 52 |
+
from urllib.parse import parse_qs, urlsplit
|
| 53 |
+
|
| 54 |
+
import torch
|
| 55 |
+
|
| 56 |
+
from .calibrate import apply
|
| 57 |
+
from .model import decode, pack_request, to_device
|
| 58 |
+
from .packing import MAX_OPTIONS, TYPES, batches, collate, pack
|
| 59 |
+
|
| 60 |
+
STATE_CACHE_TOKENS = 16384 # cached state tokens per engine, across all cached states
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class BadRequest(ValueError):
|
| 64 |
+
pass
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def render(x) -> str:
|
| 68 |
+
return x if isinstance(x, str) else json.dumps(x, indent=2, ensure_ascii=False)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def image_bytes(x: str) -> bytes:
|
| 72 |
+
"""An image as sent: plain base64, or a data URL ("data:image/png;base64,...")."""
|
| 73 |
+
return base64.b64decode(x.split(",", 1)[1] if x.startswith("data:") else x)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def parse(req: dict):
|
| 77 |
+
"""Validate a request -> (state text, [question id], [internal question])."""
|
| 78 |
+
if not isinstance(req, dict) or "state" not in req or not isinstance(req.get("questions"), dict) or not req["questions"]:
|
| 79 |
+
raise BadRequest("need 'state' and a non-empty 'questions' object")
|
| 80 |
+
ids, qs = [], []
|
| 81 |
+
for qid, q in req["questions"].items():
|
| 82 |
+
if not isinstance(q, dict) or q.get("type") not in TYPES or "instructions" not in q:
|
| 83 |
+
raise BadRequest(f"question {qid!r}: need 'type' in {list(TYPES)} and 'instructions'")
|
| 84 |
+
t, c = q["type"], q.get("criteria")
|
| 85 |
+
out = {"type": t, "text": render(q["instructions"])}
|
| 86 |
+
if t == "noul" and c is not None:
|
| 87 |
+
if not isinstance(c, dict):
|
| 88 |
+
raise BadRequest(f"question {qid!r}: noul criteria must be an object")
|
| 89 |
+
out["criteria"] = {k: render(v) for k, v in c.items() if k in ("true", "false") and v is not None}
|
| 90 |
+
elif t in ("choice", "multi"):
|
| 91 |
+
if not isinstance(c, dict) or not 2 <= len(c) <= MAX_OPTIONS:
|
| 92 |
+
raise BadRequest(f"question {qid!r}: {t} criteria must map 2-{MAX_OPTIONS} options to descriptions")
|
| 93 |
+
out["names"] = list(c)
|
| 94 |
+
out["options"] = [k if v is None else v if isinstance(v, dict) and "image" in v else f"{k}: {render(v)}"
|
| 95 |
+
for k, v in c.items()]
|
| 96 |
+
elif t == "score":
|
| 97 |
+
if not isinstance(c, list) or not 2 <= len(c) <= MAX_OPTIONS:
|
| 98 |
+
raise BadRequest(f"question {qid!r}: score criteria must be a list of 2-{MAX_OPTIONS} levels")
|
| 99 |
+
out["options"] = [render(v) for v in c]
|
| 100 |
+
ids.append(qid)
|
| 101 |
+
qs.append(out)
|
| 102 |
+
return render(req["state"]), ids, qs
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def free_gpu_cache(min_unused_bytes: int = 1 << 30) -> bool:
|
| 106 |
+
"""Hand the CUDA caching allocator's unused blocks back to the device when they pass min_unused_bytes. #3 serves
|
| 107 |
+
two models on one GPU: a long request leaves the large holding gigabytes it no longer uses, and the medium then
|
| 108 |
+
runs out of memory on a long request of its own (2026-10-09, JevBench's hard tier). True when it freed."""
|
| 109 |
+
try:
|
| 110 |
+
import torch
|
| 111 |
+
except ImportError:
|
| 112 |
+
return False
|
| 113 |
+
if not torch.cuda.is_available() or torch.cuda.memory_reserved() - torch.cuda.memory_allocated() < min_unused_bytes:
|
| 114 |
+
return False
|
| 115 |
+
torch.cuda.empty_cache()
|
| 116 |
+
return True
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def shift(probs, cut: float) -> list[float]:
|
| 120 |
+
"""Pick-any probabilities moved on the logit scale so 0.5 falls where the fitted cut was: p >= cut <=> p' >= 0.5."""
|
| 121 |
+
b = math.log(cut / (1 - cut))
|
| 122 |
+
return [1 / (1 + math.exp(b - math.log(max(p, 1e-9) / max(1 - p, 1e-9)))) for p in probs]
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def choice_confidence(probs):
|
| 126 |
+
k = len(probs)
|
| 127 |
+
return (max(probs) - 1 / k) / (1 - 1 / k)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def score_confidence(probs):
|
| 131 |
+
L = len(probs)
|
| 132 |
+
mode = max(range(L), key=probs.__getitem__)
|
| 133 |
+
spread = sum(abs(i - (L - 1) / 2) for i in range(L)) / L
|
| 134 |
+
return max(0.0, 1 - sum(p * abs(i - mode) for i, p in enumerate(probs)) / spread)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def answer(q: dict, r: dict) -> dict:
|
| 138 |
+
t = q["type"]
|
| 139 |
+
if t == "noul":
|
| 140 |
+
a = {"type": t, "noul": r["p_yes"]}
|
| 141 |
+
elif t == "choice":
|
| 142 |
+
a = {"type": t, "choice": q["names"][r["choice"]], "probabilities": dict(zip(q["names"], r["probs"])),
|
| 143 |
+
"confidence": choice_confidence(r["probs"])}
|
| 144 |
+
elif t == "score":
|
| 145 |
+
a = {"type": t, "score": r["expected"], "legend": {str(i): o for i, o in enumerate(q["options"])},
|
| 146 |
+
"probabilities": {str(i): p for i, p in enumerate(r["probs"])}, "confidence": score_confidence(r["probs"])}
|
| 147 |
+
elif t == "multi":
|
| 148 |
+
a = {"type": t, "probabilities": dict(zip(q["names"], r["probs"]))}
|
| 149 |
+
elif t == "region":
|
| 150 |
+
a = {"type": t, "box": r["box"], "cell": r["cell"], "confidence": r["p"]}
|
| 151 |
+
else:
|
| 152 |
+
a = {"type": t, "span": r["text"], "start": r["start"], "end": r["end"], "confidence": r["p"]}
|
| 153 |
+
if "set" in r:
|
| 154 |
+
labels = [False, True] if t == "noul" else q.get("names") or [str(i) for i in range(len(q["options"]))]
|
| 155 |
+
a["set"], a["abstain"] = [labels[i] for i in r["set"]], r["abstain"]
|
| 156 |
+
return a
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class Engine:
|
| 160 |
+
def __init__(self, run: str, device: str, name: str | None = None, *, strict: bool = False,
|
| 161 |
+
state_cache: bool = True):
|
| 162 |
+
from .model import load_run
|
| 163 |
+
|
| 164 |
+
self.model, self.codec = load_run(run, device)
|
| 165 |
+
self.device, self.run = device, run
|
| 166 |
+
self.vision = self.model.vision is not None
|
| 167 |
+
self.strict = strict
|
| 168 |
+
self.use_state_cache = state_cache
|
| 169 |
+
cal = Path(run) / "calibration.json"
|
| 170 |
+
report = json.loads(cal.read_text()) if cal.exists() else {}
|
| 171 |
+
self.cal = report.get("calibration")
|
| 172 |
+
cfg = Path(run) / "config.json"
|
| 173 |
+
cfg = json.loads(cfg.read_text()) if cfg.exists() else {}
|
| 174 |
+
enc, args = cfg.get("encoder", {}), cfg.get("args", {})
|
| 175 |
+
self.strict = strict or args.get("strict_inputs", False)
|
| 176 |
+
self.max_len = args.get("max_len", self.codec.max_state + 1024)
|
| 177 |
+
size = (cfg.get("repo") or "").rsplit("-", 1)[-1] # jhu-clsp/ettin-encoder-32m -> 32m
|
| 178 |
+
self.name = name or {"32m": "amos-1-mini", "68m": "amos-1"}.get(size, f"amos-1-{size}")
|
| 179 |
+
self.card = { # what /v1/info reports about the model
|
| 180 |
+
"base": cfg.get("repo"),
|
| 181 |
+
"parameters": sum(p.numel() for p in self.model.parameters()),
|
| 182 |
+
"layers": enc.get("num_hidden_layers"),
|
| 183 |
+
"hidden": enc.get("hidden_size"),
|
| 184 |
+
"heads": enc.get("num_attention_heads"),
|
| 185 |
+
"max_state": cfg.get("max_state"),
|
| 186 |
+
"max_len": args.get("max_len"),
|
| 187 |
+
"strict_inputs": self.strict,
|
| 188 |
+
"compare_options": self.model.encoder.config.compare_options,
|
| 189 |
+
# a run may have trained a region head yet be served without a vision tower: only then can it point
|
| 190 |
+
"types": [*(t for t in args.get("types", "").split(",") if t and t != "region"),
|
| 191 |
+
*(["region"] if self.model.vision else [])],
|
| 192 |
+
"temperature": (self.cal or {}).get("temperature", {}),
|
| 193 |
+
"held_out": report.get("test_held_out", {}),
|
| 194 |
+
"in_distribution": report.get("test_in_distribution", {}),
|
| 195 |
+
"benchmarks": {},
|
| 196 |
+
}
|
| 197 |
+
td = Path(run) / "typed_decisions.json" # written by `python -m amos.typed_decisions RUN --out ...`
|
| 198 |
+
if td.exists():
|
| 199 |
+
day = time.strftime("%Y-%m-%d", time.localtime(td.stat().st_mtime))
|
| 200 |
+
self.card["benchmarks"]["typed_decisions"] = {"measured": day, **json.loads(td.read_text())["all"]}
|
| 201 |
+
self.lock = threading.Lock() # one forward at a time; requests queue on it
|
| 202 |
+
self.image_cache: OrderedDict[str, torch.Tensor] = OrderedDict() # content hash -> vision features
|
| 203 |
+
# state + image hashes -> encoded state; bounded by cached tokens, since one 8k-token state is ~2 GB of keys
|
| 204 |
+
# and values on a 400m encoder
|
| 205 |
+
self.state_cache: OrderedDict[str, dict] = OrderedDict()
|
| 206 |
+
|
| 207 |
+
def image(self, data: str) -> torch.Tensor:
|
| 208 |
+
"""Base64 image -> frozen-tower features, cached by content hash (LRU, 1024 images)."""
|
| 209 |
+
from PIL import Image, UnidentifiedImageError
|
| 210 |
+
|
| 211 |
+
from .vision import preprocess
|
| 212 |
+
|
| 213 |
+
if self.model.vision is None:
|
| 214 |
+
raise BadRequest("this model has no vision tower")
|
| 215 |
+
key = hashlib.sha256(data.encode()).hexdigest()
|
| 216 |
+
if key not in self.image_cache:
|
| 217 |
+
try:
|
| 218 |
+
pixels = preprocess(Image.open(io.BytesIO(image_bytes(data))))
|
| 219 |
+
except (ValueError, UnidentifiedImageError) as e:
|
| 220 |
+
raise BadRequest(f"could not decode image: {e}") from e
|
| 221 |
+
self.image_cache[key] = self.model.vision(pixels[None].to(self.device))[0]
|
| 222 |
+
if len(self.image_cache) > 1024:
|
| 223 |
+
self.image_cache.popitem(last=False)
|
| 224 |
+
self.image_cache.move_to_end(key)
|
| 225 |
+
return self.image_cache[key]
|
| 226 |
+
|
| 227 |
+
@torch.no_grad()
|
| 228 |
+
def __call__(self, req: dict) -> dict:
|
| 229 |
+
state, ids, qs = parse(req)
|
| 230 |
+
alpha = req.get("abstain")
|
| 231 |
+
if alpha is not None and (self.cal is None or str(alpha) not in self.cal["qhat"].get("choice", {})):
|
| 232 |
+
raise BadRequest(f"abstain={alpha!r} needs a calibration.json fitted at that alpha")
|
| 233 |
+
with self.lock:
|
| 234 |
+
images = [self.image(x) for x in req.get("images") or []]
|
| 235 |
+
qs_in = [{**q, "options": [self.image(o["image"]) if isinstance(o, dict) else o for o in q["options"]]}
|
| 236 |
+
if "options" in q else q for q in qs]
|
| 237 |
+
try:
|
| 238 |
+
ps = pack_request(self.model, self.codec, state, qs_in, images, strict=self.strict)
|
| 239 |
+
(p,) = ps
|
| 240 |
+
if self.strict and len(p) > self.max_len:
|
| 241 |
+
raise ValueError(f"maximum context length: request has {len(p)} tokens, limit {self.max_len}")
|
| 242 |
+
except ValueError as e:
|
| 243 |
+
raise BadRequest(str(e)) from e
|
| 244 |
+
key = hashlib.sha256(json.dumps([state, [hashlib.sha256(x.encode()).hexdigest() for x in req.get("images") or []]]).encode()).hexdigest()
|
| 245 |
+
batch = to_device(collate(ps, self.codec.pad), self.device)
|
| 246 |
+
if self.use_state_cache:
|
| 247 |
+
out, self.state_cache[key] = self.model.forward_cached(
|
| 248 |
+
batch, p.decide[0], self.state_cache.get(key))
|
| 249 |
+
self.state_cache.move_to_end(key)
|
| 250 |
+
while len(self.state_cache) > 1 and (
|
| 251 |
+
len(self.state_cache) > 32
|
| 252 |
+
or sum(c["pos"].shape[1] for c in self.state_cache.values()) > STATE_CACHE_TOKENS):
|
| 253 |
+
self.state_cache.popitem(last=False)
|
| 254 |
+
else:
|
| 255 |
+
out = self.model(batch)
|
| 256 |
+
results = [r for rs in decode(out, ps) for r in rs]
|
| 257 |
+
if self.cal:
|
| 258 |
+
results = [apply(r, self.cal, alpha if r["type"] in ("noul", "choice", "score") else None) for r in results]
|
| 259 |
+
return {"model": self.name, "answers": {i: answer(q, r) for i, q, r in zip(ids, qs, results)},
|
| 260 |
+
"usage": {"input_tokens": len(p), "output_tokens": 0, "state_tokens": p.decide[0],
|
| 261 |
+
"branch_tokens": {i: p.seg.count(k) for k, i in enumerate(ids, start=1)}}}
|
| 262 |
+
|
| 263 |
+
@torch.no_grad()
|
| 264 |
+
def explain(self, req: dict) -> dict:
|
| 265 |
+
"""How much each sentence of the state moves one question's answer.
|
| 266 |
+
|
| 267 |
+
Every variant of the state with one sentence removed is packed as its own row and all rows run in a
|
| 268 |
+
single batched forward pass. Influence = answer score with the full state minus the score without
|
| 269 |
+
that sentence (positive: the sentence supports the current answer).
|
| 270 |
+
"""
|
| 271 |
+
state, ids, qs = parse(req)
|
| 272 |
+
qid = req.get("question")
|
| 273 |
+
if qid not in ids:
|
| 274 |
+
raise BadRequest("'question' must name one of the questions")
|
| 275 |
+
q = qs[ids.index(qid)]
|
| 276 |
+
if q["type"] == "region":
|
| 277 |
+
raise BadRequest("explanations cover text states; region questions are not supported")
|
| 278 |
+
sents = [(m.start(), m.end()) for m in re.finditer(r"[^\n.!?]+[.!?]*[\"'”’)]*", state) if m.group().strip()][:64]
|
| 279 |
+
variants = [state] + [state[:a] + " " * (b - a) + state[b:] for a, b in sents] # blank, keep offsets
|
| 280 |
+
with self.lock:
|
| 281 |
+
images = [self.image(x) for x in req.get("images") or []]
|
| 282 |
+
q_in = {**q, "options": [self.image(o["image"]) if isinstance(o, dict) else o for o in q["options"]]} if "options" in q else q
|
| 283 |
+
packs = [pack(self.codec, s, [q_in], images) for s in variants]
|
| 284 |
+
res = [None] * len(packs)
|
| 285 |
+
for idx in batches([len(p) for p in packs], 24000): # 65 variants of an 8k state do not fit in one batch
|
| 286 |
+
chunk = [packs[i] for i in idx]
|
| 287 |
+
for i, r in zip(idx, decode(self.model(to_device(collate(chunk, self.codec.pad), self.device)), chunk)):
|
| 288 |
+
res[i] = r[0]
|
| 289 |
+
|
| 290 |
+
def score(r):
|
| 291 |
+
if r["type"] == "noul":
|
| 292 |
+
return r["p_yes"]
|
| 293 |
+
if r["type"] in ("choice", "score"):
|
| 294 |
+
return r["probs"][res[0]["choice"]]
|
| 295 |
+
if r["type"] == "multi":
|
| 296 |
+
return sum(r["probs"]) / len(r["probs"])
|
| 297 |
+
return r["p"] if r["text"] == res[0]["text"] else 0.0
|
| 298 |
+
|
| 299 |
+
base = score(res[0])
|
| 300 |
+
return {"question": qid, "base": base,
|
| 301 |
+
"sentences": [{"start": a, "end": b, "influence": base - score(r)} for (a, b), r in zip(sents, res[1:])]}
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
class DecoderEngine:
|
| 305 |
+
"""A decoder run (amos.decoder: RUN/model is a Hugging Face checkpoint) behind the same API. Yes/no, choice and
|
| 306 |
+
score read option letters, pick-any asks one yes/no per option in the same prompt; state images and image options
|
| 307 |
+
need a checkpoint with a vision tower (decoder vision-merge); quote and point need trained heads."""
|
| 308 |
+
|
| 309 |
+
MAX_LEN = 16384 # tokens per request, images included; inputs are never shortened
|
| 310 |
+
|
| 311 |
+
def __init__(self, run: str, device: str, name: str | None = None, max_len: int | None = None,
|
| 312 |
+
rotations: int = 1, think_below: float | None = None, think_tokens: int = 512):
|
| 313 |
+
from .decoder import (
|
| 314 |
+
calibration,
|
| 315 |
+
load_heads,
|
| 316 |
+
load_model,
|
| 317 |
+
multi_cut,
|
| 318 |
+
span_decoding,
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
self.multi_cut = multi_cut(run)
|
| 322 |
+
self.think_below, self.think_tokens = think_below, think_tokens # decoder.results: think when unsure
|
| 323 |
+
self.MAX_LEN = max_len or self.MAX_LEN
|
| 324 |
+
path = Path(run) / "model"
|
| 325 |
+
self.model, self.codec = load_model(str(path), device)
|
| 326 |
+
self.model.eval()
|
| 327 |
+
self.heads = load_heads(path, self.model, self.codec)
|
| 328 |
+
if self.heads is not None:
|
| 329 |
+
self.heads.span = span_decoding(run) # the run's own quote cap and penalty, if fitted
|
| 330 |
+
self.rotations, self.temperature = rotations, calibration(run) # temperatures from `decoder calibrate`, if fitted
|
| 331 |
+
self.device, self.run, self.cal = device, run, None
|
| 332 |
+
self.vision = self.codec.image_processor is not None
|
| 333 |
+
self.autocast = device == "cuda" and getattr(self.model.config, "amos_export_dtype", None) != "float32"
|
| 334 |
+
cfg = Path(run) / "config.json"
|
| 335 |
+
cfg = json.loads(cfg.read_text()) if cfg.exists() else {}
|
| 336 |
+
text = getattr(self.model.config, "text_config", self.model.config)
|
| 337 |
+
self.name = name or f"amos-1-{(cfg.get('base') or 'decoder').rsplit('-', 1)[-1].lower()}"
|
| 338 |
+
heads = self.heads.kinds if self.heads is not None else []
|
| 339 |
+
self.types = ["noul", "choice", "score", "multi", *(k for k in heads if k == "span" or self.vision)]
|
| 340 |
+
self.card = {
|
| 341 |
+
"base": cfg.get("base"),
|
| 342 |
+
"parameters": sum(p.numel() for p in self.model.parameters()),
|
| 343 |
+
"layers": text.num_hidden_layers,
|
| 344 |
+
"hidden": text.hidden_size,
|
| 345 |
+
"heads": text.num_attention_heads,
|
| 346 |
+
"max_state": None,
|
| 347 |
+
"max_len": self.MAX_LEN,
|
| 348 |
+
"strict_inputs": True,
|
| 349 |
+
"compare_options": None,
|
| 350 |
+
"types": self.types,
|
| 351 |
+
"explain": False, # no /v1/explain for decoder runs
|
| 352 |
+
"temperature": self.temperature or {},
|
| 353 |
+
"rotations": self.rotations,
|
| 354 |
+
"held_out": {},
|
| 355 |
+
"in_distribution": {},
|
| 356 |
+
"benchmarks": {},
|
| 357 |
+
}
|
| 358 |
+
td = Path(run) / "typed_decisions.json"
|
| 359 |
+
if td.exists():
|
| 360 |
+
day = time.strftime("%Y-%m-%d", time.localtime(td.stat().st_mtime))
|
| 361 |
+
self.card["benchmarks"]["typed_decisions"] = {"measured": day, **json.loads(td.read_text())["all"]}
|
| 362 |
+
self.lock = threading.Lock()
|
| 363 |
+
# thoughts written by a vLLM server serving the same checkpoint (AMOS_THINK_URL, e.g. http://10.0.0.5:8100):
|
| 364 |
+
# batched across requests there, while the letters are still read here; this model is free meanwhile
|
| 365 |
+
self.think_url = os.environ.get("AMOS_THINK_URL")
|
| 366 |
+
|
| 367 |
+
def remote_thoughts(self, prefixes: list[str], max_tokens: int) -> list[tuple[str, int]]:
|
| 368 |
+
def one(prefix):
|
| 369 |
+
body = json.dumps({"model": "amos", "prompt": prefix, "max_tokens": max_tokens, "temperature": 0,
|
| 370 |
+
"stop": ["</think>"], "skip_special_tokens": False}).encode()
|
| 371 |
+
req = urllib.request.Request(f"{self.think_url}/v1/completions", body, {"Content-Type": "application/json"})
|
| 372 |
+
out = json.loads(urllib.request.urlopen(req, timeout=1800).read())
|
| 373 |
+
return out["choices"][0]["text"], out["usage"]["completion_tokens"]
|
| 374 |
+
|
| 375 |
+
self.lock.release() # held by __call__; other requests use the model while this one waits for its thoughts
|
| 376 |
+
try:
|
| 377 |
+
with ThreadPoolExecutor(len(prefixes)) as pool:
|
| 378 |
+
return list(pool.map(one, prefixes))
|
| 379 |
+
finally:
|
| 380 |
+
self.lock.acquire()
|
| 381 |
+
|
| 382 |
+
def image(self, data: str):
|
| 383 |
+
from PIL import Image, UnidentifiedImageError
|
| 384 |
+
|
| 385 |
+
if not self.vision:
|
| 386 |
+
raise BadRequest("this model has no vision tower")
|
| 387 |
+
try:
|
| 388 |
+
return Image.open(io.BytesIO(image_bytes(data))).convert("RGB")
|
| 389 |
+
except (ValueError, UnidentifiedImageError) as e:
|
| 390 |
+
raise BadRequest(f"could not decode image: {e}") from e
|
| 391 |
+
|
| 392 |
+
@torch.no_grad()
|
| 393 |
+
def __call__(self, req: dict) -> dict:
|
| 394 |
+
from .decoder import results
|
| 395 |
+
|
| 396 |
+
state, ids, qs = parse(req)
|
| 397 |
+
if req.get("abstain") is not None:
|
| 398 |
+
raise BadRequest("abstain is not available for this model yet: it has no calibration.json")
|
| 399 |
+
for qid, q in zip(ids, qs):
|
| 400 |
+
if q["type"] not in self.types:
|
| 401 |
+
raise BadRequest(f"question {qid!r}: {q['type']} is not available for this model yet "
|
| 402 |
+
f"(it has no trained {q['type']} head{'' if self.vision else ' or no vision tower'})")
|
| 403 |
+
images = [self.image(x) for x in req.get("images") or []]
|
| 404 |
+
qs_in = [{**q, "options": [self.image(o["image"]) if isinstance(o, dict) else o for o in q["options"]]}
|
| 405 |
+
if "options" in q else q for q in qs]
|
| 406 |
+
with self.lock, torch.autocast("cuda", dtype=torch.bfloat16, enabled=self.autocast):
|
| 407 |
+
try:
|
| 408 |
+
rs, n = results(self.model, self.codec, state, qs_in, images, self.heads, self.MAX_LEN, self.rotations,
|
| 409 |
+
self.temperature, self.think_below, self.think_tokens,
|
| 410 |
+
self.remote_thoughts if self.think_url else None)
|
| 411 |
+
except ValueError as e:
|
| 412 |
+
raise BadRequest(str(e)) from e
|
| 413 |
+
if self.multi_cut:
|
| 414 |
+
rs = [{**r, "probs": shift(r["probs"], self.multi_cut)} if r["type"] == "multi" else r for r in rs]
|
| 415 |
+
return {"model": self.name, "answers": {i: answer(q, r) for i, q, r in zip(ids, qs, rs)},
|
| 416 |
+
"usage": {"input_tokens": n, "output_tokens": 0}}
|
| 417 |
+
|
| 418 |
+
def explain(self, req: dict) -> dict:
|
| 419 |
+
raise BadRequest("explanations are not available for this model yet")
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def load_engine(run: str, device: str, name: str | None = None, max_len: int | None = None, rotations: int = 1, **kw):
|
| 423 |
+
"""DecoderEngine when the run holds a decoder checkpoint (RUN/model/config.json), else the encoder Engine."""
|
| 424 |
+
think = {k: kw.pop(k) for k in ("think_below", "think_tokens") if k in kw}
|
| 425 |
+
if (Path(run) / "model" / "config.json").exists():
|
| 426 |
+
return DecoderEngine(run, device, name, max_len, rotations, **think)
|
| 427 |
+
return Engine(run, device, name, **kw)
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
UI = Path(__file__).parent / "ui" # the playground's static build (cd playground && bun run build)
|
| 431 |
+
MAX_BODY = 4 << 20 # bytes
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
class Server(ThreadingHTTPServer):
|
| 435 |
+
"""socketserver's default backlog of 5 resets connections under a burst; queued requests wait in max_queue instead."""
|
| 436 |
+
|
| 437 |
+
request_queue_size = 256
|
| 438 |
+
daemon_threads = True
|
| 439 |
+
RELEASE_DATE = "2026-09-30"
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
class RequestLog:
|
| 443 |
+
"""Every answered request, kept to improve the models (the site's /privacy page says so). One folder per UTC day:
|
| 444 |
+
requests.jsonl and, under images/, each photo once by content hash. A record holds the time, model, source
|
| 445 |
+
(playground or api), the case, questions, photo hashes, answers and server time; never the caller's address.
|
| 446 |
+
Folders older than `days` are deleted.
|
| 447 |
+
|
| 448 |
+
Writes run on a background thread, so the disk never slows an answer; when it falls behind, records are dropped
|
| 449 |
+
rather than queued without bound."""
|
| 450 |
+
|
| 451 |
+
def __init__(self, root, days: int = 90, clock=time.time):
|
| 452 |
+
self.root, self.days, self.clock = Path(root), days, clock
|
| 453 |
+
self.root.mkdir(parents=True, exist_ok=True)
|
| 454 |
+
self.queue: queue.Queue = queue.Queue(maxsize=1000)
|
| 455 |
+
self.pruned = None
|
| 456 |
+
threading.Thread(target=self.run, daemon=True).start()
|
| 457 |
+
|
| 458 |
+
def add(self, record: dict, images: list) -> bool:
|
| 459 |
+
try:
|
| 460 |
+
self.queue.put_nowait((record, images))
|
| 461 |
+
return True
|
| 462 |
+
except queue.Full:
|
| 463 |
+
return False
|
| 464 |
+
|
| 465 |
+
def run(self):
|
| 466 |
+
while True:
|
| 467 |
+
record, images = self.queue.get()
|
| 468 |
+
try:
|
| 469 |
+
self.write(record, images)
|
| 470 |
+
except Exception:
|
| 471 |
+
traceback.print_exc()
|
| 472 |
+
finally:
|
| 473 |
+
self.queue.task_done()
|
| 474 |
+
|
| 475 |
+
def write(self, record: dict, images: list):
|
| 476 |
+
now = self.clock()
|
| 477 |
+
day = time.strftime("%Y-%m-%d", time.gmtime(now))
|
| 478 |
+
folder = self.root / day
|
| 479 |
+
(folder / "images").mkdir(parents=True, exist_ok=True)
|
| 480 |
+
refs = []
|
| 481 |
+
for x in images:
|
| 482 |
+
raw = image_bytes(x)
|
| 483 |
+
ext = ".jpg" if raw[:2] == b"\xff\xd8" else ".png" if raw[:4] == b"\x89PNG" else \
|
| 484 |
+
".webp" if raw[8:12] == b"WEBP" else ".bin"
|
| 485 |
+
name = hashlib.sha256(raw).hexdigest() + ext
|
| 486 |
+
if not (folder / "images" / name).exists():
|
| 487 |
+
(folder / "images" / name).write_bytes(raw)
|
| 488 |
+
refs.append(name)
|
| 489 |
+
line = {"ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now)), **record, "images": refs}
|
| 490 |
+
with open(folder / "requests.jsonl", "a") as f:
|
| 491 |
+
f.write(json.dumps(line, ensure_ascii=False) + "\n")
|
| 492 |
+
if self.pruned != day: # once a day: drop folders past retention
|
| 493 |
+
self.pruned = day
|
| 494 |
+
cutoff = time.strftime("%Y-%m-%d", time.gmtime(now - self.days * 86400))
|
| 495 |
+
for old in self.root.iterdir():
|
| 496 |
+
if old.is_dir() and re.fullmatch(r"\d{4}-\d{2}-\d{2}", old.name) and old.name < cutoff:
|
| 497 |
+
shutil.rmtree(old)
|
| 498 |
+
|
| 499 |
+
|
| 500 |
+
class RateLimit:
|
| 501 |
+
"""Per-client token bucket: per_min requests a minute on average, bursts of up to burst."""
|
| 502 |
+
|
| 503 |
+
def __init__(self, per_min: float, burst: int, clock=time.monotonic):
|
| 504 |
+
self.rate, self.burst, self.clock = per_min / 60, burst, clock
|
| 505 |
+
self.buckets, self.lock = {}, threading.Lock()
|
| 506 |
+
|
| 507 |
+
def wait(self, key: str) -> float:
|
| 508 |
+
"""0 if this request may go now (and it is counted), else the seconds until one may."""
|
| 509 |
+
with self.lock:
|
| 510 |
+
now = self.clock()
|
| 511 |
+
if len(self.buckets) > 10000: # forget clients whose bucket has refilled
|
| 512 |
+
self.buckets = {k: v for k, v in self.buckets.items() if (now - v[1]) * self.rate < self.burst}
|
| 513 |
+
tokens, last = self.buckets.get(key, (self.burst, now))
|
| 514 |
+
tokens = min(self.burst, tokens + (now - last) * self.rate)
|
| 515 |
+
self.buckets[key] = (tokens - 1 if tokens >= 1 else tokens, now)
|
| 516 |
+
return 0.0 if tokens >= 1 else (1 - tokens) / self.rate
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
PLAYGROUND_ORIGINS = ("https://amos.scira.ai",)
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def handler(engine: Engine, cors: str | None = None, max_queue: int = 8, limit: RateLimit | None = None,
|
| 523 |
+
extra: list | None = None, log: RequestLog | None = None, retired: dict[str, str] | None = None):
|
| 524 |
+
"""engine answers by default; extra engines answer requests whose "model" names them. With a log, every answered
|
| 525 |
+
/v1/systemone request is kept. retired maps a withdrawn model's name to its replacement: asking for it gets 410
|
| 526 |
+
rather than another model's answers under the old name."""
|
| 527 |
+
retired = retired or {}
|
| 528 |
+
|
| 529 |
+
def gone(name):
|
| 530 |
+
return {"error": f"{name} has been retired; use {retired[name]} instead"}
|
| 531 |
+
# Forwards run one at a time (Engine.lock); past max_queue waiting requests, refuse rather than pile up.
|
| 532 |
+
slots = threading.BoundedSemaphore(max_queue)
|
| 533 |
+
extra = extra or []
|
| 534 |
+
models = {e.name: e for e in extra} # lookups fall back to engine, so it needs no entry
|
| 535 |
+
|
| 536 |
+
class H(BaseHTTPRequestHandler):
|
| 537 |
+
def client(self) -> str:
|
| 538 |
+
# Behind Vercel: x-vercel-forwarded-for is set by Vercel, while a caller's own x-forwarded-for passes
|
| 539 |
+
# through untouched, so it is only the fallback when Vercel is not in front. These headers are trusted as
|
| 540 |
+
# sent: a caller who reaches the server directly can set any of them.
|
| 541 |
+
forwarded = (self.headers.get("x-vercel-forwarded-for") or self.headers.get("x-real-ip")
|
| 542 |
+
or (self.headers.get("x-forwarded-for") or "").split(",")[0])
|
| 543 |
+
return forwarded.strip() or self.client_address[0]
|
| 544 |
+
|
| 545 |
+
def address_string(self): # the access log names the caller, not the proxy in front
|
| 546 |
+
return self.client()
|
| 547 |
+
|
| 548 |
+
def do_GET(self):
|
| 549 |
+
path = self.path.split("?")[0]
|
| 550 |
+
if path == "/v1/info": # ?model=NAME for a model other than the default
|
| 551 |
+
name = parse_qs(urlsplit(self.path).query).get("model", [engine.name])[0]
|
| 552 |
+
if name in retired:
|
| 553 |
+
return self.send(410, gone(name))
|
| 554 |
+
e = engine if name == engine.name else models.get(name)
|
| 555 |
+
if e is None:
|
| 556 |
+
return self.send(404, {"error": f"no model {name!r}; served: {[engine.name, *models]}"})
|
| 557 |
+
alphas = sorted(e.cal["qhat"].get("choice", {})) if e.cal else []
|
| 558 |
+
return self.send(200, {"model": e.name, "vision": e.vision,
|
| 559 |
+
"abstain": [float(a) for a in alphas], "card": e.card})
|
| 560 |
+
if path == "/v1/models": # what the TypeSafe SDKs' models.list() reads
|
| 561 |
+
return self.send(200, {"models": [{"name": e.name, "release_date": RELEASE_DATE,
|
| 562 |
+
"description": f"{e.card['parameters'] / 1e9:.1f}B parameters: "
|
| 563 |
+
+ ", ".join(e.card["types"])}
|
| 564 |
+
for e in [engine, *extra]]})
|
| 565 |
+
if not UI.is_dir():
|
| 566 |
+
return self.send(404, {"error": "playground not built: run `bun install && bun run build` in playground/"})
|
| 567 |
+
file = (UI / path.lstrip("/")).resolve()
|
| 568 |
+
if file.is_dir():
|
| 569 |
+
file = file / "index.html"
|
| 570 |
+
if not file.is_relative_to(UI.resolve()) or not file.is_file():
|
| 571 |
+
return self.send(404, {"error": "not found"})
|
| 572 |
+
self.send_bytes(200, file.read_bytes(), mimetypes.guess_type(file.name)[0] or "application/octet-stream")
|
| 573 |
+
|
| 574 |
+
def do_OPTIONS(self): # CORS preflight, only when a dev origin was allowed
|
| 575 |
+
self.send_bytes(204, b"", "text/plain")
|
| 576 |
+
|
| 577 |
+
def do_POST(self):
|
| 578 |
+
if self.path not in ("/v1/systemone", "/v1/explain"):
|
| 579 |
+
return self.send(404, {"error": "not found"})
|
| 580 |
+
try:
|
| 581 |
+
size = int(self.headers.get("Content-Length", 0))
|
| 582 |
+
except ValueError:
|
| 583 |
+
return self.send(400, {"error": "bad Content-Length"})
|
| 584 |
+
if size > MAX_BODY:
|
| 585 |
+
self.close_connection = True # the unread body cannot be reused as the next request
|
| 586 |
+
return self.send(413, {"error": f"request body over {MAX_BODY >> 20} MB"})
|
| 587 |
+
if limit and (wait := limit.wait(self.client())):
|
| 588 |
+
self.close_connection = True
|
| 589 |
+
return self.send(429, {"error": "rate limit reached; retry after the Retry-After header"},
|
| 590 |
+
{"Retry-After": str(math.ceil(wait))})
|
| 591 |
+
if not slots.acquire(blocking=False):
|
| 592 |
+
self.close_connection = True
|
| 593 |
+
return self.send(503, {"error": "busy, retry shortly"}, {"Retry-After": "1"})
|
| 594 |
+
try:
|
| 595 |
+
req = json.loads(self.rfile.read(size))
|
| 596 |
+
if isinstance(req, dict) and req.get("model") in retired:
|
| 597 |
+
return self.send(410, gone(req["model"]))
|
| 598 |
+
# the request's model picks among served models; any other name (the SDKs send jev-latest) gets the first
|
| 599 |
+
eng = models.get(req.get("model"), engine) if isinstance(req, dict) else engine
|
| 600 |
+
if self.path != "/v1/systemone":
|
| 601 |
+
return self.send(200, eng.explain(req))
|
| 602 |
+
t = time.perf_counter()
|
| 603 |
+
try:
|
| 604 |
+
res = eng(req)
|
| 605 |
+
except Exception as e: # out of GPU memory: give back the cache and try once more
|
| 606 |
+
if type(e).__name__ != "OutOfMemoryError" or not free_gpu_cache(0):
|
| 607 |
+
raise
|
| 608 |
+
res = eng(req)
|
| 609 |
+
if log:
|
| 610 |
+
log.add({"model": res.get("model"), "source": "playground" if self.headers.get("Origin") in
|
| 611 |
+
PLAYGROUND_ORIGINS else "api", "state": req["state"], "questions": req["questions"],
|
| 612 |
+
"answers": res.get("answers"), "ms": round((time.perf_counter() - t) * 1000, 1)},
|
| 613 |
+
list(req.get("images") or []))
|
| 614 |
+
self.send(200, res)
|
| 615 |
+
except (BadRequest, json.JSONDecodeError, ValueError) as e:
|
| 616 |
+
self.send(422, {"error": str(e)})
|
| 617 |
+
except Exception: # a bug, not the request: answer instead of dropping the connection
|
| 618 |
+
traceback.print_exc()
|
| 619 |
+
self.send(500, {"error": "internal error: the server failed to answer this request"})
|
| 620 |
+
finally:
|
| 621 |
+
slots.release()
|
| 622 |
+
free_gpu_cache() # so the other model on this GPU can use what this request no longer needs
|
| 623 |
+
|
| 624 |
+
def send(self, code, body, headers=None):
|
| 625 |
+
self.send_bytes(code, json.dumps(body).encode(), "application/json", headers)
|
| 626 |
+
|
| 627 |
+
def send_bytes(self, code, data, content_type, headers=None):
|
| 628 |
+
self.send_response(code)
|
| 629 |
+
self.send_header("Content-Type", content_type)
|
| 630 |
+
self.send_header("Content-Length", str(len(data)))
|
| 631 |
+
for k, v in (headers or {}).items():
|
| 632 |
+
self.send_header(k, v)
|
| 633 |
+
if cors:
|
| 634 |
+
self.send_header("Access-Control-Allow-Origin", cors)
|
| 635 |
+
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
| 636 |
+
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
| 637 |
+
self.send_header("Access-Control-Max-Age", "7200") # a far caller would otherwise pay a preflight each call
|
| 638 |
+
self.end_headers()
|
| 639 |
+
self.wfile.write(data)
|
| 640 |
+
|
| 641 |
+
def log_message(self, *args):
|
| 642 |
+
pass
|
| 643 |
+
|
| 644 |
+
return H
|
| 645 |
+
|
| 646 |
+
|
| 647 |
+
def bench(engine: Engine, runs=20):
|
| 648 |
+
"""Median latency by state length and number of questions: new state (cold) and cached state (warm)."""
|
| 649 |
+
word = "The shipment of 40 units arrived late and two boxes were damaged in transit. "
|
| 650 |
+
rows = []
|
| 651 |
+
for words in (50, 200, 800):
|
| 652 |
+
for n_q in (1, 8, 32):
|
| 653 |
+
req = {"state": (word * (words // 13 + 1))[: words * 6],
|
| 654 |
+
"questions": {f"q{i}": [{"type": "noul", "instructions": "Was anything damaged?"},
|
| 655 |
+
{"type": "choice", "instructions": "Main issue?", "criteria": {"late": None, "damage": None, "missing": None}},
|
| 656 |
+
{"type": "score", "instructions": "How severe?", "criteria": ["minor", "moderate", "major"]}][i % 3]
|
| 657 |
+
for i in range(n_q)}}
|
| 658 |
+
engine(req)
|
| 659 |
+
med = {}
|
| 660 |
+
for mode in ("cold", "warm"):
|
| 661 |
+
times = []
|
| 662 |
+
for _ in range(runs):
|
| 663 |
+
if mode == "cold":
|
| 664 |
+
getattr(engine, "state_cache", {}).clear() # a decoder keeps no state cache
|
| 665 |
+
t = time.perf_counter()
|
| 666 |
+
engine(req)
|
| 667 |
+
times.append(time.perf_counter() - t)
|
| 668 |
+
med[mode] = sorted(times)[runs // 2] * 1000
|
| 669 |
+
rows.append((words, n_q, med["cold"], med["warm"]))
|
| 670 |
+
print(f"{engine.device} state ~{words} words {n_q:>2} questions cold {med['cold']:6.1f} ms warm {med['warm']:6.1f} ms", flush=True)
|
| 671 |
+
return rows
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
def main(argv=None):
|
| 675 |
+
ap = argparse.ArgumentParser()
|
| 676 |
+
ap.add_argument("run", help="a run directory (encoder, or decoder with RUN/model)")
|
| 677 |
+
ap.add_argument("--host", default="127.0.0.1", help="0.0.0.0 inside a container")
|
| 678 |
+
ap.add_argument("--port", type=int, default=int(os.environ.get("PORT", 8000)))
|
| 679 |
+
ap.add_argument("--max-queue", type=int, default=8, help="requests allowed to wait for the model before 503")
|
| 680 |
+
ap.add_argument("--rate-limit", type=float, default=0,
|
| 681 |
+
help="requests a minute per caller before 429 (zero: no limit); for a public endpoint")
|
| 682 |
+
ap.add_argument("--burst", type=int, default=20, help="requests a caller may send at once under --rate-limit")
|
| 683 |
+
ap.add_argument("--device", default="cpu")
|
| 684 |
+
ap.add_argument("--name", help="model name to report (default: amos-1 for 68m, amos-1-mini for 32m)")
|
| 685 |
+
ap.add_argument("--bench", action="store_true")
|
| 686 |
+
ap.add_argument("--check", action="store_true", help="load the model and exit (an image build caches the tokenizer)")
|
| 687 |
+
ap.add_argument("--strict", action="store_true", help="preserve question/option text and reject context overflow")
|
| 688 |
+
ap.add_argument("--max-len", type=int, help="decoder runs: tokens allowed per request (default DecoderEngine.MAX_LEN)")
|
| 689 |
+
ap.add_argument("--rotations", type=int, default=1, help="decoder runs: average choice reads over this many option orders")
|
| 690 |
+
ap.add_argument("--think-below", type=float, help="decoder runs: re-ask an answer less sure than this after the "
|
| 691 |
+
"model reasons in its thinking block")
|
| 692 |
+
ap.add_argument("--think-tokens", type=int, default=512, help="decoder runs: the most tokens a thought may take")
|
| 693 |
+
ap.add_argument("--no-state-cache", action="store_true", help="encode each request without retaining state tensors")
|
| 694 |
+
ap.add_argument("--cors", help="allow this browser origin to call the API (e.g. http://localhost:3000 for `bun dev`)")
|
| 695 |
+
ap.add_argument("--also", action="append", default=[], metavar="NAME=RUN",
|
| 696 |
+
help="serve another run beside the first, answering requests whose \"model\" is NAME")
|
| 697 |
+
ap.add_argument("--retired", action="append", default=[], metavar="NAME=REPLACEMENT",
|
| 698 |
+
help="a withdrawn model: requests naming it get 410 and the replacement's name")
|
| 699 |
+
ap.add_argument("--typed-decisions", help="typed-decisions metrics JSON for /v1/info (default: the run's "
|
| 700 |
+
"typed_decisions.json)")
|
| 701 |
+
ap.add_argument("--log-dir", help="keep every answered request here to improve the models (RequestLog). Publish "
|
| 702 |
+
"the notice (/privacy) before turning this on")
|
| 703 |
+
ap.add_argument("--log-days", type=int, default=90, help="days of logged requests to keep")
|
| 704 |
+
a = ap.parse_args(argv)
|
| 705 |
+
engine = load_engine(a.run, a.device, a.name, a.max_len, a.rotations, strict=a.strict,
|
| 706 |
+
state_cache=not a.no_state_cache, think_below=a.think_below, think_tokens=a.think_tokens)
|
| 707 |
+
if a.typed_decisions:
|
| 708 |
+
td = Path(a.typed_decisions)
|
| 709 |
+
day = time.strftime("%Y-%m-%d", time.localtime(td.stat().st_mtime))
|
| 710 |
+
engine.card["benchmarks"]["typed_decisions"] = {"measured": day, **json.loads(td.read_text())["all"]}
|
| 711 |
+
if a.check:
|
| 712 |
+
print(f"{engine.name}: {engine.card['parameters']:,} parameters, max_state {engine.card['max_state']}", flush=True)
|
| 713 |
+
return
|
| 714 |
+
if a.bench:
|
| 715 |
+
bench(engine)
|
| 716 |
+
return
|
| 717 |
+
extra = [load_engine(run, a.device, name, a.max_len, a.rotations)
|
| 718 |
+
for name, _, run in (s.partition("=") for s in a.also)]
|
| 719 |
+
print(f"serving {a.run}{''.join(f', {e.name}' for e in extra)}: playground http://{a.host}:{a.port}/, "
|
| 720 |
+
f"API POST http://{a.host}:{a.port}/v1/systemone", flush=True)
|
| 721 |
+
limit = RateLimit(a.rate_limit, a.burst) if a.rate_limit else None
|
| 722 |
+
log = RequestLog(a.log_dir, a.log_days) if a.log_dir else None
|
| 723 |
+
Server((a.host, a.port), handler(engine, a.cors, a.max_queue, limit, extra, log,
|
| 724 |
+
dict(s.split("=", 1) for s in a.retired))).serve_forever()
|
| 725 |
+
|
| 726 |
+
|
| 727 |
+
if __name__ == "__main__":
|
| 728 |
+
main(sys.argv[1:])
|
serve/amos/vision.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SigLIP-architecture vision tower written from scratch (loads SigLIP 2 weights), plus the projector.
|
| 2 |
+
|
| 3 |
+
An image becomes 14 x 14 patch vectors; the projector merges each 2 x 2 block (4x fewer tokens) and maps
|
| 4 |
+
it into the text encoder's embedding space with a 2-layer MLP, giving 49 image tokens.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn.functional as F
|
| 11 |
+
from huggingface_hub import hf_hub_download
|
| 12 |
+
from safetensors.torch import load_file
|
| 13 |
+
from torch import nn
|
| 14 |
+
|
| 15 |
+
IMAGE_SIZE, PATCH, GRID = 224, 16, 14
|
| 16 |
+
MERGED = (GRID // 2) ** 2 # image tokens per image after 2 x 2 merging
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def preprocess(image) -> torch.Tensor:
|
| 20 |
+
"""PIL image -> [3, 224, 224] in [-1, 1] (bilinear resize, as SigLIP's processor does)."""
|
| 21 |
+
from PIL import Image
|
| 22 |
+
|
| 23 |
+
image = image.convert("RGB").resize((IMAGE_SIZE, IMAGE_SIZE), Image.Resampling.BILINEAR)
|
| 24 |
+
x = torch.frombuffer(bytearray(image.tobytes()), dtype=torch.uint8).view(IMAGE_SIZE, IMAGE_SIZE, 3)
|
| 25 |
+
return x.permute(2, 0, 1).float() / 127.5 - 1
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class Block(nn.Module):
|
| 29 |
+
def __init__(self, d, heads, ff, eps):
|
| 30 |
+
super().__init__()
|
| 31 |
+
self.heads = heads
|
| 32 |
+
self.layer_norm1, self.layer_norm2 = nn.LayerNorm(d, eps=eps), nn.LayerNorm(d, eps=eps)
|
| 33 |
+
self.q_proj, self.k_proj, self.v_proj, self.out_proj = (nn.Linear(d, d) for _ in range(4))
|
| 34 |
+
self.fc1, self.fc2 = nn.Linear(d, ff), nn.Linear(ff, d)
|
| 35 |
+
|
| 36 |
+
def forward(self, x):
|
| 37 |
+
B, N, d = x.shape
|
| 38 |
+
h = self.layer_norm1(x)
|
| 39 |
+
q, k, v = (p(h).view(B, N, self.heads, -1).transpose(1, 2) for p in (self.q_proj, self.k_proj, self.v_proj))
|
| 40 |
+
x = x + self.out_proj(F.scaled_dot_product_attention(q, k, v).transpose(1, 2).reshape(B, N, d))
|
| 41 |
+
return x + self.fc2(F.gelu(self.fc1(self.layer_norm2(x)), approximate="tanh"))
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class VisionTower(nn.Module):
|
| 45 |
+
def __init__(self, d=768, layers=12, heads=12, ff=3072, eps=1e-6):
|
| 46 |
+
super().__init__()
|
| 47 |
+
self.patch_embedding = nn.Conv2d(3, d, PATCH, PATCH)
|
| 48 |
+
self.position_embedding = nn.Parameter(torch.zeros(GRID * GRID, d))
|
| 49 |
+
self.blocks = nn.ModuleList(Block(d, heads, ff, eps) for _ in range(layers))
|
| 50 |
+
self.post_layernorm = nn.LayerNorm(d, eps=eps)
|
| 51 |
+
self.hidden_size = d
|
| 52 |
+
|
| 53 |
+
def forward(self, pixels): # [B, 3, 224, 224] -> [B, 196, d]
|
| 54 |
+
# cuDNN disabled: its runtime-compiled engines fail to load on some CUDA hosts. The one conv is a cheap patchify.
|
| 55 |
+
from torch.nn.attention import SDPBackend, sdpa_kernel
|
| 56 |
+
|
| 57 |
+
with torch.backends.cudnn.flags(enabled=False), sdpa_kernel([SDPBackend.FLASH_ATTENTION, SDPBackend.EFFICIENT_ATTENTION, SDPBackend.MATH]):
|
| 58 |
+
x = self.patch_embedding(pixels).flatten(2).transpose(1, 2) + self.position_embedding
|
| 59 |
+
for b in self.blocks:
|
| 60 |
+
x = b(x)
|
| 61 |
+
return self.post_layernorm(x)
|
| 62 |
+
|
| 63 |
+
@classmethod
|
| 64 |
+
def from_pretrained(cls, repo: str) -> "VisionTower":
|
| 65 |
+
with open(hf_hub_download(repo, "config.json")) as f:
|
| 66 |
+
vc = json.load(f).get("vision_config", {})
|
| 67 |
+
m = cls(vc.get("hidden_size", 768), vc.get("num_hidden_layers", 12), vc.get("num_attention_heads", 12),
|
| 68 |
+
vc.get("intermediate_size", 3072), vc.get("layer_norm_eps", 1e-6))
|
| 69 |
+
sd = {}
|
| 70 |
+
for k, v in load_file(hf_hub_download(repo, "model.safetensors")).items():
|
| 71 |
+
if not k.startswith("vision_model.") or ".head." in k:
|
| 72 |
+
continue
|
| 73 |
+
k = k.removeprefix("vision_model.").replace("embeddings.", "").replace("encoder.layers.", "blocks.")
|
| 74 |
+
k = k.replace("self_attn.", "").replace("mlp.", "")
|
| 75 |
+
sd["position_embedding" if k == "position_embedding.weight" else k] = v
|
| 76 |
+
m.load_state_dict(sd, strict=True)
|
| 77 |
+
return m.eval().requires_grad_(False)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class Projector(nn.Module):
|
| 81 |
+
"""2 x 2 patch merge (concatenate neighbours) + 2-layer MLP into the text embedding space."""
|
| 82 |
+
|
| 83 |
+
def __init__(self, d_vision: int, d_text: int):
|
| 84 |
+
super().__init__()
|
| 85 |
+
self.fc1, self.fc2 = nn.Linear(4 * d_vision, d_text), nn.Linear(d_text, d_text)
|
| 86 |
+
|
| 87 |
+
def forward(self, feats): # [N, 196, dv] -> [N, 49, dt]
|
| 88 |
+
N, _, dv = feats.shape
|
| 89 |
+
g = GRID // 2
|
| 90 |
+
x = feats.view(N, g, 2, g, 2, dv).permute(0, 1, 3, 2, 4, 5).reshape(N, g * g, 4 * dv)
|
| 91 |
+
return self.fc2(F.gelu(self.fc1(x)))
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def cell_boxes(rows: int = GRID // 2, cols: int = GRID // 2) -> torch.Tensor:
|
| 95 |
+
"""[rows * cols, 4] normalized (x0, y0, x1, y1) of each merged image token, row-major (49 by default)."""
|
| 96 |
+
i = torch.arange(rows * cols)
|
| 97 |
+
r, c = i // cols, i % cols
|
| 98 |
+
return torch.stack([c / cols, r / rows, (c + 1) / cols, (r + 1) / rows], 1).float()
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def box_target(box, rows: int = GRID // 2, cols: int = GRID // 2) -> torch.Tensor:
|
| 102 |
+
"""Distribution over the cells proportional to overlap with a normalized box (x0, y0, x1, y1)."""
|
| 103 |
+
cells, (x0, y0, x1, y1) = cell_boxes(rows, cols), box
|
| 104 |
+
w = (cells[:, 2].clamp(max=x1) - cells[:, 0].clamp(min=x0)).clamp(min=0)
|
| 105 |
+
h = (cells[:, 3].clamp(max=y1) - cells[:, 1].clamp(min=y0)).clamp(min=0)
|
| 106 |
+
area = w * h
|
| 107 |
+
return area / area.sum() if area.sum() > 0 else torch.full((len(cells),), 1 / len(cells))
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def peak_box(p: torch.Tensor, rows: int, cols: int, keep: float) -> tuple[list[float], float]:
|
| 111 |
+
"""Box around the most likely cell and the cells 4-connected to it that hold at least `keep` of its probability ->
|
| 112 |
+
(normalized (x0, y0, x1, y1), probability inside the box). keep=1 is (about) the most likely cell alone."""
|
| 113 |
+
g = p.view(rows, cols)
|
| 114 |
+
r, c = divmod(int(p.argmax()), cols)
|
| 115 |
+
on, seen, todo = (g >= keep * g[r, c]).tolist(), {(r, c)}, [(r, c)]
|
| 116 |
+
while todo:
|
| 117 |
+
i, j = todo.pop()
|
| 118 |
+
for y, x in ((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)):
|
| 119 |
+
if 0 <= y < rows and 0 <= x < cols and on[y][x] and (y, x) not in seen:
|
| 120 |
+
seen.add((y, x))
|
| 121 |
+
todo.append((y, x))
|
| 122 |
+
r0, r1 = min(y for y, _ in seen), max(y for y, _ in seen) + 1
|
| 123 |
+
c0, c1 = min(x for _, x in seen), max(x for _, x in seen) + 1
|
| 124 |
+
return [c0 / cols, r0 / rows, c1 / cols, r1 / rows], float(g[r0:r1, c0:c1].sum())
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def box_scores(box, gold) -> tuple[bool, float]:
|
| 128 |
+
"""(the box's centre lies inside the gold box, intersection over union) of normalized (x0, y0, x1, y1) boxes."""
|
| 129 |
+
cx, cy = (box[0] + box[2]) / 2, (box[1] + box[3]) / 2
|
| 130 |
+
w = max(0.0, min(box[2], gold[2]) - max(box[0], gold[0]))
|
| 131 |
+
h = max(0.0, min(box[3], gold[3]) - max(box[1], gold[1]))
|
| 132 |
+
union = (box[2] - box[0]) * (box[3] - box[1]) + (gold[2] - gold[0]) * (gold[3] - gold[1]) - w * h
|
| 133 |
+
return gold[0] <= cx <= gold[2] and gold[1] <= cy <= gold[3], w * h / union if union > 0 else 0.0
|
serve/pyproject.toml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "amos-serve"
|
| 3 |
+
version = "1.0.0"
|
| 4 |
+
description = "Serving code for the Amos 1 decision models: one HTTP server, POST /v1/systemone, answers typed questions from one forward pass"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.12"
|
| 7 |
+
license = "Apache-2.0"
|
| 8 |
+
# pinned to the versions the published numbers were measured with: with these three the package answers byte for
|
| 9 |
+
# byte like our own servers; transformers 5.19 or the pure-PyTorch linear attention move probabilities by up to 0.02
|
| 10 |
+
dependencies = [
|
| 11 |
+
"torch==2.14.0",
|
| 12 |
+
"transformers==5.17.0",
|
| 13 |
+
"flash-linear-attention==0.5.2",
|
| 14 |
+
"safetensors>=0.8.0",
|
| 15 |
+
"tokenizers>=0.23.2",
|
| 16 |
+
"pillow>=12.3.0",
|
| 17 |
+
"huggingface-hub>=1.33.0",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
[build-system]
|
| 21 |
+
requires = ["setuptools>=75"]
|
| 22 |
+
build-backend = "setuptools.build_meta"
|
| 23 |
+
|
| 24 |
+
[tool.setuptools.packages.find]
|
| 25 |
+
include = ["amos*"]
|
serve/sources.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"commit": "cd57f1762f3029565fb3a877dce9e598739b971e",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"files": {
|
| 5 |
+
"amos/serve.py": {
|
| 6 |
+
"from": "src/amos/serve.py",
|
| 7 |
+
"verbatim": true,
|
| 8 |
+
"sha256": "b0523591c320a074a0ec0751b68ebc205aeedd090ff5e7ca10bda3fb94a07ef8"
|
| 9 |
+
},
|
| 10 |
+
"amos/model.py": {
|
| 11 |
+
"from": "src/amos/model.py",
|
| 12 |
+
"verbatim": true,
|
| 13 |
+
"sha256": "d4f4354f3e4d3b475b4c1f832a4a9741da757a368a1d2e0e74544d06009c2c27"
|
| 14 |
+
},
|
| 15 |
+
"amos/packing.py": {
|
| 16 |
+
"from": "src/amos/packing.py",
|
| 17 |
+
"verbatim": true,
|
| 18 |
+
"sha256": "14f027c9e15c456abb0f264c07dae06afb7ace7b64270f2374e97eabeb04f993"
|
| 19 |
+
},
|
| 20 |
+
"amos/encoder.py": {
|
| 21 |
+
"from": "src/amos/encoder.py",
|
| 22 |
+
"verbatim": true,
|
| 23 |
+
"sha256": "b4a5a606a2dee99eedf86cab50be759eca0908a318e4f3037ba92560d837bd67"
|
| 24 |
+
},
|
| 25 |
+
"amos/facts.py": {
|
| 26 |
+
"from": "src/amos/facts.py",
|
| 27 |
+
"verbatim": true,
|
| 28 |
+
"sha256": "f2efd7ef25453f7f2dc204dd247c86733a55697ec48714f30560df8ed2da1f23"
|
| 29 |
+
},
|
| 30 |
+
"amos/vision.py": {
|
| 31 |
+
"from": "src/amos/vision.py",
|
| 32 |
+
"verbatim": true,
|
| 33 |
+
"sha256": "133d1e29cc64e371f5df1f0cb0328fd77b8b9093d8465319580cf5ea4af5d081"
|
| 34 |
+
},
|
| 35 |
+
"amos/decoder.py": {
|
| 36 |
+
"from": "src/amos/decoder.py",
|
| 37 |
+
"verbatim": false,
|
| 38 |
+
"kept": [
|
| 39 |
+
"load_model",
|
| 40 |
+
"load_heads",
|
| 41 |
+
"results",
|
| 42 |
+
"calibration",
|
| 43 |
+
"span_decoding",
|
| 44 |
+
"multi_cut"
|
| 45 |
+
],
|
| 46 |
+
"source_sha256": "8d76889e60e05606ea2de24ee40648208dac2f49b3a0e0b5277285397ddcbb73",
|
| 47 |
+
"sha256": "0dcacb27a05d31781f8310cbe6123b0ee351c62456cc6b5f8576b40b8ddad080"
|
| 48 |
+
},
|
| 49 |
+
"amos/calibrate.py": {
|
| 50 |
+
"from": "src/amos/calibrate.py",
|
| 51 |
+
"verbatim": false,
|
| 52 |
+
"kept": [
|
| 53 |
+
"apply"
|
| 54 |
+
],
|
| 55 |
+
"source_sha256": "552322fa793eea43148ed0b7860a5bca1484fec806dfe485a2ddb664f17b8524",
|
| 56 |
+
"sha256": "ba094d9eaedd229a99b2bdcd35a371ffdeb9aaee258a342524cc19e2f85ec421"
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
}
|
smoke_decoder.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse,asyncio,json
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import torch
|
| 4 |
+
from coreai_decoder import AmosDecoder
|
| 5 |
+
async def main(name):
|
| 6 |
+
def forbidden(*a,**kw):raise AssertionError('PyTorch neural execution forbidden')
|
| 7 |
+
torch.nn.Linear.forward=forbidden;torch.nn.Embedding.forward=forbidden;torch.nn.Conv2d.forward=forbidden;torch.nn.Conv3d.forward=forbidden
|
| 8 |
+
root=Path('bundles')/(name+'-CoreAI');m=await AmosDecoder.load(root)
|
| 9 |
+
out=await m.request({'state':'Please refund the duplicate charge.','questions':{'refund':{'type':'noul','instructions':'Is a refund requested?'}}})
|
| 10 |
+
assert out['answers']['refund']['noul']>0.5,out
|
| 11 |
+
(root/'runtime-smoke.json').write_text(json.dumps({'passed':True,'pytorch_neural_execution_disabled':True,'result':out},indent=2));print(out)
|
| 12 |
+
if __name__=='__main__':
|
| 13 |
+
p=argparse.ArgumentParser();p.add_argument('name');a=p.parse_args();asyncio.run(main(a.name))
|
text.aimodel/main.hash
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
F��C���T��Hݛ2�?T��Ô������r
|
text.aimodel/main.mlirb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1c469415a143a1d9d75486ed48dd9b32c1033f1854a6bfc394ae9eb08caa8c72
|
| 3 |
+
size 7534240163
|
text.aimodel/metadata.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"producer" : "coreai-core 1.0.0b3",
|
| 3 |
+
"assetVersion" : "2.0",
|
| 4 |
+
"creationDate" : "20261014T012228Z"
|
| 5 |
+
}
|
validate_decoder.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse,asyncio,json,sys,shutil
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import numpy as np
|
| 4 |
+
import torch
|
| 5 |
+
from PIL import Image
|
| 6 |
+
from coreai_decoder import AmosDecoder
|
| 7 |
+
|
| 8 |
+
async def main(name,mode='both'):
|
| 9 |
+
source=(Path('upstream')/name).resolve();root=Path('bundles')/(name+'-CoreAI')
|
| 10 |
+
shutil.copytree(source/'serve',root/'serve',dirs_exist_ok=True)
|
| 11 |
+
(root/'model').mkdir(exist_ok=True)
|
| 12 |
+
for f in (source/'model').iterdir():
|
| 13 |
+
if f.suffix not in ['.safetensors','.pt']:shutil.copy2(f,root/'model'/f.name)
|
| 14 |
+
for f in ['config.json','calibration.json','LICENSE']:shutil.copy2(source/f,root/f)
|
| 15 |
+
sys.path.insert(0,str(source/'serve'))
|
| 16 |
+
import amos.decoder as decoder
|
| 17 |
+
from transformers import Qwen2VLImageProcessorPil
|
| 18 |
+
decoder.IMAGE_PIXELS=224*224
|
| 19 |
+
precision=json.loads((root/'manifest.json').read_text())['precision']
|
| 20 |
+
torch.set_num_threads(4)
|
| 21 |
+
if mode!='coreai':
|
| 22 |
+
model,codec=decoder.load_model(str(source/'model'),'cpu',dtype=getattr(torch,precision));model.eval()
|
| 23 |
+
codec.image_processor=Qwen2VLImageProcessorPil.from_pretrained(source/'model',size={'shortest_edge':224*224,'longest_edge':224*224})
|
| 24 |
+
heads=decoder.load_heads(source/'model',model,codec);heads.span=decoder.span_decoding(source)
|
| 25 |
+
if mode!='reference':ai=await AmosDecoder.load(root)
|
| 26 |
+
pixels=np.zeros((224,224,3),np.uint8);pixels[:,:112]=[255,0,0];pixels[:,112:]=[0,0,255];im=Image.fromarray(pixels)
|
| 27 |
+
cases=[
|
| 28 |
+
('refund','Invoice INV-2231 for £306.00 was charged twice. Please refund one.',[{'type':'noul','text':'Is a refund requested?'},{'type':'choice','text':'Which team?','options':['billing','technical','sales']}],[]),
|
| 29 |
+
('span','The invoice total is £306.00 and the invoice number is INV-2231.',[{'type':'span','text':'Quote the invoice number.'}],[]),
|
| 30 |
+
('score','The service is down for every customer.',[{'type':'score','text':'How severe is the outage?','options':['none','minor','major','complete']}],[]),
|
| 31 |
+
('multi','I need a refund and a password reset.',[{'type':'multi','text':'Which topics apply?','options':['refund','password','delivery']}],[]),
|
| 32 |
+
('negative','The customer explicitly says no refund is needed.',[{'type':'noul','text':'Is a refund requested?'}],[]),
|
| 33 |
+
('unicode','José paid €123.45 for café supplies.',[{'type':'span','text':'Quote the amount paid.'},{'type':'choice','text':'Who paid?','options':['José','Maria','Bob']}],[]),
|
| 34 |
+
('options','This is about category 17.',[{'type':'choice','text':'Which category?','options':[f'category {i}' for i in range(24)]}],[]),
|
| 35 |
+
('vision','Two colored halves.',[{'type':'region','text':'Point to the red half.'},{'type':'choice','text':'Which colors?','options':['red and blue','green and yellow']}],[im]),
|
| 36 |
+
('image-option','Find the red-blue image.',[{'type':'choice','text':'Which image matches?','options':[im,Image.new('RGB',(224,224),'green')]}],[]),
|
| 37 |
+
]
|
| 38 |
+
records=[];refs=json.loads((root/'reference.json').read_text()) if mode=='coreai' else []
|
| 39 |
+
for case,state,qs,ims in cases:
|
| 40 |
+
if mode=='coreai':
|
| 41 |
+
ref=next(r for r in refs if r['case']==case);expected,tokens=ref['upstream'],ref['tokens']
|
| 42 |
+
else:
|
| 43 |
+
with torch.no_grad():expected,tokens=decoder.results(model,codec,state,qs,ims,heads,512,temperature=decoder.calibration(source))
|
| 44 |
+
if mode=='reference':
|
| 45 |
+
records.append({'case':case,'tokens':tokens,'reference_precision':precision,'upstream':expected});print(case,tokens,flush=True);continue
|
| 46 |
+
actual=await ai.predict(state,qs,ims)
|
| 47 |
+
error=0;match=True
|
| 48 |
+
for x,y in zip(expected,actual):
|
| 49 |
+
for key in ['p_yes','probs','p','box']:
|
| 50 |
+
if key in x:error=max(error,float(np.abs(np.asarray(x[key])-np.asarray(y[key])).max()))
|
| 51 |
+
for key in ['choice','start','end','text','cell']:
|
| 52 |
+
if key in x and x[key]!=y[key]:match=False
|
| 53 |
+
rec={'case':case,'tokens':tokens,'reference_precision':precision,'max_probability_or_box_error':error,'decisions_match':match,'upstream':expected,'coreai':actual};records.append(rec);print(json.dumps(rec),flush=True)
|
| 54 |
+
if not match or error>0.02:raise AssertionError(rec)
|
| 55 |
+
(root/('reference.json' if mode=='reference' else 'validation.json')).write_text(json.dumps(records,indent=2));print('PASS',flush=True)
|
| 56 |
+
if __name__=='__main__':
|
| 57 |
+
p=argparse.ArgumentParser();p.add_argument('name');p.add_argument('--mode',choices=['both','reference','coreai'],default='both');a=p.parse_args();asyncio.run(main(a.name,a.mode))
|
validation.json
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"case": "refund",
|
| 4 |
+
"tokens": 122,
|
| 5 |
+
"reference_precision": "float32",
|
| 6 |
+
"max_probability_or_box_error": 1.1920928955078125e-07,
|
| 7 |
+
"decisions_match": true,
|
| 8 |
+
"upstream": [
|
| 9 |
+
{
|
| 10 |
+
"type": "noul",
|
| 11 |
+
"p_yes": 0.9725584983825684
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"type": "choice",
|
| 15 |
+
"probs": [
|
| 16 |
+
0.9617580771446228,
|
| 17 |
+
0.014839934185147285,
|
| 18 |
+
0.02340202033519745
|
| 19 |
+
],
|
| 20 |
+
"choice": 0
|
| 21 |
+
}
|
| 22 |
+
],
|
| 23 |
+
"coreai": [
|
| 24 |
+
{
|
| 25 |
+
"type": "noul",
|
| 26 |
+
"p_yes": 0.9725583791732788
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"type": "choice",
|
| 30 |
+
"probs": [
|
| 31 |
+
0.9617581963539124,
|
| 32 |
+
0.014839922077953815,
|
| 33 |
+
0.023401914164423943
|
| 34 |
+
],
|
| 35 |
+
"choice": 0
|
| 36 |
+
}
|
| 37 |
+
]
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"case": "span",
|
| 41 |
+
"tokens": 123,
|
| 42 |
+
"reference_precision": "float32",
|
| 43 |
+
"max_probability_or_box_error": 0,
|
| 44 |
+
"decisions_match": true,
|
| 45 |
+
"upstream": [
|
| 46 |
+
{
|
| 47 |
+
"type": "span",
|
| 48 |
+
"start": 55,
|
| 49 |
+
"end": 63,
|
| 50 |
+
"text": "INV-2231",
|
| 51 |
+
"p": 1.0
|
| 52 |
+
}
|
| 53 |
+
],
|
| 54 |
+
"coreai": [
|
| 55 |
+
{
|
| 56 |
+
"type": "span",
|
| 57 |
+
"start": 55,
|
| 58 |
+
"end": 63,
|
| 59 |
+
"text": "INV-2231",
|
| 60 |
+
"p": 1.0
|
| 61 |
+
}
|
| 62 |
+
]
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"case": "score",
|
| 66 |
+
"tokens": 98,
|
| 67 |
+
"reference_precision": "float32",
|
| 68 |
+
"max_probability_or_box_error": 1.7285346984863281e-06,
|
| 69 |
+
"decisions_match": true,
|
| 70 |
+
"upstream": [
|
| 71 |
+
{
|
| 72 |
+
"type": "score",
|
| 73 |
+
"probs": [
|
| 74 |
+
0.011814874596893787,
|
| 75 |
+
0.012464769184589386,
|
| 76 |
+
0.05883542448282242,
|
| 77 |
+
0.9168848395347595
|
| 78 |
+
],
|
| 79 |
+
"choice": 3,
|
| 80 |
+
"expected": 2.880790136754513
|
| 81 |
+
}
|
| 82 |
+
],
|
| 83 |
+
"coreai": [
|
| 84 |
+
{
|
| 85 |
+
"type": "score",
|
| 86 |
+
"probs": [
|
| 87 |
+
0.01181461475789547,
|
| 88 |
+
0.012464579194784164,
|
| 89 |
+
0.058834243565797806,
|
| 90 |
+
0.916886568069458
|
| 91 |
+
],
|
| 92 |
+
"choice": 3,
|
| 93 |
+
"expected": 2.880792770534754
|
| 94 |
+
}
|
| 95 |
+
]
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"case": "multi",
|
| 99 |
+
"tokens": 144,
|
| 100 |
+
"reference_precision": "float32",
|
| 101 |
+
"max_probability_or_box_error": 2.384185791015625e-07,
|
| 102 |
+
"decisions_match": true,
|
| 103 |
+
"upstream": [
|
| 104 |
+
{
|
| 105 |
+
"type": "multi",
|
| 106 |
+
"probs": [
|
| 107 |
+
0.9698117971420288,
|
| 108 |
+
0.9617175459861755,
|
| 109 |
+
0.021801624447107315
|
| 110 |
+
]
|
| 111 |
+
}
|
| 112 |
+
],
|
| 113 |
+
"coreai": [
|
| 114 |
+
{
|
| 115 |
+
"type": "multi",
|
| 116 |
+
"probs": [
|
| 117 |
+
0.9698116779327393,
|
| 118 |
+
0.9617173075675964,
|
| 119 |
+
0.021801499649882317
|
| 120 |
+
]
|
| 121 |
+
}
|
| 122 |
+
]
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
"case": "negative",
|
| 126 |
+
"tokens": 83,
|
| 127 |
+
"reference_precision": "float32",
|
| 128 |
+
"max_probability_or_box_error": 1.825392246246338e-07,
|
| 129 |
+
"decisions_match": true,
|
| 130 |
+
"upstream": [
|
| 131 |
+
{
|
| 132 |
+
"type": "noul",
|
| 133 |
+
"p_yes": 0.050108104944229126
|
| 134 |
+
}
|
| 135 |
+
],
|
| 136 |
+
"coreai": [
|
| 137 |
+
{
|
| 138 |
+
"type": "noul",
|
| 139 |
+
"p_yes": 0.0501079224050045
|
| 140 |
+
}
|
| 141 |
+
]
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
"case": "unicode",
|
| 145 |
+
"tokens": 128,
|
| 146 |
+
"reference_precision": "float32",
|
| 147 |
+
"max_probability_or_box_error": 2.384185791015625e-07,
|
| 148 |
+
"decisions_match": true,
|
| 149 |
+
"upstream": [
|
| 150 |
+
{
|
| 151 |
+
"type": "span",
|
| 152 |
+
"start": 10,
|
| 153 |
+
"end": 17,
|
| 154 |
+
"text": "\u20ac123.45",
|
| 155 |
+
"p": 0.9975249171257019
|
| 156 |
+
},
|
| 157 |
+
{
|
| 158 |
+
"type": "choice",
|
| 159 |
+
"probs": [
|
| 160 |
+
0.975895881652832,
|
| 161 |
+
0.012953696772456169,
|
| 162 |
+
0.011150466278195381
|
| 163 |
+
],
|
| 164 |
+
"choice": 0
|
| 165 |
+
}
|
| 166 |
+
],
|
| 167 |
+
"coreai": [
|
| 168 |
+
{
|
| 169 |
+
"type": "span",
|
| 170 |
+
"start": 10,
|
| 171 |
+
"end": 17,
|
| 172 |
+
"text": "\u20ac123.45",
|
| 173 |
+
"p": 0.9975249171257019
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"type": "choice",
|
| 177 |
+
"probs": [
|
| 178 |
+
0.9758961200714111,
|
| 179 |
+
0.012953538447618484,
|
| 180 |
+
0.011150323785841465
|
| 181 |
+
],
|
| 182 |
+
"choice": 0
|
| 183 |
+
}
|
| 184 |
+
]
|
| 185 |
+
},
|
| 186 |
+
{
|
| 187 |
+
"case": "options",
|
| 188 |
+
"tokens": 230,
|
| 189 |
+
"reference_precision": "float32",
|
| 190 |
+
"max_probability_or_box_error": 5.960464477539063e-08,
|
| 191 |
+
"decisions_match": true,
|
| 192 |
+
"upstream": [
|
| 193 |
+
{
|
| 194 |
+
"type": "choice",
|
| 195 |
+
"probs": [
|
| 196 |
+
0.004064494278281927,
|
| 197 |
+
0.0038580188993364573,
|
| 198 |
+
0.004268646240234375,
|
| 199 |
+
0.0035152093041688204,
|
| 200 |
+
0.0033423479180783033,
|
| 201 |
+
0.0027638780884444714,
|
| 202 |
+
0.0034003546461462975,
|
| 203 |
+
0.0028926823288202286,
|
| 204 |
+
0.002699152799323201,
|
| 205 |
+
0.002128811087459326,
|
| 206 |
+
0.0030107321217656136,
|
| 207 |
+
0.0031014070846140385,
|
| 208 |
+
0.0031818263232707977,
|
| 209 |
+
0.0027776346541941166,
|
| 210 |
+
0.002535774139687419,
|
| 211 |
+
0.003921506926417351,
|
| 212 |
+
0.0034300386905670166,
|
| 213 |
+
0.9298884868621826,
|
| 214 |
+
0.003782431362196803,
|
| 215 |
+
0.0032975035719573498,
|
| 216 |
+
0.0014300656039267778,
|
| 217 |
+
0.001529851695522666,
|
| 218 |
+
0.0019381233723834157,
|
| 219 |
+
0.0032409545965492725
|
| 220 |
+
],
|
| 221 |
+
"choice": 17
|
| 222 |
+
}
|
| 223 |
+
],
|
| 224 |
+
"coreai": [
|
| 225 |
+
{
|
| 226 |
+
"type": "choice",
|
| 227 |
+
"probs": [
|
| 228 |
+
0.004064517095685005,
|
| 229 |
+
0.00385798211209476,
|
| 230 |
+
0.004268613178282976,
|
| 231 |
+
0.0035152393393218517,
|
| 232 |
+
0.0033423572313040495,
|
| 233 |
+
0.0027638806495815516,
|
| 234 |
+
0.0034003155305981636,
|
| 235 |
+
0.0028926795348525047,
|
| 236 |
+
0.0026991423219442368,
|
| 237 |
+
0.00212881900370121,
|
| 238 |
+
0.0030107346829026937,
|
| 239 |
+
0.00310140079818666,
|
| 240 |
+
0.0031818472780287266,
|
| 241 |
+
0.002777676796540618,
|
| 242 |
+
0.002535800449550152,
|
| 243 |
+
0.003921480383723974,
|
| 244 |
+
0.003430022159591317,
|
| 245 |
+
0.9298885464668274,
|
| 246 |
+
0.003782423911616206,
|
| 247 |
+
0.0032975096255540848,
|
| 248 |
+
0.0014300619950518012,
|
| 249 |
+
0.0015298567013815045,
|
| 250 |
+
0.0019381335005164146,
|
| 251 |
+
0.0032409762497991323
|
| 252 |
+
],
|
| 253 |
+
"choice": 17
|
| 254 |
+
}
|
| 255 |
+
]
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"case": "vision",
|
| 259 |
+
"tokens": 155,
|
| 260 |
+
"reference_precision": "float32",
|
| 261 |
+
"max_probability_or_box_error": 1.0192394256591797e-05,
|
| 262 |
+
"decisions_match": true,
|
| 263 |
+
"upstream": [
|
| 264 |
+
{
|
| 265 |
+
"type": "region",
|
| 266 |
+
"cell": 9,
|
| 267 |
+
"box": [
|
| 268 |
+
0.1936,
|
| 269 |
+
0.0,
|
| 270 |
+
0.5538,
|
| 271 |
+
0.8461
|
| 272 |
+
],
|
| 273 |
+
"p": 0.5629251003265381
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
"type": "choice",
|
| 277 |
+
"probs": [
|
| 278 |
+
0.9847151041030884,
|
| 279 |
+
0.015284925699234009
|
| 280 |
+
],
|
| 281 |
+
"choice": 0
|
| 282 |
+
}
|
| 283 |
+
],
|
| 284 |
+
"coreai": [
|
| 285 |
+
{
|
| 286 |
+
"type": "region",
|
| 287 |
+
"cell": 9,
|
| 288 |
+
"box": [
|
| 289 |
+
0.1936,
|
| 290 |
+
0.0,
|
| 291 |
+
0.5538,
|
| 292 |
+
0.8461
|
| 293 |
+
],
|
| 294 |
+
"p": 0.5629352927207947
|
| 295 |
+
},
|
| 296 |
+
{
|
| 297 |
+
"type": "choice",
|
| 298 |
+
"probs": [
|
| 299 |
+
0.9847150444984436,
|
| 300 |
+
0.015284995548427105
|
| 301 |
+
],
|
| 302 |
+
"choice": 0
|
| 303 |
+
}
|
| 304 |
+
]
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
"case": "image-option",
|
| 308 |
+
"tokens": 181,
|
| 309 |
+
"reference_precision": "float32",
|
| 310 |
+
"max_probability_or_box_error": 3.3527612686157227e-08,
|
| 311 |
+
"decisions_match": true,
|
| 312 |
+
"upstream": [
|
| 313 |
+
{
|
| 314 |
+
"type": "choice",
|
| 315 |
+
"probs": [
|
| 316 |
+
0.9767023324966431,
|
| 317 |
+
0.023297706618905067
|
| 318 |
+
],
|
| 319 |
+
"choice": 0
|
| 320 |
+
}
|
| 321 |
+
],
|
| 322 |
+
"coreai": [
|
| 323 |
+
{
|
| 324 |
+
"type": "choice",
|
| 325 |
+
"probs": [
|
| 326 |
+
0.9767023324966431,
|
| 327 |
+
0.02329767309129238
|
| 328 |
+
],
|
| 329 |
+
"choice": 0
|
| 330 |
+
}
|
| 331 |
+
]
|
| 332 |
+
}
|
| 333 |
+
]
|
vision.aimodel/main.hash
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
dj�g��'��-+��&��zuh^���N���
|
vision.aimodel/main.mlirb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1f646ae2677fb9ba2793ec942d2b9ed82690199d7a75685ecdf4a84ea791f38f
|
| 3 |
+
size 1317394842
|
vision.aimodel/metadata.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"assetVersion" : "2.0",
|
| 3 |
+
"creationDate" : "20261014T012236Z",
|
| 4 |
+
"producer" : "coreai-core 1.0.0b3"
|
| 5 |
+
}
|