File size: 7,294 Bytes
533964d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
"""Answer temperatures from decider_config.json (1.4.0).

Every answer is softmax(logits / T) over its option letters.  decider_config.json sets T:

    "temperature": 1.3                                   one value for every answer (the only form before 1.4.0)
    "temperature_by_type": {"choice": 1.48, "noul": 2.22, "score": 1.38}
                                                         optional; one value per answer type, a missing type uses "temperature"
    "temperature_schema_first": 1.18                     optional; the schema cache (questions-first layout), as before
    "temperature_schema_first_by_type": {...}            optional; per answer type on the schema cache

The answer types are the /v1/systemone question types.  A /decide field maps onto them: "choice" -> "choice", "bool" -> "noul",
"scale" -> "score".  Plain `Decider.decide` questions (a question and its options, no type) are "choice".  A Score question
read with isolated levels (one yes/no row per level) uses the "score" temperature on every one of its level rows: the rows form
one Score answer, and a temperature fitted on Score answers is fitted through that same readout (decider.calibrate).

On the state-first layout the temperature of an answer of type t is temperature_by_type[t], else temperature.  On the schema
cache it is temperature_schema_first_by_type[t], else temperature_schema_first, else (no schema-first value at all) the
state-first temperature of t.  An explicit override (Decider(temperature=...), DECIDER_TEMPERATURE) replaces the state-first
"temperature" and switches temperature_by_type off, so the override is the one temperature of every state-first answer, as it
was before 1.4.0.

A config without a by-type map gives every path the single number it gave in 1.3.0, and that number reaches the engines as the
same Python float, so the probabilities are bit-identical to 1.3.0.
"""
import math

TYPES = ("choice", "noul", "score")
FIELD_TYPES = {"choice": "choice", "bool": "noul", "scale": "score"}        # /decide field type -> answer type
_KEYS_TEXT = ('the keys are "choice", "noul" and "score" (a /decide "bool" field is "noul", a "scale" field is "score"; '
              'a /v1/systemone "bool" question is "noul")')


def positive(value, where):
    """A temperature: a number (or a numeric string, as float() reads it, which 1.3.0 accepted for "temperature") that is finite
    and > 0.  Raises ValueError naming `where`."""
    if isinstance(value, bool):
        raise ValueError(f"{where} must be a finite number > 0, got {value!r}")
    try:
        v = float(value)
    except (TypeError, ValueError):
        raise ValueError(f"{where} must be a finite number > 0, got {value!r}") from None
    if not math.isfinite(v) or v <= 0:
        raise ValueError(f"{where} must be a finite number > 0, got {value!r}")
    return v


def by_type(m, where):
    """Validate a {answer type: temperature} map.  None -> {}.  Unknown keys, non-numbers and values that are not finite and > 0
    raise ValueError."""
    if m is None:
        return {}
    if not isinstance(m, dict):
        raise ValueError(f"{where} must be a map {{answer type: temperature}}, got {type(m).__name__}; " + _KEYS_TEXT)
    out = {}
    for k, v in m.items():
        if k not in TYPES:
            raise ValueError(f"{where} has the unknown key {k!r}; " + _KEYS_TEXT)
        if isinstance(v, bool) or not isinstance(v, (int, float)):
            raise ValueError(f"{where}[{k!r}] must be a finite number > 0, got {v!r}")
        out[k] = positive(v, f"{where}[{k!r}]")
    return out


def from_config(cfg, temperature=None, temperature_by_type=None):
    """-> ((T, by_type) for the state-first layout, (T, by_type) for the schema cache).

    temperature / temperature_by_type: explicit overrides (Decider arguments, DECIDER_TEMPERATURE).  An explicit temperature
    without an explicit map switches the config's map off (see the module docstring)."""
    cfg = cfg or {}
    where = "decider_config.json"
    if temperature is not None:
        T = positive(temperature, "temperature")
        m = by_type(temperature_by_type, "temperature_by_type") if temperature_by_type is not None else {}
    else:
        T = positive(cfg.get("temperature", 1.0), f'{where} "temperature"')
        m = (by_type(temperature_by_type, "temperature_by_type") if temperature_by_type is not None
             else by_type(cfg.get("temperature_by_type"), f'{where} "temperature_by_type"'))
    ms = by_type(cfg.get("temperature_schema_first_by_type"), f'{where} "temperature_schema_first_by_type"')
    if "temperature_schema_first" in cfg:
        schema = (positive(cfg["temperature_schema_first"], f'{where} "temperature_schema_first"'), ms)
    else:
        schema = (T, {**m, **ms})
    return (T, m), schema


def effective(T, m):
    """{answer type: the temperature it gets} for reporting (/health, the ready line)."""
    return {t: m.get(t, T) for t in TYPES}


def for_types(T, m, types):
    """One temperature per slot, or the scalar T itself when there is no map (the 1.3.0 call)."""
    if not m:
        return T
    return [m.get(t, T) for t in types]


def item_types(it):
    """The answer type of every slot of a prompt item ("types", set where the item is built); None for an item without it."""
    ts = it.get("types")
    return list(ts) if ts is not None else [None] * len(it["slots"])


def for_items(T, m, items):
    """The `temperature` argument of Engine.score_items / score_shared: the scalar T when there is no map (the 1.3.0 call),
    else one list of per-slot temperatures per item."""
    if not m:
        return T
    return [[m.get(t, T) for t in item_types(it)] for it in items]


def slot_temperatures(temperature, items):
    """Engine side.  temperature: a scalar (returned as it is), or one entry per item, each a scalar or one value per slot of
    that item.  -> the scalar, or a flat list with one temperature per slot in item order."""
    if not isinstance(temperature, (list, tuple)):
        return temperature
    if len(temperature) != len(items):
        raise ValueError(f"temperature: {len(temperature)} entries for {len(items)} items")
    flat = []
    for t, it in zip(temperature, items):
        n = len(it["slots"])
        if isinstance(t, (list, tuple)):
            if len(t) != n:
                raise ValueError(f"temperature: {len(t)} values for an item with {n} slots")
            flat += list(t)
        else:
            flat += [t] * n
    return flat


def item_slice(temperature, lo, hi):
    """The per-item temperature entries of items[lo:hi] (a scalar is shared by every item)."""
    return temperature[lo:hi] if isinstance(temperature, (list, tuple)) else temperature


def scaled_softmax(lg, temperature):
    """softmax(lg / T) over the last axis.  A scalar T is the 1.3.0 expression unchanged; a list gives one T per row of lg."""
    import torch
    if isinstance(temperature, (list, tuple)):
        if len(temperature) != lg.shape[0]:
            raise ValueError(f"temperature: {len(temperature)} values for {lg.shape[0]} slots")
        t = torch.tensor(temperature, dtype=lg.dtype).to(lg.device, non_blocking=True)[:, None]
        return torch.softmax(lg / t, -1)
    return torch.softmax(lg / temperature, -1)