Module neuralops.analysis.layerwise_analyzer
Streaming Layer-wise Analysis Engine for Foundation Models & Transformers.
Computes representation drift, cosine similarities, kurtosis-based outlier metrics, effective spectral rank, and intrinsic dimensionality on-the-fly without accumulating all 32+ layer activations simultaneously in VRAM, eliminating streaming OOM crashes.
Functions
def estimate_intrinsic_dimension(hidden_states: np.ndarray, k: int = 5) ‑> float-
Expand source code
def estimate_intrinsic_dimension(hidden_states: np.ndarray, k: int = 5) -> float: """ Estimates the intrinsic dimension of a representation manifold using k-NN distances. Args: hidden_states: Sample representation array of shape [N, D]. k: Neighbor rank for distance estimation. Returns: float: Estimated intrinsic dimension between 1.0 and D. """ hidden_states = np.asarray(hidden_states, dtype=np.float64) if hidden_states.ndim == 1: return 1.0 N, D = hidden_states.shape if k + 1 > N: return float(min(N, D)) from scipy.spatial.distance import pdist, squareform try: dists = squareform(pdist(hidden_states)) np.fill_diagonal(dists, np.inf) kth_dists = np.sort(dists, axis=1)[:, k - 1] valid = kth_dists > 1e-10 if not valid.any(): return float(D) # Maximum likelihood estimation of intrinsic dimension log_ratios = np.log(kth_dists[valid] / (kth_dists[valid][:, None] + 1e-12)) id_estimate = -1.0 / (np.mean(log_ratios) + 1e-12) return float(max(1.0, min(float(D), abs(id_estimate)))) except Exception: return float(min(N, D))Estimates the intrinsic dimension of a representation manifold using k-NN distances.
- Args
- -----=
hidden_states- Sample representation array of shape [N, D].
k- Neighbor rank for distance estimation.
- Returns
- -----=
float- Estimated intrinsic dimension between 1.0 and D.
Classes
class LayerwiseAnalyzer (model: nn.Module, tokenizer: Any, device: str | torch.device | None = None)-
Expand source code
class LayerwiseAnalyzer: """ Streaming Layerwise Analyzer that captures inter-layer representations, outlier distribution spikes, and effective rank without accumulating VRAM memory. """ def __init__( self, model: nn.Module, tokenizer: Any, device: str | torch.device | None = None, ) -> None: self.model = model self.tokenizer = tokenizer self.device = str(device) if device is not None else self._get_device() self.model.to(self.device) self.model.eval() self.layer_accumulators: list[StreamingLayerAccumulator] = [] self.hooks: list[torch.utils.hooks.RemovableHandle] = [] def _get_device(self) -> str: """Detect best available compute device.""" if torch.cuda.is_available(): return "cuda" if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): return "mps" return "cpu" def _find_transformer_layers(self) -> list[tuple[str, nn.Module]]: """Identify candidate Transformer / Encoder / Decoder block modules.""" target_layers = [] for name, module in self.model.named_modules(): name_lower = name.lower() # Match standard transformer layer patterns if any( pattern in name_lower for pattern in [ "encoder.layer.", "transformer.layer.", "transformer.h.", "model.layers.", "layers.", "h.", "decoder.layer.", "transformer.blocks.", ] ): # Filter to only the top-level block (e.g. layers.0, not layers.0.attention) parts = name.split(".") if parts[-1].isdigit(): target_layers.append((name, module)) # Fallback: if no digit-indexed layers found, look for any layer module if not target_layers: for name, module in self.model.named_modules(): if "layer" in name.lower() and isinstance(module, (nn.ModuleList, nn.Sequential)): for sub_idx, sub_mod in enumerate(module): target_layers.append((f"{name}.{sub_idx}", sub_mod)) break return target_layers def register_streaming_hooks(self) -> None: """Registers streaming forward hooks on all model transformer layers.""" self.remove_hooks() layers = self._find_transformer_layers() num_layers = len(layers) self.layer_accumulators = [StreamingLayerAccumulator() for _ in range(num_layers)] for layer_idx, (layer_name, module) in enumerate(layers): def make_hook(idx: int): def hook_fn(mod: nn.Module, inputs: Any, output: Any) -> None: # Extract tensor tensor = output[0] if isinstance(output, tuple) else output if isinstance(tensor, torch.Tensor): # FIX: Update streaming accumulator dynamically and release activation immediately self.layer_accumulators[idx].update(tensor) # Explicitly release references del tensor return hook_fn hook_handle = module.register_forward_hook(make_hook(layer_idx)) self.hooks.append(hook_handle) def remove_hooks(self) -> None: """Remove all active hooks and free memory.""" for hook in self.hooks: hook.remove() self.hooks.clear() if torch.cuda.is_available(): torch.cuda.empty_cache() gc.collect() def analyze(self, texts: list[str], batch_size: int = 8) -> dict[str, Any]: """ Executes streaming layerwise analysis on input text corpus. Args: texts: List of input string prompts/sentences. batch_size: Batch size for tokenization and model inference. Returns: Dict containing cosine similarities, effective ranks, outlier statistics, and least-compressible layer rankings. """ self.register_streaming_hooks() try: for i in range(0, len(texts), batch_size): batch_texts = texts[i : i + batch_size] encoded = self.tokenizer( batch_texts, return_tensors="pt", truncation=True, max_length=128, padding=True, ) inputs = {k: v.to(self.device) for k, v in encoded.items()} with torch.no_grad(): self.model(**inputs) # Cleanup batch inputs del inputs, encoded if torch.cuda.is_available(): torch.cuda.empty_cache() finally: self.remove_hooks() num_layers = len(self.layer_accumulators) if num_layers == 0: return { "num_layers": 0, "cosine_similarities": [], "effective_ranks": [], "max_outlier_magnitudes": [], "outlier_ratios": [], "kurtosis_scores": [], "intrinsic_dimensions": [], } # Extract per-layer mean vectors mean_vectors = [ ( acc.running_mean_vector if acc.running_mean_vector is not None else np.zeros(128, dtype=np.float32) ) for acc in self.layer_accumulators ] # Compute cosine similarity between adjacent layers similarities = [] for i in range(num_layers - 1): v1, v2 = mean_vectors[i], mean_vectors[i + 1] norm1, norm2 = np.linalg.norm(v1), np.linalg.norm(v2) if norm1 > 1e-8 and norm2 > 1e-8: sim = 1.0 - float(cosine(v1, v2)) sim_clamped = max(0.0, min(1.0, sim)) else: sim_clamped = 1.0 similarities.append(sim_clamped) effective_ranks = [acc.compute_effective_rank() for acc in self.layer_accumulators] max_outliers = [acc.max_activation for acc in self.layer_accumulators] outlier_ratios = [acc.compute_outlier_ratio() for acc in self.layer_accumulators] kurtosis_scores = [acc.compute_kurtosis() for acc in self.layer_accumulators] # Estimate intrinsic dimension using mean vectors np.stack(mean_vectors) if num_layers > 1 else np.zeros((1, 128)) intrinsic_dims = [ estimate_intrinsic_dimension( acc.cov_matrix if acc.cov_matrix is not None else np.eye(10), k=3 ) for acc in self.layer_accumulators ] return { "num_layers": num_layers, "cosine_similarities": similarities, "effective_ranks": effective_ranks, "max_outlier_magnitudes": max_outliers, "outlier_ratios": outlier_ratios, "kurtosis_scores": kurtosis_scores, "intrinsic_dimensions": intrinsic_dims, }Streaming Layerwise Analyzer that captures inter-layer representations, outlier distribution spikes, and effective rank without accumulating VRAM memory.
Methods
def analyze(self, texts: list[str], batch_size: int = 8) ‑> dict[str, typing.Any]-
Expand source code
def analyze(self, texts: list[str], batch_size: int = 8) -> dict[str, Any]: """ Executes streaming layerwise analysis on input text corpus. Args: texts: List of input string prompts/sentences. batch_size: Batch size for tokenization and model inference. Returns: Dict containing cosine similarities, effective ranks, outlier statistics, and least-compressible layer rankings. """ self.register_streaming_hooks() try: for i in range(0, len(texts), batch_size): batch_texts = texts[i : i + batch_size] encoded = self.tokenizer( batch_texts, return_tensors="pt", truncation=True, max_length=128, padding=True, ) inputs = {k: v.to(self.device) for k, v in encoded.items()} with torch.no_grad(): self.model(**inputs) # Cleanup batch inputs del inputs, encoded if torch.cuda.is_available(): torch.cuda.empty_cache() finally: self.remove_hooks() num_layers = len(self.layer_accumulators) if num_layers == 0: return { "num_layers": 0, "cosine_similarities": [], "effective_ranks": [], "max_outlier_magnitudes": [], "outlier_ratios": [], "kurtosis_scores": [], "intrinsic_dimensions": [], } # Extract per-layer mean vectors mean_vectors = [ ( acc.running_mean_vector if acc.running_mean_vector is not None else np.zeros(128, dtype=np.float32) ) for acc in self.layer_accumulators ] # Compute cosine similarity between adjacent layers similarities = [] for i in range(num_layers - 1): v1, v2 = mean_vectors[i], mean_vectors[i + 1] norm1, norm2 = np.linalg.norm(v1), np.linalg.norm(v2) if norm1 > 1e-8 and norm2 > 1e-8: sim = 1.0 - float(cosine(v1, v2)) sim_clamped = max(0.0, min(1.0, sim)) else: sim_clamped = 1.0 similarities.append(sim_clamped) effective_ranks = [acc.compute_effective_rank() for acc in self.layer_accumulators] max_outliers = [acc.max_activation for acc in self.layer_accumulators] outlier_ratios = [acc.compute_outlier_ratio() for acc in self.layer_accumulators] kurtosis_scores = [acc.compute_kurtosis() for acc in self.layer_accumulators] # Estimate intrinsic dimension using mean vectors np.stack(mean_vectors) if num_layers > 1 else np.zeros((1, 128)) intrinsic_dims = [ estimate_intrinsic_dimension( acc.cov_matrix if acc.cov_matrix is not None else np.eye(10), k=3 ) for acc in self.layer_accumulators ] return { "num_layers": num_layers, "cosine_similarities": similarities, "effective_ranks": effective_ranks, "max_outlier_magnitudes": max_outliers, "outlier_ratios": outlier_ratios, "kurtosis_scores": kurtosis_scores, "intrinsic_dimensions": intrinsic_dims, }Executes streaming layerwise analysis on input text corpus.
- Args
- -----=
texts- List of input string prompts/sentences.
batch_size- Batch size for tokenization and model inference.
Returns -----= Dict containing cosine similarities, effective ranks, outlier statistics, and least-compressible layer rankings.
def register_streaming_hooks(self) ‑> None-
Expand source code
def register_streaming_hooks(self) -> None: """Registers streaming forward hooks on all model transformer layers.""" self.remove_hooks() layers = self._find_transformer_layers() num_layers = len(layers) self.layer_accumulators = [StreamingLayerAccumulator() for _ in range(num_layers)] for layer_idx, (layer_name, module) in enumerate(layers): def make_hook(idx: int): def hook_fn(mod: nn.Module, inputs: Any, output: Any) -> None: # Extract tensor tensor = output[0] if isinstance(output, tuple) else output if isinstance(tensor, torch.Tensor): # FIX: Update streaming accumulator dynamically and release activation immediately self.layer_accumulators[idx].update(tensor) # Explicitly release references del tensor return hook_fn hook_handle = module.register_forward_hook(make_hook(layer_idx)) self.hooks.append(hook_handle)Registers streaming forward hooks on all model transformer layers.
def remove_hooks(self) ‑> None-
Expand source code
def remove_hooks(self) -> None: """Remove all active hooks and free memory.""" for hook in self.hooks: hook.remove() self.hooks.clear() if torch.cuda.is_available(): torch.cuda.empty_cache() gc.collect()Remove all active hooks and free memory.
class StreamingLayerAccumulator (total_tokens: int = 0,
total_batches: int = 0,
running_mean_vector: np.ndarray | None = None,
max_activation: float = 0.0,
sum_outlier_count_6sigma: int = 0,
total_elements: int = 0,
sum_m2: float = 0.0,
sum_m4: float = 0.0,
cov_matrix: np.ndarray | None = None,
cov_samples: int = 0)-
Expand source code
@dataclass class StreamingLayerAccumulator: """ Online Welford & spectral accumulator for single-layer activations. Updates statistical moments dynamically and frees raw activation buffers immediately. """ total_tokens: int = 0 total_batches: int = 0 running_mean_vector: np.ndarray | None = None max_activation: float = 0.0 sum_outlier_count_6sigma: int = 0 total_elements: int = 0 # Online moments for kurtosis and variance sum_m2: float = 0.0 sum_m4: float = 0.0 # Covariance sketch for effective rank (subsampled rank estimation) cov_matrix: np.ndarray | None = None cov_samples: int = 0 def update(self, act_tensor: torch.Tensor) -> None: """ Dynamically updates moments from an incoming activation batch [B, S, D] or [N, D] and releases tensor references. """ # FIX: Compute outlier and spectral metrics dynamically per layer and immediately free activations with torch.no_grad(): act_f32 = act_tensor.detach().float() # Flatten to [N, D] if act_f32.ndim == 3: b, s, d = act_f32.shape flat_act = act_f32.reshape(-1, d) elif act_f32.ndim == 2: flat_act = act_f32 d = flat_act.shape[1] else: flat_act = act_f32.reshape(-1, act_f32.shape[-1]) d = flat_act.shape[-1] n_tokens = flat_act.shape[0] if n_tokens == 0: return # 1. Update running mean representation vector (CPU numpy) batch_mean = flat_act.mean(dim=0).cpu().numpy() if self.running_mean_vector is None: self.running_mean_vector = batch_mean.copy() else: self.running_mean_vector = self.running_mean_vector * ( self.total_tokens / (self.total_tokens + n_tokens) ) + batch_mean * (n_tokens / (self.total_tokens + n_tokens)) # 2. Update Peak Outlier Magnitude batch_max = float(flat_act.abs().max().item()) self.max_activation = max(self.max_activation, batch_max) # 3. Update Outlier Ratio & Kurtosis Moments act_cpu = flat_act.cpu().numpy() global_elem = act_cpu.size self.total_elements += global_elem mu = float(np.mean(act_cpu)) centered = act_cpu - mu m2 = float(np.mean(centered**2)) m4 = float(np.mean(centered**4)) self.sum_m2 += m2 * global_elem self.sum_m4 += m4 * global_elem std = np.sqrt(max(1e-12, m2)) outliers = np.sum(np.abs(centered) > (6.0 * std)) self.sum_outlier_count_6sigma += int(outliers) # 4. Running Subsampled Covariance for Effective Rank sub_n = min(n_tokens, 256) sub_act = act_cpu[:sub_n] - batch_mean batch_cov = (sub_act.T @ sub_act) / max(1, sub_n) if self.cov_matrix is None: self.cov_matrix = batch_cov self.cov_samples = sub_n else: total_s = self.cov_samples + sub_n self.cov_matrix = self.cov_matrix * (self.cov_samples / total_s) + batch_cov * ( sub_n / total_s ) self.cov_samples = total_s self.total_tokens += n_tokens self.total_batches += 1 def compute_kurtosis(self) -> float: """Compute excess kurtosis: E[(x-mu)^4] / (E[(x-mu)^2])^2 - 3.""" if self.total_elements == 0 or self.sum_m2 == 0: return 0.0 m2 = self.sum_m2 / self.total_elements m4 = self.sum_m4 / self.total_elements var_sq = m2**2 if var_sq < 1e-12: return 0.0 return float((m4 / var_sq) - 3.0) def compute_outlier_ratio(self) -> float: """Compute 6-sigma outlier fraction.""" if self.total_elements == 0: return 0.0 return float(self.sum_outlier_count_6sigma / self.total_elements) def compute_effective_rank(self) -> float: """ Computes the effective rank erank(Sigma) = exp(H(p)) where p_i = lambda_i / sum(lambda). """ if self.cov_matrix is None: return 1.0 try: eigvals = np.linalg.eigvalsh(self.cov_matrix) eigvals = np.maximum(eigvals, 1e-12) eigvals_norm = eigvals / np.sum(eigvals) entropy = -np.sum(eigvals_norm * np.log(eigvals_norm + 1e-12)) return float(np.exp(entropy)) except Exception: return 1.0Online Welford & spectral accumulator for single-layer activations. Updates statistical moments dynamically and frees raw activation buffers immediately.
Instance variables
var cov_matrix : numpy.ndarray | None-
The type of the None singleton.
var cov_samples : int-
The type of the None singleton.
var max_activation : float-
The type of the None singleton.
var running_mean_vector : numpy.ndarray | None-
The type of the None singleton.
var sum_m2 : float-
The type of the None singleton.
var sum_m4 : float-
The type of the None singleton.
var sum_outlier_count_6sigma : int-
The type of the None singleton.
var total_batches : int-
The type of the None singleton.
var total_elements : int-
The type of the None singleton.
var total_tokens : int-
The type of the None singleton.
Methods
def compute_effective_rank(self) ‑> float-
Expand source code
def compute_effective_rank(self) -> float: """ Computes the effective rank erank(Sigma) = exp(H(p)) where p_i = lambda_i / sum(lambda). """ if self.cov_matrix is None: return 1.0 try: eigvals = np.linalg.eigvalsh(self.cov_matrix) eigvals = np.maximum(eigvals, 1e-12) eigvals_norm = eigvals / np.sum(eigvals) entropy = -np.sum(eigvals_norm * np.log(eigvals_norm + 1e-12)) return float(np.exp(entropy)) except Exception: return 1.0Computes the effective rank erank(Sigma) = exp(H(p)) where p_i = lambda_i / sum(lambda).
def compute_kurtosis(self) ‑> float-
Expand source code
def compute_kurtosis(self) -> float: """Compute excess kurtosis: E[(x-mu)^4] / (E[(x-mu)^2])^2 - 3.""" if self.total_elements == 0 or self.sum_m2 == 0: return 0.0 m2 = self.sum_m2 / self.total_elements m4 = self.sum_m4 / self.total_elements var_sq = m2**2 if var_sq < 1e-12: return 0.0 return float((m4 / var_sq) - 3.0)Compute excess kurtosis: E[(x-mu)^4] / (E[(x-mu)^2])^2 - 3.
def compute_outlier_ratio(self) ‑> float-
Expand source code
def compute_outlier_ratio(self) -> float: """Compute 6-sigma outlier fraction.""" if self.total_elements == 0: return 0.0 return float(self.sum_outlier_count_6sigma / self.total_elements)Compute 6-sigma outlier fraction.
def update(self, act_tensor: torch.Tensor) ‑> None-
Expand source code
def update(self, act_tensor: torch.Tensor) -> None: """ Dynamically updates moments from an incoming activation batch [B, S, D] or [N, D] and releases tensor references. """ # FIX: Compute outlier and spectral metrics dynamically per layer and immediately free activations with torch.no_grad(): act_f32 = act_tensor.detach().float() # Flatten to [N, D] if act_f32.ndim == 3: b, s, d = act_f32.shape flat_act = act_f32.reshape(-1, d) elif act_f32.ndim == 2: flat_act = act_f32 d = flat_act.shape[1] else: flat_act = act_f32.reshape(-1, act_f32.shape[-1]) d = flat_act.shape[-1] n_tokens = flat_act.shape[0] if n_tokens == 0: return # 1. Update running mean representation vector (CPU numpy) batch_mean = flat_act.mean(dim=0).cpu().numpy() if self.running_mean_vector is None: self.running_mean_vector = batch_mean.copy() else: self.running_mean_vector = self.running_mean_vector * ( self.total_tokens / (self.total_tokens + n_tokens) ) + batch_mean * (n_tokens / (self.total_tokens + n_tokens)) # 2. Update Peak Outlier Magnitude batch_max = float(flat_act.abs().max().item()) self.max_activation = max(self.max_activation, batch_max) # 3. Update Outlier Ratio & Kurtosis Moments act_cpu = flat_act.cpu().numpy() global_elem = act_cpu.size self.total_elements += global_elem mu = float(np.mean(act_cpu)) centered = act_cpu - mu m2 = float(np.mean(centered**2)) m4 = float(np.mean(centered**4)) self.sum_m2 += m2 * global_elem self.sum_m4 += m4 * global_elem std = np.sqrt(max(1e-12, m2)) outliers = np.sum(np.abs(centered) > (6.0 * std)) self.sum_outlier_count_6sigma += int(outliers) # 4. Running Subsampled Covariance for Effective Rank sub_n = min(n_tokens, 256) sub_act = act_cpu[:sub_n] - batch_mean batch_cov = (sub_act.T @ sub_act) / max(1, sub_n) if self.cov_matrix is None: self.cov_matrix = batch_cov self.cov_samples = sub_n else: total_s = self.cov_samples + sub_n self.cov_matrix = self.cov_matrix * (self.cov_samples / total_s) + batch_cov * ( sub_n / total_s ) self.cov_samples = total_s self.total_tokens += n_tokens self.total_batches += 1Dynamically updates moments from an incoming activation batch [B, S, D] or [N, D] and releases tensor references.