Spaces:
Sleeping
Sleeping
feat: /geometry endpoint — per-layer direction norms, residual projection, cross-layer cosine matrix
Browse files
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 "
|