sileod commited on
Commit
afa49af
·
verified ·
1 Parent(s): 535a361

v2: shared-state joint cross-encoder (ettin-reranker-150m), Decision Index 0.3 run; v1 kept at tag v1

Browse files
1_Dense/config.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "in_features": 768,
3
- "out_features": 1536,
4
- "bias": false,
5
- "activation_function": "torch.nn.modules.linear.Identity",
6
- "use_residual": true
7
- }
 
 
 
 
 
 
 
 
1_Dense/model.safetensors DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:7575f7325b112aee78ff749c0f9fca59c827ec1769b7f267ce030a3e0a63bb44
3
- size 9437360
 
 
 
 
2_Dense/config.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "in_features": 1536,
3
- "out_features": 768,
4
- "bias": false,
5
- "activation_function": "torch.nn.modules.linear.Identity",
6
- "use_residual": true
7
- }
 
 
 
 
 
 
 
 
2_Dense/model.safetensors DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:8001a5b0366548829e3029afd3ca769e307fa8bfd60ddd2c06c1ef22dfae824e
3
- size 9437360
 
 
 
 
3_Dense/config.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "in_features": 768,
3
- "out_features": 128,
4
- "bias": false,
5
- "activation_function": "torch.nn.modules.linear.Identity",
6
- "use_residual": false
7
- }
 
 
 
 
 
 
 
 
3_Dense/model.safetensors DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:b8046cc411a84607fdd84b8776c59674c31de51670a29a141a831ca25111563e
3
- size 393304
 
 
 
 
README.md CHANGED
@@ -1,92 +1,93 @@
1
  ---
2
  license: apache-2.0
3
- language:
4
- - en
5
- library_name: PyLate
6
- pipeline_tag: decision-model
7
- base_model: lightonai/LateOn
8
  datasets:
9
  - tasksource/tasksource-jev-typed-decisions
 
10
  tags:
11
- - decision-model
12
- - jev
13
- - ColBERT
14
- - multi-vector
15
- - PyLate
16
- - sentence-transformers
17
  ---
18
 
19
- # tasksource-jev-nano-v1
20
 
21
- A **149M-parameter decision model**. Given a state, a question and a set of options, it returns a probability for each option. It follows the "Jev-style" typed-decision interface (choice / noul) and is evaluated on the [Decision Index](https://huggingface.co/spaces/multimodalart/jev-decision-index).
 
22
 
23
- It is [LateOn](https://huggingface.co/lightonai/LateOn) (ModernBERT-base, ColBERT multi-vector), fine-tuned on Tasksource typed decisions. Options are scored by **late interaction**:
 
 
24
 
25
- ```
26
- context = state + "\nQuestion: " + question → encoded once (ColBERT document side)
27
- option_i → encoded independently (ColBERT query side)
28
- logit_i = MaxSim(option_i, context) / T → softmax over options (T = 0.3168)
29
- ```
 
30
 
31
- The context never sees the options, so you can encode a state once and score any number of options against it. This also makes the scores permutation-equivariant.
32
 
33
- ## Usage
 
 
 
34
 
35
- ```python
36
- import torch
37
- from pylate import models
38
- from pylate.scores import colbert_scores
39
 
40
- model = models.ColBERT("tasksource/tasksource-jev-nano-v1")
41
- T = 0.31683406233787537 # learned temperature (decision_meta.json: init_temperature)
42
 
43
- def decide(state, question, options):
44
- ctx = model.encode([f"{state}\nQuestion: {question}"], is_query=False)[0]
45
- opts = model.encode(options, is_query=True)
46
- scores = torch.stack([colbert_scores(torch.as_tensor(o)[None], torch.as_tensor(ctx)[None])[0, 0] for o in opts])
47
- return dict(zip(options, torch.softmax(scores / T, 0).tolist()))
48
 
49
- decide("The movie was a total waste of two hours.", "What is the sentiment?", ["positive", "negative"])
50
- # {'positive': 0.0088, 'negative': 0.9912}
 
 
 
 
51
  ```
52
 
53
- This snippet reproduces our evaluation engine's probabilities exactly. For **noul** (yes/no) questions, score the two options `false` and `true` and report P(true).
54
-
55
- When an option has both a key and a description, the engine renders it as `"key: description"`, or as the description alone when the key is a placeholder like `A` or `option_1`.
56
-
57
- ## Training
58
 
59
- - **Init:** `lightonai/LateOn`.
60
- - **Data:** grouped requests (one state, 1–5 questions, 2–235 options each) from [tasksource-jev-typed-decisions](https://huggingface.co/datasets/tasksource/tasksource-jev-typed-decisions), covering 503 Tasksource training tasks.
61
- - Training used a 64k-request **double-firewalled** manifest: every row that fingerprint-matches a Decision Index suite row was removed before training.
62
- - **Recipe:**
63
- - Stage 1: 4,000 steps of grouped MaxSim training with soft cross-entropy over options and a learned temperature.
64
- - Stage 2: continued for 400 steps with *question packing* at p=0.5 (several questions share one context half the time).
65
- - **Checkpoint selection:** NLL on a held-out set of unseen Tasksource tasks.
66
-
67
- ## Decision Index results (public index)
68
-
69
- These are scores on the **public** part of the Decision Index, run with the official kit: full suite, 100% coverage, chance-corrected skill, 0–100.
70
-
71
- | Edition | Public index (skill) | Raw |
72
- |---|---|---|
73
- | 0.3 | **10.84** | 33.12 |
74
- | 0.2.1 | 10.46 | 32.09 |
75
-
76
- 0.3 areas (skill): Knowledge & Reasoning 2.2 · Language Understanding 5.0 · Retrieval & Classification 23.5 · Tools & Automation 13.6 · Arts & Human Taste 17.7.
77
-
78
- **This is not the board's headline number.** From 0.3 on, the board ranks a *Full* score:
79
-
80
- | Part | Weight | Who runs it |
81
- |---|---|---|
82
- | Public index | 20% | Anyone, with the kit; the table above |
83
- | Private tests of the same skills | 50% | Maintainers |
84
- | Private tasks from new domains | 30% | Maintainers |
85
 
86
- We do not have this model's Full score; see the board for it once it has been scored.
87
 
88
- ## Limitations
89
 
90
- - **Small model, broad but shallow.** It does well on retrieval, classification and tool selection. Knowledge and reasoning are near chance: 149M parameters do not store much world knowledge.
91
- - **Typed decisions only.** It handles `choice` and `noul`. It does not generate text, and it does not natively produce scores or multilabel outputs.
92
- - **English only.**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
+ library_name: transformers
4
+ base_model: cross-encoder/ettin-reranker-150m-v1
 
 
 
5
  datasets:
6
  - tasksource/tasksource-jev-typed-decisions
7
+ - tasksource/synthetic-typed-decisions
8
  tags:
9
+ - typed-decisions
10
+ - decision-index
11
+ - cross-encoder
 
 
 
12
  ---
13
 
14
+ # tasksource-decider-nano
15
 
16
+ This repository was `tasksource/tasksource-jev-nano-v1`. Its `main` (tag `v2`) is now a new model; the previous
17
+ multi-vector model (v1) stays at tag [`v1`](https://huggingface.co/tasksource/tasksource-decider-nano/tree/v1).
18
 
19
+ A 150M-parameter typed-decision model for the [Decision Index](https://huggingface.co/spaces/multimodalart/jev-decision-index).
20
+ It takes a `state` and typed questions (`choice`, `noul`) and returns a probability for every option. It does not
21
+ generate text.
22
 
23
+ | | |
24
+ |---|---|
25
+ | Decision Index 0.3, public index | **26.77** |
26
+ | Requests | 140,171 `ok`, 7 `unsupported` (pairs above 8,192 tokens) |
27
+ | Latency, 1× NVIDIA A10 23 GB, one request at a time | median 45 ms · mean 71 ms · p80 71 ms |
28
+ | Complete results | [runs/tasksource-decider-nano](https://huggingface.co/datasets/tasksource/decision-index-results/tree/main/runs/tasksource-decider-nano) |
29
 
30
+ ## Architecture
31
 
32
+ A joint cross-encoder on [cross-encoder/ettin-reranker-150m-v1](https://huggingface.co/cross-encoder/ettin-reranker-150m-v1)
33
+ (ModernBERT-style encoder, 8,192-token context). Every option is scored against the full state, its question
34
+ and the option text, like the reranker pair `(state, "Question: <instructions>\nOption: <option>")`, by a linear
35
+ head on the mean of the option's hidden states. The scores are softmaxed over each question's options.
36
 
37
+ The state is encoded **once per request**. All questions and options of a request share it through a tree
38
+ attention mask: the state attends to itself, a question to the state and itself, and an option to the state, its
39
+ question and itself. Rotary positions are those of the single pair, so every option gets the score it would get on its
40
+ own (up to float rounding), while a request with 50 options costs one state encoding instead of 50.
41
 
42
+ Inference reads every input in full. There is no truncation: a pair above 8,192 tokens raises `Unsupported`.
 
43
 
44
+ ## Usage
 
 
 
 
45
 
46
+ ```python
47
+ from decider import Decider
48
+ model = Decider.from_pretrained("tasksource/tasksource-decider-nano")
49
+ model.answer(state={"review": "The battery died after a week."},
50
+ questions={"q": {"type": "choice", "instructions": "What is the sentiment?",
51
+ "criteria": {"neg": "negative", "pos": "positive"}}})
52
  ```
53
 
54
+ Decision Index run (kit 0.3, `9eb2dbe`), from a local copy of this repository:
 
 
 
 
55
 
56
+ ```bash
57
+ hf download tasksource/tasksource-decider-nano --local-dir tasksource-decider-nano
58
+ cd tasksource-decider-nano
59
+ python -m decision_index pipeline --engine decision_index_engine:DeciderEngine \
60
+ --option repo=. --edition 0.3 --out runs/tasksource-decider-nano
61
+ ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
+ `decider.py` and `decision_index_engine.py` are self-contained (PyTorch and transformers only).
64
 
65
+ ## Training
66
 
67
+ 12,000 steps, batches of 8 requests, learning rate 2e-5, inputs up to 2,048 tokens, seed 7. The loss is soft
68
+ cross-entropy over each question's options. The training mix has 240,000 requests:
69
+
70
+ | Share | Source |
71
+ |---:|---|
72
+ | 42% | [tasksource/tasksource-jev-typed-decisions](https://huggingface.co/datasets/tasksource/tasksource-jev-typed-decisions) (658 tasks, at most 600 questions per task) |
73
+ | 30% | *train* splits of public datasets behind Decision Index benchmarks, e.g. MMLU, ANLI, WinoGrande, HellaSwag, ARC, CLINC150, BANKING77, Amazon ESCI, HoVer, RAGTruth, New Yorker caption contest, When2Call, ACOS, iSarcasmEval, NLI4CT, ContractNLI, GSM8K (at most 10,000 requests each) |
74
+ | 12% | tool use: ToolRet train, Glaive function calling, Hermes function calling |
75
+ | 8% | LLM routing: xRouteBench and LLMRouterBench train data |
76
+ | 8% | [tasksource/synthetic-typed-decisions](https://huggingface.co/datasets/tasksource/synthetic-typed-decisions) |
77
+
78
+ No evaluation split is used, and benchmarks without a train split (for example RouterBench) are evaluation only.
79
+ Every training request goes through a firewall against the Decision Index 0.3 suite: an exact match of the
80
+ normalized state or of the (state, question) pair, and a template-insensitive word 6-gram content check, drop the
81
+ request.
82
+
83
+ ## Evaluation
84
+
85
+ | Area | Skill | Raw |
86
+ |---|---:|---:|
87
+ | Knowledge & Reasoning | 6.0 | 29.5 |
88
+ | Language Understanding | 28.2 | 47.9 |
89
+ | Retrieval & Classification | 42.0 | 58.2 |
90
+ | Tools & Automation | 44.6 | 49.8 |
91
+ | Arts & Human Taste | 13.9 | 43.5 |
92
+
93
+ Raw index 45.1; kit `9eb2dbe`, edition 0.3, 140,171 requests scored.
config.json CHANGED
@@ -4,7 +4,8 @@
4
  ],
5
  "attention_bias": false,
6
  "attention_dropout": 0.0,
7
- "bos_token_id": 50281,
 
8
  "classifier_activation": "gelu",
9
  "classifier_bias": false,
10
  "classifier_dropout": 0.0,
@@ -14,14 +15,21 @@
14
  "deterministic_flash_attn": false,
15
  "dtype": "float32",
16
  "embedding_dropout": 0.0,
17
- "eos_token_id": 50282,
18
  "global_attn_every_n_layers": 3,
19
  "gradient_checkpointing": false,
20
  "hidden_activation": "gelu",
21
  "hidden_size": 768,
 
 
 
22
  "initializer_cutoff_factor": 2.0,
23
  "initializer_range": 0.02,
24
  "intermediate_size": 1152,
 
 
 
 
25
  "layer_norm_eps": 1e-05,
26
  "layer_types": [
27
  "full_attention",
@@ -48,7 +56,7 @@
48
  "full_attention"
49
  ],
50
  "local_attention": 128,
51
- "max_position_embeddings": 8192,
52
  "mlp_bias": false,
53
  "mlp_dropout": 0.0,
54
  "model_type": "modernbert",
@@ -57,15 +65,14 @@
57
  "num_attention_heads": 12,
58
  "num_hidden_layers": 22,
59
  "pad_token_id": 50283,
60
- "position_embedding_type": "absolute",
61
- "repad_logits_with_grad": false,
62
  "rope_parameters": {
63
  "full_attention": {
64
  "rope_theta": 160000.0,
65
  "rope_type": "default"
66
  },
67
  "sliding_attention": {
68
- "rope_theta": 10000.0,
69
  "rope_type": "default"
70
  }
71
  },
@@ -74,5 +81,5 @@
74
  "sparse_prediction": false,
75
  "tie_word_embeddings": true,
76
  "transformers_version": "5.17.0",
77
- "vocab_size": 50372
78
  }
 
4
  ],
5
  "attention_bias": false,
6
  "attention_dropout": 0.0,
7
+ "bos_token_id": null,
8
+ "causal_mask": false,
9
  "classifier_activation": "gelu",
10
  "classifier_bias": false,
11
  "classifier_dropout": 0.0,
 
15
  "deterministic_flash_attn": false,
16
  "dtype": "float32",
17
  "embedding_dropout": 0.0,
18
+ "eos_token_id": null,
19
  "global_attn_every_n_layers": 3,
20
  "gradient_checkpointing": false,
21
  "hidden_activation": "gelu",
22
  "hidden_size": 768,
23
+ "id2label": {
24
+ "0": "LABEL_0"
25
+ },
26
  "initializer_cutoff_factor": 2.0,
27
  "initializer_range": 0.02,
28
  "intermediate_size": 1152,
29
+ "is_causal": false,
30
+ "label2id": {
31
+ "LABEL_0": 0
32
+ },
33
  "layer_norm_eps": 1e-05,
34
  "layer_types": [
35
  "full_attention",
 
56
  "full_attention"
57
  ],
58
  "local_attention": 128,
59
+ "max_position_embeddings": 7999,
60
  "mlp_bias": false,
61
  "mlp_dropout": 0.0,
62
  "model_type": "modernbert",
 
65
  "num_attention_heads": 12,
66
  "num_hidden_layers": 22,
67
  "pad_token_id": 50283,
68
+ "position_embedding_type": "sans_pos",
 
69
  "rope_parameters": {
70
  "full_attention": {
71
  "rope_theta": 160000.0,
72
  "rope_type": "default"
73
  },
74
  "sliding_attention": {
75
+ "rope_theta": 160000.0,
76
  "rope_type": "default"
77
  }
78
  },
 
81
  "sparse_prediction": false,
82
  "tie_word_embeddings": true,
83
  "transformers_version": "5.17.0",
84
+ "vocab_size": 50368
85
  }
config_sentence_transformers.json DELETED
@@ -1,54 +0,0 @@
1
- {
2
- "model_type": "ColBERT",
3
- "__version__": {
4
- "sentence_transformers": "5.1.2",
5
- "transformers": "5.17.0",
6
- "pytorch": "2.7.1+cu126"
7
- },
8
- "prompts": {
9
- "query": "",
10
- "document": ""
11
- },
12
- "default_prompt_name": null,
13
- "similarity_fn_name": "MaxSim",
14
- "query_prefix": "[Q] ",
15
- "document_prefix": "[D] ",
16
- "query_length": 256,
17
- "document_length": 2048,
18
- "attend_to_expansion_tokens": false,
19
- "skiplist_words": [
20
- "!",
21
- "\"",
22
- "#",
23
- "$",
24
- "%",
25
- "&",
26
- "'",
27
- "(",
28
- ")",
29
- "*",
30
- "+",
31
- ",",
32
- "-",
33
- ".",
34
- "/",
35
- ":",
36
- ";",
37
- "<",
38
- "=",
39
- ">",
40
- "?",
41
- "@",
42
- "[",
43
- "\\",
44
- "]",
45
- "^",
46
- "_",
47
- "`",
48
- "{",
49
- "|",
50
- "}",
51
- "~"
52
- ],
53
- "do_query_expansion": false
54
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
decider.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Self-contained inference code for a joint typed-decision cross-encoder (ModernBERT-family encoder).
2
+
3
+ Each option of a question is read as one tokenizer pair
4
+
5
+ (state, "Question: <instructions>\\nOption: <option>")
6
+
7
+ and scored by a linear head on the attention-masked mean of the last hidden states; a question's
8
+ probabilities are the softmax over its options' scores. Nothing is truncated: a pair longer than
9
+ `max_length` (default 8,192 tokens, the encoder's native context) raises `TooLong`. All the
10
+ (question, option) pairs of a request are sorted by length and run together in padded batches.
11
+
12
+ Layout "shared" (decider_config.json) reads exactly the same tokens, but writes the state once per
13
+ request: [CLS] state [SEP], then each "Question: <instructions>\nOption:" block once, then each
14
+ " <option> [SEP]" block, with tree attention (state sees state; a question block sees the state and
15
+ itself; an option block sees the state, its question and itself, never other options). RoPE
16
+ positions follow the virtual pair, so the per-pair limit is unchanged. Pooling "opt" averages the
17
+ option block only.
18
+
19
+ from decider import Decider
20
+ model = Decider.from_pretrained("<repo or directory>")
21
+ model.answer(state={"text": "..."}, questions={"q1": {"type": "choice", "instructions": "...",
22
+ "criteria": {"a": "...", "b": "..."}}})
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ from pathlib import Path
29
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
30
+
31
+ import numpy as np
32
+
33
+ FILES = ["config.json", "decider_config.json", "model.safetensors", "tokenizer.json", "tokenizer_config.json",
34
+ "special_tokens_map.json"]
35
+
36
+
37
+ class TooLong(ValueError):
38
+ """An input does not fit the model's context; inputs are never truncated."""
39
+
40
+
41
+ def text(x: Any) -> str:
42
+ return x if isinstance(x, str) else json.dumps(x, ensure_ascii=False, separators=(",", ":"))
43
+
44
+
45
+ def state_text(state: Any) -> str:
46
+ return text(state) if state not in ("", None, {}, []) else "(empty)"
47
+
48
+
49
+ def format_option(key: str, desc: Any) -> str:
50
+ desc_str = text(desc).strip() if desc is not None else ""
51
+ key_str = str(key).strip()
52
+ if not desc_str:
53
+ return key_str
54
+ is_dummy_key = (key_str.lower().startswith(("option_", "k_", "choice_", "item_"))
55
+ or (len(key_str) == 1 and key_str.isalpha())
56
+ or (key_str.startswith("(") and key_str.endswith(")") and len(key_str) <= 4))
57
+ if is_dummy_key:
58
+ return desc_str
59
+ if desc_str.lower() == key_str.lower():
60
+ return key_str
61
+ return f"{key_str}: {desc_str}"
62
+
63
+
64
+ def question_options(q: dict) -> Tuple[List[str], List[str]]:
65
+ """(answer keys, rendered option texts) for a choice or noul question."""
66
+ crit = q.get("criteria", {}) or {}
67
+ if q.get("type", "choice") == "choice":
68
+ keys = list(crit)
69
+ return keys, [format_option(k, crit[k]) for k in keys]
70
+ descs = (crit.get("false", crit.get("False", "False")), crit.get("true", crit.get("True", "True")))
71
+ return ["false", "true"], [format_option("false", descs[0]), format_option("true", descs[1])]
72
+
73
+
74
+ def tail_text(question: str, option: str) -> str:
75
+ return f"Question: {question}\nOption: {option}"
76
+
77
+
78
+ def _resolve(path_or_repo: str, revision: Optional[str] = None) -> Path:
79
+ p = Path(path_or_repo)
80
+ if p.is_dir():
81
+ return p
82
+ from huggingface_hub import snapshot_download
83
+ return Path(snapshot_download(path_or_repo, allow_patterns=FILES, revision=revision))
84
+
85
+
86
+ class Decider:
87
+ def __init__(self, directory: Path, device: Optional[str] = None, max_length: Optional[int] = None,
88
+ token_budget: int = 65536):
89
+ import torch
90
+ import torch.nn as nn
91
+ from safetensors.torch import load_file
92
+ from transformers import AutoConfig, AutoModel, AutoTokenizer
93
+
94
+ self.torch = torch
95
+ self.config = json.loads((directory / "decider_config.json").read_text())
96
+ self.max_length = int(max_length or self.config.get("inference_max_length", 8192))
97
+ self.token_budget = int(token_budget)
98
+ self.tok = AutoTokenizer.from_pretrained(str(directory))
99
+ cfg = AutoConfig.from_pretrained(str(directory))
100
+ net = nn.Module()
101
+ net.backbone = AutoModel.from_config(cfg)
102
+ net.drop = nn.Dropout(0.1)
103
+ net.scorer = nn.Linear(cfg.hidden_size, 1)
104
+ net.load_state_dict(load_file(str(directory / "model.safetensors")), strict=True)
105
+ self.device = torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu"))
106
+ self.net = net.to(self.device).eval()
107
+ self.layout = self.config.get("layout", "pair")
108
+ self.pool = self.config.get("pool", "opt")
109
+ self.window = getattr(cfg, "sliding_window", None)
110
+
111
+ @classmethod
112
+ def from_pretrained(cls, path_or_repo: str, revision: Optional[str] = None, **kw) -> "Decider":
113
+ return cls(_resolve(path_or_repo, revision), **kw)
114
+
115
+ def score_many(self, items: Sequence[Tuple[str, str, Sequence[str]]]) -> List[np.ndarray]:
116
+ """items: [(state, instructions, option texts)] -> one logit array per item."""
117
+ if self.layout == "shared":
118
+ return self._score_shared(items)
119
+ torch = self.torch
120
+ pairs = [self.tok(s, tail_text(q, o), truncation=False)["input_ids"] for s, q, opts in items for o in opts]
121
+ longest = max(map(len, pairs))
122
+ if longest > self.max_length:
123
+ raise TooLong(f"a (state, question, option) pair needs {longest} tokens > {self.max_length}")
124
+ order = sorted(range(len(pairs)), key=lambda i: len(pairs[i]))
125
+ scores = np.zeros(len(pairs), dtype=np.float64)
126
+ i = 0
127
+ with torch.no_grad():
128
+ while i < len(order):
129
+ j = i
130
+ while j < len(order) and (j - i + 1) * len(pairs[order[j]]) <= max(self.token_budget, len(pairs[order[i]])):
131
+ j += 1
132
+ idx = order[i:j]
133
+ width = len(pairs[idx[-1]])
134
+ ids = torch.full((len(idx), width), self.tok.pad_token_id, dtype=torch.long)
135
+ mask = torch.zeros((len(idx), width), dtype=torch.long)
136
+ for r, k in enumerate(idx):
137
+ ids[r, :len(pairs[k])] = torch.tensor(pairs[k])
138
+ mask[r, :len(pairs[k])] = 1
139
+ ids, mask = ids.to(self.device), mask.to(self.device)
140
+ with torch.autocast(self.device.type, dtype=torch.bfloat16, enabled=self.device.type == "cuda"):
141
+ h = self.net.backbone(input_ids=ids, attention_mask=mask).last_hidden_state
142
+ m = mask.unsqueeze(-1).to(h.dtype)
143
+ z = self.net.scorer((h * m).sum(1) / m.sum(1).clamp_min(1)).squeeze(-1)
144
+ scores[idx] = z.float().cpu().numpy()
145
+ i = j
146
+ out, start = [], 0
147
+ for _, _, opts in items:
148
+ out.append(scores[start:start + len(opts)])
149
+ start += len(opts)
150
+ return out
151
+
152
+ def _score_shared(self, items, row_extra: int = 2048) -> List[np.ndarray]:
153
+ torch, tok = self.torch, self.tok
154
+ rows, oid, prefix = [], 0, {}
155
+ for s, q, opts in items: # one row group per distinct state; rows of max(2P, P + row_extra) tokens
156
+ p = prefix.setdefault(s, tok(s)["input_ids"])
157
+ qi = tok(f"Question: {q}\nOption:", add_special_tokens=False)["input_ids"]
158
+ ob = [tok(" " + o, add_special_tokens=False)["input_ids"] + [tok.sep_token_id] for o in opts]
159
+ longest = len(p) + len(qi) + max(map(len, ob))
160
+ if longest > self.max_length:
161
+ raise TooLong(f"a (state, question, option) pair needs {longest} tokens > {self.max_length}")
162
+ cap = max(2 * len(p), len(p) + row_extra)
163
+ if not rows or rows[-1][0] is not p or rows[-1][2] + len(qi) + len(ob[0]) > cap:
164
+ rows.append([p, [], len(p)])
165
+ k = 0
166
+ while k < len(ob):
167
+ row = rows[-1]
168
+ take, size = [], len(qi)
169
+ while k + len(take) < len(ob) and (not take or row[2] + size + len(ob[k + len(take)]) <= cap):
170
+ take.append((oid + k + len(take), ob[k + len(take)]))
171
+ size += len(take[-1][1])
172
+ if row[1] and row[2] + size > cap:
173
+ rows.append([p, [], len(p)])
174
+ continue
175
+ row[1].append((qi, take))
176
+ row[2] += size
177
+ k += len(take)
178
+ oid += len(ob)
179
+ scores = np.zeros(oid, dtype=np.float64)
180
+ order = sorted(range(len(rows)), key=lambda i: rows[i][2])
181
+ i = 0
182
+ with torch.no_grad():
183
+ while i < len(order):
184
+ j = i + 1
185
+ while (j < len(order) and (j - i + 1) * rows[order[j]][2] <= self.token_budget
186
+ and (j - i + 1) * rows[order[j]][2] ** 2 <= 2 ** 26): # bound dense mask size
187
+ j += 1
188
+ batch = [rows[k] for k in order[i:j]]
189
+ L, B = max(r[2] for r in batch), len(batch)
190
+ ids = torch.full((B, L), tok.pad_token_id, dtype=torch.long)
191
+ pos = torch.zeros((B, L), dtype=torch.long)
192
+ kind = torch.full((B, L), -1, dtype=torch.long)
193
+ qix = torch.full((B, L), -1, dtype=torch.long)
194
+ oix = torch.full((B, L), -1, dtype=torch.long)
195
+ spans = []
196
+ for r, (p, blocks, _) in enumerate(batch):
197
+ ids[r, :len(p)], pos[r, :len(p)], kind[r, :len(p)] = torch.tensor(p), torch.arange(len(p)), 0
198
+ c = len(p)
199
+ for b, (qi, take) in enumerate(blocks):
200
+ n = len(qi)
201
+ ids[r, c:c + n], pos[r, c:c + n] = torch.tensor(qi), torch.arange(len(p), len(p) + n)
202
+ kind[r, c:c + n], qix[r, c:c + n] = 1, b
203
+ c += n
204
+ for o, x in take:
205
+ m = len(x)
206
+ ids[r, c:c + m] = torch.tensor(x)
207
+ pos[r, c:c + m] = torch.arange(len(p) + n, len(p) + n + m)
208
+ kind[r, c:c + m], qix[r, c:c + m], oix[r, c:c + m] = 2, b, o
209
+ spans.append((o, r, c, m, len(p), (r, len(p) + sum(len(bb[0]) + sum(len(y) for _, y in bb[1]) for bb in blocks[:b])), n))
210
+ c += m
211
+ ids, pos, kind, qix, oix = (t.to(self.device) for t in (ids, pos, kind, qix, oix))
212
+ ki, kj = kind[:, :, None], kind[:, None, :]
213
+ allow = (kj == 0) & (ki >= 0)
214
+ allow |= (kj == 1) & (ki >= 1) & (qix[:, :, None] == qix[:, None, :])
215
+ allow |= (kj == 2) & (ki == 2) & (oix[:, :, None] == oix[:, None, :])
216
+ allow |= torch.eye(L, dtype=torch.bool, device=self.device)[None]
217
+ masks = {"full_attention": allow[:, None]}
218
+ if self.window is not None:
219
+ masks["sliding_attention"] = (allow & ((pos[:, :, None] - pos[:, None, :]).abs() <= self.window))[:, None]
220
+ with torch.autocast(self.device.type, dtype=torch.bfloat16, enabled=self.device.type == "cuda"):
221
+ h = self.net.backbone(input_ids=ids, attention_mask=masks, position_ids=pos).last_hidden_state
222
+ h = h.float()
223
+ if self.pool == "opt": # vectorized mean over each option block
224
+ sel = oix >= 0
225
+ flat = oix[sel]
226
+ uniq, inv = torch.unique(flat, return_inverse=True)
227
+ v = torch.zeros(len(uniq), h.shape[-1], device=h.device).index_add_(0, inv, h[sel])
228
+ v = v / torch.bincount(inv).unsqueeze(-1).to(v.dtype)
229
+ with torch.autocast(self.device.type, dtype=torch.bfloat16, enabled=self.device.type == "cuda"):
230
+ z = self.net.scorer(v).squeeze(-1)
231
+ scores[uniq.cpu().numpy()] = z.float().cpu().numpy()
232
+ i = j
233
+ continue
234
+ vecs = []
235
+ for o, r, c, m, plen, (_, qstart), n in spans:
236
+ v = h[r, c:c + m].sum(0)
237
+ if self.pool == "pair":
238
+ v = (v + h[r, :plen].sum(0) + h[r, qstart:qstart + n].sum(0)) / (plen + n + m)
239
+ else:
240
+ v = v / m
241
+ vecs.append(v)
242
+ with torch.autocast(self.device.type, dtype=torch.bfloat16, enabled=self.device.type == "cuda"):
243
+ z = self.net.scorer(torch.stack(vecs)).squeeze(-1)
244
+ scores[[sp[0] for sp in spans]] = z.float().cpu().numpy()
245
+ i = j
246
+ out, start = [], 0
247
+ for _, _, opts in items:
248
+ out.append(scores[start:start + len(opts)])
249
+ start += len(opts)
250
+ return out
251
+
252
+ def answer(self, state: Any, questions: Dict[str, Any]) -> Dict[str, Any]:
253
+ """Decision Index request -> {"answers": {qid: answer}}. Supports `choice` and `noul`."""
254
+ s = state_text(state)
255
+ parsed = []
256
+ for qid, q in questions.items():
257
+ kind = q.get("type", "choice")
258
+ if kind not in ("choice", "noul") or (kind == "choice" and not q.get("criteria")):
259
+ raise ValueError(f"unsupported question type {kind!r}")
260
+ keys, opts = question_options(q)
261
+ parsed.append((qid, kind, keys, opts, text(q.get("instructions", ""))))
262
+ logits = self.score_many([(s, ins, opts) for _, _, _, opts, ins in parsed])
263
+ answers = {}
264
+ for (qid, kind, keys, _, _), z in zip(parsed, logits):
265
+ p = np.exp(z - z.max())
266
+ p /= p.sum()
267
+ if kind == "noul":
268
+ answers[qid] = {"type": "noul", "noul": float(np.clip(p[1], 0.0, 1.0))}
269
+ else:
270
+ answers[qid] = {"type": "choice", "choice": keys[int(p.argmax())],
271
+ "probabilities": {k: float(v) for k, v in zip(keys, p)}}
272
+ return {"answers": answers}
decider_config.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "format": "pair-v1",
3
+ "arch": "joint",
4
+ "base": "cross-encoder/ettin-reranker-150m-v1",
5
+ "train_max_length": 2048,
6
+ "inference_max_length": 8192,
7
+ "step": 12000,
8
+ "train": {
9
+ "arch": "joint",
10
+ "opt_pool": "marker",
11
+ "shuffle_options": false,
12
+ "manifest": "runs/manifests/mix_all.jsonl",
13
+ "extra_manifest": null,
14
+ "extra_frac": 0.0,
15
+ "lr": 2e-05,
16
+ "batch_requests": 8,
17
+ "max_k": 32,
18
+ "seed": 7,
19
+ "total_steps": 12000,
20
+ "max_length": 2048,
21
+ "pack": 0,
22
+ "init": null,
23
+ "init_backbone": null,
24
+ "gc_tokens": 0,
25
+ "layout": "shared",
26
+ "pool": "opt",
27
+ "row_tokens": 8192,
28
+ "tok_budget": 12288
29
+ },
30
+ "layout": "shared",
31
+ "pool": "opt"
32
+ }
decision_index_engine.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Decision Index engine for this model (https://github.com/apolinario/decision-index).
2
+
3
+ python -m decision_index run --engine decision_index_engine:DeciderEngine \
4
+ --option repo=<repo or directory> [--option revision=<commit>] --edition 0.3 --out runs/<name>
5
+
6
+ Put this file next to `decider.py` (both are in the model repository) and on PYTHONPATH.
7
+ Kit rules: nothing is truncated and no option is dropped. A (state, question, option) pair longer than
8
+ 8,192 tokens (the encoder's native context) raises `Unsupported`. One call = one request: all questions
9
+ of the request are scored together, in length-sorted padded batches.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ from decision_index.engines import Engine, Unsupported
18
+
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
20
+ from decider import Decider, TooLong # noqa: E402
21
+
22
+
23
+ class DeciderEngine(Engine):
24
+ name = "decider"
25
+ latency = ("In-process request wall time: tokenization plus the encoder passes of one request "
26
+ "(all its question/option pairs batched together); excludes model loading.")
27
+
28
+ def __init__(self, repo: str = str(Path(__file__).resolve().parent), revision: str | None = None,
29
+ device: str | None = None, max_length: int | None = None, token_budget: int = 65536, **options):
30
+ super().__init__(**options)
31
+ self.model = Decider.from_pretrained(repo, revision=revision, device=device, max_length=max_length,
32
+ token_budget=int(token_budget))
33
+ self.repo = repo
34
+ self.provenance = {"repo": repo, "revision": revision or "main", "device": str(self.model.device),
35
+ "dtype": "bfloat16 autocast" if self.model.device.type == "cuda" else "float32",
36
+ "max_length": self.model.max_length, "token_budget": self.model.token_budget,
37
+ "truncation": f"none: pairs above {self.model.max_length} tokens raise Unsupported"}
38
+
39
+ def __call__(self, state, questions):
40
+ try:
41
+ out = self.model.answer(state, questions)
42
+ except TooLong as e:
43
+ raise Unsupported(f"context window: {e}")
44
+ except ValueError as e:
45
+ raise Unsupported(str(e))
46
+ return {"model": self.repo, "answers": out["answers"]}, None
47
+
48
+ def synchronize(self):
49
+ if self.model.device.type == "cuda":
50
+ self.model.torch.cuda.synchronize()
decision_meta.json DELETED
@@ -1,37 +0,0 @@
1
- {
2
- "init_temperature": 0.31683406233787537,
3
- "path_mode": "sq_o",
4
- "pooling_budget": null,
5
- "protected_tokens": 1,
6
- "rel_slots": 2,
7
- "joint_rel_distinct": false,
8
- "rel_score_scale": 256.0,
9
- "role_tokens": false,
10
- "trimax_rank": null,
11
- "has_query_gate": false,
12
- "gate_dim": null,
13
- "document_length": 2048,
14
- "query_length": 256,
15
- "model_max_length": 8192,
16
- "length_norm_gamma": 0.0,
17
- "config": {
18
- "model_name_or_path": "/mnt/nfs_share_magnet2/dsileo/libs/train_jev/runs/multivector/scaling_grouped_p2/step-4000",
19
- "embedding_dim": 128,
20
- "state_max_length": 512,
21
- "query_max_length": 64,
22
- "init_temperature": 0.3215,
23
- "learn_temperature": true,
24
- "path_mode": "sq_o",
25
- "pooling_budget": null,
26
- "protected_tokens": 1,
27
- "rel_slots": 2,
28
- "joint_rel_distinct": false,
29
- "rel_score_scale": null,
30
- "role_tokens": false,
31
- "trimax_rank": null,
32
- "query_prefix": "[Q] ",
33
- "document_prefix": "[D] ",
34
- "backend": "torch",
35
- "metadata": {}
36
- }
37
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:48578d08f6b50c9ba6654d3d26fa71c381c7a34ea9791a6493877dea475f036d
3
- size 596082424
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a96dcfdf2d4e7da62429ba9f836535adf3a328f30cb51366095ad2c38fb13659
3
+ size 596074556
modules.json DELETED
@@ -1,26 +0,0 @@
1
- [
2
- {
3
- "idx": 0,
4
- "name": "0",
5
- "path": "",
6
- "type": "sentence_transformers.models.Transformer"
7
- },
8
- {
9
- "idx": 1,
10
- "name": "1",
11
- "path": "1_Dense",
12
- "type": "pylate.models.Dense.Dense"
13
- },
14
- {
15
- "idx": 2,
16
- "name": "2",
17
- "path": "2_Dense",
18
- "type": "pylate.models.Dense.Dense"
19
- },
20
- {
21
- "idx": 3,
22
- "name": "3",
23
- "path": "3_Dense",
24
- "type": "pylate.models.Dense.Dense"
25
- }
26
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sentence_bert_config.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "max_seq_length": 255,
3
- "do_lower_case": false
4
- }
 
 
 
 
 
tokenizer.json CHANGED
@@ -2,18 +2,11 @@
2
  "version": "1.0",
3
  "truncation": {
4
  "direction": "Right",
5
- "max_length": 255,
6
  "strategy": "LongestFirst",
7
  "stride": 0
8
  },
9
- "padding": {
10
- "strategy": "BatchLongest",
11
- "direction": "Right",
12
- "pad_to_multiple_of": null,
13
- "pad_id": 50284,
14
- "pad_type_id": 0,
15
- "pad_token": "[MASK]"
16
- },
17
  "added_tokens": [
18
  {
19
  "id": 0,
@@ -1058,42 +1051,6 @@
1058
  "rstrip": false,
1059
  "normalized": true,
1060
  "special": false
1061
- },
1062
- {
1063
- "id": 50368,
1064
- "content": "[Q] ",
1065
- "single_word": false,
1066
- "lstrip": false,
1067
- "rstrip": false,
1068
- "normalized": true,
1069
- "special": false
1070
- },
1071
- {
1072
- "id": 50369,
1073
- "content": "[D] ",
1074
- "single_word": false,
1075
- "lstrip": false,
1076
- "rstrip": false,
1077
- "normalized": true,
1078
- "special": false
1079
- },
1080
- {
1081
- "id": 50370,
1082
- "content": "[REL0]",
1083
- "single_word": false,
1084
- "lstrip": false,
1085
- "rstrip": false,
1086
- "normalized": false,
1087
- "special": true
1088
- },
1089
- {
1090
- "id": 50371,
1091
- "content": "[REL1]",
1092
- "single_word": false,
1093
- "lstrip": false,
1094
- "rstrip": false,
1095
- "normalized": false,
1096
- "special": true
1097
  }
1098
  ],
1099
  "normalizer": {
 
2
  "version": "1.0",
3
  "truncation": {
4
  "direction": "Right",
5
+ "max_length": 7999,
6
  "strategy": "LongestFirst",
7
  "stride": 0
8
  },
9
+ "padding": null,
 
 
 
 
 
 
 
10
  "added_tokens": [
11
  {
12
  "id": 0,
 
1051
  "rstrip": false,
1052
  "normalized": true,
1053
  "special": false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1054
  }
1055
  ],
1056
  "normalizer": {
tokenizer_config.json CHANGED
@@ -2,19 +2,16 @@
2
  "backend": "tokenizers",
3
  "clean_up_tokenization_spaces": true,
4
  "cls_token": "[CLS]",
5
- "is_local": true,
6
  "local_files_only": false,
7
  "mask_token": "[MASK]",
8
- "max_length": 299,
9
  "model_input_names": [
10
  "input_ids",
11
  "attention_mask"
12
  ],
13
- "model_max_length": 8192,
14
- "pad_to_multiple_of": null,
15
- "pad_token": "[MASK]",
16
- "pad_token_type_id": 0,
17
- "padding_side": "right",
18
  "sep_token": "[SEP]",
19
  "stride": 0,
20
  "tokenizer_class": "TokenizersBackend",
 
2
  "backend": "tokenizers",
3
  "clean_up_tokenization_spaces": true,
4
  "cls_token": "[CLS]",
5
+ "is_local": false,
6
  "local_files_only": false,
7
  "mask_token": "[MASK]",
8
+ "max_length": 7999,
9
  "model_input_names": [
10
  "input_ids",
11
  "attention_mask"
12
  ],
13
+ "model_max_length": 7999,
14
+ "pad_token": "[PAD]",
 
 
 
15
  "sep_token": "[SEP]",
16
  "stride": 0,
17
  "tokenizer_class": "TokenizersBackend",