Skip to main content

theory_core/
optimization.rs

1//! Phase 2C: Performance Optimization Engine
2//!
3//! This module implements intelligent performance optimization including:
4//! - Adaptive backend selection (CPU vs GPU vs SMT vs Neural)
5//! - Workload analysis and resource allocation
6//! - Dynamic parameter tuning based on constraint characteristics
7//! - Performance prediction and bottleneck identification
8
9use crate::ReasoningStrategy;
10use crate::ir::{ConstraintIR, TheoryTag};
11use crate::meta_reasoning::SessionPerformance;
12use anyhow::Result;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::time::Instant;
16
17/// Configuration for the performance optimization engine
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct OptimizationConfig {
20    /// Enable adaptive backend selection
21    pub enable_backend_selection: bool,
22    /// Enable dynamic parameter tuning
23    pub enable_parameter_tuning: bool,
24    /// Enable performance prediction
25    pub enable_performance_prediction: bool,
26    /// Threshold for switching backends based on performance
27    pub backend_switch_threshold: f32,
28    /// Maximum time to spend on optimization analysis (ms)
29    pub max_analysis_time_ms: f64,
30    /// Sample size for performance calibration
31    pub calibration_sample_size: usize,
32}
33
34impl Default for OptimizationConfig {
35    fn default() -> Self {
36        Self {
37            enable_backend_selection: true,
38            enable_parameter_tuning: true,
39            enable_performance_prediction: true,
40            backend_switch_threshold: 0.3, // 30% improvement needed to switch
41            max_analysis_time_ms: 50.0,
42            calibration_sample_size: 100,
43        }
44    }
45}
46
47/// Available computation backends
48#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
49pub enum ComputeBackend {
50    /// CPU-based computation
51    CPU,
52    /// GPU-accelerated computation  
53    GPU,
54    /// SMT solver backend
55    SMT,
56    /// Neural network approximation
57    Neural,
58    /// Hybrid approach combining multiple backends
59    Hybrid {
60        primary: Box<ComputeBackend>,
61        fallback: Box<ComputeBackend>,
62    },
63}
64
65/// Performance characteristics of a computation backend
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct BackendPerformance {
68    /// Average processing time in milliseconds
69    pub avg_time_ms: f64,
70    /// Success rate (0.0 to 1.0)
71    pub success_rate: f32,
72    /// Memory usage in MB
73    pub memory_usage_mb: f64,
74    /// Throughput in operations per second
75    pub throughput_ops_per_sec: f64,
76    /// Confidence in accuracy (0.0 to 1.0)
77    pub accuracy_confidence: f32,
78    /// Number of samples this is based on
79    pub sample_count: u32,
80    /// Last updated timestamp
81    pub last_updated: u64,
82}
83
84impl Default for BackendPerformance {
85    fn default() -> Self {
86        Self {
87            avg_time_ms: 1000.0,
88            success_rate: 0.7,
89            memory_usage_mb: 100.0,
90            throughput_ops_per_sec: 1.0,
91            accuracy_confidence: 0.8,
92            sample_count: 0,
93            last_updated: 0,
94        }
95    }
96}
97
98/// Workload characteristics for optimization decisions
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct WorkloadProfile {
101    /// Constraint complexity (0.0 to 1.0)
102    pub complexity: f32,
103    /// Number of variables
104    pub variable_count: u32,
105    /// Number of constraints
106    pub constraint_count: u32,
107    /// Theory tags involved
108    pub theories: Vec<TheoryTag>,
109    /// Expected computation intensity
110    pub compute_intensity: ComputeIntensity,
111    /// Memory requirements
112    pub memory_requirements: MemoryRequirements,
113    /// Time sensitivity (real-time vs batch)
114    pub time_sensitivity: TimeSensitivity,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub enum ComputeIntensity {
119    Light,   // Simple linear constraints
120    Medium,  // Moderate nonlinear problems
121    Heavy,   // Complex optimization problems
122    Extreme, // Highly constrained or large-scale problems
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub enum MemoryRequirements {
127    Small,  // < 10 MB
128    Medium, // 10-100 MB
129    Large,  // 100MB - 1GB
130    Huge,   // > 1GB
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub enum TimeSensitivity {
135    RealTime,    // < 10ms required
136    Interactive, // < 100ms preferred
137    Batch,       // seconds acceptable
138    Offline,     // minutes acceptable
139}
140
141/// Optimization recommendation from the performance engine
142#[derive(Debug, Clone)]
143pub struct OptimizationRecommendation {
144    /// Recommended backend
145    pub backend: ComputeBackend,
146    /// Recommended reasoning strategy
147    pub strategy: ReasoningStrategy,
148    /// Optimized parameters
149    pub parameters: OptimizationParameters,
150    /// Confidence in recommendation (0.0 to 1.0)
151    pub confidence: f32,
152    /// Reasoning for the recommendation
153    pub reasoning: String,
154    /// Expected performance metrics
155    pub expected_performance: ExpectedPerformance,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct OptimizationParameters {
160    /// Timeout for computation (milliseconds)
161    pub timeout_ms: f64,
162    /// Parallel worker threads
163    pub worker_threads: u32,
164    /// Memory budget (MB)
165    pub memory_budget_mb: f64,
166    /// Precision requirements
167    pub precision_level: PrecisionLevel,
168    /// Caching strategy
169    pub caching_strategy: CachingStrategy,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub enum PrecisionLevel {
174    Low,    // Fast approximation
175    Medium, // Balanced accuracy
176    High,   // Maximum precision
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub enum CachingStrategy {
181    None,       // No caching
182    Aggressive, // Cache everything
183    Selective,  // Cache based on cost/benefit
184    Adaptive,   // Learn optimal caching
185}
186
187#[derive(Debug, Clone)]
188pub struct ExpectedPerformance {
189    pub estimated_time_ms: f64,
190    pub success_probability: f32,
191    pub memory_usage_mb: f64,
192    pub accuracy_confidence: f32,
193}
194
195/// Performance optimization engine
196pub struct PerformanceOptimizer {
197    config: OptimizationConfig,
198    backend_stats: HashMap<ComputeBackend, BackendPerformance>,
199    workload_history: Vec<(WorkloadProfile, SessionPerformance)>,
200    parameter_tuner: ParameterTuner,
201    backend_selector: BackendSelector,
202}
203
204impl PerformanceOptimizer {
205    pub fn new(config: OptimizationConfig) -> Self {
206        Self {
207            config,
208            backend_stats: HashMap::new(),
209            workload_history: Vec::new(),
210            parameter_tuner: ParameterTuner::new(),
211            backend_selector: BackendSelector::new(),
212        }
213    }
214
215    /// Analyze constraints and recommend optimal configuration
216    pub fn optimize(
217        &mut self,
218        constraints: &ConstraintIR,
219        verbose: bool,
220    ) -> Result<OptimizationRecommendation> {
221        let start_time = Instant::now();
222
223        if verbose {
224            println!("🚀 Performance Optimizer: Analyzing constraints...");
225        }
226
227        // 1. Profile the workload
228        let profile = self.analyze_workload(constraints)?;
229
230        if verbose {
231            println!(
232                "   Workload: {} variables, {} constraints, complexity {:.2}",
233                profile.variable_count, profile.constraint_count, profile.complexity
234            );
235        }
236
237        // 2. Select optimal backend based on workload characteristics
238        let backend = if self.config.enable_backend_selection {
239            match (&profile.compute_intensity, profile.variable_count) {
240                (ComputeIntensity::Light, _) => ComputeBackend::CPU,
241                (ComputeIntensity::Medium, v) if v > 10 => ComputeBackend::GPU,
242                (ComputeIntensity::Heavy, _) => ComputeBackend::GPU,
243                (ComputeIntensity::Extreme, _) => ComputeBackend::GPU,
244                _ => {
245                    // Use theory tags to decide
246                    if profile.theories.contains(&TheoryTag::NLA)
247                        || profile.theories.contains(&TheoryTag::BV)
248                    {
249                        ComputeBackend::GPU // Nonlinear and bitvector benefit from parallel computation
250                    } else {
251                        ComputeBackend::CPU
252                    }
253                }
254            }
255        } else {
256            ComputeBackend::CPU // Default fallback
257        };
258
259        if verbose {
260            println!("   Recommended backend: {:?}", backend);
261        }
262
263        // 3. Tune parameters for this workload
264        let parameters = if self.config.enable_parameter_tuning {
265            self.parameter_tuner
266                .tune_parameters(&profile, &backend, &self.workload_history)?
267        } else {
268            OptimizationParameters::default(&profile)
269        };
270
271        // 4. Predict performance
272        let expected_performance = if self.config.enable_performance_prediction {
273            self.predict_performance(&profile, &backend, &parameters)?
274        } else {
275            ExpectedPerformance::default()
276        };
277
278        // 5. Select reasoning strategy
279        let strategy = self.select_strategy(&profile, &backend)?;
280
281        let elapsed = start_time.elapsed().as_millis() as f64;
282        if verbose {
283            println!("   Optimization analysis completed in {:.1}ms", elapsed);
284        }
285
286        Ok(OptimizationRecommendation {
287            backend,
288            strategy,
289            parameters,
290            confidence: 0.85, // TODO: Compute based on data quality
291            reasoning: format!(
292                "Selected based on workload analysis: {:?} complexity, {:?} time sensitivity",
293                profile.compute_intensity, profile.time_sensitivity
294            ),
295            expected_performance,
296        })
297    }
298
299    /// Record actual performance for learning
300    pub fn record_performance(
301        &mut self,
302        constraints: &ConstraintIR,
303        backend: &ComputeBackend,
304        performance: &SessionPerformance,
305    ) -> Result<()> {
306        let profile = self.analyze_workload(constraints)?;
307
308        // Update backend statistics
309        {
310            let stats = self.backend_stats.entry(backend.clone()).or_default();
311            let alpha = 0.1f64; // Learning rate
312            stats.avg_time_ms =
313                (1.0 - alpha) * stats.avg_time_ms + alpha * performance.total_solve_time_ms;
314            stats.success_rate = ((1.0 - alpha) * stats.success_rate as f64
315                + alpha * if performance.success { 1.0 } else { 0.0 })
316                as f32;
317            stats.accuracy_confidence = ((1.0 - alpha) * stats.accuracy_confidence as f64
318                + alpha * performance.confidence_achieved as f64)
319                as f32;
320            stats.sample_count += 1;
321            stats.last_updated = std::time::SystemTime::now()
322                .duration_since(std::time::UNIX_EPOCH)
323                .unwrap_or_default()
324                .as_secs();
325        }
326
327        // Add to workload history
328        self.workload_history.push((profile, performance.clone()));
329
330        // Keep history bounded
331        if self.workload_history.len() > self.config.calibration_sample_size {
332            self.workload_history.remove(0);
333        }
334
335        // Note: verbose parameter removed - callers should print if needed
336
337        Ok(())
338    }
339
340    fn analyze_workload(&self, constraints: &ConstraintIR) -> Result<WorkloadProfile> {
341        let variable_count = constraints.variables.len() as u32;
342        let constraint_count = constraints.constraints.len() as u32;
343
344        // Compute complexity based on problem characteristics
345        let complexity = self.compute_complexity(constraints);
346
347        // Determine compute intensity
348        let compute_intensity = match (variable_count, constraint_count, complexity) {
349            (v, c, _) if v <= 5 && c <= 3 => ComputeIntensity::Light,
350            (v, c, x) if v <= 20 && c <= 10 && x < 0.5 => ComputeIntensity::Medium,
351            (v, c, x) if v <= 100 && c <= 50 && x < 0.8 => ComputeIntensity::Heavy,
352            _ => ComputeIntensity::Extreme,
353        };
354
355        // Estimate memory requirements
356        let memory_requirements = match variable_count + constraint_count {
357            n if n <= 10 => MemoryRequirements::Small,
358            n if n <= 100 => MemoryRequirements::Medium,
359            n if n <= 1000 => MemoryRequirements::Large,
360            _ => MemoryRequirements::Huge,
361        };
362
363        // Default to interactive for now
364        let time_sensitivity = TimeSensitivity::Interactive;
365
366        Ok(WorkloadProfile {
367            complexity,
368            variable_count,
369            constraint_count,
370            theories: constraints.theory_tags.to_vec(),
371            compute_intensity,
372            memory_requirements,
373            time_sensitivity,
374        })
375    }
376
377    fn compute_complexity(&self, constraints: &ConstraintIR) -> f32 {
378        let var_factor = (constraints.variables.len() as f32).ln() / 10.0;
379        let constraint_factor = (constraints.constraints.len() as f32).ln() / 5.0;
380
381        // Theory complexity weights
382        let theory_complexity: f32 = constraints
383            .theory_tags
384            .iter()
385            .map(|tag| match tag {
386                TheoryTag::LIA | TheoryTag::LRA => 0.2,
387                TheoryTag::NLA => 0.8,
388                TheoryTag::BV => 0.4,
389                TheoryTag::Array => 0.6,
390                TheoryTag::String => 0.5,
391                _ => 0.3,
392            })
393            .sum();
394
395        ((var_factor + constraint_factor + theory_complexity) / 3.0).min(1.0)
396    }
397
398    fn select_strategy(
399        &self,
400        profile: &WorkloadProfile,
401        backend: &ComputeBackend,
402    ) -> Result<ReasoningStrategy> {
403        match (
404            backend,
405            &profile.time_sensitivity,
406            &profile.compute_intensity,
407        ) {
408            (ComputeBackend::Neural, _, _) => Ok(ReasoningStrategy::Speculate),
409            (ComputeBackend::SMT, _, _) => Ok(ReasoningStrategy::Verify),
410            (_, TimeSensitivity::RealTime, _) => Ok(ReasoningStrategy::Speculate),
411            (_, _, ComputeIntensity::Extreme) => Ok(ReasoningStrategy::Verify),
412            _ => Ok(ReasoningStrategy::Hybrid),
413        }
414    }
415
416    fn predict_performance(
417        &self,
418        profile: &WorkloadProfile,
419        backend: &ComputeBackend,
420        parameters: &OptimizationParameters,
421    ) -> Result<ExpectedPerformance> {
422        // Apply parameter-based adjustments
423        let timeout_factor = if parameters.timeout_ms > 0.0 {
424            1.0 + (parameters.timeout_ms / 1000.0).ln() / 10.0
425        } else {
426            1.0
427        };
428
429        let parallel_factor = if parameters.worker_threads > 1 {
430            0.7 // Parallel execution improves performance
431        } else {
432            1.0
433        };
434
435        // Use historical data if available
436        if let Some(stats) = self.backend_stats.get(backend) {
437            let complexity_factor = 1.0 + profile.complexity as f64;
438            let size_factor = 1.0 + (profile.variable_count as f64).ln() / 10.0;
439
440            Ok(ExpectedPerformance {
441                estimated_time_ms: stats.avg_time_ms
442                    * complexity_factor
443                    * size_factor
444                    * timeout_factor
445                    * parallel_factor,
446                success_probability: stats.success_rate,
447                memory_usage_mb: stats.memory_usage_mb * size_factor,
448                accuracy_confidence: stats.accuracy_confidence,
449            })
450        } else {
451            // Fallback estimates with parameter adjustments
452            let mut fallback = ExpectedPerformance::default();
453            fallback.estimated_time_ms *= timeout_factor * parallel_factor;
454            Ok(fallback)
455        }
456    }
457}
458
459impl OptimizationParameters {
460    fn default(profile: &WorkloadProfile) -> Self {
461        let timeout_ms = match profile.compute_intensity {
462            ComputeIntensity::Light => 100.0,
463            ComputeIntensity::Medium => 1000.0,
464            ComputeIntensity::Heavy => 5000.0,
465            ComputeIntensity::Extreme => 30000.0,
466        };
467
468        let worker_threads = match profile.compute_intensity {
469            ComputeIntensity::Light => 1,
470            ComputeIntensity::Medium => 2,
471            ComputeIntensity::Heavy => 4,
472            ComputeIntensity::Extreme => 8,
473        };
474
475        let memory_budget_mb = match profile.memory_requirements {
476            MemoryRequirements::Small => 50.0,
477            MemoryRequirements::Medium => 200.0,
478            MemoryRequirements::Large => 1000.0,
479            MemoryRequirements::Huge => 4000.0,
480        };
481
482        Self {
483            timeout_ms,
484            worker_threads,
485            memory_budget_mb,
486            precision_level: PrecisionLevel::Medium,
487            caching_strategy: CachingStrategy::Selective,
488        }
489    }
490}
491
492impl Default for ExpectedPerformance {
493    fn default() -> Self {
494        Self {
495            estimated_time_ms: 1000.0,
496            success_probability: 0.8,
497            memory_usage_mb: 100.0,
498            accuracy_confidence: 0.85,
499        }
500    }
501}
502
503/// Parameter tuning component
504pub struct ParameterTuner {
505    // Placeholder for ML-based parameter tuning
506}
507
508impl ParameterTuner {
509    pub fn new() -> Self {
510        Self {}
511    }
512
513    pub fn tune_parameters(
514        &self,
515        profile: &WorkloadProfile,
516        _backend: &ComputeBackend,
517        history: &[(WorkloadProfile, SessionPerformance)],
518    ) -> Result<OptimizationParameters> {
519        // Find similar workloads in history
520        let similar_workloads: Vec<_> = history
521            .iter()
522            .filter(|(p, _)| self.workload_similarity(profile, p) > 0.7)
523            .collect();
524
525        if similar_workloads.is_empty() {
526            return Ok(OptimizationParameters::default(profile));
527        }
528
529        // Analyze what parameters worked best
530        let avg_success_rate: f32 = similar_workloads
531            .iter()
532            .map(|(_, perf)| if perf.success { 1.0 } else { 0.0 })
533            .sum::<f32>()
534            / similar_workloads.len() as f32;
535
536        let avg_time: f64 = similar_workloads
537            .iter()
538            .map(|(_, perf)| perf.total_solve_time_ms)
539            .sum::<f64>()
540            / similar_workloads.len() as f64;
541
542        // Adjust parameters based on historical performance
543        let mut params = OptimizationParameters::default(profile);
544
545        if avg_success_rate < 0.7 {
546            // Increase timeout and resources for better success rate
547            params.timeout_ms *= 1.5;
548            params.worker_threads = (params.worker_threads * 2).min(16);
549        }
550
551        if avg_time > 5000.0 {
552            // Problem tends to be slow, prepare accordingly
553            params.precision_level = PrecisionLevel::Medium;
554            params.caching_strategy = CachingStrategy::Aggressive;
555        }
556
557        Ok(params)
558    }
559
560    fn workload_similarity(&self, a: &WorkloadProfile, b: &WorkloadProfile) -> f32 {
561        let complexity_sim = 1.0 - (a.complexity - b.complexity).abs();
562        let var_sim = 1.0
563            - ((a.variable_count as f32 - b.variable_count as f32).abs()
564                / a.variable_count.max(b.variable_count).max(1) as f32);
565        let theory_sim = self.theory_similarity(&a.theories, &b.theories);
566
567        (complexity_sim + var_sim + theory_sim) / 3.0
568    }
569
570    fn theory_similarity(&self, a: &[TheoryTag], b: &[TheoryTag]) -> f32 {
571        if a.is_empty() && b.is_empty() {
572            return 1.0;
573        }
574
575        let intersection: usize = a.iter().filter(|tag| b.contains(tag)).count();
576        let union: usize = a.len() + b.len() - intersection;
577
578        if union == 0 {
579            1.0
580        } else {
581            intersection as f32 / union as f32
582        }
583    }
584}
585
586/// Backend selection component with intelligent routing
587pub struct BackendSelector {
588    /// Performance history for each backend
589    backend_stats: HashMap<ComputeBackend, BackendPerformance>,
590    /// System capabilities (GPU availability, etc.)
591    gpu_available: bool,
592    /// SMT solver availability
593    smt_available: bool,
594}
595
596impl Default for BackendSelector {
597    fn default() -> Self {
598        Self::new()
599    }
600}
601
602impl BackendSelector {
603    pub fn new() -> Self {
604        // Detect system capabilities
605        let gpu_available = Self::detect_gpu();
606        let smt_available = Self::detect_smt();
607
608        Self {
609            backend_stats: HashMap::new(),
610            gpu_available,
611            smt_available,
612        }
613    }
614
615    /// Detect if GPU is available
616    fn detect_gpu() -> bool {
617        // Check if WGPU can create an adapter
618        #[cfg(feature = "gpu")]
619        {
620            // In real implementation, we'd try to create a WGPU adapter
621            // For now, assume GPU might be available
622            true
623        }
624        #[cfg(not(feature = "gpu"))]
625        {
626            false
627        }
628    }
629
630    /// Detect if SMT solver is available
631    fn detect_smt() -> bool {
632        #[cfg(feature = "z3-solver")]
633        {
634            true
635        }
636        #[cfg(not(feature = "z3-solver"))]
637        {
638            false
639        }
640    }
641
642    /// Update performance statistics for a backend
643    pub fn update_stats(&mut self, backend: ComputeBackend, perf: BackendPerformance) {
644        self.backend_stats.insert(backend, perf);
645    }
646
647    pub fn select_backend(&self, profile: &WorkloadProfile) -> Result<ComputeBackend> {
648        // Use instance stats and system capabilities
649        let stats = &self.backend_stats;
650
651        // Calculate problem size score
652        let problem_size = profile.variable_count * profile.constraint_count;
653
654        // Rule-based selection with performance history and system capabilities
655        match (&profile.time_sensitivity, &profile.compute_intensity) {
656            (TimeSensitivity::RealTime, _) => {
657                // Prefer fastest backends for real-time
658                if self.is_fast_enough(&ComputeBackend::Neural, stats, 10.0) {
659                    Ok(ComputeBackend::Neural)
660                } else {
661                    Ok(ComputeBackend::CPU)
662                }
663            }
664            (TimeSensitivity::Interactive, ComputeIntensity::Light) => {
665                // Light workload - CPU is fine
666                Ok(ComputeBackend::CPU)
667            }
668            (TimeSensitivity::Interactive, ComputeIntensity::Medium) => {
669                // Medium workload - use GPU if available and problem is large enough
670                if self.gpu_available
671                    && problem_size > 500
672                    && self.is_available(&ComputeBackend::GPU, stats)
673                {
674                    Ok(ComputeBackend::GPU)
675                } else {
676                    Ok(ComputeBackend::CPU)
677                }
678            }
679            (_, ComputeIntensity::Heavy | ComputeIntensity::Extreme) => {
680                // Heavy workloads - prefer SMT solver for accuracy on nonlinear problems
681                if self.smt_available && profile.theories.contains(&TheoryTag::NLA) {
682                    Ok(ComputeBackend::SMT)
683                } else if self.gpu_available && problem_size > 1000 {
684                    // Large problem - use GPU with CPU fallback
685                    Ok(ComputeBackend::Hybrid {
686                        primary: Box::new(ComputeBackend::GPU),
687                        fallback: Box::new(ComputeBackend::CPU),
688                    })
689                } else {
690                    Ok(ComputeBackend::CPU)
691                }
692            }
693            _ => Ok(ComputeBackend::CPU), // Safe default
694        }
695    }
696
697    fn is_fast_enough(
698        &self,
699        backend: &ComputeBackend,
700        stats: &HashMap<ComputeBackend, BackendPerformance>,
701        threshold_ms: f64,
702    ) -> bool {
703        stats
704            .get(backend)
705            .map(|s| s.avg_time_ms < threshold_ms)
706            .unwrap_or(false)
707    }
708
709    fn is_available(
710        &self,
711        backend: &ComputeBackend,
712        stats: &HashMap<ComputeBackend, BackendPerformance>,
713    ) -> bool {
714        stats
715            .get(backend)
716            .map(|s| s.success_rate > 0.5)
717            .unwrap_or(false)
718    }
719}