MyeongHoJeong commited on
Commit
c1264c3
·
verified ·
1 Parent(s): 10e5ace

Standard One 8B SH: adapter, schema head and server code

Browse files
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2023-2024 SGLang Team
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ base_model: StandardThinking/StandardOne-8B
3
+ base_model_relation: adapter
4
+ library_name: peft
5
+ license: apache-2.0
6
+ pipeline_tag: text-classification
7
+ language:
8
+ - en
9
+ tags:
10
+ - mistral3
11
+ - decision-model
12
+ - typed-decisions
13
+ - schema-head
14
+ - jev
15
+ - calibration
16
+ - decode-free
17
+ ---
18
+
19
+ # Standard One 8B SH
20
+
21
+ **Standard One 8B SH** is Standard One 8B with a **joint schema head** (SH). It reads the scenario once and
22
+ scores every option of every question in a single forward pass, then returns probabilities through the same
23
+ `POST /v1/systemone` contract as Standard One. It does not generate text.
24
+
25
+ | | |
26
+ |---|---|
27
+ | Backbone | [StandardOne-8B](https://huggingface.co/StandardThinking/StandardOne-8B), revision `v2.2` (`ac495d1c221297f0ae951941eff27bcf58b5ce52`) |
28
+ | This repository | LoRA adapter (rank 64) for the backbone, the schema head (70.6M parameters) and the server code |
29
+ | Question types | `choice` (2–255 options), `noul` (yes/no), `score` (ordinal scale) |
30
+ | Context | up to 32,768 tokens per request |
31
+ | License | Apache-2.0 |
32
+
33
+ ## Why a schema head
34
+
35
+ Standard One reads one answer letter per option, so a question is limited by the label alphabet and every
36
+ question in a request needs its own pass over the scenario. The schema head instead attends over the hidden states
37
+ of the whole request (the final layer and one intermediate layer), places every question and option in one joint
38
+ graph, and returns a calibrated distribution for each question at once. In practice:
39
+
40
+ - **Many options.** Intent and routing questions with 100–255 options keep nearly the accuracy of small option sets.
41
+ - **Many questions per scenario.** All questions of a request share one pass over the scenario.
42
+ - **Long scenarios.** Decisions buried in long documents are found far more reliably than with Standard One v2.2.
43
+
44
+ ## Results
45
+
46
+ All numbers below compare Standard One 8B SH with Standard One 8B v2.2 on identical items, scored the same way.
47
+
48
+ | Benchmark | Standard One 8B v2.2 | Standard One 8B SH |
49
+ |---|---:|---:|
50
+ | [Decision Index](https://huggingface.co/spaces/multimodalart/jev-decision-index) 0.2.1 (self-run, full suite) | 41.14 | **46.17** |
51
+ | JevBench public — hard tier (111 items) | 56.76 | **69.37** |
52
+ | JevBench public — original tier (72 items) | 98.61 | 98.61 |
53
+ | JevBench public — easy tier (48 items) | 100.00 | 100.00 |
54
+ | Intent labels with 27–151 options (CLINC150, BANKING77, MASSIVE; 18,000 items) | 82.67 | **92.09** |
55
+ | Intent labels with 152–255 options (CLINC150 utterances, 4,400 items) | 85.34 | **96.91** |
56
+ | Decision hidden in a long document (150 items) | 40.00 | **88.00** |
57
+ | WinoGrande (dev, 2,010 items) | 87.86 | **94.98** |
58
+ | Policy-judgement repair (1,200 items) | 60.58 | **74.25** |
59
+ | Game positions: dots and boxes / snake and 2048 / tetris | 38.37 / 58.63 / 54.86 | **51.96 / 68.37 / 64.19** |
60
+ | Held-out decision suite (600 items) | 82.50 | **88.33** |
61
+ | Held-out realistic decision suite (600 items) | 91.50 | **91.67** |
62
+
63
+ Accuracy in percent, except the Decision Index (its own 0–100 index). The Decision Index run covers all 150,759
64
+ in-edition requests of edition 0.2.1; it is our own run with the kit's scorer, not a leaderboard entry.
65
+
66
+ ## Quick start
67
+
68
+ ```bash
69
+ pip install "torch>=2.4" "transformers==5.12.1" "peft==0.21.0" safetensors
70
+ hf download StandardThinking/StandardOne-8B --revision v2.2 --local-dir StandardOne-8B
71
+ hf download StandardThinking/StandardOne-8B-SH --local-dir StandardOne-8B-SH
72
+
73
+ python StandardOne-8B-SH/code/serve_head.py \
74
+ --snapshot StandardOne-8B --adapter StandardOne-8B-SH/adapter --head StandardOne-8B-SH/head \
75
+ --port 30171
76
+ ```
77
+
78
+ ```bash
79
+ curl -s localhost:30171/v1/systemone -H 'content-type: application/json' -d '{
80
+ "state": {"message": "Please close my card, I lost it yesterday."},
81
+ "questions": {
82
+ "intent": {"type": "choice", "instructions": "What does the customer want?",
83
+ "criteria": {"cancel": "close the card", "limit": "change the limit", "other": null}},
84
+ "urgent": {"type": "noul", "instructions": "The request is urgent."},
85
+ "mood": {"type": "score", "instructions": "How upset is the customer?", "criteria": ["calm", "annoyed", "angry"]}
86
+ }
87
+ }'
88
+ ```
89
+
90
+ The server batches concurrent requests (`--max-batch-requests`, `--max-batch-tokens`) and binds `127.0.0.1` by
91
+ default. It runs on one GPU with the backbone in bfloat16 and the head in float32.
92
+
93
+ ## Limitations
94
+
95
+ - Requests longer than 32,768 tokens are refused, not truncated.
96
+ - On our hard held-out decision suite (600 items; long policies, multi-step lookups, date and number reasoning)
97
+ it scores 43.50 against 53.50 for Standard One 8B v2.2. Prefer Standard One 8B for that kind of workload.
98
+ - Text input only has been validated for this release.
99
+ - The server is a PyTorch reference server; it does not yet run on an inference engine, so throughput under heavy
100
+ load is lower than the engine-served Standard One models.
101
+ - Probabilities are calibrated on our held-out data; refit thresholds on your own distribution before relying on them.
102
+
103
+ ## License
104
+
105
+ Apache-2.0. The backbone is [Standard One 8B](https://huggingface.co/StandardThinking/StandardOne-8B) (Apache-2.0),
106
+ built on [Ministral 3 8B](https://huggingface.co/mistralai/Ministral-3-8B-Instruct-2512-BF16) (Apache-2.0).
adapter/adapter_config.json ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "alora_invocation_tokens": null,
3
+ "alpha_pattern": {},
4
+ "arrow_config": null,
5
+ "auto_mapping": {
6
+ "base_model_class": "Mistral3ForConditionalGeneration",
7
+ "parent_library": "transformers.models.mistral3.modeling_mistral3"
8
+ },
9
+ "base_model_name_or_path": "StandardThinking/StandardOne-8B",
10
+ "bias": "none",
11
+ "corda_config": null,
12
+ "ensure_weight_tying": false,
13
+ "eva_config": null,
14
+ "exclude_modules": ".*\\.(vision_tower|multi_modal_projector)\\..*",
15
+ "fan_in_fan_out": false,
16
+ "inference_mode": true,
17
+ "init_lora_weights": true,
18
+ "kasa_config": null,
19
+ "layer_replication": null,
20
+ "layers_pattern": null,
21
+ "layers_to_transform": null,
22
+ "loftq_config": {},
23
+ "lora_alpha": 128.0,
24
+ "lora_bias": false,
25
+ "lora_dropout": 0.0,
26
+ "lora_ga_config": null,
27
+ "megatron_config": null,
28
+ "megatron_core": "megatron.core",
29
+ "modules_to_save": null,
30
+ "monteclora_config": null,
31
+ "peft_type": "LORA",
32
+ "peft_version": "0.21.0",
33
+ "qalora_group_size": 16,
34
+ "r": 64,
35
+ "rank_pattern": {},
36
+ "revision": null,
37
+ "target_modules": [
38
+ "v_proj",
39
+ "k_proj",
40
+ "q_proj",
41
+ "gate_proj",
42
+ "down_proj",
43
+ "o_proj",
44
+ "up_proj"
45
+ ],
46
+ "target_parameters": null,
47
+ "task_type": null,
48
+ "trainable_token_indices": null,
49
+ "use_bdlora": null,
50
+ "use_dora": false,
51
+ "use_qalora": false,
52
+ "use_rslora": false,
53
+ "velora_config": null
54
+ }
adapter/adapter_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e41861bdf42c041c3b2018b65ca718a3ff0c523336335fa8b3a8cd832a59e27f
3
+ size 713103144
code/__pycache__/head.cpython-312.pyc ADDED
Binary file (29.1 kB). View file
 
code/__pycache__/model.cpython-312.pyc ADDED
Binary file (19.4 kB). View file
 
code/__pycache__/render.cpython-312.pyc ADDED
Binary file (27.4 kB). View file
 
code/__pycache__/scoring.cpython-312.pyc ADDED
Binary file (15.6 kB). View file
 
code/__pycache__/serve_head.cpython-312.pyc ADDED
Binary file (13.1 kB). View file
 
code/head.py ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Joint schema head (DESIGN.md section 2.2). Own design; provenance in DESIGN.md section 0.
2
+
3
+ Reads tapped backbone hidden states for ONE request at a time (a micro-batch is a Python loop over requests; the
4
+ head is small) and returns one logit vector per question over that question's options in CANONICAL order.
5
+
6
+ Nodes: one per question and one per option. No positional encoding over nodes, attention pooling inside spans and
7
+ relation-type attention biases that depend only on structure make the head exactly equivariant to the order of
8
+ choice/noul options and of questions (tests/test_equivariance.py). Score levels carry an ordinal encoding and are
9
+ deliberately not permutation-equivariant.
10
+ """
11
+ import math
12
+ from dataclasses import asdict, dataclass, field
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ import torch.nn.functional as F
17
+ from torch.utils.checkpoint import checkpoint
18
+
19
+ QTYPE_ID = {"choice": 0, "noul": 1, "score": 2}
20
+ # relation types for node pairs (i attends to j)
21
+ REL_SELF, REL_SIBLING, REL_OPT_TO_OWN_Q, REL_Q_TO_OWN_OPT, REL_OPT_TO_OTHER_OPT, REL_OPT_TO_OTHER_Q, \
22
+ REL_Q_TO_OTHER_Q, REL_Q_TO_OTHER_OPT = range(8)
23
+ N_REL = 8
24
+ ORD_FEATS = 33
25
+
26
+
27
+ @dataclass
28
+ class HeadConfig:
29
+ hidden_size: int = 4096
30
+ n_taps: int = 2
31
+ d: int = 1024
32
+ heads: int = 16
33
+ ffn: int = 2816
34
+ route_blocks: int = 2
35
+ interact_blocks: int = 2
36
+ dropout: float = 0.1
37
+ n_roles: int = 5
38
+ ord_hidden: int = 256
39
+ max_nodes: int = 4096
40
+ grad_checkpoint: bool = True
41
+ taps: list = field(default_factory=lambda: ["final", 25])
42
+ template: str = "schema-v1"
43
+
44
+ def to_dict(self):
45
+ return asdict(self)
46
+
47
+
48
+ class RMSNorm(nn.Module):
49
+ def __init__(self, dim, eps=1e-6):
50
+ super().__init__()
51
+ self.weight = nn.Parameter(torch.ones(dim))
52
+ self.eps = eps
53
+
54
+ def forward(self, x):
55
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) * self.weight
56
+
57
+
58
+ class SwiGLU(nn.Module):
59
+ def __init__(self, d, hidden, dropout):
60
+ super().__init__()
61
+ self.w1 = nn.Linear(d, hidden, bias=False)
62
+ self.w3 = nn.Linear(d, hidden, bias=False)
63
+ self.w2 = nn.Linear(hidden, d, bias=False)
64
+ self.drop = nn.Dropout(dropout)
65
+
66
+ def forward(self, x):
67
+ return self.drop(self.w2(F.silu(self.w1(x)) * self.w3(x)))
68
+
69
+
70
+ class Attention(nn.Module):
71
+ """Multi-head attention (queries from nodes, keys/values from `kv`), SDPA with optional additive bias."""
72
+
73
+ def __init__(self, d, heads, dropout):
74
+ super().__init__()
75
+ self.h, self.dh = heads, d // heads
76
+ self.q = nn.Linear(d, d, bias=False)
77
+ self.k = nn.Linear(d, d, bias=False)
78
+ self.v = nn.Linear(d, d, bias=False)
79
+ self.o = nn.Linear(d, d, bias=False)
80
+ self.p = dropout
81
+
82
+ def _chunk(self, q, part, p):
83
+ k = self.k(part).view(-1, self.h, self.dh).transpose(0, 1).unsqueeze(0)
84
+ v = self.v(part).view(-1, self.h, self.dh).transpose(0, 1).unsqueeze(0)
85
+ scores = (q @ k.transpose(-1, -2)) / math.sqrt(self.dh)
86
+ lse = torch.logsumexp(scores, -1, keepdim=True)
87
+ probs = F.dropout(torch.softmax(scores, -1), p, self.training)
88
+ return probs @ v, lse
89
+
90
+ def forward(self, x, kv, bias=None, kv_chunk=0):
91
+ n, m = x.shape[0], kv.shape[0]
92
+ q = self.q(x).view(n, self.h, self.dh).transpose(0, 1).unsqueeze(0)
93
+ p = self.p if self.training else 0.0
94
+ if kv_chunk and m > kv_chunk and bias is None:
95
+ # exact chunked attention over keys (log-sum-exp merge) to bound K/V memory at long contexts.
96
+ # Dropout is applied to each chunk's attention PROBABILITIES (as SDPA's dropout_p): the global weight of
97
+ # key j is softmax_chunk(j) * exp(lse_chunk - lse_all), so dropping the chunk-local probability with
98
+ # scale 1/(1-p) is the same random variable as dropping the global one. When training, every chunk is
99
+ # recomputed in backward (checkpoint), so its scores/probabilities are not kept for the whole sequence.
100
+ outs, lses = [], []
101
+ use_ckpt = self.training and torch.is_grad_enabled()
102
+ for s in range(0, m, kv_chunk):
103
+ part = kv[s:s + kv_chunk]
104
+ if use_ckpt:
105
+ o, lse = checkpoint(self._chunk, q, part, p, use_reentrant=False)
106
+ else:
107
+ o, lse = self._chunk(q, part, p)
108
+ outs.append(o)
109
+ lses.append(lse)
110
+ lse_all = torch.logsumexp(torch.cat(lses, -1), -1, keepdim=True)
111
+ out = sum(o * torch.exp(l - lse_all) for o, l in zip(outs, lses))
112
+ else:
113
+ k = self.k(kv).view(m, self.h, self.dh).transpose(0, 1).unsqueeze(0)
114
+ v = self.v(kv).view(m, self.h, self.dh).transpose(0, 1).unsqueeze(0)
115
+ mask = None if bias is None else bias.unsqueeze(0)
116
+ out = F.scaled_dot_product_attention(q, k, v, attn_mask=mask, dropout_p=p)
117
+ return self.o(out.squeeze(0).transpose(0, 1).reshape(n, -1))
118
+
119
+
120
+ class RouteBlock(nn.Module):
121
+ """Stage 1 (evidence routing): nodes cross-attend over the state tokens, then FFN."""
122
+
123
+ def __init__(self, cfg):
124
+ super().__init__()
125
+ self.n1, self.n2, self.nkv = RMSNorm(cfg.d), RMSNorm(cfg.d), RMSNorm(cfg.d)
126
+ self.cross = Attention(cfg.d, cfg.heads, cfg.dropout)
127
+ self.ffn = SwiGLU(cfg.d, cfg.ffn, cfg.dropout)
128
+ self.drop = nn.Dropout(cfg.dropout)
129
+
130
+ def forward(self, x, state, kv_chunk=0):
131
+ x = x + self.drop(self.cross(self.n1(x), self.nkv(state), kv_chunk=kv_chunk))
132
+ return x + self.ffn(self.n2(x))
133
+
134
+
135
+ class InteractBlock(nn.Module):
136
+ """Stage 2 (field interaction): node self-attention with relation-type biases, cross-attention back to the
137
+ state tokens, FFN."""
138
+
139
+ def __init__(self, cfg):
140
+ super().__init__()
141
+ self.n1, self.n2, self.n3, self.nkv = RMSNorm(cfg.d), RMSNorm(cfg.d), RMSNorm(cfg.d), RMSNorm(cfg.d)
142
+ self.self_attn = Attention(cfg.d, cfg.heads, cfg.dropout)
143
+ self.cross = Attention(cfg.d, cfg.heads, cfg.dropout)
144
+ self.rel_bias = nn.Parameter(torch.zeros(N_REL, cfg.heads))
145
+ self.ffn = SwiGLU(cfg.d, cfg.ffn, cfg.dropout)
146
+ self.drop = nn.Dropout(cfg.dropout)
147
+
148
+ def forward(self, x, state, rel, kv_chunk=0):
149
+ bias = self.rel_bias[rel].permute(2, 0, 1) # [heads, N, N]
150
+ y = self.n1(x)
151
+ x = x + self.drop(self.self_attn(y, y, bias=bias))
152
+ x = x + self.drop(self.cross(self.n2(x), self.nkv(state), kv_chunk=kv_chunk))
153
+ return x + self.ffn(self.n3(x))
154
+
155
+
156
+ def ordinal_features(k, K):
157
+ """33 features of score level k of K (2 <= K <= 10): position, scale size, one-hots, sin/cos."""
158
+ pos = k / (K - 1)
159
+ feats = [pos, K / 10.0]
160
+ feats += [1.0 if i == k else 0.0 for i in range(10)]
161
+ feats += [1.0 if K == i else 0.0 for i in range(2, 11)]
162
+ for f in range(1, 7):
163
+ feats += [math.sin(math.pi * f * pos), math.cos(math.pi * f * pos)]
164
+ assert len(feats) == ORD_FEATS
165
+ return feats
166
+
167
+
168
+ class JointSchemaHead(nn.Module):
169
+ def __init__(self, cfg: HeadConfig):
170
+ super().__init__()
171
+ self.cfg = cfg
172
+ d = cfg.d
173
+ self.tap_norms = nn.ModuleList([RMSNorm(cfg.hidden_size) for _ in range(cfg.n_taps)])
174
+ self.tap_mix = nn.Parameter(torch.zeros(cfg.n_taps))
175
+ self.tap_gain = nn.Parameter(torch.ones(1))
176
+ self.in_proj = nn.Linear(cfg.hidden_size, d, bias=False)
177
+ self.role_emb = nn.Embedding(cfg.n_roles, d)
178
+ self.qtype_emb = nn.Embedding(3, d)
179
+ self.kind_emb = nn.Embedding(2, d) # 0 question node, 1 option node
180
+ self.pool_query = nn.Parameter(torch.randn(2, d) / math.sqrt(d))
181
+ self.pool_key = nn.Linear(d, d, bias=False)
182
+ self.pool_out = nn.Linear(2 * d, d, bias=False)
183
+ self.ord_mlp = nn.Sequential(nn.Linear(ORD_FEATS, cfg.ord_hidden), nn.GELU(), nn.Linear(cfg.ord_hidden, d))
184
+ self.route = nn.ModuleList([RouteBlock(cfg) for _ in range(cfg.route_blocks)])
185
+ self.interact = nn.ModuleList([InteractBlock(cfg) for _ in range(cfg.interact_blocks)])
186
+ self.final_norm = RMSNorm(d)
187
+ self.scorer = nn.Sequential(nn.Linear(3 * d, d), nn.GELU(), nn.Linear(d, 1))
188
+ self.log_temp = nn.Parameter(torch.zeros(3))
189
+ self.kv_chunk = 0
190
+ self._init()
191
+
192
+ def _init(self):
193
+ for m in self.modules():
194
+ if isinstance(m, nn.Linear):
195
+ nn.init.normal_(m.weight, std=0.02)
196
+ if m.bias is not None:
197
+ nn.init.zeros_(m.bias)
198
+ elif isinstance(m, nn.Embedding):
199
+ nn.init.normal_(m.weight, std=0.02)
200
+
201
+ # ------------------------------------------------------------------ structure
202
+ @staticmethod
203
+ def plan(enc, device):
204
+ """Index tensors for one encoded request (see render.Encoder.encode)."""
205
+ node_kind, node_q, node_qtype, node_ord = [], [], [], []
206
+ pool_node, pool_tok, last_tok = [], [], []
207
+ q_node, opt_nodes = [], []
208
+ nq = len(enc["keys"])
209
+ for i in range(nq):
210
+ qt = enc["qtype"][i]
211
+ idx = len(node_kind)
212
+ q_node.append(idx)
213
+ node_kind.append(0); node_q.append(i); node_qtype.append(qt); node_ord.append(None)
214
+ toks = list(range(*enc["q_span"][i]))
215
+ if enc["p_span"][i]:
216
+ toks = list(range(*enc["p_span"][i])) + toks
217
+ pool_node += [idx] * len(toks); pool_tok += toks
218
+ last_tok.append(enc["q_span"][i][1] - 1)
219
+ ids = []
220
+ K = enc["n_opt"][i]
221
+ for j in range(K):
222
+ idx = len(node_kind)
223
+ ids.append(idx)
224
+ node_kind.append(1); node_q.append(i); node_qtype.append(qt)
225
+ node_ord.append((j, K) if qt == "score" else None)
226
+ a, b = enc["o_span"][i][j]
227
+ pool_node += [idx] * (b - a); pool_tok += list(range(a, b))
228
+ last_tok.append(b - 1)
229
+ opt_nodes.append(ids)
230
+ n = len(node_kind)
231
+ kind = torch.tensor(node_kind)
232
+ qn = torch.tensor(node_q)
233
+ same_q = qn[:, None] == qn[None, :]
234
+ ki, kj = kind[:, None], kind[None, :]
235
+ rel = torch.empty(n, n, dtype=torch.long)
236
+ rel[(ki == 1) & (kj == 1) & same_q] = REL_SIBLING
237
+ rel[(ki == 1) & (kj == 0) & same_q] = REL_OPT_TO_OWN_Q
238
+ rel[(ki == 0) & (kj == 1) & same_q] = REL_Q_TO_OWN_OPT
239
+ rel[(ki == 1) & (kj == 1) & ~same_q] = REL_OPT_TO_OTHER_OPT
240
+ rel[(ki == 1) & (kj == 0) & ~same_q] = REL_OPT_TO_OTHER_Q
241
+ rel[(ki == 0) & (kj == 0)] = REL_Q_TO_OTHER_Q
242
+ rel[(ki == 0) & (kj == 1) & ~same_q] = REL_Q_TO_OTHER_OPT
243
+ rel.fill_diagonal_(REL_SELF)
244
+ ords = [ordinal_features(*o) if o else [0.0] * ORD_FEATS for o in node_ord]
245
+ state_tok = [t for a, b in enc["state_spans"] for t in range(a, b)]
246
+ p = {"n": n, "kind": kind.to(device), "qtype": torch.tensor([QTYPE_ID[t] for t in node_qtype]).to(device),
247
+ "is_score": torch.tensor([o is not None for o in node_ord]).to(device),
248
+ "ord": torch.tensor(ords, dtype=torch.float32).to(device), "rel": rel.to(device),
249
+ "pool_node": torch.tensor(pool_node).to(device), "pool_tok": torch.tensor(pool_tok).to(device),
250
+ "last_tok": torch.tensor(last_tok).to(device), "q_node": q_node, "opt_nodes": opt_nodes,
251
+ "state_tok": torch.tensor(state_tok).to(device), "qtypes": list(enc["qtype"])}
252
+ return p
253
+
254
+ # ------------------------------------------------------------------ forward
255
+ def mix_taps(self, taps):
256
+ """taps: list of [T, hidden] tensors (any float dtype) -> [T, hidden] in the head's dtype (fp32)."""
257
+ w = torch.softmax(self.tap_mix, 0)
258
+ dtype = self.in_proj.weight.dtype
259
+ out = 0
260
+ for i, t in enumerate(taps):
261
+ out = out + w[i] * self.tap_norms[i](t.to(dtype))
262
+ return out * self.tap_gain
263
+
264
+ def _token_proj(self, roles, *taps):
265
+ return self.in_proj(self.mix_taps(list(taps))) + self.role_emb(roles)
266
+
267
+ def project_tokens(self, taps, roles, use_ckpt, chunk=8192):
268
+ """Per-token tap mix + in-proj + role embedding, in token chunks (each checkpointed when training), so the
269
+ fp32 intermediates of only one chunk exist at a time (forward and recompute)."""
270
+ outs = []
271
+ for a in range(0, roles.shape[0], chunk):
272
+ args = (roles[a:a + chunk], *[t[a:a + chunk] for t in taps])
273
+ outs.append(checkpoint(self._token_proj, *args, use_reentrant=False) if use_ckpt else self._token_proj(*args))
274
+ return outs[0] if len(outs) == 1 else torch.cat(outs, 0)
275
+
276
+ def forward_one(self, taps, enc, roles):
277
+ """taps: list of [T, hidden]; enc: encoded request; roles: LongTensor [T]. Returns list of logit vectors
278
+ (one per question in enc['keys'] order), each over CANONICAL option order, already divided by the
279
+ per-type temperature."""
280
+ cfg = self.cfg
281
+ dev = taps[0].device
282
+ p = self.plan(enc, dev)
283
+ require_nodes = p["n"] <= cfg.max_nodes
284
+ if not require_nodes:
285
+ raise ValueError(f"too many nodes: {p['n']} > {cfg.max_nodes}")
286
+ use_ckpt = cfg.grad_checkpoint and self.training and torch.is_grad_enabled()
287
+ # token-level projection: chunked + checkpointed so only the (already alive, bf16) tapped states are kept for
288
+ # backward, not ~4 fp32 [T, hidden] intermediates per tap (about 2 GB per tap at 32k tokens)
289
+ x_tok = self.project_tokens(taps, roles, use_ckpt)
290
+ state = x_tok.index_select(0, p["state_tok"])
291
+ # attention pooling inside spans (segment softmax via scatter)
292
+ toks = x_tok.index_select(0, p["pool_tok"])
293
+ node_of = p["pool_node"]
294
+ kind_of = p["kind"].index_select(0, node_of)
295
+ query = self.pool_query.index_select(0, kind_of)
296
+ scores = (self.pool_key(toks) * query).sum(-1) / math.sqrt(cfg.d)
297
+ mx = torch.full((p["n"],), -1e30, device=dev, dtype=scores.dtype)
298
+ mx = mx.scatter_reduce(0, node_of, scores, reduce="amax", include_self=True)
299
+ e = torch.exp(scores - mx.index_select(0, node_of))
300
+ den = torch.zeros(p["n"], device=dev, dtype=e.dtype).index_add(0, node_of, e)
301
+ w = e / den.index_select(0, node_of)
302
+ pooled = torch.zeros(p["n"], cfg.d, device=dev, dtype=toks.dtype).index_add(0, node_of, toks * w[:, None])
303
+ last = x_tok.index_select(0, p["last_tok"])
304
+ x = self.pool_out(torch.cat([pooled, last], -1))
305
+ x = x + self.qtype_emb(p["qtype"]) + self.kind_emb(p["kind"])
306
+ x = x + self.ord_mlp(p["ord"].to(x.dtype)) * p["is_score"][:, None].to(x.dtype)
307
+ for blk in self.route:
308
+ x = checkpoint(blk, x, state, self.kv_chunk, use_reentrant=False) if use_ckpt else blk(x, state, self.kv_chunk)
309
+ for blk in self.interact:
310
+ x = (checkpoint(blk, x, state, p["rel"], self.kv_chunk, use_reentrant=False) if use_ckpt
311
+ else blk(x, state, p["rel"], self.kv_chunk))
312
+ x = self.final_norm(x)
313
+ out = []
314
+ for i, qn in enumerate(p["q_node"]):
315
+ opts = torch.tensor(p["opt_nodes"][i], device=dev)
316
+ o = x.index_select(0, opts)
317
+ q = x[qn].expand_as(o)
318
+ z = self.scorer(torch.cat([o, q, o * q], -1)).squeeze(-1)
319
+ t = torch.exp(self.log_temp[QTYPE_ID[p["qtypes"][i]]])
320
+ out.append(z / t)
321
+ return out
322
+
323
+
324
+ def count_parameters(module):
325
+ return sum(p.numel() for p in module.parameters())
code/model.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Backbone (Ministral-3-8B, Mistral3ForConditionalGeneration) + LoRA + joint schema head (DESIGN.md 2.1, 3.1).
2
+
3
+ - The inner model (`.model`, Mistral3Model) is called; its `last_hidden_state` is the post-norm final state.
4
+ `lm_head` is never called and is deleted after load (131,072 x 4,096 bf16 = 1.07e9 B).
5
+ - Extra taps: outputs of decoder layers by index, captured with forward hooks.
6
+ - LoRA: language-model linear layers only (q/k/v/o/gate/up/down), vision tower + projector frozen, alpha = 2r.
7
+ Same leaf discovery and vision-exclusion rule as the Standard One LoRA trainer.
8
+ """
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+
13
+ import torch
14
+ import torch.nn as nn
15
+
16
+ from head import HeadConfig, JointSchemaHead
17
+ from render import token_roles
18
+
19
+ VISION_EXCLUDE_REGEX = r".*\.(vision_tower|multi_modal_projector)\..*"
20
+
21
+
22
+ def lora_leaves(model):
23
+ """Leaf names of the language-model attention + MLP linears (as the trainer's `attention_mlp` targets)."""
24
+ attention, mlp = set(), set()
25
+ for name, module in model.named_modules():
26
+ if not isinstance(module, nn.Linear) or ".language_model." not in name:
27
+ continue
28
+ leaf = name.rsplit(".", 1)[-1]
29
+ if ".self_attn." in name:
30
+ attention.add(leaf)
31
+ elif ".mlp." in name and leaf in {"gate_proj", "up_proj", "down_proj"}:
32
+ mlp.add(leaf)
33
+ if not attention:
34
+ raise ValueError("no attention projections found")
35
+ return sorted(attention | mlp)
36
+
37
+
38
+ def decoder_layers(model):
39
+ lm = model.model.language_model
40
+ return lm.layers
41
+
42
+
43
+ def tiny_config(vocab_size=131072, layers=3, hidden=64, heads=4, kv_heads=2, intermediate=128):
44
+ """A tiny, randomly initialised Mistral3 (Ministral3 text + Pixtral vision) config for CPU smoke tests."""
45
+ from transformers import Mistral3Config
46
+ text = {"model_type": "ministral3", "hidden_size": hidden, "intermediate_size": intermediate,
47
+ "num_hidden_layers": layers, "num_attention_heads": heads, "num_key_value_heads": kv_heads,
48
+ "head_dim": hidden // heads, "vocab_size": vocab_size, "max_position_embeddings": 262144,
49
+ "rms_norm_eps": 1e-5, "hidden_act": "silu", "tie_word_embeddings": False, "sliding_window": None,
50
+ "rope_parameters": {"beta_fast": 32.0, "beta_slow": 1.0, "factor": 16.0, "llama_4_scaling_beta": 0.1,
51
+ "mscale": 1.0, "mscale_all_dim": 1.0, "original_max_position_embeddings": 16384,
52
+ "rope_theta": 1000000.0, "rope_type": "yarn", "type": "yarn"}}
53
+ vision = {"model_type": "pixtral", "hidden_size": 32, "intermediate_size": 64, "num_hidden_layers": 1,
54
+ "num_attention_heads": 2, "head_dim": 16, "image_size": 1540, "patch_size": 14, "num_channels": 3,
55
+ "rope_parameters": {"rope_theta": 10000.0, "rope_type": "default"}, "rope_theta": 10000.0}
56
+ return Mistral3Config(text_config=text, vision_config=vision, image_token_index=10, spatial_merge_size=2,
57
+ multimodal_projector_bias=False, projector_hidden_act="gelu", vision_feature_layer=-1,
58
+ tie_word_embeddings=False)
59
+
60
+
61
+ class SchemaModel(nn.Module):
62
+ """Backbone (optionally LoRA-wrapped) + head. `forward_micro` runs one micro-batch of encoded requests."""
63
+
64
+ def __init__(self, backbone, head, taps, peft_model=None):
65
+ super().__init__()
66
+ self.backbone = backbone # Mistral3ForConditionalGeneration (LoRA layers injected in place)
67
+ self.peft_model = peft_model # the PeftModel wrapper (for save_pretrained) or None
68
+ self.head = head
69
+ self.taps = list(taps)
70
+ self._captured = {}
71
+ self._capturing = False # hooks record only during our own forward, not during checkpoint recompute
72
+ self._hooks = []
73
+ layers = decoder_layers(backbone)
74
+ for t in self.taps:
75
+ if t == "final":
76
+ continue
77
+ idx = int(t)
78
+ if not 0 <= idx < len(layers):
79
+ raise ValueError(f"tap layer {idx} out of range (0..{len(layers) - 1})")
80
+ self._hooks.append(layers[idx].register_forward_hook(self._make_hook(idx)))
81
+
82
+ def _make_hook(self, idx):
83
+ def hook(module, inputs, output):
84
+ if self._capturing:
85
+ self._captured[idx] = output[0] if isinstance(output, (tuple, list)) else output
86
+ return hook
87
+
88
+ def device(self):
89
+ return next(self.head.parameters()).device
90
+
91
+ def backbone_states(self, batch):
92
+ """Run the inner model on a padded batch; returns list of [B, T, H] tensors in tap order."""
93
+ self._captured = {}
94
+ dev = batch["input_ids"].device
95
+ kwargs = {k: v for k, v in batch.items() if k in ("input_ids", "attention_mask", "position_ids",
96
+ "pixel_values", "image_sizes")}
97
+ use_bf16 = dev.type == "cuda"
98
+ self._capturing = True
99
+ try:
100
+ with torch.autocast(device_type=dev.type, dtype=torch.bfloat16, enabled=use_bf16):
101
+ out = self.backbone.model(**kwargs, use_cache=False, return_dict=True)
102
+ finally:
103
+ self._capturing = False
104
+ states = []
105
+ for t in self.taps:
106
+ states.append(out.last_hidden_state if t == "final" else self._captured[int(t)])
107
+ self._captured = {}
108
+ return states
109
+
110
+ def forward_micro(self, encs, batch):
111
+ """encs: list of encoded requests (render.Encoder.encode outputs, right-padded in `batch`).
112
+ Returns list (per request) of lists (per question) of fp32 logit vectors (canonical option order)."""
113
+ states = self.backbone_states(batch)
114
+ outputs = []
115
+ dev = states[0].device
116
+ with torch.autocast(device_type=dev.type, enabled=False):
117
+ for i, enc in enumerate(encs):
118
+ n = enc["n_tokens"]
119
+ taps = [s[i, :n] for s in states] # bf16 on GPU; the head casts inside its checkpointed projection
120
+ roles = torch.tensor(enc.get("_roles") or token_roles(enc), device=dev)
121
+ outputs.append(self.head.forward_one(taps, enc, roles))
122
+ return outputs
123
+
124
+ def trainable_named(self):
125
+ return [(n, p) for n, p in self.named_parameters() if p.requires_grad]
126
+
127
+
128
+ def pad_pixel_values(chunks):
129
+ """Join per-row pixel tensors [n_i, C, H_i, W_i] into one [sum n_i, C, maxH, maxW] tensor, zero-padded at the
130
+ bottom/right. Every row went through the processor on its own, so H/W differ between rows; this is exactly what
131
+ PixtralImageProcessor._pad_for_batching does for a multi-image call, and the vision tower crops each image back
132
+ to its `image_sizes` entry (patch_embeds[..., :h // patch, :w // patch]) so the padding never reaches the model."""
133
+ h = max(int(c.shape[-2]) for c in chunks)
134
+ w = max(int(c.shape[-1]) for c in chunks)
135
+ return torch.cat([torch.nn.functional.pad(c, (0, w - int(c.shape[-1]), 0, h - int(c.shape[-2])))
136
+ for c in chunks], 0)
137
+
138
+
139
+ def make_batch(encs, pad_id, device):
140
+ """Right-padded text batch (position ids restart per row); image micro-batches carry pixel values."""
141
+ longest = max(e["n_tokens"] for e in encs)
142
+ ids = torch.full((len(encs), longest), pad_id, dtype=torch.long)
143
+ mask = torch.zeros((len(encs), longest), dtype=torch.long)
144
+ for i, e in enumerate(encs):
145
+ ids[i, :e["n_tokens"]] = torch.tensor(e["input_ids"])
146
+ mask[i, :e["n_tokens"]] = 1
147
+ pos = torch.arange(longest).unsqueeze(0).expand(len(encs), -1).contiguous()
148
+ batch = {"input_ids": ids.to(device), "attention_mask": mask.to(device), "position_ids": pos.to(device)}
149
+ pix = [e.get("_pixel") for e in encs]
150
+ if any(p is not None for p in pix):
151
+ if not all(p is not None for p in pix):
152
+ raise ValueError("image micro-batches must be homogeneous")
153
+ batch["pixel_values"] = pad_pixel_values([torch.as_tensor(p["pixel_values"]) for p in pix]).to(device)
154
+ batch["image_sizes"] = torch.cat([torch.as_tensor(p["image_sizes"]) for p in pix], 0).to(device)
155
+ if device.type == "cuda":
156
+ batch["pixel_values"] = batch["pixel_values"].to(torch.bfloat16)
157
+ return batch
158
+
159
+
160
+ def build_model(snapshot=None, tiny=None, lora_rank=64, lora_alpha=None, lora_dropout=0.0, taps=("final", 25),
161
+ head_overrides=None, device="cpu", grad_checkpointing=True, init_adapter=None, attn="sdpa",
162
+ seed=0):
163
+ """Load (or create tiny) backbone, delete lm_head, freeze, add LoRA (rank 0 = none), attach the head.
164
+
165
+ Returns (SchemaModel, info dict)."""
166
+ import peft
167
+ import transformers
168
+ torch.manual_seed(seed)
169
+ if tiny is not None:
170
+ torch.manual_seed(1234) # fixed: the tiny random "base" must be identical in training and serving
171
+ cfg = tiny if not isinstance(tiny, dict) else tiny_config(**tiny)
172
+ backbone = transformers.Mistral3ForConditionalGeneration._from_config(cfg, attn_implementation=attn)
173
+ backbone = backbone.to(device)
174
+ else:
175
+ backbone = transformers.Mistral3ForConditionalGeneration.from_pretrained(
176
+ str(snapshot), local_files_only=True, trust_remote_code=False, dtype=torch.bfloat16,
177
+ attn_implementation=attn, device_map={"": device} if str(device) != "cpu" else None)
178
+ if str(device) == "cpu":
179
+ backbone = backbone.to(device)
180
+ torch.manual_seed(seed)
181
+ backbone.config.use_cache = False
182
+ lm_head_bytes = backbone.lm_head.weight.numel() * backbone.lm_head.weight.element_size()
183
+ del backbone.lm_head
184
+ backbone.lm_head = nn.Identity() # never called; keeps attribute access working
185
+ for p in backbone.parameters():
186
+ p.requires_grad_(False)
187
+ if grad_checkpointing:
188
+ backbone.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
189
+ vt = getattr(backbone.model, "vision_tower", None)
190
+ if vt is not None and hasattr(vt, "gradient_checkpointing_disable"):
191
+ vt.gradient_checkpointing_disable()
192
+ leaves = lora_leaves(backbone)
193
+ peft_model = None
194
+ if init_adapter:
195
+ peft_model = peft.PeftModel.from_pretrained(backbone, str(init_adapter), is_trainable=True)
196
+ elif lora_rank:
197
+ alpha = lora_alpha if lora_alpha is not None else 2 * lora_rank
198
+ peft_model = peft.get_peft_model(backbone, peft.LoraConfig(
199
+ r=lora_rank, lora_alpha=alpha, lora_dropout=lora_dropout, target_modules=leaves,
200
+ exclude_modules=VISION_EXCLUDE_REGEX, bias="none"))
201
+ leaked = 0
202
+ for name, p in backbone.named_parameters():
203
+ if "vision_tower" in name or "multi_modal_projector" in name:
204
+ if p.requires_grad:
205
+ leaked += 1
206
+ p.requires_grad_(False)
207
+ elif p.requires_grad:
208
+ p.data = p.data.float() # LoRA weights fp32 (as the trainer)
209
+ text_cfg = backbone.config.text_config
210
+ hcfg = HeadConfig(hidden_size=text_cfg.hidden_size, n_taps=len(taps), taps=list(taps),
211
+ **(head_overrides or {}))
212
+ head = JointSchemaHead(hcfg).to(device=device, dtype=torch.float32)
213
+ model = SchemaModel(backbone, head, taps, peft_model)
214
+ info = {"lora_leaves": leaves, "lora_rank": lora_rank,
215
+ "lora_alpha": (lora_alpha if lora_alpha is not None else 2 * lora_rank) if lora_rank else None,
216
+ "lora_params": sum(p.numel() for n, p in backbone.named_parameters() if p.requires_grad),
217
+ "head_params": sum(p.numel() for p in head.parameters()), "lm_head_bytes_freed": lm_head_bytes,
218
+ "vision_lora_leaked_frozen": leaked, "taps": list(taps), "head_config": hcfg.to_dict(),
219
+ "layers": len(decoder_layers(backbone)), "hidden_size": text_cfg.hidden_size}
220
+ return model, info
221
+
222
+
223
+ def save_head(head, folder, extra=None):
224
+ from safetensors.torch import save_file
225
+ folder = Path(folder)
226
+ folder.mkdir(parents=True, exist_ok=True)
227
+ save_file({k: v.detach().contiguous().cpu() for k, v in head.state_dict().items()},
228
+ str(folder / "schema_head.safetensors"))
229
+ cfg = {"head": head.cfg.to_dict(), **(extra or {})}
230
+ tmp = folder / "schema_head_config.json.tmp"
231
+ tmp.write_text(json.dumps(cfg, indent=2) + "\n")
232
+ os.replace(tmp, folder / "schema_head_config.json")
233
+
234
+
235
+ def load_head_weights(head, folder):
236
+ from safetensors.torch import load_file
237
+ state = load_file(str(Path(folder) / "schema_head.safetensors"))
238
+ missing, unexpected = head.load_state_dict(state, strict=True), None
239
+ return missing
code/render.py ADDED
@@ -0,0 +1,424 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Template `schema-v1`: render a schema request into one user turn and locate every question/option span.
2
+
3
+ Design: request rendering for the joint schema head.
4
+
5
+ A *schema request* (the internal, normalised form used everywhere in this package):
6
+
7
+ {"state": str | dict | list,
8
+ "images": [data URL, ...], # optional
9
+ "questions": {key: {"type": "choice" | "noul" | "score",
10
+ "instructions": str | dict | list,
11
+ "options": [{"name": str, "description": str | dict | list | None}, ...]}}}
12
+
13
+ Options are always stored in CANONICAL order: choice = caller order, noul = [true, false], score = levels 0..K-1.
14
+ Rendering may show them in another order (augmentation); spans are always returned per canonical index.
15
+
16
+ Encoding (``Encoder.encode``) returns token ids plus token spans. Spans are half-open [start, end) token indices.
17
+ """
18
+ import hashlib
19
+ import json
20
+ import math
21
+ import random
22
+
23
+ TEMPLATE_ID = "schema-v1"
24
+ MAX_LENGTH = 32768
25
+ MAX_OPTIONS = 255
26
+ MAX_QUESTIONS = 256
27
+ MAX_SCORE_LEVELS = 10
28
+ QTYPES = ("choice", "noul", "score")
29
+ # Token roles used by the head's role embedding.
30
+ ROLE_OTHER, ROLE_STATE, ROLE_PREVIEW, ROLE_QUESTION, ROLE_OPTION = 0, 1, 2, 3, 4
31
+ N_ROLES = 5
32
+
33
+ # Wording variants. W0 is canonical (eval always uses W0); W1..W3 are augmentation only.
34
+ WORDINGS = {
35
+ "W0": {"preamble": "Read the state and answer every question. Each question lists its possible answers.",
36
+ "preview": "Questions to answer:", "state": "State:", "questions": "Questions:",
37
+ "choice": "(choose one)", "noul": "(true or false)", "score": "(score 0 to {top})", "bullet": "- "},
38
+ "W1": {"preamble": "Answer each question below using only the information provided. Every question lists "
39
+ "the answers it allows.",
40
+ "preview": "You will be asked:", "state": "Context:", "questions": "Answer these:",
41
+ "choice": "[pick one]", "noul": "[true/false]", "score": "[rate 0-{top}]", "bullet": "* "},
42
+ "W2": {"preamble": "Use the input to decide every question. Pick exactly one of the listed answers for each.",
43
+ "preview": "Questions:", "state": "Input:", "questions": "Decide:",
44
+ "choice": "(one of)", "noul": "(true/false)", "score": "(scale 0..{top})", "bullet": "- "},
45
+ "W3": {"preamble": "Below is some material followed by questions. Choose one listed answer per question.",
46
+ "preview": "Asked below:", "state": "Material:", "questions": "Questions and answers:",
47
+ "choice": "[choose one]", "noul": "[true or false]", "score": "[score from 0 to {top}]", "bullet": "* "},
48
+ }
49
+
50
+
51
+ class EncodeError(ValueError):
52
+ """A request that cannot be encoded; `reason` is a short machine-readable code (drop statistics)."""
53
+
54
+ def __init__(self, reason, message=""):
55
+ super().__init__(f"{reason}: {message}" if message else reason)
56
+ self.reason = reason
57
+
58
+
59
+ def require(condition, reason, message=""):
60
+ if not condition:
61
+ raise EncodeError(reason, message)
62
+
63
+
64
+ def render_value(value):
65
+ """Plain strings verbatim; structured values as sorted, indented JSON (= jev-adapter render_native_state_text)."""
66
+ if value is None:
67
+ return ""
68
+ if isinstance(value, str):
69
+ return value
70
+ return json.dumps(value, sort_keys=True, ensure_ascii=False, indent=2, allow_nan=False)
71
+
72
+
73
+ # ----------------------------------------------------------------------------------------------- validation
74
+ def validate_request(req):
75
+ """Structural checks of a schema request (raises EncodeError). Returns the list of question keys."""
76
+ require(isinstance(req, dict) and isinstance(req.get("questions"), dict), "bad_request", "questions missing")
77
+ keys = list(req["questions"])
78
+ require(1 <= len(keys) <= MAX_QUESTIONS, "bad_question_count", str(len(keys)))
79
+ for key in keys:
80
+ require(isinstance(key, str) and key.strip() and "\n" not in key and "[" not in key and "]" not in key,
81
+ "bad_question_key", repr(key))
82
+ q = req["questions"][key]
83
+ t = q.get("type")
84
+ require(t in QTYPES, "bad_type", repr(t))
85
+ opts = q.get("options")
86
+ require(isinstance(opts, list), "bad_options", key)
87
+ names = [o.get("name") for o in opts]
88
+ require(all(isinstance(n, str) and n.strip() and "\n" not in n for n in names), "bad_option_name", key)
89
+ require(len(set(names)) == len(names), "duplicate_option_name", key)
90
+ if t == "choice":
91
+ require(2 <= len(opts) <= MAX_OPTIONS, "bad_option_count", f"{key}: {len(opts)}")
92
+ elif t == "noul":
93
+ require(names == ["true", "false"], "bad_noul_options", f"{key}: {names}")
94
+ else:
95
+ require(2 <= len(opts) <= MAX_SCORE_LEVELS, "bad_score_levels", f"{key}: {len(opts)}")
96
+ require(names == [str(i) for i in range(len(opts))], "bad_score_names", key)
97
+ return keys
98
+
99
+
100
+ # ----------------------------------------------------------------------------------------------- rendering
101
+ def render(req, wording="W0", question_order=None, option_orders=None, preview=True):
102
+ """Render the user text. Returns (content, segments).
103
+
104
+ question_order: list of keys in display order (default: request order).
105
+ option_orders: {key: [canonical index shown at display position 0, 1, ...]} (default identity; score must be
106
+ identity because levels are ordinal).
107
+ segments: {"state": (c0, c1), "preview": {key: (c0, c1)}, "question": {key: (c0, c1)},
108
+ "option": {key: [(c0, c1) per CANONICAL option index]}} as character offsets into `content`.
109
+ The state segment includes its header line so it is never empty.
110
+ """
111
+ keys = validate_request(req)
112
+ order = list(question_order) if question_order is not None else keys
113
+ require(sorted(order) == sorted(keys), "bad_question_order")
114
+ w = WORDINGS[wording]
115
+ parts, pos = [], 0
116
+ segs = {"state": None, "preview": {}, "question": {}, "option": {}}
117
+
118
+ def emit(text):
119
+ nonlocal pos
120
+ start = pos
121
+ parts.append(text)
122
+ pos += len(text)
123
+ return start, pos
124
+
125
+ emit(w["preamble"] + "\n\n")
126
+ if preview:
127
+ emit(w["preview"] + "\n")
128
+ for key in order:
129
+ text = f"[{key}] {render_value(req['questions'][key]['instructions'])}"
130
+ segs["preview"][key] = emit(text)
131
+ emit("\n")
132
+ emit("\n")
133
+ s0, _ = emit(w["state"] + "\n")
134
+ _, s1 = emit(render_value(req.get("state", "")))
135
+ segs["state"] = (s0, s1)
136
+ emit("\n\n" + w["questions"])
137
+ for key in order:
138
+ q = req["questions"][key]
139
+ n = len(q["options"])
140
+ tag = w[q["type"]].format(top=n - 1)
141
+ emit("\n")
142
+ segs["question"][key] = emit(f"[{key}] {tag} {render_value(q['instructions'])}")
143
+ perm = list(range(n)) if not option_orders or key not in option_orders else list(option_orders[key])
144
+ require(sorted(perm) == list(range(n)), "bad_option_order", key)
145
+ require(q["type"] != "score" or perm == list(range(n)), "score_order_must_be_identity", key)
146
+ spans = [None] * n
147
+ for canonical in perm:
148
+ o = q["options"][canonical]
149
+ desc = render_value(o.get("description"))
150
+ text = o["name"] if not desc else f"{o['name']}: {desc}"
151
+ emit("\n" + w["bullet"])
152
+ spans[canonical] = emit(text)
153
+ segs["option"][key] = spans
154
+ return "".join(parts), segs
155
+
156
+
157
+ # ----------------------------------------------------------------------------------------------- augmentation
158
+ def _draw(row_id, epoch, what):
159
+ return int(hashlib.sha256(f"{row_id}|{epoch}|{what}|schema-aug-v1".encode()).hexdigest()[:16], 16)
160
+
161
+
162
+ def augmentation(req, row_id, epoch, p_choice=1.0, p_noul=0.5, p_field=1.0, p_wording=0.5):
163
+ """Deterministic per (row id, epoch) augmentation plan: wording, question order, option orders."""
164
+ keys = list(req["questions"])
165
+ frac = lambda what: _draw(row_id, epoch, what) / float(1 << 64) # noqa: E731
166
+ wording = "W0"
167
+ if frac("wording") < p_wording:
168
+ wording = ("W1", "W2", "W3")[_draw(row_id, epoch, "wording-pick") % 3]
169
+ order = list(keys)
170
+ if len(keys) > 1 and frac("fields") < p_field:
171
+ random.Random(_draw(row_id, epoch, "field-perm")).shuffle(order)
172
+ option_orders = {}
173
+ for key in keys:
174
+ q = req["questions"][key]
175
+ n = len(q["options"])
176
+ p = p_choice if q["type"] == "choice" else p_noul if q["type"] == "noul" else 0.0
177
+ if p > 0 and frac("opt-gate|" + key) < p:
178
+ perm = list(range(n))
179
+ random.Random(_draw(row_id, epoch, "opt-perm|" + key)).shuffle(perm)
180
+ option_orders[key] = perm
181
+ return {"wording": wording, "question_order": order, "option_orders": option_orders}
182
+
183
+
184
+ # ----------------------------------------------------------------------------------------------- encoding
185
+ class Encoder:
186
+ """Request -> token ids + token spans, with the trainer's chat-template call shape.
187
+
188
+ tokenizer: the canonical tokenizer (AutoTokenizer as the trainer loads it); used for apply_chat_template and
189
+ the round-trip check.
190
+ fast: a `tokenizers.Tokenizer` loaded from the same tokenizer.json (character offsets).
191
+ processor: AutoProcessor, only needed for requests with images.
192
+ """
193
+
194
+ def __init__(self, tokenizer, fast, processor=None, max_length=MAX_LENGTH, image_token_id=None,
195
+ image_end_id=None, check_roundtrip=True):
196
+ self.tokenizer, self.fast, self.processor = tokenizer, fast, processor
197
+ self.max_length = max_length
198
+ self.check_roundtrip = check_roundtrip
199
+ self.image_token_id = image_token_id
200
+ self.image_end_id = image_end_id
201
+ self.special_ids = set(getattr(tokenizer, "all_special_ids", []) or [])
202
+
203
+ @classmethod
204
+ def from_snapshot(cls, snapshot, with_processor=True, max_length=MAX_LENGTH, mistral_regex_fix=True):
205
+ import transformers
206
+ from tokenizers import Tokenizer
207
+ kwargs = {"fix_mistral_regex": True} if mistral_regex_fix else {}
208
+ tok = transformers.AutoTokenizer.from_pretrained(str(snapshot), local_files_only=True,
209
+ trust_remote_code=False, token=False, **kwargs)
210
+ fast = Tokenizer.from_file(str(snapshot) + "/tokenizer.json")
211
+ proc = img = img_end = None
212
+ if with_processor:
213
+ proc = transformers.AutoProcessor.from_pretrained(str(snapshot), local_files_only=True,
214
+ trust_remote_code=False, token=False, **kwargs)
215
+ img = tok.convert_tokens_to_ids(proc.image_token)
216
+ img_end = tok.convert_tokens_to_ids(proc.image_end_token)
217
+ enc = cls(tok, fast, proc, max_length, img, img_end)
218
+ enc.snapshot = str(snapshot)
219
+ return enc
220
+
221
+ # -- helpers
222
+ def _chat_text(self, content, n_images):
223
+ if n_images:
224
+ messages = [{"role": "user", "content": [{"type": "image"} for _ in range(n_images)]
225
+ + [{"type": "text", "text": content}]}]
226
+ # no enable_thinking here: the Ministral template has no such variable, and the processor's
227
+ # apply_chat_template treats unknown kwargs as processor kwargs (warning on every image encode). The text
228
+ # is identical either way (tests/test_image_batch.py::test_processor_kwargs_accepted).
229
+ return self.processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True), messages
230
+ messages = [{"role": "user", "content": content}]
231
+ return self.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True,
232
+ enable_thinking=False), messages
233
+
234
+ def encode(self, req, wording="W0", question_order=None, option_orders=None, preview=True, pil_images=None):
235
+ """Returns a dict (all plain Python ints/lists):
236
+ input_ids, n_tokens, keys (rendered question order), qtype, n_opt,
237
+ q_span[i] = (s, e) question line, p_span[i] = (s, e) preview line or None,
238
+ o_span[i] = [(s, e) per CANONICAL option], o_display[i] = canonical index per display position,
239
+ state_spans = [(s, e), ...] (state text incl. header, plus the image block), roles = run-length
240
+ [(role, start, end)], image info."""
241
+ content, segs = render(req, wording, question_order, option_orders, preview)
242
+ images = req.get("images") or []
243
+ chat, _messages = self._chat_text(content, len(images))
244
+ require(chat.count(content) == 1, "content_not_unique_in_chat")
245
+ c0 = chat.index(content)
246
+ enc = self.fast.encode(chat, add_special_tokens=False)
247
+ ids, offsets = list(enc.ids), list(enc.offsets)
248
+ if self.check_roundtrip and not images:
249
+ ref = self.tokenizer.apply_chat_template([{"role": "user", "content": content}], tokenize=True,
250
+ add_generation_prompt=True, enable_thinking=False,
251
+ return_dict=False)
252
+ require(list(ref) == ids, "roundtrip_mismatch")
253
+ image_span = None
254
+ pixel = None
255
+ if images:
256
+ require(self.processor is not None, "no_processor")
257
+ img, img_end = self.image_token_id, self.image_end_id
258
+ require(ids.count(img) == len(images), "image_placeholder_count")
259
+ if pil_images is None:
260
+ pil_images = decode_images(images)
261
+ # flat add_special_tokens: _merge_kwargs routes it to the tokenizer (no extra BOS; checked below and in
262
+ # tests/test_image_batch.py)
263
+ out = self.processor(text=chat, images=pil_images, return_tensors="pt", add_special_tokens=False)
264
+ expanded = out["input_ids"][0].tolist()
265
+ plain_ids, plain_off = ids, offsets
266
+ first_plain = plain_ids.index(img)
267
+ last_plain = len(plain_ids) - 1 - plain_ids[::-1].index(img)
268
+ require(plain_off[last_plain][1] <= c0, "images_not_before_text")
269
+ require(img in expanded and img_end in expanded, "no_image_tokens_after_processor")
270
+ first_exp = expanded.index(img)
271
+ last_exp = len(expanded) - 1 - expanded[::-1].index(img_end)
272
+ require(expanded[:first_exp] == plain_ids[:first_plain], "image_prefix_mismatch")
273
+ require(expanded[last_exp + 1:] == plain_ids[last_plain + 1:], "image_suffix_mismatch")
274
+ shift = last_exp - last_plain
275
+ image_span = (first_exp, last_exp + 1)
276
+ ids = expanded
277
+ offsets = [None] * len(expanded)
278
+ for i in range(first_plain):
279
+ offsets[i] = plain_off[i]
280
+ for i in range(last_plain + 1, len(plain_ids)):
281
+ offsets[i + shift] = plain_off[i]
282
+ pixel = {"pixel_values": out["pixel_values"], "image_sizes": out.get("image_sizes")}
283
+ n = len(ids)
284
+ require(1 <= n <= self.max_length, "too_long", str(n))
285
+
286
+ # char -> segment id map over the content region
287
+ seg_names = []
288
+ seg_ranges = []
289
+
290
+ def add(name, rng):
291
+ seg_names.append(name)
292
+ seg_ranges.append((rng[0] + c0, rng[1] + c0))
293
+ return len(seg_names) - 1
294
+
295
+ keys = list(question_order) if question_order is not None else list(req["questions"])
296
+ sid_state = add(("state",), segs["state"])
297
+ sid_prev, sid_q, sid_o = {}, {}, {}
298
+ for key in keys:
299
+ if key in segs["preview"]:
300
+ sid_prev[key] = add(("preview", key), segs["preview"][key])
301
+ sid_q[key] = add(("question", key), segs["question"][key])
302
+ sid_o[key] = [add(("option", key, j), r) for j, r in enumerate(segs["option"][key])]
303
+ # sort segment ranges for binary search
304
+ order = sorted(range(len(seg_ranges)), key=lambda i: seg_ranges[i][0])
305
+ starts = [seg_ranges[i][0] for i in order]
306
+ import bisect
307
+
308
+ def seg_of(ch):
309
+ k = bisect.bisect_right(starts, ch) - 1
310
+ if k < 0:
311
+ return -1
312
+ sid = order[k]
313
+ a, b = seg_ranges[sid]
314
+ return sid if a <= ch < b else -1
315
+
316
+ token_seg = [-1] * n
317
+ content_end = c0 + len(content)
318
+ for t, off in enumerate(offsets):
319
+ if off is None:
320
+ continue
321
+ a, b = off
322
+ if b <= c0 or a >= content_end:
323
+ continue
324
+ ch = a
325
+ while ch < b and chat[ch].isspace():
326
+ ch += 1
327
+ if ch >= b:
328
+ ch = a
329
+ token_seg[t] = seg_of(ch)
330
+ # special tokens must not appear inside the caller's content (e.g. a key spelled like a control token)
331
+ require(ids[t] not in self.special_ids, "special_token_in_content", str(ids[t]))
332
+
333
+ # spans per segment + coverage check (every non-space char of a segment lies in one of its tokens)
334
+ spans = {}
335
+ for t, sid in enumerate(token_seg):
336
+ if sid < 0:
337
+ continue
338
+ s = spans.get(sid)
339
+ spans[sid] = (t, t + 1) if s is None else (s[0], t + 1)
340
+ for sid, (a, b) in spans.items():
341
+ require(all(token_seg[t] == sid for t in range(a, b)), "span_not_contiguous", str(seg_names[sid]))
342
+ for sid, (ca, cb) in enumerate(seg_ranges):
343
+ require(sid in spans, "empty_span", str(seg_names[sid]))
344
+ a, b = spans[sid]
345
+ covered_lo = offsets[a][0]
346
+ covered_hi = offsets[b - 1][1]
347
+ first = ca
348
+ while first < cb and chat[first].isspace():
349
+ first += 1
350
+ last = cb
351
+ while last > first and chat[last - 1].isspace():
352
+ last -= 1
353
+ require(covered_lo <= first and covered_hi >= last, "span_straddle", str(seg_names[sid]))
354
+
355
+ state_spans = [spans[sid_state]]
356
+ if image_span is not None:
357
+ state_spans.insert(0, image_span)
358
+ out = {"template": TEMPLATE_ID, "wording": wording, "input_ids": ids, "n_tokens": n, "keys": keys,
359
+ "qtype": [req["questions"][k]["type"] for k in keys],
360
+ "n_opt": [len(req["questions"][k]["options"]) for k in keys],
361
+ "q_span": [spans[sid_q[k]] for k in keys],
362
+ "p_span": [spans[sid_prev[k]] if k in sid_prev else None for k in keys],
363
+ "o_span": [[spans[s] for s in sid_o[k]] for k in keys],
364
+ "o_display": [list(option_orders[k]) if option_orders and k in option_orders
365
+ else list(range(len(req["questions"][k]["options"]))) for k in keys],
366
+ "state_spans": state_spans, "n_images": len(images), "image_tokens": (image_span[1] - image_span[0]
367
+ if image_span else 0)}
368
+ if pixel is not None:
369
+ out["_pixel"] = pixel
370
+ return out
371
+
372
+
373
+ def decode_images(data_urls):
374
+ import base64
375
+ import io
376
+ from PIL import Image
377
+ out = []
378
+ for url in data_urls:
379
+ require(isinstance(url, str) and url.startswith("data:image/") and "," in url, "bad_image")
380
+ out.append(Image.open(io.BytesIO(base64.b64decode(url.split(",", 1)[1]))).convert("RGB"))
381
+ return out
382
+
383
+
384
+ def token_roles(encoded):
385
+ """Per-token role ids (list of length n_tokens)."""
386
+ roles = [ROLE_OTHER] * encoded["n_tokens"]
387
+ for a, b in encoded["state_spans"]:
388
+ for t in range(a, b):
389
+ roles[t] = ROLE_STATE
390
+ for i in range(len(encoded["keys"])):
391
+ if encoded["p_span"][i]:
392
+ a, b = encoded["p_span"][i]
393
+ for t in range(a, b):
394
+ roles[t] = ROLE_PREVIEW
395
+ a, b = encoded["q_span"][i]
396
+ for t in range(a, b):
397
+ roles[t] = ROLE_QUESTION
398
+ for a, b in encoded["o_span"][i]:
399
+ for t in range(a, b):
400
+ roles[t] = ROLE_OPTION
401
+ return roles
402
+
403
+
404
+ def request_from_systemone(body):
405
+ """/v1/systemone request JSON (jev-adapter protocol) -> schema request (canonical option order)."""
406
+ questions = {}
407
+ for key, q in body["questions"].items():
408
+ t = q["type"]
409
+ if t == "choice":
410
+ opts = [{"name": n, "description": d} for n, d in q["criteria"].items()]
411
+ elif t == "noul":
412
+ crit = q.get("criteria") or {}
413
+ opts = [{"name": "true", "description": crit.get("true")}, {"name": "false", "description": crit.get("false")}]
414
+ elif t == "score":
415
+ opts = [{"name": str(i), "description": d} for i, d in enumerate(q["criteria"])]
416
+ else:
417
+ raise EncodeError("bad_type", repr(t))
418
+ questions[key] = {"type": t, "instructions": q["instructions"], "options": opts}
419
+ return {"state": body.get("state", ""), "images": list(body.get("images") or []), "questions": questions}
420
+
421
+
422
+ def entropy_confidence(p):
423
+ h = -sum(x * math.log(x) for x in p if x > 0)
424
+ return min(1.0, max(0.0, 1.0 - h / math.log(len(p))))
code/requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ torch>=2.4
2
+ transformers==5.12.1
3
+ peft==0.21.0
4
+ safetensors
code/scoring.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """In-process /v1/systemone scoring with the joint schema head (shared by serve_head.py and eval_head.py).
2
+
3
+ Response JSON follows the jev-adapter contract (protocol.reduce_probabilities / service.score), so the unchanged
4
+ runners (for example `jev_adapter.benchmarks.run`) work as is:
5
+ answers[q] = {type, probabilities | noul, choice, confidence, score, legend, logprobs?}
6
+ usage = {input_tokens, output_tokens: 0}
7
+ metadata.evaluations = questions x permutations (the runners' per-question accounting; the head needs only
8
+ `prefills` = permutations forward passes for the whole request), plus prefill_ms / head_ms.
9
+ Option permutations (options.permutations = K) rotate choice and noul options (display position p shows original
10
+ (p + r) mod n, as the adapter's rotation_order); score levels always stay in ascending order.
11
+ """
12
+ import json
13
+ import math
14
+ import time
15
+ from pathlib import Path
16
+
17
+ import torch
18
+
19
+ from render import EncodeError, entropy_confidence, request_from_systemone, token_roles
20
+
21
+ CONFIDENCE_METHOD = "1 - normalized_entropy"
22
+
23
+
24
+ class RequestError(ValueError):
25
+ def __init__(self, code, message, field=None, status=422):
26
+ super().__init__(message)
27
+ self.code, self.field, self.status = code, field, status
28
+
29
+
30
+ def validate_body(body):
31
+ """Light structural validation (the adapter's pydantic schema is used when importable)."""
32
+ try:
33
+ from jev_adapter.protocol import SystemOneRequest # noqa: F401
34
+ SystemOneRequest.model_validate(body)
35
+ except ImportError:
36
+ pass
37
+ except Exception as e: # pydantic ValidationError
38
+ errors = getattr(e, "errors", lambda: [{"msg": str(e), "loc": ()}])()
39
+ first = errors[0] if errors else {"msg": str(e), "loc": ()}
40
+ raise RequestError("invalid_request", first.get("msg", str(e)),
41
+ ".".join(str(p) for p in first.get("loc", ()))) from None
42
+ if not isinstance(body, dict) or not isinstance(body.get("questions"), dict) or not body["questions"]:
43
+ raise RequestError("invalid_request", "questions are required", "questions")
44
+
45
+
46
+ class HeadScorer:
47
+ def __init__(self, model, encoder, served_model, pad_id, temperature_by_type=None, max_concurrency=1,
48
+ max_batch_tokens=32768, preview=True):
49
+ self.model, self.encoder, self.served_model, self.pad_id = model, encoder, served_model, pad_id
50
+ self.preview = bool(preview) # must match training (head config "preview"; --no-preview ablation)
51
+ self.temperature_by_type = dict(temperature_by_type or {})
52
+ self.max_concurrency = max_concurrency
53
+ self.max_batch_tokens = max_batch_tokens
54
+ self.device = model.device()
55
+
56
+ def plan(self, body):
57
+ validate_body(body)
58
+ req = request_from_systemone(body)
59
+ opts = body.get("options") or {}
60
+ k = int(opts.get("permutations", 1))
61
+ encs = []
62
+ for r in range(k):
63
+ orders = {}
64
+ for key, q in req["questions"].items():
65
+ n = len(q["options"])
66
+ if q["type"] != "score" and r:
67
+ orders[key] = [(p + r) % n for p in range(n)]
68
+ try:
69
+ encs.append(self.encoder.encode(req, "W0", list(req["questions"]), orders, preview=self.preview))
70
+ except EncodeError as e:
71
+ raise RequestError(e.reason, str(e), "questions" if e.reason != "too_long" else "state") from None
72
+ return req, opts, encs
73
+
74
+ @torch.no_grad()
75
+ def run(self, encs):
76
+ """Forward the encodings (one padded micro-batch); returns (per-encoding logits lists, prefill_s, head_s)."""
77
+ from model import make_batch
78
+ dev = self.device
79
+ sync = (lambda: torch.cuda.synchronize()) if dev.type == "cuda" else (lambda: None)
80
+ batch = make_batch(encs, self.pad_id, dev)
81
+ sync()
82
+ t0 = time.perf_counter()
83
+ states = self.model.backbone_states(batch)
84
+ sync()
85
+ t1 = time.perf_counter()
86
+ outs = []
87
+ with torch.autocast(device_type=dev.type, enabled=False):
88
+ for i, enc in enumerate(encs):
89
+ n = enc["n_tokens"]
90
+ taps = [s[i, :n].float() for s in states]
91
+ roles = torch.tensor(token_roles(enc), device=dev)
92
+ outs.append([z.float().cpu() for z in self.model.head.forward_one(taps, enc, roles)])
93
+ sync()
94
+ t2 = time.perf_counter()
95
+ return outs, t1 - t0, t2 - t1
96
+
97
+ def respond(self, req, opts, encs, outs, prefill_s, head_s, started):
98
+ temperature = float(opts.get("temperature", 1.0)) if opts.get("temperature_scaling", True) else 1.0
99
+ return_logprobs = bool(opts.get("return_logprobs", False))
100
+ answers = {}
101
+ keys = list(req["questions"])
102
+ for key in keys:
103
+ q = req["questions"][key]
104
+ names = [o["name"] for o in q["options"]]
105
+ t_type = self.temperature_by_type.get(q["type"], 1.0)
106
+ probs_sum = [0.0] * len(names)
107
+ raw = []
108
+ for enc, out in zip(encs, outs):
109
+ z = out[enc["keys"].index(key)].double()
110
+ logp = torch.log_softmax(z, -1)
111
+ raw.append({n: float(v) for n, v in zip(names, logp.tolist())})
112
+ p = torch.softmax(z / (t_type * temperature), -1).tolist()
113
+ for j, v in enumerate(p):
114
+ probs_sum[j] += v
115
+ total = math.fsum(probs_sum)
116
+ probs = [v / total for v in probs_sum]
117
+ ans = {"type": q["type"]}
118
+ if q["type"] == "noul":
119
+ ans["noul"] = probs[0]
120
+ else:
121
+ ans["probabilities"] = dict(zip(names, probs))
122
+ ans["confidence"] = entropy_confidence(probs)
123
+ if q["type"] == "choice":
124
+ ans["choice"] = names[max(range(len(names)), key=probs.__getitem__)]
125
+ else:
126
+ ans["score"] = math.fsum(i * p for i, p in enumerate(probs))
127
+ ans["legend"] = {o["name"]: o.get("description") for o in q["options"]}
128
+ if return_logprobs:
129
+ ans["logprobs"] = raw
130
+ answers[key] = ans
131
+ input_tokens = sum(e["n_tokens"] for e in encs)
132
+ usage = {"input_tokens": input_tokens, "output_tokens": 0}
133
+ if req["images"]:
134
+ usage["input_tokens_details"] = {"image_tokens": sum(e["image_tokens"] for e in encs)}
135
+ return {"model": self.served_model, "answers": answers, "usage": usage,
136
+ "metadata": {"confidence_method": CONFIDENCE_METHOD, "temperature": temperature,
137
+ **({"temperature_by_type": self.temperature_by_type} if self.temperature_by_type else {}),
138
+ "evaluations": len(keys) * len(encs), "prefills": len(encs),
139
+ "service_max_concurrency": self.max_concurrency, "label_scheme": "schema-v1",
140
+ "cached_tokens": 0, "prefill_ms": prefill_s * 1000, "head_ms": head_s * 1000,
141
+ "adapter_elapsed_ms": (time.perf_counter() - started) * 1000}}
142
+
143
+ def score(self, body):
144
+ started = time.perf_counter()
145
+ if body.get("model") not in (None, self.served_model, "jev-latest"):
146
+ raise RequestError("model_not_found", "Requested model is not configured.", "model", 404)
147
+ req, opts, encs = self.plan(body)
148
+ outs, ps, hs = self.run(encs)
149
+ return self.respond(req, opts, encs, outs, ps, hs, started)
150
+
151
+
152
+ def load_scorer(snapshot=None, head_dir=None, adapter=None, tiny=None, tokenizer=None, served_model=None,
153
+ temperature_file=None, device="auto", merge=True, max_concurrency=1, memory_cap_bytes=0):
154
+ """Backbone (+ LoRA adapter merged) + trained head -> HeadScorer.
155
+ memory_cap_bytes > 0 caps the torch allocator (set_per_process_memory_fraction) before the model is loaded."""
156
+ from model import build_model, load_head_weights
157
+ from render import Encoder
158
+ dev = torch.device(("cuda" if torch.cuda.is_available() else "cpu") if device == "auto" else device)
159
+ if dev.type == "cuda" and memory_cap_bytes:
160
+ total = torch.cuda.get_device_properties(0).total_memory
161
+ torch.cuda.set_per_process_memory_fraction(min(1.0, float(memory_cap_bytes) / total), 0)
162
+ cfg = json.loads((Path(head_dir) / "schema_head_config.json").read_text()) if head_dir else {}
163
+ hc = cfg.get("head", {})
164
+ taps = cfg.get("taps") or hc.get("taps") or ["final", 25]
165
+ overrides = {k: hc[k] for k in ("d", "heads", "ffn", "route_blocks", "interact_blocks", "ord_hidden", "max_nodes")
166
+ if k in hc}
167
+ overrides["dropout"] = 0.0
168
+ overrides["grad_checkpoint"] = False
169
+ model, info = build_model(snapshot, tiny=tiny, lora_rank=0, taps=taps, device=dev, grad_checkpointing=False,
170
+ init_adapter=adapter, head_overrides=overrides)
171
+ if adapter and merge and model.peft_model is not None:
172
+ model.backbone = model.peft_model.merge_and_unload()
173
+ model.peft_model = None
174
+ if head_dir:
175
+ load_head_weights(model.head, head_dir)
176
+ model.eval()
177
+ if dev.type == "cuda":
178
+ model.backbone.to(torch.bfloat16)
179
+ encoder = Encoder.from_snapshot(tokenizer or snapshot, with_processor=True)
180
+ pad = encoder.tokenizer.pad_token_id if encoder.tokenizer.pad_token_id is not None else encoder.tokenizer.eos_token_id
181
+ temps = json.loads(Path(temperature_file).read_text()).get("temperature_by_type") if temperature_file else None
182
+ return HeadScorer(model, encoder, served_model or "standardthinking/standard-schema-8b", pad, temps,
183
+ max_concurrency=max_concurrency, preview=cfg.get("preview", True)), info
code/serve_head.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """HTTP server implementing the jev-adapter /v1/systemone contract with the joint schema head (DESIGN.md 5.1).
3
+
4
+ python3 serve_head.py --snapshot <base or merged dir> --adapter <run>/final/adapter --head <run>/final/head \
5
+ --port 30171 [--temperature-file calib.json]
6
+
7
+ Endpoints: POST /v1/systemone, GET /v1/models, /model_info, /server_info (also /get_model_info, /get_server_info),
8
+ /health. The engine snapshot fields the benchmark runner checks are reported (disable_radix_cache=true,
9
+ mm_preprocess_cache_size_mb=0, speculative_algorithm=null): every request is a full prefill, nothing is cached.
10
+ Dynamic batching: requests waiting in the queue are forwarded together while the padded token count stays within
11
+ --max-batch-tokens (image requests run alone). Stdlib HTTP server; binds 127.0.0.1 by default.
12
+ """
13
+ import argparse
14
+ import json
15
+ import queue
16
+ import sys
17
+ import threading
18
+ import time
19
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
20
+ from pathlib import Path
21
+
22
+ HERE = Path(__file__).resolve().parent
23
+ sys.path.insert(0, str(HERE))
24
+
25
+ from scoring import RequestError, load_scorer # noqa: E402
26
+
27
+
28
+ class Batcher:
29
+ def __init__(self, scorer, max_batch_tokens, max_batch_requests, wait_ms):
30
+ self.scorer, self.max_tokens, self.max_requests, self.wait = scorer, max_batch_tokens, max_batch_requests, wait_ms / 1000
31
+ self.q = queue.Queue()
32
+ threading.Thread(target=self.loop, daemon=True).start()
33
+
34
+ def submit(self, body):
35
+ started = time.perf_counter()
36
+ req, opts, encs = self.scorer.plan(body) # validation + encoding in the caller's thread
37
+ job = {"req": req, "opts": opts, "encs": encs, "started": started, "done": threading.Event()}
38
+ self.q.put(job)
39
+ job["done"].wait()
40
+ if "error" in job:
41
+ raise job["error"]
42
+ return job["response"]
43
+
44
+ def loop(self):
45
+ while True:
46
+ jobs = [self.q.get()]
47
+ image = bool(jobs[0]["req"]["images"])
48
+ deadline = time.perf_counter() + self.wait
49
+ while not image and len(jobs) < self.max_requests:
50
+ try:
51
+ nxt = self.q.get(timeout=max(0.0, deadline - time.perf_counter()))
52
+ except queue.Empty:
53
+ break
54
+ encs = [e for j in jobs + [nxt] for e in j["encs"]]
55
+ cost = len(encs) * max(e["n_tokens"] for e in encs)
56
+ if nxt["req"]["images"] or cost > self.max_tokens:
57
+ self.q.put(nxt) # goes to the next batch
58
+ break
59
+ jobs.append(nxt)
60
+ try:
61
+ encs = [e for j in jobs for e in j["encs"]]
62
+ outs, ps, hs = self.scorer.run(encs)
63
+ k = 0
64
+ for j in jobs:
65
+ n = len(j["encs"])
66
+ j["response"] = self.scorer.respond(j["req"], j["opts"], j["encs"], outs[k:k + n], ps, hs, j["started"])
67
+ j["response"]["metadata"]["batch_requests"] = len(jobs)
68
+ k += n
69
+ except Exception as e: # noqa: BLE001
70
+ for j in jobs:
71
+ j["error"] = e
72
+ for j in jobs:
73
+ j["done"].set()
74
+
75
+
76
+ def make_handler(scorer, batcher, info):
77
+ model_info = {"model_path": info["model_path"], "served_model_name": scorer.served_model, "is_generation": False,
78
+ "has_image_understanding": True, "model_type": "schema-head", "architectures": ["SchemaHeadModel"]}
79
+ server_info = {"model_path": info["model_path"], "served_model_name": scorer.served_model,
80
+ "disable_radix_cache": True, "mm_preprocess_cache_size_mb": 0, "enable_prefix_mm_cache": False,
81
+ "enable_mm_global_cache": False, "speculative_algorithm": None, "context_length": 32768,
82
+ "dtype": info["dtype"], "attention_backend": "torch-sdpa", "version": "schema-head-serve-v1"}
83
+
84
+ class Handler(BaseHTTPRequestHandler):
85
+ protocol_version = "HTTP/1.1"
86
+
87
+ def log_message(self, fmt, *args):
88
+ pass
89
+
90
+ def _send(self, code, obj):
91
+ data = json.dumps(obj, ensure_ascii=False, allow_nan=False).encode()
92
+ self.send_response(code)
93
+ self.send_header("Content-Type", "application/json")
94
+ self.send_header("Content-Length", str(len(data)))
95
+ self.end_headers()
96
+ self.wfile.write(data)
97
+
98
+ def do_GET(self):
99
+ path = self.path.split("?")[0].rstrip("/")
100
+ if path == "/health":
101
+ return self._send(200, {"status": "ok"})
102
+ if path == "/v1/models":
103
+ return self._send(200, {"object": "list", "data": [{"id": scorer.served_model, "object": "model",
104
+ "owned_by": "schema-head", "label_scheme": "schema-v1"}]})
105
+ if path in ("/model_info", "/get_model_info"):
106
+ return self._send(200, model_info)
107
+ if path in ("/server_info", "/get_server_info"):
108
+ return self._send(200, server_info)
109
+ return self._send(404, {"error": {"code": "not_found", "message": path}})
110
+
111
+ def do_POST(self):
112
+ if self.path.split("?")[0].rstrip("/") != "/v1/systemone":
113
+ return self._send(404, {"error": {"code": "not_found", "message": self.path}})
114
+ try:
115
+ body = json.loads(self.rfile.read(int(self.headers.get("Content-Length", "0"))))
116
+ return self._send(200, batcher.submit(body))
117
+ except RequestError as e:
118
+ return self._send(e.status, {"error": {"code": e.code, "message": str(e), "field": e.field}})
119
+ except json.JSONDecodeError as e:
120
+ return self._send(400, {"error": {"code": "invalid_json", "message": str(e)}})
121
+ except Exception as e: # noqa: BLE001
122
+ return self._send(500, {"error": {"code": "scoring_failed", "message": f"{type(e).__name__}: {e}"}})
123
+ return Handler
124
+
125
+
126
+ def main():
127
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
128
+ ap.add_argument("--snapshot", type=Path)
129
+ ap.add_argument("--tokenizer", type=Path)
130
+ ap.add_argument("--tiny", help="JSON tiny config (tests only)")
131
+ ap.add_argument("--adapter", type=Path)
132
+ ap.add_argument("--head", type=Path, required=True)
133
+ ap.add_argument("--served-model-name", default="standardthinking/standard-schema-8b")
134
+ ap.add_argument("--temperature-file", type=Path)
135
+ ap.add_argument("--host", default="127.0.0.1")
136
+ ap.add_argument("--port", type=int, default=30171)
137
+ ap.add_argument("--device", default="auto")
138
+ ap.add_argument("--max-batch-tokens", type=int, default=32768)
139
+ ap.add_argument("--max-batch-requests", type=int, default=8)
140
+ ap.add_argument("--batch-wait-ms", type=float, default=5.0)
141
+ ap.add_argument("--memory-cap-bytes", type=float, default=0, help="torch allocator cap on GPU (0 = none)")
142
+ args = ap.parse_args()
143
+ tiny = json.loads(args.tiny) if args.tiny else None
144
+ scorer, info = load_scorer(args.snapshot, args.head, args.adapter, tiny, args.tokenizer, args.served_model_name,
145
+ args.temperature_file, args.device, max_concurrency=args.max_batch_requests,
146
+ memory_cap_bytes=args.memory_cap_bytes)
147
+ batcher = Batcher(scorer, args.max_batch_tokens, args.max_batch_requests, args.batch_wait_ms)
148
+ meta = {"model_path": str(args.snapshot or "tiny"), "dtype": "bfloat16" if scorer.device.type == "cuda" else "float32"}
149
+ server = ThreadingHTTPServer((args.host, args.port), make_handler(scorer, batcher, meta))
150
+ print(json.dumps({"serving": f"http://{args.host}:{args.port}", "model": args.served_model_name,
151
+ "head_params": info["head_params"], "device": str(scorer.device)}), flush=True)
152
+ server.serve_forever()
153
+
154
+
155
+ if __name__ == "__main__":
156
+ main()
head/schema_head.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3b1fda8d60cb8daa76bb54abfa380ab8b01dbccb26242b1cc829e6be57f364fb
3
+ size 282265276
head/schema_head_config.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "head": {
3
+ "hidden_size": 4096,
4
+ "n_taps": 2,
5
+ "d": 1024,
6
+ "heads": 16,
7
+ "ffn": 2816,
8
+ "route_blocks": 2,
9
+ "interact_blocks": 2,
10
+ "dropout": 0.1,
11
+ "n_roles": 5,
12
+ "ord_hidden": 256,
13
+ "max_nodes": 4096,
14
+ "grad_checkpoint": true,
15
+ "taps": [
16
+ "final",
17
+ 25
18
+ ],
19
+ "template": "schema-v1"
20
+ },
21
+ "taps": [
22
+ "final",
23
+ 25
24
+ ],
25
+ "template": "schema-v1",
26
+ "step": 982,
27
+ "lora_rank": 64,
28
+ "snapshot": "StandardThinking/StandardOne-8B",
29
+ "preview": true,
30
+ "snapshot_revision": "ac495d1c221297f0ae951941eff27bcf58b5ce52"
31
+ }