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(¢ered)?.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(¢ered)?.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}