Skip to main content

optirs_learned/adaptive/
performance_predictor.rs

1//! Landscape-and-adaptation driven performance prediction.
2//!
3//! This is the `TransformerPerformancePredictor` façade that
4//! `AdaptiveTransformerEnhancement` holds. The learning machinery it delegates
5//! to lives in [`crate::adaptive::predictor`]; this file is only the bridge
6//! between the crate's landscape/adaptation types and that machinery.
7//!
8//! It was split out of `adaptive/types.rs` to keep every file under the
9//! 2000-line cap.
10
11use scirs2_core::numeric::Float;
12use std::fmt::Debug;
13
14use crate::error::Result;
15
16use super::predictor::{
17    PredictionCache, PredictionFeatures, PredictorFitReport, PredictorNetwork, PredictorSample,
18    PREDICTION_FEATURE_COUNT,
19};
20use super::types::{
21    ArchitectureAdaptation, ArchitectureChange, LandscapeAnalysis, OptimizationStrategy,
22    PerformancePrediction, UncertaintyMethod,
23};
24use super::AdaptiveConfig;
25
26/// Performance predictor for transformer variants.
27///
28/// Wraps the real predictor from [`crate::adaptive::predictor`]: a
29/// Xavier-initialized fixed random feature map with ridge-fitted output heads,
30/// a bounded prediction cache with real hit accounting, and a predictive
31/// standard deviation derived from the ridge posterior.
32///
33/// Before [`Self::train`] has been called, [`Self::predict_improvement`] answers
34/// "no information" (zero means, unit uncertainty, zero confidence) instead of
35/// the constants `0.15 / 0.92 / 0.85 / 0.05` it used to fabricate.
36#[derive(Debug)]
37pub struct TransformerPerformancePredictor<
38    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
39> {
40    /// Neural predictor network (fixed random feature map + learned heads)
41    predictor_network: PredictorNetwork<T>,
42    /// Prediction cache
43    prediction_cache: PredictionCache,
44    /// Uncertainty estimation method (recorded so the choice is inspectable)
45    uncertainty_method: UncertaintyMethod,
46    /// Prediction horizon, taken from the configuration; also the number of
47    /// history samples the analysis confidence saturates at.
48    prediction_horizon: usize,
49}
50impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
51    TransformerPerformancePredictor<T>
52{
53    /// Build the predictor from the adaptive configuration.
54    ///
55    /// The configuration is genuinely read: `prediction_horizon` sizes both the
56    /// hidden feature map and the cache, and `adaptation_lr` supplies the ridge
57    /// coefficient (a smaller adaptation rate means a more conservative, more
58    /// strongly regularized fit).
59    pub fn new(config: &AdaptiveConfig<T>) -> Result<Self> {
60        let horizon = config.prediction_horizon.max(1);
61        // Feature-map width scales with the horizon, floored so the map is
62        // always wider than the raw feature vector and capped so the ridge solve
63        // stays small.
64        let hidden = (2 * PREDICTION_FEATURE_COUNT).max(horizon).min(128);
65        let feature_width = PREDICTION_FEATURE_COUNT.max(horizon.min(64));
66        let ridge = {
67            let lr = config.adaptation_lr.to_f64().unwrap_or(0.0).abs();
68            let lambda = if lr > 0.0 { lr * 1e-2 } else { 1e-4 };
69            scirs2_core::numeric::NumCast::from(lambda.clamp(1e-8, 1.0)).unwrap_or_else(|| T::one())
70        };
71        Ok(Self {
72            predictor_network: PredictorNetwork::new(
73                vec![PREDICTION_FEATURE_COUNT, hidden, feature_width],
74                ridge,
75            )?,
76            prediction_cache: PredictionCache::new(8 * horizon),
77            uncertainty_method: UncertaintyMethod::Ensemble,
78            prediction_horizon: horizon,
79        })
80    }
81
82    /// Fit the prediction heads on observed `(landscape+adaptation, outcome)`
83    /// pairs. Returns the training RMSE of each head.
84    pub fn train(&mut self, samples: &[PredictorSample]) -> Result<PredictorFitReport> {
85        self.prediction_cache.clear();
86        self.predictor_network.fit(samples)
87    }
88
89    /// Whether the heads have been fitted.
90    pub fn is_trained(&self) -> bool {
91        self.predictor_network.is_trained()
92    }
93
94    /// Observed cache hit rate.
95    pub fn cache_hit_rate(&self) -> f64 {
96        self.prediction_cache.hit_rate()
97    }
98
99    /// Uncertainty estimation method in use.
100    pub fn uncertainty_method(&self) -> UncertaintyMethod {
101        self.uncertainty_method
102    }
103
104    /// Prediction horizon this predictor was configured with.
105    pub fn prediction_horizon(&self) -> usize {
106        self.prediction_horizon
107    }
108
109    /// Turn a landscape analysis plus a proposed adaptation into the real
110    /// feature vector the network consumes.
111    pub fn extract_features(
112        landscape: &LandscapeAnalysis<T>,
113        adaptation: &ArchitectureAdaptation<T>,
114    ) -> PredictionFeatures {
115        let complexity = landscape.complexity.to_f64().unwrap_or(0.0);
116        let difficulty = landscape.difficulty.to_f64().unwrap_or(0.0);
117
118        // Strategy family, folded into three slots.
119        let mut conservative = 0.0;
120        let mut aggressive = 0.0;
121        let mut exploratory = 0.0;
122        for strategy in &landscape.recommended_strategies {
123            match strategy {
124                OptimizationStrategy::Conservative | OptimizationStrategy::Exploitative => {
125                    conservative += 1.0
126                }
127                OptimizationStrategy::Aggressive => aggressive += 1.0,
128                OptimizationStrategy::Exploratory => exploratory += 1.0,
129                OptimizationStrategy::Adaptive => {
130                    // Adaptive sits between the two extremes.
131                    conservative += 0.5;
132                    aggressive += 0.5;
133                }
134            }
135        }
136        let strategy_total = (conservative + aggressive + exploratory).max(1.0);
137
138        // Architecture proposal: start from the adapted config, then let the
139        // explicit change list override it (a change list is what the adapter
140        // actually promises to apply).
141        let mut layers = adaptation.adapted_config.num_transformer_layers as f64;
142        let mut hidden = adaptation.adapted_config.model_dimension as f64;
143        let mut heads = adaptation.adapted_config.num_attention_heads as f64;
144        let mut dropout = adaptation.adapted_config.dropout_rate;
145        for change in &adaptation.changes {
146            match change {
147                ArchitectureChange::LayerCountChange(n) => layers = *n as f64,
148                ArchitectureChange::HiddenSizeChange(n) => hidden = *n as f64,
149                ArchitectureChange::AttentionHeadChange(n) => heads = *n as f64,
150                ArchitectureChange::DropoutChange(d) => dropout = *d,
151                ArchitectureChange::ActivationChange(_) => {}
152            }
153        }
154
155        PredictionFeatures {
156            complexity,
157            difficulty,
158            landscape_confidence: landscape.confidence.to_f64().unwrap_or(0.0),
159            expected_improvement: adaptation.expected_improvement.to_f64().unwrap_or(0.0),
160            adaptation_confidence: adaptation.confidence.to_f64().unwrap_or(0.0),
161            change_count: (adaptation.changes.len() as f64 / 8.0).min(1.0),
162            layer_count: (layers / 24.0).min(1.0),
163            hidden_size: (hidden / 2048.0).min(1.0),
164            head_count: (heads / 32.0).min(1.0),
165            dropout: dropout.clamp(0.0, 1.0),
166            interaction: complexity * difficulty,
167            complexity_sq: complexity * complexity,
168            difficulty_sq: difficulty * difficulty,
169            strategy_conservative: conservative / strategy_total,
170            strategy_aggressive: aggressive / strategy_total,
171            strategy_exploratory: exploratory / strategy_total,
172        }
173    }
174
175    pub fn predict_improvement(
176        &mut self,
177        landscape: &LandscapeAnalysis<T>,
178        adaptation: &ArchitectureAdaptation<T>,
179    ) -> Result<PerformancePrediction<T>> {
180        let features = Self::extract_features(landscape, adaptation);
181        let key = features.cache_key();
182        let output = match self.prediction_cache.get(&key) {
183            Some(cached) => cached,
184            None => {
185                let fresh = self.predictor_network.predict(&features)?;
186                self.prediction_cache.insert(key, fresh.clone());
187                fresh
188            }
189        };
190
191        let cast =
192            |v: f64| -> T { scirs2_core::numeric::NumCast::from(v).unwrap_or_else(|| T::zero()) };
193        Ok(PerformancePrediction {
194            convergence_improvement: cast(output.convergence_improvement),
195            final_performance: cast(output.final_performance),
196            confidence: cast(output.confidence),
197            uncertainty: cast(output.uncertainty),
198        })
199    }
200}