RiverRider's picture
feat: /geometry endpoint — per-layer direction norms, residual projection, cross-layer cosine matrix
2885e84 verified
Raw
History Blame Contribute Delete
38.9 kB
"""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 = 128
MAX_NEW_TOKENS_LIMIT = 512 # ZeroGPU 180s budget: ~512 tok × BoN=16 fits
_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_cpu() -> None:
"""Build model graph + load weights on CPU at module import.
ZeroGPU does NOT intercept ``torch._C._cuda_init`` at module load, so any
``.to('cuda')`` outside an ``@spaces.GPU`` function raises ``Found no NVIDIA
driver``. We therefore keep everything on CPU here, and move to GPU on the
first request inside :func:`_ensure_gpu`.
"""
if _state.get("cpu_ready"):
return
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 on CPU (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,
)
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 on CPU")
av = ActivationVerbalizer(cfg, backbone, tok).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()
print(f"[nla] mu loaded: ||mu||={float(mu.norm()):.4f}")
_state.update({
"cpu_ready": True,
"gpu_ready": False,
"device": torch.device("cpu"),
"cfg": cfg,
"tok": tok,
"backbone": backbone,
"av": av,
"mu": mu,
})
print("[nla] cpu-ready (GPU move deferred to first request)")
def _ensure_gpu() -> None:
"""Move model + μ to CUDA. MUST be called from inside @spaces.GPU."""
if _state.get("gpu_ready"):
return
device = torch.device("cuda")
print("[nla] moving model to GPU (first request)")
_state["backbone"] = _state["backbone"].to(device)
_state["av"] = _state["av"].to(device)
_state["mu"] = _state["mu"].to(device)
_state["device"] = device
_state["gpu_ready"] = True
print("[nla] gpu-ready")
@torch.no_grad()
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)
@torch.no_grad()
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 ─────────────────────────
@spaces.GPU(duration=180)
def roundtrip(passage: str, n_samples: int, max_new: int, temperature: float):
if not passage or not passage.strip():
return "", "", "Enter a passage."
_ensure_gpu()
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 ─────────────────────────
@spaces.GPU(duration=180)
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."
_ensure_gpu()
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
# ───────────────────────── Tab 3: steering ───────────────────────────
def _resolve_layer_module() -> torch.nn.Module:
"""Return the backbone block whose OUTPUT corresponds to
`hidden_states[extraction_layer]` from `output_hidden_states=True`.
With HF convention `hidden_states[0] = embeddings`, the i-th entry is
the output of `model.model.layers[i-1]`."""
cfg: NLAConfig = _state["cfg"]
backbone = _state["backbone"]
layers = backbone.model.layers # type: ignore[attr-defined]
idx = cfg.extraction_layer - 1
return layers[idx]
def _make_steer_hook(steer_vec: torch.Tensor):
"""Forward hook adding `steer_vec` (shape (d,)) to all positions of the
block's output hidden states. `steer_vec` must already be on-device and
in the backbone's dtype."""
def hook(_module, _inputs, output):
# Qwen2 block output is a tuple: (hidden_states, ...)
if isinstance(output, tuple):
h = output[0]
h2 = h + steer_vec.to(h.dtype).to(h.device)
return (h2,) + output[1:]
h = output
return h + steer_vec.to(h.dtype).to(h.device)
return hook
@torch.no_grad()
def _generate_with_steering(prompt: str, steer_vec: torch.Tensor | None,
max_new: int) -> str:
"""Run greedy generation on the raw backbone. If `steer_vec` is not None
it is added to L20 output at every forward pass."""
tok = _state["tok"]
backbone = _state["backbone"]
device = _state["device"]
enc = tok(prompt, return_tensors="pt", truncation=True,
max_length=MAX_INPUT_TOKENS).to(device)
handle = None
if steer_vec is not None:
layer = _resolve_layer_module()
handle = layer.register_forward_hook(_make_steer_hook(steer_vec))
try:
out_ids = backbone.generate(
input_ids=enc.input_ids,
attention_mask=enc.attention_mask,
max_new_tokens=int(max_new),
do_sample=False,
temperature=1.0,
top_p=1.0,
pad_token_id=tok.pad_token_id or tok.eos_token_id,
)
finally:
if handle is not None:
handle.remove()
# Strip the prompt from the decoded text.
new_ids = out_ids[0, enc.input_ids.shape[1]:]
return tok.decode(new_ids, skip_special_tokens=True)
@spaces.GPU(duration=180)
def steer(prompt: str, text_a: str, text_b: str, alpha: float, max_new: int):
"""Activation-patching probe.
Compute v_A = encode(text_A), v_B = encode(text_B) at L20 last-token.
Add alpha * (v_B - v_A) to every L20 hidden state during a real
greedy Qwen forward pass on `prompt`. Returns the baseline generation
(no steering) and the steered generation side by side.
"""
if not (prompt and prompt.strip()):
return "", "", "Provide a prompt."
if not (text_a and text_a.strip() and text_b and text_b.strip()):
return "", "", "Provide both A and B anchors."
_ensure_gpu()
v_a = _encode_text(text_a.strip()) # (1, d) fp32
v_b = _encode_text(text_b.strip())
direction = (v_b - v_a).squeeze(0) # (d,) fp32
dir_norm = float(direction.norm())
a = float(alpha)
steer_vec = (a * direction).contiguous()
baseline = _generate_with_steering(prompt, steer_vec=None, max_new=int(max_new))
steered = _generate_with_steering(prompt, steer_vec=steer_vec, max_new=int(max_new))
info = (
f"**alpha = {a:.2f}** · steering direction = v_B − v_A, "
f"||v_B − v_A|| = `{dir_norm:.3f}` · ||α·dir|| = `{a*dir_norm:.3f}`\n\n"
f"Steering vector is added to **every position** of the L{_state['cfg'].extraction_layer} "
"output during the real Qwen2.5-7B forward pass (no AV involved)."
)
return baseline.strip(), steered.strip(), info
# ───────────── Tab 4: layer-scan + ablation + multi-pair ─────────────
@torch.no_grad()
def _encode_text_at_layer(text: str, layer: int) -> torch.Tensor:
"""Pooled last-token hidden at an arbitrary block. (1, d) fp32.
layer is 1..N (1 = first block output, N = final block output)."""
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[layer] # hidden_states[0]=embed, [layer]=output of layers[layer-1]
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 _make_ablate_hook(unit_dir: torch.Tensor):
"""Project the direction out of every position's hidden state.
unit_dir is (d,) fp32 on-device, unit norm.
h'_t = h_t − (h_t · d̂) d̂
"""
def hook(_module, _inputs, output):
if isinstance(output, tuple):
h = output[0]
else:
h = output
d = unit_dir.to(h.dtype).to(h.device)
# (B, T, d) · (d,) -> (B, T)
proj = (h * d).sum(dim=-1, keepdim=True)
h2 = h - proj * d
if isinstance(output, tuple):
return (h2,) + output[1:]
return h2
return hook
@torch.no_grad()
def _generate_with_hook(prompt: str, layer: int, hook_fn, max_new: int) -> str:
"""Greedy generation with `hook_fn` (or None) attached to layers[layer-1]."""
tok = _state["tok"]
backbone = _state["backbone"]
device = _state["device"]
enc = tok(prompt, return_tensors="pt", truncation=True,
max_length=MAX_INPUT_TOKENS).to(device)
handle = None
if hook_fn is not None:
layer_mod = backbone.model.layers[layer - 1] # type: ignore[attr-defined]
handle = layer_mod.register_forward_hook(hook_fn)
try:
out_ids = backbone.generate(
input_ids=enc.input_ids, attention_mask=enc.attention_mask,
max_new_tokens=int(max_new), do_sample=False,
temperature=1.0, top_p=1.0,
pad_token_id=tok.pad_token_id or tok.eos_token_id,
)
finally:
if handle is not None:
handle.remove()
return tok.decode(out_ids[0, enc.input_ids.shape[1]:], skip_special_tokens=True)
def _mean_encode(texts: list[str], layer: int) -> torch.Tensor:
"""Difference-of-means anchor: mean of L<layer> last-token hiddens. (d,) fp32."""
vs = [_encode_text_at_layer(t, layer).squeeze(0) for t in texts]
return torch.stack(vs, dim=0).mean(dim=0)
@spaces.GPU(duration=180)
def steer_layer(prompt: str, texts_a: str, texts_b: str, alpha: float,
layer: int, mode: str, max_new: int):
"""Layer-scan + ablation + multi-pair difference-of-means.
`texts_a`, `texts_b`: pipe-separated lists. Empty entries are dropped.
`mode`: 'add' = h + α·(μ_B − μ_A); 'ablate' = h − (h·d̂)·d̂.
`layer`: 1..num_hidden_layers. Direction is computed at the same layer.
"""
if not (prompt and prompt.strip()):
return "", "", "Provide a prompt."
backbone = _state.get("backbone")
cfg = _state.get("cfg")
if backbone is None or cfg is None:
_ensure_gpu()
backbone = _state["backbone"]; cfg = _state["cfg"]
_ensure_gpu()
n_layers = backbone.config.num_hidden_layers # type: ignore[attr-defined]
layer = int(layer)
if not (1 <= layer <= n_layers):
return "", "", f"layer must be in [1, {n_layers}]"
A = [t.strip() for t in (texts_a or "").split("|") if t.strip()]
B = [t.strip() for t in (texts_b or "").split("|") if t.strip()]
if not A or not B:
return "", "", "Provide at least one A and one B (pipe-separated for multi-pair)."
mu_A = _mean_encode(A, layer)
mu_B = _mean_encode(B, layer)
direction = mu_B - mu_A
dir_norm = float(direction.norm())
if dir_norm < 1e-6:
return "", "", "v_B − v_A is degenerate (zero norm)."
baseline = _generate_with_hook(prompt, layer, None, int(max_new))
if mode == "ablate":
unit = (direction / dir_norm).contiguous()
hook_fn = _make_ablate_hook(unit)
a_used = 0.0
else:
steer_vec = (float(alpha) * direction).contiguous()
def hook_fn(_module, _inputs, output, _sv=steer_vec):
if isinstance(output, tuple):
h = output[0]
return (h + _sv.to(h.dtype).to(h.device),) + output[1:]
return output + _sv.to(output.dtype).to(output.device)
a_used = float(alpha)
steered = _generate_with_hook(prompt, layer, hook_fn, int(max_new))
info = (
f"**layer={layer}/{n_layers}** · mode=**{mode}** · "
f"|A|={len(A)} |B|={len(B)} · "
f"||μ_B − μ_A|| = `{dir_norm:.3f}` · α·||dir|| = `{a_used*dir_norm:.3f}`\n\n"
f"{'Ablating' if mode=='ablate' else 'Adding'} the direction "
f"at every position of `layers[{layer-1}]` output during a real "
f"Qwen2.5-7B greedy forward pass."
)
return baseline.strip(), steered.strip(), info
# ───────────────── Tab 5: geometry (direction quality) ───────────────
@torch.no_grad()
def _all_layer_last_token(text: str, layers: list[int]) -> dict[int, torch.Tensor]:
"""One forward pass → last-token hidden at each requested layer."""
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,
)
last = (enc.attention_mask.sum(-1) - 1).clamp(min=0).long()
rows = torch.arange(enc.input_ids.size(0), device=device)
return {L: out.hidden_states[L][rows, last, :].detach().to(torch.float32).squeeze(0)
for L in layers}
@torch.no_grad()
def _all_layer_all_tokens(text: str, layers: list[int]) -> tuple[dict[int, torch.Tensor], int]:
"""One forward pass → (T, d) hidden at each layer; returns (dict, T)."""
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,
)
T = int(enc.attention_mask.sum().item())
# hidden_states[L] is (1, T_full, d). Use attention_mask to take valid tokens.
mask = enc.attention_mask[0].bool()
return ({L: out.hidden_states[L][0, mask, :].detach().to(torch.float32)
for L in layers}, T)
@spaces.GPU(duration=180)
def geometry(prompt: str, texts_a: str, texts_b: str, layers_str: str):
"""Per-layer geometric report on the (μ_B − μ_A) direction.
For each requested layer L:
- compute μ_A, μ_B over the A/B anchor banks, d_L = μ_B − μ_A
- report ||d_L||, ||μ_A||, ||μ_B||
- run `prompt` forward, get (T, d) at L
- report mean_t |h_t · d̂_L| (alignment magnitude of residual stream
with the direction, averaged over the prompt's tokens) and
||h_last||
Also report the cosine matrix between d̂_L across the requested layers.
"""
if not (prompt and prompt.strip()):
return "Provide a prompt."
_ensure_gpu()
backbone = _state["backbone"]
n_layers = backbone.config.num_hidden_layers # type: ignore[attr-defined]
try:
layers = sorted({int(x.strip()) for x in layers_str.split(",") if x.strip()})
except Exception:
return "layers must be a comma-separated list of ints."
layers = [L for L in layers if 1 <= L <= n_layers]
if not layers:
return f"no valid layers (allowed 1..{n_layers})"
A = [t.strip() for t in (texts_a or "").split("|") if t.strip()]
B = [t.strip() for t in (texts_b or "").split("|") if t.strip()]
if not A or not B:
return "Provide at least one A and one B anchor (pipe-separated)."
# Compute μ_A, μ_B at each layer via individual forward passes (small N).
a_stacks: dict[int, list[torch.Tensor]] = {L: [] for L in layers}
b_stacks: dict[int, list[torch.Tensor]] = {L: [] for L in layers}
for t in A:
d = _all_layer_last_token(t, layers)
for L in layers: a_stacks[L].append(d[L])
for t in B:
d = _all_layer_last_token(t, layers)
for L in layers: b_stacks[L].append(d[L])
mu_A = {L: torch.stack(a_stacks[L]).mean(0) for L in layers}
mu_B = {L: torch.stack(b_stacks[L]).mean(0) for L in layers}
d = {L: (mu_B[L] - mu_A[L]) for L in layers}
d_norm = {L: float(d[L].norm()) for L in layers}
d_hat = {L: (d[L] / (d_norm[L] + 1e-9)) for L in layers}
# Prompt residual stream at each layer.
h_dict, T = _all_layer_all_tokens(prompt.strip(), layers)
per_layer = []
for L in layers:
h = h_dict[L] # (T, d) fp32
# projection scalars per token
proj = (h * d_hat[L]).sum(dim=-1) # (T,)
proj_abs_mean = float(proj.abs().mean())
proj_signed_mean = float(proj.mean())
h_norms = h.norm(dim=-1) # (T,)
h_norm_mean = float(h_norms.mean())
# cosine of last-token h with d_hat
h_last = h[-1]
cos_last = float(F.cosine_similarity(h_last.unsqueeze(0),
d_hat[L].unsqueeze(0), dim=-1))
per_layer.append({
"L": L,
"d_norm": d_norm[L],
"mu_A_norm": float(mu_A[L].norm()),
"mu_B_norm": float(mu_B[L].norm()),
"h_norm_mean": h_norm_mean,
"proj_abs_mean": proj_abs_mean,
"proj_signed_mean": proj_signed_mean,
"frac_of_hnorm": proj_abs_mean / max(h_norm_mean, 1e-9),
"cos_last_token": cos_last,
})
# Cross-layer cosine matrix on d_hat.
cos_matrix = []
for L1 in layers:
row = []
for L2 in layers:
row.append(float(F.cosine_similarity(d_hat[L1].unsqueeze(0),
d_hat[L2].unsqueeze(0), dim=-1)))
cos_matrix.append(row)
# Render markdown.
lines = [
f"**Prompt tokens:** T = {T} · **|A|** = {len(A)} · **|B|** = {len(B)}",
"",
"### Per-layer direction quality & residual stream alignment",
"",
"| L | ‖d_L‖ | ‖μ_A‖ | ‖μ_B‖ | mean‖h_t‖ | mean<sub>t</sub>|h·d̂| | as frac of ‖h‖ | cos(h_last, d̂) |",
"|---|---:|---:|---:|---:|---:|---:|---:|",
]
for r in per_layer:
lines.append(
f"| {r['L']} | `{r['d_norm']:.2f}` | `{r['mu_A_norm']:.2f}` | "
f"`{r['mu_B_norm']:.2f}` | `{r['h_norm_mean']:.2f}` | "
f"`{r['proj_abs_mean']:.3f}` | `{r['frac_of_hnorm']*100:.2f}%` | "
f"`{r['cos_last_token']:+.4f}` |"
)
lines += [
"",
"### Cross-layer direction cosine matrix · cos(d̂_L, d̂_L′)",
"",
"| L \\ L' | " + " | ".join(str(L) for L in layers) + " |",
"|" + "---|" * (len(layers) + 1),
]
for L1, row in zip(layers, cos_matrix):
lines.append(
"| **" + str(L1) + "** | " +
" | ".join(f"`{v:+.3f}`" for v in row) + " |"
)
return "\n".join(lines)
# ───────────────────────────── 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, MAX_NEW_TOKENS_LIMIT, 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, MAX_NEW_TOKENS_LIMIT, 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])
# ---------------- Tab 3 ----------------
with gr.Tab("Activation steering"):
gr.Markdown(
"Activation-patching probe. Encode two anchors A, B at L20 "
"→ steering direction **s = v_B − v_A**. Run a real greedy "
"Qwen2.5-7B forward pass on `prompt`, adding **α · s** to "
"every token's L20 hidden state. **The AV decoder is not "
"used here** — generation comes straight from the backbone. "
"Compares baseline (α=0) and steered output side by side."
)
with gr.Row():
with gr.Column():
s_prompt = gr.Textbox(label="Prompt to Qwen", lines=3,
placeholder="e.g. How are you today?")
s_text_a = gr.Textbox(label="Anchor A", lines=3)
s_text_b = gr.Textbox(label="Anchor B", lines=3)
s_alpha = gr.Slider(-2.0, 2.0, value=1.0, step=0.05,
label="α (steering strength)")
s_max = gr.Slider(16, MAX_NEW_TOKENS_LIMIT,
value=MAX_NEW_TOKENS_DEFAULT, step=16,
label="Max new tokens")
go_s = gr.Button("Steer", variant="primary")
with gr.Column():
s_baseline = gr.Textbox(label="Baseline (α = 0)", lines=8)
s_steered = gr.Textbox(label="Steered output", lines=8)
s_info = gr.Markdown()
go_s.click(steer,
inputs=[s_prompt, s_text_a, s_text_b, s_alpha, s_max],
outputs=[s_baseline, s_steered, s_info])
# ---------------- Tab 4 ----------------
with gr.Tab("Layer scan / ablation"):
gr.Markdown(
"Generalised activation patching: choose **any layer** "
"(1..28 for Qwen2.5-7B), choose **mode** = `add` (h + α·d) "
"or `ablate` (project d̂ out of h), and use **multi-pair** "
"anchors by separating with `|`. Direction is computed at "
"the same layer being patched (difference-of-means: "
"`μ_B − μ_A`).\n\n"
"_Tab 3 is the single-layer single-pair specialisation of this._"
)
with gr.Row():
with gr.Column():
l_prompt = gr.Textbox(label="Prompt to Qwen", lines=3)
l_texts_a = gr.Textbox(
label="Anchors A (pipe-separated for multi-pair)",
lines=4,
placeholder="text1 | text2 | text3",
)
l_texts_b = gr.Textbox(
label="Anchors B (pipe-separated for multi-pair)",
lines=4,
placeholder="text1 | text2 | text3",
)
l_alpha = gr.Slider(-2.0, 2.0, value=0.10, step=0.01,
label="α (used only when mode='add')")
l_layer = gr.Slider(1, 28, value=20, step=1,
label="Layer (1..28)")
l_mode = gr.Radio(["add", "ablate"], value="add",
label="Mode")
l_max = gr.Slider(16, MAX_NEW_TOKENS_LIMIT,
value=MAX_NEW_TOKENS_DEFAULT,
step=16, label="Max new tokens")
go_l = gr.Button("Run", variant="primary")
with gr.Column():
l_baseline = gr.Textbox(label="Baseline (no hook)", lines=8)
l_steered = gr.Textbox(label="Patched output", lines=8)
l_info = gr.Markdown()
go_l.click(steer_layer,
inputs=[l_prompt, l_texts_a, l_texts_b,
l_alpha, l_layer, l_mode, l_max],
outputs=[l_baseline, l_steered, l_info])
# ---------------- Tab 5 ----------------
with gr.Tab("Geometry"):
gr.Markdown(
"**Geometric report on the (μ_B − μ_A) direction.** "
"For each requested layer L this computes "
"‖d_L‖, the mean magnitude of the prompt's residual-stream "
"projection onto d̂_L (averaged over tokens), and the "
"cosine matrix between d̂_L across layers (does the "
"direction stay fixed through the network?).\n\n"
"_If `mean|h·d̂|` is near zero, the model isn't using "
"the direction at that layer — which is exactly why "
"ablation does nothing._"
)
with gr.Row():
with gr.Column():
g_prompt = gr.Textbox(label="Prompt", lines=3)
g_texts_a = gr.Textbox(
label="Anchors A (pipe-separated)", lines=4)
g_texts_b = gr.Textbox(
label="Anchors B (pipe-separated)", lines=4)
g_layers = gr.Textbox(
label="Layers (comma-separated, 1..28)",
value="4, 8, 12, 16, 20, 24, 28")
go_g = gr.Button("Measure", variant="primary")
with gr.Column():
g_report = gr.Markdown()
go_g.click(geometry,
inputs=[g_prompt, g_texts_a, g_texts_b, g_layers],
outputs=[g_report])
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 + build the model graph on CPU at import. The
# CUDA move is deferred until the first @spaces.GPU call (see _ensure_gpu),
# because ZeroGPU does not stub out torch._C._cuda_init at module-load time.
_prefetch()
_setup_cpu()
if __name__ == "__main__":
app = build_app()
app.queue(max_size=8).launch()