Files
rustytorch/crates/training/rtx-model-merging/src/planning.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00

895 lines
29 KiB
Rust

//! 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<String, bool>,
}
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<MergeStrategy> {
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<CompatibilityReport> {
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<usize> = (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<ExecutionPlan> {
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<ModelAnalysis> {
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::<usize>() 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<MergeStrategy> {
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<PairwiseCompatibility> {
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::<usize>() 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<String> {
let mut requirements = Vec::new();
let total_size_gb = models
.iter()
.map(|m| m.size_bytes.unwrap_or(0))
.sum::<usize>() 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<Vec<ExecutionPhase>> {
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::<usize>()
/ (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<u64> {
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::<usize>() 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<usize> {
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<Vec<String>> {
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<Vec<String>> {
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<Vec<String>> {
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<ModelStats>,
avg_model_size_mb: f32,
diversity_score: f32,
merge_complexity: MergeComplexity,
special_requirements: Vec<String>,
}
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<PairwiseCompatibility>,
pub compatibility_issues: Vec<String>,
pub similarity_matrix: Vec<Vec<f32>>,
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<String>,
}
/// Resource constraints for merge execution
#[derive(Debug)]
pub struct ResourceConstraints {
pub max_memory_mb: Option<usize>,
pub max_time_sec: Option<usize>,
pub max_gpu_memory_mb: Option<usize>,
pub parallel_workers: Option<usize>,
}
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<ExecutionPhase>,
pub estimated_duration_ms: u64,
pub bottlenecks: Vec<String>,
pub checkpoints: Vec<String>,
pub risks: Vec<String>,
}
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<String>,
}
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<Model> = (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(())
}
}