Skip to main content

torsh_jit/
analysis.rs

1//! Analysis utilities for JIT compilation
2
3use crate::graph::{ComputationGraph, Node, NodeId, Operation};
4use crate::{JitError, JitResult};
5use petgraph::visit::EdgeRef;
6use std::collections::{HashMap, HashSet};
7use std::hash::{Hash, Hasher};
8
9/// Analysis results for a computation graph
10#[derive(Debug, Clone)]
11pub struct GraphAnalysis {
12    /// Memory usage per node
13    pub memory_usage: HashMap<NodeId, MemoryInfo>,
14
15    /// Computational complexity per node
16    pub compute_cost: HashMap<NodeId, ComputeCost>,
17
18    /// Data dependencies
19    pub dependencies: DependencyInfo,
20
21    /// Critical path through the graph
22    pub critical_path: Vec<NodeId>,
23
24    /// Parallelization opportunities
25    pub parallel_groups: Vec<Vec<NodeId>>,
26}
27
28/// Memory usage information
29#[derive(Debug, Clone)]
30pub struct MemoryInfo {
31    /// Output tensor size in bytes
32    pub output_size: usize,
33
34    /// Temporary memory required
35    pub temp_size: usize,
36
37    /// Total memory footprint
38    pub total_size: usize,
39
40    /// Memory access pattern
41    pub access_pattern: AccessPattern,
42}
43
44/// Memory access pattern
45#[derive(Debug, Clone, PartialEq)]
46pub enum AccessPattern {
47    Sequential,
48    Strided { stride: usize },
49    Random,
50    Broadcast,
51}
52
53/// Computational cost estimation
54#[derive(Debug, Clone)]
55pub struct ComputeCost {
56    /// Floating point operations
57    pub flops: u64,
58
59    /// Memory operations (loads + stores)
60    pub memory_ops: u64,
61
62    /// Estimated cycles (device-specific)
63    pub cycles: u64,
64
65    /// Operation intensity (flops / memory_ops)
66    pub intensity: f32,
67}
68
69/// Dependency information
70#[derive(Debug, Clone)]
71pub struct DependencyInfo {
72    /// Direct dependencies (node -> predecessors)
73    pub direct: HashMap<NodeId, Vec<NodeId>>,
74
75    /// Transitive dependencies (node -> all ancestors)
76    pub transitive: HashMap<NodeId, HashSet<NodeId>>,
77
78    /// Dependency depth for each node
79    pub depth: HashMap<NodeId, usize>,
80}
81
82/// Graph analyzer
83pub struct GraphAnalyzer;
84
85impl GraphAnalyzer {
86    /// Analyze a computation graph
87    pub fn analyze(graph: &ComputationGraph) -> JitResult<GraphAnalysis> {
88        let memory_usage = Self::analyze_memory(graph)?;
89        let compute_cost = Self::analyze_compute(graph)?;
90        let dependencies = Self::analyze_dependencies(graph)?;
91        let critical_path = Self::find_critical_path(graph, &compute_cost)?;
92        let parallel_groups = Self::find_parallel_groups(graph, &dependencies)?;
93
94        Ok(GraphAnalysis {
95            memory_usage,
96            compute_cost,
97            dependencies,
98            critical_path,
99            parallel_groups,
100        })
101    }
102
103    /// Analyze memory usage
104    fn analyze_memory(graph: &ComputationGraph) -> JitResult<HashMap<NodeId, MemoryInfo>> {
105        let mut memory_info = HashMap::new();
106
107        for (node_id, node) in graph.nodes() {
108            let info = Self::compute_memory_info(node)?;
109            memory_info.insert(node_id, info);
110        }
111
112        Ok(memory_info)
113    }
114
115    /// Compute memory info for a node
116    fn compute_memory_info(node: &Node) -> JitResult<MemoryInfo> {
117        let element_size = match node.dtype {
118            torsh_core::DType::F32 => 4,
119            torsh_core::DType::F64 => 8,
120            torsh_core::DType::I32 => 4,
121            torsh_core::DType::I64 => 8,
122            torsh_core::DType::I8 => 1,
123            torsh_core::DType::U8 => 1,
124            torsh_core::DType::U32 => 4,
125            torsh_core::DType::U64 => 8,
126            torsh_core::DType::Bool => 1,
127            torsh_core::DType::F16 | torsh_core::DType::BF16 | torsh_core::DType::I16 => 2,
128            torsh_core::DType::C64 => 8,
129            torsh_core::DType::C128 => 16,
130            torsh_core::DType::QInt8 | torsh_core::DType::QUInt8 => 1,
131            torsh_core::DType::QInt32 => 4, // Quantized 32-bit type
132        };
133
134        let num_elements = node.output_shape.numel();
135        let output_size = num_elements * element_size;
136
137        // Estimate temporary memory based on operation
138        let (temp_size, access_pattern) = match &node.op {
139            Operation::MatMul | Operation::BatchMatMul => {
140                // Matrix multiplication may need temporary storage
141                (output_size, AccessPattern::Sequential)
142            }
143            Operation::Conv2d(_) => {
144                // Convolution needs im2col buffer
145                (output_size * 2, AccessPattern::Strided { stride: 1 })
146            }
147            Operation::Transpose { .. } => (0, AccessPattern::Strided { stride: 1 }),
148            Operation::Sum { .. } | Operation::Mean { .. } => {
149                (element_size * 1024, AccessPattern::Sequential) // Small temp buffer
150            }
151            _ => (0, AccessPattern::Sequential),
152        };
153
154        Ok(MemoryInfo {
155            output_size,
156            temp_size,
157            total_size: output_size + temp_size,
158            access_pattern,
159        })
160    }
161
162    /// Analyze computational cost
163    fn analyze_compute(graph: &ComputationGraph) -> JitResult<HashMap<NodeId, ComputeCost>> {
164        let mut compute_costs = HashMap::new();
165
166        for (node_id, node) in graph.nodes() {
167            let cost = Self::estimate_compute_cost(node)?;
168            compute_costs.insert(node_id, cost);
169        }
170
171        Ok(compute_costs)
172    }
173
174    /// Estimate computational cost for a node
175    fn estimate_compute_cost(node: &Node) -> JitResult<ComputeCost> {
176        let num_elements = node.output_shape.numel();
177
178        let (flops, memory_ops) = match &node.op {
179            // Element-wise operations
180            Operation::Add | Operation::Sub => (num_elements as u64, num_elements as u64 * 3),
181            Operation::Mul | Operation::Div => (num_elements as u64, num_elements as u64 * 3),
182            Operation::Exp | Operation::Log | Operation::Sqrt => {
183                (num_elements as u64 * 10, num_elements as u64 * 2)
184            }
185            Operation::Sin | Operation::Cos => (num_elements as u64 * 20, num_elements as u64 * 2),
186
187            // Activations
188            Operation::Relu => (num_elements as u64, num_elements as u64 * 2),
189            Operation::Sigmoid | Operation::Tanh => {
190                (num_elements as u64 * 5, num_elements as u64 * 2)
191            }
192            Operation::Gelu => (num_elements as u64 * 10, num_elements as u64 * 2),
193
194            // Matrix operations
195            Operation::MatMul => {
196                // Assuming shape is [M, K] x [K, N] -> [M, N]
197                if node.output_shape.ndim() >= 2 {
198                    let dims = node.output_shape.dims();
199                    let m = dims[dims.len() - 2];
200                    let n = dims[dims.len() - 1];
201                    let k = m; // Estimate, would need input shapes
202                    ((2 * m * n * k) as u64, (m * k + k * n + m * n) as u64)
203                } else {
204                    (num_elements as u64, num_elements as u64 * 2)
205                }
206            }
207
208            // Reductions
209            Operation::Sum { .. } | Operation::Mean { .. } => {
210                (num_elements as u64, num_elements as u64 + 1)
211            }
212
213            // Convolution
214            Operation::Conv2d(info) => {
215                // FLOPs = 2 * output_size * kernel_size * in_channels
216                let kernel_ops = info.kernel_size.0 * info.kernel_size.1 * info.in_channels;
217                (
218                    num_elements as u64 * kernel_ops as u64 * 2,
219                    num_elements as u64 * 3,
220                )
221            }
222
223            _ => (num_elements as u64, num_elements as u64 * 2),
224        };
225
226        let intensity = if memory_ops > 0 {
227            flops as f32 / memory_ops as f32
228        } else {
229            0.0
230        };
231
232        // Simple cycle estimation (would be device-specific in practice)
233        let cycles = flops.max(memory_ops * 4);
234
235        Ok(ComputeCost {
236            flops,
237            memory_ops,
238            cycles,
239            intensity,
240        })
241    }
242
243    /// Analyze dependencies
244    fn analyze_dependencies(graph: &ComputationGraph) -> JitResult<DependencyInfo> {
245        let mut direct: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
246        let mut transitive: HashMap<NodeId, HashSet<NodeId>> = HashMap::new();
247        let mut depth: HashMap<NodeId, usize> = HashMap::new();
248
249        // Get topological order
250        let order = graph
251            .topological_sort()
252            .map_err(|e| JitError::GraphError(format!("{:?}", e)))?;
253
254        // Build dependency information
255        for &node_id in &order {
256            // Direct dependencies
257            let preds: Vec<_> = graph.predecessors(node_id).collect();
258            direct.insert(node_id, preds.clone());
259
260            // Transitive dependencies
261            let mut trans_deps = HashSet::new();
262            for &pred in &preds {
263                trans_deps.insert(pred);
264                if let Some(pred_trans) = transitive.get(&pred) {
265                    trans_deps.extend(pred_trans);
266                }
267            }
268            transitive.insert(node_id, trans_deps);
269
270            // Depth
271            let node_depth = preds
272                .iter()
273                .map(|&p| depth.get(&p).copied().unwrap_or(0))
274                .max()
275                .unwrap_or(0)
276                + 1;
277            depth.insert(node_id, node_depth);
278        }
279
280        Ok(DependencyInfo {
281            direct,
282            transitive,
283            depth,
284        })
285    }
286
287    /// Find the critical path through the graph
288    fn find_critical_path(
289        graph: &ComputationGraph,
290        compute_costs: &HashMap<NodeId, ComputeCost>,
291    ) -> JitResult<Vec<NodeId>> {
292        let mut distances = HashMap::new();
293        let mut predecessors = HashMap::new();
294
295        // Initialize distances
296        for (node_id, _) in graph.nodes() {
297            distances.insert(node_id, 0u64);
298        }
299
300        // Compute longest path using topological order
301        let order = graph
302            .topological_sort()
303            .map_err(|e| JitError::GraphError(format!("{:?}", e)))?;
304
305        for &node_id in &order {
306            let node_cost = compute_costs.get(&node_id).map(|c| c.cycles).unwrap_or(0);
307
308            let current_dist = distances[&node_id] + node_cost;
309
310            // Update distances to successors
311            for succ_id in graph.successors(node_id) {
312                let succ_dist = distances.get(&succ_id).copied().unwrap_or(0);
313
314                if current_dist > succ_dist {
315                    distances.insert(succ_id, current_dist);
316                    predecessors.insert(succ_id, node_id);
317                }
318            }
319        }
320
321        // Find the output with maximum distance
322        let mut end_node = None;
323        let mut max_dist = 0;
324
325        for &output in &graph.outputs {
326            if let Some(&dist) = distances.get(&output) {
327                if dist > max_dist {
328                    max_dist = dist;
329                    end_node = Some(output);
330                }
331            }
332        }
333
334        // Reconstruct path
335        let mut path = Vec::new();
336        let mut current = end_node;
337
338        while let Some(node) = current {
339            path.push(node);
340            current = predecessors.get(&node).copied();
341        }
342
343        path.reverse();
344        Ok(path)
345    }
346
347    /// Find groups of nodes that can be executed in parallel
348    fn find_parallel_groups(
349        _graph: &ComputationGraph,
350        dependencies: &DependencyInfo,
351    ) -> JitResult<Vec<Vec<NodeId>>> {
352        let mut groups = Vec::new();
353        let mut assigned = HashSet::new();
354
355        // Group nodes by depth
356        let mut depth_groups: HashMap<usize, Vec<NodeId>> = HashMap::new();
357        for (&node_id, &depth) in &dependencies.depth {
358            depth_groups.entry(depth).or_default().push(node_id);
359        }
360
361        // Create parallel groups from each depth level
362        let mut depths: Vec<_> = depth_groups.keys().copied().collect();
363        depths.sort();
364
365        for depth in depths {
366            if let Some(nodes) = depth_groups.get(&depth) {
367                let mut current_group = Vec::new();
368
369                for &node in nodes {
370                    if !assigned.contains(&node) {
371                        // Check if node can be added to current group
372                        let can_add = current_group.iter().all(|&other| {
373                            !Self::has_dependency(dependencies, node, other)
374                                && !Self::has_dependency(dependencies, other, node)
375                        });
376
377                        if can_add {
378                            current_group.push(node);
379                            assigned.insert(node);
380                        }
381                    }
382                }
383
384                if !current_group.is_empty() {
385                    groups.push(current_group);
386                }
387            }
388        }
389
390        Ok(groups)
391    }
392
393    /// Check if node1 depends on node2
394    fn has_dependency(dependencies: &DependencyInfo, node1: NodeId, node2: NodeId) -> bool {
395        dependencies
396            .transitive
397            .get(&node1)
398            .map(|deps| deps.contains(&node2))
399            .unwrap_or(false)
400    }
401}
402
403/// Data flow analysis for optimization opportunities
404#[derive(Debug, Clone)]
405pub struct DataFlowAnalysis {
406    /// Variable definitions: which node defines each variable
407    pub definitions: HashMap<String, NodeId>,
408
409    /// Variable uses: which nodes use each variable
410    pub uses: HashMap<String, Vec<NodeId>>,
411
412    /// Live variables at each node
413    pub live_variables: HashMap<NodeId, HashSet<String>>,
414
415    /// Reaching definitions for each node
416    pub reaching_definitions: HashMap<NodeId, HashMap<String, NodeId>>,
417
418    /// Available expressions at each node
419    pub available_expressions: HashMap<NodeId, HashSet<Expression>>,
420
421    /// Dead code nodes
422    pub dead_code: Vec<NodeId>,
423
424    /// Common subexpressions
425    pub common_subexpressions: Vec<CommonSubexpression>,
426}
427
428/// Expression representation for CSE analysis
429#[derive(Debug, Clone, PartialEq, Eq)]
430pub struct Expression {
431    /// Operation type
432    pub operation: String,
433    /// Input variables
434    pub inputs: Vec<String>,
435    /// Additional attributes for matching
436    pub attributes: HashMap<String, String>,
437}
438
439impl Hash for Expression {
440    fn hash<H: Hasher>(&self, state: &mut H) {
441        self.operation.hash(state);
442        self.inputs.hash(state);
443        // Hash attributes by sorting keys to ensure consistent order
444        let mut attr_pairs: Vec<_> = self.attributes.iter().collect();
445        attr_pairs.sort_by_key(|(k, _)| *k);
446        attr_pairs.hash(state);
447    }
448}
449
450/// Common subexpression that can be eliminated
451#[derive(Debug, Clone)]
452pub struct CommonSubexpression {
453    /// The expression
454    pub expression: Expression,
455    /// Nodes that compute this expression
456    pub instances: Vec<NodeId>,
457    /// Potential savings (memory, compute)
458    pub savings: OptimizationSavings,
459}
460
461/// Savings from applying an optimization
462#[derive(Debug, Clone)]
463pub struct OptimizationSavings {
464    /// Memory savings in bytes
465    pub memory_bytes: usize,
466    /// Compute savings in FLOPs
467    pub compute_flops: u64,
468    /// Estimated speedup factor
469    pub speedup_factor: f32,
470}
471
472/// Data flow analyzer
473pub struct DataFlowAnalyzer;
474
475impl DataFlowAnalyzer {
476    /// Perform complete data flow analysis
477    pub fn analyze(graph: &ComputationGraph) -> JitResult<DataFlowAnalysis> {
478        let mut analysis = DataFlowAnalysis {
479            definitions: HashMap::new(),
480            uses: HashMap::new(),
481            live_variables: HashMap::new(),
482            reaching_definitions: HashMap::new(),
483            available_expressions: HashMap::new(),
484            dead_code: Vec::new(),
485            common_subexpressions: Vec::new(),
486        };
487
488        // Build def-use chains
489        Self::build_def_use_chains(graph, &mut analysis)?;
490
491        // Compute live variables
492        Self::compute_live_variables(graph, &mut analysis)?;
493
494        // Compute reaching definitions
495        Self::compute_reaching_definitions(graph, &mut analysis)?;
496
497        // Find available expressions
498        Self::compute_available_expressions(graph, &mut analysis)?;
499
500        // Identify dead code
501        Self::identify_dead_code(graph, &mut analysis)?;
502
503        // Find common subexpressions
504        Self::find_common_subexpressions(graph, &mut analysis)?;
505
506        Ok(analysis)
507    }
508
509    /// Build definition-use chains
510    fn build_def_use_chains(
511        graph: &ComputationGraph,
512        analysis: &mut DataFlowAnalysis,
513    ) -> JitResult<()> {
514        for (node_id, node) in graph.nodes() {
515            let var_name = node.name.clone();
516
517            // This node defines the variable
518            analysis.definitions.insert(var_name.clone(), node_id);
519
520            // Find which variables this node uses
521            let used_vars = Self::get_input_variables(graph, node_id);
522            for var in used_vars {
523                analysis.uses.entry(var).or_default().push(node_id);
524            }
525        }
526        Ok(())
527    }
528
529    /// Get input variables for a node
530    fn get_input_variables(graph: &ComputationGraph, node_id: NodeId) -> Vec<String> {
531        let mut vars = Vec::new();
532
533        // Get predecessors and their variable names
534        for edge in graph.edges_directed(node_id, petgraph::Direction::Incoming) {
535            if let Some(pred_node) = graph.node(edge.source()) {
536                vars.push(pred_node.name.clone());
537            }
538        }
539
540        vars
541    }
542
543    /// Compute live variables using backward data flow analysis
544    fn compute_live_variables(
545        graph: &ComputationGraph,
546        analysis: &mut DataFlowAnalysis,
547    ) -> JitResult<()> {
548        let topo_order = graph
549            .topological_sort()
550            .map_err(|e| JitError::AnalysisError(format!("Topological sort failed: {}", e)))?;
551
552        // Initialize with empty sets
553        for &node_id in &topo_order {
554            analysis.live_variables.insert(node_id, HashSet::new());
555        }
556
557        // Backward pass
558        let mut changed = true;
559        while changed {
560            changed = false;
561
562            for &node_id in topo_order.iter().rev() {
563                let mut new_live = HashSet::new();
564
565                // Add variables used by successors
566                for edge in graph.edges_directed(node_id, petgraph::Direction::Outgoing) {
567                    let succ_id = edge.target();
568                    let used_vars = Self::get_input_variables(graph, succ_id);
569                    for var in used_vars {
570                        new_live.insert(var);
571                    }
572
573                    if let Some(succ_live) = analysis.live_variables.get(&succ_id) {
574                        new_live.extend(succ_live.clone());
575                    }
576                }
577
578                // Remove variable defined by this node
579                if let Some(node) = graph.node(node_id) {
580                    new_live.remove(&node.name);
581                }
582
583                // Add variables used by this node
584                let used_vars = Self::get_input_variables(graph, node_id);
585                for var in used_vars {
586                    new_live.insert(var);
587                }
588
589                if analysis.live_variables.get(&node_id) != Some(&new_live) {
590                    analysis.live_variables.insert(node_id, new_live);
591                    changed = true;
592                }
593            }
594        }
595
596        Ok(())
597    }
598
599    /// Compute reaching definitions
600    fn compute_reaching_definitions(
601        graph: &ComputationGraph,
602        analysis: &mut DataFlowAnalysis,
603    ) -> JitResult<()> {
604        let topo_order = graph
605            .topological_sort()
606            .map_err(|e| JitError::AnalysisError(format!("Topological sort failed: {}", e)))?;
607
608        // Initialize
609        for &node_id in &topo_order {
610            analysis
611                .reaching_definitions
612                .insert(node_id, HashMap::new());
613        }
614
615        // Forward pass
616        let mut changed = true;
617        while changed {
618            changed = false;
619
620            for &node_id in &topo_order {
621                let mut new_defs = HashMap::new();
622
623                // Union of reaching definitions from predecessors
624                for edge in graph.edges_directed(node_id, petgraph::Direction::Incoming) {
625                    let pred_id = edge.source();
626                    if let Some(pred_defs) = analysis.reaching_definitions.get(&pred_id) {
627                        for (var, &def_node) in pred_defs {
628                            new_defs.insert(var.clone(), def_node);
629                        }
630                    }
631                }
632
633                // This node defines a variable
634                if let Some(node) = graph.node(node_id) {
635                    new_defs.insert(node.name.clone(), node_id);
636                }
637
638                if analysis.reaching_definitions.get(&node_id) != Some(&new_defs) {
639                    analysis.reaching_definitions.insert(node_id, new_defs);
640                    changed = true;
641                }
642            }
643        }
644
645        Ok(())
646    }
647
648    /// Compute available expressions
649    fn compute_available_expressions(
650        graph: &ComputationGraph,
651        analysis: &mut DataFlowAnalysis,
652    ) -> JitResult<()> {
653        let topo_order = graph
654            .topological_sort()
655            .map_err(|e| JitError::AnalysisError(format!("Topological sort failed: {}", e)))?;
656
657        // Initialize
658        for &node_id in &topo_order {
659            analysis
660                .available_expressions
661                .insert(node_id, HashSet::new());
662        }
663
664        // Forward pass
665        let mut changed = true;
666        while changed {
667            changed = false;
668
669            for &node_id in &topo_order {
670                let mut new_exprs = HashSet::new();
671
672                // Intersection of available expressions from predecessors
673                let mut pred_exprs = None;
674                for edge in graph.edges_directed(node_id, petgraph::Direction::Incoming) {
675                    let pred_id = edge.source();
676                    if let Some(exprs) = analysis.available_expressions.get(&pred_id) {
677                        match pred_exprs {
678                            None => pred_exprs = Some(exprs.clone()),
679                            Some(ref mut current) => {
680                                *current = current.intersection(exprs).cloned().collect();
681                            }
682                        }
683                    }
684                }
685
686                if let Some(exprs) = pred_exprs {
687                    new_exprs = exprs;
688                }
689
690                // Add expression computed by this node
691                if let Some(node) = graph.node(node_id) {
692                    let expr = Self::node_to_expression(graph, node_id, node);
693                    new_exprs.insert(expr);
694                }
695
696                if analysis.available_expressions.get(&node_id) != Some(&new_exprs) {
697                    analysis.available_expressions.insert(node_id, new_exprs);
698                    changed = true;
699                }
700            }
701        }
702
703        Ok(())
704    }
705
706    /// Convert a node to an expression
707    fn node_to_expression(graph: &ComputationGraph, node_id: NodeId, node: &Node) -> Expression {
708        let operation = format!("{:?}", node.op);
709        let inputs = Self::get_input_variables(graph, node_id);
710        let mut attributes = HashMap::new();
711
712        // Add relevant attributes
713        for (key, attr) in &node.attrs {
714            let value = match attr {
715                crate::graph::Attribute::String(s) => s.clone(),
716                crate::graph::Attribute::Int(i) => i.to_string(),
717                crate::graph::Attribute::Float(f) => f.to_string(),
718                crate::graph::Attribute::Bool(b) => b.to_string(),
719                _ => "complex".to_string(),
720            };
721            attributes.insert(key.clone(), value);
722        }
723
724        Expression {
725            operation,
726            inputs,
727            attributes,
728        }
729    }
730
731    /// Identify dead code
732    fn identify_dead_code(
733        graph: &ComputationGraph,
734        analysis: &mut DataFlowAnalysis,
735    ) -> JitResult<()> {
736        let outputs: HashSet<_> = graph.outputs.iter().copied().collect();
737
738        for (node_id, _) in graph.nodes() {
739            // A node is dead if:
740            // 1. It's not an output node
741            // 2. No live variable depends on it
742            // 3. It has no side effects
743
744            if outputs.contains(&node_id) {
745                continue; // Output nodes are always live
746            }
747
748            let is_used = analysis.uses.values().any(|users| users.contains(&node_id));
749
750            if !is_used {
751                if let Some(node) = graph.node(node_id) {
752                    // Check if the operation has side effects
753                    if !Self::has_side_effects(&node.op) {
754                        analysis.dead_code.push(node_id);
755                    }
756                }
757            }
758        }
759
760        Ok(())
761    }
762
763    /// Check if an operation has side effects
764    fn has_side_effects(op: &Operation) -> bool {
765        match op {
766            Operation::Custom(_) => true, // Conservative assumption
767            _ => false,                   // Pure operations
768        }
769    }
770
771    /// Find common subexpressions
772    fn find_common_subexpressions(
773        graph: &ComputationGraph,
774        analysis: &mut DataFlowAnalysis,
775    ) -> JitResult<()> {
776        let mut expr_to_nodes: HashMap<Expression, Vec<NodeId>> = HashMap::new();
777
778        // Group nodes by their expressions
779        for (node_id, node) in graph.nodes() {
780            let expr = Self::node_to_expression(graph, node_id, node);
781            expr_to_nodes.entry(expr).or_default().push(node_id);
782        }
783
784        // Find expressions computed by multiple nodes
785        for (expr, nodes) in expr_to_nodes {
786            if nodes.len() > 1 {
787                let savings = Self::estimate_cse_savings(graph, &nodes);
788                analysis.common_subexpressions.push(CommonSubexpression {
789                    expression: expr,
790                    instances: nodes,
791                    savings,
792                });
793            }
794        }
795
796        Ok(())
797    }
798
799    /// Estimate savings from eliminating a common subexpression
800    fn estimate_cse_savings(graph: &ComputationGraph, nodes: &[NodeId]) -> OptimizationSavings {
801        let mut total_memory = 0;
802        let mut total_flops = 0;
803
804        for &node_id in nodes {
805            if let Some(node) = graph.node(node_id) {
806                // Estimate memory savings (all but one instance)
807                let element_size = match node.dtype {
808                    torsh_core::DType::F32 => 4,
809                    torsh_core::DType::F64 => 8,
810                    _ => 4, // Default
811                };
812                total_memory += node.output_shape.numel() * element_size;
813
814                // Estimate compute savings
815                total_flops += match &node.op {
816                    Operation::Add | Operation::Sub | Operation::Mul => {
817                        node.output_shape.numel() as u64
818                    }
819                    Operation::MatMul => {
820                        // Simplified: assume square matrices
821                        let n = (node.output_shape.numel() as f64).sqrt() as u64;
822                        n * n * n // O(n^3) for matrix multiplication
823                    }
824                    _ => node.output_shape.numel() as u64,
825                };
826            }
827        }
828
829        // Savings from eliminating all but one instance
830        let instances = nodes.len();
831        if instances > 1 {
832            let memory_savings = total_memory * (instances - 1) / instances;
833            let compute_savings = total_flops * (instances - 1) as u64 / instances as u64;
834            let speedup = 1.0 + (instances - 1) as f32 * 0.1; // Conservative estimate
835
836            OptimizationSavings {
837                memory_bytes: memory_savings,
838                compute_flops: compute_savings,
839                speedup_factor: speedup,
840            }
841        } else {
842            OptimizationSavings {
843                memory_bytes: 0,
844                compute_flops: 0,
845                speedup_factor: 1.0,
846            }
847        }
848    }
849}
850
851impl DataFlowAnalysis {
852    /// Get optimization recommendations
853    pub fn get_recommendations(&self) -> Vec<OptimizationRecommendation> {
854        let mut recommendations = Vec::new();
855
856        // Dead code elimination
857        if !self.dead_code.is_empty() {
858            recommendations.push(OptimizationRecommendation {
859                optimization_type: OptimizationType::DeadCodeElimination,
860                description: format!("Remove {} dead code nodes", self.dead_code.len()),
861                nodes: self.dead_code.clone(),
862                estimated_savings: OptimizationSavings {
863                    memory_bytes: self.dead_code.len() * 1024, // Rough estimate
864                    compute_flops: self.dead_code.len() as u64 * 100,
865                    speedup_factor: 1.0 + self.dead_code.len() as f32 * 0.01,
866                },
867            });
868        }
869
870        // Common subexpression elimination
871        for cse in &self.common_subexpressions {
872            if cse.instances.len() > 1 {
873                recommendations.push(OptimizationRecommendation {
874                    optimization_type: OptimizationType::CommonSubexpressionElimination,
875                    description: format!(
876                        "Eliminate common subexpression computed by {} nodes",
877                        cse.instances.len()
878                    ),
879                    nodes: cse.instances.clone(),
880                    estimated_savings: cse.savings.clone(),
881                });
882            }
883        }
884
885        recommendations
886    }
887}
888
889/// Optimization recommendation
890#[derive(Debug, Clone)]
891pub struct OptimizationRecommendation {
892    /// Type of optimization
893    pub optimization_type: OptimizationType,
894    /// Human-readable description
895    pub description: String,
896    /// Nodes involved in the optimization
897    pub nodes: Vec<NodeId>,
898    /// Estimated savings
899    pub estimated_savings: OptimizationSavings,
900}
901
902/// Types of optimizations
903#[derive(Debug, Clone, PartialEq)]
904pub enum OptimizationType {
905    DeadCodeElimination,
906    CommonSubexpressionElimination,
907    LoopInvariantCodeMotion,
908    ConstantFolding,
909    StrengthReduction,
910}
911
912#[cfg(test)]
913mod tests {
914    use super::*;
915    use torsh_core::{DType, DeviceType, Shape};
916
917    #[test]
918    fn test_memory_info_computation() {
919        let node = Node::new(Operation::Relu, "test".to_string())
920            .with_output_shapes(vec![Some(Shape::new(vec![32, 64]))])
921            .with_dtypes(vec![DType::F32])
922            .with_device(DeviceType::Cpu);
923
924        let info = GraphAnalyzer::compute_memory_info(&node).unwrap();
925        assert_eq!(info.output_size, 32 * 64 * 4); // 4 bytes per f32
926        assert_eq!(info.temp_size, 0); // ReLU needs no temp storage
927    }
928
929    #[test]
930    fn test_compute_cost_estimation() {
931        let node = Node::new(Operation::Add, "add".to_string())
932            .with_output_shapes(vec![Some(Shape::new(vec![1000]))])
933            .with_dtypes(vec![DType::F32])
934            .with_device(DeviceType::Cpu);
935
936        let cost = GraphAnalyzer::estimate_compute_cost(&node).unwrap();
937        assert_eq!(cost.flops, 1000);
938        assert_eq!(cost.memory_ops, 3000); // 2 reads + 1 write
939        assert!(cost.intensity < 1.0); // Memory bound
940    }
941}