Download code/build_features.py from wepiqx/MERNIK-gnom: direct link, hf CLI and curl.
- Browser
- Download file 7.88 kB
-
https://huggingface.co/wepiqx/MERNIK-gnom/resolve/main/code/build_features.py
- Command line
-
hf download hf://wepiqx/MERNIK-gnom/code/build_features.py
-
curl -L -o build_features.py https://huggingface.co/wepiqx/MERNIK-gnom/resolve/main/code/build_features.py
7.88 kB
| #!/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() | |