Skip to main content

torsh_fx/interpreter/
debug.rs

1//! Debug and Development Tools for FX Graph Interpretation
2//!
3//! This module provides comprehensive debugging capabilities for FX graph execution,
4//! including debug execution environments, step-by-step execution logging,
5//! and graph validation utilities.
6
7use crate::interpreter::execution::ExecutionEnvironment;
8use crate::interpreter::metrics::ExecutionMetrics;
9use crate::{FxGraph, Node, TorshResult};
10use petgraph::graph::NodeIndex;
11use torsh_core::{device::DeviceType, error::TorshError};
12use torsh_tensor::Tensor;
13
14/// Enhanced execution environment with debugging capabilities
15///
16/// Extends the basic execution environment with detailed logging, step-by-step execution,
17/// and comprehensive debugging information for development and troubleshooting.
18pub struct DebugExecutionEnvironment {
19    /// Base execution environment
20    env: ExecutionEnvironment,
21    /// Execution metrics
22    metrics: ExecutionMetrics,
23    /// Debug mode flag
24    debug_mode: bool,
25    /// Step-by-step execution log
26    execution_log: Vec<String>,
27}
28
29impl DebugExecutionEnvironment {
30    /// Create a new debug execution environment
31    ///
32    /// # Arguments
33    /// * `device` - Device type to execute on
34    /// * `debug_mode` - Whether to enable detailed debugging
35    ///
36    /// # Returns
37    /// * `Self` - New debug execution environment
38    pub fn new(device: DeviceType, debug_mode: bool) -> Self {
39        Self {
40            env: ExecutionEnvironment::new(device),
41            metrics: ExecutionMetrics::new(),
42            debug_mode,
43            execution_log: Vec::new(),
44        }
45    }
46
47    /// Get reference to base execution environment
48    ///
49    /// # Returns
50    /// * `&ExecutionEnvironment` - Reference to underlying execution environment
51    pub fn env(&self) -> &ExecutionEnvironment {
52        &self.env
53    }
54
55    /// Get mutable reference to base execution environment
56    ///
57    /// # Returns
58    /// * `&mut ExecutionEnvironment` - Mutable reference to execution environment
59    pub fn env_mut(&mut self) -> &mut ExecutionEnvironment {
60        &mut self.env
61    }
62
63    /// Get execution metrics
64    ///
65    /// # Returns
66    /// * `&ExecutionMetrics` - Reference to collected execution metrics
67    pub fn metrics(&self) -> &ExecutionMetrics {
68        &self.metrics
69    }
70
71    /// Check if debug mode is enabled
72    ///
73    /// # Returns
74    /// * `bool` - True if debug mode is enabled
75    pub fn is_debug_mode(&self) -> bool {
76        self.debug_mode
77    }
78
79    /// Set debug mode
80    ///
81    /// # Arguments
82    /// * `enabled` - Whether to enable debug mode
83    pub fn set_debug_mode(&mut self, enabled: bool) {
84        self.debug_mode = enabled;
85    }
86
87    /// Add entry to execution log
88    ///
89    /// # Arguments
90    /// * `message` - Log message to add
91    pub fn log(&mut self, message: String) {
92        if self.debug_mode {
93            self.execution_log.push(message);
94        }
95    }
96
97    /// Get execution log
98    ///
99    /// # Returns
100    /// * `&[String]` - Reference to execution log entries
101    pub fn get_log(&self) -> &[String] {
102        &self.execution_log
103    }
104
105    /// Clear execution log
106    pub fn clear_log(&mut self) {
107        self.execution_log.clear();
108    }
109
110    /// Log node execution start
111    ///
112    /// # Arguments
113    /// * `node_idx` - Index of node being executed
114    /// * `node` - Node information
115    pub fn log_node_start(&mut self, node_idx: NodeIndex, node: &Node) {
116        let message = match node {
117            Node::Input(name) => format!("Executing input node {:?}: '{}'", node_idx, name),
118            Node::Call(op_name, args) => format!(
119                "Executing call node {:?}: operation '{}' with {} args",
120                node_idx,
121                op_name,
122                args.len()
123            ),
124            Node::Output => format!("Executing output node {:?}", node_idx),
125            Node::Conditional { condition, .. } => format!(
126                "Executing conditional node {:?}: condition '{}'",
127                node_idx, condition
128            ),
129            Node::Loop { condition, .. } => format!(
130                "Executing loop node {:?}: condition '{}'",
131                node_idx, condition
132            ),
133            Node::Merge { inputs, .. } => format!(
134                "Executing merge node {:?}: {} inputs",
135                node_idx,
136                inputs.len()
137            ),
138            Node::GetAttr { target, attr } => format!(
139                "Executing get_attr node {:?}: {}.{}",
140                node_idx, target, attr
141            ),
142        };
143        self.log(message);
144    }
145
146    /// Log node execution completion
147    ///
148    /// # Arguments
149    /// * `node_idx` - Index of completed node
150    /// * `duration_ms` - Execution duration in milliseconds
151    pub fn log_node_completion(&mut self, node_idx: NodeIndex, duration_ms: f64) {
152        let message = format!("Completed node {:?} in {:.2} ms", node_idx, duration_ms);
153        self.log(message);
154    }
155
156    /// Log tensor information
157    ///
158    /// # Arguments
159    /// * `node_idx` - Index of node associated with tensor
160    /// * `tensor` - Tensor to log information about
161    pub fn log_tensor_info(&mut self, node_idx: NodeIndex, tensor: &Tensor) {
162        if self.debug_mode {
163            let message = format!(
164                "Node {:?} tensor: shape={:?}, dtype={:?}, device={:?}",
165                node_idx,
166                tensor.shape().dims(),
167                tensor.dtype(),
168                tensor.device()
169            );
170            self.log(message);
171        }
172    }
173
174    /// Log operation execution
175    ///
176    /// # Arguments
177    /// * `op_name` - Name of operation
178    /// * `input_count` - Number of input tensors
179    /// * `duration_ms` - Operation execution time
180    pub fn log_operation(&mut self, op_name: &str, input_count: usize, duration_ms: f64) {
181        if self.debug_mode {
182            let message = format!(
183                "Operation '{}' with {} inputs completed in {:.2} ms",
184                op_name, input_count, duration_ms
185            );
186            self.log(message);
187        }
188        self.metrics.add_operation_time(op_name, duration_ms);
189    }
190
191    /// Log error information
192    ///
193    /// # Arguments
194    /// * `node_idx` - Index of node where error occurred
195    /// * `error` - Error that occurred
196    pub fn log_error(&mut self, node_idx: NodeIndex, error: &TorshError) {
197        let message = format!("Error at node {:?}: {}", node_idx, error);
198        self.log(message);
199    }
200
201    /// Generate debug report
202    ///
203    /// # Returns
204    /// * `String` - Comprehensive debug report
205    pub fn generate_debug_report(&self) -> String {
206        let mut report = String::new();
207
208        report.push_str("=== Debug Execution Report ===\n\n");
209
210        // Environment information
211        report.push_str(&format!("Device: {:?}\n", self.env.device()));
212        report.push_str(&format!("Debug Mode: {}\n", self.debug_mode));
213        report.push_str(&format!("Stored Values: {}\n", self.env.value_count()));
214        report.push_str(&format!("Log Entries: {}\n\n", self.execution_log.len()));
215
216        // Performance metrics
217        if !self.metrics.is_empty() {
218            report.push_str("=== Performance Metrics ===\n");
219            report.push_str(&self.metrics.generate_report());
220            report.push_str("\n\n");
221        }
222
223        // Execution log
224        if !self.execution_log.is_empty() {
225            report.push_str("=== Execution Log ===\n");
226            for (i, entry) in self.execution_log.iter().enumerate() {
227                report.push_str(&format!("{:4}: {}\n", i + 1, entry));
228            }
229        }
230
231        report
232    }
233
234    /// Print debug information to console
235    pub fn print_debug_info(&self) {
236        if self.debug_mode {
237            println!("{}", self.generate_debug_report());
238        }
239    }
240
241    /// Save debug report to string
242    ///
243    /// # Returns
244    /// * `String` - Complete debug report
245    pub fn save_debug_report(&self) -> String {
246        self.generate_debug_report()
247    }
248
249    /// Reset debug state
250    pub fn reset(&mut self) {
251        self.env.clear();
252        self.metrics.clear();
253        self.execution_log.clear();
254    }
255
256    /// Get summary of execution state
257    ///
258    /// # Returns
259    /// * `String` - Brief execution state summary
260    pub fn execution_summary(&self) -> String {
261        format!(
262            "Debug Environment: {} values stored, {} operations, {:.2}ms total, {} log entries",
263            self.env.value_count(),
264            self.metrics.operation_count,
265            self.metrics.total_time_ms,
266            self.execution_log.len()
267        )
268    }
269}
270
271/// Utility functions for interpreter debugging and validation
272pub mod utils {
273    use super::*;
274    use crate::interpreter::operations::is_operation_registered;
275
276    /// Validate that a graph can be executed (all required operations are available)
277    ///
278    /// Checks that all operations referenced in the graph are either built-in
279    /// or registered as custom operations.
280    ///
281    /// # Arguments
282    /// * `graph` - FX graph to validate
283    ///
284    /// # Returns
285    /// * `TorshResult<()>` - Ok if graph is executable, error with missing operations
286    pub fn validate_graph_executability(graph: &FxGraph) -> TorshResult<()> {
287        let mut missing_ops = Vec::new();
288
289        for (_, node) in graph.call_nodes() {
290            if let Node::Call(op_name, _) = node {
291                if !is_operation_registered(op_name) && !is_builtin_operation(op_name) {
292                    missing_ops.push(op_name.clone());
293                }
294            }
295        }
296
297        if !missing_ops.is_empty() {
298            return Err(TorshError::InvalidArgument(format!(
299                "Missing operations: {}",
300                missing_ops.join(", ")
301            )));
302        }
303
304        Ok(())
305    }
306
307    /// Check if an operation is a built-in operation
308    ///
309    /// # Arguments
310    /// * `op_name` - Name of operation to check
311    ///
312    /// # Returns
313    /// * `bool` - True if operation is built-in
314    pub fn is_builtin_operation(op_name: &str) -> bool {
315        matches!(
316            op_name,
317            "add"
318                | "sub"
319                | "mul"
320                | "div"
321                | "matmul"
322                | "relu"
323                | "sigmoid"
324                | "tanh"
325                | "gelu"
326                | "softmax"
327                | "layer_norm"
328                | "batch_norm"
329                | "conv2d"
330                | "linear"
331                | "linear_relu"
332                | "conv2d_relu"
333        )
334    }
335
336    /// Estimate execution complexity of a graph
337    ///
338    /// Provides a rough estimate of computational complexity based on
339    /// operation types and counts.
340    ///
341    /// # Arguments
342    /// * `graph` - FX graph to analyze
343    ///
344    /// # Returns
345    /// * `usize` - Estimated complexity score
346    pub fn estimate_execution_complexity(graph: &FxGraph) -> usize {
347        let mut complexity = 0;
348
349        for (_, node) in graph.call_nodes() {
350            if let Node::Call(op_name, _) = node {
351                complexity += match op_name.as_str() {
352                    "add" | "sub" | "mul" | "div" => 1,
353                    "matmul" | "linear" => 10,
354                    "conv2d" => 20,
355                    "relu" | "sigmoid" | "tanh" => 2,
356                    "gelu" | "softmax" | "layer_norm" | "batch_norm" => 5,
357                    "linear_relu" => 12, // linear + relu
358                    "conv2d_relu" => 22, // conv2d + relu
359                    _ => 3,              // Default complexity for unknown ops
360                };
361            }
362        }
363
364        complexity
365    }
366
367    /// Generate execution summary for a graph
368    ///
369    /// Creates a detailed summary of graph structure and expected execution
370    /// characteristics.
371    ///
372    /// # Arguments
373    /// * `graph` - FX graph to analyze
374    ///
375    /// # Returns
376    /// * `String` - Formatted execution summary
377    pub fn generate_execution_summary(graph: &FxGraph) -> String {
378        let call_nodes = graph.call_nodes();
379        let op_counts = graph.operation_counts();
380        let complexity = estimate_execution_complexity(graph);
381
382        let mut summary = format!(
383            "Graph Execution Summary:\n\
384             Total Operations: {}\n\
385             Estimated Complexity: {}\n\
386             Operation Types: {}\n\
387             Input Nodes: {}\n\
388             Output Nodes: {}\n\n\
389             Operation Distribution:",
390            call_nodes.len(),
391            complexity,
392            op_counts.len(),
393            graph.inputs().len(),
394            graph.outputs().len()
395        );
396
397        let mut sorted_ops: Vec<_> = op_counts.iter().collect();
398        sorted_ops.sort_by(|a, b| b.1.cmp(a.1));
399
400        for (op_name, count) in sorted_ops {
401            let op_complexity = match op_name.as_str() {
402                "add" | "sub" | "mul" | "div" => 1,
403                "matmul" | "linear" => 10,
404                "conv2d" => 20,
405                "relu" | "sigmoid" | "tanh" => 2,
406                "gelu" | "softmax" | "layer_norm" | "batch_norm" => 5,
407                _ => 3,
408            };
409            summary.push_str(&format!(
410                "\n  {}: {} instances (complexity: {} each)",
411                op_name, count, op_complexity
412            ));
413        }
414
415        // Add recommendations
416        summary.push_str("\n\nRecommendations:");
417        if complexity > 1000 {
418            summary.push_str("\n  - High complexity graph: consider optimization");
419        }
420        if op_counts.len() > 50 {
421            summary.push_str("\n  - Many operation types: verify all are available");
422        }
423        if call_nodes.len() > 500 {
424            summary.push_str("\n  - Large graph: consider batching or partitioning");
425        }
426
427        summary
428    }
429
430    /// Validate graph structure integrity
431    ///
432    /// Performs comprehensive validation of graph structure, checking for
433    /// common issues and inconsistencies.
434    ///
435    /// # Arguments
436    /// * `graph` - FX graph to validate
437    ///
438    /// # Returns
439    /// * `TorshResult<()>` - Ok if graph structure is valid, error otherwise
440    pub fn validate_graph_structure(graph: &FxGraph) -> TorshResult<()> {
441        // Check for empty graph
442        if graph.graph.node_count() == 0 {
443            return Err(TorshError::InvalidArgument("Graph is empty".to_string()));
444        }
445
446        // Check for inputs and outputs
447        if graph.inputs().is_empty() {
448            return Err(TorshError::InvalidArgument(
449                "Graph has no input nodes".to_string(),
450            ));
451        }
452
453        if graph.outputs().is_empty() {
454            return Err(TorshError::InvalidArgument(
455                "Graph has no output nodes".to_string(),
456            ));
457        }
458
459        // Check for cycles (this would be caught during execution, but good to check early)
460        use petgraph::algo::is_cyclic_directed;
461        if is_cyclic_directed(&graph.graph) {
462            return Err(TorshError::InvalidArgument(
463                "Graph contains cycles".to_string(),
464            ));
465        }
466
467        // Check that all nodes are reachable from inputs
468        // (This is a simplified check - a full implementation would use graph traversal)
469        let node_count = graph.graph.node_count();
470        let input_count = graph.inputs().len();
471        let output_count = graph.outputs().len();
472
473        if node_count < input_count + output_count {
474            return Err(TorshError::InvalidArgument(
475                "Invalid node count relationship".to_string(),
476            ));
477        }
478
479        Ok(())
480    }
481
482    /// Create debug-friendly graph description
483    ///
484    /// # Arguments
485    /// * `graph` - FX graph to describe
486    ///
487    /// # Returns
488    /// * `String` - Human-readable graph description
489    pub fn describe_graph(graph: &FxGraph) -> String {
490        let mut description = String::new();
491
492        description.push_str("=== FX Graph Description ===\n\n");
493
494        // Basic statistics
495        description.push_str(&format!("Nodes: {}\n", graph.graph.node_count()));
496        description.push_str(&format!("Edges: {}\n", graph.graph.edge_count()));
497        description.push_str(&format!("Inputs: {}\n", graph.inputs().len()));
498        description.push_str(&format!("Outputs: {}\n", graph.outputs().len()));
499
500        // Input nodes
501        description.push_str("\nInput Nodes:\n");
502        for &input_idx in graph.inputs() {
503            if let Some(Node::Input(name)) = graph.get_node(input_idx) {
504                description.push_str(&format!("  {:?}: '{}'\n", input_idx, name));
505            }
506        }
507
508        // Output nodes
509        description.push_str("\nOutput Nodes:\n");
510        for &output_idx in graph.outputs() {
511            if let Some(Node::Output) = graph.get_node(output_idx) {
512                description.push_str(&format!("  {:?}\n", output_idx));
513            }
514        }
515
516        // Operation summary
517        let op_counts = graph.operation_counts();
518        if !op_counts.is_empty() {
519            description.push_str("\nOperations:\n");
520            let mut sorted_ops: Vec<_> = op_counts.iter().collect();
521            sorted_ops.sort_by(|a, b| b.1.cmp(a.1));
522            for (op_name, count) in sorted_ops {
523                description.push_str(&format!("  {}: {} instances\n", op_name, count));
524            }
525        }
526
527        description
528    }
529}