File size: 21,846 Bytes
5889269
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
"""Wildcat Mini served at Bonsai's own ternary size: student-base's language Linears (every PQ2_0 matmul weight of
Ternary Bonsai 2 27B) held on the GPU exactly as the GGUF stores them -- 2-bit codes (value (q-1)*d) plus one fp16
scale d per 128 weights, 2.125 bits/weight -- in the Hadamard-rotated input basis prism quantized them in.

The math (prism converter, undone by tools/gguf2hf.py, which made student-base):
    W_hf = W_t . H . D        W_t = ternary codes * group scales (the GGUF), H = block-1024 normalized Walsh-Hadamard
                              on the input axis, D = diag(explicit +-1 signs per input width)
so  y = W_hf x = W_t (H (D x)): flip the activation's signs, rotate each 1024 block with H (one small bf16 matmul,
shared by every Linear that reads the same tensor), then a plain matmul with the unpacked ternary weight. The 2-bit
codes are unpacked by one Triton kernel into a shared bf16 scratch buffer right before F.linear (as d3_q3.py).
token_embd is PQ2_0 too, with the inverse fold (plain row = (e_t H) D): rows are gathered packed and decoded per call.
The LoRA stays separate on the unrotated input: y += f * B(A x). in_proj_a / in_proj_b (BF16 in the GGUF), norms,
conv, A_log, dt_bias, the vision tower and the readout stay as student-base stores them.

At load (from student-base) every packed Linear is decoded back to W_t H D in fp32 and compared with student-base's
bf16 weight; a relative error above 1e-2 (a mapping bug, not rounding) stops the load.

    TERN_GGUF=<PQ2_0 gguf>  source of the codes (default: flamma's Ternary-Bonsai-2-27B-PQ2_0.gguf)
    TERN_SAVE=<file>        after packing, save everything the engine needs (packed + the non-ternary tensors)
    TERN_LOAD=<file>        build the backbone from that file alone (no 49 GB bf16 checkpoint read); .pt or
                            .safetensors. Default: <model dir>/mini-base.safetensors when it exists.
    PYTHONPATH=<model dir> python -m decision_index run --engine d3_tern:D3TernEngine --option model=<model dir> ...
        (the LoRA in <model dir>/adapter attaches on load; --option adapter=<dir> picks another)
"""
import json
import os
import re
import sys

import numpy as np
import torch
import triton
import triton.language as tl

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

GGUF = os.environ.get("TERN_GGUF",
                      "/mnt/tosh/llm-bench/bonsai2/Ternary-Bonsai-2-27B-gguf/Ternary-Bonsai-2-27B-PQ2_0.gguf")
GGUF_PY = os.environ.get("TERN_GGUF_PY", "/home/josh/llm-bench/llama.cpp-prism/gguf-py")
QK, GB, BS = 128, 34, 1024  # PQ2_0: 128 weights = fp16 d + 32 bytes of 2-bit codes; Hadamard block 1024
_SCRATCH = {}
_HAD = {}
_ROT = [None]  # (x, version, rotated): the last rotated activation, held so its address cannot be reused


@triton.jit
def _unpack2(c_ptr, s_ptr, out_ptr, K, BN: tl.constexpr, BK: tl.constexpr):
    n = tl.program_id(0) * BN + tl.arange(0, BN)[:, None]
    k = tl.program_id(1) * BK + tl.arange(0, BK)[None, :]
    c = tl.load(c_ptr + n * (K // 4) + k // 4).to(tl.int32)
    q = ((c >> ((k % 4) * 2)) & 3) - 1
    s = tl.load(s_ptr + n * (K // 128) + k // 128).to(tl.float32)
    tl.store(out_ptr + n * K + k, (q.to(tl.float32) * s).to(out_ptr.dtype.element_ty))


def unpack(codes, scales, out):
    n, k = out.shape
    _unpack2[(n // 16, k // 128)](codes, scales, out, k, BN=16, BK=128)
    return out


def hadamard(device, dtype):
    key = (str(device), dtype)
    if key not in _HAD:
        h = torch.ones(1, 1, dtype=torch.float64)
        while h.shape[0] < BS:
            h = torch.cat([torch.cat([h, h], 1), torch.cat([h, -h], 1)], 0)
        _HAD[key] = (h / BS ** 0.5).to(device, dtype)  # entries +-1/32: exact in bf16
    return _HAD[key]


def unrotate(w, signs):
    """fp32 W_t [N, K] -> W_t H D (what gguf2hf.py stored, before its bf16 cast)."""
    n, k = w.shape
    return (w.view(n, k // BS, BS) @ hadamard(w.device, torch.float32)).view(n, k) * signs.to(w.device, torch.float32)


class Rotation:
    """Per-width explicit signs; rotate(x) = H(D x) in x's dtype (fp32 accumulation), cached for the last x."""
    signs = {}

    @classmethod
    def rotate(cls, x):
        last = _ROT[0]
        try:
            ver = x._version
        except RuntimeError:  # inference-mode tensors keep no version counter: identity alone
            ver = None
        if last is not None and last[0] is x and last[1] == ver:
            return last[2]
        k = x.shape[-1]
        s = cls.signs[(k, x.device, x.dtype)] if (k, x.device, x.dtype) in cls.signs else cls._signs(k, x)
        r = ((x * s).reshape(-1, BS) @ hadamard(x.device, x.dtype)).reshape(x.shape)
        _ROT[0] = (x, ver, r)
        return r

    @classmethod
    def _signs(cls, k, x):
        s = cls.signs[(k, x.device, x.dtype)] = cls.signs[k].to(x.device, x.dtype)
        return s


class TernLinear(torch.nn.Module):
    def __init__(self, codes, scales, bias=None):
        super().__init__()
        self.out_features, self.in_features = codes.shape[0], codes.shape[1] * 4
        assert self.out_features % 16 == 0 and self.in_features % BS == 0
        self.register_buffer("codes", codes)
        self.register_buffer("scales", scales)
        self.bias = bias
        self.lora = None  # (A [r, K], B [N, r] already times alpha / r)

    def unpacked(self, dtype=torch.bfloat16):
        n, k = self.out_features, self.in_features
        if dtype != torch.bfloat16:
            return unpack(self.codes, self.scales, torch.empty(n, k, dtype=dtype, device=self.codes.device))
        buf = _SCRATCH.get(self.codes.device)
        if buf is None or buf.numel() < n * k:
            _SCRATCH[self.codes.device] = buf = torch.empty(n * k, dtype=torch.bfloat16, device=self.codes.device)
        return unpack(self.codes, self.scales, buf[: n * k].view(n, k))

    def forward(self, x):
        y = torch.nn.functional.linear(Rotation.rotate(x), self.unpacked(), self.bias)
        if self.lora is not None:
            a, b = self.lora
            y = y + torch.nn.functional.linear(torch.nn.functional.linear(x.to(a.dtype), a), b).to(y.dtype)
        return y


class TernEmbedding(torch.nn.Module):
    """PQ2_0 token_embd with the inverse fold: row = ((codes * d) H) D, decoded per call in fp32, returned in bf16."""

    def __init__(self, codes, scales, dtype=torch.bfloat16):
        super().__init__()
        self.num_embeddings, self.embedding_dim = codes.shape[0], codes.shape[1] * 4
        self.padding_idx = None
        self.register_buffer("codes", codes)
        self.register_buffer("scales", scales)
        self.out_dtype = dtype

    def plain(self, ids):
        """fp32 (codes * d) rows, before the fold; the unpack kernel works on 16-row tiles, so pad."""
        n = ids.shape[0]
        pad = (-n) % 16
        if pad:
            ids = torch.cat([ids, ids.new_zeros(pad)])
        w = unpack(self.codes[ids], self.scales[ids],
                   torch.empty(ids.shape[0], self.embedding_dim, dtype=torch.float32, device=self.codes.device))
        return w[:n]

    def rows(self, ids):
        return unrotate(self.plain(ids).contiguous(), Rotation.signs[self.embedding_dim])

    def forward(self, ids):
        flat = ids.reshape(-1)
        out = self.rows(flat).to(self.out_dtype)
        return out.view(*ids.shape, self.embedding_dim)


# ---------------------------------------------------------------- GGUF -> packed, in student-base's row order

def _gguf():
    sys.path.insert(0, GGUF_PY)
    from gguf import GGUFReader
    return GGUFReader(GGUF)


def _field(r, k):
    x = r.fields[k]
    vals = [x.parts[i] for i in x.data]
    if x.types[-1].name == "STRING":
        return [bytes(v).decode() for v in vals]
    return [v.tolist()[0] for v in vals]


HF_OF = {"attn_q": "self_attn.q_proj", "attn_k": "self_attn.k_proj", "attn_v": "self_attn.v_proj",
         "attn_output": "self_attn.o_proj", "ffn_gate": "mlp.gate_proj", "ffn_up": "mlp.up_proj",
         "ffn_down": "mlp.down_proj", "attn_qkv": "linear_attn.in_proj_qkv", "attn_gate": "linear_attn.in_proj_z",
         "ssm_out": "linear_attn.out_proj"}


def _row_order(kind, n, tc):
    """Row permutation gguf2hf.py applies (linear-attention V heads tiled [rep, nk, hd] -> grouped [nk, rep, hd])."""
    nk, nv, hd = tc["linear_num_key_heads"], tc["linear_num_value_heads"], tc["linear_value_head_dim"]
    rep = nv // nk

    def grouped(m):
        return torch.arange(m).view(rep, nk, hd).transpose(0, 1).reshape(-1)
    if kind == "attn_qkv":
        qk = 2 * nk * tc["linear_key_head_dim"]
        return torch.cat([torch.arange(qk), qk + grouped(n - qk)])
    if kind == "attn_gate":
        return grouped(n)
    return None


def read_gguf(tc):
    """{hf name (relative to language_model): (codes uint8 [N, K/4], scales fp16 [N, K/128])} + per-width signs."""
    r = _gguf()
    assert _field(r, "prism.hadamard.block_size") == [BS] and _field(r, "prism.hadamard.sign_mode") == ["explicit"]
    assert _field(r, "prism.hadamard.transform") == ["normalized-sylvester-walsh-hadamard"]
    assert _field(r, "prism.hadamard.gdn_v_grouped") == [True]  # ssm_out input axis already in HF order
    rotated = set(_field(r, "prism.hadamard.weight_names"))
    inverse = set(_field(r, "prism.hadamard.inverse_weight_names"))
    vals, o = np.array(_field(r, "prism.hadamard.sign_values"), np.float32), 0
    for w in _field(r, "prism.hadamard.sign_widths"):
        Rotation.signs[w] = torch.from_numpy(vals[o:o + w].copy()); o += w
    out = {}
    for t in r.tensors:
        if t.tensor_type.name != "PQ2_0" or t.name == "output.weight":
            continue
        k, n = int(t.shape[0]), int(np.prod([int(s) for s in t.shape[1:]]))
        raw = np.asarray(t.data).reshape(n, k // QK, GB)
        scales = torch.from_numpy(raw[:, :, :2].copy().view(np.float16).reshape(n, k // QK))
        codes = torch.from_numpy(raw[:, :, 2:].copy().reshape(n, k // 4))
        if t.name == "token_embd.weight":
            assert t.name in inverse
            out["embed_tokens"] = (codes, scales)
            continue
        m = re.fullmatch(r"blk\.(\d+)\.(\w+)\.weight", t.name)
        if int(m[1]) >= tc["num_hidden_layers"]:
            continue  # MTP head
        assert t.name in rotated, t.name
        order = _row_order(m[2], n, tc)
        if order is not None:
            codes, scales = codes[order].contiguous(), scales[order].contiguous()
        out[f"layers.{m[1]}.{HF_OF[m[2]]}"] = (codes, scales)
    return out


def _set(model, name, mod):
    parent, _, child = name.rpartition(".")
    setattr(model.get_submodule(parent) if parent else model, child, mod)


def check_layer(name, wt, ref, stats):
    """W_t (fp32, decoded from the packed tensors) -> W_t H D, compared with student-base's bf16 weight."""
    w = unrotate(wt, Rotation.signs[wt.shape[1]])
    ref = ref.to(w.device, torch.float32)
    d = (w.to(torch.bfloat16).float() - ref).abs()
    rel = float((w - ref).abs().max() / ref.abs().max())
    stats["tensors"] += 1
    stats["elements"] += ref.numel()
    stats["bf16_mismatch"] += int((d > 0).sum())
    stats["max_abs_bf16"] = max(stats["max_abs_bf16"], float(d.max()))
    stats["sum_abs_bf16"] += float(d.double().sum())
    stats["max_rel_fp32"] = max(stats["max_rel_fp32"], rel)
    if rel > 1e-2:  # bf16 rounding alone is up to 2^-8 relative; a mapping bug is O(1)
        raise RuntimeError(f"{name}: packed ternary weight does not reproduce student-base (rel max err {rel:.3g})")


def pack_model(model, device, check=True):
    lm = model.language_model
    tc = model.config.text_config.to_dict()
    packed = read_gguf(tc)
    stats = dict(tensors=0, elements=0, bf16_mismatch=0, max_abs_bf16=0.0, sum_abs_bf16=0.0, max_rel_fp32=0.0)
    for name, (codes, scales) in packed.items():
        old = lm.get_submodule(name)
        if name == "embed_tokens":
            m = TernEmbedding(codes.to(device), scales.to(device))
            ref_w = old.weight.data
            if check:
                ids = torch.arange(m.num_embeddings, device=device)
                for i in range(0, m.num_embeddings, 16384):
                    check_layer(name, m.plain(ids[i:i + 16384]), ref_w[i:i + 16384], stats)
        else:
            assert old.weight.shape == (codes.shape[0], codes.shape[1] * 4), (name, old.weight.shape)
            bias = None if old.bias is None else torch.nn.Parameter(old.bias.data.to(device), requires_grad=False)
            m = TernLinear(codes.to(device), scales.to(device), bias)
            if check:
                check_layer(name, m.unpacked(torch.float32), old.weight.data, stats)
        _set(lm, name, m)
        del old
    left = [n for n, mod in lm.named_modules() if isinstance(mod, torch.nn.Linear)]
    assert all(n.endswith(("in_proj_a", "in_proj_b")) for n in left), left[:5]
    if check:
        stats["mean_abs_bf16"] = stats["sum_abs_bf16"] / stats["elements"]
    return len(packed), stats


def _rest_to(model, device):
    for m in model.modules():
        if isinstance(m, (TernLinear, TernEmbedding)):
            continue
        for k, p in list(m.named_parameters(recurse=False)):
            m._parameters[k] = torch.nn.Parameter(p.data.to(device), requires_grad=False)
        for k, b in list(m.named_buffers(recurse=False)):
            m._buffers[k] = b.to(device)


def save_packed(model, path):
    packed, rest = {}, {}
    for n, m in model.named_modules():
        if isinstance(m, (TernLinear, TernEmbedding)):
            packed[n] = {"codes": m.codes.cpu(), "scales": m.scales.cpu(),
                         **({"bias": m.bias.cpu()} if getattr(m, "bias", None) is not None else {})}
    pk = tuple(packed)
    for k, v in model.state_dict().items():
        if not any(k.startswith(p + ".") for p in pk):
            rest[k] = v.cpu()
    signs = {k: v for k, v in Rotation.signs.items() if isinstance(k, int)}
    torch.save({"format": "d3-tern-pq2-v1", "gguf": GGUF, "signs": signs, "packed": packed, "rest": rest}, path)
    return len(packed), len(rest)


def _read_packed(path):
    """A TERN_SAVE dict, from torch.save (.pt) or flattened into safetensors (packed/<module>/<field>, rest/<key>,
    signs/<width>)."""
    if not path.endswith(".safetensors"):
        return torch.load(path, map_location="cpu")
    from safetensors.torch import load_file
    pk = {"packed": {}, "rest": {}, "signs": {}}
    for k, v in load_file(path).items():
        kind, _, name = k.partition("/")
        if kind == "packed":
            mod, _, field = name.rpartition("/")
            pk["packed"].setdefault(mod, {})[field] = v
        elif kind == "rest":
            pk["rest"][name] = v
        else:
            pk["signs"][int(name)] = v
    return pk


def load_packed(cls, path, root, device):
    """Backbone from a TERN_SAVE file: weights-free skeleton (buffers real), packed modules, then the rest."""
    from accelerate import init_empty_weights
    pk = _read_packed(path)
    Rotation.signs.update(pk["signs"])
    config = cls.config_class.from_pretrained(root)
    config._attn_implementation = "sdpa"
    with init_empty_weights(include_buffers=False):
        model = cls._from_config(config, dtype=torch.bfloat16)
    for n, t in pk["packed"].items():
        if n.endswith("embed_tokens"):
            mod = TernEmbedding(t["codes"].to(device), t["scales"].to(device))
        else:
            b = t.get("bias")
            mod = TernLinear(t["codes"].to(device), t["scales"].to(device),
                             None if b is None else torch.nn.Parameter(b.to(device), requires_grad=False))
        _set(model, n, mod)
    res = model.load_state_dict(pk["rest"], strict=False, assign=True)
    real_missing = [k for k in res.missing_keys if not any(k.startswith(p + ".") for p in pk["packed"])]
    if real_missing or res.unexpected_keys:
        raise RuntimeError(f"TERN_LOAD: {len(real_missing)} missing ({real_missing[:3]}), "
                           f"{len(res.unexpected_keys)} unexpected ({res.unexpected_keys[:3]})")
    model.eval()
    _rest_to(model, device)
    meta = [n for n, p in list(model.named_parameters()) + list(model.named_buffers()) if p.device.type == "meta"]
    if meta:
        raise RuntimeError(f"TERN_LOAD left {len(meta)} tensors on meta: {meta[:3]}")
    return model, len(pk["packed"])


class _TernLoader:
    def __init__(self, cls):
        self.cls = cls

    def from_pretrained(self, path, *args, **kwargs):
        device = torch.device(kwargs["device_map"][""])
        packed = os.environ.get("TERN_LOAD") or os.path.join(path, "mini-base.safetensors")
        if os.path.exists(packed):
            model, n = load_packed(self.cls, packed, path, device)
            info = {"packed": n, "source": os.path.basename(packed)}
        else:
            kwargs["device_map"] = {"": "cpu"}
            model = self.cls.from_pretrained(path, *args, **kwargs)
            n, stats = pack_model(model, device, check=os.environ.get("TERN_CHECK", "1") == "1")
            _rest_to(model, device)
            info = {"packed": n, "source": GGUF, "check_vs_student_base": stats}
            if os.environ.get("TERN_SAVE"):
                info["saved"] = dict(zip(("packed", "rest"), save_packed(model, os.environ["TERN_SAVE"])))
        torch.cuda.empty_cache()
        info["gib_allocated"] = round(torch.cuda.memory_allocated(device) / 2**30, 3)
        LOAD_INFO.update(info)
        print(f"tern: {json.dumps(info)}", file=sys.stderr, flush=True)
        return model


LOAD_INFO = {}


def attach_adapter(decision, path, scale=1.0):
    from safetensors.torch import load_file
    lm = decision.backbone.language_model
    meta = json.load(open(os.path.join(path, "adapter.json")))
    f = scale * meta["lora_alpha"] / meta["r"]
    state = load_file(os.path.join(path, "adapter.safetensors"))
    n = 0
    for key, a in state.items():
        if not key.endswith(".lora_A.weight"):
            continue
        mod = key[: -len(".lora_A.weight")]
        m = lm.get_submodule(mod)
        if not isinstance(m, TernLinear):
            raise RuntimeError(f"adapter targets {mod}, which is not a packed ternary Linear")
        b = state[mod + ".lora_B.weight"]
        m.lora = (a.to(m.codes.device, torch.bfloat16), (f * b.float()).to(m.codes.device, torch.bfloat16))
        n += 1
    readout = os.path.join(path, "readout.safetensors")
    if os.path.exists(readout):
        decision.readout = load_file(readout)["weight"].to(decision.device, getattr(torch, decision.readout_dtype))
    return {"adapter": os.path.abspath(path), "attached": n, "readout": os.path.exists(readout), **meta}


def install():
    import d3_q3
    import d3_runtime
    if not getattr(d3_runtime, "_tern", False):
        base = d3_runtime.backbone_class
        d3_runtime.backbone_class = lambda kind: _TernLoader(base(kind))
        if hasattr(d3_runtime.D3, "_run_state_once"):  # newer runtimes (d3-ultra); student-base's has no state-once
            d3_q3._RUN_STATE_ONCE.append(d3_runtime.D3._run_state_once)
            d3_runtime.D3._run_state_once = d3_q3._run_state_once_or_plain
        d3_runtime._tern = True


from d3_engine import D3Engine  # noqa: E402


class D3TernEngine(D3Engine):
    name = "wildcat-mini-tern"

    def __init__(self, adapter=None, lora_scale=1.0, **options):
        import d3_q3
        install()
        device = options.get("device", "cuda:0")
        total = torch.cuda.get_device_properties(device).total_memory
        so_div = int(os.environ.get("Q3_SO_DIV", "4" if total < 40 * 2**30 else "1"))
        import d3_runtime
        if not hasattr(d3_runtime, "STATE_ONCE_ROWS"):
            so_div = 0  # no state-once pass in this runtime: nothing to shrink
        if so_div > 1:
            d3_q3._small_card(so_div)
        options.setdefault("verify", "none")  # d3's MODEL_MANIFEST describes d3's weights, not these
        if adapter is None and os.path.exists(os.path.join(options.get("model", ""), "adapter", "adapter.json")):
            adapter = os.path.join(options["model"], "adapter")
        super().__init__(**options)
        self.provenance = dict(self.provenance)
        info = {"quant": "Bonsai PQ2_0 ternary (2-bit codes + fp16 scale / 128), Hadamard-rotated activations, "
                         "Triton unpack per matmul", "load": dict(LOAD_INFO),
                "card_gib": round(total / 2**30, 1), "state_once_div": so_div}
        if adapter:
            info["adapter"] = attach_adapter(self.decision, adapter, float(lora_scale))
        info["gib_allocated"] = round(torch.cuda.memory_allocated(device) / 2**30, 3)
        self.provenance["wildcat"] = info
        print(f"tern engine {json.dumps(info)}", file=sys.stderr, flush=True)

    def __call__(self, state, questions, images=None, videos=None):
        """student-base's runtime has no state-once fallback: a request that runs out of memory is retried with the
        questions per forward pass halved (down to 1) instead of stopping the run. Counted in OOM_RETRIES."""
        bs = self.decision.batch_size
        try:
            while True:
                try:
                    return super().__call__(state, questions, images, videos)
                except torch.OutOfMemoryError:
                    if self.decision.batch_size == 1:
                        raise
                # retry outside `except`: there the traceback still pins the failed pass's tensors
                torch.cuda.empty_cache()
                self.decision.batch_size = max(1, self.decision.batch_size // 2)
                OOM_RETRIES[0] += 1
                print(f"tern oom retry {OOM_RETRIES[0]}: batch_size -> {self.decision.batch_size}",
                      file=sys.stderr, flush=True)
        finally:
            self.decision.batch_size = bs


OOM_RETRIES = [0]