1pub mod distillation;
31pub mod graph_opt;
32pub mod pruning;
33pub mod quantization;
34
35pub use distillation::{
36 DenseLayer,
38 DistillationConfig,
40 DistillationConfigBuilder,
41 DistillationLoss,
42 DistillationStats,
43 DistillationTrainer,
44 EarlyStopping,
46 ForwardCache,
47 LearningRateSchedule,
48 MLPGradients,
49 OptimizerType,
50 SimpleMLP,
51 SimpleRng,
52 Temperature,
53 TrainingState,
54 cross_entropy_loss,
56 cross_entropy_with_label,
57 kl_divergence,
58 kl_divergence_from_logits,
59 log_softmax,
60 mse_loss,
61 soft_targets,
62 softmax,
63 train_student_model,
64};
65pub use graph_opt::{
66 GraphOptConfig, OptimizationBenchmark, apply_graph_optimization,
67 apply_graph_optimization_from_bytes, benchmark_optimization,
68};
69pub use pruning::{
70 FineTuneCallback,
72 GradientInfo,
73 ImportanceMethod,
74 LotteryTicketState,
75 MaskCreationMode,
76 NoOpFineTune,
77 PruningConfig,
79 PruningConfigBuilder,
80 PruningGranularity,
81 PruningMask,
82 PruningSchedule,
83 PruningStats,
84 PruningStrategy,
85 UnstructuredPruner,
86 WeightStatistics,
87 WeightTensor,
88 compute_channel_importance,
90 compute_gradient_importance,
91 compute_magnitude_importance,
92 compute_taylor_importance,
93 iterative_pruning,
94 prune_model,
95 prune_weights_direct,
96 prune_weights_with_gradients,
97 select_weights_to_prune,
98 structured_pruning,
99 unstructured_pruning,
100};
101pub use quantization::{
102 QuantizationConfig, QuantizationMode, QuantizationParams, QuantizationResult, QuantizationType,
103 calibrate_quantization, dequantize_tensor, quantize_model, quantize_tensor,
104};
105
106use crate::error::Result;
107use std::path::Path;
108use tracing::info;
109
110#[derive(Debug, Clone)]
112pub struct OptimizationStats {
113 pub original_size: usize,
115 pub optimized_size: usize,
117 pub compression_ratio: f32,
119 pub speedup: f32,
121 pub accuracy_delta: f32,
123}
124
125impl OptimizationStats {
126 #[must_use]
128 pub fn new(
129 original_size: usize,
130 optimized_size: usize,
131 speedup: f32,
132 accuracy_delta: f32,
133 ) -> Self {
134 let compression_ratio = if optimized_size > 0 {
135 original_size as f32 / optimized_size as f32
136 } else {
137 0.0
138 };
139
140 Self {
141 original_size,
142 optimized_size,
143 compression_ratio,
144 speedup,
145 accuracy_delta,
146 }
147 }
148
149 #[must_use]
151 pub fn size_reduction(&self) -> usize {
152 self.original_size.saturating_sub(self.optimized_size)
153 }
154
155 #[must_use]
157 pub fn size_reduction_percent(&self) -> f32 {
158 if self.original_size > 0 {
159 (self.size_reduction() as f32 / self.original_size as f32) * 100.0
160 } else {
161 0.0
162 }
163 }
164
165 #[must_use]
167 pub fn is_worthwhile(&self) -> bool {
168 self.size_reduction_percent() > 20.0 && self.accuracy_delta.abs() < 2.0
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum OptimizationProfile {
175 Accuracy,
177 Balanced,
179 Speed,
181 Size,
183}
184
185pub struct OptimizationPipeline {
187 pub quantization: Option<QuantizationConfig>,
189 pub pruning: Option<PruningConfig>,
191 pub weight_sharing: bool,
193 pub operator_fusion: bool,
195 pub graph_opt_config: Option<GraphOptConfig>,
198}
199
200impl OptimizationPipeline {
201 #[must_use]
206 pub fn effective_graph_opt_config(&self) -> GraphOptConfig {
207 if let Some(ref config) = self.graph_opt_config {
208 config.clone()
209 } else if self.operator_fusion {
210 GraphOptConfig::default()
211 } else {
212 GraphOptConfig::none()
213 }
214 }
215
216 #[must_use]
219 pub fn opt_level(&self) -> oxionnx::OptLevel {
220 self.effective_graph_opt_config().to_opt_level()
221 }
222}
223
224impl OptimizationPipeline {
225 #[must_use]
227 pub fn from_profile(profile: OptimizationProfile) -> Self {
228 match profile {
229 OptimizationProfile::Accuracy => Self {
230 quantization: Some(
231 QuantizationConfig::builder()
232 .quantization_type(QuantizationType::Float16)
233 .build(),
234 ),
235 pruning: None,
236 weight_sharing: false,
237 operator_fusion: true,
238 graph_opt_config: Some(GraphOptConfig::default()),
239 },
240 OptimizationProfile::Balanced => Self {
241 quantization: Some(
242 QuantizationConfig::builder()
243 .quantization_type(QuantizationType::Int8)
244 .per_channel(true)
245 .build(),
246 ),
247 pruning: Some(
248 PruningConfig::builder()
249 .sparsity_target(0.3)
250 .strategy(PruningStrategy::Magnitude)
251 .build(),
252 ),
253 weight_sharing: true,
254 operator_fusion: true,
255 graph_opt_config: Some(GraphOptConfig::default()),
256 },
257 OptimizationProfile::Speed => Self {
258 quantization: Some(
259 QuantizationConfig::builder()
260 .quantization_type(QuantizationType::Int8)
261 .per_channel(true)
262 .build(),
263 ),
264 pruning: Some(
265 PruningConfig::builder()
266 .sparsity_target(0.5)
267 .strategy(PruningStrategy::Structured)
268 .build(),
269 ),
270 weight_sharing: true,
271 operator_fusion: true,
272 graph_opt_config: Some(GraphOptConfig {
273 constant_folding: true,
274 dead_node_elimination: true,
275 common_subexpression_elimination: true,
276 operator_fusion: true,
277 }),
278 },
279 OptimizationProfile::Size => Self {
280 quantization: Some(
281 QuantizationConfig::builder()
282 .quantization_type(QuantizationType::Int8)
283 .per_channel(true)
284 .build(),
285 ),
286 pruning: Some(
287 PruningConfig::builder()
288 .sparsity_target(0.7)
289 .strategy(PruningStrategy::Structured)
290 .build(),
291 ),
292 weight_sharing: true,
293 operator_fusion: true,
294 graph_opt_config: Some(GraphOptConfig::default()),
295 },
296 }
297 }
298
299 pub fn optimize<P: AsRef<std::path::Path>>(
304 &self,
305 input_path: P,
306 output_path: P,
307 ) -> Result<OptimizationStats> {
308 use tracing::info;
309
310 info!("Running optimization pipeline");
311
312 let input = input_path.as_ref();
313 let output = output_path.as_ref();
314
315 let original_size = std::fs::metadata(input)
317 .map(|m| m.len() as usize)
318 .unwrap_or(0);
319
320 let mut current_path = input.to_path_buf();
322
323 if let Some(ref config) = self.pruning {
325 let pruned_path = output.with_extension("pruned.onnx");
326 prune_model(¤t_path, &pruned_path, config)?;
327 current_path = pruned_path;
328 }
329
330 if let Some(ref config) = self.quantization {
332 let quantized_path = output.with_extension("quantized.onnx");
333 quantize_model(¤t_path, &quantized_path, config)?;
334 current_path = quantized_path;
335 }
336
337 std::fs::rename(¤t_path, output)?;
339
340 let optimized_size = std::fs::metadata(output)
342 .map(|m| m.len() as usize)
343 .unwrap_or(0);
344
345 let opt_level = self.opt_level();
347 let speedup = Self::measure_speedup(input, output, opt_level)?;
348
349 let accuracy_delta = Self::estimate_accuracy_delta(self);
352
353 Ok(OptimizationStats::new(
354 original_size,
355 optimized_size,
356 speedup,
357 accuracy_delta,
358 ))
359 }
360
361 fn measure_speedup(
366 original_path: &Path,
367 optimized_path: &Path,
368 opt_level: oxionnx::OptLevel,
369 ) -> Result<f32> {
370 const WARMUP_ITERS: usize = 5;
372 const BENCH_ITERS: usize = 20;
373
374 if !original_path.exists() || !optimized_path.exists() {
376 info!("Skipping speedup measurement: model files not accessible");
377 return Ok(1.5); }
379
380 let dummy_input = vec![0.0f32; 224 * 224 * 3]; let input_shape = vec![1, 3, 224, 224];
384
385 let original_time = match Self::benchmark_model(
387 original_path,
388 &dummy_input,
389 &input_shape,
390 WARMUP_ITERS,
391 BENCH_ITERS,
392 oxionnx::OptLevel::None,
393 ) {
394 Ok(t) => t,
395 Err(e) => {
396 info!("Could not benchmark original model: {}, using estimate", e);
397 return Ok(1.5);
398 }
399 };
400
401 let optimized_time = match Self::benchmark_model(
403 optimized_path,
404 &dummy_input,
405 &input_shape,
406 WARMUP_ITERS,
407 BENCH_ITERS,
408 opt_level,
409 ) {
410 Ok(t) => t,
411 Err(e) => {
412 info!("Could not benchmark optimized model: {}, using estimate", e);
413 return Ok(1.5);
414 }
415 };
416
417 if optimized_time > 0.0 {
418 let speedup = (original_time / optimized_time) as f32;
419 info!(
420 "Measured speedup: {:.2}x (original: {:.2}ms, optimized: {:.2}ms)",
421 speedup,
422 original_time * 1000.0,
423 optimized_time * 1000.0
424 );
425 Ok(speedup)
426 } else {
427 Ok(1.5) }
429 }
430
431 fn benchmark_model(
433 model_path: &Path,
434 input: &[f32],
435 input_shape: &[usize],
436 warmup_iters: usize,
437 bench_iters: usize,
438 opt_level: oxionnx::OptLevel,
439 ) -> Result<f64> {
440 use ndarray::{Array, IxDyn};
441 use oxionnx::{SessionBuilder, Tensor};
442 use std::time::Instant;
443
444 let session = SessionBuilder::new()
446 .with_optimization_level(opt_level)
447 .commit_from_file(model_path)
448 .map_err(|e| crate::error::ModelError::LoadFailed {
449 reason: format!("Failed to load model for benchmarking: {}", e),
450 })?;
451
452 let input_name = session
454 .input_info()
455 .first()
456 .ok_or_else(|| crate::error::ModelError::LoadFailed {
457 reason: "No input tensors found in model".to_string(),
458 })?
459 .name
460 .clone();
461
462 let array_shape: Vec<usize> = input_shape.to_vec();
464 let total_elements: usize = array_shape.iter().product();
465
466 if input.len() != total_elements {
468 return Err(crate::error::InferenceError::InvalidInputShape {
469 expected: array_shape.clone(),
470 actual: vec![input.len()],
471 }
472 .into());
473 }
474
475 let input_array =
477 Array::from_shape_vec(IxDyn(&array_shape), input.to_vec()).map_err(|e| {
478 crate::error::InferenceError::Failed {
479 reason: format!("Failed to create input array: {}", e),
480 }
481 })?;
482
483 for _ in 0..warmup_iters {
485 let input_tensor = Tensor::from_ndarray_view(input_array.view());
487 let inputs_map =
488 oxionnx::inputs![input_name.as_str() => input_tensor].map_err(|e| {
489 crate::error::InferenceError::Failed {
490 reason: format!("Failed to build inputs map: {}", e),
491 }
492 })?;
493
494 let _ = session
495 .run(&inputs_map)
496 .map_err(|e| crate::error::InferenceError::Failed {
497 reason: format!("Warmup inference failed: {}", e),
498 })?;
499 }
500
501 let start = Instant::now();
503 for _ in 0..bench_iters {
504 let input_tensor = Tensor::from_ndarray_view(input_array.view());
505 let inputs_map =
506 oxionnx::inputs![input_name.as_str() => input_tensor].map_err(|e| {
507 crate::error::InferenceError::Failed {
508 reason: format!("Failed to build inputs map: {}", e),
509 }
510 })?;
511
512 let _ = session
513 .run(&inputs_map)
514 .map_err(|e| crate::error::InferenceError::Failed {
515 reason: format!("Benchmark inference failed: {}", e),
516 })?;
517 }
518 let elapsed = start.elapsed();
519
520 let avg_time = elapsed.as_secs_f64() / bench_iters as f64;
522
523 Ok(avg_time)
524 }
525
526 fn estimate_accuracy_delta(&self) -> f32 {
528 let mut delta = 0.0f32;
529
530 if let Some(ref quant) = self.quantization {
532 delta += match quant.quantization_type {
533 QuantizationType::Float16 => -0.1, QuantizationType::Int8 => -0.5, QuantizationType::UInt8 => -0.5, QuantizationType::Int4 => -2.0, };
538 }
539
540 if let Some(ref prune) = self.pruning {
542 delta += -prune.sparsity_target * 2.0; }
544
545 delta
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552
553 #[test]
554 fn test_optimization_stats() {
555 let stats = OptimizationStats::new(
556 1000000, 250000, 2.0, -0.5, );
561
562 assert_eq!(stats.size_reduction(), 750000);
563 assert!((stats.size_reduction_percent() - 75.0).abs() < 0.1);
564 assert!((stats.compression_ratio - 4.0).abs() < 0.1);
565 assert!(stats.is_worthwhile());
566 }
567
568 #[test]
569 fn test_optimization_profile_accuracy() {
570 let pipeline = OptimizationPipeline::from_profile(OptimizationProfile::Accuracy);
571 assert!(pipeline.quantization.is_some());
572 assert!(pipeline.pruning.is_none());
573 assert!(pipeline.operator_fusion);
574 assert!(pipeline.graph_opt_config.is_some());
575 assert_eq!(pipeline.opt_level(), oxionnx::OptLevel::All);
576 }
577
578 #[test]
579 fn test_optimization_profile_speed() {
580 let pipeline = OptimizationPipeline::from_profile(OptimizationProfile::Speed);
581 assert!(pipeline.quantization.is_some());
582 assert!(pipeline.pruning.is_some());
583 assert!(pipeline.weight_sharing);
584 assert!(pipeline.operator_fusion);
585 assert_eq!(pipeline.opt_level(), oxionnx::OptLevel::All);
586 }
587
588 #[test]
589 fn test_optimization_profile_size() {
590 let pipeline = OptimizationPipeline::from_profile(OptimizationProfile::Size);
591 assert!(pipeline.quantization.is_some());
592 assert!(pipeline.pruning.is_some());
593
594 if let Some(pruning) = &pipeline.pruning {
595 assert!(pruning.sparsity_target >= 0.6);
597 }
598 assert_eq!(pipeline.opt_level(), oxionnx::OptLevel::All);
599 }
600
601 #[test]
602 fn test_effective_graph_opt_config_from_fusion_flag() {
603 let pipeline = OptimizationPipeline {
605 quantization: None,
606 pruning: None,
607 weight_sharing: false,
608 operator_fusion: true,
609 graph_opt_config: None,
610 };
611 let config = pipeline.effective_graph_opt_config();
612 assert!(config.any_enabled());
613 assert_eq!(pipeline.opt_level(), oxionnx::OptLevel::All);
614
615 let pipeline_no_fusion = OptimizationPipeline {
617 quantization: None,
618 pruning: None,
619 weight_sharing: false,
620 operator_fusion: false,
621 graph_opt_config: None,
622 };
623 let config_none = pipeline_no_fusion.effective_graph_opt_config();
624 assert!(!config_none.any_enabled());
625 assert_eq!(pipeline_no_fusion.opt_level(), oxionnx::OptLevel::None);
626 }
627
628 #[test]
629 fn test_effective_graph_opt_config_explicit_override() {
630 let custom_config = GraphOptConfig {
632 constant_folding: true,
633 dead_node_elimination: false,
634 common_subexpression_elimination: false,
635 operator_fusion: false,
636 };
637 let pipeline = OptimizationPipeline {
638 quantization: None,
639 pruning: None,
640 weight_sharing: false,
641 operator_fusion: false, graph_opt_config: Some(custom_config),
643 };
644 let config = pipeline.effective_graph_opt_config();
645 assert!(config.constant_folding);
646 assert!(!config.operator_fusion);
647 assert_eq!(pipeline.opt_level(), oxionnx::OptLevel::All);
649 }
650}