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

ssm_code.py

42_state_space_models/ssm_code.py  ·  370 lines  ·  view on GitHub

"""
State Space Models: Complete Implementation
Simple implementations of SSM, S4, and Mamba
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from typing import Optional, Tuple

# ==================== BASIC STATE SPACE MODEL ====================

class StateSpaceModel(nn.Module):
    """
    Basic State Space Model
    
    Continuous-time SSM:
    dh/dt = A h + B u
    y = C h + D u
    
    Discrete-time SSM:
    h[k+1] = A_d h[k] + B_d u[k]
    y[k] = C_d h[k] + D_d u[k]
    
    COMPLEXITY: O(n) - linear in sequence length
    """
    def __init__(self, state_dim: int = 64, input_dim: int = 1, output_dim: int = 1):
        super().__init__()
        self.state_dim = state_dim
        self.input_dim = input_dim
        self.output_dim = output_dim
        
        # State matrices
        # A: State transition matrix (state_dim × state_dim)
        self.A = nn.Parameter(torch.randn(state_dim, state_dim) * 0.1)
        
        # B: Input matrix (state_dim × input_dim)
        self.B = nn.Parameter(torch.randn(state_dim, input_dim) * 0.1)
        
        # C: Output matrix (output_dim × state_dim)
        self.C = nn.Parameter(torch.randn(output_dim, state_dim) * 0.1)
        
        # D: Feedthrough (output_dim × input_dim)
        self.D = nn.Parameter(torch.randn(output_dim, input_dim) * 0.1)
        
        # Initial state
        self.h0 = nn.Parameter(torch.zeros(state_dim))
    
    def discretize(self, delta: float = 1.0) -> Tuple[torch.Tensor, torch.Tensor]:
        """
        Discretize continuous-time SSM to discrete-time
        
        Zero-order hold method:
        A_d = exp(Δ A)
        B_d = (A_d - I) A^(-1) B
        
        Args:
            delta: Time step size
        Returns:
            A_d, B_d: Discretized matrices
        """
        # A_d = exp(Δ A)
        A_d = torch.matrix_exp(delta * self.A)
        
        # B_d = (A_d - I) A^(-1) B
        # For numerical stability, use matrix inverse or solve
        I = torch.eye(self.state_dim, device=self.A.device)
        A_inv = torch.linalg.inv(self.A)
        B_d = (A_d - I) @ A_inv @ self.B
        
        return A_d, B_d
    
    def forward(self, u: torch.Tensor, delta: float = 1.0) -> torch.Tensor:
        """
        Forward pass through SSM
        
        Args:
            u: Input sequence, shape (batch, seq_len, input_dim)
            delta: Time step size
        Returns:
            y: Output sequence, shape (batch, seq_len, output_dim)
        """
        batch_size, seq_len, _ = u.shape
        
        # Discretize
        A_d, B_d = self.discretize(delta)
        
        # Initialize state
        h = self.h0.unsqueeze(0).expand(batch_size, -1)  # (batch, state_dim)
        
        outputs = []
        
        # Recurrence: process sequence step by step
        for k in range(seq_len):
            # State update: h[k+1] = A_d h[k] + B_d u[k]
            u_k = u[:, k, :]  # (batch, input_dim)
            h = torch.matmul(h, A_d.t()) + torch.matmul(u_k, B_d.t())
            # h shape: (batch, state_dim)
            
            # Output: y[k] = C h[k] + D u[k]
            y_k = torch.matmul(h, self.C.t()) + torch.matmul(u_k, self.D.t())
            # y_k shape: (batch, output_dim)
            
            outputs.append(y_k)
        
        # Stack outputs
        y = torch.stack(outputs, dim=1)  # (batch, seq_len, output_dim)
        
        return y


# ==================== S4 LAYER ====================

class S4Layer(nn.Module):
    """
    S4 (Structured State Space for Sequence Modeling) Layer
    
    Uses structured state matrices for efficiency
    Can be computed via convolution (FFT) or recurrence
    """
    def __init__(self, d_model: int, state_dim: int = 64, dropout: float = 0.1):
        super().__init__()
        self.d_model = d_model
        self.state_dim = state_dim
        
        # Input projection
        self.input_proj = nn.Linear(d_model, d_model)
        
        # S4 parameters (simplified - would use structured matrices in practice)
        self.ssm = StateSpaceModel(state_dim=state_dim, input_dim=d_model, output_dim=d_model)
        
        # Output projection
        self.output_proj = nn.Linear(d_model, d_model)
        self.dropout = nn.Dropout(dropout)
        self.norm = nn.LayerNorm(d_model)
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        S4 forward pass
        
        Args:
            x: Input, shape (batch, seq_len, d_model)
        Returns:
            Output, shape (batch, seq_len, d_model)
        """
        # Residual connection
        residual = x
        
        # Input projection
        x = self.input_proj(x)
        
        # S4 processing
        x = self.ssm(x)  # (batch, seq_len, d_model)
        
        # Output projection
        x = self.output_proj(x)
        x = self.dropout(x)
        
        # Residual + normalization
        x = self.norm(x + residual)
        
        return x


# ==================== MAMBA (SELECTIVE SSM) ====================

class MambaBlock(nn.Module):
    """
    Mamba Block: Selective State Space Model
    
    KEY INNOVATION: Makes B and C input-dependent
    
    Standard SSM: Fixed A, B, C
    Mamba: Fixed A, but B[k] and C[k] depend on input u[k]
    
    This enables selective information processing
    """
    def __init__(self, d_model: int, state_dim: int = 16, expand: int = 2, dropout: float = 0.1):
        super().__init__()
        self.d_model = d_model
        self.state_dim = state_dim
        self.d_inner = d_model * expand
        
        # Input projection
        self.in_proj = nn.Linear(d_model, self.d_inner * 2)  # *2 for gate
        
        # Selective parameters (input-dependent)
        self.B_proj = nn.Linear(self.d_inner, state_dim)  # B depends on input
        self.C_proj = nn.Linear(self.d_inner, state_dim)  # C depends on input
        self.delta_proj = nn.Linear(self.d_inner, state_dim)  # Δ depends on input
        
        # Fixed A matrix (for efficiency)
        # In practice, would use structured form (e.g., diagonal)
        self.A = nn.Parameter(torch.randn(state_dim, state_dim) * 0.1)
        
        # Output projection
        self.out_proj = nn.Linear(self.d_inner, d_model)
        self.dropout = nn.Dropout(dropout)
        self.norm = nn.LayerNorm(d_model)
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        Mamba forward pass
        
        Args:
            x: Input, shape (batch, seq_len, d_model)
        Returns:
            Output, shape (batch, seq_len, d_model)
        """
        batch_size, seq_len, _ = x.shape
        residual = x
        
        # Input projection + gate
        x_proj = self.in_proj(x)  # (batch, seq_len, d_inner * 2)
        x, gate = x_proj.chunk(2, dim=-1)  # Split into x and gate
        x = F.silu(x)  # SiLU activation
        gate = F.silu(gate)
        
        # Selective parameters (input-dependent)
        B = self.B_proj(x)  # (batch, seq_len, state_dim) - depends on input!
        C = self.C_proj(x)  # (batch, seq_len, state_dim) - depends on input!
        delta = F.softplus(self.delta_proj(x))  # (batch, seq_len, state_dim) - step size
        
        # Discretize with input-dependent delta
        # A_d = exp(Δ A) for each position
        A_d = torch.exp(delta.unsqueeze(-1) * self.A.unsqueeze(0).unsqueeze(0))
        # Shape: (batch, seq_len, state_dim, state_dim)
        
        # B_d = (A_d - I) A^(-1) B (simplified)
        I = torch.eye(self.state_dim, device=x.device)
        A_inv = torch.linalg.inv(self.A)
        B_d = (A_d - I.unsqueeze(0).unsqueeze(0)) @ A_inv.unsqueeze(0).unsqueeze(0) @ B.unsqueeze(-1)
        B_d = B_d.squeeze(-1)  # (batch, seq_len, state_dim)
        
        # State recurrence (sequential)
        h = torch.zeros(batch_size, self.state_dim, device=x.device)
        outputs = []
        
        for k in range(seq_len):
            # State update: h[k+1] = A_d[k] h[k] + B_d[k] u[k]
            A_d_k = A_d[:, k, :, :]  # (batch, state_dim, state_dim)
            B_d_k = B_d[:, k, :]  # (batch, state_dim)
            u_k = x[:, k, :]  # (batch, d_inner)
            
            # Project u_k to state_dim for state update
            # In practice, would have separate projection
            h = torch.bmm(A_d_k, h.unsqueeze(-1)).squeeze(-1) + B_d_k
            
            # Output: y[k] = C[k] h[k]
            C_k = C[:, k, :]  # (batch, state_dim)
            y_k = (C_k * h).sum(dim=-1, keepdim=True)  # Simplified
            outputs.append(y_k)
        
        y = torch.stack(outputs, dim=1)  # (batch, seq_len, 1)
        
        # Expand back to d_inner (simplified - would have proper projection)
        y = y.expand(-1, -1, self.d_inner)
        
        # Apply gate
        y = y * gate
        
        # Output projection
        y = self.out_proj(y)
        y = self.dropout(y)
        
        # Residual + normalization
        y = self.norm(y + residual)
        
        return y


# ==================== COMPARISON ====================

def compare_ssm_vs_transformer(seq_len: int = 10000, d_model: int = 768):
    """
    Compare SSM vs Transformer complexity
    """
    print("=" * 80)
    print("SSM vs Transformer Complexity Comparison")
    print("=" * 80)
    
    print(f"\nConfiguration:")
    print(f"  seq_len: {seq_len:,}")
    print(f"  d_model: {d_model}")
    
    # Transformer attention
    transformer_time = seq_len * seq_len * d_model  # O(n²d)
    transformer_space = seq_len * seq_len  # O(n²)
    
    # SSM recurrence
    ssm_time = seq_len * d_model  # O(nd)
    ssm_space = seq_len * d_model  # O(nd)
    
    print("\n" + "-" * 80)
    print("TIME COMPLEXITY (operations)")
    print("-" * 80)
    print(f"Transformer: {transformer_time:>15,} operations (O(n²d))")
    print(f"SSM:         {ssm_time:>15,} operations (O(nd))")
    print(f"Speedup:     {transformer_time/ssm_time:.1f}×")
    
    print("\n" + "-" * 80)
    print("SPACE COMPLEXITY (memory)")
    print("-" * 80)
    print(f"Transformer: {transformer_space:>15,} values (O(n²))")
    print(f"SSM:         {ssm_space:>15,} values (O(nd))")
    print(f"Reduction:   {transformer_space/ssm_space:.1f}×")
    
    print("\n" + "-" * 80)
    print("KEY INSIGHT")
    print("-" * 80)
    print(f"For seq_len={seq_len:,}:")
    print(f"  - Transformer: Quadratic scaling ({seq_len:,}² = {seq_len*seq_len:,})")
    print(f"  - SSM: Linear scaling ({seq_len:,})")
    print(f"  - SSM is {transformer_time/ssm_time:.0f}× faster for this sequence length")


# ==================== USAGE EXAMPLE ====================

if __name__ == "__main__":
    print("State Space Models Implementation")
    print("=" * 80)
    
    # Comparison
    compare_ssm_vs_transformer(seq_len=10000, d_model=768)
    
    print("\n\n" + "=" * 80)
    print("Example: Basic SSM")
    print("=" * 80)
    
    state_dim = 64
    input_dim = 1
    output_dim = 1
    
    ssm = StateSpaceModel(state_dim, input_dim, output_dim)
    
    batch_size = 2
    seq_len = 100
    u = torch.randn(batch_size, seq_len, input_dim)
    
    y = ssm(u)
    
    print(f"\nInput shape: {u.shape}")
    print(f"Output shape: {y.shape}")
    print(f"SSM parameters: {sum(p.numel() for p in ssm.parameters()):,}")
    
    print("\n" + "=" * 80)
    print("Example: Mamba Block")
    print("=" * 80)
    
    d_model = 768
    mamba = MambaBlock(d_model, state_dim=16)
    
    x = torch.randn(batch_size, seq_len, d_model)
    output = mamba(x)
    
    print(f"\nInput shape: {x.shape}")
    print(f"Output shape: {output.shape}")
    print(f"Mamba parameters: {sum(p.numel() for p in mamba.parameters()):,}")
    
    print("\n" + "=" * 80)
    print("KEY INSIGHTS")
    print("=" * 80)
    print("""
    1. SSMs use linear recurrence: O(n) complexity
    2. Transformers use attention: O(n²) complexity
    3. SSMs better for very long sequences (100K+)
    4. Mamba makes parameters input-dependent (selective)
    5. State-of-the-art for long sequence modeling
    """)