1use anyhow::Result;
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet, VecDeque};
10use std::fmt;
11use uuid::Uuid;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ComputationGraph {
16 pub id: Uuid,
18 pub nodes: HashMap<String, GraphNode>,
20 pub edges: HashMap<String, Vec<String>>,
22 pub root_nodes: HashSet<String>,
24 pub leaf_nodes: HashSet<String>,
26 pub metadata: GraphMetadata,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct GraphMetadata {
33 pub name: String,
35 pub node_count: usize,
37 pub edge_count: usize,
39 pub max_depth: usize,
41 pub estimated_memory_usage: u64,
43 pub estimated_flops: u64,
45 pub created_at: chrono::DateTime<chrono::Utc>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct GraphNode {
52 pub id: String,
54 pub name: String,
56 pub operation_type: OperationType,
58 pub input_shapes: Vec<Vec<usize>>,
60 pub output_shapes: Vec<Vec<usize>>,
62 pub flop_count: Option<u64>,
71 pub memory_usage: Option<u64>,
74 pub execution_time_us: Option<u64>,
76 pub parameter_count: Option<u64>,
84 pub topo_order: Option<usize>,
86 pub depth: usize,
88 pub metadata: HashMap<String, String>,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
94pub enum OperationType {
95 Add,
97 Subtract,
98 Multiply,
99 Divide,
100 MatMul,
101 Dot,
102
103 ReLU,
105 Sigmoid,
106 Tanh,
107 GELU,
108 Softmax,
109
110 LayerNorm,
112 BatchNorm,
113 RMSNorm,
114
115 Conv1D,
117 Conv2D,
118 Conv3D,
119 ConvTranspose,
120
121 MaxPool,
123 AvgPool,
124 AdaptivePool,
125
126 Reshape,
128 Transpose,
129 Concat,
130 Split,
131 Slice,
132 Gather,
133 Scatter,
134
135 Sum,
137 Mean,
138 Max,
139 Min,
140
141 Attention,
143 MultiHeadAttention,
144 SelfAttention,
145 CrossAttention,
146
147 Embedding,
149 PositionalEmbedding,
150
151 CrossEntropyLoss,
153 MSELoss,
154 L1Loss,
155
156 If,
158 While,
159 Loop,
160
161 Custom(String),
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct GraphAnalysisConfig {
168 pub enable_memory_analysis: bool,
170 pub enable_flop_analysis: bool,
172 pub enable_optimization_analysis: bool,
174 pub enable_bottleneck_detection: bool,
176 pub enable_dataflow_analysis: bool,
178 pub bottleneck_threshold_us: u64,
180 pub large_memory_threshold: u64,
182}
183
184impl Default for GraphAnalysisConfig {
185 fn default() -> Self {
186 Self {
187 enable_memory_analysis: true,
188 enable_flop_analysis: true,
189 enable_optimization_analysis: true,
190 enable_bottleneck_detection: true,
191 enable_dataflow_analysis: true,
192 bottleneck_threshold_us: 1000, large_memory_threshold: 1024 * 1024 * 100, }
195 }
196}
197
198#[derive(Debug)]
200pub struct ComputationGraphAnalyzer {
201 config: GraphAnalysisConfig,
202 graphs: HashMap<Uuid, ComputationGraph>,
203 analysis_results: HashMap<Uuid, GraphAnalysisResult>,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct GraphAnalysisResult {
209 pub graph_id: Uuid,
211 pub memory_analysis: Option<MemoryAnalysis>,
213 pub flop_analysis: Option<FlopAnalysis>,
215 pub optimization_opportunities: Vec<OptimizationOpportunity>,
217 pub bottleneck_analysis: Option<BottleneckAnalysis>,
219 pub dataflow_analysis: Option<DataFlowAnalysis>,
221 pub critical_path: Vec<String>,
223 pub statistics: GraphStatistics,
225 pub recommendations: Vec<String>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct OperationSpec {
236 pub node_id: String,
238 pub operation_type: OperationType,
240 pub dependencies: Vec<String>,
242 pub input_shapes: Vec<Vec<usize>>,
246 pub output_shapes: Vec<Vec<usize>>,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct MemoryAnalysis {
253 pub total_memory_usage: u64,
255 pub peak_memory_usage: u64,
264 pub memory_by_operation: HashMap<OperationType, u64>,
266 pub memory_hotspots: Vec<(String, u64)>,
268 pub fragmentation_ratio: Option<f64>,
278 pub optimization_suggestions: Vec<String>,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct FlopAnalysis {
285 pub total_flops: u64,
287 pub flops_by_operation: HashMap<OperationType, u64>,
289 pub compute_hotspots: Vec<(String, u64)>,
291 pub arithmetic_intensity: f64,
293 pub complexity_analysis: ComplexityAnalysis,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct ComplexityAnalysis {
300 pub time_complexity: Option<String>,
307 pub space_complexity: Option<String>,
311 pub parallelization_potential: f64,
321 pub sequential_dependencies: usize,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct OptimizationOpportunity {
328 pub optimization_type: OptimizationType,
330 pub description: String,
332 pub affected_nodes: Vec<String>,
334 pub estimated_improvement: EstimatedImprovement,
336 pub implementation_difficulty: u8,
338 pub priority: OptimizationPriority,
340}
341
342#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
344pub enum OptimizationType {
345 OperationFusion,
347 RedundancyElimination,
349 MemoryLayoutOptimization,
351 AlgorithmicOptimization,
353 Parallelization,
355 DataAccessOptimization,
357 PrecisionOptimization,
359 Memoization,
361 ControlFlowOptimization,
363}
364
365#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
367pub enum OptimizationPriority {
368 Low,
369 Medium,
370 High,
371 Critical,
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct EstimatedImprovement {
377 pub speedup_factor: f64,
379 pub memory_reduction: u64,
381 pub energy_savings: f64,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct BottleneckAnalysis {
388 pub bottleneck_nodes: Vec<String>,
390 pub critical_path_nodes: Vec<String>,
392 pub critical_path_time_us: u64,
394 pub parallelizable_nodes: Vec<String>,
396 pub scheduling_suggestions: Vec<String>,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct DataFlowAnalysis {
403 pub data_dependencies: HashMap<String, Vec<String>>,
405 pub live_variables: HashMap<String, HashSet<String>>,
407 pub variable_lifetimes: HashMap<String, VariableLifetime>,
409 pub memory_reuse_opportunities: Vec<MemoryReuseOpportunity>,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize)]
415pub struct VariableLifetime {
416 pub birth_node: String,
418 pub death_node: String,
420 pub usage_nodes: Vec<String>,
422 pub memory_footprint: u64,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct MemoryReuseOpportunity {
429 pub reusable_variables: Vec<String>,
431 pub memory_savings: u64,
433 pub complexity: u8,
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct GraphStatistics {
440 pub nodes_by_type: HashMap<OperationType, usize>,
442 pub average_fan_in: f64,
444 pub average_fan_out: f64,
446 pub diameter: usize,
448 pub clustering_coefficient: f64,
450 pub strongly_connected_components: usize,
452}
453
454impl ComputationGraphAnalyzer {
455 pub fn new(config: GraphAnalysisConfig) -> Self {
457 Self {
458 config,
459 graphs: HashMap::new(),
460 analysis_results: HashMap::new(),
461 }
462 }
463
464 pub fn add_graph(&mut self, graph: ComputationGraph) -> Result<()> {
466 let graph_id = graph.id;
467 self.graphs.insert(graph_id, graph);
468 Ok(())
469 }
470
471 pub fn create_graph(
479 &mut self,
480 name: String,
481 operations: Vec<(String, OperationType, Vec<String>)>, ) -> Result<Uuid> {
483 self.create_graph_with_shapes(
484 name,
485 operations
486 .into_iter()
487 .map(|(node_id, operation_type, dependencies)| OperationSpec {
488 node_id,
489 operation_type,
490 dependencies,
491 input_shapes: Vec::new(),
492 output_shapes: Vec::new(),
493 })
494 .collect(),
495 )
496 }
497
498 pub fn create_graph_with_shapes(
502 &mut self,
503 name: String,
504 operations: Vec<OperationSpec>,
505 ) -> Result<Uuid> {
506 let graph_id = Uuid::new_v4();
507 let mut nodes = HashMap::new();
508 let mut edges = HashMap::new();
509 let mut root_nodes = HashSet::new();
510 let mut leaf_nodes = HashSet::new();
511
512 for spec in &operations {
514 let OperationSpec {
515 node_id,
516 operation_type: op_type,
517 dependencies,
518 input_shapes,
519 output_shapes,
520 } = spec;
521 let node = GraphNode {
522 id: node_id.clone(),
523 name: node_id.clone(),
524 operation_type: op_type.clone(),
525 input_shapes: input_shapes.clone(),
526 output_shapes: output_shapes.clone(),
527 flop_count: self.estimate_flops(op_type, input_shapes),
528 memory_usage: self.estimate_memory(op_type, input_shapes),
529 execution_time_us: None,
530 parameter_count: self.estimate_parameters(op_type, input_shapes),
531 topo_order: None,
532 depth: 0,
533 metadata: HashMap::new(),
534 };
535 nodes.insert(node_id.clone(), node);
536
537 if dependencies.is_empty() {
539 root_nodes.insert(node_id.clone());
540 }
541 edges.insert(node_id.clone(), dependencies.clone());
542 }
543
544 let all_dependencies: HashSet<String> = edges.values().flatten().cloned().collect();
546 for node_id in nodes.keys() {
547 if !all_dependencies.contains(node_id) {
548 leaf_nodes.insert(node_id.clone());
549 }
550 }
551
552 self.calculate_depth_and_topo_order(&mut nodes, &edges)?;
554
555 let metadata = GraphMetadata {
556 name,
557 node_count: nodes.len(),
558 edge_count: edges.values().map(|deps| deps.len()).sum(),
559 max_depth: nodes.values().map(|n| n.depth).max().unwrap_or(0),
560 estimated_memory_usage: nodes.values().filter_map(|n| n.memory_usage).sum(),
561 estimated_flops: nodes.values().filter_map(|n| n.flop_count).sum(),
562 created_at: chrono::Utc::now(),
563 };
564
565 let graph = ComputationGraph {
566 id: graph_id,
567 nodes,
568 edges,
569 root_nodes,
570 leaf_nodes,
571 metadata,
572 };
573
574 self.graphs.insert(graph_id, graph);
575 Ok(graph_id)
576 }
577
578 pub fn analyze_graph(&mut self, graph_id: Uuid) -> Result<GraphAnalysisResult> {
580 let graph = self
581 .graphs
582 .get(&graph_id)
583 .ok_or_else(|| anyhow::anyhow!("Graph not found: {}", graph_id))?;
584
585 let mut result = GraphAnalysisResult {
586 graph_id,
587 memory_analysis: None,
588 flop_analysis: None,
589 optimization_opportunities: Vec::new(),
590 bottleneck_analysis: None,
591 dataflow_analysis: None,
592 critical_path: Vec::new(),
593 statistics: self.calculate_statistics(graph)?,
594 recommendations: Vec::new(),
595 };
596
597 if self.config.enable_memory_analysis {
599 result.memory_analysis = Some(self.analyze_memory_usage(graph)?);
600 }
601
602 if self.config.enable_flop_analysis {
603 result.flop_analysis = Some(self.analyze_flop_usage(graph)?);
604 }
605
606 if self.config.enable_optimization_analysis {
607 result.optimization_opportunities = self.detect_optimization_opportunities(graph)?;
608 }
609
610 if self.config.enable_bottleneck_detection {
611 result.bottleneck_analysis = Some(self.analyze_bottlenecks(graph)?);
612 }
613
614 if self.config.enable_dataflow_analysis {
615 result.dataflow_analysis = Some(self.analyze_dataflow(graph)?);
616 }
617
618 result.critical_path = self.find_critical_path(graph)?;
619 result.recommendations = self.generate_recommendations(&result)?;
620
621 self.analysis_results.insert(graph_id, result.clone());
622 Ok(result)
623 }
624
625 pub fn get_analysis_result(&self, graph_id: Uuid) -> Option<&GraphAnalysisResult> {
627 self.analysis_results.get(&graph_id)
628 }
629
630 pub fn export_to_dot(&self, graph_id: Uuid) -> Result<String> {
632 let graph = self
633 .graphs
634 .get(&graph_id)
635 .ok_or_else(|| anyhow::anyhow!("Graph not found: {}", graph_id))?;
636
637 let mut dot = String::new();
638 dot.push_str(&format!("digraph \"{}\" {{\n", graph.metadata.name));
639 dot.push_str(" rankdir=TB;\n");
640 dot.push_str(" node [shape=box, style=filled];\n\n");
641
642 for node in graph.nodes.values() {
644 let color = self.get_node_color(&node.operation_type);
645 let label = format!(
646 "{}\\n{}\\n{}\\n{}",
647 node.name,
648 format!("{:?}", node.operation_type),
649 node.flop_count.map_or_else(
650 || "FLOPs n/a".to_string(),
651 |f| format!("{:.1} GFLOP", f as f64 / 1e9)
652 ),
653 node.memory_usage.map_or_else(
654 || "memory n/a".to_string(),
655 |m| format!("{:.1} MB", m as f64 / (1024.0 * 1024.0))
656 )
657 );
658
659 dot.push_str(&format!(
660 " \"{}\" [label=\"{}\", fillcolor=\"{}\"];\n",
661 node.id, label, color
662 ));
663 }
664
665 dot.push('\n');
666
667 for (node_id, dependencies) in &graph.edges {
669 for dep in dependencies {
670 dot.push_str(&format!(" \"{}\" -> \"{}\";\n", dep, node_id));
671 }
672 }
673
674 dot.push_str("}\n");
675 Ok(dot)
676 }
677
678 fn calculate_depth_and_topo_order(
681 &self,
682 nodes: &mut HashMap<String, GraphNode>,
683 edges: &HashMap<String, Vec<String>>,
684 ) -> Result<()> {
685 let mut in_degree: HashMap<String, usize> = HashMap::new();
687 let mut adj_list: HashMap<String, Vec<String>> = HashMap::new();
688
689 for node_id in nodes.keys() {
691 in_degree.insert(node_id.clone(), 0);
692 adj_list.insert(node_id.clone(), Vec::new());
693 }
694
695 for (node_id, dependencies) in edges {
696 in_degree.insert(node_id.clone(), dependencies.len());
697 for dep in dependencies {
698 if let Some(adj) = adj_list.get_mut(dep) {
699 adj.push(node_id.clone());
700 }
701 }
702 }
703
704 let mut queue = VecDeque::new();
706 let mut topo_order = 0;
707
708 for (node_id, °ree) in &in_degree {
710 if degree == 0 {
711 queue.push_back((node_id.clone(), 0)); }
713 }
714
715 while let Some((node_id, depth)) = queue.pop_front() {
716 if let Some(node) = nodes.get_mut(&node_id) {
718 node.depth = depth;
719 node.topo_order = Some(topo_order);
720 topo_order += 1;
721 }
722
723 if let Some(neighbors) = adj_list.get(&node_id) {
725 for neighbor in neighbors {
726 if let Some(degree) = in_degree.get_mut(neighbor) {
727 *degree -= 1;
728 if *degree == 0 {
729 queue.push_back((neighbor.clone(), depth + 1));
730 }
731 }
732 }
733 }
734 }
735
736 Ok(())
737 }
738
739 fn estimate_flops(&self, op_type: &OperationType, shapes: &[Vec<usize>]) -> Option<u64> {
746 let elements = |s: &Vec<usize>| s.iter().product::<usize>() as u64;
747 match op_type {
748 OperationType::MatMul => {
749 let (a_shape, b_shape) = (shapes.first()?, shapes.get(1)?);
750 if a_shape.len() < 2 || b_shape.len() < 2 {
751 return None;
752 }
753 let m = a_shape[a_shape.len() - 2];
754 let k = a_shape[a_shape.len() - 1];
755 let n = b_shape[b_shape.len() - 1];
756 Some((2 * m * k * n) as u64)
757 },
758 OperationType::Add
759 | OperationType::Subtract
760 | OperationType::Multiply
761 | OperationType::ReLU
762 | OperationType::Sigmoid
763 | OperationType::Tanh => shapes.first().map(elements),
764 OperationType::LayerNorm | OperationType::BatchNorm => {
767 shapes.first().map(|s| elements(s) * 5)
768 },
769 _ => None,
772 }
773 }
774
775 fn estimate_memory(&self, op_type: &OperationType, shapes: &[Vec<usize>]) -> Option<u64> {
778 const ELEMENT_SIZE: u64 = 4;
779 if shapes.is_empty() {
780 return None;
781 }
782 match op_type {
783 OperationType::MatMul => Some(
784 shapes
785 .iter()
786 .map(|s| s.iter().product::<usize>() as u64 * ELEMENT_SIZE)
787 .sum::<u64>(),
788 ),
789 _ => shapes.first().map(|s| s.iter().product::<usize>() as u64 * ELEMENT_SIZE),
790 }
791 }
792
793 fn estimate_parameters(&self, op_type: &OperationType, shapes: &[Vec<usize>]) -> Option<u64> {
802 match op_type {
803 OperationType::MatMul => {
804 let weights = shapes.get(1)?;
805 Some(weights.iter().product::<usize>() as u64)
806 },
807 OperationType::LayerNorm => {
808 let normalised = shapes.first()?;
809 Some(2 * (*normalised.last()?) as u64)
810 },
811 _ => None,
812 }
813 }
814
815 fn analyze_memory_usage(&self, graph: &ComputationGraph) -> Result<MemoryAnalysis> {
816 let total_memory_usage = graph.nodes.values().filter_map(|n| n.memory_usage).sum();
820
821 let mut memory_by_operation: HashMap<OperationType, u64> = HashMap::new();
822 for node in graph.nodes.values() {
823 if let Some(memory) = node.memory_usage {
824 *memory_by_operation.entry(node.operation_type.clone()).or_insert(0) += memory;
825 }
826 }
827
828 let mut memory_hotspots: Vec<(String, u64)> = graph
829 .nodes
830 .values()
831 .filter_map(|n| n.memory_usage.map(|m| (n.id.clone(), m)))
832 .collect();
833 memory_hotspots.sort_by_key(|item| std::cmp::Reverse(item.1));
834 memory_hotspots.truncate(10); let peak_memory_usage = self.compute_peak_memory_usage(graph);
837 let fragmentation_ratio = None;
841
842 let optimization_suggestions = vec![
843 "Consider memory pooling for frequently allocated tensors".to_string(),
844 "Implement in-place operations where possible".to_string(),
845 "Use gradient checkpointing for memory-intensive layers".to_string(),
846 ];
847
848 Ok(MemoryAnalysis {
849 total_memory_usage,
850 peak_memory_usage,
851 memory_by_operation,
852 memory_hotspots,
853 fragmentation_ratio,
854 optimization_suggestions,
855 })
856 }
857
858 fn compute_peak_memory_usage(&self, graph: &ComputationGraph) -> u64 {
868 let mut ordered: Vec<&GraphNode> = graph.nodes.values().collect();
869 ordered.sort_by_key(|n| n.topo_order.unwrap_or(usize::MAX));
870
871 let mut last_use: HashMap<&str, usize> = HashMap::new();
875 for node in &ordered {
876 let Some(topo) = node.topo_order else {
877 continue;
878 };
879 for dep in graph.edges.get(&node.id).into_iter().flatten() {
880 last_use.entry(dep.as_str()).and_modify(|t| *t = (*t).max(topo)).or_insert(topo);
881 }
882 }
883
884 let mut live: u64 = 0;
885 let mut peak: u64 = 0;
886 for node in &ordered {
887 let Some(topo) = node.topo_order else {
888 continue;
889 };
890 live = live.saturating_add(node.memory_usage.unwrap_or(0));
891 peak = peak.max(live);
892 let unique_deps: HashSet<&str> =
899 graph.edges.get(&node.id).into_iter().flatten().map(|s| s.as_str()).collect();
900 for dep in unique_deps {
901 let is_last_use = last_use.get(dep) == Some(&topo);
902 if is_last_use && !graph.leaf_nodes.contains(dep) {
903 if let Some(dep_node) = graph.nodes.get(dep) {
904 live = live.saturating_sub(dep_node.memory_usage.unwrap_or(0));
905 }
906 }
907 }
908 }
909 peak
910 }
911
912 fn analyze_flop_usage(&self, graph: &ComputationGraph) -> Result<FlopAnalysis> {
913 let total_flops = graph.nodes.values().filter_map(|n| n.flop_count).sum();
914
915 let mut flops_by_operation: HashMap<OperationType, u64> = HashMap::new();
916 for node in graph.nodes.values() {
917 if let Some(flops) = node.flop_count {
918 *flops_by_operation.entry(node.operation_type.clone()).or_insert(0) += flops;
919 }
920 }
921
922 let mut compute_hotspots: Vec<(String, u64)> = graph
923 .nodes
924 .values()
925 .filter_map(|n| n.flop_count.map(|f| (n.id.clone(), f)))
926 .collect();
927 compute_hotspots.sort_by_key(|item| std::cmp::Reverse(item.1));
928 compute_hotspots.truncate(10); let total_memory = graph.nodes.values().filter_map(|n| n.memory_usage).sum::<u64>();
931 let arithmetic_intensity =
932 if total_memory > 0 { total_flops as f64 / total_memory as f64 } else { 0.0 };
933
934 let complexity_analysis = ComplexityAnalysis {
935 time_complexity: None,
938 space_complexity: None,
939 parallelization_potential: self.compute_parallelization_potential(graph),
940 sequential_dependencies: graph.metadata.max_depth,
941 };
942
943 Ok(FlopAnalysis {
944 total_flops,
945 flops_by_operation,
946 compute_hotspots,
947 arithmetic_intensity,
948 complexity_analysis,
949 })
950 }
951
952 fn compute_parallelization_potential(&self, graph: &ComputationGraph) -> f64 {
971 let node_count = graph.nodes.len();
972 if node_count == 0 {
973 return 0.0;
974 }
975 let span = graph.metadata.max_depth + 1;
976 (1.0 - span as f64 / node_count as f64).clamp(0.0, 1.0)
977 }
978
979 fn detect_optimization_opportunities(
980 &self,
981 graph: &ComputationGraph,
982 ) -> Result<Vec<OptimizationOpportunity>> {
983 let mut opportunities = Vec::new();
984
985 opportunities.extend(self.detect_fusion_opportunities(graph)?);
987
988 opportunities.extend(self.detect_redundancy_opportunities(graph)?);
990
991 opportunities.extend(self.detect_memory_optimizations(graph)?);
993
994 Ok(opportunities)
995 }
996
997 fn detect_fusion_opportunities(
998 &self,
999 graph: &ComputationGraph,
1000 ) -> Result<Vec<OptimizationOpportunity>> {
1001 let mut opportunities = Vec::new();
1002
1003 for node in graph.nodes.values() {
1005 if let OperationType::Add = node.operation_type {
1006 let empty_deps = vec![];
1007 let dependencies = graph.edges.get(&node.id).unwrap_or(&empty_deps);
1008 for dep in dependencies {
1009 if let Some(dep_node) = graph.nodes.get(dep) {
1010 if let OperationType::MatMul = dep_node.operation_type {
1011 opportunities.push(OptimizationOpportunity {
1012 optimization_type: OptimizationType::OperationFusion,
1013 description:
1014 "Fuse MatMul and Add operations into a single GEMM operation"
1015 .to_string(),
1016 affected_nodes: vec![dep.clone(), node.id.clone()],
1017 estimated_improvement: EstimatedImprovement {
1018 speedup_factor: 1.2,
1019 memory_reduction: 1024 * 1024, energy_savings: 0.1,
1021 },
1022 implementation_difficulty: 2,
1023 priority: OptimizationPriority::Medium,
1024 });
1025 }
1026 }
1027 }
1028 }
1029 }
1030
1031 Ok(opportunities)
1032 }
1033
1034 fn detect_redundancy_opportunities(
1049 &self,
1050 graph: &ComputationGraph,
1051 ) -> Result<Vec<OptimizationOpportunity>> {
1052 let empty_deps: Vec<String> = Vec::new();
1053 let mut signature_groups: HashMap<(&OperationType, &[String]), Vec<&str>> = HashMap::new();
1054 for node in graph.nodes.values() {
1055 let deps = graph.edges.get(&node.id).unwrap_or(&empty_deps);
1056 if deps.is_empty() {
1057 continue; }
1059 signature_groups
1060 .entry((&node.operation_type, deps.as_slice()))
1061 .or_default()
1062 .push(node.id.as_str());
1063 }
1064
1065 let mut opportunities = Vec::new();
1066 for ((op_type, deps), mut node_ids) in signature_groups {
1067 if node_ids.len() < 2 {
1068 continue;
1069 }
1070 node_ids.sort_unstable(); let redundant_count = node_ids.len() - 1;
1073 let per_node_memory = node_ids
1074 .iter()
1075 .filter_map(|id| graph.nodes.get(*id))
1076 .filter_map(|n| n.memory_usage)
1077 .max()
1078 .unwrap_or(0);
1079
1080 opportunities.push(OptimizationOpportunity {
1081 optimization_type: OptimizationType::RedundancyElimination,
1082 description: format!(
1083 "{} node(s) recompute the identical {} over the same {} input(s); keep one \
1084 and reuse its output for the other {}",
1085 node_ids.len(),
1086 op_type,
1087 deps.len(),
1088 redundant_count,
1089 ),
1090 affected_nodes: node_ids.iter().map(|s| s.to_string()).collect(),
1091 estimated_improvement: EstimatedImprovement {
1092 speedup_factor: node_ids.len() as f64,
1097 memory_reduction: per_node_memory * redundant_count as u64,
1098 energy_savings: (redundant_count as f64 / node_ids.len() as f64)
1099 .clamp(0.0, 1.0),
1100 },
1101 implementation_difficulty: 2,
1102 priority: if redundant_count >= 3 {
1103 OptimizationPriority::High
1104 } else {
1105 OptimizationPriority::Medium
1106 },
1107 });
1108 }
1109
1110 opportunities.sort_by(|a, b| a.affected_nodes.cmp(&b.affected_nodes));
1111 Ok(opportunities)
1112 }
1113
1114 fn detect_memory_optimizations(
1115 &self,
1116 graph: &ComputationGraph,
1117 ) -> Result<Vec<OptimizationOpportunity>> {
1118 let mut opportunities = Vec::new();
1119
1120 for node in graph.nodes.values() {
1122 let Some(node_memory) = node.memory_usage else {
1123 continue;
1126 };
1127 if node_memory > self.config.large_memory_threshold {
1128 opportunities.push(OptimizationOpportunity {
1129 optimization_type: OptimizationType::MemoryLayoutOptimization,
1130 description: format!(
1131 "Optimize memory layout for large operation: {}",
1132 node.name
1133 ),
1134 affected_nodes: vec![node.id.clone()],
1135 estimated_improvement: EstimatedImprovement {
1136 speedup_factor: 1.1,
1137 memory_reduction: node_memory / 4, energy_savings: 0.05,
1139 },
1140 implementation_difficulty: 3,
1141 priority: OptimizationPriority::Medium,
1142 });
1143 }
1144 }
1145
1146 Ok(opportunities)
1147 }
1148
1149 fn analyze_bottlenecks(&self, graph: &ComputationGraph) -> Result<BottleneckAnalysis> {
1150 let mut bottleneck_nodes = Vec::new();
1151 let mut parallelizable_nodes = Vec::new();
1152
1153 for node in graph.nodes.values() {
1154 if let Some(exec_time) = node.execution_time_us {
1155 if exec_time > self.config.bottleneck_threshold_us {
1156 bottleneck_nodes.push(node.id.clone());
1157 }
1158 }
1159
1160 match node.operation_type {
1162 OperationType::MatMul | OperationType::Conv2D | OperationType::Add => {
1163 parallelizable_nodes.push(node.id.clone());
1164 },
1165 _ => {},
1166 }
1167 }
1168
1169 let critical_path_nodes = self.find_critical_path(graph)?;
1170 let critical_path_time_us = critical_path_nodes
1171 .iter()
1172 .filter_map(|id| graph.nodes.get(id))
1173 .filter_map(|node| node.execution_time_us)
1174 .sum();
1175
1176 let scheduling_suggestions = vec![
1177 "Consider parallel execution of independent operations".to_string(),
1178 "Use asynchronous execution for I/O operations".to_string(),
1179 "Implement pipeline parallelism for sequential operations".to_string(),
1180 ];
1181
1182 Ok(BottleneckAnalysis {
1183 bottleneck_nodes,
1184 critical_path_nodes,
1185 critical_path_time_us,
1186 parallelizable_nodes,
1187 scheduling_suggestions,
1188 })
1189 }
1190
1191 fn analyze_dataflow(&self, graph: &ComputationGraph) -> Result<DataFlowAnalysis> {
1192 let mut data_dependencies = HashMap::new();
1193 let mut live_variables = HashMap::new();
1194 for (node_id, dependencies) in &graph.edges {
1195 data_dependencies.insert(node_id.clone(), dependencies.clone());
1196 live_variables.insert(node_id.clone(), dependencies.iter().cloned().collect());
1197 }
1198
1199 let mut ordered: Vec<&GraphNode> = graph.nodes.values().collect();
1210 ordered.sort_by_key(|n| n.topo_order.unwrap_or(usize::MAX));
1211
1212 let mut consumers_by_dep: HashMap<&str, Vec<(usize, &str)>> = HashMap::new();
1213 for node in &ordered {
1214 let Some(topo) = node.topo_order else {
1215 continue;
1216 };
1217 for dep in graph.edges.get(&node.id).into_iter().flatten() {
1218 consumers_by_dep.entry(dep.as_str()).or_default().push((topo, node.id.as_str()));
1219 }
1220 }
1221 let max_topo = ordered.iter().filter_map(|n| n.topo_order).max().unwrap_or(0);
1222
1223 let mut variable_lifetimes = HashMap::new();
1224 let mut birth_death_topo: HashMap<&str, (usize, usize)> = HashMap::new();
1225 for node in &ordered {
1226 let Some(birth_topo) = node.topo_order else {
1227 continue;
1228 };
1229 let mut consumers = consumers_by_dep.get(node.id.as_str()).cloned().unwrap_or_default();
1230 consumers.sort(); let death_topo = if graph.leaf_nodes.contains(&node.id) {
1233 max_topo
1234 } else {
1235 consumers.iter().map(|&(t, _)| t).max().unwrap_or(birth_topo)
1236 };
1237 let death_node = consumers
1242 .iter()
1243 .find(|&&(t, _)| t == death_topo)
1244 .map(|&(_, id)| id.to_string())
1245 .unwrap_or_else(|| node.id.clone());
1246
1247 birth_death_topo.insert(node.id.as_str(), (birth_topo, death_topo));
1248 variable_lifetimes.insert(
1249 node.id.clone(),
1250 VariableLifetime {
1251 birth_node: node.id.clone(),
1252 death_node,
1253 usage_nodes: consumers.iter().map(|&(_, id)| id.to_string()).collect(),
1254 memory_footprint: node.memory_usage.unwrap_or(0),
1255 },
1256 );
1257 }
1258
1259 let memory_reuse_opportunities =
1260 self.find_memory_reuse_opportunities(graph, &ordered, &birth_death_topo);
1261
1262 Ok(DataFlowAnalysis {
1263 data_dependencies,
1264 live_variables,
1265 variable_lifetimes,
1266 memory_reuse_opportunities,
1267 })
1268 }
1269
1270 fn find_memory_reuse_opportunities(
1287 &self,
1288 graph: &ComputationGraph,
1289 ordered: &[&GraphNode],
1290 birth_death_topo: &HashMap<&str, (usize, usize)>,
1291 ) -> Vec<MemoryReuseOpportunity> {
1292 const REUSE_CANDIDATE_LIMIT: usize = 200;
1293
1294 let mut candidates: Vec<&GraphNode> = ordered
1295 .iter()
1296 .filter(|n| n.memory_usage.is_some_and(|m| m > 0) && !graph.leaf_nodes.contains(&n.id))
1297 .copied()
1298 .collect();
1299 candidates.sort_by_key(|n| std::cmp::Reverse(n.memory_usage.unwrap_or(0)));
1300 candidates.truncate(REUSE_CANDIDATE_LIMIT);
1301
1302 let mut opportunities = Vec::new();
1303 for (i, &a) in candidates.iter().enumerate() {
1304 let Some(&(a_birth, a_death)) = birth_death_topo.get(a.id.as_str()) else {
1305 continue;
1306 };
1307 for &b in &candidates[i + 1..] {
1308 let Some(&(b_birth, b_death)) = birth_death_topo.get(b.id.as_str()) else {
1309 continue;
1310 };
1311 let non_overlapping = a_death < b_birth || b_death < a_birth;
1316 if !non_overlapping {
1317 continue;
1318 }
1319 let savings = a.memory_usage.unwrap_or(0).min(b.memory_usage.unwrap_or(0));
1320 if savings == 0 {
1321 continue;
1322 }
1323 let mut reusable_variables = vec![a.id.clone(), b.id.clone()];
1324 reusable_variables.sort();
1325 opportunities.push(MemoryReuseOpportunity {
1326 reusable_variables,
1327 memory_savings: savings,
1328 complexity: 2,
1329 });
1330 }
1331 }
1332
1333 opportunities.sort_by_key(|o| std::cmp::Reverse(o.memory_savings));
1334 opportunities.truncate(10);
1335 opportunities
1336 }
1337
1338 fn find_critical_path(&self, graph: &ComputationGraph) -> Result<Vec<String>> {
1354 let mut ordered: Vec<&GraphNode> = graph.nodes.values().collect();
1355 ordered.sort_by_key(|n| n.topo_order.unwrap_or(usize::MAX));
1356
1357 let use_time = graph.nodes.values().any(|n| n.execution_time_us.is_some());
1358 let weight = |node: &GraphNode| -> f64 {
1359 if use_time {
1360 node.execution_time_us.unwrap_or(0) as f64
1361 } else {
1362 node.flop_count.unwrap_or(0) as f64
1365 }
1366 };
1367
1368 let mut best_cost: HashMap<&str, f64> = HashMap::new();
1372 let mut predecessor: HashMap<&str, &str> = HashMap::new();
1373
1374 for node in &ordered {
1375 if node.topo_order.is_none() {
1376 continue;
1377 }
1378 let mut best_dep_cost = 0.0_f64;
1379 let mut best_dep: Option<&str> = None;
1380 for dep in graph.edges.get(&node.id).into_iter().flatten() {
1381 if let Some(&cost) = best_cost.get(dep.as_str()) {
1382 let better = match best_dep {
1386 None => true,
1387 Some(bd) => {
1388 cost > best_dep_cost || (cost == best_dep_cost && dep.as_str() > bd)
1389 },
1390 };
1391 if better {
1392 best_dep_cost = cost;
1393 best_dep = Some(dep.as_str());
1394 }
1395 }
1396 }
1397 best_cost.insert(node.id.as_str(), weight(node) + best_dep_cost);
1398 if let Some(dep) = best_dep {
1399 predecessor.insert(node.id.as_str(), dep);
1400 }
1401 }
1402
1403 let Some((&end_node, _)) = best_cost.iter().max_by(|a, b| {
1408 a.1.partial_cmp(b.1)
1409 .unwrap_or(std::cmp::Ordering::Equal)
1410 .then_with(|| a.0.cmp(b.0))
1411 }) else {
1412 return Ok(Vec::new());
1413 };
1414
1415 let mut path = vec![end_node.to_string()];
1416 let mut current = end_node;
1417 while let Some(&pred) = predecessor.get(current) {
1418 path.push(pred.to_string());
1419 current = pred;
1420 }
1421 path.reverse();
1422 Ok(path)
1423 }
1424
1425 fn calculate_statistics(&self, graph: &ComputationGraph) -> Result<GraphStatistics> {
1426 let mut nodes_by_type: HashMap<OperationType, usize> = HashMap::new();
1427 for node in graph.nodes.values() {
1428 *nodes_by_type.entry(node.operation_type.clone()).or_insert(0) += 1;
1429 }
1430
1431 let total_fan_in: usize = graph.edges.values().map(|deps| deps.len()).sum();
1432 let total_fan_out = total_fan_in; let average_fan_in = total_fan_in as f64 / graph.nodes.len() as f64;
1434 let average_fan_out = total_fan_out as f64 / graph.nodes.len() as f64;
1435
1436 Ok(GraphStatistics {
1437 nodes_by_type,
1438 average_fan_in,
1439 average_fan_out,
1440 diameter: graph.metadata.max_depth,
1441 clustering_coefficient: self.compute_clustering_coefficient(graph),
1442 strongly_connected_components: graph.nodes.len(), })
1444 }
1445
1446 fn compute_clustering_coefficient(&self, graph: &ComputationGraph) -> f64 {
1464 if graph.nodes.is_empty() {
1465 return 0.0;
1466 }
1467
1468 let mut neighbors: HashMap<&str, HashSet<&str>> = HashMap::new();
1469 for node_id in graph.nodes.keys() {
1470 neighbors.entry(node_id.as_str()).or_default();
1471 }
1472 for (node_id, deps) in &graph.edges {
1473 for dep in deps {
1474 neighbors.entry(node_id.as_str()).or_default().insert(dep.as_str());
1475 neighbors.entry(dep.as_str()).or_default().insert(node_id.as_str());
1476 }
1477 }
1478
1479 let mut coefficient_sum = 0.0;
1480 for neighs in neighbors.values() {
1481 let k = neighs.len();
1482 if k < 2 {
1483 continue; }
1485 let neigh_vec: Vec<&str> = neighs.iter().copied().collect();
1486 let mut connected_pairs = 0usize;
1487 for (i, &a) in neigh_vec.iter().enumerate() {
1488 for &b in &neigh_vec[i + 1..] {
1489 if neighbors.get(a).is_some_and(|n| n.contains(b)) {
1490 connected_pairs += 1;
1491 }
1492 }
1493 }
1494 let possible_pairs = k * (k - 1) / 2;
1495 coefficient_sum += connected_pairs as f64 / possible_pairs as f64;
1496 }
1497
1498 coefficient_sum / graph.nodes.len() as f64
1499 }
1500
1501 fn generate_recommendations(&self, analysis: &GraphAnalysisResult) -> Result<Vec<String>> {
1502 let mut recommendations = Vec::new();
1503
1504 if let Some(ref memory_analysis) = analysis.memory_analysis {
1506 if memory_analysis.total_memory_usage > 1024 * 1024 * 1024 {
1507 recommendations.push(
1509 "Consider using gradient checkpointing to reduce memory usage".to_string(),
1510 );
1511 }
1512 if let Some(ratio) = memory_analysis.fragmentation_ratio {
1513 if ratio > 0.2 {
1514 recommendations
1515 .push("Implement memory pooling to reduce fragmentation".to_string());
1516 }
1517 }
1518 }
1519
1520 if let Some(ref flop_analysis) = analysis.flop_analysis {
1522 if flop_analysis.arithmetic_intensity < 1.0 {
1523 recommendations
1524 .push("Consider kernel fusion to improve arithmetic intensity".to_string());
1525 }
1526 if flop_analysis.complexity_analysis.parallelization_potential > 0.5 {
1527 recommendations.push(
1528 "Explore parallelization opportunities for compute-intensive operations"
1529 .to_string(),
1530 );
1531 }
1532 }
1533
1534 if analysis.optimization_opportunities.len() > 3 {
1536 recommendations.push(
1537 "Multiple optimization opportunities detected - prioritize by estimated impact"
1538 .to_string(),
1539 );
1540 }
1541
1542 if let Some(ref bottleneck_analysis) = analysis.bottleneck_analysis {
1544 if !bottleneck_analysis.bottleneck_nodes.is_empty() {
1545 recommendations.push(
1546 "Address bottleneck operations through optimization or parallelization"
1547 .to_string(),
1548 );
1549 }
1550 }
1551
1552 Ok(recommendations)
1553 }
1554
1555 fn get_node_color(&self, op_type: &OperationType) -> &'static str {
1556 match op_type {
1557 OperationType::MatMul | OperationType::Dot => "lightblue",
1558 OperationType::Add
1559 | OperationType::Subtract
1560 | OperationType::Multiply
1561 | OperationType::Divide => "lightgreen",
1562 OperationType::ReLU
1563 | OperationType::Sigmoid
1564 | OperationType::Tanh
1565 | OperationType::GELU => "orange",
1566 OperationType::LayerNorm | OperationType::BatchNorm | OperationType::RMSNorm => {
1567 "yellow"
1568 },
1569 OperationType::Conv1D | OperationType::Conv2D | OperationType::Conv3D => "lightcoral",
1570 OperationType::Attention | OperationType::MultiHeadAttention => "purple",
1571 OperationType::Embedding | OperationType::PositionalEmbedding => "pink",
1572 _ => "lightgray",
1573 }
1574 }
1575}
1576
1577impl fmt::Display for OperationType {
1578 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1579 match self {
1580 OperationType::Custom(name) => write!(f, "Custom({})", name),
1581 _ => write!(f, "{:?}", self),
1582 }
1583 }
1584}
1585
1586impl Default for ComputationGraphAnalyzer {
1587 fn default() -> Self {
1588 Self::new(GraphAnalysisConfig::default())
1589 }
1590}
1591
1592#[cfg(test)]
1593#[path = "computation_graph_tests.rs"]
1594mod tests;