mLattice / experiments /polish_forgetting_weakkeys.py
Ex0bit's picture
Docker Space: actual demo server, 3 checkpoints, readable dark text
2f06744 verified
Raw History Blame Contribute Delete
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()
@torch.no_grad()
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()