RiverRider commited on
Commit
2885e84
·
verified ·
1 Parent(s): 5986ea1

feat: /geometry endpoint — per-layer direction norms, residual projection, cross-layer cosine matrix

Browse files
Files changed (1) hide show
  1. app.py +184 -0
app.py CHANGED
@@ -540,6 +540,160 @@ def steer_layer(prompt: str, texts_a: str, texts_b: str, alpha: float,
540
  return baseline.strip(), steered.strip(), info
541
 
542
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
  # ───────────────────────────── UI ────────────────────────────────────
544
 
545
  EXAMPLES_RT = [
@@ -711,6 +865,36 @@ def build_app() -> gr.Blocks:
711
  l_alpha, l_layer, l_mode, l_max],
712
  outputs=[l_baseline, l_steered, l_info])
713
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
714
  gr.Markdown(
715
  "---\n"
716
  "**About.** This demo runs the published `srt-nla-av-v1` checkpoint "
 
540
  return baseline.strip(), steered.strip(), info
541
 
542
 
543
+ # ───────────────── Tab 5: geometry (direction quality) ───────────────
544
+
545
+ @torch.no_grad()
546
+ def _all_layer_last_token(text: str, layers: list[int]) -> dict[int, torch.Tensor]:
547
+ """One forward pass → last-token hidden at each requested layer."""
548
+ tok = _state["tok"]
549
+ backbone = _state["backbone"]
550
+ device = _state["device"]
551
+ enc = tok(text, truncation=True, max_length=MAX_INPUT_TOKENS,
552
+ return_tensors="pt").to(device)
553
+ out = backbone(
554
+ input_ids=enc.input_ids, attention_mask=enc.attention_mask,
555
+ output_hidden_states=True, use_cache=False,
556
+ )
557
+ last = (enc.attention_mask.sum(-1) - 1).clamp(min=0).long()
558
+ rows = torch.arange(enc.input_ids.size(0), device=device)
559
+ return {L: out.hidden_states[L][rows, last, :].detach().to(torch.float32).squeeze(0)
560
+ for L in layers}
561
+
562
+
563
+ @torch.no_grad()
564
+ def _all_layer_all_tokens(text: str, layers: list[int]) -> tuple[dict[int, torch.Tensor], int]:
565
+ """One forward pass → (T, d) hidden at each layer; returns (dict, T)."""
566
+ tok = _state["tok"]
567
+ backbone = _state["backbone"]
568
+ device = _state["device"]
569
+ enc = tok(text, truncation=True, max_length=MAX_INPUT_TOKENS,
570
+ return_tensors="pt").to(device)
571
+ out = backbone(
572
+ input_ids=enc.input_ids, attention_mask=enc.attention_mask,
573
+ output_hidden_states=True, use_cache=False,
574
+ )
575
+ T = int(enc.attention_mask.sum().item())
576
+ # hidden_states[L] is (1, T_full, d). Use attention_mask to take valid tokens.
577
+ mask = enc.attention_mask[0].bool()
578
+ return ({L: out.hidden_states[L][0, mask, :].detach().to(torch.float32)
579
+ for L in layers}, T)
580
+
581
+
582
+ @spaces.GPU(duration=180)
583
+ def geometry(prompt: str, texts_a: str, texts_b: str, layers_str: str):
584
+ """Per-layer geometric report on the (μ_B − μ_A) direction.
585
+
586
+ For each requested layer L:
587
+ - compute μ_A, μ_B over the A/B anchor banks, d_L = μ_B − μ_A
588
+ - report ||d_L||, ||μ_A||, ||μ_B||
589
+ - run `prompt` forward, get (T, d) at L
590
+ - report mean_t |h_t · d̂_L| (alignment magnitude of residual stream
591
+ with the direction, averaged over the prompt's tokens) and
592
+ ||h_last||
593
+ Also report the cosine matrix between d̂_L across the requested layers.
594
+ """
595
+ if not (prompt and prompt.strip()):
596
+ return "Provide a prompt."
597
+ _ensure_gpu()
598
+ backbone = _state["backbone"]
599
+ n_layers = backbone.config.num_hidden_layers # type: ignore[attr-defined]
600
+
601
+ try:
602
+ layers = sorted({int(x.strip()) for x in layers_str.split(",") if x.strip()})
603
+ except Exception:
604
+ return "layers must be a comma-separated list of ints."
605
+ layers = [L for L in layers if 1 <= L <= n_layers]
606
+ if not layers:
607
+ return f"no valid layers (allowed 1..{n_layers})"
608
+
609
+ A = [t.strip() for t in (texts_a or "").split("|") if t.strip()]
610
+ B = [t.strip() for t in (texts_b or "").split("|") if t.strip()]
611
+ if not A or not B:
612
+ return "Provide at least one A and one B anchor (pipe-separated)."
613
+
614
+ # Compute μ_A, μ_B at each layer via individual forward passes (small N).
615
+ a_stacks: dict[int, list[torch.Tensor]] = {L: [] for L in layers}
616
+ b_stacks: dict[int, list[torch.Tensor]] = {L: [] for L in layers}
617
+ for t in A:
618
+ d = _all_layer_last_token(t, layers)
619
+ for L in layers: a_stacks[L].append(d[L])
620
+ for t in B:
621
+ d = _all_layer_last_token(t, layers)
622
+ for L in layers: b_stacks[L].append(d[L])
623
+
624
+ mu_A = {L: torch.stack(a_stacks[L]).mean(0) for L in layers}
625
+ mu_B = {L: torch.stack(b_stacks[L]).mean(0) for L in layers}
626
+ d = {L: (mu_B[L] - mu_A[L]) for L in layers}
627
+ d_norm = {L: float(d[L].norm()) for L in layers}
628
+ d_hat = {L: (d[L] / (d_norm[L] + 1e-9)) for L in layers}
629
+
630
+ # Prompt residual stream at each layer.
631
+ h_dict, T = _all_layer_all_tokens(prompt.strip(), layers)
632
+ per_layer = []
633
+ for L in layers:
634
+ h = h_dict[L] # (T, d) fp32
635
+ # projection scalars per token
636
+ proj = (h * d_hat[L]).sum(dim=-1) # (T,)
637
+ proj_abs_mean = float(proj.abs().mean())
638
+ proj_signed_mean = float(proj.mean())
639
+ h_norms = h.norm(dim=-1) # (T,)
640
+ h_norm_mean = float(h_norms.mean())
641
+ # cosine of last-token h with d_hat
642
+ h_last = h[-1]
643
+ cos_last = float(F.cosine_similarity(h_last.unsqueeze(0),
644
+ d_hat[L].unsqueeze(0), dim=-1))
645
+ per_layer.append({
646
+ "L": L,
647
+ "d_norm": d_norm[L],
648
+ "mu_A_norm": float(mu_A[L].norm()),
649
+ "mu_B_norm": float(mu_B[L].norm()),
650
+ "h_norm_mean": h_norm_mean,
651
+ "proj_abs_mean": proj_abs_mean,
652
+ "proj_signed_mean": proj_signed_mean,
653
+ "frac_of_hnorm": proj_abs_mean / max(h_norm_mean, 1e-9),
654
+ "cos_last_token": cos_last,
655
+ })
656
+
657
+ # Cross-layer cosine matrix on d_hat.
658
+ cos_matrix = []
659
+ for L1 in layers:
660
+ row = []
661
+ for L2 in layers:
662
+ row.append(float(F.cosine_similarity(d_hat[L1].unsqueeze(0),
663
+ d_hat[L2].unsqueeze(0), dim=-1)))
664
+ cos_matrix.append(row)
665
+
666
+ # Render markdown.
667
+ lines = [
668
+ f"**Prompt tokens:** T = {T} · **|A|** = {len(A)} · **|B|** = {len(B)}",
669
+ "",
670
+ "### Per-layer direction quality & residual stream alignment",
671
+ "",
672
+ "| L | ‖d_L‖ | ‖μ_A‖ | ‖μ_B‖ | mean‖h_t‖ | mean<sub>t</sub>|h·d̂| | as frac of ‖h‖ | cos(h_last, d̂) |",
673
+ "|---|---:|---:|---:|---:|---:|---:|---:|",
674
+ ]
675
+ for r in per_layer:
676
+ lines.append(
677
+ f"| {r['L']} | `{r['d_norm']:.2f}` | `{r['mu_A_norm']:.2f}` | "
678
+ f"`{r['mu_B_norm']:.2f}` | `{r['h_norm_mean']:.2f}` | "
679
+ f"`{r['proj_abs_mean']:.3f}` | `{r['frac_of_hnorm']*100:.2f}%` | "
680
+ f"`{r['cos_last_token']:+.4f}` |"
681
+ )
682
+ lines += [
683
+ "",
684
+ "### Cross-layer direction cosine matrix · cos(d̂_L, d̂_L′)",
685
+ "",
686
+ "| L \\ L' | " + " | ".join(str(L) for L in layers) + " |",
687
+ "|" + "---|" * (len(layers) + 1),
688
+ ]
689
+ for L1, row in zip(layers, cos_matrix):
690
+ lines.append(
691
+ "| **" + str(L1) + "** | " +
692
+ " | ".join(f"`{v:+.3f}`" for v in row) + " |"
693
+ )
694
+ return "\n".join(lines)
695
+
696
+
697
  # ───────────────────────────── UI ────────────────────────────────────
698
 
699
  EXAMPLES_RT = [
 
865
  l_alpha, l_layer, l_mode, l_max],
866
  outputs=[l_baseline, l_steered, l_info])
867
 
868
+ # ---------------- Tab 5 ----------------
869
+ with gr.Tab("Geometry"):
870
+ gr.Markdown(
871
+ "**Geometric report on the (μ_B − μ_A) direction.** "
872
+ "For each requested layer L this computes "
873
+ "‖d_L‖, the mean magnitude of the prompt's residual-stream "
874
+ "projection onto d̂_L (averaged over tokens), and the "
875
+ "cosine matrix between d̂_L across layers (does the "
876
+ "direction stay fixed through the network?).\n\n"
877
+ "_If `mean|h·d̂|` is near zero, the model isn't using "
878
+ "the direction at that layer — which is exactly why "
879
+ "ablation does nothing._"
880
+ )
881
+ with gr.Row():
882
+ with gr.Column():
883
+ g_prompt = gr.Textbox(label="Prompt", lines=3)
884
+ g_texts_a = gr.Textbox(
885
+ label="Anchors A (pipe-separated)", lines=4)
886
+ g_texts_b = gr.Textbox(
887
+ label="Anchors B (pipe-separated)", lines=4)
888
+ g_layers = gr.Textbox(
889
+ label="Layers (comma-separated, 1..28)",
890
+ value="4, 8, 12, 16, 20, 24, 28")
891
+ go_g = gr.Button("Measure", variant="primary")
892
+ with gr.Column():
893
+ g_report = gr.Markdown()
894
+ go_g.click(geometry,
895
+ inputs=[g_prompt, g_texts_a, g_texts_b, g_layers],
896
+ outputs=[g_report])
897
+
898
  gr.Markdown(
899
  "---\n"
900
  "**About.** This demo runs the published `srt-nla-av-v1` checkpoint "