File size: 7,883 Bytes
be2a1e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""Build per-tensor feature matrix for Gnom training (deterministic).

    build_features.py --model M.gguf --imatrix I.gguf --out gnom/features/<m>.npz
                      [--ssim-table models/ssim_table.npz] [--n-layers N]

Output .npz: X (n_tensors x 16, RAW, see gnom/README), names (tensor names),
stats (timp_mean/std, logN_median for per-model standardization at train).
16 = 6 type one-hot + layer + logN + timp_mean + timp_max + frag + econc
+ 4 weight stats (std/kurt/max-mean/outlier-fraction). The README's older
"15-dim" counts predate the outlier-fraction column (RL-PLAN step 2).
Weight stats are computed chunk-wise from the mmap (bounded RAM) and BF16
tensors are byte-viewed through ml_dtypes (scar 2026-10-16: the old
np.array(t.data, float32) path read BF16 bytes as numbers — 4 garbage
columns on every BF16 model, and the 18 GB Ornith build OOMed on a
float64 copy).

--ssim-table is DEAD (2026-10-16): 4/4 structural metrics rank together and
orthogonal to KLD/PPL — kept only to reproduce archival features byte-for-byte.
"""
import argparse
import json
import os
import sys

import numpy as np

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import ml_dtypes  # noqa: F401  (bfloat16 decode)
from gguf import GGUFReader
from llama_inspect import read_model
from imatrix_reader import read_imatrix, build_importance_table
from tinynet import featurize


def load_ssim(path):
    if not path or not os.path.exists(path):
        return {}
    try:
        z = np.load(path, allow_pickle=False)
        return {n: float(np.ravel(z[n])[0]) for n in z.files}  # Q4_K first
    except Exception:
        return {}


def _w_view(t):
    """Reader tensor -> typed array view, or None if not decodable.
    gguf yields F16/F32/F64 natively; BF16 arrives as raw uint8 bytes
    (rows x bytes) and must be byte-viewed, never cast."""
    arr = t.data
    if arr.dtype in (np.float16, np.float32, np.float64):
        return arr
    if arr.dtype == np.uint8 and arr.ndim >= 1 and arr.shape[-1] % 2 == 0:
        try:
            return arr.view(ml_dtypes.bfloat16)
        except Exception:
            return None
    return None


def w_stats_stream(arr, chunk_rows=1024):
    """The 4 weight-stat columns of featurize, computed in bounded RAM.
    Two chunked passes; accumulation in float64 scalars over small slabs.
    Formulas are exactly featurize's: log10 std, clipped kurt/10,
    log10(max|w|/mean|w|+1), fraction(|w| > 6*std).
    (skewness was trialed and REMOVED 2026-10-18: zero effect on v4-tz
    0.575, directionally negative on v2-raw, and it switched the best
    variant under noise — destabilizing, not neutral.)"""
    two_d = arr.ndim > 1
    rows = arr.shape[0] if two_d else 1
    step = max(1, int(chunk_rows))
    m1 = m2 = m3 = m4 = amax = asum = 0.0
    nn = 0
    for i in range(0, rows, step):
        blk = np.asarray(arr[i:i + step] if two_d else arr, dtype=np.float64)
        blk = blk.ravel()
        blk2 = blk * blk
        m1 += float(blk.sum())
        m2 += float(blk2.sum())
        m3 += float((blk2 * blk).sum())
        m4 += float((blk2 * blk2).sum())
        a = np.abs(blk)
        am = float(a.max())
        if am > amax:
            amax = am
        asum += float(a.sum())
        nn += blk.size
    if nn == 0:
        return None
    mean = m1 / nn
    var = max(m2 / nn - mean * mean, 0.0)
    std = var ** 0.5
    m4c = m4 / nn - 4.0 * mean * (m3 / nn) + 6.0 * mean * mean * (m2 / nn) \
        - 3.0 * mean ** 4
    kurt = m4c / (var * var + 1e-12) - 3.0
    mxm = amax / (asum / nn + 1e-12)
    thr = 6.0 * (std + 1e-12)
    cnt = 0
    for i in range(0, rows, step):
        blk = np.asarray(arr[i:i + step] if two_d else arr, dtype=np.float32)
        cnt += int(np.count_nonzero(np.abs(blk) > thr))
    outl = cnt / nn
    return (float(np.log10(std + 1e-12)),
            float(np.clip(kurt, -2, 50)) / 10.0,
            float(np.log10(mxm + 1)), float(outl))


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--model", required=True)
    ap.add_argument("--imatrix", required=True)
    ap.add_argument("--out", required=True)
    ap.add_argument("--ssim-table", default=None)
    ap.add_argument("--n-layers", type=int, default=0,
                    help="0 = estimate from tensor names")
    ap.add_argument("--labels", default=None,
                    help="jsonl damage file: unit tensors the imatrix does "
                         "not cover (MoE/SSM/MLA) still get feature rows")
    args = ap.parse_args()

    model = read_model(args.model)
    im = read_imatrix(args.imatrix)
    table = build_importance_table(im, model)
    ssim = load_ssim(args.ssim_table)

    # Ling scar (2026-10-25): the external imatrix covered 332/526 tensors —
    # no ssm_a/ssm_conv1d/ssm_dt entries — so 85 labelled units would have
    # had zero feature rows and the pool loader would refuse the pool.
    # Importance stays 0 (no imatrix evidence); n_elements and the weight
    # stats are real. No --labels = byte-identical old behaviour.
    if args.labels:
        lab_t = set()
        with open(args.labels) as f:
            for line in f:
                d = json.loads(line)
                if d.get("unit") == "BASELINE":
                    continue
                lab_t.update(d.get("tensors", []))
        missing = sorted(lab_t - set(table.keys()))
        for tname in missing:
            tinfo = model.get("tensors", {}).get(tname, {})
            table[tname] = {
                "importance_mean": 0.0, "importance_max": 0.0,
                "importance_min": 0.0, "importance_sum": 0.0,
                "n_elements": int(tinfo.get("n_elements", 0)),
                "type": tname,
            }
        print(f"  +{len(missing)} label tensors absent from the imatrix "
              f"→ zero-importance rows (table now {len(table)})", flush=True)

    reader = GGUFReader(args.model)
    wmap = {t.name: t for t in reader.tensors}

    names = sorted(table.keys())
    if args.n_layers <= 0:
        layers = [int(n.split(".")[1]) for n in names
                  if n.startswith("blk.") and n.split(".")[1].isdigit()]
        n_layers = max(layers) + 1 if layers else 28
    else:
        n_layers = args.n_layers

    X, kept = [], []
    for idx, n in enumerate(names):
        info = table[n]
        frag = 1.0 - ssim.get(n, 1.0)
        tm0, tx0 = info.get("importance_mean", 0.0), info.get("importance_max", 0.0)
        tmn = info.get("importance_min", 0.0)
        tsm = info.get("importance_sum", 0.0)
        econc = tx0 / (tm0 + 1e-12)  # peak/mean concentration ratio
        ws = None
        if n in wmap:
            try:
                v = _w_view(wmap[n])
                if v is not None and v.size:
                    ws = w_stats_stream(v)
            except Exception:
                ws = None
        row = featurize(n, info.get("n_elements", 0), tm0, tx0,
                        frag=frag, econc=econc, n_layers=n_layers, wstats=ws,
                        imp_min=tmn, imp_sum=tsm)
        X.append(row)
        kept.append(n)
        if (idx + 1) % 32 == 0 or idx + 1 == len(names):
            print(f"  [{idx + 1}/{len(names)}] {n}", flush=True)
    X = np.array(X, dtype=np.float64)
    tm = np.array([table[n].get("importance_mean", 0.0) for n in kept])
    logn = np.log10(np.array([table[n].get("n_elements", 1)
                              for n in kept]) + 1)
    os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
    np.savez(args.out, X=X, names=np.array(kept),
             timp_mean=np.array([tm.mean()]), timp_std=np.array([tm.std()]),
             logN_med=np.array([np.median(logn)]))
    print(f"{args.out}: {X.shape[0]} tensors x {X.shape[1]}, "
          f"n_layers={n_layers}", flush=True)


if __name__ == "__main__":
    main()