decido-ai 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- decido_ai-1.0.0/PKG-INFO +62 -0
- decido_ai-1.0.0/README.md +36 -0
- decido_ai-1.0.0/pyproject.toml +29 -0
- decido_ai-1.0.0/setup.cfg +4 -0
- decido_ai-1.0.0/src/decido/__init__.py +5 -0
- decido_ai-1.0.0/src/decido/_vendor/__init__.py +0 -0
- decido_ai-1.0.0/src/decido/_vendor/ds/__init__.py +0 -0
- decido_ai-1.0.0/src/decido/_vendor/ds/derive.py +42 -0
- decido_ai-1.0.0/src/decido/_vendor/ds/predictor_v4.py +85 -0
- decido_ai-1.0.0/src/decido/_vendor/ds/readout.py +175 -0
- decido_ai-1.0.0/src/decido/_vendor/ds/template.py +333 -0
- decido_ai-1.0.0/src/decido/_vendor/ds/v4/__init__.py +3 -0
- decido_ai-1.0.0/src/decido/_vendor/ds/v4/attention.py +109 -0
- decido_ai-1.0.0/src/decido/_vendor/ds/v4/experts.py +53 -0
- decido_ai-1.0.0/src/decido/_vendor/ds/v4/fp4.py +71 -0
- decido_ai-1.0.0/src/decido/_vendor/ds/v4/grouped.py +165 -0
- decido_ai-1.0.0/src/decido/_vendor/ds/v4/loader.py +144 -0
- decido_ai-1.0.0/src/decido/_vendor/ds/v4/numerics.py +91 -0
- decido_ai-1.0.0/src/decido/_vendor/ds/v4/pipeline.py +111 -0
- decido_ai-1.0.0/src/decido/_vendor/ds/v4/score.py +113 -0
- decido_ai-1.0.0/src/decido/_vendor/teacher/__init__.py +0 -0
- decido_ai-1.0.0/src/decido/_vendor/teacher/derive.py +42 -0
- decido_ai-1.0.0/src/decido/_vendor/teacher/predictor_hf.py +143 -0
- decido_ai-1.0.0/src/decido/_vendor/teacher/predictor_reason.py +317 -0
- decido_ai-1.0.0/src/decido/_vendor/teacher/readout.py +159 -0
- decido_ai-1.0.0/src/decido/_vendor/teacher/template.py +301 -0
- decido_ai-1.0.0/src/decido/api.py +86 -0
- decido_ai-1.0.0/src/decido/build.py +90 -0
- decido_ai-1.0.0/src/decido/cli.py +86 -0
- decido_ai-1.0.0/src/decido/core/__init__.py +0 -0
- decido_ai-1.0.0/src/decido/core/derive.py +42 -0
- decido_ai-1.0.0/src/decido/core/readout.py +159 -0
- decido_ai-1.0.0/src/decido/core/template.py +301 -0
- decido_ai-1.0.0/src/decido/cpu.py +56 -0
- decido_ai-1.0.0/src/decido/csrc/vserve.cpp +237 -0
- decido_ai-1.0.0/src/decido/gpu_systems.py +115 -0
- decido_ai-1.0.0/src/decido/models.py +154 -0
- decido_ai-1.0.0/src/decido/pro.py +78 -0
- decido_ai-1.0.0/src/decido/questions.py +56 -0
- decido_ai-1.0.0/src/decido/server.py +288 -0
- decido_ai-1.0.0/src/decido/torch_engine.py +65 -0
- decido_ai-1.0.0/src/decido_ai.egg-info/PKG-INFO +62 -0
- decido_ai-1.0.0/src/decido_ai.egg-info/SOURCES.txt +45 -0
- decido_ai-1.0.0/src/decido_ai.egg-info/dependency_links.txt +1 -0
- decido_ai-1.0.0/src/decido_ai.egg-info/entry_points.txt +2 -0
- decido_ai-1.0.0/src/decido_ai.egg-info/requires.txt +20 -0
- decido_ai-1.0.0/src/decido_ai.egg-info/top_level.txt +1 -0
decido_ai-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: decido-ai
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Calibrated decisions (yes/no, pick-one, ordered scale) from a document — Decido models from the Hugging Face Hub, on a laptop CPU or a GPU.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://huggingface.co/decido-ai
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: numpy>=1.24
|
|
10
|
+
Requires-Dist: tokenizers>=0.20
|
|
11
|
+
Requires-Dist: huggingface_hub>=0.25
|
|
12
|
+
Provides-Extra: gpu
|
|
13
|
+
Requires-Dist: torch>=2.4; extra == "gpu"
|
|
14
|
+
Requires-Dist: transformers>=5.17; extra == "gpu"
|
|
15
|
+
Requires-Dist: accelerate; extra == "gpu"
|
|
16
|
+
Provides-Extra: xl
|
|
17
|
+
Requires-Dist: torch>=2.6; extra == "xl"
|
|
18
|
+
Requires-Dist: transformers>=5.22; extra == "xl"
|
|
19
|
+
Requires-Dist: accelerate; extra == "xl"
|
|
20
|
+
Requires-Dist: peft>=0.21; extra == "xl"
|
|
21
|
+
Requires-Dist: triton>=3.2; extra == "xl"
|
|
22
|
+
Requires-Dist: safetensors; extra == "xl"
|
|
23
|
+
Provides-Extra: pro
|
|
24
|
+
Requires-Dist: vllm==0.31.0; extra == "pro"
|
|
25
|
+
Requires-Dist: transformers>=5.17; extra == "pro"
|
|
26
|
+
|
|
27
|
+
# decido
|
|
28
|
+
|
|
29
|
+
**Calibrated decisions from a document — on a laptop CPU or a GPU.** Give Decido a document and questions (yes/no, pick-one,
|
|
30
|
+
or an ordered scale) and get a probability for every answer. This package runs the [Decido 1 models](https://huggingface.co/decido-ai)
|
|
31
|
+
from the Hugging Face Hub.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install decido-ai
|
|
35
|
+
decido build-runtime # only on Linux / Intel Macs (one-time, needs cmake + a C++ compiler); Apple Silicon Macs: skip
|
|
36
|
+
```
|
|
37
|
+
```python
|
|
38
|
+
from decido import Decido
|
|
39
|
+
|
|
40
|
+
m = Decido("decido-ai/decido-1-fast-1.7b") # or decido-1-26b-a4b (most accurate on CPU), decido-1-ultra-0.6b (fastest)
|
|
41
|
+
m.decide("Customer: I was charged twice for order 4821. Please refund one of the charges today.",
|
|
42
|
+
{"refund": "Is the customer asking for a refund?",
|
|
43
|
+
"team": {"q": "Which team should handle this?", "options": ["billing", "shipping", "tech"]},
|
|
44
|
+
"urgency": {"q": "How urgent is it?", "scale": ["low", "normal", "urgent"]}})
|
|
45
|
+
```
|
|
46
|
+
```bash
|
|
47
|
+
decido models # the family, with scores and latency
|
|
48
|
+
decido run decido-ai/decido-1-fast-1.7b "Customer: charged twice" -q refund="Is a refund requested?"
|
|
49
|
+
decido serve decido-ai/decido-1-26b-a4b # Jev-compatible HTTP API on http://127.0.0.1:8080/v1/systemone
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| model | runs on | six-group score (Jev 1.13 = 83.85) | latency (M4 Pro CPU, 1k-token document, 5 questions) |
|
|
53
|
+
|---|---|---|---|
|
|
54
|
+
| [decido-1-26b-a4b](https://huggingface.co/decido-ai/decido-1-26b-a4b) | laptop CPU (~30 GB RAM) · GPU | 85.38 | 4.1 s |
|
|
55
|
+
| [decido-1-fast-1.7b](https://huggingface.co/decido-ai/decido-1-fast-1.7b) | laptop CPU | 73.01 | 0.64 s |
|
|
56
|
+
| [decido-1-ultra-0.6b](https://huggingface.co/decido-ai/decido-1-ultra-0.6b) | laptop CPU | 62.16 | 0.41 s |
|
|
57
|
+
| [decido-1-think-26b-a4b](https://huggingface.co/decido-ai/decido-1-think-26b-a4b) | GPU · laptop CPU (slow) | 88.71 (GPU) | see card |
|
|
58
|
+
| [decido-1-xl](https://huggingface.co/decido-ai/decido-1-xl) | 4× A100 80 GB | 87.25 (GPU) | — |
|
|
59
|
+
| [decido-1-pro](https://huggingface.co/decido-ai/decido-1-pro) | GPU | 89.57 (GPU) | — |
|
|
60
|
+
|
|
61
|
+
GPU extras: `pip install "decido-ai[gpu]"` (transformers), `"decido-ai[xl]"`, `"decido-ai[pro]"` (vLLM). Results, calibration,
|
|
62
|
+
disclosures and limitations are on each model card. License: Apache-2.0; the CPU runtime links llama.cpp (MIT).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# decido
|
|
2
|
+
|
|
3
|
+
**Calibrated decisions from a document — on a laptop CPU or a GPU.** Give Decido a document and questions (yes/no, pick-one,
|
|
4
|
+
or an ordered scale) and get a probability for every answer. This package runs the [Decido 1 models](https://huggingface.co/decido-ai)
|
|
5
|
+
from the Hugging Face Hub.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install decido-ai
|
|
9
|
+
decido build-runtime # only on Linux / Intel Macs (one-time, needs cmake + a C++ compiler); Apple Silicon Macs: skip
|
|
10
|
+
```
|
|
11
|
+
```python
|
|
12
|
+
from decido import Decido
|
|
13
|
+
|
|
14
|
+
m = Decido("decido-ai/decido-1-fast-1.7b") # or decido-1-26b-a4b (most accurate on CPU), decido-1-ultra-0.6b (fastest)
|
|
15
|
+
m.decide("Customer: I was charged twice for order 4821. Please refund one of the charges today.",
|
|
16
|
+
{"refund": "Is the customer asking for a refund?",
|
|
17
|
+
"team": {"q": "Which team should handle this?", "options": ["billing", "shipping", "tech"]},
|
|
18
|
+
"urgency": {"q": "How urgent is it?", "scale": ["low", "normal", "urgent"]}})
|
|
19
|
+
```
|
|
20
|
+
```bash
|
|
21
|
+
decido models # the family, with scores and latency
|
|
22
|
+
decido run decido-ai/decido-1-fast-1.7b "Customer: charged twice" -q refund="Is a refund requested?"
|
|
23
|
+
decido serve decido-ai/decido-1-26b-a4b # Jev-compatible HTTP API on http://127.0.0.1:8080/v1/systemone
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| model | runs on | six-group score (Jev 1.13 = 83.85) | latency (M4 Pro CPU, 1k-token document, 5 questions) |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| [decido-1-26b-a4b](https://huggingface.co/decido-ai/decido-1-26b-a4b) | laptop CPU (~30 GB RAM) · GPU | 85.38 | 4.1 s |
|
|
29
|
+
| [decido-1-fast-1.7b](https://huggingface.co/decido-ai/decido-1-fast-1.7b) | laptop CPU | 73.01 | 0.64 s |
|
|
30
|
+
| [decido-1-ultra-0.6b](https://huggingface.co/decido-ai/decido-1-ultra-0.6b) | laptop CPU | 62.16 | 0.41 s |
|
|
31
|
+
| [decido-1-think-26b-a4b](https://huggingface.co/decido-ai/decido-1-think-26b-a4b) | GPU · laptop CPU (slow) | 88.71 (GPU) | see card |
|
|
32
|
+
| [decido-1-xl](https://huggingface.co/decido-ai/decido-1-xl) | 4× A100 80 GB | 87.25 (GPU) | — |
|
|
33
|
+
| [decido-1-pro](https://huggingface.co/decido-ai/decido-1-pro) | GPU | 89.57 (GPU) | — |
|
|
34
|
+
|
|
35
|
+
GPU extras: `pip install "decido-ai[gpu]"` (transformers), `"decido-ai[xl]"`, `"decido-ai[pro]"` (vLLM). Results, calibration,
|
|
36
|
+
disclosures and limitations are on each model card. License: Apache-2.0; the CPU runtime links llama.cpp (MIT).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "decido-ai"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Calibrated decisions (yes/no, pick-one, ordered scale) from a document — Decido models from the Hugging Face Hub, on a laptop CPU or a GPU."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "Apache-2.0"}
|
|
12
|
+
dependencies = ["numpy>=1.24", "tokenizers>=0.20", "huggingface_hub>=0.25"]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
gpu = ["torch>=2.4", "transformers>=5.17", "accelerate"]
|
|
16
|
+
xl = ["torch>=2.6", "transformers>=5.22", "accelerate", "peft>=0.21", "triton>=3.2", "safetensors"]
|
|
17
|
+
pro = ["vllm==0.31.0", "transformers>=5.17"]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
decido = "decido.cli:main"
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://huggingface.co/decido-ai"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.package-data]
|
|
29
|
+
decido = ["_bin/*", "csrc/*.cpp"]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Jev 1.13 wire answers + derived fields (formulas from research/jev-teardown.md §4), computed unrounded."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def choice_confidence(p) -> float:
|
|
9
|
+
n = len(p)
|
|
10
|
+
if n <= 1:
|
|
11
|
+
return 1.0
|
|
12
|
+
return float((n * max(p) - 1) / (n - 1))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def score_confidence(p) -> float:
|
|
16
|
+
p = np.asarray(p, dtype=float)
|
|
17
|
+
n = len(p)
|
|
18
|
+
if n <= 1:
|
|
19
|
+
return 1.0
|
|
20
|
+
m = int(np.argmax(p))
|
|
21
|
+
idx = np.arange(n)
|
|
22
|
+
mad_unif = float(np.mean(np.abs(idx - (n - 1) / 2)))
|
|
23
|
+
return float(max(0.0, 1.0 - float((p * np.abs(idx - m)).sum()) / mad_unif))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def wire_answer(q: dict, p, round_dp: int | None = None) -> dict:
|
|
27
|
+
"""p is aligned with eval.parity.common.option_keys(q)."""
|
|
28
|
+
p = np.asarray(p, dtype=float)
|
|
29
|
+
r = (lambda x: round(float(x), round_dp)) if round_dp is not None else float
|
|
30
|
+
t = q["type"]
|
|
31
|
+
if t == "noul":
|
|
32
|
+
return {"type": "noul", "noul": r(p[1])}
|
|
33
|
+
if t == "choice":
|
|
34
|
+
keys = list(q["criteria"].keys())
|
|
35
|
+
return {"type": "choice", "choice": keys[int(np.argmax(p))],
|
|
36
|
+
"probabilities": {k: r(v) for k, v in zip(keys, p)}, "confidence": r(choice_confidence(p))}
|
|
37
|
+
if t == "score":
|
|
38
|
+
levels = list(q["criteria"])
|
|
39
|
+
return {"type": "score", "score": r(float((np.arange(len(p)) * p).sum())),
|
|
40
|
+
"legend": {str(i): levels[i] for i in range(len(levels))},
|
|
41
|
+
"probabilities": {str(i): r(v) for i, v in enumerate(p)}, "confidence": r(score_confidence(p))}
|
|
42
|
+
raise ValueError(t)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""DeepSeek-V4-Flash predictor for the parity harness and gold dev (GPU, HF eager forward via verdict.v4).
|
|
2
|
+
|
|
3
|
+
VERDICT_ALLOW_GPU=1 python -m eval.parity.run --allow-gpu --batch-size 2048 \
|
|
4
|
+
--predictor verdict.predictor_v4:VerdictV4 --predictor-kw adapter=runs/train/dsD/frac0.6-stepN
|
|
5
|
+
|
|
6
|
+
Every question is scored as a whole prompt (state prefix + one question [+ first label symbol for >26 options]); V4's
|
|
7
|
+
compressor cache is stateful, so there is no prefix reuse (cost = full re-prefill per question). Batches are
|
|
8
|
+
length-sorted under a padded-token and a padded B*L^2 budget. The LoRA adapter stays unmerged (peft), FP4 experts stay
|
|
9
|
+
packed. Throughput (prompts, tokens, tok/s) is appended to `stats` after every batch call.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
from decido._vendor.ds.readout import Readout
|
|
18
|
+
from decido._vendor.ds.template import ChatFormat
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VerdictV4:
|
|
22
|
+
def __init__(self, model: str = "deepseek-ai/DeepSeek-V4-Flash-0731", adapter: str | None = None, perm: int = 1,
|
|
23
|
+
temps=None, max_prompt_tokens: int = 8192, max_tokens: int = 32768, max_s2: int = 16_000_000,
|
|
24
|
+
attn: str = "band", on_overflow: str = "truncate", pipeline_mb: int = 8):
|
|
25
|
+
import torch
|
|
26
|
+
from decido._vendor.ds.v4.loader import load_v4
|
|
27
|
+
self.model, self.tok, self.info = load_v4(model, log=print)
|
|
28
|
+
if attn == "band":
|
|
29
|
+
from decido._vendor.ds.v4.attention import register
|
|
30
|
+
register(self.model)
|
|
31
|
+
if adapter:
|
|
32
|
+
from peft import PeftModel
|
|
33
|
+
from decido._vendor.ds.template import check_model_template
|
|
34
|
+
check_model_template(adapter, "verdict-ds-t1")
|
|
35
|
+
self.model = PeftModel.from_pretrained(self.model, adapter, is_trainable=False)
|
|
36
|
+
self.model.eval()
|
|
37
|
+
torch.set_grad_enabled(False)
|
|
38
|
+
if isinstance(temps, str) and temps:
|
|
39
|
+
temps = json.load(open(temps))
|
|
40
|
+
self.cf = ChatFormat(self.tok, family="deepseek_v4", version="verdict-ds-t1")
|
|
41
|
+
self.readout = Readout(self.cf, perm=perm, temps=temps or None, max_prompt_tokens=max_prompt_tokens,
|
|
42
|
+
on_overflow=on_overflow)
|
|
43
|
+
self.kw = {"max_tokens": max_tokens, "max_s2": max_s2, "pad_id": self.tok.pad_token_id or 0,
|
|
44
|
+
"pipeline_mb": int(pipeline_mb)}
|
|
45
|
+
self.stats = []
|
|
46
|
+
|
|
47
|
+
def _base(self):
|
|
48
|
+
m = self.model
|
|
49
|
+
return m.get_base_model() if hasattr(m, "get_base_model") else m
|
|
50
|
+
|
|
51
|
+
def predict_batch(self, requests):
|
|
52
|
+
from decido._vendor.ds.v4.score import score_plans
|
|
53
|
+
plans, out_idx, outs = [], [], [None] * len(requests)
|
|
54
|
+
for i, r in enumerate(requests):
|
|
55
|
+
try:
|
|
56
|
+
plans.append(self.readout.plan(r))
|
|
57
|
+
out_idx.append(i)
|
|
58
|
+
except ValueError as e:
|
|
59
|
+
outs[i] = {"__error__": str(e)[:200]}
|
|
60
|
+
t0 = time.time()
|
|
61
|
+
rec = {}
|
|
62
|
+
res = score_plans(self._base(), self.readout, plans, log=lambda m: rec.setdefault("log", m), **self.kw)
|
|
63
|
+
for i, pl, r in zip(out_idx, plans, res):
|
|
64
|
+
raw = r["answers"]
|
|
65
|
+
from decido._vendor.ds.derive import wire_answer
|
|
66
|
+
import numpy as np
|
|
67
|
+
resp = {"answers": {qid: wire_answer(pl.request["questions"][qid], np.asarray(p)) for qid, p in raw.items()}}
|
|
68
|
+
if "_logp" in r:
|
|
69
|
+
resp["_logp"] = r["_logp"]
|
|
70
|
+
if pl.truncated_tokens:
|
|
71
|
+
resp["truncated_state_tokens"] = pl.truncated_tokens
|
|
72
|
+
outs[i] = resp
|
|
73
|
+
self.stats.append({"requests": len(requests), "s": round(time.time() - t0, 2), **rec})
|
|
74
|
+
return outs
|
|
75
|
+
|
|
76
|
+
def predict(self, request):
|
|
77
|
+
return self.predict_batch([request])[0]
|
|
78
|
+
|
|
79
|
+
def predict_raw(self, requests):
|
|
80
|
+
"""Raw (temperature-applied) distributions + pre-temperature log-probs, for gold dev / calib."""
|
|
81
|
+
from decido._vendor.ds.v4.score import score_plans
|
|
82
|
+
import os
|
|
83
|
+
os.environ["VERDICT_DUMP_DIST"] = "1"
|
|
84
|
+
plans = [self.readout.plan(r) for r in requests]
|
|
85
|
+
return score_plans(self._base(), self.readout, plans, **self.kw)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Backend-agnostic verbalizer readout: request -> token queries -> calibrated distributions.
|
|
2
|
+
|
|
3
|
+
A backend only has to answer two kinds of query on (prefix_ids + suffix_ids [+ extra_ids]):
|
|
4
|
+
"log-probabilities of these token ids at the next position".
|
|
5
|
+
Everything else (labels, variants, permutation averaging, temperature, wire format) lives here so the
|
|
6
|
+
vLLM, HF and llama.cpp paths produce the same numbers up to backend numerics.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
import os
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from decido._vendor.ds.derive import wire_answer
|
|
18
|
+
from decido._vendor.ds.template import LabelTrie
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _lse(xs):
|
|
22
|
+
m = max(xs)
|
|
23
|
+
if m == -math.inf:
|
|
24
|
+
return -math.inf
|
|
25
|
+
return m + math.log(sum(math.exp(x - m) for x in xs))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def request_key(request: dict) -> str:
|
|
29
|
+
import hashlib
|
|
30
|
+
import json
|
|
31
|
+
return hashlib.sha256(json.dumps({"state": request["state"], "questions": request["questions"]}, sort_keys=True,
|
|
32
|
+
ensure_ascii=False).encode()).hexdigest()[:32]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def cyclic_orders(k: int, m: int) -> list[list[int]]:
|
|
36
|
+
m = max(1, min(m, k))
|
|
37
|
+
shifts = sorted({round(j * k / m) % k for j in range(m)})
|
|
38
|
+
return [[(i + s) % k for i in range(k)] for s in shifts]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Query:
|
|
43
|
+
qid: str
|
|
44
|
+
order: list[int]
|
|
45
|
+
suffix_ids: list[int]
|
|
46
|
+
trie: LabelTrie
|
|
47
|
+
step1: dict = field(default_factory=dict) # token id -> logprob
|
|
48
|
+
step2: dict = field(default_factory=dict) # first symbol -> {token id -> logprob}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class Plan:
|
|
53
|
+
request: dict
|
|
54
|
+
prefix_ids: list[int]
|
|
55
|
+
queries: list[Query]
|
|
56
|
+
truncated_tokens: int = 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Readout:
|
|
60
|
+
def __init__(self, chat_format, perm: int = 1, temps: dict | None = None, max_prompt_tokens: int = 16000,
|
|
61
|
+
on_overflow: str = "error"):
|
|
62
|
+
self.cf = chat_format
|
|
63
|
+
self.tok = chat_format.tok
|
|
64
|
+
self.perm = int(perm)
|
|
65
|
+
self.temps = temps or {}
|
|
66
|
+
self.max_prompt_tokens = max_prompt_tokens
|
|
67
|
+
self.on_overflow = on_overflow # "error" (Jev-like max_tokens_exceeded) or "truncate" (flagged)
|
|
68
|
+
self._trie_cache = {}
|
|
69
|
+
self._enc_cache = {}
|
|
70
|
+
|
|
71
|
+
def trie(self, labels: list[str]) -> LabelTrie:
|
|
72
|
+
key = tuple(labels)
|
|
73
|
+
if key not in self._trie_cache:
|
|
74
|
+
self._trie_cache[key] = LabelTrie(self.tok, labels)
|
|
75
|
+
return self._trie_cache[key]
|
|
76
|
+
|
|
77
|
+
def _enc(self, text: str) -> list[int]:
|
|
78
|
+
ids = self._enc_cache.get(text)
|
|
79
|
+
if ids is None:
|
|
80
|
+
ids = self.cf.encode(text)
|
|
81
|
+
if len(self._enc_cache) < 200000:
|
|
82
|
+
self._enc_cache[text] = ids
|
|
83
|
+
return ids
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def validate(request: dict):
|
|
87
|
+
"""Jev request rules (research/jev-teardown.md §2.2): choice 2-255 options, score 2-10 levels."""
|
|
88
|
+
qs = request.get("questions")
|
|
89
|
+
if not isinstance(qs, dict) or not qs:
|
|
90
|
+
raise ValueError("invalid_request: questions must be a non-empty map")
|
|
91
|
+
for qid, q in qs.items():
|
|
92
|
+
t = q.get("type")
|
|
93
|
+
if t not in ("noul", "choice", "score"):
|
|
94
|
+
raise ValueError(f"invalid_request: question {qid!r} has unsupported type {t!r}")
|
|
95
|
+
if q.get("instructions") in (None, ""):
|
|
96
|
+
raise ValueError(f"invalid_request: question {qid!r} needs instructions")
|
|
97
|
+
if t == "choice" and not (isinstance(q.get("criteria"), dict) and 2 <= len(q["criteria"]) <= 255):
|
|
98
|
+
raise ValueError(f"invalid_request: choice {qid!r} needs 2-255 options")
|
|
99
|
+
if t == "score" and not (isinstance(q.get("criteria"), list) and 2 <= len(q["criteria"]) <= 10):
|
|
100
|
+
raise ValueError(f"invalid_request: score {qid!r} needs 2-10 levels")
|
|
101
|
+
|
|
102
|
+
def plan(self, request: dict, perm: int | None = None) -> Plan:
|
|
103
|
+
self.validate(request)
|
|
104
|
+
perm = self.perm if perm is None else perm
|
|
105
|
+
prefix_ids = self.cf.encode(self.cf.prefix_text(request["state"]))
|
|
106
|
+
queries = []
|
|
107
|
+
max_suffix = 0
|
|
108
|
+
for qid, q in request["questions"].items():
|
|
109
|
+
orders = [None]
|
|
110
|
+
if q["type"] == "choice" and perm > 1:
|
|
111
|
+
orders = cyclic_orders(len(q["criteria"]), perm)
|
|
112
|
+
for order in orders:
|
|
113
|
+
text, labels, order_used = self.cf.qblock(q, order)
|
|
114
|
+
sids = self._enc(self.cf.suffix_text(text))
|
|
115
|
+
max_suffix = max(max_suffix, len(sids))
|
|
116
|
+
queries.append(Query(qid, order_used, sids, self.trie(labels)))
|
|
117
|
+
budget = self.max_prompt_tokens - max_suffix - 4
|
|
118
|
+
truncated = 0
|
|
119
|
+
if len(prefix_ids) > budget:
|
|
120
|
+
if self.on_overflow != "truncate":
|
|
121
|
+
raise ValueError(f"max_tokens_exceeded: state+question needs {len(prefix_ids) + max_suffix} tokens, "
|
|
122
|
+
f"limit {self.max_prompt_tokens}")
|
|
123
|
+
closing = self.cf.encode("\n</document>\n\n") # keep the head of the state; flagged in the response
|
|
124
|
+
truncated = len(prefix_ids) - max(budget - len(closing), 16) - len(closing)
|
|
125
|
+
prefix_ids = prefix_ids[:max(budget - len(closing), 16)] + closing
|
|
126
|
+
pl = Plan(request, prefix_ids, queries)
|
|
127
|
+
pl.truncated_tokens = truncated
|
|
128
|
+
return pl
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------- combination
|
|
131
|
+
|
|
132
|
+
def label_logprobs(self, qu: Query) -> np.ndarray:
|
|
133
|
+
tr = qu.trie
|
|
134
|
+
if not tr.two_step:
|
|
135
|
+
s = [_lse([qu.step1.get(t, -math.inf) for t in tr.first_variants[sym]]) for sym in tr.labels]
|
|
136
|
+
else:
|
|
137
|
+
s = []
|
|
138
|
+
for lab in tr.labels:
|
|
139
|
+
a = _lse([qu.step1.get(t, -math.inf) for t in tr.first_variants[lab[0]]])
|
|
140
|
+
b = _lse([qu.step2.get(lab[0], {}).get(t, -math.inf) for t in tr.second_variants[lab[1]]])
|
|
141
|
+
s.append(a + b)
|
|
142
|
+
s = np.array(s, dtype=float)
|
|
143
|
+
if not np.isfinite(s).any():
|
|
144
|
+
s = np.zeros_like(s)
|
|
145
|
+
s = np.where(np.isfinite(s), s, -1e4)
|
|
146
|
+
return s - (s.max() + np.log(np.exp(s - s.max()).sum()))
|
|
147
|
+
|
|
148
|
+
def combine(self, plan: Plan, round_dp: int | None = None, raw: bool = False) -> dict:
|
|
149
|
+
req = plan.request
|
|
150
|
+
by_q: dict[str, list[np.ndarray]] = {}
|
|
151
|
+
for qu in plan.queries:
|
|
152
|
+
lp_disp = self.label_logprobs(qu) # display positions
|
|
153
|
+
lp = np.empty_like(lp_disp)
|
|
154
|
+
lp[np.array(qu.order)] = lp_disp # -> option_keys order
|
|
155
|
+
by_q.setdefault(qu.qid, []).append(lp)
|
|
156
|
+
answers, logp = {}, {}
|
|
157
|
+
for qid, q in req["questions"].items():
|
|
158
|
+
z = np.mean(by_q[qid], axis=0)
|
|
159
|
+
logp[qid] = z.tolist()
|
|
160
|
+
T = float(self.temps.get(q["type"], 1.0))
|
|
161
|
+
z = z / T
|
|
162
|
+
p = np.exp(z - z.max())
|
|
163
|
+
p /= p.sum()
|
|
164
|
+
answers[qid] = p.tolist() if raw else wire_answer(q, p, round_dp)
|
|
165
|
+
out = {"answers": answers}
|
|
166
|
+
if os.environ.get("VERDICT_DUMP_DIST") == "1": # eval/parity/run.py --dump-dist: pre-temperature log-probs
|
|
167
|
+
out["_logp"] = logp
|
|
168
|
+
cache = os.environ.get("VERDICT_LOGP_CACHE")
|
|
169
|
+
if cache: # request-content-keyed log-probs (verdict.replay rebuilds ensembles for any request, incl. flips)
|
|
170
|
+
import json
|
|
171
|
+
with open(cache, "a") as fh:
|
|
172
|
+
fh.write(json.dumps({"h": request_key(req), "logp": logp}) + "\n")
|
|
173
|
+
if plan.truncated_tokens:
|
|
174
|
+
out["truncated_state_tokens"] = plan.truncated_tokens
|
|
175
|
+
return out
|