standalone single-file demo (no repo checkout needed)
Browse files- 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()
|