Mapika commited on
Commit
585473d
·
verified ·
1 Parent(s): 99d7fd5

decider-4b v3: text and image decisions (vision/), Noul T 0.05; v2.1 under tag v2.1

Browse files
README.md CHANGED
@@ -3,441 +3,152 @@ license: apache-2.0
3
  base_model: Qwen/Qwen3.5-4B-Base
4
  language: [en]
5
  pipeline_tag: text-classification
6
- tags: [decision-model, calibrated, structured-output, multi-task, system-one, one-pass]
7
  ---
8
 
9
- # decider-4b: typed decisions with calibrated probabilities in one forward pass, 4B dense
10
 
11
  A language model that does not generate text. It reads a state and one or more typed questions, each with an explicit option
12
- list, and returns a probability distribution over the options for every question from one forward pass. There is no decoding,
13
- no parsing and no output outside the options you defined. It is called from software, not chatted with. It is an open
14
- reproduction of the "System One" model class (TypeSafe AI's Jev).
15
 
16
- Base model: [Qwen/Qwen3.5-4B-Base](https://huggingface.co/Qwen/Qwen3.5-4B-Base): 4.2B parameters, 32 layers, 8 with full
17
- attention and 24 with gated delta-net linear attention, hidden size 2,560. Two supervised stages. Stage 1 (decider-4b v1): one
18
- pass of cross-entropy on the slot readout over mixture v2, the public decision mixture of
19
- [decider-2b](https://huggingface.co/Mapika/decider-2b) plus 26 further public decision datasets and ten programmatically
20
- generated families with verifiable gold (742M tokens). Stage 2 (v2.1): a LoRA of rank 64 on the attention and MLP weights,
21
- trained for 2 epochs on 29,325 rows of harder decisions and replay, with the replay rows trained toward v1's own answer
22
- distribution, then merged into the weights. There is no reinforcement-learning stage. **This repository holds v2.1**, the bf16
23
- weights (8.4 GB), with one temperature per answer type. v2 stays available under the Hub tag `v2` and v1 under the tag `v1`
24
- (see [Changes from v2](#changes-from-v2) for who should keep using them). The other sizes are listed under The decider family.
25
- `decider/` in this repository is the inference subset of the GitHub package.
26
 
27
- v2.1 keeps most of v2's gain on hard decisions and gets back most of what v2 lost in sampled play. Against v2 on the same rows:
28
- bag-draw games in sampled play 52.0% against 37.9% wins (v1 56.6%), zero-shot games sampled 26.9% against 22.4% (v1 27.8%), live
29
- browser tasks sampled 93.2% against 88.1% (v1 90.9%), CliffWalking −13 against −60, the regression set 0.831 / 0.784 against 0.824
30
- / 0.779 (v1 0.834 / 0.788); held-out generated decision families 0.556 against 0.560 (v1 0.469) and the JevBench public hard tier
31
- 0.649 against 0.676 (v1 0.550). It is less well calibrated than v2 on hard items (details under
32
- [Calibration](#calibration)), still answers one of the two form-filling cases of issue #9 wrongly, and is worse than v1 on
33
- BabyAI-GoTo and on greedy bag-draw play. Against decider-35b-a3b it is 2.4 and 2.6 points lower on the regression set.
34
 
35
- **Contents:** [Changes from v2](#changes-from-v2) · [The decider family](#the-decider-family) · [Usage](#usage) · [How it works](#how-it-works) · [Training](#training) · [Evaluation](#evaluation) · [Calibration](#calibration) · [Speed](#speed) · [Limitations](#limitations) · [Changelog](#changelog) · [Reproduction](#reproduction)
36
 
37
- ## Changes from v2
38
-
39
- v2.1 differs from v2 in two things:
40
-
41
- * **Replay rows are trained toward v1's own distribution.** Stage 2 of v2 trained every row, including 6,700 replay rows from v1's
42
- training mixture, on its hard label. v2.1 trains the replay rows toward v1's answer distribution instead, with the loss
43
- KL(p_v1 ‖ p_model) over the options of each answer, and keeps hard labels on every other row. Everything else is v2's recipe:
44
- the same rows (29,325 after removing 31 rows that shared a context with an evaluation file; v2 had 29,356), LoRA rank 64, learning
45
- rate 1e-4, 2 epochs. Training the replay rows on hard labels had sharpened the logits everywhere; the fitted temperature then
46
- rose to 1.935 and flattened every served answer, which is what cost v2 its sampled play. With the replay rows trained toward
47
- v1, the fitted temperature is 1.099 (v1 1.05). On 1,000 held-out replay rows the mean KL(p_v1 ‖ p_model) at temperature 1 is 0.021
48
- nats (v2: 0.112), and 96.3% of the argmax answers equal v1's (v2: 94.1%).
49
- * **One temperature per answer type.** `decider_config.json` has `temperature` 1.099 and `temperature_by_type`
50
- `{"choice": 1.110, "noul": 1.560, "score": 1.287}`, fitted by NLL with `decider.calibrate` (decider-ai 1.4.0). decider-ai 1.4.0
51
- and later use the map. **decider-ai 1.3.0 and earlier ignore the map and serve every answer at 1.099**; a temperature does not
52
- change which option is most probable, so the answers are the same either way (except exact ties between the level rows of an
53
- isolated Score answer, where rounding can break the tie differently: 1 of 5,000 held-out generated-family rows and 2 of 447
54
- document-question validation rows changed); the
55
- probabilities differ, mostly on yes/no and Score answers. The map changes little in practice: sampled
56
- play, the fixtures and the probes are the same within noise with and without it, and it lowers the calibration error on
57
- held-out yes/no answers (0.146 to 0.101) and Score answers (0.140 to 0.108). It does not fix the overconfidence on hard
58
- Choice answers (see Calibration).
59
-
60
- All rows below are on identical inputs and seeds: v2.1 through decider-ai 1.4.0 with its map, v1 and v2 through decider-ai 1.3.0
61
- at their stored temperatures (1.05 and 1.935), measured in the same session on 2026-09-24. Intervals are 95% paired bootstrap
62
- intervals (rows for the fixtures, boards for the games, task-seed pairs for the browser). The regression set and the two held-out
63
- sets were read from stored temperature-1 logits at each model's served temperatures. The JevBench files of v1 and v2 were read
64
- earlier through decider-ai 1.2.1, v2's at the candidate temperature 1.719; accuracy does not depend on the temperature.
65
-
66
- | set | v1 (T 1.05) | v2 (T 1.935) | v2.1 (map) | v2.1 minus v1 | v2.1 minus v2 |
67
- |---|---|---|---|---|---|
68
- | regression set, 67 in-task tasks, accuracy / NLL / ECE | 0.834 / 0.404 / 0.027 | 0.824 / 0.441 / 0.041 | 0.831 / 0.414 / 0.031 | −0.3 | +0.7 |
69
- | regression set, 28 held-out tasks | 0.788 / 0.558 / 0.071 | 0.779 / 0.566 / 0.080 | 0.784 / 0.569 / 0.077 | −0.4 | +0.5 |
70
- | 847 in-task validation rows, accuracy / NLL | 86.1% / 0.417 | 85.0% / 0.419 | 85.0% / 0.432 | −1.1 (−2.1 to +0.0); NLL +0.015 (+0.003 to +0.028) | 0.0 (−1.5 to +1.5); NLL +0.014 (−0.006 to +0.035) |
71
- | OpenJev, 5,252 rows, accuracy / NLL | 63.9% / 0.893 | 66.7% / 0.789 | 66.0% / 0.846 | +2.1 (+1.2 to +2.9); NLL −0.047 (−0.060 to −0.035) | −0.7 (−1.6 to +0.2); NLL +0.057 (+0.044 to +0.070) |
72
- | Mind2Web, 1,770 rows, accuracy / NLL | 88.4% / 0.366 | 87.4% / 0.393 | 87.5% / 0.375 | −0.8 (−1.7 to +0.0); NLL +0.009 (−0.004 to +0.023) | +0.1 (−0.8 to +1.1); NLL −0.018 (−0.032 to −0.005) |
73
- | TypeSafe workflow decisions, 102 rows, accuracy / NLL | 81.4% / 0.611 | 86.3% / 0.410 | 84.3% / 0.441 | +2.9 (−2.9 to +8.8); NLL −0.170 (−0.324 to −0.036) | −2.0 (−7.8 to +3.9); NLL +0.031 (−0.069 to +0.134) |
74
- | held-out generated families (heldout_jb), 5,000 rows, accuracy / ECE | 0.469 / 0.260 | 0.560 / 0.046 | 0.556 / 0.147 | +8.7 | −0.4 |
75
- | held-out document questions (test_teacher2), 449 rows, accuracy / ECE | 0.726 / 0.110 | 0.826 / 0.053 | 0.820 / 0.043 | +9.4 | −0.7 |
76
- | JevBench public items, easy / standard / hard accuracy | 1.000 / 0.958 / 0.550 | 1.000 / 0.986 / 0.676 | 1.000 / 0.986 / 0.649 | hard +11 items | hard −3 items |
77
- | JevBench hard tier, top-label ECE (v1, v2: files read through 1.2.1, v2 at T 1.719) | 0.288 | 0.104 | 0.184 | | |
78
- | Bespoke's public suite, macro / micro | 0.757 / 0.765 | 0.773 / 0.781 | 0.756 / 0.765 | −0.1 macro | −1.6 macro |
79
- | live MiniWoB++, sampled, all 22 tasks | 90.9% | 88.1% | 93.2% | +2.3 (−1.7 to +6.2) | +5.1 (+0.6 to +9.7) |
80
- | live MiniWoB++, sampled, 16 rewarded tasks | 96.1% | 93.0% | 95.3% | −0.8 (−3.9 to +2.3) | +2.3 (−2.3 to +7.0) |
81
- | live MiniWoB++, sampled, 6 held-out tasks | 77.1% | 75.0% | 87.5% | +10.4 (0.0 to +20.8) | +12.5 (0.0 to +25.0) |
82
- | live MiniWoB++, greedy, all 22 tasks | 91.5% | 92.6% | 93.8% | +2.3 (−0.6 to +5.7) | +1.1 (−1.7 to +4.5) |
83
- | live MiniWoB++, greedy, 16 rewarded tasks | 97.7% | 96.9% | 96.1% | −1.6 (−3.9 to +0.0) | −0.8 (−2.3 to +0.0) |
84
- | live MiniWoB++, greedy, 6 held-out tasks | 75.0% | 81.2% | 87.5% | +12.5 (+4.2 to +22.9) | +6.2 (−4.2 to +16.7) |
85
- | zero-shot games, 234 boards, sampled, win rate | 27.8% | 22.4% | 26.9% | −0.9 (−2.6 to +1.0) | +4.5 (+2.5 to +6.6) |
86
- | bag-draw games, 64 boards, sampled, win rate | 56.6% | 37.9% | 52.0% | −4.7 (−9.0 to −0.4) | +14.1 (+8.2 to +19.9) |
87
- | slippery-grid games, 64 boards, sampled, win rate | 16.4% | 16.0% | 16.8% | +0.4 (−2.7 to +3.5) | +0.8 (−2.3 to +3.9) |
88
- | zero-shot games, 234 boards, greedy, win rate | 29.1% | 27.8% | 25.2% | −3.8 (−7.3 to −0.4) | −2.6 (−6.4 to +1.3) |
89
- | bag-draw games, 64 boards, greedy, win rate | 62.5% | 48.4% | 53.1% | −9.4 (−17.2 to −3.1) | +4.7 (−3.1 to +12.5) |
90
- | slippery-grid games, 64 boards, greedy, win rate | 12.5% | 18.8% | 10.9% | −1.6 (−7.9 to +4.7) | −7.8 (−15.6 to +0.0) |
91
- | ten text games, greedy: Pong / Breakout / CliffWalking / BabyAI-GoTo / Freeway / Blackjack | −21 / 14 / −13 / 0.54 / 0 / −0.6 | −21 / 12 / −60 / 0.35 / 1 / −0.6 | −5 / 35 / −13 / 0.19 / 0 / −0.6 | | |
92
- | behaviour probes: model-router tier / needs-live-data (31 items) | 0.968 / 0.871 | 0.935 / 0.839 | 0.968 / 0.839 | 0 / −1 item | +1 / 0 items |
93
- | behaviour probes: command risk / touches-outside-project (45 items) | 0.889 / 0.956 | 0.911 / 0.933 | 0.889 / 0.933 | 0 / −1 item | −1 / 0 items |
94
- | behaviour probes: generic bucket / catch-all / abstention battery / browser element and action | 1.00 / 0.90 / 7 of 8 / 0.875 and 0.875 | 0.95 / 0.95 / 8 of 8 / 0.938 and 0.938 | 1.00 / 0.95 / 8 of 8 / 0.938 and 0.750 | | |
95
- | issue #9 form cases c_1 / c_2 (probability of the gold option) | right (0.79) / right (0.98) | wrong (0.16) / right (0.28) | wrong (0.20) / right (0.41) | | |
96
-
97
- **Regressions, stated plainly.**
98
- * BabyAI-GoTo (greedy text game): 0.19 against v1's 0.54 and v2's 0.35.
99
- * Greedy bag-draw play: 53.1% against v1's 62.5% wins (−9.4 points, interval −17.2 to −3.1); greedy zero-shot games overall 25.2%
100
- against 29.1% (−3.8, interval −7.3 to −0.4). Sampled bag-draw play is 4.7 points under v1 (interval −9.0 to −0.4).
101
- * Behaviour probes: needs-live-data 0.839 against v1's 0.871 and touches-outside-project 0.933 against 0.956, one item each (as
102
- v2); the browser-agent action choice is 12 of 16 items (0.750), 2 fewer than v1 (0.875) and 3 fewer than v2
103
- (0.938).
104
- * Form filling: issue #9 case c_1 is still answered wrongly. The field is "Degree earned" and the document entity is "Studied:
105
- Associate of Arts"; v2.1 chooses "skip" at 0.76 and gives the gold entity 0.20 (v1: gold 0.79; v2: gold 0.16). Case c_2 is
106
- answered correctly at 0.41. For form filling we suggest v1.
107
- * Calibration on hard items: on the held-out generated families the calibration error is 0.147 against v2's 0.046 (see
108
- Calibration), and on the JevBench public hard tier 0.184 against v2's 0.104.
109
- * JevBench public hard tier: 72 of 111 items (0.649) against v2's 75 (0.676).
110
- * Against v2: TypeSafe −2.0 points and OpenJev −0.7 (intervals include zero), NLL higher on both; Bespoke's suite 0.756 against
111
- 0.773 macro (v1 0.757); greedy slippery-grid play 10.9% against 18.8%.
112
-
113
- **How v2.1 was chosen, and why it is released although it did not pass.** The run had pre-registered release rules. v2.1 passed
114
- the numeric items (held-out document questions 0.820 against the required 0.816, held-out generated families 0.556 against 0.550,
115
- regression accuracy and calibration not worse than v2's, sampled bag-draw above the midpoint of v1 and v2, sampled zero-shot
116
- games and browser play not shown to be below v1's (upper end of the 95% interval at least 0), each probe at most one item below v1) and failed the last item, which required both
117
- issue #9 form cases to be right: c_1 is wrong. By the rule it was not recommended. A second pre-registered rule for the
118
- temperature map required the calibration error on the held-out generated families to be at most 0.08; the map reaches 0.147
119
- (0.163 without it), so the map did not pass either. v2.1 is released with the map on a decision made after reading the full comparison above: it is better than v2 on sampled play, CliffWalking, the model-router probe and the regression set, at v2's
120
- level on our held-out hard sets, and its failures are listed in this section. The JevBench public items were read once for the
121
- candidate at its global temperature and once with the map, after the rule decisions; they were not used for training,
122
- selection or the temperatures.
123
-
124
- **Which version to use.**
125
- * v2.1 (this revision): the default. Sampled play (games, browser agents that sample actions), hard decisions.
126
- * v2 (`revision="v2"`): if you rely on confidence values on hard multi-step items, where v2 is better calibrated (held-out
127
- generated families 0.046, JevBench hard tier 0.104), or on its slightly higher JevBench hard tier and TypeSafe accuracy.
128
- * v1 (`revision="v1"`): form filling (issue #9), BabyAI-GoTo-like grid navigation, greedy bag-draw play.
129
-
130
- The package loads a local folder, so download the revision first:
131
-
132
- ```python
133
- from huggingface_hub import snapshot_download
134
- from decider.infer import Decider
135
- d = Decider(snapshot_download("Mapika/decider-4b", revision="v2")) # or revision="v1"
136
- ```
137
-
138
- For the HTTP server, set `DECIDER_MODEL` to the same downloaded folder.
139
 
140
  ## The decider family
141
 
142
- All six repositories share one interface (`decider.infer.Decider`, `POST /v1/systemone` in TypeSafe's format) and one
143
- readout: the letter logits at an answer slot, softmaxed over the options. Pick by size and input.
144
-
145
- | model | base | weights | use it for | numbers |
146
- |---|---|---|---|---|
147
- | [decider-2b](https://huggingface.co/Mapika/decider-2b) v11 | Qwen3.5-2B-Base | 3.8 GB bf16 | the default: routing, classification, judgments, browser agents; 4 ms per request with CUDA graphs on one GPU; v10 under the tag `v10` | regression set 0.802 in-task / 0.752 held-out; JevBench hard 0.577; live browser 90% sampled; Bespoke suite 0.706 |
148
- | [decider-4b](https://huggingface.co/Mapika/decider-4b) v2.1 | Qwen3.5-4B-Base | 8.4 GB bf16 | the middle point: knowledge questions and hard judgments above the 2B in a dense 8.4 GB model; no RL stage; v2 and v1 under the tags `v2` and `v1` | 0.831 / 0.784; JevBench hard 0.649; live browser 93% sampled; Bespoke 0.756 |
149
- | [decider-35b-a3b](https://huggingface.co/Mapika/decider-35b-a3b) v1 | Qwen3.5-35B-A3B-Base (3B active) | 65 GB bf16 | when accuracy is worth 3 to 4 times the cost per decision: knowledge and multi-step questions, long policies | 0.855 / 0.810, above the 2B on 93 of 95 tasks; JevBench hard 0.676; Bespoke 0.774; no RL stage |
150
- | [decider-35b-a3b-nvfp4](https://huggingface.co/Mapika/decider-35b-a3b-nvfp4) | the 35B in NVFP4 | 19.6 GB | the 35B on Blackwell through vLLM or TensorRT-LLM | 1.0 to 1.5 points under bf16 on the measured fixtures |
151
- | [decider-0.8b](https://huggingface.co/Mapika/decider-0.8b) | Qwen3.5-0.8B-Base | 1.4 GB bf16 | the smallest: routing, yes/no and short-state lookups within 1 to 4 points of the 2B, 1.5x faster | 0.776 / 0.707 on the single-run protocol (2B: 0.809 / 0.739) |
152
- | [decider-2b-vision](https://huggingface.co/Mapika/decider-2b-vision) | Qwen3.5-2B vision-language, v5 text weights | 4.1 GB bf16 | decisions from an image plus a question; game frames | Visual7W 0.89; Breakout 41 from pixels |
153
 
154
- Code, data registry, training scripts, the changelog and the per-version history: https://github.com/Mapika/decider.
 
155
 
156
  ## Usage
157
 
 
 
158
  ```python
159
- from decider.infer import Decider # decider/ is included in this repo
160
  d = Decider("Mapika/decider-4b")
161
  d.decide("My card was charged twice for the same purchase.",
162
  [{"question": "Which department should handle this?", "options": ["billing", "technical support", "sales"]},
163
  {"question": "Does this need a refund action?", "options": ["no", "yes"]}])
164
- # [{'choice': 'billing', 'confidence': ..., 'probs': {...}}, {'choice': 'yes', 'confidence': ..., 'probs': {...}}]
165
  ```
166
 
167
- The API is the same as decider-2b's: `decide_batch` scores many states with many questions in one call, `abstain_below=t`
168
- returns `None` under a confidence threshold, a question can have 2 to 255 options, and `system_one` / `decider.serve` accept
169
- TypeSafe's `POST /v1/systemone` request shape. Every question and every Score level is scored in its own row. The state may be a
170
- string, object or array of up to 32k tokens. See the decider-2b card for the full description of the request shape, field types
171
- and the schema cache.
172
-
173
- Requirements: `torch`, `transformers>=5`, and `flash-linear-attention` (Triton kernels for the Qwen3.5 linear-attention layers;
174
- the model runs without it but several times slower). The weights take 8.4 GB in bf16. v2.1 uses the plain prompt layout, as v1 and
175
- v2 do. The per-type temperatures need decider-ai 1.4.0 or later (or the `decider/` subset in this repository, taken from 1.4.0).
176
- Checked on CUDA, eager path: decider-ai 1.3.0 loads v2.1 with the name `decider-4b-v2.1` and temperature 1.099 and gives exactly
177
- the probabilities that 1.4.0 gives with the map switched off; 1.4.0 and the `decider/` subset in this repository load the map
178
- and give the same probabilities as each other. For Choice and yes/no answers the 1.4.0 output with the map equals
179
- softmax(log(p) · 1.099 / T_type) of the 1.3.0 output p (to 1e-7 on `decide`, to 8e-5 on the four-decimal `system_one` values);
180
- an isolated-level Score answer is rescaled per level row (each level's yes/no pair at 1.287 instead of 1.099) before the
181
- levels are normalised, so it cannot be recomputed from the combined Score probabilities. The 1.4.0 HTTP server with CUDA graphs was checked with `/v1/systemone` and `/decide` requests. Other package
182
- versions, MPS and the FP8 path were not checked on v2.1. On Blackwell GPUs use 1.0.2 or later (1.0.0 and 1.0.1 have the cuDNN
183
- attention fault fixed in 1.0.2). The model is dense, so the CUDA-graph engine, `torch.compile` and the FP8 path of the helper
184
- package apply to it as to decider-2b (`use_graphs=False` selects eager PyTorch).
185
 
186
- Without the helper package, the same computation in plain `transformers`:
 
 
 
187
 
188
  ```python
189
- import torch
190
- from transformers import AutoTokenizer, AutoModelForCausalLM
191
- tok = AutoTokenizer.from_pretrained(REPO); m = AutoModelForCausalLM.from_pretrained(REPO, dtype=torch.bfloat16).cuda().eval()
192
- prompt = ("Context:\nMy card was charged twice for the same purchase.\n\n"
193
- "Question: Which department should handle this?\nOptions:\n(A) billing\n(B) technical support\n(C) sales\nAnswer: (")
194
- ids = tok(prompt, return_tensors="pt").to("cuda")
195
- with torch.no_grad():
196
- logits = m(**ids).logits[0, -1]
197
- letters = [tok.encode(L, add_special_tokens=False)[0] for L in "ABC"]
198
- probs = torch.softmax(logits[letters].float() / 1.110, -1) # 1.110 is the stored choice temperature (yes/no 1.560, Score 1.287)
199
  ```
200
 
201
- ## How it works
202
-
203
- The prompt is `Context: ...` followed by, for each question, the question text, the lettered options `(A) ... (B) ...` and an
204
- answer slot `Answer k: (`. The hidden state at each slot is projected with the option-letter rows of the LM head and softmaxed
205
- over the valid letters, divided by the temperature in `decider_config.json`. Letters are never generated, so all slots are read
206
- from one pass. From decider-ai 1.4.0 the config may also hold `temperature_by_type`, one temperature per answer type
207
- (`choice`, `noul`, `score`; a missing type uses `temperature`). This release's config has such a map: Choice answers use 1.110,
208
- yes/no (noul) answers 1.560, Score answers 1.287 on each of their level rows. Package versions before 1.4.0 use `temperature`
209
- (1.099) for every answer. `DECIDER_TEMPERATURE=T` or `Decider(path, temperature=T)` replaces `temperature` with T and switches the
210
- map off, so every answer then uses T. Large label sets were sub-sampled to at most 10 options per
211
- training example (gold always kept, order shuffled), so the model conditions on the supplied candidates rather than on a fixed
212
- head.
213
 
214
  ## Training
215
 
216
- **Stage 1 (decider-4b v1).** One supervised pass over **mixture v2**, in the two prompt layouts (state-first and schema-first,
217
- 50/50), with isolated Score levels and 10% abstention rows, 1,892,408 items and 742M tokens, pre-tokenized once and read in the
218
- same order by both ranks. Three sources:
219
-
220
- | source | rows | share of tokens | content |
221
- |---|---|---|---|
222
- | the public decision mixture of decider-2b (`scripts/train.sh full` of the GitHub repository) | 1,539,860 | 60% | about 95 public decision datasets, agent trajectories, Mind2Web element choice, teacher-written custom questions, Jev's input shapes |
223
- | 26 further public datasets with gold labels | 131,318 | 8% | code defect, clone and review-needed judgments; log anomaly and severity (HDFS, BGL); legal (LEDGAR, Unfair-ToS, CaseHOLD, SCOTUS, ECtHR); tables (TabFact, WikiTQ, FeTaQA, InfoTabs, TAT-QA); finance headlines; German credit; symptom diagnosis and medical specialty; SciTail, SciEntsBank, Climate-FEVER; XNLI, PAWS-X, MASSIVE (multilingual), Belebele, XCOPA; MC-TACO, TRACIE, TimeQA; ProofWriter, RuleTaker, LogiQA 2; essay scoring; agent next action |
224
- | ten programmatic families with verifiable gold, each with a held-out variant | 221,230 | 32% | code, dates and times, logs, long documents, plans, policies, probability, schedules, tables, tools |
225
-
226
- Ten of the 26 public datasets (Belebele, code clone, ECtHR, essay scoring, InfoTabs, PAWS-X, RuleTaker, SCOTUS, XCOPA, TRACIE)
227
- and the held-out variant of every programmatic family were kept out of training; every evaluation row was checked against every
228
- training row of all three sources and 4,168 overlapping training pairs were dropped. No game rows are in mixture v2 (the
229
- public mixture rebuilt on this machine has none), so every game result below is zero-shot.
230
-
231
- | stage 1 | |
232
- |---|---|
233
- | trainable parameters | all 4.2B (426 tensors) |
234
- | optimizer | `torch.optim.AdamW` applied directly to the bf16 parameters, no FP32 master copy; betas 0.9 / 0.95, no weight decay |
235
- | schedule | peak learning rate 1e-5, 150 warm-up steps, cosine to zero, 26,729 steps of 32,768 tokens, gradient clip 1.0 |
236
- | hardware | 2 NVIDIA B300, data parallel, 16,384-token micro-batches per GPU, gradient checkpointing; 577 minutes at about 25,000 tokens per second, 39.6 GB peak per GPU |
237
- | training cross-entropy | 0.97 over the first 200 steps, 0.42 at 25%, 0.35 at 50%, 0.35 over the last 300 steps |
238
-
239
- Why this optimizer: on a controlled quarter-data comparison of the same 4B (`ref_4b` against `ref_4b_bf16opt`, identical data and
240
- schedule, only the optimizer changed), AdamW on the bf16 parameters beat AdamW with FP32 master weights by 3.3 held-out points and
241
- 0.072 nats (0.791 against 0.758 held-out accuracy), and by 7 points on nine knowledge tasks. The master copy lets every small update
242
- through and moves the weights further from the base model; without it, updates below the bf16 resolution round away and more of
243
- the base model's knowledge is kept.
244
-
245
- **Stage 2 (v2.1).** A LoRA of rank 64 (alpha 128) on the attention and MLP weights of v1, trained for 2 epochs over 29,325 rows
246
- in v1's plain state-first layout with isolated Score levels, then merged into the bf16 weights:
247
 
248
- | source | rows | content | target |
249
- |---|---|---|---|
250
- | generated decision families | 8,000 | ten families (temporal and numeric decisions, subtle answer judgment, long policies, multi-hop lookup, abstention, probability, constrained trade-offs, safety judgment, paraphrase sensitivity, adversarial traps); the answers are computed by the generating code | the label |
251
- | questions over business documents, written by Qwen3.6-27B with thinking on | 11,356 | in two rounds, one realistic business document (up to 34 domains and 22 document kinds) plus three or four typed questions per writer call; each question was answered twice more by the same model in fresh contexts, with shuffled options and without the writer's answer, and kept only when both answers agreed with the writer's (89% and 91% kept) | the label |
252
- | human-labelled public sets (training halves) | 3,293 | MMLU, ARC, CommonsenseQA, BoolQ, MNLI, SNLI, Banking77, RACE, OpenBookQA, LogiQA 2, MedQA, Winogrande | the label |
253
- | replay of mixture v2 | 6,676 | 100 rows from the training half of each of the 67 in-task regression tasks | v1's answer distribution: loss KL(p_v1 ‖ p_model) at temperature 1 |
 
254
 
255
- These are v2's rows without 31 that the v2.1 checks found to share a context with an evaluation file (7 human-labelled rows and
256
- 24 replay rows).
257
-
258
- | stage 2 | |
259
- |---|---|
260
- | trainable parameters | LoRA rank 64, alpha 128, on the attention and MLP projections; merged after training |
261
- | loss | cross-entropy on the slot readout for labelled rows; KL(p_v1 ‖ p_model) over the options for replay rows, with p_v1 from the frozen v1 weights; the step loss is the sum over answers divided by the number of answers |
262
- | schedule | learning rate 1e-4, 5% warm-up then cosine, 1,518 steps of 65,536 tokens (2 epochs), seed 0 |
263
- | hardware | one NVIDIA B300, 94 minutes |
264
-
265
- No JevBench item and no Decision Index item was used for training, for writing the generators or the document questions, for
266
- selecting the checkpoint, or for the temperatures. The ten generated families and the skill list of the document questions were
267
- written from the family names that JevBench publishes for its sealed set, not from its items. Every training row was checked
268
- against every evaluation file used for selection, the canonical examples of the evaluation half of all 153 mixture-v2 evaluation
269
- tasks, the four request fixtures and the issue #9 cases: there is no exact state-and-question overlap (a few contexts under 40
270
- characters, such as a short utterance, occur under another task and question). The held-out sets used for selection are
271
- generated families from held-out templates (heldout_jb) and document questions from business domains that are not in the
272
- training data (test_teacher2). Two arms were trained to the end (B4 and C4, which differ in the size of the replay; two further arms were
273
- stopped or cancelled at the user's request); the rule selected B4 after epoch 2.
274
-
275
- **Temperatures.** `temperature` 1.099, fitted by NLL (all rows pooled) on the in-task half of the public regression set without
276
- Banking77, CLINC-OOS, MMLU, ARC, Winogrande and HellaSwag: 61 tasks, 102,804 rows. `temperature_by_type` fitted by NLL per answer
277
- type with `decider.calibrate.fit_by_type` on a pool of those regression rows and our own validation rows (the validation halves of
278
- the generated families and document questions, human-labelled validation rows, held-out replay rows): 108,927 Choice answers,
279
- 1,677 yes/no answers and 535 Score answers. The Choice pool is 94% regression rows, so the Choice temperature (1.110) stays close
280
- to the global one. v2's temperature (1.935) and v1's (1.05) were one value each.
281
 
282
  ## Evaluation
283
 
284
- All v2.1 numbers are with the per-type map through decider-ai 1.4.0, except where a paragraph says otherwise. Greedy play and
285
- accuracy do not depend on the temperature; sampled play and calibration do.
286
-
287
- **Public regression set**, rebuilt on this machine (95 tasks: 67 in-task, 28 held-out; large label sets sub-sampled to 10
288
- options; the temperatures fitted on in-task data). The rows are the same for every model. ECE is the expected calibration
289
- error with 15 bins, per-task mean. The regression rows are all Choice answers, so v2.1 reads them at the Choice temperature 1.110.
290
-
291
- | model | in-task acc / NLL / ECE (67 tasks) | held-out acc / NLL / ECE (28 tasks) |
292
- |---|---|---|
293
- | decider-2b v10, T=1.30 | 0.806 / 0.474 / 0.038 | 0.755 / 0.622 / 0.084 |
294
- | decider-2b v11, per-type map | 0.802 / 0.481 / 0.038 | 0.752 / 0.626 / 0.083 |
295
- | decider-4b v1, T=1.05 | 0.834 / 0.404 / 0.027 | 0.788 / 0.558 / 0.071 |
296
- | decider-4b v2, T=1.935 | 0.824 / 0.441 / 0.041 | 0.779 / 0.566 / 0.080 |
297
- | **decider-4b v2.1 (this repository), per-type map** | **0.831 / 0.414 / 0.031** | **0.784 / 0.569 / 0.077** |
298
- | decider-35b-a3b v1, T=1.08 | 0.855 / 0.357 / 0.026 | 0.810 / 0.497 / 0.069 |
299
-
300
- Per-task paired intervals were not recomputed for v2.1; the v2 card (tag `v2`) has them for v2 against v10 and the 35B, and v2.1
301
- is within 0.7 points of v2 on both halves.
302
-
303
- **The same rows as v1 and v2** are in the table under [Changes from v2](#changes-from-v2): the four fixtures (847 in-task
304
- validation rows, OpenJev, Mind2Web, TypeSafe), our held-out hard sets, JevBench, Bespoke, live browser tasks, zero-shot games,
305
- text games, behaviour probes and the issue #9 cases.
306
-
307
- **Ten text games, zero-shot** (`decider.games.play`, five episodes per game, greedy, eager; the 2B was trained on the first four,
308
- the 4B on none): Pong −5 (v1 −21, v2 −21; the 2B 8), Breakout 35 (v1 14, v2 12; the 2B 22), CliffWalking −13 (v1 −13, v2 −60;
309
- teacher −13), BabyAI-GoTo 0.19 (v1 0.54, v2 0.35), Freeway 0 (v2 1), Blackjack −0.6, FrozenLake, MiniGrid-Empty, LavaGap and DoorKey
310
- 0 as for v1 and v2. Greedy play takes the most probable option, so these results do not depend on the temperature.
311
-
312
- **JevBench public items** (231 items of [Benchmark Heaven](https://benchmarkheaven.com/jev-models); argmax over the exact
313
- label set with the request the harness's TypeSafe adapter builds). Read once for v2.1 at its global temperature (1.3.0) and once
314
- with the map (1.4.0), both after the rule decisions: easy 1.000, standard 0.986, hard 0.649 (72 of 111) in both reads. Top-label
315
- ECE on the hard tier is 0.184 with the map (0.210 at the global temperature); by answer type with the map, Choice 0.156 (67 items),
316
- yes/no 0.259 (38), Score 0.354 (6). Mean confidence on the hard tier is 0.799 at accuracy 0.649. For comparison on the same items:
317
- v2 0.676 (ECE 0.104 as read at T 1.719; 0.071 recomputed at its release temperature 1.935 with 6 Score items kept at 1.719), v1
318
- 0.550 (ECE 0.288), decider-35b-a3b 0.676, Jev 1.13.0 0.730. The public hard tier is 111 items (95% interval about ±9 points).
319
-
320
- **Bespoke's public suite** (13 human-labelled subsets, 3,880 records in Jev's wire format, answered through `system_one` as
321
- shipped). v1 and v2 through decider-ai 1.3.0, v2.1 through 1.4.0 with the map, same session. Nimble-9B (0.748 / 0.759 macro /
322
- micro) and Jev 1.13.0 (0.760 / 0.773) are in Bespoke's report.
323
-
324
- | subset (type) | decider-4b v1 | decider-4b v2 | decider-4b v2.1 |
325
- |---|---|---|---|
326
- | vitaminc-dev (choice) | 0.756 | 0.778 | 0.743 |
327
- | massive-en-US (choice; trained) | 0.860 | 0.869 | 0.863 |
328
- | massive-de-DE (choice) | 0.843 | 0.837 | 0.840 |
329
- | boolq (noul; trained) | 0.873 | 0.860 | 0.867 |
330
- | squad2 (noul) | 0.706 | 0.793 | 0.789 |
331
- | paws (noul; trained) | 0.716 | 0.832 | 0.764 |
332
- | multinli (choice; trained) | 0.933 | 0.926 | 0.933 |
333
- | civil_comments (noul; trained) | 0.880 | 0.857 | 0.873 |
334
- | aegis2 (noul) | 0.820 | 0.812 | 0.804 |
335
- | helpsteer2 (score; trained) | 0.466 | 0.466 | 0.450 |
336
- | summeval-relevance (score) | 0.425 | 0.463 | 0.396 |
337
- | summeval-consistency (score) | 0.833 | 0.826 | 0.799 |
338
- | pubmedqa (choice; trained) | 0.728 | 0.724 | 0.708 |
339
- | **macro / micro** | 0.757 / 0.765 | 0.773 / 0.781 | 0.756 / 0.765 |
340
- | macro over the subsets not trained on | 0.731 | 0.751 | 0.728 |
341
-
342
- The multinli subset (299 items) is easier than MNLI as a whole. On all 9,815 MNLI validation_matched items, decider-4b
343
- (revision `eb5fbdf`) scores **0.904** (entailment 0.928, neutral 0.830, contradiction 0.950); on the 299 suite items it
344
- scores 0.933. An independent run on 120 other validation_matched items measured 0.842
345
- ([discussion #3](https://huggingface.co/Mapika/decider-4b/discussions/3)).
346
 
347
- v2.1 is at v1's level on this suite and 1.6 points under v2 (macro). The largest differences to v2 are PAWS (0.764 against 0.832),
348
- SummEval relevance (0.396 against 0.463) and VitaminC (0.743 against 0.778).
349
-
350
- **Behaviour probes** (teacher-labelled, same probes as the other releases; v1 / v2 in brackets): generic-versus-specific bucket
351
- choice 1.00 / 1.00 (1.00 / 1.00; 0.95 / 1.00), catch-all when nothing fits 0.95 (0.90; 0.95), abstention battery 8 of 8 (7 of 8;
352
- 8 of 8); model-router tier 0.968 (0.968; 0.935) and needs-live-data 0.839 (0.871; 0.839); command-risk classification 0.889
353
- (0.889; 0.911) with no destructive command called safe, touches-outside-project 0.933 (0.956; 0.933); browser-agent element and
354
- action choice 0.938 / 0.750 (0.875 / 0.875; 0.938 / 0.938). The yes/no Brier score on the probe batteries is 0.010 with the map
355
- and 0.006 at the global temperature: the yes/no temperature 1.560 flattens easy yes/no answers there.
356
-
357
- ## Calibration
358
-
359
- The per-type map was fitted on everyday regression rows and our own validation rows. It helps where it can: yes/no and Score
360
- answers on the held-out sets. It does not change Choice answers much, because the Choice temperature fitted on a pool that is
361
- 94% everyday rows stays at 1.110. On the held-out generated families, whose answers are two-thirds Choice, v2.1 is
362
- overconfident: calibration error 0.147 with the map, where our own release limit is 0.08. v2 is at 0.046 there, only because its
363
- one temperature of 1.935 flattens every answer, which is also what cost it sampled play. ECE below is 10-bin top-label ECE on
364
- stored temperature-1 logits read at the served temperatures.
365
-
366
- | set | rows | accuracy | ECE at the global T / with the map | choice / noul / score ECE with the map |
367
- |---|---|---|---|---|
368
- | heldout_jb | 5,000 | 0.556 | 0.163 / 0.147 | 0.170 / 0.101 / 0.108 |
369
- | test_teacher2 | 449 | 0.820 | 0.050 / 0.043 | 0.057 / 0.048 / 0.086 |
370
- | guard | 2,994 | 0.819 | 0.019 / 0.017 | 0.017 / − / − |
371
- | cal_human | 1,595 | 0.856 | 0.019 / 0.021 | 0.021 / − / − |
372
-
373
- On the held-out generated families: v1 (T 1.05) 0.260, v2 (T 1.935) 0.046, v2.1 at T = 1 0.182. A map fitted without the
374
- regression rows (Choice 1.259) would reach 0.127 there and would make the regression set less calibrated (in-task ECE 0.0375
375
- against 0.0309); it was not used. On the fixtures (10-bin ECE, map): 847 validation rows 0.035 (v1 0.037, v2 0.031), TypeSafe
376
- 0.063 (v1 0.124, v2 0.072), OpenJev 0.154 (v1 0.158, v2 0.131), Mind2Web 0.017 (v1 0.016, v2 0.051). The Decision Index
377
- sample was not read for v2.1. If you route on confidence, calibrate on your own labels; `python -m decider.calibrate` fits a
378
- per-type map from your own answers read at temperature 1.
379
-
380
- ## Speed
381
-
382
- v2.1 has the same architecture and size as v1 and v2, and the temperature map is a division per answer; the speed was not
383
- measured again. The measurements of v1 and v2 (the same weights layout): in one session on one unshared NVIDIA B300 (bf16), 34.6
384
- ms (v2) and 32.4 ms (v1) median per decision over 200 game-state decisions of 156 tokens median, batch of one, eager PyTorch
385
- without CUDA graphs or `torch.compile`, timed around the forward pass with `torch.cuda.synchronize()`. The eager path is
386
- launch-bound, so host load changes it: v1's first measurement was 24.7 ms (10th to 90th percentile 24.6 to 43.5 ms), with
387
- decider-2b at 17.9 ms and decider-35b-a3b at 41.4 ms on the same decisions and method. With the helper's CUDA graphs and
388
- `torch.compile`, one support-ticket request (228 tokens, 3 questions) takes 5.2 ms (FP8 5.0 ms); a batch of 32 such states takes
389
- 81.5 ms, 1,178 decisions per second (FP8 71.2 ms, 1,349 per second). The HTTP server (`/decide`, bf16) answers 72.8 requests per
390
- second at a median of 13.3 ms with one client and 190 requests per second with 64 clients.
391
 
392
  ## Limitations
393
 
394
- * No reinforcement-learning stage: stated beliefs about action outcomes were not trained against exact laws. On live browser
395
- tasks v2.1 is at 93.2% sampled and 87.5% on the six held-out tasks; decider-2b v10, which has the RL stage, is at 93.2% and 91.7%.
396
- * Overconfident on hard multi-step items: calibration error 0.147 on our held-out generated families and 0.184 on the JevBench
397
- public hard tier, against v2's 0.046 and 0.104. On those items, do not read a confidence of 0.8 as an 80% chance of being right.
398
- * Issue #9 form case c_1 is answered wrongly (see Changes from v2); BabyAI-GoTo 0.19 against v1's 0.54; greedy bag-draw play 9.4
399
- points under v1.
400
- * About 0.3 points under v1 on the regression set and 1.1 points under it on the 847 validation rows; needs-live-data and
401
- touches-outside-project probes one item under v1 each.
402
- * The per-type map needs decider-ai 1.4.0 or later. With 1.3.0 or earlier, or with `DECIDER_TEMPERATURE` set, every answer uses
403
- 1.099: the answers are the same, yes/no answers are sharper (on the held-out generated families their ECE is 0.146 instead of
404
- 0.101) and Score answers are sharper.
405
- * Below the 35B by 2.4 / 2.6 points on the regression set and 2.7 on the JevBench public hard tier (3 items).
406
- * The JevBench public hard tier is 111 items; differences of a few items between versions are within its noise.
407
- * The stage-2 LoRA was trained only in the plain state-first layout. The schema-first layout (the server's opt-in schema cache)
408
- was not measured on v2.1, and `decider_config.json` does not mark it as trained for that layout (`schema_first_trained:
409
- false`), so `DECIDER_SCHEMA_CACHE=1` does not turn the schema cache on for this model.
410
- * Mixture v2's 26 additional public datasets and ten programmatic families, and stage 2's generators and document questions, are
411
- described above but their builders are not in the public package; `scripts/train.sh full` reproduces the 60% of stage 1's data
412
- that is the public mixture.
413
- * English is the main language; the multilingual rows (XNLI, PAWS-X, MASSIVE, Belebele, XCOPA) are a small share of the data
414
- and were not measured beyond the mixture-v2 evaluation set and Bespoke's German MASSIVE subset.
415
- * Everything else in the decider-2b card's limitations (packed questions see each other, long JSON arrays by position, full
416
- label sets against sampled options, abstention wording, rules in the question) applies; those shapes were not re-measured at
417
- this size.
418
-
419
- ## Changelog
420
-
421
- | version | what changed |
422
- |---|---|
423
- | **v2.1** (2026-09-24, these weights) | v1 + a merged LoRA on v2's rows (29,325 after removing 31 that overlapped evaluation files), with the replay rows trained toward v1's own answer distribution; temperature 1.099 and `temperature_by_type` {choice 1.110, noul 1.560, score 1.287} (decider-ai 1.4.0; older versions use 1.099). Against v2: sampled play recovered (bag-draw 52.0% against 37.9%, browser 93.2% against 88.1%), CliffWalking −13 against −60, regression set +0.7 / +0.5; hard sets at v2's level; less calibrated on hard items (0.147 against 0.046); issue #9 c_1 still wrong. Did not pass its pre-registered rules; released on the full comparison |
424
- | v2 (2026-09-24, Hub tag `v2`) | v1 + a merged LoRA (rank 64, attention and MLP, 2 epochs, 29,356 rows: generated decision families, document questions written by Qwen3.6-27B and kept when two independent answers agreed, human-labelled public sets, replay of mixture v2); temperature 1.935. Better on hard judgments, TypeSafe, OpenJev and Bespoke's suite; worse on sampled play, some text games and by about 1 point on everyday tasks |
425
- | v1 (2026-09-22, Hub tag `v1`) | first release: one pass over mixture v2 on Qwen3.5-4B-Base with AdamW on bf16 parameters, no RL stage; temperature 1.05 |
426
-
427
- The GitHub repository's [docs/CHANGELOG.md](https://github.com/Mapika/decider/blob/main/docs/CHANGELOG.md) lists every
428
- decider release.
429
-
430
- ## Reproduction
431
-
432
- Code, data registry, training and evaluation scripts and the per-version history: https://github.com/Mapika/decider
433
- (`docs/HISTORY.md`, section "decider-4b"). Stage 1 was trained with the data-parallel trainer of the architecture A/B study
434
- (`arch_ab/train_dp_optvar.py` in the research repository, optimizer variant `bf16`); stage 2 with a LoRA trainer in the research
435
- repository. Both were evaluated with the public `decider.evaluate` and the head-to-head tools. `eval_results.json` in this
436
- repository has the regression metrics with the map and at the global temperature, our held-out sets by answer type, the
437
- fixtures, games, browser and Bespoke results with the paired comparisons against v1 and v2, the text games, the behaviour
438
- probes, the issue #9 cases and the JevBench public items.
439
-
440
- **Independence.** This is an independent project. It is not affiliated with or endorsed by TypeSafe AI. It is an open
441
- reproduction of the "System One" model class (TypeSafe AI's Jev); nothing was distilled from Jev. The training data is public
442
- datasets, programmatically generated rows with verifiable gold, and data written or labelled by local Qwen3.5-27B and
443
- Qwen3.6-27B models. License: Apache 2.0.
 
3
  base_model: Qwen/Qwen3.5-4B-Base
4
  language: [en]
5
  pipeline_tag: text-classification
6
+ tags: [decision-model, calibrated, structured-output, multi-task, system-one, one-pass, vision]
7
  ---
8
 
9
+ # decider-4b: typed decisions with calibrated probabilities in one forward pass, 4B dense, text and images
10
 
11
  A language model that does not generate text. It reads a state and one or more typed questions, each with an explicit option
12
+ list, and returns a probability distribution over the options for every question from one forward pass. It is called from
13
+ software, not chatted with. From v3 the state may include images.
 
14
 
15
+ **This repository holds v3 (2026-10-20).** v2.1 stays available under the Hub tag `v2.1`, with its own card; v2 and v1 under
16
+ `v2` and `v1`.
 
 
 
 
 
 
 
 
17
 
18
+ - **Weights:** the text model at the repository root (Qwen3.5-4B-Base architecture, bf16, 8.4 GB), loaded as before by
19
+ `decider.infer.Decider`, `decider.serve` and the GGUF builds. `vision/` holds the image part: the vision tower of
20
+ Qwen3.5-4B-Base (unchanged, 0.6 GB) and the config of the image-text model. The image path reuses the text model's weights.
21
+ - **Code:** [decider-ai](https://github.com/Mapika/decider) 1.10.0 for images; text works with 1.4.0 and later.
 
 
 
22
 
23
+ ## Changes from v2.1
24
 
25
+ | | v2.1 | v3 |
26
+ |---|---|---|
27
+ | Decision Index 0.3, 4,500-request sample | 41.55 | **51.02** |
28
+ | Image questions, mean of 11 sets | 0.840 (v2.1 + base vision tower) | **0.891** |
29
+ | Visual7W / VSR (never trained on) | 0.857 / 0.813 | 0.873 / 0.833 |
30
+ | JevBench public items, hard tier | 0.649 | 0.640 |
31
+ | JevBench public items, standard tier | 0.986 | 0.986 |
32
+ | Fresh held-out yes/no set, scored the JevBench way | −1.9 | **44.3** |
33
+
34
+ - **Decision Index:** +9.5 on the sample, from the decider-31b training mixture (train splits of benchmarks the index also tests;
35
+ see Training).
36
+ - **Images:** new. The gain over the plain base vision tower is mostly on the kinds of image questions trained.
37
+ - **JevBench public items:** level (one hard item fewer).
38
+ - **Yes/no answers:** the config now uses temperature 0.05 for Noul answers, so they come out near 0 or 1. JevBench counts a yes/no
39
+ probability between 0.2 and 0.8 as wrong; at 0.05 almost none fall there, and our 399-item held-out yes/no set scores 44.3
40
+ instead of −0.3 at the fitted temperature. The cost: the yes/no probabilities are no longer calibrated (ECE 0.265 instead of
41
+ 0.102 on that set). For calibrated yes/no probabilities use `temperature_by_type={"choice": 2.098, "noul": 2.795, "score":
42
+ 3.152}` (the fitted values).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  ## The decider family
45
 
46
+ | model | base | use it for |
47
+ |---|---|---|
48
+ | [decider-2b](https://huggingface.co/Mapika/decider-2b) v12 | Qwen3.5-2B-Base | the fastest: routing, classification, judgments; text and images |
49
+ | **decider-4b** v3 | Qwen3.5-4B-Base | the middle point; text and images |
50
+ | [decider-12b](https://huggingface.co/Mapika/decider-12b) v2 | Gemma-4-12B-it | our JevBench model; text |
51
+ | [decider-12b-vision](https://huggingface.co/Mapika/decider-12b-vision) | Gemma-4-12B-it | the Decision Index task mix and images; calibrated yes/no |
52
+ | [decider-31b](https://huggingface.co/Mapika/decider-31b) v2 | Gemma-4-31B-it, NVFP4 | the strongest; text and images; Blackwell GPU, vLLM |
53
+ | [decider-35b-a3b](https://huggingface.co/Mapika/decider-35b-a3b) | Qwen3.5-35B-A3B-Base | text |
 
 
 
54
 
55
+ All share one interface (`POST /v1/systemone` in TypeSafe's format) and one readout. Code and changelog:
56
+ https://github.com/Mapika/decider.
57
 
58
  ## Usage
59
 
60
+ Text, as before:
61
+
62
  ```python
63
+ from decider.infer import Decider
64
  d = Decider("Mapika/decider-4b")
65
  d.decide("My card was charged twice for the same purchase.",
66
  [{"question": "Which department should handle this?", "options": ["billing", "technical support", "sales"]},
67
  {"question": "Does this need a refund action?", "options": ["no", "yes"]}])
 
68
  ```
69
 
70
+ Images, through the server (decider-ai 1.10.0; the config's `"images": true` turns image input on):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
+ ```bash
73
+ pip install "decider-ai>=1.10.0" pillow
74
+ DECIDER_MODEL=Mapika/decider-4b uvicorn decider.serve:app --host 127.0.0.1 --port 8000
75
+ ```
76
 
77
  ```python
78
+ import base64, httpx
79
+ img = base64.b64encode(open("chart.png", "rb").read()).decode()
80
+ r = httpx.post("http://127.0.0.1:8000/v1/systemone", timeout=60, json={
81
+ "state": {"chart": "q3-chart", "note": "Escalate if any region fell by more than 10%."},
82
+ "assets": {"q3-chart": {"type": "image", "format": "png", "data_base64": img}},
83
+ "questions": {"escalate": {"type": "noul", "instructions": "Does the `chart` call for escalation?",
84
+ "criteria": {"true": "escalate", "false": "no escalation"}}}})
 
 
 
85
  ```
86
 
87
+ The asset's key is the reference; each place the state names it becomes "the image" in the prompt, and the image goes in front
88
+ of the prompt, as in training. PNG and JPEG; up to 4 images (`DECIDER_MAX_IMAGES`); references in question text and box
89
+ answers are not supported. Image rows are read one at a time, without batching or CUDA graphs. Requirements as before (`torch`,
90
+ `transformers>=5`, `flash-linear-attention`), plus `pillow` for images.
 
 
 
 
 
 
 
 
91
 
92
  ## Training
93
 
94
+ v3 starts from v2.1. Its text weights were placed into the Qwen3.5-4B-Base image-text model (vision tower from the base); the
95
+ language-model weights are identical to the text model's. Then a LoRA stage (rank 64, learning rate 1e-4, 2 epochs, 110 minutes
96
+ on one B300) on the language model only:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
+ - **Text, 39.5k rows:** the decider-31b training mixture (train splits of CLINC150, iSarcasmEval, VAST, ACOS, POP909-CL, GSM8K
99
+ and NLI4CT, decontaminated against every Decision Index 0.3 suite row; generated smart-home requests; generated
100
+ state-tracking decisions and decision families; document questions written by Qwen3.6-27B; labelled public rows), plus 6.7k
101
+ rows replayed from v2.1's training data.
102
+ - **Images, 18,129 rows:** train splits of seven the_cauldron subsets: AI2D, A-OKVQA, Hateful Memes, IconQA, RAVEN, ScienceQA,
103
+ TQA. Visual7W and VSR were not used.
104
+ - Not used: Decision Index test rows, JevBench or ImageJevBench items.
105
 
106
+ Temperatures were fitted on our own validation rows: Choice 2.098, Score 3.152, Noul 2.795 (the config uses 0.05 for Noul, see
107
+ above). They are about twice v2.1's because v3's raw answers are sharper.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
  ## Evaluation
110
 
111
+ All numbers measured by us; none were verified by the benchmark maintainers.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ - **Decision Index 0.3,** 4,500-request sample of the public suite: 51.02 (v2.1 41.55); through `decider.serve` 1.10.0, 50.85.
114
+ Benchmark-weighted ECE on the sample: 0.160 (v2.1 0.150); with about 88 requests per benchmark this number is inflated for
115
+ every model. The full public suite was not run.
116
+ - **Image questions,** eval splits of 11 the_cauldron sets, 300 items each: mean 0.891; trained kinds 0.902, never-trained
117
+ (Visual7W, VSR) 0.853. v2.1 with the base vision tower and no image training: 0.841 / 0.835. Stock Qwen3.5-4B with our
118
+ readout: 0.827 mean. Through `decider.serve` 1.10.0 with the image sent as an asset: VSR 0.837, Visual7W 0.873.
119
+ - **JevBench public items** (read once, never used for training, fitting or selection): easy 1.000, standard 0.986, hard 0.640.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
  ## Limitations
122
 
123
+ - **One pass only:** knowledge stays at the base model's level; multi-step tasks are not solved by reading harder.
124
+ - **Yes/no calibration** is traded for JevBench's rule in the default config (see Changes).
125
+ - **Images:** new image question types read about as well as the stock model. Not measured on ImageJevBench.
126
+ - **Latency** was not measured again; the text path is unchanged from v2.1.
127
+
128
+ ## License
129
+
130
+ The weights are released under Apache 2.0, as the base model. Training sources and their terms:
131
+
132
+ - Image questions, from the train splits of [the_cauldron](https://huggingface.co/datasets/HuggingFaceM4/the_cauldron) subsets
133
+ (the_cauldron licenses only its own prompts, CC BY 4.0; each source keeps its own terms):
134
+ - AI2D (Kembhavi et al., 2016): CC BY-SA 4.0.
135
+ - A-OKVQA (Schwenk et al., 2022): Apache 2.0 (repository); images from COCO.
136
+ - IconQA (Lu et al., 2021): CC BY-NC-SA 4.0, non-commercial.
137
+ - ScienceQA (Lu et al., 2022): CC BY-NC-SA 4.0, non-commercial research use.
138
+ - TQA (Kembhavi et al., 2017): CC BY-NC 3.0, non-commercial.
139
+ - RAVEN (Zhang et al., 2019): no data licence stated (the code is GPL-3.0).
140
+ - Hateful Memes (Kiela et al., 2020): Facebook's Hateful Memes dataset licence. It permits use of the data to research and
141
+ develop models designed to detect manipulated media, and states that models derived from it may be used for academic and
142
+ commercial purposes. This is a research project; check the licence for your own use.
143
+
144
+ - CLINC150: CC BY 3.0 (Larson et al., 2019). Credit: "An Evaluation Dataset for Intent Classification and Out-of-Scope
145
+ Prediction", clinc/oos-eval.
146
+ - iSarcasmEval: MIT (Abu Farha et al., 2022). The texts are tweets.
147
+ - POP909-CL: MIT; built on POP909 (Wang et al., 2020), MIT.
148
+ - VAST (Allaway and McKeown, 2020) and ACOS (Cai et al., 2021): no licence stated by the authors.
149
+ - GSM8K (Cobbe et al., 2021): MIT.
150
+ - NLI4CT (SemEval-2024 Task 2, Jullien et al.): no licence file in the task repository; released for the shared task.
151
+ - RACE: non-commercial research use. It is a small part of the replayed data (250 rows).
152
+ - ARC, SNLI, LogiQA 2.0 (copy dependent): CC BY-SA 4.0; BoolQ: CC BY-SA 3.0; BANKING77, MedQA: CC BY 4.0; MMLU,
153
+ CommonsenseQA: MIT; MNLI: mixed per genre.
154
+ - The sources of v2.1 and earlier, listed on the v2.1 card (Hub tag `v2.1`).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/__init__.py DELETED
File without changes
decider/batching.py DELETED
@@ -1,95 +0,0 @@
1
- """Batch planning for the server's queue: which queued rows share one forward.
2
-
3
- Pure python, no torch, no engine: `plan_batches` takes the row lengths and two callables that describe the engine's
4
- bucket grid, and returns the groups to run. `decider.serve.batcher` calls it once per collection.
5
-
6
- Cost model. A forward of B rows padded to length T costs `overhead + B * T` token-units: `overhead` is the fixed cost of
7
- one forward (launch, replay, the slot gather and the device-to-host copy) expressed as the number of padded tokens that
8
- take the same time, and `B * T` is the padded token work. Two rows of different lengths are therefore worth merging
9
- into the longer row's bucket exactly when the padding they add is cheaper than a second forward's overhead.
10
- `DEFAULT_MERGE_OVERHEAD_TOKENS` is measured on decider-2b (docs/SERVING.md, section on the batching policy); the server
11
- reads `DECIDER_MERGE_OVERHEAD_TOKENS` over it.
12
-
13
- The partition is exact, not greedy. Rows are sorted by padded length descending (stable, so rows of the same bucket keep
14
- their arrival order) and split into consecutive groups; each group runs at the bucket of its longest member, so a group
15
- starting at position k costs `overhead + g * T[k]`. A dynamic program over the sorted sequence takes the cheapest split,
16
- subject to `g <= min(max_batch, max_rows(T))`, with ties going to the larger group. The per-bucket grouping the server
17
- did before is one of the partitions the program may choose (rows of equal length are adjacent in the sorted order), so
18
- the planned cost is never above it, and with `overhead = 0` the plan is exactly that grouping.
19
- """
20
-
21
- DEFAULT_MERGE_OVERHEAD_TOKENS = 512
22
-
23
-
24
- def group_cap(T, max_rows, max_batch):
25
- """Rows one forward may take at padded length T: the engine's widest captured batch bucket, and the server's cap."""
26
- return max(1, min(int(max_batch), int(max_rows(T))))
27
-
28
-
29
- def batch_cost(size, T, overhead=DEFAULT_MERGE_OVERHEAD_TOKENS):
30
- """Cost of one forward of `size` rows padded to `T`, in token-units."""
31
- return overhead + size * T
32
-
33
-
34
- def plan_cost(groups, overhead=DEFAULT_MERGE_OVERHEAD_TOKENS):
35
- """Cost of a plan: the sum over its forwards."""
36
- return sum(batch_cost(len(idx), T, overhead) for T, idx in groups)
37
-
38
-
39
- def per_bucket_groups(lengths, pad_len, max_rows, max_batch):
40
- """The grouping of 1.1.0: rows of the same padded length, chunked to the cap. The baseline `plan_batches` improves on."""
41
- return _exact_bucket(list(range(len(lengths))), [pad_len(n) for n in lengths], max_rows, max_batch)
42
-
43
-
44
- def _exact_bucket(idx, pads, max_rows, max_batch):
45
- buckets = {}
46
- for i in idx:
47
- buckets.setdefault(pads[i], []).append(i)
48
- out = []
49
- for T, members in buckets.items():
50
- cap = group_cap(T, max_rows, max_batch)
51
- for k in range(0, len(members), cap):
52
- out.append((T, members[k:k + cap]))
53
- return out
54
-
55
-
56
- def plan_batches(lengths, pad_len, max_rows, max_batch, overhead=DEFAULT_MERGE_OVERHEAD_TOKENS, mergeable=None):
57
- """Partition queued rows into forwards.
58
-
59
- lengths: token count of every queued row, in arrival order.
60
- pad_len(n): the engine's padded length for a row of n tokens (`EngineV2.pad_len`).
61
- max_rows(T):rows the engine runs in one forward at padded length T (`EngineV2.max_rows`).
62
- max_batch: the server's own cap (`DECIDER_MAX_BATCH`).
63
- overhead: fixed cost of a forward in token-units (see the module docstring).
64
- mergeable(n): False for a row that must not be padded into another row's bucket. Rows above the last captured
65
- length bucket run eager at a request-specific shape, so they are grouped by exact length as before.
66
-
67
- -> [(padded length, [row indices]), ...]. Every row appears in exactly one group; a group's padded length is at
68
- least every member's own padded length; a group holds at most `min(max_batch, max_rows(T))` rows.
69
- """
70
- n = len(lengths)
71
- if n == 0:
72
- return []
73
- pads = [pad_len(x) for x in lengths]
74
- merge = [True] * n if mergeable is None else [bool(mergeable(x)) for x in lengths]
75
- out = _exact_bucket([i for i in range(n) if not merge[i]], pads, max_rows, max_batch)
76
-
77
- order = sorted((i for i in range(n) if merge[i]), key=lambda i: -pads[i]) # stable: equal buckets keep arrival order
78
- m = len(order)
79
- dp = [0] * (m + 1) # dp[k]: cheapest cost of covering order[k:]
80
- take = [0] * (m + 1)
81
- for k in range(m - 1, -1, -1):
82
- T = pads[order[k]]
83
- cap = min(group_cap(T, max_rows, max_batch), m - k)
84
- best, best_g = None, 1
85
- for g in range(1, cap + 1):
86
- c = overhead + g * T + dp[k + g]
87
- if best is None or c <= best: # ties go to the larger group: same modelled cost, one fewer launch
88
- best, best_g = c, g
89
- dp[k], take[k] = best, best_g
90
- k = 0
91
- while k < m:
92
- g = take[k]
93
- out.append((pads[order[k]], sorted(order[k:k + g]))) # inside a group, arrival order
94
- k += g
95
- return out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/calibrate.py DELETED
@@ -1,222 +0,0 @@
1
- """Fit decider_config.json "temperature_by_type" by NLL, one temperature per answer type (decider.temperature).
2
-
3
- python -m decider.calibrate records.jsonl [--min-rows 50]
4
- -> {"temperature_by_type": {"choice": 1.48, "noul": 2.22, "score": 1.38}, "temperature": 1.6, "rows": {...}, "nll": {...}}
5
-
6
- A record is one answer with its gold, read at temperature 1:
7
- {"type": "choice" | "noul" | "score", "logits": [one value per option], "gold": option index}
8
- {"type": "score", "level_logits": [[no, yes], one pair per level], "gold": level index} Score with isolated levels
9
- "probs" / "level_probs" (probabilities at temperature 1) may stand in for "logits" / "level_logits": log p is the logit up to a
10
- constant, which the softmax ignores. A noul gold is 1 for true, 0 for false. Every record is checked first (check_record):
11
- a malformed one (gold out of range or not an integer, wrong shape, NaN, a row without a finite logit, an isolated Score whose
12
- levels all have P(yes) = 0) raises ValueError naming its position.
13
-
14
- An isolated-levels Score answer is fitted through the readout the server uses: every level row is softmax(row / T), and the
15
- answer is P(yes) of each level divided by their sum (decider.systemone.combine_isolated). So the "score" temperature is fitted on
16
- Score answers whichever readout the model serves them with; fit it on records collected with the same isolated_levels setting.
17
-
18
- `collect(decider, examples)` produces records from a decider.infer.Decider and labelled /v1/systemone-shaped examples.
19
- "temperature" in the output is one temperature fitted on all records together, for comparison; a type with fewer than
20
- --min-rows records is left out of the map (it then uses "temperature" of the config).
21
- """
22
- import json, math, sys
23
- import numpy as np
24
-
25
- from decider.temperature import TYPES
26
-
27
- GRID = np.exp(np.linspace(math.log(0.05), math.log(20.0), 801)) # 0.05 .. 20, about 0.75 % apart
28
-
29
-
30
- def _log(x):
31
- x = np.asarray(x, dtype=np.float64)
32
- with np.errstate(divide="ignore"):
33
- return np.where(x > 0, np.log(np.clip(x, 1e-300, None)), -np.inf)
34
-
35
-
36
- def _logits(rec, key):
37
- if key in rec:
38
- return np.asarray(rec[key], dtype=np.float64)
39
- alt = {"logits": "probs", "level_logits": "level_probs"}[key]
40
- return _log(rec[alt])
41
-
42
-
43
- def _lse(z, axis=-1):
44
- m = np.max(z, axis=axis, keepdims=True)
45
- m = np.where(np.isfinite(m), m, 0.0)
46
- with np.errstate(divide="ignore"): # an all -inf row gives -inf, handled by the callers
47
- return (m + np.log(np.sum(np.exp(z - m), axis=axis, keepdims=True))).squeeze(axis)
48
-
49
-
50
- def nll(rec, T):
51
- """Negative log-likelihood of one record's gold at temperature T."""
52
- return float(_Batch([rec]).nll(T))
53
-
54
-
55
- def _isolated(rec):
56
- return "level_logits" in rec or "level_probs" in rec
57
-
58
-
59
- def check_record(rec, i=None):
60
- """Raise ValueError unless `rec` is a well-formed record (module docstring): an integral gold within the options or levels,
61
- a 1-D row of at least two options or an [levels, 2] array of at least two levels, logits that are not NaN or +inf,
62
- probabilities in [0, 1] with positive mass, and isolated levels only on a Score record."""
63
- where = f"record {i}" if i is not None else "record"
64
- if not isinstance(rec, dict):
65
- raise ValueError(f"{where}: not a JSON object")
66
- if rec.get("type") not in TYPES:
67
- raise ValueError(f"{where}: type {rec.get('type')!r}, expected one of {', '.join(TYPES)}")
68
- iso = _isolated(rec)
69
- keys = ("level_logits", "level_probs") if iso else ("logits", "probs")
70
- if sum(k in rec for k in keys) != 1 or (iso and ("logits" in rec or "probs" in rec)):
71
- raise ValueError(f"{where}: give exactly one of logits, probs, level_logits, level_probs")
72
- if iso and rec["type"] != "score":
73
- raise ValueError(f"{where}: isolated levels (level_logits / level_probs) belong to a score record, not {rec['type']!r}")
74
- key = keys[0] if keys[0] in rec else keys[1]
75
- try:
76
- a = np.asarray(rec[key], dtype=np.float64)
77
- except (TypeError, ValueError):
78
- raise ValueError(f"{where}: {key} is not a numeric array") from None
79
- if iso and (a.ndim != 2 or a.shape[1] != 2 or a.shape[0] < 2):
80
- raise ValueError(f"{where}: {key} must be one [no, yes] pair per level, at least two levels; got shape {a.shape}")
81
- if not iso and (a.ndim != 1 or a.shape[0] < 2):
82
- raise ValueError(f"{where}: {key} must be one value per option, at least two options; got shape {a.shape}")
83
- if key.endswith("probs"):
84
- if not np.all(np.isfinite(a)) or np.any(a < 0) or np.any(a > 1) or np.any(a.sum(-1) <= 0):
85
- raise ValueError(f"{where}: {key} must be probabilities in [0, 1] with positive mass per row")
86
- elif np.any(np.isnan(a)) or np.any(a == np.inf) or not np.all(np.any(np.isfinite(a), axis=-1)):
87
- raise ValueError(f"{where}: {key} contains NaN or +inf, or a row without a finite value")
88
- if rec["type"] == "noul" and a.shape[0] != 2:
89
- raise ValueError(f"{where}: a noul record has exactly two options (false, true); got {a.shape[0]}")
90
- if iso and not np.any(a[:, 1] > 0 if key == "level_probs" else np.isfinite(a[:, 1])):
91
- raise ValueError(f"{where}: no level has a yes probability above 0, so the served Score answer has no distribution")
92
- g = rec.get("gold")
93
- if isinstance(g, bool) or not isinstance(g, (int, np.integer)) or not 0 <= g < a.shape[0]:
94
- raise ValueError(f"{where}: gold must be an integer index in 0..{a.shape[0] - 1}, got {g!r}")
95
- return rec
96
-
97
-
98
- class _Batch:
99
- """Records packed into padded arrays, so one temperature is scored over all of them at once."""
100
- def __init__(self, records):
101
- records = [check_record(r, i) for i, r in enumerate(records)]
102
- lists = [r for r in records if not _isolated(r)]
103
- isos = [r for r in records if _isolated(r)]
104
- self.n = len(lists) + len(isos)
105
- self.L = self.Lg = self.I = self.Im = self.Ig = None
106
- if lists:
107
- zs = [_logits(r, "logits") for r in lists]
108
- K = max(len(z) for z in zs)
109
- self.L = np.full((len(zs), K), -np.inf)
110
- for i, z in enumerate(zs):
111
- self.L[i, :len(z)] = z
112
- self.Lg = np.array([int(r["gold"]) for r in lists])
113
- if isos:
114
- zs = [_logits(r, "level_logits").reshape(-1, 2) for r in isos]
115
- n = max(len(z) for z in zs)
116
- self.I = np.zeros((len(zs), n, 2)); self.Im = np.zeros((len(zs), n), dtype=bool)
117
- for i, z in enumerate(zs):
118
- self.I[i, :len(z)] = z; self.Im[i, :len(z)] = True
119
- self.Ig = np.array([int(r["gold"]) for r in isos])
120
-
121
- def nll(self, T):
122
- """Sum of the records' NLL at temperature T."""
123
- tot = 0.0
124
- if self.L is not None:
125
- z = self.L / T
126
- zg = z[np.arange(len(z)), self.Lg]
127
- tot += float(np.sum(np.where(np.isfinite(zg), _lse(z) - zg, 690.0)))
128
- if self.I is not None: # in log space: P(yes) of very confident rows underflows otherwise
129
- z = self.I / T
130
- with np.errstate(invalid="ignore"):
131
- lp = np.where(self.Im, z[..., 1] - _lse(z), -np.inf) # log P(yes) per level, -inf on padding
132
- norm = _lse(lp) # log of the summed P(yes)
133
- lg = lp[np.arange(len(z)), self.Ig]
134
- with np.errstate(invalid="ignore"):
135
- per = np.where(np.isfinite(lg), norm - lg, 690.0) # check_record guarantees a finite norm
136
- tot += float(np.sum(per))
137
- return tot
138
-
139
-
140
- def mean_nll(records, T):
141
- b = records if isinstance(records, _Batch) else _Batch(records)
142
- return b.nll(T) / b.n
143
-
144
-
145
- def fit(records, grid=GRID):
146
- """The temperature with the lowest mean NLL on the grid, refined by a golden-section search between its neighbours."""
147
- records = records if isinstance(records, _Batch) else _Batch(list(records))
148
- if not records.n:
149
- raise ValueError("no records to fit")
150
- vals = [mean_nll(records, T) for T in grid]
151
- i = int(np.argmin(vals))
152
- lo, hi = math.log(grid[max(i - 1, 0)]), math.log(grid[min(i + 1, len(grid) - 1)])
153
- r = (math.sqrt(5) - 1) / 2
154
- a, b = hi - r * (hi - lo), lo + r * (hi - lo)
155
- fa, fb = mean_nll(records, math.exp(a)), mean_nll(records, math.exp(b))
156
- for _ in range(40):
157
- if fa < fb:
158
- hi, b, fb = b, a, fa; a = hi - r * (hi - lo); fa = mean_nll(records, math.exp(a))
159
- else:
160
- lo, a, fa = a, b, fb; b = lo + r * (hi - lo); fb = mean_nll(records, math.exp(b))
161
- T = math.exp((lo + hi) / 2)
162
- return T if mean_nll(records, T) <= vals[i] else float(grid[i])
163
-
164
-
165
- def fit_by_type(records, min_rows=50):
166
- """-> {"temperature_by_type": {type: T}, "temperature": pooled T, "rows": {type: n}, "nll": {type: {"T=1": .., "fitted": ..}}}"""
167
- records = [check_record(r, i) for i, r in enumerate(records)]
168
- groups = {t: [] for t in TYPES}
169
- for r in records:
170
- groups[r["type"]].append(r)
171
- out = {"temperature_by_type": {}, "temperature": round(fit(records), 3) if records else None,
172
- "rows": {t: len(g) for t, g in groups.items()}, "nll": {}}
173
- for t, g in groups.items():
174
- if len(g) < max(1, min_rows):
175
- continue
176
- b = _Batch(g); T = fit(b)
177
- out["temperature_by_type"][t] = round(T, 3)
178
- out["nll"][t] = {"T=1": round(mean_nll(b, 1.0), 4), "fitted": round(mean_nll(b, T), 4)}
179
- return out
180
-
181
-
182
- def collect(decider, examples, independent=True):
183
- """Records for fit_by_type from a decider.infer.Decider, read at temperature 1 on the uncached state-first path.
184
- examples: iterable of (state, questions, golds) with questions as /v1/systemone takes them and golds {question id: gold},
185
- the gold being a Choice option name, a Noul true/false, or a Score level index. Questions without a gold are skipped.
186
- Score questions are read with the Decider's isolated_levels setting, as system_one serves them."""
187
- recs = []
188
- for state, questions, golds in examples:
189
- rqs, index, items = decider._system_one_items(state, questions, independent, layout="state_first")
190
- rows = decider._system_one_probs(items, "state_first", temperature=1.0)
191
- for k, kind, s, n in index:
192
- if k not in golds:
193
- continue
194
- rq, g = rqs[k], golds[k]
195
- if rq["type"] == "noul" and not (isinstance(g, bool) or g in (0, 1)):
196
- raise ValueError(f"gold of noul question {k!r} must be true or false, got {g!r}")
197
- if rq["type"] == "score" and (isinstance(g, bool) or not isinstance(g, int) or not 0 <= g < len(rq["names"])):
198
- raise ValueError(f"gold of score question {k!r} must be a level index in 0..{len(rq['names']) - 1}, got {g!r}")
199
- key = bool(g) if rq["type"] == "noul" else g
200
- if key not in rq["names"]:
201
- raise ValueError(f"gold of choice question {k!r} is not one of its options: {g!r}")
202
- gold = rq["names"].index(key)
203
- if kind == "iso":
204
- recs.append({"type": rq["type"], "level_probs": [rows[s + j][:2] for j in range(n)], "gold": gold})
205
- else:
206
- recs.append({"type": rq["type"], "probs": rows[s][:len(rq["options"])], "gold": gold})
207
- return recs
208
-
209
-
210
- def main(argv=None):
211
- import argparse
212
- ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
213
- ap.add_argument("records", help="JSON lines, one record per line (see the module docstring)")
214
- ap.add_argument("--min-rows", type=int, default=50, help="fit a type only when it has at least this many records")
215
- a = ap.parse_args(argv)
216
- with open(a.records) as f:
217
- recs = [json.loads(line) for line in f if line.strip()]
218
- print(json.dumps(fit_by_type(recs, a.min_rows), indent=1))
219
-
220
-
221
- if __name__ == "__main__":
222
- main(sys.argv[1:])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/engine.py DELETED
@@ -1,216 +0,0 @@
1
- """Low-latency inference engine: shape-bucketed CUDA graphs over the one-pass decision model.
2
-
3
- Right padding + causal layers => pad positions never influence earlier slots, so no attention
4
- mask is needed and every (B, T) bucket can be captured once and replayed. The graph outputs
5
- option-letter logits for all positions [B, T, K]; slots are gathered outside.
6
- """
7
- import time, torch, torch._dynamo, torch.nn.functional as F
8
- from decider.model import DecisionModel, collate
9
- from decider.prompt import build, MAX_OPTIONS
10
- from decider.temperature import scaled_softmax, slot_temperatures
11
-
12
- T_BUCKETS = [64, 128, 192, 256, 320, 384, 512, 640, 768, 1024, 1280, 1536, 2048]
13
- B_BUCKETS = [1, 2, 4, 8, 16, 32, 64]
14
- GRAPH_MAX_T = 2048 # longer inputs (up to the 32k request budget) run eagerly: compute dominates there, and one graph
15
- LONG_STEP = 1024 # per (B, T) shape would cost a compile + capture for every new length
16
-
17
-
18
- def _bucket(x, buckets):
19
- for b in buckets:
20
- if x <= b:
21
- return b
22
- return None
23
-
24
-
25
- def fused_causal_conv1d_fn(hidden_states, weight, bias=None, activation=None, **kwargs):
26
- """Depthwise causal conv (kernel k) as k shifted multiply-adds: fuses under torch.compile,
27
- unlike the cuDNN grouped conv fallback (which was ~11% of batched GPU time)."""
28
- B, C, T = hidden_states.shape; k = weight.shape[-1]
29
- x = F.pad(hidden_states.to(weight.dtype), (k - 1, 0))
30
- out = x[:, :, k - 1:k - 1 + T] * weight[:, k - 1][None, :, None]
31
- for j in range(k - 1):
32
- out = out + x[:, :, j:j + T] * weight[:, j][None, :, None]
33
- if bias is not None:
34
- out = out + bias[None, :, None]
35
- if activation == "silu":
36
- out = F.silu(out)
37
- elif activation is not None:
38
- from transformers.activations import ACT2FN
39
- out = ACT2FN[activation](out)
40
- return out.to(hidden_states.dtype)
41
-
42
-
43
- def patch_conv():
44
- from transformers.models.qwen3_5 import modeling_qwen3_5 as mq
45
- mq.causal_conv1d_fn = fused_causal_conv1d_fn
46
-
47
-
48
- def read_slots(out, rows, slots, nopts, temperature, n_per_item):
49
- """One gather + one softmax + one device-to-host copy for the whole batch (was: three small kernels and a sync per item).
50
- out [B, T, K] logits; rows/slots/nopts: flat python lists, one entry per question; n_per_item: questions per item.
51
- temperature: a number for every question, or a flat list with one temperature per question (decider.temperature)."""
52
- dev = out.device; idx = torch.tensor([rows, slots, nopts], dtype=torch.long).to(dev, non_blocking=True)
53
- lg = out[idx[0], idx[1]] # [N, K]
54
- lg = lg.masked_fill(torch.arange(lg.shape[1], device=dev)[None, :] >= idx[2][:, None], float("-inf"))
55
- p = scaled_softmax(lg, temperature).cpu()
56
- return list(torch.split(p, n_per_item))
57
-
58
-
59
- def fill_ids(items_ids, B, T, pad):
60
- import numpy as np
61
- a = np.full((B, T), pad, dtype=np.int64)
62
- for b, x in enumerate(items_ids): a[b, :len(x)] = x
63
- return torch.from_numpy(a)
64
-
65
-
66
- def set_attention_backend_policy():
67
- """Turn off the cuDNN scaled-dot-product-attention backend. On Blackwell with torch 2.14 / CUDA 13 it returns wrong,
68
- finite output for masked rectangular attention, which is what the shared-state path (`Engine.score_shared`) and the schema
69
- cache run when a suffix is scored against a cached prefix; the math and memory-efficient backends are correct. Measured on
70
- a Decision Index row: the cached path answered a wrong option at p=0.93 where the full forward and the corrected cached path
71
- both give option_4 at p=0.95 (decider2/SERVING_V2_REVIEW.md in the research notes). Must run before torch.compile and CUDA
72
- graph capture: captured graphs keep the backend they were captured with."""
73
- if hasattr(torch.backends.cuda, "enable_cudnn_sdp"):
74
- torch.backends.cuda.enable_cudnn_sdp(False)
75
-
76
-
77
- class Engine:
78
- """compile: torch.compile the forward (needs use_cache=False; ~1.4x batched, fuses elementwise work).
79
- fp8: e4m3 weights + per-token activation scaling on the big linears (Hopper tensor cores).
80
- conv_patch: fusable depthwise causal conv instead of the cuDNN fallback."""
81
- def __init__(self, path, device="cuda", dtype=torch.bfloat16, use_graphs=True, max_ctx_tokens=1536,
82
- compile=True, fp8=False, conv_patch=True):
83
- set_attention_backend_policy()
84
- if conv_patch:
85
- if str(device).startswith("mps"):
86
- from decider.mps_ops import patch_mps
87
- patch_mps()
88
- else:
89
- patch_conv()
90
- self.m = DecisionModel(path, dtype=dtype, grad_ckpt=False).to(device).eval()
91
- use_graphs = use_graphs and torch.device(device).type == "cuda"
92
- self.tok = self.m.tok; self.dev = device; self.use_graphs = use_graphs; self.max_ctx = max_ctx_tokens
93
- self.core, self.W = self.m.lm.model, self.m.lm.lm_head.weight[self.m.letters].detach().clone()
94
- self.cfg = dict(compile=compile, fp8=fp8, conv_patch=conv_patch, graphs=use_graphs)
95
- if fp8:
96
- from decider.fp8 import convert_to_fp8
97
- self.cfg["fp8_layers"] = convert_to_fp8(self.core)
98
- if compile:
99
- torch._dynamo.config.cache_size_limit = 128
100
- self._fwd_impl = torch.compile(self._fwd_eager, dynamic=False)
101
- else:
102
- self._fwd_impl = self._fwd_eager
103
- self.graphs = {} # (B, T) -> (static_ids, static_out, graph)
104
- self.pool = torch.cuda.graph_pool_handle() if (use_graphs and str(device).startswith("cuda")) else None
105
- self.stats = dict(graph_captures=0, forwards=0)
106
-
107
- def _fwd_eager(self, ids):
108
- h = self.core(input_ids=ids, use_cache=False).last_hidden_state
109
- return F.linear(h, self.W).float() # [B, T, K]
110
-
111
- @torch.no_grad()
112
- def _fwd(self, ids):
113
- return self._fwd_impl(ids)
114
-
115
- def _capture(self, B, T):
116
- s_ids = torch.full((B, T), self.tok.pad_token_id, dtype=torch.long, device=self.dev)
117
- st = torch.cuda.Stream(); st.wait_stream(torch.cuda.current_stream())
118
- with torch.cuda.stream(st):
119
- for _ in range(3): self._fwd(s_ids) # warm-up: compile / triton autotune
120
- torch.cuda.current_stream().wait_stream(st)
121
- g = torch.cuda.CUDAGraph()
122
- with torch.cuda.graph(g, pool=self.pool):
123
- s_out = self._fwd(s_ids)
124
- self.stats["graph_captures"] += 1
125
- return s_ids, s_out, g
126
-
127
- @torch.no_grad()
128
- def logits_all(self, ids):
129
- """ids: [B, T] long on device (already right-padded to a bucket). Returns [B, T, K] float."""
130
- B, T = ids.shape; self.stats["forwards"] += 1
131
- if T > GRAPH_MAX_T:
132
- self.stats["long_forwards"] = self.stats.get("long_forwards", 0) + 1
133
- return self._fwd_eager(ids)
134
- if not self.use_graphs:
135
- return self._fwd(ids)
136
- key = (B, T)
137
- if key not in self.graphs:
138
- self.graphs[key] = self._capture(B, T)
139
- s_ids, s_out, g = self.graphs[key]
140
- s_ids.copy_(ids); g.replay()
141
- return s_out
142
-
143
- @torch.no_grad()
144
- def score_items(self, items, temperature=1.0):
145
- """items: list of dicts from prompt.build. Returns list of [n_q, MAX_OPTIONS] prob tensors (cpu).
146
- temperature: a number, or one entry per item (a number or one number per slot; decider.temperature.for_items)."""
147
- Tmax = max(len(it["ids"]) for it in items)
148
- T = _bucket(Tmax, T_BUCKETS) or -(-Tmax // LONG_STEP) * LONG_STEP
149
- B = (_bucket(len(items), B_BUCKETS) or len(items)) if T <= GRAPH_MAX_T else len(items)
150
- ids = fill_ids([it["ids"] for it in items], B, T, self.tok.pad_token_id)
151
- out = self.logits_all(ids.to(self.dev, non_blocking=True))
152
- return read_slots(out, [b for b, it in enumerate(items) for _ in it["slots"]], [s for it in items for s in it["slots"]],
153
- [n for it in items for n in it["nopts"]], slot_temperatures(temperature, items), [len(it["slots"]) for it in items])
154
-
155
- @torch.no_grad()
156
- def score_shared(self, items, temperature=1.0, min_prefix=192):
157
- """Rows that start with the same tokens (one state, one question per row): run the shared prefix once, fork its
158
- cache (attention KV + delta-net conv/recurrent states), and run only the question suffixes.
159
- Same answers as score_items up to kernel round-off; cost ~ state + sum(questions) instead of n * state.
160
- The fork is made in chunks that fit `DECIDER_SHARED_FORK_GB`, so the peak memory does not grow with the question
161
- count; the implementation is decider.shared_prefix, shared with EngineV2."""
162
- from decider import shared_prefix # imported here: decider.shared_prefix imports this module
163
- out = shared_prefix.score_shared(self, items, temperature, min_prefix)
164
- if out is None:
165
- return self.score_items(items, temperature)
166
- self.stats["shared_prefix_calls"] = self.stats.get("shared_prefix_calls", 0) + 1
167
- return out
168
-
169
- def warmup(self, shapes=((1, 128), (1, 256), (1, 384), (1, 512), (8, 256), (8, 512), (32, 256), (32, 512))):
170
- t = time.time()
171
- for B, T in shapes:
172
- self.logits_all(torch.full((B, T), self.tok.pad_token_id, dtype=torch.long, device=self.dev))
173
- torch.cuda.synchronize(); return time.time() - t
174
-
175
-
176
- if __name__ == "__main__":
177
- import sys, random, numpy as np
178
- from decider import data as D
179
- from decider.infer import Decider
180
- path = sys.argv[1] if len(sys.argv) > 1 else "runs/r3_v2/model"
181
- cfg = dict(compile="nocompile" not in sys.argv[2:], fp8="fp8" in sys.argv[2:], conv_patch="noconv" not in sys.argv[2:])
182
- _, evals = D.load_cache("data/tasks.pkl")
183
- eng = Engine(path, **cfg); print("engine cfg", eng.cfg)
184
- rng = random.Random(0)
185
- exs = evals["support_tickets"][:64] + evals["clinc_oos"][:64] + evals["race"][:32]
186
- from decider.prompt import chat_for_model
187
- chat = chat_for_model(path, eng.tok)
188
- items = [build(e, eng.tok, rng, max_ctx_tokens=1536, chat=chat) for e in exs]
189
- # correctness vs eager masked forward (DecisionModel.slot_logits)
190
- ref = []
191
- with torch.no_grad():
192
- for i in range(0, len(items), 16):
193
- b = collate(items[i:i + 16], eng.tok.pad_token_id)
194
- lg = eng.m.slot_logits(b["input_ids"].cuda(), b["attention_mask"].cuda(), b["slot_idx"].cuda(), b["slot_batch"].cuda(), b["nopts"].cuda())
195
- ref.append(torch.softmax(lg, -1).cpu())
196
- ref = torch.cat(ref)
197
- got = torch.cat(eng.score_items(items))
198
- print(f"max |p_graph - p_eager| = {(ref - got).abs().max():.4f} over {len(ref)} questions; argmax agreement {(ref.argmax(1) == got.argmax(1)).float().mean():.4f}")
199
- print(f"warmup capture of 8 buckets: {eng.warmup():.1f}s; captures so far {eng.stats['graph_captures']}")
200
- # latency: single real requests
201
- for name, pool in [("support_tickets", exs[:64]), ("clinc_oos", exs[64:128]), ("race", exs[128:])]:
202
- its = [build(e, eng.tok, rng, chat=chat) for e in pool]
203
- ts = []
204
- for it in its[:40]:
205
- torch.cuda.synchronize(); t = time.time(); eng.score_items([it]); torch.cuda.synchronize(); ts.append(time.time() - t)
206
- ts = np.array(ts[5:]) * 1000
207
- print(f"single request {name:16s}: p50 {np.median(ts):5.1f} ms p90 {np.percentile(ts, 90):5.1f} ms (avg {np.mean([len(i['ids']) for i in its]):.0f} tok, {len(its[0]['slots'])} q)")
208
- for bs in (8, 32):
209
- ts = []
210
- for i in range(0, min(len(its), bs * 6), bs):
211
- chunk = its[i:i + bs]
212
- if len(chunk) < bs: break
213
- torch.cuda.synchronize(); t = time.time(); eng.score_items(chunk); torch.cuda.synchronize(); ts.append(time.time() - t)
214
- ts = np.array(ts[1:]) * 1000
215
- print(f" batch {bs:2d}: p50 {np.median(ts):6.1f} ms -> {bs/np.median(ts)*1000:6.0f} ctx/s, {bs*len(its[0]['slots'])/np.median(ts)*1000:6.0f} decisions/s")
216
- print("stats", eng.stats, "graphs", len(eng.graphs), f"mem {torch.cuda.memory_reserved()/1e9:.1f} GB")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/engine_v2.py DELETED
@@ -1,213 +0,0 @@
1
- """Serving engine: every CUDA graph is keyed on (batch bucket, padded length bucket) only.
2
-
3
- Difference from decider.engine.Engine (the library engine behind decider.infer.Decider):
4
- * the full bucket grid is captured at start-up by `warmup()`, then `seal()` forbids any further capture, so no request
5
- can ever pay a graph capture or a torch.compile;
6
- * the length ladder reaches 8192 instead of 2048, so long rows replay a graph instead of running eager;
7
- * a batch larger than the widest captured batch bucket for its length is split into captured chunks rather than
8
- capturing a new shape; rows longer than the last bucket run eager in chunks of at most `token_budget` padded tokens;
9
- * torch.compile and FP8 are off by default.
10
-
11
- Numerics are the same as Engine: right padding + causal layers means padded positions never influence earlier slots, so
12
- the answer for a row does not depend on which (B, T) bucket it was padded into, up to kernel reduction order.
13
-
14
- The attention backend policy of decider.engine (cuDNN SDPA off) is applied in __init__, before compile and capture. It
15
- is what makes `score_shared` (a suffix scored against a cached prefix) return the same answers as the full forward; see
16
- docs/CHANGELOG.md 1.0.2 and tests/test_engine_v2_cuda.py.
17
- """
18
- import time, torch, torch.nn.functional as F
19
- from decider import shared_prefix
20
- from decider.engine import read_slots, fill_ids, patch_conv, set_attention_backend_policy
21
- from decider.model import DecisionModel
22
- from decider.temperature import item_slice, slot_temperatures
23
-
24
- T_BUCKETS = [64, 128, 192, 256, 320, 384, 512, 640, 768, 1024, 1280, 1536, 2048, 3072, 4096, 6144, 8192]
25
- B_BUCKETS = [1, 2, 4, 8, 16, 32]
26
- TOKEN_BUDGET = 32768 # capture (B, T) only when B * T fits this; B = 1 is always captured
27
- LONG_STEP = 1024 # rows longer than the last bucket: pad to a multiple of this and run eager
28
-
29
-
30
- class EngineV2:
31
- """compile / fp8 default to False. `model` lets a test inject a stand-in DecisionModel instead of loading one."""
32
-
33
- def __init__(self, path=None, device="cuda", dtype=torch.bfloat16, use_graphs=None, compile=False, fp8=False,
34
- conv_patch=None, t_buckets=None, b_buckets=None, token_budget=TOKEN_BUDGET, max_ctx_tokens=32768,
35
- model=None):
36
- set_attention_backend_policy() # before compile and capture: captured graphs keep their backend
37
- if conv_patch is None:
38
- conv_patch = bool(compile) # the unrolled depthwise conv only pays off inside a compiled region
39
- if str(device).startswith("mps"):
40
- from decider.mps_ops import patch_mps # the Apple Silicon path of decider.infer.Decider
41
- patch_mps()
42
- elif conv_patch:
43
- patch_conv()
44
- self.m = model if model is not None else DecisionModel(path, dtype=dtype, grad_ckpt=False).to(device).eval()
45
- self.tok = self.m.tok; self.dev = device; self.max_ctx = max_ctx_tokens
46
- self.core = self.m.lm.model
47
- self.W = self.m.lm.lm_head.weight[self.m.letters].detach().clone()
48
- self.t_buckets = sorted(t_buckets or T_BUCKETS)
49
- self.b_buckets = sorted(b_buckets or B_BUCKETS)
50
- self.token_budget = int(token_budget)
51
- self.use_graphs = (str(device).startswith("cuda") if use_graphs is None else bool(use_graphs))
52
- self.cfg = dict(compile=compile, fp8=fp8, conv_patch=conv_patch, graphs=self.use_graphs,
53
- t_buckets=self.t_buckets, b_buckets=self.b_buckets, token_budget=self.token_budget)
54
- if fp8:
55
- from decider.fp8 import convert_to_fp8
56
- self.cfg["fp8_layers"] = convert_to_fp8(self.core)
57
- if compile:
58
- from torch import _dynamo # not `import torch._dynamo`: that rebinds `torch` locally
59
- n = len(self.graph_shapes())
60
- # one specialisation per captured shape, times the frames inside the model. Without the accumulated
61
- # limits dynamo stops compiling part way through warm-up and silently leaves the rest eager, which with
62
- # conv_patch on is slower than not compiling at all.
63
- _dynamo.config.cache_size_limit = max(256, n + 16)
64
- _dynamo.config.accumulated_cache_size_limit = max(1 << 16, 64 * n)
65
- for k in ("accumulated_recompile_limit", "recompile_limit"):
66
- if hasattr(_dynamo.config, k):
67
- setattr(_dynamo.config, k, max(1 << 16, 64 * n))
68
- self._fwd_impl = torch.compile(self._fwd_eager, dynamic=False)
69
- else:
70
- self._fwd_impl = self._fwd_eager
71
- self.graphs = {} # (B, T) -> (static ids, static out, graph)
72
- self.b_for_t = {} # T -> sorted captured batch buckets
73
- self.pool = torch.cuda.graph_pool_handle() if (self.use_graphs and str(device).startswith("cuda")) else None
74
- self.sealed = False
75
- self.stats = dict(graph_captures=0, forwards=0, replays=0, eager_forwards=0, eager_rows=0,
76
- shared_calls=0, unbucketed_requests=0)
77
-
78
- # ---- bucket arithmetic -------------------------------------------------
79
- def graph_shapes(self):
80
- """The full grid captured at start-up: every (B, T) whose padded token count fits the budget, plus all of B = 1."""
81
- return [(B, T) for T in self.t_buckets for B in self.b_buckets if B == 1 or B * T <= self.token_budget]
82
-
83
- def t_bucket(self, n):
84
- """Padded length bucket for a row of n tokens, or None when it is longer than the last bucket."""
85
- for t in self.t_buckets:
86
- if n <= t:
87
- return t
88
- return None
89
-
90
- def pad_len(self, n):
91
- return self.t_bucket(n) or -(-n // LONG_STEP) * LONG_STEP
92
-
93
- def max_rows(self, T):
94
- """Rows per forward at padded length T: the widest captured batch bucket, or, when nothing is captured at T, as
95
- many rows as fit the token budget (at least 1)."""
96
- av = self.b_for_t.get(T)
97
- return av[-1] if av else max(1, self.token_budget // max(T, 1))
98
-
99
- def b_plan(self, n, T):
100
- """Batch sizes covering n rows at length T, each one a captured bucket. One chunk when a bucket fits."""
101
- av = self.b_for_t.get(T)
102
- if not av:
103
- return [n]
104
- fit = next((b for b in av if b >= n), None)
105
- if fit is not None:
106
- return [fit]
107
- out = []; big = av[-1]
108
- while n > big:
109
- out.append(big); n -= big
110
- out.append(next(b for b in av if b >= n))
111
- return out
112
-
113
- # ---- forward -----------------------------------------------------------
114
- def _fwd_eager(self, ids):
115
- h = self.core(input_ids=ids, use_cache=False).last_hidden_state
116
- return F.linear(h, self.W).float() # [B, T, K]
117
-
118
- @torch.no_grad()
119
- def _fwd(self, ids):
120
- return self._fwd_impl(ids)
121
-
122
- def _capture(self, B, T):
123
- s_ids = torch.full((B, T), self.tok.pad_token_id, dtype=torch.long, device=self.dev)
124
- st = torch.cuda.Stream(); st.wait_stream(torch.cuda.current_stream())
125
- with torch.cuda.stream(st):
126
- for _ in range(3): self._fwd(s_ids)
127
- torch.cuda.current_stream().wait_stream(st)
128
- g = torch.cuda.CUDAGraph()
129
- with torch.cuda.graph(g, pool=self.pool):
130
- s_out = self._fwd(s_ids)
131
- self.stats["graph_captures"] += 1
132
- return s_ids, s_out, g
133
-
134
- @torch.no_grad()
135
- def logits_all(self, ids):
136
- """ids: [B, T] long on device, already right-padded to a captured shape where one exists."""
137
- B, T = ids.shape; self.stats["forwards"] += 1
138
- if self.use_graphs:
139
- g = self.graphs.get((B, T))
140
- if g is None and not self.sealed: # only reachable before seal(); a sealed engine never captures
141
- g = self.graphs[(B, T)] = self._capture(B, T)
142
- self.b_for_t.setdefault(T, [])
143
- if B not in self.b_for_t[T]: self.b_for_t[T] = sorted(self.b_for_t[T] + [B])
144
- if g is not None:
145
- s_ids, s_out, gr = g
146
- s_ids.copy_(ids); gr.replay(); self.stats["replays"] += 1
147
- return s_out
148
- self.stats["eager_forwards"] += 1; self.stats["eager_rows"] += B
149
- return self._fwd_eager(ids) # never the compiled callable: a new shape must not compile
150
-
151
- # ---- scoring -----------------------------------------------------------
152
- @torch.no_grad()
153
- def score_items(self, items, temperature=1.0):
154
- """items: dicts from prompt.build / build_rows. -> one [n_q, MAX_OPTIONS] cpu probability tensor per item.
155
- temperature: a number, or one entry per item (a number or one number per slot; decider.temperature.for_items)."""
156
- if not items:
157
- return []
158
- slot_temperatures(temperature, items) # a length mismatch fails before any forward
159
- Tmax = max(len(it["ids"]) for it in items)
160
- T = self.t_bucket(Tmax)
161
- if T is None:
162
- self.stats["unbucketed_requests"] += 1
163
- T = -(-Tmax // LONG_STEP) * LONG_STEP
164
- per = self.max_rows(T); plan = [per] * (len(items) // per) + ([len(items) % per] if len(items) % per else [])
165
- else:
166
- plan = self.b_plan(len(items), T)
167
- out = []; i = 0
168
- for B in plan:
169
- chunk = items[i:i + B]; i += len(chunk)
170
- ids = fill_ids([it["ids"] for it in chunk], B, T, self.tok.pad_token_id)
171
- lg = self.logits_all(ids.to(self.dev, non_blocking=True))
172
- out += read_slots(lg, [b for b, it in enumerate(chunk) for _ in it["slots"]],
173
- [s for it in chunk for s in it["slots"]], [n for it in chunk for n in it["nopts"]],
174
- slot_temperatures(item_slice(temperature, i - len(chunk), i), chunk), [len(it["slots"]) for it in chunk])
175
- return out
176
-
177
- @torch.no_grad()
178
- def score_shared(self, items, temperature=1.0, min_prefix=192, budget_bytes=None, rows_per_fork=None):
179
- """Rows that start with the same tokens (one state, one question per row): run the shared prefix once, fork its
180
- cache in chunks that fit a byte budget, run only the question suffixes. The algorithm of Engine.score_shared,
181
- shared with it in decider.shared_prefix: it is per request, never per schema, so it adds no state that outlives
182
- the request and no shape that depends on the question set. The prefix and suffix forwards are eager
183
- (request-specific shapes). Correct only with the cuDNN SDPA backend off, which __init__ arranges."""
184
- out = shared_prefix.score_shared(self, items, temperature, min_prefix, budget_bytes, rows_per_fork)
185
- if out is None:
186
- return self.score_items(items, temperature)
187
- self.stats["shared_calls"] += 1
188
- return out
189
-
190
- # ---- start-up ----------------------------------------------------------
191
- def warmup(self, shapes=None, log=None):
192
- """Capture the whole grid. Call seal() afterwards: from then on an unknown shape runs eager, never captures."""
193
- t = time.time(); shapes = list(shapes if shapes is not None else self.graph_shapes())
194
- for j, (B, T) in enumerate(shapes):
195
- self.logits_all(torch.full((B, T), self.tok.pad_token_id, dtype=torch.long, device=self.dev))
196
- if log and (j + 1) % 10 == 0:
197
- log(f"[engine_v2] {j + 1}/{len(shapes)} graphs, {time.time() - t:.0f}s")
198
- if self.use_graphs:
199
- torch.cuda.synchronize()
200
- return time.time() - t
201
-
202
- def seal(self):
203
- self.sealed = True
204
- self.b_for_t = {}
205
- for B, T in self.graphs:
206
- self.b_for_t.setdefault(T, []).append(B)
207
- for T in self.b_for_t:
208
- self.b_for_t[T].sort()
209
- return self
210
-
211
- def describe(self):
212
- return dict(self.cfg, graphs=len(self.graphs), sealed=self.sealed,
213
- grid={str(T): self.b_for_t.get(T, []) for T in self.t_buckets})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/fp8.py DELETED
@@ -1,54 +0,0 @@
1
- """FP8 (e4m3) linear layers for Hopper via torch._scaled_mm.
2
- Weights: per-output-channel scales, quantised once. Activations: per-token dynamic scales.
3
- Under torch.compile the quantisation ops fuse into the surrounding elementwise work."""
4
- import torch, torch.nn as nn
5
-
6
- E4M3_MAX = 448.0
7
-
8
-
9
- def _quant_rowwise(x):
10
- s = x.abs().amax(dim=-1, keepdim=True).float().clamp(min=1e-12) / E4M3_MAX
11
- return (x.float() / s).clamp(-E4M3_MAX, E4M3_MAX).to(torch.float8_e4m3fn), s
12
-
13
-
14
- class FP8Linear(nn.Module):
15
- def __init__(self, lin: nn.Linear):
16
- super().__init__()
17
- wq, sw = _quant_rowwise(lin.weight.detach()) # [N,K] fp8, [N,1]
18
- self.register_buffer("wq", wq.contiguous()) # [N,K]; passed as wq.t() -> [K,N] column-major, as _scaled_mm wants
19
- self.register_buffer("sw_t", sw.t().contiguous()) # [1,N]
20
- self.bias = None if lin.bias is None else nn.Parameter(lin.bias.detach().clone(), requires_grad=False)
21
- self.in_features, self.out_features = lin.in_features, lin.out_features
22
- self.out_dtype = lin.weight.dtype
23
-
24
- def forward(self, x):
25
- shp = x.shape[:-1]
26
- x2 = x.reshape(-1, self.in_features)
27
- xq, sx = _quant_rowwise(x2)
28
- y = torch._scaled_mm(xq, self.wq.t(), scale_a=sx, scale_b=self.sw_t, bias=self.bias, out_dtype=self.out_dtype)
29
- return y.reshape(*shp, self.out_features)
30
-
31
-
32
- def convert_to_fp8(model, skip=("lm_head",), min_dim=1024):
33
- """Replace nn.Linear (with in/out >= min_dim) by FP8Linear in place. Returns count."""
34
- n = 0
35
- for name, mod in list(model.named_modules()):
36
- for cname, child in list(mod.named_children()):
37
- full = f"{name}.{cname}" if name else cname
38
- if isinstance(child, nn.Linear) and not any(s in full for s in skip) and min(child.in_features, child.out_features) >= min_dim:
39
- setattr(mod, cname, FP8Linear(child)); n += 1
40
- return n
41
-
42
-
43
- if __name__ == "__main__":
44
- import time
45
- lin = nn.Linear(2048, 6144, bias=False).cuda().to(torch.bfloat16)
46
- f8 = FP8Linear(lin)
47
- x = torch.randn(8192, 2048, device="cuda", dtype=torch.bfloat16)
48
- ref = lin(x); got = f8(x)
49
- print("rel err", ((ref.float() - got.float()).abs().mean() / ref.float().abs().mean()).item())
50
- for f, name in [(lin, "bf16 linear"), (f8, "fp8 linear (eager)"), (torch.compile(f8), "fp8 linear (compiled)")]:
51
- for _ in range(3): f(x)
52
- torch.cuda.synchronize(); t = time.time()
53
- for _ in range(20): f(x)
54
- torch.cuda.synchronize(); print(f"{name:24s} {(time.time()-t)/20*1000:.3f} ms")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/infer.py DELETED
@@ -1,357 +0,0 @@
1
- """Usable inference API: typed decisions with probabilities, all from one forward pass.
2
-
3
- from decider.infer import Decider
4
- d = Decider("runs/r2_full/model")
5
- out = d.decide("My card was charged twice for the same purchase.",
6
- [{"question": "Which department should handle this?", "options": ["billing", "technical", "sales"]},
7
- {"question": "How urgent is this?", "options": ["low", "medium", "high"]}])
8
- # -> [{'choice': 'billing', 'confidence': 0.97, 'probs': {...}}, {...}]
9
- """
10
- import logging
11
-
12
- import torch
13
- from decider.model import DecisionModel, collate
14
- from decider.prompt import build, MAX_OPTIONS, resolve_layout, chat_template
15
- from decider import temperature as TT
16
- from dataclasses import dataclass
17
-
18
-
19
- @dataclass
20
- class Q:
21
- text: str; options: list; gold: int = 0
22
-
23
-
24
- @dataclass
25
- class Example:
26
- context: str; qs: list; task: str = "infer"; image: bytes = None
27
-
28
-
29
- logger = logging.getLogger(__name__)
30
- NEUTRAL_NONE = "not listed here"
31
-
32
-
33
- def neutralize_options(options):
34
- """The training augmentation used the literal 'none of the above', and the model learned that exact string as an
35
- abstain signal (it abstains even on clear cases when the string is offered). Any option that reads like it is
36
- rewritten to a neutral phrasing for the model and mapped back in the output."""
37
- out, back = [], {}
38
- for o in options:
39
- key = o.strip().lower()
40
- if key.startswith("none of the above") or key in ("none of the above", "none", "n/a", "none of these"):
41
- out.append(NEUTRAL_NONE); back[NEUTRAL_NONE] = o
42
- else:
43
- out.append(o)
44
- return out, back
45
-
46
-
47
- class _NoShuffle: # keep option order as given
48
- def shuffle(self, x): pass
49
- def sample(self, xs, k): return xs[:k]
50
-
51
-
52
- class CompiledSchema:
53
- def __init__(self, d, rqs, h, index):
54
- from decider.systemone import row_types
55
- self.d, self.rqs, self.h, self.index = d, rqs, h, index
56
- self.types = row_types(rqs, index) # answer type of every schema row (decider.temperature)
57
-
58
- def batch(self, states, max_state_tokens=32768):
59
- from decider.systemone import render_state, assemble
60
- T = TT.for_types(self.d.T_schema, self.d.T_schema_by_type, self.types)
61
- probs = self.d._se.score(self.h, [render_state(s) for s in states], temperature=T, max_ctx_tokens=max_state_tokens)
62
- return [{"model": self.d.name, "answers": assemble(self.rqs, self.index, [p.tolist() for p in pr])} for pr in probs]
63
-
64
- def __call__(self, state, max_state_tokens=32768):
65
- return self.batch([state], max_state_tokens)[0]
66
-
67
-
68
- class Decider:
69
- """One-pass decisions with automatic CUDA, MPS, or CPU device selection.
70
-
71
- CUDA uses shape-bucketed graphs by default. MPS defaults to float16 and uses
72
- the optional MPS patch; CPU defaults to bfloat16. Set ``use_graphs=False``
73
- for eager execution or debugging.
74
- """
75
- def __init__(self, path, device=None, dtype=None, temperature=None, abstain_below=0.0, use_graphs=None, temperature_by_type=None):
76
- """The prompt layout comes from decider_config.json: "layout": "chat" (chat-trained checkpoints) wraps every prompt in the
77
- tokenizer's chat template (decider.prompt.build_chat); no "layout" key is the plain layout of every earlier model.
78
- An unknown layout raises ValueError before the weights are loaded.
79
-
80
- Temperatures (decider.temperature): decider_config.json "temperature" and the optional "temperature_by_type"
81
- {"choice": T, "noul": T, "score": T}. temperature= overrides "temperature" and switches the config's by-type map off;
82
- temperature_by_type= sets the map explicitly. An invalid temperature or map key raises ValueError before the weights
83
- are loaded."""
84
- if device is None:
85
- device = "cuda" if torch.cuda.is_available() else ("mps" if torch.backends.mps.is_available() else "cpu")
86
- if dtype is None:
87
- dtype = torch.float16 if str(device).startswith("mps") else torch.bfloat16
88
- logger.info("Decider device=%s dtype=%s", device, dtype)
89
- import json, os
90
- cfg = {}
91
- try: # model folder may carry decider_config.json (temperature, flags)
92
- from huggingface_hub import hf_hub_download
93
- cfg_path = os.path.join(path, "decider_config.json") if os.path.isdir(path) else hf_hub_download(path, "decider_config.json")
94
- cfg = json.load(open(cfg_path))
95
- except Exception:
96
- pass
97
- self.layout = resolve_layout(cfg)
98
- (temperature, self.T_by_type), (T_schema, self.T_schema_by_type) = TT.from_config(cfg, temperature, temperature_by_type)
99
- self.neutralize_none = bool(cfg.get("neutralize_none", True)) # v4 and earlier learned the literal string as an abstain signal
100
- if use_graphs is None:
101
- use_graphs = str(device).startswith("cuda")
102
- if use_graphs:
103
- from decider.engine import Engine
104
- self.eng = Engine(path, device=device, dtype=dtype); self.m = self.eng.m
105
- else:
106
- if str(device).startswith("mps"):
107
- from decider.mps_ops import patch_mps
108
- patch_mps()
109
- self.eng = None; self.m = DecisionModel(path, dtype=dtype, grad_ckpt=False).to(device).eval()
110
- self.chat = chat_template(self.m.tok) if self.layout == "chat" else None # None: plain layout, prompts as in 1.1.x
111
- self.dev = device; self.T = temperature; self.abstain_below = abstain_below
112
- self.name = "decider-" + str(cfg.get("version", "dev"))
113
- self.schema_first = bool(cfg.get("schema_first", False)) and self.eng is not None # default layout. Questions-first (the cacheable one) costs accuracy
114
- self.T_schema = T_schema # (about 1.5 points on fixed label sets, more elsewhere): opt in with schema()
115
- self.isolated_levels = bool(cfg.get("isolated_levels", False)) # Score levels judged one per row (v8+)
116
- self._se = None; self._schemas = {}
117
-
118
- def _decide_items(self, requests, max_ctx_tokens=1536):
119
- """The prompt rows of decide_batch: -> (requests with neutralized options, one item per request)."""
120
- exs = []
121
- if self.neutralize_none:
122
- requests = [(context, [dict(q, options=neutralize_options(q["options"])[0], _back=neutralize_options(q["options"])[1]) for q in qs]) for context, qs in requests]
123
- for context, qs in requests:
124
- for q in qs:
125
- assert 2 <= len(q["options"]) <= MAX_OPTIONS, f"2..{MAX_OPTIONS} options required"
126
- exs.append(Example(context, [Q(q["question"], list(q["options"]), 0) for q in qs], "infer"))
127
- items = [build(e, self.m.tok, _NoShuffle(), max_options=MAX_OPTIONS, max_ctx_tokens=max_ctx_tokens, chat=self.chat) for e in exs]
128
- for it, (_, qs) in zip(items, requests): # answer type per slot: a plain question is "choice"
129
- it["types"] = [q.get("_type", "choice") for q in qs]
130
- return requests, items
131
-
132
- @torch.no_grad()
133
- def decide_batch(self, requests, max_ctx_tokens=1536):
134
- """requests: list of (context:str, questions:list[dict(question, options)]). One forward pass for everything."""
135
- requests, items = self._decide_items(requests, max_ctx_tokens)
136
- T = TT.for_items(self.T, self.T_by_type, items) # the scalar self.T when there is no by-type map
137
- if self.eng is not None:
138
- probs = torch.cat(self.eng.score_items(items, temperature=T))
139
- else:
140
- b = collate(items, self.m.tok.pad_token_id)
141
- logits = self.m.slot_logits(b["input_ids"].to(self.dev), b["attention_mask"].to(self.dev), b["slot_idx"].to(self.dev),
142
- b["slot_batch"].to(self.dev), b["nopts"].to(self.dev))
143
- probs = TT.scaled_softmax(logits, TT.slot_temperatures(T, items)).cpu()
144
- out, k = [], 0
145
- for context, qs in requests:
146
- res = []
147
- for q in qs:
148
- p = probs[k, :len(q["options"])].tolist(); k += 1
149
- j = max(range(len(p)), key=p.__getitem__); back = q.get("_back", {})
150
- names = [back.get(o, o) for o in q["options"]]
151
- res.append(dict(choice=names[j] if p[j] >= self.abstain_below else None, confidence=p[j],
152
- probs={o: pi for o, pi in zip(names, p)}, probs_list=p))
153
- out.append(res)
154
- return out
155
-
156
- def decide(self, context, questions, **kw):
157
- return self.decide_batch([(context, questions)], **kw)[0]
158
-
159
- # ---- Jev-shaped interface (decider.systemone): state + {id: Choice | Score | Noul with criteria}
160
- # ---- schema cache (v7+): the questions are run once, requests only run the state (decider.schema_engine)
161
- def schema(self, questions, independent=True, isolated=None, compile=False):
162
- """Compile a fixed set of Jev-shaped questions: schema(state) -> answers; schema.batch([state, ...]) -> [answers]."""
163
- import json
164
- from decider.schema_engine import SchemaEngine
165
- from decider.systemone import render_question
166
- isolated = self.isolated_levels if isolated is None else isolated
167
- key = (json.dumps(questions, sort_keys=True, ensure_ascii=False), independent, isolated)
168
- if key not in self._schemas:
169
- if self._se is None: self._se = SchemaEngine(self.eng, chat=self.chat)
170
- if len(self._schemas) >= 64: # drop the oldest schema and its graphs
171
- old = next(iter(self._schemas)); hid = self._schemas.pop(old)[1].id
172
- for k in [k for k in self._se.graphs if k[0] == hid]: del self._se.graphs[k]
173
- from decider.systemone import plan_rows
174
- rqs = {k: render_question(v) for k, v in questions.items()}
175
- rows, index = plan_rows(rqs, isolated and independent)
176
- h = self._se.prepare(rows, independent=independent, compile=compile) # compile=True: ~25 s per (batch, length) shape, 1.6x faster after
177
- self._schemas[key] = (rqs, h, index)
178
- return CompiledSchema(self, *self._schemas[key])
179
-
180
- def system_one(self, state, questions, independent=True, max_state_tokens=32768, max_fwd_tokens=65536, layout=None, isolated=None):
181
- layout = layout or ("schema_first" if self.schema_first else "state_first")
182
- isolated = (self.isolated_levels if isolated is None else isolated) and independent
183
- if layout == "schema_first" and self.eng is not None:
184
- return self.schema(questions, independent, isolated)(state, max_state_tokens)
185
- """independent=True scores every question in its own row (state + that question only), so adding, removing or
186
- reordering questions cannot change any other answer; the state is run once and its cache forked to every
187
- question (Engine.score_shared). independent=False packs all questions behind one copy of the state in one row
188
- (later questions can then see earlier question texts)."""
189
- from decider.systemone import unique_tokens, assemble
190
- rqs, index, items = self._system_one_items(state, questions, independent, max_state_tokens, layout, isolated)
191
- flatp = self._system_one_probs(items, layout, max_fwd_tokens, TT.for_items(self.T, self.T_by_type, items))
192
- return {"model": self.name, "answers": assemble(rqs, index, flatp),
193
- "usage": {"input_tokens": unique_tokens(items), "output_tokens": 0}}
194
-
195
- @torch.no_grad()
196
- def _system_one_probs(self, items, layout, max_fwd_tokens=65536, temperature=None):
197
- """Score system_one's uncached rows. temperature: as Engine.score_items takes it (a number, or one entry per item).
198
- -> one probability list per question row, in row order."""
199
- if self.eng is not None and len(items) > 1 and layout == "state_first":
200
- probs = self.eng.score_shared(items, temperature=temperature)
201
- else:
202
- probs = []; per = max(1, max_fwd_tokens // max(len(it["ids"]) for it in items))
203
- for i in range(0, len(items), per):
204
- T = TT.item_slice(temperature, i, i + per)
205
- if self.eng is not None:
206
- probs += self.eng.score_items(items[i:i + per], temperature=T)
207
- else:
208
- bt = collate(items[i:i + per], self.m.tok.pad_token_id)
209
- lg = self.m.slot_logits(*[bt[k].to(self.dev) for k in ("input_ids", "attention_mask", "slot_idx", "slot_batch", "nopts")])
210
- pr = TT.scaled_softmax(lg, TT.slot_temperatures(T, items[i:i + per])).cpu(); c = 0
211
- for it in items[i:i + per]:
212
- probs.append(pr[c:c + len(it["slots"])]); c += len(it["slots"])
213
- return [p.tolist() for ps in probs for p in ps]
214
-
215
- def _system_one_items(self, state, questions, independent=True, max_state_tokens=32768, layout=None, isolated=None):
216
- """The prompt rows of system_one's uncached path: -> (rendered questions, answer index, items)."""
217
- from decider.systemone import render_state, render_question, plan_rows, row_types
218
- layout = layout or ("schema_first" if self.schema_first else "state_first")
219
- isolated = (self.isolated_levels if isolated is None else isolated) and independent
220
- ctx = render_state(state); rqs = {k: render_question(v) for k, v in questions.items()}
221
- opts = (lambda r: neutralize_options(r["options"])[0]) if self.neutralize_none else (lambda r: list(r["options"]))
222
- flat, index = plan_rows(rqs, isolated)
223
- rows = [[r] for r in flat] if independent else [flat]
224
- items = [build(Example(ctx, [Q(r["question"], opts(r), 0) for r in row]), self.m.tok, _NoShuffle(), max_options=MAX_OPTIONS,
225
- max_ctx_tokens=max_state_tokens, layout=layout, chat=self.chat) for row in rows]
226
- types = row_types(rqs, index) # answer type per row (decider.temperature)
227
- for it, ts in zip(items, [[t] for t in types] if independent else [types]):
228
- it["types"] = ts
229
- return rqs, index, items
230
-
231
- # ---- typed schema interface: {question: {"type": "bool"} | {"type": "choice", "options": [...]}
232
- # | {"type": "scale", "legend": {"0": "none", "1": "low", ...}}}
233
- SCHEMA_FORM = ('schema: a map {question: field}, each field one of {"type": "choice", "options": [option strings]}, '
234
- '{"type": "bool"} or {"type": "scale", "legend": [descriptions] or {level number: description}}')
235
-
236
- @staticmethod
237
- def _check_schema(schema):
238
- """Raise ValueError naming the expected form when `schema` does not have it (the HTTP server turns this into a 422).
239
- Every schema that 1.1.2 answered stays accepted, except options or a legend given as a bare string (1.1.2 split it into
240
- characters); an empty schema is answered with {}."""
241
- import json, math
242
- form = Decider.SCHEMA_FORM
243
- if schema is None:
244
- raise ValueError("schema is required; " + form)
245
- if not isinstance(schema, dict):
246
- raise ValueError(f"schema is a {type(schema).__name__}; " + form)
247
- for qtext, spec in schema.items():
248
- where = f"schema[{qtext!r}]"
249
- if not isinstance(spec, dict):
250
- raise ValueError(f"{where} is a {type(spec).__name__}, not a field object; " + form)
251
- t = spec.get("type", "choice")
252
- if t == "choice":
253
- opts = spec.get("options")
254
- if not isinstance(opts, (list, tuple, dict)) or not opts or not all(isinstance(o, str) for o in opts):
255
- raise ValueError(f'{where}: "options" must be a non-empty list of strings; ' + form)
256
- elif t == "scale":
257
- leg = spec.get("legend")
258
- if not isinstance(leg, (list, tuple, dict)) or not leg:
259
- raise ValueError(f'{where}: "legend" must be a non-empty list or {{level number: description}} map; ' + form)
260
- try: # the legend is echoed in the answer, which must serialise
261
- json.dumps(leg, allow_nan=False)
262
- except (TypeError, ValueError):
263
- raise ValueError(f'{where}: the "legend" contains a value that is not finite JSON (NaN or Infinity); ' + form)
264
- if isinstance(leg, dict):
265
- try:
266
- ok = all(math.isfinite(float(k)) for k in leg)
267
- except (TypeError, ValueError, OverflowError):
268
- ok = False
269
- if not ok:
270
- raise ValueError(f'{where}: the keys of a "legend" map must be finite numbers, e.g. {{"0": "none", "1": "low"}}; ' + form)
271
- elif t != "bool":
272
- raise ValueError(f"{where}: unknown field type {t!r}; " + form)
273
-
274
- @staticmethod
275
- def _schema_to_questions(schema):
276
- Decider._check_schema(schema)
277
- qs = []
278
- for qtext, spec in schema.items():
279
- t = spec.get("type", "choice")
280
- if t == "bool":
281
- qs.append(dict(question=qtext, options=["no", "yes"], _type="noul"))
282
- elif t == "choice":
283
- qs.append(dict(question=qtext, options=list(spec["options"]), _type="choice"))
284
- elif t == "scale":
285
- leg = spec["legend"]
286
- keys = sorted(leg, key=lambda k: float(k)) if isinstance(leg, dict) else list(range(len(leg)))
287
- labels = [f"{k}: {leg[k]}" if isinstance(leg, dict) else f"{i}: {leg[i]}" for i, k in enumerate(keys)]
288
- qs.append(dict(question=qtext, options=labels, _keys=keys, _legend=leg, _type="score"))
289
- else:
290
- raise ValueError(f"unknown field type {t}")
291
- return qs
292
-
293
- def decide_json_batch(self, requests, **kw):
294
- """requests: list of (context, schema). Returns one dict per context keyed by question."""
295
- qss = [self._schema_to_questions(schema) for _, schema in requests]
296
- raw = self.decide_batch([(ctx, qs) for (ctx, _), qs in zip(requests, qss)], **kw)
297
- out = []
298
- for (ctx, schema), qs, res in zip(requests, qss, raw):
299
- o = {}
300
- for (qtext, spec), q, r in zip(schema.items(), qs, res):
301
- t = spec.get("type", "choice")
302
- if t == "bool":
303
- o[qtext] = {"noul": round(r["probs"]["yes"], 4), "type": "noul"}
304
- elif t == "choice":
305
- o[qtext] = {"choice": r["choice"], "confidence": round(r["confidence"], 4), "type": "choice",
306
- "probabilities": {k: round(v, 4) for k, v in r["probs"].items()}}
307
- else:
308
- p = [r["probs"][lab] for lab in q["options"]]
309
- keys = q["_keys"]; n = len(p)
310
- score = sum(float(k) * pi for k, pi in zip(keys, p)) # expected level on the legend scale
311
- j = max(range(n), key=p.__getitem__)
312
- o[qtext] = {"score": round(score, 2), "confidence": round(p[j], 4), "type": "scale", "legend": q["_legend"],
313
- "probabilities": {str(keys[i]): round(pi, 4) for i, pi in enumerate(p)}}
314
- out.append(o)
315
- return out
316
-
317
- def decide_json(self, context, schema, **kw):
318
- return self.decide_json_batch([(context, schema)], **kw)[0]
319
-
320
-
321
- if __name__ == "__main__":
322
- import sys, json, time
323
- d = Decider(sys.argv[1] if len(sys.argv) > 1 else "runs/r1_200k/model")
324
- demo = [
325
- ("My card was charged twice for the same purchase and I want the extra charge refunded.",
326
- [{"question": "Which department should handle this?", "options": ["billing", "technical support", "sales"]},
327
- {"question": "What is the customer's sentiment?", "options": ["angry", "neutral", "happy"]},
328
- {"question": "Does this need a refund action?", "options": ["no", "yes"]}]),
329
- ("hey can u turn the lights off in the kitchen",
330
- [{"question": "What is the intent?", "options": ["smart home control", "set alarm", "play music", "none of the above"]},
331
- {"question": "Is this request toxic?", "options": ["no", "yes"]}]),
332
- ("The quarterly report shows revenue fell 12% while costs rose sharply.",
333
- [{"question": "What is the financial sentiment?", "options": ["bearish", "neutral", "bullish"]}]),
334
- ]
335
- t = time.time(); res = d.decide_batch(demo); dt = time.time() - t
336
- for (ctx, qs), r in zip(demo, res):
337
- print("\n>>", ctx)
338
- for q, a in zip(qs, r):
339
- print(f" {q['question']:45s} -> {a['choice']!s:22s} p={a['confidence']:.2f} " + " ".join(f"{o}:{p:.2f}" for o, p in a['probs'].items()))
340
- print(f"\n{sum(len(q) for _, q in demo)} decisions in {dt*1000:.0f} ms (one forward pass)")
341
- schema = {
342
- "Revenue currently impacted?": {"type": "bool"},
343
- "What business impact?": {"type": "choice", "options": ["none", "degraded", "outage"]},
344
- "Integration issue present?": {"type": "bool"},
345
- "Account health status?": {"type": "choice", "options": ["healthy", "watch", "at risk"]},
346
- "Which incident scope?": {"type": "choice", "options": ["single_account", "multi_account", "platform_wide"]},
347
- "Security concern present?": {"type": "bool"},
348
- "Duplicate charge reported?": {"type": "bool"},
349
- "Churn likelihood level?": {"type": "scale", "legend": {"0": "none", "1": "low", "2": "medium", "3": "high"}},
350
- "Human attention needed?": {"type": "bool"},
351
- "Immediate feature request?": {"type": "bool"},
352
- }
353
- ctx = ("Hi, since this morning our Stripe webhook integration stopped firing and our checkout is down for all customers. "
354
- "We are losing orders every minute and our partner launch is on Thursday. Also I think we got billed twice last week. "
355
- "If this is not fixed today we will have to look at other providers.")
356
- t = time.time(); js = d.decide_json(ctx, schema); dt = time.time() - t
357
- print(f"\n>> {ctx[:80]}...\n" + json.dumps(js, indent=1)[:3000]); print(f"{len(schema)} typed fields in {dt*1000:.0f} ms (one forward pass)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/metrics.py DELETED
@@ -1,37 +0,0 @@
1
- import numpy as np
2
-
3
-
4
- def ece(conf, correct, bins=15):
5
- conf = np.asarray(conf); correct = np.asarray(correct, dtype=float)
6
- edges = np.linspace(0, 1, bins + 1); e = 0.0
7
- for lo, hi in zip(edges[:-1], edges[1:]):
8
- m = (conf > lo) & (conf <= hi)
9
- if m.any():
10
- e += m.mean() * abs(conf[m].mean() - correct[m].mean())
11
- return float(e)
12
-
13
-
14
- def aurc(conf, correct):
15
- """Area under risk-coverage curve (lower is better)."""
16
- order = np.argsort(-np.asarray(conf)); c = np.asarray(correct, dtype=float)[order]
17
- risk = np.cumsum(1 - c) / np.arange(1, len(c) + 1)
18
- return float(risk.mean())
19
-
20
-
21
- def sel_acc(conf, correct, coverage):
22
- order = np.argsort(-np.asarray(conf)); c = np.asarray(correct, dtype=float)[order]
23
- n = max(1, int(round(coverage * len(c))))
24
- return float(c[:n].mean())
25
-
26
-
27
- def summarize(probs, golds, nopts):
28
- """probs [N,K] (masked entries 0), golds [N], nopts [N]."""
29
- probs = np.asarray(probs); golds = np.asarray(golds); nopts = np.asarray(nopts)
30
- pred = probs.argmax(1); conf = probs.max(1); correct = (pred == golds)
31
- p_gold = probs[np.arange(len(golds)), golds]
32
- nll = -np.log(np.clip(p_gold, 1e-12, 1)).mean()
33
- onehot = np.zeros_like(probs); onehot[np.arange(len(golds)), golds] = 1
34
- brier = ((probs - onehot) ** 2).sum(1).mean()
35
- return dict(n=int(len(golds)), acc=float(correct.mean()), nll=float(nll), brier=float(brier), ece=ece(conf, correct),
36
- aurc=aurc(conf, correct), acc_at_80=sel_acc(conf, correct, 0.8), acc_at_50=sel_acc(conf, correct, 0.5),
37
- chance=float((1.0 / nopts).mean()), mean_conf=float(conf.mean()))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/model.py DELETED
@@ -1,49 +0,0 @@
1
- """Backbone -> slot hidden states -> restricted logits over option letters."""
2
- import torch, torch.nn as nn, torch.nn.functional as F
3
- from transformers import AutoModelForCausalLM, AutoTokenizer
4
- from decider.prompt import letter_ids, MAX_OPTIONS
5
-
6
-
7
- class DecisionModel(nn.Module):
8
- def __init__(self, name, dtype=torch.bfloat16, grad_ckpt=True):
9
- super().__init__()
10
- if hasattr(torch.backends.cuda, "enable_cudnn_sdp"): # as decider.engine.set_attention_backend_policy: the cuDNN SDPA
11
- torch.backends.cuda.enable_cudnn_sdp(False) # backend is wrong for masked attention on Blackwell (torch 2.14)
12
- self.tok = AutoTokenizer.from_pretrained(name)
13
- self.lm = AutoModelForCausalLM.from_pretrained(name, dtype=dtype)
14
- if grad_ckpt:
15
- self.lm.gradient_checkpointing_enable()
16
- self.register_buffer("letters", torch.tensor(letter_ids(self.tok)), persistent=False)
17
-
18
- def slot_logits(self, input_ids, attention_mask, slot_idx, slot_batch, nopts):
19
- """input_ids [B,T]; slot_idx/slot_batch [N] flat slot positions; nopts [N].
20
- Returns [N, MAX_OPTIONS] logits with invalid options masked to -inf."""
21
- h = self.lm.model(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state
22
- hs = h[slot_batch, slot_idx] # [N,H]
23
- W = self.lm.lm_head.weight[self.letters] # [K,H]
24
- logits = F.linear(hs, W).float() # [N,K]
25
- ar = torch.arange(MAX_OPTIONS, device=logits.device)[None, :]
26
- logits = logits.masked_fill(ar >= nopts[:, None], float("-inf"))
27
- return logits
28
-
29
- def forward(self, batch):
30
- return self.slot_logits(batch["input_ids"], batch["attention_mask"], batch["slot_idx"], batch["slot_batch"], batch["nopts"])
31
-
32
-
33
- def collate(items, pad_id):
34
- """items: list of dicts from prompt.build (+ 'task', 'ex_id'). Right-pad."""
35
- T = max(len(it["ids"]) for it in items)
36
- T = ((T + 63) // 64) * 64 # few distinct shapes -> fewer kernel (re)compiles
37
- B = len(items)
38
- input_ids = torch.full((B, T), pad_id, dtype=torch.long)
39
- attn = torch.zeros((B, T), dtype=torch.long)
40
- slot_idx, slot_batch, golds, nopts, tasks, qidx = [], [], [], [], [], []
41
- for b, it in enumerate(items):
42
- n = len(it["ids"])
43
- input_ids[b, :n] = torch.tensor(it["ids"])
44
- attn[b, :n] = 1
45
- for k, s in enumerate(it["slots"]):
46
- slot_idx.append(s); slot_batch.append(b); golds.append(it["golds"][k]); nopts.append(it["nopts"][k])
47
- tasks.append(it.get("task", "")); qidx.append(k)
48
- return dict(input_ids=input_ids, attention_mask=attn, slot_idx=torch.tensor(slot_idx), slot_batch=torch.tensor(slot_batch),
49
- golds=torch.tensor(golds), nopts=torch.tensor(nopts), tasks=tasks, qidx=qidx)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/mps_moe.py DELETED
@@ -1,102 +0,0 @@
1
- """Two MPS slow paths in transformers' Qwen3.5-MoE reference code, replaced in-process (decider-35b-a3b on Apple Silicon).
2
-
3
- Contributed by @nassersala in issue #6 under the project's Apache-2.0 licence; adapted for the package (out-of-range expert
4
- ids are dropped as `histc` drops them, and the replacements are installed from `decider.mps_ops.patch_mps`).
5
-
6
- On an M-series Mac (torch 2.14, transformers 5.17) `torch.histc` takes about 45 ms a call on MPS (once per MoE layer, to
7
- count tokens per expert) and `torch.linalg.solve_triangular` about 17 ms (twice per linear-attention layer): most of a
8
- 2.8-4 s decision. With both replaced the 35B takes 0.23-0.5 s a decision for typical inputs (reported in issue #6).
9
- The `histc` replacement is an exact count. The block inverse is within 1e-6 of the MPS solver on real systems; on six items
10
- the probabilities moved at most 0.033, less than switching to the exact CPU solver does (0.042), and no argmax changed.
11
-
12
- The replacements are visible only inside those two transformers modules (install() gives each its own view of `torch`), and
13
- there they act only on MPS tensors of the matching call shape; everything else goes to torch.
14
- """
15
- import torch
16
-
17
- _histc, _solve = torch.histc, torch.linalg.solve_triangular
18
-
19
-
20
- def count_ids(x, bins, min=0):
21
- """histc for integer-valued ids binned one per bin: a count of ids min..min+bins-1; other values are dropped as histc
22
- drops them (transformers passes expert-parallel sentinels >= num_experts and relies on that)."""
23
- idx = x.long() - int(min)
24
- keep = (idx >= 0) & (idx < bins)
25
- out = torch.zeros(bins, device=x.device, dtype=x.dtype)
26
- return out.scatter_add_(0, idx.clamp(0, bins - 1), keep.to(x.dtype))
27
-
28
-
29
- def histc(input, bins=100, min=0, max=0, *, out=None):
30
- # expert ids 0..n-1 into n bins over [0, n-1]: bin k holds id k exactly, so this is a count (histc: ~45 ms on MPS)
31
- if input.device.type == "mps" and input.dim() == 1 and out is None and min == 0 and max > 0 and bins == max - min + 1:
32
- return count_ids(input, bins, min)
33
- return _histc(input, bins=bins, min=min, max=max) if out is None else _histc(input, bins=bins, min=min, max=max, out=out)
34
-
35
-
36
- def unit_lower_inverse(A):
37
- """Inverse of unit lower-triangular A (last dim a power of two) by block doubling:
38
- [[A11, 0], [A21, A22]]^-1 = [[X11, 0], [-X22 A21 X11, X22]]."""
39
- n = A.shape[-1]
40
- inv = torch.ones(*A.shape[:-2], n, 1, 1, device=A.device, dtype=A.dtype) # n diagonal 1x1 blocks
41
- b = 1
42
- while b < n:
43
- nb = n // (2 * b)
44
- blocks = A.reshape(*A.shape[:-2], nb, 2 * b, nb, 2 * b).diagonal(dim1=-4, dim2=-2).movedim(-1, -3)
45
- x11, x22, a21 = inv[..., 0::2, :, :], inv[..., 1::2, :, :], blocks[..., b:, :b]
46
- new = torch.zeros(*A.shape[:-2], nb, 2 * b, 2 * b, device=A.device, dtype=A.dtype)
47
- new[..., :b, :b], new[..., b:, b:], new[..., b:, :b] = x11, x22, -(x22 @ a21 @ x11)
48
- inv, b = new, 2 * b
49
- return inv[..., 0, :, :]
50
-
51
-
52
- def solve_unit_lower(A, B):
53
- """solve_triangular(A, B, upper=False, unitriangular=True): only the strict lower triangle of A is read."""
54
- n = A.shape[-1]
55
- return unit_lower_inverse(A.tril(-1) + torch.eye(n, device=A.device, dtype=A.dtype)) @ B
56
-
57
-
58
- def solve_triangular(A, B, *, upper, left=True, unitriangular=False, out=None):
59
- # unit lower-triangular: invert by block doubling, all batched matmuls (the MPS solver: ~17 ms per call)
60
- n = A.shape[-1]
61
- if A.device.type == "mps" and not upper and left and unitriangular and out is None and n > 0 and n & (n - 1) == 0:
62
- return solve_unit_lower(A, B)
63
- return _solve(A, B, upper=upper, left=left, unitriangular=unitriangular, out=out)
64
-
65
-
66
- class _Proxy:
67
- """A module's view of `torch` (or `torch.linalg`) with some attributes replaced; everything else is the real one."""
68
- def __init__(self, real, **over):
69
- self._real, self._over = real, over
70
-
71
- def __getattr__(self, name):
72
- return self._over[name] if name in self._over else getattr(self._real, name)
73
-
74
-
75
- _installed = {}
76
-
77
-
78
- def install():
79
- """Replace the two operations inside the two transformers modules that call them, and nowhere else: the grouped-MoE
80
- expert count in `transformers.integrations.moe` and the gated delta rule in `modeling_qwen3_5_moe` see a `torch` whose
81
- `histc` / `linalg.solve_triangular` are the replacements; every other caller keeps torch's. -> names patched."""
82
- import importlib
83
- targets = {"transformers.integrations.moe": dict(histc=histc),
84
- "transformers.models.qwen3_5_moe.modeling_qwen3_5_moe": dict(linalg=_Proxy(torch.linalg, solve_triangular=solve_triangular))}
85
- for name, over in targets.items():
86
- if name in _installed:
87
- continue
88
- try:
89
- mod = importlib.import_module(name)
90
- except ImportError:
91
- continue
92
- if getattr(mod, "torch", None) is not torch:
93
- continue # not the layout this was written for: leave it alone
94
- _installed[name] = mod
95
- mod.torch = _Proxy(torch, **over)
96
- return sorted(_installed)
97
-
98
-
99
- def uninstall():
100
- for name, mod in list(_installed.items()):
101
- mod.torch = torch
102
- del _installed[name]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/mps_ops.py DELETED
@@ -1,268 +0,0 @@
1
- """MPS kernels for Qwen3.5 gated-delta attention.
2
-
3
- The optimization is measured against Transformers' PyTorch reference fallback for
4
- this kernel; it is not an end-to-end model speedup claim.
5
-
6
- The gated-delta and L2-normalization code follows Transformers 5.17's
7
- ``modeling_qwen3_5.py`` under the Apache License 2.0. The MPS inversion and
8
- backend dispatch are Decider additions.
9
- """
10
- import functools
11
- import inspect
12
- import logging
13
- import warnings
14
-
15
- import torch, torch.nn.functional as F
16
- from decider.engine import fused_causal_conv1d_fn
17
-
18
-
19
- # Adapted from transformers 5.17.0's modeling_qwen3_5.py (Apache-2.0).
20
- # The original license applies to the gated-delta and normalization portions;
21
- # the MPS inversion and dispatch below are Decider additions.
22
- # Native Metal Shading Language (MSL) JIT kernel via MLX/Metal
23
- _metal_invert_kernel = None
24
- _metal_failure_reported = False
25
- _compat_warning_reported = False
26
- try:
27
- import mlx.core as mx
28
- import mlx.core.fast as fast
29
-
30
- _METAL_INVERT_SRC = """
31
- uint col = thread_position_in_threadgroup.x;
32
- uint mat_idx = threadgroup_position_in_grid.x;
33
- threadgroup float s_inv[4096];
34
-
35
- for (uint r = 0; r < 64; ++r) {
36
- s_inv[r * 64 + col] = (r == col) ? 1.0f : 0.0f;
37
- }
38
- threadgroup_barrier(mem_flags::mem_threadgroup);
39
-
40
- uint mat_offset = mat_idx * 4096;
41
- for (uint r = 1; r < 64; ++r) {
42
- float sum = 0.0f;
43
- if (col < r) {
44
- for (uint k = col; k < r; ++k) {
45
- sum += L[mat_offset + r * 64 + k] * s_inv[k * 64 + col];
46
- }
47
- }
48
- threadgroup_barrier(mem_flags::mem_threadgroup);
49
- if (col < r) {
50
- s_inv[r * 64 + col] = -sum;
51
- }
52
- threadgroup_barrier(mem_flags::mem_threadgroup);
53
- }
54
- for (uint r = 0; r < 64; ++r) {
55
- out_inv[mat_offset + r * 64 + col] = s_inv[r * 64 + col];
56
- }
57
- """
58
- _metal_invert_kernel = fast.metal_kernel(
59
- name="invert_unitriangular_64",
60
- input_names=["L"],
61
- output_names=["out_inv"],
62
- source=_METAL_INVERT_SRC,
63
- )
64
- except (ImportError, OSError, RuntimeError, AttributeError):
65
- logging.getLogger(__name__).debug("MLX Metal kernel unavailable; using PyTorch MPS fallback", exc_info=True)
66
- _metal_invert_kernel = None
67
-
68
-
69
- def l2norm(x: torch.Tensor, dim: int = -1, eps: float = 1e-6) -> torch.Tensor:
70
- return x * torch.rsqrt((x * x).sum(dim=dim, keepdim=True) + eps)
71
-
72
-
73
- def fast_invert_unitriangular_64(L: torch.Tensor) -> torch.Tensor:
74
- """Invert batches of 64x64 unit lower-triangular matrices."""
75
- global _metal_invert_kernel, _metal_failure_reported
76
- if _metal_invert_kernel is not None and L.is_mps and not L.requires_grad:
77
- assert L.dtype == torch.float32, "Metal inversion requires float32 input"
78
- try:
79
- orig_shape = L.shape
80
- L_flat = L.reshape(-1, 64, 64).contiguous()
81
- num_m = L_flat.shape[0]
82
- torch.mps.synchronize()
83
- L_mx = mx.from_dlpack(torch.to_dlpack(L_flat))
84
- mx.eval(L_mx)
85
- out_mx = _metal_invert_kernel(
86
- inputs=[L_mx],
87
- grid=(num_m * 64, 1, 1),
88
- threadgroup=(64, 1, 1),
89
- output_shapes=[(num_m, 64, 64)],
90
- output_dtypes=[mx.float32],
91
- )[0]
92
- mx.eval(out_mx)
93
- torch.mps.synchronize()
94
- return torch.from_dlpack(out_mx).to(L.device).reshape(orig_shape)
95
- except Exception:
96
- # Optional acceleration must not turn a recoverable backend issue into
97
- # a failed request; disable the failing kernel for this process.
98
- _metal_invert_kernel = None
99
- if not _metal_failure_reported:
100
- logging.getLogger(__name__).warning("MLX Metal inversion failed; using the PyTorch MPS fallback", exc_info=True)
101
- _metal_failure_reported = True
102
-
103
- N = 64
104
- inv = torch.eye(N, device=L.device, dtype=L.dtype).expand_as(L).clone()
105
-
106
- # b = 1: 32 blocks of 2x2. For [[1, 0], [l, 1]], inverse is [[1, 0], [-l, 1]]
107
- idx_row = torch.arange(1, 64, 2, device=L.device)
108
- idx_col = torch.arange(0, 64, 2, device=L.device)
109
- inv[..., idx_row, idx_col] = -L[..., idx_row, idx_col]
110
-
111
- # b = 2: 16 blocks of 4x4
112
- for j in range(16):
113
- r1, r2, r3 = j * 4, j * 4 + 2, j * 4 + 4
114
- inv[..., r2:r3, r1:r2] = -inv[..., r2:r3, r2:r3] @ L[..., r2:r3, r1:r2] @ inv[..., r1:r2, r1:r2]
115
-
116
- # b = 4: 8 blocks of 8x8
117
- for j in range(8):
118
- r1, r2, r3 = j * 8, j * 8 + 4, j * 8 + 8
119
- inv[..., r2:r3, r1:r2] = -inv[..., r2:r3, r2:r3] @ L[..., r2:r3, r1:r2] @ inv[..., r1:r2, r1:r2]
120
-
121
- # b = 8: 4 blocks of 16x16
122
- for j in range(4):
123
- r1, r2, r3 = j * 16, j * 16 + 8, j * 16 + 16
124
- inv[..., r2:r3, r1:r2] = -inv[..., r2:r3, r2:r3] @ L[..., r2:r3, r1:r2] @ inv[..., r1:r2, r1:r2]
125
-
126
- # b = 16: 2 blocks of 32x32
127
- for j in range(2):
128
- r1, r2, r3 = j * 32, j * 32 + 16, j * 32 + 32
129
- inv[..., r2:r3, r1:r2] = -inv[..., r2:r3, r2:r3] @ L[..., r2:r3, r1:r2] @ inv[..., r1:r2, r1:r2]
130
-
131
- # b = 32: 1 block of 64x64
132
- inv[..., 32:64, 0:32] = -inv[..., 32:64, 32:64] @ L[..., 32:64, 0:32] @ inv[..., 0:32, 0:32]
133
- return inv
134
-
135
-
136
- def mps_chunk_gated_delta_rule(
137
- query: torch.Tensor,
138
- key: torch.Tensor,
139
- value: torch.Tensor,
140
- g: torch.Tensor,
141
- beta: torch.Tensor,
142
- chunk_size: int = 64,
143
- initial_state: torch.Tensor | None = None,
144
- output_final_state: bool = False,
145
- use_qk_l2norm_in_kernel: bool = False,
146
- **kwargs,
147
- ) -> tuple[torch.Tensor, torch.Tensor | None]:
148
- """Optimized chunk-gated delta rule for Apple Silicon (MPS)."""
149
- assert chunk_size == 64, f"MPS optimized delta rule requires chunk_size=64, got {chunk_size}"
150
- initial_dtype = query.dtype
151
- batch_size, sequence_length, _, k_head_dim = key.shape
152
- num_v_heads, v_head_dim = value.shape[-2:]
153
- recurrent_state_shape = (batch_size, num_v_heads, k_head_dim, v_head_dim)
154
- padded_output_shape = (batch_size, num_v_heads, -1, v_head_dim)
155
- decay = g
156
-
157
- query, key, value, beta, decay = [
158
- x.transpose(1, 2).to(torch.float32, memory_format=torch.contiguous_format)
159
- for x in (query, key, value, beta, decay)
160
- ]
161
- if use_qk_l2norm_in_kernel:
162
- query = l2norm(query, dim=-1, eps=1e-6)
163
- key = l2norm(key, dim=-1, eps=1e-6)
164
- scaling = query.shape[-1] ** -0.5
165
- query = query * scaling
166
-
167
- pad_size = (chunk_size - sequence_length % chunk_size) % chunk_size
168
- query = F.pad(query, (0, 0, 0, pad_size))
169
- key = F.pad(key, (0, 0, 0, pad_size))
170
- value = F.pad(value, (0, 0, 0, pad_size))
171
- beta = F.pad(beta, (0, pad_size))
172
- decay = F.pad(decay, (0, pad_size))
173
-
174
- v_beta = value * beta.unsqueeze(-1)
175
- k_beta = key * beta.unsqueeze(-1)
176
-
177
- query, key, k_beta, v_beta = [
178
- x.reshape(x.shape[0], x.shape[1], -1, chunk_size, x.shape[-1])
179
- for x in (query, key, k_beta, v_beta)
180
- ]
181
- decay = decay.reshape(decay.shape[0], decay.shape[1], -1, chunk_size)
182
-
183
- strictly_upper_mask = torch.ones(chunk_size, chunk_size, dtype=torch.bool, device=query.device).triu(1)
184
- cum_decay = decay.cumsum(dim=3)
185
- pairwise_decay = (cum_decay.unsqueeze(4) - cum_decay.unsqueeze(3)).masked_fill(strictly_upper_mask, float("-inf")).exp()
186
-
187
- ut_system = (k_beta @ key.transpose(-1, -2)) * pairwise_decay
188
- intra_chunk_attn = (query @ key.transpose(-1, -2)) * pairwise_decay
189
- decayed_k_beta = k_beta * cum_decay.exp().unsqueeze(-1)
190
-
191
- # Fast block divide-and-conquer unitriangular inverse on MPS
192
- L = ut_system.tril(-1)
193
- inv = fast_invert_unitriangular_64(L)
194
- new_values = inv @ v_beta
195
- k_cumdecay = inv @ decayed_k_beta
196
-
197
- if initial_state is None:
198
- last_recurrent_state = torch.zeros(recurrent_state_shape, dtype=new_values.dtype, device=new_values.device)
199
- else:
200
- last_recurrent_state = initial_state.to(new_values)
201
- core_attn_out = torch.zeros_like(new_values)
202
-
203
- query = query * cum_decay.exp().unsqueeze(-1)
204
- key = key * (cum_decay[..., -1:] - cum_decay).exp().unsqueeze(-1)
205
- chunk_decay = cum_decay[..., -1].exp()[..., None, None]
206
-
207
- num_chunks = query.shape[2]
208
- qk = torch.cat([query, k_cumdecay], dim=3)
209
- kt = key.transpose(-1, -2)
210
-
211
- for i in range(num_chunks):
212
- qk_state = qk[:, :, i] @ last_recurrent_state
213
- inter_chunk_attn = qk_state[:, :, :chunk_size]
214
- v_new = new_values[:, :, i] - qk_state[:, :, chunk_size:]
215
- core_attn_out[:, :, i] = inter_chunk_attn + intra_chunk_attn[:, :, i] @ v_new
216
- last_recurrent_state = last_recurrent_state * chunk_decay[:, :, i] + kt[:, :, i] @ v_new
217
-
218
- last_recurrent_state = None if not output_final_state else last_recurrent_state
219
- core_attn_out = core_attn_out.reshape(padded_output_shape)[:, :, :sequence_length]
220
- core_attn_out = core_attn_out.transpose(1, 2).to(initial_dtype, memory_format=torch.contiguous_format)
221
- return core_attn_out, last_recurrent_state
222
-
223
-
224
- def patch_mps():
225
- """Patch Qwen3.5 operations on MPS tensors while preserving other backends."""
226
- global _compat_warning_reported
227
- if not torch.backends.mps.is_available():
228
- return False
229
- import os
230
- if os.environ.get("DECIDER_MPS_MOE_PATCH", "1") != "0": # histc + unit-triangular solve replacements (issue #6, @nassersala)
231
- from decider import mps_moe
232
- mps_moe.install()
233
- try:
234
- import transformers
235
- import transformers.models.qwen3_5.modeling_qwen3_5 as mq
236
- version = tuple(int(part) for part in transformers.__version__.split(".")[:2])
237
- if not all(callable(getattr(mq, name, None)) for name in ("torch_chunk_gated_delta_rule", "causal_conv1d_fn")):
238
- raise AttributeError("unsupported Transformers Qwen3.5 implementation")
239
- params = inspect.signature(mq.torch_chunk_gated_delta_rule).parameters
240
- required = {"query", "key", "value", "g", "beta", "chunk_size"}
241
- if version < (5, 17) or not required.issubset(params):
242
- if not _compat_warning_reported:
243
- warnings.warn(f"MPS patch requires Transformers >=5.17 with the Qwen3.5 gated-delta signature; got {transformers.__version__}", RuntimeWarning, stacklevel=2)
244
- _compat_warning_reported = True
245
- return False
246
- if getattr(mq.torch_chunk_gated_delta_rule, "_decider_mps_patch", False):
247
- return True
248
-
249
- original_delta, original_conv = mq.torch_chunk_gated_delta_rule, mq.causal_conv1d_fn
250
- delta_params = tuple(inspect.signature(original_delta).parameters)
251
- query_index, chunk_index = delta_params.index("query"), delta_params.index("chunk_size")
252
-
253
- @functools.wraps(original_delta)
254
- def delta(*args, **kwargs):
255
- query = args[query_index] if len(args) > query_index else kwargs["query"]
256
- chunk_size = args[chunk_index] if len(args) > chunk_index else kwargs.get("chunk_size", 64)
257
- return mps_chunk_gated_delta_rule(*args, **kwargs) if query.is_mps and chunk_size == 64 else original_delta(*args, **kwargs)
258
-
259
- @functools.wraps(original_conv)
260
- def conv(hidden_states, *args, **kwargs):
261
- return fused_causal_conv1d_fn(hidden_states, *args, **kwargs) if hidden_states.is_mps else original_conv(hidden_states, *args, **kwargs)
262
-
263
- delta._decider_mps_patch = True
264
- mq.torch_chunk_gated_delta_rule, mq.causal_conv1d_fn = delta, conv
265
- return True
266
- except (ImportError, AttributeError, TypeError, ValueError):
267
- logging.getLogger(__name__).debug("MPS patch unavailable; using Transformers fallback", exc_info=True)
268
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/prompt.py DELETED
@@ -1,287 +0,0 @@
1
- """Prompt construction. One context, N typed questions, N answer slots.
2
-
3
- All N decisions are read from a single forward pass: the logits at each
4
- "Answer k: (" slot are restricted to the option-letter tokens. No answer
5
- letters are ever inserted, so slot k sees the context and all questions but
6
- no earlier answers (the decisions are conditionally independent given input).
7
- """
8
- import random
9
-
10
- LETTERS = "ABCDEFGHIJ"
11
- NARROW = len(LETTERS) # <= NARROW options: the original "(A) .. (J)" rendering, tokenized as a string (unchanged since v1)
12
- MAX_OPTIONS = 255 # width of the label head. > NARROW options: "wide" rendering, one label token per option:
13
- # A..Z then the first 229 two-letter upper-case strings that are single tokens (AA, AB, ...)
14
- ABSTAIN_PREFIXES = ("none of the above", "none of these", "not listed", "no suitable", "does not apply", "cannot tell")
15
- ABSTAIN_EXACT = ("other", "unsure", "something else", "neither of these", "other / not covered")
16
-
17
-
18
- def is_abstain_option(o):
19
- o = o.strip().lower()
20
- return o.startswith(ABSTAIN_PREFIXES) or o in ABSTAIN_EXACT
21
-
22
-
23
- _LABELS = {}
24
- _OPT_CACHE = {}
25
-
26
-
27
- def _enc_opt(tok, text):
28
- """Token ids of ") <option text>" (cached: fixed label sets repeat the same strings millions of times)."""
29
- key = (id(tok), text)
30
- v = _OPT_CACHE.get(key)
31
- if v is None:
32
- v = tok.encode(f") {text}", add_special_tokens=False)
33
- if len(_OPT_CACHE) < 2_000_000:
34
- _OPT_CACHE[key] = v
35
- return v
36
-
37
-
38
- def label_table(tok):
39
- """(label strings, label token ids), MAX_OPTIONS entries; the first NARROW are A..J so narrow questions are unchanged."""
40
- key = id(tok)
41
- if key not in _LABELS:
42
- import string
43
- U = string.ascii_uppercase
44
- names = list(U) + [a + b for a in U for b in U]
45
- out = []
46
- for n in names:
47
- t = tok.encode(n, add_special_tokens=False)
48
- if len(t) == 1:
49
- out.append((n, t[0]))
50
- if len(out) == MAX_OPTIONS:
51
- break
52
- assert len(out) == MAX_OPTIONS and len({i for _, i in out}) == MAX_OPTIONS
53
- _LABELS[key] = ([n for n, _ in out], [i for _, i in out],
54
- tok.encode("\n(", add_special_tokens=False))
55
- return _LABELS[key]
56
-
57
-
58
- def _select(q, rng, max_options):
59
- opts = list(range(len(q.options)))
60
- if len(opts) > max_options:
61
- # always keep the gold and any abstain-style option (its mere presence must not carry information)
62
- forced = {q.gold} | {i for i, o in enumerate(q.options) if is_abstain_option(o)}
63
- others = [i for i in opts if i not in forced]
64
- opts = rng.sample(others, max_options - len(forced)) + list(forced)
65
- rng.shuffle(opts)
66
- return opts
67
-
68
-
69
- def _options_ids(tok, q, opts):
70
- if len(opts) <= NARROW:
71
- return tok.encode("".join(f"\n({LETTERS[j]}) {q.options[oi]}" for j, oi in enumerate(opts)), add_special_tokens=False)
72
- _, lab_ids, open_ids = label_table(tok); out = []
73
- for j, oi in enumerate(opts):
74
- out += open_ids + [lab_ids[j]] + _enc_opt(tok, q.options[oi])
75
- return out
76
-
77
-
78
- def build_schema_first(example, tok, rng=None, max_options=NARROW, max_ctx_tokens=1536, chat=None):
79
- """Schema-first layout: all question/option blocks, then the context, then one answer slot per question.
80
-
81
- Question 1: ...\nOptions:\n(A) ... <- prefix: depends only on the questions, so its cache (attention KV and
82
- \n\nQuestion 2: ... delta-net states) is computed once per schema and reused for every state
83
- \n\nContext:\n<state>\n\nAnswer 1: (\nAnswer 2: (
84
-
85
- The three parts are tokenized separately, so `ids[:prefix_len]` is identical for every state.
86
- chat: a ChatTemplate for a chat-layout model (see `build_chat`); None renders the plain layout above."""
87
- rng = rng or random
88
- perms = [_select(q, rng, max_options) for q in example.qs]
89
- pre = schema_prefix_ids(tok, example.qs, perms, chat=chat)
90
- suf, slots = schema_suffix_ids(tok, example.context, len(example.qs), max_ctx_tokens, chat=chat)
91
- return dict(ids=pre + suf, slots=[len(pre) + s for s in slots], golds=[p.index(q.gold) if q.gold in p else -1 for p, q in zip(perms, example.qs)],
92
- nopts=[len(p) for p in perms], perms=perms, prefix_len=len(pre))
93
-
94
-
95
- def schema_prefix_ids(tok, qs, perms=None, chat=None):
96
- """Token ids of the question/option blocks (the cacheable part of the schema-first layout).
97
- chat: a ChatTemplate; the prefix then starts with the template head (the user turn opens before the first question)."""
98
- multi = len(qs) > 1; pre = [] if chat is None else list(chat.head)
99
- for k, q in enumerate(qs):
100
- opts = perms[k] if perms is not None else list(range(len(q.options)))
101
- pre += tok.encode(f"{chr(10) * 2 if k else ''}Question{' ' + str(k + 1) if multi else ''}: {q.text}\nOptions:", add_special_tokens=False) + _options_ids(tok, q, opts)
102
- return pre
103
-
104
-
105
- def schema_suffix_ids(tok, context, n_q, max_ctx_tokens=1536, chat=None):
106
- """Token ids after the schema prefix: the context and one answer slot per question. Returns (ids, slot positions in ids).
107
- chat: a ChatTemplate; the context then ends the user turn, and the template tail and the answer pieces follow."""
108
- ids = tok.encode("\n\nContext:\n", add_special_tokens=False) + tok.encode(context, add_special_tokens=False)[:max_ctx_tokens]; slots = []
109
- if chat is not None:
110
- ids += chat.tail
111
- for k in range(n_q):
112
- ids += chat.answer_ids(tok, k, n_q > 1); slots.append(len(ids) - 1)
113
- return ids, slots
114
- for k in range(n_q):
115
- ids += tok.encode(f"{chr(10) * 2 if k == 0 else chr(10)}Answer{' ' + str(k + 1) if n_q > 1 else ''}: (", add_special_tokens=False); slots.append(len(ids) - 1)
116
- return ids, slots
117
-
118
-
119
- def build(example, tok, rng=None, max_options=NARROW, max_ctx_tokens=1536, layout="state_first", chat=None):
120
- """Returns dict(ids=list[int], slots=list[int], golds=list[int], nopts=list[int], perms=list[list[int]]).
121
- chat: a ChatTemplate (chat_template(tok)) for a model trained in the chat layout (decider_config.json "layout": "chat");
122
- None, the default, is the plain layout every earlier model uses, unchanged."""
123
- if layout == "schema_first":
124
- return build_schema_first(example, tok, rng, max_options, max_ctx_tokens, chat=chat)
125
- if chat is not None:
126
- return build_chat(example, tok, chat, rng, max_options, max_ctx_tokens)
127
- rng = rng or random
128
- ctx_ids = tok.encode("Context:\n" + example.context, add_special_tokens=False)[:max_ctx_tokens]
129
- ids = list(ctx_ids)
130
- slots, golds, nopts, perms = [], [], [], []
131
- multi = len(example.qs) > 1
132
- for k, q in enumerate(example.qs):
133
- opts = list(range(len(q.options)))
134
- if len(opts) > max_options:
135
- # always keep the gold and any abstain-style option (its mere presence must not carry information)
136
- forced = {q.gold} | {i for i, o in enumerate(q.options) if is_abstain_option(o)}
137
- others = [i for i in opts if i not in forced]
138
- keep = rng.sample(others, max_options - len(forced)) + list(forced)
139
- opts = keep
140
- rng.shuffle(opts)
141
- head = f"\n\nQuestion{' ' + str(k + 1) if multi else ''}: {q.text}\nOptions:"
142
- tail = f"\nAnswer{' ' + str(k + 1) if multi else ''}: ("
143
- if len(opts) <= NARROW:
144
- lines = [head] + [f"\n({LETTERS[j]}) {q.options[oi]}" for j, oi in enumerate(opts)] + [tail]
145
- piece = tok.encode("".join(lines), add_special_tokens=False)
146
- else: # wide: "\n(" + <label token> + ") text", built from ids so every label is one token
147
- _, lab_ids, open_ids = label_table(tok)
148
- piece = tok.encode(head, add_special_tokens=False)
149
- for j, oi in enumerate(opts):
150
- piece += open_ids + [lab_ids[j]] + _enc_opt(tok, q.options[oi])
151
- piece += tok.encode(tail, add_special_tokens=False)
152
- ids.extend(piece)
153
- slots.append(len(ids) - 1) # position of " (" token
154
- golds.append(opts.index(q.gold) if q.gold in opts else -1)
155
- nopts.append(len(opts))
156
- perms.append(opts)
157
- return dict(ids=ids, slots=slots, golds=golds, nopts=nopts, perms=perms)
158
-
159
-
160
- # ---- chat layout (1.2.0) ----------------------------------------------------------------------------------------------
161
- # A model whose decider_config.json has "layout": "chat" (or "chat_template": true) was trained with every prompt wrapped
162
- # in its tokenizer's chat template: one user turn holding the plain content, thinking switched off, and the answer pieces in
163
- # the assistant turn. For Qwen3.5, state-first:
164
- #
165
- # <|im_start|>user\n Context:\n<state> \n\nQuestion: <q>\nOptions:\n(A) ..\n(B) ..
166
- # <|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n Answer: (
167
- #
168
- # With several questions in one row all question blocks come first and the answer pieces ("Answer 1: (", "\nAnswer 2: (",
169
- # ...) follow the template tail. Schema-first: the user turn holds the question blocks and then "\n\nContext:\n<state>".
170
- # Every part is tokenized on its own (head, context, each question header, each option block, tail, each answer piece);
171
- # the letter is read at the final " (" token as in the plain layout.
172
- LAYOUTS = ("plain", "chat")
173
-
174
-
175
- def resolve_layout(cfg):
176
- """The prompt layout a decider_config.json names: "plain" (no "layout" key and no "chat_template": true, which is every
177
- released model) or "chat". Raises ValueError for any other value, so a model trained in a layout
178
- this version does not know is refused instead of being read in the wrong one."""
179
- cfg = cfg or {}
180
- layout = cfg.get("layout")
181
- if layout is None:
182
- layout = "chat" if cfg.get("chat_template") is True else "plain"
183
- if layout not in LAYOUTS:
184
- raise ValueError(f"decider_config.json names the prompt layout {layout!r}; this version of decider-ai knows "
185
- f"{', '.join(repr(x) for x in LAYOUTS)}. Upgrade decider-ai, or check the model's config.")
186
- if layout == "plain" and cfg.get("chat_template") is True:
187
- raise ValueError('decider_config.json says "layout": "plain" and "chat_template": true; these contradict each other')
188
- return layout
189
-
190
-
191
- class ChatTemplate:
192
- """Token ids of the tokenizer's chat template around one user turn, and the answer pieces of the assistant turn.
193
-
194
- head/tail come from tok.apply_chat_template([user turn], add_generation_prompt=True) with thinking disabled
195
- (enable_thinking=False where the template knows the switch; a thinking block the template leaves open is closed here),
196
- with no system prompt. This is the computation of the chat-layout research server the chat models were trained against."""
197
- SENTINEL = "@@DECIDER_USER_CONTENT@@"
198
-
199
- def __init__(self, tok, answer="Answer"):
200
- if not getattr(tok, "chat_template", None):
201
- raise ValueError('the model is configured for the chat layout ("layout": "chat") but its tokenizer has no chat template')
202
- msgs = [{"role": "user", "content": self.SENTINEL}]
203
- kw = {"enable_thinking": False} if "enable_thinking" in tok.chat_template else {}
204
- s = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True, **kw)
205
- head, tail = s.split(self.SENTINEL)
206
- for opened, closed in (("<think>\n", "</think>\n\n"), ("<think>", "</think>\n\n"), ("<|channel>thought\n", "<channel|>")):
207
- if tail.endswith(opened):
208
- tail += closed
209
- self.head_text, self.tail_text = head, tail
210
- self.head = tok.encode(head, add_special_tokens=False)
211
- self.tail = tok.encode(tail, add_special_tokens=False)
212
- self.answer = answer
213
- names, ids, _ = label_table(tok)
214
- pre = tok.encode(f"{answer}: (", add_special_tokens=False)
215
- for n, i in list(zip(names, ids))[:NARROW]:
216
- if tok.encode(f"{answer}: ({n}", add_special_tokens=False) != pre + [i]:
217
- raise ValueError(f"chat layout: the label {n!r} does not tokenize as one token after {answer!r}: (")
218
- if tok.encode(tail + f"{answer}: (", add_special_tokens=False) != self.tail + pre:
219
- raise ValueError("chat layout: the chat template tail merges with the answer prefix")
220
-
221
- def answer_ids(self, tok, k, multi):
222
- """Token ids of answer piece k: "Answer: (" (or "Answer k: (" with several questions), later pieces after a newline."""
223
- return tok.encode(f"{'' if k == 0 else chr(10)}{self.answer}{' ' + str(k + 1) if multi else ''}: (", add_special_tokens=False)
224
-
225
-
226
- _CHAT = {}
227
-
228
-
229
- def chat_template(tok):
230
- """The ChatTemplate of a tokenizer, cached per tokenizer object."""
231
- key = id(tok)
232
- if key not in _CHAT:
233
- _CHAT[key] = (tok, ChatTemplate(tok)) # keep tok alive so its id is not reused
234
- return _CHAT[key][1]
235
-
236
-
237
- def chat_for(tok, cfg):
238
- """The ChatTemplate to pass as `chat=` for a model with this decider_config.json, or None for a plain-layout model."""
239
- return chat_template(tok) if resolve_layout(cfg) == "chat" else None
240
-
241
-
242
- def load_decider_config(path):
243
- """decider_config.json of a model folder or a Hub repository id; {} when the model has none (a raw base model)."""
244
- import json, os
245
- if os.path.isdir(path):
246
- f = os.path.join(path, "decider_config.json")
247
- return json.load(open(f)) if os.path.exists(f) else {}
248
- try:
249
- from huggingface_hub import hf_hub_download
250
- return json.load(open(hf_hub_download(path, "decider_config.json")))
251
- except Exception:
252
- return {}
253
-
254
-
255
- def chat_for_model(path, tok):
256
- """chat_for(tok, the model's decider_config.json): what every script that builds prompts for `path` passes as `chat=`."""
257
- return chat_for(tok, load_decider_config(path))
258
-
259
-
260
- def build_chat(example, tok, chat, rng=None, max_options=NARROW, max_ctx_tokens=1536):
261
- """State-first chat layout (see the comment above LAYOUTS). Same return fields as `build`. max_ctx_tokens caps the
262
- tokens of "Context:\\n" + state, as in the plain layout; the template head and tail are not counted."""
263
- rng = rng or random
264
- qs = example.qs; multi = len(qs) > 1
265
- ids = list(chat.head) + tok.encode("Context:\n" + example.context, add_special_tokens=False)[:max_ctx_tokens]
266
- golds, nopts, perms = [], [], []
267
- for k, q in enumerate(qs):
268
- opts = _select(q, rng, max_options)
269
- ids += tok.encode(f"\n\nQuestion{' ' + str(k + 1) if multi else ''}: {q.text}\nOptions:", add_special_tokens=False) + _options_ids(tok, q, opts)
270
- golds.append(opts.index(q.gold) if q.gold in opts else -1); nopts.append(len(opts)); perms.append(opts)
271
- ids += chat.tail
272
- slots = []
273
- for k in range(len(qs)):
274
- ids += chat.answer_ids(tok, k, multi); slots.append(len(ids) - 1)
275
- return dict(ids=ids, slots=slots, golds=golds, nopts=nopts, perms=perms)
276
-
277
-
278
- def letter_ids(tok):
279
- ids = label_table(tok)[1]
280
- for j, L in enumerate(LETTERS):
281
- assert tok.encode(L, add_special_tokens=False) == [ids[j]], L
282
- return ids
283
-
284
-
285
- def render(example, tok, **kw):
286
- b = build(example, tok, **kw)
287
- return tok.decode(b["ids"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/prompt_fast.py DELETED
@@ -1,90 +0,0 @@
1
- """Row construction for the systemone path that tokenizes the state once per request instead of once per question.
2
-
3
- `prompt.build` encodes `"Context:\\n" + state` for every row it is called with, so an independent request with n
4
- questions tokenizes the whole state n times. The question block is encoded as its own string there and simply appended,
5
- so the ids are exactly `ctx_ids + question_piece`; this module reuses that fact and shares `ctx_ids` across the rows.
6
-
7
- `build_rows` returns the same item dicts as
8
- [prompt.build(Example(ctx, [Q(text, options, 0) for text, options in row]), tok, <no-shuffle rng>,
9
- max_options=MAX_OPTIONS, max_ctx_tokens=max_ctx_tokens) for row in rows]
10
- (ids, slots, golds, nopts, perms) and is checked against it in tests/test_prompt_fast.py.
11
- """
12
- from decider.prompt import LETTERS, NARROW, MAX_OPTIONS, label_table, _enc_opt, _options_ids
13
-
14
-
15
- def context_ids(tok, context, max_ctx_tokens=32768):
16
- return tok.encode("Context:\n" + context, add_special_tokens=False)[:max_ctx_tokens]
17
-
18
-
19
- def question_piece(tok, text, options, k=0, multi=False):
20
- """Token ids of one `\\n\\nQuestion...: <text>\\nOptions:...\\nAnswer...: (` block, independent of the state."""
21
- head = f"\n\nQuestion{' ' + str(k + 1) if multi else ''}: {text}\nOptions:"
22
- tail = f"\nAnswer{' ' + str(k + 1) if multi else ''}: ("
23
- if len(options) <= NARROW:
24
- return tok.encode(head + "".join(f"\n({LETTERS[j]}) {o}" for j, o in enumerate(options)) + tail,
25
- add_special_tokens=False)
26
- _, lab_ids, open_ids = label_table(tok)
27
- piece = tok.encode(head, add_special_tokens=False)
28
- for j, o in enumerate(options):
29
- piece += open_ids + [lab_ids[j]] + _enc_opt(tok, o)
30
- return piece + tok.encode(tail, add_special_tokens=False)
31
-
32
-
33
- class _Opts:
34
- def __init__(self, options): self.options = options
35
-
36
-
37
- def _chat_row(tok, chat, ctx, row):
38
- """One chat-layout row after the shared ids `ctx` (template head + context): every question block, the template tail,
39
- then the answer pieces. Equal to prompt.build_chat for the same row (tests/test_layout.py)."""
40
- multi = len(row) > 1
41
- ids = list(ctx)
42
- for k, (text, options) in enumerate(row):
43
- ids += tok.encode(f"\n\nQuestion{' ' + str(k + 1) if multi else ''}: {text}\nOptions:", add_special_tokens=False)
44
- ids += _options_ids(tok, _Opts(options), list(range(len(options))))
45
- ids += chat.tail
46
- slots = []
47
- for k in range(len(row)):
48
- ids += chat.answer_ids(tok, k, multi); slots.append(len(ids) - 1)
49
- return ids, slots
50
-
51
-
52
- def build_rows(tok, context, rows, max_ctx_tokens=32768, chat=None):
53
- """rows: list of rows, each a list of (question text, options). -> (items, len(ctx_ids)).
54
-
55
- Option order is kept as given (no shuffling, no subsetting): systemone.render_question already caps a choice at
56
- MAX_OPTIONS options, so prompt.build's sampling branch is unreachable here.
57
- chat: a ChatTemplate (prompt.chat_template) for a chat-layout model; the shared ids are then the template head plus the
58
- context, and each row is prompt.build_chat's rendering."""
59
- ctx = context_ids(tok, context, max_ctx_tokens)
60
- if chat is not None:
61
- ctx = list(chat.head) + ctx
62
- items = []
63
- for row in rows:
64
- multi = len(row) > 1
65
- ids = list(ctx); slots = []; nopts = []
66
- for k, (text, options) in enumerate(row):
67
- if not 2 <= len(options) <= MAX_OPTIONS:
68
- raise ValueError(f"2..{MAX_OPTIONS} options required")
69
- if chat is None:
70
- ids.extend(question_piece(tok, text, options, k, multi))
71
- slots.append(len(ids) - 1)
72
- nopts.append(len(options))
73
- if chat is not None:
74
- ids, slots = _chat_row(tok, chat, ctx, row)
75
- items.append(dict(ids=ids, slots=slots, golds=[0] * len(row), nopts=nopts,
76
- perms=[list(range(len(o))) for _, o in row]))
77
- return items, len(ctx)
78
-
79
-
80
- def unique_tokens(items, ctx_len):
81
- """systemone.unique_tokens(items) without re-walking the shared context (all rows start with the same ctx_len ids).
82
- No rows (a request with no questions) counts 0, as systemone.unique_tokens does."""
83
- if not items:
84
- return 0
85
- sufs = [it["ids"][ctx_len:] for it in items]
86
- if len(sufs) < 2:
87
- return ctx_len + sum(len(s) for s in sufs)
88
- lcp = 0; short = min(len(s) for s in sufs); s0 = sufs[0]
89
- while lcp < short and all(s[lcp] == s0[lcp] for s in sufs): lcp += 1
90
- return ctx_len + lcp + sum(len(s) - lcp for s in sufs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/schema_engine.py DELETED
@@ -1,150 +0,0 @@
1
- """Schema cache: compute a question schema once, then score states against it.
2
-
3
- In production the questions are fixed and only the state changes. With the schema-first prompt layout
4
- (prompt.build_schema_first) the question/option blocks are a prefix that does not depend on the state, so their
5
- cache - attention K/V for the 6 full-attention layers, conv + recurrent state for the 18 delta-net layers - is computed
6
- once (`prepare`). A request then runs only "Context: <state>" plus one answer slot per question, as a CUDA graph per
7
- (batch, length) bucket. The prefix cache is read-only during a request (nothing is written back), so one copy serves
8
- every batch and every graph.
9
-
10
- se = SchemaEngine(engine); h = se.prepare([{"question": ..., "options": [...]}, ...])
11
- probs = se.score(h, ["state 1", "state 2", ...]) # list of [n_questions, MAX_OPTIONS] tensors
12
- """
13
- import time, types, torch, torch.nn.functional as F
14
- from decider.prompt import schema_prefix_ids, schema_suffix_ids, MAX_OPTIONS
15
- from decider.engine import read_slots, fill_ids
16
-
17
- TS_BUCKETS = [32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024]
18
- B_BUCKETS = [1, 2, 4, 8, 16, 32, 64]
19
-
20
-
21
- class _Q:
22
- def __init__(self, text, options): self.text, self.options = text, options
23
-
24
-
25
- class PrefixCache:
26
- """Duck-typed transformers Cache over fixed, read-only prefixes, for one suffix forward pass.
27
- A handle holds P prefixes (P = 1: all questions packed in one prefix; P = n_questions: one prefix per question, so every
28
- question is scored independently). A batch of R states has R * P rows; row r * P + p continues prefix p."""
29
- def __init__(self, h, R):
30
- rep = (lambda t: t.expand(R, *t.shape[1:])) if h.P == 1 else (lambda t: t.repeat(R, *([1] * (t.dim() - 1))))
31
- self.tp = h.tpmax; self.k = {i: rep(k) for i, k in h.k.items()}; self.v = {i: rep(v) for i, v in h.v.items()}
32
- self.conv = {i: rep(c).contiguous() for i, c in h.conv.items()}
33
- self.layers = {i: types.SimpleNamespace(record_past=False, recurrent_states={0: rep(r).contiguous()}) for i, r in h.rec.items()}
34
-
35
- def has_previous_state(self, layer_idx=None, state_idx=None): return True
36
- def get_seq_length(self, *a, **k): return self.tp
37
- def update(self, key, value, layer_idx, *a, **k): return torch.cat([self.k[layer_idx], key], 2), torch.cat([self.v[layer_idx], value], 2)
38
- def update_conv_state(self, x, layer_idx, **k): return torch.cat([self.conv[layer_idx].to(x.dtype), x], -1)
39
- def update_recurrent_state(self, s, layer_idx, **k): return s
40
-
41
-
42
- class SchemaEngine:
43
- def __init__(self, engine, use_graphs=True, chat=None):
44
- """chat: the ChatTemplate of a chat-layout model (decider.prompt.chat_template); the prefix then starts with the template
45
- head and the suffix ends with the template tail and the answer pieces (decider.prompt.build_schema_first with chat)."""
46
- self.chat = chat
47
- self.e = engine; self.core = engine.core; self.W = engine.W; self.tok = engine.tok; self.dev = engine.dev
48
- self.use_graphs = use_graphs and engine.use_graphs; self.graphs = {}; self.stats = dict(prepared=0, captures=0, replays=0, eager=0)
49
- self.compile = bool(engine.cfg.get("compile")); self._compiled = {}
50
- if self.compile: # every compiled schema graph specialises the model frames again (its cache tensors are constants)
51
- import torch._dynamo
52
- torch._dynamo.config.cache_size_limit = 4096; torch._dynamo.config.accumulated_cache_size_limit = 1 << 16
53
-
54
- @torch.no_grad()
55
- def prepare(self, questions, independent=False, compile=False):
56
- """questions: [{"question": str, "options": [str]}] in the order answers are wanted. Runs the prefix(es) once.
57
- independent=False: one prefix holding every question (cheapest: a request costs state + n slots).
58
- independent=True: one prefix per question, one row per question (a request costs n * (state + 1 slot); no question
59
- can influence another)."""
60
- qs = [_Q(q["question"], list(q["options"])) for q in questions]
61
- groups = [[q] for q in qs] if independent else [qs]; pres = [schema_prefix_ids(self.tok, g, chat=self.chat) for g in groups]
62
- h = types.SimpleNamespace(P=len(groups), nq=len(qs), slots_per_row=1 if independent else len(qs), nopts=[len(q.options) for q in qs], tps=[len(p) for p in pres],
63
- tpmax=max(len(p) for p in pres), k={}, v={}, conv={}, rec={}, id=self.stats["prepared"],
64
- compile=bool(compile and self.compile))
65
- parts = []
66
- for pre in pres:
67
- out = self.core(input_ids=torch.tensor(pre, device=self.dev)[None], use_cache=True).past_key_values; d = dict(k={}, v={}, conv={}, rec={})
68
- for i, layer in enumerate(out.layers):
69
- if getattr(layer, "recurrent_states", None) is not None and layer.recurrent_states.get(0) is not None:
70
- d["conv"][i] = layer.conv_states[0]; d["rec"][i] = layer.recurrent_states[0]
71
- else: # right-pad every prefix's K/V to the longest; the mask hides the padding
72
- pad = (0, 0, 0, h.tpmax - len(pre)); d["k"][i] = F.pad(layer.keys, pad); d["v"][i] = F.pad(layer.values, pad)
73
- parts.append(d)
74
- for name in ("k", "v", "conv", "rec"):
75
- getattr(h, name).update({i: torch.cat([d[name][i] for d in parts], 0).clone() for i in parts[0][name]})
76
- self.stats["prepared"] += 1
77
- return h
78
-
79
- def _fwd(self, ids, cache, mask, pos):
80
- hs = self.core(input_ids=ids, past_key_values=cache, attention_mask={"full_attention": mask, "linear_attention": None}, position_ids=pos, use_cache=True).last_hidden_state
81
- return F.linear(hs, self.W).float()
82
-
83
- def _static(self, h, R, Ts):
84
- """R request slots -> R * P rows. Mask: a row sees its own prefix (not the padding up to tpmax) and the causal suffix."""
85
- ar = torch.arange(Ts, device=self.dev); tps = torch.tensor(h.tps, device=self.dev).repeat(R) # [R*P]
86
- pre = (torch.arange(h.tpmax, device=self.dev)[None, :] < tps[:, None])[:, None, None, :].expand(-1, 1, Ts, -1) # [B,1,Ts,tpmax]
87
- mask = torch.cat([pre, (ar[:, None] >= ar[None, :])[None, None].expand(len(tps), 1, -1, -1)], 3).contiguous()
88
- return PrefixCache(h, R), mask, (tps[:, None] + ar[None, :]).contiguous()
89
-
90
- def _capture(self, h, R, Ts):
91
- B = R * h.P
92
- ids = torch.full((B, Ts), self.tok.pad_token_id, dtype=torch.long, device=self.dev); cache, mask, pos = self._static(h, R, Ts)
93
- fwd = self._fwd
94
- if h.compile: # one compiled function per graph (20-30 s each: only for preloaded schemas): the cache tensors are constants of that graph
95
- fwd = torch.compile(lambda i: self._fwd(i, cache, mask, pos), dynamic=False)
96
- call = lambda: fwd(ids)
97
- else:
98
- call = lambda: fwd(ids, cache, mask, pos)
99
- st = torch.cuda.Stream(); st.wait_stream(torch.cuda.current_stream())
100
- with torch.cuda.stream(st):
101
- for _ in range(3): call()
102
- torch.cuda.current_stream().wait_stream(st)
103
- g = torch.cuda.CUDAGraph()
104
- with torch.cuda.graph(g, pool=self.e.pool):
105
- out = call()
106
- self.stats["captures"] += 1
107
- return ids, out, g, (cache, mask, pos)
108
-
109
- def warmup(self, h, batch_sizes=(1, 8, 32), state_tokens=(64, 128, 256)):
110
- """Capture (and, for a compiled schema, compile) the graphs for these request-batch sizes and suffix lengths ahead of traffic."""
111
- t = time.time()
112
- for R in batch_sizes:
113
- for Ts in state_tokens:
114
- Ts = next((x for x in TS_BUCKETS if x >= Ts), TS_BUCKETS[-1])
115
- if (h.id, R, Ts) not in self.graphs: self.graphs[(h.id, R, Ts)] = self._capture(h, R, Ts)
116
- torch.cuda.synchronize(); return time.time() - t
117
-
118
- def tokenize(self, h, context, max_ctx_tokens=1536):
119
- """CPU part of a request (do it outside any GPU lock): -> (suffix ids, slot positions)."""
120
- return schema_suffix_ids(self.tok, context, h.slots_per_row, max_ctx_tokens, chat=self.chat)
121
-
122
- @staticmethod
123
- def bucket(n_tokens):
124
- return next((t for t in TS_BUCKETS if t >= n_tokens), -(-n_tokens // 256) * 256)
125
-
126
- def score(self, h, contexts, temperature=1.0, max_ctx_tokens=1536):
127
- """-> one [n_questions, MAX_OPTIONS] probability tensor per context. temperature: as in score_rows."""
128
- return self.score_rows(h, [self.tokenize(h, c, max_ctx_tokens) for c in contexts], temperature)
129
-
130
- @torch.no_grad()
131
- def score_rows(self, h, rows, temperature=1.0):
132
- """rows: [(suffix ids, slots)] from tokenize(). temperature: a number, or a list with one temperature per schema row
133
- (h.nq values, in the order of prepare's questions), applied to every request."""
134
- if isinstance(temperature, (list, tuple)) and len(temperature) != h.nq:
135
- raise ValueError(f"temperature: {len(temperature)} values for a schema with {h.nq} rows")
136
- Tmax = max(len(r[0]) for r in rows); Ts = next((t for t in TS_BUCKETS if t >= Tmax), None); n = len(rows)
137
- R = next((b for b in B_BUCKETS if b >= n), n) if Ts else n; Ts = Ts or -(-Tmax // 256) * 256
138
- ids = fill_ids([x for x, _ in rows for _ in range(h.P)], R * h.P, Ts, self.tok.pad_token_id).to(self.dev, non_blocking=True)
139
- if self.use_graphs and Ts <= TS_BUCKETS[-1]:
140
- key = (h.id, R, Ts)
141
- if key not in self.graphs: self.graphs[key] = self._capture(h, R, Ts)
142
- s_ids, s_out, g, _ = self.graphs[key]; s_ids.copy_(ids); g.replay(); out = s_out; self.stats["replays"] += 1
143
- else:
144
- out = self._fwd(ids, *self._static(h, R, Ts)); self.stats["eager"] += 1
145
- if h.P == 1: # packed: n slots in one row per request
146
- rws = [r for r in range(n) for _ in range(h.nq)]; sls = [x for _, sl in rows for x in sl]
147
- else: # independent: one slot in each of the request's P rows
148
- rws = [r * h.P + p for r in range(n) for p in range(h.P)]; sls = [sl[0] for _, sl in rows for _ in range(h.P)]
149
- temps = list(temperature) * n if isinstance(temperature, (list, tuple)) else temperature
150
- return read_slots(out, rws, sls, h.nopts * n, temps, [h.nq] * n)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/serve.py DELETED
@@ -1,562 +0,0 @@
1
- """HTTP server.
2
- POST /decide {"context": str, "schema": {...}} -> typed JSON decisions (all questions packed in one row)
3
- schema: {question: {"type": "choice", "options": [...]} | {"type": "bool"} | {"type": "scale", "legend": [...]}};
4
- a missing or malformed schema is a 422 naming this form (decider.infer.Decider._check_schema)
5
- POST /v1/systemone {"state": str|object|array, "questions": {id: {...}}} -> the TypeSafe/Jev wire format (decider.systemone):
6
- Choice (up to 255 described options), Score, Noul; every question is scored in its own row, so answers
7
- are independent of each other ("independent": false packs them behind one copy of the state instead).
8
- GET /v1/models /health /stats
9
-
10
- uvicorn decider.serve:app --host 0.0.0.0 --port 8000 (env: DECIDER_MODEL and the variables below)
11
-
12
- Execution (1.1.1; docs/SERVING.md has the design and the measurements):
13
- * decider.engine_v2.EngineV2: CUDA graphs keyed on (batch bucket, padded length bucket) only. The whole grid is captured
14
- during start-up and the engine is then sealed, so no request pays a graph capture or a torch.compile. Rows longer than the
15
- last length bucket (8,192 tokens) run eager, in chunks bounded by DECIDER_GRAPH_TOKEN_BUDGET padded tokens.
16
- * one GPU thread runs every forward; tokenisation runs on a CPU pool; rows waiting at the same moment are partitioned by
17
- decider.batching.plan_batches, which pads a shorter row into a longer row's bucket when that costs less than a second
18
- forward (cost model: DECIDER_MERGE_OVERHEAD_TOKENS + rows * padded length). When a collection held rows from more than
19
- one live request the next one waits up to DECIDER_BATCH_ADAPTIVE_WAIT_MS for more rows, unless its own first row took
20
- more than 50 ms to arrive (a quiet queue drops the window again); after a single-request collection it does not wait.
21
- * an independent request with more than one question over a state of at least DECIDER_SHARED_MIN_TOKENS tokens runs the state
22
- once and forks its cache per question (EngineV2.score_shared, decider.shared_prefix). The fork is made in chunks that fit
23
- DECIDER_SHARED_FORK_GB, so the peak memory does not grow with the question count. The cuDNN SDPA backend is off
24
- (decider.engine.set_attention_backend_policy), which is what makes that path agree with the full forward.
25
- * the schema cache (questions-first layout, prefix cached per question set) is honoured exactly as before: on when
26
- decider_config.json has "schema_first": true, or with DECIDER_SCHEMA_CACHE=1 on a model trained for that layout. It is
27
- the one path that does GPU work after start-up that is not a graph replay: the first request with a new schema runs its
28
- prefix and captures one graph per (schema, batch bucket, state bucket) as in 1.0.x (/stats -> schema_cache.captures).
29
- Its requests are bounded and admitted like the others, on prefix plus suffix tokens, before any GPU preparation.
30
- * prompt layout (1.2.0): a model whose decider_config.json has "layout": "chat" (a chat-trained checkpoint) is read in the chat layout
31
- (decider.prompt.build_chat: the tokenizer's chat template around one user turn, thinking off, "Answer: (" in the assistant
32
- turn) on every route, including the shared-prefix fork and the schema cache. Every other model is read in the plain
33
- layout, with the same token ids as 1.1.x. A config naming an unknown layout stops start-up with a ValueError.
34
- * requests are bounded before they reach the GPU: HTTP 413 when a row, the request or the expanded question count exceeds the
35
- limits below, HTTP 503 when the outstanding work exceeds DECIDER_MAX_QUEUE_ROWS.
36
-
37
- Variables (default): DECIDER_DEVICE (auto: cuda, else mps, else cpu) DECIDER_COMPILE (0) DECIDER_FP8 (0) DECIDER_SHARED (1) DECIDER_SHARED_MIN_TOKENS (768)
38
- DECIDER_SHARED_FORK_GB (8) DECIDER_MERGE_OVERHEAD_TOKENS (512) DECIDER_BATCH_ADAPTIVE_WAIT_MS (2)
39
- DECIDER_MAX_BATCH (32) DECIDER_BATCH_WAIT_MS (0; DECIDER_MAX_WAIT_MS is an alias) DECIDER_MAX_STATE_TOKENS (32768)
40
- DECIDER_T_BUCKETS DECIDER_B_BUCKETS DECIDER_GRAPH_TOKEN_BUDGET (32768) DECIDER_WARMUP (1) DECIDER_TOKENIZE_THREADS (8)
41
- DECIDER_MAX_ROWS (1024) DECIDER_MAX_ROW_TOKENS (DECIDER_MAX_STATE_TOKENS + 4096) DECIDER_MAX_REQUEST_TOKENS (1048576)
42
- DECIDER_MAX_QUEUE_ROWS (4096) DECIDER_TEMPERATURE DECIDER_SCHEMA_CACHE (0) DECIDER_SCHEMA_MIN_SEEN (2) DECIDER_SCHEMAS
43
-
44
- Temperatures (1.4.0, decider.temperature): decider_config.json "temperature", and optionally "temperature_by_type"
45
- {"choice": T, "noul": T, "score": T} (a /decide "bool" field is "noul", a "scale" field is "score"; a missing type uses
46
- "temperature"). Every row carries the answer type of each of its slots, so a batch that mixes requests and types still
47
- applies each answer's own temperature. DECIDER_TEMPERATURE replaces "temperature" and switches the by-type map off.
48
- /health and the ready line report the temperature every answer type gets.
49
- """
50
- import asyncio, json, os, time
51
- from concurrent.futures import ThreadPoolExecutor
52
- from contextlib import asynccontextmanager
53
- from fastapi import FastAPI, HTTPException
54
- from pydantic import BaseModel
55
- from decider import systemone as S1
56
- from decider import temperature as TT
57
- from decider.batching import DEFAULT_MERGE_OVERHEAD_TOKENS, plan_batches
58
- from decider.prompt import build, MAX_OPTIONS, resolve_layout, chat_template
59
- from decider.prompt_fast import build_rows, unique_tokens
60
-
61
-
62
- def _env_int(name, default):
63
- return int(os.environ.get(name, default))
64
-
65
-
66
- MODEL = os.environ.get("DECIDER_MODEL", "runs/r3_v2/model")
67
- MAX_BATCH = _env_int("DECIDER_MAX_BATCH", 32)
68
- BATCH_WAIT_MS = float(os.environ.get("DECIDER_BATCH_WAIT_MS", os.environ.get("DECIDER_MAX_WAIT_MS", "0")))
69
- ADAPTIVE_WAIT_MS = float(os.environ.get("DECIDER_BATCH_ADAPTIVE_WAIT_MS", "2")) # window after a collection that held >1 request; 0 disables
70
- ADAPTIVE_IDLE_RESET_MS = 50.0 # an idler queue than this drops the adaptive window again
71
- MERGE_OVERHEAD_TOKENS = _env_int("DECIDER_MERGE_OVERHEAD_TOKENS", DEFAULT_MERGE_OVERHEAD_TOKENS)
72
- MAX_STATE_TOKENS = _env_int("DECIDER_MAX_STATE_TOKENS", 32768)
73
- SHARED = os.environ.get("DECIDER_SHARED", "1") == "1"
74
- SHARED_MIN_TOKENS = _env_int("DECIDER_SHARED_MIN_TOKENS", 768)
75
- DEVICE = os.environ.get("DECIDER_DEVICE", "auto") # auto | cuda[:i] | mps | cpu
76
- COMPILE = os.environ.get("DECIDER_COMPILE", "0") == "1"
77
- FP8 = os.environ.get("DECIDER_FP8", "0") == "1"
78
- WARMUP = os.environ.get("DECIDER_WARMUP", "1") == "1"
79
- TOKENIZE_THREADS = _env_int("DECIDER_TOKENIZE_THREADS", 8)
80
- GRAPH_TOKEN_BUDGET = _env_int("DECIDER_GRAPH_TOKEN_BUDGET", 32768)
81
- # request bounds, checked after tokenisation and before anything is queued
82
- MAX_ROWS = _env_int("DECIDER_MAX_ROWS", 1024) # scoring rows per request (questions, expanded score levels)
83
- MAX_ROW_TOKENS = _env_int("DECIDER_MAX_ROW_TOKENS", MAX_STATE_TOKENS + 4096) # tokens in one row: truncated state + question block
84
- MAX_REQUEST_TOKENS = _env_int("DECIDER_MAX_REQUEST_TOKENS", 1 << 20) # sum of row lengths of one request
85
- MAX_QUEUE_ROWS = _env_int("DECIDER_MAX_QUEUE_ROWS", 4096) # rows admitted and not yet scored, over all requests
86
- DECIDE_MAX_CTX_TOKENS = 1536 # /decide context cap, unchanged from 1.0.x
87
-
88
- MODEL_NAME = "decider"; TEMP = 1.0; TEMP_SCHEMA = 1.0; TEMP_BY_TYPE = {}; TEMP_SCHEMA_BY_TYPE = {}; RELEASE_DATE = "2026-09-17"; ISOLATED = False; NEUTRALIZE_NONE = True
89
- SCHEMA_FIRST = False; LAYOUT = "plain"; CHAT = None; se = None; squeue = None; schemas = {}; seen = {}
90
- eng = None; queue = None; gpu = None; cpu = None; batcher_task = None; schema_task = None
91
- outstanding = 0; REQ_SEQ = 0
92
- stats = dict(requests=0, batches=0, decisions=0, rows=0, shared_prefix_requests=0, errors=0, rejected_too_large=0,
93
- rejected_overloaded=0, batch_hist={}, bucket_hist={})
94
-
95
-
96
- def _ints(name):
97
- v = os.environ.get(name)
98
- return [int(x) for x in v.split(",")] if v else None
99
-
100
-
101
- def load_config(path):
102
- """decider_config.json from a model folder or a Hub repository (the way decider.infer.Decider resolves it)."""
103
- try:
104
- if os.path.isdir(path):
105
- return json.load(open(os.path.join(path, "decider_config.json")))
106
- from huggingface_hub import hf_hub_download
107
- return json.load(open(hf_hub_download(path, "decider_config.json")))
108
- except Exception:
109
- return {}
110
-
111
-
112
- # ---- request preparation (CPU) -------------------------------------------
113
- class _NoShuffle:
114
- def shuffle(self, x): pass
115
- def sample(self, xs, k): return xs[:k]
116
-
117
-
118
- def prepare(tok, state, questions, independent, isolated=False, max_state_tokens=32768, chat=None):
119
- """Render, plan the rows, tokenize the state once. -> (rqs, index, items, ctx_len). The rows are those
120
- `prompt.build` produces for the same request (tests/test_prompt_fast.py, tests/test_serve_prepare.py); chat: the
121
- ChatTemplate of a chat-layout model, None for the plain layout."""
122
- ctx = S1.render_state(state)
123
- rqs = {k: S1.render_question(v) for k, v in questions.items()}
124
- flat, index = S1.plan_rows(rqs, isolated and independent)
125
- pairs = [(r["question"], list(r["options"])) for r in flat]
126
- rows = [[p] for p in pairs] if independent else [pairs]
127
- items, ctx_len = build_rows(tok, ctx, rows, max_ctx_tokens=max_state_tokens, chat=chat)
128
- types = S1.row_types(rqs, index) # answer type per slot (decider.temperature)
129
- for it, ts in zip(items, [[t] for t in types] if independent else [types]):
130
- it["types"] = ts
131
- return rqs, index, items, ctx_len
132
-
133
-
134
- def _prepare_s1(state, questions, independent):
135
- return prepare(eng.tok, state, questions, independent, ISOLATED, MAX_STATE_TOKENS, chat=CHAT)
136
-
137
-
138
- def _prepare_decide(context, schema):
139
- from decider.infer import Decider, Example, Q, neutralize_options
140
- qs = Decider._schema_to_questions(schema)
141
- for q in qs:
142
- if NEUTRALIZE_NONE:
143
- q["options"], q["_back"] = neutralize_options(q["options"])
144
- ex = Example(context, [Q(q["question"], list(q["options"]), 0) for q in qs])
145
- it = build(ex, eng.tok, _NoShuffle(), max_options=MAX_OPTIONS, max_ctx_tokens=DECIDE_MAX_CTX_TOKENS, chat=CHAT)
146
- it["types"] = [q["_type"] for q in qs] # bool -> noul, scale -> score (decider.temperature)
147
- return qs, it
148
-
149
-
150
- def _format_decide(schema, qs, probs):
151
- o = {}
152
- for (qtext, spec), q, p in zip(schema.items(), qs, probs):
153
- p = p[:len(q["options"])].tolist(); t = spec.get("type", "choice"); j = max(range(len(p)), key=p.__getitem__)
154
- back = q.get("_back", {}); names = [back.get(x, x) for x in q["options"]]
155
- if t == "bool":
156
- o[qtext] = {"noul": round(p[1], 4), "type": "noul"}
157
- elif t == "choice":
158
- o[qtext] = {"choice": names[j], "confidence": round(p[j], 4), "type": "choice",
159
- "probabilities": {k: round(v, 4) for k, v in zip(names, p)}}
160
- else:
161
- keys = q["_keys"]; score = sum(float(k) * pi for k, pi in zip(keys, p))
162
- o[qtext] = {"score": round(score, 2), "confidence": round(p[j], 4), "type": "scale", "legend": q["_legend"],
163
- "probabilities": {str(keys[i]): round(pi, 4) for i, pi in enumerate(p)}}
164
- return o
165
-
166
-
167
- # ---- limits ---------------------------------------------------------------
168
- def check_size(row_lengths, max_rows=None, max_row_tokens=None, max_request_tokens=None):
169
- """Raise HTTPException(413) when a request exceeds the row count, per-row token or total token limit."""
170
- max_rows = MAX_ROWS if max_rows is None else max_rows
171
- max_row_tokens = MAX_ROW_TOKENS if max_row_tokens is None else max_row_tokens
172
- max_request_tokens = MAX_REQUEST_TOKENS if max_request_tokens is None else max_request_tokens
173
- n, total, longest = len(row_lengths), sum(row_lengths), max(row_lengths, default=0)
174
- if n > max_rows:
175
- msg = f"too many questions: the request expands to {n} scoring rows, the limit is {max_rows} (DECIDER_MAX_ROWS)"
176
- elif longest > max_row_tokens:
177
- msg = f"too many tokens: one row has {longest} tokens, the limit is {max_row_tokens} per row (DECIDER_MAX_ROW_TOKENS)"
178
- elif total > max_request_tokens:
179
- msg = f"too many tokens: the request has {total} tokens over {n} rows, the limit is {max_request_tokens} (DECIDER_MAX_REQUEST_TOKENS)"
180
- else:
181
- return
182
- stats["rejected_too_large"] += 1
183
- raise HTTPException(413, msg)
184
-
185
-
186
- def _admit(n):
187
- """Reserve n rows of outstanding work or raise HTTPException(503)."""
188
- global outstanding
189
- if outstanding + n > MAX_QUEUE_ROWS:
190
- stats["rejected_overloaded"] += 1
191
- raise HTTPException(503, f"server busy: {outstanding} rows queued, the limit is {MAX_QUEUE_ROWS} (DECIDER_MAX_QUEUE_ROWS); retry later")
192
- outstanding += n
193
-
194
-
195
- def _release(n):
196
- global outstanding
197
- outstanding -= n
198
-
199
-
200
- # ---- GPU work ----------------------------------------------------------------
201
- def _score_items(items):
202
- return eng.score_items(items, temperature=TT.for_items(TEMP, TEMP_BY_TYPE, items)) # the scalar TEMP without a by-type map
203
-
204
-
205
- def _score_shared(items):
206
- return eng.score_shared(items, temperature=TT.for_items(TEMP, TEMP_BY_TYPE, items))
207
-
208
-
209
- async def _collect(q, wait_ms=None, adaptive_ms=0.0, idle_reset_ms=None):
210
- """Take what is already queued and go.
211
-
212
- `wait_ms` (DECIDER_BATCH_WAIT_MS) is the unconditional collection window. `adaptive_ms` is the extra window the
213
- batcher asks for after a collection that held more than one live request; it is dropped again when the first row of
214
- this collection took longer than `idle_reset_ms` to arrive, so an isolated request after a quiet period never waits.
215
- """
216
- wait = BATCH_WAIT_MS if wait_ms is None else wait_ms
217
- reset = ADAPTIVE_IDLE_RESET_MS if idle_reset_ms is None else idle_reset_ms
218
- t0 = time.monotonic()
219
- batch = [await q.get()]
220
- if adaptive_ms > 0 and (time.monotonic() - t0) * 1000 <= reset:
221
- wait = max(wait, adaptive_ms)
222
- deadline = time.monotonic() + wait / 1000
223
- while len(batch) < MAX_BATCH:
224
- try:
225
- batch.append(q.get_nowait())
226
- except asyncio.QueueEmpty:
227
- timeout = deadline - time.monotonic()
228
- if timeout <= 0: break
229
- try: batch.append(await asyncio.wait_for(q.get(), timeout))
230
- except asyncio.TimeoutError: break
231
- return batch
232
-
233
-
234
- def _bucketed(n):
235
- """False for a row longer than the engine's last captured length bucket. Those run eager at a request-specific shape,
236
- so they are grouped by exact padded length as in 1.1.0 instead of being padded into another row's bucket."""
237
- t_bucket = getattr(eng, "t_bucket", None)
238
- return True if t_bucket is None else t_bucket(n) is not None
239
-
240
-
241
- def adaptive_ms(batch):
242
- """The extra collection window the next collection may use: DECIDER_BATCH_ADAPTIVE_WAIT_MS when this collection held
243
- live rows from more than one request, 0 otherwise. Rows whose future is already done (a cancelled or failed request)
244
- are not counted: they are not evidence that requests are overlapping."""
245
- if ADAPTIVE_WAIT_MS <= 0:
246
- return 0.0
247
- return ADAPTIVE_WAIT_MS if len({rid for fut, _, rid in batch if not fut.done()}) > 1 else 0.0
248
-
249
-
250
- async def batcher():
251
- """One forward per planned group. Rows queued at the same moment are partitioned by decider.batching.plan_batches:
252
- a shorter row is padded into a longer row's bucket when that costs less than running a second forward."""
253
- loop = asyncio.get_running_loop()
254
- extra = 0.0 # the adaptive window the previous collection earned
255
- while True:
256
- batch = await _collect(queue, BATCH_WAIT_MS, extra)
257
- extra = adaptive_ms(batch)
258
- groups = plan_batches([len(it["ids"]) for _, it, _ in batch], eng.pad_len, eng.max_rows, MAX_BATCH,
259
- MERGE_OVERHEAD_TOKENS, _bucketed)
260
- for T, idx in groups:
261
- part = [batch[i] for i in idx]
262
- stats["bucket_hist"][T] = stats["bucket_hist"].get(T, 0) + len(part)
263
- try:
264
- probs = await loop.run_in_executor(gpu, _score_items, [it for _, it, _ in part])
265
- for (fut, _, _), p in zip(part, probs):
266
- if not fut.done(): fut.set_result(p)
267
- except Exception as e:
268
- for fut, _, _ in part:
269
- if not fut.done(): fut.set_exception(e)
270
- stats["batches"] += 1
271
- stats["batch_hist"][len(part)] = stats["batch_hist"].get(len(part), 0) + 1
272
-
273
-
274
- # ---- schema cache (questions-first layout; opt-in) --------------------------------
275
- def _schema_key(questions, independent):
276
- return (json.dumps(questions, sort_keys=True, ensure_ascii=False), independent)
277
-
278
-
279
- class _SQ:
280
- def __init__(self, text, options): self.text, self.options = text, options
281
-
282
-
283
- def _plan_schema(questions, independent, state):
284
- """CPU part of a schema-cache request: prefix token lengths (from the cached handle when the schema is known, otherwise
285
- tokenised here, without touching the GPU) and the suffix row (context plus answer slots). -> (tps, (suffix ids, slots)).
286
- Raises ValueError for an invalid question."""
287
- from decider.prompt import schema_prefix_ids, schema_suffix_ids
288
- rqs = {k: S1.render_question(v) for k, v in questions.items()}; rows, _ = S1.plan_rows(rqs, ISOLATED and independent)
289
- cached = schemas.get(_schema_key(questions, independent))
290
- if cached is not None:
291
- tps = list(cached[1].tps)
292
- else:
293
- qs = [_SQ(x["question"], list(x["options"])) for x in rows]
294
- tps = [len(schema_prefix_ids(eng.tok, g, chat=CHAT)) for g in ([[q] for q in qs] if independent else [qs])]
295
- row = schema_suffix_ids(eng.tok, S1.render_state(state), 1 if independent else len(rows), MAX_STATE_TOKENS, chat=CHAT)
296
- return tps, row
297
-
298
-
299
- def _schema_handle(questions, independent, compile=False):
300
- """Compile (or look up) the question schema: its prefix is run once, requests then only run the state. GPU thread."""
301
- key = _schema_key(questions, independent)
302
- if key not in schemas:
303
- rqs = {k: S1.render_question(v) for k, v in questions.items()}; rows, index = S1.plan_rows(rqs, ISOLATED and independent)
304
- if len(schemas) >= 128:
305
- old = next(iter(schemas)); hid = schemas.pop(old)[1].id
306
- for k in [k for k in se.graphs if k[0] == hid]: del se.graphs[k]
307
- h = se.prepare(rows, independent=independent, compile=compile)
308
- h.types = S1.row_types(rqs, index) # answer type per schema row (decider.temperature)
309
- schemas[key] = (rqs, h, index)
310
- return schemas[key]
311
-
312
-
313
- def _worth_caching(questions, independent):
314
- """A schema gets a cached prefix and CUDA graphs from its second request on."""
315
- key = _schema_key(questions, independent)
316
- if key in schemas: return True
317
- if len(seen) > 50000: seen.clear()
318
- seen[key] = seen.get(key, 0) + 1
319
- return seen[key] >= _env_int("DECIDER_SCHEMA_MIN_SEEN", 2)
320
-
321
-
322
- def _score_schema(h, rows):
323
- return se.score_rows(h, rows, temperature=TT.for_types(TEMP_SCHEMA, TEMP_SCHEMA_BY_TYPE, h.types))
324
-
325
-
326
- async def schema_batcher():
327
- loop = asyncio.get_running_loop()
328
- while True:
329
- batch = await _collect(squeue)
330
- groups = {}
331
- for fut, h, row in batch: groups.setdefault((h.id, se.bucket(len(row[0]))), (h, []))[1].append((fut, row))
332
- for h, items in groups.values():
333
- step = max(1, MAX_BATCH // h.P)
334
- for i in range(0, len(items), step):
335
- chunk = items[i:i + step]
336
- try:
337
- probs = await loop.run_in_executor(gpu, _score_schema, h, [c for _, c in chunk])
338
- for (fut, _), p in zip(chunk, probs):
339
- if not fut.done(): fut.set_result(p)
340
- except Exception as e:
341
- for fut, _ in chunk:
342
- if not fut.done(): fut.set_exception(e)
343
- stats["schema_batches"] = stats.get("schema_batches", 0) + 1
344
-
345
-
346
- # ---- start-up / shutdown -------------------------------------------------------
347
- def apply_config(cfg):
348
- global MODEL_NAME, TEMP, TEMP_SCHEMA, TEMP_BY_TYPE, TEMP_SCHEMA_BY_TYPE, RELEASE_DATE, ISOLATED, NEUTRALIZE_NONE, SCHEMA_FIRST, LAYOUT
349
- LAYOUT = resolve_layout(cfg) # ValueError for an unknown layout, before the engine is built
350
- NEUTRALIZE_NONE = bool(cfg.get("neutralize_none", True))
351
- MODEL_NAME = "decider-" + str(cfg.get("version", "dev"))
352
- # ValueError for a temperature that is not finite and > 0 or an unknown by-type key, before the engine is built.
353
- # DECIDER_TEMPERATURE replaces "temperature" and switches "temperature_by_type" off (decider.temperature).
354
- (TEMP, TEMP_BY_TYPE), (TEMP_SCHEMA, TEMP_SCHEMA_BY_TYPE) = TT.from_config(cfg, os.environ.get("DECIDER_TEMPERATURE"))
355
- RELEASE_DATE = str(cfg.get("release_date", RELEASE_DATE))
356
- ISOLATED = bool(cfg.get("isolated_levels", False))
357
- trained = bool(cfg.get("schema_first", False) or cfg.get("schema_first_trained", False))
358
- SCHEMA_FIRST = trained and (bool(cfg.get("schema_first", False)) or os.environ.get("DECIDER_SCHEMA_CACHE", "0") == "1")
359
-
360
-
361
- def start_workers(loop=None):
362
- """Queues, executors and batcher tasks. Needs `eng` set; a test can set a stand-in engine and call this directly."""
363
- global queue, gpu, cpu, batcher_task, squeue, schema_task
364
- loop = loop or asyncio.get_running_loop()
365
- if gpu is None: gpu = ThreadPoolExecutor(max_workers=1, thread_name_prefix="gpu")
366
- if cpu is None: cpu = ThreadPoolExecutor(max_workers=TOKENIZE_THREADS, thread_name_prefix="tok")
367
- queue = asyncio.Queue(); batcher_task = loop.create_task(batcher())
368
- if SCHEMA_FIRST:
369
- squeue = asyncio.Queue(); schema_task = loop.create_task(schema_batcher())
370
-
371
-
372
- def resolve_device(requested=None):
373
- """The device and dtype the server runs on. `auto` picks as decider.infer.Decider does: CUDA, else MPS, else CPU; float16
374
- on MPS, bfloat16 elsewhere. An explicit device that is not available, or FP8 / torch.compile off CUDA, is a start-up error
375
- that says so, instead of the torch assertion a CUDA call raises on a build without CUDA."""
376
- import torch
377
- req = (DEVICE if requested is None else requested).strip().lower()
378
- if req in ("", "auto"):
379
- dev = "cuda" if torch.cuda.is_available() else ("mps" if torch.backends.mps.is_available() else "cpu")
380
- else:
381
- dev = req
382
- kind = dev.split(":")[0]
383
- if kind == "cuda" and not torch.cuda.is_available():
384
- raise RuntimeError(f"DECIDER_DEVICE={req}, but torch.cuda.is_available() is False on this machine. "
385
- "Set DECIDER_DEVICE=mps or cpu (or leave it at auto).")
386
- if kind == "mps" and not torch.backends.mps.is_available():
387
- raise RuntimeError(f"DECIDER_DEVICE={req}, but torch.backends.mps.is_available() is False on this machine.")
388
- if kind not in ("cuda", "mps", "cpu"):
389
- raise RuntimeError(f"DECIDER_DEVICE={req}: expected auto, cuda, cuda:<index>, mps or cpu.")
390
- if not dev.startswith("cuda") and (FP8 or COMPILE):
391
- raise RuntimeError(f"DECIDER_FP8 and DECIDER_COMPILE need CUDA; the server is starting on {dev}. Unset them.")
392
- return dev, (torch.float16 if dev.startswith("mps") else torch.bfloat16)
393
-
394
-
395
- async def _start():
396
- global eng, se, gpu, CHAT
397
- from decider.engine_v2 import EngineV2
398
- apply_config(load_config(MODEL))
399
- gpu = ThreadPoolExecutor(max_workers=1, thread_name_prefix="gpu")
400
- loop = asyncio.get_running_loop()
401
- dev, dtype = resolve_device()
402
- eng = await loop.run_in_executor(gpu, lambda: EngineV2(
403
- MODEL, device=dev, dtype=dtype, compile=COMPILE, fp8=FP8, max_ctx_tokens=MAX_STATE_TOKENS, t_buckets=_ints("DECIDER_T_BUCKETS"),
404
- b_buckets=_ints("DECIDER_B_BUCKETS"), token_budget=GRAPH_TOKEN_BUDGET))
405
- CHAT = chat_template(eng.tok) if LAYOUT == "chat" else None
406
- print("[serve] engine", dict({k: v for k, v in eng.cfg.items() if k not in ("t_buckets", "b_buckets")}, device=dev, layout=LAYOUT), flush=True)
407
- if SCHEMA_FIRST:
408
- from decider.schema_engine import SchemaEngine
409
- se = SchemaEngine(eng, chat=CHAT); print("[serve] schema cache on", flush=True)
410
- pre = os.environ.get("DECIDER_SCHEMAS") # JSON: [{"questions": {...}, "independent": true, "batch_sizes": [1, 8, 32], "state_tokens": [64, 256]}]
411
- for spec in (json.load(open(pre)) if pre else []):
412
- _, h, _ = await loop.run_in_executor(gpu, _schema_handle, spec["questions"], spec.get("independent", True), COMPILE)
413
- t = await loop.run_in_executor(gpu, se.warmup, h, spec.get("batch_sizes", (1, 8, 32)), spec.get("state_tokens", (64, 128, 256)))
414
- print(f"[serve] preloaded schema with {h.nq} rows, prefix {sum(h.tps)} tokens, graphs ready in {t:.0f}s", flush=True)
415
- if WARMUP and eng.use_graphs: # off CUDA there are no graphs to capture; every request runs eager
416
- t = await loop.run_in_executor(gpu, lambda: eng.warmup(log=lambda s: print(s, flush=True)))
417
- print(f"[serve] captured {len(eng.graphs)} graphs in {t:.0f}s", flush=True)
418
- eng.seal()
419
- print("[serve] ready", json.dumps(dict(model=MODEL_NAME, layout=LAYOUT, temperature=TEMP, temperature_by_type=TT.effective(TEMP, TEMP_BY_TYPE),
420
- **({"temperature_schema_first_by_type": TT.effective(TEMP_SCHEMA, TEMP_SCHEMA_BY_TYPE)} if SCHEMA_FIRST else {}),
421
- isolated_levels=ISOLATED, schema_first=SCHEMA_FIRST,
422
- shared=SHARED, graphs=len(eng.graphs), limits=dict(
423
- max_rows=MAX_ROWS, max_row_tokens=MAX_ROW_TOKENS, max_request_tokens=MAX_REQUEST_TOKENS,
424
- max_queue_rows=MAX_QUEUE_ROWS))), flush=True)
425
- start_workers(loop)
426
-
427
-
428
- def _stop():
429
- global gpu, cpu, batcher_task, schema_task
430
- for t in (batcher_task, schema_task):
431
- if t is not None: t.cancel()
432
- for ex in (gpu, cpu):
433
- if ex is not None: ex.shutdown(wait=False, cancel_futures=True)
434
- gpu = cpu = batcher_task = schema_task = None
435
-
436
-
437
- @asynccontextmanager
438
- async def lifespan(app):
439
- await _start()
440
- yield
441
- _stop()
442
-
443
-
444
- app = FastAPI(title="decider", lifespan=lifespan)
445
-
446
-
447
- # ---- routes -------------------------------------------------------------------
448
- class Req(BaseModel):
449
- context: str
450
- schema_: object = None # any JSON value: Decider._check_schema gives the 422 (pydantic's echoes NaN/Infinity and cannot be serialised)
451
- model_config = {"populate_by_name": True}
452
- def __init__(self, **kw):
453
- if "schema" in kw: kw["schema_"] = kw.pop("schema")
454
- super().__init__(**kw)
455
-
456
-
457
- class S1Req(BaseModel):
458
- state: object
459
- questions: dict
460
- model: str | None = None
461
- independent: bool = True
462
- layout: str | None = None # "state_first" forces the uncached layout on a schema-first model
463
-
464
-
465
- def _alive():
466
- return batcher_task is not None and not batcher_task.done() and (schema_task is None or not schema_task.done())
467
-
468
-
469
- async def _queued(items):
470
- """Queue one request's rows. Every row carries the request's id, which is what the batcher's adaptive wait reads."""
471
- global REQ_SEQ
472
- loop = asyncio.get_running_loop(); futs = []
473
- REQ_SEQ += 1; rid = REQ_SEQ
474
- for it in items:
475
- f = loop.create_future(); futs.append(f); queue.put_nowait((f, it, rid))
476
- return await asyncio.gather(*futs)
477
-
478
-
479
- @app.post("/decide")
480
- async def decide(r: Req):
481
- loop = asyncio.get_running_loop()
482
- try:
483
- qs, it = await loop.run_in_executor(cpu, _prepare_decide, r.context, r.schema_)
484
- except (ValueError, KeyError) as e:
485
- stats["errors"] += 1
486
- raise HTTPException(422, str(e))
487
- if not qs: # empty schema: nothing to score (the 1.1.2 answer, without a forward)
488
- stats["requests"] += 1; return {}
489
- check_size([len(it["ids"])])
490
- _admit(1)
491
- try:
492
- probs = (await _queued([it]))[0]
493
- except Exception:
494
- stats["errors"] += 1; raise
495
- finally:
496
- _release(1)
497
- stats["requests"] += 1; stats["decisions"] += len(qs); stats["rows"] += 1
498
- return _format_decide(r.schema_, qs, probs)
499
-
500
-
501
- @app.post("/v1/systemone")
502
- async def systemone(r: S1Req):
503
- loop = asyncio.get_running_loop()
504
- if SCHEMA_FIRST and r.questions and r.layout != "state_first" and _worth_caching(r.questions, r.independent):
505
- try: # CPU: rows, prefix lengths, suffix ids -> the complete cost before any GPU work
506
- tps, row = await loop.run_in_executor(cpu, _plan_schema, r.questions, r.independent, r.state)
507
- except ValueError as e:
508
- stats["errors"] += 1
509
- raise HTTPException(422, str(e))
510
- check_size([tp + len(row[0]) for tp in tps])
511
- _admit(len(tps))
512
- try:
513
- rqs, h, index = await loop.run_in_executor(gpu, _schema_handle, r.questions, r.independent)
514
- fut = loop.create_future(); await squeue.put((fut, h, row)); p = await fut
515
- except Exception:
516
- stats["errors"] += 1; raise
517
- finally:
518
- _release(len(tps))
519
- stats["requests"] += 1; stats["decisions"] += len(rqs); stats["schema_requests"] = stats.get("schema_requests", 0) + 1
520
- return {"model": MODEL_NAME, "answers": S1.assemble(rqs, index, [pk.tolist() for pk in p]),
521
- "usage": {"input_tokens": len(row[0]) * h.P, "cached_tokens": sum(h.tps), "output_tokens": 0}}
522
- try:
523
- rqs, index, items, ctx_len = await loop.run_in_executor(cpu, _prepare_s1, r.state, r.questions, r.independent)
524
- except ValueError as e:
525
- stats["errors"] += 1
526
- raise HTTPException(422, str(e))
527
- check_size([len(it["ids"]) for it in items])
528
- _admit(len(items))
529
- try:
530
- if SHARED and len(items) > 1 and min(len(it["ids"]) for it in items) >= SHARED_MIN_TOKENS:
531
- res = await loop.run_in_executor(gpu, _score_shared, items) # long state: run it once, fork the cache per question
532
- stats["shared_prefix_requests"] += 1
533
- else:
534
- res = await _queued(items)
535
- except Exception:
536
- stats["errors"] += 1; raise
537
- finally:
538
- _release(len(items))
539
- probs = [p for ps in res for p in ps] # one prob row per question, request order
540
- stats["requests"] += 1; stats["decisions"] += len(rqs); stats["rows"] += len(items)
541
- return {"model": MODEL_NAME, "answers": S1.assemble(rqs, index, [p.tolist() for p in probs]),
542
- "usage": {"input_tokens": unique_tokens(items, ctx_len), "output_tokens": 0}}
543
-
544
-
545
- @app.get("/v1/models")
546
- async def models():
547
- return {"models": [{"name": MODEL_NAME, "description": "decider: one-pass typed decisions with calibrated probabilities", "release_date": RELEASE_DATE}]}
548
-
549
-
550
- @app.get("/health")
551
- async def health():
552
- return {"ok": eng is not None and bool(getattr(eng, "sealed", True)) and _alive(), "model": MODEL,
553
- "device": str(getattr(eng, "dev", "")) if eng is not None else None, "layout": LAYOUT,
554
- "temperature": TEMP, "temperature_by_type": TT.effective(TEMP, TEMP_BY_TYPE),
555
- **({"temperature_schema_first_by_type": TT.effective(TEMP_SCHEMA, TEMP_SCHEMA_BY_TYPE)} if SCHEMA_FIRST else {})}
556
-
557
-
558
- @app.get("/stats")
559
- async def get_stats():
560
- return dict(stats, outstanding_rows=outstanding, engine=eng.stats if eng else None, graphs=len(eng.graphs) if eng else 0,
561
- sealed=bool(eng and getattr(eng, "sealed", False)), schema_cache=dict(se.stats, schemas=len(schemas)) if se else None,
562
- limits=dict(max_rows=MAX_ROWS, max_row_tokens=MAX_ROW_TOKENS, max_request_tokens=MAX_REQUEST_TOKENS, max_queue_rows=MAX_QUEUE_ROWS))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/shared_prefix.py DELETED
@@ -1,200 +0,0 @@
1
- """Shared-prefix scoring with a bounded memory fork, used by both engines.
2
-
3
- Rows that start with the same tokens (one state, one question per row) run the shared prefix once and then only the
4
- question suffixes, against a copy of the prefix cache. Until 1.1.0 the copy was made with
5
- `cache.reorder_cache(zeros(n))`, which expands the prefix to all n rows at once: a 31k-token state with 32 questions
6
- costs n times the prefix cache, which is 133 GB on a 31B model and is wasteful on the 2B. Here the prefix cache is
7
- forked in chunks of m rows, m chosen so that one fork fits a byte budget, and the rows are scored chunk by chunk.
8
-
9
- The budget is `DECIDER_SHARED_FORK_GB` (8 GB), capped at half of the memory currently free on the device (device-free
10
- plus the caching allocator's reserved-but-unused blocks); `m = clamp(budget // prefix_bytes, 1, n)`. Chunking changes
11
- which rows share a forward and how far each chunk's suffixes are padded, so answers can move by the usual bf16
12
- reduction-order amount; they do not depend on it mathematically (right padding, causal layers).
13
-
14
- Cache layout. transformers 5.17 keeps one layer object per layer in `cache.layers`. An attention layer carries
15
- `keys` and `values` tensors `[batch, heads, seq, head_dim]`; a sparse-attention layer adds `indexer_keys`
16
- `[batch, seq, dim]`; a linear-attention layer carries `conv_states` and `recurrent_states` dicts of tensors whose first
17
- dimension is the batch. Qwen3.5 gives a `DynamicCache` whose 24 layers are a mix of the two objects. `cache_state_tensors`
18
- enumerates whatever of `STATE_NAMES` is present rather than assuming a layout, and `fork_cache` builds a new cache
19
- object from them without touching the original (the linear-attention layers update their states with `copy_()`, so a
20
- fork that shared storage with the prefix would corrupt it for the next chunk).
21
-
22
- A layout we do not know is not chunked. If a layer holds any other tensor attribute, or a dict of tensors, outside
23
- `STATE_NAMES`, we cannot tell whether it carries a batch dimension, so `score_shared` takes one fork of all n rows --
24
- the 1.1.0 behaviour, correct but unbounded -- and counts it in `engine.stats["shared_unchunked_layout"]`.
25
-
26
- The prefix and suffix forwards are eager, at request-specific shapes, and are correct only with the cuDNN SDPA backend
27
- off (`decider.engine.set_attention_backend_policy`, applied in both engines' `__init__`).
28
- """
29
- import copy
30
- import os
31
-
32
- import torch
33
- import torch.nn.functional as F
34
-
35
- from decider.engine import fill_ids, read_slots
36
- from decider.temperature import item_slice, slot_temperatures
37
-
38
- DEFAULT_FORK_GB = 8.0
39
- STATE_NAMES = ("keys", "values", "indexer_keys", "conv_states", "recurrent_states")
40
-
41
-
42
- def common_prefix_len(ids):
43
- """Length of the longest common prefix of the rows, capped one token below the shortest row."""
44
- lcp = 0
45
- short = min(len(x) for x in ids) - 1
46
- while lcp < short and all(x[lcp] == ids[0][lcp] for x in ids):
47
- lcp += 1
48
- return lcp
49
-
50
-
51
- def _get(container, key):
52
- return container[key] if isinstance(container, dict) else getattr(container, key)
53
-
54
-
55
- def _set(container, key, value):
56
- if isinstance(container, dict):
57
- container[key] = value
58
- else:
59
- setattr(container, key, value)
60
-
61
-
62
- def cache_state_tensors(cache):
63
- """Every tensor in the cache whose first dimension is the batch, as (container, key) pairs.
64
-
65
- Covers `STATE_NAMES`: the attention layers' `keys`/`values`, a sparse-attention layer's `indexer_keys` and the
66
- linear-attention layers' `conv_states`/`recurrent_states`, whether those are dicts of tensors (transformers 5.17) or
67
- single tensors, and ignores anything not present."""
68
- out = []
69
- for layer in getattr(cache, "layers", None) or []:
70
- for attr in STATE_NAMES:
71
- v = getattr(layer, attr, None)
72
- if isinstance(v, torch.Tensor):
73
- if v.numel():
74
- out.append((layer, attr))
75
- elif isinstance(v, dict):
76
- for k, t in v.items():
77
- if isinstance(t, torch.Tensor) and t.numel():
78
- out.append((v, k))
79
- return out
80
-
81
-
82
- def cache_row_bytes(cache):
83
- """Bytes one row of this cache holds, summed over the layers and over `STATE_NAMES`."""
84
- return sum(_get(c, k).nbytes // max(_get(c, k).shape[0], 1) for c, k in cache_state_tensors(cache))
85
-
86
-
87
- def unknown_state_names(cache):
88
- """Attribute names the cache's layers hold that carry tensors and are not in `STATE_NAMES`.
89
-
90
- A tensor outside the enumerated set may or may not have a batch dimension, and `fork_cache` would leave it at one
91
- row. When this is not empty the caller must not chunk."""
92
- out = set()
93
- for layer in getattr(cache, "layers", None) or []:
94
- for name, v in vars(layer).items():
95
- if name in STATE_NAMES:
96
- continue
97
- if isinstance(v, torch.Tensor) or (isinstance(v, dict) and any(isinstance(t, torch.Tensor) for t in v.values())):
98
- out.add(name)
99
- return out
100
-
101
-
102
- def fork_budget_bytes(device=None, gb=None):
103
- """DECIDER_SHARED_FORK_GB, capped at half of the memory free on the device right now.
104
-
105
- The cap applies only when the CUDA memory queries succeed: on CPU, on MPS and when the driver does not know the
106
- device string, the configured budget is kept as it is."""
107
- gb = float(os.environ.get("DECIDER_SHARED_FORK_GB", DEFAULT_FORK_GB)) if gb is None else float(gb)
108
- budget = int(gb * (1 << 30))
109
- try:
110
- free, _ = torch.cuda.mem_get_info(device)
111
- free += torch.cuda.memory_reserved(device) - torch.cuda.memory_allocated(device)
112
- budget = min(budget, free // 2)
113
- except Exception: # no CUDA device, or a device string the driver does not know
114
- pass
115
- return max(int(budget), 1)
116
-
117
-
118
- def chunk_rows(prefix_bytes, n, budget_bytes=None, device=None):
119
- """Rows per fork: as many copies of the prefix cache as the budget holds, at least 1 and at most n. One row is the
120
- minimum even when a single copy is over the budget: the budget bounds the fork, it cannot make it free."""
121
- if budget_bytes is None:
122
- budget_bytes = fork_budget_bytes(device)
123
- if prefix_bytes <= 0:
124
- return max(1, int(n))
125
- return max(1, min(int(n), int(budget_bytes // prefix_bytes)))
126
-
127
-
128
- def fork_cache(cache, m, row=0):
129
- """A new cache holding `m` copies of `cache`'s row `row`. The original is not read from again and not modified."""
130
- fork = copy.copy(cache)
131
- layers = getattr(cache, "layers", None)
132
- if layers is not None:
133
- new = []
134
- for layer in layers:
135
- nl = copy.copy(layer)
136
- for k, v in list(vars(nl).items()): # the per-state dicts are mutated by the forward: give the fork its own
137
- if isinstance(v, dict):
138
- setattr(nl, k, dict(v))
139
- new.append(nl)
140
- fork.layers = new
141
- idx = {}
142
- for container, key in cache_state_tensors(fork):
143
- t = _get(container, key)
144
- i = idx.get(t.device)
145
- if i is None:
146
- i = idx[t.device] = torch.full((m,), row, dtype=torch.long, device=t.device)
147
- _set(container, key, t.index_select(0, i)) # a fresh contiguous tensor: the fork never shares storage
148
- return fork
149
-
150
-
151
- def _count(engine, key):
152
- stats = getattr(engine, "stats", None)
153
- if isinstance(stats, dict):
154
- stats[key] = stats.get(key, 0) + 1
155
-
156
-
157
- @torch.no_grad()
158
- def score_shared(engine, items, temperature=1.0, min_prefix=192, budget_bytes=None, rows_per_fork=None):
159
- """Score `items` through the shared prefix. -> one probability tensor per item, in item order, or None when the
160
- request does not qualify (fewer than two rows, or a common prefix below `min_prefix`) and the caller should use
161
- `score_items`.
162
-
163
- `temperature`: a number, or one entry per item (decider.temperature.slot_temperatures).
164
- `rows_per_fork` forces the chunk size; it exists for the tests that compare chunked against unchunked answers."""
165
- ids = [it["ids"] for it in items]
166
- n = len(ids)
167
- if n < 2:
168
- return None
169
- lcp = common_prefix_len(ids)
170
- if lcp < min_prefix:
171
- return None
172
- slot_temperatures(temperature, items) # a length mismatch fails before any forward
173
- core, W, dev, pad = engine.core, engine.W, engine.dev, engine.tok.pad_token_id
174
- pre = torch.tensor(ids[0][:lcp], device=dev)[None]
175
- cache = core(input_ids=pre, use_cache=True).past_key_values
176
- unknown = unknown_state_names(cache)
177
- if unknown: # a state we cannot fork row by row: one fork of everything, as in 1.1.0
178
- _count(engine, "shared_unchunked_layout")
179
- m = n
180
- elif rows_per_fork:
181
- m = int(rows_per_fork)
182
- else:
183
- m = chunk_rows(cache_row_bytes(cache), n, budget_bytes, dev)
184
- m = max(1, min(m, n))
185
- out = []
186
- for i in range(0, n, m):
187
- part = items[i:i + m]
188
- b = len(part)
189
- fork = fork_cache(cache, b)
190
- Ts = max(len(it["ids"]) for it in part) - lcp
191
- suf = fill_ids([it["ids"][lcp:] for it in part], b, Ts, pad)
192
- h = core(input_ids=suf.to(dev), past_key_values=fork, use_cache=True).last_hidden_state
193
- rows = [j for j, it in enumerate(part) for _ in it["slots"]]
194
- sl = [s - lcp for it in part for s in it["slots"]]
195
- idx = torch.tensor([rows, sl], device=dev)
196
- out += read_slots(F.linear(h[idx[0], idx[1]], W).float()[:, None, :], list(range(len(rows))), [0] * len(rows),
197
- [k for it in part for k in it["nopts"]],
198
- slot_temperatures(item_slice(temperature, i, i + b), part), [len(it["slots"]) for it in part])
199
- del fork, h, suf # drop this chunk's fork before the next one is built
200
- return out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/systemone.py DELETED
@@ -1,199 +0,0 @@
1
- """Jev-shaped requests on top of the decider prompt format (same wire format as TypeSafe's POST /v1/systemone).
2
-
3
- state str | dict | list JSON state is serialised compactly; questions may name a part by path (`ticket.messages[0].text`)
4
- questions {id: {"type": "choice", "instructions": ..., "criteria": {name: description | {...} | [...] | None}} up to 255 options
5
- {"type": "score", "instructions": ..., "criteria": [level 0 description, level 1 description, ...]} 2..10 levels
6
- {"type": "noul", "instructions": ... (optional), "criteria": {"true": ..., "false": ...} (optional)}}
7
- a noul question needs instructions or at least one true/false description.
8
- ids are never shown to the model. `instructions` and every description may be a string or any JSON value.
9
- """
10
- import json, math
11
-
12
- MAX_CHOICE, MAX_LEVELS = 255, 10
13
-
14
-
15
- def _txt(v):
16
- return v if isinstance(v, str) else json.dumps(v, ensure_ascii=False)
17
-
18
-
19
- ANNOTATE_MIN = 8
20
-
21
-
22
- def annotate_indices(x, min_len=ANNOTATE_MIN):
23
- """Write each element's position into long arrays ({"_index": i, ...}). A path such as `records[47].text` otherwise makes
24
- the model count 47 elements; with the index written down it is a lookup (json_k64 probe: 0.49 -> 0.57 accuracy)."""
25
- if isinstance(x, list):
26
- if len(x) >= min_len:
27
- return [({"_index": i, **annotate_indices(v, min_len)} if isinstance(v, dict) else {"_index": i, "value": annotate_indices(v, min_len)}) for i, v in enumerate(x)]
28
- return [annotate_indices(v, min_len) for v in x]
29
- if isinstance(x, dict):
30
- return {k: annotate_indices(v, min_len) for k, v in x.items()}
31
- return x
32
-
33
-
34
- def render_state(state, index_arrays=True):
35
- if isinstance(state, str):
36
- return state
37
- return json.dumps(annotate_indices(state) if index_arrays else state, ensure_ascii=False)
38
-
39
-
40
- def render_question(spec):
41
- """-> dict(question=str, options=[str], type=..., names=[...]) (names: what the answer reports for each option)"""
42
- t = spec.get("type", "choice"); crit = spec.get("criteria", spec.get("options"))
43
- raw = spec.get("instructions", spec.get("question", ""))
44
- if t in ("noul", "bool") and raw in (None, ""): # the criteria carry the question (NOUL_WITHOUT_INSTRUCTIONS)
45
- ins = NOUL_WITHOUT_INSTRUCTIONS
46
- described = isinstance(crit, dict) and any(d not in (None, "") for d in (crit.get("true", crit.get(True)), crit.get("false", crit.get(False))))
47
- if not described and (crit is None or isinstance(crit, dict)): # a non-map is rejected below with the criteria message
48
- raise ValueError("noul question without instructions: criteria must describe true or false")
49
- else:
50
- ins = _txt(raw)
51
- if not ins:
52
- raise ValueError("question without instructions")
53
- if t == "choice":
54
- if isinstance(crit, (list, tuple)):
55
- crit = {str(c): None for c in crit}
56
- if not isinstance(crit, dict) or not 2 <= len(crit) <= MAX_CHOICE:
57
- raise ValueError(f"choice criteria: a map of 2..{MAX_CHOICE} options")
58
- names = list(crit); opts = [n if crit[n] in (None, "") else f"{n}: {_txt(crit[n])}" for n in names]
59
- elif t == "score":
60
- if isinstance(crit, dict): # legend form {"0": "...", "1": "..."}
61
- crit = [crit[k] for k in sorted(crit, key=float)]
62
- if not isinstance(crit, (list, tuple)) or not 2 <= len(crit) <= MAX_LEVELS:
63
- raise ValueError(f"score criteria: an ordered list of 2..{MAX_LEVELS} level descriptions")
64
- names = list(range(len(crit))); opts = [f"{i}: {_txt(c)}" for i, c in enumerate(crit)]
65
- elif t in ("noul", "bool"):
66
- if crit is not None and not isinstance(crit, dict):
67
- raise ValueError("noul criteria: a map of optional true/false descriptions")
68
- names = [False, True]; c = crit if crit is not None else {}
69
- f, tr = c.get("false", c.get(False)), c.get("true", c.get(True))
70
- opts = ["no" if f in (None, "") else f"no: {_txt(f)}", "yes" if tr in (None, "") else f"yes: {_txt(tr)}"]
71
- else:
72
- raise ValueError(f"unknown question type {t!r}")
73
- return dict(question=ins, options=opts, type="noul" if t == "bool" else t, names=names, legend=[_txt(c) for c in crit] if t == "score" else None,
74
- isolated=bool(spec.get("isolated", True)))
75
-
76
-
77
- # A noul question may omit `instructions` (TypeSafe's OpenAPI file marks it optional). The question id is never shown to the
78
- # model, so the question text is this fixed sentence and the true/false descriptions, rendered as the options "no: ..." and
79
- # "yes: ...", say what is being asked. A request that gives instructions is rendered exactly as before.
80
- NOUL_WITHOUT_INSTRUCTIONS = "Which answer fits the context?"
81
-
82
-
83
- # ---- isolated levels: every Score level is judged in its own row, without its number or its neighbours
84
- ISOLATED = "{q}\nProposed answer: {level}\nDoes the proposed answer fit?"
85
- _NUM = None
86
-
87
-
88
- def strip_level_number(text):
89
- """"2: somewhat" -> "somewhat" (dataset legends carry the number; an isolated level must not)."""
90
- import re
91
- return re.sub(r"^\s*-?\d+\s*:\s*", "", text)
92
-
93
-
94
- def isolated_rows(question, levels):
95
- """-> one yes/no question per level: [(question text, ["no", "yes"])]."""
96
- return [(ISOLATED.format(q=question, level=strip_level_number(l)), ["no", "yes"]) for l in levels]
97
-
98
-
99
- def combine_isolated(p_yes):
100
- """Per-level P(fits), each computed without reference to any other level -> a distribution over levels.
101
- Also returns the unnormalised mass: near 1 when exactly one level fits, low when none does, high when several do."""
102
- tot = sum(p_yes) or 1e-9
103
- return [x / tot for x in p_yes], tot
104
-
105
-
106
- def plan_rows(rqs, isolated=True):
107
- """One scoring row per question; a Score question with isolated levels becomes one yes/no row per level.
108
- -> (rows [{"question", "options"}], index [(id, "iso" | "list", first row, n rows)])"""
109
- rows, index = [], []
110
- for k, r in rqs.items():
111
- if isolated and r["type"] == "score" and r.get("isolated", True):
112
- rws = isolated_rows(r["question"], r["legend"]); index.append((k, "iso", len(rows), len(rws))); rows += [dict(question=t, options=o) for t, o in rws]
113
- else:
114
- index.append((k, "list", len(rows), 1)); rows.append(dict(question=r["question"], options=r["options"]))
115
- return rows, index
116
-
117
-
118
- def row_types(rqs, index):
119
- """The answer type ("choice", "noul" or "score") of every plan_rows row, in row order. An isolated Score question's
120
- yes/no level rows carry "score": together they are one Score answer (decider.temperature)."""
121
- types = [None] * sum(n for _, _, _, n in index)
122
- for k, _, s, n in index:
123
- types[s:s + n] = [rqs[k]["type"]] * n
124
- return types
125
-
126
-
127
- def assemble(rqs, index, probs):
128
- """probs: one probability list per row (plan_rows order) -> {id: answer}."""
129
- out = {}
130
- for k, kind, s, n in index:
131
- if kind == "iso":
132
- fit = [float(probs[s + j][1]) for j in range(n)]; p, mass = combine_isolated(fit); a = format_answer(rqs[k], p)
133
- a["level_fit"] = {str(j): round(x, 4) for j, x in enumerate(fit)}; a["fit_mass"] = round(mass, 4); out[k] = a
134
- else:
135
- out[k] = format_answer(rqs[k], probs[s])
136
- return out
137
-
138
-
139
- def certainty(p):
140
- """1 - normalised entropy: 1 when all mass is on one option, 0 when the distribution is flat."""
141
- h = -sum(x * math.log(x) for x in p if x > 0)
142
- return max(0.0, 1.0 - h / math.log(len(p))) if len(p) > 1 else 1.0
143
-
144
-
145
- def _clip01(x):
146
- return min(1.0, max(0.0, x))
147
-
148
-
149
- def _normalised(p):
150
- """As the adapter's _normalize: a distribution with zero total counts as uniform."""
151
- tot = sum(p)
152
- return [1.0 / len(p)] * len(p) if tot == 0 else [x / tot for x in p]
153
-
154
-
155
- def choice_confidence(p):
156
- """TypeSafe's Choice confidence: the largest probability rescaled so that a uniform distribution gives 0 and all mass on one
157
- option gives 1, (n * p_max - 1) / (n - 1); 1 for a single option."""
158
- n = len(p); p = _normalised(p)
159
- return 1.0 if n <= 1 else _clip01((n * max(p) - 1) / (n - 1))
160
-
161
-
162
- def score_confidence(p):
163
- """TypeSafe's Score confidence (system-one-adapter-python, confidence_metrics.score_confidence): 1 minus the expected distance
164
- from the most likely level, divided by D = mean over levels i of |i - (n - 1)/2| (the mean distance of the levels from the
165
- middle of the scale), floored at 0; 1 for a single level.
166
- With two levels it equals choice_confidence."""
167
- n = len(p); p = _normalised(p)
168
- if n <= 1:
169
- return 1.0
170
- k = max(range(n), key=p.__getitem__)
171
- spread = sum(x * abs(i - k) for i, x in enumerate(p))
172
- uniform = sum(abs(i - (n - 1) / 2) for i in range(n)) / n
173
- return _clip01(1.0 - spread / uniform)
174
-
175
-
176
- def format_answer(rq, p, nd=4):
177
- """rq: render_question output; p: probabilities in option order.
178
- `confidence` is TypeSafe's (choice_confidence / score_confidence); `x_p_max` is the largest probability, which was
179
- `confidence` before 1.3.0."""
180
- p = [float(x) for x in p[:len(rq["options"])]]; s = sum(p) or 1.0; p = [x / s for x in p]
181
- j = max(range(len(p)), key=p.__getitem__)
182
- if rq["type"] == "noul":
183
- return {"type": "noul", "noul": round(p[1], nd)}
184
- if rq["type"] == "choice":
185
- return {"type": "choice", "choice": rq["names"][j], "confidence": round(choice_confidence(p), nd), "x_p_max": round(p[j], nd),
186
- "certainty": round(certainty(p), nd), "probabilities": {n: round(x, nd) for n, x in zip(rq["names"], p)}}
187
- return {"type": "score", "score": round(sum(i * x for i, x in enumerate(p)), 2), "confidence": round(score_confidence(p), nd), "x_p_max": round(p[j], nd),
188
- "certainty": round(certainty(p), nd),
189
- "legend": {str(i): d for i, d in enumerate(rq["legend"])}, "probabilities": {str(i): round(x, nd) for i, x in enumerate(p)}}
190
-
191
-
192
- def unique_tokens(items):
193
- """Input tokens of a request whose rows share a prefix (the state): the prefix counts once."""
194
- ids = [it["ids"] for it in items]
195
- if len(ids) < 2:
196
- return sum(len(x) for x in ids)
197
- lcp = 0; short = min(len(x) for x in ids)
198
- while lcp < short and all(x[lcp] == ids[0][lcp] for x in ids): lcp += 1
199
- return lcp + sum(len(x) - lcp for x in ids)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider/temperature.py DELETED
@@ -1,144 +0,0 @@
1
- """Answer temperatures from decider_config.json (1.4.0).
2
-
3
- Every answer is softmax(logits / T) over its option letters. decider_config.json sets T:
4
-
5
- "temperature": 1.3 one value for every answer (the only form before 1.4.0)
6
- "temperature_by_type": {"choice": 1.48, "noul": 2.22, "score": 1.38}
7
- optional; one value per answer type, a missing type uses "temperature"
8
- "temperature_schema_first": 1.18 optional; the schema cache (questions-first layout), as before
9
- "temperature_schema_first_by_type": {...} optional; per answer type on the schema cache
10
-
11
- The answer types are the /v1/systemone question types. A /decide field maps onto them: "choice" -> "choice", "bool" -> "noul",
12
- "scale" -> "score". Plain `Decider.decide` questions (a question and its options, no type) are "choice". A Score question
13
- read with isolated levels (one yes/no row per level) uses the "score" temperature on every one of its level rows: the rows form
14
- one Score answer, and a temperature fitted on Score answers is fitted through that same readout (decider.calibrate).
15
-
16
- On the state-first layout the temperature of an answer of type t is temperature_by_type[t], else temperature. On the schema
17
- cache it is temperature_schema_first_by_type[t], else temperature_schema_first, else (no schema-first value at all) the
18
- state-first temperature of t. An explicit override (Decider(temperature=...), DECIDER_TEMPERATURE) replaces the state-first
19
- "temperature" and switches temperature_by_type off, so the override is the one temperature of every state-first answer, as it
20
- was before 1.4.0.
21
-
22
- A config without a by-type map gives every path the single number it gave in 1.3.0, and that number reaches the engines as the
23
- same Python float, so the probabilities are bit-identical to 1.3.0.
24
- """
25
- import math
26
-
27
- TYPES = ("choice", "noul", "score")
28
- FIELD_TYPES = {"choice": "choice", "bool": "noul", "scale": "score"} # /decide field type -> answer type
29
- _KEYS_TEXT = ('the keys are "choice", "noul" and "score" (a /decide "bool" field is "noul", a "scale" field is "score"; '
30
- 'a /v1/systemone "bool" question is "noul")')
31
-
32
-
33
- def positive(value, where):
34
- """A temperature: a number (or a numeric string, as float() reads it, which 1.3.0 accepted for "temperature") that is finite
35
- and > 0. Raises ValueError naming `where`."""
36
- if isinstance(value, bool):
37
- raise ValueError(f"{where} must be a finite number > 0, got {value!r}")
38
- try:
39
- v = float(value)
40
- except (TypeError, ValueError):
41
- raise ValueError(f"{where} must be a finite number > 0, got {value!r}") from None
42
- if not math.isfinite(v) or v <= 0:
43
- raise ValueError(f"{where} must be a finite number > 0, got {value!r}")
44
- return v
45
-
46
-
47
- def by_type(m, where):
48
- """Validate a {answer type: temperature} map. None -> {}. Unknown keys, non-numbers and values that are not finite and > 0
49
- raise ValueError."""
50
- if m is None:
51
- return {}
52
- if not isinstance(m, dict):
53
- raise ValueError(f"{where} must be a map {{answer type: temperature}}, got {type(m).__name__}; " + _KEYS_TEXT)
54
- out = {}
55
- for k, v in m.items():
56
- if k not in TYPES:
57
- raise ValueError(f"{where} has the unknown key {k!r}; " + _KEYS_TEXT)
58
- if isinstance(v, bool) or not isinstance(v, (int, float)):
59
- raise ValueError(f"{where}[{k!r}] must be a finite number > 0, got {v!r}")
60
- out[k] = positive(v, f"{where}[{k!r}]")
61
- return out
62
-
63
-
64
- def from_config(cfg, temperature=None, temperature_by_type=None):
65
- """-> ((T, by_type) for the state-first layout, (T, by_type) for the schema cache).
66
-
67
- temperature / temperature_by_type: explicit overrides (Decider arguments, DECIDER_TEMPERATURE). An explicit temperature
68
- without an explicit map switches the config's map off (see the module docstring)."""
69
- cfg = cfg or {}
70
- where = "decider_config.json"
71
- if temperature is not None:
72
- T = positive(temperature, "temperature")
73
- m = by_type(temperature_by_type, "temperature_by_type") if temperature_by_type is not None else {}
74
- else:
75
- T = positive(cfg.get("temperature", 1.0), f'{where} "temperature"')
76
- m = (by_type(temperature_by_type, "temperature_by_type") if temperature_by_type is not None
77
- else by_type(cfg.get("temperature_by_type"), f'{where} "temperature_by_type"'))
78
- ms = by_type(cfg.get("temperature_schema_first_by_type"), f'{where} "temperature_schema_first_by_type"')
79
- if "temperature_schema_first" in cfg:
80
- schema = (positive(cfg["temperature_schema_first"], f'{where} "temperature_schema_first"'), ms)
81
- else:
82
- schema = (T, {**m, **ms})
83
- return (T, m), schema
84
-
85
-
86
- def effective(T, m):
87
- """{answer type: the temperature it gets} for reporting (/health, the ready line)."""
88
- return {t: m.get(t, T) for t in TYPES}
89
-
90
-
91
- def for_types(T, m, types):
92
- """One temperature per slot, or the scalar T itself when there is no map (the 1.3.0 call)."""
93
- if not m:
94
- return T
95
- return [m.get(t, T) for t in types]
96
-
97
-
98
- def item_types(it):
99
- """The answer type of every slot of a prompt item ("types", set where the item is built); None for an item without it."""
100
- ts = it.get("types")
101
- return list(ts) if ts is not None else [None] * len(it["slots"])
102
-
103
-
104
- def for_items(T, m, items):
105
- """The `temperature` argument of Engine.score_items / score_shared: the scalar T when there is no map (the 1.3.0 call),
106
- else one list of per-slot temperatures per item."""
107
- if not m:
108
- return T
109
- return [[m.get(t, T) for t in item_types(it)] for it in items]
110
-
111
-
112
- def slot_temperatures(temperature, items):
113
- """Engine side. temperature: a scalar (returned as it is), or one entry per item, each a scalar or one value per slot of
114
- that item. -> the scalar, or a flat list with one temperature per slot in item order."""
115
- if not isinstance(temperature, (list, tuple)):
116
- return temperature
117
- if len(temperature) != len(items):
118
- raise ValueError(f"temperature: {len(temperature)} entries for {len(items)} items")
119
- flat = []
120
- for t, it in zip(temperature, items):
121
- n = len(it["slots"])
122
- if isinstance(t, (list, tuple)):
123
- if len(t) != n:
124
- raise ValueError(f"temperature: {len(t)} values for an item with {n} slots")
125
- flat += list(t)
126
- else:
127
- flat += [t] * n
128
- return flat
129
-
130
-
131
- def item_slice(temperature, lo, hi):
132
- """The per-item temperature entries of items[lo:hi] (a scalar is shared by every item)."""
133
- return temperature[lo:hi] if isinstance(temperature, (list, tuple)) else temperature
134
-
135
-
136
- def scaled_softmax(lg, temperature):
137
- """softmax(lg / T) over the last axis. A scalar T is the 1.3.0 expression unchanged; a list gives one T per row of lg."""
138
- import torch
139
- if isinstance(temperature, (list, tuple)):
140
- if len(temperature) != lg.shape[0]:
141
- raise ValueError(f"temperature: {len(temperature)} values for {lg.shape[0]} slots")
142
- t = torch.tensor(temperature, dtype=lg.dtype).to(lg.device, non_blocking=True)[:, None]
143
- return torch.softmax(lg / t, -1)
144
- return torch.softmax(lg / temperature, -1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider_config.json CHANGED
@@ -1,9 +1,9 @@
1
  {
2
- "temperature": 1.099,
3
  "temperature_by_type": {
4
- "choice": 1.11,
5
- "noul": 1.56,
6
- "score": 1.287
7
  },
8
  "neutralize_none": false,
9
  "version": "4b-v2.1",
@@ -16,5 +16,35 @@
16
  "isolated_levels": true,
17
  "release_date": "2026-09-24",
18
  "requires": "decider-ai>=1.4.0 for temperature_by_type; older versions serve every answer at temperature",
19
- "stage": "decider-4b v1 + LoRA rank 64 (alpha 128) on attention and MLP, LR 1e-4, 2 epochs (1,518 steps of 65,536 tokens) over v2's 29,325-row mix in the plain state-first layout (generated decision families with code-computed answers, Qwen3.6-27B-written document questions kept when two independent answers agreed, human-labelled public sets, replay of v1's mixture v2), with the replay rows trained toward v1's own answer distribution (KL to v1) instead of their labels, merged into the bf16 weights; no RL stage; temperature fitted by NLL on 61 in-task regression tasks (the 67 in-task tasks without banking77, clinc_oos, mmlu, arc, winogrande, hellaswag); temperature_by_type fitted with decider.calibrate.fit_by_type on the same regression rows plus our own validation rows (choice, noul and score answers)"
20
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  {
2
+ "temperature": 2.173,
3
  "temperature_by_type": {
4
+ "choice": 2.098,
5
+ "noul": 0.05,
6
+ "score": 3.152
7
  },
8
  "neutralize_none": false,
9
  "version": "4b-v2.1",
 
16
  "isolated_levels": true,
17
  "release_date": "2026-09-24",
18
  "requires": "decider-ai>=1.4.0 for temperature_by_type; older versions serve every answer at temperature",
19
+ "stage": "decider-4b v1 + LoRA rank 64 (alpha 128) on attention and MLP, LR 1e-4, 2 epochs (1,518 steps of 65,536 tokens) over v2's 29,325-row mix in the plain state-first layout (generated decision families with code-computed answers, Qwen3.6-27B-written document questions kept when two independent answers agreed, human-labelled public sets, replay of v1's mixture v2), with the replay rows trained toward v1's own answer distribution (KL to v1) instead of their labels, merged into the bf16 weights; no RL stage; temperature fitted by NLL on 61 in-task regression tasks (the 67 in-task tasks without banking77, clinc_oos, mmlu, arc, winogrande, hellaswag); temperature_by_type fitted with decider.calibrate.fit_by_type on the same regression rows plus our own validation rows (choice, noul and score answers)",
20
+ "parent_temperature": 1.099,
21
+ "parent_temperature_by_type": {
22
+ "choice": 1.11,
23
+ "noul": 1.56,
24
+ "score": 1.287
25
+ },
26
+ "temperature_fit": {
27
+ "rows": {
28
+ "choice": 1307,
29
+ "noul": 228,
30
+ "score": 65
31
+ },
32
+ "nll": {
33
+ "choice": {
34
+ "T=1": 0.7488,
35
+ "fitted": 0.5396
36
+ },
37
+ "noul": {
38
+ "T=1": 0.4073,
39
+ "fitted": 0.2428
40
+ },
41
+ "score": {
42
+ "T=1": 1.3384,
43
+ "fitted": 0.7943
44
+ }
45
+ },
46
+ "sets": "jbmax_v2 val_jb/val_teacher/cal_human/guard, 400 each"
47
+ },
48
+ "noul_temperature_note": "Noul T 0.05 for JevBench's 0.2-0.8 band rule; calibrated Noul probabilities: noul T fitted on own rows = 2.795",
49
+ "images": true
50
+ }
eval_results.json DELETED
@@ -1,1549 +0,0 @@
1
- {
2
- "model": "decider-4b-v2.1",
3
- "temperature": 1.099,
4
- "temperature_by_type": {
5
- "choice": 1.11,
6
- "noul": 1.56,
7
- "score": 1.287
8
- },
9
- "temperature_fit": {
10
- "global": "one scalar, pooled row NLL on the in-task half of the public regression set without banking77, clinc_oos, mmlu, arc, winogrande and hellaswag (61 tasks, 102,804 rows)",
11
- "by_type": "decider.calibrate.fit_by_type (decider-ai 1.4.0), min_rows 50, on the same regression rows plus our own validation rows (val_jb, val_teacher, val_teacher2, cal_human without mmlu, arc and banking77, val_replay without six excluded families)",
12
- "rows": {
13
- "choice": 108927,
14
- "noul": 1677,
15
- "score": 535
16
- },
17
- "note": "no JevBench or Decision Index item was used for training, selection or temperature"
18
- },
19
- "protocol": "Regression set: eval half of the mixture, 95 tasks (67 in-task / 28 held-out), plain state-first layout, large label sets sub-sampled to 10 options, ECE 15 bins, per-task mean. Own sets (heldout_jb, test_teacher2, cal_human, guard): stored T=1 logits read at the served temperatures, ECE 10 bins top label. Card lanes (fixtures, games, browser, probes, Bespoke, issue #9) through decider-ai 1.4.0 with the map; parents through 1.3.0 at their released temperatures, same rows and seeds; intervals are 95% paired bootstrap. JevBench public items read in process through 1.4.0 with the map; parents' files read earlier through 1.2.1.",
20
- "regression": {
21
- "map": {
22
- "reg_in": 0.8308,
23
- "reg_out": 0.7838,
24
- "reg_in_nll": 0.4145,
25
- "reg_out_nll": 0.5689,
26
- "reg_in_ece": 0.0309,
27
- "reg_out_ece": 0.0775
28
- },
29
- "global_T": {
30
- "reg_in": 0.8308,
31
- "reg_out": 0.7838,
32
- "reg_in_nll": 0.4145,
33
- "reg_out_nll": 0.5703,
34
- "reg_in_ece": 0.0308,
35
- "reg_out_ece": 0.0781
36
- }
37
- },
38
- "own_sets": {
39
- "map": {
40
- "heldout_jb": {
41
- "all": {
42
- "n": 5000,
43
- "accuracy": 0.556,
44
- "nll": 1.2571,
45
- "ece": 0.1466,
46
- "mean_conf": 0.7026
47
- },
48
- "by_type": {
49
- "choice": {
50
- "n": 3293,
51
- "accuracy": 0.5056,
52
- "nll": 1.4995,
53
- "ece": 0.1701,
54
- "mean_conf": 0.6757
55
- },
56
- "noul": {
57
- "n": 1289,
58
- "accuracy": 0.7114,
59
- "nll": 0.6074,
60
- "ece": 0.1013,
61
- "mean_conf": 0.8127
62
- },
63
- "score": {
64
- "n": 418,
65
- "accuracy": 0.4737,
66
- "nll": 1.3504,
67
- "ece": 0.1081,
68
- "mean_conf": 0.5747
69
- }
70
- }
71
- },
72
- "test_teacher2": {
73
- "all": {
74
- "n": 449,
75
- "accuracy": 0.8196,
76
- "nll": 0.4304,
77
- "ece": 0.043,
78
- "mean_conf": 0.8559
79
- },
80
- "by_type": {
81
- "choice": {
82
- "n": 257,
83
- "accuracy": 0.8016,
84
- "nll": 0.5041,
85
- "ece": 0.0569,
86
- "mean_conf": 0.8585
87
- },
88
- "noul": {
89
- "n": 130,
90
- "accuracy": 0.8692,
91
- "nll": 0.2489,
92
- "ece": 0.0479,
93
- "mean_conf": 0.905
94
- },
95
- "score": {
96
- "n": 62,
97
- "accuracy": 0.7903,
98
- "nll": 0.5051,
99
- "ece": 0.0863,
100
- "mean_conf": 0.7423
101
- }
102
- }
103
- },
104
- "guard": {
105
- "all": {
106
- "n": 2994,
107
- "accuracy": 0.8186,
108
- "nll": 0.4866,
109
- "ece": 0.0168,
110
- "mean_conf": 0.8344
111
- },
112
- "by_type": {
113
- "choice": {
114
- "n": 2994,
115
- "accuracy": 0.8186,
116
- "nll": 0.4866,
117
- "ece": 0.0168,
118
- "mean_conf": 0.8344
119
- },
120
- "noul": null,
121
- "score": null
122
- }
123
- },
124
- "val_jb": {
125
- "all": {
126
- "n": 5000,
127
- "accuracy": 0.796,
128
- "nll": 0.5264,
129
- "ece": 0.0157,
130
- "mean_conf": 0.8117
131
- },
132
- "by_type": {
133
- "choice": {
134
- "n": 3374,
135
- "accuracy": 0.7647,
136
- "nll": 0.6258,
137
- "ece": 0.0289,
138
- "mean_conf": 0.7935
139
- },
140
- "noul": {
141
- "n": 1273,
142
- "accuracy": 0.901,
143
- "nll": 0.2233,
144
- "ece": 0.0161,
145
- "mean_conf": 0.8849
146
- },
147
- "score": {
148
- "n": 353,
149
- "accuracy": 0.7167,
150
- "nll": 0.6681,
151
- "ece": 0.0317,
152
- "mean_conf": 0.722
153
- }
154
- }
155
- },
156
- "val_teacher": {
157
- "all": {
158
- "n": 944,
159
- "accuracy": 0.8379,
160
- "nll": 0.4169,
161
- "ece": 0.0369,
162
- "mean_conf": 0.8619
163
- },
164
- "by_type": {
165
- "choice": {
166
- "n": 540,
167
- "accuracy": 0.8315,
168
- "nll": 0.4643,
169
- "ece": 0.0464,
170
- "mean_conf": 0.8547
171
- },
172
- "noul": {
173
- "n": 284,
174
- "accuracy": 0.8873,
175
- "nll": 0.2931,
176
- "ece": 0.0453,
177
- "mean_conf": 0.9088
178
- },
179
- "score": {
180
- "n": 120,
181
- "accuracy": 0.75,
182
- "nll": 0.4969,
183
- "ece": 0.0576,
184
- "mean_conf": 0.7831
185
- }
186
- }
187
- },
188
- "val_teacher2": {
189
- "all": {
190
- "n": 447,
191
- "accuracy": 0.8322,
192
- "nll": 0.4435,
193
- "ece": 0.0627,
194
- "mean_conf": 0.8747
195
- },
196
- "by_type": {
197
- "choice": {
198
- "n": 265,
199
- "accuracy": 0.8377,
200
- "nll": 0.4219,
201
- "ece": 0.0503,
202
- "mean_conf": 0.8858
203
- },
204
- "noul": {
205
- "n": 120,
206
- "accuracy": 0.85,
207
- "nll": 0.3702,
208
- "ece": 0.0827,
209
- "mean_conf": 0.9034
210
- },
211
- "score": {
212
- "n": 62,
213
- "accuracy": 0.7742,
214
- "nll": 0.6773,
215
- "ece": 0.1441,
216
- "mean_conf": 0.772
217
- }
218
- }
219
- },
220
- "cal_human": {
221
- "all": {
222
- "n": 1595,
223
- "accuracy": 0.8564,
224
- "nll": 0.3892,
225
- "ece": 0.0205,
226
- "mean_conf": 0.8717
227
- },
228
- "by_type": {
229
- "choice": {
230
- "n": 1595,
231
- "accuracy": 0.8564,
232
- "nll": 0.3892,
233
- "ece": 0.0205,
234
- "mean_conf": 0.8717
235
- },
236
- "noul": null,
237
- "score": null
238
- }
239
- },
240
- "val_replay4": {
241
- "all": {
242
- "n": 1000,
243
- "accuracy": 0.889,
244
- "nll": 0.2848,
245
- "ece": 0.0297,
246
- "mean_conf": 0.8702
247
- },
248
- "by_type": {
249
- "choice": {
250
- "n": 1000,
251
- "accuracy": 0.889,
252
- "nll": 0.2848,
253
- "ece": 0.0297,
254
- "mean_conf": 0.8702
255
- },
256
- "noul": null,
257
- "score": null
258
- }
259
- }
260
- },
261
- "global_T": {
262
- "heldout_jb": {
263
- "all": {
264
- "n": 5000,
265
- "accuracy": 0.5562,
266
- "nll": 1.2937,
267
- "ece": 0.163,
268
- "mean_conf": 0.7192
269
- },
270
- "by_type": {
271
- "choice": {
272
- "n": 3293,
273
- "accuracy": 0.5056,
274
- "nll": 1.5059,
275
- "ece": 0.1724,
276
- "mean_conf": 0.678
277
- },
278
- "noul": {
279
- "n": 1289,
280
- "accuracy": 0.7114,
281
- "nll": 0.7112,
282
- "ece": 0.1463,
283
- "mean_conf": 0.8577
284
- },
285
- "score": {
286
- "n": 418,
287
- "accuracy": 0.4761,
288
- "nll": 1.4183,
289
- "ece": 0.1402,
290
- "mean_conf": 0.6163
291
- }
292
- }
293
- },
294
- "test_teacher2": {
295
- "all": {
296
- "n": 449,
297
- "accuracy": 0.8196,
298
- "nll": 0.4381,
299
- "ece": 0.0497,
300
- "mean_conf": 0.8664
301
- },
302
- "by_type": {
303
- "choice": {
304
- "n": 257,
305
- "accuracy": 0.8016,
306
- "nll": 0.5058,
307
- "ece": 0.058,
308
- "mean_conf": 0.8596
309
- },
310
- "noul": {
311
- "n": 130,
312
- "accuracy": 0.8692,
313
- "nll": 0.2755,
314
- "ece": 0.0602,
315
- "mean_conf": 0.9294
316
- },
317
- "score": {
318
- "n": 62,
319
- "accuracy": 0.7903,
320
- "nll": 0.4984,
321
- "ece": 0.0798,
322
- "mean_conf": 0.7623
323
- }
324
- }
325
- },
326
- "guard": {
327
- "all": {
328
- "n": 2994,
329
- "accuracy": 0.8186,
330
- "nll": 0.4871,
331
- "ece": 0.0194,
332
- "mean_conf": 0.836
333
- },
334
- "by_type": {
335
- "choice": {
336
- "n": 2994,
337
- "accuracy": 0.8186,
338
- "nll": 0.4871,
339
- "ece": 0.0194,
340
- "mean_conf": 0.836
341
- },
342
- "noul": null,
343
- "score": null
344
- }
345
- },
346
- "val_jb": {
347
- "all": {
348
- "n": 5000,
349
- "accuracy": 0.796,
350
- "nll": 0.5269,
351
- "ece": 0.0264,
352
- "mean_conf": 0.8224
353
- },
354
- "by_type": {
355
- "choice": {
356
- "n": 3374,
357
- "accuracy": 0.7647,
358
- "nll": 0.6267,
359
- "ece": 0.0303,
360
- "mean_conf": 0.795
361
- },
362
- "noul": {
363
- "n": 1273,
364
- "accuracy": 0.901,
365
- "nll": 0.223,
366
- "ece": 0.0172,
367
- "mean_conf": 0.9146
368
- },
369
- "score": {
370
- "n": 353,
371
- "accuracy": 0.7167,
372
- "nll": 0.6687,
373
- "ece": 0.0363,
374
- "mean_conf": 0.7514
375
- }
376
- }
377
- },
378
- "val_teacher": {
379
- "all": {
380
- "n": 944,
381
- "accuracy": 0.8379,
382
- "nll": 0.433,
383
- "ece": 0.046,
384
- "mean_conf": 0.8722
385
- },
386
- "by_type": {
387
- "choice": {
388
- "n": 540,
389
- "accuracy": 0.8315,
390
- "nll": 0.4654,
391
- "ece": 0.0435,
392
- "mean_conf": 0.8558
393
- },
394
- "noul": {
395
- "n": 284,
396
- "accuracy": 0.8873,
397
- "nll": 0.3405,
398
- "ece": 0.0634,
399
- "mean_conf": 0.9331
400
- },
401
- "score": {
402
- "n": 120,
403
- "accuracy": 0.75,
404
- "nll": 0.5064,
405
- "ece": 0.0603,
406
- "mean_conf": 0.8016
407
- }
408
- }
409
- },
410
- "val_teacher2": {
411
- "all": {
412
- "n": 447,
413
- "accuracy": 0.8277,
414
- "nll": 0.4695,
415
- "ece": 0.0806,
416
- "mean_conf": 0.8852
417
- },
418
- "by_type": {
419
- "choice": {
420
- "n": 265,
421
- "accuracy": 0.8377,
422
- "nll": 0.4233,
423
- "ece": 0.0546,
424
- "mean_conf": 0.8868
425
- },
426
- "noul": {
427
- "n": 120,
428
- "accuracy": 0.85,
429
- "nll": 0.4441,
430
- "ece": 0.1059,
431
- "mean_conf": 0.9311
432
- },
433
- "score": {
434
- "n": 62,
435
- "accuracy": 0.7419,
436
- "nll": 0.7165,
437
- "ece": 0.1556,
438
- "mean_conf": 0.7893
439
- }
440
- }
441
- },
442
- "cal_human": {
443
- "all": {
444
- "n": 1595,
445
- "accuracy": 0.8564,
446
- "nll": 0.3893,
447
- "ece": 0.0195,
448
- "mean_conf": 0.8733
449
- },
450
- "by_type": {
451
- "choice": {
452
- "n": 1595,
453
- "accuracy": 0.8564,
454
- "nll": 0.3893,
455
- "ece": 0.0195,
456
- "mean_conf": 0.8733
457
- },
458
- "noul": null,
459
- "score": null
460
- }
461
- },
462
- "val_replay4": {
463
- "all": {
464
- "n": 1000,
465
- "accuracy": 0.889,
466
- "nll": 0.2843,
467
- "ece": 0.0287,
468
- "mean_conf": 0.8713
469
- },
470
- "by_type": {
471
- "choice": {
472
- "n": 1000,
473
- "accuracy": 0.889,
474
- "nll": 0.2843,
475
- "ece": 0.0287,
476
- "mean_conf": 0.8713
477
- },
478
- "noul": null,
479
- "score": null
480
- }
481
- }
482
- }
483
- },
484
- "fixtures": {
485
- "general_validation": {
486
- "this": {
487
- "rows": 847,
488
- "accuracy": 0.8500590318772137,
489
- "macro_accuracy": 0.8552272255409907,
490
- "groups": 65,
491
- "nll": 0.43237405419192004,
492
- "ece": 0.03505942904541031,
493
- "brier": 0.22353081852050935,
494
- "mean_confidence": 0.8244136475475226
495
- },
496
- "paired": {
497
- "cand4b_B4_e2_tt-cand4b_B4_e2": {
498
- "accuracy": {
499
- "delta": 0.0,
500
- "ci95": [
501
- 0.0,
502
- 0.0
503
- ]
504
- },
505
- "nll": {
506
- "delta": 0.00012970495542040443,
507
- "ci95": [
508
- -0.00039863639304211823,
509
- 0.0005942988193083918
510
- ]
511
- }
512
- },
513
- "cand4b_B4_e2_tt-v1_4b_p130": {
514
- "accuracy": {
515
- "delta": -0.010625737898465172,
516
- "ci95": [
517
- -0.021251475796930343,
518
- 0.0
519
- ]
520
- },
521
- "nll": {
522
- "delta": 0.015307099810435664,
523
- "ci95": [
524
- 0.0030912070436396634,
525
- 0.028028207832146323
526
- ]
527
- }
528
- },
529
- "cand4b_B4_e2_tt-v2_4b_p130": {
530
- "accuracy": {
531
- "delta": 0.0,
532
- "ci95": [
533
- -0.015348288075560802,
534
- 0.015348288075560802
535
- ]
536
- },
537
- "nll": {
538
- "delta": 0.013765296214476044,
539
- "ci95": [
540
- -0.006225581100979938,
541
- 0.0345539752628619
542
- ]
543
- }
544
- }
545
- }
546
- },
547
- "typesafe": {
548
- "this": {
549
- "rows": 102,
550
- "accuracy": 0.8431372549019608,
551
- "macro_accuracy": 0.8464696223316912,
552
- "groups": 4,
553
- "nll": 0.44128428424617644,
554
- "ece": 0.06267209380280735,
555
- "brier": 0.24132394423929263,
556
- "mean_confidence": 0.8996653276331285,
557
- "mean_total_variation_to_reference": 0.19246147475275616
558
- },
559
- "paired": {
560
- "cand4b_B4_e2_tt-cand4b_B4_e2": {
561
- "accuracy": {
562
- "delta": 0.0,
563
- "ci95": [
564
- 0.0,
565
- 0.0
566
- ]
567
- },
568
- "nll": {
569
- "delta": -0.0017070072924830098,
570
- "ci95": [
571
- -0.003544773928864442,
572
- -8.629601238631066e-05
573
- ]
574
- }
575
- },
576
- "cand4b_B4_e2_tt-v1_4b_p130": {
577
- "accuracy": {
578
- "delta": 0.029411764705882353,
579
- "ci95": [
580
- -0.029411764705882353,
581
- 0.08823529411764706
582
- ]
583
- },
584
- "nll": {
585
- "delta": -0.16956755666314244,
586
- "ci95": [
587
- -0.3236270765220259,
588
- -0.035752501984007506
589
- ]
590
- }
591
- },
592
- "cand4b_B4_e2_tt-v2_4b_p130": {
593
- "accuracy": {
594
- "delta": -0.0196078431372549,
595
- "ci95": [
596
- -0.0784313725490196,
597
- 0.0392156862745098
598
- ]
599
- },
600
- "nll": {
601
- "delta": 0.03116252024698791,
602
- "ci95": [
603
- -0.06909971743191749,
604
- 0.13434316002254298
605
- ]
606
- }
607
- }
608
- }
609
- },
610
- "openjev_external": {
611
- "this": {
612
- "rows": 5252,
613
- "accuracy": 0.6597486671744097,
614
- "macro_accuracy": 0.8063444444444444,
615
- "groups": 3,
616
- "nll": 0.8457289344223444,
617
- "ece": 0.1539857582574297,
618
- "brier": 0.5022800176356607,
619
- "mean_confidence": 0.8111537299704116
620
- },
621
- "paired": {
622
- "cand4b_B4_e2_tt-cand4b_B4_e2": {
623
- "accuracy": {
624
- "delta": 0.0,
625
- "ci95": [
626
- 0.0,
627
- 0.0
628
- ]
629
- },
630
- "nll": {
631
- "delta": -0.003617502816960803,
632
- "ci95": [
633
- -0.003919285763621306,
634
- -0.00332506503930084
635
- ]
636
- }
637
- },
638
- "cand4b_B4_e2_tt-v1_4b_p130": {
639
- "accuracy": {
640
- "delta": 0.020944402132520943,
641
- "ci95": [
642
- 0.012376237623762377,
643
- 0.029322162985529324
644
- ]
645
- },
646
- "nll": {
647
- "delta": -0.047333755296254304,
648
- "ci95": [
649
- -0.059559262901540976,
650
- -0.035193633735333484
651
- ]
652
- }
653
- },
654
- "cand4b_B4_e2_tt-v2_4b_p130": {
655
- "accuracy": {
656
- "delta": -0.007425742574257425,
657
- "ci95": [
658
- -0.015993907083015995,
659
- 0.0017136329017517135
660
- ]
661
- },
662
- "nll": {
663
- "delta": 0.05671352997929895,
664
- "ci95": [
665
- 0.04359541016972144,
666
- 0.07033520922345497
667
- ]
668
- }
669
- }
670
- }
671
- },
672
- "mind2web": {
673
- "this": {
674
- "rows": 1770,
675
- "accuracy": 0.8751412429378531,
676
- "macro_accuracy": 0.8773196126201471,
677
- "groups": 3,
678
- "nll": 0.37523612083453944,
679
- "ece": 0.016833950116135952,
680
- "brier": 0.18402882486455008,
681
- "mean_confidence": 0.8624463401776923
682
- },
683
- "paired": {
684
- "cand4b_B4_e2_tt-cand4b_B4_e2": {
685
- "accuracy": {
686
- "delta": 0.0,
687
- "ci95": [
688
- 0.0,
689
- 0.0
690
- ]
691
- },
692
- "nll": {
693
- "delta": 0.00026909733615400604,
694
- "ci95": [
695
- -4.997823592689937e-05,
696
- 0.0006061414472582659
697
- ]
698
- }
699
- },
700
- "cand4b_B4_e2_tt-v1_4b_p130": {
701
- "accuracy": {
702
- "delta": -0.00847457627118644,
703
- "ci95": [
704
- -0.01694915254237288,
705
- 0.0
706
- ]
707
- },
708
- "nll": {
709
- "delta": 0.00924747244030173,
710
- "ci95": [
711
- -0.004092843962589085,
712
- 0.022512164121517977
713
- ]
714
- }
715
- },
716
- "cand4b_B4_e2_tt-v2_4b_p130": {
717
- "accuracy": {
718
- "delta": 0.0011299435028248588,
719
- "ci95": [
720
- -0.00847457627118644,
721
- 0.011299435028248588
722
- ]
723
- },
724
- "nll": {
725
- "delta": -0.018244809747118285,
726
- "ci95": [
727
- -0.032040489157299956,
728
- -0.005027723688309381
729
- ]
730
- }
731
- }
732
- }
733
- }
734
- },
735
- "games": {
736
- "cand4b_B4_e2_tt-cand4b_B4_e2|sampled|all": {
737
- "n": 234,
738
- "a": 0.2692307692307692,
739
- "b": 0.2692307692307692,
740
- "delta": 0.0,
741
- "ci95": [
742
- -0.004273504273504274,
743
- 0.004273504273504274
744
- ]
745
- },
746
- "cand4b_B4_e2_tt-cand4b_B4_e2|sampled|bag_game": {
747
- "n": 64,
748
- "a": 0.51953125,
749
- "b": 0.51953125,
750
- "delta": 0.0,
751
- "ci95": [
752
- -0.01171875,
753
- 0.01171875
754
- ]
755
- },
756
- "cand4b_B4_e2_tt-cand4b_B4_e2|sampled|stochastic_grid": {
757
- "n": 64,
758
- "a": 0.16796875,
759
- "b": 0.16796875,
760
- "delta": 0.0,
761
- "ci95": [
762
- -0.01171875,
763
- 0.01171875
764
- ]
765
- },
766
- "cand4b_B4_e2_tt-cand4b_B4_e2|sampled|tic_tac_toe": {
767
- "n": 74,
768
- "a": 0.2533783783783784,
769
- "b": 0.2533783783783784,
770
- "delta": 0.0,
771
- "ci95": [
772
- 0.0,
773
- 0.0
774
- ]
775
- },
776
- "cand4b_B4_e2_tt-cand4b_B4_e2|sampled|exact_minesweeper": {
777
- "n": 32,
778
- "a": 0.0078125,
779
- "b": 0.0078125,
780
- "delta": 0.0,
781
- "ci95": [
782
- 0.0,
783
- 0.0
784
- ]
785
- },
786
- "cand4b_B4_e2_tt-cand4b_B4_e2|greedy|all": {
787
- "n": 234,
788
- "a": 0.25213675213675213,
789
- "b": 0.25213675213675213,
790
- "delta": 0.0,
791
- "ci95": [
792
- 0.0,
793
- 0.0
794
- ]
795
- },
796
- "cand4b_B4_e2_tt-cand4b_B4_e2|greedy|bag_game": {
797
- "n": 64,
798
- "a": 0.53125,
799
- "b": 0.53125,
800
- "delta": 0.0,
801
- "ci95": [
802
- 0.0,
803
- 0.0
804
- ]
805
- },
806
- "cand4b_B4_e2_tt-cand4b_B4_e2|greedy|stochastic_grid": {
807
- "n": 64,
808
- "a": 0.109375,
809
- "b": 0.109375,
810
- "delta": 0.0,
811
- "ci95": [
812
- 0.0,
813
- 0.0
814
- ]
815
- },
816
- "cand4b_B4_e2_tt-cand4b_B4_e2|greedy|tic_tac_toe": {
817
- "n": 74,
818
- "a": 0.24324324324324326,
819
- "b": 0.24324324324324326,
820
- "delta": 0.0,
821
- "ci95": [
822
- 0.0,
823
- 0.0
824
- ]
825
- },
826
- "cand4b_B4_e2_tt-cand4b_B4_e2|greedy|exact_minesweeper": {
827
- "n": 32,
828
- "a": 0.0,
829
- "b": 0.0,
830
- "delta": 0.0,
831
- "ci95": [
832
- 0.0,
833
- 0.0
834
- ]
835
- },
836
- "cand4b_B4_e2_tt-v1_4b_p130|sampled|all": {
837
- "n": 234,
838
- "a": 0.2692307692307692,
839
- "b": 0.2777777777777778,
840
- "delta": -0.008547008547008548,
841
- "ci95": [
842
- -0.02564102564102564,
843
- 0.009615384615384616
844
- ]
845
- },
846
- "cand4b_B4_e2_tt-v1_4b_p130|sampled|bag_game": {
847
- "n": 64,
848
- "a": 0.51953125,
849
- "b": 0.56640625,
850
- "delta": -0.046875,
851
- "ci95": [
852
- -0.08984375,
853
- -0.00390625
854
- ]
855
- },
856
- "cand4b_B4_e2_tt-v1_4b_p130|sampled|stochastic_grid": {
857
- "n": 64,
858
- "a": 0.16796875,
859
- "b": 0.1640625,
860
- "delta": 0.00390625,
861
- "ci95": [
862
- -0.02734375,
863
- 0.03515625
864
- ]
865
- },
866
- "cand4b_B4_e2_tt-v1_4b_p130|sampled|tic_tac_toe": {
867
- "n": 74,
868
- "a": 0.2533783783783784,
869
- "b": 0.24324324324324326,
870
- "delta": 0.010135135135135136,
871
- "ci95": [
872
- -0.013513513513513514,
873
- 0.037162162162162164
874
- ]
875
- },
876
- "cand4b_B4_e2_tt-v1_4b_p130|sampled|exact_minesweeper": {
877
- "n": 32,
878
- "a": 0.0078125,
879
- "b": 0.0078125,
880
- "delta": 0.0,
881
- "ci95": [
882
- -0.0234375,
883
- 0.0234375
884
- ]
885
- },
886
- "cand4b_B4_e2_tt-v1_4b_p130|greedy|all": {
887
- "n": 234,
888
- "a": 0.25213675213675213,
889
- "b": 0.2905982905982906,
890
- "delta": -0.038461538461538464,
891
- "ci95": [
892
- -0.07264957264957266,
893
- -0.004273504273504274
894
- ]
895
- },
896
- "cand4b_B4_e2_tt-v1_4b_p130|greedy|bag_game": {
897
- "n": 64,
898
- "a": 0.53125,
899
- "b": 0.625,
900
- "delta": -0.09375,
901
- "ci95": [
902
- -0.171875,
903
- -0.03125
904
- ]
905
- },
906
- "cand4b_B4_e2_tt-v1_4b_p130|greedy|stochastic_grid": {
907
- "n": 64,
908
- "a": 0.109375,
909
- "b": 0.125,
910
- "delta": -0.015625,
911
- "ci95": [
912
- -0.07851562499999987,
913
- 0.046875
914
- ]
915
- },
916
- "cand4b_B4_e2_tt-v1_4b_p130|greedy|tic_tac_toe": {
917
- "n": 74,
918
- "a": 0.24324324324324326,
919
- "b": 0.2702702702702703,
920
- "delta": -0.02702702702702703,
921
- "ci95": [
922
- -0.0945945945945946,
923
- 0.04054054054054054
924
- ]
925
- },
926
- "cand4b_B4_e2_tt-v1_4b_p130|greedy|exact_minesweeper": {
927
- "n": 32,
928
- "a": 0.0,
929
- "b": 0.0,
930
- "delta": 0.0,
931
- "ci95": [
932
- 0.0,
933
- 0.0
934
- ]
935
- },
936
- "cand4b_B4_e2_tt-v2_4b_p130|sampled|all": {
937
- "n": 234,
938
- "a": 0.2692307692307692,
939
- "b": 0.22435897435897437,
940
- "delta": 0.04487179487179487,
941
- "ci95": [
942
- 0.024572649572649572,
943
- 0.06623931623931624
944
- ]
945
- },
946
- "cand4b_B4_e2_tt-v2_4b_p130|sampled|bag_game": {
947
- "n": 64,
948
- "a": 0.51953125,
949
- "b": 0.37890625,
950
- "delta": 0.140625,
951
- "ci95": [
952
- 0.08203125,
953
- 0.19921875
954
- ]
955
- },
956
- "cand4b_B4_e2_tt-v2_4b_p130|sampled|stochastic_grid": {
957
- "n": 64,
958
- "a": 0.16796875,
959
- "b": 0.16015625,
960
- "delta": 0.0078125,
961
- "ci95": [
962
- -0.0234375,
963
- 0.0390625
964
- ]
965
- },
966
- "cand4b_B4_e2_tt-v2_4b_p130|sampled|tic_tac_toe": {
967
- "n": 74,
968
- "a": 0.2533783783783784,
969
- "b": 0.24324324324324326,
970
- "delta": 0.010135135135135136,
971
- "ci95": [
972
- -0.010135135135135136,
973
- 0.033783783783783786
974
- ]
975
- },
976
- "cand4b_B4_e2_tt-v2_4b_p130|sampled|exact_minesweeper": {
977
- "n": 32,
978
- "a": 0.0078125,
979
- "b": 0.0,
980
- "delta": 0.0078125,
981
- "ci95": [
982
- 0.0,
983
- 0.0234375
984
- ]
985
- },
986
- "cand4b_B4_e2_tt-v2_4b_p130|greedy|all": {
987
- "n": 234,
988
- "a": 0.25213675213675213,
989
- "b": 0.2777777777777778,
990
- "delta": -0.02564102564102564,
991
- "ci95": [
992
- -0.0641025641025641,
993
- 0.01282051282051282
994
- ]
995
- },
996
- "cand4b_B4_e2_tt-v2_4b_p130|greedy|bag_game": {
997
- "n": 64,
998
- "a": 0.53125,
999
- "b": 0.484375,
1000
- "delta": 0.046875,
1001
- "ci95": [
1002
- -0.03125,
1003
- 0.125
1004
- ]
1005
- },
1006
- "cand4b_B4_e2_tt-v2_4b_p130|greedy|stochastic_grid": {
1007
- "n": 64,
1008
- "a": 0.109375,
1009
- "b": 0.1875,
1010
- "delta": -0.078125,
1011
- "ci95": [
1012
- -0.15625,
1013
- 0.0
1014
- ]
1015
- },
1016
- "cand4b_B4_e2_tt-v2_4b_p130|greedy|tic_tac_toe": {
1017
- "n": 74,
1018
- "a": 0.24324324324324326,
1019
- "b": 0.2972972972972973,
1020
- "delta": -0.05405405405405406,
1021
- "ci95": [
1022
- -0.12162162162162163,
1023
- 0.02702702702702703
1024
- ]
1025
- },
1026
- "cand4b_B4_e2_tt-v2_4b_p130|greedy|exact_minesweeper": {
1027
- "n": 32,
1028
- "a": 0.0,
1029
- "b": 0.0,
1030
- "delta": 0.0,
1031
- "ci95": [
1032
- 0.0,
1033
- 0.0
1034
- ]
1035
- }
1036
- },
1037
- "browser": {
1038
- "cand4b_B4_e2_tt-cand4b_B4_e2|sampled|all": {
1039
- "n": 176,
1040
- "a": 0.9318181818181818,
1041
- "b": 0.9318181818181818,
1042
- "delta": 0.0,
1043
- "ci95": [
1044
- 0.0,
1045
- 0.0
1046
- ]
1047
- },
1048
- "cand4b_B4_e2_tt-cand4b_B4_e2|sampled|rewarded": {
1049
- "n": 128,
1050
- "a": 0.953125,
1051
- "b": 0.953125,
1052
- "delta": 0.0,
1053
- "ci95": [
1054
- 0.0,
1055
- 0.0
1056
- ]
1057
- },
1058
- "cand4b_B4_e2_tt-cand4b_B4_e2|sampled|held_out": {
1059
- "n": 48,
1060
- "a": 0.875,
1061
- "b": 0.875,
1062
- "delta": 0.0,
1063
- "ci95": [
1064
- 0.0,
1065
- 0.0
1066
- ]
1067
- },
1068
- "cand4b_B4_e2_tt-cand4b_B4_e2|greedy|all": {
1069
- "n": 176,
1070
- "a": 0.9375,
1071
- "b": 0.9375,
1072
- "delta": 0.0,
1073
- "ci95": [
1074
- 0.0,
1075
- 0.0
1076
- ]
1077
- },
1078
- "cand4b_B4_e2_tt-cand4b_B4_e2|greedy|rewarded": {
1079
- "n": 128,
1080
- "a": 0.9609375,
1081
- "b": 0.9609375,
1082
- "delta": 0.0,
1083
- "ci95": [
1084
- 0.0,
1085
- 0.0
1086
- ]
1087
- },
1088
- "cand4b_B4_e2_tt-cand4b_B4_e2|greedy|held_out": {
1089
- "n": 48,
1090
- "a": 0.875,
1091
- "b": 0.875,
1092
- "delta": 0.0,
1093
- "ci95": [
1094
- 0.0,
1095
- 0.0
1096
- ]
1097
- },
1098
- "cand4b_B4_e2_tt-v1_4b_p130|sampled|all": {
1099
- "n": 176,
1100
- "a": 0.9318181818181818,
1101
- "b": 0.9090909090909091,
1102
- "delta": 0.022727272727272728,
1103
- "ci95": [
1104
- -0.017045454545454544,
1105
- 0.0625
1106
- ]
1107
- },
1108
- "cand4b_B4_e2_tt-v1_4b_p130|sampled|rewarded": {
1109
- "n": 128,
1110
- "a": 0.953125,
1111
- "b": 0.9609375,
1112
- "delta": -0.0078125,
1113
- "ci95": [
1114
- -0.0390625,
1115
- 0.0234375
1116
- ]
1117
- },
1118
- "cand4b_B4_e2_tt-v1_4b_p130|sampled|held_out": {
1119
- "n": 48,
1120
- "a": 0.875,
1121
- "b": 0.7708333333333334,
1122
- "delta": 0.10416666666666667,
1123
- "ci95": [
1124
- 0.0,
1125
- 0.20833333333333334
1126
- ]
1127
- },
1128
- "cand4b_B4_e2_tt-v1_4b_p130|greedy|all": {
1129
- "n": 176,
1130
- "a": 0.9375,
1131
- "b": 0.9147727272727273,
1132
- "delta": 0.022727272727272728,
1133
- "ci95": [
1134
- -0.005681818181818182,
1135
- 0.056818181818181816
1136
- ]
1137
- },
1138
- "cand4b_B4_e2_tt-v1_4b_p130|greedy|rewarded": {
1139
- "n": 128,
1140
- "a": 0.9609375,
1141
- "b": 0.9765625,
1142
- "delta": -0.015625,
1143
- "ci95": [
1144
- -0.0390625,
1145
- 0.0
1146
- ]
1147
- },
1148
- "cand4b_B4_e2_tt-v1_4b_p130|greedy|held_out": {
1149
- "n": 48,
1150
- "a": 0.875,
1151
- "b": 0.75,
1152
- "delta": 0.125,
1153
- "ci95": [
1154
- 0.041666666666666664,
1155
- 0.22916666666666666
1156
- ]
1157
- },
1158
- "cand4b_B4_e2_tt-v2_4b_p130|sampled|all": {
1159
- "n": 176,
1160
- "a": 0.9318181818181818,
1161
- "b": 0.8806818181818182,
1162
- "delta": 0.05113636363636364,
1163
- "ci95": [
1164
- 0.005681818181818182,
1165
- 0.09659090909090909
1166
- ]
1167
- },
1168
- "cand4b_B4_e2_tt-v2_4b_p130|sampled|rewarded": {
1169
- "n": 128,
1170
- "a": 0.953125,
1171
- "b": 0.9296875,
1172
- "delta": 0.0234375,
1173
- "ci95": [
1174
- -0.0234375,
1175
- 0.0703125
1176
- ]
1177
- },
1178
- "cand4b_B4_e2_tt-v2_4b_p130|sampled|held_out": {
1179
- "n": 48,
1180
- "a": 0.875,
1181
- "b": 0.75,
1182
- "delta": 0.125,
1183
- "ci95": [
1184
- 0.0,
1185
- 0.25
1186
- ]
1187
- },
1188
- "cand4b_B4_e2_tt-v2_4b_p130|greedy|all": {
1189
- "n": 176,
1190
- "a": 0.9375,
1191
- "b": 0.9261363636363636,
1192
- "delta": 0.011363636363636364,
1193
- "ci95": [
1194
- -0.017045454545454544,
1195
- 0.045454545454545456
1196
- ]
1197
- },
1198
- "cand4b_B4_e2_tt-v2_4b_p130|greedy|rewarded": {
1199
- "n": 128,
1200
- "a": 0.9609375,
1201
- "b": 0.96875,
1202
- "delta": -0.0078125,
1203
- "ci95": [
1204
- -0.0234375,
1205
- 0.0
1206
- ]
1207
- },
1208
- "cand4b_B4_e2_tt-v2_4b_p130|greedy|held_out": {
1209
- "n": 48,
1210
- "a": 0.875,
1211
- "b": 0.8125,
1212
- "delta": 0.0625,
1213
- "ci95": [
1214
- -0.041666666666666664,
1215
- 0.16666666666666666
1216
- ]
1217
- }
1218
- },
1219
- "text_games_greedy": {
1220
- "pong": -5.0,
1221
- "freeway": 0.0,
1222
- "breakout": 35.0,
1223
- "frozenlake": 0.0,
1224
- "cliffwalking": -13.0,
1225
- "blackjack": -0.6,
1226
- "minigrid_empty": 0.0,
1227
- "minigrid_lavagap": 0.0,
1228
- "minigrid_doorkey": 0.0,
1229
- "babyai_goto": 0.1915625
1230
- },
1231
- "probes": {
1232
- "router_n": 31,
1233
- "router_tier": 0.968,
1234
- "needs_live_data": 0.839,
1235
- "commands_n": 45,
1236
- "command_risk": 0.889,
1237
- "destructive_called_safe": 0,
1238
- "outside_project": 0.933,
1239
- "browser_element": 0.938,
1240
- "browser_action": 0.75,
1241
- "batteries": {
1242
- "generic": 1.0,
1243
- "specific": 1.0,
1244
- "catchall": 0.95,
1245
- "noul_acc": 1.0,
1246
- "noul_brier": 0.01,
1247
- "n_choice": 60,
1248
- "n_noul": 49,
1249
- "abstain_battery": "8/8"
1250
- }
1251
- },
1252
- "issue9": {
1253
- "c_1": {
1254
- "pred": 26,
1255
- "gold": 6,
1256
- "ok": false,
1257
- "p_gold": 0.198,
1258
- "p_pred": 0.764,
1259
- "T": 1.099
1260
- },
1261
- "c_2": {
1262
- "pred": 20,
1263
- "gold": 20,
1264
- "ok": true,
1265
- "p_gold": 0.409,
1266
- "p_pred": 0.409,
1267
- "T": 1.099
1268
- },
1269
- "b_10": {
1270
- "pred": 0,
1271
- "gold": 0,
1272
- "ok": true,
1273
- "p_gold": 0.732,
1274
- "p_pred": 0.732,
1275
- "T": 1.099
1276
- },
1277
- "a2_4": {
1278
- "pred": 4,
1279
- "gold": 4,
1280
- "ok": true,
1281
- "p_gold": 0.577,
1282
- "p_pred": 0.577,
1283
- "T": 1.099
1284
- }
1285
- },
1286
- "bespoke": {
1287
- "vitaminc-dev": {
1288
- "n": 599,
1289
- "type": "choice",
1290
- "acc": 0.7429,
1291
- "ece": 0.1456,
1292
- "brier": 0.3945,
1293
- "nll": 0.7789,
1294
- "trained": false,
1295
- "sec": 11.8
1296
- },
1297
- "massive-en-US": {
1298
- "n": 350,
1299
- "type": "choice",
1300
- "acc": 0.8629,
1301
- "ece": 0.0411,
1302
- "brier": 0.1936,
1303
- "nll": 0.4173,
1304
- "trained": true,
1305
- "sec": 6.2
1306
- },
1307
- "massive-de-DE": {
1308
- "n": 350,
1309
- "type": "choice",
1310
- "acc": 0.84,
1311
- "ece": 0.0297,
1312
- "brier": 0.2269,
1313
- "nll": 0.4947,
1314
- "trained": false,
1315
- "sec": 4.5
1316
- },
1317
- "boolq": {
1318
- "n": 300,
1319
- "type": "noul",
1320
- "acc": 0.8667,
1321
- "ece": 0.0319,
1322
- "brier": 0.1862,
1323
- "nll": 0.3007,
1324
- "trained": true,
1325
- "sec": 6.2
1326
- },
1327
- "squad2": {
1328
- "n": 299,
1329
- "type": "noul",
1330
- "acc": 0.7893,
1331
- "ece": 0.0441,
1332
- "brier": 0.2955,
1333
- "nll": 0.452,
1334
- "trained": false,
1335
- "sec": 7.2
1336
- },
1337
- "paws": {
1338
- "n": 250,
1339
- "type": "noul",
1340
- "acc": 0.764,
1341
- "ece": 0.1238,
1342
- "brier": 0.3398,
1343
- "nll": 0.5538,
1344
- "trained": true,
1345
- "sec": 3.0
1346
- },
1347
- "multinli": {
1348
- "n": 299,
1349
- "type": "choice",
1350
- "acc": 0.9331,
1351
- "ece": 0.0103,
1352
- "brier": 0.1089,
1353
- "nll": 0.2099,
1354
- "trained": true,
1355
- "sec": 3.1
1356
- },
1357
- "civil_comments": {
1358
- "n": 300,
1359
- "type": "noul",
1360
- "acc": 0.8733,
1361
- "ece": 0.0566,
1362
- "brier": 0.1917,
1363
- "nll": 0.3199,
1364
- "trained": true,
1365
- "sec": 3.2
1366
- },
1367
- "aegis2": {
1368
- "n": 250,
1369
- "type": "noul",
1370
- "acc": 0.804,
1371
- "ece": 0.0467,
1372
- "brier": 0.2797,
1373
- "nll": 0.4527,
1374
- "trained": false,
1375
- "sec": 6.1
1376
- },
1377
- "helpsteer2": {
1378
- "n": 249,
1379
- "type": "score",
1380
- "acc": 0.4498,
1381
- "ece": 0.142,
1382
- "brier": 0.6713,
1383
- "nll": 1.2502,
1384
- "trained": true,
1385
- "sec": 25.8,
1386
- "score_mae": 0.797
1387
- },
1388
- "summeval-relevance": {
1389
- "n": 240,
1390
- "type": "score",
1391
- "acc": 0.3958,
1392
- "ece": 0.0696,
1393
- "brier": 0.6844,
1394
- "nll": 1.2541,
1395
- "trained": false,
1396
- "sec": 20.1,
1397
- "score_mae": 0.6172
1398
- },
1399
- "summeval-consistency": {
1400
- "n": 144,
1401
- "type": "score",
1402
- "acc": 0.7986,
1403
- "ece": 0.3095,
1404
- "brier": 0.4741,
1405
- "nll": 0.9007,
1406
- "trained": false,
1407
- "sec": 12.2,
1408
- "score_mae": 0.7974
1409
- },
1410
- "pubmedqa": {
1411
- "n": 250,
1412
- "type": "choice",
1413
- "acc": 0.708,
1414
- "ece": 0.0863,
1415
- "brier": 0.4012,
1416
- "nll": 0.7205,
1417
- "trained": true,
1418
- "sec": 3.8
1419
- },
1420
- "_macro": 0.756,
1421
- "_micro": 0.7652,
1422
- "_macro_untrained": 0.7284
1423
- },
1424
- "jevbench_public": {
1425
- "easy": {
1426
- "n": 48,
1427
- "accuracy": 1.0,
1428
- "invalid": 0,
1429
- "ece": 0.0027979166666670663,
1430
- "by_family": {
1431
- "extraction": {
1432
- "n": 12,
1433
- "accuracy": 1.0
1434
- },
1435
- "fact": {
1436
- "n": 12,
1437
- "accuracy": 1.0
1438
- },
1439
- "intent": {
1440
- "n": 12,
1441
- "accuracy": 1.0
1442
- },
1443
- "tool_selection": {
1444
- "n": 12,
1445
- "accuracy": 1.0
1446
- }
1447
- }
1448
- },
1449
- "standard": {
1450
- "n": 72,
1451
- "accuracy": 0.9861111111111112,
1452
- "invalid": 0,
1453
- "ece": 0.0753152777777778,
1454
- "by_family": {
1455
- "adequacy": {
1456
- "n": 12,
1457
- "accuracy": 0.9166666666666666
1458
- },
1459
- "extraction": {
1460
- "n": 12,
1461
- "accuracy": 1.0
1462
- },
1463
- "intent": {
1464
- "n": 12,
1465
- "accuracy": 1.0
1466
- },
1467
- "ordinal": {
1468
- "n": 12,
1469
- "accuracy": 1.0
1470
- },
1471
- "policy": {
1472
- "n": 12,
1473
- "accuracy": 1.0
1474
- },
1475
- "routing": {
1476
- "n": 12,
1477
- "accuracy": 1.0
1478
- }
1479
- }
1480
- },
1481
- "hard": {
1482
- "n": 111,
1483
- "accuracy": 0.6486486486486487,
1484
- "invalid": 0,
1485
- "ece": 0.18370720720720732,
1486
- "by_family": {
1487
- "adversarial": {
1488
- "n": 6,
1489
- "accuracy": 1.0
1490
- },
1491
- "ambiguous": {
1492
- "n": 7,
1493
- "accuracy": 0.7142857142857143
1494
- },
1495
- "judge_hard": {
1496
- "n": 17,
1497
- "accuracy": 0.5294117647058824
1498
- },
1499
- "long_policy": {
1500
- "n": 19,
1501
- "accuracy": 0.47368421052631576
1502
- },
1503
- "multi_hop": {
1504
- "n": 18,
1505
- "accuracy": 0.8333333333333334
1506
- },
1507
- "probability": {
1508
- "n": 10,
1509
- "accuracy": 0.8
1510
- },
1511
- "routing_hard": {
1512
- "n": 5,
1513
- "accuracy": 1.0
1514
- },
1515
- "temporal_numeric": {
1516
- "n": 15,
1517
- "accuracy": 0.3333333333333333
1518
- },
1519
- "tradeoff": {
1520
- "n": 6,
1521
- "accuracy": 0.3333333333333333
1522
- },
1523
- "trap": {
1524
- "n": 8,
1525
- "accuracy": 1.0
1526
- }
1527
- }
1528
- }
1529
- },
1530
- "references": {
1531
- "v1_4b": {
1532
- "reg_in": 0.8338,
1533
- "reg_out": 0.7876,
1534
- "reg_in_nll": 0.4044,
1535
- "reg_out_nll": 0.5583,
1536
- "reg_in_ece": 0.0269,
1537
- "reg_out_ece": 0.0708
1538
- },
1539
- "v2_4b": {
1540
- "reg_in": 0.8241,
1541
- "reg_out": 0.7788,
1542
- "reg_in_nll": 0.4411,
1543
- "reg_out_nll": 0.5663,
1544
- "reg_in_ece": 0.0413,
1545
- "reg_out_ece": 0.0803
1546
- }
1547
- },
1548
- "speed": "not measured again: the architecture and size equal the parent's, and the temperature map does not change the computation"
1549
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:ee8ce585b3cedd93206dd149b09b4bdd683174874211f85c77a36090b90c9fdd
3
  size 8411558400
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e83c74cc305ef4643c61a3c44cb71ed03aef7560ece63a64c14c91875f5ea47e
3
  size 8411558400
vision/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 false %}
150
+ {{- '<think>\n\n</think>\n\n' }}
151
+ {%- else %}
152
+ {{- '<think>\n' }}
153
+ {%- endif %}
154
+ {%- endif %}
vision/config.json ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "Qwen3_5ForConditionalGeneration"
4
+ ],
5
+ "dtype": "bfloat16",
6
+ "image_token_id": 248056,
7
+ "model_type": "qwen3_5",
8
+ "text_config": {
9
+ "attention_bias": false,
10
+ "attention_dropout": 0.0,
11
+ "attn_output_gate": true,
12
+ "bos_token_id": null,
13
+ "dtype": "bfloat16",
14
+ "eos_token_id": 248044,
15
+ "full_attention_interval": 4,
16
+ "head_dim": 256,
17
+ "hidden_act": "silu",
18
+ "hidden_size": 2560,
19
+ "initializer_range": 0.02,
20
+ "intermediate_size": 9216,
21
+ "layer_types": [
22
+ "linear_attention",
23
+ "linear_attention",
24
+ "linear_attention",
25
+ "full_attention",
26
+ "linear_attention",
27
+ "linear_attention",
28
+ "linear_attention",
29
+ "full_attention",
30
+ "linear_attention",
31
+ "linear_attention",
32
+ "linear_attention",
33
+ "full_attention",
34
+ "linear_attention",
35
+ "linear_attention",
36
+ "linear_attention",
37
+ "full_attention",
38
+ "linear_attention",
39
+ "linear_attention",
40
+ "linear_attention",
41
+ "full_attention",
42
+ "linear_attention",
43
+ "linear_attention",
44
+ "linear_attention",
45
+ "full_attention",
46
+ "linear_attention",
47
+ "linear_attention",
48
+ "linear_attention",
49
+ "full_attention",
50
+ "linear_attention",
51
+ "linear_attention",
52
+ "linear_attention",
53
+ "full_attention"
54
+ ],
55
+ "linear_conv_kernel_dim": 4,
56
+ "linear_key_head_dim": 128,
57
+ "linear_num_key_heads": 16,
58
+ "linear_num_value_heads": 32,
59
+ "linear_value_head_dim": 128,
60
+ "mamba_ssm_dtype": "float32",
61
+ "max_position_embeddings": 262144,
62
+ "mlp_only_layers": [],
63
+ "model_type": "qwen3_5_text",
64
+ "mtp_num_hidden_layers": 1,
65
+ "mtp_use_dedicated_embeddings": false,
66
+ "num_attention_heads": 16,
67
+ "num_hidden_layers": 32,
68
+ "num_key_value_heads": 4,
69
+ "pad_token_id": null,
70
+ "partial_rotary_factor": 0.25,
71
+ "rms_norm_eps": 1e-06,
72
+ "rope_parameters": {
73
+ "mrope_interleaved": true,
74
+ "mrope_section": [
75
+ 11,
76
+ 11,
77
+ 10
78
+ ],
79
+ "partial_rotary_factor": 0.25,
80
+ "rope_theta": 10000000,
81
+ "rope_type": "default"
82
+ },
83
+ "tie_word_embeddings": true,
84
+ "use_cache": true,
85
+ "vocab_size": 248320
86
+ },
87
+ "tie_word_embeddings": true,
88
+ "transformers_version": "5.17.0",
89
+ "use_cache": true,
90
+ "video_token_id": 248057,
91
+ "vision_config": {
92
+ "deepstack_visual_indexes": [],
93
+ "depth": 24,
94
+ "dtype": "bfloat16",
95
+ "hidden_act": "gelu_pytorch_tanh",
96
+ "hidden_size": 1024,
97
+ "in_channels": 3,
98
+ "initializer_range": 0.02,
99
+ "intermediate_size": 4096,
100
+ "model_type": "qwen3_5_vision",
101
+ "num_heads": 16,
102
+ "num_position_embeddings": 2304,
103
+ "out_hidden_size": 2560,
104
+ "patch_size": 16,
105
+ "rope_parameters": {
106
+ "rope_theta": 10000.0,
107
+ "rope_type": "axial"
108
+ },
109
+ "spatial_merge_size": 2,
110
+ "temporal_patch_size": 2
111
+ },
112
+ "vision_end_token_id": 248054,
113
+ "vision_start_token_id": 248053
114
+ }
vision/processor_config.json ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "image_processor": {
3
+ "do_convert_rgb": true,
4
+ "do_normalize": true,
5
+ "do_rescale": true,
6
+ "do_resize": true,
7
+ "image_mean": [
8
+ 0.5,
9
+ 0.5,
10
+ 0.5
11
+ ],
12
+ "image_processor_type": "Qwen2VLImageProcessor",
13
+ "image_std": [
14
+ 0.5,
15
+ 0.5,
16
+ 0.5
17
+ ],
18
+ "merge_size": 2,
19
+ "patch_size": 16,
20
+ "resample": 3,
21
+ "rescale_factor": 0.00392156862745098,
22
+ "size": {
23
+ "longest_edge": 16777216,
24
+ "shortest_edge": 65536
25
+ },
26
+ "temporal_patch_size": 2
27
+ },
28
+ "processor_class": "Qwen3VLProcessor",
29
+ "video_processor": {
30
+ "do_convert_rgb": true,
31
+ "do_normalize": true,
32
+ "do_rescale": true,
33
+ "do_resize": true,
34
+ "do_sample_frames": true,
35
+ "fps": 2,
36
+ "image_mean": [
37
+ 0.5,
38
+ 0.5,
39
+ 0.5
40
+ ],
41
+ "image_std": [
42
+ 0.5,
43
+ 0.5,
44
+ 0.5
45
+ ],
46
+ "max_frames": 768,
47
+ "max_video_tokens": 768,
48
+ "merge_size": 2,
49
+ "min_frames": 4,
50
+ "patch_size": 16,
51
+ "resample": 3,
52
+ "rescale_factor": 0.00392156862745098,
53
+ "return_metadata": false,
54
+ "size": {
55
+ "longest_edge": 234881024,
56
+ "shortest_edge": 4096
57
+ },
58
+ "temporal_patch_size": 2,
59
+ "video_processor_type": "Qwen3VLVideoProcessor"
60
+ }
61
+ }
vision/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523
3
+ size 19989325
vision/tokenizer_config.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "audio_bos_token": "<|audio_start|>",
4
+ "audio_eos_token": "<|audio_end|>",
5
+ "audio_token": "<|audio_pad|>",
6
+ "backend": "tokenizers",
7
+ "bos_token": null,
8
+ "clean_up_tokenization_spaces": false,
9
+ "eos_token": "<|endoftext|>",
10
+ "errors": "replace",
11
+ "image_token": "<|image_pad|>",
12
+ "is_local": true,
13
+ "local_files_only": false,
14
+ "model_max_length": 262144,
15
+ "model_specific_special_tokens": {
16
+ "audio_bos_token": "<|audio_start|>",
17
+ "audio_eos_token": "<|audio_end|>",
18
+ "audio_token": "<|audio_pad|>",
19
+ "image_token": "<|image_pad|>",
20
+ "video_token": "<|video_pad|>",
21
+ "vision_bos_token": "<|vision_start|>",
22
+ "vision_eos_token": "<|vision_end|>"
23
+ },
24
+ "pad_token": "<|endoftext|>",
25
+ "pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
26
+ "split_special_tokens": false,
27
+ "tokenizer_class": "Qwen2Tokenizer",
28
+ "unk_token": null,
29
+ "video_token": "<|video_pad|>",
30
+ "vision_bos_token": "<|vision_start|>",
31
+ "vision_eos_token": "<|vision_end|>"
32
+ }
vision/visual.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:87f0f1e2be1c073a49761003a1a635da88fd5e00d3a79dd4a5e3099b4e9a9306
3
+ size 667061480