Mapika commited on
Commit
14d6555
·
verified ·
1 Parent(s): 7789eb6

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. README.md +178 -232
  2. decider_config.json +1 -1
  3. eval_results.json +0 -0
  4. model.safetensors +1 -1
README.md CHANGED
@@ -6,39 +6,39 @@ pipeline_tag: text-classification
6
  tags: [decision-model, calibrated, structured-output, multi-task, system-one, one-pass]
7
  ---
8
 
9
- # decider-2B: typed decisions with calibrated probabilities in one forward pass
10
-
11
- An open replication of the "System One model" idea: a language model that does not
12
- generate text. It reads a context plus one or more typed questions, each with an
13
- explicit option list, and returns a probability distribution over the options for
14
- every question from a single forward pass. No decoding, no JSON parsing, no
15
- schema violations. It is meant to be called from software, not chatted with.
16
-
17
- Base model: [Qwen/Qwen3.5-2B-Base](https://huggingface.co/Qwen/Qwen3.5-2B-Base) (1.9B parameters),
18
- fully fine-tuned for one epoch (942k examples, 183M tokens, 2.5 hours on one
19
- NVIDIA GH200) with cross-entropy, a proper scoring rule, on a mixture of 64
20
- public decision datasets, then continued for one epoch on 45k situation-to-action
21
- examples (agent trajectories, web element choice, synthetic situations, game states)
22
- with a replay of the general mixture (v4). v6 to v8 continue on the input shapes of TypeSafe's Jev API (described options, up to
23
- 255 options, JSON states with path references, long inputs, questions scored independently), on teacher-written custom questions
24
- (free-form yes/no, user-named options, a generic option next to a catch-all), on a second, cacheable prompt layout, and on
25
- isolated Score levels. This card describes v8.
 
26
 
27
  ## Usage
28
 
29
  ```python
30
  from decider.infer import Decider # decider/ is included in this repo
31
- d = Decider("<this repo>")
32
  d.decide("My card was charged twice for the same purchase.",
33
  [{"question": "Which department should handle this?", "options": ["billing", "technical support", "sales"]},
34
  {"question": "Does this need a refund action?", "options": ["no", "yes"]}])
35
  # [{'choice': 'billing', 'confidence': 0.99, 'probs': {...}}, {'choice': 'yes', 'confidence': 0.99, 'probs': {...}}]
36
  ```
37
 
38
- `decide_batch` scores many contexts, each with many questions, in one call.
39
- Set `abstain_below=t` to return `None` for decisions with confidence under `t`
40
- (route to a human). 2 to 255 options per question (v6; more than 10 options use one label token per
41
- option, see `decider/prompt.py`).
42
 
43
  The same request shape as TypeSafe's Jev (`POST /v1/systemone`), in process or over HTTP:
44
 
@@ -50,26 +50,25 @@ d.system_one({"ticket": {"messages": [{"from": "customer", "text": "I was charge
50
  "billing": {"what": "Charges, invoices", "not_for": "delivery"}, "other": None}},
51
  "refund_requested": {"type": "noul", "instructions": "Does `ticket.messages[0].text` request a refund?"},
52
  "frustration": {"type": "score", "instructions": "How frustrated is the customer?", "criteria": ["calm", "frustrated", "very frustrated"]}})
53
- # {"model": "decider-v6", "answers": {"department": {"type": "choice", "choice": "billing", "confidence": ..., "certainty": ..., "probabilities": {...}},
54
  # "refund_requested": {"type": "noul", "noul": ...}, "frustration": {"type": "score", "score": ..., "legend": {...}, ...}}, "usage": {...}}
55
  ```
56
 
57
- State may be a string, object or array (up to 32k tokens with the questions); `instructions` and every option
58
- description may be a string or any JSON value; question ids are never shown to the model. Each question is scored in
59
- its own row, so answers do not depend on which other questions are asked (`independent=False` packs them into one row,
60
- about half the latency for short states). Each Score level is likewise judged in its own row, without its number or its
61
- neighbours, and the per-level fits are normalised (`"isolated": false` on a question restores listwise scoring); the answer
62
- also reports `level_fit` and their sum `fit_mass` (near 1 when exactly one level fits).
63
 
64
  For a fixed set of questions, `s = d.schema(questions)` computes the question prefix once and `s(state)` / `s.batch(states)`
65
- then run only the state (1.2-2.4x faster per request, up to 19x per batch). It uses a questions-first prompt layout that costs
66
- accuracy: about 1.5 points on fixed label sets, 5 on per-example options, more on 50+ options and multi-thousand-token states. `decider.serve` exposes the same thing as `POST /v1/systemone`; the official
67
- `typesafe-sdk` works against it unchanged with `TYPESAFE_BASE_URL` pointing at the server.
 
68
 
69
- Requirements: `torch`, `transformers>=5`, and `flash-linear-attention` (Triton
70
- kernels for the Qwen3.5 linear-attention layers; the model runs without it but
71
- several times slower). Python 3.11+ recommended so those kernels can use
72
- `torch.compile`.
73
 
74
  Without the helper package, the same computation in plain `transformers`:
75
 
@@ -83,230 +82,177 @@ ids = tok(prompt, return_tensors="pt").to("cuda")
83
  with torch.no_grad():
84
  logits = m(**ids).logits[0, -1]
85
  letters = [tok.encode(L, add_special_tokens=False)[0] for L in "ABC"]
86
- probs = torch.softmax(logits[letters].float(), -1) # -> P(billing), P(technical support), P(sales)
87
  ```
88
 
89
- For several questions in one pass, append further `Question k: ... Answer k: (`
90
- blocks and read the logits at each `(` position (see `decider/prompt.py`).
91
 
92
  ## How it works
93
 
94
- Prompt: `Context: ...` followed by, for each question, the question text, the
95
- numbered options `(A) ... (B) ...`, and an answer slot `Answer k: (`. The hidden
96
- state at each slot is projected with the option-letter rows of the LM head and
97
- softmaxed over the valid letters. Letters are never generated, so all slots are
98
- read from one pass. Large label sets were sub-sampled to at most 10 options per
99
- training example (gold always kept, order shuffled), so the model conditions on
100
- the supplied candidates rather than a fixed head.
101
 
102
  ## Field types
103
 
104
- * **bool** (`noul`): probability of "yes".
105
- * **choice**: argmax option, its probability, and the full distribution.
106
- * Jev names: `noul` (bool), `choice` with `criteria` {name: description | JSON | null}, `score` with `criteria`
107
- [level descriptions]. Choice and score answers carry `confidence` (top probability, the calibrated number) and
108
- `certainty` (1 - normalised entropy of the distribution).
109
- * **scale**: an ordered legend (e.g. 0: none ... 3: high); returns the expected
110
- level (`score`), the probability of the most likely level, and the distribution.
111
-
112
- ## Training data
113
-
114
- 69 public datasets plus synthetic situations, up to 20k examples each (`decider/data.py`, `decider/data2.py`, `decider/data3.py`):
115
- intent detection, ticket routing, topic classification, sentiment, emotion,
116
- moderation (toxicity, hate, spam, jailbreak, safety), NLI, paraphrase, fact
117
- verification, passage relevance, reading comprehension, multiple-choice QA,
118
- ordinal rating scales (HelpSteer2 attributes, STS-B, hate-speech intensity,
119
- LIAR2 truthfulness), pairwise response preference (HelpSteer3, UltraFeedback,
120
- SHP, HH-RLHF) and tool selection (Glaive, ToolACE).
121
- v4 adds next-action choice from agent trajectories (AgentGym AgentTraj-L), web element
122
- choice (Mind2Web), 1.5k synthetic situations written by Qwen3.5-27B, and teacher-labelled
123
- states from Pong, Breakout, CliffWalking, MiniGrid and Super Mario Bros.
124
- Abstention augmentation: in 10% of questions with three or more options an abstain
125
- option with one of twelve wordings is added; in a quarter of those the whole option list is
126
- replaced by labels from an unrelated task, making the abstain option correct.
 
 
 
 
 
 
127
 
128
  ## Evaluation
129
 
130
- | Model | Split | Acc | NLL | Brier | ECE | AURC | Acc@80% |
131
- |---|---|---|---|---|---|---|---|
132
- | Qwen3.5-2B-Base, zero-shot | in-task (64) | 0.620 | 0.908 | 0.493 | 0.121 | 0.280 | 0.663 |
133
- | Qwen3.5-2B-Base, zero-shot | held-out (23) | 0.642 | 0.853 | 0.460 | 0.105 | 0.242 | 0.685 |
134
- | Qwen3.5-4B-Base, zero-shot | in-task (64) | 0.695 | 0.768 | 0.405 | 0.090 | 0.206 | 0.742 |
135
- | Qwen3.5-4B-Base, zero-shot | held-out (23) | 0.711 | 0.734 | 0.390 | 0.089 | 0.169 | 0.761 |
136
- | **this model (v5)** | in-task (69) | 0.815 | 0.445 | 0.248 | 0.028 | 0.093 | 0.866 |
137
- | **this model (v5)** | held-out (24) | 0.738 | 0.678 | 0.360 | 0.084 | 0.145 | 0.793 |
138
- | **this model (v6, T=1.15)** | in-task (69) | 0.813 | 0.450 | 0.251 | 0.032 | 0.094 | 0.864 |
139
- | **this model (v6, T=1.15)** | held-out (24) | 0.736 | 0.664 | 0.358 | 0.084 | 0.145 | 0.793 |
140
- | **this model (v8, T=1.30)** | in-task (69) | 0.811 | 0.460 | | 0.037 | | |
141
- | **this model (v8, T=1.30)** | held-out (24) | 0.741 | 0.655 | | 0.088 | | |
142
- | v8, questions-first layout (schema cache), T=1.18 | held-out (24) | 0.707 | 0.757 | | 0.104 | | |
143
 
 
 
 
 
 
 
 
 
144
 
145
- Per-task accuracy / ECE on the held-out datasets:
 
 
146
 
147
- | Task | Qwen3.5-2B-Base, zero-shot | Qwen3.5-4B-Base, zero-shot | this model |
148
- |---|---|---|---|
149
- | abstain_probe | 0.377 / 0.193 | 0.453 / 0.242 | 0.785 / 0.037 |
150
- | ade | 0.794 / 0.117 | 0.746 / 0.101 | 0.808 / 0.038 |
151
- | arena_pref | 0.382 / 0.228 | 0.434 / 0.159 | 0.474 / 0.144 |
152
- | bbc_news | 0.910 / 0.010 | 0.927 / 0.024 | 0.941 / 0.016 |
153
- | cb | 0.554 / 0.115 | 0.696 / 0.087 | 0.893 / 0.115 |
154
- | cr_reviews | 0.882 / 0.073 | 0.923 / 0.015 | 0.902 / 0.020 |
155
- | dolly_category | 0.269 / 0.137 | 0.351 / 0.149 | 0.318 / 0.211 |
156
- | fin_phrasebank | 0.560 / 0.032 | 0.713 / 0.050 | 0.660 / 0.082 |
157
- | fin_sentiment | 0.345 / 0.335 | 0.437 / 0.303 | 0.769 / 0.031 |
158
- | hermes_tools | 0.704 / 0.067 | 0.702 / 0.160 | 0.737 / 0.155 |
159
- | massive_scenario | 0.706 / 0.094 | 0.707 / 0.032 | 0.815 / 0.023 |
160
- | paws | 0.759 / 0.123 | 0.834 / 0.018 | 0.691 / 0.221 |
161
- | pubmedqa | 0.728 / 0.042 | 0.768 / 0.082 | 0.790 / 0.063 |
162
- | quality | 0.457 / 0.205 | 0.519 / 0.184 | 0.513 / 0.137 |
163
- | reward_bench | 0.624 / 0.074 | 0.772 / 0.045 | 0.799 / 0.043 |
164
- | sciq | 0.978 / 0.011 | 0.988 / 0.019 | 0.982 / 0.017 |
165
- | social_iqa | 0.659 / 0.089 | 0.739 / 0.053 | 0.712 / 0.060 |
166
- | strategyqa | 0.566 / 0.029 | 0.646 / 0.037 | 0.613 / 0.069 |
167
- | student_questions | 0.903 / 0.059 | 0.933 / 0.021 | 0.927 / 0.030 |
168
- | trec | 0.696 / 0.062 | 0.822 / 0.050 | 0.766 / 0.036 |
169
- | truthfulqa | 0.460 / 0.111 | 0.591 / 0.113 | 0.497 / 0.091 |
170
- | tweet_irony | 0.511 / 0.158 | 0.676 / 0.073 | 0.779 / 0.059 |
171
- | xstory_cloze | 0.941 / 0.061 | 0.981 / 0.041 | 0.965 / 0.028 |
172
-
173
- | Task | Qwen3.5-2B-Base, zero-shot | Qwen3.5-4B-Base, zero-shot | this model |
174
- |---|---|---|---|
175
- | abstain_probe | 0.377 / 0.193 | 0.453 / 0.242 | 0.785 / 0.037 |
176
- | ade | 0.794 / 0.117 | 0.746 / 0.101 | 0.808 / 0.038 |
177
- | arena_pref | 0.382 / 0.228 | 0.434 / 0.159 | 0.474 / 0.144 |
178
- | bbc_news | 0.910 / 0.010 | 0.927 / 0.024 | 0.941 / 0.016 |
179
- | cb | 0.554 / 0.115 | 0.696 / 0.087 | 0.893 / 0.115 |
180
- | cr_reviews | 0.882 / 0.073 | 0.923 / 0.015 | 0.902 / 0.020 |
181
- | dolly_category | 0.269 / 0.137 | 0.351 / 0.149 | 0.318 / 0.211 |
182
- | fin_phrasebank | 0.560 / 0.032 | 0.713 / 0.050 | 0.660 / 0.082 |
183
- | fin_sentiment | 0.345 / 0.335 | 0.437 / 0.303 | 0.769 / 0.031 |
184
- | hermes_tools | 0.704 / 0.067 | 0.702 / 0.160 | 0.737 / 0.155 |
185
- | massive_scenario | 0.706 / 0.094 | 0.707 / 0.032 | 0.815 / 0.023 |
186
- | paws | 0.759 / 0.123 | 0.834 / 0.018 | 0.691 / 0.221 |
187
- | pubmedqa | 0.728 / 0.042 | 0.768 / 0.082 | 0.790 / 0.063 |
188
- | quality | 0.457 / 0.205 | 0.519 / 0.184 | 0.513 / 0.137 |
189
- | reward_bench | 0.624 / 0.074 | 0.772 / 0.045 | 0.799 / 0.043 |
190
- | sciq | 0.978 / 0.011 | 0.988 / 0.019 | 0.982 / 0.017 |
191
- | social_iqa | 0.659 / 0.089 | 0.739 / 0.053 | 0.712 / 0.060 |
192
- | strategyqa | 0.566 / 0.029 | 0.646 / 0.037 | 0.613 / 0.069 |
193
- | student_questions | 0.903 / 0.059 | 0.933 / 0.021 | 0.927 / 0.030 |
194
- | trec | 0.696 / 0.062 | 0.822 / 0.050 | 0.766 / 0.036 |
195
- | truthfulqa | 0.460 / 0.111 | 0.591 / 0.113 | 0.497 / 0.091 |
196
- | tweet_irony | 0.511 / 0.158 | 0.676 / 0.073 | 0.779 / 0.059 |
197
- | xstory_cloze | 0.941 / 0.061 | 0.981 / 0.041 | 0.965 / 0.028 |
198
-
199
- | Task | Qwen3.5-2B-Base, zero-shot | Qwen3.5-4B-Base, zero-shot | this model |
200
- |---|---|---|---|
201
- | ade | 0.794 / 0.117 | 0.746 / 0.101 | 0.827 / 0.023 |
202
- | bbc_news | 0.910 / 0.010 | 0.927 / 0.024 | 0.919 / 0.025 |
203
- | cr_reviews | 0.882 / 0.073 | 0.923 / 0.015 | 0.911 / 0.019 |
204
- | dolly_category | 0.269 / 0.137 | 0.351 / 0.149 | 0.316 / 0.212 |
205
- | fin_phrasebank | 0.560 / 0.032 | 0.713 / 0.050 | 0.640 / 0.125 |
206
- | fin_sentiment | 0.345 / 0.335 | 0.437 / 0.303 | 0.773 / 0.029 |
207
- | massive_scenario | 0.706 / 0.094 | 0.707 / 0.032 | 0.822 / 0.017 |
208
- | paws | 0.759 / 0.123 | 0.834 / 0.018 | 0.693 / 0.189 |
209
- | pubmedqa | 0.728 / 0.042 | 0.768 / 0.082 | 0.768 / 0.043 |
210
- | sciq | 0.978 / 0.011 | 0.988 / 0.019 | 0.985 / 0.016 |
211
- | social_iqa | 0.659 / 0.089 | 0.739 / 0.053 | 0.712 / 0.055 |
212
- | strategyqa | 0.566 / 0.029 | 0.646 / 0.037 | 0.594 / 0.095 |
213
- | student_questions | 0.903 / 0.059 | 0.933 / 0.021 | 0.930 / 0.017 |
214
- | trec | 0.696 / 0.062 | 0.822 / 0.050 | 0.772 / 0.044 |
215
- | truthfulqa | 0.460 / 0.111 | 0.591 / 0.113 | 0.541 / 0.062 |
216
- | tweet_irony | 0.511 / 0.158 | 0.676 / 0.073 | 0.754 / 0.054 |
217
-
218
- | Task | Qwen3.5-2B-Base, zero-shot | Qwen3.5-4B-Base, zero-shot | this model (200k-example run) |
219
  |---|---|---|---|
220
- | ade | 0.794 / 0.117 | 0.746 / 0.101 | 0.808 / 0.046 |
221
- | bbc_news | 0.910 / 0.010 | 0.927 / 0.024 | 0.928 / 0.014 |
222
- | cr_reviews | 0.882 / 0.073 | 0.923 / 0.015 | 0.915 / 0.012 |
223
- | dolly_category | 0.269 / 0.137 | 0.351 / 0.149 | 0.325 / 0.193 |
224
- | fin_phrasebank | 0.560 / 0.032 | 0.713 / 0.050 | 0.652 / 0.108 |
225
- | fin_sentiment | 0.345 / 0.335 | 0.437 / 0.303 | 0.796 / 0.042 |
226
- | massive_scenario | 0.706 / 0.094 | 0.707 / 0.032 | 0.808 / 0.022 |
227
- | paws | 0.759 / 0.123 | 0.834 / 0.018 | 0.713 / 0.139 |
228
- | pubmedqa | 0.728 / 0.042 | 0.768 / 0.082 | 0.772 / 0.048 |
229
- | sciq | 0.978 / 0.011 | 0.988 / 0.019 | 0.983 / 0.021 |
230
- | social_iqa | 0.659 / 0.089 | 0.739 / 0.053 | 0.703 / 0.060 |
231
- | strategyqa | 0.566 / 0.029 | 0.646 / 0.037 | 0.597 / 0.089 |
232
- | student_questions | 0.903 / 0.059 | 0.933 / 0.021 | 0.927 / 0.029 |
233
- | trec | 0.696 / 0.062 | 0.822 / 0.050 | 0.816 / 0.039 |
234
- | truthfulqa | 0.460 / 0.111 | 0.591 / 0.113 | 0.529 / 0.058 |
235
- | tweet_irony | 0.511 / 0.158 | 0.676 / 0.073 | 0.769 / 0.048 |
236
-
237
-
238
- Probabilities use a temperature of 1.05 fitted on in-task data (stored in `decider_config.json`, applied by the helper).
239
- *In-task* = test splits of the training datasets (in-task rows for the zero-shot baselines cover the original 64). *Held-out* = 23 datasets never
240
- seen in training: TREC, BBC news, PAWS, SciQ, Social IQa, StrategyQA, PubMedQA,
241
- TruthfulQA, tweet irony, financial sentiment, ADE, MASSIVE scenario, student
242
- question categories, Dolly categories, CR reviews, Financial PhraseBank,
243
- CommitmentBank, QuALITY, XStoryCloze, RewardBench, Arena preferences (3-way),
244
- Hermes tool selection, and an abstention probe (held-out classification tasks
245
- where in half the cases the correct option is absent and "none of the above" is
246
- right). Chance accuracy is 0.33 on both sets. ECE = expected calibration error
247
- (15 bins), AURC = area under the risk-coverage curve, acc@80 = accuracy on
248
- the 80% most confident decisions.
249
 
250
  ## Speed
251
 
252
- One NVIDIA GH200, bf16. `decider.infer.Decider` uses shape-bucketed CUDA
253
- graphs (`decider/engine.py`); the micro-batching server is `decider/serve.py`
254
- in the GitHub repo. Support-ticket contexts of ~230 tokens with 3 to 5 typed
255
- questions each:
256
 
257
  | setting | p50 latency | throughput |
258
  |---|---|---|
259
  | single request, eager PyTorch | 49 ms | |
260
  | single request, CUDA graphs + torch.compile (helper default) | 4.0 ms | |
261
- | batch of 32, in-process, bf16 | 70 ms | ~1370 decisions/s |
262
- | batch of 32, in-process, FP8 linears | 58 ms | ~1670 decisions/s |
263
  | HTTP server (FP8), 1 client | 6.8 ms | 134 req/s |
264
- | HTTP server (FP8), 64 clients | 126 ms | 431 req/s, 2152 decisions/s |
265
 
266
- FP8 (e4m3 weights, per-token activation scales) changes accuracy and calibration by
267
- less than the evaluation noise (18-task check: accuracy 0.833 vs 0.835, ECE equal).
 
268
 
269
  ## Limitations
270
 
271
- * English only. Options must be short phrases; free-text fields are not supported.
272
- * Calibration is measured on public datasets; verify it on your own labelled
273
- data before using confidence for routing.
274
- * No reasoning: this is a fast pattern-matching decision model, not a chat model.
275
- * Inputs up to 32k tokens are accepted (v6 trained to 16k, probed to 30k). Plain long reading works (QuALITY, whole
276
- 5-8k-token article: 0.71, against 0.51 clipped). Picking one record out of a long JSON array by position is the weak
277
- case: 0.70 with 4 records, 0.64 with 16, 0.49 with 64 (single-record ceiling 0.72); address records by key where
278
- possible. The helper writes `"_index": i` into arrays of 8 or more elements, which recovers part of it (0.57 at 64).
279
- * Full label sets (v6): 0.84 on held-out HWU64 (64 options), 0.76 TREC-fine (50), 0.87 DBpedia level 3 (219), 0.70 DBpedia
280
- level 2 (70, ECE 0.14: the least calibrated of these). In-task CLINC 151-way 0.88 against 0.98 with 10 sampled options.
281
- * Questions packed into one row (`independent=False`, or `decide` with several questions) still see earlier question
282
- texts: reversing their order changes up to 12% of answers on multi-question tasks. The default `system_one` path
283
- scores each question alone and has no such dependence.
284
- * Held-out text game Freeway fell from 6 (v4) to 3 (v5) to 0 (v6) over three episodes; trained games are unchanged.
285
- * Knowledge-heavy multiple choice (MMLU, MedQA, ARC) improves only modestly over the
286
- base model; fine-tuning on decisions does not add world knowledge.
287
- * Compared with a run on the 47-dataset v1 mixture, adding the v2 datasets
288
- raised held-out accuracy but lowered two held-out tasks: Hermes tool selection
289
- (0.80 to 0.74) and TruthfulQA (0.54 to 0.50).
290
- * Scale fields are the least trained type; expect wider distributions there.
291
- * Abstention (v5): an option such as "none of the above", "other" or "unsure" is chosen when
292
- nothing on offer fits the situation, not when the exact fine-grained label is merely absent
293
- (it then takes the best available option). Trained with twelve abstain wordings and
294
- off-topic option lists; on a held-out probe with off-topic option lists it scores 0.83
295
- (v4: 0.68). Earlier versions (v4 and before) had learned the literal phrase as an abstain
296
- signal; the bundled helper's rewrite for that is disabled for v5 via `decider_config.json`.
297
- * Catch-all options next to generic ones ("support" vs "other"): v6 sent in-scope messages that fit only the generic option to
298
- the catch-all (0.60 on a hand-written battery, 0.50 on held-out teacher-written routing messages). v8: 0.85 and 0.94, with the
299
- catch-all cases at 0.95 and 0.90. Question wordings far from the training data (public datasets plus 24k teacher-written
300
- questions) remain the main risk; verify on your own examples.
301
- * The catch-all fix depends on the generic option looking generic (`general IT help`, `customer_service`, `general_query`). With the bare
302
- label `support` next to `other`, a plain app complaint still goes to `other` at 0.99. Name or describe the generic option as a bucket.
303
- * Isolated Score levels match listwise scoring within about a point (LIAR2: 3 points lower). Levels should describe situations,
304
- not degrees.
305
- * One in-task dataset, `tweet_hate` (SemEval-2019 HatEval), stays near chance on its
306
- test split. That split is known to differ from its training split in collection
307
- and label definition; the number is reported as measured.
308
 
309
  ## Reproduction
310
 
311
- Code, data registry, training and evaluation scripts: https://github.com/Mapika/decider
312
- (`decider/` in this model repo is the inference subset of that package).
 
 
6
  tags: [decision-model, calibrated, structured-output, multi-task, system-one, one-pass]
7
  ---
8
 
9
+ # decider-2b: typed decisions with calibrated probabilities in one forward pass
10
+
11
+ A language model that does not generate text. It reads a state and one or more typed questions, each with an explicit option
12
+ list, and returns a probability distribution over the options for every question from one forward pass. There is no decoding,
13
+ no parsing and no output outside the options you defined. It is called from software, not chatted with. It is an open
14
+ reproduction of the "System One" model class (TypeSafe AI's Jev).
15
+
16
+ Base model: [Qwen/Qwen3.5-2B-Base](https://huggingface.co/Qwen/Qwen3.5-2B-Base) (1.9B parameters). The supervised stages
17
+ (v1 to v8) fine-tune it with cross-entropy, a proper scoring rule, on a mixture of about 95 public decision datasets, agent
18
+ trajectories, web element choice, game states and teacher-written custom questions, in two prompt layouts and with isolated
19
+ Score levels. **This repository holds v10**: the v8 weights continued for 384 steps of calibration-aware reinforcement learning
20
+ whose only rewards are outcomes (live browser task checkers and the exact probability laws of games), with a hard KL limit to
21
+ the v8 weights on replayed training rows. Code, data registry, training scripts and the recipe are at
22
+ https://github.com/Mapika/decider; `decider/` in this repository is the inference subset of that package.
23
+
24
+ What changed from v8, measured on the same rows: live browser click tasks 83% to 93% sampled success (held-out tasks 73% to
25
+ 92%), stated beliefs about action outcomes 0.47 to 0.22 nats above the exact law, Mind2Web +1.5 points, general accuracy and
26
+ Bespoke's public suite unchanged, OpenJev −0.8 points. Details under Evaluation.
27
 
28
  ## Usage
29
 
30
  ```python
31
  from decider.infer import Decider # decider/ is included in this repo
32
+ d = Decider("Mapika/decider-2b")
33
  d.decide("My card was charged twice for the same purchase.",
34
  [{"question": "Which department should handle this?", "options": ["billing", "technical support", "sales"]},
35
  {"question": "Does this need a refund action?", "options": ["no", "yes"]}])
36
  # [{'choice': 'billing', 'confidence': 0.99, 'probs': {...}}, {'choice': 'yes', 'confidence': 0.99, 'probs': {...}}]
37
  ```
38
 
39
+ `decide_batch` scores many states, each with many questions, in one call. `abstain_below=t` returns `None` for decisions with
40
+ confidence under `t`. A question can have 2 to 255 options (more than 10 options use one label token per option, see
41
+ `decider/prompt.py`).
 
42
 
43
  The same request shape as TypeSafe's Jev (`POST /v1/systemone`), in process or over HTTP:
44
 
 
50
  "billing": {"what": "Charges, invoices", "not_for": "delivery"}, "other": None}},
51
  "refund_requested": {"type": "noul", "instructions": "Does `ticket.messages[0].text` request a refund?"},
52
  "frustration": {"type": "score", "instructions": "How frustrated is the customer?", "criteria": ["calm", "frustrated", "very frustrated"]}})
53
+ # {"model": "decider-v10", "answers": {"department": {"type": "choice", "choice": "billing", "confidence": ..., "certainty": ..., "probabilities": {...}},
54
  # "refund_requested": {"type": "noul", "noul": ...}, "frustration": {"type": "score", "score": ..., "legend": {...}, ...}}, "usage": {...}}
55
  ```
56
 
57
+ The state may be a string, object or array (up to 32k tokens with the questions). `instructions` and every option
58
+ description may be a string or any JSON value. Question ids are never shown to the model. Each question is scored in its own
59
+ row, so an answer does not depend on which other questions are asked (`independent=False` packs them into one row, about half
60
+ the latency for short states). Each Score level is likewise judged in its own row, without its number or its neighbours, and the
61
+ per-level fits are normalised (`"isolated": false` restores listwise scoring). The answer also reports `level_fit` and their
62
+ sum `fit_mass`, which is near 1 when exactly one level fits.
63
 
64
  For a fixed set of questions, `s = d.schema(questions)` computes the question prefix once and `s(state)` / `s.batch(states)`
65
+ then run only the state (1.2 to 2.4x faster per request, up to 19x per batch). It uses a questions-first prompt layout that
66
+ costs accuracy: about 1.5 points on fixed label sets, 5 on per-example options, more on 50 or more options and on states of
67
+ several thousand tokens. `decider.serve` exposes the same thing as `POST /v1/systemone`; the official `typesafe-sdk` works
68
+ against it unchanged with `TYPESAFE_BASE_URL` pointing at the server.
69
 
70
+ Requirements: `torch`, `transformers>=5`, and `flash-linear-attention` (Triton kernels for the Qwen3.5 linear-attention
71
+ layers; the model runs without it but several times slower). Python 3.11 or newer lets those kernels use `torch.compile`.
 
 
72
 
73
  Without the helper package, the same computation in plain `transformers`:
74
 
 
82
  with torch.no_grad():
83
  logits = m(**ids).logits[0, -1]
84
  letters = [tok.encode(L, add_special_tokens=False)[0] for L in "ABC"]
85
+ probs = torch.softmax(logits[letters].float() / 1.30, -1) # -> P(billing), P(technical support), P(sales); 1.30 is the stored temperature
86
  ```
87
 
88
+ For several questions in one pass, append further `Question k: ... Answer k: (` blocks and read the logits at each `(`
89
+ position (see `decider/prompt.py`).
90
 
91
  ## How it works
92
 
93
+ The prompt is `Context: ...` followed by, for each question, the question text, the lettered options `(A) ... (B) ...` and an
94
+ answer slot `Answer k: (`. The hidden state at each slot is projected with the option-letter rows of the LM head and softmaxed
95
+ over the valid letters, divided by the temperature in `decider_config.json`. Letters are never generated, so all slots are read
96
+ from one pass. Large label sets were sub-sampled to at most 10 options per training example (gold always kept, order shuffled),
97
+ so the model conditions on the supplied candidates rather than on a fixed head.
 
 
98
 
99
  ## Field types
100
 
101
+ * **noul**: probability of "yes".
102
+ * **choice** with `criteria` {name: description | JSON | null}: the argmax option, its probability (`confidence`, the calibrated
103
+ number), `certainty` (1 minus the normalised entropy) and the full distribution.
104
+ * **score** with `criteria` [level descriptions]: the expected level, the probability of the most likely level, the
105
+ distribution, and the per-level fits.
106
+
107
+ ## Training
108
+
109
+ **Supervised stages (v1 to v8).** One epoch on a mixture of public decision datasets (intent detection, ticket routing, topic
110
+ classification, sentiment, emotion, moderation, NLI, paraphrase, fact verification, passage relevance, reading comprehension,
111
+ multiple-choice QA, ordinal rating scales, pairwise response preference, tool selection), then continuation epochs that added
112
+ next-action choice from agent trajectories (AgentGym), web element choice (Mind2Web), teacher-written situations and game states,
113
+ the input shapes of the Jev API (described options, up to 255 options, JSON states with path references, long inputs), teacher-
114
+ written custom questions with a generic option next to a catch-all, a second cacheable prompt layout, and isolated Score levels.
115
+ In 10% of questions with three or more options an abstain option is added; in a quarter of those the option list is replaced by
116
+ labels from an unrelated task so that the abstain option is correct. The full list of components with sizes is in
117
+ `decider/data/mixture.py` of the GitHub repository; `scripts/train.sh full` reproduces the supervised stages in one run.
118
+
119
+ **Reinforcement learning stage (v8 to v10).** 384 optimizer steps at a peak learning rate of 1e-6 (cosine, 16 warm-up steps),
120
+ selected among the checkpoints of a 576-step run. Each of the 48 iterations plays 4 live MiniWoB++ click tasks, 4 minesweeper
121
+ boards and 4 game boards (a 5x5 grid with a slippery move, draws from bags of known composition), 4 repeats each, through the
122
+ same one-pass readout that serves requests. Three loss terms use those rollouts: a PPO clipped surrogate (clip 0.2) on the
123
+ terminal outcome with a leave-one-replicate-out baseline; a proper log score of the model's stated belief about the immediate
124
+ outcome of its action against the exact law (games, minesweeper) or the realised outcome (browser); and a rendering-consistency
125
+ term that pulls the model's answer in the other prompt layout and the reversed option order toward its served answer. A fourth
126
+ term keeps the model where it was: on 8 replayed supervised rows per step, KL(v8 ‖ student) on the served distribution must
127
+ stay under 0.01 nats on average and 0.05 on any row, otherwise the step drops the reward terms and follows only the KL
128
+ gradient. Six browser tasks were held out from reward and used for validation only. No gold labels were used. The recipe and
129
+ every measurement are in `docs/RL.md` of the GitHub repository.
130
 
131
  ## Evaluation
132
 
133
+ **94 public tasks, original protocol.** Large label sets sub-sampled to 10 options; one temperature fitted on in-task data
134
+ and stored in `decider_config.json`. "In-task" means the test splits of the training datasets; "held-out" means datasets never
135
+ seen in training (TREC, BBC news, PAWS, SciQ, Social IQa, StrategyQA, PubMedQA, TruthfulQA, tweet irony, financial sentiment,
136
+ ADE, MASSIVE scenario, student question categories, Dolly categories, CR reviews, Financial PhraseBank, CommitmentBank,
137
+ QuALITY, XStoryCloze, RewardBench, Arena preferences, Hermes tool selection, and an abstention probe). ECE is the expected
138
+ calibration error with 15 bins.
 
 
 
 
 
 
 
139
 
140
+ | model | in-task (69 tasks) acc / NLL / ECE | held-out (24 tasks) acc / NLL / ECE |
141
+ |---|---|---|
142
+ | Qwen3.5-2B-Base, zero-shot | 0.620 / 0.908 / 0.121 | 0.642 / 0.853 / 0.105 |
143
+ | decider-2b v8, T=1.30 | 0.811 / 0.460 / 0.037 | 0.741 / 0.655 / 0.088 |
144
+ | decider-2b v9, T=1.36 | 0.812 / 0.464 / 0.041 | 0.741 / 0.655 / 0.087 |
145
+ | decider-2b v8, rebuilt set (67 / 28 tasks, see note), T=1.30 | 0.806 / 0.473 / 0.038 | 0.757 / 0.622 / 0.083 |
146
+ | **decider-2b v10 (this repository), rebuilt set, T=1.30** | 0.805 / 0.474 / 0.037 | 0.755 / 0.622 / 0.084 |
147
+ | v8, questions-first layout (schema cache), T=1.18 | 0.790 / 0.500 / 0.038 | 0.707 / 0.757 / 0.104 |
148
 
149
+ The two "rebuilt set" rows were measured after the data pipeline was rebuilt on another machine: two datasets no longer download
150
+ (TREC-fine, the game states) and the current mixture adds held-out probes, so that set has 67 in-task and 28 held-out tasks. Its
151
+ numbers are comparable to each other, not to the rows above. v10 matches v8 on it.
152
 
153
+ Per-task accuracy / ECE on the held-out datasets of the rebuilt set, v8 against v10:
154
+
155
+ | task | v8 acc / ECE | v10 acc / ECE |
156
+ |---|---|---|
157
+ | abstain_probe | 0.633 / 0.112 | 0.606 / 0.134 |
158
+ | ade | 0.811 / 0.044 | 0.817 / 0.038 |
159
+ | arena_pref | 0.487 / 0.173 | 0.483 / 0.189 |
160
+ | bbc_news | 0.924 / 0.014 | 0.927 / 0.013 |
161
+ | cb | 0.911 / 0.090 | 0.857 / 0.093 |
162
+ | cr_reviews | 0.900 / 0.027 | 0.903 / 0.031 |
163
+ | dbpedia_l2 | 0.948 / 0.017 | 0.950 / 0.018 |
164
+ | dbpedia_l3 | 0.989 / 0.007 | 0.987 / 0.005 |
165
+ | dolly_category | 0.291 / 0.209 | 0.299 / 0.203 |
166
+ | fin_phrasebank | 0.684 / 0.043 | 0.694 / 0.042 |
167
+ | fin_sentiment | 0.794 / 0.069 | 0.793 / 0.058 |
168
+ | hermes_tools | 0.718 / 0.209 | 0.723 / 0.208 |
169
+ | hwu64 | 0.964 / 0.031 | 0.961 / 0.030 |
170
+ | massive_scenario | 0.766 / 0.040 | 0.756 / 0.041 |
171
+ | offtopic_probe | 0.841 / 0.033 | 0.841 / 0.027 |
172
+ | paws | 0.707 / 0.169 | 0.724 / 0.145 |
173
+ | pubmedqa | 0.752 / 0.083 | 0.756 / 0.085 |
174
+ | quality | 0.495 / 0.236 | 0.494 / 0.233 |
175
+ | quality_full | 0.505 / 0.205 | 0.508 / 0.198 |
176
+ | reward_bench | 0.825 / 0.042 | 0.819 / 0.045 |
177
+ | sciq | 0.982 / 0.022 | 0.982 / 0.024 |
178
+ | social_iqa | 0.698 / 0.072 | 0.708 / 0.077 |
179
+ | strategyqa | 0.559 / 0.123 | 0.552 / 0.138 |
180
+ | student_questions | 0.927 / 0.036 | 0.925 / 0.045 |
181
+ | trec | 0.792 / 0.057 | 0.784 / 0.066 |
182
+ | truthfulqa | 0.529 / 0.102 | 0.537 / 0.090 |
183
+ | tweet_irony | 0.801 / 0.048 | 0.795 / 0.052 |
184
+ | xstory_cloze | 0.962 / 0.017 | 0.962 / 0.017 |
185
+
186
+ **v10 against v8 on the same rows.** Every row below is scored by both models on identical inputs and seeds. Intervals are
187
+ 95% bootstrap or paired intervals.
188
+
189
+ | | v8 | v10 | difference |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  |---|---|---|---|
191
+ | live MiniWoB++ click tasks, 22 tasks x 8 seeds, sampled play | 83.0% | 93.2% | +10.2 (+5.1 to +15.9) |
192
+ | the 6 tasks never used for reward | 72.9% | 91.7% | +18.8 (+6.2 to +31.2) |
193
+ | same tasks, greedy play | 90.3% | 90.9% | +0.6 |
194
+ | Mind2Web element and action choice, 1,770 rows | 81.1% | 82.7% | +1.5 (+0.7 to +2.4) |
195
+ | bag-draw games, win rate | | | +6.2 (+0.8 to +12.1) |
196
+ | stated belief, nats above the exact law (lower is better) | 0.473 | 0.219 | |
197
+ | click-outcome prediction, log score (higher is better) | −0.349 | −0.034 | |
198
+ | TypeSafe workflow decisions, 102 rows, accuracy / NLL | 78.4% / 0.594 | 80.4% / 0.585 | +2.0 (−2.0 to +5.9) |
199
+ | 847 in-task validation rows, accuracy / NLL | 83.6% / 0.443 | 83.2% / 0.444 | −0.4 (−1.3 to +0.6) |
200
+ | Bespoke's public suite, 13 subsets, macro accuracy | 0.706 | 0.704 | |
201
+ | OpenJev, 5,252 rows, accuracy / NLL | 64.1% / 0.906 | 63.3% / 0.916 | −0.8 (−1.3 to −0.3) |
202
+
203
+ The browser gain is in the served distribution rather than in the argmax: sampled play improves by ten points, greedy play by
204
+ under one. Tic-tac-toe, grid and minesweeper play did not change; a 2B model without search loses most of those games either
205
+ way. The one measured regression is OpenJev, under one point.
206
+
207
+ **Bespoke's public suite** (13 human-labelled subsets, 3,880 records in Jev's wire format, answered through `system_one` as
208
+ shipped). decider-2b v10 macro 0.704 / micro 0.711; v9 0.701 / 0.711; Nimble-9B 0.748 / 0.759; Jev 1.13.0 0.760 / 0.773
209
+ (the last two copied from Bespoke's report). Per-subset numbers are in the GitHub README.
 
 
 
 
 
 
 
 
 
 
210
 
211
  ## Speed
212
 
213
+ One NVIDIA GH200, bf16, unchanged from v8 (same architecture, readout and temperature). `decider.infer.Decider` uses
214
+ shape-bucketed CUDA graphs; the batching server is `decider/serve.py`. Support-ticket states of about 230 tokens with 3 to 5
215
+ typed questions each:
 
216
 
217
  | setting | p50 latency | throughput |
218
  |---|---|---|
219
  | single request, eager PyTorch | 49 ms | |
220
  | single request, CUDA graphs + torch.compile (helper default) | 4.0 ms | |
221
+ | batch of 32, in-process, bf16 | 70 ms | about 1,370 decisions/s |
222
+ | batch of 32, in-process, FP8 linears | 58 ms | about 1,670 decisions/s |
223
  | HTTP server (FP8), 1 client | 6.8 ms | 134 req/s |
224
+ | HTTP server (FP8), 64 clients | 126 ms | 431 req/s, 2,152 decisions/s |
225
 
226
+ With the schema cache (`Decider.schema`), 10 described questions on short chat messages run at 11,180 decisions/s in a batch,
227
+ and one question with 151 options at 19x the full-forward rate. FP8 (e4m3 weights, per-token activation scales) changes
228
+ accuracy and calibration by less than the evaluation noise.
229
 
230
  ## Limitations
231
 
232
+ * A 2B model without reasoning. Knowledge-heavy multiple choice (MMLU, MedQA, ARC) improves little over the base model, and a
233
+ judgment that needs several steps should be split into several questions.
234
+ * English only. Calibration is measured on public datasets and teacher-labelled probes, not on your traffic. Check it on your
235
+ own labels before using confidence for routing.
236
+ * v10 continues the v8 weights. The v9 data for terse bucket names (`support`, `help`, `account` next to `other`) is not in it:
237
+ on held-out terse-bucket messages v8 chose the generic bucket correctly 59% of the time where v9 reached 86%. Name or
238
+ describe the generic option as a bucket (`general_support`, or a description).
239
+ * Rules written into the question ("fill if empty, otherwise skip") are not followed at this size. State the decision as a
240
+ plain question with described options.
241
+ * Picking one record out of a long JSON array by position is the least accurate input shape (0.51 with 64 records against
242
+ 0.70 with one). Address records by key, or let the helper write the index into the array (0.62).
243
+ * Full label sets cost accuracy against 10 sampled options: CLINC 151-way 0.88 against 0.98; DBpedia level 2 with 70 labels
244
+ is the least calibrated case (ECE 0.14).
245
+ * Questions packed into one row (`independent=False`) see the earlier question texts, and reversing their order changes up to
246
+ 12% of answers. The default path scores each question alone.
247
+ * The v10 browser results are on 22 click-only MiniWoB++ tasks: small synthetic pages with the elements listed as text. Typing,
248
+ scrolling and real websites were not tested. OpenJev accuracy is 0.8 points lower than v8.
249
+ * Abstention: a catch-all option ("none of the above", "other", "unsure") is chosen when nothing on offer fits, not when the
250
+ exact fine-grained label is merely absent. Wordings far from the training data remain the main risk.
251
+ * One in-task dataset, `tweet_hate` (SemEval-2019 HatEval), stays near chance on its test split, whose collection and label
252
+ definition differ from the training split. The number is reported as measured.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
  ## Reproduction
255
 
256
+ Code, data registry, training and evaluation scripts, the RL recipe and the per-version history:
257
+ https://github.com/Mapika/decider. Each release is staged with `scripts/stage_release.py` and uploaded with
258
+ `scripts/upload_hf.py`; the previous weights are kept under the tag `v8` in this repository.
decider_config.json CHANGED
@@ -1 +1 @@
1
- {"temperature": 1.3, "temperature_schema_first": 1.18, "neutralize_none": false, "version": "v8", "base": "Qwen/Qwen3.5-2B-Base", "max_options": 255, "max_state_tokens": 32768, "schema_first": false, "schema_first_trained": true, "isolated_levels": true, "release_date": "2026-09-17"}
 
1
+ {"temperature": 1.3, "temperature_schema_first": 1.18, "neutralize_none": false, "version": "v10", "base": "Qwen/Qwen3.5-2B-Base", "max_options": 255, "max_state_tokens": 32768, "schema_first": false, "schema_first_trained": true, "isolated_levels": true, "release_date": "2026-09-19", "parent": "decider-2b v8 (Mapika/decider-2b, tag v8)", "stage": "calibration-aware RL, 384 steps, outcome rewards from live MiniWoB++ click tasks and exact games, KL retention to v8 (docs/RL.md)"}
eval_results.json CHANGED
The diff for this file is too large to render. See raw diff
 
model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:49ca58057fd0cc34a70ffdbf5b8353659868a551fdcf1d3c395693041db1d588
3
  size 3763692048
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1bf79b6aa6966a0faf930940799b1f54a831368d9738123722d483597c0ac2e7
3
  size 3763692048