#!/usr/bin/env rust-script //! Test for magnitude pruning optimizer integration //! Tests the complete training workflow with gradual pruning and optimizer integration use std::collections::HashMap; /// Result type for this standalone test type Result = std::result::Result>; /// Simple error type for testing #[derive(Debug)] struct TestError(String); impl std::fmt::Display for TestError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Test error: {}", self.0) } } impl std::error::Error for TestError {} // Include all the implementations from our full magnitude pruning module /// Configuration for magnitude-based pruning #[derive(Debug, Clone, PartialEq)] pub struct PruningConfig { /// Pruning strategy to apply pub strategy: PruningStrategy, /// Target sparsity ratio (0.0 to 1.0) pub sparsity_ratio: f64, /// Pruning granularity pub granularity: PruningGranularity, /// Optional pruning schedule for gradual pruning pub schedule: Option, /// Random seed for deterministic pruning pub seed: Option, } /// Strategy for selecting weights to prune #[derive(Debug, Clone, PartialEq)] pub enum PruningStrategy { /// Global magnitude pruning across all parameters Global, /// Layer-wise magnitude pruning within each layer LayerWise, /// Structured pruning (channels, filters) Structured { /// Type of structured pattern pattern_type: StructuredPattern, }, } /// Granularity of pruning operations #[derive(Debug, Clone, PartialEq)] pub enum PruningGranularity { /// Element-wise pruning (unstructured) Element, /// Channel-wise pruning Channel, /// Filter-wise pruning Filter, /// Block-wise pruning Block { height: usize, width: usize }, } /// Structured pruning patterns #[derive(Debug, Clone, PartialEq)] pub enum StructuredPattern { /// Channel pruning Channel, /// Filter pruning Filter, /// N:M sparsity pattern NM { n: usize, m: usize }, } /// Pruning schedule for gradual sparsification during training #[derive(Debug, Clone, PartialEq)] pub enum PruningSchedule { /// Linear schedule from initial to final sparsity Linear { /// Initial sparsity ratio initial_sparsity: f64, /// Final sparsity ratio final_sparsity: f64, /// Duration in training steps duration_steps: usize, }, /// Polynomial schedule Polynomial { /// Initial sparsity ratio initial_sparsity: f64, /// Final sparsity ratio final_sparsity: f64, /// Duration in training steps duration_steps: usize, /// Polynomial exponent exponent: f64, }, /// Exponential schedule Exponential { /// Initial sparsity ratio initial_sparsity: f64, /// Final sparsity ratio final_sparsity: f64, /// Duration in training steps duration_steps: usize, /// Decay rate decay_rate: f64, }, } /// Binary mask for indicating which weights to keep/prune #[derive(Debug, Clone)] pub struct PruningMask { /// Binary mask values (true = keep, false = prune) mask: Vec, /// Shape of the mask shape: Vec, /// Number of parameters kept kept_parameters: usize, /// Total number of parameters total_parameters: usize, } /// Statistics about pruning results #[derive(Debug, Clone, PartialEq)] pub struct PruningStats { /// Original parameter count pub original_params: usize, /// Parameters after pruning pub pruned_params: usize, /// Achieved sparsity ratio pub sparsity_ratio: f64, /// Compression ratio pub compression_ratio: f64, /// Memory savings (in bytes, estimated) pub memory_savings: usize, } /// Optimizer integration for pruned training #[derive(Debug, Clone)] pub struct PrunedOptimizer { /// Active pruning masks for each parameter group pub masks: HashMap, /// Update count for scheduling pub update_count: usize, /// Pruning schedule configuration pub schedule_config: Option, } /// Training state for pruned networks #[derive(Debug, Clone)] pub struct PrunedTrainingState { /// Current training step pub current_step: usize, /// Active sparsity ratio pub current_sparsity: f64, /// Accumulated pruning statistics pub stats: PruningStats, /// Whether gradual pruning is active pub gradual_pruning_active: bool, } /// Gradual pruning controller for training loops #[derive(Debug)] pub struct GradualPruningController { /// Pruning configuration config: PruningConfig, /// Current training state state: PrunedTrainingState, /// Parameter tensors being pruned parameters: HashMap, /// Current masks for each parameter masks: HashMap, } /// Main struct for magnitude-based pruning operations #[derive(Debug)] pub struct MagnitudePruning; /// Simple tensor for testing #[derive(Debug, Clone)] pub struct DenseTensor { pub data: Vec, pub shape: Vec, } impl DenseTensor { pub fn new(data: Vec, shape: Vec) -> Self { assert_eq!(data.len(), shape.iter().product::()); Self { data, shape } } pub fn shape(&self) -> &[usize] { &self.shape } pub fn data(&self) -> &[f32] { &self.data } pub fn zeros(shape: Vec) -> Self { let total_elements = shape.iter().product::(); Self { data: vec![0.0; total_elements], shape, } } /// Simulate parameter update (add gradients) pub fn update_with_gradients(&mut self, gradients: &DenseTensor, learning_rate: f32) -> Result<()> { if self.shape != gradients.shape { return Err(Box::new(TestError("Parameter and gradient shapes must match".into()))); } for (param, grad) in self.data.iter_mut().zip(gradients.data.iter()) { *param -= learning_rate * grad; } Ok(()) } } impl PruningConfig { /// Create global magnitude pruning configuration pub fn global(sparsity_ratio: f64) -> Self { Self { strategy: PruningStrategy::Global, sparsity_ratio, granularity: PruningGranularity::Element, schedule: None, seed: None, } } /// Create layer-wise magnitude pruning configuration pub fn layerwise(sparsity_ratio: f64) -> Self { Self { strategy: PruningStrategy::LayerWise, sparsity_ratio, granularity: PruningGranularity::Element, schedule: None, seed: None, } } /// Create structured magnitude pruning configuration pub fn structured(sparsity_ratio: f64, pattern: StructuredPattern) -> Self { let granularity = match pattern { StructuredPattern::Channel => PruningGranularity::Channel, StructuredPattern::Filter => PruningGranularity::Filter, StructuredPattern::NM { .. } => PruningGranularity::Element, }; Self { strategy: PruningStrategy::Structured { pattern_type: pattern, }, sparsity_ratio, granularity, schedule: None, seed: None, } } /// Add pruning schedule pub fn with_schedule(mut self, schedule: PruningSchedule) -> Self { self.schedule = Some(schedule); self } /// Validate configuration parameters pub fn validate(&self) -> Result<()> { if !(0.0..=1.0).contains(&self.sparsity_ratio) { return Err(Box::new(TestError("Sparsity ratio must be between 0.0 and 1.0".into()))); } Ok(()) } } impl PruningSchedule { /// Create linear pruning schedule pub fn linear(duration_steps: usize, final_sparsity: f64) -> Self { Self::Linear { initial_sparsity: 0.0, final_sparsity, duration_steps, } } /// Compute sparsity ratio at given training step pub fn compute_sparsity(&self, current_step: usize) -> f64 { match self { Self::Linear { initial_sparsity, final_sparsity, duration_steps } => { if current_step >= *duration_steps { return *final_sparsity; } let progress = current_step as f64 / *duration_steps as f64; initial_sparsity + progress * (final_sparsity - initial_sparsity) } Self::Polynomial { initial_sparsity, final_sparsity, duration_steps, exponent } => { if current_step >= *duration_steps { return *final_sparsity; } let progress = current_step as f64 / *duration_steps as f64; let poly_progress = progress.powf(*exponent); initial_sparsity + poly_progress * (final_sparsity - initial_sparsity) } Self::Exponential { initial_sparsity, final_sparsity, duration_steps, decay_rate } => { if current_step >= *duration_steps { return *final_sparsity; } let progress = current_step as f64 / *duration_steps as f64; let exp_progress = 1.0 - (-decay_rate * progress).exp(); initial_sparsity + exp_progress * (final_sparsity - initial_sparsity) } } } } impl PruningMask { /// Create new pruning mask pub fn new(mask: Vec, shape: Vec) -> Self { let total_parameters = shape.iter().product::(); let kept_parameters = mask.iter().filter(|&&x| x).count(); Self { mask, shape, kept_parameters, total_parameters, } } /// Get mask values pub fn mask(&self) -> &[bool] { &self.mask } /// Get mask shape pub fn shape(&self) -> &[usize] { &self.shape } /// Get sparsity ratio pub fn sparsity_ratio(&self) -> f64 { 1.0 - (self.kept_parameters as f64 / self.total_parameters as f64) } /// Apply mask to tensor (element-wise multiplication) pub fn apply(&self, tensor: &DenseTensor) -> Result { if tensor.shape != self.shape { return Err(Box::new(TestError("Tensor and mask shapes must match".into()))); } let masked_data: Vec = tensor.data.iter() .zip(self.mask.iter()) .map(|(&value, &keep)| if keep { value } else { 0.0 }) .collect(); Ok(DenseTensor::new(masked_data, tensor.shape.clone())) } /// Apply mask to gradients (used in optimizer integration) pub fn apply_to_gradients(&self, gradients: &DenseTensor) -> Result { if gradients.shape != self.shape { return Err(Box::new(TestError("Gradient and mask shapes must match".into()))); } // Zero out gradients for pruned weights let masked_gradients: Vec = gradients.data.iter() .zip(self.mask.iter()) .map(|(&grad, &keep)| if keep { grad } else { 0.0 }) .collect(); Ok(DenseTensor::new(masked_gradients, gradients.shape.clone())) } } impl PruningStats { /// Create new pruning statistics pub fn new(original_params: usize, pruned_params: usize) -> Self { let sparsity_ratio = 1.0 - (pruned_params as f64 / original_params as f64); let compression_ratio = if pruned_params == 0 { f64::INFINITY } else { original_params as f64 / pruned_params as f64 }; // Estimate memory savings (assuming 4 bytes per f32) let memory_savings = (original_params - pruned_params) * 4; Self { original_params, pruned_params, sparsity_ratio, compression_ratio, memory_savings, } } /// Check if tensor meets target sparsity threshold pub fn meets_sparsity_target(&self, target_sparsity: f64) -> bool { self.sparsity_ratio >= target_sparsity } /// Merge statistics from multiple pruning operations pub fn merge(&self, other: &PruningStats) -> Self { let total_original = self.original_params + other.original_params; let total_pruned = self.pruned_params + other.pruned_params; Self::new(total_original, total_pruned) } } impl MagnitudePruning { /// Apply magnitude-based pruning to tensor pub fn prune(tensor: &DenseTensor, config: &PruningConfig) -> Result { config.validate()?; match config.strategy { PruningStrategy::Global => { Self::prune_global(tensor, config) } PruningStrategy::LayerWise => { Self::prune_layerwise(tensor, config) } PruningStrategy::Structured { ref pattern_type } => { Self::prune_structured(tensor, config, pattern_type) } } } /// Create pruning mask without modifying the original tensor pub fn create_mask(tensor: &DenseTensor, config: &PruningConfig) -> Result { config.validate()?; match config.strategy { PruningStrategy::Global => { Self::create_global_mask(tensor, config) } PruningStrategy::LayerWise => { Self::create_layerwise_mask(tensor, config) } PruningStrategy::Structured { ref pattern_type } => { Self::create_structured_mask(tensor, config, pattern_type) } } } /// Compute importance scores based on magnitude pub fn compute_importance_scores(tensor: &DenseTensor) -> Result> { let scores: Vec = tensor.data.iter().map(|&x| x.abs()).collect(); Ok(scores) } fn prune_global(tensor: &DenseTensor, config: &PruningConfig) -> Result { let mask = Self::create_global_mask(tensor, config)?; mask.apply(tensor) } fn prune_layerwise(tensor: &DenseTensor, config: &PruningConfig) -> Result { let mask = Self::create_layerwise_mask(tensor, config)?; mask.apply(tensor) } fn prune_structured( tensor: &DenseTensor, config: &PruningConfig, _pattern: &StructuredPattern, ) -> Result { // Simplified for test Self::prune_global(tensor, config) } fn create_global_mask(tensor: &DenseTensor, config: &PruningConfig) -> Result { let total_elements = tensor.data.len(); let elements_to_keep = ((1.0 - config.sparsity_ratio) * total_elements as f64).round() as usize; // Compute importance scores (absolute magnitudes) let importance_scores = Self::compute_importance_scores(tensor)?; // Create (index, score) pairs for sorting let mut indexed_scores: Vec<(usize, f32)> = importance_scores.iter() .enumerate() .map(|(i, &score)| (i, score)) .collect(); // Sort by importance score in descending order (keep highest magnitude values) indexed_scores.sort_by(|a, b| b.1.total_cmp(&a.1)); // Create mask: true for kept elements, false for pruned elements let mut mask_data = vec![false; total_elements]; for i in 0..elements_to_keep { if i < indexed_scores.len() { let (index, _) = indexed_scores[i]; mask_data[index] = true; } } Ok(PruningMask::new(mask_data, tensor.shape.clone())) } fn create_layerwise_mask(tensor: &DenseTensor, config: &PruningConfig) -> Result { // For layer-wise pruning, we currently support 2D tensors (treating each row as a layer) if tensor.shape.len() != 2 { return Err(Box::new(TestError("Layer-wise pruning currently only supports 2D tensors".into()))); } let rows = tensor.shape[0]; let cols = tensor.shape[1]; let elements_per_row = cols; let elements_to_keep_per_row = ((1.0 - config.sparsity_ratio) * elements_per_row as f64).round() as usize; let mut mask_data = vec![false; tensor.data.len()]; // Process each row independently for row in 0..rows { let row_start = row * cols; let row_end = row_start + cols; // Get magnitudes for this row let row_scores: Vec<(usize, f32)> = (row_start..row_end) .map(|i| (i, tensor.data[i].abs())) .collect(); // Sort by magnitude in descending order let mut sorted_scores = row_scores; sorted_scores.sort_by(|a, b| b.1.total_cmp(&a.1)); // Keep the top elements_to_keep_per_row elements in this row for i in 0..elements_to_keep_per_row.min(sorted_scores.len()) { let (original_index, _) = sorted_scores[i]; mask_data[original_index] = true; } } Ok(PruningMask::new(mask_data, tensor.shape.clone())) } fn create_structured_mask( tensor: &DenseTensor, config: &PruningConfig, _pattern: &StructuredPattern, ) -> Result { // Simplified for test - use global mask Self::create_global_mask(tensor, config) } } impl PrunedOptimizer { /// Create new pruned optimizer state pub fn new() -> Self { Self { masks: HashMap::new(), update_count: 0, schedule_config: None, } } /// Create pruned optimizer with schedule pub fn with_schedule(schedule: PruningSchedule) -> Self { Self { masks: HashMap::new(), update_count: 0, schedule_config: Some(schedule), } } /// Add parameter mask for tracking pub fn add_parameter_mask(&mut self, name: &str, mask: PruningMask) { self.masks.insert(name.to_string(), mask); } /// Get current sparsity ratio based on schedule pub fn get_current_sparsity(&self) -> f64 { if let Some(ref schedule) = self.schedule_config { schedule.compute_sparsity(self.update_count) } else { // If no schedule, return average sparsity of all masks if self.masks.is_empty() { 0.0 } else { let total_sparsity: f64 = self.masks.values() .map(|mask| mask.sparsity_ratio()) .sum(); total_sparsity / self.masks.len() as f64 } } } /// Apply pruning masks to gradients before optimizer step pub fn apply_masks_to_gradients(&self, gradients: &HashMap) -> Result> { let mut masked_gradients = HashMap::new(); for (param_name, grad_tensor) in gradients { if let Some(mask) = self.masks.get(param_name) { let masked_grad = mask.apply_to_gradients(grad_tensor)?; masked_gradients.insert(param_name.clone(), masked_grad); } else { // No mask for this parameter, keep gradients as-is masked_gradients.insert(param_name.clone(), grad_tensor.clone()); } } Ok(masked_gradients) } /// Update masks based on current parameters and schedule pub fn update_masks(&mut self, parameters: &HashMap, config: &PruningConfig) -> Result<()> { let target_sparsity = if let Some(ref schedule) = self.schedule_config { schedule.compute_sparsity(self.update_count) } else { config.sparsity_ratio }; // Create updated config with current target sparsity let mut updated_config = config.clone(); updated_config.sparsity_ratio = target_sparsity; // Update masks for each parameter for (param_name, tensor) in parameters { let new_mask = MagnitudePruning::create_mask(tensor, &updated_config)?; self.masks.insert(param_name.clone(), new_mask); } self.update_count += 1; Ok(()) } /// Get pruning statistics across all parameters pub fn get_overall_stats(&self, parameters: &HashMap) -> Result { let mut total_original = 0; let mut total_pruned = 0; for (param_name, tensor) in parameters { let original_count = tensor.data.len(); total_original += original_count; if let Some(mask) = self.masks.get(param_name) { total_pruned += mask.kept_parameters; } else { // No mask means all parameters are kept total_pruned += original_count; } } Ok(PruningStats::new(total_original, total_pruned)) } } impl PrunedTrainingState { /// Create new training state pub fn new() -> Self { Self { current_step: 0, current_sparsity: 0.0, stats: PruningStats::new(0, 0), gradual_pruning_active: false, } } /// Update training state pub fn update_step(&mut self, new_stats: PruningStats, sparsity: f64) { self.current_step += 1; self.current_sparsity = sparsity; self.stats = new_stats; } /// Check if gradual pruning should be applied pub fn should_update_pruning(&self, update_frequency: usize) -> bool { self.gradual_pruning_active && (self.current_step % update_frequency == 0) } } impl GradualPruningController { /// Create new gradual pruning controller pub fn new(config: PruningConfig) -> Self { Self { config, state: PrunedTrainingState::new(), parameters: HashMap::new(), masks: HashMap::new(), } } /// Add parameter to be pruned pub fn add_parameter(&mut self, name: &str, tensor: DenseTensor) { self.parameters.insert(name.to_string(), tensor); } /// Update parameters and recompute masks if needed pub fn update_parameters(&mut self, parameters: &HashMap) -> Result<()> { // Update stored parameters for (name, tensor) in parameters { self.parameters.insert(name.clone(), tensor.clone()); } // Compute current sparsity based on schedule let current_sparsity = if let Some(ref schedule) = self.config.schedule { schedule.compute_sparsity(self.state.current_step) } else { self.config.sparsity_ratio }; // Update config with current sparsity let mut current_config = self.config.clone(); current_config.sparsity_ratio = current_sparsity; // Recompute masks for all parameters for (name, tensor) in &self.parameters { let mask = MagnitudePruning::create_mask(tensor, ¤t_config)?; self.masks.insert(name.clone(), mask); } // Update training state let overall_stats = self.compute_overall_stats(); self.state.update_step(overall_stats, current_sparsity); Ok(()) } /// Get current pruning masks pub fn get_masks(&self) -> &HashMap { &self.masks } /// Get current training state pub fn get_state(&self) -> &PrunedTrainingState { &self.state } /// Apply current masks to parameters pub fn apply_pruning(&self, parameters: &HashMap) -> Result> { let mut pruned_parameters = HashMap::new(); for (name, tensor) in parameters { if let Some(mask) = self.masks.get(name) { let pruned_tensor = mask.apply(tensor)?; pruned_parameters.insert(name.clone(), pruned_tensor); } else { // No mask, keep original pruned_parameters.insert(name.clone(), tensor.clone()); } } Ok(pruned_parameters) } /// Enable gradual pruning mode pub fn enable_gradual_pruning(&mut self) { self.state.gradual_pruning_active = true; } /// Disable gradual pruning mode pub fn disable_gradual_pruning(&mut self) { self.state.gradual_pruning_active = false; } /// Compute overall statistics fn compute_overall_stats(&self) -> PruningStats { let mut total_original = 0; let mut total_kept = 0; for (name, tensor) in &self.parameters { total_original += tensor.data.len(); if let Some(mask) = self.masks.get(name) { total_kept += mask.kept_parameters; } else { total_kept += tensor.data.len(); } } PruningStats::new(total_original, total_kept) } } /// Simulates a simple training loop with gradual pruning fn simulate_training_with_pruning( initial_parameters: &HashMap, config: PruningConfig, num_steps: usize, ) -> Result<()> { println!("=== Starting Training Simulation ==="); // Initialize gradual pruning controller let mut controller = GradualPruningController::new(config); controller.enable_gradual_pruning(); // Add parameters for (name, tensor) in initial_parameters { controller.add_parameter(name, tensor.clone()); } // Initialize optimizer with schedule let schedule = PruningSchedule::linear(num_steps, 0.8); let mut optimizer = PrunedOptimizer::with_schedule(schedule); // Current parameters (will be updated during training) let mut current_parameters = initial_parameters.clone(); println!("Initial parameters:"); for (name, tensor) in ¤t_parameters { println!(" {}: {} elements", name, tensor.data.len()); } // Training loop for step in 0..num_steps { println!("\n--- Training Step {} ---", step); // Update controller with current parameters controller.update_parameters(¤t_parameters)?; let state = controller.get_state(); println!("Current sparsity: {:.2}%", state.current_sparsity * 100.0); println!("Active parameters: {}/{}", state.stats.pruned_params, state.stats.original_params); // Update optimizer masks let base_config = PruningConfig::global(0.0); // Will be overridden by schedule optimizer.update_masks(¤t_parameters, &base_config)?; // Simulate gradients let mut gradients = HashMap::new(); for (name, tensor) in ¤t_parameters { let grad_data: Vec = tensor.data.iter() .map(|_| (rand::random::() - 0.5) * 0.01) // Small random gradients .collect(); gradients.insert(name.clone(), DenseTensor::new(grad_data, tensor.shape.clone())); } // Apply pruning masks to gradients let masked_gradients = optimizer.apply_masks_to_gradients(&gradients)?; // Update parameters with masked gradients let learning_rate = 0.01; for (name, param) in current_parameters.iter_mut() { if let Some(grad) = masked_gradients.get(name) { param.update_with_gradients(grad, learning_rate)?; } } // Apply pruning to ensure pruned weights remain zero current_parameters = controller.apply_pruning(¤t_parameters)?; // Print statistics every 10 steps if step % 10 == 0 || step == num_steps - 1 { let stats = optimizer.get_overall_stats(¤t_parameters)?; println!("Overall stats: {:.1}% sparsity, {:.1}x compression", stats.sparsity_ratio * 100.0, stats.compression_ratio); } } println!("\n=== Training Complete ==="); let final_stats = optimizer.get_overall_stats(¤t_parameters)?; println!("Final sparsity: {:.1}%", final_stats.sparsity_ratio * 100.0); println!("Final compression: {:.1}x", final_stats.compression_ratio); println!("Memory saved: {} bytes", final_stats.memory_savings); Ok(()) } // Simple random number generator for testing mod rand { use std::sync::atomic::{AtomicU32, Ordering}; static SEED: AtomicU32 = AtomicU32::new(12345); pub fn random() -> T where T: From, { let mut seed = SEED.load(Ordering::Relaxed); seed = seed.wrapping_mul(1103515245).wrapping_add(12345); SEED.store(seed, Ordering::Relaxed); let normalized = (seed as f32) / (u32::MAX as f32); T::from(normalized) } } fn main() -> Result<()> { println!("Running magnitude pruning optimizer integration tests..."); // Test 1: Basic optimizer integration println!("\n=== Test 1: Basic Optimizer Integration ==="); let mut optimizer = PrunedOptimizer::new(); // Create test parameters let mut parameters = HashMap::new(); parameters.insert("weight1".to_string(), DenseTensor::new(vec![0.1, 0.9, 0.2, 0.8], vec![4])); parameters.insert("weight2".to_string(), DenseTensor::new(vec![0.3, 0.7, 0.4, 0.6], vec![4])); // Create pruning masks let config = PruningConfig::global(0.5); // 50% sparsity for (name, tensor) in ¶meters { let mask = MagnitudePruning::create_mask(tensor, &config)?; optimizer.add_parameter_mask(name, mask); } println!("Average sparsity: {:.1}%", optimizer.get_current_sparsity() * 100.0); // Create gradients let mut gradients = HashMap::new(); gradients.insert("weight1".to_string(), DenseTensor::new(vec![0.01, 0.02, 0.03, 0.04], vec![4])); gradients.insert("weight2".to_string(), DenseTensor::new(vec![0.05, 0.06, 0.07, 0.08], vec![4])); // Apply masks to gradients let masked_gradients = optimizer.apply_masks_to_gradients(&gradients)?; println!("Original gradients for weight1: {:?}", gradients["weight1"].data); println!("Masked gradients for weight1: {:?}", masked_gradients["weight1"].data); let stats = optimizer.get_overall_stats(¶meters)?; println!("Overall stats: {:.1}% sparsity, {:.1}x compression", stats.sparsity_ratio * 100.0, stats.compression_ratio); println!("✓ Basic optimizer integration passed"); // Test 2: Gradual pruning with schedule println!("\n=== Test 2: Gradual Pruning with Schedule ==="); let schedule = PruningSchedule::linear(50, 0.9); // Reach 90% sparsity in 50 steps let mut scheduled_optimizer = PrunedOptimizer::with_schedule(schedule); println!("Sparsity progression:"); for step in [0, 10, 25, 50, 100] { let sparsity = scheduled_optimizer.get_current_sparsity(); println!(" Step {}: {:.1}% sparsity", step, sparsity * 100.0); scheduled_optimizer.update_count = step; } println!("✓ Gradual pruning schedule passed"); // Test 3: Training state management println!("\n=== Test 3: Training State Management ==="); let mut state = PrunedTrainingState::new(); state.gradual_pruning_active = true; // Simulate training steps for step in 1..=20 { let mock_stats = PruningStats::new(100, 100 - step * 2); // Gradual increase in sparsity state.update_step(mock_stats, step as f64 * 0.02); if step % 5 == 0 { println!("Step {}: {:.1}% sparsity, should update: {}", step, state.current_sparsity * 100.0, state.should_update_pruning(5)); } } println!("✓ Training state management passed"); // Test 4: Full training simulation println!("\n=== Test 4: Full Training Simulation ==="); let mut simulation_params = HashMap::new(); simulation_params.insert("layer1_weight".to_string(), DenseTensor::new((0..16).map(|i| (i as f32) * 0.1).collect(), vec![4, 4])); simulation_params.insert("layer2_weight".to_string(), DenseTensor::new((0..12).map(|i| (i as f32) * 0.05).collect(), vec![3, 4])); let sim_config = PruningConfig::global(0.0).with_schedule(PruningSchedule::linear(30, 0.8)); simulate_training_with_pruning(&simulation_params, sim_config, 30)?; println!("✓ Full training simulation passed"); // Test 5: Statistics merging println!("\n=== Test 5: Statistics Merging ==="); let stats1 = PruningStats::new(100, 50); let stats2 = PruningStats::new(200, 80); let merged = stats1.merge(&stats2); println!("Stats1: {} -> {} ({:.1}% sparsity)", stats1.original_params, stats1.pruned_params, stats1.sparsity_ratio * 100.0); println!("Stats2: {} -> {} ({:.1}% sparsity)", stats2.original_params, stats2.pruned_params, stats2.sparsity_ratio * 100.0); println!("Merged: {} -> {} ({:.1}% sparsity)", merged.original_params, merged.pruned_params, merged.sparsity_ratio * 100.0); assert_eq!(merged.original_params, 300); assert_eq!(merged.pruned_params, 130); assert!(merged.meets_sparsity_target(0.5)); println!("✓ Statistics merging passed"); println!("\n🎉 All optimizer integration tests passed!"); Ok(()) }