Spaces:
Sleeping
Sleeping
zerogpu: python 3.10.13 (3.11 not supported by spaces CUDA emulation), restore module-level .to(cuda)
3804244 verified | """SRT-NLA v1 demo Space β round-trip autoencoder + latent arithmetic. | |
| Two tabs: | |
| Tab 1 β Round-trip autoencoder: | |
| text -- AR (frozen Qwen L20) --> v -- AV (this model) --> text' | |
| Score: fve_nrm_centered(v, AR(text')), rho_norm against published anchors. | |
| Best-of-N rerank by AR fidelity. | |
| Tab 2 β Latent arithmetic: | |
| Encode text A and text B, interpolate v = (1-a)*v_A + a*v_B, verbalize. | |
| Public model: RiverRider/srt-nla-av-v1 | |
| Public assets pulled at runtime: | |
| - best_av.pt (12.7M adapter weights) | |
| - config.json (NLAConfig) | |
| - mu_l20.pt (anisotropy mean for centered fve_nrm) | |
| Backbone: Qwen/Qwen2.5-7B (bf16, frozen). | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from pathlib import Path | |
| from typing import Any | |
| # `spaces` must be imported BEFORE torch (and before gradio, which imports torch) | |
| # so its fake-CUDA patches install. Otherwise `.to("cuda")` at module level | |
| # triggers a real `torch._C._cuda_init()` which fails outside the GPU slot. | |
| import spaces | |
| import torch | |
| import torch.nn.functional as F | |
| import gradio as gr | |
| from huggingface_hub import hf_hub_download | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| from srt.nla.config import NLAConfig | |
| from srt.nla.verbalizer import ActivationVerbalizer | |
| MODEL_REPO = "RiverRider/srt-nla-av-v1" | |
| # Published anchors (paper Β§3, artifacts/nla/oracle_ceiling_30k_v2.json). | |
| RANDOM_FLOOR_CEN = 0.510 | |
| PARAPHRASE_CEILING_CEN = 0.799 | |
| RHO_DENOM = PARAPHRASE_CEILING_CEN - RANDOM_FLOOR_CEN | |
| MAX_INPUT_TOKENS = 256 | |
| MAX_NEW_TOKENS_DEFAULT = 64 | |
| _state: dict[str, Any] = {} | |
| def _strip_adapter_state(sd: Any) -> dict: | |
| if isinstance(sd, dict): | |
| for k in ("trainable", "av", "av_state_dict", "model", "state_dict"): | |
| if k in sd and isinstance(sd[k], dict): | |
| return sd[k] | |
| return sd | |
| _paths: dict[str, str] = {} | |
| def _prefetch() -> None: | |
| """Download every needed asset to disk at module import (no GPU needed). | |
| This is the ZeroGPU-friendly part of setup: pulls the AV adapter, ΞΌ, NLA | |
| config and the full Qwen2.5-7B snapshot into the HF hub cache so that the | |
| in-GPU-slot load is just shard mmap + .to('cuda') (~10s) instead of a 14GB | |
| download (~3 min, which previously consumed the entire 180s GPU budget). | |
| """ | |
| if _paths: | |
| return | |
| from huggingface_hub import snapshot_download | |
| print("[nla] prefetching AV adapter + ΞΌ + config") | |
| _paths["cfg"] = hf_hub_download(MODEL_REPO, "config.json") | |
| _paths["weights"] = hf_hub_download(MODEL_REPO, "best_av.pt") | |
| _paths["mu"] = hf_hub_download(MODEL_REPO, "mu_l20.pt") | |
| cfg = NLAConfig.from_json(_paths["cfg"]) | |
| _paths["backbone_id"] = cfg.backbone_id | |
| print(f"[nla] prefetching backbone {cfg.backbone_id} (this is the slow part)") | |
| snapshot_download( | |
| cfg.backbone_id, | |
| allow_patterns=[ | |
| "*.json", | |
| "*.txt", | |
| "tokenizer*", | |
| "*.safetensors", | |
| "*.safetensors.index.json", | |
| ], | |
| ) | |
| print("[nla] prefetch complete") | |
| def _setup() -> None: | |
| """Materialise model on GPU. MUST be called from inside @spaces.GPU.""" | |
| if _state.get("ready"): | |
| return | |
| device = torch.device("cuda") | |
| cfg = NLAConfig.from_json(_paths["cfg"]) | |
| print(f"[nla] backbone={cfg.backbone_id} L={cfg.extraction_layer} " | |
| f"np={cfg.num_prefix_tokens} slots={cfg.num_inject_slots}") | |
| print("[nla] loading backbone (cached on disk)") | |
| tok = AutoTokenizer.from_pretrained(cfg.backbone_id) | |
| if tok.pad_token_id is None: | |
| tok.pad_token = tok.eos_token | |
| backbone = AutoModelForCausalLM.from_pretrained( | |
| cfg.backbone_id, | |
| torch_dtype=torch.bfloat16, | |
| ).to(device) | |
| backbone.eval() | |
| for p in backbone.parameters(): | |
| p.requires_grad = False | |
| if backbone.config.pad_token_id is None: | |
| backbone.config.pad_token_id = tok.pad_token_id | |
| print("[nla] loading adapter") | |
| av = ActivationVerbalizer(cfg, backbone, tok).to(device).eval() | |
| sd = torch.load(_paths["weights"], map_location="cpu", weights_only=False) | |
| sd = _strip_adapter_state(sd) | |
| missing, unexpected = av.load_state_dict(sd, strict=False) | |
| print(f"[nla] adapter loaded: missing={len(missing)} unexpected={len(unexpected)}") | |
| mu_blob = torch.load(_paths["mu"], map_location="cpu", weights_only=False) | |
| mu = mu_blob["mu"].float().to(device) | |
| print(f"[nla] mu loaded: ||mu||={float(mu.norm()):.4f}") | |
| _state.update({ | |
| "ready": True, | |
| "device": device, | |
| "cfg": cfg, | |
| "tok": tok, | |
| "backbone": backbone, | |
| "av": av, | |
| "mu": mu, | |
| }) | |
| print("[nla] ready") | |
| def _encode_text(text: str) -> torch.Tensor: | |
| """Run frozen Qwen forward, return pooled L20 last-token hidden (1, d) fp32.""" | |
| cfg: NLAConfig = _state["cfg"] | |
| tok = _state["tok"] | |
| backbone = _state["backbone"] | |
| device = _state["device"] | |
| enc = tok( | |
| text, | |
| truncation=True, | |
| max_length=MAX_INPUT_TOKENS, | |
| return_tensors="pt", | |
| ).to(device) | |
| out = backbone( | |
| input_ids=enc.input_ids, | |
| attention_mask=enc.attention_mask, | |
| output_hidden_states=True, | |
| use_cache=False, | |
| ) | |
| h = out.hidden_states[cfg.extraction_layer] | |
| last = enc.attention_mask.sum(-1) - 1 | |
| idx = last.clamp(min=0).long() | |
| rows = torch.arange(h.size(0), device=device) | |
| return h[rows, idx, :].detach().to(torch.float32) | |
| def _verbalize(v: torch.Tensor, n: int, max_new: int, temperature: float) -> list[str]: | |
| """Decode v into `n` candidate strings. v shape (1, d).""" | |
| av: ActivationVerbalizer = _state["av"] | |
| tok = _state["tok"] | |
| if n <= 1: | |
| ids = av.generate( | |
| v, | |
| max_new_tokens=max_new, | |
| do_sample=False, | |
| temperature=1.0, | |
| top_p=1.0, | |
| ) | |
| return tok.batch_decode(ids, skip_special_tokens=True) | |
| v_rep = v.expand(n, -1).contiguous() | |
| ids = av.generate( | |
| v_rep, | |
| max_new_tokens=max_new, | |
| do_sample=True, | |
| temperature=temperature, | |
| top_p=0.95, | |
| ) | |
| return tok.batch_decode(ids, skip_special_tokens=True) | |
| def _fve_cen(h: torch.Tensor, v: torch.Tensor) -> float: | |
| mu = _state["mu"] | |
| return float(0.5 * (1.0 + F.cosine_similarity( | |
| (h - mu).float(), (v - mu).float(), dim=-1 | |
| ))) | |
| def _rho(cen: float) -> float: | |
| return (cen - RANDOM_FLOOR_CEN) / RHO_DENOM | |
| def _score_band(rho: float) -> str: | |
| if rho >= 0.80: | |
| return "near paraphrase ceiling" | |
| if rho >= 0.50: | |
| return "well above NN-retrieval baseline" | |
| if rho >= 0.20: | |
| return "above random floor" | |
| return "near random floor" | |
| # βββββββββββββββββββββββββ Tab 1: round-trip βββββββββββββββββββββββββ | |
| def roundtrip(passage: str, n_samples: int, max_new: int, temperature: float): | |
| if not passage or not passage.strip(): | |
| return "", "", "Enter a passage." | |
| v = _encode_text(passage.strip()) # (1, d) | |
| n = max(1, min(int(n_samples), 16)) | |
| candidates = _verbalize(v, n=n, max_new=int(max_new), temperature=float(temperature)) | |
| # Re-encode each candidate and pick the one maximizing centered fve_nrm. | |
| scored: list[tuple[float, float, str]] = [] | |
| for text in candidates: | |
| if not text.strip(): | |
| continue | |
| h = _encode_text(text) | |
| cen = _fve_cen(h, v) | |
| rho = _rho(cen) | |
| scored.append((cen, rho, text)) | |
| if not scored: | |
| return "", "", "AV produced only empty strings; try lowering temperature." | |
| scored.sort(key=lambda r: r[0], reverse=True) | |
| best_cen, best_rho, best_text = scored[0] | |
| lines = [ | |
| f"**Best-of-{n}** (reranked by AR fidelity):", | |
| f"- centered fve_nrm: **{best_cen:.3f}**", | |
| f"- rho_norm: **{best_rho:+.2f}** _( {_score_band(best_rho)} )_", | |
| "", | |
| "_Anchors_: random floor `0.510` Β· NN-retrieval `0.71` Β·" | |
| " paraphrase ceiling `0.799`.", | |
| ] | |
| if n > 1: | |
| lines += ["", f"Candidate spread: cen β [{scored[-1][0]:.3f}, {scored[0][0]:.3f}]"] | |
| lines += ["", "Other candidates:"] | |
| for cen, rho, text in scored[1:5]: | |
| preview = text.strip().replace("\n", " ")[:160] | |
| lines.append(f"- `cen={cen:.3f} rho={rho:+.2f}` {preview}") | |
| return best_text.strip(), "\n".join(lines), "" | |
| # βββββββββββββββββββββββββ Tab 2: arithmetic βββββββββββββββββββββββββ | |
| def arithmetic(text_a: str, text_b: str, alpha: float, max_new: int): | |
| if not (text_a and text_a.strip() and text_b and text_b.strip()): | |
| return "", "Provide both texts." | |
| v_a = _encode_text(text_a.strip()) | |
| v_b = _encode_text(text_b.strip()) | |
| a = float(alpha) | |
| v = (1.0 - a) * v_a + a * v_b | |
| [text] = _verbalize(v, n=1, max_new=int(max_new), temperature=1.0) | |
| h = _encode_text(text) | |
| cen_a = _fve_cen(h, v_a) | |
| cen_b = _fve_cen(h, v_b) | |
| cen_v = _fve_cen(h, v) | |
| info = ( | |
| f"**alpha = {a:.2f}** β v = (1-Ξ±)Β·A + Ξ±Β·B\n\n" | |
| f"- centered fve_nrm to **A**: `{cen_a:.3f}` (rho `{_rho(cen_a):+.2f}`)\n" | |
| f"- centered fve_nrm to **B**: `{cen_b:.3f}` (rho `{_rho(cen_b):+.2f}`)\n" | |
| f"- centered fve_nrm to **mix v**: `{cen_v:.3f}` (rho `{_rho(cen_v):+.2f}`)\n" | |
| ) | |
| return text.strip(), info | |
| # βββββββββββββββββββββββββββββ UI ββββββββββββββββββββββββββββββββββββ | |
| EXAMPLES_RT = [ | |
| ["The Roman Senate convened in the Curia Julia to debate the future of the republic, weighing the consuls' authority against the rising influence of the equestrian class.", 4, 64, 0.9], | |
| ["A black hole's event horizon marks the boundary beyond which no information can escape; an outside observer sees infalling matter freeze and redshift toward it.", 4, 64, 0.9], | |
| ["def quicksort(arr):\n if len(arr) <= 1: return arr\n pivot = arr[len(arr)//2]\n return quicksort([x for x in arr if x < pivot]) + [x for x in arr if x == pivot] + quicksort([x for x in arr if x > pivot])", 4, 64, 0.9], | |
| ] | |
| EXAMPLES_AR = [ | |
| [ | |
| "The trial dragged on for weeks; jurors grew restless and the judge's patience wore visibly thin.", | |
| "After the ceremony, the wedding party spilled into the gardens and danced until dawn.", | |
| 0.5, | |
| 64, | |
| ], | |
| [ | |
| "Quantum entanglement implies non-local correlations between particle pairs.", | |
| "Classical thermodynamics treats heat as a flow between bodies at different temperatures.", | |
| 0.5, | |
| 64, | |
| ], | |
| ] | |
| def build_app() -> gr.Blocks: | |
| with gr.Blocks(title="SRT-NLA v1 β latent autoencoder demo", | |
| theme=gr.themes.Soft()) as demo: | |
| gr.Markdown( | |
| "# SRT-NLA v1 β latent autoencoder demo\n" | |
| "Round-trip a passage through Qwen2.5-7B's residual stream at L20: " | |
| "the **encoder** is a frozen Qwen forward, the **decoder** is the " | |
| "12.7M-parameter NLA verbalizer from " | |
| "[`RiverRider/srt-nla-av-v1`](https://huggingface.co/RiverRider/srt-nla-av-v1).\n\n" | |
| "_Score: centered fve_nrm = Β½(1 + cos(hβΞΌ, vβΞΌ)), with anchors " | |
| "random=0.510, NN-retrieval=0.71, paraphrase ceiling=0.799._\n\n" | |
| "_First request loads the backbone (~20s on ZeroGPU). Subsequent requests are fast._" | |
| ) | |
| with gr.Tabs(): | |
| # ---------------- Tab 1 ---------------- | |
| with gr.Tab("Round-trip autoencoder"): | |
| gr.Markdown( | |
| "Pipeline: `text β Qwen L20 hidden v β AV decoder β text' β re-encode β score`.\n" | |
| "Best-of-N samples are reranked by AR fidelity (this is how the paper " | |
| "gets greedy Ο=0.26 β BoN=0.92)." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| passage = gr.Textbox( | |
| label="Source passage", | |
| lines=6, | |
| placeholder="Paste 1-3 sentences...", | |
| ) | |
| with gr.Row(): | |
| n_samples = gr.Slider(1, 16, value=4, step=1, | |
| label="Best-of-N samples") | |
| max_new = gr.Slider(16, 128, value=MAX_NEW_TOKENS_DEFAULT, | |
| step=16, label="Max new tokens") | |
| temperature = gr.Slider(0.3, 1.5, value=0.9, step=0.05, | |
| label="Temperature") | |
| go = gr.Button("Round-trip", variant="primary") | |
| with gr.Column(): | |
| rewrite = gr.Textbox(label="AV verbalization (best of N)", | |
| lines=6) | |
| info = gr.Markdown() | |
| err = gr.Markdown() | |
| gr.Examples(examples=EXAMPLES_RT, | |
| inputs=[passage, n_samples, max_new, temperature]) | |
| go.click(roundtrip, | |
| inputs=[passage, n_samples, max_new, temperature], | |
| outputs=[rewrite, info, err]) | |
| # ---------------- Tab 2 ---------------- | |
| with gr.Tab("Latent arithmetic"): | |
| gr.Markdown( | |
| "Encode two passages β v_A, v_B in R^3584. " | |
| "Verbalize `v = (1-Ξ±)Β·v_A + Ξ±Β·v_B`.\n" | |
| "Ξ± = 0 reproduces A; Ξ± = 1 reproduces B; in between the AV " | |
| "shows what the L20 mixture decodes to." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| text_a = gr.Textbox(label="Passage A", lines=4) | |
| text_b = gr.Textbox(label="Passage B", lines=4) | |
| alpha = gr.Slider(0.0, 1.0, value=0.5, step=0.05, | |
| label="Ξ± (0 = A, 1 = B)") | |
| max_new_a = gr.Slider(16, 128, value=MAX_NEW_TOKENS_DEFAULT, | |
| step=16, label="Max new tokens") | |
| go_a = gr.Button("Verbalize mix", variant="primary") | |
| with gr.Column(): | |
| mix_out = gr.Textbox(label="AV verbalization of mix", | |
| lines=6) | |
| info_a = gr.Markdown() | |
| gr.Examples(examples=EXAMPLES_AR, | |
| inputs=[text_a, text_b, alpha, max_new_a]) | |
| go_a.click(arithmetic, | |
| inputs=[text_a, text_b, alpha, max_new_a], | |
| outputs=[mix_out, info_a]) | |
| gr.Markdown( | |
| "---\n" | |
| "**About.** This demo runs the published `srt-nla-av-v1` checkpoint " | |
| "(12.7M trainable params on top of frozen Qwen/Qwen2.5-7B). " | |
| "Encoder = frozen Qwen L20 last-token, decoder = injection-prefix " | |
| "verbalizer (16 learned prefix tokens, 1 inject slot). " | |
| "All metrics use the published anisotropy mean ΞΌ " | |
| "(`||ΞΌ|| = 55.178`, derived from 2000 held-out targets).\n\n" | |
| "Code & paper: https://github.com/space-bacon/SRT (branch `nla`)." | |
| ) | |
| return demo | |
| # Pre-download all assets at import (CPU-only, no GPU slot consumed), | |
| # then load model on 'cuda' at module level. ZeroGPU's CUDA emulation | |
| # (active outside @spaces.GPU functions on python 3.10.13 / 3.12.12) | |
| # defers real allocation until first GPU call. | |
| _prefetch() | |
| _setup() | |
| if __name__ == "__main__": | |
| app = build_app() | |
| app.queue(max_size=8).launch() | |