Skip to content

Latest commit

 

History

History
601 lines (468 loc) · 16.3 KB

File metadata and controls

601 lines (468 loc) · 16.3 KB

VSA Performance Optimization Guide

Introduction

This guide provides comprehensive strategies for optimizing Vector Symbolic Architecture (VSA) performance. We cover dimension selection, operation optimization, memory management, and hardware acceleration techniques.

Performance Fundamentals

Computational Complexity

Operation Time Complexity Space Complexity Bottleneck
Encoding O(1) O(d) Memory allocation
Binding O(d) to O(d log d) O(d) Element-wise ops / FFT
Bundling O(nd) O(d) Memory bandwidth
Similarity O(d) O(1) Cache efficiency
Cleanup O(Md) O(Md) Memory access patterns

Where:

  • d = dimension
  • n = number of items to bundle
  • M = number of stored items

Key Performance Factors

  1. Dimension Size: Affects all operations linearly
  2. Vector Type: Determines operation cost
  3. Binding Method: Varies from O(d) to O(d log d)
  4. Memory Layout: Impact on cache efficiency
  5. Parallelization: Potential for speedup

Dimension Optimization

Choosing the Right Dimension

def calculate_optimal_dimension(num_items, error_rate=0.01):
    """Calculate dimension for desired capacity and error rate."""
    # For bundling capacity
    bundle_dim = int((num_items / error_rate) ** 2)
    
    # For binding pairs
    binding_dim = int(num_items * np.log(1/error_rate) * 100)
    
    # Choose larger for safety
    return max(bundle_dim, binding_dim, 1000)

Dimension Guidelines

Use Case Items Recommended Dimension
Small demos < 10 1,000 (default)
Medium applications 10-100 5,000 - 10,000
Large systems 100-1000 10,000 - 50,000
Research/production 1000+ 50,000 - 100,000

Dynamic Dimension Adjustment

class AdaptiveVSA:
    def __init__(self, initial_dim=1000):
        self.dimension = initial_dim
        self.vsa = create_vsa(VSAConfig(dimension=initial_dim))
        self.stored_items = 0
    
    def check_capacity(self):
        """Increase dimension if approaching capacity."""
        capacity = np.sqrt(self.dimension)
        if self.stored_items > 0.7 * capacity:
            self.increase_dimension()
    
    def increase_dimension(self):
        """Double dimension and migrate vectors."""
        new_dim = self.dimension * 2
        # Pad existing vectors with zeros
        # Or regenerate with new dimension

Vector Type Performance

Performance Comparison

Vector Type Memory/Element Binding Speed Best For
Binary 1 bit Fastest (XOR) Hardware, memory-limited
Bipolar 8 bits Fast (multiply) General purpose
Ternary 2-8 bits Fast (sparse) Large-scale, sparse data
Complex 128 bits Slow (complex ops) Frequency domain
Integer 8-16 bits Moderate Special algorithms

Optimization by Type

Binary Vectors

# Use bit-packed arrays for memory efficiency
import numpy as np

class PackedBinaryVector:
    def __init__(self, dimension):
        self.dimension = dimension
        self.data = np.packbits(np.random.randint(0, 2, dimension))
    
    def unpack(self):
        return np.unpackbits(self.data)[:self.dimension]

Sparse Ternary

# Store only non-zero elements
class SparseTernaryVector:
    def __init__(self, dimension, sparsity=0.1):
        self.dimension = dimension
        nnz = int(dimension * sparsity)
        self.indices = np.random.choice(dimension, nnz, replace=False)
        self.values = np.random.choice([-1, 1], nnz)
    
    def to_dense(self):
        dense = np.zeros(self.dimension, dtype=np.int8)
        dense[self.indices] = self.values
        return dense

Operation Optimization

Binding Operations

XOR Optimization

# Vectorized XOR for binary arrays
def fast_xor_binding(x, y):
    """Optimized XOR using NumPy."""
    return np.bitwise_xor(x, y)

# For packed bits
def packed_xor_binding(x_packed, y_packed):
    """XOR on packed binary data."""
    return np.bitwise_xor(x_packed, y_packed)

Multiplication Optimization

# Use BLAS for large vectors
def fast_multiplication_binding(x, y):
    """Optimized multiplication."""
    return np.multiply(x, y, dtype=np.int8)  # Minimize precision

# SIMD-friendly version
def simd_multiplication(x, y):
    """Ensure alignment for SIMD."""
    x_aligned = np.ascontiguousarray(x)
    y_aligned = np.ascontiguousarray(y)
    return np.multiply(x_aligned, y_aligned)

Convolution Optimization

# Cache FFT plans
from scipy import fft

class CachedConvolution:
    def __init__(self, dimension):
        self.dimension = dimension
        self.fft_plan = fft.next_fast_len(dimension)
    
    def convolve(self, x, y):
        """Convolution with cached FFT plan."""
        x_fft = fft.fft(x, n=self.fft_plan)
        y_fft = fft.fft(y, n=self.fft_plan)
        return fft.ifft(x_fft * y_fft)[:self.dimension].real

Bundling Optimization

Batch Bundling

def fast_bundle(vectors, weights=None):
    """Optimized bundling using matrix operations."""
    if weights is None:
        # Use matrix sum
        matrix = np.vstack(vectors)
        summed = np.sum(matrix, axis=0)
    else:
        # Weighted sum
        matrix = np.vstack(vectors)
        weights = np.array(weights).reshape(-1, 1)
        summed = np.sum(matrix * weights, axis=0)
    
    # Threshold for bipolar
    return np.sign(summed)

Incremental Bundling

class IncrementalBundle:
    """Maintain running sum for efficiency."""
    
    def __init__(self, dimension):
        self.sum = np.zeros(dimension)
        self.count = 0
    
    def add(self, vector):
        """Add vector to bundle."""
        self.sum += vector
        self.count += 1
    
    def get_bundle(self):
        """Get current bundle."""
        return np.sign(self.sum)

Similarity Optimization

Batch Similarity

def batch_similarity(query, database):
    """Compute similarity to many vectors at once."""
    # Normalize if needed
    query_norm = query / np.linalg.norm(query)
    
    # Matrix multiplication for dot products
    similarities = np.dot(database, query_norm)
    
    return similarities

# GPU acceleration with CuPy (if available)
try:
    import cupy as cp
    
    def gpu_batch_similarity(query, database):
        """GPU-accelerated similarity."""
        query_gpu = cp.asarray(query)
        database_gpu = cp.asarray(database)
        
        similarities = cp.dot(database_gpu, query_gpu)
        return cp.asnumpy(similarities)
except ImportError:
    gpu_batch_similarity = batch_similarity

Memory Management

Memory Layout Optimization

# Column-major for better cache locality in some operations
vectors_fortran = np.asfortranarray(vectors)

# Ensure contiguous memory
vectors_c = np.ascontiguousarray(vectors)

# Memory-mapped arrays for large datasets
vectors_mmap = np.memmap('vectors.dat', dtype='float32', 
                        mode='r', shape=(num_vectors, dimension))

Memory Pool Pattern

class VectorPool:
    """Reuse vector memory to reduce allocation overhead."""
    
    def __init__(self, dimension, pool_size=100):
        self.dimension = dimension
        self.pool = [np.zeros(dimension) for _ in range(pool_size)]
        self.available = list(range(pool_size))
        self.in_use = set()
    
    def get_vector(self):
        """Get a vector from pool."""
        if self.available:
            idx = self.available.pop()
            self.in_use.add(idx)
            return self.pool[idx]
        else:
            # Allocate new if pool exhausted
            return np.zeros(self.dimension)
    
    def return_vector(self, vector):
        """Return vector to pool."""
        # Find index and return to available
        # Clear vector data for reuse
        vector.fill(0)

Cleanup Memory Optimization

class EfficientCleanupMemory:
    """Memory-efficient cleanup implementation."""
    
    def __init__(self, dimension, max_items=10000):
        self.dimension = dimension
        self.max_items = max_items
        
        # Use memory-mapped file for large storage
        self.vectors = np.memmap(
            'cleanup_memory.dat',
            dtype='float32',
            mode='w+',
            shape=(max_items, dimension)
        )
        self.labels = []
        self.count = 0
        
        # Precompute normalized vectors for faster similarity
        self.normalized = True
    
    def add(self, vector, label):
        """Add normalized vector."""
        if self.count >= self.max_items:
            raise MemoryError("Cleanup memory full")
        
        # Normalize and store
        norm = np.linalg.norm(vector)
        self.vectors[self.count] = vector / norm
        self.labels.append(label)
        self.count += 1
    
    def find_nearest(self, query, k=1):
        """Efficient nearest neighbor search."""
        # Normalize query
        query_norm = query / np.linalg.norm(query)
        
        # Batch similarity computation
        similarities = np.dot(self.vectors[:self.count], query_norm)
        
        # Get top-k
        top_k_indices = np.argpartition(similarities, -k)[-k:]
        top_k_indices = top_k_indices[np.argsort(similarities[top_k_indices])[::-1]]
        
        return [(self.labels[i], similarities[i]) for i in top_k_indices]

Parallelization Strategies

Thread-Level Parallelism

from concurrent.futures import ThreadPoolExecutor
import numpy as np

def parallel_binding(vectors_a, vectors_b, binding_fn, num_threads=4):
    """Parallel binding of vector pairs."""
    n = len(vectors_a)
    results = [None] * n
    
    def bind_chunk(start, end):
        for i in range(start, end):
            results[i] = binding_fn(vectors_a[i], vectors_b[i])
    
    chunk_size = n // num_threads
    with ThreadPoolExecutor(max_workers=num_threads) as executor:
        futures = []
        for i in range(num_threads):
            start = i * chunk_size
            end = n if i == num_threads - 1 else (i + 1) * chunk_size
            futures.append(executor.submit(bind_chunk, start, end))
        
        # Wait for completion
        for future in futures:
            future.result()
    
    return results

Vectorization with NumPy

# Vectorize custom operations
@np.vectorize
def custom_binding(x, y):
    """Custom element-wise operation."""
    return (x * y + x + y) % 3

# Better: use NumPy's built-in vectorization
def vectorized_binding(X, Y):
    """Fully vectorized operation."""
    return (X * Y + X + Y) % 3

GPU Acceleration

# Optional GPU support
try:
    import cupy as cp
    
    class GPUAcceleratedVSA:
        def __init__(self, dimension):
            self.dimension = dimension
            self.use_gpu = True
        
        def bind(self, x, y):
            """GPU-accelerated binding."""
            x_gpu = cp.asarray(x)
            y_gpu = cp.asarray(y)
            result_gpu = x_gpu * y_gpu
            return cp.asnumpy(result_gpu)
        
        def bundle(self, vectors):
            """GPU-accelerated bundling."""
            matrix_gpu = cp.asarray(vectors)
            sum_gpu = cp.sum(matrix_gpu, axis=0)
            result_gpu = cp.sign(sum_gpu)
            return cp.asnumpy(result_gpu)
            
except ImportError:
    print("CuPy not available, using CPU only")

Caching Strategies

Vector Cache

from functools import lru_cache
import hashlib

class CachedVSA(VSA):
    def __init__(self, config):
        super().__init__(config)
        self._cache_size = 10000
    
    @lru_cache(maxsize=10000)
    def encode_cached(self, item):
        """Cache frequently encoded items."""
        return self.encode(item)
    
    def bind_cached(self, x_key, y_key):
        """Cache binding results."""
        cache_key = f"{x_key}_{y_key}"
        if cache_key not in self._bind_cache:
            x = self.encode_cached(x_key)
            y = self.encode_cached(y_key)
            self._bind_cache[cache_key] = self.bind(x, y)
        return self._bind_cache[cache_key]

Precomputation

class PrecomputedVSA:
    def __init__(self, dimension, vocabulary):
        self.dimension = dimension
        self.vsa = create_vsa(VSAConfig(dimension=dimension))
        
        # Precompute all vocabulary vectors
        self.vectors = {
            item: self.vsa.encode(item)
            for item in vocabulary
        }
        
        # Precompute common bindings
        self.common_bindings = {}
        for role in ['subject', 'verb', 'object']:
            for item in vocabulary:
                key = f"{role}_{item}"
                role_vec = self.vectors[role]
                item_vec = self.vectors[item]
                self.common_bindings[key] = self.vsa.bind(role_vec, item_vec)

Profiling and Benchmarking

Performance Profiling

import time
import cProfile
import pstats

def profile_vsa_operations(vsa, num_operations=1000):
    """Profile VSA operations."""
    
    profiler = cProfile.Profile()
    profiler.enable()
    
    # Encoding
    for i in range(num_operations):
        _ = vsa.encode(f"item_{i}")
    
    # Binding
    vectors = [vsa.encode(f"vec_{i}") for i in range(100)]
    for i in range(num_operations):
        _ = vsa.bind(vectors[i % 100], vectors[(i + 1) % 100])
    
    # Bundling
    for i in range(num_operations // 10):
        _ = vsa.bundle(vectors[:10])
    
    profiler.disable()
    
    # Print stats
    stats = pstats.Stats(profiler)
    stats.sort_stats('cumulative')
    stats.print_stats(10)

Benchmarking Suite

def benchmark_vsa_architectures():
    """Compare different VSA configurations."""
    
    dimensions = [1000, 5000, 10000, 50000]
    architectures = ['bsc', 'map', 'fhrr', 'sparse']
    
    results = {}
    
    for dim in dimensions:
        for arch in architectures:
            vsa = create_architecture(arch, dimension=dim)
            
            # Time encoding
            start = time.perf_counter()
            for i in range(1000):
                _ = vsa.encode(f"item_{i}")
            encode_time = time.perf_counter() - start
            
            # Time binding
            vecs = [vsa.encode(f"v_{i}") for i in range(100)]
            start = time.perf_counter()
            for i in range(1000):
                _ = vsa.bind(vecs[i % 100], vecs[(i + 1) % 100])
            bind_time = time.perf_counter() - start
            
            results[f"{arch}_{dim}"] = {
                'encode': encode_time,
                'bind': bind_time
            }
    
    return results

Best Practices Summary

Do's

  1. Choose appropriate dimension based on capacity needs
  2. Use batch operations when possible
  3. Preallocate memory for known sizes
  4. Cache frequently used vectors
  5. Profile before optimizing
  6. Use sparse representations when applicable
  7. Leverage NumPy's vectorization

Don'ts

  1. Don't use larger dimensions than necessary
  2. Don't create vectors in loops - batch instead
  3. Don't ignore memory layout - use contiguous arrays
  4. Don't neglect cleanup memory size
  5. Don't optimize prematurely - profile first

Quick Optimization Checklist

  • Dimension appropriate for capacity?
  • Vector type matches use case?
  • Binding operation optimal for data?
  • Batch operations used?
  • Memory layout optimized?
  • Caching implemented?
  • Parallelization considered?
  • Profiling completed?

Platform-Specific Optimizations

Intel Processors

# Use MKL-accelerated NumPy
import numpy as np
np.show_config()  # Check for MKL

# Enable AVX instructions
import os
os.environ['MKL_ENABLE_INSTRUCTIONS'] = 'AVX2'

ARM Processors

# Use NEON optimizations
# Ensure NumPy compiled with NEON support

GPU Platforms

# NVIDIA GPUs - use CuPy
# AMD GPUs - use ROCm
# Intel GPUs - use oneAPI

Conclusion

VSA performance optimization involves:

  1. Algorithmic choices (dimension, vector type, binding)
  2. Implementation efficiency (vectorization, caching)
  3. Hardware utilization (parallelization, GPU)
  4. Memory management (layout, pooling)

Always profile first, optimize based on measurements, and consider the trade-offs between performance and code complexity.