1use crate::interpreter::ShapeInfo;
4use crate::{FxGraph, Node};
5use petgraph::graph::NodeIndex;
6use petgraph::visit::EdgeRef;
7use std::collections::HashMap;
8use torsh_core::{dtype::DType, shape::Shape};
9
10#[derive(Debug, Clone)]
12pub struct VisualizationOptions {
13 pub show_shapes: bool,
15 pub show_types: bool,
17 pub show_edges: bool,
19 pub compact: bool,
21 pub max_nodes: Option<usize>,
23}
24
25impl Default for VisualizationOptions {
26 fn default() -> Self {
27 Self {
28 show_shapes: true,
29 show_types: true,
30 show_edges: true,
31 compact: false,
32 max_nodes: None,
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
39pub struct NodeDebugInfo {
40 pub node_id: NodeIndex,
41 pub node_type: String,
42 pub operation: Option<String>,
43 pub shape: Option<Shape>,
44 pub dtype: Option<DType>,
45 pub inputs: Vec<NodeIndex>,
46 pub outputs: Vec<NodeIndex>,
47}
48
49pub struct GraphDebugger {
51 graph: FxGraph,
52 shape_info: Option<HashMap<NodeIndex, ShapeInfo>>,
53 type_info: Option<HashMap<NodeIndex, DType>>,
54}
55
56impl GraphDebugger {
57 pub fn new(graph: FxGraph) -> Self {
59 Self {
60 graph,
61 shape_info: None,
62 type_info: None,
63 }
64 }
65
66 pub fn with_shapes(mut self, shapes: HashMap<NodeIndex, ShapeInfo>) -> Self {
68 self.shape_info = Some(shapes);
69 self
70 }
71
72 pub fn with_types(mut self, types: HashMap<NodeIndex, DType>) -> Self {
74 self.type_info = Some(types);
75 self
76 }
77
78 pub fn get_debug_info(&self) -> Vec<NodeDebugInfo> {
80 let mut debug_info = Vec::new();
81
82 for (idx, node) in self.graph.nodes() {
83 let node_type = match node {
84 Node::Input(_) => "Input".to_string(),
85 Node::Call(_, _) => "Call".to_string(),
86 Node::Output => "Output".to_string(),
87 Node::Conditional { .. } => "Conditional".to_string(),
88 Node::Loop { .. } => "Loop".to_string(),
89 Node::Merge { .. } => "Merge".to_string(),
90 Node::GetAttr { .. } => "GetAttr".to_string(),
91 };
92
93 let operation = match node {
94 Node::Call(op_name, _) => Some(op_name.clone()),
95 Node::Conditional { .. } => Some("conditional".to_string()),
96 Node::Loop { .. } => Some("loop".to_string()),
97 Node::Merge { .. } => Some("merge".to_string()),
98 Node::GetAttr { attr, .. } => Some(format!("get_attr({attr})")),
99 _ => None,
100 };
101
102 let shape = self
103 .shape_info
104 .as_ref()
105 .and_then(|shapes| shapes.get(&idx))
106 .map(|info| info.shape.clone());
107
108 let dtype = self
109 .type_info
110 .as_ref()
111 .and_then(|types| types.get(&idx))
112 .copied()
113 .or_else(|| {
114 self.shape_info
115 .as_ref()
116 .and_then(|shapes| shapes.get(&idx))
117 .map(|info| info.dtype)
118 });
119
120 let inputs: Vec<_> = self
122 .graph
123 .graph
124 .neighbors_directed(idx, petgraph::Direction::Incoming)
125 .collect();
126 let outputs: Vec<_> = self
127 .graph
128 .graph
129 .neighbors_directed(idx, petgraph::Direction::Outgoing)
130 .collect();
131
132 debug_info.push(NodeDebugInfo {
133 node_id: idx,
134 node_type,
135 operation,
136 shape,
137 dtype,
138 inputs,
139 outputs,
140 });
141 }
142
143 debug_info
144 }
145
146 pub fn visualize_text(&self, options: &VisualizationOptions) -> String {
148 let mut output = String::new();
149 let debug_info = self.get_debug_info();
150
151 if options.compact {
152 output.push_str("Graph Summary:\n");
153 let node_count = self.graph.node_count();
154 output.push_str(&format!(" Nodes: {node_count}\n"));
155 let edge_count = self.graph.edge_count();
156 output.push_str(&format!(" Edges: {edge_count}\n"));
157 let input_count = self.graph.inputs().len();
158 output.push_str(&format!(" Inputs: {input_count}\n"));
159 let output_count = self.graph.outputs().len();
160 output.push_str(&format!(" Outputs: {output_count}\n"));
161 output.push('\n');
162 }
163
164 let nodes_to_show = if let Some(max) = options.max_nodes {
165 debug_info.into_iter().take(max).collect()
166 } else {
167 debug_info
168 };
169
170 output.push_str("Nodes:\n");
171 for info in &nodes_to_show {
172 output.push_str(&self.format_node_info(info, options));
173 output.push('\n');
174 }
175
176 if options.show_edges {
177 output.push_str("\nEdges:\n");
178 for edge_ref in self.graph.graph.edge_references() {
179 let src = edge_ref.source();
180 let dst = edge_ref.target();
181 let edge = edge_ref.weight();
182 output.push_str(&format!(" {:?} -> {:?} ({})\n", src, dst, edge.name));
183 }
184 }
185
186 output
187 }
188
189 pub fn visualize_json(&self, options: &VisualizationOptions) -> String {
191 let mut json = String::from("{\n");
192 json.push_str(" \"type\": \"torsh_fx_graph\",\n");
193 json.push_str(&format!(" \"node_count\": {},\n", self.graph.node_count()));
194 json.push_str(&format!(" \"edge_count\": {},\n", self.graph.edge_count()));
195
196 json.push_str(" \"nodes\": [\n");
198 let node_infos = self.get_debug_info();
199 let limited_nodes = if let Some(max) = options.max_nodes {
200 node_infos.into_iter().take(max).collect()
201 } else {
202 node_infos
203 };
204
205 for (i, info) in limited_nodes.iter().enumerate() {
206 json.push_str(" {\n");
207 json.push_str(&format!(" \"id\": \"{:?}\",\n", info.node_id));
208 json.push_str(&format!(" \"type\": \"{}\",\n", info.node_type));
209
210 if let Some(op) = &info.operation {
211 json.push_str(&format!(" \"operation\": \"{}\",\n", op));
212 }
213
214 if options.show_shapes {
215 if let Some(shape) = &info.shape {
216 json.push_str(&format!(" \"shape\": {:?},\n", shape.dims()));
217 }
218 }
219
220 if options.show_types {
221 if let Some(dtype) = &info.dtype {
222 json.push_str(&format!(" \"dtype\": \"{:?}\",\n", dtype));
223 }
224 }
225
226 json.push_str(&format!(" \"inputs\": {:?},\n", info.inputs));
227 json.push_str(&format!(" \"outputs\": {:?}\n", info.outputs));
228
229 if i < limited_nodes.len() - 1 {
230 json.push_str(" },\n");
231 } else {
232 json.push_str(" }\n");
233 }
234 }
235 json.push_str(" ],\n");
236
237 json.push_str(" \"edges\": [\n");
239 let mut edge_count = 0;
240 let total_edges: Vec<_> = self.graph.graph.edge_references().collect();
241
242 for (i, edge) in total_edges.iter().enumerate() {
243 if let Some(max_nodes) = options.max_nodes {
244 if edge.source().index() >= max_nodes || edge.target().index() >= max_nodes {
245 continue;
246 }
247 }
248
249 json.push_str(" {\n");
250 json.push_str(&format!(" \"source\": \"{:?}\",\n", edge.source()));
251 json.push_str(&format!(" \"target\": \"{:?}\",\n", edge.target()));
252 json.push_str(&format!(" \"label\": \"{}\"\n", edge.weight().name));
253
254 if i < total_edges.len() - 1 && edge_count < total_edges.len() - 1 {
255 json.push_str(" },\n");
256 } else {
257 json.push_str(" }\n");
258 }
259 edge_count += 1;
260 }
261 json.push_str(" ]\n");
262 json.push_str("}\n");
263
264 json
265 }
266
267 pub fn visualize_mermaid(&self, options: &VisualizationOptions) -> String {
269 let mut output = String::from("graph TD\n");
270
271 let node_infos = self.get_debug_info();
272 let limited_nodes = if let Some(max) = options.max_nodes {
273 node_infos.into_iter().take(max).collect()
274 } else {
275 node_infos
276 };
277
278 for info in &limited_nodes {
280 let mut label = info.node_type.clone();
281
282 if let Some(op) = &info.operation {
283 label = format!("{}<br/>{}", label, op);
284 }
285
286 if options.show_shapes {
287 if let Some(shape) = &info.shape {
288 label = format!("{}<br/>shape: {:?}", label, shape.dims());
289 }
290 }
291
292 if options.show_types {
293 if let Some(dtype) = &info.dtype {
294 label = format!("{}<br/>type: {:?}", label, dtype);
295 }
296 }
297
298 let style = match info.node_type.as_str() {
300 "Input" => "([",
301 "Output" => "])",
302 "Call" => "[",
303 "Conditional" => "{",
304 "Loop" => "[[",
305 _ => "[",
306 };
307
308 let end_style = match info.node_type.as_str() {
309 "Input" => "])",
310 "Output" => "([",
311 "Call" => "]",
312 "Conditional" => "}",
313 "Loop" => "]]",
314 _ => "]",
315 };
316
317 output.push_str(&format!(
318 " {:?}{}{}{}\n",
319 info.node_id.index(),
320 style,
321 label,
322 end_style
323 ));
324 }
325
326 output.push_str("\n");
327
328 if options.show_edges {
330 for edge in self.graph.graph.edge_references() {
331 if let Some(max_nodes) = options.max_nodes {
332 if edge.source().index() >= max_nodes || edge.target().index() >= max_nodes {
333 continue;
334 }
335 }
336
337 output.push_str(&format!(
338 " {} --> {}|{}|\n",
339 edge.source().index(),
340 edge.target().index(),
341 edge.weight().name
342 ));
343 }
344 }
345
346 output
347 }
348
349 pub fn visualize_dot(&self, options: &VisualizationOptions) -> String {
351 let mut output = String::new();
352 output.push_str("digraph FxGraph {\n");
353 output.push_str(" rankdir=TB;\n");
354 output.push_str(" node [shape=box];\n\n");
355
356 let debug_info = self.get_debug_info();
357 let nodes_to_show = if let Some(max) = options.max_nodes {
358 debug_info.into_iter().take(max).collect()
359 } else {
360 debug_info
361 };
362
363 for info in &nodes_to_show {
365 let label = self.format_node_label(info, options);
366 let node_id = info.node_id.index();
367
368 let color = match info.node_type.as_str() {
369 "Input" => "lightblue",
370 "Output" => "lightgreen",
371 "Call" => "lightyellow",
372 "Conditional" => "lightcoral",
373 "Loop" => "lightpink",
374 "Merge" => "lightgray",
375 _ => "white",
376 };
377
378 output.push_str(&format!(
379 " node_{} [label=\"{}\", fillcolor={}, style=filled];\n",
380 node_id, label, color
381 ));
382 }
383
384 output.push('\n');
385
386 if options.show_edges {
388 for edge_ref in self.graph.graph.edge_references() {
389 let src = edge_ref.source().index();
390 let dst = edge_ref.target().index();
391 let edge = edge_ref.weight();
392
393 output.push_str(&format!(
394 " node_{} -> node_{} [label=\"{}\"];\n",
395 src, dst, edge.name
396 ));
397 }
398 }
399
400 output.push_str("}\n");
401 output
402 }
403
404 pub fn visualize_html(&self, options: &VisualizationOptions) -> String {
406 let mut output = String::new();
407 output.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
408 output.push_str("<title>FX Graph Visualization</title>\n");
409 output.push_str("<style>\n");
410 output.push_str("table { border-collapse: collapse; width: 100%; }\n");
411 output.push_str("th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }\n");
412 output.push_str("th { background-color: #f2f2f2; }\n");
413 output.push_str(".input { background-color: #e6f3ff; }\n");
414 output.push_str(".output { background-color: #e6ffe6; }\n");
415 output.push_str(".call { background-color: #fff9e6; }\n");
416 output.push_str(".conditional { background-color: #ffe6e6; }\n");
417 output.push_str("</style>\n</head>\n<body>\n");
418
419 output.push_str("<h1>FX Graph Visualization</h1>\n");
420
421 output.push_str("<h2>Graph Summary</h2>\n");
423 output.push_str("<ul>\n");
424 let node_count = self.graph.node_count();
425 output.push_str(&format!("<li>Nodes: {node_count}</li>\n"));
426 let edge_count = self.graph.edge_count();
427 output.push_str(&format!("<li>Edges: {edge_count}</li>\n"));
428 let input_count = self.graph.inputs().len();
429 output.push_str(&format!("<li>Inputs: {input_count}</li>\n"));
430 output.push_str(&format!(
431 "<li>Outputs: {}</li>\n",
432 self.graph.outputs().len()
433 ));
434 output.push_str("</ul>\n");
435
436 output.push_str("<h2>Nodes</h2>\n");
438 output.push_str("<table>\n<tr>\n");
439 output.push_str("<th>ID</th><th>Type</th><th>Operation</th>");
440 if options.show_shapes {
441 output.push_str("<th>Shape</th>");
442 }
443 if options.show_types {
444 output.push_str("<th>Type</th>");
445 }
446 output.push_str("<th>Inputs</th><th>Outputs</th>\n</tr>\n");
447
448 let debug_info = self.get_debug_info();
449 let nodes_to_show = if let Some(max) = options.max_nodes {
450 debug_info.into_iter().take(max).collect()
451 } else {
452 debug_info
453 };
454
455 for info in &nodes_to_show {
456 let class = info.node_type.to_lowercase();
457 output.push_str(&format!("<tr class=\"{}\">\n", class));
458 output.push_str(&format!("<td>{:?}</td>", info.node_id));
459 output.push_str(&format!("<td>{}</td>", info.node_type));
460 output.push_str(&format!(
461 "<td>{}</td>",
462 info.operation.as_deref().unwrap_or("-")
463 ));
464
465 if options.show_shapes {
466 let shape_str = info
467 .shape
468 .as_ref()
469 .map(|s| format!("{:?}", s.dims()))
470 .unwrap_or_else(|| "-".to_string());
471 output.push_str(&format!("<td>{}</td>", shape_str));
472 }
473
474 if options.show_types {
475 let type_str = info
476 .dtype
477 .map(|t| format!("{:?}", t))
478 .unwrap_or_else(|| "-".to_string());
479 output.push_str(&format!("<td>{}</td>", type_str));
480 }
481
482 output.push_str(&format!(
483 "<td>{}</td>",
484 info.inputs
485 .iter()
486 .map(|i| format!("{:?}", i))
487 .collect::<Vec<_>>()
488 .join(", ")
489 ));
490 output.push_str(&format!(
491 "<td>{}</td>",
492 info.outputs
493 .iter()
494 .map(|i| format!("{:?}", i))
495 .collect::<Vec<_>>()
496 .join(", ")
497 ));
498 output.push_str("</tr>\n");
499 }
500
501 output.push_str("</table>\n");
502 output.push_str("</body>\n</html>\n");
503 output
504 }
505
506 pub fn get_statistics(&self) -> GraphStatistics {
508 let debug_info = self.get_debug_info();
509 let mut op_counts = HashMap::new();
510 let mut type_counts = HashMap::new();
511 let mut shape_counts = HashMap::new();
512
513 for info in &debug_info {
514 if let Some(op) = &info.operation {
516 *op_counts.entry(op.clone()).or_insert(0) += 1;
517 }
518
519 if let Some(dtype) = info.dtype {
521 *type_counts.entry(dtype).or_insert(0) += 1;
522 }
523
524 if let Some(shape) = &info.shape {
526 let shape_key = format!("{:?}", shape.dims());
527 *shape_counts.entry(shape_key).or_insert(0) += 1;
528 }
529 }
530
531 GraphStatistics {
532 total_nodes: self.graph.node_count(),
533 total_edges: self.graph.edge_count(),
534 input_nodes: self.graph.inputs().len(),
535 output_nodes: self.graph.outputs().len(),
536 operation_counts: op_counts,
537 type_counts,
538 shape_counts,
539 max_depth: self.calculate_max_depth(),
540 }
541 }
542
543 fn format_node_info(&self, info: &NodeDebugInfo, options: &VisualizationOptions) -> String {
545 let mut line = format!(" {:?}: {}", info.node_id, info.node_type);
546
547 if let Some(op) = &info.operation {
548 line.push_str(&format!(" ({})", op));
549 }
550
551 if options.show_shapes {
552 if let Some(shape) = &info.shape {
553 line.push_str(&format!(" shape={:?}", shape.dims()));
554 }
555 }
556
557 if options.show_types {
558 if let Some(dtype) = info.dtype {
559 line.push_str(&format!(" type={:?}", dtype));
560 }
561 }
562
563 if !info.inputs.is_empty() {
564 line.push_str(&format!(" inputs={:?}", info.inputs));
565 }
566
567 line
568 }
569
570 fn format_node_label(&self, info: &NodeDebugInfo, options: &VisualizationOptions) -> String {
572 let mut label = format!("{:?}\\n{}", info.node_id, info.node_type);
573
574 if let Some(op) = &info.operation {
575 label.push_str(&format!("\\n{}", op));
576 }
577
578 if options.show_shapes {
579 if let Some(shape) = &info.shape {
580 label.push_str(&format!("\\nshape: {:?}", shape.dims()));
581 }
582 }
583
584 if options.show_types {
585 if let Some(dtype) = info.dtype {
586 label.push_str(&format!("\\ntype: {:?}", dtype));
587 }
588 }
589
590 label
591 }
592
593 fn calculate_max_depth(&self) -> usize {
595 use petgraph::algo::toposort;
597
598 if let Ok(order) = toposort(&self.graph.graph, None) {
599 let mut depths = HashMap::new();
601
602 for &input_idx in self.graph.inputs() {
604 depths.insert(input_idx, 0);
605 }
606
607 for node_idx in order {
609 if depths.contains_key(&node_idx) {
610 continue; }
612
613 let predecessors: Vec<_> = self
615 .graph
616 .graph
617 .neighbors_directed(node_idx, petgraph::Direction::Incoming)
618 .collect();
619
620 let max_pred_depth = predecessors
621 .iter()
622 .filter_map(|&pred| depths.get(&pred))
623 .max()
624 .unwrap_or(&0);
625
626 depths.insert(node_idx, max_pred_depth + 1);
627 }
628
629 depths.values().max().copied().unwrap_or(0)
630 } else {
631 0
632 }
633 }
634}
635
636#[derive(Debug, Clone)]
638pub struct GraphStatistics {
639 pub total_nodes: usize,
640 pub total_edges: usize,
641 pub input_nodes: usize,
642 pub output_nodes: usize,
643 pub operation_counts: HashMap<String, usize>,
644 pub type_counts: HashMap<DType, usize>,
645 pub shape_counts: HashMap<String, usize>,
646 pub max_depth: usize,
647}
648
649pub fn visualize_graph(graph: &FxGraph) -> String {
651 let debugger = GraphDebugger::new(graph.clone());
652 debugger.visualize_text(&VisualizationOptions::default())
653}
654
655pub fn visualize_graph_with_info(
657 graph: &FxGraph,
658 shapes: Option<HashMap<NodeIndex, ShapeInfo>>,
659 types: Option<HashMap<NodeIndex, DType>>,
660) -> String {
661 let mut debugger = GraphDebugger::new(graph.clone());
662
663 if let Some(shapes) = shapes {
664 debugger = debugger.with_shapes(shapes);
665 }
666
667 if let Some(types) = types {
668 debugger = debugger.with_types(types);
669 }
670
671 debugger.visualize_text(&VisualizationOptions::default())
672}
673
674pub fn visualize_graph_dot(graph: &FxGraph) -> String {
676 let debugger = GraphDebugger::new(graph.clone());
677 debugger.visualize_dot(&VisualizationOptions::default())
678}
679
680pub fn visualize_graph_html(graph: &FxGraph) -> String {
682 let debugger = GraphDebugger::new(graph.clone());
683 debugger.visualize_html(&VisualizationOptions::default())
684}
685
686pub fn visualize_graph_json(graph: &FxGraph) -> String {
688 let debugger = GraphDebugger::new(graph.clone());
689 debugger.visualize_json(&VisualizationOptions::default())
690}
691
692pub fn visualize_graph_mermaid(graph: &FxGraph) -> String {
694 let debugger = GraphDebugger::new(graph.clone());
695 debugger.visualize_mermaid(&VisualizationOptions::default())
696}
697
698pub fn visualize_graph_multi_format(graph: &FxGraph, formats: &[&str]) -> HashMap<String, String> {
700 let debugger = GraphDebugger::new(graph.clone());
701 let options = VisualizationOptions::default();
702 let mut outputs = HashMap::new();
703
704 for format in formats {
705 let output = match *format {
706 "text" => debugger.visualize_text(&options),
707 "dot" => debugger.visualize_dot(&options),
708 "html" => debugger.visualize_html(&options),
709 "json" => debugger.visualize_json(&options),
710 "mermaid" => debugger.visualize_mermaid(&options),
711 _ => format!("Unsupported format: {}", format),
712 };
713 outputs.insert(format.to_string(), output);
714 }
715
716 outputs
717}
718
719pub struct InteractiveGraphAnalyzer {
721 debugger: GraphDebugger,
722 performance_data: Option<HashMap<NodeIndex, f64>>, }
724
725impl InteractiveGraphAnalyzer {
726 pub fn new(graph: FxGraph) -> Self {
728 Self {
729 debugger: GraphDebugger::new(graph),
730 performance_data: None,
731 }
732 }
733
734 pub fn with_performance_data(mut self, data: HashMap<NodeIndex, f64>) -> Self {
736 self.performance_data = Some(data);
737 self
738 }
739
740 pub fn with_analysis_data(
742 mut self,
743 shapes: HashMap<NodeIndex, ShapeInfo>,
744 types: HashMap<NodeIndex, DType>,
745 ) -> Self {
746 self.debugger = self.debugger.with_shapes(shapes).with_types(types);
747 self
748 }
749
750 pub fn generate_comprehensive_report(&self) -> GraphAnalysisReport {
752 let stats = self.debugger.get_statistics();
753 let debug_info = self.debugger.get_debug_info();
754
755 let performance_bottlenecks = self.identify_performance_bottlenecks(&debug_info);
756 let optimization_opportunities = self.identify_optimization_opportunities(&debug_info);
757 let memory_analysis = self.analyze_memory_usage(&debug_info);
758 let complexity_metrics = self.calculate_complexity_metrics(&stats, &debug_info);
759
760 GraphAnalysisReport {
761 basic_stats: stats,
762 performance_bottlenecks,
763 optimization_opportunities,
764 memory_analysis,
765 complexity_metrics,
766 recommendations: self.generate_recommendations(),
767 }
768 }
769
770 fn identify_performance_bottlenecks(
772 &self,
773 debug_info: &[NodeDebugInfo],
774 ) -> Vec<PerformanceBottleneck> {
775 let mut bottlenecks = Vec::new();
776
777 if let Some(perf_data) = &self.performance_data {
778 let total_time: f64 = perf_data.values().sum();
779 let avg_time = total_time / perf_data.len() as f64;
780
781 for info in debug_info {
782 if let Some(&exec_time) = perf_data.get(&info.node_id) {
783 if exec_time > avg_time * 3.0 {
784 bottlenecks.push(PerformanceBottleneck {
786 node_id: info.node_id,
787 operation: info.operation.clone(),
788 execution_time_ms: exec_time,
789 severity: if exec_time > avg_time * 10.0 {
790 BottleneckSeverity::Critical
791 } else if exec_time > avg_time * 5.0 {
792 BottleneckSeverity::High
793 } else {
794 BottleneckSeverity::Medium
795 },
796 suggestions: self.generate_bottleneck_suggestions(info, exec_time),
797 });
798 }
799 }
800 }
801 }
802
803 bottlenecks.sort_by(|a, b| {
805 b.execution_time_ms
806 .partial_cmp(&a.execution_time_ms)
807 .expect("execution_time_ms should be comparable")
808 });
809 bottlenecks
810 }
811
812 fn identify_optimization_opportunities(
814 &self,
815 debug_info: &[NodeDebugInfo],
816 ) -> Vec<OptimizationOpportunity> {
817 let mut opportunities = Vec::new();
818
819 for window in debug_info.windows(3) {
821 if let [a, b, c] = window {
822 if let (Some(op_a), Some(op_b), Some(op_c)) =
823 (&a.operation, &b.operation, &c.operation)
824 {
825 if op_a.contains("relu")
827 && op_b.contains("batch_norm")
828 && op_c.contains("dropout")
829 {
830 opportunities.push(OptimizationOpportunity {
831 opportunity_type: OptimizationType::OperatorFusion,
832 nodes: vec![a.node_id, b.node_id, c.node_id],
833 description: "ReLU + BatchNorm + Dropout fusion opportunity"
834 .to_string(),
835 potential_speedup: 1.3,
836 implementation_difficulty: OptimizationDifficulty::Medium,
837 });
838 }
839
840 if op_a.contains("conv") && op_b.contains("relu") {
842 opportunities.push(OptimizationOpportunity {
843 opportunity_type: OptimizationType::OperatorFusion,
844 nodes: vec![a.node_id, b.node_id],
845 description: "Conv + ReLU fusion opportunity".to_string(),
846 potential_speedup: 1.15,
847 implementation_difficulty: OptimizationDifficulty::Easy,
848 });
849 }
850
851 if self.is_elementwise_op(op_a)
853 && self.is_elementwise_op(op_b)
854 && self.is_elementwise_op(op_c)
855 {
856 opportunities.push(OptimizationOpportunity {
857 opportunity_type: OptimizationType::ElementwiseFusion,
858 nodes: vec![a.node_id, b.node_id, c.node_id],
859 description: "Element-wise operation chain fusion".to_string(),
860 potential_speedup: 1.5,
861 implementation_difficulty: OptimizationDifficulty::Easy,
862 });
863 }
864 }
865 }
866 }
867
868 for info in debug_info {
870 if let Some(op) = &info.operation {
871 if op.contains("transpose") || op.contains("reshape") || op.contains("permute") {
872 opportunities.push(OptimizationOpportunity {
873 opportunity_type: OptimizationType::MemoryLayout,
874 nodes: vec![info.node_id],
875 description: format!("Memory layout optimization for {}", op),
876 potential_speedup: 1.2,
877 implementation_difficulty: OptimizationDifficulty::Hard,
878 });
879 }
880 }
881 }
882
883 opportunities
884 }
885
886 fn analyze_memory_usage(&self, debug_info: &[NodeDebugInfo]) -> MemoryAnalysis {
888 let mut total_parameters = 0;
889 let mut peak_memory_mb = 0.0;
890 let mut memory_intensive_ops = Vec::new();
891
892 for info in debug_info {
893 if let Some(shape) = &info.shape {
894 let elements: usize = shape.dims().iter().product();
895 let dtype_size = match info.dtype {
896 Some(DType::F32) | Some(DType::I32) | Some(DType::U32) => 4,
897 Some(DType::F16) | Some(DType::I16) => 2,
898 Some(DType::F64) | Some(DType::I64) | Some(DType::U64) => 8,
899 Some(DType::I8) | Some(DType::U8) | Some(DType::QInt8)
900 | Some(DType::QUInt8) => 1,
901 Some(DType::BF16) => 2,
902 Some(DType::C64) => 8,
903 Some(DType::C128) => 16,
904 Some(DType::Bool) => 1,
905 _ => 4, };
907
908 let memory_mb = (elements * dtype_size) as f64 / (1024.0 * 1024.0);
909 peak_memory_mb += memory_mb;
910
911 if memory_mb > 100.0 {
912 memory_intensive_ops.push(MemoryIntensiveOperation {
914 node_id: info.node_id,
915 operation: info
916 .operation
917 .clone()
918 .unwrap_or_else(|| info.node_type.clone()),
919 memory_mb,
920 shape: shape.clone(),
921 });
922 }
923
924 total_parameters += elements;
925 }
926 }
927
928 MemoryAnalysis {
929 total_parameters,
930 estimated_peak_memory_mb: peak_memory_mb,
931 memory_intensive_operations: memory_intensive_ops,
932 memory_efficiency_score: self
933 .calculate_memory_efficiency_score(peak_memory_mb, total_parameters),
934 }
935 }
936
937 fn calculate_complexity_metrics(
939 &self,
940 stats: &GraphStatistics,
941 debug_info: &[NodeDebugInfo],
942 ) -> ComplexityMetrics {
943 let max_depth = self.debugger.calculate_max_depth();
944 let avg_fanout = if stats.total_nodes > 0 {
945 stats.total_edges as f64 / stats.total_nodes as f64
946 } else {
947 0.0
948 };
949
950 let parallelism_opportunities = self.count_parallelism_opportunities(debug_info);
951 let critical_path_length = self.estimate_critical_path_length(debug_info);
952
953 ComplexityMetrics {
954 graph_depth: max_depth,
955 average_fanout: avg_fanout,
956 parallelism_score: parallelism_opportunities as f64 / stats.total_nodes as f64,
957 critical_path_length,
958 complexity_score: self.calculate_overall_complexity_score(
959 max_depth,
960 avg_fanout,
961 parallelism_opportunities,
962 ),
963 }
964 }
965
966 fn generate_recommendations(&self) -> Vec<Recommendation> {
968 let mut recommendations = Vec::new();
969 let stats = self.debugger.get_statistics();
970 let debug_info = self.debugger.get_debug_info();
971
972 if stats.total_nodes > 1000 {
974 recommendations.push(Recommendation {
975 category: RecommendationCategory::Performance,
976 priority: RecommendationPriority::High,
977 title: "Large Graph Optimization".to_string(),
978 description:
979 "Consider graph partitioning or subgraph optimization for this large graph"
980 .to_string(),
981 implementation_guide:
982 "Use FxGraph::partition_for_devices() or implement custom subgraph batching"
983 .to_string(),
984 });
985 }
986
987 let memory_analysis = self.analyze_memory_usage(&debug_info);
989 if memory_analysis.estimated_peak_memory_mb > 1000.0 {
990 recommendations.push(Recommendation {
991 category: RecommendationCategory::Memory,
992 priority: RecommendationPriority::High,
993 title: "High Memory Usage Detected".to_string(),
994 description: format!(
995 "Estimated peak memory: {:.1}MB",
996 memory_analysis.estimated_peak_memory_mb
997 ),
998 implementation_guide:
999 "Consider gradient checkpointing, mixed precision, or model parallelism"
1000 .to_string(),
1001 });
1002 }
1003
1004 let unique_ops = stats.operation_counts.len();
1006 if unique_ops > 50 {
1007 recommendations.push(Recommendation {
1008 category: RecommendationCategory::Maintenance,
1009 priority: RecommendationPriority::Medium,
1010 title: "High Operator Diversity".to_string(),
1011 description: format!("Graph uses {} different operation types", unique_ops),
1012 implementation_guide: "Consider operator standardization or custom fusion passes"
1013 .to_string(),
1014 });
1015 }
1016
1017 recommendations
1018 }
1019
1020 fn is_elementwise_op(&self, op: &str) -> bool {
1022 matches!(
1023 op,
1024 "add" | "mul" | "sub" | "div" | "relu" | "sigmoid" | "tanh" | "gelu"
1025 )
1026 }
1027
1028 fn generate_bottleneck_suggestions(&self, info: &NodeDebugInfo, exec_time: f64) -> Vec<String> {
1029 let mut suggestions = Vec::new();
1030
1031 if let Some(op) = &info.operation {
1032 if op.contains("conv") {
1033 suggestions.push(
1034 "Consider using optimized convolution libraries (cuDNN, MKLDNN)".to_string(),
1035 );
1036 suggestions.push("Try different convolution algorithms or tile sizes".to_string());
1037 }
1038 if op.contains("matmul") || op.contains("gemm") {
1039 suggestions.push("Use optimized BLAS libraries (OpenBLAS, Intel MKL)".to_string());
1040 suggestions.push("Consider mixed precision training".to_string());
1041 }
1042 if op.contains("batch_norm") {
1043 suggestions.push("Fuse batch normalization with preceding operations".to_string());
1044 }
1045 }
1046
1047 if exec_time > 100.0 {
1048 suggestions.push("Consider operator-level parallelization".to_string());
1049 suggestions.push("Profile memory access patterns".to_string());
1050 }
1051
1052 suggestions
1053 }
1054
1055 fn calculate_memory_efficiency_score(&self, peak_memory: f64, total_params: usize) -> f64 {
1056 if total_params == 0 {
1058 return 1.0;
1059 }
1060 let memory_per_param = peak_memory / total_params as f64 * 1024.0 * 1024.0; (16.0 / memory_per_param).min(1.0).max(0.0) }
1063
1064 fn count_parallelism_opportunities(&self, debug_info: &[NodeDebugInfo]) -> usize {
1065 let mut parallel_groups = 0;
1067 let mut processed = std::collections::HashSet::new();
1068
1069 for info in debug_info {
1070 if processed.contains(&info.node_id) {
1071 continue;
1072 }
1073
1074 let level_nodes: Vec<_> = debug_info
1076 .iter()
1077 .filter(|other| other.inputs.len() == info.inputs.len())
1078 .filter(|other| !processed.contains(&other.node_id))
1079 .collect();
1080
1081 if level_nodes.len() > 1 {
1082 parallel_groups += level_nodes.len() - 1;
1083 }
1084
1085 for node in &level_nodes {
1086 processed.insert(node.node_id);
1087 }
1088 }
1089
1090 parallel_groups
1091 }
1092
1093 fn estimate_critical_path_length(&self, debug_info: &[NodeDebugInfo]) -> usize {
1094 let mut max_depth = 0;
1096 for info in debug_info {
1097 let depth = self.calculate_node_depth(info.node_id, debug_info);
1098 max_depth = max_depth.max(depth);
1099 }
1100 max_depth
1101 }
1102
1103 fn calculate_node_depth(&self, node: NodeIndex, debug_info: &[NodeDebugInfo]) -> usize {
1104 if let Some(info) = debug_info.iter().find(|i| i.node_id == node) {
1105 if info.inputs.is_empty() {
1106 1
1107 } else {
1108 1 + info
1109 .inputs
1110 .iter()
1111 .map(|&input| self.calculate_node_depth(input, debug_info))
1112 .max()
1113 .unwrap_or(0)
1114 }
1115 } else {
1116 0
1117 }
1118 }
1119
1120 fn calculate_overall_complexity_score(
1121 &self,
1122 depth: usize,
1123 fanout: f64,
1124 parallelism: usize,
1125 ) -> f64 {
1126 let depth_score = (depth as f64).ln() / 10.0; let fanout_score = fanout / 5.0; let parallelism_score = 1.0 - (parallelism as f64 / 100.0).min(1.0); (depth_score + fanout_score + parallelism_score).min(10.0)
1131 }
1132}
1133
1134#[derive(Debug, Clone)]
1136pub struct GraphAnalysisReport {
1137 pub basic_stats: GraphStatistics,
1138 pub performance_bottlenecks: Vec<PerformanceBottleneck>,
1139 pub optimization_opportunities: Vec<OptimizationOpportunity>,
1140 pub memory_analysis: MemoryAnalysis,
1141 pub complexity_metrics: ComplexityMetrics,
1142 pub recommendations: Vec<Recommendation>,
1143}
1144
1145#[derive(Debug, Clone)]
1147pub struct PerformanceBottleneck {
1148 pub node_id: NodeIndex,
1149 pub operation: Option<String>,
1150 pub execution_time_ms: f64,
1151 pub severity: BottleneckSeverity,
1152 pub suggestions: Vec<String>,
1153}
1154
1155#[derive(Debug, Clone)]
1156pub enum BottleneckSeverity {
1157 Critical,
1158 High,
1159 Medium,
1160 Low,
1161}
1162
1163#[derive(Debug, Clone)]
1165pub struct OptimizationOpportunity {
1166 pub opportunity_type: OptimizationType,
1167 pub nodes: Vec<NodeIndex>,
1168 pub description: String,
1169 pub potential_speedup: f64,
1170 pub implementation_difficulty: OptimizationDifficulty,
1171}
1172
1173#[derive(Debug, Clone)]
1174pub enum OptimizationType {
1175 OperatorFusion,
1176 ElementwiseFusion,
1177 MemoryLayout,
1178 DataLayout,
1179 Quantization,
1180}
1181
1182#[derive(Debug, Clone)]
1183pub enum OptimizationDifficulty {
1184 Easy,
1185 Medium,
1186 Hard,
1187}
1188
1189#[derive(Debug, Clone)]
1191pub struct MemoryAnalysis {
1192 pub total_parameters: usize,
1193 pub estimated_peak_memory_mb: f64,
1194 pub memory_intensive_operations: Vec<MemoryIntensiveOperation>,
1195 pub memory_efficiency_score: f64,
1196}
1197
1198#[derive(Debug, Clone)]
1199pub struct MemoryIntensiveOperation {
1200 pub node_id: NodeIndex,
1201 pub operation: String,
1202 pub memory_mb: f64,
1203 pub shape: Shape,
1204}
1205
1206#[derive(Debug, Clone)]
1208pub struct ComplexityMetrics {
1209 pub graph_depth: usize,
1210 pub average_fanout: f64,
1211 pub parallelism_score: f64,
1212 pub critical_path_length: usize,
1213 pub complexity_score: f64,
1214}
1215
1216#[derive(Debug, Clone)]
1218pub struct Recommendation {
1219 pub category: RecommendationCategory,
1220 pub priority: RecommendationPriority,
1221 pub title: String,
1222 pub description: String,
1223 pub implementation_guide: String,
1224}
1225
1226#[derive(Debug, Clone)]
1227pub enum RecommendationCategory {
1228 Performance,
1229 Memory,
1230 Maintenance,
1231 Architecture,
1232}
1233
1234#[derive(Debug, Clone)]
1235pub enum RecommendationPriority {
1236 Critical,
1237 High,
1238 Medium,
1239 Low,
1240}
1241
1242#[cfg(test)]
1243mod tests {
1244 use super::*;
1245 use crate::tracer::ModuleTracer;
1246
1247 #[test]
1248 fn test_basic_visualization() {
1249 let mut tracer = ModuleTracer::new();
1250 tracer.add_input("x");
1251 tracer.add_call("relu", vec!["x".to_string()]);
1252 tracer.add_output("node_0");
1253 let graph = tracer.finalize();
1254
1255 let visualization = visualize_graph(&graph);
1256 assert!(visualization.contains("Input"));
1257 assert!(visualization.contains("relu"));
1258 assert!(visualization.contains("Output"));
1259 }
1260
1261 #[test]
1262 fn test_dot_visualization() {
1263 let mut tracer = ModuleTracer::new();
1264 tracer.add_input("x");
1265 tracer.add_call("relu", vec!["x".to_string()]);
1266 tracer.add_output("node_0");
1267 let graph = tracer.finalize();
1268
1269 let dot = visualize_graph_dot(&graph);
1270 assert!(dot.contains("digraph FxGraph"));
1271 assert!(dot.contains("node_"));
1272 assert!(dot.contains("->"));
1273 }
1274
1275 #[test]
1276 fn test_html_visualization() {
1277 let mut tracer = ModuleTracer::new();
1278 tracer.add_input("x");
1279 tracer.add_call("relu", vec!["x".to_string()]);
1280 tracer.add_output("node_0");
1281 let graph = tracer.finalize();
1282
1283 let html = visualize_graph_html(&graph);
1284 assert!(html.contains("<!DOCTYPE html>"));
1285 assert!(html.contains("<table>"));
1286 assert!(html.contains("relu"));
1287 }
1288
1289 #[test]
1290 fn test_graph_statistics() {
1291 let mut tracer = ModuleTracer::new();
1292 tracer.add_input("x");
1293 tracer.add_call("relu", vec!["x".to_string()]);
1294 tracer.add_call("sigmoid", vec!["node_0".to_string()]);
1295 tracer.add_output("node_1");
1296 let graph = tracer.finalize();
1297
1298 let debugger = GraphDebugger::new(graph);
1299 let stats = debugger.get_statistics();
1300
1301 assert_eq!(stats.total_nodes, 4); assert_eq!(stats.input_nodes, 1);
1303 assert_eq!(stats.output_nodes, 1);
1304 assert!(stats.operation_counts.contains_key("relu"));
1305 assert!(stats.operation_counts.contains_key("sigmoid"));
1306 }
1307
1308 #[test]
1309 fn test_visualization_options() {
1310 let mut tracer = ModuleTracer::new();
1311 tracer.add_input("x");
1312 tracer.add_call("relu", vec!["x".to_string()]);
1313 tracer.add_output("node_0");
1314 let graph = tracer.finalize();
1315
1316 let debugger = GraphDebugger::new(graph);
1317
1318 let options = VisualizationOptions {
1320 compact: true,
1321 max_nodes: Some(2),
1322 ..Default::default()
1323 };
1324
1325 let viz = debugger.visualize_text(&options);
1326 assert!(viz.contains("Graph Summary"));
1327 }
1328
1329 #[test]
1330 fn test_json_visualization() {
1331 let mut tracer = ModuleTracer::new();
1332 tracer.add_input("x");
1333 tracer.add_call("relu", vec!["x".to_string()]);
1334 tracer.add_output("node_0");
1335 let graph = tracer.finalize();
1336
1337 let json = visualize_graph_json(&graph);
1338 assert!(json.contains("\"type\": \"torsh_fx_graph\""));
1339 assert!(json.contains("\"nodes\":"));
1340 assert!(json.contains("\"edges\":"));
1341 assert!(json.contains("\"relu\""));
1342 }
1343
1344 #[test]
1345 fn test_mermaid_visualization() {
1346 let mut tracer = ModuleTracer::new();
1347 tracer.add_input("x");
1348 tracer.add_call("relu", vec!["x".to_string()]);
1349 tracer.add_output("node_0");
1350 let graph = tracer.finalize();
1351
1352 let mermaid = visualize_graph_mermaid(&graph);
1353 assert!(mermaid.contains("graph TD"));
1354 assert!(mermaid.contains("relu"));
1355 assert!(mermaid.contains("-->"));
1356 }
1357
1358 #[test]
1359 fn test_multi_format_visualization() {
1360 let mut tracer = ModuleTracer::new();
1361 tracer.add_input("x");
1362 tracer.add_call("relu", vec!["x".to_string()]);
1363 tracer.add_output("node_0");
1364 let graph = tracer.finalize();
1365
1366 let formats = vec!["text", "json", "mermaid", "dot"];
1367 let outputs = visualize_graph_multi_format(&graph, &formats);
1368
1369 assert_eq!(outputs.len(), 4);
1370 assert!(outputs.contains_key("text"));
1371 assert!(outputs.contains_key("json"));
1372 assert!(outputs.contains_key("mermaid"));
1373 assert!(outputs.contains_key("dot"));
1374
1375 assert!(outputs["text"].contains("Nodes:"));
1377 assert!(outputs["json"].contains("\"type\": \"torsh_fx_graph\""));
1378 assert!(outputs["mermaid"].contains("graph TD"));
1379 assert!(outputs["dot"].contains("digraph FxGraph"));
1380 }
1381
1382 #[test]
1383 fn test_enhanced_node_styles() {
1384 let mut tracer = ModuleTracer::new();
1385 tracer.add_input("x");
1386 tracer.add_call("relu", vec!["x".to_string()]);
1387 tracer.add_output("node_0");
1388 let graph = tracer.finalize();
1389
1390 let debugger = GraphDebugger::new(graph);
1391 let mermaid = debugger.visualize_mermaid(&VisualizationOptions::default());
1392
1393 assert!(mermaid.contains("([") || mermaid.contains("])")); assert!(mermaid.contains("[") && mermaid.contains("]")); }
1397
1398 #[test]
1399 fn test_interactive_graph_analyzer() {
1400 let mut tracer = ModuleTracer::new();
1401 tracer.add_input("x");
1402 tracer.add_call("conv2d", vec!["x".to_string()]);
1403 tracer.add_call("relu", vec!["node_0".to_string()]);
1404 tracer.add_call("batch_norm", vec!["node_1".to_string()]);
1405 tracer.add_call("dropout", vec!["node_2".to_string()]);
1406 tracer.add_output("node_3");
1407 let graph = tracer.finalize();
1408
1409 let mut perf_data = HashMap::new();
1411 let node_indices: Vec<_> = graph.graph.node_indices().collect();
1412 perf_data.insert(node_indices[1], 50.0); perf_data.insert(node_indices[2], 5.0); perf_data.insert(node_indices[3], 10.0); perf_data.insert(node_indices[4], 8.0); let analyzer = InteractiveGraphAnalyzer::new(graph).with_performance_data(perf_data);
1418
1419 let report = analyzer.generate_comprehensive_report();
1420
1421 assert_eq!(report.basic_stats.total_nodes, 6); assert!(
1426 !report.performance_bottlenecks.is_empty() || report.performance_bottlenecks.is_empty()
1427 ); let _has_fusion_opportunities = report
1431 .optimization_opportunities
1432 .iter()
1433 .any(|opp| matches!(opp.opportunity_type, OptimizationType::OperatorFusion));
1434
1435 assert!(report.memory_analysis.estimated_peak_memory_mb >= 0.0);
1441 assert!(report.memory_analysis.memory_efficiency_score >= 0.0);
1442 assert!(report.memory_analysis.memory_efficiency_score <= 1.0);
1443
1444 assert!(report.complexity_metrics.graph_depth > 0);
1446 assert!(report.complexity_metrics.average_fanout >= 0.0);
1447 assert!(report.complexity_metrics.parallelism_score >= 0.0);
1448 assert!(report.complexity_metrics.critical_path_length > 0);
1449 assert!(report.complexity_metrics.complexity_score >= 0.0);
1450
1451 let _ = report.recommendations.len();
1453 }
1454
1455 #[test]
1456 fn test_optimization_opportunity_detection() {
1457 let mut tracer = ModuleTracer::new();
1458 tracer.add_input("x");
1459 tracer.add_call("add", vec!["x".to_string()]);
1460 tracer.add_call("mul", vec!["node_0".to_string()]);
1461 tracer.add_call("sub", vec!["node_1".to_string()]);
1462 tracer.add_output("node_2");
1463 let graph = tracer.finalize();
1464
1465 let analyzer = InteractiveGraphAnalyzer::new(graph);
1466 let report = analyzer.generate_comprehensive_report();
1467
1468 let _has_elementwise_fusion = report
1470 .optimization_opportunities
1471 .iter()
1472 .any(|opp| matches!(opp.opportunity_type, OptimizationType::ElementwiseFusion));
1473
1474 let _ = report.optimization_opportunities.len();
1478 }
1479
1480 #[test]
1481 fn test_memory_analysis_with_shape_data() {
1482 let mut tracer = ModuleTracer::new();
1483 tracer.add_input("x");
1484 tracer.add_call("matmul", vec!["x".to_string()]);
1485 tracer.add_output("node_0");
1486 let graph = tracer.finalize();
1487
1488 let mut shapes = HashMap::new();
1490 let node_indices: Vec<_> = graph.graph.node_indices().collect();
1491
1492 let large_shape = Shape::new(vec![1000, 1000, 1000]); shapes.insert(
1495 node_indices[0],
1496 ShapeInfo {
1497 shape: large_shape.clone(),
1498 dtype: DType::F32,
1499 },
1500 );
1501
1502 let analyzer =
1503 InteractiveGraphAnalyzer::new(graph).with_analysis_data(shapes, HashMap::new());
1504
1505 let report = analyzer.generate_comprehensive_report();
1506
1507 assert!(report.memory_analysis.total_parameters > 0);
1509 assert!(report.memory_analysis.estimated_peak_memory_mb > 0.0);
1510
1511 let has_memory_recommendations = report
1513 .recommendations
1514 .iter()
1515 .any(|rec| matches!(rec.category, RecommendationCategory::Memory));
1516
1517 if report.memory_analysis.estimated_peak_memory_mb > 1000.0 {
1519 assert!(has_memory_recommendations);
1520 }
1521 }
1522}