1use 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#[derive(Debug, Clone)]
11pub struct GraphAnalysis {
12 pub memory_usage: HashMap<NodeId, MemoryInfo>,
14
15 pub compute_cost: HashMap<NodeId, ComputeCost>,
17
18 pub dependencies: DependencyInfo,
20
21 pub critical_path: Vec<NodeId>,
23
24 pub parallel_groups: Vec<Vec<NodeId>>,
26}
27
28#[derive(Debug, Clone)]
30pub struct MemoryInfo {
31 pub output_size: usize,
33
34 pub temp_size: usize,
36
37 pub total_size: usize,
39
40 pub access_pattern: AccessPattern,
42}
43
44#[derive(Debug, Clone, PartialEq)]
46pub enum AccessPattern {
47 Sequential,
48 Strided { stride: usize },
49 Random,
50 Broadcast,
51}
52
53#[derive(Debug, Clone)]
55pub struct ComputeCost {
56 pub flops: u64,
58
59 pub memory_ops: u64,
61
62 pub cycles: u64,
64
65 pub intensity: f32,
67}
68
69#[derive(Debug, Clone)]
71pub struct DependencyInfo {
72 pub direct: HashMap<NodeId, Vec<NodeId>>,
74
75 pub transitive: HashMap<NodeId, HashSet<NodeId>>,
77
78 pub depth: HashMap<NodeId, usize>,
80}
81
82pub struct GraphAnalyzer;
84
85impl GraphAnalyzer {
86 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 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 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, };
133
134 let num_elements = node.output_shape.numel();
135 let output_size = num_elements * element_size;
136
137 let (temp_size, access_pattern) = match &node.op {
139 Operation::MatMul | Operation::BatchMatMul => {
140 (output_size, AccessPattern::Sequential)
142 }
143 Operation::Conv2d(_) => {
144 (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) }
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 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 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 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 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 Operation::MatMul => {
196 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; ((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 Operation::Sum { .. } | Operation::Mean { .. } => {
210 (num_elements as u64, num_elements as u64 + 1)
211 }
212
213 Operation::Conv2d(info) => {
215 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 let cycles = flops.max(memory_ops * 4);
234
235 Ok(ComputeCost {
236 flops,
237 memory_ops,
238 cycles,
239 intensity,
240 })
241 }
242
243 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 let order = graph
251 .topological_sort()
252 .map_err(|e| JitError::GraphError(format!("{:?}", e)))?;
253
254 for &node_id in &order {
256 let preds: Vec<_> = graph.predecessors(node_id).collect();
258 direct.insert(node_id, preds.clone());
259
260 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 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 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 for (node_id, _) in graph.nodes() {
297 distances.insert(node_id, 0u64);
298 }
299
300 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
405pub struct DataFlowAnalysis {
406 pub definitions: HashMap<String, NodeId>,
408
409 pub uses: HashMap<String, Vec<NodeId>>,
411
412 pub live_variables: HashMap<NodeId, HashSet<String>>,
414
415 pub reaching_definitions: HashMap<NodeId, HashMap<String, NodeId>>,
417
418 pub available_expressions: HashMap<NodeId, HashSet<Expression>>,
420
421 pub dead_code: Vec<NodeId>,
423
424 pub common_subexpressions: Vec<CommonSubexpression>,
426}
427
428#[derive(Debug, Clone, PartialEq, Eq)]
430pub struct Expression {
431 pub operation: String,
433 pub inputs: Vec<String>,
435 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 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#[derive(Debug, Clone)]
452pub struct CommonSubexpression {
453 pub expression: Expression,
455 pub instances: Vec<NodeId>,
457 pub savings: OptimizationSavings,
459}
460
461#[derive(Debug, Clone)]
463pub struct OptimizationSavings {
464 pub memory_bytes: usize,
466 pub compute_flops: u64,
468 pub speedup_factor: f32,
470}
471
472pub struct DataFlowAnalyzer;
474
475impl DataFlowAnalyzer {
476 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 Self::build_def_use_chains(graph, &mut analysis)?;
490
491 Self::compute_live_variables(graph, &mut analysis)?;
493
494 Self::compute_reaching_definitions(graph, &mut analysis)?;
496
497 Self::compute_available_expressions(graph, &mut analysis)?;
499
500 Self::identify_dead_code(graph, &mut analysis)?;
502
503 Self::find_common_subexpressions(graph, &mut analysis)?;
505
506 Ok(analysis)
507 }
508
509 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 analysis.definitions.insert(var_name.clone(), node_id);
519
520 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 fn get_input_variables(graph: &ComputationGraph, node_id: NodeId) -> Vec<String> {
531 let mut vars = Vec::new();
532
533 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 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 for &node_id in &topo_order {
554 analysis.live_variables.insert(node_id, HashSet::new());
555 }
556
557 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 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 if let Some(node) = graph.node(node_id) {
580 new_live.remove(&node.name);
581 }
582
583 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 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 for &node_id in &topo_order {
610 analysis
611 .reaching_definitions
612 .insert(node_id, HashMap::new());
613 }
614
615 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 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 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 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 for &node_id in &topo_order {
659 analysis
660 .available_expressions
661 .insert(node_id, HashSet::new());
662 }
663
664 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 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 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 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 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 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 if outputs.contains(&node_id) {
745 continue; }
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 if !Self::has_side_effects(&node.op) {
754 analysis.dead_code.push(node_id);
755 }
756 }
757 }
758 }
759
760 Ok(())
761 }
762
763 fn has_side_effects(op: &Operation) -> bool {
765 match op {
766 Operation::Custom(_) => true, _ => false, }
769 }
770
771 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 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 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 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 let element_size = match node.dtype {
808 torsh_core::DType::F32 => 4,
809 torsh_core::DType::F64 => 8,
810 _ => 4, };
812 total_memory += node.output_shape.numel() * element_size;
813
814 total_flops += match &node.op {
816 Operation::Add | Operation::Sub | Operation::Mul => {
817 node.output_shape.numel() as u64
818 }
819 Operation::MatMul => {
820 let n = (node.output_shape.numel() as f64).sqrt() as u64;
822 n * n * n }
824 _ => node.output_shape.numel() as u64,
825 };
826 }
827 }
828
829 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; 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 pub fn get_recommendations(&self) -> Vec<OptimizationRecommendation> {
854 let mut recommendations = Vec::new();
855
856 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, 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 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#[derive(Debug, Clone)]
891pub struct OptimizationRecommendation {
892 pub optimization_type: OptimizationType,
894 pub description: String,
896 pub nodes: Vec<NodeId>,
898 pub estimated_savings: OptimizationSavings,
900}
901
902#[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); assert_eq!(info.temp_size, 0); }
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); assert!(cost.intensity < 1.0); }
941}