Agnes-AI's picture
Add inference code
fc3971e verified
Raw History Blame Contribute Delete
8.87 kB
"""Offline diagnostics for signed TENP vs actual removal damage."""
from __future__ import annotations
import torch
from .collector import perturbation_sensitivity, stacked_token_cache
from .score import PROJ_EPS
def spearman_corr(x: torch.Tensor, y: torch.Tensor, eps: float = 1e-8) -> float:
"""Average-rank Spearman. Fine for distinct scores; ties share mid-ranks."""
a = x.detach().float().reshape(-1)
b = y.detach().float().reshape(-1)
if a.numel() != b.numel() or a.numel() < 3:
return float("nan")
ra = _average_rank(a)
rb = _average_rank(b)
ra = ra - ra.mean()
rb = rb - rb.mean()
denom = (ra.norm() * rb.norm()).clamp_min(eps)
return float((ra * rb).sum() / denom)
def _average_rank(x: torch.Tensor) -> torch.Tensor:
n = int(x.numel())
order = torch.argsort(x)
ranks = torch.empty(n, device=x.device, dtype=torch.float32)
sorted_x = x[order]
i = 0
while i < n:
j = i
while j + 1 < n and float(sorted_x[j + 1]) == float(sorted_x[i]):
j += 1
mid = 0.5 * (i + j) + 1.0
ranks[order[i : j + 1]] = mid
i = j + 1
return ranks
def single_neuron_removal_damage(
rel_abs_z: torch.Tensor,
n_tokens: int,
down_weight: torch.Tensor,
) -> torch.Tensor:
"""Mean relative L2 of masking neuron i at expert output (top-1 MoE-equivalent).
During collect we store sum_t |z_{t,i}| / ||y_t||. Then:
Δ_i = (rel_abs_z_i / N) * ||W_down[:, i]||_2
which equals E_t ||z_i W_i|| / ||y||.
"""
if n_tokens <= 0:
return torch.zeros_like(rel_abs_z, dtype=torch.float32)
col_norm = down_weight.detach().float().norm(dim=0)
return rel_abs_z.detach().float() / float(n_tokens) * col_norm
def group_mean_damage(damage: torch.Tensor, idx: torch.Tensor) -> float:
if idx.numel() == 0:
return float("nan")
return float(damage[idx.long()].mean())
def group_removal_relative_l2(
z: torch.Tensor,
y: torch.Tensor,
down_weight: torch.Tensor,
idx: torch.Tensor,
eps: float = PROJ_EPS,
) -> float:
"""Exact mean ||sum_{i in G} z_i W_i|| / ||y|| on cached tokens."""
if idx is None or int(idx.numel()) == 0 or z.numel() == 0:
return float("nan")
keep = idx.detach().long().to(device=z.device)
w = down_weight.detach().to(device=z.device, dtype=torch.float32)
z_f = z.float()
y_n = y.float().norm(dim=-1).clamp_min(eps)
delta = z_f.index_select(1, keep) @ w.index_select(1, keep).T
return float((delta.norm(dim=-1) / y_n).mean())
def sensitivity_by_bin(
cache_z: list,
cache_y: list,
down_weight: torch.Tensor,
ratios: tuple[float, ...] = (0.05, 0.10),
repeats: int = 3,
seed: int = 0,
) -> dict[str, list[float]]:
"""Offline D^ρ per timestep bin from the token reservoir."""
gen = torch.Generator(device="cpu")
gen.manual_seed(int(seed))
out: dict[str, list[float]] = {f"{rho:.2f}": [] for rho in ratios}
n_bins = max(len(cache_z or []), len(cache_y or []))
for bin_id in range(n_bins):
z = None if cache_z is None or bin_id >= len(cache_z) else cache_z[bin_id]
y = None if cache_y is None or bin_id >= len(cache_y) else cache_y[bin_id]
for rho in ratios:
key = f"{rho:.2f}"
if z is None or y is None or int(z.numel()) == 0:
out[key].append(float("nan"))
continue
out[key].append(
perturbation_sensitivity(z, y, down_weight, rho, repeats, gen)
)
return out
def redundancy_pair_causal(
z: torch.Tensor,
y: torch.Tensor,
down_weight: torch.Tensor,
score: torch.Tensor,
r_plus: torch.Tensor | None = None,
gram: torch.Tensor | None = None,
n_cand: int = 256,
n_pairs: int = 16,
seed: int = 0,
) -> dict:
"""Compare high-R vs low-R pairs: additivity = Δij / (Δi+Δj).
Same-direction (redundant) pairs add constructively, so additivity is
closer to 1 than for near-orthogonal controls with similar TENP scores.
"""
from .redundancy import contribution_redundancy
s = score.detach().float().reshape(-1)
pos = (s > 0).nonzero(as_tuple=False).reshape(-1)
if int(pos.numel()) < 4 or z.numel() == 0:
return {"skipped": "need cached tokens and >=4 positive-score neurons"}
k = min(int(n_cand), int(pos.numel()))
_, local = torch.topk(s[pos], k)
idx = pos[local]
if r_plus is not None:
sub = r_plus.detach().float()[idx][:, idx].clone()
else:
g_sub = None if gram is None else gram.detach().float()[idx][:, idx]
sub = contribution_redundancy(g_sub, down_weight.detach().float()[:, idx])
sub.fill_diagonal_(0)
tri_i, tri_j = torch.triu_indices(k, k, offset=1)
r_flat = sub[tri_i, tri_j]
if int(r_flat.numel()) < 2:
return {"skipped": "not enough pairs"}
n_take = min(int(n_pairs), int(r_flat.numel()))
high_sel = torch.topk(r_flat, n_take).indices
low_sel = torch.topk(-r_flat, n_take).indices
def _pack(sel: torch.Tensor) -> dict:
additivities = []
overlaps = []
r_vals = []
for t in sel.tolist():
li = int(tri_i[t])
lj = int(tri_j[t])
a = int(idx[li].item())
b = int(idx[lj].item())
di = group_removal_relative_l2(z, y, down_weight, torch.tensor([a]))
dj = group_removal_relative_l2(z, y, down_weight, torch.tensor([b]))
both = group_removal_relative_l2(z, y, down_weight, torch.tensor([a, b]))
denom = di + dj
additivity = float("nan") if denom <= 0 else float(both / denom)
overlap = float("nan") if denom <= 0 else float((denom - both) / denom)
additivities.append(additivity)
overlaps.append(overlap)
r_vals.append(float(sub[li, lj]))
finite_a = [x for x in additivities if x == x]
finite_o = [x for x in overlaps if x == x]
return {
"n_pairs": len(additivities),
"r_mean": round(float(sum(r_vals) / max(len(r_vals), 1)), 4),
"additivity_mean": (
round(float(sum(finite_a) / len(finite_a)), 4) if finite_a else float("nan")
),
"overlap_mean": (
round(float(sum(finite_o) / len(finite_o)), 4) if finite_o else float("nan")
),
}
del seed
high = _pack(high_sel)
low = _pack(low_sel)
return {
"high_r": high,
"low_r": low,
"additivity_gap": _finite_gap(high, low, "additivity_mean"),
"overlap_gap": _finite_gap(high, low, "overlap_mean"),
"note": "aligned/redundant pairs have additivity Δij/(Δi+Δj) closer to 1",
}
def _finite_gap(high: dict, low: dict, key: str) -> float:
a = high.get(key)
b = low.get(key)
if not isinstance(a, float) or not isinstance(b, float):
return float("nan")
if a != a or b != b:
return float("nan")
return round(float(a - b), 4)
def cache_group_report(
stats,
down_weight: torch.Tensor,
signed: torch.Tensor,
damage: torch.Tensor,
frac: float = 0.10,
) -> dict:
"""Exact group-removal Δ on the reservoir vs mean of single-neuron Δ."""
n_bot = max(1, int(round(float(frac) * int(signed.numel()))))
bot = torch.topk(signed, n_bot, largest=False).indices
top = torch.topk(signed, n_bot, largest=True).indices
packed = stacked_token_cache(stats)
rec = {
"approx_mean_delta": {
"lowest_signed_10pct": round(group_mean_damage(damage, bot), 6),
"highest_signed_10pct": round(group_mean_damage(damage, top), 6),
}
}
if packed is None:
rec["exact_group_delta"] = {"skipped": "missing token cache; re-collect"}
rec["sensitivity_rho"] = {"skipped": "missing token cache; re-collect"}
rec["redundancy_pairs"] = {"skipped": "missing token cache; re-collect"}
return rec
z, y, _gate = packed
rec["n_cached_tokens"] = int(z.shape[0])
rec["exact_group_delta"] = {
"lowest_signed_10pct": round(
group_removal_relative_l2(z, y, down_weight, bot), 6
),
"highest_signed_10pct": round(
group_removal_relative_l2(z, y, down_weight, top), 6
),
}
rec["sensitivity_rho"] = sensitivity_by_bin(
stats.cache_z or [],
stats.cache_y or [],
down_weight,
ratios=(0.05, 0.10),
)
d5 = torch.tensor(rec["sensitivity_rho"]["0.05"], dtype=torch.float32)
d10 = torch.tensor(rec["sensitivity_rho"]["0.10"], dtype=torch.float32)
live = torch.isfinite(d5) & torch.isfinite(d10)
rec["spearman_d05_vs_d10"] = (
round(spearman_corr(d5[live], d10[live]), 4) if int(live.sum()) >= 3 else float("nan")
)
return rec