File size: 10,591 Bytes
27cf837
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
233
234
235
"""Layer-streaming MLX runtime for Cloudflare/clef-flash (text path only) + its joint-schema head.

Backbone: mlx-lm's Qwen3.5 DecoderLayer, built one layer at a time from the release shards (np.memmap of the bf16 bytes,
so only the layer being run is resident). The same sanitisation mlx-lm applies to an HF Qwen3.5 checkpoint is done per
layer: conv1d [C,1,K] -> [C,K,1]; RMSNorm weights (input/post-attention/q/k/final) stored as w are shifted to 1 + w.
Records are run layer-major in token-budgeted chunks (each record alone, batch 1, no padding), so the activations of
one chunk + one layer are what is resident.

Head: the release's JointSchemaHead (joint_schema_model.py from the snapshot, unchanged), run in torch on the CPU in
fp32 on the backbone's final-norm hidden states. It reads lm_head rows for the option tokens; lm_head is never resident:
bf16 rows come from a memmap of the shard, 4-bit rows from mlx affine codes (group 64) dequantized per lookup.
"""
import glob, json, math, os, struct, sys, time
from pathlib import Path

import numpy as np
import torch
import mlx.core as mx
import mlx.nn as nn
from mlx.utils import tree_flatten
from mlx_lm.models.qwen3_5 import TextModelArgs, DecoderLayer
from mlx_lm.models.base import create_attention_mask, create_ssm_mask

_HERE = Path(__file__).resolve().parent
# release dir: CLEF_SNAPSHOT, else this file's directory when it is a packaged release (ships joint_schema_model.py), else the HF cache
SNAP = os.environ.get("CLEF_SNAPSHOT") or (str(_HERE) if (_HERE / "joint_schema_model.py").exists() else
                                           sorted(glob.glob("/Volumes/1Tb/hf-cache/hub/models--Cloudflare--clef-flash/snapshots/*"))[-1])
sys.path.insert(0, SNAP)
sys.dont_write_bytecode = True   # keep the HF snapshot clean (no __pycache__)
from joint_schema_model import JointSchemaHead, encode_record   # noqa: E402  (release code, unchanged)

mx.set_cache_limit(1 << 30)   # as kev/mlx_model.py: bound MLX's buffer cache (one buffer per new record shape otherwise)
LM = "model.language_model."
NORM_SHIFT = (".input_layernorm.weight", ".post_attention_layernorm.weight", ".q_norm.weight", ".k_norm.weight")
SKIP = ("in_proj_a", "in_proj_b")


def bf16_np_to_mx(a):
    return mx.view(mx.array(np.ascontiguousarray(a)), mx.bfloat16)


def bf16_np_to_torch(a):
    return torch.from_numpy(np.ascontiguousarray(a).view(np.int16)).view(torch.bfloat16).float()


class Shards:
    """Header index of the release safetensors shards; tensors are memmapped on demand (bf16 bytes, never converted)."""
    def __init__(self, snap=SNAP):
        self.snap = Path(snap)
        files = set(json.loads((self.snap / "model.safetensors.index.json").read_text())["weight_map"].values())
        self.hdr = {}
        for f in sorted(files):
            p = self.snap / f
            with open(p, "rb") as fh:
                n = struct.unpack("<Q", fh.read(8))[0]; h = json.loads(fh.read(n))
            for k, v in h.items():
                if k != "__metadata__":
                    self.hdr[k] = (str(p), 8 + n + v["data_offsets"][0], v["dtype"], tuple(v["shape"]))

    def memmap(self, k):
        p, off, dt, shape = self.hdr[k]
        assert dt == "BF16", (k, dt)
        return np.memmap(p, dtype=np.uint16, mode="r", offset=off, shape=shape)

    def mx(self, k): return bf16_np_to_mx(self.memmap(k))
    def torch(self, k): return bf16_np_to_torch(self.memmap(k))


def text_args(snap=SNAP):
    return TextModelArgs.from_dict(json.loads((Path(snap) / "config.json").read_text())["text_config"])


def sanitize(n, v):
    if n.endswith("conv1d.weight") and v.shape[-1] != 1: v = v.moveaxis(2, 1)
    if n == "norm.weight" or any(("." + n).endswith(s) for s in NORM_SHIFT): v = v + 1.0   # final norm too (Qwen3_5RMSNorm: 1 + w)
    return v


def layer_weights(sh, i):
    pre = f"{LM}layers.{i}."
    return {k[len(pre):]: sanitize(k[len(pre):], sh.mx(k)) for k in sh.hdr if k.startswith(pre)}


def build_layer(args, i, weights):
    layer = DecoderLayer(args, i)
    layer.eval()   # mlx Modules start in training mode, where GatedDeltaNet takes the slow reference ops instead of the Metal kernel
    layer.load_weights(list(weights.items()), strict=True)
    mx.eval(layer.parameters())
    return layer


def linears(layer, i):
    """(i, name, module, packed name) for every quantizable nn.Linear of a layer (in_proj_a / in_proj_b stay bf16)."""
    return [(i, n, m, f"{LM}layers.{i}.{n}.weight") for n, m in layer.named_modules()
            if isinstance(m, nn.Linear) and n.split(".")[-1] not in SKIP]


def set_module(layer, name, mod):
    parent = layer
    for p in name.split(".")[:-1]: parent = getattr(parent, p)
    setattr(parent, name.split(".")[-1], mod)


class QLoRA(nn.Module):
    """Quantized linear + bf16 low-rank compensation (CLoQ), kept unfolded: y = Q(x) + (x A^T) B^T."""
    def __init__(self, q, a, b):
        super().__init__(); self.q = q; self.lora_a = a; self.lora_b = b
    def __call__(self, x):
        return self.q(x) + (x @ self.lora_a.T) @ self.lora_b.T


def mlx_pack(codes, bits):
    """uint8 codes [N, K] -> mlx affine packed uint32 [N, K*bits/32] (element j of a word at bit j*bits)."""
    N, K = codes.shape; per = 32 // bits
    c = codes.to(torch.int64).reshape(N, K // per, per)
    w = (c << (torch.arange(per, dtype=torch.int64) * bits)).sum(-1)
    return mx.array(w.numpy().astype(np.uint32))


def qlinear(codes, scales, biases, bits, group=64):
    """Build an nn.QuantizedLinear from explicit codes / bf16 scales / bf16 biases (w = scale * code + bias)."""
    N, K = codes.shape
    q = nn.QuantizedLinear(K, N, bias=False, group_size=group, bits=bits)
    q.weight = mlx_pack(codes, bits)
    q.scales = mx.array(scales.numpy()).astype(mx.bfloat16)
    q.biases = mx.array(biases.numpy()).astype(mx.bfloat16)
    return q


def qembedding(w):
    V, D = w.shape
    e = nn.QuantizedEmbedding(V, D, group_size=64, bits=4)
    e.weight, e.scales, e.biases = mx.quantize(w, group_size=64, bits=4)
    return e


class Head:
    """JointSchemaHead (fp32, CPU) + lm_head row lookup in bf16 ('bf16') or 4-bit mlx affine ('q4')."""
    def __init__(self, snap=SNAP, sh=None, lm_head_q=None):
        from safetensors.torch import load_file
        torch.set_num_threads(8)
        cfg = json.loads((Path(snap) / "joint_head_config.json").read_text())
        self.head = JointSchemaHead(**cfg)
        self.head.load_state_dict({k: v.float() for k, v in load_file(str(Path(snap) / "joint_head.safetensors")).items()}, strict=True)
        self.head.eval()
        self.sh = sh; self.q = lm_head_q   # (wq, scales, biases) or None

    def rows_bf16(self, ids):
        return bf16_np_to_torch(self.sh.memmap("lm_head.weight")[np.asarray(ids)])

    def rows_q4(self, ids):
        ix = mx.array(np.asarray(ids, dtype=np.int32)); wq, s, b = self.q
        return torch.from_numpy(np.array(mx.dequantize(wq[ix], s[ix], b[ix], group_size=64, bits=4).astype(mx.float32)))

    @torch.no_grad()
    def logits(self, hidden, enc, mode):
        class Rows:
            def __getitem__(_, ids): return (self.rows_bf16 if mode == "bf16" else self.rows_q4)(ids.cpu().numpy())
        L = len(enc.input_ids)
        out = self.head(hidden.reshape(1, L, -1), torch.tensor([enc.input_ids]), torch.ones(1, L, dtype=torch.long), [enc], Rows())
        return [z.float() for z in out[0]]


def chunks_by_tokens(encs, budget):
    out, cur, n = [], [], 0
    for j, e in enumerate(encs):
        if cur and n + len(e.input_ids) > budget: out.append(cur); cur, n = [], 0
        cur.append(j); n += len(e.input_ids)
    if cur: out.append(cur)
    return out


def run_layer(layer, xs):
    ys = []
    for x in xs:
        m = create_ssm_mask(x) if layer.is_linear else create_attention_mask(x)
        y = layer(x, mask=m, cache=None); mx.eval(y); ys.append(y)
    return ys


def forward_logits(encs, embed, layer_at, final_norm, head, modes=("q4", "bf16"), budget=120_000, log=print):
    """Backbone (layer-major per chunk) + head. embed(ids)->[1,L,D]; layer_at(i)->DecoderLayer (built or resident).
    Returns {mode: [per-record list of per-question fp32 logit tensors]}."""
    nl = len_layers = layer_at.n
    res = {m: [None] * len(encs) for m in modes}
    for ci, idx in enumerate(chunks_by_tokens(encs, budget)):
        t0 = time.time()
        xs = [embed(encs[j].input_ids) for j in idx]
        for i in range(nl):
            layer = layer_at(i); xs = run_layer(layer, xs); del layer
            mx.clear_cache()
        for j, x in zip(idx, xs):
            h = final_norm(x); mx.eval(h)
            ht = torch.from_numpy(np.array(h.astype(mx.float32)))
            for m in modes: res[m][j] = head.logits(ht, encs[j], m)
        del xs; mx.clear_cache()
        log(f"chunk {ci}: {len(idx)} records in {time.time()-t0:.0f}s, peak {mx.get_peak_memory()/1e9:.2f} GB")
    return res


class FinalNorm:
    def __init__(self, w, eps):
        self.w = w; self.eps = eps
    def __call__(self, x): return mx.fast.rms_norm(x, self.w, self.eps)


class Bf16Backbone:
    """Streams bf16 layers from the shards; embeddings gathered from the memmap (never fully resident)."""
    def __init__(self, sh, args):
        self.sh, self.args, self.n = sh, args, args.num_hidden_layers
        self.norm = FinalNorm(sanitize("norm.weight", sh.mx(LM + "norm.weight")), args.rms_norm_eps)
        self.emb = sh.memmap(LM + "embed_tokens.weight")
    def __call__(self, i): return build_layer(self.args, i, layer_weights(self.sh, i))
    def embed(self, ids): return bf16_np_to_mx(self.emb[np.asarray(ids)])[None]


def predictions(records, encs, logits):
    """kev prediction dicts (probabilities + logits keyed like kev.api.question_keys) from Clef per-option logits."""
    out = []
    for r, e, zs in zip(records, encs, logits):
        probs, lg = {}, {}
        for q, z in zip(e.questions, zs):
            p = torch.softmax(z, -1)
            probs[q.question_id] = {o: float(v) for o, v in zip(q.option_ids, p.tolist())}
            lg[q.question_id] = {o: float(v) for o, v in zip(q.option_ids, z.tolist())}
        out.append({"probabilities": probs, "logits": lg, "inference_temperature": 1.0, "latency_ms": 0.0, "input_tokens": len(e.input_ids)})
    return out


class Cached:
    """A predictor for kev.benchmark.evaluate_records that returns precomputed predictions in record order."""
    def __init__(self, preds): self.preds = preds; self.k = 0; self.temperature = 1.0
    def __call__(self, record):
        p = self.preds[self.k]; self.k += 1; return p