File size: 12,985 Bytes
2b605e8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
"""The checkpoint's joint schema head (joint_schema_model.py `JointSchemaHead.forward`, one record) in NumPy float32.

Weights: joint_head.safetensors (bfloat16, 122 tensors, widened to float32 exactly; `load_state_dict(strict=True)` names)
and joint_head_config.json {hidden_size 4096, width 1024, routing_layers 2, layers 4, heads 16, feedforward 4096}.
Inputs: the row graph's hidden states at the record's T real positions (after the final RMSNorm), the record's ids
(processor form) and spans, and the lm_head table (`lm_head.weight` rows, for the lexical vectors).

    x        = hidden_norm(hidden)                          LayerNorm(4096); memory = memory_projection(x) [T, 1024]
    global   = x[T - 1];  q_i = mean of x over question i's span;  ctx_o = mean of x over option o's span
    lexical_o = mean of the lm_head rows of option o's span ids              (not normalized)
    queries_o = option_context_projection(ctx_o) + option_lexical_projection(lexical_o)
                + option_question_projection(q_i)                               i = the question of option o
    2 x evidence routing: queries += MHA(query_norm(queries), memory_norm(memory)); queries += FFN(feedforward_norm(.))
                          (pre-LN, 16 heads of 64, FFN = Linear 1024->4096, GELU, Linear 4096->1024)
    per question: w = softmax over its options of routed . question_projection(q_i) / 32;  summary_i = sum w * routed
    fields   = question_projection(q) + option_summary_norm(summary) + global_projection(global) + type_embedding(type)
    4 x TransformerDecoderLayer (norm_first, GELU): self-attention over the questions, cross-attention to the memory
              (not normalized), FFN;  fields = field_norm(fields)
    prior_o  = e^min(prior_logit_scale, ln 100) * cos(lexical_o, q_i + global)
    joint_o  = e^min(joint_logit_scale, ln 100) * cos(field_i, option_norm(routed_o)) + residual_scorer([f, o, f*o, |f-o|])
    logit_o  = prior_o + sigmoid(residual_gate) * joint_o

Every tensor is float32 as in the author's float32 module; LayerNorm eps 1e-5; attention = softmax(q k^T / 8) v per
head in float32; GELU = the exact erf form (evaluated in float64, rounded to float32); F.normalize eps 1e-12 and
F.cosine_similarity eps 1e-8 (each side divided by max(norm, eps), then the dot product, as torch computes it).
Dropout is the identity (eval). The per-question softmax of the logits is the response's (response.py)."""
from __future__ import annotations

import json
import math
from pathlib import Path

import numpy as np

from ._erf import gelu
from .tables import load_bf16_tensors

CONFIG = {"hidden_size": 4096, "width": 1024, "routing_layers": 2, "layers": 4, "heads": 16, "feedforward": 4096}
LN_EPS = 1e-5
F32 = np.float32


def layer_norm(x: np.ndarray, w: np.ndarray, b: np.ndarray, eps: float = LN_EPS) -> np.ndarray:
    mean = x.mean(axis=-1, keepdims=True, dtype=F32)
    xc = x - mean
    var = (xc * xc).mean(axis=-1, keepdims=True, dtype=F32)
    return (xc * (F32(1.0) / np.sqrt(var + F32(eps)))) * w + b


def normalize(x: np.ndarray, eps: float) -> np.ndarray:
    """x / max(||x||_2, eps) over the last axis (F.normalize)."""
    n = np.sqrt((x * x).sum(axis=-1, keepdims=True, dtype=F32))
    return x / np.maximum(n, F32(eps))


def softmax(x: np.ndarray, axis: int = -1) -> np.ndarray:
    e = np.exp(x - x.max(axis=axis, keepdims=True))
    return e / e.sum(axis=axis, keepdims=True, dtype=F32)


class _Linear:
    def __init__(self, w: np.ndarray, b: np.ndarray | None = None):
        self.wt = np.ascontiguousarray(w.T, dtype=F32)        # [in, out]
        self.b = None if b is None else np.ascontiguousarray(b, dtype=F32)

    def __call__(self, x: np.ndarray) -> np.ndarray:
        y = x @ self.wt
        return y if self.b is None else y + self.b


class _LayerNorm:
    def __init__(self, w: np.ndarray, b: np.ndarray):
        self.w, self.b = w.astype(F32), b.astype(F32)

    def __call__(self, x: np.ndarray) -> np.ndarray:
        return layer_norm(x, self.w, self.b)


class _Attention:
    """nn.MultiheadAttention (batch_first, eval, no masks) for one sequence: q_in [Lq, E], kv_in [Lk, E] -> [Lq, E]."""

    def __init__(self, t: dict, prefix: str, heads: int):
        w, b = t[prefix + "in_proj_weight"], t[prefix + "in_proj_bias"]
        e = w.shape[1]
        self.q = _Linear(w[:e], b[:e])
        self.k = _Linear(w[e:2 * e], b[e:2 * e])
        self.v = _Linear(w[2 * e:], b[2 * e:])
        self.out = _Linear(t[prefix + "out_proj.weight"], t[prefix + "out_proj.bias"])
        self.heads, self.dim = heads, e // heads
        self.scale = F32(1.0 / math.sqrt(self.dim))

    def __call__(self, q_in: np.ndarray, kv_in: np.ndarray) -> np.ndarray:
        h, d = self.heads, self.dim
        q = self.q(q_in).reshape(q_in.shape[0], h, d).transpose(1, 0, 2)          # [H, Lq, D]
        k = self.k(kv_in).reshape(kv_in.shape[0], h, d).transpose(1, 0, 2)        # [H, Lk, D]
        v = self.v(kv_in).reshape(kv_in.shape[0], h, d).transpose(1, 0, 2)
        p = softmax((q @ k.transpose(0, 2, 1)) * self.scale)                     # [H, Lq, Lk]
        o = (p @ v).transpose(1, 0, 2).reshape(q_in.shape[0], h * d)
        return self.out(o)


class _Tracked(dict):
    """The weights by name, recording which names were read (load_state_dict(strict=True): none missing, none unused)."""

    def __init__(self, *a):
        super().__init__(*a)
        self.read = set()

    def __getitem__(self, key):
        self.read.add(key)
        return super().__getitem__(key)


class JointHead:
    """The author's JointSchemaHead for one record. `scores(hidden, ids, questions, lm_head)` -> one float32 logit
    array per question, in the author's option order."""

    def __init__(self, tensors: dict[str, np.ndarray], config: dict | None = None):
        cfg = dict(CONFIG if config is None else config)
        if cfg != CONFIG:
            raise ValueError(f"head config {cfg} is not the release's {CONFIG}")
        self.config = cfg
        t = _Tracked({k: np.asarray(v, dtype=F32) for k, v in tensors.items()})
        heads = cfg["heads"]
        ln = lambda p: _LayerNorm(t[p + ".weight"], t[p + ".bias"])                # noqa: E731
        lin = lambda p, bias=True: _Linear(t[p + ".weight"], t[p + ".bias"] if bias else None)  # noqa: E731
        self.hidden_norm = ln("hidden_norm")
        self.memory_projection = lin("memory_projection", False)
        self.question_projection = lin("question_projection", False)
        self.option_question_projection = lin("option_question_projection", False)
        self.global_projection = lin("global_projection", False)
        self.option_context_projection = lin("option_context_projection", False)
        self.option_lexical_projection = lin("option_lexical_projection", False)
        self.type_embedding = t["type_embedding.weight"]
        self.evidence = [{"query_norm": ln(f"evidence_layers.{i}.query_norm"),
                          "memory_norm": ln(f"evidence_layers.{i}.memory_norm"),
                          "attention": _Attention(t, f"evidence_layers.{i}.attention.", heads),
                          "feedforward_norm": ln(f"evidence_layers.{i}.feedforward_norm"),
                          "ff0": lin(f"evidence_layers.{i}.feedforward.0"),
                          "ff3": lin(f"evidence_layers.{i}.feedforward.3")}
                         for i in range(cfg["routing_layers"])]
        self.option_summary_norm = ln("option_summary_norm")
        self.layers = [{"norm1": ln(f"layers.{i}.norm1"), "norm2": ln(f"layers.{i}.norm2"),
                        "norm3": ln(f"layers.{i}.norm3"),
                        "self_attn": _Attention(t, f"layers.{i}.self_attn.", heads),
                        "multihead_attn": _Attention(t, f"layers.{i}.multihead_attn.", heads),
                        "linear1": lin(f"layers.{i}.linear1"), "linear2": lin(f"layers.{i}.linear2")}
                       for i in range(cfg["layers"])]
        self.field_norm = ln("field_norm")
        self.option_norm = ln("option_norm")
        self.scorer0 = lin("residual_scorer.0")
        self.scorer3 = lin("residual_scorer.3")
        cap = F32(math.log(100.0))
        self.prior_scale = np.exp(np.minimum(t["prior_logit_scale"].reshape(()), cap)).astype(F32)
        self.joint_scale = np.exp(np.minimum(t["joint_logit_scale"].reshape(()), cap)).astype(F32)
        g = t["residual_gate"].reshape(())
        self.gate = (F32(1.0) / (F32(1.0) + np.exp(-g))).astype(F32)
        unused = set(t) - t.read                     # a missing name already raised KeyError when it was read
        if unused:
            raise ValueError(f"head weights not used: {sorted(unused)}")
        self.n_tensors = len(t)
        self.n_params = int(sum(v.size for v in dict.values(t)))

    @classmethod
    def from_files(cls, weights: str | Path, config: str | Path | None = None) -> "JointHead":
        cfg = json.loads(Path(config).read_text()) if config is not None else None
        return cls(load_bf16_tensors(weights), cfg)

    @staticmethod
    def lexical_rows(lm_head, ids, questions) -> list[np.ndarray]:
        """Per question, [n_options, 4096]: the mean of the lm_head rows of each option span's ids (float32)."""
        ids = np.asarray(ids, dtype=np.int64)
        out = []
        for q in questions:
            rows = [lm_head.rows(ids[a:b]).mean(axis=0, dtype=F32) for a, b in q.option_spans]
            out.append(np.stack(rows).astype(F32))
        return out

    def scores(self, hidden: np.ndarray, ids, questions, lm_head=None, lexical: list | None = None) -> list[np.ndarray]:
        """hidden [T, 4096] (the record's real positions), ids (len T), questions (request.EncodedQuestion or
        anything with question_span / option_spans / type_id) -> [float32 logits per question].
        `lexical` (one [n_options, 4096] array per question) replaces the lm_head gather (controls)."""
        hidden = np.asarray(hidden, dtype=F32)
        if hidden.ndim != 2 or hidden.shape[0] != len(ids):
            raise ValueError(f"hidden {hidden.shape} for {len(ids)} ids")
        if lexical is None:
            lexical = self.lexical_rows(lm_head, ids, questions)
        x = self.hidden_norm(hidden)                                            # [T, 4096]
        memory = self.memory_projection(x)                                      # [T, 1024]
        global_vector = x[-1]
        question_vectors = np.stack([x[a:b].mean(axis=0, dtype=F32) for a, b in (q.question_span for q in questions)])
        type_rows = self.type_embedding[[q.type_id for q in questions]]
        counts = [len(q.option_spans) for q in questions]
        context = [np.stack([x[a:b].mean(axis=0, dtype=F32) for a, b in q.option_spans]) for q in questions]

        queries = np.concatenate([
            self.option_context_projection(ctx) + self.option_lexical_projection(lex)
            + self.option_question_projection(question_vectors[i])[None]
            for i, (ctx, lex) in enumerate(zip(context, lexical))])             # [O, 1024]
        for layer in self.evidence:
            routed = layer["attention"](layer["query_norm"](queries), layer["memory_norm"](memory))
            queries = queries + routed
            queries = queries + layer["ff3"](gelu(layer["ff0"](layer["feedforward_norm"](queries))))
        split = np.split(queries, np.cumsum(counts)[:-1])

        base = self.question_projection(question_vectors)                       # [Q, 1024]
        summaries = []
        for field, options in zip(base, split):
            w = softmax((options @ field) / F32(math.sqrt(options.shape[-1])), axis=0)
            summaries.append((w[:, None] * options).sum(axis=0, dtype=F32))
        fields = (base + self.option_summary_norm(np.stack(summaries)) + self.global_projection(global_vector)[None]
                  + type_rows)
        for layer in self.layers:
            f1 = layer["norm1"](fields)
            fields = fields + layer["self_attn"](f1, f1)
            fields = fields + layer["multihead_attn"](layer["norm2"](fields), memory)
            fields = fields + layer["linear2"](gelu(layer["linear1"](layer["norm3"](fields))))
        fields = self.field_norm(fields)

        out = []
        for i, (field, lex, routed) in enumerate(zip(fields, lexical, split)):
            anchor = normalize(question_vectors[i] + global_vector, 1e-12)
            prior = self.prior_scale * (normalize(lex, 1e-12) @ anchor)
            options = self.option_norm(routed)
            f = np.broadcast_to(field, options.shape)
            cosine = (normalize(f, 1e-8) * normalize(options, 1e-8)).sum(axis=-1, dtype=F32)
            features = np.concatenate([f, options, f * options, np.abs(f - options)], axis=-1)
            residual = self.scorer3(gelu(self.scorer0(features)))[:, 0]
            joint = self.joint_scale * cosine + residual
            out.append((prior + self.gate * joint).astype(F32))
        return out