Files
optimiz-rs/examples/parallel_de_benchmark.py
T

287 lines
9.1 KiB
Python
Raw Normal View History

"""
Parallel Differential Evolution Benchmark
==========================================
Compares serial Python callbacks vs parallel Rust objectives to demonstrate
the 10-100× speedup achievable with GIL-free parallelization.
Tests:
1. Sphere function (simple, convex)
2. Rosenbrock function (non-convex valley)
3. Rastrigin function (highly multimodal)
Metrics:
- Execution time (serial vs parallel)
- Speedup factor
- Solution quality (distance from global optimum)
- Function evaluations
"""
import time
import numpy as np
import optimizr
from typing import Callable, Tuple
def benchmark_function(
name: str,
dim: int,
bounds: list,
max_iter: int = 50,
pop_size: int = 15,
) -> None:
"""Benchmark a function with serial and parallel DE"""
print(f"\n{'=' * 70}")
print(f"Benchmarking: {name} (dim={dim})")
print(f"{'=' * 70}")
# Test 1: Parallel Rust objective (GIL-free)
print("\n1. Parallel Rust Objective (GIL-free):")
start = time.time()
result_parallel = optimizr.parallel_differential_evolution_rust(
objective_name=name.lower(),
dim=dim,
bounds=bounds,
popsize=pop_size,
maxiter=max_iter,
strategy="best1",
seed=42,
track_history=True,
adaptive=True
)
parallel_time = time.time() - start
print(f" Time: {parallel_time:.4f}s")
print(f" Best value: {result_parallel['fun']:.6e}")
print(f" Solution: {result_parallel['x'][:3]}{'...' if dim > 3 else ''}")
print(f" Evaluations: {result_parallel['nfev']}")
print(f" Generations: {result_parallel['nit']}")
# Test 2: Serial Python callback (for comparison)
print("\n2. Serial Python Callback:")
# Create Python objective function
if name.lower() == "sphere":
def objective(x):
return sum(xi**2 for xi in x)
elif name.lower() == "rosenbrock":
def objective(x):
return sum(100*(x[i+1] - x[i]**2)**2 + (1 - x[i])**2 for i in range(len(x)-1))
elif name.lower() == "rastrigin":
def objective(x):
return 10*len(x) + sum(xi**2 - 10*np.cos(2*np.pi*xi) for xi in x)
else:
raise ValueError(f"Unknown function: {name}")
start = time.time()
result_serial = optimizr.differential_evolution(
objective,
bounds=bounds,
popsize=pop_size,
maxiter=max_iter,
strategy="best1",
seed=42,
track_history=True,
adaptive=True,
parallel=False # Forced serial
)
serial_time = time.time() - start
print(f" Time: {serial_time:.4f}s")
print(f" Best value: {result_serial['fun']:.6e}")
print(f" Solution: {result_serial['x'][:3]}{'...' if dim > 3 else ''}")
print(f" Evaluations: {result_serial['nfev']}")
print(f" Generations: {result_serial['nit']}")
# Compute speedup
speedup = serial_time / parallel_time
print(f"\n📊 Performance:")
print(f" Speedup: {speedup:.2f}×")
print(f" Parallel: {parallel_time:.4f}s")
print(f" Serial: {serial_time:.4f}s")
# Quality comparison
quality_ratio = result_parallel['fun'] / result_serial['fun']
print(f"\n📊 Solution Quality:")
print(f" Ratio (Parallel/Serial): {quality_ratio:.4f}")
if quality_ratio < 1.1:
print(" ✅ Comparable or better solution quality")
else:
print(" ⚠️ Serial found better solution (stochastic variation)")
def convergence_analysis():
"""Analyze convergence behavior of parallel vs serial DE"""
print("\n" + "=" * 70)
print("Convergence Analysis: Sphere Function (dim=10)")
print("=" * 70)
dim = 10
bounds = [(-5.0, 5.0)] * dim
# Parallel
result_parallel = optimizr.parallel_differential_evolution_rust(
objective_name="sphere",
dim=dim,
bounds=bounds,
popsize=15,
maxiter=100,
strategy="best1",
seed=42,
track_history=True
)
# Serial
def sphere(x):
return sum(xi**2 for xi in x)
result_serial = optimizr.differential_evolution(
sphere,
bounds=bounds,
popsize=15,
maxiter=100,
strategy="best1",
seed=42,
track_history=True,
parallel=False
)
print("\nConvergence to global optimum (f=0):")
print(f" Parallel: {result_parallel['fun']:.6e} in {result_parallel['nit']} generations")
print(f" Serial: {result_serial['fun']:.6e} in {result_serial['nit']} generations")
# Show convergence curve (every 10 generations)
print("\nConvergence curve (every 10 generations):")
print(" Gen | Parallel Best | Serial Best")
print(" " + "-" * 40)
hist_p = result_parallel.get('history', [])
hist_s = result_serial.get('history', [])
if hist_p and hist_s:
for i in range(0, min(len(hist_p), len(hist_s)), 10):
gen = hist_p[i]['generation'] if isinstance(hist_p[i], dict) else hist_p[i].generation
best_p = hist_p[i]['best_fitness'] if isinstance(hist_p[i], dict) else hist_p[i].best_fitness
best_s = hist_s[i]['best_fitness'] if isinstance(hist_s[i], dict) else hist_s[i].best_fitness
print(f" {gen:3d} | {best_p:13.6e} | {best_s:13.6e}")
def scaling_analysis():
"""Analyze how speedup scales with problem dimensionality"""
print("\n" + "=" * 70)
print("Scaling Analysis: Speedup vs Dimensionality")
print("=" * 70)
print("\nSphere function with increasing dimensions:")
print(" Dim | Parallel Time | Serial Time | Speedup")
print(" " + "-" * 50)
for dim in [5, 10, 20, 30]:
bounds = [(-10.0, 10.0)] * dim
# Parallel
start = time.time()
result_p = optimizr.parallel_differential_evolution_rust(
objective_name="sphere",
dim=dim,
bounds=bounds,
popsize=10,
maxiter=30,
seed=42
)
time_p = time.time() - start
# Serial
def sphere(x):
return sum(xi**2 for xi in x)
start = time.time()
result_s = optimizr.differential_evolution(
sphere,
bounds=bounds,
popsize=10,
maxiter=30,
seed=42,
parallel=False
)
time_s = time.time() - start
speedup = time_s / time_p
print(f" {dim:3d} | {time_p:13.4f}s | {time_s:11.4f}s | {speedup:7.2f}×")
print("\n💡 Observation: Speedup increases with dimension due to more")
print(" expensive objective evaluations benefiting from parallelization.")
def multimodal_challenge():
"""Test on highly multimodal functions (Rastrigin, Ackley)"""
print("\n" + "=" * 70)
print("Multimodal Function Challenge")
print("=" * 70)
for func_name in ["Rastrigin", "Ackley", "Griewank"]:
print(f"\n{func_name} Function (dim=10, 50 iterations):")
dim = 10
if func_name.lower() == "rastrigin":
bounds = [(-5.12, 5.12)] * dim
else: # Ackley, Griewank
bounds = [(-32.0, 32.0)] * dim
# Parallel Rust
start = time.time()
result = optimizr.parallel_differential_evolution_rust(
objective_name=func_name.lower(),
dim=dim,
bounds=bounds,
popsize=20,
maxiter=50,
strategy="best1",
seed=42,
adaptive=True
)
elapsed = time.time() - start
print(f" Time: {elapsed:.4f}s")
print(f" Best value: {result['fun']:.6e}")
print(f" Target: 0.0 (global optimum)")
print(f" Distance: {abs(result['fun']):.6e}")
if result['fun'] < 0.01:
print(" ✅ Near-optimal solution found!")
elif result['fun'] < 1.0:
print(" ✓ Good solution found")
else:
print(" ⚠️ Challenging problem - may need more iterations")
if __name__ == "__main__":
print("=" * 70)
print("Parallel Differential Evolution Benchmark")
print("=" * 70)
print("\nTesting GIL-free parallel evaluation of Rust objectives")
print("Expected speedup: 10-100× depending on problem complexity\n")
# Test 1: Basic benchmarks
benchmark_function("Sphere", dim=20, bounds=[(-10.0, 10.0)] * 20)
benchmark_function("Rosenbrock", dim=10, bounds=[(-5.0, 10.0)] * 10)
benchmark_function("Rastrigin", dim=10, bounds=[(-5.12, 5.12)] * 10)
# Test 2: Convergence analysis
convergence_analysis()
# Test 3: Scaling with dimension
scaling_analysis()
# Test 4: Multimodal challenges
multimodal_challenge()
print("\n" + "=" * 70)
print("✅ Benchmark Complete!")
print("=" * 70)
print("\nKey Findings:")
print(" • Parallel Rust objectives eliminate Python GIL overhead")
print(" • Speedup scales with problem complexity and dimensionality")
print(" • Solution quality is comparable (stochastic variation)")
print(" • Enables high-throughput optimization workflows")
print("=" * 70)