TuHan commited on
Commit
84176ea
·
verified ·
1 Parent(s): 6b951ca

standalone single-file demo (no repo checkout needed)

Browse files
Files changed (1) hide show
  1. nla_code_demo.py +181 -0
nla_code_demo.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Standalone demo for the tiny_nla code decoder. No repo checkout needed.
3
+
4
+ pip install torch transformers huggingface_hub numpy pyyaml
5
+ hf auth login
6
+ hf download TuHan/tiny-nla nla_code_demo.py --local-dir .
7
+ python nla_code_demo.py --code 'def is_palindrome(s):
8
+ s = s.lower()
9
+ return s == s[::-1]'
10
+
11
+ Encodes a Python function to ONE 4096-float activation vector with the frozen
12
+ base model, injects that vector into the trained decoder, and asks it to write
13
+ the function back out.
14
+
15
+ This is not a lossless codec. On a 300-function held-out set: 1% exact, 7.7%
16
+ structurally near-identical, 71% retrieval@1 (is the output nearest its own
17
+ target among 300). The typical error is a SIBLING -- ask for send_video and get
18
+ send_document, same signature, wrong entity. Always run with --control, which
19
+ generates from a random vector so you can see what the code prior alone gives.
20
+
21
+ Needs one GPU with >=24 GB. The two models load sequentially, not together.
22
+ """
23
+ import argparse, ast, builtins, difflib, keyword, re, sys
24
+ from pathlib import Path
25
+ import numpy as np, torch, yaml
26
+ from huggingface_hub import hf_hub_download
27
+ from transformers import AutoModelForCausalLM, AutoTokenizer
28
+
29
+ CODE_RE = re.compile(r"<code>\s*(.*?)\s*</code>", re.DOTALL)
30
+ STOP = set(keyword.kwlist) | set(dir(builtins)) | {"self", "cls"}
31
+
32
+
33
+ def inject_at_marked_positions(input_ids, embeddings, vectors, inj_id, left_id, right_id):
34
+ """Overwrite embedding rows at injection markers. Copied verbatim from
35
+ nla/training/injection.py -- the most correctness-critical path: if
36
+ injection fails the model sees the literal marker char and emits Chinese.
37
+ The neighbour check prevents false positives from the marker appearing in
38
+ generated text."""
39
+ out = embeddings.clone()
40
+ vectors = vectors.to(out.device, out.dtype)
41
+ seq_len = input_ids.shape[-1]
42
+ vec_idx = 0
43
+ for b, p in (input_ids == inj_id).nonzero(as_tuple=False).tolist():
44
+ if p == 0 or p == seq_len - 1:
45
+ continue
46
+ if input_ids[b, p - 1] != left_id or input_ids[b, p + 1] != right_id:
47
+ continue
48
+ out[b, p] = vectors[vec_idx]
49
+ vec_idx += 1
50
+ if vec_idx != vectors.shape[0]:
51
+ raise RuntimeError(f"found {vec_idx} injection sites, expected {vectors.shape[0]}")
52
+ return out
53
+
54
+
55
+ def normalize_activation(v, target_scale):
56
+ if target_scale is None:
57
+ return v
58
+ return v / (v.float().norm(dim=-1, keepdim=True).clamp_min(1e-12) / target_scale).to(v.dtype)
59
+
60
+
61
+ def canon(src):
62
+ try:
63
+ return ast.unparse(ast.parse(src))
64
+ except SyntaxError as e:
65
+ sys.exit(f"input is not valid Python: {e}")
66
+
67
+
68
+ def skeleton(src):
69
+ try:
70
+ return [type(n).__name__ for n in ast.walk(ast.parse(src))]
71
+ except SyntaxError:
72
+ return []
73
+
74
+
75
+ def idents(src):
76
+ try:
77
+ t = ast.parse(src)
78
+ except SyntaxError:
79
+ return set()
80
+ o = set()
81
+ for n in ast.walk(t):
82
+ for a in ("id", "arg", "name", "attr"):
83
+ v = getattr(n, a, None)
84
+ if isinstance(v, str) and v not in STOP and not v.startswith("__"):
85
+ o.add(v)
86
+ return o
87
+
88
+
89
+ def main():
90
+ p = argparse.ArgumentParser(description="reconstruct a Python function from its activation vector")
91
+ g = p.add_mutually_exclusive_group(required=True)
92
+ g.add_argument("--code", help="function source")
93
+ g.add_argument("--file", help="path to a .py file")
94
+ p.add_argument("--repo", default="TuHan/tiny-nla")
95
+ p.add_argument("--subfolder", default="code-decoder")
96
+ p.add_argument("--base-model", default="Qwen/Qwen3-8B")
97
+ p.add_argument("--injection-scale", type=float, default=300.0)
98
+ p.add_argument("--max-new-tokens", type=int, default=256)
99
+ p.add_argument("--control", action="store_true",
100
+ help="also generate from a random vector -- the code prior with no information")
101
+ p.add_argument("--device", default="cuda")
102
+ a = p.parse_args()
103
+
104
+ code = canon(Path(a.file).read_text() if a.file else a.code)
105
+
106
+ meta = yaml.safe_load(open(hf_hub_download(a.repo, f"{a.subfolder}/nla_meta.yaml")))["tokens"]
107
+ inj, L, R, ch = (meta["injection_token_id"], meta["injection_left_neighbor_id"],
108
+ meta["injection_right_neighbor_id"], meta["injection_char"])
109
+ # A centred decoder given a raw vector produces fluent, plausible, entirely
110
+ # unrelated code with no error. Fetch the mean rather than trust the caller.
111
+ try:
112
+ mu = np.load(hf_hub_download(a.repo, f"{a.subfolder}/centre_mean.npy")).reshape(-1)
113
+ except Exception:
114
+ mu = None
115
+ print("note: no centre_mean.npy in repo -- treating decoder as raw-vector")
116
+
117
+ tok = AutoTokenizer.from_pretrained(a.base_model)
118
+ if tok.pad_token_id is None:
119
+ tok.pad_token_id = tok.eos_token_id
120
+
121
+ base = AutoModelForCausalLM.from_pretrained(
122
+ a.base_model, torch_dtype=torch.bfloat16, device_map={"": a.device}).eval()
123
+ layer = (2 * len(base.model.layers)) // 3
124
+ cap = {}
125
+ h = base.model.layers[layer].register_forward_hook(
126
+ lambda _m, _i, o: cap.__setitem__("h", (o[0] if isinstance(o, tuple) else o).detach()))
127
+ with torch.no_grad():
128
+ e = tok(code, return_tensors="pt", truncation=True, max_length=1024)
129
+ ids, m = e["input_ids"].to(a.device), e["attention_mask"].to(a.device)
130
+ base(input_ids=ids, attention_mask=m, use_cache=False)
131
+ h.remove()
132
+ v = cap["h"][0, int(m.sum()) - 1].float().cpu().numpy()
133
+ print(f"encoded {int(m.sum())} tokens at layer {layer} ‖v‖ = {np.linalg.norm(v):.1f}")
134
+ if mu is not None:
135
+ v = v - mu
136
+ print(f"centred (‖μ‖ = {np.linalg.norm(mu):.1f})")
137
+ del base
138
+ torch.cuda.empty_cache()
139
+
140
+ actor = AutoModelForCausalLM.from_pretrained(
141
+ a.repo, subfolder=a.subfolder, torch_dtype=torch.bfloat16,
142
+ device_map={"": a.device}).eval()
143
+ embed = actor.get_input_embeddings()
144
+
145
+ def gen(vec):
146
+ bv = normalize_activation(torch.tensor(vec).unsqueeze(0), a.injection_scale).to(a.device)
147
+ s = tok.apply_chat_template([{"role": "user", "content": f"<concept>{ch}</concept>"}],
148
+ tokenize=False, add_generation_prompt=True)
149
+ enc = tok(s, return_tensors="pt", add_special_tokens=False)
150
+ i2, m2 = enc["input_ids"].to(a.device), enc["attention_mask"].to(a.device)
151
+ hk = embed.register_forward_hook(
152
+ lambda _m, i, o: o if o.shape[1] <= 1 else inject_at_marked_positions(i[0], o, bv, inj, L, R))
153
+ try:
154
+ with torch.no_grad():
155
+ out = actor.generate(i2, attention_mask=m2, max_new_tokens=a.max_new_tokens,
156
+ do_sample=False, pad_token_id=tok.pad_token_id,
157
+ eos_token_id=tok.eos_token_id)
158
+ finally:
159
+ hk.remove()
160
+ mm = CODE_RE.search(tok.decode(out[0][i2.shape[1]:], skip_special_tokens=True))
161
+ return mm.group(1) if mm else None
162
+
163
+ pred = gen(v)
164
+ bar = "=" * 70
165
+ print(f"\n{bar}\nORIGINAL\n{bar}\n{code}")
166
+ print(f"{bar}\nRECONSTRUCTED\n{bar}\n{pred or '(no <code> block emitted)'}")
167
+ if pred:
168
+ pu, cu = idents(pred), idents(code)
169
+ print(bar)
170
+ print(f"exact match : {pred.strip() == code.strip()}")
171
+ print(f"AST-skeleton sim : {difflib.SequenceMatcher(None, skeleton(pred), skeleton(code)).ratio():.3f}"
172
+ f" (~0.52 for two arbitrary functions)")
173
+ print(f"identifier recall : {len(pu & cu) / max(len(cu), 1):.2f} shared: {sorted(pu & cu)[:8]}")
174
+ if a.control:
175
+ rng = np.random.default_rng(0)
176
+ print(f"{bar}\nCONTROL (random vector -- the code prior with no information)\n{bar}")
177
+ print(gen(rng.normal(0, np.std(v), size=v.shape).astype(np.float32)) or "(none)")
178
+
179
+
180
+ if __name__ == "__main__":
181
+ main()