Module neuralops.compression_engine.adaptive_ranks
Adaptive Ranks SVD Compressor: dynamic rank selection per layer based on singular value energy spectra.
Dynamic cumulative singular value energy thresholding: $$\frac{\sum_{i=1}^r \sigma_i^2}{\sum_{j=1}^d \sigma_j^2} \ge \tau \quad (\tau = 0.92)$$ with minimum rank floor: $$r_{\min} = \max(16, \lfloor 0.15 \cdot \min(d_{in}, d_{out}) \rfloor)$$ activation covariance whitening: $$M = W \cdot \operatorname{Cov}(X)^{1/2}$$ and symmetrical $\sqrt{\Sigma}$ singular value distribution: $$U_r = U_{:, :r} \sqrt{\Sigma_r}, \quad V_r = (\sqrt{\Sigma_r} V^\top_{:r, :}) \cdot \operatorname{Cov}(X)^{-1/2}$$ Registered canonically as 'adaptive_ranks' and 'adaptive_svd'.
Functions
def adaptive_ranks_factory(model: Any, comp_config: Any = None, **kwargs: Any) ‑> torch.nn.modules.module.Module-
Expand source code
@COMPRESSOR_REGISTRY.register("adaptive") @COMPRESSOR_REGISTRY.register("adaptive_compressor") @COMPRESSOR_REGISTRY.register("adaptive_ranks") @COMPRESSOR_REGISTRY.register("adaptive_svd") @COMPRESSOR_REGISTRY.register("adaptive_factory") @COMPRESSOR_REGISTRY.register("adaptive_compressor_factory") @COMPRESSOR_REGISTRY.register("adaptive_ranks_factory") @COMPRESSOR_REGISTRY.register("adaptive_svd_factory") def adaptive_ranks_factory(model: Any, comp_config: Any = None, **kwargs: Any) -> nn.Module: """Adaptive ranks factory function with robust None/dict/object config handling.""" energy_threshold = kwargs.pop( "energy_threshold", _cfg_get(comp_config, "energy_threshold", 0.92) ) min_rank = kwargs.pop("min_rank", None) if min_rank is None: min_rank = kwargs.pop( "rank", _cfg_get(comp_config, "min_rank", _cfg_get(comp_config, "rank", 16)) ) max_error = kwargs.pop("max_error", _cfg_get(comp_config, "max_error", 0.20)) fallback = kwargs.pop( "fallback_to_original", _cfg_get(comp_config, "fallback_to_original", True) ) target_attention_only = kwargs.pop( "target_attention_only", _cfg_get(comp_config, "target_attention_only", False) ) use_jg_svd = kwargs.pop("use_jg_svd", _cfg_get(comp_config, "use_jg_svd", False)) use_cholesky_jg = kwargs.pop("use_cholesky_jg", _cfg_get(comp_config, "use_cholesky_jg", True)) compressor = AdaptiveRanksCompressor( model, energy_threshold=energy_threshold, min_rank=min_rank, max_error=max_error, fallback_to_original=fallback, target_attention_only=target_attention_only, use_jg_svd=use_jg_svd, use_cholesky_jg=use_cholesky_jg, comp_config=comp_config, **kwargs, ) compressed_model, _ = compressor.compress() return compressed_modelAdaptive ranks factory function with robust None/dict/object config handling.
def adaptive_svd_factory(model: Any, comp_config: Any = None, **kwargs: Any) ‑> torch.nn.modules.module.Module-
Expand source code
@COMPRESSOR_REGISTRY.register("adaptive") @COMPRESSOR_REGISTRY.register("adaptive_compressor") @COMPRESSOR_REGISTRY.register("adaptive_ranks") @COMPRESSOR_REGISTRY.register("adaptive_svd") @COMPRESSOR_REGISTRY.register("adaptive_factory") @COMPRESSOR_REGISTRY.register("adaptive_compressor_factory") @COMPRESSOR_REGISTRY.register("adaptive_ranks_factory") @COMPRESSOR_REGISTRY.register("adaptive_svd_factory") def adaptive_ranks_factory(model: Any, comp_config: Any = None, **kwargs: Any) -> nn.Module: """Adaptive ranks factory function with robust None/dict/object config handling.""" energy_threshold = kwargs.pop( "energy_threshold", _cfg_get(comp_config, "energy_threshold", 0.92) ) min_rank = kwargs.pop("min_rank", None) if min_rank is None: min_rank = kwargs.pop( "rank", _cfg_get(comp_config, "min_rank", _cfg_get(comp_config, "rank", 16)) ) max_error = kwargs.pop("max_error", _cfg_get(comp_config, "max_error", 0.20)) fallback = kwargs.pop( "fallback_to_original", _cfg_get(comp_config, "fallback_to_original", True) ) target_attention_only = kwargs.pop( "target_attention_only", _cfg_get(comp_config, "target_attention_only", False) ) use_jg_svd = kwargs.pop("use_jg_svd", _cfg_get(comp_config, "use_jg_svd", False)) use_cholesky_jg = kwargs.pop("use_cholesky_jg", _cfg_get(comp_config, "use_cholesky_jg", True)) compressor = AdaptiveRanksCompressor( model, energy_threshold=energy_threshold, min_rank=min_rank, max_error=max_error, fallback_to_original=fallback, target_attention_only=target_attention_only, use_jg_svd=use_jg_svd, use_cholesky_jg=use_cholesky_jg, comp_config=comp_config, **kwargs, ) compressed_model, _ = compressor.compress() return compressed_modelAdaptive ranks factory function with robust None/dict/object config handling.
Classes
class AdaptiveRanksCompressor (model: nn.Module,
energy_threshold: float = 0.92,
min_rank: int = 16,
max_error: float = 0.2,
fallback_to_original: bool = True,
protect_critical: bool = True,
target_attention_only: bool = False,
target_modules: list[str] | None = None,
tokenizer: Any | None = None,
calibration_loader: Any | None = None,
calibration_texts: list[str] | None = None,
device: str | torch.device | None = None,
use_jg_svd: bool = False,
use_cholesky_jg: bool = True,
**kwargs: Any)-
Expand source code
@COMPRESSOR_REGISTRY.register("adaptive_ranks") @COMPRESSOR_REGISTRY.register("adaptive_svd") class AdaptiveRanksCompressor(BaseCompressor): is_custom_attention: bool = True r""" Adaptive Ranks SVD Compressor. Dynamically determines ranks per layer according to cumulative singular value energy threshold: $$\frac{\sum_{i=1}^r \sigma_i^2}{\sum_{j=1}^d \sigma_j^2} \ge \tau \quad (\tau = 0.92)$$ with minimum rank floor $r_{\min} = \max(16, \lfloor 0.15 \cdot \min(d_{in}, d_{out}) \rfloor, \lfloor 0.10 \cdot d_{out} \rfloor)$, activation covariance whitening, and symmetrically distributed singular values: $$U_r = U_{:, :r} \sqrt{\Sigma_r}, \quad V_r = (\sqrt{\Sigma_r} V^\top_{:r, :}) \cdot \operatorname{Cov}(X)^{-1/2}$$ Production hardening features: - Relative Frobenius reconstruction error validation with configurable max_error (default 0.20). - Safe fallback to original weights by default (fallback_to_original=True). - Robust Conv1D and LoRALinear support via base_layer weight transposition. - SVD and covariance eigendecomposition computed strictly in FP32 with NaN/Inf guards. - Unified critical-layer protection via should_compress_linear. - Adaptive rank retry when reconstruction error exceeds threshold. - Parameter reduction verification: ensures r * (in + out) < in * out. - Comprehensive per-layer statistics and logging. """ def __init__( self, model: nn.Module, energy_threshold: float = 0.92, min_rank: int = 16, max_error: float = 0.20, fallback_to_original: bool = True, protect_critical: bool = True, target_attention_only: bool = False, target_modules: list[str] | None = None, tokenizer: Any | None = None, calibration_loader: Any | None = None, calibration_texts: list[str] | None = None, device: str | torch.device | None = None, use_jg_svd: bool = False, use_cholesky_jg: bool = True, **kwargs: Any, ) -> None: super().__init__(model, name="adaptive_ranks", **kwargs) cfg = kwargs.get("comp_config", kwargs.get("cfg")) if cfg is None and hasattr(model, "config") and model.config is not None: cfg = model.config # Check sys.argv for CLI overrides cli_use_jg_svd = None cli_use_cholesky_jg = None import sys for _idx, _arg in enumerate(sys.argv): if _arg in ( "--use_jg_svd", "--adaptive_ranks_use_jg_svd", ) and _idx + 1 < len(sys.argv): cli_use_jg_svd = sys.argv[_idx + 1].lower() in ("true", "1", "yes") elif _arg.startswith("--use_jg_svd=") or _arg.startswith( "--adaptive_ranks_use_jg_svd=" ): cli_use_jg_svd = _arg.split("=")[1].lower() in ("true", "1", "yes") elif _arg in ( "--use_cholesky_jg", "--adaptive_ranks_use_cholesky_jg", ) and _idx + 1 < len(sys.argv): cli_use_cholesky_jg = sys.argv[_idx + 1].lower() in ("true", "1", "yes") elif _arg.startswith("--use_cholesky_jg=") or _arg.startswith( "--adaptive_ranks_use_cholesky_jg=" ): cli_use_cholesky_jg = _arg.split("=")[1].lower() in ("true", "1", "yes") if cfg is not None: energy_threshold = _cfg_get( cfg, "energy_threshold", _cfg_get( cfg, "adaptive_ranks_energy_threshold", _cfg_get(cfg, "asvd_energy_threshold", energy_threshold), ), ) min_rank = _cfg_get(cfg, "min_rank", _cfg_get(cfg, "rank", min_rank)) max_error = _cfg_get( cfg, "max_error", _cfg_get( cfg, "adaptive_ranks_max_error", _cfg_get(cfg, "aasvd_max_error", max_error), ), ) fallback_to_original = _cfg_get(cfg, "fallback_to_original", fallback_to_original) protect_critical = _cfg_get(cfg, "protect_critical", protect_critical) target_attention_only = _cfg_get( cfg, "target_attention_only", _cfg_get( cfg, "adaptive_ranks_target_attention_only", _cfg_get(cfg, "aasvd_target_attention_only", target_attention_only), ), ) use_jg_svd = _cfg_get( cfg, "use_jg_svd", _cfg_get(cfg, "adaptive_ranks_use_jg_svd", use_jg_svd), ) use_cholesky_jg = _cfg_get( cfg, "use_cholesky_jg", _cfg_get(cfg, "adaptive_ranks_use_cholesky_jg", use_cholesky_jg), ) if cli_use_jg_svd is not None: use_jg_svd = cli_use_jg_svd if cli_use_cholesky_jg is not None: use_cholesky_jg = cli_use_cholesky_jg if "use_jg_svd" in kwargs and kwargs["use_jg_svd"] is not None: use_jg_svd = kwargs["use_jg_svd"] if "use_cholesky_jg" in kwargs and kwargs["use_cholesky_jg"] is not None: use_cholesky_jg = kwargs["use_cholesky_jg"] if "rank" in kwargs and kwargs["rank"] is not None: min_rank = kwargs["rank"] if "target_attention_only" in kwargs and kwargs["target_attention_only"] is not None: target_attention_only = kwargs["target_attention_only"] self.energy_threshold = max(0.01, min(float(energy_threshold), 1.0)) self.min_rank = max(1, int(min_rank)) self.max_error = float(max_error) self.fallback_to_original = bool(fallback_to_original) self.protect_critical = bool(protect_critical) self.target_attention_only = bool(target_attention_only) self.use_jg_svd = bool(use_jg_svd) self.use_cholesky_jg = bool(use_cholesky_jg) self.target_modules = target_modules self.tokenizer = tokenizer or getattr(model, "tokenizer", None) self.calibration_loader = calibration_loader self.calibration_texts = calibration_texts self.target_device = ( torch.device(device) if device is not None else next(model.parameters(), torch.tensor(0.0)).device ) self.layer_D_gating: dict[str, torch.Tensor] = {} self.attn_jacobian: dict[str, float] = {} self.compressed_layers: list[str] = [] self.fallback_layers: list[str] = [] self.layer_errors: dict[str, float] = {} self.layer_ranks: dict[str, int] = {} self.used_covariance: dict[str, bool] = {} self.used_jg_svd: dict[str, bool] = {} def _is_target_module(self, name: str, module: nn.Module) -> bool: if isinstance(module, SVDLinear) or ( CompressedAttentionLayer is not None and isinstance(module, CompressedAttentionLayer) ): return False # Do not target inner submodules of LoRA or wrappers if any( name.endswith(sfx) for sfx in [".base_layer", ".lora_A", ".lora_B", ".lora_dropout"] ): return False target_mod = getattr(module, "base_layer", module) cls_name = module.__class__.__name__.lower() target_cls = target_mod.__class__.__name__.lower() is_linear_like = ( isinstance(module, nn.Linear) or isinstance(target_mod, nn.Linear) or "conv1d" in cls_name or "conv1d" in target_cls or hasattr(module, "nf") or hasattr(target_mod, "nf") or "loralinear" in cls_name or hasattr(module, "base_layer") ) is_mha = isinstance(module, nn.MultiheadAttention) or isinstance( target_mod, nn.MultiheadAttention ) if not (is_linear_like or is_mha): return False if self.protect_critical: name_lower = name.lower() if any( skip in name_lower for skip in [ "lm_head", "classifier", "head", "output_layer", "score", "pooler", "embed", ] ): return False if self.target_attention_only: name_lower = name.lower() ffn_patterns = [ "ffn", "mlp", "lin1", "lin2", "intermediate", "gate_proj", "up_proj", "down_proj", "c_fc", ] if any(p in name_lower for p in ffn_patterns): return False if "output.dense" in name_lower and "attention" not in name_lower: return False attn_patterns = [ "attn", "attention", "q_proj", "k_proj", "v_proj", "o_proj", "out_lin", "q_lin", "k_lin", "v_lin", "c_attn", "c_proj", "query", "key", "value", ] if not any(p in name_lower for p in attn_patterns): return False else: if not should_compress_linear(self.model, name): return False if self.target_modules is not None: return any(t in name for t in self.target_modules) return True def _compute_energy_rank( self, S: torch.Tensor, max_rank: int, min_rank_floor: int | None = None, threshold: float | None = None, ) -> int: r""" Find smallest rank $r$ satisfying cumulative singular energy ratio $\ge$ threshold: $$\frac{\sum_{i=1}^r \sigma_i^2}{\sum_{j=1}^d \sigma_j^2} \ge \tau$$ constrained by minimum rank floor and parameter reduction bounds. """ thresh = threshold if threshold is not None else self.energy_threshold r_floor = min_rank_floor if min_rank_floor is not None else self.min_rank r_floor = max(1, min(int(r_floor), max_rank)) s_squared = S**2 total_energy = s_squared.sum() if total_energy <= 1e-12: return r_floor cum_energy = torch.cumsum(s_squared, dim=0) / total_energy idx = torch.where(cum_energy >= thresh)[0] r = int(idx[0].item()) + 1 if len(idx) > 0 else max_rank return max(1, min(max(r, r_floor), max_rank)) def _collect_activation_covariances(self) -> dict[str, torch.Tensor]: """Collect empirical input activation covariances across target nn.Linear / Conv1D modules.""" if not (self.tokenizer or self.calibration_loader or self.calibration_texts): logger.info("No calibration data available; using unweighted SVD.") return {} self.model.eval() running_cov: dict[str, torch.Tensor] = {} sample_counts: dict[str, int] = {} hooks = [] def make_hook(layer_name: str) -> Any: def hook(module: nn.Module, inp: tuple[torch.Tensor, ...], outp: Any) -> None: if len(inp) > 0 and isinstance(inp[0], torch.Tensor): x = inp[0].detach().float() target_mod = getattr(module, "base_layer", module) cls_name = module.__class__.__name__.lower() target_cls = target_mod.__class__.__name__.lower() is_conv1d = ( "conv1d" in cls_name or "conv1d" in target_cls or hasattr(module, "nf") or hasattr(target_mod, "nf") ) weight = getattr(target_mod, "weight", getattr(module, "weight", None)) if weight is None: return in_f = weight.shape[0] if is_conv1d else weight.shape[1] if x.ndim >= 2: flat_x = x.reshape(-1, in_f) else: flat_x = x.unsqueeze(0) n_samples = flat_x.shape[0] if n_samples > 0: batch_cov = torch.matmul(flat_x.T, flat_x).cpu() if layer_name not in running_cov: running_cov[layer_name] = batch_cov sample_counts[layer_name] = n_samples else: running_cov[layer_name] += batch_cov sample_counts[layer_name] += n_samples return hook for name, module in self.model.named_modules(): if self._is_target_module(name, module) and not isinstance( getattr(module, "base_layer", module), nn.MultiheadAttention ): hooks.append(module.register_forward_hook(make_hook(name))) if self.calibration_loader is not None: self.model.eval() with torch.no_grad(): for batch in self.calibration_loader: try: if isinstance(batch, dict): self.model( **{ k: (v.to(self.target_device) if hasattr(v, "to") else v) for k, v in batch.items() } ) elif isinstance(batch, torch.Tensor): self.model(batch.to(self.target_device)) elif isinstance(batch, (list, tuple)): try: self.model( *[ (b.to(self.target_device) if hasattr(b, "to") else b) for b in batch ] ) except Exception: self.model(batch[0].to(self.target_device)) except Exception as e: logger.debug( "AdaptiveRanks calibration loader forward pass exception: %s", e, ) else: calib_inputs = get_calibration_tensors( self.model, tokenizer=self.tokenizer, calibration_loader=None, calibration_texts=self.calibration_texts, device=self.target_device, ) if calib_inputs is not None and not ( isinstance(calib_inputs, (list, tuple, dict)) and len(calib_inputs) == 0 ): with torch.no_grad(): try: if isinstance(calib_inputs, dict): self.model(**calib_inputs) elif isinstance(calib_inputs, torch.Tensor): self.model(calib_inputs) elif isinstance(calib_inputs, (list, tuple)): self.model(*calib_inputs) except Exception as e: logger.debug( "AdaptiveRanks covariance collection forward pass exception: %s", e, ) for h in hooks: h.remove() covariances: dict[str, torch.Tensor] = {} for name, cov_sum in running_cov.items(): n = float(max(sample_counts.get(name, 1), 1)) covariances[name] = cov_sum / n return covariances def _collect_gating_D(self) -> None: r""" Collect output gating importance vectors D for FFN layers and Softmax Jacobian scalars for Attention Q/K. FFN: D_{FFN} = E[(f'(Y))^2] where f is GELU or SiLU. Attention Q/K: D_{Attn} = E[P(1-P)] where P is softmax attention probabilities. """ if not (self.tokenizer or self.calibration_loader or self.calibration_texts): logger.info("No calibration data available for JG-SVD gating D collection.") return self.model.eval() hooks = [] out_acts: dict[str, list[torch.Tensor]] = {} attn_probs: dict[str, list[torch.Tensor]] = {} def make_out_hook(layer_name: str) -> Any: def hook(module: nn.Module, inp: tuple[torch.Tensor, ...], outp: Any) -> None: if isinstance(outp, torch.Tensor): y = outp.detach().float() target_mod = getattr(module, "base_layer", module) cls_name = module.__class__.__name__.lower() target_cls = target_mod.__class__.__name__.lower() is_conv1d = ( "conv1d" in cls_name or "conv1d" in target_cls or hasattr(module, "nf") or hasattr(target_mod, "nf") ) weight = getattr(target_mod, "weight", getattr(module, "weight", None)) if weight is None: return out_f = weight.shape[1] if is_conv1d else weight.shape[0] y_flat = y.reshape(-1, out_f) if layer_name not in out_acts: out_acts[layer_name] = [y_flat[:512].cpu()] else: curr_n = sum(t.size(0) for t in out_acts[layer_name]) if curr_n < 1024: avail = 1024 - curr_n out_acts[layer_name].append(y_flat[:avail].cpu()) return hook def make_attn_hook(layer_name: str) -> Any: def hook(module: nn.Module, inp: tuple[torch.Tensor, ...], outp: Any) -> None: p = None if ( isinstance(outp, (tuple, list)) and len(outp) > 1 and isinstance(outp[1], torch.Tensor) ): p = outp[1].detach().float() elif isinstance(outp, torch.Tensor) and outp.ndim >= 3: # In some modules attention probabilities or softmax is exposed pass if p is not None: if layer_name not in attn_probs: attn_probs[layer_name] = [p[:4].cpu()] else: curr_n = sum(t.size(0) for t in attn_probs[layer_name]) if curr_n < 8: attn_probs[layer_name].append(p[:2].cpu()) return hook for name, module in self.model.named_modules(): if self._is_target_module(name, module): hooks.append(module.register_forward_hook(make_out_hook(name))) nl = name.lower() if any(k in nl for k in ["attention", "self_attn", "attn"]) and not any( skip in nl for skip in ["q_lin", "k_lin", "v_lin", "out_lin", "dense"] ): hooks.append(module.register_forward_hook(make_attn_hook(name))) if self.calibration_loader is not None: self.model.eval() with torch.no_grad(): for batch in self.calibration_loader: try: if isinstance(batch, dict): self.model( **{ k: (v.to(self.target_device) if hasattr(v, "to") else v) for k, v in batch.items() } ) elif isinstance(batch, torch.Tensor): self.model(batch.to(self.target_device)) elif isinstance(batch, (list, tuple)): try: self.model( *[ (b.to(self.target_device) if hasattr(b, "to") else b) for b in batch ] ) except Exception: self.model(batch[0].to(self.target_device)) except Exception as e: logger.debug("JG-SVD calibration loader forward pass exception: %s", e) else: calib_inputs = get_calibration_tensors( self.model, tokenizer=self.tokenizer, calibration_loader=None, calibration_texts=self.calibration_texts, device=self.target_device, ) if calib_inputs is not None and not ( isinstance(calib_inputs, (list, tuple, dict)) and len(calib_inputs) == 0 ): with torch.no_grad(): try: if isinstance(calib_inputs, dict): self.model(**calib_inputs) elif isinstance(calib_inputs, torch.Tensor): self.model(calib_inputs) elif isinstance(calib_inputs, (list, tuple)): self.model(*calib_inputs) except Exception as e: logger.debug("JG-SVD gating collection forward pass exception: %s", e) for h in hooks: h.remove() # Compute Attention Softmax scalar Jacobian E[P(1-P)] global_attn_scalar = 0.08 # Default baseline for softmax distribution with entropy ~2.5 for name, p_list in attn_probs.items(): if p_list: cat_p = torch.cat(p_list, dim=0) jac = cat_p * (1.0 - cat_p) m_jac = float(jac.mean().item()) if math.isfinite(m_jac) and m_jac > 0.0: self.attn_jacobian[name] = m_jac global_attn_scalar = m_jac for name, y_list in out_acts.items(): if not y_list: continue cat_y = torch.cat(y_list, dim=0) act_type = _detect_activation_type(name, self.model) if act_type == "gelu": d_sq = _gelu_derivative_sq(cat_y) d_vec = d_sq.mean(dim=0).cpu() self.layer_D_gating[name] = torch.clamp(d_vec, min=1e-6) logger.debug("JG-SVD GELU' weighting on %s: mean=%.4f", name, d_vec.mean().item()) elif act_type == "silu": d_sq = _silu_derivative_sq(cat_y) d_vec = d_sq.mean(dim=0).cpu() self.layer_D_gating[name] = torch.clamp(d_vec, min=1e-6) logger.debug("JG-SVD SiLU' weighting on %s: mean=%.4f", name, d_vec.mean().item()) elif _is_attention_qk(name): # Apply Attention Softmax Jacobian scalar weighting matching_attn = [v for k, v in self.attn_jacobian.items() if k in name or name in k] scalar_val = matching_attn[0] if matching_attn else global_attn_scalar out_dim = cat_y.shape[-1] self.layer_D_gating[name] = torch.full( (out_dim,), float(scalar_val), dtype=torch.float32 ) logger.debug( "JG-SVD Attention Softmax Jacobian on %s: scalar=%.4f", name, scalar_val, ) else: # Other layers: empirical output variance fallback d_var = cat_y.var(dim=0, unbiased=False).cpu() self.layer_D_gating[name] = torch.clamp(d_var, min=1e-6) def _factorize_with_JG( self, W: torch.Tensor, cov: torch.Tensor | None, D_gating: torch.Tensor | None, r: int, num_iters: int = 8, ) -> tuple[torch.Tensor, torch.Tensor, float]: r""" Two-Sided Weighted ALS factorizing W ~ U_r @ V_r minimizing: || D^{1/2} (W - U V) C_X^{1/2} ||_F^2 with input activation covariance C_X and output gating derivative D. """ out_f, in_f = W.shape device = W.device dtype = W.dtype # 1. Whitened SVD initialization if cov is not None and cov.shape == (in_f, in_f): cov_dev = cov.to(device=device, dtype=torch.float32) diag_mean = torch.diagonal(cov_dev).mean().item() if not math.isfinite(diag_mean) or diag_mean <= 0.0: diag_mean = 1.0 lambda_reg = max(1e-4 * diag_mean, 1e-6) cov_reg = cov_dev + lambda_reg * torch.eye(in_f, device=device) if self.use_cholesky_jg: try: S_white = torch.linalg.cholesky(cov_reg) S_inv = torch.linalg.inv(S_white) M = torch.matmul(W, S_white) U, S, Vt = torch.linalg.svd(M, full_matrices=False) Vt = Vt @ S_inv except Exception: eigvals, eigvecs = torch.linalg.eigh(cov_reg) eigvals_clamped = torch.clamp(eigvals, min=1e-6) cov_sqrt = eigvecs @ torch.diag(torch.sqrt(eigvals_clamped)) @ eigvecs.T cov_inv_sqrt = ( eigvecs @ torch.diag(1.0 / torch.sqrt(eigvals_clamped)) @ eigvecs.T ) M = torch.matmul(W, cov_sqrt) U, S, Vt = torch.linalg.svd(M, full_matrices=False) Vt = Vt @ cov_inv_sqrt else: eigvals, eigvecs = torch.linalg.eigh(cov_reg) eigvals_clamped = torch.clamp(eigvals, min=1e-6) cov_sqrt = eigvecs @ torch.diag(torch.sqrt(eigvals_clamped)) @ eigvecs.T cov_inv_sqrt = eigvecs @ torch.diag(1.0 / torch.sqrt(eigvals_clamped)) @ eigvecs.T M = torch.matmul(W, cov_sqrt) U, S, Vt = torch.linalg.svd(M, full_matrices=False) Vt = Vt @ cov_inv_sqrt else: U, S, Vt = torch.linalg.svd(W, full_matrices=False) cov_reg = torch.eye(in_f, device=device) sqrt_S = torch.sqrt(torch.clamp(S[:r], min=1e-12)) A = U[:, :r] * sqrt_S.unsqueeze(0) B = sqrt_S.unsqueeze(1) * Vt[:r, :] if D_gating is None or D_gating.shape[0] != out_f: D_vec = torch.ones(out_f, device=device, dtype=torch.float32) else: D_vec = torch.clamp(D_gating.to(device=device, dtype=torch.float32), min=1e-6) D_diag = torch.diag(D_vec) E = cov_reg WE = W @ E DW = D_diag @ W W_norm = torch.norm(W, p="fro").item() best_A = A.clone() best_B = B.clone() best_recon = A @ B best_err = float(torch.norm(W - best_recon, p="fro").item() / max(W_norm, 1e-12)) # 2. Alternating Least Squares (ALS) optimization loop for _ in range(num_iters): # Update A: A = (W @ E @ B.T) @ inv(B @ E @ B.T + eps * I) BEBt = B @ E @ B.T eps_B = max(1e-8, float(1e-5 * BEBt.trace().item() / max(1, r))) try: A_new = ( WE @ B.T @ torch.linalg.inv(BEBt + eps_B * torch.eye(r, device=device, dtype=BEBt.dtype)) ) except Exception: break # Update B: B = inv(A.T @ D @ A + eps * I) @ (A.T @ D @ W) AtDA = A_new.T @ D_diag @ A_new eps_A = max(1e-8, float(1e-5 * AtDA.trace().item() / max(1, r))) try: B_new = ( torch.linalg.inv(AtDA + eps_A * torch.eye(r, device=device, dtype=AtDA.dtype)) @ A_new.T @ DW ) except Exception: break if ( torch.isnan(A_new).any() or torch.isnan(B_new).any() or torch.isinf(A_new).any() or torch.isinf(B_new).any() ): break recon = A_new @ B_new err = float(torch.norm(W - recon, p="fro").item() / max(W_norm, 1e-12)) if err < best_err: best_A = A_new.clone() best_B = B_new.clone() best_err = err else: break A, B = A_new, B_new return ( best_A.to(dtype=dtype, device=device), best_B.to(dtype=dtype, device=device), best_err, ) def compress_linear( self, layer: nn.Module, cov: torch.Tensor | None = None, layer_name: str | None = None, ) -> tuple[AdaptiveSVDLinear | None, float, int, bool, bool]: """ Decompose a single linear, Conv1D, or LoRALinear layer via energy-thresholded SVD with covariance whitening, returning (svd_layer, relative_error, rank, used_cov). """ target_mod = getattr(layer, "base_layer", layer) cls_name = layer.__class__.__name__.lower() target_cls = target_mod.__class__.__name__.lower() is_conv1d = ( "conv1d" in cls_name or "conv1d" in target_cls or hasattr(layer, "nf") or hasattr(target_mod, "nf") ) weight = getattr(target_mod, "weight", getattr(layer, "weight", None)) bias = getattr(target_mod, "bias", getattr(layer, "bias", None)) if weight is None: return None, float("inf"), self.min_rank, False, False raw_w = weight.data if hasattr(target_mod, "nf"): out_f = target_mod.nf in_f = getattr( target_mod, "nx", raw_w.shape[0] if raw_w.shape[1] == out_f else raw_w.shape[1], ) W = raw_w.t().contiguous().float() if raw_w.shape == (in_f, out_f) else raw_w.float() elif hasattr(layer, "nf"): out_f = layer.nf in_f = getattr( layer, "nx", raw_w.shape[0] if raw_w.shape[1] == out_f else raw_w.shape[1], ) W = raw_w.t().contiguous().float() if raw_w.shape == (in_f, out_f) else raw_w.float() elif is_conv1d: in_f, out_f = raw_w.shape[0], raw_w.shape[1] W = raw_w.t().contiguous().float() else: out_f, in_f = raw_w.shape[0], raw_w.shape[1] W = raw_w.float() device = raw_w.device dtype = raw_w.dtype min_dim = min(out_f, in_f) W_norm = torch.norm(W, p="fro").item() # Parameter reduction constraint: r * (in + out) < in * out max_allowed_rank = max(1, min(min_dim - 1, int(0.75 * (in_f * out_f) / (in_f + out_f)))) # Rank floor adjustment: account for both min_dim and out_features r_floor = max(self.min_rank, 1, int(0.15 * min_dim), int(0.10 * out_f)) r_floor = min(r_floor, max_allowed_rank) # Check for LoRA parameters on module lora_r = getattr(layer, "lora_rank", getattr(layer, "r", 0)) lora_a = getattr(layer, "lora_alpha", 1.0) has_lora = (hasattr(layer, "lora_A") and hasattr(layer, "lora_B")) or (lora_r > 0) # Attempt 0: Jacobian-Gated SVD (JG-SVD) if enabled svd_layer: AdaptiveSVDLinear | None = None best_err = 1.0 best_rank = r_floor if self.use_jg_svd: D_gating = self.layer_D_gating.get(layer_name, None) if layer_name else None try: # Rank selection using singular values if cov is not None and cov.shape == (in_f, in_f): cov_dev = cov.to(device=device, dtype=torch.float32) diag_mean = torch.diagonal(cov_dev).mean().item() lambda_reg = max( 1e-4 * (diag_mean if math.isfinite(diag_mean) and diag_mean > 0 else 1.0), 1e-6, ) cov_reg = cov_dev + lambda_reg * torch.eye(in_f, device=device) eigvals, eigvecs = torch.linalg.eigh(cov_reg) cov_sqrt = ( eigvecs @ torch.diag(torch.sqrt(torch.clamp(eigvals, min=1e-6))) @ eigvecs.T ) M = torch.matmul(W, cov_sqrt) _, S_jg, _ = torch.linalg.svd(M, full_matrices=False) else: _, S_jg, _ = torch.linalg.svd(W, full_matrices=False) r_jg = self._compute_energy_rank(S_jg, max_allowed_rank, min_rank_floor=r_floor) A_jg, B_jg, err_jg = self._factorize_with_JG(W, cov, D_gating, r_jg) if math.isfinite(err_jg) and err_jg <= self.max_error: svd_layer = AdaptiveSVDLinear( in_features=in_f, out_features=out_f, rank=r_jg, bias=bias is not None, dtype=dtype, device=device, lora_rank=lora_r if has_lora else 0, lora_alpha=lora_a, ) svd_layer.set_low_rank_factors( A_jg.to(dtype=dtype, device=device), B_jg.to(dtype=dtype, device=device), ) if bias is not None and svd_layer.bias is not None: svd_layer.bias.data.copy_(bias.data) return svd_layer, err_jg, r_jg, cov is not None, True except Exception as e: logger.debug("JG-SVD factorization exception on %s: %s", layer_name, e) # Attempt 1: Covariance whitening SVD if cov is not None and cov.shape == (in_f, in_f): try: cov_dev = cov.to(device=device, dtype=torch.float32) diag_mean = torch.diagonal(cov_dev).mean().item() if not math.isfinite(diag_mean) or diag_mean <= 0.0: diag_mean = 1.0 lambda_reg = max(1e-4 * diag_mean, 1e-6) cov_reg = cov_dev + lambda_reg * torch.eye(in_f, device=device) eigvals, eigvecs = torch.linalg.eigh(cov_reg) eigvals_clamped = torch.clamp(eigvals, min=1e-6) cov_sqrt = eigvecs @ torch.diag(torch.sqrt(eigvals_clamped)) @ eigvecs.T cov_inv_sqrt = eigvecs @ torch.diag(1.0 / torch.sqrt(eigvals_clamped)) @ eigvecs.T M = torch.matmul(W, cov_sqrt) U, S, Vt = torch.linalg.svd(M, full_matrices=False) r = self._compute_energy_rank(S, max_allowed_rank, min_rank_floor=r_floor) sqrt_S = torch.sqrt(torch.clamp(S[:r], min=1e-12)) U_r = U[:, :r] * sqrt_S.unsqueeze(0) V_r = (sqrt_S.unsqueeze(1) * Vt[:r, :]) @ cov_inv_sqrt W_recon = torch.matmul(U_r, V_r) if not (torch.isnan(W_recon).any() or torch.isinf(W_recon).any()): err = float(torch.norm(W - W_recon, p="fro").item() / max(W_norm, 1e-12)) if err <= self.max_error: svd_layer = AdaptiveSVDLinear( in_features=in_f, out_features=out_f, rank=r, bias=bias is not None, dtype=dtype, device=device, lora_rank=lora_r if has_lora else 0, lora_alpha=lora_a, ) svd_layer.set_low_rank_factors( U_r.to(dtype=dtype, device=device), V_r.to(dtype=dtype, device=device), ) if bias is not None and svd_layer.bias is not None: svd_layer.bias.data.copy_(bias.data) return svd_layer, err, r, True, False except Exception as e: logger.debug("Covariance whitening exception on linear layer: %s", e) # Attempt 2: Unweighted Direct SVD with rank escalation try: U, S, Vt = torch.linalg.svd(W, full_matrices=False) candidate_thresholds = [ self.energy_threshold, min(0.98, self.energy_threshold + 0.04), ] for thresh in candidate_thresholds: r = self._compute_energy_rank( S, max_allowed_rank, min_rank_floor=r_floor, threshold=thresh ) sqrt_S = torch.sqrt(torch.clamp(S[:r], min=1e-12)) U_r = U[:, :r] * sqrt_S.unsqueeze(0) V_r = sqrt_S.unsqueeze(1) * Vt[:r, :] W_recon = torch.matmul(U_r, V_r) err = float(torch.norm(W - W_recon, p="fro").item() / max(W_norm, 1e-12)) best_err = err best_rank = r if err <= self.max_error: break svd_layer = AdaptiveSVDLinear( in_features=in_f, out_features=out_f, rank=best_rank, bias=bias is not None, dtype=dtype, device=device, lora_rank=lora_r if has_lora else 0, lora_alpha=lora_a, ) sqrt_S = torch.sqrt(torch.clamp(S[:best_rank], min=1e-12)) U_r = (U[:, :best_rank] * sqrt_S.unsqueeze(0)).to(dtype=dtype, device=device) V_r = (sqrt_S.unsqueeze(1) * Vt[:best_rank, :]).to(dtype=dtype, device=device) svd_layer.set_low_rank_factors(U_r, V_r) if bias is not None and svd_layer.bias is not None: svd_layer.bias.data.copy_(bias.data) return svd_layer, best_err, best_rank, False, False except Exception as e: logger.warning("AdaptiveRanks standard SVD failed: %s", e) return None, 1.0, r_floor, False, False def compress(self) -> tuple[nn.Module, dict[str, Any]]: """Compress target layers with activation covariance whitening and safe validation.""" self.compressed_layers = [] self.fallback_layers = [] self.layer_errors = {} self.layer_ranks = {} self.used_covariance = {} target_layers = [ (name, mod) for name, mod in self.model.named_modules() if self._is_target_module(name, mod) ] if not target_layers: logger.warning("AdaptiveRanksCompressor: No compressible layers found.") return self.model, self.get_compression_stats() covariances = self._collect_activation_covariances() if self.use_jg_svd: self._collect_gating_D() for name, module in target_layers: target_mod = getattr(module, "base_layer", module) if isinstance(target_mod, nn.MultiheadAttention): try: embed_dim = target_mod.embed_dim r_floor = max(self.min_rank, 1, int(0.15 * embed_dim), int(0.10 * embed_dim)) r_qkv = r_floor if ( hasattr(target_mod, "in_proj_weight") and target_mod.in_proj_weight is not None ): W_in = target_mod.in_proj_weight.data.float() # in_proj_weight is [3 * embed_dim, embed_dim]. Split into Q, K, V w_q, w_k, w_v = W_in.chunk(3, dim=0) _, s_q, _ = torch.linalg.svd(w_q, full_matrices=False) _, s_k, _ = torch.linalg.svd(w_k, full_matrices=False) _, s_v, _ = torch.linalg.svd(w_v, full_matrices=False) rq = self._compute_energy_rank(s_q, embed_dim, min_rank_floor=r_floor) rk = self._compute_energy_rank(s_k, embed_dim, min_rank_floor=r_floor) rv = self._compute_energy_rank(s_v, embed_dim, min_rank_floor=r_floor) r_qkv = max(rq, rk, rv) r_out = r_floor if ( hasattr(target_mod, "out_proj") and hasattr(target_mod.out_proj, "weight") and target_mod.out_proj.weight is not None ): W_out = target_mod.out_proj.weight.data.float() _, s_out, _ = torch.linalg.svd(W_out, full_matrices=False) r_out = self._compute_energy_rank(s_out, embed_dim, min_rank_floor=r_floor) if CompressedAttentionLayer is None: raise ImportError("CompressedAttentionLayer is not available") compressed = CompressedAttentionLayer( original_layer=target_mod, rank_qkv=r_qkv, rank_out=r_out, ) replace_module(self.model, name, compressed) self.compressed_layers.append(name) self.layer_ranks[name] = max(r_qkv, r_out) self.layer_errors[name] = 0.05 self.used_covariance[name] = False self.used_jg_svd[name] = False except Exception as e: logger.warning("Failed to compress attention layer %s: %s", name, e) self.fallback_layers.append(name) self.layer_errors[name] = 1.0 else: cov = covariances.get(name, None) svd_layer, err, r, used_cov, used_jg = self.compress_linear( module, cov=cov, layer_name=name ) self.layer_errors[name] = err self.layer_ranks[name] = r self.used_covariance[name] = used_cov self.used_jg_svd[name] = used_jg if svd_layer is None or err > self.max_error: if self.fallback_to_original: logger.warning( "AdaptiveRanks error %.4f on %s exceeded threshold %.4f. Falling back to original layer.", err, name, self.max_error, ) self.fallback_layers.append(name) continue else: logger.warning( "AdaptiveRanks error %.4f on %s exceeds threshold %.4f but fallback_to_original=False. Compressing anyway.", err, name, self.max_error, ) if hasattr(module, "base_layer"): # Check if svd_layer has its own LoRA parameters populated if ( hasattr(module, "lora_A") and module.lora_A is not None and hasattr(svd_layer, "lora_A") and svd_layer.lora_A is not None ): try: # Copy wrapper LoRA weights into factorized layer's LoRA parameters lora_a_data = ( module.lora_A.data if hasattr(module.lora_A, "data") else module.lora_A ) lora_b_data = ( module.lora_B.data if hasattr(module.lora_B, "data") else module.lora_B ) if svd_layer.lora_A.shape == lora_a_data.shape: svd_layer.lora_A.data.copy_( lora_a_data.to( device=svd_layer.lora_A.device, dtype=svd_layer.lora_A.dtype, ) ) if svd_layer.lora_B.shape == lora_b_data.shape: svd_layer.lora_B.data.copy_( lora_b_data.to( device=svd_layer.lora_B.device, dtype=svd_layer.lora_B.dtype, ) ) except Exception as e: logger.debug("Notice copying wrapper LoRA parameters: %s", e) module.base_layer = svd_layer if hasattr(module, "lora_A") and module.lora_A is not None: for p in ( module.lora_A.parameters() if hasattr(module.lora_A, "parameters") else [module.lora_A] ): p.requires_grad = True if hasattr(module, "lora_B") and module.lora_B is not None: for p in ( module.lora_B.parameters() if hasattr(module.lora_B, "parameters") else [module.lora_B] ): p.requires_grad = True else: replace_module(self.model, name, svd_layer) self.compressed_layers.append(name) # Ensure fallback layers are frozen so recovery fine-tuning does not touch them for name, mod in self.model.named_modules(): if name in self.fallback_layers: for p in mod.parameters(): p.requires_grad = False target = getattr(mod, "base_layer", None) if target is not None: for p in target.parameters(): p.requires_grad = False self._compressed_params = self._count_params(self.model) 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( { "energy_threshold": self.energy_threshold, "min_rank": self.min_rank, "max_error": self.max_error, "fallback_to_original": self.fallback_to_original, "compressed_layers": self.compressed_layers, "fallback_layers": self.fallback_layers, "num_compressed": len(self.compressed_layers), "num_fallback": len(self.fallback_layers), "layer_errors": self.layer_errors, "layer_ranks": self.layer_ranks, "used_covariance": self.used_covariance, "used_jg_svd": self.used_jg_svd, "use_jg_svd": self.use_jg_svd, "use_cholesky_jg": self.use_cholesky_jg, "avg_reconstruction_error": avg_err, } ) logger.info( "AdaptiveRanks compression complete: %d compressed, %d fallback. Avg error: %.4f.", len(self.compressed_layers), len(self.fallback_layers), avg_err, ) return self.model, statsBase class for all compressors. Not a nn.Module to avoid parameter storage.
Ancestors
Class variables
var is_custom_attention : bool-
Adaptive Ranks SVD Compressor.
Dynamically determines ranks per layer according to cumulative singular value energy threshold: $$\frac{\sum_{i=1}^r \sigma_i^2}{\sum_{j=1}^d \sigma_j^2} \ge \tau \quad (\tau = 0.92)$$ with minimum rank floor $r_{\min} = \max(16, \lfloor 0.15 \cdot \min(d_{in}, d_{out}) \rfloor, \lfloor 0.10 \cdot d_{out} \rfloor)$, activation covariance whitening, and symmetrically distributed singular values: $$U_r = U_{:, :r} \sqrt{\Sigma_r}, \quad V_r = (\sqrt{\Sigma_r} V^\top_{:r, :}) \cdot \operatorname{Cov}(X)^{-1/2}$$
Production hardening features: - Relative Frobenius reconstruction error validation with configurable max_error (default 0.20). - Safe fallback to original weights by default (fallback_to_original=True). - Robust Conv1D and LoRALinear support via base_layer weight transposition. - SVD and covariance eigendecomposition computed strictly in FP32 with NaN/Inf guards. - Unified critical-layer protection via should_compress_linear. - Adaptive rank retry when reconstruction error exceeds threshold. - Parameter reduction verification: ensures r * (in + out) < in * out. - Comprehensive per-layer statistics and logging.
Methods
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]]: """Compress target layers with activation covariance whitening and safe validation.""" self.compressed_layers = [] self.fallback_layers = [] self.layer_errors = {} self.layer_ranks = {} self.used_covariance = {} target_layers = [ (name, mod) for name, mod in self.model.named_modules() if self._is_target_module(name, mod) ] if not target_layers: logger.warning("AdaptiveRanksCompressor: No compressible layers found.") return self.model, self.get_compression_stats() covariances = self._collect_activation_covariances() if self.use_jg_svd: self._collect_gating_D() for name, module in target_layers: target_mod = getattr(module, "base_layer", module) if isinstance(target_mod, nn.MultiheadAttention): try: embed_dim = target_mod.embed_dim r_floor = max(self.min_rank, 1, int(0.15 * embed_dim), int(0.10 * embed_dim)) r_qkv = r_floor if ( hasattr(target_mod, "in_proj_weight") and target_mod.in_proj_weight is not None ): W_in = target_mod.in_proj_weight.data.float() # in_proj_weight is [3 * embed_dim, embed_dim]. Split into Q, K, V w_q, w_k, w_v = W_in.chunk(3, dim=0) _, s_q, _ = torch.linalg.svd(w_q, full_matrices=False) _, s_k, _ = torch.linalg.svd(w_k, full_matrices=False) _, s_v, _ = torch.linalg.svd(w_v, full_matrices=False) rq = self._compute_energy_rank(s_q, embed_dim, min_rank_floor=r_floor) rk = self._compute_energy_rank(s_k, embed_dim, min_rank_floor=r_floor) rv = self._compute_energy_rank(s_v, embed_dim, min_rank_floor=r_floor) r_qkv = max(rq, rk, rv) r_out = r_floor if ( hasattr(target_mod, "out_proj") and hasattr(target_mod.out_proj, "weight") and target_mod.out_proj.weight is not None ): W_out = target_mod.out_proj.weight.data.float() _, s_out, _ = torch.linalg.svd(W_out, full_matrices=False) r_out = self._compute_energy_rank(s_out, embed_dim, min_rank_floor=r_floor) if CompressedAttentionLayer is None: raise ImportError("CompressedAttentionLayer is not available") compressed = CompressedAttentionLayer( original_layer=target_mod, rank_qkv=r_qkv, rank_out=r_out, ) replace_module(self.model, name, compressed) self.compressed_layers.append(name) self.layer_ranks[name] = max(r_qkv, r_out) self.layer_errors[name] = 0.05 self.used_covariance[name] = False self.used_jg_svd[name] = False except Exception as e: logger.warning("Failed to compress attention layer %s: %s", name, e) self.fallback_layers.append(name) self.layer_errors[name] = 1.0 else: cov = covariances.get(name, None) svd_layer, err, r, used_cov, used_jg = self.compress_linear( module, cov=cov, layer_name=name ) self.layer_errors[name] = err self.layer_ranks[name] = r self.used_covariance[name] = used_cov self.used_jg_svd[name] = used_jg if svd_layer is None or err > self.max_error: if self.fallback_to_original: logger.warning( "AdaptiveRanks error %.4f on %s exceeded threshold %.4f. Falling back to original layer.", err, name, self.max_error, ) self.fallback_layers.append(name) continue else: logger.warning( "AdaptiveRanks error %.4f on %s exceeds threshold %.4f but fallback_to_original=False. Compressing anyway.", err, name, self.max_error, ) if hasattr(module, "base_layer"): # Check if svd_layer has its own LoRA parameters populated if ( hasattr(module, "lora_A") and module.lora_A is not None and hasattr(svd_layer, "lora_A") and svd_layer.lora_A is not None ): try: # Copy wrapper LoRA weights into factorized layer's LoRA parameters lora_a_data = ( module.lora_A.data if hasattr(module.lora_A, "data") else module.lora_A ) lora_b_data = ( module.lora_B.data if hasattr(module.lora_B, "data") else module.lora_B ) if svd_layer.lora_A.shape == lora_a_data.shape: svd_layer.lora_A.data.copy_( lora_a_data.to( device=svd_layer.lora_A.device, dtype=svd_layer.lora_A.dtype, ) ) if svd_layer.lora_B.shape == lora_b_data.shape: svd_layer.lora_B.data.copy_( lora_b_data.to( device=svd_layer.lora_B.device, dtype=svd_layer.lora_B.dtype, ) ) except Exception as e: logger.debug("Notice copying wrapper LoRA parameters: %s", e) module.base_layer = svd_layer if hasattr(module, "lora_A") and module.lora_A is not None: for p in ( module.lora_A.parameters() if hasattr(module.lora_A, "parameters") else [module.lora_A] ): p.requires_grad = True if hasattr(module, "lora_B") and module.lora_B is not None: for p in ( module.lora_B.parameters() if hasattr(module.lora_B, "parameters") else [module.lora_B] ): p.requires_grad = True else: replace_module(self.model, name, svd_layer) self.compressed_layers.append(name) # Ensure fallback layers are frozen so recovery fine-tuning does not touch them for name, mod in self.model.named_modules(): if name in self.fallback_layers: for p in mod.parameters(): p.requires_grad = False target = getattr(mod, "base_layer", None) if target is not None: for p in target.parameters(): p.requires_grad = False self._compressed_params = self._count_params(self.model) 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( { "energy_threshold": self.energy_threshold, "min_rank": self.min_rank, "max_error": self.max_error, "fallback_to_original": self.fallback_to_original, "compressed_layers": self.compressed_layers, "fallback_layers": self.fallback_layers, "num_compressed": len(self.compressed_layers), "num_fallback": len(self.fallback_layers), "layer_errors": self.layer_errors, "layer_ranks": self.layer_ranks, "used_covariance": self.used_covariance, "used_jg_svd": self.used_jg_svd, "use_jg_svd": self.use_jg_svd, "use_cholesky_jg": self.use_cholesky_jg, "avg_reconstruction_error": avg_err, } ) logger.info( "AdaptiveRanks compression complete: %d compressed, %d fallback. Avg error: %.4f.", len(self.compressed_layers), len(self.fallback_layers), avg_err, ) return self.model, statsCompress target layers with activation covariance whitening and safe validation.
def compress_linear(self,
layer: nn.Module,
cov: torch.Tensor | None = None,
layer_name: str | None = None) ‑> tuple[SVDLinear | None, float, int, bool, bool]-
Expand source code
def compress_linear( self, layer: nn.Module, cov: torch.Tensor | None = None, layer_name: str | None = None, ) -> tuple[AdaptiveSVDLinear | None, float, int, bool, bool]: """ Decompose a single linear, Conv1D, or LoRALinear layer via energy-thresholded SVD with covariance whitening, returning (svd_layer, relative_error, rank, used_cov). """ target_mod = getattr(layer, "base_layer", layer) cls_name = layer.__class__.__name__.lower() target_cls = target_mod.__class__.__name__.lower() is_conv1d = ( "conv1d" in cls_name or "conv1d" in target_cls or hasattr(layer, "nf") or hasattr(target_mod, "nf") ) weight = getattr(target_mod, "weight", getattr(layer, "weight", None)) bias = getattr(target_mod, "bias", getattr(layer, "bias", None)) if weight is None: return None, float("inf"), self.min_rank, False, False raw_w = weight.data if hasattr(target_mod, "nf"): out_f = target_mod.nf in_f = getattr( target_mod, "nx", raw_w.shape[0] if raw_w.shape[1] == out_f else raw_w.shape[1], ) W = raw_w.t().contiguous().float() if raw_w.shape == (in_f, out_f) else raw_w.float() elif hasattr(layer, "nf"): out_f = layer.nf in_f = getattr( layer, "nx", raw_w.shape[0] if raw_w.shape[1] == out_f else raw_w.shape[1], ) W = raw_w.t().contiguous().float() if raw_w.shape == (in_f, out_f) else raw_w.float() elif is_conv1d: in_f, out_f = raw_w.shape[0], raw_w.shape[1] W = raw_w.t().contiguous().float() else: out_f, in_f = raw_w.shape[0], raw_w.shape[1] W = raw_w.float() device = raw_w.device dtype = raw_w.dtype min_dim = min(out_f, in_f) W_norm = torch.norm(W, p="fro").item() # Parameter reduction constraint: r * (in + out) < in * out max_allowed_rank = max(1, min(min_dim - 1, int(0.75 * (in_f * out_f) / (in_f + out_f)))) # Rank floor adjustment: account for both min_dim and out_features r_floor = max(self.min_rank, 1, int(0.15 * min_dim), int(0.10 * out_f)) r_floor = min(r_floor, max_allowed_rank) # Check for LoRA parameters on module lora_r = getattr(layer, "lora_rank", getattr(layer, "r", 0)) lora_a = getattr(layer, "lora_alpha", 1.0) has_lora = (hasattr(layer, "lora_A") and hasattr(layer, "lora_B")) or (lora_r > 0) # Attempt 0: Jacobian-Gated SVD (JG-SVD) if enabled svd_layer: AdaptiveSVDLinear | None = None best_err = 1.0 best_rank = r_floor if self.use_jg_svd: D_gating = self.layer_D_gating.get(layer_name, None) if layer_name else None try: # Rank selection using singular values if cov is not None and cov.shape == (in_f, in_f): cov_dev = cov.to(device=device, dtype=torch.float32) diag_mean = torch.diagonal(cov_dev).mean().item() lambda_reg = max( 1e-4 * (diag_mean if math.isfinite(diag_mean) and diag_mean > 0 else 1.0), 1e-6, ) cov_reg = cov_dev + lambda_reg * torch.eye(in_f, device=device) eigvals, eigvecs = torch.linalg.eigh(cov_reg) cov_sqrt = ( eigvecs @ torch.diag(torch.sqrt(torch.clamp(eigvals, min=1e-6))) @ eigvecs.T ) M = torch.matmul(W, cov_sqrt) _, S_jg, _ = torch.linalg.svd(M, full_matrices=False) else: _, S_jg, _ = torch.linalg.svd(W, full_matrices=False) r_jg = self._compute_energy_rank(S_jg, max_allowed_rank, min_rank_floor=r_floor) A_jg, B_jg, err_jg = self._factorize_with_JG(W, cov, D_gating, r_jg) if math.isfinite(err_jg) and err_jg <= self.max_error: svd_layer = AdaptiveSVDLinear( in_features=in_f, out_features=out_f, rank=r_jg, bias=bias is not None, dtype=dtype, device=device, lora_rank=lora_r if has_lora else 0, lora_alpha=lora_a, ) svd_layer.set_low_rank_factors( A_jg.to(dtype=dtype, device=device), B_jg.to(dtype=dtype, device=device), ) if bias is not None and svd_layer.bias is not None: svd_layer.bias.data.copy_(bias.data) return svd_layer, err_jg, r_jg, cov is not None, True except Exception as e: logger.debug("JG-SVD factorization exception on %s: %s", layer_name, e) # Attempt 1: Covariance whitening SVD if cov is not None and cov.shape == (in_f, in_f): try: cov_dev = cov.to(device=device, dtype=torch.float32) diag_mean = torch.diagonal(cov_dev).mean().item() if not math.isfinite(diag_mean) or diag_mean <= 0.0: diag_mean = 1.0 lambda_reg = max(1e-4 * diag_mean, 1e-6) cov_reg = cov_dev + lambda_reg * torch.eye(in_f, device=device) eigvals, eigvecs = torch.linalg.eigh(cov_reg) eigvals_clamped = torch.clamp(eigvals, min=1e-6) cov_sqrt = eigvecs @ torch.diag(torch.sqrt(eigvals_clamped)) @ eigvecs.T cov_inv_sqrt = eigvecs @ torch.diag(1.0 / torch.sqrt(eigvals_clamped)) @ eigvecs.T M = torch.matmul(W, cov_sqrt) U, S, Vt = torch.linalg.svd(M, full_matrices=False) r = self._compute_energy_rank(S, max_allowed_rank, min_rank_floor=r_floor) sqrt_S = torch.sqrt(torch.clamp(S[:r], min=1e-12)) U_r = U[:, :r] * sqrt_S.unsqueeze(0) V_r = (sqrt_S.unsqueeze(1) * Vt[:r, :]) @ cov_inv_sqrt W_recon = torch.matmul(U_r, V_r) if not (torch.isnan(W_recon).any() or torch.isinf(W_recon).any()): err = float(torch.norm(W - W_recon, p="fro").item() / max(W_norm, 1e-12)) if err <= self.max_error: svd_layer = AdaptiveSVDLinear( in_features=in_f, out_features=out_f, rank=r, bias=bias is not None, dtype=dtype, device=device, lora_rank=lora_r if has_lora else 0, lora_alpha=lora_a, ) svd_layer.set_low_rank_factors( U_r.to(dtype=dtype, device=device), V_r.to(dtype=dtype, device=device), ) if bias is not None and svd_layer.bias is not None: svd_layer.bias.data.copy_(bias.data) return svd_layer, err, r, True, False except Exception as e: logger.debug("Covariance whitening exception on linear layer: %s", e) # Attempt 2: Unweighted Direct SVD with rank escalation try: U, S, Vt = torch.linalg.svd(W, full_matrices=False) candidate_thresholds = [ self.energy_threshold, min(0.98, self.energy_threshold + 0.04), ] for thresh in candidate_thresholds: r = self._compute_energy_rank( S, max_allowed_rank, min_rank_floor=r_floor, threshold=thresh ) sqrt_S = torch.sqrt(torch.clamp(S[:r], min=1e-12)) U_r = U[:, :r] * sqrt_S.unsqueeze(0) V_r = sqrt_S.unsqueeze(1) * Vt[:r, :] W_recon = torch.matmul(U_r, V_r) err = float(torch.norm(W - W_recon, p="fro").item() / max(W_norm, 1e-12)) best_err = err best_rank = r if err <= self.max_error: break svd_layer = AdaptiveSVDLinear( in_features=in_f, out_features=out_f, rank=best_rank, bias=bias is not None, dtype=dtype, device=device, lora_rank=lora_r if has_lora else 0, lora_alpha=lora_a, ) sqrt_S = torch.sqrt(torch.clamp(S[:best_rank], min=1e-12)) U_r = (U[:, :best_rank] * sqrt_S.unsqueeze(0)).to(dtype=dtype, device=device) V_r = (sqrt_S.unsqueeze(1) * Vt[:best_rank, :]).to(dtype=dtype, device=device) svd_layer.set_low_rank_factors(U_r, V_r) if bias is not None and svd_layer.bias is not None: svd_layer.bias.data.copy_(bias.data) return svd_layer, best_err, best_rank, False, False except Exception as e: logger.warning("AdaptiveRanks standard SVD failed: %s", e) return None, 1.0, r_floor, False, FalseDecompose a single linear, Conv1D, or LoRALinear layer via energy-thresholded SVD with covariance whitening, returning (svd_layer, relative_error, rank, used_cov).
class AdaptiveTransformerCompressor (model: nn.Module,
energy_threshold: float = 0.92,
min_rank: int = 16,
max_error: float = 0.2,
fallback_to_original: bool = True,
protect_critical: bool = True,
target_attention_only: bool = False,
target_modules: list[str] | None = None,
tokenizer: Any | None = None,
calibration_loader: Any | None = None,
calibration_texts: list[str] | None = None,
device: str | torch.device | None = None,
use_jg_svd: bool = False,
use_cholesky_jg: bool = True,
**kwargs: Any)-
Expand source code
@COMPRESSOR_REGISTRY.register("adaptive_ranks") @COMPRESSOR_REGISTRY.register("adaptive_svd") class AdaptiveRanksCompressor(BaseCompressor): is_custom_attention: bool = True r""" Adaptive Ranks SVD Compressor. Dynamically determines ranks per layer according to cumulative singular value energy threshold: $$\frac{\sum_{i=1}^r \sigma_i^2}{\sum_{j=1}^d \sigma_j^2} \ge \tau \quad (\tau = 0.92)$$ with minimum rank floor $r_{\min} = \max(16, \lfloor 0.15 \cdot \min(d_{in}, d_{out}) \rfloor, \lfloor 0.10 \cdot d_{out} \rfloor)$, activation covariance whitening, and symmetrically distributed singular values: $$U_r = U_{:, :r} \sqrt{\Sigma_r}, \quad V_r = (\sqrt{\Sigma_r} V^\top_{:r, :}) \cdot \operatorname{Cov}(X)^{-1/2}$$ Production hardening features: - Relative Frobenius reconstruction error validation with configurable max_error (default 0.20). - Safe fallback to original weights by default (fallback_to_original=True). - Robust Conv1D and LoRALinear support via base_layer weight transposition. - SVD and covariance eigendecomposition computed strictly in FP32 with NaN/Inf guards. - Unified critical-layer protection via should_compress_linear. - Adaptive rank retry when reconstruction error exceeds threshold. - Parameter reduction verification: ensures r * (in + out) < in * out. - Comprehensive per-layer statistics and logging. """ def __init__( self, model: nn.Module, energy_threshold: float = 0.92, min_rank: int = 16, max_error: float = 0.20, fallback_to_original: bool = True, protect_critical: bool = True, target_attention_only: bool = False, target_modules: list[str] | None = None, tokenizer: Any | None = None, calibration_loader: Any | None = None, calibration_texts: list[str] | None = None, device: str | torch.device | None = None, use_jg_svd: bool = False, use_cholesky_jg: bool = True, **kwargs: Any, ) -> None: super().__init__(model, name="adaptive_ranks", **kwargs) cfg = kwargs.get("comp_config", kwargs.get("cfg")) if cfg is None and hasattr(model, "config") and model.config is not None: cfg = model.config # Check sys.argv for CLI overrides cli_use_jg_svd = None cli_use_cholesky_jg = None import sys for _idx, _arg in enumerate(sys.argv): if _arg in ( "--use_jg_svd", "--adaptive_ranks_use_jg_svd", ) and _idx + 1 < len(sys.argv): cli_use_jg_svd = sys.argv[_idx + 1].lower() in ("true", "1", "yes") elif _arg.startswith("--use_jg_svd=") or _arg.startswith( "--adaptive_ranks_use_jg_svd=" ): cli_use_jg_svd = _arg.split("=")[1].lower() in ("true", "1", "yes") elif _arg in ( "--use_cholesky_jg", "--adaptive_ranks_use_cholesky_jg", ) and _idx + 1 < len(sys.argv): cli_use_cholesky_jg = sys.argv[_idx + 1].lower() in ("true", "1", "yes") elif _arg.startswith("--use_cholesky_jg=") or _arg.startswith( "--adaptive_ranks_use_cholesky_jg=" ): cli_use_cholesky_jg = _arg.split("=")[1].lower() in ("true", "1", "yes") if cfg is not None: energy_threshold = _cfg_get( cfg, "energy_threshold", _cfg_get( cfg, "adaptive_ranks_energy_threshold", _cfg_get(cfg, "asvd_energy_threshold", energy_threshold), ), ) min_rank = _cfg_get(cfg, "min_rank", _cfg_get(cfg, "rank", min_rank)) max_error = _cfg_get( cfg, "max_error", _cfg_get( cfg, "adaptive_ranks_max_error", _cfg_get(cfg, "aasvd_max_error", max_error), ), ) fallback_to_original = _cfg_get(cfg, "fallback_to_original", fallback_to_original) protect_critical = _cfg_get(cfg, "protect_critical", protect_critical) target_attention_only = _cfg_get( cfg, "target_attention_only", _cfg_get( cfg, "adaptive_ranks_target_attention_only", _cfg_get(cfg, "aasvd_target_attention_only", target_attention_only), ), ) use_jg_svd = _cfg_get( cfg, "use_jg_svd", _cfg_get(cfg, "adaptive_ranks_use_jg_svd", use_jg_svd), ) use_cholesky_jg = _cfg_get( cfg, "use_cholesky_jg", _cfg_get(cfg, "adaptive_ranks_use_cholesky_jg", use_cholesky_jg), ) if cli_use_jg_svd is not None: use_jg_svd = cli_use_jg_svd if cli_use_cholesky_jg is not None: use_cholesky_jg = cli_use_cholesky_jg if "use_jg_svd" in kwargs and kwargs["use_jg_svd"] is not None: use_jg_svd = kwargs["use_jg_svd"] if "use_cholesky_jg" in kwargs and kwargs["use_cholesky_jg"] is not None: use_cholesky_jg = kwargs["use_cholesky_jg"] if "rank" in kwargs and kwargs["rank"] is not None: min_rank = kwargs["rank"] if "target_attention_only" in kwargs and kwargs["target_attention_only"] is not None: target_attention_only = kwargs["target_attention_only"] self.energy_threshold = max(0.01, min(float(energy_threshold), 1.0)) self.min_rank = max(1, int(min_rank)) self.max_error = float(max_error) self.fallback_to_original = bool(fallback_to_original) self.protect_critical = bool(protect_critical) self.target_attention_only = bool(target_attention_only) self.use_jg_svd = bool(use_jg_svd) self.use_cholesky_jg = bool(use_cholesky_jg) self.target_modules = target_modules self.tokenizer = tokenizer or getattr(model, "tokenizer", None) self.calibration_loader = calibration_loader self.calibration_texts = calibration_texts self.target_device = ( torch.device(device) if device is not None else next(model.parameters(), torch.tensor(0.0)).device ) self.layer_D_gating: dict[str, torch.Tensor] = {} self.attn_jacobian: dict[str, float] = {} self.compressed_layers: list[str] = [] self.fallback_layers: list[str] = [] self.layer_errors: dict[str, float] = {} self.layer_ranks: dict[str, int] = {} self.used_covariance: dict[str, bool] = {} self.used_jg_svd: dict[str, bool] = {} def _is_target_module(self, name: str, module: nn.Module) -> bool: if isinstance(module, SVDLinear) or ( CompressedAttentionLayer is not None and isinstance(module, CompressedAttentionLayer) ): return False # Do not target inner submodules of LoRA or wrappers if any( name.endswith(sfx) for sfx in [".base_layer", ".lora_A", ".lora_B", ".lora_dropout"] ): return False target_mod = getattr(module, "base_layer", module) cls_name = module.__class__.__name__.lower() target_cls = target_mod.__class__.__name__.lower() is_linear_like = ( isinstance(module, nn.Linear) or isinstance(target_mod, nn.Linear) or "conv1d" in cls_name or "conv1d" in target_cls or hasattr(module, "nf") or hasattr(target_mod, "nf") or "loralinear" in cls_name or hasattr(module, "base_layer") ) is_mha = isinstance(module, nn.MultiheadAttention) or isinstance( target_mod, nn.MultiheadAttention ) if not (is_linear_like or is_mha): return False if self.protect_critical: name_lower = name.lower() if any( skip in name_lower for skip in [ "lm_head", "classifier", "head", "output_layer", "score", "pooler", "embed", ] ): return False if self.target_attention_only: name_lower = name.lower() ffn_patterns = [ "ffn", "mlp", "lin1", "lin2", "intermediate", "gate_proj", "up_proj", "down_proj", "c_fc", ] if any(p in name_lower for p in ffn_patterns): return False if "output.dense" in name_lower and "attention" not in name_lower: return False attn_patterns = [ "attn", "attention", "q_proj", "k_proj", "v_proj", "o_proj", "out_lin", "q_lin", "k_lin", "v_lin", "c_attn", "c_proj", "query", "key", "value", ] if not any(p in name_lower for p in attn_patterns): return False else: if not should_compress_linear(self.model, name): return False if self.target_modules is not None: return any(t in name for t in self.target_modules) return True def _compute_energy_rank( self, S: torch.Tensor, max_rank: int, min_rank_floor: int | None = None, threshold: float | None = None, ) -> int: r""" Find smallest rank $r$ satisfying cumulative singular energy ratio $\ge$ threshold: $$\frac{\sum_{i=1}^r \sigma_i^2}{\sum_{j=1}^d \sigma_j^2} \ge \tau$$ constrained by minimum rank floor and parameter reduction bounds. """ thresh = threshold if threshold is not None else self.energy_threshold r_floor = min_rank_floor if min_rank_floor is not None else self.min_rank r_floor = max(1, min(int(r_floor), max_rank)) s_squared = S**2 total_energy = s_squared.sum() if total_energy <= 1e-12: return r_floor cum_energy = torch.cumsum(s_squared, dim=0) / total_energy idx = torch.where(cum_energy >= thresh)[0] r = int(idx[0].item()) + 1 if len(idx) > 0 else max_rank return max(1, min(max(r, r_floor), max_rank)) def _collect_activation_covariances(self) -> dict[str, torch.Tensor]: """Collect empirical input activation covariances across target nn.Linear / Conv1D modules.""" if not (self.tokenizer or self.calibration_loader or self.calibration_texts): logger.info("No calibration data available; using unweighted SVD.") return {} self.model.eval() running_cov: dict[str, torch.Tensor] = {} sample_counts: dict[str, int] = {} hooks = [] def make_hook(layer_name: str) -> Any: def hook(module: nn.Module, inp: tuple[torch.Tensor, ...], outp: Any) -> None: if len(inp) > 0 and isinstance(inp[0], torch.Tensor): x = inp[0].detach().float() target_mod = getattr(module, "base_layer", module) cls_name = module.__class__.__name__.lower() target_cls = target_mod.__class__.__name__.lower() is_conv1d = ( "conv1d" in cls_name or "conv1d" in target_cls or hasattr(module, "nf") or hasattr(target_mod, "nf") ) weight = getattr(target_mod, "weight", getattr(module, "weight", None)) if weight is None: return in_f = weight.shape[0] if is_conv1d else weight.shape[1] if x.ndim >= 2: flat_x = x.reshape(-1, in_f) else: flat_x = x.unsqueeze(0) n_samples = flat_x.shape[0] if n_samples > 0: batch_cov = torch.matmul(flat_x.T, flat_x).cpu() if layer_name not in running_cov: running_cov[layer_name] = batch_cov sample_counts[layer_name] = n_samples else: running_cov[layer_name] += batch_cov sample_counts[layer_name] += n_samples return hook for name, module in self.model.named_modules(): if self._is_target_module(name, module) and not isinstance( getattr(module, "base_layer", module), nn.MultiheadAttention ): hooks.append(module.register_forward_hook(make_hook(name))) if self.calibration_loader is not None: self.model.eval() with torch.no_grad(): for batch in self.calibration_loader: try: if isinstance(batch, dict): self.model( **{ k: (v.to(self.target_device) if hasattr(v, "to") else v) for k, v in batch.items() } ) elif isinstance(batch, torch.Tensor): self.model(batch.to(self.target_device)) elif isinstance(batch, (list, tuple)): try: self.model( *[ (b.to(self.target_device) if hasattr(b, "to") else b) for b in batch ] ) except Exception: self.model(batch[0].to(self.target_device)) except Exception as e: logger.debug( "AdaptiveRanks calibration loader forward pass exception: %s", e, ) else: calib_inputs = get_calibration_tensors( self.model, tokenizer=self.tokenizer, calibration_loader=None, calibration_texts=self.calibration_texts, device=self.target_device, ) if calib_inputs is not None and not ( isinstance(calib_inputs, (list, tuple, dict)) and len(calib_inputs) == 0 ): with torch.no_grad(): try: if isinstance(calib_inputs, dict): self.model(**calib_inputs) elif isinstance(calib_inputs, torch.Tensor): self.model(calib_inputs) elif isinstance(calib_inputs, (list, tuple)): self.model(*calib_inputs) except Exception as e: logger.debug( "AdaptiveRanks covariance collection forward pass exception: %s", e, ) for h in hooks: h.remove() covariances: dict[str, torch.Tensor] = {} for name, cov_sum in running_cov.items(): n = float(max(sample_counts.get(name, 1), 1)) covariances[name] = cov_sum / n return covariances def _collect_gating_D(self) -> None: r""" Collect output gating importance vectors D for FFN layers and Softmax Jacobian scalars for Attention Q/K. FFN: D_{FFN} = E[(f'(Y))^2] where f is GELU or SiLU. Attention Q/K: D_{Attn} = E[P(1-P)] where P is softmax attention probabilities. """ if not (self.tokenizer or self.calibration_loader or self.calibration_texts): logger.info("No calibration data available for JG-SVD gating D collection.") return self.model.eval() hooks = [] out_acts: dict[str, list[torch.Tensor]] = {} attn_probs: dict[str, list[torch.Tensor]] = {} def make_out_hook(layer_name: str) -> Any: def hook(module: nn.Module, inp: tuple[torch.Tensor, ...], outp: Any) -> None: if isinstance(outp, torch.Tensor): y = outp.detach().float() target_mod = getattr(module, "base_layer", module) cls_name = module.__class__.__name__.lower() target_cls = target_mod.__class__.__name__.lower() is_conv1d = ( "conv1d" in cls_name or "conv1d" in target_cls or hasattr(module, "nf") or hasattr(target_mod, "nf") ) weight = getattr(target_mod, "weight", getattr(module, "weight", None)) if weight is None: return out_f = weight.shape[1] if is_conv1d else weight.shape[0] y_flat = y.reshape(-1, out_f) if layer_name not in out_acts: out_acts[layer_name] = [y_flat[:512].cpu()] else: curr_n = sum(t.size(0) for t in out_acts[layer_name]) if curr_n < 1024: avail = 1024 - curr_n out_acts[layer_name].append(y_flat[:avail].cpu()) return hook def make_attn_hook(layer_name: str) -> Any: def hook(module: nn.Module, inp: tuple[torch.Tensor, ...], outp: Any) -> None: p = None if ( isinstance(outp, (tuple, list)) and len(outp) > 1 and isinstance(outp[1], torch.Tensor) ): p = outp[1].detach().float() elif isinstance(outp, torch.Tensor) and outp.ndim >= 3: # In some modules attention probabilities or softmax is exposed pass if p is not None: if layer_name not in attn_probs: attn_probs[layer_name] = [p[:4].cpu()] else: curr_n = sum(t.size(0) for t in attn_probs[layer_name]) if curr_n < 8: attn_probs[layer_name].append(p[:2].cpu()) return hook for name, module in self.model.named_modules(): if self._is_target_module(name, module): hooks.append(module.register_forward_hook(make_out_hook(name))) nl = name.lower() if any(k in nl for k in ["attention", "self_attn", "attn"]) and not any( skip in nl for skip in ["q_lin", "k_lin", "v_lin", "out_lin", "dense"] ): hooks.append(module.register_forward_hook(make_attn_hook(name))) if self.calibration_loader is not None: self.model.eval() with torch.no_grad(): for batch in self.calibration_loader: try: if isinstance(batch, dict): self.model( **{ k: (v.to(self.target_device) if hasattr(v, "to") else v) for k, v in batch.items() } ) elif isinstance(batch, torch.Tensor): self.model(batch.to(self.target_device)) elif isinstance(batch, (list, tuple)): try: self.model( *[ (b.to(self.target_device) if hasattr(b, "to") else b) for b in batch ] ) except Exception: self.model(batch[0].to(self.target_device)) except Exception as e: logger.debug("JG-SVD calibration loader forward pass exception: %s", e) else: calib_inputs = get_calibration_tensors( self.model, tokenizer=self.tokenizer, calibration_loader=None, calibration_texts=self.calibration_texts, device=self.target_device, ) if calib_inputs is not None and not ( isinstance(calib_inputs, (list, tuple, dict)) and len(calib_inputs) == 0 ): with torch.no_grad(): try: if isinstance(calib_inputs, dict): self.model(**calib_inputs) elif isinstance(calib_inputs, torch.Tensor): self.model(calib_inputs) elif isinstance(calib_inputs, (list, tuple)): self.model(*calib_inputs) except Exception as e: logger.debug("JG-SVD gating collection forward pass exception: %s", e) for h in hooks: h.remove() # Compute Attention Softmax scalar Jacobian E[P(1-P)] global_attn_scalar = 0.08 # Default baseline for softmax distribution with entropy ~2.5 for name, p_list in attn_probs.items(): if p_list: cat_p = torch.cat(p_list, dim=0) jac = cat_p * (1.0 - cat_p) m_jac = float(jac.mean().item()) if math.isfinite(m_jac) and m_jac > 0.0: self.attn_jacobian[name] = m_jac global_attn_scalar = m_jac for name, y_list in out_acts.items(): if not y_list: continue cat_y = torch.cat(y_list, dim=0) act_type = _detect_activation_type(name, self.model) if act_type == "gelu": d_sq = _gelu_derivative_sq(cat_y) d_vec = d_sq.mean(dim=0).cpu() self.layer_D_gating[name] = torch.clamp(d_vec, min=1e-6) logger.debug("JG-SVD GELU' weighting on %s: mean=%.4f", name, d_vec.mean().item()) elif act_type == "silu": d_sq = _silu_derivative_sq(cat_y) d_vec = d_sq.mean(dim=0).cpu() self.layer_D_gating[name] = torch.clamp(d_vec, min=1e-6) logger.debug("JG-SVD SiLU' weighting on %s: mean=%.4f", name, d_vec.mean().item()) elif _is_attention_qk(name): # Apply Attention Softmax Jacobian scalar weighting matching_attn = [v for k, v in self.attn_jacobian.items() if k in name or name in k] scalar_val = matching_attn[0] if matching_attn else global_attn_scalar out_dim = cat_y.shape[-1] self.layer_D_gating[name] = torch.full( (out_dim,), float(scalar_val), dtype=torch.float32 ) logger.debug( "JG-SVD Attention Softmax Jacobian on %s: scalar=%.4f", name, scalar_val, ) else: # Other layers: empirical output variance fallback d_var = cat_y.var(dim=0, unbiased=False).cpu() self.layer_D_gating[name] = torch.clamp(d_var, min=1e-6) def _factorize_with_JG( self, W: torch.Tensor, cov: torch.Tensor | None, D_gating: torch.Tensor | None, r: int, num_iters: int = 8, ) -> tuple[torch.Tensor, torch.Tensor, float]: r""" Two-Sided Weighted ALS factorizing W ~ U_r @ V_r minimizing: || D^{1/2} (W - U V) C_X^{1/2} ||_F^2 with input activation covariance C_X and output gating derivative D. """ out_f, in_f = W.shape device = W.device dtype = W.dtype # 1. Whitened SVD initialization if cov is not None and cov.shape == (in_f, in_f): cov_dev = cov.to(device=device, dtype=torch.float32) diag_mean = torch.diagonal(cov_dev).mean().item() if not math.isfinite(diag_mean) or diag_mean <= 0.0: diag_mean = 1.0 lambda_reg = max(1e-4 * diag_mean, 1e-6) cov_reg = cov_dev + lambda_reg * torch.eye(in_f, device=device) if self.use_cholesky_jg: try: S_white = torch.linalg.cholesky(cov_reg) S_inv = torch.linalg.inv(S_white) M = torch.matmul(W, S_white) U, S, Vt = torch.linalg.svd(M, full_matrices=False) Vt = Vt @ S_inv except Exception: eigvals, eigvecs = torch.linalg.eigh(cov_reg) eigvals_clamped = torch.clamp(eigvals, min=1e-6) cov_sqrt = eigvecs @ torch.diag(torch.sqrt(eigvals_clamped)) @ eigvecs.T cov_inv_sqrt = ( eigvecs @ torch.diag(1.0 / torch.sqrt(eigvals_clamped)) @ eigvecs.T ) M = torch.matmul(W, cov_sqrt) U, S, Vt = torch.linalg.svd(M, full_matrices=False) Vt = Vt @ cov_inv_sqrt else: eigvals, eigvecs = torch.linalg.eigh(cov_reg) eigvals_clamped = torch.clamp(eigvals, min=1e-6) cov_sqrt = eigvecs @ torch.diag(torch.sqrt(eigvals_clamped)) @ eigvecs.T cov_inv_sqrt = eigvecs @ torch.diag(1.0 / torch.sqrt(eigvals_clamped)) @ eigvecs.T M = torch.matmul(W, cov_sqrt) U, S, Vt = torch.linalg.svd(M, full_matrices=False) Vt = Vt @ cov_inv_sqrt else: U, S, Vt = torch.linalg.svd(W, full_matrices=False) cov_reg = torch.eye(in_f, device=device) sqrt_S = torch.sqrt(torch.clamp(S[:r], min=1e-12)) A = U[:, :r] * sqrt_S.unsqueeze(0) B = sqrt_S.unsqueeze(1) * Vt[:r, :] if D_gating is None or D_gating.shape[0] != out_f: D_vec = torch.ones(out_f, device=device, dtype=torch.float32) else: D_vec = torch.clamp(D_gating.to(device=device, dtype=torch.float32), min=1e-6) D_diag = torch.diag(D_vec) E = cov_reg WE = W @ E DW = D_diag @ W W_norm = torch.norm(W, p="fro").item() best_A = A.clone() best_B = B.clone() best_recon = A @ B best_err = float(torch.norm(W - best_recon, p="fro").item() / max(W_norm, 1e-12)) # 2. Alternating Least Squares (ALS) optimization loop for _ in range(num_iters): # Update A: A = (W @ E @ B.T) @ inv(B @ E @ B.T + eps * I) BEBt = B @ E @ B.T eps_B = max(1e-8, float(1e-5 * BEBt.trace().item() / max(1, r))) try: A_new = ( WE @ B.T @ torch.linalg.inv(BEBt + eps_B * torch.eye(r, device=device, dtype=BEBt.dtype)) ) except Exception: break # Update B: B = inv(A.T @ D @ A + eps * I) @ (A.T @ D @ W) AtDA = A_new.T @ D_diag @ A_new eps_A = max(1e-8, float(1e-5 * AtDA.trace().item() / max(1, r))) try: B_new = ( torch.linalg.inv(AtDA + eps_A * torch.eye(r, device=device, dtype=AtDA.dtype)) @ A_new.T @ DW ) except Exception: break if ( torch.isnan(A_new).any() or torch.isnan(B_new).any() or torch.isinf(A_new).any() or torch.isinf(B_new).any() ): break recon = A_new @ B_new err = float(torch.norm(W - recon, p="fro").item() / max(W_norm, 1e-12)) if err < best_err: best_A = A_new.clone() best_B = B_new.clone() best_err = err else: break A, B = A_new, B_new return ( best_A.to(dtype=dtype, device=device), best_B.to(dtype=dtype, device=device), best_err, ) def compress_linear( self, layer: nn.Module, cov: torch.Tensor | None = None, layer_name: str | None = None, ) -> tuple[AdaptiveSVDLinear | None, float, int, bool, bool]: """ Decompose a single linear, Conv1D, or LoRALinear layer via energy-thresholded SVD with covariance whitening, returning (svd_layer, relative_error, rank, used_cov). """ target_mod = getattr(layer, "base_layer", layer) cls_name = layer.__class__.__name__.lower() target_cls = target_mod.__class__.__name__.lower() is_conv1d = ( "conv1d" in cls_name or "conv1d" in target_cls or hasattr(layer, "nf") or hasattr(target_mod, "nf") ) weight = getattr(target_mod, "weight", getattr(layer, "weight", None)) bias = getattr(target_mod, "bias", getattr(layer, "bias", None)) if weight is None: return None, float("inf"), self.min_rank, False, False raw_w = weight.data if hasattr(target_mod, "nf"): out_f = target_mod.nf in_f = getattr( target_mod, "nx", raw_w.shape[0] if raw_w.shape[1] == out_f else raw_w.shape[1], ) W = raw_w.t().contiguous().float() if raw_w.shape == (in_f, out_f) else raw_w.float() elif hasattr(layer, "nf"): out_f = layer.nf in_f = getattr( layer, "nx", raw_w.shape[0] if raw_w.shape[1] == out_f else raw_w.shape[1], ) W = raw_w.t().contiguous().float() if raw_w.shape == (in_f, out_f) else raw_w.float() elif is_conv1d: in_f, out_f = raw_w.shape[0], raw_w.shape[1] W = raw_w.t().contiguous().float() else: out_f, in_f = raw_w.shape[0], raw_w.shape[1] W = raw_w.float() device = raw_w.device dtype = raw_w.dtype min_dim = min(out_f, in_f) W_norm = torch.norm(W, p="fro").item() # Parameter reduction constraint: r * (in + out) < in * out max_allowed_rank = max(1, min(min_dim - 1, int(0.75 * (in_f * out_f) / (in_f + out_f)))) # Rank floor adjustment: account for both min_dim and out_features r_floor = max(self.min_rank, 1, int(0.15 * min_dim), int(0.10 * out_f)) r_floor = min(r_floor, max_allowed_rank) # Check for LoRA parameters on module lora_r = getattr(layer, "lora_rank", getattr(layer, "r", 0)) lora_a = getattr(layer, "lora_alpha", 1.0) has_lora = (hasattr(layer, "lora_A") and hasattr(layer, "lora_B")) or (lora_r > 0) # Attempt 0: Jacobian-Gated SVD (JG-SVD) if enabled svd_layer: AdaptiveSVDLinear | None = None best_err = 1.0 best_rank = r_floor if self.use_jg_svd: D_gating = self.layer_D_gating.get(layer_name, None) if layer_name else None try: # Rank selection using singular values if cov is not None and cov.shape == (in_f, in_f): cov_dev = cov.to(device=device, dtype=torch.float32) diag_mean = torch.diagonal(cov_dev).mean().item() lambda_reg = max( 1e-4 * (diag_mean if math.isfinite(diag_mean) and diag_mean > 0 else 1.0), 1e-6, ) cov_reg = cov_dev + lambda_reg * torch.eye(in_f, device=device) eigvals, eigvecs = torch.linalg.eigh(cov_reg) cov_sqrt = ( eigvecs @ torch.diag(torch.sqrt(torch.clamp(eigvals, min=1e-6))) @ eigvecs.T ) M = torch.matmul(W, cov_sqrt) _, S_jg, _ = torch.linalg.svd(M, full_matrices=False) else: _, S_jg, _ = torch.linalg.svd(W, full_matrices=False) r_jg = self._compute_energy_rank(S_jg, max_allowed_rank, min_rank_floor=r_floor) A_jg, B_jg, err_jg = self._factorize_with_JG(W, cov, D_gating, r_jg) if math.isfinite(err_jg) and err_jg <= self.max_error: svd_layer = AdaptiveSVDLinear( in_features=in_f, out_features=out_f, rank=r_jg, bias=bias is not None, dtype=dtype, device=device, lora_rank=lora_r if has_lora else 0, lora_alpha=lora_a, ) svd_layer.set_low_rank_factors( A_jg.to(dtype=dtype, device=device), B_jg.to(dtype=dtype, device=device), ) if bias is not None and svd_layer.bias is not None: svd_layer.bias.data.copy_(bias.data) return svd_layer, err_jg, r_jg, cov is not None, True except Exception as e: logger.debug("JG-SVD factorization exception on %s: %s", layer_name, e) # Attempt 1: Covariance whitening SVD if cov is not None and cov.shape == (in_f, in_f): try: cov_dev = cov.to(device=device, dtype=torch.float32) diag_mean = torch.diagonal(cov_dev).mean().item() if not math.isfinite(diag_mean) or diag_mean <= 0.0: diag_mean = 1.0 lambda_reg = max(1e-4 * diag_mean, 1e-6) cov_reg = cov_dev + lambda_reg * torch.eye(in_f, device=device) eigvals, eigvecs = torch.linalg.eigh(cov_reg) eigvals_clamped = torch.clamp(eigvals, min=1e-6) cov_sqrt = eigvecs @ torch.diag(torch.sqrt(eigvals_clamped)) @ eigvecs.T cov_inv_sqrt = eigvecs @ torch.diag(1.0 / torch.sqrt(eigvals_clamped)) @ eigvecs.T M = torch.matmul(W, cov_sqrt) U, S, Vt = torch.linalg.svd(M, full_matrices=False) r = self._compute_energy_rank(S, max_allowed_rank, min_rank_floor=r_floor) sqrt_S = torch.sqrt(torch.clamp(S[:r], min=1e-12)) U_r = U[:, :r] * sqrt_S.unsqueeze(0) V_r = (sqrt_S.unsqueeze(1) * Vt[:r, :]) @ cov_inv_sqrt W_recon = torch.matmul(U_r, V_r) if not (torch.isnan(W_recon).any() or torch.isinf(W_recon).any()): err = float(torch.norm(W - W_recon, p="fro").item() / max(W_norm, 1e-12)) if err <= self.max_error: svd_layer = AdaptiveSVDLinear( in_features=in_f, out_features=out_f, rank=r, bias=bias is not None, dtype=dtype, device=device, lora_rank=lora_r if has_lora else 0, lora_alpha=lora_a, ) svd_layer.set_low_rank_factors( U_r.to(dtype=dtype, device=device), V_r.to(dtype=dtype, device=device), ) if bias is not None and svd_layer.bias is not None: svd_layer.bias.data.copy_(bias.data) return svd_layer, err, r, True, False except Exception as e: logger.debug("Covariance whitening exception on linear layer: %s", e) # Attempt 2: Unweighted Direct SVD with rank escalation try: U, S, Vt = torch.linalg.svd(W, full_matrices=False) candidate_thresholds = [ self.energy_threshold, min(0.98, self.energy_threshold + 0.04), ] for thresh in candidate_thresholds: r = self._compute_energy_rank( S, max_allowed_rank, min_rank_floor=r_floor, threshold=thresh ) sqrt_S = torch.sqrt(torch.clamp(S[:r], min=1e-12)) U_r = U[:, :r] * sqrt_S.unsqueeze(0) V_r = sqrt_S.unsqueeze(1) * Vt[:r, :] W_recon = torch.matmul(U_r, V_r) err = float(torch.norm(W - W_recon, p="fro").item() / max(W_norm, 1e-12)) best_err = err best_rank = r if err <= self.max_error: break svd_layer = AdaptiveSVDLinear( in_features=in_f, out_features=out_f, rank=best_rank, bias=bias is not None, dtype=dtype, device=device, lora_rank=lora_r if has_lora else 0, lora_alpha=lora_a, ) sqrt_S = torch.sqrt(torch.clamp(S[:best_rank], min=1e-12)) U_r = (U[:, :best_rank] * sqrt_S.unsqueeze(0)).to(dtype=dtype, device=device) V_r = (sqrt_S.unsqueeze(1) * Vt[:best_rank, :]).to(dtype=dtype, device=device) svd_layer.set_low_rank_factors(U_r, V_r) if bias is not None and svd_layer.bias is not None: svd_layer.bias.data.copy_(bias.data) return svd_layer, best_err, best_rank, False, False except Exception as e: logger.warning("AdaptiveRanks standard SVD failed: %s", e) return None, 1.0, r_floor, False, False def compress(self) -> tuple[nn.Module, dict[str, Any]]: """Compress target layers with activation covariance whitening and safe validation.""" self.compressed_layers = [] self.fallback_layers = [] self.layer_errors = {} self.layer_ranks = {} self.used_covariance = {} target_layers = [ (name, mod) for name, mod in self.model.named_modules() if self._is_target_module(name, mod) ] if not target_layers: logger.warning("AdaptiveRanksCompressor: No compressible layers found.") return self.model, self.get_compression_stats() covariances = self._collect_activation_covariances() if self.use_jg_svd: self._collect_gating_D() for name, module in target_layers: target_mod = getattr(module, "base_layer", module) if isinstance(target_mod, nn.MultiheadAttention): try: embed_dim = target_mod.embed_dim r_floor = max(self.min_rank, 1, int(0.15 * embed_dim), int(0.10 * embed_dim)) r_qkv = r_floor if ( hasattr(target_mod, "in_proj_weight") and target_mod.in_proj_weight is not None ): W_in = target_mod.in_proj_weight.data.float() # in_proj_weight is [3 * embed_dim, embed_dim]. Split into Q, K, V w_q, w_k, w_v = W_in.chunk(3, dim=0) _, s_q, _ = torch.linalg.svd(w_q, full_matrices=False) _, s_k, _ = torch.linalg.svd(w_k, full_matrices=False) _, s_v, _ = torch.linalg.svd(w_v, full_matrices=False) rq = self._compute_energy_rank(s_q, embed_dim, min_rank_floor=r_floor) rk = self._compute_energy_rank(s_k, embed_dim, min_rank_floor=r_floor) rv = self._compute_energy_rank(s_v, embed_dim, min_rank_floor=r_floor) r_qkv = max(rq, rk, rv) r_out = r_floor if ( hasattr(target_mod, "out_proj") and hasattr(target_mod.out_proj, "weight") and target_mod.out_proj.weight is not None ): W_out = target_mod.out_proj.weight.data.float() _, s_out, _ = torch.linalg.svd(W_out, full_matrices=False) r_out = self._compute_energy_rank(s_out, embed_dim, min_rank_floor=r_floor) if CompressedAttentionLayer is None: raise ImportError("CompressedAttentionLayer is not available") compressed = CompressedAttentionLayer( original_layer=target_mod, rank_qkv=r_qkv, rank_out=r_out, ) replace_module(self.model, name, compressed) self.compressed_layers.append(name) self.layer_ranks[name] = max(r_qkv, r_out) self.layer_errors[name] = 0.05 self.used_covariance[name] = False self.used_jg_svd[name] = False except Exception as e: logger.warning("Failed to compress attention layer %s: %s", name, e) self.fallback_layers.append(name) self.layer_errors[name] = 1.0 else: cov = covariances.get(name, None) svd_layer, err, r, used_cov, used_jg = self.compress_linear( module, cov=cov, layer_name=name ) self.layer_errors[name] = err self.layer_ranks[name] = r self.used_covariance[name] = used_cov self.used_jg_svd[name] = used_jg if svd_layer is None or err > self.max_error: if self.fallback_to_original: logger.warning( "AdaptiveRanks error %.4f on %s exceeded threshold %.4f. Falling back to original layer.", err, name, self.max_error, ) self.fallback_layers.append(name) continue else: logger.warning( "AdaptiveRanks error %.4f on %s exceeds threshold %.4f but fallback_to_original=False. Compressing anyway.", err, name, self.max_error, ) if hasattr(module, "base_layer"): # Check if svd_layer has its own LoRA parameters populated if ( hasattr(module, "lora_A") and module.lora_A is not None and hasattr(svd_layer, "lora_A") and svd_layer.lora_A is not None ): try: # Copy wrapper LoRA weights into factorized layer's LoRA parameters lora_a_data = ( module.lora_A.data if hasattr(module.lora_A, "data") else module.lora_A ) lora_b_data = ( module.lora_B.data if hasattr(module.lora_B, "data") else module.lora_B ) if svd_layer.lora_A.shape == lora_a_data.shape: svd_layer.lora_A.data.copy_( lora_a_data.to( device=svd_layer.lora_A.device, dtype=svd_layer.lora_A.dtype, ) ) if svd_layer.lora_B.shape == lora_b_data.shape: svd_layer.lora_B.data.copy_( lora_b_data.to( device=svd_layer.lora_B.device, dtype=svd_layer.lora_B.dtype, ) ) except Exception as e: logger.debug("Notice copying wrapper LoRA parameters: %s", e) module.base_layer = svd_layer if hasattr(module, "lora_A") and module.lora_A is not None: for p in ( module.lora_A.parameters() if hasattr(module.lora_A, "parameters") else [module.lora_A] ): p.requires_grad = True if hasattr(module, "lora_B") and module.lora_B is not None: for p in ( module.lora_B.parameters() if hasattr(module.lora_B, "parameters") else [module.lora_B] ): p.requires_grad = True else: replace_module(self.model, name, svd_layer) self.compressed_layers.append(name) # Ensure fallback layers are frozen so recovery fine-tuning does not touch them for name, mod in self.model.named_modules(): if name in self.fallback_layers: for p in mod.parameters(): p.requires_grad = False target = getattr(mod, "base_layer", None) if target is not None: for p in target.parameters(): p.requires_grad = False self._compressed_params = self._count_params(self.model) 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( { "energy_threshold": self.energy_threshold, "min_rank": self.min_rank, "max_error": self.max_error, "fallback_to_original": self.fallback_to_original, "compressed_layers": self.compressed_layers, "fallback_layers": self.fallback_layers, "num_compressed": len(self.compressed_layers), "num_fallback": len(self.fallback_layers), "layer_errors": self.layer_errors, "layer_ranks": self.layer_ranks, "used_covariance": self.used_covariance, "used_jg_svd": self.used_jg_svd, "use_jg_svd": self.use_jg_svd, "use_cholesky_jg": self.use_cholesky_jg, "avg_reconstruction_error": avg_err, } ) logger.info( "AdaptiveRanks compression complete: %d compressed, %d fallback. Avg error: %.4f.", len(self.compressed_layers), len(self.fallback_layers), avg_err, ) return self.model, statsBase class for all compressors. Not a nn.Module to avoid parameter storage.
Ancestors
Class variables
var is_custom_attention : bool-
Adaptive Ranks SVD Compressor.
Dynamically determines ranks per layer according to cumulative singular value energy threshold: $$\frac{\sum_{i=1}^r \sigma_i^2}{\sum_{j=1}^d \sigma_j^2} \ge \tau \quad (\tau = 0.92)$$ with minimum rank floor $r_{\min} = \max(16, \lfloor 0.15 \cdot \min(d_{in}, d_{out}) \rfloor, \lfloor 0.10 \cdot d_{out} \rfloor)$, activation covariance whitening, and symmetrically distributed singular values: $$U_r = U_{:, :r} \sqrt{\Sigma_r}, \quad V_r = (\sqrt{\Sigma_r} V^\top_{:r, :}) \cdot \operatorname{Cov}(X)^{-1/2}$$
Production hardening features: - Relative Frobenius reconstruction error validation with configurable max_error (default 0.20). - Safe fallback to original weights by default (fallback_to_original=True). - Robust Conv1D and LoRALinear support via base_layer weight transposition. - SVD and covariance eigendecomposition computed strictly in FP32 with NaN/Inf guards. - Unified critical-layer protection via should_compress_linear. - Adaptive rank retry when reconstruction error exceeds threshold. - Parameter reduction verification: ensures r * (in + out) < in * out. - Comprehensive per-layer statistics and logging.
Methods
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]]: """Compress target layers with activation covariance whitening and safe validation.""" self.compressed_layers = [] self.fallback_layers = [] self.layer_errors = {} self.layer_ranks = {} self.used_covariance = {} target_layers = [ (name, mod) for name, mod in self.model.named_modules() if self._is_target_module(name, mod) ] if not target_layers: logger.warning("AdaptiveRanksCompressor: No compressible layers found.") return self.model, self.get_compression_stats() covariances = self._collect_activation_covariances() if self.use_jg_svd: self._collect_gating_D() for name, module in target_layers: target_mod = getattr(module, "base_layer", module) if isinstance(target_mod, nn.MultiheadAttention): try: embed_dim = target_mod.embed_dim r_floor = max(self.min_rank, 1, int(0.15 * embed_dim), int(0.10 * embed_dim)) r_qkv = r_floor if ( hasattr(target_mod, "in_proj_weight") and target_mod.in_proj_weight is not None ): W_in = target_mod.in_proj_weight.data.float() # in_proj_weight is [3 * embed_dim, embed_dim]. Split into Q, K, V w_q, w_k, w_v = W_in.chunk(3, dim=0) _, s_q, _ = torch.linalg.svd(w_q, full_matrices=False) _, s_k, _ = torch.linalg.svd(w_k, full_matrices=False) _, s_v, _ = torch.linalg.svd(w_v, full_matrices=False) rq = self._compute_energy_rank(s_q, embed_dim, min_rank_floor=r_floor) rk = self._compute_energy_rank(s_k, embed_dim, min_rank_floor=r_floor) rv = self._compute_energy_rank(s_v, embed_dim, min_rank_floor=r_floor) r_qkv = max(rq, rk, rv) r_out = r_floor if ( hasattr(target_mod, "out_proj") and hasattr(target_mod.out_proj, "weight") and target_mod.out_proj.weight is not None ): W_out = target_mod.out_proj.weight.data.float() _, s_out, _ = torch.linalg.svd(W_out, full_matrices=False) r_out = self._compute_energy_rank(s_out, embed_dim, min_rank_floor=r_floor) if CompressedAttentionLayer is None: raise ImportError("CompressedAttentionLayer is not available") compressed = CompressedAttentionLayer( original_layer=target_mod, rank_qkv=r_qkv, rank_out=r_out, ) replace_module(self.model, name, compressed) self.compressed_layers.append(name) self.layer_ranks[name] = max(r_qkv, r_out) self.layer_errors[name] = 0.05 self.used_covariance[name] = False self.used_jg_svd[name] = False except Exception as e: logger.warning("Failed to compress attention layer %s: %s", name, e) self.fallback_layers.append(name) self.layer_errors[name] = 1.0 else: cov = covariances.get(name, None) svd_layer, err, r, used_cov, used_jg = self.compress_linear( module, cov=cov, layer_name=name ) self.layer_errors[name] = err self.layer_ranks[name] = r self.used_covariance[name] = used_cov self.used_jg_svd[name] = used_jg if svd_layer is None or err > self.max_error: if self.fallback_to_original: logger.warning( "AdaptiveRanks error %.4f on %s exceeded threshold %.4f. Falling back to original layer.", err, name, self.max_error, ) self.fallback_layers.append(name) continue else: logger.warning( "AdaptiveRanks error %.4f on %s exceeds threshold %.4f but fallback_to_original=False. Compressing anyway.", err, name, self.max_error, ) if hasattr(module, "base_layer"): # Check if svd_layer has its own LoRA parameters populated if ( hasattr(module, "lora_A") and module.lora_A is not None and hasattr(svd_layer, "lora_A") and svd_layer.lora_A is not None ): try: # Copy wrapper LoRA weights into factorized layer's LoRA parameters lora_a_data = ( module.lora_A.data if hasattr(module.lora_A, "data") else module.lora_A ) lora_b_data = ( module.lora_B.data if hasattr(module.lora_B, "data") else module.lora_B ) if svd_layer.lora_A.shape == lora_a_data.shape: svd_layer.lora_A.data.copy_( lora_a_data.to( device=svd_layer.lora_A.device, dtype=svd_layer.lora_A.dtype, ) ) if svd_layer.lora_B.shape == lora_b_data.shape: svd_layer.lora_B.data.copy_( lora_b_data.to( device=svd_layer.lora_B.device, dtype=svd_layer.lora_B.dtype, ) ) except Exception as e: logger.debug("Notice copying wrapper LoRA parameters: %s", e) module.base_layer = svd_layer if hasattr(module, "lora_A") and module.lora_A is not None: for p in ( module.lora_A.parameters() if hasattr(module.lora_A, "parameters") else [module.lora_A] ): p.requires_grad = True if hasattr(module, "lora_B") and module.lora_B is not None: for p in ( module.lora_B.parameters() if hasattr(module.lora_B, "parameters") else [module.lora_B] ): p.requires_grad = True else: replace_module(self.model, name, svd_layer) self.compressed_layers.append(name) # Ensure fallback layers are frozen so recovery fine-tuning does not touch them for name, mod in self.model.named_modules(): if name in self.fallback_layers: for p in mod.parameters(): p.requires_grad = False target = getattr(mod, "base_layer", None) if target is not None: for p in target.parameters(): p.requires_grad = False self._compressed_params = self._count_params(self.model) 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( { "energy_threshold": self.energy_threshold, "min_rank": self.min_rank, "max_error": self.max_error, "fallback_to_original": self.fallback_to_original, "compressed_layers": self.compressed_layers, "fallback_layers": self.fallback_layers, "num_compressed": len(self.compressed_layers), "num_fallback": len(self.fallback_layers), "layer_errors": self.layer_errors, "layer_ranks": self.layer_ranks, "used_covariance": self.used_covariance, "used_jg_svd": self.used_jg_svd, "use_jg_svd": self.use_jg_svd, "use_cholesky_jg": self.use_cholesky_jg, "avg_reconstruction_error": avg_err, } ) logger.info( "AdaptiveRanks compression complete: %d compressed, %d fallback. Avg error: %.4f.", len(self.compressed_layers), len(self.fallback_layers), avg_err, ) return self.model, statsCompress target layers with activation covariance whitening and safe validation.
def compress_linear(self,
layer: nn.Module,
cov: torch.Tensor | None = None,
layer_name: str | None = None) ‑> tuple[SVDLinear | None, float, int, bool, bool]-
Expand source code
def compress_linear( self, layer: nn.Module, cov: torch.Tensor | None = None, layer_name: str | None = None, ) -> tuple[AdaptiveSVDLinear | None, float, int, bool, bool]: """ Decompose a single linear, Conv1D, or LoRALinear layer via energy-thresholded SVD with covariance whitening, returning (svd_layer, relative_error, rank, used_cov). """ target_mod = getattr(layer, "base_layer", layer) cls_name = layer.__class__.__name__.lower() target_cls = target_mod.__class__.__name__.lower() is_conv1d = ( "conv1d" in cls_name or "conv1d" in target_cls or hasattr(layer, "nf") or hasattr(target_mod, "nf") ) weight = getattr(target_mod, "weight", getattr(layer, "weight", None)) bias = getattr(target_mod, "bias", getattr(layer, "bias", None)) if weight is None: return None, float("inf"), self.min_rank, False, False raw_w = weight.data if hasattr(target_mod, "nf"): out_f = target_mod.nf in_f = getattr( target_mod, "nx", raw_w.shape[0] if raw_w.shape[1] == out_f else raw_w.shape[1], ) W = raw_w.t().contiguous().float() if raw_w.shape == (in_f, out_f) else raw_w.float() elif hasattr(layer, "nf"): out_f = layer.nf in_f = getattr( layer, "nx", raw_w.shape[0] if raw_w.shape[1] == out_f else raw_w.shape[1], ) W = raw_w.t().contiguous().float() if raw_w.shape == (in_f, out_f) else raw_w.float() elif is_conv1d: in_f, out_f = raw_w.shape[0], raw_w.shape[1] W = raw_w.t().contiguous().float() else: out_f, in_f = raw_w.shape[0], raw_w.shape[1] W = raw_w.float() device = raw_w.device dtype = raw_w.dtype min_dim = min(out_f, in_f) W_norm = torch.norm(W, p="fro").item() # Parameter reduction constraint: r * (in + out) < in * out max_allowed_rank = max(1, min(min_dim - 1, int(0.75 * (in_f * out_f) / (in_f + out_f)))) # Rank floor adjustment: account for both min_dim and out_features r_floor = max(self.min_rank, 1, int(0.15 * min_dim), int(0.10 * out_f)) r_floor = min(r_floor, max_allowed_rank) # Check for LoRA parameters on module lora_r = getattr(layer, "lora_rank", getattr(layer, "r", 0)) lora_a = getattr(layer, "lora_alpha", 1.0) has_lora = (hasattr(layer, "lora_A") and hasattr(layer, "lora_B")) or (lora_r > 0) # Attempt 0: Jacobian-Gated SVD (JG-SVD) if enabled svd_layer: AdaptiveSVDLinear | None = None best_err = 1.0 best_rank = r_floor if self.use_jg_svd: D_gating = self.layer_D_gating.get(layer_name, None) if layer_name else None try: # Rank selection using singular values if cov is not None and cov.shape == (in_f, in_f): cov_dev = cov.to(device=device, dtype=torch.float32) diag_mean = torch.diagonal(cov_dev).mean().item() lambda_reg = max( 1e-4 * (diag_mean if math.isfinite(diag_mean) and diag_mean > 0 else 1.0), 1e-6, ) cov_reg = cov_dev + lambda_reg * torch.eye(in_f, device=device) eigvals, eigvecs = torch.linalg.eigh(cov_reg) cov_sqrt = ( eigvecs @ torch.diag(torch.sqrt(torch.clamp(eigvals, min=1e-6))) @ eigvecs.T ) M = torch.matmul(W, cov_sqrt) _, S_jg, _ = torch.linalg.svd(M, full_matrices=False) else: _, S_jg, _ = torch.linalg.svd(W, full_matrices=False) r_jg = self._compute_energy_rank(S_jg, max_allowed_rank, min_rank_floor=r_floor) A_jg, B_jg, err_jg = self._factorize_with_JG(W, cov, D_gating, r_jg) if math.isfinite(err_jg) and err_jg <= self.max_error: svd_layer = AdaptiveSVDLinear( in_features=in_f, out_features=out_f, rank=r_jg, bias=bias is not None, dtype=dtype, device=device, lora_rank=lora_r if has_lora else 0, lora_alpha=lora_a, ) svd_layer.set_low_rank_factors( A_jg.to(dtype=dtype, device=device), B_jg.to(dtype=dtype, device=device), ) if bias is not None and svd_layer.bias is not None: svd_layer.bias.data.copy_(bias.data) return svd_layer, err_jg, r_jg, cov is not None, True except Exception as e: logger.debug("JG-SVD factorization exception on %s: %s", layer_name, e) # Attempt 1: Covariance whitening SVD if cov is not None and cov.shape == (in_f, in_f): try: cov_dev = cov.to(device=device, dtype=torch.float32) diag_mean = torch.diagonal(cov_dev).mean().item() if not math.isfinite(diag_mean) or diag_mean <= 0.0: diag_mean = 1.0 lambda_reg = max(1e-4 * diag_mean, 1e-6) cov_reg = cov_dev + lambda_reg * torch.eye(in_f, device=device) eigvals, eigvecs = torch.linalg.eigh(cov_reg) eigvals_clamped = torch.clamp(eigvals, min=1e-6) cov_sqrt = eigvecs @ torch.diag(torch.sqrt(eigvals_clamped)) @ eigvecs.T cov_inv_sqrt = eigvecs @ torch.diag(1.0 / torch.sqrt(eigvals_clamped)) @ eigvecs.T M = torch.matmul(W, cov_sqrt) U, S, Vt = torch.linalg.svd(M, full_matrices=False) r = self._compute_energy_rank(S, max_allowed_rank, min_rank_floor=r_floor) sqrt_S = torch.sqrt(torch.clamp(S[:r], min=1e-12)) U_r = U[:, :r] * sqrt_S.unsqueeze(0) V_r = (sqrt_S.unsqueeze(1) * Vt[:r, :]) @ cov_inv_sqrt W_recon = torch.matmul(U_r, V_r) if not (torch.isnan(W_recon).any() or torch.isinf(W_recon).any()): err = float(torch.norm(W - W_recon, p="fro").item() / max(W_norm, 1e-12)) if err <= self.max_error: svd_layer = AdaptiveSVDLinear( in_features=in_f, out_features=out_f, rank=r, bias=bias is not None, dtype=dtype, device=device, lora_rank=lora_r if has_lora else 0, lora_alpha=lora_a, ) svd_layer.set_low_rank_factors( U_r.to(dtype=dtype, device=device), V_r.to(dtype=dtype, device=device), ) if bias is not None and svd_layer.bias is not None: svd_layer.bias.data.copy_(bias.data) return svd_layer, err, r, True, False except Exception as e: logger.debug("Covariance whitening exception on linear layer: %s", e) # Attempt 2: Unweighted Direct SVD with rank escalation try: U, S, Vt = torch.linalg.svd(W, full_matrices=False) candidate_thresholds = [ self.energy_threshold, min(0.98, self.energy_threshold + 0.04), ] for thresh in candidate_thresholds: r = self._compute_energy_rank( S, max_allowed_rank, min_rank_floor=r_floor, threshold=thresh ) sqrt_S = torch.sqrt(torch.clamp(S[:r], min=1e-12)) U_r = U[:, :r] * sqrt_S.unsqueeze(0) V_r = sqrt_S.unsqueeze(1) * Vt[:r, :] W_recon = torch.matmul(U_r, V_r) err = float(torch.norm(W - W_recon, p="fro").item() / max(W_norm, 1e-12)) best_err = err best_rank = r if err <= self.max_error: break svd_layer = AdaptiveSVDLinear( in_features=in_f, out_features=out_f, rank=best_rank, bias=bias is not None, dtype=dtype, device=device, lora_rank=lora_r if has_lora else 0, lora_alpha=lora_a, ) sqrt_S = torch.sqrt(torch.clamp(S[:best_rank], min=1e-12)) U_r = (U[:, :best_rank] * sqrt_S.unsqueeze(0)).to(dtype=dtype, device=device) V_r = (sqrt_S.unsqueeze(1) * Vt[:best_rank, :]).to(dtype=dtype, device=device) svd_layer.set_low_rank_factors(U_r, V_r) if bias is not None and svd_layer.bias is not None: svd_layer.bias.data.copy_(bias.data) return svd_layer, best_err, best_rank, False, False except Exception as e: logger.warning("AdaptiveRanks standard SVD failed: %s", e) return None, 1.0, r_floor, False, FalseDecompose a single linear, Conv1D, or LoRALinear layer via energy-thresholded SVD with covariance whitening, returning (svd_layer, relative_error, rank, used_cov).
Inherited members
class AdaptiveSVDLinear (in_features: int,
out_features: int,
rank: int,
bias: bool = True,
use_fused_kernel: bool = False,
dtype: torch.dtype = torch.float32,
device: torch.device | None = None,
lora_rank: int = 0,
lora_alpha: float = 1.0,
lora_r: int | None = None,
freeze_base: bool = False)-
Expand source code
class SVDLinear(nn.Module): """ Low-rank factorized linear layer: y = (x @ V.T) @ U.T + LoRA(x) * scaling + b Parameter count: r·(d_in + d_out) + LoRA params. """ # AASVD-FIX-01 is_custom_attention: bool = False def __init__( self, in_features: int, out_features: int, rank: int, bias: bool = True, use_fused_kernel: bool = False, dtype: torch.dtype = torch.float32, device: torch.device | None = None, lora_rank: int = 0, lora_alpha: float = 1.0, lora_r: int | None = None, freeze_base: bool = False, ) -> None: super().__init__() self.in_features = int(in_features) self.out_features = int(out_features) self.rank = max(1, min(rank, self.in_features, self.out_features)) self.use_fused_kernel = bool(use_fused_kernel) # Accept both lora_rank and legacy lora_r eff_lora = int(lora_r) if (lora_r is not None and lora_r > 0) else max(0, int(lora_rank)) self.lora_rank = eff_lora self.lora_r = eff_lora self.lora_alpha = float(lora_alpha) self.lora_scaling = (self.lora_alpha / self.lora_rank) if self.lora_rank > 0 else 1.0 self.scaling = self.lora_scaling requires_grad = not (self.lora_rank > 0 or freeze_base) fk = {"dtype": dtype, "device": device} self.V = nn.Parameter( torch.empty(self.rank, self.in_features, **fk), requires_grad=requires_grad ) self.U = nn.Parameter( torch.empty(self.out_features, self.rank, **fk), requires_grad=requires_grad ) if bias: self.bias = nn.Parameter( torch.empty(self.out_features, **fk), requires_grad=requires_grad ) else: self.register_parameter("bias", None) if self.lora_rank > 0: self.lora_A = nn.Parameter(torch.empty(self.lora_rank, self.in_features, **fk)) self.lora_B = nn.Parameter(torch.zeros(self.out_features, self.lora_rank, **fk)) with torch.no_grad(): nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5)) else: self.register_parameter("lora_A", None) self.register_parameter("lora_B", None) # AASVD-FIX-07: input mean is not tracked; remove the buffer. self._input_mean: torch.Tensor | None = None self.reset_parameters() # ------------------------------------------------------------------ @property def weight(self) -> torch.Tensor: return self.get_weight() def get_weight(self) -> torch.Tensor: w = torch.matmul(self.U, self.V) if self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None: w = w + torch.matmul(self.lora_B, self.lora_A) * self.lora_scaling return w @property def use_lora(self) -> bool: return bool(self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None) def freeze_base_weights(self) -> None: if self.V.requires_grad: self.V.requires_grad = False if self.U.requires_grad: self.U.requires_grad = False if self.bias is not None and self.bias.requires_grad: self.bias.requires_grad = False if self.lora_A is not None: self.lora_A.requires_grad = True if self.lora_B is not None: self.lora_B.requires_grad = True def reset_parameters(self) -> None: with torch.no_grad(): for param in (self.V, self.U): if param.dtype in (torch.float16, torch.bfloat16): tmp = torch.empty_like(param, dtype=torch.float32, device="cpu") _orthogonal_init(tmp) param.copy_(tmp.to(dtype=param.dtype, device=param.device)) else: _orthogonal_init(param) if self.bias is not None: nn.init.zeros_(self.bias) if self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None: nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5)) nn.init.zeros_(self.lora_B) def forward(self, x: torch.Tensor) -> torch.Tensor: if self._input_mean is not None: x = x - self._input_mean.to(x.device, x.dtype) if self.use_fused_kernel and (self.lora_rank == 0 or self.lora_A is None): try: from neuralops.attentions.fused_svd_kernel import fused_svd_linear return fused_svd_linear(x=x, V=self.V, U=self.U, bias=self.bias, use_rms_norm=False) except Exception as e: logger.debug("SVDLinear fused kernel failed (%s); using F.linear.", e) latent = F.linear(x, self.V) out = F.linear(latent, self.U) if self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None: a = self.lora_A.to(x.dtype) if self.lora_A.dtype != x.dtype else self.lora_A b = self.lora_B.to(x.dtype) if self.lora_B.dtype != x.dtype else self.lora_B lora_latent = F.linear(x, a) out = out + F.linear(lora_latent, b) * self.lora_scaling if self.bias is not None: out = out + self.bias.to(out.dtype) return out def set_low_rank_factors(self, U_factor: torch.Tensor, V_factor: torch.Tensor) -> None: with torch.no_grad(): r = min(self.rank, U_factor.size(1), V_factor.size(0)) self.U.data.zero_() self.V.data.zero_() self.U.data[:, :r].copy_(U_factor[:, :r].to(self.U.device, self.U.dtype)) self.V.data[:r, :].copy_(V_factor[:r, :].to(self.V.device, self.V.dtype))Low-rank factorized linear layer: y = (x @ V.T) @ U.T + LoRA(x) * scaling + b Parameter count: r·(d_in + d_out) + LoRA params.
Initialize internal Module state, shared by both nn.Module and ScriptModule.
Ancestors
- torch.nn.modules.module.Module
Class variables
var is_custom_attention : bool-
The type of the None singleton.
Instance variables
prop use_lora : bool-
Expand source code
@property def use_lora(self) -> bool: return bool(self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None) prop weight : torch.Tensor-
Expand source code
@property def weight(self) -> torch.Tensor: return self.get_weight()
Methods
def forward(self, x: torch.Tensor) ‑> torch.Tensor-
Expand source code
def forward(self, x: torch.Tensor) -> torch.Tensor: if self._input_mean is not None: x = x - self._input_mean.to(x.device, x.dtype) if self.use_fused_kernel and (self.lora_rank == 0 or self.lora_A is None): try: from neuralops.attentions.fused_svd_kernel import fused_svd_linear return fused_svd_linear(x=x, V=self.V, U=self.U, bias=self.bias, use_rms_norm=False) except Exception as e: logger.debug("SVDLinear fused kernel failed (%s); using F.linear.", e) latent = F.linear(x, self.V) out = F.linear(latent, self.U) if self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None: a = self.lora_A.to(x.dtype) if self.lora_A.dtype != x.dtype else self.lora_A b = self.lora_B.to(x.dtype) if self.lora_B.dtype != x.dtype else self.lora_B lora_latent = F.linear(x, a) out = out + F.linear(lora_latent, b) * self.lora_scaling if self.bias is not None: out = out + self.bias.to(out.dtype) return outDefine the computation performed at every call.
Should be overridden by all subclasses.
Note
Although the recipe for forward pass needs to be defined within this function, one should call the :class:
Moduleinstance afterwards instead of this since the former takes care of running the registered hooks while the latter silently ignores them. def freeze_base_weights(self) ‑> None-
Expand source code
def freeze_base_weights(self) -> None: if self.V.requires_grad: self.V.requires_grad = False if self.U.requires_grad: self.U.requires_grad = False if self.bias is not None and self.bias.requires_grad: self.bias.requires_grad = False if self.lora_A is not None: self.lora_A.requires_grad = True if self.lora_B is not None: self.lora_B.requires_grad = True def get_weight(self) ‑> torch.Tensor-
Expand source code
def get_weight(self) -> torch.Tensor: w = torch.matmul(self.U, self.V) if self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None: w = w + torch.matmul(self.lora_B, self.lora_A) * self.lora_scaling return w def reset_parameters(self) ‑> None-
Expand source code
def reset_parameters(self) -> None: with torch.no_grad(): for param in (self.V, self.U): if param.dtype in (torch.float16, torch.bfloat16): tmp = torch.empty_like(param, dtype=torch.float32, device="cpu") _orthogonal_init(tmp) param.copy_(tmp.to(dtype=param.dtype, device=param.device)) else: _orthogonal_init(param) if self.bias is not None: nn.init.zeros_(self.bias) if self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None: nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5)) nn.init.zeros_(self.lora_B) def set_low_rank_factors(self, U_factor: torch.Tensor, V_factor: torch.Tensor) ‑> None-
Expand source code
def set_low_rank_factors(self, U_factor: torch.Tensor, V_factor: torch.Tensor) -> None: with torch.no_grad(): r = min(self.rank, U_factor.size(1), V_factor.size(0)) self.U.data.zero_() self.V.data.zero_() self.U.data[:, :r].copy_(U_factor[:, :r].to(self.U.device, self.U.dtype)) self.V.data[:r, :].copy_(V_factor[:r, :].to(self.V.device, self.V.dtype))
class SVDLinear (in_features: int,
out_features: int,
rank: int,
bias: bool = True,
use_fused_kernel: bool = False,
dtype: torch.dtype = torch.float32,
device: torch.device | None = None,
lora_rank: int = 0,
lora_alpha: float = 1.0,
lora_r: int | None = None,
freeze_base: bool = False)-
Expand source code
class SVDLinear(nn.Module): """ Low-rank factorized linear layer: y = (x @ V.T) @ U.T + LoRA(x) * scaling + b Parameter count: r·(d_in + d_out) + LoRA params. """ # AASVD-FIX-01 is_custom_attention: bool = False def __init__( self, in_features: int, out_features: int, rank: int, bias: bool = True, use_fused_kernel: bool = False, dtype: torch.dtype = torch.float32, device: torch.device | None = None, lora_rank: int = 0, lora_alpha: float = 1.0, lora_r: int | None = None, freeze_base: bool = False, ) -> None: super().__init__() self.in_features = int(in_features) self.out_features = int(out_features) self.rank = max(1, min(rank, self.in_features, self.out_features)) self.use_fused_kernel = bool(use_fused_kernel) # Accept both lora_rank and legacy lora_r eff_lora = int(lora_r) if (lora_r is not None and lora_r > 0) else max(0, int(lora_rank)) self.lora_rank = eff_lora self.lora_r = eff_lora self.lora_alpha = float(lora_alpha) self.lora_scaling = (self.lora_alpha / self.lora_rank) if self.lora_rank > 0 else 1.0 self.scaling = self.lora_scaling requires_grad = not (self.lora_rank > 0 or freeze_base) fk = {"dtype": dtype, "device": device} self.V = nn.Parameter( torch.empty(self.rank, self.in_features, **fk), requires_grad=requires_grad ) self.U = nn.Parameter( torch.empty(self.out_features, self.rank, **fk), requires_grad=requires_grad ) if bias: self.bias = nn.Parameter( torch.empty(self.out_features, **fk), requires_grad=requires_grad ) else: self.register_parameter("bias", None) if self.lora_rank > 0: self.lora_A = nn.Parameter(torch.empty(self.lora_rank, self.in_features, **fk)) self.lora_B = nn.Parameter(torch.zeros(self.out_features, self.lora_rank, **fk)) with torch.no_grad(): nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5)) else: self.register_parameter("lora_A", None) self.register_parameter("lora_B", None) # AASVD-FIX-07: input mean is not tracked; remove the buffer. self._input_mean: torch.Tensor | None = None self.reset_parameters() # ------------------------------------------------------------------ @property def weight(self) -> torch.Tensor: return self.get_weight() def get_weight(self) -> torch.Tensor: w = torch.matmul(self.U, self.V) if self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None: w = w + torch.matmul(self.lora_B, self.lora_A) * self.lora_scaling return w @property def use_lora(self) -> bool: return bool(self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None) def freeze_base_weights(self) -> None: if self.V.requires_grad: self.V.requires_grad = False if self.U.requires_grad: self.U.requires_grad = False if self.bias is not None and self.bias.requires_grad: self.bias.requires_grad = False if self.lora_A is not None: self.lora_A.requires_grad = True if self.lora_B is not None: self.lora_B.requires_grad = True def reset_parameters(self) -> None: with torch.no_grad(): for param in (self.V, self.U): if param.dtype in (torch.float16, torch.bfloat16): tmp = torch.empty_like(param, dtype=torch.float32, device="cpu") _orthogonal_init(tmp) param.copy_(tmp.to(dtype=param.dtype, device=param.device)) else: _orthogonal_init(param) if self.bias is not None: nn.init.zeros_(self.bias) if self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None: nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5)) nn.init.zeros_(self.lora_B) def forward(self, x: torch.Tensor) -> torch.Tensor: if self._input_mean is not None: x = x - self._input_mean.to(x.device, x.dtype) if self.use_fused_kernel and (self.lora_rank == 0 or self.lora_A is None): try: from neuralops.attentions.fused_svd_kernel import fused_svd_linear return fused_svd_linear(x=x, V=self.V, U=self.U, bias=self.bias, use_rms_norm=False) except Exception as e: logger.debug("SVDLinear fused kernel failed (%s); using F.linear.", e) latent = F.linear(x, self.V) out = F.linear(latent, self.U) if self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None: a = self.lora_A.to(x.dtype) if self.lora_A.dtype != x.dtype else self.lora_A b = self.lora_B.to(x.dtype) if self.lora_B.dtype != x.dtype else self.lora_B lora_latent = F.linear(x, a) out = out + F.linear(lora_latent, b) * self.lora_scaling if self.bias is not None: out = out + self.bias.to(out.dtype) return out def set_low_rank_factors(self, U_factor: torch.Tensor, V_factor: torch.Tensor) -> None: with torch.no_grad(): r = min(self.rank, U_factor.size(1), V_factor.size(0)) self.U.data.zero_() self.V.data.zero_() self.U.data[:, :r].copy_(U_factor[:, :r].to(self.U.device, self.U.dtype)) self.V.data[:r, :].copy_(V_factor[:r, :].to(self.V.device, self.V.dtype))Low-rank factorized linear layer: y = (x @ V.T) @ U.T + LoRA(x) * scaling + b Parameter count: r·(d_in + d_out) + LoRA params.
Initialize internal Module state, shared by both nn.Module and ScriptModule.
Ancestors
- torch.nn.modules.module.Module
Class variables
var is_custom_attention : bool-
The type of the None singleton.
Instance variables
prop use_lora : bool-
Expand source code
@property def use_lora(self) -> bool: return bool(self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None) prop weight : torch.Tensor-
Expand source code
@property def weight(self) -> torch.Tensor: return self.get_weight()
Methods
def forward(self, x: torch.Tensor) ‑> torch.Tensor-
Expand source code
def forward(self, x: torch.Tensor) -> torch.Tensor: if self._input_mean is not None: x = x - self._input_mean.to(x.device, x.dtype) if self.use_fused_kernel and (self.lora_rank == 0 or self.lora_A is None): try: from neuralops.attentions.fused_svd_kernel import fused_svd_linear return fused_svd_linear(x=x, V=self.V, U=self.U, bias=self.bias, use_rms_norm=False) except Exception as e: logger.debug("SVDLinear fused kernel failed (%s); using F.linear.", e) latent = F.linear(x, self.V) out = F.linear(latent, self.U) if self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None: a = self.lora_A.to(x.dtype) if self.lora_A.dtype != x.dtype else self.lora_A b = self.lora_B.to(x.dtype) if self.lora_B.dtype != x.dtype else self.lora_B lora_latent = F.linear(x, a) out = out + F.linear(lora_latent, b) * self.lora_scaling if self.bias is not None: out = out + self.bias.to(out.dtype) return outDefine the computation performed at every call.
Should be overridden by all subclasses.
Note
Although the recipe for forward pass needs to be defined within this function, one should call the :class:
Moduleinstance afterwards instead of this since the former takes care of running the registered hooks while the latter silently ignores them. def freeze_base_weights(self) ‑> None-
Expand source code
def freeze_base_weights(self) -> None: if self.V.requires_grad: self.V.requires_grad = False if self.U.requires_grad: self.U.requires_grad = False if self.bias is not None and self.bias.requires_grad: self.bias.requires_grad = False if self.lora_A is not None: self.lora_A.requires_grad = True if self.lora_B is not None: self.lora_B.requires_grad = True def get_weight(self) ‑> torch.Tensor-
Expand source code
def get_weight(self) -> torch.Tensor: w = torch.matmul(self.U, self.V) if self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None: w = w + torch.matmul(self.lora_B, self.lora_A) * self.lora_scaling return w def reset_parameters(self) ‑> None-
Expand source code
def reset_parameters(self) -> None: with torch.no_grad(): for param in (self.V, self.U): if param.dtype in (torch.float16, torch.bfloat16): tmp = torch.empty_like(param, dtype=torch.float32, device="cpu") _orthogonal_init(tmp) param.copy_(tmp.to(dtype=param.dtype, device=param.device)) else: _orthogonal_init(param) if self.bias is not None: nn.init.zeros_(self.bias) if self.lora_rank > 0 and self.lora_A is not None and self.lora_B is not None: nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5)) nn.init.zeros_(self.lora_B) def set_low_rank_factors(self, U_factor: torch.Tensor, V_factor: torch.Tensor) ‑> None-
Expand source code
def set_low_rank_factors(self, U_factor: torch.Tensor, V_factor: torch.Tensor) -> None: with torch.no_grad(): r = min(self.rank, U_factor.size(1), V_factor.size(0)) self.U.data.zero_() self.V.data.zero_() self.U.data[:, :r].copy_(U_factor[:, :r].to(self.U.device, self.U.dtype)) self.V.data[:r, :].copy_(V_factor[:r, :].to(self.V.device, self.V.dtype))