Module neuralops.attentions.asvd_attention
ASVD (Activation-Aware Singular Value Decomposition) Attention. Paper: https://arxiv.org/abs/2307.07382
Hardened for production use across diverse transformer architectures: - DistilBERT (Flat MHA) - RoBERTa / BERT (Nested Encoder Self-Attention) - GPT-2 (Causal Decoder with Conv1D) - Qwen / LLaMA (GQA with Rotary Position Embeddings)
Functions
def apply_rotary_pos_emb(q: torch.Tensor,
k: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
position_ids: torch.Tensor | None = None) ‑> tuple[torch.Tensor, torch.Tensor]-
Expand source code
def apply_rotary_pos_emb( q: torch.Tensor, k: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor, position_ids: torch.Tensor | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: """Applies Rotary Position Embedding (RoPE) to query and key tensors.""" if cos.dim() == 2 and position_ids is not None: cos = cos[position_ids].unsqueeze(1) sin = sin[position_ids].unsqueeze(1) elif cos.dim() == 2: cos = cos.unsqueeze(0).unsqueeze(1) sin = sin.unsqueeze(0).unsqueeze(1) elif cos.dim() == 3: cos = cos.unsqueeze(1) sin = sin.unsqueeze(1) while cos.dim() < q.dim(): cos = cos.unsqueeze(0) sin = sin.unsqueeze(0) if cos.shape[-2] != q.shape[-2]: seq_len = q.shape[-2] cos = cos[..., :seq_len, :] sin = sin[..., :seq_len, :] rotary_dim = cos.shape[-1] if rotary_dim <= q.shape[-1]: q_rot, q_pass = q[..., :rotary_dim], q[..., rotary_dim:] k_rot, k_pass = k[..., :rotary_dim], k[..., rotary_dim:] q_rot = (q_rot * cos.to(q_rot.dtype)) + (_rotate_half(q_rot) * sin.to(q_rot.dtype)) k_rot = (k_rot * cos.to(k_rot.dtype)) + (_rotate_half(k_rot) * sin.to(k_rot.dtype)) q_embed = torch.cat([q_rot, q_pass], dim=-1) if q_pass.numel() > 0 else q_rot k_embed = torch.cat([k_rot, k_pass], dim=-1) if k_pass.numel() > 0 else k_rot else: cos_cut, sin_cut = cos[..., : q.shape[-1]], sin[..., : q.shape[-1]] q_embed = (q * cos_cut.to(q.dtype)) + (_rotate_half(q) * sin_cut.to(q.dtype)) k_embed = (k * cos_cut.to(k.dtype)) + (_rotate_half(k) * sin_cut.to(k.dtype)) return q_embed.to(q.dtype), k_embed.to(k.dtype)Applies Rotary Position Embedding (RoPE) to query and key tensors.
def inject_asvd(layer: Any = None,
model: Any = None,
model_config: Any = None,
comp_config: Any = None,
layer_idx: int = 0,
total_layers: int = 12,
old_attn: torch.nn.modules.module.Module | None = None,
**kwargs: Any)-
Expand source code
@ATTENTION_REGISTRY.register("asvd_fn") @ATTENTION_REGISTRY.register("asvd_inject") def inject_asvd( layer: Any = None, model: Any = None, model_config: Any = None, comp_config: Any = None, layer_idx: int = 0, total_layers: int = 12, old_attn: nn.Module | None = None, **kwargs: Any, ): target = layer if layer is not None else model if target is None: target = kwargs.get("model", kwargs.get("layer")) if target is None: return None cfg = model_config if model_config is not None else getattr(target, "config", None) # Whole-model traversal layer_list = None if hasattr(target, "distilbert") and hasattr(target.distilbert, "transformer"): layer_list = target.distilbert.transformer.layer elif hasattr(target, "roberta") and hasattr(target.roberta, "encoder"): layer_list = target.roberta.encoder.layer elif hasattr(target, "transformer") and hasattr(target.transformer, "h"): layer_list = target.transformer.h elif hasattr(target, "h") and isinstance(target.h, (torch.nn.ModuleList, list)): layer_list = target.h elif hasattr(target, "model") and hasattr(target.model, "layers"): layer_list = target.model.layers elif hasattr(target, "encoder") and hasattr(target.encoder, "layer"): layer_list = target.encoder.layer elif ( hasattr(target, "bert") and hasattr(target.bert, "encoder") and hasattr(target.bert.encoder, "layer") ): layer_list = target.bert.encoder.layer elif hasattr(target, "layers") and isinstance(target.layers, (torch.nn.ModuleList, list)): layer_list = target.layers if layer_list is not None: num_injected = 0 tot = len(layer_list) for idx, lyr in enumerate(layer_list): res = inject_asvd( layer=lyr, model_config=cfg, comp_config=comp_config, layer_idx=idx, total_layers=tot, **kwargs, ) if res is not None: num_injected += 1 return target if num_injected > 0 else None layer = target rank = kwargs.get("rank", 64) copy_weights = kwargs.get("copy_weights", True) config = cfg """Universal injection factory for ASVDAttention with pre-flight extraction validation.""" target_model = model or kwargs.get("model") if target_model is not None and any( hasattr(target_model, a) for a in ( "transformer", "model", "roberta", "bert", "distilbert", "gpt2", "qwen2", "layers", ) ): from neuralops.core.attention_utils import inject_attention return inject_attention( target_model, "asvd", rank=rank, copy_weights=copy_weights, **kwargs ) if old_attn is None and layer is not None: for attr in ("attention", "self_attn", "self", "attn"): if hasattr(layer, attr): cand = getattr(layer, attr) if cand is not None: old_attn = cand break if old_attn is None: old_attn = layer target_config = config if target_config is None and old_attn is not None: target_config = getattr(old_attn, "config", None) if target_config is None and layer is not None: target_config = getattr(layer, "config", None) # Pre-flight extraction test: ensure source layer has extractable projections if old_attn is not None: try: new_attn = ASVDAttention(target_config, rank=rank, pretrained_attn=old_attn, **kwargs) if layer is not None: if hasattr(layer, "attention"): if hasattr(layer.attention, "self"): layer.attention.self = new_attn else: layer.attention = new_attn elif hasattr(layer, "self_attn"): layer.self_attn = new_attn elif hasattr(layer, "self"): layer.self = new_attn elif hasattr(layer, "attn"): layer.attn = new_attn return new_attn except RuntimeError as err: logger.warning( "inject_asvd: Pre-flight weight extraction failed on %s: %s. Aborting replacement.", type(old_attn).__name__, err, ) except Exception as err: logger.warning( "inject_asvd: Unexpected error initializing ASVDAttention: %s. Aborting replacement.", err, ) # Fallback to random initialization with a warning new_attn = ASVDAttention(target_config, rank=rank, **kwargs) if layer is not None: if hasattr(layer, "attention"): if hasattr(layer.attention, "self"): layer.attention.self = new_attn else: layer.attention = new_attn elif hasattr(layer, "self_attn"): layer.self_attn = new_attn elif hasattr(layer, "self"): layer.self = new_attn elif hasattr(layer, "attn"): layer.attn = new_attn return new_attn
Classes
class ASVDAttention (config: Any | None = None,
rank: int | None = None,
pretrained_attn: torch.nn.modules.module.Module | None = None,
**kwargs: Any)-
Expand source code
@ATTENTION_REGISTRY.register( "asvd", supports_causal=True, supports_bidirectional=True, supports_gqa=True ) @ATTENTION_REGISTRY.register( "asvd_attention", supports_causal=True, supports_bidirectional=True, supports_gqa=True, ) class ASVDAttention(BaseAttention): r"""Hardened ASVD Attention with Factored Low-Rank Matrix Decomposition, GQA, RoPE, and LoRA. Computational Complexity: - Factorized Projections: $\mathcal{O}(B \cdot N \cdot r \cdot (d + d_{\text{all}}))$ - Attention Scoring: $\mathcal{O}(B \cdot H \cdot N^2 \cdot d_k)$ Tensor Dimensions: - hidden_states: $[B, N, D]$ where $B$ is batch size, $N$ is sequence length, and $D$ is hidden dimension. - Output: Tuple of $([B, N, D], \text{None})$. """ is_custom_attention: bool = True triton_kernel: str = "low_rank" def __init__( self, config: Any | None = None, rank: int | None = None, pretrained_attn: nn.Module | None = None, **kwargs: Any, ) -> None: super().__init__() self.config = config model = config self.hidden_size: int = kwargs.get( "hidden_size", kwargs.get( "d_model", getattr( model, "hidden_size", getattr(getattr(model, "config", None), "hidden_size", 64), ), ), ) self.num_heads: int = kwargs.get( "num_heads", kwargs.get( "num_attention_heads", getattr( model, "num_attention_heads", getattr(getattr(model, "config", None), "num_attention_heads", 4), ), ), ) self.num_kv_heads: int = kwargs.get( "num_kv_heads", kwargs.get( "num_key_value_heads", getattr( model, "num_key_value_heads", getattr( getattr(model, "config", None), "num_key_value_heads", self.num_heads, ), ), ), ) self.head_dim: int = kwargs.get( "head_dim", getattr( model, "head_dim", getattr(getattr(model, "config", None), "head_dim", None), ), ) or (self.hidden_size // max(1, self.num_heads)) self.all_head_size: int = self.num_heads * self.head_dim self.kv_head_size: int = self.num_kv_heads * self.head_dim self.kv_repeat: int = max(1, self.num_heads // max(1, self.num_kv_heads)) # Rank configuration self.rank: int = getattr( config, "asvd_rank", (rank if rank is not None else getattr(config, "rank", kwargs.pop("rank", 32))), ) self.lora_rank: int = getattr(config, "lora_rank", kwargs.get("lora_rank", 32)) self.freeze_base: bool = getattr( config, "freeze_base", kwargs.get("freeze_base", self.lora_rank > 0) ) self.energy_threshold: float = getattr( config, "asvd_energy_threshold", kwargs.get("energy_threshold", 0.95) ) self.error_threshold: float | None = getattr( config, "asvd_error_threshold", kwargs.get("error_threshold", 0.40) ) self.dynamic_rank: bool = getattr( config, "asvd_dynamic_rank", kwargs.get("dynamic_rank", True) ) # Flags & metadata self.supports_gqa: bool = True self.supports_rope: bool = True self.is_nested_encoder: bool = False self.rotary_emb: Any | None = None # Decoder detection model_type = getattr(config, "model_type", "").lower() is_dec = getattr(config, "is_decoder", False) decoder_prefixes = ( "gpt", "qwen", "llama", "mistral", "gemma", "phi", "opt", "bloom", "falcon", "olmo", "mpt", "stablelm", ) if ( any(model_type.startswith(p) for p in decoder_prefixes) or hasattr(config, "architectures") and any( "GPT" in str(a) or "CausalLM" in str(a) for a in (getattr(config, "architectures", []) or []) ) ): is_dec = True self.is_decoder: bool = bool(is_dec) # Projections self.query = ASVDLinear( self.hidden_size, self.all_head_size, self.rank, bias=True, lora_rank=self.lora_rank, freeze_base=self.freeze_base, ) self.key = ASVDLinear( self.hidden_size, self.kv_head_size, self.rank, bias=True, lora_rank=self.lora_rank, freeze_base=self.freeze_base, ) self.value = ASVDLinear( self.hidden_size, self.kv_head_size, self.rank, bias=True, lora_rank=self.lora_rank, freeze_base=self.freeze_base, ) self.out_proj = ASVDLinear( self.all_head_size, self.hidden_size, self.rank, bias=True, lora_rank=self.lora_rank, freeze_base=self.freeze_base, ) dropout_p = getattr( config, "dropout", getattr( config, "hidden_dropout_prob", getattr(config, "attention_probs_dropout_prob", 0.1), ), ) self.dropout = nn.Dropout(dropout_p) self.triton_kwargs = {} if pretrained_attn is not None: self.copy_weights(pretrained_attn) # -------------------------------------------------------------------------- # Compatibility properties for parameter inspection & framework integration # -------------------------------------------------------------------------- @property def q_proj(self) -> ASVDLinear: return self.query @property def k_proj(self) -> ASVDLinear: return self.key @property def v_proj(self) -> ASVDLinear: return self.value @property def o_proj(self) -> ASVDLinear: return self.out_proj @property def q_lin(self) -> ASVDLinear: return self.query @property def k_lin(self) -> ASVDLinear: return self.key @property def v_lin(self) -> ASVDLinear: return self.value @property def out_lin(self) -> ASVDLinear: return self.out_proj @property def dense(self) -> ASVDLinear: return self.out_proj @property def c_proj(self) -> ASVDLinear: return self.out_proj def _decompose_and_set( self, target_linear: ASVDLinear, src_weight: torch.Tensor, src_bias: torch.Tensor | None, proj_name: str = "", ) -> float: """Decomposes src_weight into low-rank SVD factors and verifies relative Frobenius error.""" with torch.no_grad(): W = src_weight.detach().float() # Ensure correct orientation: [out_features, in_features] if W.ndim != 2: raise ValueError( f"Expected 2D weight matrix for projection {proj_name}, got shape {W.shape}" ) if ( W.shape[0] != target_linear.out_features and W.shape[1] == target_linear.out_features ): W = W.t().contiguous() # Perform SVD strictly in FP32 U, S, Vt = torch.linalg.svd(W, full_matrices=False) # Energy-based dynamic rank selection total_energy = torch.sum(S**2) cum_energy = torch.cumsum(S**2, dim=0) / (total_energy + 1e-12) target_energy = max( self.energy_threshold, (1.0 - (self.error_threshold**2) if self.error_threshold is not None else 0.95), ) indices = (cum_energy >= target_energy).nonzero() if len(indices) > 0: r_energy = indices[0].item() + 1 else: r_energy = min(U.size(1), Vt.size(0)) # Cap rank so factorized layer strictly compresses or matches original parameter count # Dense size: in_features * out_features. Factored size: r * (in_features + out_features) + r max_param_r = max( 1, int( (target_linear.in_features * target_linear.out_features) / (target_linear.in_features + target_linear.out_features + 1) ), ) if self.dynamic_rank: r = max(16, min(r_energy, U.size(1), Vt.size(0), max_param_r)) else: r = min(self.rank, U.size(1), Vt.size(0), max_param_r) w_rec = (U[:, :r] * S[:r].unsqueeze(0)) @ Vt[:r, :] diff = torch.norm(W - w_rec, p="fro") denom = torch.norm(W, p="fro") + 1e-9 rel_err = float((diff / denom).item()) # If error exceeds threshold under dynamic rank, expand rank within safe compression bound if self.error_threshold is not None and rel_err > self.error_threshold: if self.dynamic_rank and r < min(U.size(1), Vt.size(0), max_param_r): indices_strict = ( cum_energy >= (1.0 - (self.error_threshold**2) + 1e-5) ).nonzero() if len(indices_strict) > 0: r = min(indices_strict[0].item() + 1, max_param_r) w_rec = (U[:, :r] * S[:r].unsqueeze(0)) @ Vt[:r, :] rel_err = float((torch.norm(W - w_rec) / denom).item()) if rel_err > self.error_threshold: logger.warning( f"Relative Frobenius reconstruction error ({rel_err:.2%}) for projection '{proj_name}' " f"exceeds {self.error_threshold:.0%} threshold. Proceeding with best-effort rank {r}." ) target_linear.set_svd_factors(U[:, :r], Vt[:r, :], S[:r], bias=src_bias) return rel_err def copy_weights(self, src: Any) -> None: """Extracts and factorizes pretrained weights across DistilBERT, RoBERTa, GPT-2, Qwen/LLaMA.""" extracted_weights: dict[str, tuple[torch.Tensor | None, torch.Tensor | None]] = { "q": (None, None), "k": (None, None), "v": (None, None), "o": (None, None), } # 1. Robust Nested Encoder Detection (prior to extraction) # Robustly detects both: # a) Outer RobertaAttention / BertAttention with inner 'self' (query/key/value) and 'output.dense' # b) Inner RobertaSelfAttention / BertSelfAttention # Prevents double output projection signal attenuation cls_name = src.__class__.__name__ has_out_lin = hasattr(src, "out_lin") is_nested_encoder = False if ( hasattr(src, "self") and hasattr(src.self, "query") and hasattr(src, "output") and hasattr(src.output, "dense") ): # Outer RobertaAttention containing inner self-attention and output dense is_nested_encoder = True elif ( cls_name in [ "BertSelfAttention", "RobertaSelfAttention", "XLMRobertaSelfAttention", "ElectraSelfAttention", "DebertaSelfAttention", ] or (cls_name.endswith("SelfAttention") and "distil" not in cls_name.lower()) ) and not has_out_lin: # Inner SelfAttention module is_nested_encoder = True self.is_nested_encoder = bool(is_nested_encoder) # 2. GPT-2: Fused Conv1D c_attn + Conv1D c_proj if hasattr(src, "c_attn") and src.c_attn is not None: c_attn_mod = src.c_attn base_c = getattr(c_attn_mod, "base_layer", c_attn_mod) c_w = getattr(base_c, "weight", getattr(c_attn_mod, "weight", None)) if c_w is not None: c_w = c_w.data.clone() c_b = getattr(base_c, "bias", getattr(c_attn_mod, "bias", None)) c_b = c_b.data.clone() if c_b is not None else None c_cls = getattr(base_c, "__class__", type(base_c)).__name__ if ( ("Conv1D" in c_cls) or hasattr(base_c, "nf") or ("Conv1D" in getattr(c_attn_mod, "__class__", type(c_attn_mod)).__name__) or hasattr(c_attn_mod, "nf") or (c_w.ndim == 2 and c_w.shape[0] < c_w.shape[1]) ): c_w = c_w.t().contiguous() split_size = c_w.size(0) // 3 extracted_weights["q"] = ( c_w[:split_size, :], c_b[:split_size] if c_b is not None else None, ) extracted_weights["k"] = ( c_w[split_size : 2 * split_size, :], c_b[split_size : 2 * split_size] if c_b is not None else None, ) extracted_weights["v"] = ( c_w[2 * split_size :, :], c_b[2 * split_size :] if c_b is not None else None, ) if hasattr(src, "c_proj") and src.c_proj is not None: c_proj_mod = src.c_proj base_o = getattr(c_proj_mod, "base_layer", c_proj_mod) o_w = getattr(base_o, "weight", getattr(c_proj_mod, "weight", None)) if o_w is not None: o_w = o_w.data.clone() o_b = getattr(base_o, "bias", getattr(c_proj_mod, "bias", None)) o_b = o_b.data.clone() if o_b is not None else None p_cls = getattr(base_o, "__class__", type(base_o)).__name__ if ( ("Conv1D" in p_cls) or hasattr(base_o, "nf") or ("Conv1D" in getattr(c_proj_mod, "__class__", type(c_proj_mod)).__name__) or hasattr(c_proj_mod, "nf") or (o_w.ndim == 2 and o_w.shape[0] < o_w.shape[1]) ): o_w = o_w.t().contiguous() extracted_weights["o"] = (o_w, o_b) # 3. RoBERTa / BERT Nested Encoder: self.query, self.key, self.value s = getattr(src, "self", src) name_map = {"q": "query", "k": "key", "v": "value"} for proj, attr in name_map.items(): if ( extracted_weights[proj][0] is None and hasattr(s, attr) and hasattr(getattr(s, attr), "weight") ): mod = getattr(s, attr) b = getattr(mod, "bias", None) extracted_weights[proj] = ( mod.weight.data, b.data if b is not None else None, ) if ( not self.is_nested_encoder and extracted_weights["o"][0] is None and hasattr(src, "output") and hasattr(src.output, "dense") ): mod = src.output.dense b = getattr(mod, "bias", None) extracted_weights["o"] = ( mod.weight.data, b.data if b is not None else None, ) # 4. Qwen / LLaMA: q_proj, k_proj, v_proj, o_proj for proj in ["q", "k", "v", "o"]: if extracted_weights[proj][0] is None: for candidate in [f"{proj}_proj", f"{proj}_lin", proj]: if hasattr(src, candidate) and hasattr(getattr(src, candidate), "weight"): mod = getattr(src, candidate) b = getattr(mod, "bias", None) extracted_weights[proj] = ( mod.weight.data, b.data if b is not None else None, ) break # 5. Fallback to UniversalAdapter for proj in ["q", "k", "v", "o"]: if extracted_weights[proj][0] is None: try: w, b = UniversalAdapter.get_weight_bias(src, proj) if w is not None: extracted_weights[proj] = (w, b) except Exception as e: print(f"copy_weights failed: {e}") logger.warning(f"copy_weights failed: {e}") # 6. Check for missing essential projections and Frobenius norm verification missing = [p for p in ["q", "k", "v"] if extracted_weights[p][0] is None] if not self.is_nested_encoder and extracted_weights["o"][0] is None: missing.append("o") if missing: raise RuntimeError( f"ASVDAttention.copy_weights: could not extract mandatory projections {missing} " f"from {src.__class__.__name__}. Refusing to install randomly-initialised weights." ) for p in ["q", "k", "v"]: w_check = extracted_weights[p][0] if w_check is not None: norm = float(torch.norm(w_check.float()).item()) if norm < 1e-4: logger.warning( f"ASVDAttention extracted weight for '{p}' has near-zero Frobenius norm ({norm:.2e})" ) if extracted_weights["o"][0] is not None: norm_o = float(torch.norm(extracted_weights["o"][0].float()).item()) if norm_o < 1e-4: logger.warning( f"ASVDAttention extracted weight for 'o' has near-zero Frobenius norm ({norm_o:.2e})" ) # 7. SVD Factorization & Reconstruction Verification for proj, target in [ ("q", self.query), ("k", self.key), ("v", self.value), ("o", self.out_proj), ]: if proj == "o" and self.is_nested_encoder: self.out_proj.set_identity() continue w, b = extracted_weights[proj] if w is not None: self._decompose_and_set(target, w, b, proj_name=proj) # 8. Inherit Decoder & Rotary Metadata if hasattr(src, "rotary_emb"): self.rotary_emb = src.rotary_emb if hasattr(src, "is_decoder"): self.is_decoder = bool(src.is_decoder) elif hasattr(src, "c_attn") or cls_name in ( "GPT2Attention", "Qwen2Attention", "Qwen2_5_VLAttention", "QwenAttention", ): self.is_decoder = True def forward( self, hidden_states: torch.Tensor | None = None, attention_mask: torch.Tensor | None = None, head_mask: torch.Tensor | None = None, encoder_hidden_states: torch.Tensor | None = None, encoder_attention_mask: torch.Tensor | None = None, past_key_value: Any | None = None, output_attentions: bool | None = False, output_hidden_states: bool | None = False, return_dict: bool | None = True, position_ids: torch.Tensor | None = None, position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, *args: Any, **kwargs: Any, ) -> tuple[torch.Tensor, torch.Tensor | None]: """Forward pass for factorized ASVD attention.""" if hidden_states is None: hidden_states = kwargs.get("query", kwargs.get("x")) if hidden_states is None and len(args) > 0 and isinstance(args[0], torch.Tensor): hidden_states = args[0] if attention_mask is None: attention_mask = kwargs.get("mask", kwargs.get("attn_mask")) B, N, D = hidden_states.shape H = self.num_heads d = self.head_dim kv_H = self.num_kv_heads if encoder_hidden_states is not None: raise NotImplementedError("Cross-attention is not supported in ASVDAttention.") # Extract rotary kwargs if supplied if position_embeddings is None: position_embeddings = kwargs.get("position_embeddings") if position_ids is None: position_ids = kwargs.get("position_ids") # 1. Project Q, K, V using low-rank factorized linear layers q: torch.Tensor = self.query(hidden_states).view(B, N, H, d).transpose(1, 2).contiguous() k: torch.Tensor = self.key(hidden_states).view(B, N, kv_H, d).transpose(1, 2).contiguous() v: torch.Tensor = self.value(hidden_states).view(B, N, kv_H, d).transpose(1, 2).contiguous() # 2. Apply Rotary Position Embedding (RoPE) before GQA expansion and masking if ( position_embeddings is not None and isinstance(position_embeddings, (tuple, list)) and len(position_embeddings) >= 2 ): cos, sin = position_embeddings[0], position_embeddings[1] q, k = apply_rotary_pos_emb(q, k, cos, sin, position_ids) elif self.rotary_emb is not None: try: # Use query tensor q (full H heads) to infer sequence length, shapes, and device correctly rotary_out = ( self.rotary_emb(q, position_ids) if position_ids is not None else self.rotary_emb(q) ) if isinstance(rotary_out, (tuple, list)) and len(rotary_out) >= 2: cos, sin = rotary_out[0], rotary_out[1] q, k = apply_rotary_pos_emb(q, k, cos, sin, position_ids) except Exception as e: logger.debug("UniversalAdapter fallback failed: %s", e) # 3. GQA Expansion: Expand key/value heads to match query heads if kv_H < H if kv_H < H: k = repeat_kv(k, H // kv_H) v = repeat_kv(v, H // kv_H) # 4. Attention Scoring strictly in FP32 to prevent FP16 overflow scale: float = 1.0 / math.sqrt(d) scores: torch.Tensor = ( torch.matmul(q.float(), k.float().transpose(-2, -1).contiguous()) * scale ) # 5. Causal Masking for Decoder Models if getattr(self, "is_decoder", False): causal_mask = torch.triu( torch.full((N, N), -10000.0, device=scores.device, dtype=scores.dtype), diagonal=1, ) scores = scores + causal_mask.unsqueeze(0).unsqueeze(0) # 6. Padding Attention Mask (Broadcast 2D/3D to 4D additive logits format) if attention_mask is not None: if attention_mask.dim() == 2: if attention_mask.dtype == torch.bool: extended_mask = torch.zeros_like(attention_mask, dtype=scores.dtype) extended_mask = extended_mask.masked_fill(~attention_mask, -10000.0) elif torch.is_floating_point(attention_mask) and (attention_mask < 0).any(): extended_mask = attention_mask.to(dtype=scores.dtype) else: extended_mask = (1.0 - attention_mask.to(dtype=scores.dtype)) * -10000.0 attention_mask = extended_mask.unsqueeze(1).unsqueeze(2) elif attention_mask.dim() == 3: if attention_mask.dtype == torch.bool: extended_mask = torch.zeros_like(attention_mask, dtype=scores.dtype) extended_mask = extended_mask.masked_fill(~attention_mask, -10000.0) elif torch.is_floating_point(attention_mask) and (attention_mask < 0).any(): extended_mask = attention_mask.to(dtype=scores.dtype) else: extended_mask = (1.0 - attention_mask.to(dtype=scores.dtype)) * -10000.0 attention_mask = extended_mask.unsqueeze(1) scores = apply_attention_mask(scores, attention_mask) # 7. Softmax & NaN Protection attn: torch.Tensor = F.softmax(scores, dim=-1).to(hidden_states.dtype) attn = torch.nan_to_num(attn, nan=0.0) attn = self.dropout(attn) # 8. Safe Head Mask Handling if head_mask is not None: if head_mask.dim() == 1: head_mask_tensor = head_mask.view(1, -1, 1, 1) elif head_mask.dim() == 2: head_mask_tensor = head_mask.view(head_mask.size(0), -1, 1, 1) elif head_mask.dim() == 3: head_mask_tensor = head_mask.unsqueeze(1) else: head_mask_tensor = head_mask attn = attn * head_mask_tensor # 9. Context Aggregation context: torch.Tensor = torch.matmul(attn, v) context = context.transpose(1, 2).contiguous().view(B, N, self.all_head_size) # 10. Output Projection (Bypassed for nested encoders to prevent double projection) if getattr(self, "is_nested_encoder", False): out = context else: out = self.out_proj(context) return out, attn if output_attentions else NoneHardened ASVD Attention with Factored Low-Rank Matrix Decomposition, GQA, RoPE, and LoRA.
Computational Complexity: - Factorized Projections: $\mathcal{O}(B \cdot N \cdot r \cdot (d + d_{\text{all}}))$ - Attention Scoring: $\mathcal{O}(B \cdot H \cdot N^2 \cdot d_k)$
Tensor Dimensions: - hidden_states: $[B, N, D]$ where $B$ is batch size, $N$ is sequence length, and $D$ is hidden dimension. - Output: Tuple of $([B, N, D], \text{None})$.
Initialize internal Module state, shared by both nn.Module and ScriptModule.
Ancestors
- BaseAttention
- torch.nn.modules.module.Module
Class variables
var is_custom_attention : bool-
The type of the None singleton.
var triton_kernel : str-
The type of the None singleton.
Instance variables
prop c_proj : ASVDLinear-
Expand source code
@property def c_proj(self) -> ASVDLinear: return self.out_proj prop dense : ASVDLinear-
Expand source code
@property def dense(self) -> ASVDLinear: return self.out_proj prop k_lin : ASVDLinear-
Expand source code
@property def k_lin(self) -> ASVDLinear: return self.key prop k_proj : ASVDLinear-
Expand source code
@property def k_proj(self) -> ASVDLinear: return self.key prop o_proj : ASVDLinear-
Expand source code
@property def o_proj(self) -> ASVDLinear: return self.out_proj prop out_lin : ASVDLinear-
Expand source code
@property def out_lin(self) -> ASVDLinear: return self.out_proj prop q_lin : ASVDLinear-
Expand source code
@property def q_lin(self) -> ASVDLinear: return self.query prop q_proj : ASVDLinear-
Expand source code
@property def q_proj(self) -> ASVDLinear: return self.query prop v_lin : ASVDLinear-
Expand source code
@property def v_lin(self) -> ASVDLinear: return self.value prop v_proj : ASVDLinear-
Expand source code
@property def v_proj(self) -> ASVDLinear: return self.value
Methods
def copy_weights(self, src: Any) ‑> None-
Expand source code
def copy_weights(self, src: Any) -> None: """Extracts and factorizes pretrained weights across DistilBERT, RoBERTa, GPT-2, Qwen/LLaMA.""" extracted_weights: dict[str, tuple[torch.Tensor | None, torch.Tensor | None]] = { "q": (None, None), "k": (None, None), "v": (None, None), "o": (None, None), } # 1. Robust Nested Encoder Detection (prior to extraction) # Robustly detects both: # a) Outer RobertaAttention / BertAttention with inner 'self' (query/key/value) and 'output.dense' # b) Inner RobertaSelfAttention / BertSelfAttention # Prevents double output projection signal attenuation cls_name = src.__class__.__name__ has_out_lin = hasattr(src, "out_lin") is_nested_encoder = False if ( hasattr(src, "self") and hasattr(src.self, "query") and hasattr(src, "output") and hasattr(src.output, "dense") ): # Outer RobertaAttention containing inner self-attention and output dense is_nested_encoder = True elif ( cls_name in [ "BertSelfAttention", "RobertaSelfAttention", "XLMRobertaSelfAttention", "ElectraSelfAttention", "DebertaSelfAttention", ] or (cls_name.endswith("SelfAttention") and "distil" not in cls_name.lower()) ) and not has_out_lin: # Inner SelfAttention module is_nested_encoder = True self.is_nested_encoder = bool(is_nested_encoder) # 2. GPT-2: Fused Conv1D c_attn + Conv1D c_proj if hasattr(src, "c_attn") and src.c_attn is not None: c_attn_mod = src.c_attn base_c = getattr(c_attn_mod, "base_layer", c_attn_mod) c_w = getattr(base_c, "weight", getattr(c_attn_mod, "weight", None)) if c_w is not None: c_w = c_w.data.clone() c_b = getattr(base_c, "bias", getattr(c_attn_mod, "bias", None)) c_b = c_b.data.clone() if c_b is not None else None c_cls = getattr(base_c, "__class__", type(base_c)).__name__ if ( ("Conv1D" in c_cls) or hasattr(base_c, "nf") or ("Conv1D" in getattr(c_attn_mod, "__class__", type(c_attn_mod)).__name__) or hasattr(c_attn_mod, "nf") or (c_w.ndim == 2 and c_w.shape[0] < c_w.shape[1]) ): c_w = c_w.t().contiguous() split_size = c_w.size(0) // 3 extracted_weights["q"] = ( c_w[:split_size, :], c_b[:split_size] if c_b is not None else None, ) extracted_weights["k"] = ( c_w[split_size : 2 * split_size, :], c_b[split_size : 2 * split_size] if c_b is not None else None, ) extracted_weights["v"] = ( c_w[2 * split_size :, :], c_b[2 * split_size :] if c_b is not None else None, ) if hasattr(src, "c_proj") and src.c_proj is not None: c_proj_mod = src.c_proj base_o = getattr(c_proj_mod, "base_layer", c_proj_mod) o_w = getattr(base_o, "weight", getattr(c_proj_mod, "weight", None)) if o_w is not None: o_w = o_w.data.clone() o_b = getattr(base_o, "bias", getattr(c_proj_mod, "bias", None)) o_b = o_b.data.clone() if o_b is not None else None p_cls = getattr(base_o, "__class__", type(base_o)).__name__ if ( ("Conv1D" in p_cls) or hasattr(base_o, "nf") or ("Conv1D" in getattr(c_proj_mod, "__class__", type(c_proj_mod)).__name__) or hasattr(c_proj_mod, "nf") or (o_w.ndim == 2 and o_w.shape[0] < o_w.shape[1]) ): o_w = o_w.t().contiguous() extracted_weights["o"] = (o_w, o_b) # 3. RoBERTa / BERT Nested Encoder: self.query, self.key, self.value s = getattr(src, "self", src) name_map = {"q": "query", "k": "key", "v": "value"} for proj, attr in name_map.items(): if ( extracted_weights[proj][0] is None and hasattr(s, attr) and hasattr(getattr(s, attr), "weight") ): mod = getattr(s, attr) b = getattr(mod, "bias", None) extracted_weights[proj] = ( mod.weight.data, b.data if b is not None else None, ) if ( not self.is_nested_encoder and extracted_weights["o"][0] is None and hasattr(src, "output") and hasattr(src.output, "dense") ): mod = src.output.dense b = getattr(mod, "bias", None) extracted_weights["o"] = ( mod.weight.data, b.data if b is not None else None, ) # 4. Qwen / LLaMA: q_proj, k_proj, v_proj, o_proj for proj in ["q", "k", "v", "o"]: if extracted_weights[proj][0] is None: for candidate in [f"{proj}_proj", f"{proj}_lin", proj]: if hasattr(src, candidate) and hasattr(getattr(src, candidate), "weight"): mod = getattr(src, candidate) b = getattr(mod, "bias", None) extracted_weights[proj] = ( mod.weight.data, b.data if b is not None else None, ) break # 5. Fallback to UniversalAdapter for proj in ["q", "k", "v", "o"]: if extracted_weights[proj][0] is None: try: w, b = UniversalAdapter.get_weight_bias(src, proj) if w is not None: extracted_weights[proj] = (w, b) except Exception as e: print(f"copy_weights failed: {e}") logger.warning(f"copy_weights failed: {e}") # 6. Check for missing essential projections and Frobenius norm verification missing = [p for p in ["q", "k", "v"] if extracted_weights[p][0] is None] if not self.is_nested_encoder and extracted_weights["o"][0] is None: missing.append("o") if missing: raise RuntimeError( f"ASVDAttention.copy_weights: could not extract mandatory projections {missing} " f"from {src.__class__.__name__}. Refusing to install randomly-initialised weights." ) for p in ["q", "k", "v"]: w_check = extracted_weights[p][0] if w_check is not None: norm = float(torch.norm(w_check.float()).item()) if norm < 1e-4: logger.warning( f"ASVDAttention extracted weight for '{p}' has near-zero Frobenius norm ({norm:.2e})" ) if extracted_weights["o"][0] is not None: norm_o = float(torch.norm(extracted_weights["o"][0].float()).item()) if norm_o < 1e-4: logger.warning( f"ASVDAttention extracted weight for 'o' has near-zero Frobenius norm ({norm_o:.2e})" ) # 7. SVD Factorization & Reconstruction Verification for proj, target in [ ("q", self.query), ("k", self.key), ("v", self.value), ("o", self.out_proj), ]: if proj == "o" and self.is_nested_encoder: self.out_proj.set_identity() continue w, b = extracted_weights[proj] if w is not None: self._decompose_and_set(target, w, b, proj_name=proj) # 8. Inherit Decoder & Rotary Metadata if hasattr(src, "rotary_emb"): self.rotary_emb = src.rotary_emb if hasattr(src, "is_decoder"): self.is_decoder = bool(src.is_decoder) elif hasattr(src, "c_attn") or cls_name in ( "GPT2Attention", "Qwen2Attention", "Qwen2_5_VLAttention", "QwenAttention", ): self.is_decoder = TrueExtracts and factorizes pretrained weights across DistilBERT, RoBERTa, GPT-2, Qwen/LLaMA.
def forward(self,
hidden_states: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
head_mask: torch.Tensor | None = None,
encoder_hidden_states: torch.Tensor | None = None,
encoder_attention_mask: torch.Tensor | None = None,
past_key_value: Any | None = None,
output_attentions: bool | None = False,
output_hidden_states: bool | None = False,
return_dict: bool | None = True,
position_ids: torch.Tensor | None = None,
position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None,
*args: Any,
**kwargs: Any) ‑> tuple[torch.Tensor, torch.Tensor | None]-
Expand source code
def forward( self, hidden_states: torch.Tensor | None = None, attention_mask: torch.Tensor | None = None, head_mask: torch.Tensor | None = None, encoder_hidden_states: torch.Tensor | None = None, encoder_attention_mask: torch.Tensor | None = None, past_key_value: Any | None = None, output_attentions: bool | None = False, output_hidden_states: bool | None = False, return_dict: bool | None = True, position_ids: torch.Tensor | None = None, position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, *args: Any, **kwargs: Any, ) -> tuple[torch.Tensor, torch.Tensor | None]: """Forward pass for factorized ASVD attention.""" if hidden_states is None: hidden_states = kwargs.get("query", kwargs.get("x")) if hidden_states is None and len(args) > 0 and isinstance(args[0], torch.Tensor): hidden_states = args[0] if attention_mask is None: attention_mask = kwargs.get("mask", kwargs.get("attn_mask")) B, N, D = hidden_states.shape H = self.num_heads d = self.head_dim kv_H = self.num_kv_heads if encoder_hidden_states is not None: raise NotImplementedError("Cross-attention is not supported in ASVDAttention.") # Extract rotary kwargs if supplied if position_embeddings is None: position_embeddings = kwargs.get("position_embeddings") if position_ids is None: position_ids = kwargs.get("position_ids") # 1. Project Q, K, V using low-rank factorized linear layers q: torch.Tensor = self.query(hidden_states).view(B, N, H, d).transpose(1, 2).contiguous() k: torch.Tensor = self.key(hidden_states).view(B, N, kv_H, d).transpose(1, 2).contiguous() v: torch.Tensor = self.value(hidden_states).view(B, N, kv_H, d).transpose(1, 2).contiguous() # 2. Apply Rotary Position Embedding (RoPE) before GQA expansion and masking if ( position_embeddings is not None and isinstance(position_embeddings, (tuple, list)) and len(position_embeddings) >= 2 ): cos, sin = position_embeddings[0], position_embeddings[1] q, k = apply_rotary_pos_emb(q, k, cos, sin, position_ids) elif self.rotary_emb is not None: try: # Use query tensor q (full H heads) to infer sequence length, shapes, and device correctly rotary_out = ( self.rotary_emb(q, position_ids) if position_ids is not None else self.rotary_emb(q) ) if isinstance(rotary_out, (tuple, list)) and len(rotary_out) >= 2: cos, sin = rotary_out[0], rotary_out[1] q, k = apply_rotary_pos_emb(q, k, cos, sin, position_ids) except Exception as e: logger.debug("UniversalAdapter fallback failed: %s", e) # 3. GQA Expansion: Expand key/value heads to match query heads if kv_H < H if kv_H < H: k = repeat_kv(k, H // kv_H) v = repeat_kv(v, H // kv_H) # 4. Attention Scoring strictly in FP32 to prevent FP16 overflow scale: float = 1.0 / math.sqrt(d) scores: torch.Tensor = ( torch.matmul(q.float(), k.float().transpose(-2, -1).contiguous()) * scale ) # 5. Causal Masking for Decoder Models if getattr(self, "is_decoder", False): causal_mask = torch.triu( torch.full((N, N), -10000.0, device=scores.device, dtype=scores.dtype), diagonal=1, ) scores = scores + causal_mask.unsqueeze(0).unsqueeze(0) # 6. Padding Attention Mask (Broadcast 2D/3D to 4D additive logits format) if attention_mask is not None: if attention_mask.dim() == 2: if attention_mask.dtype == torch.bool: extended_mask = torch.zeros_like(attention_mask, dtype=scores.dtype) extended_mask = extended_mask.masked_fill(~attention_mask, -10000.0) elif torch.is_floating_point(attention_mask) and (attention_mask < 0).any(): extended_mask = attention_mask.to(dtype=scores.dtype) else: extended_mask = (1.0 - attention_mask.to(dtype=scores.dtype)) * -10000.0 attention_mask = extended_mask.unsqueeze(1).unsqueeze(2) elif attention_mask.dim() == 3: if attention_mask.dtype == torch.bool: extended_mask = torch.zeros_like(attention_mask, dtype=scores.dtype) extended_mask = extended_mask.masked_fill(~attention_mask, -10000.0) elif torch.is_floating_point(attention_mask) and (attention_mask < 0).any(): extended_mask = attention_mask.to(dtype=scores.dtype) else: extended_mask = (1.0 - attention_mask.to(dtype=scores.dtype)) * -10000.0 attention_mask = extended_mask.unsqueeze(1) scores = apply_attention_mask(scores, attention_mask) # 7. Softmax & NaN Protection attn: torch.Tensor = F.softmax(scores, dim=-1).to(hidden_states.dtype) attn = torch.nan_to_num(attn, nan=0.0) attn = self.dropout(attn) # 8. Safe Head Mask Handling if head_mask is not None: if head_mask.dim() == 1: head_mask_tensor = head_mask.view(1, -1, 1, 1) elif head_mask.dim() == 2: head_mask_tensor = head_mask.view(head_mask.size(0), -1, 1, 1) elif head_mask.dim() == 3: head_mask_tensor = head_mask.unsqueeze(1) else: head_mask_tensor = head_mask attn = attn * head_mask_tensor # 9. Context Aggregation context: torch.Tensor = torch.matmul(attn, v) context = context.transpose(1, 2).contiguous().view(B, N, self.all_head_size) # 10. Output Projection (Bypassed for nested encoders to prevent double projection) if getattr(self, "is_nested_encoder", False): out = context else: out = self.out_proj(context) return out, attn if output_attentions else NoneForward pass for factorized ASVD attention.
Inherited members
class ASVDLinear (in_features: int,
out_features: int,
rank: int,
bias: bool = True,
lora_rank: int = 0,
freeze_base: bool = False,
dtype: torch.dtype | None = None,
device: torch.device | None = None)-
Expand source code
class ASVDLinear(nn.Module): is_custom_attention: bool = True r"""Low-rank factorized linear layer with explicit singular value scaling and trainable LoRA adapters: $$y = ((x V^\top) \odot \Sigma) U^\top + (x A^\top) B^\top + b$$ """ def __init__( self, in_features: int, out_features: int, rank: int, bias: bool = True, lora_rank: int = 0, freeze_base: bool = False, dtype: torch.dtype | None = None, device: torch.device | None = None, ) -> None: super().__init__() self.in_features = in_features self.out_features = out_features self.rank = rank self.lora_rank = lora_rank self.freeze_base = freeze_base factory_kwargs = {"dtype": dtype, "device": device} self.U = nn.Parameter(torch.empty(out_features, rank, **factory_kwargs)) self.S = nn.Parameter(torch.empty(rank, **factory_kwargs)) self.V = nn.Parameter(torch.empty(rank, in_features, **factory_kwargs)) if bias: self.bias = nn.Parameter(torch.empty(out_features, **factory_kwargs)) else: self.register_parameter("bias", None) # Trainable LoRA adapter parameters if self.lora_rank > 0: self.lora_A = nn.Parameter(torch.empty(lora_rank, in_features, **factory_kwargs)) self.lora_B = nn.Parameter(torch.empty(out_features, lora_rank, **factory_kwargs)) self.lora_scaling = 1.0 else: self.register_parameter("lora_A", None) self.register_parameter("lora_B", None) self.lora_scaling = 1.0 self.reset_parameters() if self.freeze_base: self.freeze_base_weights() def reset_parameters(self) -> None: with torch.no_grad(): if self.U.dtype in (torch.float16, torch.bfloat16): u_fp32 = torch.empty_like(self.U, dtype=torch.float32, device="cpu") nn.init.orthogonal_(u_fp32) self.U.copy_(u_fp32.to(dtype=self.U.dtype, device=self.U.device)) else: nn.init.orthogonal_(self.U) nn.init.ones_(self.S) if self.V.dtype in (torch.float16, torch.bfloat16): v_fp32 = torch.empty_like(self.V, dtype=torch.float32, device="cpu") nn.init.orthogonal_(v_fp32) self.V.copy_(v_fp32.to(dtype=self.V.dtype, device=self.V.device)) else: nn.init.orthogonal_(self.V) if self.bias is not None: nn.init.zeros_(self.bias) if getattr(self, "lora_A", None) is not None and self.lora_A is not None: nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5)) self.lora_A.requires_grad = True if getattr(self, "lora_B", None) is not None and self.lora_B is not None: nn.init.zeros_(self.lora_B) self.lora_B.requires_grad = True def freeze_base_weights(self) -> None: """Freeze U, S, V and bias parameters so only LoRA adapters adapt during recovery fine-tuning.""" self.U.requires_grad = False self.S.requires_grad = False self.V.requires_grad = False if self.bias is not None: self.bias.requires_grad = False if getattr(self, "lora_A", None) is not None and self.lora_A is not None: self.lora_A.requires_grad = True if getattr(self, "lora_B", None) is not None and self.lora_B is not None: self.lora_B.requires_grad = True def unfreeze_base_weights(self) -> None: """Unfreeze base weights for standard end-to-end training.""" self.U.requires_grad = True self.S.requires_grad = True self.V.requires_grad = True if self.bias is not None: self.bias.requires_grad = True def set_svd_factors( self, U: torch.Tensor, V: torch.Tensor, S: torch.Tensor | None = None, bias: torch.Tensor | None = None, ) -> None: with torch.no_grad(): r = min(U.size(1), V.size(0)) if r != self.rank: device, dtype = self.U.device, self.U.dtype req_grad = self.U.requires_grad self.rank = r self.U = nn.Parameter( torch.zeros(self.out_features, r, device=device, dtype=dtype), requires_grad=req_grad, ) self.S = nn.Parameter( torch.ones(r, device=device, dtype=dtype), requires_grad=req_grad ) self.V = nn.Parameter( torch.zeros(r, self.in_features, device=device, dtype=dtype), requires_grad=req_grad, ) self.U.data.zero_() self.V.data.zero_() self.S.data.zero_() out_copy = min(self.out_features, U.size(0)) in_copy = min(self.in_features, V.size(1)) self.U.data[:out_copy, :r].copy_( U[:out_copy, :r].to(device=self.U.device, dtype=self.U.dtype) ) self.V.data[:r, :in_copy].copy_( V[:r, :in_copy].to(device=self.V.device, dtype=self.V.dtype) ) if S is not None: self.S.data[:r].copy_(S[:r].to(device=self.S.device, dtype=self.S.dtype)) else: self.S.data[:r].fill_(1.0) if bias is not None and self.bias is not None: min_b = min(self.out_features, bias.numel()) self.bias.data[:min_b].copy_( bias[:min_b].to(device=self.bias.device, dtype=self.bias.dtype) ) elif self.bias is not None: self.bias.data.zero_() def set_identity(self) -> None: """Initializes factorized linear transformation to act as an identity pass-through.""" with torch.no_grad(): self.U.data.zero_() self.V.data.zero_() self.S.data.fill_(1.0) r = min(self.rank, self.in_features, self.out_features) self.U.data[:r, :r].fill_diagonal_(1.0) self.V.data[:r, :r].fill_diagonal_(1.0) if self.bias is not None: self.bias.data.zero_() self.freeze_base_weights() @property def weight(self) -> torch.Tensor: """Reconstructed effective weight matrix for inspection & compatibility.""" W = torch.matmul(self.U * self.S.unsqueeze(0), self.V) if getattr(self, "lora_A", None) is not None and self.lora_A is not None: W = W + torch.matmul(self.lora_B, self.lora_A) * self.lora_scaling return W triton_kernel: str = "low_rank" def forward(self, x: torch.Tensor) -> torch.Tensor: # x: (*, in_features) # Factorized SVD projection: y = ((x @ V.T) * S) @ U.T + bias V_eff = self.S.view(-1, 1) * self.V out = triton_dispatch(self.triton_kernel, x, self.U, V_eff, bias=self.bias) # 4. Residual LoRA adapter path: (x @ lora_A.T) @ lora_B.T if getattr(self, "lora_A", None) is not None and self.lora_A is not None: lora_out = F.linear(F.linear(x, self.lora_A), self.lora_B) out = out + lora_out * self.lora_scaling return outBase class for all neural network modules.
Your models should also subclass this class.
Modules can also contain other Modules, allowing them to be nested in a tree structure. You can assign the submodules as regular attributes::
import torch.nn as nn import torch.nn.functional as F class Model(nn.Module): def __init__(self) -> None: super().__init__() self.conv1 = nn.Conv2d(1, 20, 5) self.conv2 = nn.Conv2d(20, 20, 5) def forward(self, x): x = F.relu(self.conv1(x)) return F.relu(self.conv2(x))Submodules assigned in this way will be registered, and will also have their parameters converted when you call :meth:
to, etc.Note
As per the example above, an
__init__()call to the parent class must be made before assignment on the child.:ivar training: Boolean represents whether this module is in training or evaluation mode. :vartype training: bool
Initialize internal Module state, shared by both nn.Module and ScriptModule.
Ancestors
- torch.nn.modules.module.Module
Class variables
var is_custom_attention : bool-
Low-rank factorized linear layer with explicit singular value scaling and trainable LoRA adapters:
$$y = ((x V^\top) \odot \Sigma) U^\top + (x A^\top) B^\top + b$$
var triton_kernel : str-
The type of the None singleton.
Instance variables
prop weight : torch.Tensor-
Expand source code
@property def weight(self) -> torch.Tensor: """Reconstructed effective weight matrix for inspection & compatibility.""" W = torch.matmul(self.U * self.S.unsqueeze(0), self.V) if getattr(self, "lora_A", None) is not None and self.lora_A is not None: W = W + torch.matmul(self.lora_B, self.lora_A) * self.lora_scaling return WReconstructed effective weight matrix for inspection & compatibility.
Methods
def forward(self, x: torch.Tensor) ‑> torch.Tensor-
Expand source code
def forward(self, x: torch.Tensor) -> torch.Tensor: # x: (*, in_features) # Factorized SVD projection: y = ((x @ V.T) * S) @ U.T + bias V_eff = self.S.view(-1, 1) * self.V out = triton_dispatch(self.triton_kernel, x, self.U, V_eff, bias=self.bias) # 4. Residual LoRA adapter path: (x @ lora_A.T) @ lora_B.T if getattr(self, "lora_A", None) is not None and self.lora_A is not None: lora_out = F.linear(F.linear(x, self.lora_A), self.lora_B) out = out + lora_out * self.lora_scaling 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: """Freeze U, S, V and bias parameters so only LoRA adapters adapt during recovery fine-tuning.""" self.U.requires_grad = False self.S.requires_grad = False self.V.requires_grad = False if self.bias is not None: self.bias.requires_grad = False if getattr(self, "lora_A", None) is not None and self.lora_A is not None: self.lora_A.requires_grad = True if getattr(self, "lora_B", None) is not None and self.lora_B is not None: self.lora_B.requires_grad = TrueFreeze U, S, V and bias parameters so only LoRA adapters adapt during recovery fine-tuning.
def reset_parameters(self) ‑> None-
Expand source code
def reset_parameters(self) -> None: with torch.no_grad(): if self.U.dtype in (torch.float16, torch.bfloat16): u_fp32 = torch.empty_like(self.U, dtype=torch.float32, device="cpu") nn.init.orthogonal_(u_fp32) self.U.copy_(u_fp32.to(dtype=self.U.dtype, device=self.U.device)) else: nn.init.orthogonal_(self.U) nn.init.ones_(self.S) if self.V.dtype in (torch.float16, torch.bfloat16): v_fp32 = torch.empty_like(self.V, dtype=torch.float32, device="cpu") nn.init.orthogonal_(v_fp32) self.V.copy_(v_fp32.to(dtype=self.V.dtype, device=self.V.device)) else: nn.init.orthogonal_(self.V) if self.bias is not None: nn.init.zeros_(self.bias) if getattr(self, "lora_A", None) is not None and self.lora_A is not None: nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5)) self.lora_A.requires_grad = True if getattr(self, "lora_B", None) is not None and self.lora_B is not None: nn.init.zeros_(self.lora_B) self.lora_B.requires_grad = True def set_identity(self) ‑> None-
Expand source code
def set_identity(self) -> None: """Initializes factorized linear transformation to act as an identity pass-through.""" with torch.no_grad(): self.U.data.zero_() self.V.data.zero_() self.S.data.fill_(1.0) r = min(self.rank, self.in_features, self.out_features) self.U.data[:r, :r].fill_diagonal_(1.0) self.V.data[:r, :r].fill_diagonal_(1.0) if self.bias is not None: self.bias.data.zero_() self.freeze_base_weights()Initializes factorized linear transformation to act as an identity pass-through.
def set_svd_factors(self,
U: torch.Tensor,
V: torch.Tensor,
S: torch.Tensor | None = None,
bias: torch.Tensor | None = None) ‑> None-
Expand source code
def set_svd_factors( self, U: torch.Tensor, V: torch.Tensor, S: torch.Tensor | None = None, bias: torch.Tensor | None = None, ) -> None: with torch.no_grad(): r = min(U.size(1), V.size(0)) if r != self.rank: device, dtype = self.U.device, self.U.dtype req_grad = self.U.requires_grad self.rank = r self.U = nn.Parameter( torch.zeros(self.out_features, r, device=device, dtype=dtype), requires_grad=req_grad, ) self.S = nn.Parameter( torch.ones(r, device=device, dtype=dtype), requires_grad=req_grad ) self.V = nn.Parameter( torch.zeros(r, self.in_features, device=device, dtype=dtype), requires_grad=req_grad, ) self.U.data.zero_() self.V.data.zero_() self.S.data.zero_() out_copy = min(self.out_features, U.size(0)) in_copy = min(self.in_features, V.size(1)) self.U.data[:out_copy, :r].copy_( U[:out_copy, :r].to(device=self.U.device, dtype=self.U.dtype) ) self.V.data[:r, :in_copy].copy_( V[:r, :in_copy].to(device=self.V.device, dtype=self.V.dtype) ) if S is not None: self.S.data[:r].copy_(S[:r].to(device=self.S.device, dtype=self.S.dtype)) else: self.S.data[:r].fill_(1.0) if bias is not None and self.bias is not None: min_b = min(self.out_features, bias.numel()) self.bias.data[:min_b].copy_( bias[:min_b].to(device=self.bias.device, dtype=self.bias.dtype) ) elif self.bias is not None: self.bias.data.zero_() def unfreeze_base_weights(self) ‑> None-
Expand source code
def unfreeze_base_weights(self) -> None: """Unfreeze base weights for standard end-to-end training.""" self.U.requires_grad = True self.S.requires_grad = True self.V.requires_grad = True if self.bias is not None: self.bias.requires_grad = TrueUnfreeze base weights for standard end-to-end training.