GitLab Repo

amachine.am_transformers.am_basis_eval

  1import contextlib
  2import math
  3from dataclasses import dataclass
  4from typing import Any, cast
  5
  6import torch
  7
  8# --- Locating Decoder Blocks -----------------------------------------------
  9
 10from .am_module_resolver import (
 11    find_decoder_layers
 12)
 13
 14# --- Statistics & Math -----------------------------------------------------
 15
 16def _moments_inplace(x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
 17    """Per-row mean, variance, Pearson skewness, and Pearson kurtosis, in one pass.
 18
 19    Destructive to save VRAM: `x` is overwritten with centered values and then
 20    with their square, so only one extra same-sized buffer (`sq`) is ever
 21    allocated instead of one per moment.
 22    """
 23    mean = x.mean(dim=-1)
 24    x.sub_(mean.unsqueeze(-1))           # x now holds centered values
 25    sq = x.square()                      # sq = centered^2
 26    var = sq.mean(dim=-1).clamp_min(1e-12)
 27    skew = (sq * x).mean(dim=-1) / var.pow(1.5)   # sq * x = centered^3
 28    sq.square_()                         # reuse buffer: sq now = centered^4
 29    kurt = sq.mean(dim=-1) / var.square()
 30    return mean, var, skew, kurt
 31
 32def _hoyer_sparsity(x: torch.Tensor) -> torch.Tensor:
 33    """Per-row Hoyer sparseness on the raw (uncentered) values: 0 = dense/uniform
 34    magnitude across dims, 1 = maximally sparse (a single dominant dim).
 35
 36    Non-destructive and cheap (two reductions) since it must be measured before
 37    `_moments_inplace` overwrites the row with centered values.
 38    """
 39    d = x.shape[-1]
 40    l1 = x.abs().sum(dim=-1)
 41    l2 = x.square().sum(dim=-1).sqrt().clamp_min(1e-12)
 42    return (math.sqrt(d) - l1 / l2) / (math.sqrt(d) - 1)
 43
 44def _rank(x: torch.Tensor) -> torch.Tensor:
 45    """Assigns 0..n-1 ranks to the values in x. Ties are broken by index,
 46    which is fine here: this is only ever used on continuous-valued
 47    per-channel statistics, where exact ties are effectively never seen.
 48    """
 49    order = torch.argsort(x)
 50    ranks = torch.empty_like(order, dtype=torch.float64)
 51    ranks[order] = torch.arange(x.shape[0], dtype=torch.float64)
 52    return ranks
 53
 54def _spearman(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
 55    """Spearman rank correlation between two equal-length vectors."""
 56    ra_raw, rb_raw = _rank(a), _rank(b)
 57    ra, rb = ra_raw - ra_raw.mean(), rb_raw - rb_raw.mean()
 58    denom = (ra.square().sum().sqrt() * rb.square().sum().sqrt()).clamp_min(1e-12)
 59    return (ra * rb).sum() / denom
 60
 61def _haar_orthogonal(d: int, seed: int) -> torch.Tensor:
 62    """Generate a CPU float32 Haar-distributed orthogonal matrix."""
 63    generator = torch.Generator(device="cpu").manual_seed(seed % (2**63))
 64    q, r = torch.linalg.qr(torch.randn(d, d, generator=generator, dtype=torch.float32))
 65    signs = torch.where(torch.diagonal(r) >= 0, 1.0, -1.0)
 66    return q * signs
 67
 68# --- State Management ------------------------------------------------------
 69
 70@dataclass
 71class DeviceState:
 72    """Consolidates running metrics for a specific device."""
 73    native: torch.Tensor
 74    rotated: torch.Tensor
 75    count: torch.Tensor
 76    mean_native: torch.Tensor
 77    mean_rotated: torch.Tensor
 78    var_native: torch.Tensor
 79    var_rotated: torch.Tensor
 80    skew_native: torch.Tensor
 81    skew_rotated: torch.Tensor
 82    sparsity_native: torch.Tensor
 83    sparsity_rotated: torch.Tensor
 84    mask: torch.Tensor | None = None
 85    blocks: tuple[torch.Tensor, ...] | None = None
 86    sum_x: torch.Tensor | None = None
 87    sum_xx: torch.Tensor | None = None
 88    running_max: torch.Tensor | None = None
 89    running_min: torch.Tensor | None = None
 90    # Per-channel power sums (native basis only -- see PrivilegedBasisEvaluator
 91    # docstring on compute_channel_stats for why rotated channels aren't tracked)
 92    chan_sum_x: torch.Tensor | None = None
 93    chan_sum_x2: torch.Tensor | None = None
 94    chan_sum_x3: torch.Tensor | None = None
 95    chan_sum_x4: torch.Tensor | None = None
 96    # Same, split into two disjoint halves of the token stream (alternated per
 97    # observe() call) for the channel-persistence check
 98    chan_sum_x_a: torch.Tensor | None = None
 99    chan_sum_x2_a: torch.Tensor | None = None
100    chan_count_a: torch.Tensor | None = None
101    chan_sum_x_b: torch.Tensor | None = None
102    chan_sum_x2_b: torch.Tensor | None = None
103    chan_count_b: torch.Tensor | None = None
104
105# --- Evaluator -------------------------------------------------------------
106
107class PrivilegedBasisEvaluator(contextlib.AbstractContextManager):
108    _AUTO_ROTATION_BUDGET = 256
109    _MAX_CHUNK_ELEMENTS = 8_000_000
110    _MAX_ROTATION_BLOCK_ELEMENTS = 8_000_000
111
112    def __init__(
113        self,
114        model,
115        pad_id: int | None = None,
116        seed: int = 0,
117        num_rotations: int | None = None,
118        compute_effective_rank: bool = False,
119        compute_channel_stats: bool = False
120    ):
121        self.pad_id = pad_id
122        self.seed = int(seed)
123        self.num_rotations = num_rotations
124        self.compute_effective_rank = compute_effective_rank
125        # Optional and off by default: per-channel (across-token) statistics
126        # are a handful of extra reductions per chunk plus 10 small
127        # (n_layers x hidden_size or n_layers) accumulators -- cheap relative
128        # to the effective-rank covariance matrix, but not free, so this is
129        # opt-in rather than bundled into compute_effective_rank.
130        self.compute_channel_stats = compute_channel_stats
131
132        self.layers = find_decoder_layers(model)
133        self.n_layers = len(self.layers)
134
135        # Public CPU snapshots updated by finalize()
136        self.sum_kurt = torch.zeros(self.n_layers, dtype=torch.float64)
137        self.sum_kurt_rot = torch.zeros(self.n_layers, dtype=torch.float64)
138        self.sum_mean = torch.zeros(self.n_layers, dtype=torch.float64)
139        self.sum_mean_rot = torch.zeros(self.n_layers, dtype=torch.float64)
140        self.sum_var = torch.zeros(self.n_layers, dtype=torch.float64)
141        self.sum_var_rot = torch.zeros(self.n_layers, dtype=torch.float64)
142        self.sum_skew = torch.zeros(self.n_layers, dtype=torch.float64)
143        self.sum_skew_rot = torch.zeros(self.n_layers, dtype=torch.float64)
144        self.sum_sparsity = torch.zeros(self.n_layers, dtype=torch.float64)
145        self.sum_sparsity_rot = torch.zeros(self.n_layers, dtype=torch.float64)
146        self.token_count = torch.zeros(self.n_layers, dtype=torch.float64)
147        # Raw per-channel moment vectors (n_layers x hidden_size), populated by
148        # finalize() only if compute_channel_stats=True. Intended for on-demand
149        # visual inspection (see plot_channel_concentration) rather than as
150        # flat per-layer scalars like the rest of the results dict.
151        self.channel_moments: dict[str, torch.Tensor] | None = None
152        # self.outlier_dims: torch.Tensor | None = None
153
154        self._handles: list = []
155        self._token_mask_cpu: torch.Tensor | None = None
156        self._states: dict[torch.device, DeviceState] = {}
157        self._effective_rotations: int | None = None
158        self._mask_all: bool = False
159        # Alternates every observe() call, splitting the token stream into two
160        # disjoint halves for the channel-persistence check.
161        self._split_toggle: bool = False
162        self._current_split: bool = False
163
164    def __enter__(self):
165        if self._handles:
166            raise RuntimeError("Context already entered.")
167        self._handles = [
168            layer.register_forward_hook(self._make_hook(i))
169            for i, layer in enumerate(self.layers)
170        ]
171        return self
172
173    def __exit__(self, *exc):
174        for handle in self._handles:
175            handle.remove()
176        self._handles.clear()
177        self._token_mask_cpu = None
178        self._mask_all = False
179        # Only release the transient, memory-heavy per-pass caches (rotation
180        # blocks, device mask). The running accumulators (native, rotated,
181        # count, outliers) must survive __exit__ since finalize() is called
182        # by the caller *after* the `with` block ends.
183        for state in self._states.values():
184            state.mask = None
185            state.blocks = None
186        return False
187
188    def observe(self, input_ids=None, attention_mask=None) -> None:
189        """Set the valid-token mask for the next forward pass."""
190        if attention_mask is not None:
191            self._token_mask_cpu = attention_mask.detach().reshape(-1).bool().cpu()
192        elif input_ids is not None and self.pad_id is None:
193            raise ValueError("pad_id required if attention_mask is omitted.")
194        elif input_ids is not None:
195            self._token_mask_cpu = (input_ids.detach() != self.pad_id).reshape(-1).cpu()
196        else:
197            raise ValueError("Provide attention_mask or input_ids.")
198        
199        # Clear lazily loaded per-device masks
200        for state in self._states.values():
201            state.mask = None
202
203        # Alternate which half of the token stream this forward pass counts
204        # toward, for the channel-persistence check (see compute_channel_stats)
205        self._current_split = self._split_toggle
206        self._split_toggle = not self._split_toggle
207
208        # Check if we can run the unmasked fast-path
209
210        assert self._token_mask_cpu is not None
211        self._mask_all = bool(self._token_mask_cpu.all())
212
213    def _get_device_state(self, device: torch.device, hidden_size: int) -> DeviceState:
214        """Retrieve or initialize consolidated state for a device."""
215        if device not in self._states:
216            
217            sum_x = torch.zeros ( 
218                (self.n_layers, hidden_size), 
219                device=device, dtype=torch.float64
220            ) if self.compute_effective_rank else None
221
222            sum_xx = torch.zeros(
223                ( self.n_layers, hidden_size, hidden_size), 
224                device=device, dtype=torch.float64
225            ) if self.compute_effective_rank else None
226
227            def _chan_vec():
228                return torch.zeros((self.n_layers, hidden_size), device=device, dtype=torch.float64) \
229                    if self.compute_channel_stats else None
230
231            def _chan_count():
232                return torch.zeros(self.n_layers, device=device, dtype=torch.float64) \
233                    if self.compute_channel_stats else None
234
235            self._states[device] = DeviceState(
236                native=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
237                rotated=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
238                count=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
239                mean_native=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
240                mean_rotated=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
241                var_native=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
242                var_rotated=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
243                skew_native=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
244                skew_rotated=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
245                sparsity_native=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
246                sparsity_rotated=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
247                # outliers=torch.zeros(self.n_layers, hidden_size, device=device, dtype=torch.bool),
248                sum_x=sum_x,
249                sum_xx=sum_xx,
250                chan_sum_x=_chan_vec(),
251                chan_sum_x2=_chan_vec(),
252                chan_sum_x3=_chan_vec(),
253                chan_sum_x4=_chan_vec(),
254                chan_sum_x_a=_chan_vec(),
255                chan_sum_x2_a=_chan_vec(),
256                chan_count_a=_chan_count(),
257                chan_sum_x_b=_chan_vec(),
258                chan_sum_x2_b=_chan_vec(),
259                chan_count_b=_chan_count(),
260                running_max=torch.full(
261                    (self.n_layers, hidden_size), 
262                    float('-inf'), 
263                    device=device, 
264                    dtype=torch.float32
265                ),
266                running_min=torch.full(
267                    (self.n_layers, hidden_size), 
268                    float('inf'), 
269                    device=device, 
270                    dtype=torch.float32
271                )
272            )
273            
274            # Initialize batched rotations once per device
275            count = self.num_rotations or max(1, math.ceil(self._AUTO_ROTATION_BUDGET / hidden_size))
276            self._effective_rotations = count
277            
278            # Pack rotations into blocks to optimize GEMMs
279            by_matrix_budget = max(1, self._MAX_ROTATION_BLOCK_ELEMENTS // (hidden_size * hidden_size))
280            block_size = max(1, min(count, by_matrix_budget))
281            
282            blocks = []
283            for start in range(0, count, block_size):
284                block_rots = min(block_size, count - start)
285                mats = []
286                for idx in range(start, start + block_rots):
287                    seed = (self.seed + idx * 0x9E3779B97F4A7C15) % (2**63)
288                    mats.append(_haar_orthogonal(hidden_size, seed))
289                
290                packed_cpu = torch.cat(mats, dim=1).contiguous()
291                blocks.append(packed_cpu.to(device=device, dtype=torch.float32, non_blocking=True))
292            
293            self._states[device].blocks = tuple(blocks)
294
295        return self._states[device]
296
297    def _make_hook(self, layer_idx: int):
298
299        def hook(_module, _inputs, output):
300            
301            if self._token_mask_cpu is None:
302                return
303
304            hidden = output[0] if isinstance(output, (tuple, list)) else output
305            
306            # Safety check against unexpected output structures
307            if not isinstance(hidden, torch.Tensor) or getattr(hidden, "ndim", 0) < 1:
308                return
309
310            device = hidden.device
311            hidden_size = hidden.shape[-1]
312
313            amp_ctx = (
314                torch.autocast(device_type=device.type, enabled=False)
315                if device.type in ("cuda", "cpu")
316                else contextlib.nullcontext()
317            )
318
319            with torch.inference_mode(), amp_ctx:
320    
321                flat  = hidden.detach().reshape(-1, hidden_size)
322                state = self._get_device_state(device, hidden_size)
323
324                total_rows = flat.shape[0]
325
326                if self._token_mask_cpu.numel() != total_rows:
327                    raise ValueError(f"Mask mismatch: {self._token_mask_cpu.numel()} vs {total_rows}")
328
329                # Prove to static analyzer that variables initialized in _get_device_state are not None
330                assert state.blocks is not None
331                assert self._effective_rotations is not None
332
333                # Calculate memory-safe chunk sizes considering the batched rotations
334                max_block_rots = max(b.shape[1] // hidden_size for b in state.blocks)
335                denominator = hidden_size * (1.0 + max_block_rots) + 3.0 * max_block_rots
336                chunk_tokens = min(total_rows, max(1, int(self._MAX_CHUNK_ELEMENTS // max(1.0, denominator))))
337                
338                # Copy the mask lazily only if there are tokens to mask
339                mask = None
340                
341                if not self._mask_all:
342                    if state.mask is None:
343                        state.mask = self._token_mask_cpu.to(device, non_blocking=True)
344                    
345                    mask = state.mask
346
347                for start in range(0, total_rows, chunk_tokens):
348
349                    end   = min(total_rows, start + chunk_tokens)
350                    chunk = flat[start:end].to(dtype=torch.float32, copy=True)
351                    
352                    m_chunk: torch.Tensor | None = mask[start:end] if mask is not None else None
353                    
354                    # Rotated Moments (Batched): mean, variance, skewness, kurtosis, sparsity
355                    rotated_mean_total = torch.zeros((), device=device, dtype=torch.float64)
356                    rotated_var_total = torch.zeros((), device=device, dtype=torch.float64)
357                    rotated_skew_total = torch.zeros((), device=device, dtype=torch.float64)
358                    rotated_kurt_total = torch.zeros((), device=device, dtype=torch.float64)
359                    rotated_sparsity_total = torch.zeros((), device=device, dtype=torch.float64)
360
361                    for block in state.blocks:
362                        
363                        block_rots = block.shape[1] // hidden_size
364                        rotated = (chunk @ block).view(chunk.shape[0], block_rots, hidden_size)
365                        
366                        # Sparsity reads raw values, so it must run before the
367                        # destructive moments call overwrites `rotated` below.
368                        sparsity_rot = _hoyer_sparsity(rotated)
369                        mean_rot, var_rot, skew_rot, kurt_rot = _moments_inplace(rotated)
370                        if m_chunk is not None:
371
372                            invalid = (~m_chunk).view(-1, 1).expand_as(kurt_rot)
373                            mean_rot.masked_fill_(invalid, 0)
374                            var_rot.masked_fill_(invalid, 0)
375                            skew_rot.masked_fill_(invalid, 0)
376                            kurt_rot.masked_fill_(invalid, 0)
377                            sparsity_rot.masked_fill_(invalid, 0)
378                        
379                        rotated_mean_total.add_(mean_rot.sum(dtype=torch.float64))
380                        rotated_var_total.add_(var_rot.sum(dtype=torch.float64))
381                        rotated_skew_total.add_(skew_rot.sum(dtype=torch.float64))
382                        rotated_kurt_total.add_(kurt_rot.sum(dtype=torch.float64))
383                        rotated_sparsity_total.add_(sparsity_rot.sum(dtype=torch.float64))
384
385                    # Row bookkeeping and valid-token extraction, done on the
386                    # pristine chunk *before* native moments are computed
387                    # in-place below (which overwrites chunk's contents).
388                    if m_chunk is None:
389                        state.count[layer_idx].add_(chunk.shape[0])
390                        valid_chunk = chunk
391                    else:
392                        state.count[layer_idx].add_(m_chunk.sum(dtype=torch.float64))
393                        valid_chunk = chunk[m_chunk] # Extract only valid tokens (copy)
394
395                    valid_chunk_64 = None
396
397                    # Accumulate for Covariance
398                    if self.compute_effective_rank and valid_chunk.shape[0] > 0:
399
400                        assert state.sum_x is not None
401                        assert state.sum_xx is not None
402
403                        # Cast to float64 to prevent numerical overflow across millions of tokens
404                        # (also copies, so this is safe from the in-place native moments below
405                        # even when valid_chunk aliases chunk directly, i.e. the unmasked case)
406                        valid_chunk_64 = valid_chunk.to(torch.float64)
407                        state.sum_x[layer_idx].add_(valid_chunk_64.sum(dim=0))
408
409                        # Efficiently compute sum of outer products: X^T @ X
410                        state.sum_xx[layer_idx].add_(valid_chunk_64.t() @ valid_chunk_64)
411
412                        assert state.running_min is not None
413                        assert state.running_max is not None
414
415                        state.running_max[layer_idx] = torch.maximum(state.running_max[layer_idx], valid_chunk.max(dim=0)[0])
416
417                        state.running_min[layer_idx] = torch.minimum(state.running_min[layer_idx], valid_chunk.min(dim=0)[0])
418
419                    # Per-channel power sums (native basis only: channel identity
420                    # isn't preserved under a random rotation, so there's nothing
421                    # meaningful to accumulate for a "rotated channel").
422                    if self.compute_channel_stats and valid_chunk.shape[0] > 0:
423
424                        assert state.chan_sum_x is not None and state.chan_sum_x2 is not None
425                        assert state.chan_sum_x3 is not None and state.chan_sum_x4 is not None
426                        assert state.chan_sum_x_a is not None and state.chan_sum_x2_a is not None
427                        assert state.chan_sum_x_b is not None and state.chan_sum_x2_b is not None
428                        assert state.chan_count_a is not None and state.chan_count_b is not None
429
430                        # Reuse the float64 cast from the covariance block above
431                        # when available; otherwise compute it fresh here.
432
433                        if valid_chunk_64 is None :
434                            valid_chunk_64 = valid_chunk.to(torch.float64)
435
436                        valid_chunk_64_ch = valid_chunk_64 if self.compute_effective_rank else valid_chunk.to(torch.float64)
437                        sq64 = valid_chunk_64_ch.square()
438
439                        state.chan_sum_x[layer_idx].add_(valid_chunk_64_ch.sum(dim=0))
440                        state.chan_sum_x2[layer_idx].add_(sq64.sum(dim=0))
441                        state.chan_sum_x3[layer_idx].add_((sq64 * valid_chunk_64_ch).sum(dim=0))
442                        state.chan_sum_x4[layer_idx].add_(sq64.square().sum(dim=0))
443
444                        if self._current_split:
445                            state.chan_sum_x_a[layer_idx].add_(valid_chunk_64_ch.sum(dim=0))
446                            state.chan_sum_x2_a[layer_idx].add_(sq64.sum(dim=0))
447                            state.chan_count_a[layer_idx].add_(valid_chunk.shape[0])
448                        else:
449                            state.chan_sum_x_b[layer_idx].add_(valid_chunk_64_ch.sum(dim=0))
450                            state.chan_sum_x2_b[layer_idx].add_(sq64.sum(dim=0))
451                            state.chan_count_b[layer_idx].add_(valid_chunk.shape[0])
452
453                    # Native Moments (sparsity + moments both need the pristine
454                    # chunk; sparsity is non-destructive but must still be read
455                    # before the in-place moments call below overwrites it)
456                    sparsity_native = _hoyer_sparsity(chunk)
457                    mean_native, var_native, skew_native, kurt_native = _moments_inplace(chunk)
458                    if m_chunk is not None:
459                        mean_native.masked_fill_(~m_chunk, 0)
460                        var_native.masked_fill_(~m_chunk, 0)
461                        skew_native.masked_fill_(~m_chunk, 0)
462                        kurt_native.masked_fill_(~m_chunk, 0)
463                        sparsity_native.masked_fill_(~m_chunk, 0)
464
465                    # Update States
466                    state.native[layer_idx].add_(kurt_native.sum(dtype=torch.float64))
467                    state.rotated[layer_idx].add_(rotated_kurt_total / self._effective_rotations)
468
469                    state.mean_native[layer_idx].add_(mean_native.sum(dtype=torch.float64))
470                    state.mean_rotated[layer_idx].add_(rotated_mean_total / self._effective_rotations)
471
472                    state.var_native[layer_idx].add_(var_native.sum(dtype=torch.float64))
473                    state.var_rotated[layer_idx].add_(rotated_var_total / self._effective_rotations)
474
475                    state.skew_native[layer_idx].add_(skew_native.sum(dtype=torch.float64))
476                    state.skew_rotated[layer_idx].add_(rotated_skew_total / self._effective_rotations)
477
478                    state.sparsity_native[layer_idx].add_(sparsity_native.sum(dtype=torch.float64))
479                    state.sparsity_rotated[layer_idx].add_(rotated_sparsity_total / self._effective_rotations)
480
481        return hook
482
483    def finalize(self) -> dict[str, Any]:
484        """Aggregate metrics across all devices to the CPU and compute statistics."""
485        
486        if not self._states:
487            return {}
488
489        sum_kurt = torch.zeros(self.n_layers, dtype=torch.float64)
490        sum_kurt_rot = torch.zeros(self.n_layers, dtype=torch.float64)
491        sum_mean = torch.zeros(self.n_layers, dtype=torch.float64)
492        sum_mean_rot = torch.zeros(self.n_layers, dtype=torch.float64)
493        sum_var = torch.zeros(self.n_layers, dtype=torch.float64)
494        sum_var_rot = torch.zeros(self.n_layers, dtype=torch.float64)
495        sum_skew = torch.zeros(self.n_layers, dtype=torch.float64)
496        sum_skew_rot = torch.zeros(self.n_layers, dtype=torch.float64)
497        sum_sparsity = torch.zeros(self.n_layers, dtype=torch.float64)
498        sum_sparsity_rot = torch.zeros(self.n_layers, dtype=torch.float64)
499        token_count = torch.zeros(self.n_layers, dtype=torch.float64)
500
501        for state in self._states.values():
502            sum_kurt += state.native.cpu().to(dtype=torch.float64)
503            sum_kurt_rot += state.rotated.cpu().to(dtype=torch.float64)
504            sum_mean += state.mean_native.cpu().to(dtype=torch.float64)
505            sum_mean_rot += state.mean_rotated.cpu().to(dtype=torch.float64)
506            sum_var += state.var_native.cpu().to(dtype=torch.float64)
507            sum_var_rot += state.var_rotated.cpu().to(dtype=torch.float64)
508            sum_skew += state.skew_native.cpu().to(dtype=torch.float64)
509            sum_skew_rot += state.skew_rotated.cpu().to(dtype=torch.float64)
510            sum_sparsity += state.sparsity_native.cpu().to(dtype=torch.float64)
511            sum_sparsity_rot += state.sparsity_rotated.cpu().to(dtype=torch.float64)
512            token_count += state.count.cpu().to(dtype=torch.float64)
513
514        self.sum_kurt = sum_kurt
515        self.sum_kurt_rot = sum_kurt_rot
516        self.sum_mean = sum_mean
517        self.sum_mean_rot = sum_mean_rot
518        self.sum_var = sum_var
519        self.sum_var_rot = sum_var_rot
520        self.sum_skew = sum_skew
521        self.sum_skew_rot = sum_skew_rot
522        self.sum_sparsity = sum_sparsity
523        self.sum_sparsity_rot = sum_sparsity_rot
524        self.token_count = token_count
525
526        seen = token_count > 0
527        if not seen.any().item():
528            return {}
529
530        mean_kurt = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
531        mean_kurt_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
532        mean_mean = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
533        mean_mean_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
534        mean_var = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
535        mean_var_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
536        mean_skew = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
537        mean_skew_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
538        mean_sparsity = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
539        mean_sparsity_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
540
541        mean_kurt[seen] = sum_kurt[seen] / token_count[seen]
542        mean_kurt_rot[seen] = sum_kurt_rot[seen] / token_count[seen]
543        mean_mean[seen] = sum_mean[seen] / token_count[seen]
544        mean_mean_rot[seen] = sum_mean_rot[seen] / token_count[seen]
545        mean_var[seen] = sum_var[seen] / token_count[seen]
546        mean_var_rot[seen] = sum_var_rot[seen] / token_count[seen]
547        mean_skew[seen] = sum_skew[seen] / token_count[seen]
548        mean_skew_rot[seen] = sum_skew_rot[seen] / token_count[seen]
549        mean_sparsity[seen] = sum_sparsity[seen] / token_count[seen]
550        mean_sparsity_rot[seen] = sum_sparsity_rot[seen] / token_count[seen]
551
552        excess_per_layer = mean_kurt - mean_kurt_rot
553
554        ratio_per_layer = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
555        ratio_per_layer[seen] = mean_kurt[seen] / mean_kurt_rot[seen].clamp_min(1e-12)
556
557        seen_indices = torch.where(seen)[0]
558        local_max_index = excess_per_layer[seen].argmax()
559        
560        # Explicit int cast for tensor indices to satisfy static analyzer
561        max_excess_layer = int(seen_indices[local_max_index].item())
562
563        max_kurt = mean_kurt[seen].max().item()
564        rotated_baseline = mean_kurt_rot[seen].mean().item()
565        max_excess = excess_per_layer[max_excess_layer].item()
566        max_excess_ratio = ratio_per_layer[max_excess_layer].item()
567
568        if max_excess >= 1.0 and max_excess_ratio >= 2.0:
569            verdict = "strong privileged-basis signal"
570        elif max_excess >= 0.25 and max_excess_ratio >= 1.2:
571            verdict = "moderate privileged-basis signal"
572        else:
573            verdict = "no clear privileged-basis signal"
574
575        results = {
576            "pb_mean_per_layer": mean_mean.tolist(),
577            "pb_mean_per_layer_rotated": mean_mean_rot.tolist(),
578            "pb_variance_per_layer": mean_var.tolist(),
579            "pb_variance_per_layer_rotated": mean_var_rot.tolist(),
580            "pb_std_per_layer": mean_var.sqrt().tolist(),
581            "pb_std_per_layer_rotated": mean_var_rot.sqrt().tolist(),
582            "pb_skewness_per_layer": mean_skew.tolist(),
583            "pb_skewness_per_layer_rotated": mean_skew_rot.tolist(),
584            "pb_kurtosis_per_layer": mean_kurt.tolist(),
585            "pb_kurtosis_per_layer_rotated": mean_kurt_rot.tolist(),
586            "pb_sparsity_per_layer": mean_sparsity.tolist(),
587            "pb_sparsity_per_layer_rotated": mean_sparsity_rot.tolist(),
588            "pb_excess_kurtosis_per_layer": excess_per_layer.tolist(),
589            "pb_kurtosis_ratio_per_layer": ratio_per_layer.tolist(),
590            "pb_num_rotations": self._effective_rotations,
591            "pb_max_kurtosis": max_kurt,
592            "pb_rotated_kurtosis_baseline": rotated_baseline,
593            "pb_max_excess_kurtosis": max_excess,
594            "pb_max_excess_ratio": max_excess_ratio,
595            "pb_max_excess_layer": max_excess_layer,
596            "pb_verdict": verdict
597        }
598
599        # Effective Rank
600        if self.compute_effective_rank:
601
602            first_state = next(iter(self._states.values()))
603
604            assert first_state.sum_x is not None
605            assert first_state.sum_xx is not None
606
607            hidden_size = first_state.sum_x.shape[-1]
608            
609            sum_x_cpu  : torch.Tensor = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
610            sum_xx_cpu : torch.Tensor = torch.zeros((self.n_layers, hidden_size, hidden_size), dtype=torch.float64)
611
612            for state in self._states.values() :
613
614                assert state.sum_x is not None
615                assert state.sum_xx is not None
616
617                sum_x_cpu  += state.sum_x.cpu()
618                sum_xx_cpu += state.sum_xx.cpu()
619
620            effective_ranks = []
621            axis_alignment = []
622            for l in range(self.n_layers):
623                
624                if token_count[l] == 0:
625                    effective_ranks.append(float("nan"))
626                    axis_alignment.append(float("nan"))
627                    continue
628                    
629                N = token_count[l].item()
630                mean_x = sum_x_cpu[l] / N
631                
632                # Covariance matrix
633                cov = (sum_xx_cpu[l] / N) - torch.outer(mean_x, mean_x)
634                
635                # Symmetric matrix: eigh gives eigenvalues *and* eigenvectors,
636                # ascending order. The eigenvectors are what let us also check
637                # axis-alignment below, at no extra decomposition cost.
638                eigvals, eigvecs = torch.linalg.eigh(cov)
639                
640                # Relative floor: absolute clamps (e.g. a flat 1e-12) are wrong
641                # once you compare layers whose activation scale differs a lot
642                # (residual-stream norm grows with depth) — a fixed floor is
643                # either a no-op on large-scale layers or an inflating one on
644                # small-scale layers. Floor relative to this layer's own top
645                # eigenvalue instead, with a tiny absolute fallback only to
646                # guard against an all-zero covariance.
647                floor = torch.clamp_min(eigvals.max().clamp_min(0) * 1e-8, 1e-12)
648                eigvals = torch.clamp_min(eigvals, floor)
649                
650                # Normalize to probability distribution
651                p = eigvals / eigvals.sum()
652                
653                # Compute Shannon entropy
654                entropy = -(p * torch.log(p)).sum()
655                eff_rank = torch.exp(entropy).item()
656                
657                effective_ranks.append(eff_rank)
658
659                # Axis alignment: unlike the eigenvalues themselves, the
660                # eigenvectors *do* change under rotation of the underlying
661                # basis, so this is a direct (and much cheaper, since the
662                # decomposition is already in hand) complement to the
663                # kurtosis-based privileged-basis test above. For each
664                # eigenvector, the participation ratio of its own components
665                # (1 / sum(v_i^4), since ||v||=1) is 1 when it points along a
666                # single standard axis and hidden_size when it's spread
667                # evenly across all of them. We average this over eigenvectors,
668                # weighted by their eigenvalue share `p`, so directions that
669                # carry more of the variance dominate the score.
670                eigvec_ipr = 1.0 / eigvecs.pow(4).sum(dim=0).clamp_min(1e-12)
671                axis_alignment.append((p * eigvec_ipr).sum().item())
672
673            results["pb_effective_rank_per_layer"] = effective_ranks
674            results["pb_axis_alignment_per_layer"] = axis_alignment
675
676        # Per-channel statistics (native basis only -- see compute_channel_stats)
677        if self.compute_channel_stats:
678
679            first_state = next(iter(self._states.values()))
680            assert first_state.chan_sum_x is not None
681            hidden_size = first_state.chan_sum_x.shape[-1]
682
683            chan_sum_x  = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
684            chan_sum_x2 = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
685            chan_sum_x3 = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
686            chan_sum_x4 = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
687            chan_sum_x_a  = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
688            chan_sum_x2_a = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
689            chan_sum_x_b  = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
690            chan_sum_x2_b = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
691            chan_count_a = torch.zeros(self.n_layers, dtype=torch.float64)
692            chan_count_b = torch.zeros(self.n_layers, dtype=torch.float64)
693
694            for state in self._states.values():
695
696                assert state.chan_sum_x is not None and state.chan_sum_x2 is not None
697                assert state.chan_sum_x3 is not None and state.chan_sum_x4 is not None
698                assert state.chan_sum_x_a is not None and state.chan_sum_x2_a is not None
699                assert state.chan_sum_x_b is not None and state.chan_sum_x2_b is not None
700                assert state.chan_count_a is not None and state.chan_count_b is not None
701
702                chan_sum_x    += state.chan_sum_x.cpu()
703                chan_sum_x2   += state.chan_sum_x2.cpu()
704                chan_sum_x3   += state.chan_sum_x3.cpu()
705                chan_sum_x4   += state.chan_sum_x4.cpu()
706                chan_sum_x_a  += state.chan_sum_x_a.cpu()
707                chan_sum_x2_a += state.chan_sum_x2_a.cpu()
708                chan_sum_x_b  += state.chan_sum_x_b.cpu()
709                chan_sum_x2_b += state.chan_sum_x2_b.cpu()
710                chan_count_a  += state.chan_count_a.cpu()
711                chan_count_b  += state.chan_count_b.cpu()
712
713            channel_mean = torch.full((self.n_layers, hidden_size), float("nan"), dtype=torch.float64)
714            channel_var  = torch.full((self.n_layers, hidden_size), float("nan"), dtype=torch.float64)
715            channel_skew = torch.full((self.n_layers, hidden_size), float("nan"), dtype=torch.float64)
716            channel_kurt = torch.full((self.n_layers, hidden_size), float("nan"), dtype=torch.float64)
717
718            eff_channel_count = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
719            channel_gini = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
720            channel_powerlaw_exponent = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
721            channel_persistence = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
722
723            n = hidden_size
724            log_rank_desc = torch.log(torch.arange(1, n + 1, dtype=torch.float64))
725            log_rank_desc_centered = log_rank_desc - log_rank_desc.mean()
726            rank_1_to_n = torch.arange(1, n + 1, dtype=torch.float64)
727
728            for l in range(self.n_layers):
729
730                N = token_count[l].item()
731                if N == 0:
732                    continue
733
734                mean_l = chan_sum_x[l] / N
735                e_x2 = chan_sum_x2[l] / N
736                e_x3 = chan_sum_x3[l] / N
737                e_x4 = chan_sum_x4[l] / N
738
739                # Raw -> central moment conversion (single pass at finalize
740                # time, so the usual cancellation risk of this formula is a
741                # non-issue here -- it's not being iterated per chunk).
742                m2 = (e_x2 - mean_l.square()).clamp_min(1e-12)
743                m3 = e_x3 - 3 * mean_l * e_x2 + 2 * mean_l.pow(3)
744                m4 = e_x4 - 4 * mean_l * e_x3 + 6 * mean_l.square() * e_x2 - 3 * mean_l.pow(4)
745
746                channel_mean[l] = mean_l
747                channel_var[l] = m2
748                channel_skew[l] = m3 / m2.pow(1.5)
749                channel_kurt[l] = m4 / m2.square()
750
751                var_l = m2.clamp_min(0)
752                s1, s2 = var_l.sum(), var_l.square().sum()
753
754                # Effective channel count: participation ratio of per-channel
755                # variance. No privileged basis -> variance spread ~uniformly
756                # across channels -> this saturates near hidden_size;
757                # concentration in a few channels pulls it down. Threshold-free
758                # counterpart to "how many outlier channels are there".
759                if s2 > 0:
760                    eff_channel_count[l] = (s1.square() / s2).item()
761
762                # Gini coefficient of per-channel variance (0 = perfectly even
763                # across channels, ~1 = all variance in one channel).
764                sorted_var, _ = torch.sort(var_l)
765                total = sorted_var.sum().clamp_min(1e-24)
766                channel_gini[l] = ((2 * (rank_1_to_n * sorted_var).sum()) / (n * total) - (n + 1) / n).item()
767
768                # Power-law decay exponent of the sorted (descending) variance
769                # curve: fit slope of log(variance) vs log(rank). A shape
770                # descriptor, not a cutoff -- two layers can share a Gini value
771                # while one is a clean power law and the other a step function.
772                desc_var, _ = torch.sort(var_l, descending=True)
773                floor = torch.clamp_min(desc_var.max() * 1e-8, 1e-24)
774                log_v = torch.log(desc_var.clamp_min(floor))
775                log_v_centered = log_v - log_v.mean()
776                slope = (log_rank_desc_centered * log_v_centered).sum() / log_rank_desc_centered.square().sum()
777                channel_powerlaw_exponent[l] = (-slope).item()
778
779                # Persistence: Spearman rank correlation of per-channel variance
780                # between two disjoint halves of the token stream. High ->
781                # same channels dominate dataset-wide (a genuinely fixed
782                # privileged set); low -> the "privileged" channels drift
783                # between batches and the concentration seen above may just be
784                # sampling noise rather than a stable structural feature.
785                na, nb = chan_count_a[l].item(), chan_count_b[l].item()
786                if na > 0 and nb > 0:
787                    mean_a = chan_sum_x_a[l] / na
788                    var_a = (chan_sum_x2_a[l] / na - mean_a.square()).clamp_min(0)
789                    mean_b = chan_sum_x_b[l] / nb
790                    var_b = (chan_sum_x2_b[l] / nb - mean_b.square()).clamp_min(0)
791                    channel_persistence[l] = _spearman(var_a, var_b).item()
792
793            results["pb_effective_channel_count_per_layer"] = eff_channel_count.tolist()
794            results["pb_channel_gini_per_layer"] = channel_gini.tolist()
795            results["pb_channel_powerlaw_exponent_per_layer"] = channel_powerlaw_exponent.tolist()
796            results["pb_channel_persistence_per_layer"] = channel_persistence.tolist()
797
798            # Raw per-channel moment vectors, for on-demand visual inspection
799            # (see plot_channel_concentration) -- not meant to be read as flat
800            # per-layer scalars like the rest of `results`.
801            self.channel_moments = {
802                "mean": channel_mean,
803                "variance": channel_var,
804                "skewness": channel_skew,
805                "kurtosis": channel_kurt,
806            }
807
808        return results
809
810# --- On-demand visual inspection --------------------------------------------
811
812def plot_channel_concentration(channel_variance: torch.Tensor, label: str = "layer", ax=None):
813    """Sorted-magnitude curve of per-channel variance for a single layer
814    (e.g. `evaluator.channel_moments["variance"][layer_idx]`), on log-log axes.
815
816    This is the reality check for the channel-stats summary scalars above: a
817    clean straight line means the power-law exponent is actually describing
818    the shape (a genuine power law); a flat-then-cliff curve means a small,
819    sharply-separated outlier set (matches the effective-channel-count/Gini
820    story cleanly); anything else -- e.g. a single wildly dominant point, or a
821    long plateau with no clear knee -- is a sign the summary scalars for that
822    layer might be misleading (or at least need a specific story), and is
823    worth a look before trusting them. Cheap enough to call routinely on
824    layers that look unremarkable, not just ones that look suspicious.
825
826    Not wired into the evaluator: call it yourself, on whichever layer(s) or
827    checkpoints you want to inspect. Pass the same `ax` across calls to
828    overlay multiple curves (e.g. across layers, or the same layer across
829    training checkpoints) for comparison.
830    """
831    import matplotlib.pyplot as plt
832
833    if ax is None:
834        _, ax = plt.subplots(figsize=(6, 4))
835
836    sorted_desc, _ = torch.sort(channel_variance.detach().float().clamp_min(1e-12), descending=True)
837    rank = torch.arange(1, sorted_desc.shape[0] + 1)
838
839    ax.plot(rank.numpy(), sorted_desc.cpu().numpy(), marker="o", markersize=2, linewidth=1, label=label)
840    ax.set_xscale("log")
841    ax.set_yscale("log")
842    ax.set_xlabel("channel rank (descending variance)")
843    ax.set_ylabel("variance")
844    ax.legend()
845    return ax
@dataclass
class DeviceState:
 71@dataclass
 72class DeviceState:
 73    """Consolidates running metrics for a specific device."""
 74    native: torch.Tensor
 75    rotated: torch.Tensor
 76    count: torch.Tensor
 77    mean_native: torch.Tensor
 78    mean_rotated: torch.Tensor
 79    var_native: torch.Tensor
 80    var_rotated: torch.Tensor
 81    skew_native: torch.Tensor
 82    skew_rotated: torch.Tensor
 83    sparsity_native: torch.Tensor
 84    sparsity_rotated: torch.Tensor
 85    mask: torch.Tensor | None = None
 86    blocks: tuple[torch.Tensor, ...] | None = None
 87    sum_x: torch.Tensor | None = None
 88    sum_xx: torch.Tensor | None = None
 89    running_max: torch.Tensor | None = None
 90    running_min: torch.Tensor | None = None
 91    # Per-channel power sums (native basis only -- see PrivilegedBasisEvaluator
 92    # docstring on compute_channel_stats for why rotated channels aren't tracked)
 93    chan_sum_x: torch.Tensor | None = None
 94    chan_sum_x2: torch.Tensor | None = None
 95    chan_sum_x3: torch.Tensor | None = None
 96    chan_sum_x4: torch.Tensor | None = None
 97    # Same, split into two disjoint halves of the token stream (alternated per
 98    # observe() call) for the channel-persistence check
 99    chan_sum_x_a: torch.Tensor | None = None
100    chan_sum_x2_a: torch.Tensor | None = None
101    chan_count_a: torch.Tensor | None = None
102    chan_sum_x_b: torch.Tensor | None = None
103    chan_sum_x2_b: torch.Tensor | None = None
104    chan_count_b: torch.Tensor | None = None

Consolidates running metrics for a specific device.

DeviceState( native: torch.Tensor, rotated: torch.Tensor, count: torch.Tensor, mean_native: torch.Tensor, mean_rotated: torch.Tensor, var_native: torch.Tensor, var_rotated: torch.Tensor, skew_native: torch.Tensor, skew_rotated: torch.Tensor, sparsity_native: torch.Tensor, sparsity_rotated: torch.Tensor, mask: torch.Tensor | None = None, blocks: tuple[torch.Tensor, ...] | None = None, sum_x: torch.Tensor | None = None, sum_xx: torch.Tensor | None = None, running_max: torch.Tensor | None = None, running_min: torch.Tensor | None = None, chan_sum_x: torch.Tensor | None = None, chan_sum_x2: torch.Tensor | None = None, chan_sum_x3: torch.Tensor | None = None, chan_sum_x4: torch.Tensor | None = None, chan_sum_x_a: torch.Tensor | None = None, chan_sum_x2_a: torch.Tensor | None = None, chan_count_a: torch.Tensor | None = None, chan_sum_x_b: torch.Tensor | None = None, chan_sum_x2_b: torch.Tensor | None = None, chan_count_b: torch.Tensor | None = None)
native: torch.Tensor
rotated: torch.Tensor
count: torch.Tensor
mean_native: torch.Tensor
mean_rotated: torch.Tensor
var_native: torch.Tensor
var_rotated: torch.Tensor
skew_native: torch.Tensor
skew_rotated: torch.Tensor
sparsity_native: torch.Tensor
sparsity_rotated: torch.Tensor
mask: torch.Tensor | None = None
blocks: tuple[torch.Tensor, ...] | None = None
sum_x: torch.Tensor | None = None
sum_xx: torch.Tensor | None = None
running_max: torch.Tensor | None = None
running_min: torch.Tensor | None = None
chan_sum_x: torch.Tensor | None = None
chan_sum_x2: torch.Tensor | None = None
chan_sum_x3: torch.Tensor | None = None
chan_sum_x4: torch.Tensor | None = None
chan_sum_x_a: torch.Tensor | None = None
chan_sum_x2_a: torch.Tensor | None = None
chan_count_a: torch.Tensor | None = None
chan_sum_x_b: torch.Tensor | None = None
chan_sum_x2_b: torch.Tensor | None = None
chan_count_b: torch.Tensor | None = None
class PrivilegedBasisEvaluator(contextlib.AbstractContextManager):
108class PrivilegedBasisEvaluator(contextlib.AbstractContextManager):
109    _AUTO_ROTATION_BUDGET = 256
110    _MAX_CHUNK_ELEMENTS = 8_000_000
111    _MAX_ROTATION_BLOCK_ELEMENTS = 8_000_000
112
113    def __init__(
114        self,
115        model,
116        pad_id: int | None = None,
117        seed: int = 0,
118        num_rotations: int | None = None,
119        compute_effective_rank: bool = False,
120        compute_channel_stats: bool = False
121    ):
122        self.pad_id = pad_id
123        self.seed = int(seed)
124        self.num_rotations = num_rotations
125        self.compute_effective_rank = compute_effective_rank
126        # Optional and off by default: per-channel (across-token) statistics
127        # are a handful of extra reductions per chunk plus 10 small
128        # (n_layers x hidden_size or n_layers) accumulators -- cheap relative
129        # to the effective-rank covariance matrix, but not free, so this is
130        # opt-in rather than bundled into compute_effective_rank.
131        self.compute_channel_stats = compute_channel_stats
132
133        self.layers = find_decoder_layers(model)
134        self.n_layers = len(self.layers)
135
136        # Public CPU snapshots updated by finalize()
137        self.sum_kurt = torch.zeros(self.n_layers, dtype=torch.float64)
138        self.sum_kurt_rot = torch.zeros(self.n_layers, dtype=torch.float64)
139        self.sum_mean = torch.zeros(self.n_layers, dtype=torch.float64)
140        self.sum_mean_rot = torch.zeros(self.n_layers, dtype=torch.float64)
141        self.sum_var = torch.zeros(self.n_layers, dtype=torch.float64)
142        self.sum_var_rot = torch.zeros(self.n_layers, dtype=torch.float64)
143        self.sum_skew = torch.zeros(self.n_layers, dtype=torch.float64)
144        self.sum_skew_rot = torch.zeros(self.n_layers, dtype=torch.float64)
145        self.sum_sparsity = torch.zeros(self.n_layers, dtype=torch.float64)
146        self.sum_sparsity_rot = torch.zeros(self.n_layers, dtype=torch.float64)
147        self.token_count = torch.zeros(self.n_layers, dtype=torch.float64)
148        # Raw per-channel moment vectors (n_layers x hidden_size), populated by
149        # finalize() only if compute_channel_stats=True. Intended for on-demand
150        # visual inspection (see plot_channel_concentration) rather than as
151        # flat per-layer scalars like the rest of the results dict.
152        self.channel_moments: dict[str, torch.Tensor] | None = None
153        # self.outlier_dims: torch.Tensor | None = None
154
155        self._handles: list = []
156        self._token_mask_cpu: torch.Tensor | None = None
157        self._states: dict[torch.device, DeviceState] = {}
158        self._effective_rotations: int | None = None
159        self._mask_all: bool = False
160        # Alternates every observe() call, splitting the token stream into two
161        # disjoint halves for the channel-persistence check.
162        self._split_toggle: bool = False
163        self._current_split: bool = False
164
165    def __enter__(self):
166        if self._handles:
167            raise RuntimeError("Context already entered.")
168        self._handles = [
169            layer.register_forward_hook(self._make_hook(i))
170            for i, layer in enumerate(self.layers)
171        ]
172        return self
173
174    def __exit__(self, *exc):
175        for handle in self._handles:
176            handle.remove()
177        self._handles.clear()
178        self._token_mask_cpu = None
179        self._mask_all = False
180        # Only release the transient, memory-heavy per-pass caches (rotation
181        # blocks, device mask). The running accumulators (native, rotated,
182        # count, outliers) must survive __exit__ since finalize() is called
183        # by the caller *after* the `with` block ends.
184        for state in self._states.values():
185            state.mask = None
186            state.blocks = None
187        return False
188
189    def observe(self, input_ids=None, attention_mask=None) -> None:
190        """Set the valid-token mask for the next forward pass."""
191        if attention_mask is not None:
192            self._token_mask_cpu = attention_mask.detach().reshape(-1).bool().cpu()
193        elif input_ids is not None and self.pad_id is None:
194            raise ValueError("pad_id required if attention_mask is omitted.")
195        elif input_ids is not None:
196            self._token_mask_cpu = (input_ids.detach() != self.pad_id).reshape(-1).cpu()
197        else:
198            raise ValueError("Provide attention_mask or input_ids.")
199        
200        # Clear lazily loaded per-device masks
201        for state in self._states.values():
202            state.mask = None
203
204        # Alternate which half of the token stream this forward pass counts
205        # toward, for the channel-persistence check (see compute_channel_stats)
206        self._current_split = self._split_toggle
207        self._split_toggle = not self._split_toggle
208
209        # Check if we can run the unmasked fast-path
210
211        assert self._token_mask_cpu is not None
212        self._mask_all = bool(self._token_mask_cpu.all())
213
214    def _get_device_state(self, device: torch.device, hidden_size: int) -> DeviceState:
215        """Retrieve or initialize consolidated state for a device."""
216        if device not in self._states:
217            
218            sum_x = torch.zeros ( 
219                (self.n_layers, hidden_size), 
220                device=device, dtype=torch.float64
221            ) if self.compute_effective_rank else None
222
223            sum_xx = torch.zeros(
224                ( self.n_layers, hidden_size, hidden_size), 
225                device=device, dtype=torch.float64
226            ) if self.compute_effective_rank else None
227
228            def _chan_vec():
229                return torch.zeros((self.n_layers, hidden_size), device=device, dtype=torch.float64) \
230                    if self.compute_channel_stats else None
231
232            def _chan_count():
233                return torch.zeros(self.n_layers, device=device, dtype=torch.float64) \
234                    if self.compute_channel_stats else None
235
236            self._states[device] = DeviceState(
237                native=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
238                rotated=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
239                count=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
240                mean_native=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
241                mean_rotated=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
242                var_native=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
243                var_rotated=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
244                skew_native=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
245                skew_rotated=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
246                sparsity_native=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
247                sparsity_rotated=torch.zeros(self.n_layers, device=device, dtype=torch.float64),
248                # outliers=torch.zeros(self.n_layers, hidden_size, device=device, dtype=torch.bool),
249                sum_x=sum_x,
250                sum_xx=sum_xx,
251                chan_sum_x=_chan_vec(),
252                chan_sum_x2=_chan_vec(),
253                chan_sum_x3=_chan_vec(),
254                chan_sum_x4=_chan_vec(),
255                chan_sum_x_a=_chan_vec(),
256                chan_sum_x2_a=_chan_vec(),
257                chan_count_a=_chan_count(),
258                chan_sum_x_b=_chan_vec(),
259                chan_sum_x2_b=_chan_vec(),
260                chan_count_b=_chan_count(),
261                running_max=torch.full(
262                    (self.n_layers, hidden_size), 
263                    float('-inf'), 
264                    device=device, 
265                    dtype=torch.float32
266                ),
267                running_min=torch.full(
268                    (self.n_layers, hidden_size), 
269                    float('inf'), 
270                    device=device, 
271                    dtype=torch.float32
272                )
273            )
274            
275            # Initialize batched rotations once per device
276            count = self.num_rotations or max(1, math.ceil(self._AUTO_ROTATION_BUDGET / hidden_size))
277            self._effective_rotations = count
278            
279            # Pack rotations into blocks to optimize GEMMs
280            by_matrix_budget = max(1, self._MAX_ROTATION_BLOCK_ELEMENTS // (hidden_size * hidden_size))
281            block_size = max(1, min(count, by_matrix_budget))
282            
283            blocks = []
284            for start in range(0, count, block_size):
285                block_rots = min(block_size, count - start)
286                mats = []
287                for idx in range(start, start + block_rots):
288                    seed = (self.seed + idx * 0x9E3779B97F4A7C15) % (2**63)
289                    mats.append(_haar_orthogonal(hidden_size, seed))
290                
291                packed_cpu = torch.cat(mats, dim=1).contiguous()
292                blocks.append(packed_cpu.to(device=device, dtype=torch.float32, non_blocking=True))
293            
294            self._states[device].blocks = tuple(blocks)
295
296        return self._states[device]
297
298    def _make_hook(self, layer_idx: int):
299
300        def hook(_module, _inputs, output):
301            
302            if self._token_mask_cpu is None:
303                return
304
305            hidden = output[0] if isinstance(output, (tuple, list)) else output
306            
307            # Safety check against unexpected output structures
308            if not isinstance(hidden, torch.Tensor) or getattr(hidden, "ndim", 0) < 1:
309                return
310
311            device = hidden.device
312            hidden_size = hidden.shape[-1]
313
314            amp_ctx = (
315                torch.autocast(device_type=device.type, enabled=False)
316                if device.type in ("cuda", "cpu")
317                else contextlib.nullcontext()
318            )
319
320            with torch.inference_mode(), amp_ctx:
321    
322                flat  = hidden.detach().reshape(-1, hidden_size)
323                state = self._get_device_state(device, hidden_size)
324
325                total_rows = flat.shape[0]
326
327                if self._token_mask_cpu.numel() != total_rows:
328                    raise ValueError(f"Mask mismatch: {self._token_mask_cpu.numel()} vs {total_rows}")
329
330                # Prove to static analyzer that variables initialized in _get_device_state are not None
331                assert state.blocks is not None
332                assert self._effective_rotations is not None
333
334                # Calculate memory-safe chunk sizes considering the batched rotations
335                max_block_rots = max(b.shape[1] // hidden_size for b in state.blocks)
336                denominator = hidden_size * (1.0 + max_block_rots) + 3.0 * max_block_rots
337                chunk_tokens = min(total_rows, max(1, int(self._MAX_CHUNK_ELEMENTS // max(1.0, denominator))))
338                
339                # Copy the mask lazily only if there are tokens to mask
340                mask = None
341                
342                if not self._mask_all:
343                    if state.mask is None:
344                        state.mask = self._token_mask_cpu.to(device, non_blocking=True)
345                    
346                    mask = state.mask
347
348                for start in range(0, total_rows, chunk_tokens):
349
350                    end   = min(total_rows, start + chunk_tokens)
351                    chunk = flat[start:end].to(dtype=torch.float32, copy=True)
352                    
353                    m_chunk: torch.Tensor | None = mask[start:end] if mask is not None else None
354                    
355                    # Rotated Moments (Batched): mean, variance, skewness, kurtosis, sparsity
356                    rotated_mean_total = torch.zeros((), device=device, dtype=torch.float64)
357                    rotated_var_total = torch.zeros((), device=device, dtype=torch.float64)
358                    rotated_skew_total = torch.zeros((), device=device, dtype=torch.float64)
359                    rotated_kurt_total = torch.zeros((), device=device, dtype=torch.float64)
360                    rotated_sparsity_total = torch.zeros((), device=device, dtype=torch.float64)
361
362                    for block in state.blocks:
363                        
364                        block_rots = block.shape[1] // hidden_size
365                        rotated = (chunk @ block).view(chunk.shape[0], block_rots, hidden_size)
366                        
367                        # Sparsity reads raw values, so it must run before the
368                        # destructive moments call overwrites `rotated` below.
369                        sparsity_rot = _hoyer_sparsity(rotated)
370                        mean_rot, var_rot, skew_rot, kurt_rot = _moments_inplace(rotated)
371                        if m_chunk is not None:
372
373                            invalid = (~m_chunk).view(-1, 1).expand_as(kurt_rot)
374                            mean_rot.masked_fill_(invalid, 0)
375                            var_rot.masked_fill_(invalid, 0)
376                            skew_rot.masked_fill_(invalid, 0)
377                            kurt_rot.masked_fill_(invalid, 0)
378                            sparsity_rot.masked_fill_(invalid, 0)
379                        
380                        rotated_mean_total.add_(mean_rot.sum(dtype=torch.float64))
381                        rotated_var_total.add_(var_rot.sum(dtype=torch.float64))
382                        rotated_skew_total.add_(skew_rot.sum(dtype=torch.float64))
383                        rotated_kurt_total.add_(kurt_rot.sum(dtype=torch.float64))
384                        rotated_sparsity_total.add_(sparsity_rot.sum(dtype=torch.float64))
385
386                    # Row bookkeeping and valid-token extraction, done on the
387                    # pristine chunk *before* native moments are computed
388                    # in-place below (which overwrites chunk's contents).
389                    if m_chunk is None:
390                        state.count[layer_idx].add_(chunk.shape[0])
391                        valid_chunk = chunk
392                    else:
393                        state.count[layer_idx].add_(m_chunk.sum(dtype=torch.float64))
394                        valid_chunk = chunk[m_chunk] # Extract only valid tokens (copy)
395
396                    valid_chunk_64 = None
397
398                    # Accumulate for Covariance
399                    if self.compute_effective_rank and valid_chunk.shape[0] > 0:
400
401                        assert state.sum_x is not None
402                        assert state.sum_xx is not None
403
404                        # Cast to float64 to prevent numerical overflow across millions of tokens
405                        # (also copies, so this is safe from the in-place native moments below
406                        # even when valid_chunk aliases chunk directly, i.e. the unmasked case)
407                        valid_chunk_64 = valid_chunk.to(torch.float64)
408                        state.sum_x[layer_idx].add_(valid_chunk_64.sum(dim=0))
409
410                        # Efficiently compute sum of outer products: X^T @ X
411                        state.sum_xx[layer_idx].add_(valid_chunk_64.t() @ valid_chunk_64)
412
413                        assert state.running_min is not None
414                        assert state.running_max is not None
415
416                        state.running_max[layer_idx] = torch.maximum(state.running_max[layer_idx], valid_chunk.max(dim=0)[0])
417
418                        state.running_min[layer_idx] = torch.minimum(state.running_min[layer_idx], valid_chunk.min(dim=0)[0])
419
420                    # Per-channel power sums (native basis only: channel identity
421                    # isn't preserved under a random rotation, so there's nothing
422                    # meaningful to accumulate for a "rotated channel").
423                    if self.compute_channel_stats and valid_chunk.shape[0] > 0:
424
425                        assert state.chan_sum_x is not None and state.chan_sum_x2 is not None
426                        assert state.chan_sum_x3 is not None and state.chan_sum_x4 is not None
427                        assert state.chan_sum_x_a is not None and state.chan_sum_x2_a is not None
428                        assert state.chan_sum_x_b is not None and state.chan_sum_x2_b is not None
429                        assert state.chan_count_a is not None and state.chan_count_b is not None
430
431                        # Reuse the float64 cast from the covariance block above
432                        # when available; otherwise compute it fresh here.
433
434                        if valid_chunk_64 is None :
435                            valid_chunk_64 = valid_chunk.to(torch.float64)
436
437                        valid_chunk_64_ch = valid_chunk_64 if self.compute_effective_rank else valid_chunk.to(torch.float64)
438                        sq64 = valid_chunk_64_ch.square()
439
440                        state.chan_sum_x[layer_idx].add_(valid_chunk_64_ch.sum(dim=0))
441                        state.chan_sum_x2[layer_idx].add_(sq64.sum(dim=0))
442                        state.chan_sum_x3[layer_idx].add_((sq64 * valid_chunk_64_ch).sum(dim=0))
443                        state.chan_sum_x4[layer_idx].add_(sq64.square().sum(dim=0))
444
445                        if self._current_split:
446                            state.chan_sum_x_a[layer_idx].add_(valid_chunk_64_ch.sum(dim=0))
447                            state.chan_sum_x2_a[layer_idx].add_(sq64.sum(dim=0))
448                            state.chan_count_a[layer_idx].add_(valid_chunk.shape[0])
449                        else:
450                            state.chan_sum_x_b[layer_idx].add_(valid_chunk_64_ch.sum(dim=0))
451                            state.chan_sum_x2_b[layer_idx].add_(sq64.sum(dim=0))
452                            state.chan_count_b[layer_idx].add_(valid_chunk.shape[0])
453
454                    # Native Moments (sparsity + moments both need the pristine
455                    # chunk; sparsity is non-destructive but must still be read
456                    # before the in-place moments call below overwrites it)
457                    sparsity_native = _hoyer_sparsity(chunk)
458                    mean_native, var_native, skew_native, kurt_native = _moments_inplace(chunk)
459                    if m_chunk is not None:
460                        mean_native.masked_fill_(~m_chunk, 0)
461                        var_native.masked_fill_(~m_chunk, 0)
462                        skew_native.masked_fill_(~m_chunk, 0)
463                        kurt_native.masked_fill_(~m_chunk, 0)
464                        sparsity_native.masked_fill_(~m_chunk, 0)
465
466                    # Update States
467                    state.native[layer_idx].add_(kurt_native.sum(dtype=torch.float64))
468                    state.rotated[layer_idx].add_(rotated_kurt_total / self._effective_rotations)
469
470                    state.mean_native[layer_idx].add_(mean_native.sum(dtype=torch.float64))
471                    state.mean_rotated[layer_idx].add_(rotated_mean_total / self._effective_rotations)
472
473                    state.var_native[layer_idx].add_(var_native.sum(dtype=torch.float64))
474                    state.var_rotated[layer_idx].add_(rotated_var_total / self._effective_rotations)
475
476                    state.skew_native[layer_idx].add_(skew_native.sum(dtype=torch.float64))
477                    state.skew_rotated[layer_idx].add_(rotated_skew_total / self._effective_rotations)
478
479                    state.sparsity_native[layer_idx].add_(sparsity_native.sum(dtype=torch.float64))
480                    state.sparsity_rotated[layer_idx].add_(rotated_sparsity_total / self._effective_rotations)
481
482        return hook
483
484    def finalize(self) -> dict[str, Any]:
485        """Aggregate metrics across all devices to the CPU and compute statistics."""
486        
487        if not self._states:
488            return {}
489
490        sum_kurt = torch.zeros(self.n_layers, dtype=torch.float64)
491        sum_kurt_rot = torch.zeros(self.n_layers, dtype=torch.float64)
492        sum_mean = torch.zeros(self.n_layers, dtype=torch.float64)
493        sum_mean_rot = torch.zeros(self.n_layers, dtype=torch.float64)
494        sum_var = torch.zeros(self.n_layers, dtype=torch.float64)
495        sum_var_rot = torch.zeros(self.n_layers, dtype=torch.float64)
496        sum_skew = torch.zeros(self.n_layers, dtype=torch.float64)
497        sum_skew_rot = torch.zeros(self.n_layers, dtype=torch.float64)
498        sum_sparsity = torch.zeros(self.n_layers, dtype=torch.float64)
499        sum_sparsity_rot = torch.zeros(self.n_layers, dtype=torch.float64)
500        token_count = torch.zeros(self.n_layers, dtype=torch.float64)
501
502        for state in self._states.values():
503            sum_kurt += state.native.cpu().to(dtype=torch.float64)
504            sum_kurt_rot += state.rotated.cpu().to(dtype=torch.float64)
505            sum_mean += state.mean_native.cpu().to(dtype=torch.float64)
506            sum_mean_rot += state.mean_rotated.cpu().to(dtype=torch.float64)
507            sum_var += state.var_native.cpu().to(dtype=torch.float64)
508            sum_var_rot += state.var_rotated.cpu().to(dtype=torch.float64)
509            sum_skew += state.skew_native.cpu().to(dtype=torch.float64)
510            sum_skew_rot += state.skew_rotated.cpu().to(dtype=torch.float64)
511            sum_sparsity += state.sparsity_native.cpu().to(dtype=torch.float64)
512            sum_sparsity_rot += state.sparsity_rotated.cpu().to(dtype=torch.float64)
513            token_count += state.count.cpu().to(dtype=torch.float64)
514
515        self.sum_kurt = sum_kurt
516        self.sum_kurt_rot = sum_kurt_rot
517        self.sum_mean = sum_mean
518        self.sum_mean_rot = sum_mean_rot
519        self.sum_var = sum_var
520        self.sum_var_rot = sum_var_rot
521        self.sum_skew = sum_skew
522        self.sum_skew_rot = sum_skew_rot
523        self.sum_sparsity = sum_sparsity
524        self.sum_sparsity_rot = sum_sparsity_rot
525        self.token_count = token_count
526
527        seen = token_count > 0
528        if not seen.any().item():
529            return {}
530
531        mean_kurt = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
532        mean_kurt_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
533        mean_mean = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
534        mean_mean_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
535        mean_var = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
536        mean_var_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
537        mean_skew = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
538        mean_skew_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
539        mean_sparsity = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
540        mean_sparsity_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
541
542        mean_kurt[seen] = sum_kurt[seen] / token_count[seen]
543        mean_kurt_rot[seen] = sum_kurt_rot[seen] / token_count[seen]
544        mean_mean[seen] = sum_mean[seen] / token_count[seen]
545        mean_mean_rot[seen] = sum_mean_rot[seen] / token_count[seen]
546        mean_var[seen] = sum_var[seen] / token_count[seen]
547        mean_var_rot[seen] = sum_var_rot[seen] / token_count[seen]
548        mean_skew[seen] = sum_skew[seen] / token_count[seen]
549        mean_skew_rot[seen] = sum_skew_rot[seen] / token_count[seen]
550        mean_sparsity[seen] = sum_sparsity[seen] / token_count[seen]
551        mean_sparsity_rot[seen] = sum_sparsity_rot[seen] / token_count[seen]
552
553        excess_per_layer = mean_kurt - mean_kurt_rot
554
555        ratio_per_layer = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
556        ratio_per_layer[seen] = mean_kurt[seen] / mean_kurt_rot[seen].clamp_min(1e-12)
557
558        seen_indices = torch.where(seen)[0]
559        local_max_index = excess_per_layer[seen].argmax()
560        
561        # Explicit int cast for tensor indices to satisfy static analyzer
562        max_excess_layer = int(seen_indices[local_max_index].item())
563
564        max_kurt = mean_kurt[seen].max().item()
565        rotated_baseline = mean_kurt_rot[seen].mean().item()
566        max_excess = excess_per_layer[max_excess_layer].item()
567        max_excess_ratio = ratio_per_layer[max_excess_layer].item()
568
569        if max_excess >= 1.0 and max_excess_ratio >= 2.0:
570            verdict = "strong privileged-basis signal"
571        elif max_excess >= 0.25 and max_excess_ratio >= 1.2:
572            verdict = "moderate privileged-basis signal"
573        else:
574            verdict = "no clear privileged-basis signal"
575
576        results = {
577            "pb_mean_per_layer": mean_mean.tolist(),
578            "pb_mean_per_layer_rotated": mean_mean_rot.tolist(),
579            "pb_variance_per_layer": mean_var.tolist(),
580            "pb_variance_per_layer_rotated": mean_var_rot.tolist(),
581            "pb_std_per_layer": mean_var.sqrt().tolist(),
582            "pb_std_per_layer_rotated": mean_var_rot.sqrt().tolist(),
583            "pb_skewness_per_layer": mean_skew.tolist(),
584            "pb_skewness_per_layer_rotated": mean_skew_rot.tolist(),
585            "pb_kurtosis_per_layer": mean_kurt.tolist(),
586            "pb_kurtosis_per_layer_rotated": mean_kurt_rot.tolist(),
587            "pb_sparsity_per_layer": mean_sparsity.tolist(),
588            "pb_sparsity_per_layer_rotated": mean_sparsity_rot.tolist(),
589            "pb_excess_kurtosis_per_layer": excess_per_layer.tolist(),
590            "pb_kurtosis_ratio_per_layer": ratio_per_layer.tolist(),
591            "pb_num_rotations": self._effective_rotations,
592            "pb_max_kurtosis": max_kurt,
593            "pb_rotated_kurtosis_baseline": rotated_baseline,
594            "pb_max_excess_kurtosis": max_excess,
595            "pb_max_excess_ratio": max_excess_ratio,
596            "pb_max_excess_layer": max_excess_layer,
597            "pb_verdict": verdict
598        }
599
600        # Effective Rank
601        if self.compute_effective_rank:
602
603            first_state = next(iter(self._states.values()))
604
605            assert first_state.sum_x is not None
606            assert first_state.sum_xx is not None
607
608            hidden_size = first_state.sum_x.shape[-1]
609            
610            sum_x_cpu  : torch.Tensor = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
611            sum_xx_cpu : torch.Tensor = torch.zeros((self.n_layers, hidden_size, hidden_size), dtype=torch.float64)
612
613            for state in self._states.values() :
614
615                assert state.sum_x is not None
616                assert state.sum_xx is not None
617
618                sum_x_cpu  += state.sum_x.cpu()
619                sum_xx_cpu += state.sum_xx.cpu()
620
621            effective_ranks = []
622            axis_alignment = []
623            for l in range(self.n_layers):
624                
625                if token_count[l] == 0:
626                    effective_ranks.append(float("nan"))
627                    axis_alignment.append(float("nan"))
628                    continue
629                    
630                N = token_count[l].item()
631                mean_x = sum_x_cpu[l] / N
632                
633                # Covariance matrix
634                cov = (sum_xx_cpu[l] / N) - torch.outer(mean_x, mean_x)
635                
636                # Symmetric matrix: eigh gives eigenvalues *and* eigenvectors,
637                # ascending order. The eigenvectors are what let us also check
638                # axis-alignment below, at no extra decomposition cost.
639                eigvals, eigvecs = torch.linalg.eigh(cov)
640                
641                # Relative floor: absolute clamps (e.g. a flat 1e-12) are wrong
642                # once you compare layers whose activation scale differs a lot
643                # (residual-stream norm grows with depth) — a fixed floor is
644                # either a no-op on large-scale layers or an inflating one on
645                # small-scale layers. Floor relative to this layer's own top
646                # eigenvalue instead, with a tiny absolute fallback only to
647                # guard against an all-zero covariance.
648                floor = torch.clamp_min(eigvals.max().clamp_min(0) * 1e-8, 1e-12)
649                eigvals = torch.clamp_min(eigvals, floor)
650                
651                # Normalize to probability distribution
652                p = eigvals / eigvals.sum()
653                
654                # Compute Shannon entropy
655                entropy = -(p * torch.log(p)).sum()
656                eff_rank = torch.exp(entropy).item()
657                
658                effective_ranks.append(eff_rank)
659
660                # Axis alignment: unlike the eigenvalues themselves, the
661                # eigenvectors *do* change under rotation of the underlying
662                # basis, so this is a direct (and much cheaper, since the
663                # decomposition is already in hand) complement to the
664                # kurtosis-based privileged-basis test above. For each
665                # eigenvector, the participation ratio of its own components
666                # (1 / sum(v_i^4), since ||v||=1) is 1 when it points along a
667                # single standard axis and hidden_size when it's spread
668                # evenly across all of them. We average this over eigenvectors,
669                # weighted by their eigenvalue share `p`, so directions that
670                # carry more of the variance dominate the score.
671                eigvec_ipr = 1.0 / eigvecs.pow(4).sum(dim=0).clamp_min(1e-12)
672                axis_alignment.append((p * eigvec_ipr).sum().item())
673
674            results["pb_effective_rank_per_layer"] = effective_ranks
675            results["pb_axis_alignment_per_layer"] = axis_alignment
676
677        # Per-channel statistics (native basis only -- see compute_channel_stats)
678        if self.compute_channel_stats:
679
680            first_state = next(iter(self._states.values()))
681            assert first_state.chan_sum_x is not None
682            hidden_size = first_state.chan_sum_x.shape[-1]
683
684            chan_sum_x  = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
685            chan_sum_x2 = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
686            chan_sum_x3 = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
687            chan_sum_x4 = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
688            chan_sum_x_a  = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
689            chan_sum_x2_a = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
690            chan_sum_x_b  = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
691            chan_sum_x2_b = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
692            chan_count_a = torch.zeros(self.n_layers, dtype=torch.float64)
693            chan_count_b = torch.zeros(self.n_layers, dtype=torch.float64)
694
695            for state in self._states.values():
696
697                assert state.chan_sum_x is not None and state.chan_sum_x2 is not None
698                assert state.chan_sum_x3 is not None and state.chan_sum_x4 is not None
699                assert state.chan_sum_x_a is not None and state.chan_sum_x2_a is not None
700                assert state.chan_sum_x_b is not None and state.chan_sum_x2_b is not None
701                assert state.chan_count_a is not None and state.chan_count_b is not None
702
703                chan_sum_x    += state.chan_sum_x.cpu()
704                chan_sum_x2   += state.chan_sum_x2.cpu()
705                chan_sum_x3   += state.chan_sum_x3.cpu()
706                chan_sum_x4   += state.chan_sum_x4.cpu()
707                chan_sum_x_a  += state.chan_sum_x_a.cpu()
708                chan_sum_x2_a += state.chan_sum_x2_a.cpu()
709                chan_sum_x_b  += state.chan_sum_x_b.cpu()
710                chan_sum_x2_b += state.chan_sum_x2_b.cpu()
711                chan_count_a  += state.chan_count_a.cpu()
712                chan_count_b  += state.chan_count_b.cpu()
713
714            channel_mean = torch.full((self.n_layers, hidden_size), float("nan"), dtype=torch.float64)
715            channel_var  = torch.full((self.n_layers, hidden_size), float("nan"), dtype=torch.float64)
716            channel_skew = torch.full((self.n_layers, hidden_size), float("nan"), dtype=torch.float64)
717            channel_kurt = torch.full((self.n_layers, hidden_size), float("nan"), dtype=torch.float64)
718
719            eff_channel_count = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
720            channel_gini = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
721            channel_powerlaw_exponent = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
722            channel_persistence = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
723
724            n = hidden_size
725            log_rank_desc = torch.log(torch.arange(1, n + 1, dtype=torch.float64))
726            log_rank_desc_centered = log_rank_desc - log_rank_desc.mean()
727            rank_1_to_n = torch.arange(1, n + 1, dtype=torch.float64)
728
729            for l in range(self.n_layers):
730
731                N = token_count[l].item()
732                if N == 0:
733                    continue
734
735                mean_l = chan_sum_x[l] / N
736                e_x2 = chan_sum_x2[l] / N
737                e_x3 = chan_sum_x3[l] / N
738                e_x4 = chan_sum_x4[l] / N
739
740                # Raw -> central moment conversion (single pass at finalize
741                # time, so the usual cancellation risk of this formula is a
742                # non-issue here -- it's not being iterated per chunk).
743                m2 = (e_x2 - mean_l.square()).clamp_min(1e-12)
744                m3 = e_x3 - 3 * mean_l * e_x2 + 2 * mean_l.pow(3)
745                m4 = e_x4 - 4 * mean_l * e_x3 + 6 * mean_l.square() * e_x2 - 3 * mean_l.pow(4)
746
747                channel_mean[l] = mean_l
748                channel_var[l] = m2
749                channel_skew[l] = m3 / m2.pow(1.5)
750                channel_kurt[l] = m4 / m2.square()
751
752                var_l = m2.clamp_min(0)
753                s1, s2 = var_l.sum(), var_l.square().sum()
754
755                # Effective channel count: participation ratio of per-channel
756                # variance. No privileged basis -> variance spread ~uniformly
757                # across channels -> this saturates near hidden_size;
758                # concentration in a few channels pulls it down. Threshold-free
759                # counterpart to "how many outlier channels are there".
760                if s2 > 0:
761                    eff_channel_count[l] = (s1.square() / s2).item()
762
763                # Gini coefficient of per-channel variance (0 = perfectly even
764                # across channels, ~1 = all variance in one channel).
765                sorted_var, _ = torch.sort(var_l)
766                total = sorted_var.sum().clamp_min(1e-24)
767                channel_gini[l] = ((2 * (rank_1_to_n * sorted_var).sum()) / (n * total) - (n + 1) / n).item()
768
769                # Power-law decay exponent of the sorted (descending) variance
770                # curve: fit slope of log(variance) vs log(rank). A shape
771                # descriptor, not a cutoff -- two layers can share a Gini value
772                # while one is a clean power law and the other a step function.
773                desc_var, _ = torch.sort(var_l, descending=True)
774                floor = torch.clamp_min(desc_var.max() * 1e-8, 1e-24)
775                log_v = torch.log(desc_var.clamp_min(floor))
776                log_v_centered = log_v - log_v.mean()
777                slope = (log_rank_desc_centered * log_v_centered).sum() / log_rank_desc_centered.square().sum()
778                channel_powerlaw_exponent[l] = (-slope).item()
779
780                # Persistence: Spearman rank correlation of per-channel variance
781                # between two disjoint halves of the token stream. High ->
782                # same channels dominate dataset-wide (a genuinely fixed
783                # privileged set); low -> the "privileged" channels drift
784                # between batches and the concentration seen above may just be
785                # sampling noise rather than a stable structural feature.
786                na, nb = chan_count_a[l].item(), chan_count_b[l].item()
787                if na > 0 and nb > 0:
788                    mean_a = chan_sum_x_a[l] / na
789                    var_a = (chan_sum_x2_a[l] / na - mean_a.square()).clamp_min(0)
790                    mean_b = chan_sum_x_b[l] / nb
791                    var_b = (chan_sum_x2_b[l] / nb - mean_b.square()).clamp_min(0)
792                    channel_persistence[l] = _spearman(var_a, var_b).item()
793
794            results["pb_effective_channel_count_per_layer"] = eff_channel_count.tolist()
795            results["pb_channel_gini_per_layer"] = channel_gini.tolist()
796            results["pb_channel_powerlaw_exponent_per_layer"] = channel_powerlaw_exponent.tolist()
797            results["pb_channel_persistence_per_layer"] = channel_persistence.tolist()
798
799            # Raw per-channel moment vectors, for on-demand visual inspection
800            # (see plot_channel_concentration) -- not meant to be read as flat
801            # per-layer scalars like the rest of `results`.
802            self.channel_moments = {
803                "mean": channel_mean,
804                "variance": channel_var,
805                "skewness": channel_skew,
806                "kurtosis": channel_kurt,
807            }
808
809        return results

An abstract base class for context managers.

PrivilegedBasisEvaluator( model, pad_id: int | None = None, seed: int = 0, num_rotations: int | None = None, compute_effective_rank: bool = False, compute_channel_stats: bool = False)
113    def __init__(
114        self,
115        model,
116        pad_id: int | None = None,
117        seed: int = 0,
118        num_rotations: int | None = None,
119        compute_effective_rank: bool = False,
120        compute_channel_stats: bool = False
121    ):
122        self.pad_id = pad_id
123        self.seed = int(seed)
124        self.num_rotations = num_rotations
125        self.compute_effective_rank = compute_effective_rank
126        # Optional and off by default: per-channel (across-token) statistics
127        # are a handful of extra reductions per chunk plus 10 small
128        # (n_layers x hidden_size or n_layers) accumulators -- cheap relative
129        # to the effective-rank covariance matrix, but not free, so this is
130        # opt-in rather than bundled into compute_effective_rank.
131        self.compute_channel_stats = compute_channel_stats
132
133        self.layers = find_decoder_layers(model)
134        self.n_layers = len(self.layers)
135
136        # Public CPU snapshots updated by finalize()
137        self.sum_kurt = torch.zeros(self.n_layers, dtype=torch.float64)
138        self.sum_kurt_rot = torch.zeros(self.n_layers, dtype=torch.float64)
139        self.sum_mean = torch.zeros(self.n_layers, dtype=torch.float64)
140        self.sum_mean_rot = torch.zeros(self.n_layers, dtype=torch.float64)
141        self.sum_var = torch.zeros(self.n_layers, dtype=torch.float64)
142        self.sum_var_rot = torch.zeros(self.n_layers, dtype=torch.float64)
143        self.sum_skew = torch.zeros(self.n_layers, dtype=torch.float64)
144        self.sum_skew_rot = torch.zeros(self.n_layers, dtype=torch.float64)
145        self.sum_sparsity = torch.zeros(self.n_layers, dtype=torch.float64)
146        self.sum_sparsity_rot = torch.zeros(self.n_layers, dtype=torch.float64)
147        self.token_count = torch.zeros(self.n_layers, dtype=torch.float64)
148        # Raw per-channel moment vectors (n_layers x hidden_size), populated by
149        # finalize() only if compute_channel_stats=True. Intended for on-demand
150        # visual inspection (see plot_channel_concentration) rather than as
151        # flat per-layer scalars like the rest of the results dict.
152        self.channel_moments: dict[str, torch.Tensor] | None = None
153        # self.outlier_dims: torch.Tensor | None = None
154
155        self._handles: list = []
156        self._token_mask_cpu: torch.Tensor | None = None
157        self._states: dict[torch.device, DeviceState] = {}
158        self._effective_rotations: int | None = None
159        self._mask_all: bool = False
160        # Alternates every observe() call, splitting the token stream into two
161        # disjoint halves for the channel-persistence check.
162        self._split_toggle: bool = False
163        self._current_split: bool = False
pad_id
seed
num_rotations
compute_effective_rank
compute_channel_stats
layers
n_layers
sum_kurt
sum_kurt_rot
sum_mean
sum_mean_rot
sum_var
sum_var_rot
sum_skew
sum_skew_rot
sum_sparsity
sum_sparsity_rot
token_count
channel_moments: dict[str, torch.Tensor] | None
def observe(self, input_ids=None, attention_mask=None) -> None:
189    def observe(self, input_ids=None, attention_mask=None) -> None:
190        """Set the valid-token mask for the next forward pass."""
191        if attention_mask is not None:
192            self._token_mask_cpu = attention_mask.detach().reshape(-1).bool().cpu()
193        elif input_ids is not None and self.pad_id is None:
194            raise ValueError("pad_id required if attention_mask is omitted.")
195        elif input_ids is not None:
196            self._token_mask_cpu = (input_ids.detach() != self.pad_id).reshape(-1).cpu()
197        else:
198            raise ValueError("Provide attention_mask or input_ids.")
199        
200        # Clear lazily loaded per-device masks
201        for state in self._states.values():
202            state.mask = None
203
204        # Alternate which half of the token stream this forward pass counts
205        # toward, for the channel-persistence check (see compute_channel_stats)
206        self._current_split = self._split_toggle
207        self._split_toggle = not self._split_toggle
208
209        # Check if we can run the unmasked fast-path
210
211        assert self._token_mask_cpu is not None
212        self._mask_all = bool(self._token_mask_cpu.all())

Set the valid-token mask for the next forward pass.

def finalize(self) -> dict[str, typing.Any]:
484    def finalize(self) -> dict[str, Any]:
485        """Aggregate metrics across all devices to the CPU and compute statistics."""
486        
487        if not self._states:
488            return {}
489
490        sum_kurt = torch.zeros(self.n_layers, dtype=torch.float64)
491        sum_kurt_rot = torch.zeros(self.n_layers, dtype=torch.float64)
492        sum_mean = torch.zeros(self.n_layers, dtype=torch.float64)
493        sum_mean_rot = torch.zeros(self.n_layers, dtype=torch.float64)
494        sum_var = torch.zeros(self.n_layers, dtype=torch.float64)
495        sum_var_rot = torch.zeros(self.n_layers, dtype=torch.float64)
496        sum_skew = torch.zeros(self.n_layers, dtype=torch.float64)
497        sum_skew_rot = torch.zeros(self.n_layers, dtype=torch.float64)
498        sum_sparsity = torch.zeros(self.n_layers, dtype=torch.float64)
499        sum_sparsity_rot = torch.zeros(self.n_layers, dtype=torch.float64)
500        token_count = torch.zeros(self.n_layers, dtype=torch.float64)
501
502        for state in self._states.values():
503            sum_kurt += state.native.cpu().to(dtype=torch.float64)
504            sum_kurt_rot += state.rotated.cpu().to(dtype=torch.float64)
505            sum_mean += state.mean_native.cpu().to(dtype=torch.float64)
506            sum_mean_rot += state.mean_rotated.cpu().to(dtype=torch.float64)
507            sum_var += state.var_native.cpu().to(dtype=torch.float64)
508            sum_var_rot += state.var_rotated.cpu().to(dtype=torch.float64)
509            sum_skew += state.skew_native.cpu().to(dtype=torch.float64)
510            sum_skew_rot += state.skew_rotated.cpu().to(dtype=torch.float64)
511            sum_sparsity += state.sparsity_native.cpu().to(dtype=torch.float64)
512            sum_sparsity_rot += state.sparsity_rotated.cpu().to(dtype=torch.float64)
513            token_count += state.count.cpu().to(dtype=torch.float64)
514
515        self.sum_kurt = sum_kurt
516        self.sum_kurt_rot = sum_kurt_rot
517        self.sum_mean = sum_mean
518        self.sum_mean_rot = sum_mean_rot
519        self.sum_var = sum_var
520        self.sum_var_rot = sum_var_rot
521        self.sum_skew = sum_skew
522        self.sum_skew_rot = sum_skew_rot
523        self.sum_sparsity = sum_sparsity
524        self.sum_sparsity_rot = sum_sparsity_rot
525        self.token_count = token_count
526
527        seen = token_count > 0
528        if not seen.any().item():
529            return {}
530
531        mean_kurt = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
532        mean_kurt_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
533        mean_mean = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
534        mean_mean_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
535        mean_var = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
536        mean_var_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
537        mean_skew = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
538        mean_skew_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
539        mean_sparsity = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
540        mean_sparsity_rot = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
541
542        mean_kurt[seen] = sum_kurt[seen] / token_count[seen]
543        mean_kurt_rot[seen] = sum_kurt_rot[seen] / token_count[seen]
544        mean_mean[seen] = sum_mean[seen] / token_count[seen]
545        mean_mean_rot[seen] = sum_mean_rot[seen] / token_count[seen]
546        mean_var[seen] = sum_var[seen] / token_count[seen]
547        mean_var_rot[seen] = sum_var_rot[seen] / token_count[seen]
548        mean_skew[seen] = sum_skew[seen] / token_count[seen]
549        mean_skew_rot[seen] = sum_skew_rot[seen] / token_count[seen]
550        mean_sparsity[seen] = sum_sparsity[seen] / token_count[seen]
551        mean_sparsity_rot[seen] = sum_sparsity_rot[seen] / token_count[seen]
552
553        excess_per_layer = mean_kurt - mean_kurt_rot
554
555        ratio_per_layer = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
556        ratio_per_layer[seen] = mean_kurt[seen] / mean_kurt_rot[seen].clamp_min(1e-12)
557
558        seen_indices = torch.where(seen)[0]
559        local_max_index = excess_per_layer[seen].argmax()
560        
561        # Explicit int cast for tensor indices to satisfy static analyzer
562        max_excess_layer = int(seen_indices[local_max_index].item())
563
564        max_kurt = mean_kurt[seen].max().item()
565        rotated_baseline = mean_kurt_rot[seen].mean().item()
566        max_excess = excess_per_layer[max_excess_layer].item()
567        max_excess_ratio = ratio_per_layer[max_excess_layer].item()
568
569        if max_excess >= 1.0 and max_excess_ratio >= 2.0:
570            verdict = "strong privileged-basis signal"
571        elif max_excess >= 0.25 and max_excess_ratio >= 1.2:
572            verdict = "moderate privileged-basis signal"
573        else:
574            verdict = "no clear privileged-basis signal"
575
576        results = {
577            "pb_mean_per_layer": mean_mean.tolist(),
578            "pb_mean_per_layer_rotated": mean_mean_rot.tolist(),
579            "pb_variance_per_layer": mean_var.tolist(),
580            "pb_variance_per_layer_rotated": mean_var_rot.tolist(),
581            "pb_std_per_layer": mean_var.sqrt().tolist(),
582            "pb_std_per_layer_rotated": mean_var_rot.sqrt().tolist(),
583            "pb_skewness_per_layer": mean_skew.tolist(),
584            "pb_skewness_per_layer_rotated": mean_skew_rot.tolist(),
585            "pb_kurtosis_per_layer": mean_kurt.tolist(),
586            "pb_kurtosis_per_layer_rotated": mean_kurt_rot.tolist(),
587            "pb_sparsity_per_layer": mean_sparsity.tolist(),
588            "pb_sparsity_per_layer_rotated": mean_sparsity_rot.tolist(),
589            "pb_excess_kurtosis_per_layer": excess_per_layer.tolist(),
590            "pb_kurtosis_ratio_per_layer": ratio_per_layer.tolist(),
591            "pb_num_rotations": self._effective_rotations,
592            "pb_max_kurtosis": max_kurt,
593            "pb_rotated_kurtosis_baseline": rotated_baseline,
594            "pb_max_excess_kurtosis": max_excess,
595            "pb_max_excess_ratio": max_excess_ratio,
596            "pb_max_excess_layer": max_excess_layer,
597            "pb_verdict": verdict
598        }
599
600        # Effective Rank
601        if self.compute_effective_rank:
602
603            first_state = next(iter(self._states.values()))
604
605            assert first_state.sum_x is not None
606            assert first_state.sum_xx is not None
607
608            hidden_size = first_state.sum_x.shape[-1]
609            
610            sum_x_cpu  : torch.Tensor = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
611            sum_xx_cpu : torch.Tensor = torch.zeros((self.n_layers, hidden_size, hidden_size), dtype=torch.float64)
612
613            for state in self._states.values() :
614
615                assert state.sum_x is not None
616                assert state.sum_xx is not None
617
618                sum_x_cpu  += state.sum_x.cpu()
619                sum_xx_cpu += state.sum_xx.cpu()
620
621            effective_ranks = []
622            axis_alignment = []
623            for l in range(self.n_layers):
624                
625                if token_count[l] == 0:
626                    effective_ranks.append(float("nan"))
627                    axis_alignment.append(float("nan"))
628                    continue
629                    
630                N = token_count[l].item()
631                mean_x = sum_x_cpu[l] / N
632                
633                # Covariance matrix
634                cov = (sum_xx_cpu[l] / N) - torch.outer(mean_x, mean_x)
635                
636                # Symmetric matrix: eigh gives eigenvalues *and* eigenvectors,
637                # ascending order. The eigenvectors are what let us also check
638                # axis-alignment below, at no extra decomposition cost.
639                eigvals, eigvecs = torch.linalg.eigh(cov)
640                
641                # Relative floor: absolute clamps (e.g. a flat 1e-12) are wrong
642                # once you compare layers whose activation scale differs a lot
643                # (residual-stream norm grows with depth) — a fixed floor is
644                # either a no-op on large-scale layers or an inflating one on
645                # small-scale layers. Floor relative to this layer's own top
646                # eigenvalue instead, with a tiny absolute fallback only to
647                # guard against an all-zero covariance.
648                floor = torch.clamp_min(eigvals.max().clamp_min(0) * 1e-8, 1e-12)
649                eigvals = torch.clamp_min(eigvals, floor)
650                
651                # Normalize to probability distribution
652                p = eigvals / eigvals.sum()
653                
654                # Compute Shannon entropy
655                entropy = -(p * torch.log(p)).sum()
656                eff_rank = torch.exp(entropy).item()
657                
658                effective_ranks.append(eff_rank)
659
660                # Axis alignment: unlike the eigenvalues themselves, the
661                # eigenvectors *do* change under rotation of the underlying
662                # basis, so this is a direct (and much cheaper, since the
663                # decomposition is already in hand) complement to the
664                # kurtosis-based privileged-basis test above. For each
665                # eigenvector, the participation ratio of its own components
666                # (1 / sum(v_i^4), since ||v||=1) is 1 when it points along a
667                # single standard axis and hidden_size when it's spread
668                # evenly across all of them. We average this over eigenvectors,
669                # weighted by their eigenvalue share `p`, so directions that
670                # carry more of the variance dominate the score.
671                eigvec_ipr = 1.0 / eigvecs.pow(4).sum(dim=0).clamp_min(1e-12)
672                axis_alignment.append((p * eigvec_ipr).sum().item())
673
674            results["pb_effective_rank_per_layer"] = effective_ranks
675            results["pb_axis_alignment_per_layer"] = axis_alignment
676
677        # Per-channel statistics (native basis only -- see compute_channel_stats)
678        if self.compute_channel_stats:
679
680            first_state = next(iter(self._states.values()))
681            assert first_state.chan_sum_x is not None
682            hidden_size = first_state.chan_sum_x.shape[-1]
683
684            chan_sum_x  = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
685            chan_sum_x2 = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
686            chan_sum_x3 = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
687            chan_sum_x4 = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
688            chan_sum_x_a  = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
689            chan_sum_x2_a = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
690            chan_sum_x_b  = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
691            chan_sum_x2_b = torch.zeros((self.n_layers, hidden_size), dtype=torch.float64)
692            chan_count_a = torch.zeros(self.n_layers, dtype=torch.float64)
693            chan_count_b = torch.zeros(self.n_layers, dtype=torch.float64)
694
695            for state in self._states.values():
696
697                assert state.chan_sum_x is not None and state.chan_sum_x2 is not None
698                assert state.chan_sum_x3 is not None and state.chan_sum_x4 is not None
699                assert state.chan_sum_x_a is not None and state.chan_sum_x2_a is not None
700                assert state.chan_sum_x_b is not None and state.chan_sum_x2_b is not None
701                assert state.chan_count_a is not None and state.chan_count_b is not None
702
703                chan_sum_x    += state.chan_sum_x.cpu()
704                chan_sum_x2   += state.chan_sum_x2.cpu()
705                chan_sum_x3   += state.chan_sum_x3.cpu()
706                chan_sum_x4   += state.chan_sum_x4.cpu()
707                chan_sum_x_a  += state.chan_sum_x_a.cpu()
708                chan_sum_x2_a += state.chan_sum_x2_a.cpu()
709                chan_sum_x_b  += state.chan_sum_x_b.cpu()
710                chan_sum_x2_b += state.chan_sum_x2_b.cpu()
711                chan_count_a  += state.chan_count_a.cpu()
712                chan_count_b  += state.chan_count_b.cpu()
713
714            channel_mean = torch.full((self.n_layers, hidden_size), float("nan"), dtype=torch.float64)
715            channel_var  = torch.full((self.n_layers, hidden_size), float("nan"), dtype=torch.float64)
716            channel_skew = torch.full((self.n_layers, hidden_size), float("nan"), dtype=torch.float64)
717            channel_kurt = torch.full((self.n_layers, hidden_size), float("nan"), dtype=torch.float64)
718
719            eff_channel_count = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
720            channel_gini = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
721            channel_powerlaw_exponent = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
722            channel_persistence = torch.full((self.n_layers,), float("nan"), dtype=torch.float64)
723
724            n = hidden_size
725            log_rank_desc = torch.log(torch.arange(1, n + 1, dtype=torch.float64))
726            log_rank_desc_centered = log_rank_desc - log_rank_desc.mean()
727            rank_1_to_n = torch.arange(1, n + 1, dtype=torch.float64)
728
729            for l in range(self.n_layers):
730
731                N = token_count[l].item()
732                if N == 0:
733                    continue
734
735                mean_l = chan_sum_x[l] / N
736                e_x2 = chan_sum_x2[l] / N
737                e_x3 = chan_sum_x3[l] / N
738                e_x4 = chan_sum_x4[l] / N
739
740                # Raw -> central moment conversion (single pass at finalize
741                # time, so the usual cancellation risk of this formula is a
742                # non-issue here -- it's not being iterated per chunk).
743                m2 = (e_x2 - mean_l.square()).clamp_min(1e-12)
744                m3 = e_x3 - 3 * mean_l * e_x2 + 2 * mean_l.pow(3)
745                m4 = e_x4 - 4 * mean_l * e_x3 + 6 * mean_l.square() * e_x2 - 3 * mean_l.pow(4)
746
747                channel_mean[l] = mean_l
748                channel_var[l] = m2
749                channel_skew[l] = m3 / m2.pow(1.5)
750                channel_kurt[l] = m4 / m2.square()
751
752                var_l = m2.clamp_min(0)
753                s1, s2 = var_l.sum(), var_l.square().sum()
754
755                # Effective channel count: participation ratio of per-channel
756                # variance. No privileged basis -> variance spread ~uniformly
757                # across channels -> this saturates near hidden_size;
758                # concentration in a few channels pulls it down. Threshold-free
759                # counterpart to "how many outlier channels are there".
760                if s2 > 0:
761                    eff_channel_count[l] = (s1.square() / s2).item()
762
763                # Gini coefficient of per-channel variance (0 = perfectly even
764                # across channels, ~1 = all variance in one channel).
765                sorted_var, _ = torch.sort(var_l)
766                total = sorted_var.sum().clamp_min(1e-24)
767                channel_gini[l] = ((2 * (rank_1_to_n * sorted_var).sum()) / (n * total) - (n + 1) / n).item()
768
769                # Power-law decay exponent of the sorted (descending) variance
770                # curve: fit slope of log(variance) vs log(rank). A shape
771                # descriptor, not a cutoff -- two layers can share a Gini value
772                # while one is a clean power law and the other a step function.
773                desc_var, _ = torch.sort(var_l, descending=True)
774                floor = torch.clamp_min(desc_var.max() * 1e-8, 1e-24)
775                log_v = torch.log(desc_var.clamp_min(floor))
776                log_v_centered = log_v - log_v.mean()
777                slope = (log_rank_desc_centered * log_v_centered).sum() / log_rank_desc_centered.square().sum()
778                channel_powerlaw_exponent[l] = (-slope).item()
779
780                # Persistence: Spearman rank correlation of per-channel variance
781                # between two disjoint halves of the token stream. High ->
782                # same channels dominate dataset-wide (a genuinely fixed
783                # privileged set); low -> the "privileged" channels drift
784                # between batches and the concentration seen above may just be
785                # sampling noise rather than a stable structural feature.
786                na, nb = chan_count_a[l].item(), chan_count_b[l].item()
787                if na > 0 and nb > 0:
788                    mean_a = chan_sum_x_a[l] / na
789                    var_a = (chan_sum_x2_a[l] / na - mean_a.square()).clamp_min(0)
790                    mean_b = chan_sum_x_b[l] / nb
791                    var_b = (chan_sum_x2_b[l] / nb - mean_b.square()).clamp_min(0)
792                    channel_persistence[l] = _spearman(var_a, var_b).item()
793
794            results["pb_effective_channel_count_per_layer"] = eff_channel_count.tolist()
795            results["pb_channel_gini_per_layer"] = channel_gini.tolist()
796            results["pb_channel_powerlaw_exponent_per_layer"] = channel_powerlaw_exponent.tolist()
797            results["pb_channel_persistence_per_layer"] = channel_persistence.tolist()
798
799            # Raw per-channel moment vectors, for on-demand visual inspection
800            # (see plot_channel_concentration) -- not meant to be read as flat
801            # per-layer scalars like the rest of `results`.
802            self.channel_moments = {
803                "mean": channel_mean,
804                "variance": channel_var,
805                "skewness": channel_skew,
806                "kurtosis": channel_kurt,
807            }
808
809        return results

Aggregate metrics across all devices to the CPU and compute statistics.

def plot_channel_concentration(channel_variance: torch.Tensor, label: str = 'layer', ax=None):
813def plot_channel_concentration(channel_variance: torch.Tensor, label: str = "layer", ax=None):
814    """Sorted-magnitude curve of per-channel variance for a single layer
815    (e.g. `evaluator.channel_moments["variance"][layer_idx]`), on log-log axes.
816
817    This is the reality check for the channel-stats summary scalars above: a
818    clean straight line means the power-law exponent is actually describing
819    the shape (a genuine power law); a flat-then-cliff curve means a small,
820    sharply-separated outlier set (matches the effective-channel-count/Gini
821    story cleanly); anything else -- e.g. a single wildly dominant point, or a
822    long plateau with no clear knee -- is a sign the summary scalars for that
823    layer might be misleading (or at least need a specific story), and is
824    worth a look before trusting them. Cheap enough to call routinely on
825    layers that look unremarkable, not just ones that look suspicious.
826
827    Not wired into the evaluator: call it yourself, on whichever layer(s) or
828    checkpoints you want to inspect. Pass the same `ax` across calls to
829    overlay multiple curves (e.g. across layers, or the same layer across
830    training checkpoints) for comparison.
831    """
832    import matplotlib.pyplot as plt
833
834    if ax is None:
835        _, ax = plt.subplots(figsize=(6, 4))
836
837    sorted_desc, _ = torch.sort(channel_variance.detach().float().clamp_min(1e-12), descending=True)
838    rank = torch.arange(1, sorted_desc.shape[0] + 1)
839
840    ax.plot(rank.numpy(), sorted_desc.cpu().numpy(), marker="o", markersize=2, linewidth=1, label=label)
841    ax.set_xscale("log")
842    ax.set_yscale("log")
843    ax.set_xlabel("channel rank (descending variance)")
844    ax.set_ylabel("variance")
845    ax.legend()
846    return ax

Sorted-magnitude curve of per-channel variance for a single layer (e.g. evaluator.channel_moments["variance"][layer_idx]), on log-log axes.

This is the reality check for the channel-stats summary scalars above: a clean straight line means the power-law exponent is actually describing the shape (a genuine power law); a flat-then-cliff curve means a small, sharply-separated outlier set (matches the effective-channel-count/Gini story cleanly); anything else -- e.g. a single wildly dominant point, or a long plateau with no clear knee -- is a sign the summary scalars for that layer might be misleading (or at least need a specific story), and is worth a look before trusting them. Cheap enough to call routinely on layers that look unremarkable, not just ones that look suspicious.

Not wired into the evaluator: call it yourself, on whichever layer(s) or checkpoints you want to inspect. Pass the same ax across calls to overlay multiple curves (e.g. across layers, or the same layer across training checkpoints) for comparison.