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

long_context.py

07_llm_problems/long_context.py  ·  156 lines  ·  view on GitHub

"""
Long Context Solutions
Problem: Standard attention O(n²) too expensive for long sequences
Solutions: Chunking, sliding window, sparse attention
"""
import numpy as np

def softmax(x, axis=-1):
    """Softmax function"""
    exp_x = np.exp(x - np.max(x, axis=axis, keepdims=True))
    return exp_x / np.sum(exp_x, axis=axis, keepdims=True)

def standard_attention(Q: np.ndarray, K: np.ndarray, V: np.ndarray, d_k: int) -> np.ndarray:
    """
    Standard attention: O(n²) complexity
    Problem: Too expensive for long sequences
    """
    scores = Q @ K.T / np.sqrt(d_k)
    attention_weights = softmax(scores)
    return attention_weights @ V

def chunked_attention(Q: np.ndarray, K: np.ndarray, V: np.ndarray,
                     chunk_size: int, d_k: int) -> np.ndarray:
    """
    Chunked Attention: Process in chunks to reduce memory
    
    Problem: Full attention O(n²) memory
    Solution: Process sequence in chunks
    Memory: O(chunk_size × n) instead of O(n²)
    """
    seq_len = Q.shape[0]
    outputs = []
    
    for i in range(0, seq_len, chunk_size):
        chunk_end = min(i + chunk_size, seq_len)
        Q_chunk = Q[i:chunk_end]
        
        # Attend to all K, V (can also limit K/V to window)
        scores = Q_chunk @ K.T / np.sqrt(d_k)
        attention_weights = softmax(scores)
        output_chunk = attention_weights @ V
        
        outputs.append(output_chunk)
    
    return np.concatenate(outputs, axis=0)

def sliding_window_attention(Q: np.ndarray, K: np.ndarray, V: np.ndarray,
                            window_size: int, d_k: int) -> np.ndarray:
    """
    Sliding Window Attention: Each position only attends to local window
    
    Problem: Full attention O(n²) too expensive
    Solution: Local attention + some global positions
    Complexity: O(n × window_size) instead of O(n²)
    """
    seq_len = Q.shape[0]
    outputs = []
    
    for i in range(seq_len):
        # Define window around current position
        start = max(0, i - window_size // 2)
        end = min(seq_len, i + window_size // 2)
        
        Q_i = Q[i:i+1]  # Current query
        K_window = K[start:end]  # Keys in window
        V_window = V[start:end]  # Values in window
        
        # Local attention
        scores = Q_i @ K_window.T / np.sqrt(d_k)
        attention_weights = softmax(scores)
        output = attention_weights @ V_window
        
        outputs.append(output)
    
    return np.concatenate(outputs, axis=0)

def sparse_attention_with_global(Q: np.ndarray, K: np.ndarray, V: np.ndarray,
                                 window_size: int, global_indices: list,
                                 d_k: int) -> np.ndarray:
    """
    Sparse Attention: Local window + global positions
    
    Problem: Need both local and global context
    Solution: Attend to local window + specific global positions
    
    Args:
        global_indices: Positions that all tokens attend to (e.g., [0] for [CLS])
    """
    seq_len = Q.shape[0]
    outputs = []
    
    for i in range(seq_len):
        # Local window
        start = max(0, i - window_size // 2)
        end = min(seq_len, i + window_size // 2)
        local_indices = list(range(start, end))
        
        # Combine local + global
        attend_indices = sorted(set(local_indices + global_indices))
        
        Q_i = Q[i:i+1]
        K_selected = K[attend_indices]
        V_selected = V[attend_indices]
        
        # Attention
        scores = Q_i @ K_selected.T / np.sqrt(d_k)
        attention_weights = softmax(scores)
        output = attention_weights @ V_selected
        
        outputs.append(output)
    
    return np.concatenate(outputs, axis=0)


# Usage Example
if __name__ == "__main__":
    print("Long Context Solutions")
    print("=" * 60)
    
    # Simulate long sequence
    seq_len = 1000  # Long sequence
    d_k = 64
    
    np.random.seed(42)
    Q = np.random.randn(seq_len, d_k)
    K = np.random.randn(seq_len, d_k)
    V = np.random.randn(seq_len, d_k)
    
    print(f"Sequence length: {seq_len}")
    print(f"Standard attention complexity: O({seq_len}²) = {seq_len**2:,} operations")
    
    # Chunked attention
    chunk_size = 128
    print(f"\nChunked Attention (chunk_size={chunk_size}):")
    print(f"Complexity: O({seq_len} × {chunk_size}) = {seq_len * chunk_size:,} operations")
    output_chunked = chunked_attention(Q, K, V, chunk_size, d_k)
    print(f"Output shape: {output_chunked.shape}")
    
    # Sliding window
    window_size = 256
    print(f"\nSliding Window (window_size={window_size}):")
    print(f"Complexity: O({seq_len} × {window_size}) = {seq_len * window_size:,} operations")
    output_window = sliding_window_attention(Q, K, V, window_size, d_k)
    print(f"Output shape: {output_window.shape}")
    
    # Sparse with global
    print(f"\nSparse with Global (window={window_size}, global=[0]):")
    output_sparse = sparse_attention_with_global(Q, K, V, window_size, [0], d_k)
    print(f"Output shape: {output_sparse.shape}")
    
    print("\n" + "=" * 60)
    print("Summary:")
    print(f"Standard: {seq_len**2:,} operations")
    print(f"Chunked: {seq_len * chunk_size:,} operations ({seq_len**2 / (seq_len * chunk_size):.1f}x faster)")
    print(f"Window: {seq_len * window_size:,} operations ({seq_len**2 / (seq_len * window_size):.1f}x faster)")