Download nla_demo.py from TuHan/tiny-nla: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/TuHan/tiny-nla/resolve/main/nla_demo.py
- Command line
-
hf download hf://TuHan/tiny-nla/nla_demo.py
-
curl -L -o nla_demo.py https://huggingface.co/TuHan/tiny-nla/resolve/main/nla_demo.py
14.6 kB
| #!/usr/bin/env python3 | |
| """tiny_nla demo — one entry point, no model to choose. | |
| pip install torch transformers accelerate huggingface_hub numpy pyyaml | |
| hf auth login | |
| hf download ASSERT-KTH/tiny-nla nla_demo.py --local-dir . | |
| python nla_demo.py --mode code 'def gcd(a, b): | |
| while b: a, b = b, a % b | |
| return a' # -> reconstructs the function | |
| python nla_demo.py --mode text 'The Federal Reserve announced yesterday' | |
| # -> explains the activation | |
| python nla_demo.py --mode code --file mymodule.py --control | |
| Compresses the input to ONE 4096-float activation vector from a frozen | |
| Qwen3-8B, then asks a trained decoder to write it back out. YOU pick the mode; | |
| the CHECKPOINT is chosen for you -- always whatever currently sits at the | |
| pinned Hub paths, with no flag to select an older one. | |
| Needs one GPU with >=24 GB. Models load sequentially, not together. | |
| """ | |
| import argparse, ast, builtins, difflib, keyword, os, re, shutil, subprocess, sys, tempfile | |
| from pathlib import Path | |
| # HF_HOME must be set BEFORE huggingface_hub is imported -- it snapshots the | |
| # cache location at import time, so doing this inside main() is too late. When | |
| # the user has not chosen one, prefer this repo's cache: the weights are very | |
| # likely already there, and the default (~/.cache) would re-download ~33 GB | |
| # into a home directory that may not have the quota for it. | |
| if not os.environ.get("HF_HOME"): | |
| _repo_cache = Path(__file__).resolve().parent.parent / "cache" / "huggingface" | |
| if _repo_cache.is_dir(): | |
| os.environ["HF_HOME"] = str(_repo_cache) | |
| import numpy as np, torch, transformers, yaml | |
| from huggingface_hub import hf_hub_download | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| REPO = "ASSERT-KTH/tiny-nla" | |
| CODE_SUB, TEXT_SUB = "code-decoder", "nla/actor" | |
| BASE = "Qwen/Qwen3-8B" | |
| # Verified per mode by A/B (job 17444219) -- do NOT "unify" these. | |
| # text + minimal -> emits <answer>, drifts off-topic (regex misses, raw falls through) | |
| # code + sidecar -> ident recall 0.00: writes a function ABOUT activation vectors, | |
| # following the preamble's subject instead of the injected vector | |
| # the winners -> code 1.00 recall / AST 0.862 ; text = correct 2-3 snippet format | |
| # Each checkpoint wants the prompt IT was trained on. code-decoder's sidecar | |
| # prompt_templates is a stale dataset-builder default and does not record that. | |
| PROMPT_MODE = {"code": "minimal", "text": "sidecar"} | |
| # transformers 5.x renamed torch_dtype -> dtype and warns on the old name; 4.x | |
| # does not know the new one. An unpinned `pip install transformers` gets 5.x. | |
| _DT = ({"dtype": torch.bfloat16} | |
| if int(transformers.__version__.split(".")[0]) >= 5 | |
| else {"torch_dtype": torch.bfloat16}) | |
| CODE_RE = re.compile(r"<code>\s*(.*?)\s*</code>", re.DOTALL) | |
| EXPL_RE = re.compile(r"<explanation>\s*(.*?)\s*</explanation>", re.DOTALL) | |
| STOP = set(keyword.kwlist) | set(dir(builtins)) | {"self", "cls"} | |
| def inject_at_marked_positions(input_ids, embeddings, vectors, inj_id, left_id, right_id): | |
| """Verbatim from nla/training/injection.py. The neighbour check prevents | |
| false positives; if injection silently fails the model emits Chinese.""" | |
| out = embeddings.clone() | |
| vectors = vectors.to(out.device, out.dtype) | |
| n, k = input_ids.shape[-1], 0 | |
| for b, p in (input_ids == inj_id).nonzero(as_tuple=False).tolist(): | |
| if p == 0 or p == n - 1: | |
| continue | |
| if input_ids[b, p - 1] != left_id or input_ids[b, p + 1] != right_id: | |
| continue | |
| out[b, p] = vectors[k]; k += 1 | |
| if k != vectors.shape[0]: | |
| raise RuntimeError(f"found {k} injection sites, expected {vectors.shape[0]}") | |
| return out | |
| def normalize_activation(v, scale): | |
| return v / (v.float().norm(dim=-1, keepdim=True).clamp_min(1e-12) / scale).to(v.dtype) | |
| def scratch_dir(): | |
| """A writable scratch directory that is never /tmp. | |
| /tmp is off limits on this cluster and is node-local anyway, but both | |
| tempfile and triton default there. Honour an explicit TMPDIR when it does | |
| not point into /tmp, else fall back to the user's cache dir. | |
| """ | |
| for cand in (os.environ.get("TMPDIR"), os.environ.get("XDG_CACHE_HOME")): | |
| if cand and not os.path.realpath(cand).startswith("/tmp"): | |
| d = os.path.join(cand, "tiny_nla") | |
| try: | |
| os.makedirs(d, exist_ok=True) | |
| return d | |
| except OSError: | |
| pass | |
| d = os.path.join(os.path.expanduser("~"), ".cache", "tiny_nla") | |
| os.makedirs(d, exist_ok=True) | |
| return d | |
| def ensure_compiler(): | |
| """Pick a C compiler that actually works, before torch needs one. | |
| Triton JIT-compiles a CUDA helper on first GPU use and shells out to `cc`. | |
| On NSC/Berzelius the gcc first on PATH is a wrapper that refuses to run | |
| without a build-env module loaded; the refusal surfaces as a | |
| CalledProcessError fifteen frames deep inside triton, naming a .c file the | |
| user never wrote. Probe candidates once and export the first that links. | |
| Costs one ~100ms compile, and is skipped entirely if CC is already set. | |
| """ | |
| if os.environ.get("CC"): | |
| return | |
| with tempfile.TemporaryDirectory(dir=scratch_dir()) as d: | |
| src = os.path.join(d, "probe.c") | |
| with open(src, "w") as f: | |
| f.write("int main(){return 0;}") | |
| for cand in (shutil.which("cc"), shutil.which("gcc"), "/usr/bin/gcc"): | |
| if not cand or not os.path.exists(cand): | |
| continue | |
| try: | |
| r = subprocess.run([cand, "-shared", "-fPIC", "-o", | |
| os.path.join(d, "probe.so"), src], | |
| capture_output=True, timeout=60) | |
| except Exception: | |
| continue | |
| if r.returncode == 0: | |
| os.environ["CC"] = cand | |
| return | |
| def parses(s): | |
| try: | |
| ast.parse(s); return True | |
| except SyntaxError: | |
| return False | |
| # Keywords of the curly-brace languages, so a lexical scan of non-Python source | |
| # does not report `int` or `return` as recovered identifiers. | |
| OTHER_KW = {"int", "float", "double", "char", "bool", "void", "long", "short", | |
| "unsigned", "signed", "const", "static", "struct", "class", "public", | |
| "private", "protected", "return", "if", "else", "for", "while", "do", | |
| "switch", "case", "break", "continue", "new", "delete", "using", | |
| "namespace", "template", "typename", "include", "std", "auto", "let", | |
| "var", "function", "def", "fn", "pub", "impl", "final", "extends"} | |
| def idents(src): | |
| """Identifiers in `src`, Python-aware when possible, lexical otherwise.""" | |
| try: | |
| t = ast.parse(src) | |
| except SyntaxError: | |
| return {w for w in re.findall(r"[A-Za-z_][A-Za-z0-9_]*", src) | |
| if w not in STOP and w not in OTHER_KW and not w.startswith("__")} | |
| return {v for n in ast.walk(t) for a in ("id", "arg", "name", "attr") | |
| if isinstance(v := getattr(n, a, None), str) and v not in STOP and not v.startswith("__")} | |
| def main(): | |
| p = argparse.ArgumentParser(description="tiny_nla demo — compress to one vector, decode it back") | |
| p.add_argument("--prompt", choices=["minimal", "sidecar"], default=None, | |
| help="override the injection prompt (diagnostic; default is per-mode verified)") | |
| p.add_argument("--mode", required=True, choices=["code", "text"], | |
| help="code = reconstruct a Python function; text = explain the activation") | |
| p.add_argument("input", nargs="?", help="Python function, or any text") | |
| p.add_argument("--file", help="read the input from a file instead") | |
| p.add_argument("--control", action="store_true", | |
| help="also decode a RANDOM vector — what the model produces with no information") | |
| p.add_argument("--max-new-tokens", type=int, default=256) | |
| p.add_argument("--device", default="cuda") | |
| a = p.parse_args() | |
| if not a.input and not a.file: | |
| p.error("give some input, or --file") | |
| raw = Path(a.file).read_text() if a.file else a.input | |
| code_mode = a.mode == "code" | |
| if code_mode and parses(raw): | |
| raw = ast.unparse(ast.parse(raw)) # canonicalise so formatting is not counted | |
| elif code_mode: | |
| # Not Python: encode and decode it anyway. Only the AST-based scoring | |
| # needs a parse, so it degrades rather than refusing. The DECODER was | |
| # trained on Python only, so treat any other language as exploratory. | |
| print("note: input is not valid Python -- skipping canonicalisation and " | |
| "AST scoring. The decoder is Python-trained; other languages are " | |
| "out of distribution.") | |
| # Keep triton's JIT scratch and every tempfile default off /tmp. | |
| _sd = scratch_dir() | |
| if os.path.realpath(os.environ.get("TMPDIR", "/tmp")).startswith("/tmp"): | |
| os.environ["TMPDIR"] = _sd | |
| tempfile.tempdir = _sd | |
| os.environ.setdefault("TRITON_CACHE_DIR", os.path.join(_sd, "triton")) | |
| os.environ.setdefault("TORCHINDUCTOR_CACHE_DIR", os.path.join(_sd, "inductor")) | |
| ensure_compiler() | |
| sub = CODE_SUB if code_mode else TEXT_SUB | |
| print(f"mode: {'CODE reconstruction' if code_mode else 'TEXT explanation'} " | |
| f"(checkpoint chosen automatically: {REPO}/{sub})") | |
| try: | |
| sc = yaml.safe_load(open(hf_hub_download(REPO, f"{sub}/nla_meta.yaml"))) | |
| except Exception as e: | |
| if "401" in str(e) or "Repository Not Found" in str(e): | |
| sys.exit( | |
| f"cannot read {REPO} (401).\n" | |
| f" HF_HOME = {os.environ.get('HF_HOME', '<unset, using ~/.cache/huggingface>')}\n" | |
| f" token = {'found' if os.path.exists(os.path.join(os.environ.get('HF_HOME', os.path.expanduser('~/.cache/huggingface')), 'token')) else 'NOT FOUND in that HF_HOME'}\n" | |
| f" The usual cause is HF_HOME pointing at a cache with no token,\n" | |
| f" not a missing account. Point it at the cache you logged into:\n" | |
| f" export HF_HOME=/path/to/cache/huggingface\n" | |
| f" Otherwise run `hf auth login`, and ask the repo owner for access.") | |
| raise | |
| meta = sc["tokens"] | |
| inj, L, R, ch = (meta["injection_token_id"], meta["injection_left_neighbor_id"], | |
| meta["injection_right_neighbor_id"], meta["injection_char"]) | |
| try: # a centred decoder given a raw vector yields fluent, unrelated output | |
| mu = np.load(hf_hub_download(REPO, f"{sub}/centre_mean.npy")).reshape(-1) | |
| except Exception: | |
| mu = None | |
| tok = AutoTokenizer.from_pretrained(BASE) | |
| if tok.pad_token_id is None: | |
| tok.pad_token_id = tok.eos_token_id | |
| base = AutoModelForCausalLM.from_pretrained( | |
| BASE, **_DT, device_map={"": a.device}).eval() | |
| layer = (2 * len(base.model.layers)) // 3 | |
| cap = {} | |
| h = base.model.layers[layer].register_forward_hook( | |
| lambda _m, _i, o: cap.__setitem__("h", (o[0] if isinstance(o, tuple) else o).detach())) | |
| with torch.no_grad(): | |
| e = tok(raw, return_tensors="pt", truncation=True, max_length=1024) | |
| ids, m = e["input_ids"].to(a.device), e["attention_mask"].to(a.device) | |
| base(input_ids=ids, attention_mask=m, use_cache=False) | |
| h.remove() | |
| v = cap["h"][0, int(m.sum()) - 1].float().cpu().numpy() | |
| print(f"encoded {int(m.sum())} tokens at layer {layer} ‖v‖={np.linalg.norm(v):.1f}" | |
| + (f" centred (‖μ‖={np.linalg.norm(mu):.1f})" if mu is not None else "")) | |
| if mu is not None: | |
| v = v - mu | |
| del base; torch.cuda.empty_cache() | |
| actor = AutoModelForCausalLM.from_pretrained( | |
| REPO, subfolder=sub, **_DT, device_map={"": a.device}).eval() | |
| embed = actor.get_input_embeddings() | |
| # Which prompt a checkpoint wants is a property of how it was TRAINED, and the | |
| # sidecar's prompt_templates is a dataset-builder default that does not always | |
| # match. Both were measured; see PROMPT_MODE. | |
| want = a.prompt or PROMPT_MODE[a.mode] | |
| tmpl = (sc.get("prompt_templates") or {}).get("actor") if want == "sidecar" else None | |
| user_msg = tmpl.replace("{injection_char}", ch) if tmpl else f"<concept>{ch}</concept>" | |
| def gen(vec): | |
| bv = normalize_activation(torch.tensor(vec).unsqueeze(0), 300.0).to(a.device) | |
| s = tok.apply_chat_template([{"role": "user", "content": user_msg}], | |
| tokenize=False, add_generation_prompt=True) | |
| enc = tok(s, return_tensors="pt", add_special_tokens=False) | |
| i2, m2 = enc["input_ids"].to(a.device), enc["attention_mask"].to(a.device) | |
| hk = embed.register_forward_hook( | |
| lambda _m, i, o: o if o.shape[1] <= 1 else inject_at_marked_positions(i[0], o, bv, inj, L, R)) | |
| try: | |
| with torch.no_grad(): | |
| out = actor.generate(i2, attention_mask=m2, max_new_tokens=a.max_new_tokens, | |
| do_sample=False, pad_token_id=tok.pad_token_id, | |
| eos_token_id=tok.eos_token_id) | |
| finally: | |
| hk.remove() | |
| txt = tok.decode(out[0][i2.shape[1]:], skip_special_tokens=True) | |
| mm = (CODE_RE if code_mode else EXPL_RE).search(txt) | |
| return mm.group(1) if mm else txt.strip()[:600] | |
| bar = "=" * 70 | |
| pred = gen(v) | |
| print(f"\n{bar}\nINPUT\n{bar}\n{raw[:600]}") | |
| print(f"{bar}\n{'RECONSTRUCTED' if code_mode else 'EXPLANATION'}\n{bar}\n{pred}") | |
| if code_mode and pred: | |
| pu, cu = idents(pred), idents(raw) | |
| def _shape(x): | |
| return [type(n).__name__ for n in ast.walk(ast.parse(x))] | |
| sk = (difflib.SequenceMatcher(None, _shape(pred), _shape(raw)).ratio() | |
| if parses(pred) and parses(raw) else None) | |
| print(bar) | |
| print(f"exact match : {pred.strip() == raw.strip()}") | |
| print(f"AST-skeleton sim : " | |
| + (f"{sk:.3f} (~0.52 for two arbitrary functions)" if sk is not None | |
| else "n/a (needs both sides to be valid Python)")) | |
| print(f"identifier recall : {len(pu & cu)/max(len(cu),1):.2f} shared: {sorted(pu & cu)[:8]}") | |
| if a.control: | |
| rng = np.random.default_rng(0) | |
| print(f"{bar}\nCONTROL — random vector, i.e. the model's prior with no information\n{bar}") | |
| print(gen(rng.normal(0, np.std(v), size=v.shape).astype(np.float32))) | |
| if __name__ == "__main__": | |
| main() | |