decider-4b v3: text and image decisions (vision/), Noul T 0.05; v2.1 under tag v2.1
Browse files- README.md +110 -399
- decider/__init__.py +0 -0
- decider/batching.py +0 -95
- decider/calibrate.py +0 -222
- decider/engine.py +0 -216
- decider/engine_v2.py +0 -213
- decider/fp8.py +0 -54
- decider/infer.py +0 -357
- decider/metrics.py +0 -37
- decider/model.py +0 -49
- decider/mps_moe.py +0 -102
- decider/mps_ops.py +0 -268
- decider/prompt.py +0 -287
- decider/prompt_fast.py +0 -90
- decider/schema_engine.py +0 -150
- decider/serve.py +0 -562
- decider/shared_prefix.py +0 -200
- decider/systemone.py +0 -199
- decider/temperature.py +0 -144
- decider_config.json +36 -6
- eval_results.json +0 -1549
- model.safetensors +1 -1
- vision/chat_template.jinja +154 -0
- vision/config.json +114 -0
- vision/processor_config.json +61 -0
- vision/tokenizer.json +3 -0
- vision/tokenizer_config.json +32 -0
- vision/visual.safetensors +3 -0
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.
|
| 13 |
-
|
| 14 |
-
reproduction of the "System One" model class (TypeSafe AI's Jev).
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 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 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 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 |
-
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 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 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
|
| 146 |
-
|--
|
| 147 |
-
| [decider-
|
| 148 |
-
| [decider-
|
| 149 |
-
| [decider-35b-a3b](https://huggingface.co/Mapika/decider-35b-a3b)
|
| 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 |
-
|
|
|
|
| 155 |
|
| 156 |
## Usage
|
| 157 |
|
|
|
|
|
|
|
| 158 |
```python
|
| 159 |
-
from decider.infer import Decider
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
```python
|
| 189 |
-
import
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 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 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 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 |
-
|
| 217 |
-
|
| 218 |
-
|
| 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 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
|
|
|
| 254 |
|
| 255 |
-
|
| 256 |
-
|
| 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
|
| 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 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
**
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 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 |
-
*
|
| 395 |
-
|
| 396 |
-
*
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 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":
|
| 3 |
"temperature_by_type": {
|
| 4 |
-
"choice":
|
| 5 |
-
"noul":
|
| 6 |
-
"score":
|
| 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:
|
| 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
|