Skip to main content

torsh_fx/interpreter/
execution.rs

1//! Graph Execution Engine for FX Graph Interpretation
2//!
3//! This module provides the core execution capabilities for FX graphs, including
4//! execution environments, graph interpreters, and built-in operation implementations.
5//! It handles tensor storage, topological execution ordering, and operation dispatch.
6
7use crate::interpreter::operations::{execute_registered_operation, is_operation_registered};
8use crate::{FxGraph, Node, TorshResult};
9use petgraph::algo::toposort;
10use petgraph::graph::NodeIndex;
11use std::collections::HashMap;
12use torsh_core::{device::DeviceType, error::TorshError};
13use torsh_tensor::{creation::*, Tensor};
14
15/// Execution environment for graph interpretation
16///
17/// Manages tensor storage and execution context during graph interpretation.
18/// Provides methods for storing and retrieving intermediate tensor values
19/// during execution.
20pub struct ExecutionEnvironment {
21    /// Tensor storage during execution
22    pub values: HashMap<NodeIndex, Tensor>,
23    /// Device to execute on
24    device: DeviceType,
25}
26
27impl ExecutionEnvironment {
28    /// Create a new execution environment
29    ///
30    /// # Arguments
31    /// * `device` - Device type to execute tensors on
32    ///
33    /// # Returns
34    /// * `Self` - New execution environment
35    pub fn new(device: DeviceType) -> Self {
36        Self {
37            values: HashMap::new(),
38            device,
39        }
40    }
41
42    /// Store a tensor value
43    ///
44    /// # Arguments
45    /// * `node` - Node index to store the tensor for
46    /// * `tensor` - Tensor value to store
47    pub fn store(&mut self, node: NodeIndex, tensor: Tensor) {
48        self.values.insert(node, tensor);
49    }
50
51    /// Retrieve a tensor value
52    ///
53    /// # Arguments
54    /// * `node` - Node index to retrieve tensor for
55    ///
56    /// # Returns
57    /// * `Option<&Tensor>` - Reference to stored tensor if available
58    pub fn get(&self, node: NodeIndex) -> Option<&Tensor> {
59        self.values.get(&node)
60    }
61
62    /// Get device
63    ///
64    /// # Returns
65    /// * `DeviceType` - Execution device type
66    pub fn device(&self) -> DeviceType {
67        self.device
68    }
69
70    /// Clear all stored values
71    pub fn clear(&mut self) {
72        self.values.clear();
73    }
74
75    /// Get number of stored values
76    ///
77    /// # Returns
78    /// * `usize` - Number of stored tensor values
79    pub fn value_count(&self) -> usize {
80        self.values.len()
81    }
82
83    /// Check if a value is stored for a node
84    ///
85    /// # Arguments
86    /// * `node` - Node index to check
87    ///
88    /// # Returns
89    /// * `bool` - True if value is stored, false otherwise
90    pub fn has_value(&self, node: NodeIndex) -> bool {
91        self.values.contains_key(&node)
92    }
93}
94
95/// Interpreter for executing FX graphs
96///
97/// Provides high-level interface for executing complete FX graphs with input tensors.
98/// Manages execution order, node dispatch, and output collection.
99pub struct GraphInterpreter {
100    env: ExecutionEnvironment,
101}
102
103impl GraphInterpreter {
104    /// Create a new interpreter
105    ///
106    /// # Arguments
107    /// * `device` - Device type to execute on
108    ///
109    /// # Returns
110    /// * `Self` - New graph interpreter
111    pub fn new(device: DeviceType) -> Self {
112        Self {
113            env: ExecutionEnvironment::new(device),
114        }
115    }
116
117    /// Execute a graph with given inputs
118    ///
119    /// # Arguments
120    /// * `graph` - FX graph to execute
121    /// * `inputs` - Map of input node names to their tensor values
122    ///
123    /// # Returns
124    /// * `TorshResult<Vec<Tensor>>` - Vector of output tensors or error
125    pub fn run(
126        &mut self,
127        graph: &FxGraph,
128        inputs: HashMap<String, Tensor>,
129    ) -> TorshResult<Vec<Tensor>> {
130        // Clear previous execution state
131        self.env.values.clear();
132
133        // Process input nodes
134        for &input_idx in graph.inputs() {
135            if let Some(Node::Input(name)) = graph.get_node(input_idx) {
136                if let Some(input_tensor) = inputs.get(name) {
137                    self.env.store(input_idx, input_tensor.clone());
138                } else {
139                    return Err(TorshError::InvalidArgument(format!(
140                        "Missing input: {}",
141                        name
142                    )));
143                }
144            }
145        }
146
147        // Topological execution of nodes
148        let execution_order = self.compute_execution_order(graph)?;
149
150        for node_idx in execution_order {
151            self.execute_node(graph, node_idx)?;
152        }
153
154        // Collect outputs
155        let mut outputs = Vec::new();
156        for &output_idx in graph.outputs() {
157            // Find the input to this output node
158            let predecessors: Vec<_> = graph
159                .graph
160                .neighbors_directed(output_idx, petgraph::Direction::Incoming)
161                .collect();
162
163            if let Some(&pred_idx) = predecessors.first() {
164                if let Some(tensor) = self.env.get(pred_idx) {
165                    outputs.push(tensor.clone());
166                }
167            }
168        }
169
170        Ok(outputs)
171    }
172
173    /// Get reference to execution environment
174    ///
175    /// # Returns
176    /// * `&ExecutionEnvironment` - Reference to execution environment
177    pub fn env(&self) -> &ExecutionEnvironment {
178        &self.env
179    }
180
181    /// Get mutable reference to execution environment
182    ///
183    /// # Returns
184    /// * `&mut ExecutionEnvironment` - Mutable reference to execution environment
185    pub fn env_mut(&mut self) -> &mut ExecutionEnvironment {
186        &mut self.env
187    }
188
189    /// Compute topological execution order
190    ///
191    /// # Arguments
192    /// * `graph` - FX graph to compute execution order for
193    ///
194    /// # Returns
195    /// * `TorshResult<Vec<NodeIndex>>` - Topologically sorted node indices
196    fn compute_execution_order(&self, graph: &FxGraph) -> TorshResult<Vec<NodeIndex>> {
197        match toposort(&graph.graph, None) {
198            Ok(order) => Ok(order),
199            Err(_) => Err(TorshError::InvalidArgument(
200                "Graph contains cycles".to_string(),
201            )),
202        }
203    }
204
205    /// Execute a single node
206    ///
207    /// # Arguments
208    /// * `graph` - FX graph containing the node
209    /// * `node_idx` - Index of node to execute
210    ///
211    /// # Returns
212    /// * `TorshResult<()>` - Ok if execution succeeds, error otherwise
213    fn execute_node(&mut self, graph: &FxGraph, node_idx: NodeIndex) -> TorshResult<()> {
214        let node = graph
215            .get_node(node_idx)
216            .ok_or_else(|| TorshError::InvalidArgument(format!("Node {node_idx:?} not found")))?;
217
218        match node {
219            Node::Input(_) => {
220                // Input nodes are already processed
221                Ok(())
222            }
223            Node::Call(op_name, args) => {
224                // Constant nodes carry their value in the argument list rather than
225                // in incoming edges, so they are materialised before dispatch.
226                if let Some(tensor) = Self::materialize_constant(op_name, args)? {
227                    self.env.store(node_idx, tensor);
228                    return Ok(());
229                }
230
231                // Get input tensors
232                let input_tensors = self.get_inputs_for_args(graph, node_idx, args)?;
233
234                // Execute operation
235                let result = self.execute_operation(op_name, input_tensors)?;
236
237                // Store result
238                self.env.store(node_idx, result);
239                Ok(())
240            }
241            Node::Output => {
242                // Output nodes don't need execution
243                Ok(())
244            }
245            Node::Conditional {
246                condition,
247                then_branch,
248                else_branch,
249            } => self.execute_conditional(graph, node_idx, condition, then_branch, else_branch),
250            Node::Loop {
251                condition,
252                body,
253                loop_vars,
254            } => self.execute_loop(graph, node_idx, condition, body, loop_vars),
255            Node::Merge { inputs } => self.execute_merge(graph, node_idx, inputs),
256            Node::GetAttr { target, attr } => self.execute_get_attr(graph, node_idx, target, attr),
257        }
258    }
259
260    /// Materialize a constant node into a scalar tensor
261    ///
262    /// Recognises the constant nodes produced by the graph passes:
263    /// `constant(<literal>)` (constant folding), `constant_zero` and `constant_one`
264    /// (graph simplification).
265    ///
266    /// # Arguments
267    /// * `op_name` - Operation name of the node
268    /// * `args` - Node arguments
269    ///
270    /// # Returns
271    /// * `TorshResult<Option<Tensor>>` - The materialized constant, or `None` when
272    ///   the node is not a constant
273    fn materialize_constant(op_name: &str, args: &[String]) -> TorshResult<Option<Tensor>> {
274        let value = match op_name {
275            "constant" => {
276                let literal = args.first().ok_or_else(|| {
277                    TorshError::InvalidArgument(
278                        "constant node requires a literal argument".to_string(),
279                    )
280                })?;
281                literal.parse::<f32>().map_err(|_| {
282                    TorshError::InvalidArgument(format!(
283                        "constant node has a non-numeric literal: {literal}"
284                    ))
285                })?
286            }
287            "constant_zero" => 0.0,
288            "constant_one" => 1.0,
289            _ => return Ok(None),
290        };
291
292        Ok(Some(full(&[1], value)?))
293    }
294
295    /// Get input tensors for operation arguments
296    ///
297    /// Operands are ordered by the node's argument list: each argument is matched
298    /// against the name carried by the incoming edge that supplies it. Order matters
299    /// for non-commutative operations (`sub`, `div`, `matmul`, `conv2d`, `linear`),
300    /// and `neighbors_directed` yields edges in reverse insertion order, so relying
301    /// on the raw predecessor order computes `sub(a, b)` as `b - a`.
302    ///
303    /// Incoming values that no argument names (and every value when the node has no
304    /// arguments) keep their edge insertion order and are appended at the end.
305    ///
306    /// # Arguments
307    /// * `graph` - FX graph containing the node
308    /// * `node_idx` - Index of the node to get inputs for
309    /// * `args` - Operation arguments, in the order the operation expects them
310    ///
311    /// # Returns
312    /// * `TorshResult<Vec<Tensor>>` - Vector of input tensors in argument order
313    fn get_inputs_for_args(
314        &self,
315        graph: &FxGraph,
316        node_idx: NodeIndex,
317        args: &[String],
318    ) -> TorshResult<Vec<Tensor>> {
319        use petgraph::visit::EdgeRef;
320
321        // Edge insertion order: petgraph iterates incoming edges newest first.
322        let mut incoming: Vec<(petgraph::graph::EdgeIndex, String, NodeIndex)> = graph
323            .graph
324            .edges_directed(node_idx, petgraph::Direction::Incoming)
325            .map(|edge| (edge.id(), edge.weight().name.clone(), edge.source()))
326            .collect();
327        incoming.sort_by_key(|(edge_idx, _, _)| edge_idx.index());
328
329        let mut consumed = vec![false; incoming.len()];
330        let mut ordered: Vec<NodeIndex> = Vec::with_capacity(incoming.len());
331
332        for arg in args {
333            if let Some(position) = incoming
334                .iter()
335                .enumerate()
336                .position(|(slot, (_, name, _))| !consumed[slot] && name == arg)
337            {
338                consumed[position] = true;
339                ordered.push(incoming[position].2);
340            }
341        }
342
343        // Anything the argument list did not name keeps its declaration order.
344        for (slot, (_, _, source)) in incoming.iter().enumerate() {
345            if !consumed[slot] {
346                ordered.push(*source);
347            }
348        }
349
350        let mut inputs = Vec::new();
351        for pred_idx in ordered {
352            if let Some(tensor) = self.env.get(pred_idx) {
353                inputs.push(tensor.clone());
354            } else {
355                return Err(TorshError::InvalidArgument(format!(
356                    "Missing input tensor for node {:?}",
357                    pred_idx
358                )));
359            }
360        }
361
362        Ok(inputs)
363    }
364
365    /// Execute a specific operation
366    ///
367    /// Dispatches to either custom registered operations or built-in operations.
368    ///
369    /// # Arguments
370    /// * `op_name` - Name of the operation to execute
371    /// * `inputs` - Vector of input tensors
372    ///
373    /// # Returns
374    /// * `TorshResult<Tensor>` - Result tensor or error
375    fn execute_operation(&self, op_name: &str, inputs: Vec<Tensor>) -> TorshResult<Tensor> {
376        // First check if it's a registered custom operation
377        if is_operation_registered(op_name) {
378            return execute_registered_operation(op_name, inputs);
379        }
380
381        // Fallback to built-in operations
382        self.execute_builtin_operation(op_name, inputs)
383    }
384
385    /// Execute built-in operations
386    ///
387    /// Implements execution logic for all built-in tensor operations.
388    ///
389    /// # Arguments
390    /// * `op_name` - Name of the built-in operation
391    /// * `inputs` - Vector of input tensors
392    ///
393    /// # Returns
394    /// * `TorshResult<Tensor>` - Result tensor or error
395    fn execute_builtin_operation(&self, op_name: &str, inputs: Vec<Tensor>) -> TorshResult<Tensor> {
396        match op_name {
397            "add" => {
398                self.validate_input_count(&inputs, 2, "Add")?;
399                inputs[0].add_op(&inputs[1])
400            }
401            "sub" => {
402                self.validate_input_count(&inputs, 2, "Sub")?;
403                inputs[0].sub(&inputs[1])
404            }
405            "mul" => {
406                self.validate_input_count(&inputs, 2, "Mul")?;
407                inputs[0].mul_op(&inputs[1])
408            }
409            "div" => {
410                self.validate_input_count(&inputs, 2, "Div")?;
411                inputs[0].div(&inputs[1])
412            }
413            "matmul" => {
414                self.validate_input_count(&inputs, 2, "Matmul")?;
415                inputs[0].matmul(&inputs[1])
416            }
417            "relu" => {
418                self.validate_input_count(&inputs, 1, "ReLU")?;
419                inputs[0].relu()
420            }
421            "sigmoid" => {
422                self.validate_input_count(&inputs, 1, "Sigmoid")?;
423                inputs[0].sigmoid()
424            }
425            "tanh" => {
426                self.validate_input_count(&inputs, 1, "Tanh")?;
427                inputs[0].tanh()
428            }
429            "gelu" => {
430                self.validate_input_count(&inputs, 1, "GELU")?;
431                self.execute_gelu(&inputs[0])
432            }
433            "softmax" => {
434                self.validate_input_count(&inputs, 1, "Softmax")?;
435                self.execute_softmax(&inputs[0])
436            }
437            "layer_norm" => {
438                if inputs.is_empty() {
439                    return Err(TorshError::InvalidArgument(
440                        "LayerNorm operation requires at least 1 input".to_string(),
441                    ));
442                }
443                self.execute_layer_norm(&inputs)
444            }
445            "batch_norm" => {
446                if inputs.is_empty() {
447                    return Err(TorshError::InvalidArgument(
448                        "BatchNorm operation requires at least 1 input".to_string(),
449                    ));
450                }
451                self.execute_batch_norm(&inputs)
452            }
453            "conv2d" => {
454                if inputs.len() < 2 {
455                    return Err(TorshError::InvalidArgument(
456                        "Conv2D operation requires at least 2 inputs".to_string(),
457                    ));
458                }
459                self.execute_conv2d(&inputs)
460            }
461            "linear" => {
462                if inputs.len() < 2 {
463                    return Err(TorshError::InvalidArgument(
464                        "Linear operation requires at least 2 inputs (input, weight)".to_string(),
465                    ));
466                }
467                self.execute_linear(&inputs)
468            }
469            "linear_relu" => {
470                let linear_result = self.execute_linear(&inputs)?;
471                linear_result.relu()
472            }
473            "conv2d_relu" => {
474                let conv_result = self.execute_conv2d(&inputs)?;
475                conv_result.relu()
476            }
477            "conv2d_bn" => {
478                if inputs.len() < 2 {
479                    return Err(TorshError::InvalidArgument(
480                        "Fused Conv2D+BatchNorm requires at least 2 inputs (input, weight)"
481                            .to_string(),
482                    ));
483                }
484                self.execute_conv2d_bn(&inputs)
485            }
486            "conv2d_bn_relu" => {
487                if inputs.len() < 2 {
488                    return Err(TorshError::InvalidArgument(
489                        "Fused Conv2D+BatchNorm+ReLU requires at least 2 inputs (input, weight)"
490                            .to_string(),
491                    ));
492                }
493                self.execute_conv2d_bn(&inputs)?.relu()
494            }
495            "identity" => {
496                self.validate_input_count(&inputs, 1, "Identity")?;
497                Ok(inputs[0].clone())
498            }
499            _ => Err(TorshError::InvalidArgument(format!(
500                "Unknown operation: {}",
501                op_name
502            ))),
503        }
504    }
505
506    /// Validate input count for operations
507    ///
508    /// # Arguments
509    /// * `inputs` - Vector of input tensors
510    /// * `expected` - Expected number of inputs
511    /// * `op_name` - Name of operation for error messages
512    ///
513    /// # Returns
514    /// * `TorshResult<()>` - Ok if count is correct, error otherwise
515    fn validate_input_count(
516        &self,
517        inputs: &[Tensor],
518        expected: usize,
519        op_name: &str,
520    ) -> TorshResult<()> {
521        if inputs.len() != expected {
522            return Err(TorshError::InvalidArgument(format!(
523                "{} operation requires exactly {} inputs, got {}",
524                op_name,
525                expected,
526                inputs.len()
527            )));
528        }
529        Ok(())
530    }
531
532    /// Execute GELU activation function
533    ///
534    /// # Arguments
535    /// * `input` - Input tensor
536    ///
537    /// # Returns
538    /// * `TorshResult<Tensor>` - GELU activated tensor
539    fn execute_gelu(&self, input: &Tensor) -> TorshResult<Tensor> {
540        // GELU(x) = x * Φ(x) ≈ 0.5 * x * (1 + tanh(√(2/π) * (x + 0.044715 * x³)))
541        let sqrt_2_over_pi = (2.0f32 / std::f32::consts::PI).sqrt();
542        let coeff = 0.044715f32;
543        let half = 0.5f32;
544        let one = 1.0f32;
545
546        let shape = input.shape();
547        let dims = shape.dims();
548        let sqrt_2_over_pi_tensor = full(dims, sqrt_2_over_pi)?;
549        let coeff_tensor = full(dims, coeff)?;
550        let half_tensor = full(dims, half)?;
551        let one_tensor = full(dims, one)?;
552
553        // Compute x³
554        let x_squared = input.mul_op(input)?;
555        let x_cubed = x_squared.mul_op(input)?;
556
557        // Compute 0.044715 * x³
558        let coeff_x_cubed = coeff_tensor.mul_op(&x_cubed)?;
559
560        // Compute x + 0.044715 * x³
561        let inner_term = input.add_op(&coeff_x_cubed)?;
562
563        // Compute √(2/π) * (x + 0.044715 * x³)
564        let scaled_term = sqrt_2_over_pi_tensor.mul_op(&inner_term)?;
565
566        // Compute tanh(√(2/π) * (x + 0.044715 * x³))
567        let tanh_term = scaled_term.tanh()?;
568
569        // Compute 1 + tanh(...)
570        let one_plus_tanh = one_tensor.add_op(&tanh_term)?;
571
572        // Compute 0.5 * x * (1 + tanh(...))
573        let half_x = half_tensor.mul_op(input)?;
574        half_x.mul_op(&one_plus_tanh)
575    }
576
577    /// Execute softmax activation function
578    ///
579    /// # Arguments
580    /// * `input` - Input tensor
581    ///
582    /// # Returns
583    /// * `TorshResult<Tensor>` - Softmax activated tensor
584    fn execute_softmax(&self, input: &Tensor) -> TorshResult<Tensor> {
585        // softmax(x) = exp(x - max(x)) / sum(exp(x - max(x)))
586        let input_max = input.max(None, false)?;
587        let shifted = input.sub(&input_max)?;
588        let exp_values = shifted.exp()?;
589        let sum_exp = exp_values.sum()?;
590        exp_values.div(&sum_exp)
591    }
592
593    /// Execute layer normalization
594    ///
595    /// # Arguments
596    /// * `inputs` - Vector of input tensors (input, optional weight, optional bias)
597    ///
598    /// # Returns
599    /// * `TorshResult<Tensor>` - Layer normalized tensor
600    fn execute_layer_norm(&self, inputs: &[Tensor]) -> TorshResult<Tensor> {
601        let input = &inputs[0];
602        let input_shape = input.shape();
603        let dims = input_shape.dims();
604
605        let weight = inputs.get(1);
606        let bias = inputs.get(2);
607
608        let eps = 1e-5f32;
609        let epsilon_tensor = full(dims, eps)?;
610
611        // Compute mean and variance for normalization
612        let input_mean = input.mean(None, false)?;
613        let centered = input.sub(&input_mean)?;
614        let variance = centered.mul_op(&centered)?.mean(None, false)?;
615        let std_tensor = variance.add_op(&epsilon_tensor)?.sqrt()?;
616
617        let mut normalized = centered.div(&std_tensor)?;
618
619        // Apply weight (scale) if provided
620        if let Some(weight_tensor) = weight {
621            normalized = normalized.mul_op(weight_tensor)?;
622        }
623
624        // Apply bias (shift) if provided
625        if let Some(bias_tensor) = bias {
626            normalized = normalized.add_op(bias_tensor)?;
627        }
628
629        Ok(normalized)
630    }
631
632    /// Execute batch normalization
633    ///
634    /// # Arguments
635    /// * `inputs` - Vector of input tensors (input, optional weight, optional bias, optional running_mean, optional running_var)
636    ///
637    /// # Returns
638    /// * `TorshResult<Tensor>` - Batch normalized tensor
639    fn execute_batch_norm(&self, inputs: &[Tensor]) -> TorshResult<Tensor> {
640        let input = &inputs[0];
641        let input_shape = input.shape();
642        let dims = input_shape.dims();
643
644        if dims.len() < 2 {
645            return Err(TorshError::InvalidArgument(
646                "BatchNorm requires at least 2D input".to_string(),
647            ));
648        }
649
650        let weight = inputs.get(1);
651        let bias = inputs.get(2);
652        let running_mean = inputs.get(3);
653        let running_var = inputs.get(4);
654
655        let eps = 1e-5f32;
656        let epsilon_tensor = full(dims, eps)?;
657
658        // Use running statistics if available, otherwise compute batch statistics
659        let batch_mean = if let Some(r_mean) = running_mean {
660            r_mean.clone()
661        } else {
662            input.mean(None, false)?
663        };
664
665        let batch_var = if let Some(r_var) = running_var {
666            r_var.clone()
667        } else {
668            let centered = input.sub(&batch_mean)?;
669            centered.mul_op(&centered)?.mean(None, false)?
670        };
671
672        let std_tensor = batch_var.add_op(&epsilon_tensor)?.sqrt()?;
673        let centered = input.sub(&batch_mean)?;
674        let mut normalized = centered.div(&std_tensor)?;
675
676        // Apply weight (scale) if provided
677        if let Some(weight_tensor) = weight {
678            normalized = normalized.mul_op(weight_tensor)?;
679        }
680
681        // Apply bias (shift) if provided
682        if let Some(bias_tensor) = bias {
683            normalized = normalized.add_op(bias_tensor)?;
684        }
685
686        Ok(normalized)
687    }
688
689    /// Execute 2D convolution
690    ///
691    /// # Arguments
692    /// * `inputs` - Vector of input tensors (input, weight, optional bias)
693    ///
694    /// # Returns
695    /// * `TorshResult<Tensor>` - Convolved tensor
696    /// Execute the fused conv2d + batch_norm operation
697    ///
698    /// Emitted by [`crate::subgraph_rewriter::SubgraphPattern::conv_bn_fusion`]. The
699    /// operand list is the concatenation of the convolution's operands and the batch
700    /// norm's remaining operands, which is exactly what the rewriter builds when it
701    /// re-attaches the batch norm's external inputs to the fused node.
702    ///
703    /// # Arguments
704    /// * `inputs` - `[input, weight, (bn weight, bn bias, running mean, running var)]`
705    ///
706    /// # Returns
707    /// * `TorshResult<Tensor>` - Normalized convolution result
708    fn execute_conv2d_bn(&self, inputs: &[Tensor]) -> TorshResult<Tensor> {
709        let conv_result = self.execute_conv2d(&inputs[..2])?;
710
711        let mut norm_inputs = Vec::with_capacity(inputs.len() - 1);
712        norm_inputs.push(conv_result);
713        norm_inputs.extend(inputs[2..].iter().cloned());
714
715        self.execute_batch_norm(&norm_inputs)
716    }
717
718    fn execute_conv2d(&self, inputs: &[Tensor]) -> TorshResult<Tensor> {
719        let input = &inputs[0]; // Input tensor: [N, C_in, H_in, W_in]
720        let weight = &inputs[1]; // Weight tensor: [C_out, C_in, K_h, K_w]
721
722        let input_shape = input.shape();
723        let weight_shape = weight.shape();
724        let input_dims = input_shape.dims();
725        let weight_dims = weight_shape.dims();
726
727        // Validate dimensions
728        if input_dims.len() != 4 || weight_dims.len() != 4 {
729            return Err(TorshError::InvalidArgument(
730                "Conv2D requires 4D input and weight tensors".to_string(),
731            ));
732        }
733
734        if input_dims[1] != weight_dims[1] {
735            return Err(TorshError::InvalidArgument(
736                "Input and weight channel dimensions must match".to_string(),
737            ));
738        }
739
740        // Check if kernel size is too large for input dimensions
741        if input_dims[2] < weight_dims[2] || input_dims[3] < weight_dims[3] {
742            return Err(TorshError::InvalidArgument(
743                "Kernel size too large for input dimensions".to_string(),
744            ));
745        }
746
747        // Calculate output dimensions (assuming stride=1, padding=0)
748        let n = input_dims[0];
749        let c_out = weight_dims[0];
750        let h_out = input_dims[2] - weight_dims[2] + 1;
751        let w_out = input_dims[3] - weight_dims[3] + 1;
752
753        let output_shape = vec![n, c_out, h_out, w_out];
754        let mut output = zeros(&output_shape)?;
755
756        // Add bias if provided
757        if let Some(bias_tensor) = inputs.get(2) {
758            let bias_shape = bias_tensor.shape();
759            if bias_shape.dims()[0] != c_out {
760                return Err(TorshError::InvalidArgument(
761                    "Bias dimension must match output channels".to_string(),
762                ));
763            }
764            output = output.add_op(bias_tensor)?;
765        }
766
767        // Simplified convolution implementation
768        let input_mean = input.mean(None, false)?;
769        let weight_mean = weight.mean(None, false)?;
770        let scale_factor = input_mean.mul_op(&weight_mean)?;
771        output = output.add_op(&scale_factor)?;
772
773        Ok(output)
774    }
775
776    /// Execute linear transformation
777    ///
778    /// # Arguments
779    /// * `inputs` - Vector of input tensors (input, weight, optional bias)
780    ///
781    /// # Returns
782    /// * `TorshResult<Tensor>` - Linearly transformed tensor
783    fn execute_linear(&self, inputs: &[Tensor]) -> TorshResult<Tensor> {
784        let input = &inputs[0];
785        let weight = &inputs[1];
786
787        // Linear: input @ weight.T + bias (if provided)
788        let result = input.matmul(&weight.transpose(0, 1)?)?;
789
790        if let Some(bias) = inputs.get(2) {
791            result.add_op(bias)
792        } else {
793            Ok(result)
794        }
795    }
796
797    /// Execute conditional operation
798    ///
799    /// # Arguments
800    /// * `graph` - FX graph containing the conditional
801    /// * `node_idx` - Index of the conditional node
802    /// * `condition` - Condition expression
803    /// * `then_branch` - Then branch operations
804    /// * `else_branch` - Else branch operations
805    ///
806    /// # Returns
807    /// * `TorshResult<()>` - Ok if execution succeeds, error otherwise
808    fn execute_conditional(
809        &mut self,
810        _graph: &FxGraph,
811        node_idx: NodeIndex,
812        _condition: &str,
813        _then_branch: &[String],
814        _else_branch: &[String],
815    ) -> TorshResult<()> {
816        // Simplified conditional execution
817        // In a real implementation, this would evaluate the condition and execute the appropriate branch
818
819        // For now, create a dummy output
820        let dummy_tensor = zeros(&[1])?;
821        self.env.store(node_idx, dummy_tensor);
822        Ok(())
823    }
824
825    /// Execute loop operation
826    ///
827    /// # Arguments
828    /// * `graph` - FX graph containing the loop
829    /// * `node_idx` - Index of the loop node
830    /// * `condition` - Loop condition
831    /// * `body` - Loop body operations
832    /// * `loop_vars` - Loop variables
833    ///
834    /// # Returns
835    /// * `TorshResult<()>` - Ok if execution succeeds, error otherwise
836    fn execute_loop(
837        &mut self,
838        _graph: &FxGraph,
839        node_idx: NodeIndex,
840        _condition: &str,
841        _body: &[String],
842        _loop_vars: &[String],
843    ) -> TorshResult<()> {
844        // Simplified loop execution
845        // In a real implementation, this would execute the loop body while the condition is true
846
847        // For now, create a dummy output
848        let dummy_tensor = zeros(&[1])?;
849        self.env.store(node_idx, dummy_tensor);
850        Ok(())
851    }
852
853    /// Execute merge operation
854    ///
855    /// # Arguments
856    /// * `graph` - FX graph containing the merge
857    /// * `node_idx` - Index of the merge node
858    /// * `inputs` - Input names for merge
859    ///
860    /// # Returns
861    /// * `TorshResult<()>` - Ok if execution succeeds, error otherwise
862    fn execute_merge(
863        &mut self,
864        _graph: &FxGraph,
865        node_idx: NodeIndex,
866        _inputs: &[String],
867    ) -> TorshResult<()> {
868        // Simplified merge execution
869        // In a real implementation, this would merge multiple inputs
870        let dummy_tensor = zeros(&[1])?;
871        self.env.store(node_idx, dummy_tensor);
872        Ok(())
873    }
874
875    /// Execute get attribute operation
876    ///
877    /// # Arguments
878    /// * `graph` - FX graph containing the get_attr
879    /// * `node_idx` - Index of the get_attr node
880    /// * `target` - Target object name
881    /// * `attr` - Attribute name
882    ///
883    /// # Returns
884    /// * `TorshResult<()>` - Ok if execution succeeds, error otherwise
885    fn execute_get_attr(
886        &mut self,
887        _graph: &FxGraph,
888        node_idx: NodeIndex,
889        _target: &str,
890        _attr: &str,
891    ) -> TorshResult<()> {
892        // Simplified get_attr execution
893        // In a real implementation, this would get attributes from objects
894        let dummy_tensor = zeros(&[1])?;
895        self.env.store(node_idx, dummy_tensor);
896        Ok(())
897    }
898}
899
900/// Convenience function for graph interpretation
901///
902/// # Arguments
903/// * `graph` - FX graph to interpret
904///
905/// # Returns
906/// * `TorshResult<()>` - Ok if interpretation succeeds, error otherwise
907pub fn interpret(graph: &FxGraph) -> TorshResult<()> {
908    let mut interpreter = GraphInterpreter::new(DeviceType::Cpu);
909    let inputs = HashMap::new(); // Empty inputs for parameterless graphs
910    interpreter.run(graph, inputs)?;
911    Ok(())
912}
913
914/// Convenience function for interpreting graph with inputs
915///
916/// # Arguments
917/// * `graph` - FX graph to interpret
918/// * `inputs` - Map of input node names to their tensor values
919///
920/// # Returns
921/// * `TorshResult<Vec<Tensor>>` - Vector of output tensors or error
922pub fn interpret_with_inputs(
923    graph: &FxGraph,
924    inputs: HashMap<String, Tensor>,
925) -> TorshResult<Vec<Tensor>> {
926    let mut interpreter = GraphInterpreter::new(DeviceType::Cpu);
927    interpreter.run(graph, inputs)
928}