File size: 13,131 Bytes
0985f91 | 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 | # phase 4: try to break an editor. each mode is one experiment.
# usage: python methods/attack.py --mode adv|contra|unedit|quant --exp E16 --method inventions:Composite --hp ... [--tag x]
import argparse, ast, importlib, hashlib, random, sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from common import *
from harness import Judge, brief
from values import req
import spec
mean = lambda xs: sum(xs) / max(1, len(xs))
def em(model, tok, P, T, **kw): return mean([x["em"] for x in score(model, tok, P, T, **kw)])
# ------------------------------------------------------------------ adversarial paraphrases
def adv(model, tok, J, M, a):
rng = random.Random(a.seed); facts = J.facts("eval"); reqs = [req(f) for f in facts]
J.prepare(model, facts); M.precompute(reqs); M.reset(); M.edit(reqs) # all 800 in, then attack 200 of them
test = facts[:200]; ids = wikitext_train_ids(tok, 30000, seed=11)
def typo(s):
i = rng.randrange(1, len(s) - 2)
while s[i] == " " or s[i + 1] == " ": i = rng.randrange(1, len(s) - 2)
return s[:i] + s[i + 1] + s[i] + s[i + 2:]
V = {k: [] for k in ("plain", "long_context", "other_fact_first", "wrong_claim_first", "two_subjects", "lowercase_name",
"typo_name", "last_word_only")}
hij_P, hij_T = [], []
for f in test:
g = rng.choice([x for x in facts if x["rel"] == f["rel"] and x["target"] != f["target"]])
chunk = tok.decode(ids[(k := rng.randrange(0, len(ids) - 70)):k + 60]).replace("\n", " ").strip()
V["plain"].append(f["direct"])
V["long_context"].append(chunk + ". " + f["direct"])
V["other_fact_first"].append(g["statement"] + " " + f["direct"]); hij_P.append(V["other_fact_first"][-1]); hij_T.append(g["target"])
V["wrong_claim_first"].append(f"Many people wrongly believe the answer is {g['target']}. " + f["direct"])
V["two_subjects"].append(f"{g['subject']} and {f['subject']} were both mentioned yesterday. " + f["direct"])
V["lowercase_name"].append(f["direct"].replace(f["subject"], f["subject"].lower()))
V["typo_name"].append(f["direct"].replace(f["subject"], typo(f["subject"])))
V["last_word_only"].append(f["direct"].replace(f["subject"], f["subject"].split()[-1]) if " " in f["subject"] else None)
T = [f["target"] for f in test]; out = {}
for k, P in V.items():
keep = [(p, t) for p, t in zip(P, T) if p is not None]
r = score(model, tok, [p for p, _ in keep], [t for _, t in keep])
out[k] = dict(em=mean([x["em"] for x in r]), p=mean([x["p1"] for x in r]), n=len(keep))
out["hijack_by_stated_other_fact"] = em(model, tok, hij_P, hij_T)
# the judge's own paraphrases, split out per template so the single worst one is visible
for j in range(5):
out[f"judge_para_{j}"] = dict(em=em(model, tok, [f["para"][j] for f in test], T))
out["worst_case_em"] = min(v["em"] for k, v in out.items() if isinstance(v, dict) and k not in ("lowercase_name", "typo_name", "last_word_only"))
for k, v in out.items(): print(f" {k:30s} {v}")
return out
# ------------------------------------------------------------------ contradict facts the model already knows
def contra(model, tok, J, M, a):
rng = random.Random(a.seed); pools = jload(os.path.join(ROOT, "data", "pools.json")); real = jload(os.path.join(ROOT, "data", "real_nbrs.json"))
facts, kept_out = [], []
for rel, R in spec.RELATIONS.items():
known = {p: t for p, t in real[rel]}
cand = [(s, o) for s, o in spec.REAL_NEIGHBOURS[rel] if R["direct"].format(s=s) in known]
rng.shuffle(cand)
for s, old in cand[:8]:
o, h = rng.choice([p for p in pools[rel] if p[0] != old])
facts.append(dict(id=f"real_{rel}_{len(facts)}", rel=rel, subject=s, target=o, old=old, hop_target=h,
direct=R["direct"].format(s=s), statement=R["direct"].format(s=s) + " " + o + ".",
para=[p.format(s=s) for p in R["para"]], hop=R["hop"].format(s=s)))
kept_out += [(R["direct"].format(s=s), o) for s, o in cand[8:]]
reqs = [req(f) for f in facts]
before_old = em(model, tok, [f["direct"] for f in facts], [f["old"] for f in facts])
before_other = em(model, tok, [p for p, _ in kept_out], [t for _, t in kept_out])
qa0 = em(model, tok, [q for q, _ in J.qa], [x for _, x in J.qa])
M.precompute(reqs); M.reset(); M.edit(reqs)
out = dict(n_edits=len(facts), n_untouched_real=len(kept_out), old_answer_em_before=before_old, untouched_real_em_before=before_other, qa_before=qa0)
out["new_answer_em"] = em(model, tok, [f["direct"] for f in facts], [f["target"] for f in facts])
out["old_answer_em_after"] = em(model, tok, [f["direct"] for f in facts], [f["old"] for f in facts])
out["para_new_em"] = em(model, tok, [p for f in facts for p in f["para"]], [f["target"] for f in facts for _ in range(5)])
out["para_old_em"] = em(model, tok, [p for f in facts for p in f["para"]], [f["old"] for f in facts for _ in range(5)])
out["hop_new_em"] = em(model, tok, [f["hop"] for f in facts], [f["hop_target"] for f in facts])
out["untouched_real_em_after"] = em(model, tok, [p for p, _ in kept_out], [t for _, t in kept_out])
out["qa_after"] = em(model, tok, [q for q, _ in J.qa], [x for _, x in J.qa])
for k, v in out.items(): print(f" {k:28s} {v}")
return out
# ------------------------------------------------------------------ edit, then take it back
def fingerprint(model):
h = hashlib.sha256()
for n, p in sorted(model.named_parameters()): h.update(n.encode()); h.update(p.detach().cpu().numpy().tobytes())
return h.hexdigest()
def unedit(model, tok, J, M, a):
facts = J.facts("eval"); reqs = [req(f) for f in facts]; J.prepare(model, facts)
fp0 = fingerprint(model); base = next_logprobs(model, tok, J.loc[:100])
M.precompute(reqs); M.reset(); M.edit(reqs)
edited = next_logprobs(model, tok, J.loc[:100]); fp1 = fingerprint(model)
eff_in = em(model, tok, [f["direct"] for f in facts[:200]], [f["target"] for f in facts[:200]])
out = dict(pretrained_weights_changed_by_edit=fp0 != fp1, efficacy_while_edited=eff_in,
max_logprob_shift_while_edited=(edited - base).abs().max().item())
# (a) forget ONE fact, keep the other 799
if hasattr(M, "forget"):
M.forget([facts[0]["id"]])
out["forgot_one__its_em"] = em(model, tok, [facts[0]["direct"]] + facts[0]["para"], [facts[0]["target"]] * 6)
out["forgot_one__others_em"] = em(model, tok, [f["direct"] for f in facts[1:200]], [f["target"] for f in facts[1:200]])
# (b) remove everything
M.reset(); after = next_logprobs(model, tok, J.loc[:100])
out["fingerprint_identical_after_unedit"] = fingerprint(model) == fp0
out["max_logprob_diff_after_unedit"] = (after - base).abs().max().item()
out["bit_for_bit"] = out["fingerprint_identical_after_unedit"] and out["max_logprob_diff_after_unedit"] == 0.0
# (c) what if the edit had been merged into the fp16 weights and then subtracted again (how MEMIT is normally shipped)
if getattr(M, "delta", None) is not None and not M.delta.parts:
M.edit(reqs)
if getattr(M, "delta", None) is not None and M.delta.parts:
tot, bad, mx = 0, 0, 0.0
for l in M.delta.parts:
W = W_out(model, l).weight.data; D = M.delta.dense(l)
merged = (W.float() + D).half(); back = (merged.float() - D).half()
tot += W.numel(); bad += (back != W).sum().item(); mx = max(mx, (back.float() - W.float()).abs().max().item()); del D
out["merged_fp16_then_subtracted"] = dict(entries=tot, entries_not_restored=bad, frac=bad / tot, max_abs_err=mx)
M.reset()
for k, v in out.items(): print(f" {k:36s} {v}")
return out
# ------------------------------------------------------------------ 4-bit quantisation after editing
NF4 = torch.tensor([-1.0, -0.6961928009986877, -0.5250730514526367, -0.39491748809814453, -0.28444138169288635, -0.18477343022823334,
-0.09105003625154495, 0.0, 0.07958029955625534, 0.16093020141124725, 0.24611230194568634, 0.33791524171829224,
0.44070982933044434, 0.5626170039176941, 0.7229568362236023, 1.0])
def nf4(w, block=64):
"""simulated bitsandbytes NF4: blocks of 64 values scaled by their absmax, each value snapped to one of 16 levels."""
shp = w.shape; x = w.float().flatten(); padn = (-x.numel()) % block
x = torch.nn.functional.pad(x, (0, padn)).view(-1, block); s = x.abs().amax(1, keepdim=True).clamp_min(1e-12)
lv = NF4.to(x.device); out = torch.empty_like(x)
for a in range(0, x.shape[0], 1 << 16): # chunked so the distance tensor stays small
xn = x[a:a + (1 << 16)] / s[a:a + (1 << 16)]
out[a:a + (1 << 16)] = lv[(xn[..., None] - lv).abs().argmin(-1)] * s[a:a + (1 << 16)]
return out.flatten()[:w.numel()].view(shp).to(w.dtype)
def quant(model, tok, J, M, a):
facts = J.facts("eval"); reqs = [req(f) for f in facts]; J.prepare(model, facts)
M.precompute(reqs); M.reset(); M.edit(reqs)
out = dict(fp16_edited=J.evaluate(model, facts)); print("fp16 edited ", brief(out["fp16_edited"]), flush=True)
backup = {}
merged = getattr(M, "delta", None) is not None and bool(M.delta.parts)
with torch.no_grad():
if merged: # least-squares editors: fold the update into the weight first
for l in list(M.delta.parts):
W = W_out(model, l).weight; backup[f"__w{l}"] = W.data.clone().cpu(); W.data = (W.data.float() + M.delta.dense(l)).half()
M.delta.remove_hooks()
for n, mod in model.named_modules():
if isinstance(mod, torch.nn.Linear) and "embed" not in n:
backup.setdefault(n, mod.weight.data.clone().cpu()); mod.weight.data = nf4(mod.weight.data)
if getattr(M, "mem", None): # the memory block gets the same treatment
for l, o in M.mem.items():
o["W"].copy_(nf4(o["W"])); o["V"].copy_(nf4(o["V"]))
out["nf4_edited"] = J.evaluate(model, facts); print("4-bit edited ", brief(out["nf4_edited"]), flush=True)
if merged:
with torch.no_grad():
for l in list(M.delta.parts): W_out(model, l).weight.data = nf4(backup[f"__w{l}"].to(DEV))
M.delta.parts = {}
else:
M.reset()
out["nf4_unedited"] = J.evaluate(model, facts); print("4-bit, no edit ", brief(out["nf4_unedited"]), flush=True)
with torch.no_grad():
for n, mod in model.named_modules():
if n in backup: mod.weight.data = backup[n].to(DEV)
out["quantised"] = "all nn.Linear weights (attention + mlp, 16 layers) and the memory block; embeddings left in fp16"
return out
# ------------------------------------------------------------------ where does the block run out of room?
def stress(model, tok, J, M, a):
"""800 real facts + n decoy slots (fresh fictional subjects, zero values). the judge scores the 800 real ones."""
facts = J.facts("eval"); reqs = [req(f) for f in facts]; J.prepare(model, facts); M.precompute(reqs)
names = {f["subject"] for f in J.all_facts} | {nb["subject"] for f in J.all_facts for nb in f["nbrs"]}; out = {}
for n in (0, 1000, 2000, 3000, 4000, 6000):
try:
M.reset(); torch.cuda.reset_peak_memory_stats(); t = time.time(); M.edit(reqs)
if n: M.add_decoys(n, avoid=names)
m = J.evaluate(model, facts); m["slots"] = 800 + n; m["build_sec"] = round(time.time() - t, 1)
out[str(n)] = m; print(f"decoys {n:5d}", brief(m), f"peak={m['peak_mb']}MB build={m['build_sec']}s", flush=True)
except torch.OutOfMemoryError:
free(); out[str(n)] = dict(slots=800 + n, status="OOM at the 7GB cap in the joint address solve"); print(f"decoys {n}: OOM", flush=True); break
return out
def main():
ap = argparse.ArgumentParser(); ap.add_argument("--mode", required=True); ap.add_argument("--exp", required=True)
ap.add_argument("--method", required=True); ap.add_argument("--hp", nargs="*", default=[]); ap.add_argument("--tag", default="")
ap.add_argument("--seed", type=int, default=0); a = ap.parse_args(); hp = {}
for kv in a.hp:
k, v = kv.split("=", 1)
try: hp[k] = ast.literal_eval(v)
except Exception: hp[k] = v
torch.manual_seed(a.seed)
meta = jload(os.path.join(ROOT, "data", "meta.json")); model, tok = load_model(meta["model"]); J = Judge(tok)
mod, cls = a.method.split(":"); M = getattr(importlib.import_module(mod), cls)(model, tok, **hp)
t0 = time.time(); out = dict(exp=a.exp, mode=a.mode, method=cls, hp=hp, seed=a.seed)
out["result"] = dict(adv=adv, contra=contra, unedit=unedit, quant=quant, stress=stress)[a.mode](model, tok, J, M, a)
out["total_sec"] = round(time.time() - t0, 1)
result(f"{a.exp}_{a.mode}_{cls}{('_' + a.tag) if a.tag else ''}", out)
if __name__ == "__main__":
main()
|