Download ecce_vector/score.py from andyoneal/ecce-vectors: direct link, hf CLI and curl.
- Browser
- Download file 8.01 kB
-
https://huggingface.co/andyoneal/ecce-vectors/resolve/main/ecce_vector/score.py
- Command line
-
hf download hf://andyoneal/ecce-vectors/ecce_vector/score.py
-
curl -L -o score.py https://huggingface.co/andyoneal/ecce-vectors/resolve/main/ecce_vector/score.py
8.01 kB
| """Score control vectors against a fitted fine-tune: how much of the fine-tune's shift away from stock a | |
| vector-steered stock reproduces. | |
| For each arm, stock runs with the arm's vector added at the end of every layer (as llama.cpp adds it: | |
| nothing at layer 0, every position including BOS), and its next-token distributions are compared with | |
| the fine-tune's at the reference positions of both splits: | |
| recovery 1 - KL(tune || stock + vector) / KL(tune || stock): 1 is a perfect copy, 0 is no better | |
| than stock, below 0 is further from the fine-tune than stock is | |
| top1_recovery the same for top-1 agreement: the share of the fine-tune's changed top tokens restored | |
| Choose the strength on the fit split and read the held-out split; `best` in the output does that for | |
| each vector (a --vector at several strengths is one vector; each --gguf and --blend is its own). | |
| python -m ecce_vector.score --base gemma-4-31B-it/ --ref fits/some-finetune.npz \\ | |
| --vector fits/some-finetune.npz::vectors_track --alphas 0.5 0.75 1.0 1.25 1.5 --out score.json | |
| python -m ecce_vector.score --base gemma-4-31B-it/ --ref fits/merge.npz \\ | |
| --gguf vectors/merge.gguf --blend vectors/a.gguf:0.5,vectors/b.gguf:0.5 | |
| An arm is one stream through each stock layer, computed as a difference from the recorded stock stream | |
| (fp16 matmul on the difference, which reproduces full-precision recoveries to about 1e-4), so each extra | |
| arm costs a fraction of a full pass. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import time | |
| from pathlib import Path | |
| import mlx.core as mx | |
| import numpy as np | |
| from transformers import AutoTokenizer | |
| from .corpus import Corpus | |
| from .io import N_LAYERS, Checkpoint, read_gguf | |
| from .model import compare, embed, gather_rows, log_probs, make_layer, model_args | |
| SPLITS = ("fit", "held_out") | |
| REF_KEY = {"fit": "ref_fit", "held_out": "ref_ho"} | |
| GROUP = 2 # arms per GPU call | |
| def load_vector(spec: str) -> np.ndarray: | |
| """A (60, d) vector from FILE.npz[::KEY] (default key vectors_track) or FILE.gguf; layer 0 zeroed.""" | |
| if spec.endswith(".gguf"): | |
| V = read_gguf(spec) | |
| else: | |
| path, key = spec.split("::") if "::" in spec else (spec, "vectors_track") | |
| V = np.load(path)[key].astype(np.float32).copy() | |
| V[0] = 0.0 # llama.cpp has no layer-0 vector | |
| return V | |
| def reference(ref_path: Path): | |
| """The fine-tune's side of the comparison: log-probs if it has its own head, else hidden states + norm.""" | |
| z = np.load(ref_path) | |
| sibling = ref_path.with_name(ref_path.stem + ".logp.npz") | |
| if sibling.exists(): | |
| return np.load(sibling), None | |
| if "logp_file" in z.files: # fits written by the original workspace tools | |
| return np.load(str(z["logp_file"])), None | |
| return z, (z["norm"] if "norm" in z.files else None) | |
| def main(argv=None): | |
| ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) | |
| ap.add_argument("--base", required=True, help="stock google/gemma-4-31B-it checkpoint directory") | |
| ap.add_argument("--ref", required=True, type=Path, help="the fit's NAME.npz (its reference)") | |
| ap.add_argument("--vector", action="append", default=[], help="FILE.npz[::KEY] scored at each of --alphas") | |
| ap.add_argument("--alphas", type=float, nargs="+", default=[0.25, 0.5, 0.75, 1.0, 1.25, 1.5]) | |
| ap.add_argument("--gguf", action="append", default=[], help="a control-vector GGUF, scored at 1.0 as released") | |
| ap.add_argument("--blend", action="append", default=[], help="A.gguf:w1,B.gguf:w2,... summed, as llama.cpp does") | |
| ap.add_argument("--out", type=Path, help="write the results here (JSON)") | |
| a = ap.parse_args(argv) | |
| arms, family = {}, {} # family: the vector an arm belongs to; `best` is chosen within each | |
| for spec in a.vector: | |
| V = load_vector(spec) | |
| for alpha in a.alphas: | |
| arms[f"{spec}@{alpha:g}"] = alpha * V | |
| family[f"{spec}@{alpha:g}"] = spec | |
| for spec in a.gguf: | |
| arms[spec] = load_vector(spec) | |
| family[spec] = spec | |
| for spec in a.blend: | |
| parts = [p.rsplit(":", 1) for p in spec.split(",")] | |
| arms[spec] = sum(float(w) * load_vector(f) for f, w in parts) | |
| family[spec] = spec | |
| assert arms, "nothing to score: give --vector, --gguf or --blend" | |
| t0 = time.time() | |
| args = model_args(a.base) | |
| stock = Checkpoint(a.base) | |
| corpus = Corpus(AutoTokenizer.from_pretrained(a.base)) | |
| names = list(arms) | |
| groups = [names[i:i + GROUP] for i in range(0, len(names), GROUP)] | |
| h = {} | |
| for s in SPLITS: | |
| e0 = embed(stock, corpus.ids[s], args.hidden_size) | |
| h[("stock", s)] = e0 | |
| for k in names: | |
| h[(k, s)] = e0 | |
| for L in range(N_LAYERS): | |
| tl = time.time() | |
| layer, sides = make_layer(args, L, stock.layer(L)) | |
| new = {} | |
| for s in SPLITS: | |
| for w in sides.values(): | |
| w.mode = "rec" | |
| y = layer(h[("stock", s)], mask="causal")[0] | |
| mx.eval(y, [(w.xs, w.ys) for w in sides.values()]) | |
| new[("stock", s)] = y | |
| for w in sides.values(): | |
| w.mode = "diff" | |
| n = y.shape[0] | |
| for grp in groups: | |
| ys = layer(mx.concatenate([h[(k, s)] for k in grp], 0), mask="causal")[0] | |
| for j, k in enumerate(grp): | |
| new[(k, s)] = ys[j * n:(j + 1) * n] + mx.array(arms[k][L]) | |
| mx.eval([new[(k, s)] for k in grp]) | |
| for w in sides.values(): | |
| w.xs = w.ys = None | |
| h = new | |
| del layer, sides | |
| mx.clear_cache() | |
| print(f"L{L:02d} {time.time() - tl:.1f}s (total {time.time() - t0:.0f}s)", flush=True) | |
| norm0 = mx.array(stock.get("norm.weight")) | |
| emb = mx.array(stock.get("embed_tokens.weight")).astype(mx.bfloat16) | |
| ref, tune_norm = reference(a.ref) | |
| res = {"ref": str(a.ref), "stock": {}, "arms": {k: {} for k in names}} | |
| for s in SPLITS: | |
| lq_stock = log_probs(args, gather_rows(h[("stock", s)], corpus.rows[s]), norm0, emb) | |
| lp_key = REF_KEY[s].replace("ref", "logp") | |
| if lp_key in ref.files: | |
| lt = mx.array(ref[lp_key]).astype(mx.float32) | |
| else: | |
| lt = log_probs(args, mx.array(ref[REF_KEY[s]]), | |
| mx.array(tune_norm) if tune_norm is not None else norm0, emb) | |
| ks, ts, kps = compare(lt, lq_stock, corpus.spans[s]) | |
| res["stock"][s] = {"kl": ks, "top1_agree": ts, "kl_per_sequence": kps} | |
| for k in names: | |
| kv, tv, kpv = compare(lt, log_probs(args, gather_rows(h[(k, s)], corpus.rows[s]), norm0, emb), | |
| corpus.spans[s]) | |
| res["arms"][k][s] = {"kl": kv, "recovery": 1 - kv / ks, "top1_agree": tv, | |
| "top1_recovery": (tv - ts) / (1 - ts) if ts < 1 else 0.0, | |
| "recovery_per_sequence": [1 - x / y for x, y in zip(kpv, kps)]} | |
| del lt, lq_stock | |
| # for each vector, the strength with the lowest fit-split KL, reported on the held-out split | |
| res["best"] = {} | |
| for fam in dict.fromkeys(family.values()): | |
| best = min((k for k in names if family[k] == fam), key=lambda k: res["arms"][k]["fit"]["kl"]) | |
| res["best"][fam] = {"arm": best, **res["arms"][best]["held_out"]} | |
| print(f"KL(tune || stock) held out {res['stock']['held_out']['kl']:.4f}") | |
| for k in names: | |
| r = res["arms"][k] | |
| print(f"{k:60s} fit: rec {r['fit']['recovery']:+.4f} held out: rec {r['held_out']['recovery']:+.4f} " | |
| f"top-1 {r['held_out']['top1_recovery']:+.3f}") | |
| for fam, b in res["best"].items(): | |
| print(f"{fam}: chosen on the fit split {b['arm']} -> held-out recovery {b['recovery']:+.4f}, " | |
| f"top-1 {b['top1_recovery']:+.3f}") | |
| print(f"({time.time() - t0:.0f}s)") | |
| if a.out: | |
| a.out.write_text(json.dumps(res, indent=1)) | |
| if __name__ == "__main__": | |
| main() | |