1use crate::Module;
7use std::collections::{HashMap, HashSet, VecDeque};
8use torsh_core::error::{Result, TorshError};
9
10#[derive(Debug, Clone, PartialEq)]
12pub enum OptimizationStrategy {
13 KernelFusion,
15 MemoryOptimization,
17 DeadCodeElimination,
19 OperationReordering,
21 InlineOptimization,
23}
24
25#[derive(Debug, Clone)]
27pub enum FusionPattern {
28 ConvBnRelu,
30 LinearRelu,
32 LinearDropout,
34 AddRelu,
36 MulAdd,
38 SoftmaxCrossEntropy,
40}
41
42#[derive(Debug, Clone, PartialEq)]
44pub enum MemoryHint {
45 InPlace,
47 Pooled,
49 Streaming,
51 Checkpointing,
53}
54
55#[derive(Debug, Clone)]
57pub struct OpNode {
58 pub id: usize,
60 pub op_type: String,
62 pub inputs: Vec<usize>,
64 pub output_shape: Vec<usize>,
66 pub memory_bytes: usize,
68 pub flops: u64,
70 pub fusable: bool,
72}
73
74#[derive(Debug)]
76pub struct ComputationGraph {
77 pub nodes: HashMap<usize, OpNode>,
79 pub adjacency: HashMap<usize, Vec<usize>>,
81 pub inputs: Vec<usize>,
83 pub outputs: Vec<usize>,
85 next_id: usize,
87}
88
89impl ComputationGraph {
90 pub fn new() -> Self {
92 Self {
93 nodes: HashMap::new(),
94 adjacency: HashMap::new(),
95 inputs: Vec::new(),
96 outputs: Vec::new(),
97 next_id: 0,
98 }
99 }
100
101 pub fn add_node(&mut self, op_type: String, output_shape: Vec<usize>, flops: u64) -> usize {
103 let id = self.next_id;
104 self.next_id += 1;
105
106 let memory_bytes = output_shape.iter().product::<usize>() * 4; let node = OpNode {
109 id,
110 op_type,
111 inputs: Vec::new(),
112 output_shape,
113 memory_bytes,
114 flops,
115 fusable: true,
116 };
117
118 self.nodes.insert(id, node);
119 self.adjacency.insert(id, Vec::new());
120
121 id
122 }
123
124 pub fn add_edge(&mut self, from: usize, to: usize) -> Result<()> {
126 if !self.nodes.contains_key(&from) || !self.nodes.contains_key(&to) {
127 return Err(TorshError::InvalidArgument(
128 "Cannot add edge to non-existent nodes".to_string(),
129 ));
130 }
131
132 self.adjacency
133 .get_mut(&from)
134 .expect("from node should exist in adjacency")
135 .push(to);
136 self.nodes
137 .get_mut(&to)
138 .expect("to node should exist in nodes")
139 .inputs
140 .push(from);
141
142 Ok(())
143 }
144
145 pub fn topological_sort(&self) -> Result<Vec<usize>> {
147 let mut in_degree: HashMap<usize, usize> = HashMap::new();
148
149 for &node_id in self.nodes.keys() {
151 in_degree.insert(node_id, self.nodes[&node_id].inputs.len());
152 }
153
154 let mut queue = VecDeque::new();
155 let mut result = Vec::new();
156
157 for (&node_id, °ree) in &in_degree {
159 if degree == 0 {
160 queue.push_back(node_id);
161 }
162 }
163
164 while let Some(node_id) = queue.pop_front() {
165 result.push(node_id);
166
167 if let Some(neighbors) = self.adjacency.get(&node_id) {
169 for &neighbor in neighbors {
170 let degree = in_degree
171 .get_mut(&neighbor)
172 .expect("neighbor should exist in in_degree");
173 *degree -= 1;
174 if *degree == 0 {
175 queue.push_back(neighbor);
176 }
177 }
178 }
179 }
180
181 if result.len() != self.nodes.len() {
182 return Err(TorshError::InvalidArgument(
183 "Graph contains cycles".to_string(),
184 ));
185 }
186
187 Ok(result)
188 }
189
190 pub fn find_fusion_candidates(&self) -> Vec<Vec<usize>> {
192 let mut candidates = Vec::new();
193 let visited = &mut HashSet::new();
194
195 for &node_id in self.nodes.keys() {
196 if !visited.contains(&node_id) {
197 let sequence = self.find_fusion_sequence(node_id, visited);
198 if sequence.len() > 1 {
199 candidates.push(sequence);
200 }
201 }
202 }
203
204 candidates
205 }
206
207 fn find_fusion_sequence(&self, start: usize, visited: &mut HashSet<usize>) -> Vec<usize> {
209 let mut sequence = Vec::new();
210 let mut current = start;
211
212 loop {
213 if visited.contains(¤t) || !self.nodes[¤t].fusable {
214 break;
215 }
216
217 visited.insert(current);
218 sequence.push(current);
219
220 let successors = self
222 .adjacency
223 .get(¤t)
224 .expect("current node should exist in adjacency");
225 if successors.len() != 1 {
226 break; }
228
229 let next = successors[0];
230 if self.nodes[&next].inputs.len() != 1 {
231 break; }
233
234 current = next;
235 }
236
237 sequence
238 }
239
240 pub fn estimate_memory_usage(&self) -> usize {
242 self.nodes.values().map(|node| node.memory_bytes).sum()
244 }
245
246 pub fn estimate_flops(&self) -> u64 {
248 self.nodes.values().map(|node| node.flops).sum()
249 }
250}
251
252pub struct NetworkOptimizer {
254 strategies: Vec<OptimizationStrategy>,
255 fusion_patterns: Vec<FusionPattern>,
256 memory_hints: Vec<MemoryHint>,
257}
258
259impl NetworkOptimizer {
260 pub fn new() -> Self {
262 Self {
263 strategies: vec![
264 OptimizationStrategy::KernelFusion,
265 OptimizationStrategy::MemoryOptimization,
266 OptimizationStrategy::DeadCodeElimination,
267 ],
268 fusion_patterns: vec![
269 FusionPattern::ConvBnRelu,
270 FusionPattern::LinearRelu,
271 FusionPattern::AddRelu,
272 ],
273 memory_hints: vec![MemoryHint::InPlace, MemoryHint::Pooled],
274 }
275 }
276
277 pub fn with_config(
279 strategies: Vec<OptimizationStrategy>,
280 fusion_patterns: Vec<FusionPattern>,
281 memory_hints: Vec<MemoryHint>,
282 ) -> Self {
283 Self {
284 strategies,
285 fusion_patterns,
286 memory_hints,
287 }
288 }
289
290 pub fn optimize_module<M: Module>(&self, module: &M) -> Result<OptimizationReport> {
292 let graph = self.build_computation_graph(module)?;
293 let original_memory = graph.estimate_memory_usage();
294 let original_flops = graph.estimate_flops();
295
296 let mut optimizations = Vec::new();
297
298 if self
300 .strategies
301 .contains(&OptimizationStrategy::KernelFusion)
302 {
303 let fusion_results = self.apply_kernel_fusion(&graph)?;
304 optimizations.extend(fusion_results);
305 }
306
307 if self
309 .strategies
310 .contains(&OptimizationStrategy::MemoryOptimization)
311 {
312 let memory_results = self.apply_memory_optimization(&graph)?;
313 optimizations.extend(memory_results);
314 }
315
316 let optimized_memory = self.estimate_optimized_memory(&graph, &optimizations);
318 let optimized_flops = self.estimate_optimized_flops(&graph, &optimizations);
319
320 Ok(OptimizationReport {
321 original_memory,
322 optimized_memory,
323 memory_reduction: original_memory - optimized_memory,
324 original_flops,
325 optimized_flops,
326 flops_reduction: original_flops - optimized_flops,
327 optimizations,
328 })
329 }
330
331 fn build_computation_graph<M: Module>(&self, _module: &M) -> Result<ComputationGraph> {
333 let mut graph = ComputationGraph::new();
336
337 let input_id = graph.add_node("input".to_string(), vec![1, 3, 224, 224], 0);
339 let conv_id = graph.add_node("conv2d".to_string(), vec![1, 64, 112, 112], 1_000_000);
340 let bn_id = graph.add_node("batch_norm".to_string(), vec![1, 64, 112, 112], 100_000);
341 let relu_id = graph.add_node("relu".to_string(), vec![1, 64, 112, 112], 50_000);
342
343 graph.add_edge(input_id, conv_id)?;
344 graph.add_edge(conv_id, bn_id)?;
345 graph.add_edge(bn_id, relu_id)?;
346
347 Ok(graph)
348 }
349
350 fn apply_kernel_fusion(&self, graph: &ComputationGraph) -> Result<Vec<OptimizationApplied>> {
352 let mut optimizations = Vec::new();
353 let fusion_candidates = graph.find_fusion_candidates();
354
355 for candidate in fusion_candidates {
356 if candidate.len() >= 2 {
357 let ops: Vec<String> = candidate
358 .iter()
359 .map(|&id| graph.nodes[&id].op_type.clone())
360 .collect();
361
362 if self.matches_fusion_pattern(&ops) {
364 optimizations.push(OptimizationApplied {
365 optimization_type: "kernel_fusion".to_string(),
366 description: format!("Fused operations: {}", ops.join(" + ")),
367 memory_saved: self.estimate_fusion_memory_savings(&candidate, graph),
368 flops_saved: self.estimate_fusion_flops_savings(&candidate, graph),
369 });
370 }
371 }
372 }
373
374 Ok(optimizations)
375 }
376
377 fn apply_memory_optimization(
379 &self,
380 graph: &ComputationGraph,
381 ) -> Result<Vec<OptimizationApplied>> {
382 let mut optimizations = Vec::new();
383
384 if self.memory_hints.contains(&MemoryHint::InPlace) {
386 for node in graph.nodes.values() {
387 if self.can_be_inplace(&node.op_type) {
388 optimizations.push(OptimizationApplied {
389 optimization_type: "inplace_operation".to_string(),
390 description: format!("Made {} operation in-place", node.op_type),
391 memory_saved: node.memory_bytes,
392 flops_saved: 0,
393 });
394 }
395 }
396 }
397
398 Ok(optimizations)
399 }
400
401 fn matches_fusion_pattern(&self, ops: &[String]) -> bool {
403 for pattern in &self.fusion_patterns {
404 match pattern {
405 FusionPattern::ConvBnRelu => {
406 if ops.len() == 3
407 && ops[0] == "conv2d"
408 && ops[1] == "batch_norm"
409 && ops[2] == "relu"
410 {
411 return true;
412 }
413 }
414 FusionPattern::LinearRelu => {
415 if ops.len() == 2 && ops[0] == "linear" && ops[1] == "relu" {
416 return true;
417 }
418 }
419 FusionPattern::AddRelu => {
420 if ops.len() == 2 && ops[0] == "add" && ops[1] == "relu" {
421 return true;
422 }
423 }
424 _ => {}
425 }
426 }
427 false
428 }
429
430 fn can_be_inplace(&self, op_type: &str) -> bool {
432 matches!(op_type, "relu" | "dropout" | "batch_norm" | "layer_norm")
433 }
434
435 fn estimate_fusion_memory_savings(&self, _nodes: &[usize], _graph: &ComputationGraph) -> usize {
437 1024 * 1024 }
440
441 fn estimate_fusion_flops_savings(&self, _nodes: &[usize], _graph: &ComputationGraph) -> u64 {
443 1000
445 }
446
447 fn estimate_optimized_memory(
449 &self,
450 _graph: &ComputationGraph,
451 optimizations: &[OptimizationApplied],
452 ) -> usize {
453 let savings: usize = optimizations.iter().map(|opt| opt.memory_saved).sum();
454 _graph.estimate_memory_usage().saturating_sub(savings)
455 }
456
457 fn estimate_optimized_flops(
459 &self,
460 _graph: &ComputationGraph,
461 optimizations: &[OptimizationApplied],
462 ) -> u64 {
463 let savings: u64 = optimizations.iter().map(|opt| opt.flops_saved).sum();
464 _graph.estimate_flops().saturating_sub(savings)
465 }
466}
467
468impl Default for NetworkOptimizer {
469 fn default() -> Self {
470 Self::new()
471 }
472}
473
474#[derive(Debug, Clone)]
476pub struct OptimizationApplied {
477 pub optimization_type: String,
479 pub description: String,
481 pub memory_saved: usize,
483 pub flops_saved: u64,
485}
486
487#[derive(Debug, Clone)]
489pub struct OptimizationReport {
490 pub original_memory: usize,
492 pub optimized_memory: usize,
494 pub memory_reduction: usize,
496 pub original_flops: u64,
498 pub optimized_flops: u64,
500 pub flops_reduction: u64,
502 pub optimizations: Vec<OptimizationApplied>,
504}
505
506impl OptimizationReport {
507 pub fn memory_reduction_percent(&self) -> f64 {
509 if self.original_memory == 0 {
510 0.0
511 } else {
512 (self.memory_reduction as f64 / self.original_memory as f64) * 100.0
513 }
514 }
515
516 pub fn flops_reduction_percent(&self) -> f64 {
518 if self.original_flops == 0 {
519 0.0
520 } else {
521 (self.flops_reduction as f64 / self.original_flops as f64) * 100.0
522 }
523 }
524
525 pub fn format_report(&self) -> String {
527 let mut report = String::new();
528
529 report.push_str("=== Neural Network Optimization Report ===\n");
530 report.push_str(&format!("Memory Usage:\n"));
531 report.push_str(&format!(
532 " Original: {} MB\n",
533 self.original_memory / (1024 * 1024)
534 ));
535 report.push_str(&format!(
536 " Optimized: {} MB\n",
537 self.optimized_memory / (1024 * 1024)
538 ));
539 report.push_str(&format!(
540 " Reduction: {} MB ({:.1}%)\n",
541 self.memory_reduction / (1024 * 1024),
542 self.memory_reduction_percent()
543 ));
544
545 report.push_str(&format!("\nComputation Cost:\n"));
546 report.push_str(&format!(
547 " Original: {} GFLOPS\n",
548 self.original_flops / 1_000_000_000
549 ));
550 report.push_str(&format!(
551 " Optimized: {} GFLOPS\n",
552 self.optimized_flops / 1_000_000_000
553 ));
554 report.push_str(&format!(
555 " Reduction: {} GFLOPS ({:.1}%)\n",
556 self.flops_reduction / 1_000_000_000,
557 self.flops_reduction_percent()
558 ));
559
560 report.push_str(&format!("\nOptimizations Applied:\n"));
561 for opt in &self.optimizations {
562 report.push_str(&format!(
563 " - {}: {}\n",
564 opt.optimization_type, opt.description
565 ));
566 }
567
568 report
569 }
570}
571
572pub struct MemoryProfiler {
574 allocations: HashMap<String, usize>,
575 peak_usage: usize,
576 current_usage: usize,
577}
578
579impl MemoryProfiler {
580 pub fn new() -> Self {
582 Self {
583 allocations: HashMap::new(),
584 peak_usage: 0,
585 current_usage: 0,
586 }
587 }
588
589 pub fn allocate(&mut self, name: String, size: usize) {
591 self.allocations.insert(name, size);
592 self.current_usage += size;
593 self.peak_usage = self.peak_usage.max(self.current_usage);
594 }
595
596 pub fn deallocate(&mut self, name: &str) {
598 if let Some(size) = self.allocations.remove(name) {
599 self.current_usage = self.current_usage.saturating_sub(size);
600 }
601 }
602
603 pub fn current_usage(&self) -> usize {
605 self.current_usage
606 }
607
608 pub fn peak_usage(&self) -> usize {
610 self.peak_usage
611 }
612
613 pub fn reset(&mut self) {
615 self.allocations.clear();
616 self.peak_usage = 0;
617 self.current_usage = 0;
618 }
619}
620
621impl Default for MemoryProfiler {
622 fn default() -> Self {
623 Self::new()
624 }
625}
626
627pub fn optimize_module<M: Module>(module: &M) -> Result<OptimizationReport> {
629 let optimizer = NetworkOptimizer::new();
630 optimizer.optimize_module(module)
631}
632
633pub fn optimize_for_inference<M: Module>(module: &M) -> Result<OptimizationReport> {
634 let optimizer = NetworkOptimizer::with_config(
635 vec![
636 OptimizationStrategy::KernelFusion,
637 OptimizationStrategy::MemoryOptimization,
638 OptimizationStrategy::InlineOptimization,
639 ],
640 vec![
641 FusionPattern::ConvBnRelu,
642 FusionPattern::LinearRelu,
643 FusionPattern::AddRelu,
644 FusionPattern::MulAdd,
645 ],
646 vec![MemoryHint::InPlace, MemoryHint::Pooled],
647 );
648 optimizer.optimize_module(module)
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654
655 #[test]
656 #[ignore]
657 fn test_computation_graph() {
658 let mut graph = ComputationGraph::new();
659
660 let node1 = graph.add_node("input".to_string(), vec![1, 3, 224, 224], 0);
661 let node2 = graph.add_node("conv2d".to_string(), vec![1, 64, 112, 112], 1000000);
662 let node3 = graph.add_node("relu".to_string(), vec![1, 64, 112, 112], 50000);
663
664 graph.add_edge(node1, node2).unwrap();
665 graph.add_edge(node2, node3).unwrap();
666
667 assert_eq!(graph.nodes.len(), 3);
668
669 let topo_order = graph.topological_sort().unwrap();
670 assert_eq!(topo_order, vec![node1, node2, node3]);
671
672 let fusion_candidates = graph.find_fusion_candidates();
673 assert!(!fusion_candidates.is_empty());
674 }
675
676 #[test]
677 fn test_network_optimizer() {
678 let optimizer = NetworkOptimizer::new();
679 assert_eq!(optimizer.strategies.len(), 3);
680 assert_eq!(optimizer.fusion_patterns.len(), 3);
681 assert_eq!(optimizer.memory_hints.len(), 2);
682 }
683
684 #[test]
685 fn test_memory_profiler() {
686 let mut profiler = MemoryProfiler::new();
687
688 profiler.allocate("tensor1".to_string(), 1024);
689 assert_eq!(profiler.current_usage(), 1024);
690 assert_eq!(profiler.peak_usage(), 1024);
691
692 profiler.allocate("tensor2".to_string(), 2048);
693 assert_eq!(profiler.current_usage(), 3072);
694 assert_eq!(profiler.peak_usage(), 3072);
695
696 profiler.deallocate("tensor1");
697 assert_eq!(profiler.current_usage(), 2048);
698 assert_eq!(profiler.peak_usage(), 3072);
699 }
700
701 #[test]
702 fn test_optimization_report() {
703 let report = OptimizationReport {
704 original_memory: 1024 * 1024 * 10, optimized_memory: 1024 * 1024 * 8, memory_reduction: 1024 * 1024 * 2, original_flops: 1_000_000_000, optimized_flops: 800_000_000, flops_reduction: 200_000_000, optimizations: vec![OptimizationApplied {
711 optimization_type: "kernel_fusion".to_string(),
712 description: "Fused conv + relu".to_string(),
713 memory_saved: 1024 * 1024,
714 flops_saved: 100_000_000,
715 }],
716 };
717
718 assert_eq!(report.memory_reduction_percent(), 20.0);
719 assert_eq!(report.flops_reduction_percent(), 20.0);
720
721 let formatted = report.format_report();
722 assert!(formatted.contains("Memory Usage:"));
723 assert!(formatted.contains("Computation Cost:"));
724 assert!(formatted.contains("Optimizations Applied:"));
725 }
726}