Module neuralops.compression_engine.dynamic_rank
Dynamic Entropy Singular Value Decomposition (Dynamic Entropy SVD) Compressor.
Implements singular entropy computation over empirical layer activation covariances:
$$H(\Sigma_l) = -\sum_{i=1}^{d} p_i \ln(p_i), \quad p_i = \frac{\sigma_i}{\sum_j \sigma_j}$$
Dynamically budgets layer ranks $r_l \in [r_{\min}, r_{\max}]$ proportional to singular entropy,
subject to global rank or parameter budgets, and replaces dense linear layers with low-rank
factorized DynamicSVDLinear modules.
Functions
def allocate_entropy_ranks(entropies: dict[str, float],
min_rank: int = 16,
max_rank: int = 128,
total_rank_budget: int | None = None,
total_param_budget: int | None = None,
target_param_ratio: float | None = None,
layer_dims: dict[str, tuple[int, int]] | None = None) ‑> dict[str, int]-
Expand source code
def allocate_entropy_ranks( entropies: dict[str, float], min_rank: int = 16, max_rank: int = 128, total_rank_budget: int | None = None, total_param_budget: int | None = None, target_param_ratio: float | None = None, layer_dims: dict[str, tuple[int, int]] | None = None, ) -> dict[str, int]: """ Dynamically allocate low-rank budgets r_l in [min_rank, max_rank] across transformer layers proportional to singular entropy H(Sigma_l), constrained by total rank or parameter budget. Args: entropies: Dictionary mapping layer names to their singular entropy values. min_rank: Lower bound for per-layer rank. max_rank: Upper bound for per-layer rank. total_rank_budget: Optional total sum of ranks across all layers. total_param_budget: Optional total parameter budget for factorized layers. target_param_ratio: Optional parameter retention ratio in (0, 1]. layer_dims: Optional dictionary mapping layer names to (out_features, in_features). Returns: Dict[str, int]: Allocated rank for each layer. """ if not entropies: return {} min_rank = max(16, int(min_rank)) layer_names = list(entropies.keys()) len(layer_names) # Determine per-layer physical dimension bounds with minimum rank floor per_layer_min = {} per_layer_max = {} for name in layer_names: if layer_dims and name in layer_dims: out_f, in_f = layer_dims[name] phys_max = min(out_f, in_f) else: phys_max = max_rank l_min = max(1, min(max(16, min_rank), phys_max)) l_max = max(l_min, min(max_rank, phys_max)) per_layer_min[name] = l_min per_layer_max[name] = l_max # Compute baseline entropy weights raw_weights = {name: max(entropies[name], 1e-6) for name in layer_names} total_entropy = sum(raw_weights.values()) # Case 1: Parameter Budget Constraint if (total_param_budget is not None or target_param_ratio is not None) and layer_dims: total_orig_params = sum(layer_dims[name][0] * layer_dims[name][1] for name in layer_names) if target_param_ratio is not None: effective_param_budget = int(total_orig_params * target_param_ratio) else: effective_param_budget = int(total_param_budget) # Binary search for parameter budget water-filling # Parameter cost for layer l at rank r is r * (d_in + d_out) low_scale, high_scale = 0.0, 1e6 best_ranks = {} for _ in range(60): mid_scale = (low_scale + high_scale) / 2.0 candidate_ranks = {} current_params = 0 for name in layer_names: out_f, in_f = layer_dims[name] param_cost_per_rank = out_f + in_f # Proportional rank candidate target_r = int(round(mid_scale * (raw_weights[name] / total_entropy) * 100.0)) r_clamped = max(per_layer_min[name], min(per_layer_max[name], target_r)) candidate_ranks[name] = r_clamped current_params += r_clamped * param_cost_per_rank if current_params <= effective_param_budget: best_ranks = candidate_ranks low_scale = mid_scale else: high_scale = mid_scale if not best_ranks: best_ranks = {name: per_layer_min[name] for name in layer_names} return best_ranks # Case 2: Total Rank Budget Constraint if total_rank_budget is None: # Default total budget: average of min_rank and max_rank across layers total_rank_budget = sum( (per_layer_min[name] + per_layer_max[name]) // 2 for name in layer_names ) min_possible_sum = sum(per_layer_min.values()) max_possible_sum = sum(per_layer_max.values()) target_budget = max(min_possible_sum, min(max_possible_sum, total_rank_budget)) # RESEARCH FIX: Iterative bisection water-filling for exact integer rank distribution low, high = 0.0, float(target_budget * 2) allocated_ranks = {} for _ in range(50): mid = (low + high) / 2.0 candidate_ranks = {} candidate_sum = 0 for name in layer_names: w = raw_weights[name] / total_entropy r = int(round(mid * w)) r_clamped = max(per_layer_min[name], min(per_layer_max[name], r)) candidate_ranks[name] = r_clamped candidate_sum += r_clamped allocated_ranks = candidate_ranks if candidate_sum == target_budget: break elif candidate_sum < target_budget: low = mid else: high = mid # Residual greedy adjustment to ensure exact budget match if slack remains current_sum = sum(allocated_ranks.values()) diff = target_budget - current_sum if diff > 0: # Greedily allocate remaining ranks to layers with highest entropy that are below per_layer_max sorted_by_entropy = sorted(layer_names, key=lambda n: raw_weights[n], reverse=True) while diff > 0: progress = False for name in sorted_by_entropy: if allocated_ranks[name] < per_layer_max[name] and diff > 0: allocated_ranks[name] += 1 diff -= 1 progress = True if not progress: break elif diff < 0: # Greedily subtract ranks from layers with lowest entropy that are above per_layer_min sorted_by_entropy = sorted(layer_names, key=lambda n: raw_weights[n]) while diff < 0: progress = False for name in sorted_by_entropy: if allocated_ranks[name] > per_layer_min[name] and diff < 0: allocated_ranks[name] -= 1 diff += 1 progress = True if not progress: break return allocated_ranksDynamically allocate low-rank budgets r_l in [min_rank, max_rank] across transformer layers proportional to singular entropy H(Sigma_l), constrained by total rank or parameter budget.
- Args
- -----=
entropies- Dictionary mapping layer names to their singular entropy values.
min_rank- Lower bound for per-layer rank.
max_rank- Upper bound for per-layer rank.
total_rank_budget- Optional total sum of ranks across all layers.
total_param_budget- Optional total parameter budget for factorized layers.
target_param_ratio- Optional parameter retention ratio in (0, 1].
layer_dims- Optional dictionary mapping layer names to (out_features, in_features).
- Returns
- -----=
Dict[str, int]- Allocated rank for each layer.
def compute_singular_entropy(spectrum_or_matrix: torch.Tensor, eps: float = 1e-12, normalize: bool = False) ‑> float-
Expand source code
def compute_singular_entropy( spectrum_or_matrix: torch.Tensor, eps: float = 1e-12, normalize: bool = False, ) -> float: """ Compute Singular Entropy H(Sigma) = -sum(p_i * log(p_i)) of a covariance matrix, weight matrix, or 1D singular value spectrum tensor. Args: spectrum_or_matrix: 1D singular values tensor, or 2D covariance/weight matrix. eps: Small numerical stabilizer to avoid log(0) and division by zero. normalize: If True, normalize entropy by ln(dim) to bound in [0.0, 1.0]. Returns: float: Computed singular entropy H(Sigma) >= 0.0. """ if not isinstance(spectrum_or_matrix, torch.Tensor): spectrum_or_matrix = torch.tensor(spectrum_or_matrix, dtype=torch.float32) spectrum_or_matrix = spectrum_or_matrix.detach().float() if spectrum_or_matrix.ndim == 1: singular_values = spectrum_or_matrix elif spectrum_or_matrix.ndim == 2: # Check if square and symmetric (covariance matrix) or rectangular (weight matrix) r, c = spectrum_or_matrix.shape if r == c: try: # RESEARCH FIX: Use eigvalsh for symmetric covariance matrices to avoid SVD overhead eigenvals = torch.linalg.eigvalsh(spectrum_or_matrix) singular_values = torch.clamp(eigenvals, min=0.0) except RuntimeError: singular_values = torch.linalg.svdvals(spectrum_or_matrix) else: singular_values = torch.linalg.svdvals(spectrum_or_matrix) else: raise ValueError( f"compute_singular_entropy expects 1D or 2D tensor, got shape {spectrum_or_matrix.shape}" ) # RESEARCH FIX: Filter out non-positive singular values before probability normalization valid_sv = singular_values[singular_values > eps] if valid_sv.numel() == 0: return 0.0 total_sum = valid_sv.sum() if total_sum <= eps: return 0.0 # Normalized singular probability mass function p_i = sigma_i / sum(sigma_j) probs = valid_sv / total_sum probs = torch.clamp(probs, min=eps, max=1.0) entropy = -torch.sum(probs * torch.log(probs)).item() if normalize and valid_sv.numel() > 1: max_entropy = math.log(float(valid_sv.numel())) if max_entropy > 0.0: entropy = entropy / max_entropy return max(0.0, float(entropy))Compute Singular Entropy H(Sigma) = -sum(p_i * log(p_i)) of a covariance matrix, weight matrix, or 1D singular value spectrum tensor.
- Args
- -----=
spectrum_or_matrix- 1D singular values tensor, or 2D covariance/weight matrix.
eps- Small numerical stabilizer to avoid log(0) and division by zero.
normalize- If True, normalize entropy by ln(dim) to bound in [0.0, 1.0].
- Returns
- -----=
float- Computed singular entropy H(Sigma) >= 0.0.
def dynamic_entropy_inject(model: Any = None, layer: Any = None, **kwargs: Any) ‑> Any-
Expand source code
def dynamic_entropy_inject(model: Any = None, layer: Any = None, **kwargs: Any) -> Any: """Safe injection hook alias.""" return inject_dynamic_entropy(model, layer, **kwargs)Safe injection hook alias.
def dynamic_entropy_svd_factory(model: Any, comp_config: Any = None, **kwargs: Any) ‑> Any-
Expand source code
@COMPRESSOR_REGISTRY.register("dynamic_entropy") @COMPRESSOR_REGISTRY.register("dynamic_entropy_svd") @COMPRESSOR_REGISTRY.register("dynamic_rank") @COMPRESSOR_REGISTRY.register("dynamic_entropy_factory") @COMPRESSOR_REGISTRY.register("dynamic_entropy_svd_factory") @COMPRESSOR_REGISTRY.register("dynamic_rank_factory") @COMPRESSOR_REGISTRY.register("dynamic_entropy_compressor") @COMPRESSOR_REGISTRY.register("dynamic_entropy_svd_compressor") @COMPRESSOR_REGISTRY.register("dynamic_rank_compressor") @COMPRESSOR_REGISTRY.register("entropy_rank_allocator") def dynamic_entropy_svd_factory(model: Any, comp_config: Any = None, **kwargs: Any) -> Any: """Dynamic Entropy SVD factory function.""" return_stats = kwargs.pop("return_stats", kwargs.pop("return_tuple", False)) try: min_rank = kwargs.pop( "min_rank", _cfg_get(comp_config, "dynamic_min_rank", _cfg_get(comp_config, "min_rank", 16)), ) max_rank = kwargs.pop( "max_rank", _cfg_get(comp_config, "dynamic_max_rank", _cfg_get(comp_config, "max_rank", 128)), ) target_rank = kwargs.pop( "rank", _cfg_get(comp_config, "dynamic_rank", _cfg_get(comp_config, "rank", None)), ) max_error = kwargs.pop( "max_error", _cfg_get( comp_config, "dynamic_max_error", _cfg_get(comp_config, "max_error", 0.35), ), ) target_attention_only = kwargs.pop( "target_attention_only", _cfg_get( comp_config, "dynamic_target_attention_only", _cfg_get(comp_config, "target_attention_only", False), ), ) fallback = kwargs.pop( "fallback_to_original", _cfg_get( comp_config, "dynamic_fallback_to_original", _cfg_get(comp_config, "fallback_to_original", True), ), ) target_param_ratio = kwargs.pop( "target_param_ratio", _cfg_get(comp_config, "target_param_ratio", None) ) total_rank_budget = kwargs.pop( "total_rank_budget", _cfg_get(comp_config, "total_rank_budget", None) ) compressor = DynamicEntropySVDCompressor( model, min_rank=min_rank, max_rank=max_rank, target_rank=target_rank, max_error=max_error, target_attention_only=target_attention_only, fallback_to_original=fallback, target_param_ratio=target_param_ratio, total_rank_budget=total_rank_budget, cfg=comp_config, **kwargs, ) compressed_model, stats = compressor.compress() try: compressed_model.compression_stats = stats except Exception: pass if return_stats: return compressed_model, stats return compressed_model except Exception as e: logger.error("dynamic_entropy_svd_factory failed: %s", e, exc_info=True) return (model, {}) if return_stats else modelDynamic Entropy SVD factory function.
def dynamic_rank_factory(model: Any, comp_config: Any = None, **kwargs: Any) ‑> Any-
Expand source code
@COMPRESSOR_REGISTRY.register("dynamic_entropy") @COMPRESSOR_REGISTRY.register("dynamic_entropy_svd") @COMPRESSOR_REGISTRY.register("dynamic_rank") @COMPRESSOR_REGISTRY.register("dynamic_entropy_factory") @COMPRESSOR_REGISTRY.register("dynamic_entropy_svd_factory") @COMPRESSOR_REGISTRY.register("dynamic_rank_factory") @COMPRESSOR_REGISTRY.register("dynamic_entropy_compressor") @COMPRESSOR_REGISTRY.register("dynamic_entropy_svd_compressor") @COMPRESSOR_REGISTRY.register("dynamic_rank_compressor") @COMPRESSOR_REGISTRY.register("entropy_rank_allocator") def dynamic_entropy_svd_factory(model: Any, comp_config: Any = None, **kwargs: Any) -> Any: """Dynamic Entropy SVD factory function.""" return_stats = kwargs.pop("return_stats", kwargs.pop("return_tuple", False)) try: min_rank = kwargs.pop( "min_rank", _cfg_get(comp_config, "dynamic_min_rank", _cfg_get(comp_config, "min_rank", 16)), ) max_rank = kwargs.pop( "max_rank", _cfg_get(comp_config, "dynamic_max_rank", _cfg_get(comp_config, "max_rank", 128)), ) target_rank = kwargs.pop( "rank", _cfg_get(comp_config, "dynamic_rank", _cfg_get(comp_config, "rank", None)), ) max_error = kwargs.pop( "max_error", _cfg_get( comp_config, "dynamic_max_error", _cfg_get(comp_config, "max_error", 0.35), ), ) target_attention_only = kwargs.pop( "target_attention_only", _cfg_get( comp_config, "dynamic_target_attention_only", _cfg_get(comp_config, "target_attention_only", False), ), ) fallback = kwargs.pop( "fallback_to_original", _cfg_get( comp_config, "dynamic_fallback_to_original", _cfg_get(comp_config, "fallback_to_original", True), ), ) target_param_ratio = kwargs.pop( "target_param_ratio", _cfg_get(comp_config, "target_param_ratio", None) ) total_rank_budget = kwargs.pop( "total_rank_budget", _cfg_get(comp_config, "total_rank_budget", None) ) compressor = DynamicEntropySVDCompressor( model, min_rank=min_rank, max_rank=max_rank, target_rank=target_rank, max_error=max_error, target_attention_only=target_attention_only, fallback_to_original=fallback, target_param_ratio=target_param_ratio, total_rank_budget=total_rank_budget, cfg=comp_config, **kwargs, ) compressed_model, stats = compressor.compress() try: compressed_model.compression_stats = stats except Exception: pass if return_stats: return compressed_model, stats return compressed_model except Exception as e: logger.error("dynamic_entropy_svd_factory failed: %s", e, exc_info=True) return (model, {}) if return_stats else modelDynamic Entropy SVD factory function.
def dynamic_rank_inject(model: Any = None, layer: Any = None, **kwargs: Any) ‑> Any-
Expand source code
def dynamic_rank_inject(model: Any = None, layer: Any = None, **kwargs: Any) -> Any: """Safe injection hook alias for dynamic_rank.""" return inject_dynamic_entropy(model, layer, **kwargs)Safe injection hook alias for dynamic_rank.
def entropy_rank_allocator_factory(model: Any, comp_config: Any = None, **kwargs: Any) ‑> Any-
Expand source code
@COMPRESSOR_REGISTRY.register("dynamic_entropy") @COMPRESSOR_REGISTRY.register("dynamic_entropy_svd") @COMPRESSOR_REGISTRY.register("dynamic_rank") @COMPRESSOR_REGISTRY.register("dynamic_entropy_factory") @COMPRESSOR_REGISTRY.register("dynamic_entropy_svd_factory") @COMPRESSOR_REGISTRY.register("dynamic_rank_factory") @COMPRESSOR_REGISTRY.register("dynamic_entropy_compressor") @COMPRESSOR_REGISTRY.register("dynamic_entropy_svd_compressor") @COMPRESSOR_REGISTRY.register("dynamic_rank_compressor") @COMPRESSOR_REGISTRY.register("entropy_rank_allocator") def dynamic_entropy_svd_factory(model: Any, comp_config: Any = None, **kwargs: Any) -> Any: """Dynamic Entropy SVD factory function.""" return_stats = kwargs.pop("return_stats", kwargs.pop("return_tuple", False)) try: min_rank = kwargs.pop( "min_rank", _cfg_get(comp_config, "dynamic_min_rank", _cfg_get(comp_config, "min_rank", 16)), ) max_rank = kwargs.pop( "max_rank", _cfg_get(comp_config, "dynamic_max_rank", _cfg_get(comp_config, "max_rank", 128)), ) target_rank = kwargs.pop( "rank", _cfg_get(comp_config, "dynamic_rank", _cfg_get(comp_config, "rank", None)), ) max_error = kwargs.pop( "max_error", _cfg_get( comp_config, "dynamic_max_error", _cfg_get(comp_config, "max_error", 0.35), ), ) target_attention_only = kwargs.pop( "target_attention_only", _cfg_get( comp_config, "dynamic_target_attention_only", _cfg_get(comp_config, "target_attention_only", False), ), ) fallback = kwargs.pop( "fallback_to_original", _cfg_get( comp_config, "dynamic_fallback_to_original", _cfg_get(comp_config, "fallback_to_original", True), ), ) target_param_ratio = kwargs.pop( "target_param_ratio", _cfg_get(comp_config, "target_param_ratio", None) ) total_rank_budget = kwargs.pop( "total_rank_budget", _cfg_get(comp_config, "total_rank_budget", None) ) compressor = DynamicEntropySVDCompressor( model, min_rank=min_rank, max_rank=max_rank, target_rank=target_rank, max_error=max_error, target_attention_only=target_attention_only, fallback_to_original=fallback, target_param_ratio=target_param_ratio, total_rank_budget=total_rank_budget, cfg=comp_config, **kwargs, ) compressed_model, stats = compressor.compress() try: compressed_model.compression_stats = stats except Exception: pass if return_stats: return compressed_model, stats return compressed_model except Exception as e: logger.error("dynamic_entropy_svd_factory failed: %s", e, exc_info=True) return (model, {}) if return_stats else modelDynamic Entropy SVD factory function.
def inject_dynamic_entropy(model: Any = None, layer: Any = None, **kwargs: Any) ‑> Any-
Expand source code
@COMPRESSOR_REGISTRY.register("inject_dynamic_entropy") @COMPRESSOR_REGISTRY.register("dynamic_entropy_inject") def inject_dynamic_entropy(model: Any = None, layer: Any = None, **kwargs: Any) -> Any: """Safe injection hook for dynamic_entropy. Returns None on failure.""" target = layer if layer is not None else model if target is None: return None try: res = dynamic_entropy_svd_factory(target, **kwargs) if isinstance(res, tuple): return res[0] return res except Exception as e: logger.error("inject_dynamic_entropy failed: %s", e, exc_info=True) return NoneSafe injection hook for dynamic_entropy. Returns None on failure.
def inject_dynamic_rank(model: Any = None, layer: Any = None, **kwargs: Any) ‑> Any-
Expand source code
@COMPRESSOR_REGISTRY.register("inject_dynamic_rank") @COMPRESSOR_REGISTRY.register("dynamic_rank_inject") def inject_dynamic_rank(model: Any = None, layer: Any = None, **kwargs: Any) -> Any: """Safe injection hook alias for dynamic_rank.""" return inject_dynamic_entropy(model, layer, **kwargs)Safe injection hook alias for dynamic_rank.
Classes
class DynamicEntropySVDCompressor (model: nn.Module,
min_rank: int = 16,
max_rank: int = 128,
target_param_ratio: float | None = None,
total_rank_budget: int | None = None,
target_rank: int | None = None,
max_error: float = 0.35,
target_attention_only: bool = False,
fallback_to_original: bool = True,
calibration_loader: Any = None,
calibration_texts: list[str] | None = None,
target_modules: list[str] | None = None,
tokenizer: Any | None = None,
cfg: Any | None = None,
**kwargs: Any)-
Expand source code
class DynamicEntropySVDCompressor(BaseCompressor): """ Dynamic Entropy SVD Compressor. 1. Computes singular entropy H(Sigma_l) on layer activation covariances or weights. 2. Allocates layer ranks within [min_rank, max_rank] subject to parameter/rank budget. 3. Replaces dense Linear/Conv1D layers with DynamicSVDLinear factorized modules. 4. Provides relative Frobenius error validation, max_error threshold, and safe fallback. 5. Freezes fallback and non-target layers during recovery. """ def __init__( self, model: nn.Module, min_rank: int = 16, max_rank: int = 128, target_param_ratio: float | None = None, total_rank_budget: int | None = None, target_rank: int | None = None, max_error: float = 0.35, target_attention_only: bool = False, fallback_to_original: bool = True, calibration_loader: Any = None, calibration_texts: list[str] | None = None, target_modules: list[str] | None = None, tokenizer: Any | None = None, cfg: Any | None = None, **kwargs: Any, ) -> None: super().__init__(model, name="dynamic_entropy_svd", **kwargs) cfg_obj = cfg if cfg is not None else kwargs.get("comp_config") self.cfg = cfg_obj min_rank = _cfg_get(cfg_obj, "dynamic_min_rank", _cfg_get(cfg_obj, "min_rank", min_rank)) max_rank = _cfg_get(cfg_obj, "dynamic_max_rank", _cfg_get(cfg_obj, "max_rank", max_rank)) target_rank = _cfg_get(cfg_obj, "dynamic_rank", _cfg_get(cfg_obj, "rank", target_rank)) total_rank_budget = _cfg_get(cfg_obj, "total_rank_budget", total_rank_budget) target_param_ratio = _cfg_get(cfg_obj, "target_param_ratio", target_param_ratio) max_error = _cfg_get( cfg_obj, "dynamic_max_error", _cfg_get(cfg_obj, "max_error", max_error) ) target_attention_only = _cfg_get( cfg_obj, "dynamic_target_attention_only", _cfg_get(cfg_obj, "target_attention_only", target_attention_only), ) fallback_to_original = _cfg_get( cfg_obj, "dynamic_fallback_to_original", _cfg_get(cfg_obj, "fallback_to_original", fallback_to_original), ) min_rank = max(16, int(min_rank)) if "rank" in kwargs and kwargs["rank"] is not None: target_rank = kwargs["rank"] min_rank = max(16, min(min_rank, target_rank)) max_rank = max(max_rank, target_rank) self.min_rank = min_rank self.max_rank = max(min_rank, int(max_rank)) self.target_param_ratio = ( float(target_param_ratio) if target_param_ratio is not None else None ) self.total_rank_budget = int(total_rank_budget) if total_rank_budget is not None else None self.target_rank = int(target_rank) if target_rank is not None else None if max_error is None or max_error == 0.35: max_error = adaptive_max_error( self.target_rank or self.min_rank, 768, 768, "distilbert" ) self.max_error = float(max_error) self.target_attention_only = bool(target_attention_only) self.fallback_to_original = bool(fallback_to_original) self.calibration_loader = calibration_loader self.calibration_texts = calibration_texts self.target_modules = target_modules self.tokenizer = ( tokenizer or getattr(model, "tokenizer", None) or getattr(getattr(model, "config", None), "tokenizer", None) ) self.layer_entropies: dict[str, float] = {} self.layer_ranks: dict[str, int] = {} self.layer_errors: dict[str, float] = {} self.compressed_layers: list[str] = [] self.fallback_layers: list[str] = [] self.skipped_layers: list[str] = [] def _is_target_module(self, name: str, module: nn.Module) -> bool: """Check whether module is a compressible nn.Linear or Conv1D matching target filter.""" name_lower = name.lower() if any(part in name.split(".") for part in ["lora_A", "lora_B", "base_layer"]): return False if any( skip in name_lower for skip in [ "classifier", "score", "lm_head", "pooler", "embedding", "router", "gate", ] ): return False target_mod = getattr(module, "base_layer", module) cls_name = target_mod.__class__.__name__.lower() is_linear = isinstance(target_mod, nn.Linear) is_conv1d = "conv1d" in cls_name or hasattr(target_mod, "nf") if not (is_linear or is_conv1d): return False if isinstance(target_mod, DynamicSVDLinear) or isinstance(module, DynamicSVDLinear): return False if not should_compress_linear(self.model, name): return False raw_w = target_mod.weight.data if min(raw_w.shape[0], raw_w.shape[1]) < 16: return False if self.target_attention_only: last_part = name.split(".")[-1] is_attn = (last_part in _ATTN_NAMES) or any( k in name_lower for k in ("attn", "attention") ) if not is_attn: return False if self.target_modules is not None: return any(t in name for t in self.target_modules) return True def collect_activation_covariances( self, ) -> dict[str, torch.Tensor]: """ Collect empirical input activation covariance matrices C_X for each target layer using calibration forward passes. """ covariances: dict[str, torch.Tensor] = {} sample_counts: dict[str, int] = {} hooks = [] def get_hook(name: str): def hook_fn(mod: nn.Module, inp: tuple[Any, ...], out: Any) -> None: if not inp or inp[0] is None: return x = inp[0].detach() if x.ndim >= 2: x_flat = x.reshape(-1, x.shape[-1]).float() # Double-precision covariance accumulation for stability cov = x_flat.t().contiguous() @ x_flat n = x_flat.shape[0] if name not in covariances: covariances[name] = cov sample_counts[name] = n else: covariances[name] += cov sample_counts[name] += n return hook_fn target_layers = [ (name, mod) for name, mod in self.model.named_modules() if self._is_target_module(name, mod) ] if not target_layers: return {} for name, mod in target_layers: target_mod = getattr(mod, "base_layer", mod) hooks.append(target_mod.register_forward_hook(get_hook(name))) device = next(self.model.parameters()).device self.model.eval() # Track whether REAL calibration data (not random dummy) was used. # If only dummy data is used, the covariance matrix will be rank-deficient # (rank <= num_dummy_tokens << hidden_dim), causing the whitening transform # to map near-zero eigenvalue directions through inv_sqrt with scale ~100x, # which after un-whitening produces reconstruction error near 1.0 on every layer. real_calibration_used = False try: with torch.no_grad(): if self.calibration_loader is not None: batch_count = 0 for batch in self.calibration_loader: if isinstance(batch, dict): batch = { k: v.to(device) if isinstance(v, torch.Tensor) else v for k, v in batch.items() } self.model(**batch) elif isinstance(batch, (list, tuple)): batch = [ v.to(device) if isinstance(v, torch.Tensor) else v for v in batch ] self.model(*batch) elif isinstance(batch, torch.Tensor): self.model(batch.to(device)) batch_count += 1 if batch_count > 0: real_calibration_used = True logger.info( "DynamicEntropySVD: real calibration loader used (%d batches).", batch_count, ) elif self.calibration_texts is not None and len(self.calibration_texts) > 0: logger.info( "Running calibration forward on %d provided texts.", len(self.calibration_texts), ) if self.tokenizer is not None: text_count = 0 for text in self.calibration_texts[:32]: try: enc = self.tokenizer( text, return_tensors="pt", truncation=True, max_length=128, ) enc = {k: v.to(device) for k, v in enc.items()} self.model(**enc) text_count += 1 except Exception as e: logger.debug("Calibration text pass exception: %s", e) break if text_count > 0: real_calibration_used = True logger.info( "DynamicEntropySVD: real calibration texts used (%d texts).", text_count, ) else: logger.info( "No tokenizer available; calibration_texts provided but cannot encode. " "Falling back to weight-only SVD." ) except Exception as e: logger.warning( "Activation collection encountered exception (%s); falling back to weight SVD.", e, ) finally: for h in hooks: h.remove() # ROOT-CAUSE FIX: If no real calibration data was used, dummy inputs (e.g. # 2x32 random tokens) produce a rank-deficient covariance (rank<=64 for # hidden_dim=768). The whitening eigenvalue clamp (min=1e-4) then assigns # weight 100x to the 704 meaningless directions. After SVD truncation and # un-whitening, reconstruction error approaches 1.0 on every layer, causing # all 24 layers to fall back and zero compression. # # Solution: return {} so from_linear uses plain weight-only SVD instead. # Expected reconstruction error at rank 64: ~0.15-0.25 (all layers compress). if not real_calibration_used: logger.warning( "DynamicEntropySVD: no real calibration data available " "(calibration_loader=None, calibration_texts=None/empty). " "Activation-aware whitening DISABLED. Using weight-only SVD for all " "layers. Expected reconstruction error at rank=64: ~0.15-0.25." ) return {} # Normalize accumulated covariances by total sample count normalized_covs: dict[str, torch.Tensor] = {} for name, cov in covariances.items(): cnt = sample_counts.get(name, 1) normalized_covs[name] = cov / max(1, cnt) logger.info( "DynamicEntropySVD: %d activation covariances collected (real data).", len(normalized_covs), ) return normalized_covs def compute_layer_entropies(self, covariances: dict[str, torch.Tensor]) -> dict[str, float]: """ Compute singular entropy for each target layer using activation covariance or fallback weight matrix spectrum. """ entropies: dict[str, float] = {} for name, module in self.model.named_modules(): if self._is_target_module(name, module): target_mod = getattr(module, "base_layer", module) raw_w = target_mod.weight.data cls_name = target_mod.__class__.__name__.lower() is_conv1d = "conv1d" in cls_name or hasattr(target_mod, "nf") w_mat = raw_w.t().contiguous().float() if is_conv1d else raw_w.float() if name in covariances: ent = compute_singular_entropy(covariances[name]) else: ent = compute_singular_entropy(w_mat) entropies[name] = ent return entropies def compress(self) -> tuple[nn.Module, dict[str, Any]]: """ Execute dynamic entropy-budgeted low-rank SVD compression across all target layers. Includes reconstruction error validation, fallback, layer freezing, and parameter tracking. Returns: Tuple[nn.Module, Dict[str, Any]]: Compressed model and detailed statistics. """ self.compressed_layers = [] self.fallback_layers = [] self.skipped_layers = [] self.layer_errors = {} # Step 1: Collect activation covariances covariances = self.collect_activation_covariances() # Step 2: Compute singular entropies per layer self.layer_entropies = self.compute_layer_entropies(covariances) if not self.layer_entropies: logger.warning("No compressible linear layers found in model.") return self.model, self.get_compression_stats() # Step 3: Extract layer dimensions layer_dims: dict[str, tuple[int, int]] = {} total_orig_decomp_params = 0 total_comp_decomp_params = 0 for name, module in self.model.named_modules(): if name in self.layer_entropies: target_mod = getattr(module, "base_layer", module) raw_w = target_mod.weight.data cls_name = target_mod.__class__.__name__.lower() is_conv1d = "conv1d" in cls_name or hasattr(target_mod, "nf") if is_conv1d: in_f, out_f = raw_w.shape[0], raw_w.shape[1] else: out_f, in_f = raw_w.shape[0], raw_w.shape[1] layer_dims[name] = (out_f, in_f) total_orig_decomp_params += out_f * in_f # Freeze uncompressed layers when target_attention_only=True if self.target_attention_only: for name, module in list(self.model.named_modules()): target_mod = getattr(module, "base_layer", module) cls_name = target_mod.__class__.__name__.lower() is_linear = isinstance(target_mod, nn.Linear) is_conv1d = "conv1d" in cls_name or hasattr(target_mod, "nf") if is_linear or is_conv1d: if not self._is_target_module(name, module): for p in module.parameters(): p.requires_grad = False self.skipped_layers.append(name) # Step 4: Determine total rank budget total_rank_budget = self.total_rank_budget if total_rank_budget is None and self.target_rank is not None: total_rank_budget = self.target_rank * len(self.layer_entropies) # Step 5: Dynamic rank allocation self.layer_ranks = allocate_entropy_ranks( entropies=self.layer_entropies, min_rank=self.min_rank, max_rank=self.max_rank, total_rank_budget=total_rank_budget, target_param_ratio=self.target_param_ratio, layer_dims=layer_dims, ) # Step 6: Factorize, validate, and replace each target linear layer for name, rank in self.layer_ranks.items(): parent_name, child_name = name.rsplit(".", 1) if "." in name else ("", name) if parent_name: parent = self.model.get_submodule(parent_name) else: parent = self.model module = getattr(parent, child_name) target_mod = getattr(module, "base_layer", module) out_f, in_f = layer_dims[name] orig_params = out_f * in_f cov = covariances.get(name, None) factorized_layer = DynamicSVDLinear.from_linear( linear=module, rank=rank, covariance=cov, ) # Compute relative Frobenius reconstruction error raw_w = target_mod.weight.data cls_name = target_mod.__class__.__name__.lower() is_conv1d = "conv1d" in cls_name or hasattr(target_mod, "nf") W = raw_w.t().contiguous().float() if is_conv1d else raw_w.float() W_recon = factorized_layer.U.float() @ factorized_layer.V.float() norm_w = torch.norm(W, p="fro").item() err = float(torch.norm(W - W_recon, p="fro").item() / max(norm_w, 1e-12)) self.layer_errors[name] = err if err > self.max_error and self.fallback_to_original: logger.warning( "DynamicEntropySVD error %.4f on %s exceeded threshold %.4f; falling back to original.", err, name, self.max_error, ) self.fallback_layers.append(name) for p in module.parameters(): p.requires_grad = False total_comp_decomp_params += orig_params continue # If wrapped in LoRALinear, replace the inner base_layer or linear has_base_layer = hasattr(module, "base_layer") or ( hasattr(module, "linear") and (hasattr(module, "lora_A") or hasattr(module, "r")) ) if hasattr(module, "base_layer"): module.base_layer = factorized_layer elif hasattr(module, "linear") and (hasattr(module, "lora_A") or hasattr(module, "r")): module.linear = factorized_layer else: replace_module(self.model, name, factorized_layer) if has_base_layer: for p_name, p in module.named_parameters(): if any(t in p_name.lower() for t in ("lora_a", "lora_b", "adapter")): p.requires_grad = True self.compressed_layers.append(name) total_comp_decomp_params += rank * (in_f + out_f) # Forward sanity check forward_check_success = False try: self.model.eval() with torch.no_grad(): d = next(self.model.parameters()).device dummy = torch.randint(0, 100, (1, 16), device=d) self.model(dummy) forward_check_success = True except Exception as e: logger.debug("DynamicEntropySVD forward check exception: %s", e) effective_ratio = ( total_comp_decomp_params / total_orig_decomp_params if total_orig_decomp_params > 0 else 1.0 ) avg_err = ( sum(self.layer_errors[k] for k in self.compressed_layers) / max(1, len(self.compressed_layers)) if self.compressed_layers else 0.0 ) stats = self.get_compression_stats() stats.update( { "layer_entropies": self.layer_entropies, "layer_ranks": self.layer_ranks, "layer_errors": self.layer_errors, "compressed_layers": self.compressed_layers, "compressed_layer_count": len(self.compressed_layers), "fallback_layers": self.fallback_layers, "fallback_layer_count": len(self.fallback_layers), "skipped_layers": self.skipped_layers, "skipped_layer_count": len(self.skipped_layers), "original_layer_count": len(layer_dims), "max_error": self.max_error, "target_attention_only": self.target_attention_only, "fallback_to_original": self.fallback_to_original, "effective_parameter_ratio": round(effective_ratio, 4), "avg_reconstruction_error": avg_err, "forward_check_success": forward_check_success, "method": "dynamic_entropy_svd", } ) self.stats = stats logger.info( "Compressor dynamic_rank finished: %d compressed, %d fallback, avg_err=%.4f, max_err_threshold=%.4f", len(self.compressed_layers), len(self.fallback_layers), avg_err, self.max_error, ) return self.model, statsDynamic Entropy SVD Compressor.
- Computes singular entropy H(Sigma_l) on layer activation covariances or weights.
- Allocates layer ranks within [min_rank, max_rank] subject to parameter/rank budget.
- Replaces dense Linear/Conv1D layers with DynamicSVDLinear factorized modules.
- Provides relative Frobenius error validation, max_error threshold, and safe fallback.
- Freezes fallback and non-target layers during recovery.
Ancestors
Subclasses
- neuralops.compression_engine.dynamic_rank.DynamicEntropySVDCompressorRegistered
- neuralops.compression_engine.dynamic_rank.EntropyRankAllocatorRegistered
Methods
def collect_activation_covariances(self) ‑> dict[str, torch.Tensor]-
Expand source code
def collect_activation_covariances( self, ) -> dict[str, torch.Tensor]: """ Collect empirical input activation covariance matrices C_X for each target layer using calibration forward passes. """ covariances: dict[str, torch.Tensor] = {} sample_counts: dict[str, int] = {} hooks = [] def get_hook(name: str): def hook_fn(mod: nn.Module, inp: tuple[Any, ...], out: Any) -> None: if not inp or inp[0] is None: return x = inp[0].detach() if x.ndim >= 2: x_flat = x.reshape(-1, x.shape[-1]).float() # Double-precision covariance accumulation for stability cov = x_flat.t().contiguous() @ x_flat n = x_flat.shape[0] if name not in covariances: covariances[name] = cov sample_counts[name] = n else: covariances[name] += cov sample_counts[name] += n return hook_fn target_layers = [ (name, mod) for name, mod in self.model.named_modules() if self._is_target_module(name, mod) ] if not target_layers: return {} for name, mod in target_layers: target_mod = getattr(mod, "base_layer", mod) hooks.append(target_mod.register_forward_hook(get_hook(name))) device = next(self.model.parameters()).device self.model.eval() # Track whether REAL calibration data (not random dummy) was used. # If only dummy data is used, the covariance matrix will be rank-deficient # (rank <= num_dummy_tokens << hidden_dim), causing the whitening transform # to map near-zero eigenvalue directions through inv_sqrt with scale ~100x, # which after un-whitening produces reconstruction error near 1.0 on every layer. real_calibration_used = False try: with torch.no_grad(): if self.calibration_loader is not None: batch_count = 0 for batch in self.calibration_loader: if isinstance(batch, dict): batch = { k: v.to(device) if isinstance(v, torch.Tensor) else v for k, v in batch.items() } self.model(**batch) elif isinstance(batch, (list, tuple)): batch = [ v.to(device) if isinstance(v, torch.Tensor) else v for v in batch ] self.model(*batch) elif isinstance(batch, torch.Tensor): self.model(batch.to(device)) batch_count += 1 if batch_count > 0: real_calibration_used = True logger.info( "DynamicEntropySVD: real calibration loader used (%d batches).", batch_count, ) elif self.calibration_texts is not None and len(self.calibration_texts) > 0: logger.info( "Running calibration forward on %d provided texts.", len(self.calibration_texts), ) if self.tokenizer is not None: text_count = 0 for text in self.calibration_texts[:32]: try: enc = self.tokenizer( text, return_tensors="pt", truncation=True, max_length=128, ) enc = {k: v.to(device) for k, v in enc.items()} self.model(**enc) text_count += 1 except Exception as e: logger.debug("Calibration text pass exception: %s", e) break if text_count > 0: real_calibration_used = True logger.info( "DynamicEntropySVD: real calibration texts used (%d texts).", text_count, ) else: logger.info( "No tokenizer available; calibration_texts provided but cannot encode. " "Falling back to weight-only SVD." ) except Exception as e: logger.warning( "Activation collection encountered exception (%s); falling back to weight SVD.", e, ) finally: for h in hooks: h.remove() # ROOT-CAUSE FIX: If no real calibration data was used, dummy inputs (e.g. # 2x32 random tokens) produce a rank-deficient covariance (rank<=64 for # hidden_dim=768). The whitening eigenvalue clamp (min=1e-4) then assigns # weight 100x to the 704 meaningless directions. After SVD truncation and # un-whitening, reconstruction error approaches 1.0 on every layer, causing # all 24 layers to fall back and zero compression. # # Solution: return {} so from_linear uses plain weight-only SVD instead. # Expected reconstruction error at rank 64: ~0.15-0.25 (all layers compress). if not real_calibration_used: logger.warning( "DynamicEntropySVD: no real calibration data available " "(calibration_loader=None, calibration_texts=None/empty). " "Activation-aware whitening DISABLED. Using weight-only SVD for all " "layers. Expected reconstruction error at rank=64: ~0.15-0.25." ) return {} # Normalize accumulated covariances by total sample count normalized_covs: dict[str, torch.Tensor] = {} for name, cov in covariances.items(): cnt = sample_counts.get(name, 1) normalized_covs[name] = cov / max(1, cnt) logger.info( "DynamicEntropySVD: %d activation covariances collected (real data).", len(normalized_covs), ) return normalized_covsCollect empirical input activation covariance matrices C_X for each target layer using calibration forward passes.
def compress(self) ‑> tuple[torch.nn.modules.module.Module, dict[str, typing.Any]]-
Expand source code
def compress(self) -> tuple[nn.Module, dict[str, Any]]: """ Execute dynamic entropy-budgeted low-rank SVD compression across all target layers. Includes reconstruction error validation, fallback, layer freezing, and parameter tracking. Returns: Tuple[nn.Module, Dict[str, Any]]: Compressed model and detailed statistics. """ self.compressed_layers = [] self.fallback_layers = [] self.skipped_layers = [] self.layer_errors = {} # Step 1: Collect activation covariances covariances = self.collect_activation_covariances() # Step 2: Compute singular entropies per layer self.layer_entropies = self.compute_layer_entropies(covariances) if not self.layer_entropies: logger.warning("No compressible linear layers found in model.") return self.model, self.get_compression_stats() # Step 3: Extract layer dimensions layer_dims: dict[str, tuple[int, int]] = {} total_orig_decomp_params = 0 total_comp_decomp_params = 0 for name, module in self.model.named_modules(): if name in self.layer_entropies: target_mod = getattr(module, "base_layer", module) raw_w = target_mod.weight.data cls_name = target_mod.__class__.__name__.lower() is_conv1d = "conv1d" in cls_name or hasattr(target_mod, "nf") if is_conv1d: in_f, out_f = raw_w.shape[0], raw_w.shape[1] else: out_f, in_f = raw_w.shape[0], raw_w.shape[1] layer_dims[name] = (out_f, in_f) total_orig_decomp_params += out_f * in_f # Freeze uncompressed layers when target_attention_only=True if self.target_attention_only: for name, module in list(self.model.named_modules()): target_mod = getattr(module, "base_layer", module) cls_name = target_mod.__class__.__name__.lower() is_linear = isinstance(target_mod, nn.Linear) is_conv1d = "conv1d" in cls_name or hasattr(target_mod, "nf") if is_linear or is_conv1d: if not self._is_target_module(name, module): for p in module.parameters(): p.requires_grad = False self.skipped_layers.append(name) # Step 4: Determine total rank budget total_rank_budget = self.total_rank_budget if total_rank_budget is None and self.target_rank is not None: total_rank_budget = self.target_rank * len(self.layer_entropies) # Step 5: Dynamic rank allocation self.layer_ranks = allocate_entropy_ranks( entropies=self.layer_entropies, min_rank=self.min_rank, max_rank=self.max_rank, total_rank_budget=total_rank_budget, target_param_ratio=self.target_param_ratio, layer_dims=layer_dims, ) # Step 6: Factorize, validate, and replace each target linear layer for name, rank in self.layer_ranks.items(): parent_name, child_name = name.rsplit(".", 1) if "." in name else ("", name) if parent_name: parent = self.model.get_submodule(parent_name) else: parent = self.model module = getattr(parent, child_name) target_mod = getattr(module, "base_layer", module) out_f, in_f = layer_dims[name] orig_params = out_f * in_f cov = covariances.get(name, None) factorized_layer = DynamicSVDLinear.from_linear( linear=module, rank=rank, covariance=cov, ) # Compute relative Frobenius reconstruction error raw_w = target_mod.weight.data cls_name = target_mod.__class__.__name__.lower() is_conv1d = "conv1d" in cls_name or hasattr(target_mod, "nf") W = raw_w.t().contiguous().float() if is_conv1d else raw_w.float() W_recon = factorized_layer.U.float() @ factorized_layer.V.float() norm_w = torch.norm(W, p="fro").item() err = float(torch.norm(W - W_recon, p="fro").item() / max(norm_w, 1e-12)) self.layer_errors[name] = err if err > self.max_error and self.fallback_to_original: logger.warning( "DynamicEntropySVD error %.4f on %s exceeded threshold %.4f; falling back to original.", err, name, self.max_error, ) self.fallback_layers.append(name) for p in module.parameters(): p.requires_grad = False total_comp_decomp_params += orig_params continue # If wrapped in LoRALinear, replace the inner base_layer or linear has_base_layer = hasattr(module, "base_layer") or ( hasattr(module, "linear") and (hasattr(module, "lora_A") or hasattr(module, "r")) ) if hasattr(module, "base_layer"): module.base_layer = factorized_layer elif hasattr(module, "linear") and (hasattr(module, "lora_A") or hasattr(module, "r")): module.linear = factorized_layer else: replace_module(self.model, name, factorized_layer) if has_base_layer: for p_name, p in module.named_parameters(): if any(t in p_name.lower() for t in ("lora_a", "lora_b", "adapter")): p.requires_grad = True self.compressed_layers.append(name) total_comp_decomp_params += rank * (in_f + out_f) # Forward sanity check forward_check_success = False try: self.model.eval() with torch.no_grad(): d = next(self.model.parameters()).device dummy = torch.randint(0, 100, (1, 16), device=d) self.model(dummy) forward_check_success = True except Exception as e: logger.debug("DynamicEntropySVD forward check exception: %s", e) effective_ratio = ( total_comp_decomp_params / total_orig_decomp_params if total_orig_decomp_params > 0 else 1.0 ) avg_err = ( sum(self.layer_errors[k] for k in self.compressed_layers) / max(1, len(self.compressed_layers)) if self.compressed_layers else 0.0 ) stats = self.get_compression_stats() stats.update( { "layer_entropies": self.layer_entropies, "layer_ranks": self.layer_ranks, "layer_errors": self.layer_errors, "compressed_layers": self.compressed_layers, "compressed_layer_count": len(self.compressed_layers), "fallback_layers": self.fallback_layers, "fallback_layer_count": len(self.fallback_layers), "skipped_layers": self.skipped_layers, "skipped_layer_count": len(self.skipped_layers), "original_layer_count": len(layer_dims), "max_error": self.max_error, "target_attention_only": self.target_attention_only, "fallback_to_original": self.fallback_to_original, "effective_parameter_ratio": round(effective_ratio, 4), "avg_reconstruction_error": avg_err, "forward_check_success": forward_check_success, "method": "dynamic_entropy_svd", } ) self.stats = stats logger.info( "Compressor dynamic_rank finished: %d compressed, %d fallback, avg_err=%.4f, max_err_threshold=%.4f", len(self.compressed_layers), len(self.fallback_layers), avg_err, self.max_error, ) return self.model, statsExecute dynamic entropy-budgeted low-rank SVD compression across all target layers. Includes reconstruction error validation, fallback, layer freezing, and parameter tracking.
- Returns
- -----=
Tuple[nn.Module, Dict[str, Any]]- Compressed model and detailed statistics.
def compute_layer_entropies(self, covariances: dict[str, torch.Tensor]) ‑> dict[str, float]-
Expand source code
def compute_layer_entropies(self, covariances: dict[str, torch.Tensor]) -> dict[str, float]: """ Compute singular entropy for each target layer using activation covariance or fallback weight matrix spectrum. """ entropies: dict[str, float] = {} for name, module in self.model.named_modules(): if self._is_target_module(name, module): target_mod = getattr(module, "base_layer", module) raw_w = target_mod.weight.data cls_name = target_mod.__class__.__name__.lower() is_conv1d = "conv1d" in cls_name or hasattr(target_mod, "nf") w_mat = raw_w.t().contiguous().float() if is_conv1d else raw_w.float() if name in covariances: ent = compute_singular_entropy(covariances[name]) else: ent = compute_singular_entropy(w_mat) entropies[name] = ent return entropiesCompute singular entropy for each target layer using activation covariance or fallback weight matrix spectrum.
Inherited members
class DynamicSVDLinear (in_features: int,
out_features: int,
rank: int,
bias: bool = True,
dtype: torch.dtype = torch.float32,
device: torch.device | None = None)-
Expand source code
class DynamicSVDLinear(nn.Module): is_custom_attention: bool = True """ True Low-Rank Factorized Linear Layer executing as: y = (x @ V.T) @ U.T + bias where V is (rank, in_features) and U is (out_features, rank). """ def __init__( self, in_features: int, out_features: int, rank: int, bias: bool = True, dtype: torch.dtype = torch.float32, device: torch.device | None = None, ) -> None: super().__init__() self.in_features = in_features self.out_features = out_features self.rank = max(1, min(rank, in_features, out_features)) # Factorized low-rank parameter tensors self.V = nn.Parameter(torch.empty(self.rank, in_features, dtype=dtype, device=device)) self.U = nn.Parameter(torch.empty(out_features, self.rank, dtype=dtype, device=device)) if bias: self.bias = nn.Parameter(torch.empty(out_features, dtype=dtype, device=device)) else: self.register_parameter("bias", None) self.reset_parameters() @property def weight(self) -> torch.Tensor: """Reconstructed effective weight matrix for compatibility.""" return torch.matmul(self.U, self.V) def reset_parameters(self) -> None: """Kaiming uniform parameter initialization.""" nn.init.kaiming_uniform_(self.V, a=math.sqrt(5)) nn.init.kaiming_uniform_(self.U, a=math.sqrt(5)) if self.bias is not None: fan_in = self.in_features bound = 1 / math.sqrt(fan_in) if fan_in > 0 else 0 nn.init.uniform_(self.bias, -bound, bound) def forward(self, x: torch.Tensor) -> torch.Tensor: """ Forward pass executing factorized low-rank multiplication: y = (x @ V^T) @ U^T + bias """ intermediate = F.linear(x, self.V) out = F.linear(intermediate, self.U, self.bias) return out @classmethod def from_linear( cls, linear: nn.Module, rank: int, covariance: torch.Tensor | None = None, ) -> DynamicSVDLinear: """ Construct factorized DynamicSVDLinear directly from dense nn.Linear or Conv1D via SVD. Args: linear: Source dense linear or Conv1D layer (or LoRALinear wrapper). rank: Target rank for low-rank factorization. covariance: Optional empirical activation covariance C_X for activation-aware SVD. Returns: DynamicSVDLinear: Initialized factorized low-rank layer. """ target_mod = getattr(linear, "base_layer", linear) cls_name = target_mod.__class__.__name__.lower() is_conv1d = "conv1d" in cls_name or hasattr(target_mod, "nf") raw_w = target_mod.weight.data if is_conv1d: in_f, out_f = raw_w.shape[0], raw_w.shape[1] W = raw_w.t().contiguous().float() # [out_features, in_features] else: out_f, in_f = raw_w.shape[0], raw_w.shape[1] W = raw_w.float() # [out_features, in_features] target_rank = max(1, min(rank, in_f, out_f)) new_layer = cls( in_features=in_f, out_features=out_f, rank=target_rank, bias=target_mod.bias is not None, dtype=target_mod.weight.dtype, device=target_mod.weight.device, ) # Always compute the weight-only SVD baseline first. # This serves as both the default path (when covariance=None) and the # fallback comparison when the activation-aware path produces higher error. U_svd_w, S_svd_w, Vh_svd_w = torch.linalg.svd(W, full_matrices=False) sqrt_S_w = torch.sqrt(torch.clamp(S_svd_w[:target_rank], min=1e-12)) U_w = U_svd_w[:, :target_rank] * sqrt_S_w.unsqueeze(0) # [out_f, rank] V_w = sqrt_S_w.unsqueeze(1) * Vh_svd_w[:target_rank, :] # [rank, in_f] W_rec_w = U_w @ V_w norm_W = torch.norm(W, p="fro").clamp(min=1e-12) err_weight_only = (torch.norm(W - W_rec_w, p="fro") / norm_W).item() # Default to weight-only unless activation-aware is demonstrably better. U_r, V_r = U_w, V_w if covariance is not None and covariance.numel() > 0 and covariance.shape[0] == in_f: # RESEARCH FIX: Activation-aware whitening SVD with eigenvalue clamping lambda_i >= 1e-4 # NOTE: We only accept the activation-aware result if its reconstruction error is # strictly lower than the weight-only baseline. This guards against rank-deficient # covariances (e.g. from dummy calibration data) that inflate inv_sqrt by up to 100x # and destroy the approximation quality. cov = covariance.to(device=W.device, dtype=torch.float32) try: # Eigen-decomposition with regularized eigenvalue clamping >= 1e-4 eigvals, eigvecs = torch.linalg.eigh(cov + 1e-4 * torch.eye(in_f, device=W.device)) eigvals_clamped = torch.clamp(eigvals, min=1e-4) cov_sqrt = ( eigvecs @ torch.diag(torch.sqrt(eigvals_clamped)) @ eigvecs.t().contiguous() ) cov_inv_sqrt = ( eigvecs @ torch.diag(1.0 / torch.sqrt(eigvals_clamped)) @ eigvecs.t().contiguous() ) W_scaled = W @ cov_sqrt U_svd_a, S_svd_a, Vh_svd_a = torch.linalg.svd(W_scaled, full_matrices=False) # Truncate and un-whiten right singular factors sqrt_S_a = torch.sqrt(torch.clamp(S_svd_a[:target_rank], min=1e-12)) U_a = U_svd_a[:, :target_rank] * sqrt_S_a.unsqueeze(0) V_a = sqrt_S_a.unsqueeze(1) * (Vh_svd_a[:target_rank, :] @ cov_inv_sqrt) # Symmetrically normalize factor magnitudes norm_u = torch.norm(U_a, p="fro").clamp_min(1e-8) norm_v = torch.norm(V_a, p="fro").clamp_min(1e-8) scale = torch.sqrt(norm_u / norm_v) U_a = U_a / scale V_a = V_a * scale W_rec_a = U_a @ V_a err_act_aware = (torch.norm(W - W_rec_a, p="fro") / norm_W).item() if err_act_aware < err_weight_only: # Activation-aware is genuinely better: accept it U_r, V_r = U_a, V_a logger.debug( "from_linear: activation-aware SVD accepted " "(err_act=%.4f < err_weight=%.4f).", err_act_aware, err_weight_only, ) else: # Activation-aware is worse (e.g. due to rank-deficient covariance): # stick with the weight-only baseline logger.debug( "from_linear: activation-aware SVD rejected " "(err_act=%.4f >= err_weight=%.4f); using weight-only SVD.", err_act_aware, err_weight_only, ) except Exception as e: logger.warning("Activation-aware SVD failed (%s); using weight-only SVD.", e) # U_r, V_r already set to weight-only baseline above new_layer.U.data.copy_(U_r.to(dtype=target_mod.weight.dtype)) new_layer.V.data.copy_(V_r.to(dtype=target_mod.weight.dtype)) if target_mod.bias is not None and new_layer.bias is not None: new_layer.bias.data.copy_(target_mod.bias.data) return new_layer def extra_repr(self) -> str: return f"in_features={self.in_features}, out_features={self.out_features}, rank={self.rank}, bias={self.bias is not None}"Base class for all neural network modules.
Your models should also subclass this class.
Modules can also contain other Modules, allowing them to be nested in a tree structure. You can assign the submodules as regular attributes::
import torch.nn as nn import torch.nn.functional as F class Model(nn.Module): def __init__(self) -> None: super().__init__() self.conv1 = nn.Conv2d(1, 20, 5) self.conv2 = nn.Conv2d(20, 20, 5) def forward(self, x): x = F.relu(self.conv1(x)) return F.relu(self.conv2(x))Submodules assigned in this way will be registered, and will also have their parameters converted when you call :meth:
to, etc.Note
As per the example above, an
__init__()call to the parent class must be made before assignment on the child.:ivar training: Boolean represents whether this module is in training or evaluation mode. :vartype training: bool
Initialize internal Module state, shared by both nn.Module and ScriptModule.
Ancestors
- torch.nn.modules.module.Module
Class variables
var is_custom_attention : bool-
True Low-Rank Factorized Linear Layer executing as: y = (x @ V.T) @ U.T + bias where V is (rank, in_features) and U is (out_features, rank).
Static methods
def from_linear(linear: nn.Module, rank: int, covariance: torch.Tensor | None = None) ‑> DynamicSVDLinear-
Construct factorized DynamicSVDLinear directly from dense nn.Linear or Conv1D via SVD.
- Args
- -----=
linear- Source dense linear or Conv1D layer (or LoRALinear wrapper).
rank- Target rank for low-rank factorization.
covariance- Optional empirical activation covariance C_X for activation-aware SVD.
- Returns
- -----=
DynamicSVDLinear- Initialized factorized low-rank layer.
Instance variables
prop weight : torch.Tensor-
Expand source code
@property def weight(self) -> torch.Tensor: """Reconstructed effective weight matrix for compatibility.""" return torch.matmul(self.U, self.V)Reconstructed effective weight matrix for compatibility.
Methods
def extra_repr(self) ‑> str-
Expand source code
def extra_repr(self) -> str: return f"in_features={self.in_features}, out_features={self.out_features}, rank={self.rank}, bias={self.bias is not None}"Return the extra representation of the module.
To print customized extra information, you should re-implement this method in your own modules. Both single-line and multi-line strings are acceptable.
def forward(self, x: torch.Tensor) ‑> torch.Tensor-
Expand source code
def forward(self, x: torch.Tensor) -> torch.Tensor: """ Forward pass executing factorized low-rank multiplication: y = (x @ V^T) @ U^T + bias """ intermediate = F.linear(x, self.V) out = F.linear(intermediate, self.U, self.bias) return outForward pass executing factorized low-rank multiplication: y = (x @ V^T) @ U^T + bias
def reset_parameters(self) ‑> None-
Expand source code
def reset_parameters(self) -> None: """Kaiming uniform parameter initialization.""" nn.init.kaiming_uniform_(self.V, a=math.sqrt(5)) nn.init.kaiming_uniform_(self.U, a=math.sqrt(5)) if self.bias is not None: fan_in = self.in_features bound = 1 / math.sqrt(fan_in) if fan_in > 0 else 0 nn.init.uniform_(self.bias, -bound, bound)Kaiming uniform parameter initialization.