Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

lora.py

25_adapters_lora/lora.py  ·  176 lines  ·  view on GitHub

"""
LoRA: Low-Rank Adaptation
Simple implementation
"""
import torch
import torch.nn as nn
import numpy as np

class LoRALinear(nn.Module):
    """
    LoRA Linear Layer
    
    Mathematical Formulation:
    W' = W + ΔW = W + BA
    
    Where:
    - W: Original weight (frozen) (out_features × in_features)
    - B: Low-rank matrix (out_features × rank)
    - A: Low-rank matrix (rank × in_features)
    - rank << min(out_features, in_features)
    
    Forward: y = xW^T + xA^TB^T * (alpha/rank)
    """
    
    def __init__(self, in_features: int, out_features: int, 
                 rank: int = 8, alpha: int = 8):
        super().__init__()
        self.in_features = in_features
        self.out_features = out_features
        self.rank = rank
        self.alpha = alpha
        
        # Original weight (frozen - not trained)
        self.weight = nn.Parameter(torch.randn(out_features, in_features))
        self.weight.requires_grad = False  # Freeze original weights
        
        # LoRA matrices (trainable)
        # A: initialized with small random values
        self.lora_A = nn.Parameter(torch.randn(rank, in_features) * 0.02)
        # B: initialized to zero (so initial ΔW = 0)
        self.lora_B = nn.Parameter(torch.zeros(out_features, rank))
        
        # Scaling factor: alpha/rank
        self.scale = alpha / rank
    
    def forward(self, x):
        """
        Forward pass
        
        Args:
            x: Input (batch_size, ..., in_features)
        Returns:
            Output (batch_size, ..., out_features)
        """
        # Original: x @ W^T
        original_out = x @ self.weight.T
        
        # LoRA: x @ A^T @ B^T * scale
        # Step 1: x @ A^T -> (..., rank)
        lora_intermediate = x @ self.lora_A.T
        # Step 2: @ B^T -> (..., out_features)
        lora_out = lora_intermediate @ self.lora_B.T
        # Step 3: Scale
        lora_out = lora_out * self.scale
        
        # Combined output
        return original_out + lora_out
    
    def get_trainable_params(self):
        """Get number of trainable parameters"""
        lora_params = self.lora_A.numel() + self.lora_B.numel()
        return lora_params
    
    def get_total_params(self):
        """Get total parameters (frozen + trainable)"""
        return self.weight.numel() + self.get_trainable_params()


class Adapter(nn.Module):
    """
    Adapter Layer
    
    Architecture:
    Input → Down Projection → Activation → Up Projection → Output
    With residual connection
    """
    
    def __init__(self, d_model: int, adapter_size: int = 64):
        super().__init__()
        self.d_model = d_model
        self.adapter_size = adapter_size
        
        # Down projection: d_model → adapter_size
        self.down_proj = nn.Linear(d_model, adapter_size)
        # Activation
        self.activation = nn.ReLU()
        # Up projection: adapter_size → d_model
        self.up_proj = nn.Linear(adapter_size, d_model)
    
    def forward(self, x):
        """
        Forward pass with residual connection
        
        Args:
            x: Input (batch_size, seq_len, d_model)
        Returns:
            Output (batch_size, seq_len, d_model)
        """
        # Adapter path
        adapter_out = self.down_proj(x)  # (..., adapter_size)
        adapter_out = self.activation(adapter_out)
        adapter_out = self.up_proj(adapter_out)  # (..., d_model)
        
        # Residual connection
        return x + adapter_out


# Usage Example
if __name__ == "__main__":
    print("LoRA and Adapters")
    print("=" * 60)
    
    # Example: LoRA for attention layer
    in_features = 768
    out_features = 768
    rank = 8
    alpha = 8
    
    lora_layer = LoRALinear(in_features, out_features, rank, alpha)
    
    print("LoRA Layer:")
    print(f"  Input features: {in_features}")
    print(f"  Output features: {out_features}")
    print(f"  Rank: {rank}")
    print(f"  Alpha: {alpha}")
    print(f"  Scale: {lora_layer.scale}")
    print()
    
    # Parameter count
    total_params = lora_layer.get_total_params()
    trainable_params = lora_layer.get_trainable_params()
    frozen_params = total_params - trainable_params
    
    print("Parameter Count:")
    print(f"  Total: {total_params:,}")
    print(f"  Frozen (W): {frozen_params:,}")
    print(f"  Trainable (A + B): {trainable_params:,}")
    print(f"  Reduction: {trainable_params/total_params*100:.2f}% of total")
    print()
    
    # Forward pass
    batch_size = 2
    seq_len = 10
    x = torch.randn(batch_size, seq_len, in_features)
    output = lora_layer(x)
    print(f"Input shape: {x.shape}")
    print(f"Output shape: {output.shape}")
    print()
    
    # Adapter example
    d_model = 768
    adapter_size = 64
    adapter = Adapter(d_model, adapter_size)
    
    print("Adapter Layer:")
    print(f"  d_model: {d_model}")
    print(f"  adapter_size: {adapter_size}")
    print(f"  Parameters: {sum(p.numel() for p in adapter.parameters()):,}")
    print()
    
    # Forward pass
    x_adapter = torch.randn(batch_size, seq_len, d_model)
    output_adapter = adapter(x_adapter)
    print(f"Input shape: {x_adapter.shape}")
    print(f"Output shape: {output_adapter.shape}")