File size: 11,623 Bytes
fc3971e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
"""Timestep-bin TENP aggregation. Default apply path still sums all bins (plain TENP)."""

from __future__ import annotations

import torch

from .scoring import fuse_modality_scores

DEFAULT_NUM_BINS = 5
DEFAULT_N_MIN = 32
DEFAULT_TEMPORAL_BETA = 0.3
DEFAULT_TEMPORAL_TOPK = 2
TEMPORAL_MODES = ("none", "mean", "ta", "st")
T_MAX_DEFAULT = 1000.0


def raw_signed_projection_score(table: torch.Tensor) -> torch.Tensor:
    """Sum routed signed projections without z-score changing their sign.

    Accepts either ``[3, d_int]`` or ``[K, 3, d_int]`` and sums only the
    modality axis. This score is deliberately separate from the normalized
    TENP ranking score.
    """
    if table.ndim == 2 and table.shape[0] == 3:
        return table.detach().float().sum(dim=0)
    if table.ndim == 3 and table.shape[1] == 3:
        return table.detach().float().sum(dim=1)
    raise ValueError(f"projection table must be [3,d] or [K,3,d], got {tuple(table.shape)}")


def _positive_percentile_rank(
    importance_bin: torch.Tensor,
    signed_bin: torch.Tensor,
) -> torch.Tensor:
    """Rank neurons within each bin, while raw non-positive neurons score zero."""
    if importance_bin.ndim != 2 or signed_bin.shape != importance_bin.shape:
        raise ValueError(
            f"importance/signed bins must share [K,d], got "
            f"{tuple(importance_bin.shape)} and {tuple(signed_bin.shape)}"
        )
    ranks = torch.zeros_like(importance_bin, dtype=torch.float32)
    for bin_id in range(int(importance_bin.shape[0])):
        positive = torch.nonzero(signed_bin[bin_id] > 0, as_tuple=False).reshape(-1)
        n_pos = int(positive.numel())
        if n_pos == 0:
            continue
        order = torch.argsort(importance_bin[bin_id, positive], stable=True)
        percentile = torch.empty(n_pos, device=importance_bin.device, dtype=torch.float32)
        percentile[order] = (
            torch.arange(1, n_pos + 1, device=importance_bin.device, dtype=torch.float32)
            / float(n_pos)
        )
        ranks[bin_id, positive] = percentile
    return ranks


def temporal_criticality(
    importance_bin: torch.Tensor,
    signed_bin: torch.Tensor,
    topq: int = DEFAULT_TEMPORAL_TOPK,
    adjacent: bool = False,
) -> torch.Tensor:
    """Top-Q mean of per-bin positive percentile ranks for each neuron."""
    ranks = _positive_percentile_rank(importance_bin, signed_bin)
    k = int(ranks.shape[0])
    q = min(max(int(topq), 1), k)
    if not adjacent or q == 1:
        return torch.topk(ranks, q, dim=0).values.mean(dim=0)
    windows = torch.stack(
        [ranks[start : start + q].mean(dim=0) for start in range(k - q + 1)],
        dim=0,
    )
    return windows.max(dim=0).values


def temporal_coverage(
    importance_bin: torch.Tensor,
    keep_idx: torch.Tensor,
    eps: float = 1e-8,
) -> torch.Tensor:
    """Fraction of positive per-bin importance retained by ``keep_idx``."""
    if importance_bin.ndim != 2:
        raise ValueError(f"importance_bin must be [K,d], got {tuple(importance_bin.shape)}")
    keep = keep_idx.detach().long().reshape(-1)
    positive = importance_bin.detach().float().clamp_min(0)
    retained = positive[:, keep].sum(dim=1)
    return retained / positive.sum(dim=1).clamp_min(eps)


def timestep_to_bin(
    timestep: float | None,
    num_bins: int,
    step_index: int | None = None,
    num_steps: int | None = None,
    t_max: float = T_MAX_DEFAULT,
) -> int:
    """Map a diffusion step onto `num_bins` equal bins.

    Prefer inference `step_index` so each bin gets a similar number of solver
    steps. Fall back to the scheduler value in `[0, t_max)`. Missing timestep
    lands in bin 0, which keeps legacy dumps identical to plain TENP.
    """
    k = max(int(num_bins), 1)
    if k == 1:
        return 0
    if step_index is not None and num_steps:
        n = max(int(num_steps), 1)
        return min(k - 1, max(0, int(step_index) * k // n))
    if timestep is None:
        return 0
    t = float(timestep)
    if t_max <= 0:
        return 0
    return min(k - 1, max(0, int(t / float(t_max) * k)))


def merge_sparse_bins(
    n_tokens_bin: torch.Tensor,
    n_min: int = DEFAULT_N_MIN,
) -> torch.Tensor:
    """Keep-mask over bins. Sparse bins are folded into the nearest kept neighbor."""
    counts = n_tokens_bin.detach().long().reshape(-1)
    k = int(counts.numel())
    keep = counts >= int(n_min)
    if bool(keep.any()) is False:
        keep = counts > 0
    if bool(keep.any()) is False:
        keep = torch.ones(k, dtype=torch.bool, device=counts.device)
        keep[1:] = False
    return keep


def fold_sparse_bin_mass(
    n_tokens_bin: torch.Tensor,
    values: torch.Tensor | None = None,
    n_min: int = DEFAULT_N_MIN,
) -> tuple[torch.Tensor, torch.Tensor]:
    """Move sparse-bin token mass (and optional values) onto the nearest kept bin.

    Returns (folded_counts, folded_values). `values` is typically D_{e,k};
    when omitted, only counts are folded.
    """
    n = n_tokens_bin.detach().float().reshape(-1).clone()
    keep = merge_sparse_bins(n, n_min=n_min)
    if values is None:
        v = torch.zeros_like(n)
        has_v = False
    else:
        v = values.detach().float().reshape(-1).clone()
        if v.numel() != n.numel():
            raise ValueError(f"values {tuple(v.shape)} != n_tokens_bin {tuple(n.shape)}")
        has_v = True
    kept_idx = torch.nonzero(keep, as_tuple=False).reshape(-1)
    for i in range(int(n.numel())):
        if bool(keep[i]):
            continue
        if float(n[i]) <= 0:
            continue
        nearest = int(kept_idx[torch.argmin((kept_idx - i).abs())].item())
        if has_v:
            mass = float(n[nearest]) + float(n[i])
            if mass > 0:
                v[nearest] = (v[nearest] * n[nearest] + v[i] * n[i]) / mass
            v[i] = 0
        n[nearest] = n[nearest] + n[i]
        n[i] = 0
    return n, v


def routing_sensitivity_alpha(
    n_tokens_bin: torch.Tensor,
    sensitivity: torch.Tensor,
    n_min: int = DEFAULT_N_MIN,
    eps: float = 1e-8,
    layer_sensitivity: torch.Tensor | None = None,
) -> torch.Tensor:
    """α_{e,k} = π_{e,k} D_{e,k} / sum_j(π D). Sparse bins fold into the nearest kept bin."""
    n = n_tokens_bin.detach().float().reshape(-1)
    d = sensitivity.detach().float().reshape(-1)
    if d.numel() != n.numel():
        raise ValueError(f"sensitivity {tuple(d.shape)} != n_tokens_bin {tuple(n.shape)}")
    if layer_sensitivity is not None:
        d_layer = layer_sensitivity.detach().float().reshape(-1)
        if d_layer.numel() == d.numel():
            keep = merge_sparse_bins(n, n_min=n_min)
            d = torch.where(~keep, d_layer, d)
    n, d = fold_sparse_bin_mass(n, d, n_min=n_min)
    pi = n / n.sum().clamp_min(1.0)
    raw = pi * d.clamp_min(0)
    if float(raw.sum()) <= 0:
        raw = pi
    return raw / raw.sum().clamp_min(eps)


def mean_alpha(n_tokens_bin: torch.Tensor, n_min: int = DEFAULT_N_MIN) -> torch.Tensor:
    keep = merge_sparse_bins(n_tokens_bin, n_min=n_min)
    n_fold, _ = fold_sparse_bin_mass(n_tokens_bin, n_min=n_min)
    w = torch.where(n_fold > 0, torch.ones_like(n_fold), torch.zeros_like(n_fold))
    if float(w.sum()) <= 0:
        w = keep.float()
    return w / w.sum().clamp_min(1.0)


def fuse_bins_to_importance(
    table_bin: torch.Tensor,
    n_tokens_bin: torch.Tensor,
    lambdas: tuple[float, float, float],
    n_min: int = DEFAULT_N_MIN,
    zscore: bool = True,
) -> torch.Tensor:
    """Per-bin modality fusion. `table_bin`: [K, 3, d_int] -> [K, d_int]."""
    if table_bin.ndim != 3 or table_bin.shape[1] != 3:
        raise ValueError(f"table_bin must be [K, 3, d_int], got {tuple(table_bin.shape)}")
    k, _, d_int = table_bin.shape
    folded = table_bin.detach().float().clone()
    keep = merge_sparse_bins(n_tokens_bin, n_min=n_min)
    kept_idx = torch.nonzero(keep, as_tuple=False).reshape(-1)
    for bin_id in range(k):
        if bool(keep[bin_id]):
            continue
        if float(folded[bin_id].abs().sum()) == 0:
            continue
        nearest = int(kept_idx[torch.argmin((kept_idx - bin_id).abs())].item())
        folded[nearest] = folded[nearest] + folded[bin_id]
        folded[bin_id] = 0
    out = torch.zeros(k, d_int, device=table_bin.device, dtype=torch.float32)
    for bin_id in range(k):
        if not bool(keep[bin_id]):
            continue
        row = folded[bin_id]
        if float(row.abs().sum()) == 0:
            continue
        out[bin_id] = fuse_modality_scores(row, lambdas=lambdas, zscore=zscore)
    return out


def aggregate_temporal(
    importance_bin: torch.Tensor,
    alpha: torch.Tensor | None = None,
    mode: str = "st",
    beta: float = DEFAULT_TEMPORAL_BETA,
    temporal_topk: int = DEFAULT_TEMPORAL_TOPK,
) -> torch.Tensor:
    """Reduce [K, d_int] bin scores to one neuron vector."""
    if mode not in TEMPORAL_MODES:
        raise ValueError(f"unknown temporal mode {mode!r}; expected one of {TEMPORAL_MODES}")
    if importance_bin.ndim != 2:
        raise ValueError(f"importance_bin must be [K, d_int], got {tuple(importance_bin.shape)}")
    k = int(importance_bin.shape[0])
    if mode == "none":
        raise ValueError("mode 'none' is handled by the summed projection table, not here")
    live = importance_bin.abs().sum(dim=1) > 0
    if not bool(live.any()):
        return importance_bin.sum(dim=0)
    stacked = importance_bin[live]
    if mode == "mean":
        if alpha is None:
            return stacked.mean(dim=0)
        w = alpha.detach().float().reshape(-1)[live]
        w = w / w.sum().clamp_min(1e-8)
        return (w.unsqueeze(-1) * stacked).sum(dim=0)
    if mode == "ta":
        mean = stacked.mean(dim=0)
        topk = min(max(int(temporal_topk), 1), int(stacked.shape[0]))
        peak = stacked.topk(topk, dim=0).values.mean(dim=0)
        return mean + float(beta) * peak
    if alpha is None:
        raise ValueError("ST-TENP needs alpha")
    w = alpha.detach().float().reshape(-1)
    if w.numel() != k:
        raise ValueError(f"alpha {tuple(w.shape)} != K={k}")
    w = torch.where(live, w, torch.zeros_like(w))
    w = w / w.sum().clamp_min(1e-8)
    return (w.unsqueeze(-1) * importance_bin).sum(dim=0)


def bin_coefficient_of_variation(importance_bin: torch.Tensor, eps: float = 1e-8) -> torch.Tensor:
    """Per-neuron CV across timestep bins. Empty bins are ignored."""
    live = importance_bin.abs().sum(dim=1) > 0
    stacked = importance_bin[live] if bool(live.any()) else importance_bin
    mu = stacked.mean(dim=0)
    sigma = stacked.std(dim=0, unbiased=False)
    return sigma / (mu.abs() + eps)


def topk_jaccard(importance_bin: torch.Tensor, frac: float = 0.20) -> torch.Tensor:
    """Pairwise Jaccard of per-bin top-(frac) neurons. Diagonal is 1."""
    k, d_int = importance_bin.shape
    n_keep = max(1, int(round(float(frac) * d_int)))
    n_keep = min(n_keep, d_int)
    sets: list[set[int]] = []
    live = importance_bin.abs().sum(dim=1) > 0
    for bin_id in range(k):
        if not bool(live[bin_id]):
            sets.append(set())
            continue
        _, idx = torch.topk(importance_bin[bin_id], n_keep)
        sets.append(set(int(x) for x in idx.tolist()))
    jac = torch.zeros(k, k, dtype=torch.float32)
    for i in range(k):
        for j in range(k):
            if not sets[i] or not sets[j]:
                jac[i, j] = 0.0 if i != j else 1.0
                continue
            inter = len(sets[i] & sets[j])
            union = len(sets[i] | sets[j])
            jac[i, j] = float(inter) / float(union) if union else 0.0
    return jac