//! Merge planning and strategy recommendation system use crate::algorithms::MergeUtils; use crate::config::MergeStrategy; use crate::error::{MergeError, Result}; use crate::strategies::StrategyUtils; use crate::types::{Model, ModelMetadata}; use std::collections::HashMap; use tracing::{debug, info}; /// Model merge planner and strategy recommender pub struct MergePlanner { compatibility_cache: HashMap, } impl MergePlanner { /// Create a new merge planner pub fn new() -> Self { Self { compatibility_cache: HashMap::new(), } } /// Recommend the best merge strategy for given models pub fn recommend_strategy(&self, models: &[ModelMetadata]) -> Result { if models.len() < 2 { return Err(MergeError::algorithm( "MergePlanner", "At least 2 models required for strategy recommendation", )); } info!( "Analyzing {} models for strategy recommendation", models.len() ); // Analyze model characteristics let analysis = self.analyze_models(models)?; // Generate strategy recommendation based on analysis let recommended_strategy = self.select_optimal_strategy(&analysis)?; info!("Recommended strategy: {:?}", recommended_strategy); Ok(recommended_strategy) } /// Analyze model compatibility and characteristics pub fn analyze_compatibility(&self, models: &[Model]) -> Result { let mut report = CompatibilityReport::new(); if models.len() < 2 { report.overall_compatible = true; return Ok(report); } // Check pairwise compatibility for i in 0..models.len() { for j in (i + 1)..models.len() { let compatibility = self.check_model_pair_compatibility(&models[i], &models[j])?; if !compatibility.compatible { report.overall_compatible = false; report .compatibility_issues .extend(compatibility.issues.clone()); } report.pairwise_compatibility.push(compatibility); } } // Compute similarity matrix report.similarity_matrix = StrategyUtils::compute_pairwise_similarities(models)?; // Analyze diversity let all_indices: Vec = (0..models.len()).collect(); report.diversity_score = StrategyUtils::compute_group_diversity(models, &all_indices)?; // Estimate resource requirements report.memory_requirements = self.estimate_merge_memory_requirements(models)?; info!( "Compatibility analysis: {}/{} compatible, diversity: {:.3}", report .pairwise_compatibility .iter() .filter(|c| c.compatible) .count(), report.pairwise_compatibility.len(), report.diversity_score ); Ok(report) } /// Plan merge execution with resource constraints pub fn plan_merge_execution( &self, models: &[Model], strategy: &MergeStrategy, constraints: &ResourceConstraints, ) -> Result { info!("Planning merge execution for {} models", models.len()); let mut plan = ExecutionPlan::new(); // Validate resource constraints self.validate_resource_constraints(models, strategy, constraints)?; // Determine execution phases plan.phases = self.plan_execution_phases(models, strategy, constraints)?; // Estimate execution time plan.estimated_duration_ms = self.estimate_execution_time(models, strategy)?; // Identify potential bottlenecks plan.bottlenecks = self.identify_bottlenecks(models, strategy, constraints)?; // Generate checkpoints plan.checkpoints = self.plan_checkpoints(models, strategy)?; // Risk assessment plan.risks = self.assess_execution_risks(models, strategy)?; info!( "Execution plan created: {} phases, estimated duration: {}ms", plan.phases.len(), plan.estimated_duration_ms ); Ok(plan) } /// Analyze models for strategy selection fn analyze_models(&self, models: &[ModelMetadata]) -> Result { let mut analysis = ModelAnalysis::new(); // Compute model statistics for model in models { let stats = ModelStats { size_mb: model.size_bytes.unwrap_or(0) / (1024 * 1024), complexity_score: self.estimate_model_complexity(model), similarity_to_others: 0.0, // Will be computed below training_quality: model.metrics.get("accuracy").copied().unwrap_or(0.5), }; analysis.model_stats.push(stats); } // Compute average model size analysis.avg_model_size_mb = analysis .model_stats .iter() .map(|s| s.size_mb) .sum::() as f32 / models.len() as f32; // Compute diversity (placeholder - would need actual model data) analysis.diversity_score = 0.6; // Placeholder // Determine merge complexity analysis.merge_complexity = self.assess_merge_complexity(models); // Check for special requirements analysis.special_requirements = self.identify_special_requirements(models); Ok(analysis) } /// Select optimal merge strategy based on analysis fn select_optimal_strategy(&self, analysis: &ModelAnalysis) -> Result { debug!( "Selecting strategy for analysis: {:?}", analysis.merge_complexity ); match analysis.merge_complexity { MergeComplexity::Simple => { // For simple merges, use efficient uniform averaging if analysis.model_stats.len() > 5 { Ok(MergeStrategy::ModelSoup(crate::config::ModelSoupConfig { soup_method: crate::config::SoupMethod::Uniform, max_models: 10, ..Default::default() })) } else { Ok(MergeStrategy::Slerp(crate::config::SlerpConfig { t: 0.5, normalization: crate::config::NormalizationMethod::L2, ..Default::default() })) } } MergeComplexity::Moderate => { // For moderate complexity, consider parameter conflicts if analysis.diversity_score > 0.7 { Ok(MergeStrategy::Ties(crate::config::TiesConfig { sign_threshold: 0.6, magnitude_threshold: 0.1, density: 0.8, ..Default::default() })) } else { Ok(MergeStrategy::TaskArithmetic( crate::config::TaskArithmeticConfig { scaling_factors: vec![1.0; analysis.model_stats.len() - 1], preserve_signs: true, ..Default::default() }, )) } } MergeComplexity::Complex => { // For complex merges, use sophisticated methods if analysis.avg_model_size_mb > 1000.0 { Ok(MergeStrategy::Dare(crate::config::DareConfig { drop_probability: 0.1, adaptive_dropping: true, importance_threshold: 0.05, ..Default::default() })) } else { Ok(MergeStrategy::Fisher(crate::config::FisherConfig { fisher_method: crate::config::FisherMethod::Empirical, diagonal_fisher: true, num_samples: 1000, ..Default::default() })) } } MergeComplexity::VeryComplex => { // For very complex scenarios, use progressive merging Ok(MergeStrategy::Progressive( crate::config::ProgressiveConfig { num_steps: 10, validation_frequency: 2, early_stopping_patience: Some(3), enable_rollback: true, ..Default::default() }, )) } } } /// Check compatibility between two models fn check_model_pair_compatibility( &self, model1: &Model, model2: &Model, ) -> Result { let mut compatibility = PairwiseCompatibility { model1_name: model1.name.clone(), model2_name: model2.name.clone(), compatible: true, similarity_score: 0.0, issues: Vec::new(), }; // Check architecture compatibility if model1.architecture != model2.architecture { compatibility.compatible = false; compatibility.issues.push(format!( "Architecture mismatch: {} vs {}", model1.architecture.arch_type, model2.architecture.arch_type )); } // Check parameter compatibility if model1.parameters.len() != model2.parameters.len() { compatibility.compatible = false; compatibility.issues.push(format!( "Parameter count mismatch: {} vs {}", model1.parameters.len(), model2.parameters.len() )); } else { // Check individual parameters for (name, param1) in &model1.parameters { if let Some(param2) = model2.parameters.get(name) { if param1.shape != param2.shape { compatibility.compatible = false; compatibility.issues.push(format!( "Parameter shape mismatch in {}: {:?} vs {:?}", name, param1.shape, param2.shape )); } } else { compatibility.compatible = false; compatibility .issues .push(format!("Parameter {name} missing in model2")); } } } // Compute similarity if compatible if compatibility.compatible { compatibility.similarity_score = MergeUtils::compute_similarity(model1, model2)?; } Ok(compatibility) } /// Estimate model complexity from metadata fn estimate_model_complexity(&self, metadata: &ModelMetadata) -> f32 { let size_factor = (metadata.size_bytes.unwrap_or(0) as f32 / (100 * 1024 * 1024) as f32).min(1.0); let metric_factor = 1.0 - metadata.metrics.get("accuracy").unwrap_or(&0.5); f32::midpoint(size_factor, metric_factor) } /// Assess overall merge complexity fn assess_merge_complexity(&self, models: &[ModelMetadata]) -> MergeComplexity { let avg_size_mb = models .iter() .map(|m| m.size_bytes.unwrap_or(0) / (1024 * 1024)) .sum::() as f32 / models.len() as f32; let model_count = models.len(); match (avg_size_mb, model_count) { (size, count) if size < 100.0 && count <= 3 => MergeComplexity::Simple, (size, count) if size < 500.0 && count <= 5 => MergeComplexity::Moderate, (size, count) if size < 2000.0 || count <= 10 => MergeComplexity::Complex, _ => MergeComplexity::VeryComplex, } } /// Identify special requirements for models fn identify_special_requirements(&self, models: &[ModelMetadata]) -> Vec { let mut requirements = Vec::new(); let total_size_gb = models .iter() .map(|m| m.size_bytes.unwrap_or(0)) .sum::() as f32 / (1024.0 * 1024.0 * 1024.0); if total_size_gb > 10.0 { requirements.push("high_memory".to_string()); } if models.len() > 8 { requirements.push("parallel_processing".to_string()); } // Check for diverse architectures (would need actual architecture comparison) requirements.push("architecture_validation".to_string()); requirements } /// Validate resource constraints fn validate_resource_constraints( &self, models: &[Model], strategy: &MergeStrategy, constraints: &ResourceConstraints, ) -> Result<()> { // Memory validation if let Some(max_memory_mb) = constraints.max_memory_mb { let estimated_memory = StrategyUtils::estimate_memory_requirements(models, "merge")?; let estimated_mb = estimated_memory / (1024 * 1024); if estimated_mb > max_memory_mb { return Err(MergeError::memory(estimated_mb)); } } // Time validation if let Some(max_time_sec) = constraints.max_time_sec { let estimated_time_ms = self.estimate_execution_time(models, strategy)?; let estimated_sec = estimated_time_ms / 1000; if estimated_sec > max_time_sec as u64 { return Err(MergeError::timeout(estimated_time_ms)); } } Ok(()) } /// Plan execution phases fn plan_execution_phases( &self, models: &[Model], strategy: &MergeStrategy, _constraints: &ResourceConstraints, ) -> Result> { let mut phases = Vec::new(); // Phase 1: Preparation phases.push(ExecutionPhase { name: "Preparation".to_string(), description: "Load and validate models".to_string(), estimated_duration_ms: 5000 * models.len() as u64, memory_requirement_mb: models .iter() .map(super::types::Model::memory_size) .sum::() / (1024 * 1024), dependencies: Vec::new(), }); // Phase 2: Strategy-specific execution let merge_duration = match strategy { MergeStrategy::Ties(_) => 10000 + 2000 * models.len() as u64, MergeStrategy::Dare(_) => 15000 + 3000 * models.len() as u64, MergeStrategy::Slerp(_) => 8000 + 1500 * models.len() as u64, MergeStrategy::TaskArithmetic(_) => 12000 + 2500 * models.len() as u64, MergeStrategy::Fisher(_) => 20000 + 5000 * models.len() as u64, MergeStrategy::ModelSoup(_) => 7000 + 1000 * models.len() as u64, MergeStrategy::Frankenmerge(_) => 25000 + 4000 * models.len() as u64, MergeStrategy::Progressive(_) => 30000 + 6000 * models.len() as u64, }; phases.push(ExecutionPhase { name: "Merging".to_string(), description: format!("Execute {} merge strategy", strategy.name()), estimated_duration_ms: merge_duration, memory_requirement_mb: StrategyUtils::estimate_memory_requirements( models, strategy.name(), )? / (1024 * 1024), dependencies: vec!["Preparation".to_string()], }); // Phase 3: Validation phases.push(ExecutionPhase { name: "Validation".to_string(), description: "Validate merged model".to_string(), estimated_duration_ms: 3000, memory_requirement_mb: 100, // Minimal for validation dependencies: vec!["Merging".to_string()], }); Ok(phases) } /// Estimate execution time fn estimate_execution_time(&self, models: &[Model], strategy: &MergeStrategy) -> Result { let base_time = 10000; // 10 seconds base let model_factor = models.len() as u64 * 2000; // 2 seconds per model let param_factor = models .iter() .map(super::types::Model::parameter_count) .sum::() as u64 / 1000000; // 1ms per 1M params let strategy_multiplier = match strategy { MergeStrategy::Ties(_) => 2.0, MergeStrategy::Dare(_) => 2.5, MergeStrategy::Slerp(_) => 1.5, MergeStrategy::TaskArithmetic(_) => 2.0, MergeStrategy::Fisher(_) => 4.0, MergeStrategy::ModelSoup(_) => 1.2, MergeStrategy::Frankenmerge(_) => 3.0, MergeStrategy::Progressive(_) => 5.0, }; Ok(((base_time + model_factor + param_factor) as f32 * strategy_multiplier) as u64) } /// Estimate memory requirements for merging fn estimate_merge_memory_requirements(&self, models: &[Model]) -> Result { let total_model_memory: usize = models.iter().map(super::types::Model::memory_size).sum(); let overhead = (total_model_memory as f32 * 0.5) as usize; // 50% overhead Ok(total_model_memory + overhead) } /// Identify potential bottlenecks fn identify_bottlenecks( &self, models: &[Model], strategy: &MergeStrategy, constraints: &ResourceConstraints, ) -> Result> { let mut bottlenecks = Vec::new(); // Memory bottleneck let estimated_memory = self.estimate_merge_memory_requirements(models)?; if let Some(max_memory) = constraints.max_memory_mb && estimated_memory / (1024 * 1024) > max_memory * 9 / 10 { bottlenecks.push("Memory usage near limit".to_string()); } // CPU bottleneck for complex strategies match strategy { MergeStrategy::Fisher(_) | MergeStrategy::Progressive(_) => { bottlenecks.push("CPU-intensive strategy".to_string()); } _ => {} } // I/O bottleneck for many models if models.len() > 10 { bottlenecks.push("I/O bottleneck with many models".to_string()); } Ok(bottlenecks) } /// Plan execution checkpoints fn plan_checkpoints(&self, _models: &[Model], strategy: &MergeStrategy) -> Result> { let mut checkpoints = vec![ "Models loaded".to_string(), "Compatibility validated".to_string(), ]; match strategy { MergeStrategy::Progressive(_) => { checkpoints.extend(vec![ "Phase 1 complete".to_string(), "Phase 2 complete".to_string(), "Phase 3 complete".to_string(), ]); } _ => { checkpoints.push("Merge parameters computed".to_string()); } } checkpoints.extend(vec![ "Merge complete".to_string(), "Validation passed".to_string(), ]); Ok(checkpoints) } /// Assess execution risks fn assess_execution_risks( &self, models: &[Model], strategy: &MergeStrategy, ) -> Result> { let mut risks = Vec::new(); // Large model risk let max_model_size = models .iter() .map(super::types::Model::memory_size) .max() .unwrap_or(0); if max_model_size > 2 * 1024 * 1024 * 1024 { // 2GB risks.push("Large model may cause memory issues".to_string()); } // Strategy-specific risks match strategy { MergeStrategy::Dare(_) => { risks.push("Random dropping may affect model quality".to_string()); } MergeStrategy::Fisher(_) => { risks.push("Fisher information computation is resource-intensive".to_string()); } MergeStrategy::Progressive(_) => { risks.push("Progressive merge may fail at intermediate steps".to_string()); } _ => {} } // Model diversity risk if models.len() > 15 { risks.push("Many models may reduce merge quality".to_string()); } Ok(risks) } } // Strategy name extension impl MergeStrategy { fn name(&self) -> &'static str { match self { Self::Ties(_) => "TIES", Self::Dare(_) => "DARE", Self::Slerp(_) => "SLERP", Self::TaskArithmetic(_) => "TaskArithmetic", Self::Fisher(_) => "Fisher", Self::ModelSoup(_) => "ModelSoup", Self::Frankenmerge(_) => "Frankenmerge", Self::Progressive(_) => "Progressive", } } } /// Model analysis results #[derive(Debug)] struct ModelAnalysis { model_stats: Vec, avg_model_size_mb: f32, diversity_score: f32, merge_complexity: MergeComplexity, special_requirements: Vec, } impl ModelAnalysis { fn new() -> Self { Self { model_stats: Vec::new(), avg_model_size_mb: 0.0, diversity_score: 0.0, merge_complexity: MergeComplexity::Simple, special_requirements: Vec::new(), } } } /// Statistics for individual models #[derive(Debug)] struct ModelStats { size_mb: usize, complexity_score: f32, similarity_to_others: f32, training_quality: f32, } /// Merge complexity levels #[derive(Debug, PartialEq)] enum MergeComplexity { Simple, Moderate, Complex, VeryComplex, } /// Compatibility report for models #[derive(Debug)] pub struct CompatibilityReport { pub overall_compatible: bool, pub pairwise_compatibility: Vec, pub compatibility_issues: Vec, pub similarity_matrix: Vec>, pub diversity_score: f32, pub memory_requirements: usize, } impl CompatibilityReport { fn new() -> Self { Self { overall_compatible: true, pairwise_compatibility: Vec::new(), compatibility_issues: Vec::new(), similarity_matrix: Vec::new(), diversity_score: 0.0, memory_requirements: 0, } } } /// Pairwise model compatibility #[derive(Debug)] pub struct PairwiseCompatibility { pub model1_name: String, pub model2_name: String, pub compatible: bool, pub similarity_score: f32, pub issues: Vec, } /// Resource constraints for merge execution #[derive(Debug)] pub struct ResourceConstraints { pub max_memory_mb: Option, pub max_time_sec: Option, pub max_gpu_memory_mb: Option, pub parallel_workers: Option, } impl Default for ResourceConstraints { fn default() -> Self { Self { max_memory_mb: Some(8192), // 8GB default max_time_sec: Some(3600), // 1 hour default max_gpu_memory_mb: None, parallel_workers: Some(4), } } } /// Merge execution plan #[derive(Debug)] pub struct ExecutionPlan { pub phases: Vec, pub estimated_duration_ms: u64, pub bottlenecks: Vec, pub checkpoints: Vec, pub risks: Vec, } impl ExecutionPlan { fn new() -> Self { Self { phases: Vec::new(), estimated_duration_ms: 0, bottlenecks: Vec::new(), checkpoints: Vec::new(), risks: Vec::new(), } } } /// Execution phase description #[derive(Debug)] pub struct ExecutionPhase { pub name: String, pub description: String, pub estimated_duration_ms: u64, pub memory_requirement_mb: usize, pub dependencies: Vec, } impl Default for MergePlanner { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use crate::types::{DataType, ModelArchitecture, ParameterTensor}; fn create_test_metadata(name: &str, size_mb: usize, accuracy: f32) -> ModelMetadata { let mut metadata = ModelMetadata::default(); metadata.size_bytes = Some(size_mb * 1024 * 1024); metadata.metrics.insert("accuracy".to_string(), accuracy); metadata } fn create_test_model(name: &str) -> Model { let arch = ModelArchitecture { arch_type: "test".to_string(), num_layers: 12, hidden_dim: 768, params: std::collections::HashMap::new(), }; let mut model = Model::new(name.to_string(), arch); let param = ParameterTensor::new( "weight".to_string(), vec![768, 768], DataType::Float32, vec![0.0; 768 * 768], ); model.add_parameter(param); model } #[test] fn test_strategy_recommendation_simple() -> Result<()> { let models = vec![ create_test_metadata("model1", 50, 0.8), // Small, good model create_test_metadata("model2", 60, 0.75), // Small, good model ]; let planner = MergePlanner::new(); let strategy = planner.recommend_strategy(&models)?; // Should recommend simple strategy for small models match strategy { MergeStrategy::Slerp(_) | MergeStrategy::ModelSoup(_) => (), _ => panic!("Expected simple strategy for small models"), } Ok(()) } #[test] fn test_strategy_recommendation_complex() -> Result<()> { let models = vec![ create_test_metadata("model1", 2000, 0.9), // Large model create_test_metadata("model2", 2500, 0.85), // Large model create_test_metadata("model3", 1800, 0.88), // Large model ]; let planner = MergePlanner::new(); let strategy = planner.recommend_strategy(&models)?; // Should recommend complex strategy for large models match strategy { MergeStrategy::Dare(_) | MergeStrategy::Fisher(_) | MergeStrategy::Progressive(_) => (), _ => panic!("Expected complex strategy for large models"), } Ok(()) } #[test] fn test_compatibility_analysis() -> Result<()> { let model1 = create_test_model("model1"); let model2 = create_test_model("model2"); let models = vec![model1, model2]; let planner = MergePlanner::new(); let report = planner.analyze_compatibility(&models)?; assert!(report.overall_compatible); assert_eq!(report.pairwise_compatibility.len(), 1); assert!(report.similarity_matrix.len() == 2); assert!(report.diversity_score >= 0.0); Ok(()) } #[test] fn test_execution_planning() -> Result<()> { let models = vec![create_test_model("model1"), create_test_model("model2")]; let strategy = MergeStrategy::Ties(crate::config::TiesConfig::default()); let constraints = ResourceConstraints::default(); let planner = MergePlanner::new(); let plan = planner.plan_merge_execution(&models, &strategy, &constraints)?; assert!(!plan.phases.is_empty()); assert!(plan.estimated_duration_ms > 0); assert!(!plan.checkpoints.is_empty()); Ok(()) } #[test] fn test_resource_constraint_validation() -> Result<()> { let models = vec![create_test_model("model1")]; let strategy = MergeStrategy::Slerp(crate::config::SlerpConfig::default()); // Very restrictive constraints should fail let restrictive_constraints = ResourceConstraints { max_memory_mb: Some(1), // 1MB - too small max_time_sec: Some(1), // 1 second - too short ..Default::default() }; let planner = MergePlanner::new(); let result = planner.validate_resource_constraints(&models, &strategy, &restrictive_constraints); assert!(result.is_err()); // Should fail validation Ok(()) } #[test] fn test_merge_complexity_assessment() { let planner = MergePlanner::new(); // Simple case let simple_models = vec![create_test_metadata("model1", 50, 0.8)]; let simple_complexity = planner.assess_merge_complexity(&simple_models); assert_eq!(simple_complexity, MergeComplexity::Simple); // Complex case let complex_models = vec![ create_test_metadata("model1", 2000, 0.9), create_test_metadata("model2", 2500, 0.85), create_test_metadata("model3", 1800, 0.88), ]; let complex_complexity = planner.assess_merge_complexity(&complex_models); assert!(matches!( complex_complexity, MergeComplexity::Complex | MergeComplexity::VeryComplex )); } #[test] fn test_bottleneck_identification() -> Result<()> { let large_models: Vec = (0..15) .map(|i| create_test_model(&format!("model{}", i))) .collect(); let strategy = MergeStrategy::Fisher(crate::config::FisherConfig::default()); let constraints = ResourceConstraints { max_memory_mb: Some(100), // Very low memory limit ..Default::default() }; let planner = MergePlanner::new(); let bottlenecks = planner.identify_bottlenecks(&large_models, &strategy, &constraints)?; assert!(!bottlenecks.is_empty()); assert!( bottlenecks .iter() .any(|b| b.contains("Memory") || b.contains("CPU") || b.contains("I/O")) ); Ok(()) } }