scirs2_fft/
planning_adaptive.rs

1//! Adaptive FFT Planning
2//!
3//! This module extends the basic planning system with runtime adaptivity,
4//! allowing the planner to adjust its strategy based on observed performance
5//! and system conditions.
6
7use crate::error::FFTResult;
8use crate::planning::{FftPlan, PlannerBackend, PlanningStrategy};
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, Instant};
11
12/// Configuration options for adaptive planning
13#[derive(Debug, Clone)]
14pub struct AdaptivePlanningConfig {
15    /// Whether adaptivity is enabled
16    pub enabled: bool,
17
18    /// Minimum number of FFTs before adapting strategy
19    pub min_samples: usize,
20
21    /// Time between strategy evaluations
22    pub evaluation_interval: Duration,
23
24    /// Maximum number of strategies to try
25    pub max_strategy_switches: usize,
26
27    /// Whether to enable backend switching
28    pub enable_backend_switching: bool,
29
30    /// Threshold for considering a strategy better (ratio)
31    pub improvement_threshold: f64,
32}
33
34impl Default for AdaptivePlanningConfig {
35    fn default() -> Self {
36        Self {
37            enabled: true,
38            min_samples: 5,
39            evaluation_interval: Duration::from_secs(10),
40            max_strategy_switches: 3,
41            enable_backend_switching: true,
42            improvement_threshold: 1.1, // 10% improvement needed
43        }
44    }
45}
46
47/// Performance metrics for a specific strategy
48#[derive(Debug, Clone)]
49struct StrategyMetrics {
50    /// Total execution time
51    total_time: Duration,
52
53    /// Number of executions
54    count: usize,
55
56    /// Average execution time
57    avg_time: Duration,
58
59    /// Last time this strategy was evaluated
60    /// Used for future time-based strategy optimization
61    #[allow(dead_code)]
62    last_evaluated: Instant,
63}
64
65impl StrategyMetrics {
66    /// Create new empty metrics
67    fn new() -> Self {
68        Self {
69            total_time: Duration::from_nanos(0),
70            count: 0,
71            avg_time: Duration::from_nanos(0),
72            last_evaluated: Instant::now(),
73        }
74    }
75
76    /// Record a new execution time
77    fn record(&mut self, time: Duration) {
78        self.total_time += time;
79        self.count += 1;
80        self.avg_time =
81            Duration::from_nanos((self.total_time.as_nanos() / self.count as u128) as u64);
82    }
83}
84
85/// Adaptive planner that switches strategies based on runtime performance
86pub struct AdaptivePlanner {
87    /// The FFT size this planner is optimized for
88    size: Vec<usize>,
89
90    /// Direction of the transform (forward or inverse)
91    forward: bool,
92
93    /// Current strategy being used
94    current_strategy: PlanningStrategy,
95
96    /// Current backend being used
97    current_backend: PlannerBackend,
98
99    /// Performance metrics for each strategy
100    metrics: HashMap<PlanningStrategy, StrategyMetrics>,
101
102    /// Last time the strategy was switched
103    last_strategy_switch: Instant,
104
105    /// Number of strategy switches performed
106    strategy_switches: usize,
107
108    /// Configuration options
109    config: AdaptivePlanningConfig,
110
111    /// Currently active plan
112    current_plan: Option<Arc<FftPlan>>,
113}
114
115use std::collections::HashMap;
116
117impl AdaptivePlanner {
118    /// Create a new adaptive planner
119    pub fn new(size: &[usize], forward: bool, config: Option<AdaptivePlanningConfig>) -> Self {
120        let config = config.unwrap_or_default();
121        let mut metrics = HashMap::new();
122
123        // Initialize metrics for all strategies
124        metrics.insert(PlanningStrategy::AlwaysNew, StrategyMetrics::new());
125        metrics.insert(PlanningStrategy::CacheFirst, StrategyMetrics::new());
126        metrics.insert(PlanningStrategy::SerializedFirst, StrategyMetrics::new());
127        metrics.insert(PlanningStrategy::AutoTuned, StrategyMetrics::new());
128
129        Self {
130            size: size.to_vec(),
131            forward,
132            current_strategy: PlanningStrategy::CacheFirst, // Start with a reasonable default
133            current_backend: PlannerBackend::default(),
134            metrics,
135            last_strategy_switch: Instant::now(),
136            strategy_switches: 0,
137            config,
138            current_plan: None,
139        }
140    }
141
142    /// Get the current strategy
143    pub fn current_strategy(&self) -> PlanningStrategy {
144        self.current_strategy
145    }
146
147    /// Get the current backend
148    pub fn current_backend(&self) -> PlannerBackend {
149        self.current_backend.clone()
150    }
151
152    /// Get the current plan
153    pub fn get_plan(&mut self) -> FFTResult<Arc<FftPlan>> {
154        // If we have a current plan, return it
155        if let Some(plan) = &self.current_plan {
156            return Ok(plan.clone());
157        }
158
159        // Otherwise create a new plan using the regular planner
160        use crate::planning::{AdvancedFftPlanner, PlanningConfig};
161
162        let config = PlanningConfig {
163            strategy: self.current_strategy,
164            ..Default::default()
165        };
166
167        let mut planner = AdvancedFftPlanner::with_config(config);
168        let plan = planner.plan_fft(&self.size, self.forward, self.current_backend.clone())?;
169
170        self.current_plan = Some(plan.clone());
171        Ok(plan)
172    }
173
174    /// Record execution time and potentially adapt strategy
175    pub fn record_execution(&mut self, executiontime: Duration) -> FFTResult<()> {
176        if !self.config.enabled {
177            return Ok(());
178        }
179
180        // Record metrics for current strategy
181        if let Some(metrics) = self.metrics.get_mut(&self.current_strategy) {
182            metrics.record(executiontime);
183        }
184
185        // Check if we should evaluate strategies
186        let should_evaluate =
187            // We have enough samples
188            self.metrics[&self.current_strategy].count >= self.config.min_samples &&
189            // It's been long enough since last evaluation
190            self.last_strategy_switch.elapsed() >= self.config.evaluation_interval &&
191            // We haven't switched too many times
192            self.strategy_switches < self.config.max_strategy_switches;
193
194        if should_evaluate {
195            self.evaluate_strategies()?;
196        }
197
198        Ok(())
199    }
200
201    /// Evaluate all strategies and potentially switch
202    fn evaluate_strategies(&mut self) -> FFTResult<()> {
203        // Find the strategy with the best performance
204        let mut best_strategy = self.current_strategy;
205        let mut best_time = self.metrics[&self.current_strategy].avg_time;
206
207        for (strategy, metrics) in &self.metrics {
208            // Skip strategies with no data
209            if metrics.count == 0 {
210                continue;
211            }
212
213            // Improvement must exceed threshold
214            let improvement_ratio =
215                best_time.as_nanos() as f64 / metrics.avg_time.as_nanos() as f64;
216            if improvement_ratio > self.config.improvement_threshold {
217                best_strategy = *strategy;
218                best_time = metrics.avg_time;
219            }
220        }
221
222        // If we found a better strategy, switch to it
223        if best_strategy != self.current_strategy {
224            self.current_strategy = best_strategy;
225            self.last_strategy_switch = Instant::now();
226            self.strategy_switches += 1;
227
228            // Clear the current plan to force creation with new strategy
229            self.current_plan = None;
230        }
231
232        // Backend switching would go here if enabled
233        if self.config.enable_backend_switching {
234            // This would require additional metrics tracking
235            // Not implemented in this simplified version
236        }
237
238        Ok(())
239    }
240
241    /// Get performance statistics for all strategies
242    pub fn get_statistics(&self) -> HashMap<PlanningStrategy, (Duration, usize)> {
243        let mut stats = HashMap::new();
244
245        for (strategy, metrics) in &self.metrics {
246            stats.insert(*strategy, (metrics.avg_time, metrics.count));
247        }
248
249        stats
250    }
251}
252
253/// Executor that uses adaptive planning
254pub struct AdaptiveExecutor {
255    /// Adaptive planner instance
256    planner: Arc<Mutex<AdaptivePlanner>>,
257}
258
259impl AdaptiveExecutor {
260    /// Create a new adaptive executor
261    pub fn new(size: &[usize], forward: bool, config: Option<AdaptivePlanningConfig>) -> Self {
262        let planner = AdaptivePlanner::new(size, forward, config);
263
264        Self {
265            planner: Arc::new(Mutex::new(planner)),
266        }
267    }
268
269    /// Execute an FFT with adaptive planning
270    pub fn execute(
271        &self,
272        input: &[scirs2_core::numeric::Complex64],
273        output: &mut [scirs2_core::numeric::Complex64],
274    ) -> FFTResult<()> {
275        let start = Instant::now();
276
277        // Get the current plan
278        let plan = {
279            let mut planner = self.planner.lock().unwrap();
280            planner.get_plan()?
281        };
282
283        // Create an executor for the plan
284        let executor = crate::planning::FftPlanExecutor::new(plan);
285
286        // Execute the plan
287        executor.execute(input, output)?;
288
289        // Record execution time
290        let execution_time = start.elapsed();
291
292        {
293            let mut planner = self.planner.lock().unwrap();
294            planner.record_execution(execution_time)?;
295        }
296
297        Ok(())
298    }
299
300    /// Get current strategy
301    pub fn current_strategy(&self) -> PlanningStrategy {
302        let planner = self.planner.lock().unwrap();
303        planner.current_strategy()
304    }
305
306    /// Get performance statistics
307    pub fn get_statistics(&self) -> HashMap<PlanningStrategy, (Duration, usize)> {
308        let planner = self.planner.lock().unwrap();
309        planner.get_statistics()
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use scirs2_core::numeric::Complex64;
317
318    #[test]
319    fn test_adaptive_planner_basics() {
320        let mut planner = AdaptivePlanner::new(&[16], true, None);
321
322        // Should start with CacheFirst strategy
323        assert_eq!(planner.current_strategy(), PlanningStrategy::CacheFirst);
324
325        // Record some executions
326        for _ in 0..10 {
327            planner
328                .record_execution(Duration::from_micros(100))
329                .unwrap();
330        }
331
332        // Check that metrics were recorded
333        let stats = planner.get_statistics();
334        assert_eq!(stats[&PlanningStrategy::CacheFirst].1, 10);
335    }
336
337    #[test]
338    fn test_adaptive_executor() {
339        let executor = AdaptiveExecutor::new(&[16], true, None);
340
341        // Create test data
342        let input = vec![Complex64::new(1.0, 0.0); 16];
343        let mut output = vec![Complex64::default(); 16];
344
345        // Execute several times
346        for _ in 0..5 {
347            executor.execute(&input, &mut output).unwrap();
348        }
349
350        // Check statistics
351        let stats = executor.get_statistics();
352        assert!(stats[&executor.current_strategy()].1 >= 5);
353    }
354}