Module neuralops.compression_engine.modegpt_compressor
MoDeGPT: Modular Decomposition of Transformer Blocks — Production-Hardened (v2).
Decomposes pairs of linear projections sharing the same input dimension (e.g., Q & K, Q & V, Gate & Up) into shared low-rank bases:
W1 ≈ U1 @ V_shared U1 ∈ R^{out1 × r}, V_shared ∈ R^{r × in}
W2 ≈ U2 @ V_shared U2 ∈ R^{out2 × r}
The shared factor V_shared is stored ONCE per pair. This yields real parameter and storage reduction for the pair as a whole.
For unpaired layers, both U and V are stored per-layer.
Functions
def modegpt_factory(model: nn.Module, comp_config: Any = None, **kwargs: Any) ‑> Any-
Expand source code
@COMPRESSOR_REGISTRY.register("modegpt_factory") def modegpt_factory(model: nn.Module, comp_config: Any = None, **kwargs: Any) -> Any: rank = kwargs.pop( "rank", _cfg_get(comp_config, "modegpt_rank", _cfg_get(comp_config, "rank", 64)) ) method = kwargs.pop( "method", _cfg_get(comp_config, "modegpt_method", _cfg_get(comp_config, "method", "svd")), ) max_error = kwargs.pop( "max_error", _cfg_get(comp_config, "modegpt_max_error", _cfg_get(comp_config, "max_error", 0.50)), ) fallback = kwargs.pop( "fallback_to_original", _cfg_get( comp_config, "modegpt_fallback_to_original", _cfg_get(comp_config, "fallback_to_original", True), ), ) lora_r = kwargs.pop("lora_r", _cfg_get(comp_config, "lora_r", 0)) lora_alpha = kwargs.pop("lora_alpha", _cfg_get(comp_config, "lora_alpha", 1.0)) target_attention_only = kwargs.pop( "target_attention_only", _cfg_get(comp_config, "target_attention_only", False) ) return_stats = kwargs.pop("return_stats", kwargs.pop("return_tuple", False)) compressor = MoDeGPTCompressor( model, rank=rank, method=method, max_error=max_error, fallback_to_original=fallback, lora_r=lora_r, lora_alpha=lora_alpha, target_attention_only=target_attention_only, comp_config=comp_config, **kwargs, ) compressed_model, stats = compressor.compress() if return_stats: return compressed_model, stats return compressed_model
Classes
class MoDeGPTCompressor (model: nn.Module,
rank: int = 64,
method: str = 'svd',
max_error: float = 0.5,
fallback_to_original: bool = True,
lora_r: int = 0,
lora_alpha: float = 1.0,
skip_attention_qkv: bool = False,
target_attention_only: bool = False,
min_rank: int = 4,
target_modules: list[str] | None = None,
**kwargs: Any)-
Expand source code
@COMPRESSOR_REGISTRY.register("modegpt") @COMPRESSOR_REGISTRY.register("modegpt_compressor") class MoDeGPTCompressor(BaseCompressor): """ MoDeGPT: Modular Decomposition of Transformer Projection Blocks. Shares a low-rank input basis V across paired projections sharing the same input dimension. This yields real storage reduction for the pair: Original: out1·in + out2·in = (out1+out2)·in MoDeGPT : r·in + out1·r + out2·r = r·(in + out1 + out2) Storage reduction when r < in·(out1+out2) / (in + out1 + out2). """ is_custom_attention: bool = False def __init__( self, model: nn.Module, rank: int = 64, method: str = "svd", max_error: float = 0.50, fallback_to_original: bool = True, lora_r: int = 0, lora_alpha: float = 1.0, skip_attention_qkv: bool = False, target_attention_only: bool = False, min_rank: int = 4, target_modules: list[str] | None = None, **kwargs: Any, ) -> None: super().__init__(model, name="modegpt", **kwargs) cfg = kwargs.get("comp_config", kwargs.get("cfg")) if cfg is not None: rank = _cfg_get(cfg, "modegpt_rank", _cfg_get(cfg, "rank", rank)) method = _cfg_get(cfg, "modegpt_method", _cfg_get(cfg, "method", method)) max_error = _cfg_get(cfg, "modegpt_max_error", _cfg_get(cfg, "max_error", max_error)) fallback_to_original = _cfg_get( cfg, "modegpt_fallback_to_original", _cfg_get(cfg, "fallback_to_original", fallback_to_original), ) lora_r = _cfg_get(cfg, "lora_r", lora_r) lora_alpha = _cfg_get(cfg, "lora_alpha", lora_alpha) skip_attention_qkv = _cfg_get(cfg, "skip_attention_qkv", skip_attention_qkv) target_attention_only = _cfg_get(cfg, "target_attention_only", target_attention_only) min_rank = _cfg_get(cfg, "min_rank", min_rank) self.rank = max(1, int(rank)) self.method = str(method).lower() if max_error is None or max_error == 0.50: max_error = adaptive_max_error(self.rank, 768, 768, "distilbert") self.max_error = float(max_error) self.fallback_to_original = bool(fallback_to_original) self.lora_r = max(0, int(lora_r)) self.lora_alpha = float(lora_alpha) self.skip_attention_qkv = bool(skip_attention_qkv) self.target_attention_only = bool(target_attention_only) self.min_rank = max(1, int(min_rank)) # MD-FIX-09: empty list → None if target_modules is not None and len(target_modules) == 0: target_modules = None self.target_modules = target_modules self.compressed_layers: list[str] = [] self.fallback_layers: list[str] = [] self.skipped_layers: list[str] = [] self.layer_errors: dict[str, float] = {} self.layer_ranks: dict[str, int] = {} self.stats: dict[str, Any] = {} self._orig_storage_bytes: int = 0 self._comp_storage_bytes: int = 0 # ------------------------------------------------------------------ def _is_gqa_asymmetric(self, name: str, module: nn.Module) -> bool: # MD-FIX-03: always active cfg = getattr(self.model, "config", None) if cfg is None: return False num_heads = getattr(cfg, "num_attention_heads", None) num_kv_heads = getattr(cfg, "num_key_value_heads", None) if num_heads is None or num_kv_heads is None: return False if num_kv_heads >= num_heads: return False name_lower = name.lower() return any( p in name_lower for p in ("q_proj", "k_proj", "v_proj", "query", "key", "value", "c_attn") ) def _is_target_module(self, name: str, module: nn.Module) -> bool: if isinstance(module, MoDeGPTLowRankLinear): return False if isinstance( module, (nn.LayerNorm, nn.RMSNorm, nn.Embedding, nn.BatchNorm1d, nn.BatchNorm2d), ): return False name_l = name.lower() if any( skip in name_l for skip in ( "classifier", "score", "lm_head", "pooler", "embed", "router", "gate", "logits_processor", ) ): return False cls_name = module.__class__.__name__.lower() is_linear_like = ( isinstance(module, nn.Linear) or "conv1d" in cls_name or hasattr(module, "nf") or hasattr(module, "base_layer") ) if not is_linear_like: return False try: if not should_compress_linear(self.model, name): return False except Exception: pass if self._is_gqa_asymmetric(name, module): return False if self.target_attention_only: if not any(k in name_l for k in _ATTN_NAMES): return False if self.target_modules is None: return True return any(t in name for t in self.target_modules) # ------------------------------------------------------------------ def _get_weight(self, module: nn.Module) -> tuple[torch.Tensor, bool]: target_layer = getattr(module, "base_layer", module) raw_w = target_layer.weight.data cls_name = target_layer.__class__.__name__.lower() is_conv1d = "conv1d" in cls_name or hasattr(target_layer, "nf") W = raw_w.t().contiguous().clone().float() if is_conv1d else raw_w.clone().float() return W, is_conv1d def _select_rank(self, joint_out: int, in_f: int) -> int: """MD-FIX-04: rank cap for real compression.""" max_r = min(joint_out, in_f) # Break-even for paired storage: # r·in + r·(out1+out2) < in·(out1+out2) # r < in·(out1+out2) / (in + out1 + out2) denom = in_f + joint_out cap = max(1, (in_f * joint_out) // denom - 1) if denom > 0 else max_r cap = max(1, min(cap, max_r)) r = max(min(self.min_rank, cap), min(self.rank, cap)) return max(1, min(r, cap)) # ------------------------------------------------------------------ # Joint decomposition: returns (U1, U2, V_shared, r) # ------------------------------------------------------------------ def _decompose_joint_pair( self, W1: torch.Tensor, W2: torch.Tensor, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, int] | None: out1, in_f = W1.shape out2, in2 = W2.shape if in_f != in2: return None W_joint = torch.cat([W1, W2], dim=0) # (out1+out2, in) joint_out = W_joint.shape[0] r = self._select_rank(joint_out, in_f) # SVD of concatenated matrix try: U, S, Vt = torch.linalg.svd(W_joint, full_matrices=False) except Exception as e: logger.debug("MoDeGPT joint SVD failed: %s", e) return None r = min(r, len(S)) if r <= 0: return None # U_r = U[:, :r] · diag(S[:r]); V_r = Vt[:r, :] U_r = U[:, :r] * S[:r].unsqueeze(0) # (out1+out2, r) V_shared = Vt[:r, :].contiguous() # (r, in) # Split U_r into U1, U2 U1 = U_r[:out1, :].contiguous() # (out1, r) U2 = U_r[out1:, :].contiguous() # (out2, r) return U1, U2, V_shared, r def _decompose_single( self, W: torch.Tensor, ) -> tuple[torch.Tensor, torch.Tensor, int] | None: out_f, in_f = W.shape r = self._select_rank(out_f, in_f) try: U, S, Vt = torch.linalg.svd(W, full_matrices=False) except Exception as e: logger.debug("MoDeGPT single SVD failed: %s", e) return None r = min(r, len(S)) if r <= 0: return None U_r = U[:, :r] * S[:r].unsqueeze(0) # (out, r) V_r = Vt[:r, :].contiguous() # (r, in) return U_r, V_r, r # ------------------------------------------------------------------ def _storage_bytes(self, module: nn.Module) -> int: total = 0 for p in module.parameters(): total += p.numel() * p.element_size() for b in module.buffers(): if b is not None: total += b.numel() * b.element_size() return total # ------------------------------------------------------------------ def compress(self, **kwargs: Any) -> tuple[nn.Module, dict[str, Any]]: self.compressed_layers = [] self.fallback_layers = [] self.skipped_layers = [] self.layer_errors = {} self.layer_ranks = {} seen_weights: set[int] = set() processed_names: set[str] = set() self._orig_storage_bytes = 0 self._comp_storage_bytes = 0 # ---------- Step 1: joint pairs ---------- for parent_name, parent_mod in list(self.model.named_modules()): for attr1, attr2 in _PAIR_CANDIDATES: if not (hasattr(parent_mod, attr1) and hasattr(parent_mod, attr2)): continue mod1 = getattr(parent_mod, attr1) mod2 = getattr(parent_mod, attr2) name1 = f"{parent_name}.{attr1}" if parent_name else attr1 name2 = f"{parent_name}.{attr2}" if parent_name else attr2 if name1 in processed_names or name2 in processed_names: continue if not ( self._is_target_module(name1, mod1) and self._is_target_module(name2, mod2) ): continue target_l1 = getattr(mod1, "base_layer", mod1) target_l2 = getattr(mod2, "base_layer", mod2) if not (hasattr(target_l1, "weight") and hasattr(target_l2, "weight")): continue w1_id, w2_id = id(target_l1.weight), id(target_l2.weight) if w1_id in seen_weights or w2_id in seen_weights: self.skipped_layers.extend([name1, name2]) processed_names.update([name1, name2]) continue W1_orig, _ = self._get_weight(mod1) W2_orig, _ = self._get_weight(mod2) if W1_orig.shape[1] != W2_orig.shape[1]: # Shape mismatch — skip this pair continue # Track original storage self._orig_storage_bytes += self._storage_bytes(target_l1) self._orig_storage_bytes += self._storage_bytes(target_l2) result = self._decompose_joint_pair(W1_orig, W2_orig) if result is None: self.fallback_layers.extend([name1, name2]) processed_names.update([name1, name2]) seen_weights.add(w1_id) seen_weights.add(w2_id) self._comp_storage_bytes += self._storage_bytes(target_l1) self._comp_storage_bytes += self._storage_bytes(target_l2) continue U1, U2, V_shared, r = result # Reconstruction error W1_rec = U1 @ V_shared W2_rec = U2 @ V_shared n1 = torch.norm(W1_orig).clamp_min(1e-12).item() n2 = torch.norm(W2_orig).clamp_min(1e-12).item() err1 = (torch.norm(W1_orig - W1_rec) / n1).item() err2 = (torch.norm(W2_orig - W2_rec) / n2).item() self.layer_errors[name1] = float(err1) self.layer_errors[name2] = float(err2) self.layer_ranks[name1] = int(r) self.layer_ranks[name2] = int(r) if ( not math.isfinite(err1) or not math.isfinite(err2) or err1 > self.max_error or err2 > self.max_error ): logger.warning( "MoDeGPT pair error (%.4f, %.4f) > max_error=%.4f on (%s, %s)", err1, err2, self.max_error, name1, name2, ) if self.fallback_to_original: self.fallback_layers.extend([name1, name2]) processed_names.update([name1, name2]) seen_weights.add(w1_id) seen_weights.add(w2_id) self._comp_storage_bytes += self._storage_bytes(target_l1) self._comp_storage_bytes += self._storage_bytes(target_l2) continue # Build new modules sharing V_shared # MD-FIX-13: LoRA rank inherits from wrapper if present lora_r_1 = getattr(mod1, "lora_r", getattr(mod1, "r", self.lora_r)) or self.lora_r lora_r_2 = getattr(mod2, "lora_r", getattr(mod2, "r", self.lora_r)) or self.lora_r bias1 = ( target_l1.bias.data if getattr(target_l1, "bias", None) is not None else None ) bias2 = ( target_l2.bias.data if getattr(target_l2, "bias", None) is not None else None ) in_f = W1_orig.shape[1] out1 = W1_orig.shape[0] out2 = W2_orig.shape[0] new1 = MoDeGPTLowRankLinear( in_features=in_f, out_features=out1, rank=r, U=U1.to(target_l1.weight.dtype), shared_V=V_shared.to(target_l1.weight.dtype), bias=bias1, lora_r=lora_r_1, lora_alpha=self.lora_alpha, dtype=target_l1.weight.dtype, device=target_l1.weight.device, ) new2 = MoDeGPTLowRankLinear( in_features=in_f, out_features=out2, rank=r, U=U2.to(target_l2.weight.dtype), shared_V=V_shared.to(target_l2.weight.dtype), bias=bias2, lora_r=lora_r_2, lora_alpha=self.lora_alpha, dtype=target_l2.weight.dtype, device=target_l2.weight.device, ) # Splice — MD-FIX-08 if hasattr(mod1, "base_layer"): mod1.base_layer = new1 else: replace_module(self.model, name1, new1) if hasattr(mod2, "base_layer"): mod2.base_layer = new2 else: replace_module(self.model, name2, new2) self.compressed_layers.extend([name1, name2]) processed_names.update([name1, name2]) seen_weights.add(w1_id) seen_weights.add(w2_id) # Storage: shared V counted once + U1 + U2 + biases + LoRA # Note: shared_V is NOT a Parameter, so it isn't double-counted # in module.parameters(). We count it manually. shared_v_bytes = V_shared.numel() * V_shared.element_size() self._comp_storage_bytes += shared_v_bytes self._comp_storage_bytes += self._storage_bytes(new1) self._comp_storage_bytes += self._storage_bytes(new2) # ---------- Step 2: single layers ---------- for name, module in list(self.model.named_modules()): if name in processed_names: continue if not self._is_target_module(name, module): continue target_layer = getattr(module, "base_layer", module) if not hasattr(target_layer, "weight") or target_layer.weight is None: continue w_id = id(target_layer.weight) if w_id in seen_weights: self.skipped_layers.append(name) continue seen_weights.add(w_id) self._orig_storage_bytes += self._storage_bytes(target_layer) W_orig, _ = self._get_weight(module) result = self._decompose_single(W_orig) if result is None: self.fallback_layers.append(name) self._comp_storage_bytes += self._storage_bytes(target_layer) continue U_r, V_r, r = result W_rec = U_r @ V_r n = torch.norm(W_orig).clamp_min(1e-12).item() err = (torch.norm(W_orig - W_rec) / n).item() self.layer_errors[name] = float(err) self.layer_ranks[name] = int(r) if not math.isfinite(err) or err > self.max_error: logger.warning( "MoDeGPT single error %.4f > max_error=%.4f on %s", err, self.max_error, name, ) if self.fallback_to_original: self.fallback_layers.append(name) self._comp_storage_bytes += self._storage_bytes(target_layer) continue bias = ( target_layer.bias.data if getattr(target_layer, "bias", None) is not None else None ) lora_r = getattr(module, "lora_r", getattr(module, "r", self.lora_r)) or self.lora_r in_f = W_orig.shape[1] out_f = W_orig.shape[0] new_layer = MoDeGPTLowRankLinear( in_features=in_f, out_features=out_f, rank=r, U=U_r.to(target_layer.weight.dtype), V=V_r.to(target_layer.weight.dtype), bias=bias, lora_r=lora_r, lora_alpha=self.lora_alpha, dtype=target_layer.weight.dtype, device=target_layer.weight.device, ) if hasattr(module, "base_layer"): module.base_layer = new_layer else: replace_module(self.model, name, new_layer) self.compressed_layers.append(name) processed_names.add(name) self._comp_storage_bytes += self._storage_bytes(new_layer) # ---------- Forward sanity check (MD-FIX-12) ---------- forward_check_success = True try: with torch.no_grad(): dev = next(self.model.parameters(), torch.tensor(0.0)).device cfg = getattr(self.model, "config", None) vocab = getattr(cfg, "vocab_size", None) if cfg else None if vocab is None: vocab = 100 dummy = torch.randint(0, min(vocab, 100), (1, 8), device=dev) try: self.model(input_ids=dummy, attention_mask=torch.ones_like(dummy)) except Exception: try: self.model(dummy) except Exception as e2: logger.warning("MoDeGPT post-compression forward check failed: %s", e2) forward_check_success = False except Exception as e: logger.warning("MoDeGPT forward check warning: %s", e) forward_check_success = False try: self._compressed_params = self._count_params(self.model) except Exception: self._compressed_params = sum(p.numel() for p in self.model.parameters()) 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 ) storage_ratio = ( self._orig_storage_bytes / self._comp_storage_bytes if self._comp_storage_bytes > 0 else 1.0 ) effective_ratio = ( self._comp_storage_bytes / self._orig_storage_bytes if self._orig_storage_bytes > 0 else 1.0 ) stats = self.get_compression_stats() stats.update( { "compressed_layers": self.compressed_layers, "fallback_layers": self.fallback_layers, "skipped_layers": self.skipped_layers, "num_compressed": len(self.compressed_layers), "num_fallback": len(self.fallback_layers), "num_skipped": len(self.skipped_layers), "layer_errors": self.layer_errors, "layer_ranks": self.layer_ranks, "avg_reconstruction_error": avg_err, "max_error": self.max_error, "fallback_to_original": self.fallback_to_original, "rank": self.rank, "method": self.method, "lora_r": self.lora_r, "lora_alpha": self.lora_alpha, # MD-FIX-05: honest byte accounting "orig_storage_bytes": self._orig_storage_bytes, "comp_storage_bytes": self._comp_storage_bytes, "storage_compression_ratio": round(storage_ratio, 3), "storage_reduction_pct": round(100.0 * (1.0 - effective_ratio), 2), "effective_parameter_ratio": round(effective_ratio, 4), "forward_check_success": forward_check_success, } ) self.stats = stats logger.info( "Compressor modegpt finished: %d compressed, %d fallback, avg_err=%.4f, max_err_threshold=%.4f", len(self.compressed_layers), len(self.fallback_layers), avg_err, self.max_error, ) return self.model, statsMoDeGPT: Modular Decomposition of Transformer Projection Blocks.
Shares a low-rank input basis V across paired projections sharing the same input dimension. This yields real storage reduction for the pair:
Original: out1·in + out2·in = (out1+out2)·in MoDeGPT : r·in + out1·r + out2·r = r·(in + out1 + out2)Storage reduction when r < in·(out1+out2) / (in + out1 + out2).
Ancestors
Class variables
var is_custom_attention : bool-
The type of the None singleton.
Inherited members
class MoDeGPTLowRankLinear (in_features: int,
out_features: int,
rank: int,
U: torch.Tensor,
V: torch.Tensor | None = None,
shared_V: torch.Tensor | None = None,
bias: torch.Tensor | None = None,
lora_r: int = 0,
lora_alpha: float = 1.0,
dtype: torch.dtype | None = None,
device: torch.device | None = None)-
Expand source code
class MoDeGPTLowRankLinear(nn.Module): """ Low-rank linear layer. y = x @ V^T @ U^T + bias = (x @ V^T) @ U^T + bias where U ∈ R^{out × r}, V ∈ R^{r × in} If `shared_V` is provided, this module uses the external tensor instead of its own `V` parameter. This enables sharing the input basis across paired projections (Q/K, Gate/Up, ...), giving real compression for the pair. Optional LoRA adapters (lora_A, lora_B) are trained in addition to the frozen low-rank base. """ # MD-FIX-01 is_custom_attention: bool = False def __init__( self, in_features: int, out_features: int, rank: int, U: torch.Tensor, # (out, r) V: torch.Tensor | None = None, # (r, in) — may be None if shared shared_V: torch.Tensor | None = None, bias: torch.Tensor | None = None, lora_r: int = 0, lora_alpha: float = 1.0, dtype: torch.dtype | None = None, device: torch.device | None = None, ) -> None: super().__init__() self.in_features = int(in_features) self.out_features = int(out_features) self.nf = self.out_features self.nx = self.in_features self.rank = int(rank) self.lora_r = max(0, int(lora_r)) self.lora_alpha = float(lora_alpha) self.scaling = (self.lora_alpha / self.lora_r) if self.lora_r > 0 else 1.0 target_device = device if device is not None else U.device target_dtype = dtype if dtype is not None else U.dtype self.U = nn.Parameter(U.to(device=target_device, dtype=target_dtype), requires_grad=False) if shared_V is not None: # External tensor — NOT a parameter (owned by another module) self._shared_V = shared_V.to(device=target_device, dtype=target_dtype) self.register_parameter("V", None) self._uses_shared_V = True elif V is not None: self.V = nn.Parameter( V.to(device=target_device, dtype=target_dtype), requires_grad=False ) self._shared_V = None self._uses_shared_V = False else: raise ValueError("MoDeGPTLowRankLinear requires either `V` or `shared_V`.") if bias is not None: self.bias = nn.Parameter( bias.to(device=target_device, dtype=target_dtype), requires_grad=False ) else: self.register_parameter("bias", None) # LoRA — MD-FIX-06: matching dtype if self.lora_r > 0: self.lora_A = nn.Parameter( torch.empty( self.lora_r, self.in_features, dtype=target_dtype, device=target_device, ) ) self.lora_B = nn.Parameter( torch.zeros( self.out_features, self.lora_r, dtype=target_dtype, device=target_device, ) ) with torch.no_grad(): tmp = self.lora_A.data.float() nn.init.kaiming_uniform_(tmp, a=math.sqrt(5)) self.lora_A.data.copy_(tmp.to(dtype=target_dtype)) self.lora_A.requires_grad = True self.lora_B.requires_grad = True else: self.register_parameter("lora_A", None) self.register_parameter("lora_B", None) # ------------------------------------------------------------------ def _V(self) -> torch.Tensor: if self._uses_shared_V: return self._shared_V return self.V # ------------------------------------------------------------------ def forward(self, x: torch.Tensor) -> torch.Tensor: V = self._V() # dtype-safe (MD-FIX-07: no forced .float() for base path) if V.dtype != x.dtype: V = V.to(x.dtype) U = self.U.to(x.dtype) if self.U.dtype != x.dtype else self.U b = ( self.bias.to(x.dtype) if (self.bias is not None and self.bias.dtype != x.dtype) else self.bias ) h = F.linear(x, V) # (..., r) out = F.linear(h, U, b) # (..., out) if self.lora_r > 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 bb = self.lora_B.to(x.dtype) if self.lora_B.dtype != x.dtype else self.lora_B out = out + F.linear(F.linear(x, a), bb) * self.scaling return out @property def weight(self) -> torch.Tensor: """Reconstructed (out, in) weight matrix.""" V = self._V() return (self.U.float() @ V.float()).to(self.U.dtype) def extra_repr(self) -> str: return ( f"in_features={self.in_features}, out_features={self.out_features}, " f"rank={self.rank}, shared_V={self._uses_shared_V}, lora_r={self.lora_r}" )Low-rank linear layer.
y = x @ V^T @ U^T + bias = (x @ V^T) @ U^T + bias where U ∈ R^{out × r}, V ∈ R^{r × in}If
shared_Vis provided, this module uses the external tensor instead of its ownVparameter. This enables sharing the input basis across paired projections (Q/K, Gate/Up, …), giving real compression for the pair.Optional LoRA adapters (lora_A, lora_B) are trained in addition to the frozen low-rank base.
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 weight : torch.Tensor-
Expand source code
@property def weight(self) -> torch.Tensor: """Reconstructed (out, in) weight matrix.""" V = self._V() return (self.U.float() @ V.float()).to(self.U.dtype)Reconstructed (out, in) weight matrix.
Methods
def extra_repr(self) ‑> str-
Expand source code
def extra_repr(self) -> str: return ( f"in_features={self.in_features}, out_features={self.out_features}, " f"rank={self.rank}, shared_V={self._uses_shared_V}, lora_r={self.lora_r}" )Return the extra representation of the module.
To print customized extra information, you should re-implement this method in your own modules. Both single-line and multi-line strings are acceptable.
def forward(self, x: torch.Tensor) ‑> torch.Tensor-
Expand source code
def forward(self, x: torch.Tensor) -> torch.Tensor: V = self._V() # dtype-safe (MD-FIX-07: no forced .float() for base path) if V.dtype != x.dtype: V = V.to(x.dtype) U = self.U.to(x.dtype) if self.U.dtype != x.dtype else self.U b = ( self.bias.to(x.dtype) if (self.bias is not None and self.bias.dtype != x.dtype) else self.bias ) h = F.linear(x, V) # (..., r) out = F.linear(h, U, b) # (..., out) if self.lora_r > 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 bb = self.lora_B.to(x.dtype) if self.lora_B.dtype != x.dtype else self.lora_B out = out + F.linear(F.linear(x, a), bb) * self.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.