Download experiments/polish_forgetting_weakkeys.py from Ex0bit/mLattice: direct link, hf CLI and curl.
- Browser
- Download file 7.28 kB
-
https://huggingface.co/spaces/Ex0bit/mLattice/resolve/main/experiments/polish_forgetting_weakkeys.py
- Command line
-
hf download hf://spaces/Ex0bit/mLattice/experiments/polish_forgetting_weakkeys.py
-
curl -L -o polish_forgetting_weakkeys.py https://huggingface.co/spaces/Ex0bit/mLattice/resolve/main/experiments/polish_forgetting_weakkeys.py
7.28 kB
| #!/usr/bin/env python3 | |
| """Weak-key targeted polish for the FORGETING task (endgame, mirrors | |
| polish_weakkeys.py which closed capacity's last 0.2 points). | |
| Census: probe the exact acceptance protocol (N=5,461, f=14, ow=0.5) on fresh | |
| seeds; collect (a) keys whose UPDATED value is returned wrong, and (b) keys | |
| whose ORIGINAL value is returned wrong after nearby overwrites. Then train | |
| small consistent batches (protocol.synth_batch include_keys + explicit qpos | |
| hitting every weak key, updated_idx covering the weak-updated set) with | |
| per-position 1/p CE weighting, alternating with full-size stability batches. | |
| Saves when a fresh-seed census improves; exits on two consecutive censuses | |
| with <= 2 total errors (2x1000 queries => >=0.999 average). | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import sys | |
| import time | |
| import torch | |
| import torch.nn.functional as F | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| ROOT = os.path.join(HERE, "..") | |
| sys.path.insert(0, os.path.join(ROOT, "src")) | |
| from mulattice.protocol import (SynthCfg, build_model, force_fast_scan, logits_at, | |
| spread_loss, synth_batch) | |
| DEVICE = os.environ.get("DEVICE", "cuda") | |
| CKPT_IN = os.environ.get("RESUME", os.path.join(ROOT, "checkpoints", "forgetting.pt")) | |
| CKPT_OUT = os.environ.get("OUT", os.path.join(ROOT, "checkpoints", "forgetting.pt")) | |
| MAX_STEPS = int(os.environ.get("STEPS", "500")) | |
| LR = float(os.environ.get("LR", "3e-5")) | |
| CFG = SynthCfg() | |
| N_PROTOCOL, F_PROTOCOL = 5461, 14 | |
| def save(m, path, extra=None): | |
| live_scale = float(m.blocks[0].mixer.cfg.addr_scale) | |
| payload = {"model": m.state_dict(), "proto_cfg": { | |
| "NK": CFG.NK, "NV": CFG.NV, "d_model": CFG.d_model, "n_heads": CFG.n_heads, | |
| "d_k": CFG.d_k, "d_v": CFG.d_v, "addr_scale": live_scale, | |
| "retention_init": CFG.retention_init}, "cfg": "forgetting weak-key polish"} | |
| if extra: | |
| payload.update(extra) | |
| torch.save(payload, path) | |
| def weighted_ce(logits, gold): | |
| p_gold = torch.softmax(logits, -1).gather(1, gold.unsqueeze(1)).squeeze(1) | |
| w = (1.0 / p_gold.clamp_min(1e-4)).clamp(1.0, 10.0) | |
| ce_vec = F.cross_entropy(logits, gold, reduction="none") | |
| return (w * ce_vec).sum() / w.sum() | |
| def census(m, seed): | |
| """Returns (n_err, weak_updated_keys, weak_untouched_keys) on fresh seeds.""" | |
| g = torch.Generator().manual_seed(seed) | |
| n_upd = N_PROTOCOL // 2 | |
| perm = torch.randperm(N_PROTOCOL, generator=g) | |
| upd_idx = perm[:n_upd] | |
| upd_set = set(upd_idx.tolist()) | |
| pools_u = [i for i in perm.tolist() if i in upd_set] | |
| pools_k = [i for i in perm.tolist() if i not in upd_set] | |
| qpos = torch.tensor(pools_u[:500] + pools_k[:500]).unsqueeze(0) | |
| b = synth_batch(1, N_PROTOCOL, 1000, CFG, filler=F_PROTOCOL, overwrite_frac=0.5, | |
| generator=g, qpos=qpos, updated_idx=upd_idx.unsqueeze(0)) | |
| pred = logits_at(m, b["tokens"].to(DEVICE), b["ans_pos"].to(DEVICE)).argmax(-1).cpu() | |
| ok = (pred == b["gold"]).squeeze(0) | |
| err = ~ok | |
| # map failing query slots back to stored keys, SPLIT BY CLASS: the first 500 | |
| # queries are on updated keys, the last 500 on untouched keys. The prior | |
| # version merged the classes and trained every weak key as an UPDATED one -- | |
| # but the failing class is untouched keys damaged by nearby overwrites, so | |
| # the targeted training never exercised the actual failure mode. | |
| key_ids = b["tokens"][0, b["key_pos"][0]] | |
| slots = qpos[0][err] | |
| weak_upd = set(key_ids[slots[slots < 500]].tolist()) | |
| weak_keep = set(key_ids[slots[slots >= 500]].tolist()) | |
| n_err = int(err.sum()) | |
| return n_err, weak_upd, weak_keep, b | |
| def main(): | |
| dev = DEVICE | |
| ck = torch.load(CKPT_IN, map_location="cpu", weights_only=False) | |
| cfg = SynthCfg() | |
| if ck.get("proto_cfg", {}).get("addr_scale"): | |
| cfg.addr_scale = float(ck["proto_cfg"]["addr_scale"]) | |
| m = build_model(cfg) | |
| m.load_state_dict(ck["model"]) | |
| m = m.to(dev).train() | |
| force_fast_scan(m) | |
| print(f"forgetting weak-key polish from {CKPT_IN} " | |
| f"(scale {cfg.addr_scale:.0f}), lr {LR}", flush=True) | |
| opt = torch.optim.AdamW(m.parameters(), lr=LR) | |
| g = torch.Generator().manual_seed(31) | |
| t0 = time.time() | |
| probe_seed = 601 | |
| best = 10 ** 9 | |
| good = 0 | |
| weak_list = [] | |
| for s in range(MAX_STEPS): | |
| if s % 10 == 0: # refresh the weak set every 10 steps | |
| n_err, w_upd, w_keep, _ = census(m, probe_seed) | |
| probe_seed += 1 | |
| print(f" census {s:4d}: {n_err} errors / 1000 queries " | |
| f"({len(w_upd)} weak-updated, {len(w_keep)} weak-untouched) " | |
| f"[{time.time()-t0:.0f}s]", flush=True) | |
| weak_upd_list = sorted(w_upd) | |
| weak_keep_list = sorted(w_keep) | |
| weak_list = weak_upd_list + weak_keep_list | |
| if n_err < best: | |
| best = n_err | |
| save(m, CKPT_OUT, {"census_errs": n_err}) | |
| good = good + 1 if n_err <= 2 else 0 | |
| if good >= 2: | |
| print(f"TARGET: two consecutive censuses <= 2 errors at step {s}", | |
| flush=True) | |
| save(m, CKPT_OUT, {"census_errs": n_err}) | |
| return | |
| if not weak_list: | |
| continue | |
| # targeted batch: weak keys stored, every one queried, IN ITS OWN CLASS -- | |
| # weak-updated keys get overwritten in-batch; weak-untouched keys stay | |
| # untouched while other keys around them are overwritten (the failure mode) | |
| wk_u = torch.tensor(weak_upd_list[:32]) if weak_upd_list else torch.zeros(0, dtype=torch.long) | |
| wk_k = torch.tensor(weak_keep_list[:32]) if weak_keep_list else torch.zeros(0, dtype=torch.long) | |
| nu, nk_ = len(wk_u), len(wk_k) | |
| N_small = 512 | |
| n_upd_need = N_small // 2 | |
| # slot 0..nu-1: weak-updated keys; nu..nu+nk_-1: weak-untouched keys | |
| upd = torch.cat([torch.arange(nu), # weak-updated slots | |
| (nu + nk_) + torch.randperm(N_small - nu - nk_, generator=g) | |
| [: max(0, n_upd_need - nu)]]).unsqueeze(0) | |
| qpos = torch.cat([torch.arange(nu + nk_), # all weak keys | |
| (nu + nk_) + torch.randperm(N_small - nu - nk_, generator=g) | |
| [: 128 - nu - nk_]]).unsqueeze(0) | |
| wk = torch.cat([wk_u, wk_k]).long() | |
| if wk.numel() == 0: | |
| continue | |
| b = synth_batch(1, N_small, 128, CFG, filler=F_PROTOCOL, overwrite_frac=0.5, | |
| generator=g, qpos=qpos, updated_idx=upd, | |
| kw={"include_keys": wk}) | |
| tok = b["tokens"].to(dev) | |
| logits = logits_at(m, tok, b["ans_pos"].to(dev)) | |
| ce = weighted_ce(logits.reshape(-1, CFG.V), b["gold"].to(dev).reshape(-1)) | |
| kp = torch.cat([b["key_pos"], b["upd_key_pos"]], 1) | |
| sl = spread_loss(m, tok, kp.to(dev), CFG, gate_subsample=512) | |
| loss = ce + sl | |
| opt.zero_grad(set_to_none=True) | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(m.parameters(), 1.0) | |
| opt.step() | |
| print(f"done; best census {best} errors/1000", flush=True) | |
| if __name__ == "__main__": | |
| main() | |