1use crate::{amacp::AMacPConfig, novograd::NovoGradConfig};
23use scirs2_core::random::*; use serde::{Deserialize, Serialize};
26use std::collections::HashMap;
27use std::time::{Duration, Instant};
28use trustformers_core::errors::{Result, TrustformersError};
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct HyperparameterSpace {
33 pub learning_rate: (f32, f32),
35 pub beta1: (f32, f32),
37 pub beta2: (f32, f32),
39 pub weight_decay: (f32, f32),
41 pub epsilon: (f32, f32),
43 pub batch_sizes: Vec<usize>,
45 pub log_scale_lr: bool,
47 pub custom_params: HashMap<String, (f32, f32)>,
49}
50
51impl Default for HyperparameterSpace {
52 fn default() -> Self {
53 Self {
54 learning_rate: (1e-5, 1e-1),
55 beta1: (0.8, 0.999),
56 beta2: (0.9, 0.9999),
57 weight_decay: (0.0, 1e-1),
58 epsilon: (1e-10, 1e-6),
59 batch_sizes: vec![16, 32, 64, 128, 256],
60 log_scale_lr: true,
61 custom_params: HashMap::new(),
62 }
63 }
64}
65
66impl HyperparameterSpace {
67 pub fn for_transformers() -> Self {
69 Self {
70 learning_rate: (1e-5, 5e-3),
71 beta1: (0.85, 0.95),
72 beta2: (0.95, 0.999),
73 weight_decay: (1e-3, 1e-1),
74 epsilon: (1e-8, 1e-6),
75 batch_sizes: vec![32, 64, 128, 256],
76 log_scale_lr: true,
77 custom_params: [
78 ("warmup_steps".to_string(), (1000.0, 10000.0)),
79 ("max_grad_norm".to_string(), (0.5, 2.0)),
80 ]
81 .into_iter()
82 .collect(),
83 }
84 }
85
86 pub fn for_vision() -> Self {
88 Self {
89 learning_rate: (1e-4, 1e-1),
90 beta1: (0.9, 0.99),
91 beta2: (0.999, 0.9999),
92 weight_decay: (1e-5, 1e-2),
93 epsilon: (1e-8, 1e-6),
94 batch_sizes: vec![16, 32, 64, 128],
95 log_scale_lr: true,
96 custom_params: HashMap::new(),
97 }
98 }
99
100 pub fn for_scientific_computing() -> Self {
102 Self {
103 learning_rate: (1e-6, 1e-2),
104 beta1: (0.95, 0.999),
105 beta2: (0.999, 0.9999),
106 weight_decay: (0.0, 1e-4),
107 epsilon: (1e-12, 1e-8),
108 batch_sizes: vec![32, 64, 128],
109 log_scale_lr: true,
110 custom_params: [("precision_threshold".to_string(), (1e-8, 1e-6))]
111 .into_iter()
112 .collect(),
113 }
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct HyperparameterSample {
120 pub learning_rate: f32,
121 pub beta1: f32,
122 pub beta2: f32,
123 pub weight_decay: f32,
124 pub epsilon: f32,
125 pub batch_size: usize,
126 pub custom_params: HashMap<String, f32>,
127 pub performance_score: Option<f32>,
129 pub training_time: Option<f32>,
131 pub memory_usage: Option<usize>,
133}
134
135#[derive(Debug, Clone)]
137pub struct OptimizationTask {
138 pub name: String,
139 pub model_size: usize,
140 pub dataset_size: usize,
141 pub max_epochs: usize,
142 pub convergence_threshold: f32,
143 pub target_metric: String,
144 pub task_type: TaskType,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub enum TaskType {
149 Classification,
150 Regression,
151 LanguageModeling,
152 ComputerVision,
153 ScientificComputing,
154 Reinforcement,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct PerformanceMetrics {
160 pub final_loss: f32,
161 pub convergence_epoch: usize,
162 pub training_time: Duration,
163 pub memory_peak: usize,
164 pub stability_score: f32,
165 pub throughput: f32, pub gradient_norm_variance: f32,
167 pub composite_score: f32,
168}
169
170#[derive(Debug)]
172pub struct BayesianOptimizer {
173 space: HyperparameterSpace,
174 samples: Vec<HyperparameterSample>,
175 good_samples: Vec<HyperparameterSample>,
176 poor_samples: Vec<HyperparameterSample>,
177 performance_threshold: f32,
178 #[allow(dead_code)]
179 exploration_factor: f32,
180 n_startup_trials: usize,
181 gamma: f32, }
183
184impl BayesianOptimizer {
185 pub fn new(space: HyperparameterSpace) -> Self {
186 Self {
187 space,
188 samples: Vec::new(),
189 good_samples: Vec::new(),
190 poor_samples: Vec::new(),
191 performance_threshold: 0.0,
192 exploration_factor: 0.25,
193 n_startup_trials: 20,
194 gamma: 0.25,
195 }
196 }
197
198 pub fn suggest(&mut self) -> HyperparameterSample {
200 if self.samples.len() < self.n_startup_trials {
201 self.random_sample()
203 } else {
204 self.tpe_sample()
206 }
207 }
208
209 pub fn update(&mut self, mut sample: HyperparameterSample, performance: f32) {
211 sample.performance_score = Some(performance);
212
213 let mut performances: Vec<f32> =
215 self.samples.iter().filter_map(|s| s.performance_score).collect();
216 performances.push(performance);
217 performances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
218
219 if !performances.is_empty() {
220 self.performance_threshold = performances[performances.len() / 2];
221 }
222
223 if performance > self.performance_threshold {
225 self.good_samples.push(sample.clone());
226 } else {
227 self.poor_samples.push(sample.clone());
228 }
229
230 self.samples.push(sample);
231
232 if self.good_samples.len() > 1 {
234 self.good_samples.sort_by(|a, b| {
235 b.performance_score
236 .unwrap_or(0.0)
237 .partial_cmp(&a.performance_score.unwrap_or(0.0))
238 .unwrap_or(std::cmp::Ordering::Equal)
239 });
240 let keep_count = ((self.samples.len() as f32 * self.gamma).ceil() as usize).max(1);
241 self.good_samples.truncate(keep_count);
242 }
243 }
244
245 fn random_sample(&self) -> HyperparameterSample {
246 let mut rng = thread_rng();
248
249 let learning_rate = if self.space.log_scale_lr {
250 let log_min = self.space.learning_rate.0.ln();
251 let log_max = self.space.learning_rate.1.ln();
252 (rng.random::<f32>() * (log_max - log_min) + log_min).exp()
253 } else {
254 rng.random_range(self.space.learning_rate.0..=self.space.learning_rate.1)
255 };
256
257 HyperparameterSample {
258 learning_rate,
259 beta1: rng.random_range(self.space.beta1.0..=self.space.beta1.1),
260 beta2: rng.random_range(self.space.beta2.0..=self.space.beta2.1),
261 weight_decay: rng.random_range(self.space.weight_decay.0..=self.space.weight_decay.1),
262 epsilon: rng.random_range(self.space.epsilon.0..=self.space.epsilon.1),
263 batch_size: {
264 let idx = rng.random_range(0..self.space.batch_sizes.len());
265 self.space.batch_sizes[idx]
266 },
267 custom_params: self
268 .space
269 .custom_params
270 .iter()
271 .map(|(k, &(min, max))| (k.clone(), rng.random_range(min..=max)))
272 .collect(),
273 performance_score: None,
274 training_time: None,
275 memory_usage: None,
276 }
277 }
278
279 fn tpe_sample(&self) -> HyperparameterSample {
280 let mut rng = thread_rng();
284
285 if self.good_samples.is_empty() {
286 return self.random_sample();
287 }
288
289 let idx = rng.random_range(0..self.good_samples.len());
291 let good_sample = &self.good_samples[idx];
292 let noise_factor = 0.1;
293
294 let learning_rate = if self.space.log_scale_lr {
295 let log_lr = good_sample.learning_rate.ln();
296 let noise = rng.random_range(-noise_factor..=noise_factor);
297 (log_lr + noise)
298 .exp()
299 .clamp(self.space.learning_rate.0, self.space.learning_rate.1)
300 } else {
301 let noise = rng.random_range(-noise_factor..=noise_factor)
302 * (self.space.learning_rate.1 - self.space.learning_rate.0);
303 (good_sample.learning_rate + noise)
304 .clamp(self.space.learning_rate.0, self.space.learning_rate.1)
305 };
306
307 HyperparameterSample {
308 learning_rate,
309 beta1: (good_sample.beta1 + rng.random_range(-0.01..=0.01))
310 .clamp(self.space.beta1.0, self.space.beta1.1),
311 beta2: (good_sample.beta2 + rng.random_range(-0.001..=0.001))
312 .clamp(self.space.beta2.0, self.space.beta2.1),
313 weight_decay: (good_sample.weight_decay
314 + rng.random_range(-noise_factor..=noise_factor)
315 * (self.space.weight_decay.1 - self.space.weight_decay.0))
316 .clamp(self.space.weight_decay.0, self.space.weight_decay.1),
317 epsilon: good_sample.epsilon,
318 batch_size: good_sample.batch_size,
319 custom_params: good_sample.custom_params.clone(),
320 performance_score: None,
321 training_time: None,
322 memory_usage: None,
323 }
324 }
325
326 pub fn get_best(&self) -> Option<&HyperparameterSample> {
328 self.samples.iter().filter(|s| s.performance_score.is_some()).max_by(|a, b| {
329 a.performance_score
331 .unwrap_or(0.0)
332 .partial_cmp(&b.performance_score.unwrap_or(0.0))
333 .unwrap_or(std::cmp::Ordering::Equal)
334 })
335 }
336}
337
338#[derive(Debug)]
340pub struct MultiObjectiveOptimizer {
341 bayesian_opt: BayesianOptimizer,
342 #[allow(dead_code)]
343 objectives: Vec<String>,
344 weights: Vec<f32>,
345 pareto_front: Vec<HyperparameterSample>,
346}
347
348impl MultiObjectiveOptimizer {
349 pub fn new(space: HyperparameterSpace, objectives: Vec<String>, weights: Vec<f32>) -> Self {
350 assert_eq!(
351 objectives.len(),
352 weights.len(),
353 "Objectives and weights must have same length"
354 );
355
356 Self {
357 bayesian_opt: BayesianOptimizer::new(space),
358 objectives,
359 weights,
360 pareto_front: Vec::new(),
361 }
362 }
363
364 pub fn update_multi_objective(
366 &mut self,
367 sample: HyperparameterSample,
368 metrics: &PerformanceMetrics,
369 ) {
370 let mut weighted_score = 0.0;
372 weighted_score += self.weights[0] * (1.0 / (1.0 + metrics.final_loss)); weighted_score += self.weights[1] * (1.0 / (1.0 + metrics.convergence_epoch as f32)); if self.weights.len() > 2 {
375 weighted_score += self.weights[2] * metrics.stability_score; }
377 if self.weights.len() > 3 {
378 weighted_score += self.weights[3] * (1.0 / (1.0 + metrics.training_time.as_secs_f32()));
379 }
381
382 self.bayesian_opt.update(sample, weighted_score);
383 self.update_pareto_front();
384 }
385
386 fn update_pareto_front(&mut self) {
387 self.pareto_front.clear();
389
390 for sample in &self.bayesian_opt.samples {
391 if let Some(sample_score) = sample.performance_score {
392 let mut is_dominated = false;
393
394 for other in &self.bayesian_opt.samples {
395 if let Some(other_score) = other.performance_score {
396 if other_score > sample_score {
397 is_dominated = true;
398 break;
399 }
400 }
401 }
402
403 if !is_dominated {
404 self.pareto_front.push(sample.clone());
405 }
406 }
407 }
408 }
409}
410
411#[derive(Debug)]
413pub struct HyperparameterTuner {
414 optimizer_type: OptimizerType,
415 search_space: HyperparameterSpace,
416 bayesian_opt: BayesianOptimizer,
417 multi_objective_opt: Option<MultiObjectiveOptimizer>,
418 task: OptimizationTask,
419 max_trials: usize,
420 current_trial: usize,
421 best_config: Option<HyperparameterSample>,
422 optimization_history: Vec<(HyperparameterSample, PerformanceMetrics)>,
423}
424
425#[derive(Debug, Clone)]
426pub enum OptimizerType {
427 Adam,
428 AdamW,
429 AMacP,
430 NovoGrad,
431 AveragedAdam,
432 Lion,
433 LAMB,
434}
435
436impl HyperparameterTuner {
437 pub fn new(
439 optimizer_type: OptimizerType,
440 search_space: HyperparameterSpace,
441 task: OptimizationTask,
442 max_trials: usize,
443 ) -> Self {
444 let bayesian_opt = BayesianOptimizer::new(search_space.clone());
445
446 Self {
447 optimizer_type,
448 search_space,
449 bayesian_opt,
450 multi_objective_opt: None,
451 task,
452 max_trials,
453 current_trial: 0,
454 best_config: None,
455 optimization_history: Vec::new(),
456 }
457 }
458
459 pub fn enable_multi_objective(&mut self, objectives: Vec<String>, weights: Vec<f32>) {
461 self.multi_objective_opt = Some(MultiObjectiveOptimizer::new(
462 self.search_space.clone(),
463 objectives,
464 weights,
465 ));
466 }
467
468 pub fn suggest_next(&mut self) -> Option<HyperparameterSample> {
470 if self.current_trial >= self.max_trials {
471 return None;
472 }
473
474 self.current_trial += 1;
475 Some(self.bayesian_opt.suggest())
476 }
477
478 pub fn evaluate_config(&mut self, config: HyperparameterSample) -> Result<PerformanceMetrics> {
480 let _start_time = Instant::now();
481
482 let metrics = self.simulate_training(&config)?;
484
485 if let Some(ref mut multi_opt) = self.multi_objective_opt {
487 multi_opt.update_multi_objective(config.clone(), &metrics);
488 } else {
489 self.bayesian_opt.update(config.clone(), metrics.composite_score);
490 }
491
492 let current_best_score =
494 self.best_config.as_ref().and_then(|c| c.performance_score).unwrap_or(0.0);
495 if self.best_config.is_none() || metrics.composite_score > current_best_score {
496 let mut best_config = config.clone();
497 best_config.performance_score = Some(metrics.composite_score);
498 self.best_config = Some(best_config);
499 }
500
501 self.optimization_history.push((config, metrics.clone()));
502 Ok(metrics)
503 }
504
505 fn simulate_training(&self, config: &HyperparameterSample) -> Result<PerformanceMetrics> {
506 let mut rng = thread_rng();
508
509 let lr_factor = if config.learning_rate > 1e-2 {
511 0.7_f64 } else if config.learning_rate < 1e-5 {
513 0.8_f64 } else {
515 1.0_f64 };
517
518 let momentum_factor = if config.beta1 > 0.95 { 0.9_f64 } else { 1.0_f64 };
520 let variance_factor = if config.beta2 < 0.99 { 0.85_f64 } else { 1.0_f64 };
521
522 let regularization_factor = if config.weight_decay > 1e-2 { 0.8_f64 } else { 1.0_f64 };
524
525 let base_performance = 0.8_f64;
526 let noise = rng.random_range(-0.1_f64..=0.1_f64);
527 let final_loss = (1.0_f64
528 - base_performance
529 * lr_factor
530 * momentum_factor
531 * variance_factor
532 * regularization_factor
533 + noise)
534 .max(0.01_f64);
535
536 let convergence_epoch = (50.0 / lr_factor) as usize;
537 let training_time = Duration::from_secs((convergence_epoch as f32 * 0.1) as u64);
538 let memory_peak = (config.batch_size * 1024 * 1024) + rng.random_range(0..1024 * 1024);
539
540 let stability_score = momentum_factor * variance_factor;
541 let throughput =
542 (config.batch_size as f32) / (training_time.as_secs_f32() / convergence_epoch as f32);
543 let gradient_norm_variance = rng.random_range(0.01..=0.5);
544
545 let composite_score = (1.0_f64 / final_loss) * 0.4_f64
547 + (1.0_f64 / convergence_epoch as f64) * 0.3_f64
548 + stability_score * 0.2_f64
549 + (throughput as f64 / 1000.0_f64).min(1.0_f64) * 0.1_f64;
550
551 Ok(PerformanceMetrics {
552 final_loss: final_loss as f32,
553 convergence_epoch,
554 training_time,
555 memory_peak,
556 stability_score: stability_score as f32,
557 throughput,
558 gradient_norm_variance,
559 composite_score: composite_score as f32,
560 })
561 }
562
563 pub fn optimize(&mut self) -> Result<HyperparameterSample> {
565 println!(
566 "š Starting hyperparameter optimization for {:?}",
567 self.optimizer_type
568 );
569 println!(
570 "š Task: {} (max {} trials)",
571 self.task.name, self.max_trials
572 );
573
574 let mut trial_results = Vec::new();
575
576 while let Some(config) = self.suggest_next() {
577 println!("\nš Trial {}/{}", self.current_trial, self.max_trials);
578 println!(
579 " LR: {:.2e}, βā: {:.3}, βā: {:.4}, WD: {:.2e}",
580 config.learning_rate, config.beta1, config.beta2, config.weight_decay
581 );
582
583 let metrics = self.evaluate_config(config.clone())?;
584 trial_results.push((config, metrics.clone()));
585
586 println!(
587 " š Score: {:.4}, Loss: {:.4}, Epochs: {}, Time: {:.1}s",
588 metrics.composite_score,
589 metrics.final_loss,
590 metrics.convergence_epoch,
591 metrics.training_time.as_secs_f32()
592 );
593
594 if metrics.composite_score > 0.95 {
596 println!("šÆ Early stopping - excellent configuration found!");
597 break;
598 }
599 }
600
601 self.print_optimization_summary();
602
603 self.best_config.clone().ok_or_else(|| {
604 TrustformersError::new(trustformers_core::errors::ErrorKind::InvalidConfiguration {
605 field: "hyperparameter_optimization".to_string(),
606 reason: "No valid configuration found".to_string(),
607 })
608 })
609 }
610
611 fn print_optimization_summary(&self) {
612 println!("\nš Hyperparameter Optimization Summary");
613 println!("=====================================");
614
615 if let Some(ref best) = self.best_config {
616 println!("š Best Configuration Found:");
617 println!(" Learning Rate: {:.2e}", best.learning_rate);
618 println!(" Beta1: {:.4}", best.beta1);
619 println!(" Beta2: {:.4}", best.beta2);
620 println!(" Weight Decay: {:.2e}", best.weight_decay);
621 println!(" Batch Size: {}", best.batch_size);
622 println!(
623 " Performance Score: {:.4}",
624 best.performance_score.unwrap_or(0.0)
625 );
626 }
627
628 println!("\nš Optimization Statistics:");
629 println!(" Total Trials: {}", self.optimization_history.len());
630
631 if !self.optimization_history.is_empty() {
632 let scores: Vec<f32> =
633 self.optimization_history.iter().map(|(_, m)| m.composite_score).collect();
634 let avg_score = scores.iter().sum::<f32>() / scores.len() as f32;
635 let max_score = scores.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
636 let min_score = scores.iter().fold(f32::INFINITY, |a, &b| a.min(b));
637
638 println!(" Average Score: {:.4}", avg_score);
639 println!(" Score Range: {:.4} - {:.4}", min_score, max_score);
640 println!(
641 " Improvement: {:.1}%",
642 ((max_score - min_score) / min_score * 100.0).max(0.0)
643 );
644 }
645 }
646
647 pub fn get_history(&self) -> &[(HyperparameterSample, PerformanceMetrics)] {
649 &self.optimization_history
650 }
651
652 pub fn get_pareto_front(&self) -> Option<&[HyperparameterSample]> {
654 self.multi_objective_opt.as_ref().map(|opt| opt.pareto_front.as_slice())
655 }
656}
657
658impl HyperparameterTuner {
660 pub fn optimize_amacp_for_transformers(max_trials: usize) -> Result<AMacPConfig> {
662 let space = HyperparameterSpace::for_transformers();
663 let task = OptimizationTask {
664 name: "Transformer Language Modeling".to_string(),
665 model_size: 125_000_000, dataset_size: 1_000_000,
667 max_epochs: 100,
668 convergence_threshold: 0.01,
669 target_metric: "perplexity".to_string(),
670 task_type: TaskType::LanguageModeling,
671 };
672
673 let mut tuner = HyperparameterTuner::new(OptimizerType::AMacP, space, task, max_trials);
674
675 let best_config = tuner.optimize()?;
676
677 Ok(AMacPConfig {
678 learning_rate: best_config.learning_rate,
679 beta1: best_config.beta1,
680 beta2: best_config.beta2,
681 weight_decay: best_config.weight_decay,
682 epsilon: best_config.epsilon,
683 ..AMacPConfig::for_transformers()
684 })
685 }
686
687 pub fn optimize_novograd_for_llms(max_trials: usize) -> Result<NovoGradConfig> {
689 let space = HyperparameterSpace::for_transformers();
690 let task = OptimizationTask {
691 name: "Large Language Model Training".to_string(),
692 model_size: 1_000_000_000, dataset_size: 10_000_000,
694 max_epochs: 50,
695 convergence_threshold: 0.005,
696 target_metric: "loss".to_string(),
697 task_type: TaskType::LanguageModeling,
698 };
699
700 let mut tuner = HyperparameterTuner::new(OptimizerType::NovoGrad, space, task, max_trials);
701
702 let best_config = tuner.optimize()?;
703
704 Ok(NovoGradConfig {
705 learning_rate: best_config.learning_rate,
706 beta1: best_config.beta1,
707 beta2: best_config.beta2,
708 weight_decay: best_config.weight_decay,
709 epsilon: best_config.epsilon,
710 ..NovoGradConfig::for_large_language_models()
711 })
712 }
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718
719 #[test]
720 fn test_hyperparameter_space_creation() {
721 let space = HyperparameterSpace::default();
722 assert_eq!(space.learning_rate, (1e-5, 1e-1));
723 assert!(space.log_scale_lr);
724
725 let transformer_space = HyperparameterSpace::for_transformers();
726 assert!(transformer_space.custom_params.contains_key("warmup_steps"));
727 }
728
729 #[test]
730 fn test_bayesian_optimizer_suggestion() {
731 let space = HyperparameterSpace::default();
732 let mut optimizer = BayesianOptimizer::new(space);
733
734 let sample = optimizer.suggest();
735 assert!(sample.learning_rate >= 1e-5 && sample.learning_rate <= 1e-1);
736 assert!(sample.beta1 >= 0.8 && sample.beta1 <= 0.999);
737 }
738
739 #[test]
740 fn test_bayesian_optimizer_update() {
741 let space = HyperparameterSpace::default();
742 let mut optimizer = BayesianOptimizer::new(space);
743
744 let sample = optimizer.suggest();
745 optimizer.update(sample, 0.85);
746
747 assert_eq!(optimizer.samples.len(), 1);
748 assert!(optimizer.get_best().is_some());
749 }
750
751 #[test]
752 fn test_hyperparameter_tuner_creation() {
753 let space = HyperparameterSpace::for_vision();
754 let task = OptimizationTask {
755 name: "Test Task".to_string(),
756 model_size: 1000,
757 dataset_size: 10000,
758 max_epochs: 10,
759 convergence_threshold: 0.01,
760 target_metric: "accuracy".to_string(),
761 task_type: TaskType::Classification,
762 };
763
764 let tuner = HyperparameterTuner::new(OptimizerType::Adam, space, task, 50);
765
766 assert_eq!(tuner.max_trials, 50);
767 assert_eq!(tuner.current_trial, 0);
768 }
769
770 #[test]
771 fn test_multi_objective_optimizer() {
772 let space = HyperparameterSpace::default();
773 let objectives = vec!["accuracy".to_string(), "speed".to_string()];
774 let weights = vec![0.7, 0.3];
775
776 let mut optimizer = MultiObjectiveOptimizer::new(space, objectives, weights);
777
778 let sample = HyperparameterSample {
779 learning_rate: 1e-3,
780 beta1: 0.9,
781 beta2: 0.999,
782 weight_decay: 1e-4,
783 epsilon: 1e-8,
784 batch_size: 64,
785 custom_params: HashMap::new(),
786 performance_score: None,
787 training_time: None,
788 memory_usage: None,
789 };
790
791 let metrics = PerformanceMetrics {
792 final_loss: 0.1,
793 convergence_epoch: 25,
794 training_time: Duration::from_secs(120),
795 memory_peak: 1024 * 1024,
796 stability_score: 0.9,
797 throughput: 1000.0,
798 gradient_norm_variance: 0.1,
799 composite_score: 0.85,
800 };
801
802 optimizer.update_multi_objective(sample, &metrics);
803 assert!(!optimizer.pareto_front.is_empty());
804 }
805
806 #[test]
807 fn test_performance_metrics_calculation() {
808 let space = HyperparameterSpace::default();
809 let task = OptimizationTask {
810 name: "Test".to_string(),
811 model_size: 1000,
812 dataset_size: 1000,
813 max_epochs: 10,
814 convergence_threshold: 0.01,
815 target_metric: "loss".to_string(),
816 task_type: TaskType::Regression,
817 };
818
819 let tuner = HyperparameterTuner::new(OptimizerType::Adam, space, task, 10);
820
821 let config = HyperparameterSample {
822 learning_rate: 1e-3,
823 beta1: 0.9,
824 beta2: 0.999,
825 weight_decay: 0.0,
826 epsilon: 1e-8,
827 batch_size: 32,
828 custom_params: HashMap::new(),
829 performance_score: None,
830 training_time: None,
831 memory_usage: None,
832 };
833
834 let metrics = tuner.simulate_training(&config);
835 assert!(metrics.is_ok());
836
837 let metrics = metrics.expect("Operation failed in test");
838 assert!(metrics.final_loss >= 0.0);
839 assert!(metrics.convergence_epoch > 0);
840 assert!(metrics.composite_score > 0.0);
841 }
842
843 #[test]
844 fn test_convenience_optimization_functions() {
845 let result = HyperparameterTuner::optimize_amacp_for_transformers(5);
848 assert!(result.is_ok());
849
850 let result = HyperparameterTuner::optimize_novograd_for_llms(5);
851 assert!(result.is_ok());
852 }
853}