Skip to main content

torsh_nn/container/
dynamic_graph.rs

1//! Dynamic computation graph implementation for neural network modules
2//!
3//! This module provides a flexible execution framework that allows for conditional execution,
4//! loops, parallel processing, and runtime modification of the computation graph structure.
5
6use crate::{Module, ModuleBase, Parameter};
7use torsh_core::device::DeviceType;
8use torsh_core::error::{Result, TorshError};
9use torsh_tensor::Tensor;
10
11// Conditional imports for std/no_std compatibility
12#[cfg(feature = "std")]
13use std::{boxed::Box, collections::HashMap, vec::Vec};
14
15#[cfg(not(feature = "std"))]
16use alloc::{boxed::Box, vec::Vec};
17
18#[cfg(not(feature = "std"))]
19use hashbrown::HashMap;
20
21// Use parking_lot::Mutex for both std and no_std
22use parking_lot::Mutex;
23
24/// Dynamic Graph execution node types
25///
26/// Represents different types of computational nodes that can be executed
27/// in a dynamic computation graph, enabling complex control flow patterns.
28#[derive(Debug, Clone)]
29pub enum GraphNode {
30    /// Execute a specific module by name
31    Module(String),
32    /// Conditional execution based on predicate
33    Conditional {
34        condition: String,
35        true_branch: Box<GraphNode>,
36        false_branch: Option<Box<GraphNode>>,
37    },
38    /// Sequential execution of multiple nodes
39    Sequence(Vec<GraphNode>),
40    /// Parallel execution with results combined
41    Parallel {
42        nodes: Vec<GraphNode>,
43        combiner: String, // Name of combiner function
44    },
45    /// Loop execution
46    Loop {
47        body: Box<GraphNode>,
48        condition: String,
49        max_iterations: usize,
50    },
51    /// Custom execution function
52    Function(String),
53}
54
55/// Dynamic computation graph that can modify its structure at runtime
56///
57/// This container provides a flexible execution framework for neural networks
58/// that need complex control flow, conditional execution, or runtime adaptation.
59/// It supports modules, conditions, combiners, and custom functions that can
60/// be composed into arbitrary computational graphs.
61///
62/// # Examples
63///
64/// ```rust,no_run
65/// # use torsh_nn::container::{DynamicGraph, GraphNode};
66/// # use torsh_nn::layers::linear::Linear;
67/// # use torsh_nn::Module;
68/// # use torsh_tensor::creation::randn;
69/// # use torsh_core::error::Result;
70/// # fn main() -> Result<()> {
71/// let mut graph = DynamicGraph::new();
72/// graph.add_module("linear1".to_string(), Linear::new(784, 128, true));
73/// graph.add_module("linear2".to_string(), Linear::new(128, 10, true));
74///
75/// // Create a simple sequential execution graph
76/// let seq_graph = DynamicGraph::sequential(vec![
77///     "linear1".to_string(),
78///     "linear2".to_string(),
79/// ]);
80/// graph.set_graph(seq_graph);
81///
82/// // Create input and run forward pass
83/// let input = randn(&[1, 784])?;
84/// let output = graph.forward(&input)?;
85/// # Ok(())
86/// # }
87/// ```
88pub struct DynamicGraph {
89    base: ModuleBase,
90    /// Named modules available for execution
91    modules: HashMap<String, Box<dyn Module>>,
92    /// Named condition functions
93    conditions: HashMap<String, Box<dyn Fn(&Tensor) -> bool + Send + Sync>>,
94    /// Named combiner functions for parallel execution
95    combiners: HashMap<String, Box<dyn Fn(Vec<Tensor>) -> Result<Tensor> + Send + Sync>>,
96    /// Named custom functions
97    functions: HashMap<String, Box<dyn Fn(&Tensor) -> Result<Tensor> + Send + Sync>>,
98    /// Current execution graph
99    graph: GraphNode,
100    /// Execution history for debugging
101    execution_history: Mutex<Vec<String>>,
102}
103
104impl DynamicGraph {
105    /// Create a new dynamic graph with a simple sequential structure
106    pub fn new() -> Self {
107        let mut graph = Self {
108            base: ModuleBase::new(),
109            modules: HashMap::new(),
110            conditions: HashMap::new(),
111            combiners: HashMap::new(),
112            functions: HashMap::new(),
113            graph: GraphNode::Sequence(Vec::new()),
114            execution_history: Mutex::new(Vec::new()),
115        };
116
117        // Add default combiners
118        graph.add_combiner(
119            "concat".to_string(),
120            Box::new(|tensors: Vec<Tensor>| {
121                if tensors.is_empty() {
122                    return Err(TorshError::InvalidArgument(
123                        "No tensors to concatenate".to_string(),
124                    ));
125                }
126
127                // Get the last dimension for concatenation
128                let ndim = tensors[0].ndim();
129                if ndim == 0 {
130                    return Err(TorshError::InvalidArgument(
131                        "Cannot concatenate 0-dimensional tensors".to_string(),
132                    ));
133                }
134
135                let concat_dim = (ndim - 1) as i32; // Concatenate along last dimension
136
137                // Use proper concatenation via Tensor::cat
138                // Convert Vec<Tensor> to &[&Tensor]
139                let tensor_refs: Vec<&Tensor> = tensors.iter().collect();
140                Tensor::cat(&tensor_refs, concat_dim)
141                    .map_err(|e| TorshError::Other(format!("Concatenation failed: {}", e)))
142            }),
143        );
144
145        graph.add_combiner(
146            "add".to_string(),
147            Box::new(|tensors: Vec<Tensor>| {
148                if tensors.is_empty() {
149                    return Err(TorshError::InvalidArgument("No tensors to add".to_string()));
150                }
151                let mut result = tensors[0].clone();
152                for tensor in tensors.iter().skip(1) {
153                    result = result.add_op(tensor)?;
154                }
155                Ok(result)
156            }),
157        );
158
159        graph.add_combiner(
160            "mean".to_string(),
161            Box::new(|tensors: Vec<Tensor>| {
162                if tensors.is_empty() {
163                    return Err(TorshError::InvalidArgument(
164                        "No tensors to average".to_string(),
165                    ));
166                }
167                let mut result = tensors[0].clone();
168                for tensor in tensors.iter().skip(1) {
169                    result = result.add_op(tensor)?;
170                }
171                let count = tensors.len() as f32;
172                result = result.div_scalar(count)?;
173                Ok(result)
174            }),
175        );
176
177        graph
178    }
179
180    /// Add a module to the graph
181    pub fn add_module<M: Module + 'static>(&mut self, name: String, module: M) {
182        self.modules.insert(name, Box::new(module));
183    }
184
185    /// Add a condition function
186    pub fn add_condition<F>(&mut self, name: String, condition: F)
187    where
188        F: Fn(&Tensor) -> bool + Send + Sync + 'static,
189    {
190        self.conditions.insert(name, Box::new(condition));
191    }
192
193    /// Add a combiner function for parallel execution
194    pub fn add_combiner<F>(&mut self, name: String, combiner: F)
195    where
196        F: Fn(Vec<Tensor>) -> Result<Tensor> + Send + Sync + 'static,
197    {
198        self.combiners.insert(name, Box::new(combiner));
199    }
200
201    /// Add a custom function
202    pub fn add_function<F>(&mut self, name: String, function: F)
203    where
204        F: Fn(&Tensor) -> Result<Tensor> + Send + Sync + 'static,
205    {
206        self.functions.insert(name, Box::new(function));
207    }
208
209    /// Set the execution graph
210    pub fn set_graph(&mut self, graph: GraphNode) {
211        self.graph = graph;
212    }
213
214    /// Create a sequential graph from module names
215    pub fn sequential(module_names: Vec<String>) -> GraphNode {
216        GraphNode::Sequence(
217            module_names
218                .into_iter()
219                .map(|name| GraphNode::Module(name))
220                .collect(),
221        )
222    }
223
224    /// Create a conditional graph
225    pub fn conditional(
226        condition: String,
227        true_branch: GraphNode,
228        false_branch: Option<GraphNode>,
229    ) -> GraphNode {
230        GraphNode::Conditional {
231            condition,
232            true_branch: Box::new(true_branch),
233            false_branch: false_branch.map(Box::new),
234        }
235    }
236
237    /// Create a parallel graph
238    pub fn parallel(nodes: Vec<GraphNode>, combiner: String) -> GraphNode {
239        GraphNode::Parallel { nodes, combiner }
240    }
241
242    /// Create a loop graph
243    pub fn loop_graph(body: GraphNode, condition: String, max_iterations: usize) -> GraphNode {
244        GraphNode::Loop {
245            body: Box::new(body),
246            condition,
247            max_iterations,
248        }
249    }
250
251    /// Execute a graph node
252    fn execute_node(&self, node: &GraphNode, input: &Tensor) -> Result<Tensor> {
253        let mut history = self.execution_history.lock();
254
255        match node {
256            GraphNode::Module(name) => {
257                history.push(format!("Module: {}", name));
258                let module = self.modules.get(name).ok_or_else(|| {
259                    TorshError::InvalidArgument(format!("Module '{}' not found", name))
260                })?;
261                module.forward(input)
262            }
263
264            GraphNode::Conditional {
265                condition,
266                true_branch,
267                false_branch,
268            } => {
269                history.push(format!("Conditional: {}", condition));
270                let cond_fn = self.conditions.get(condition).ok_or_else(|| {
271                    TorshError::InvalidArgument(format!("Condition '{}' not found", condition))
272                })?;
273
274                if cond_fn(input) {
275                    history.push("Taking true branch".to_string());
276                    self.execute_node(true_branch, input)
277                } else if let Some(false_branch) = false_branch {
278                    history.push("Taking false branch".to_string());
279                    self.execute_node(false_branch, input)
280                } else {
281                    history.push("No false branch, returning input".to_string());
282                    Ok(input.clone())
283                }
284            }
285
286            GraphNode::Sequence(nodes) => {
287                history.push("Sequence execution".to_string());
288                let mut output = input.clone();
289                for node in nodes {
290                    output = self.execute_node(node, &output)?;
291                }
292                Ok(output)
293            }
294
295            GraphNode::Parallel { nodes, combiner } => {
296                history.push(format!("Parallel execution with combiner: {}", combiner));
297                let mut results = Vec::new();
298                for node in nodes {
299                    results.push(self.execute_node(node, input)?);
300                }
301
302                let combiner_fn = self.combiners.get(combiner).ok_or_else(|| {
303                    TorshError::InvalidArgument(format!("Combiner '{}' not found", combiner))
304                })?;
305                combiner_fn(results)
306            }
307
308            GraphNode::Loop {
309                body,
310                condition,
311                max_iterations,
312            } => {
313                history.push(format!("Loop execution with condition: {}", condition));
314                let cond_fn = self.conditions.get(condition).ok_or_else(|| {
315                    TorshError::InvalidArgument(format!("Condition '{}' not found", condition))
316                })?;
317
318                let mut output = input.clone();
319                let mut iterations = 0;
320
321                while cond_fn(&output) && iterations < *max_iterations {
322                    output = self.execute_node(body, &output)?;
323                    iterations += 1;
324                    history.push(format!("Loop iteration: {}", iterations));
325                }
326
327                Ok(output)
328            }
329
330            GraphNode::Function(name) => {
331                history.push(format!("Function: {}", name));
332                let function = self.functions.get(name).ok_or_else(|| {
333                    TorshError::InvalidArgument(format!("Function '{}' not found", name))
334                })?;
335                function(input)
336            }
337        }
338    }
339
340    /// Get execution history for debugging
341    pub fn get_execution_history(&self) -> Vec<String> {
342        self.execution_history.lock().clone()
343    }
344
345    /// Clear execution history
346    pub fn clear_execution_history(&self) {
347        self.execution_history.lock().clear();
348    }
349
350    /// Dynamically modify the graph at runtime
351    pub fn modify_graph<F>(&mut self, modifier: F)
352    where
353        F: FnOnce(&mut GraphNode),
354    {
355        modifier(&mut self.graph);
356    }
357
358    /// Get a reference to a specific module
359    pub fn get_module(&self, name: &str) -> Option<&dyn Module> {
360        self.modules.get(name).map(|m| m.as_ref())
361    }
362
363    /// Replace a module at runtime
364    pub fn replace_module<M: Module + 'static>(&mut self, name: String, module: M) {
365        self.modules.insert(name, Box::new(module));
366    }
367
368    /// Remove a module
369    pub fn remove_module(&mut self, name: &str) -> Option<Box<dyn Module>> {
370        self.modules.remove(name)
371    }
372
373    /// Get the number of modules
374    pub fn module_count(&self) -> usize {
375        self.modules.len()
376    }
377
378    /// List all module names
379    pub fn module_names(&self) -> Vec<&String> {
380        self.modules.keys().collect()
381    }
382
383    /// List all condition names
384    pub fn condition_names(&self) -> Vec<&String> {
385        self.conditions.keys().collect()
386    }
387
388    /// List all combiner names
389    pub fn combiner_names(&self) -> Vec<&String> {
390        self.combiners.keys().collect()
391    }
392
393    /// List all function names
394    pub fn function_names(&self) -> Vec<&String> {
395        self.functions.keys().collect()
396    }
397}
398
399impl Default for DynamicGraph {
400    fn default() -> Self {
401        Self::new()
402    }
403}
404
405impl Module for DynamicGraph {
406    fn forward(&self, input: &Tensor) -> Result<Tensor> {
407        self.clear_execution_history();
408        self.execute_node(&self.graph, input)
409    }
410
411    fn parameters(&self) -> HashMap<String, Parameter> {
412        let mut params = HashMap::new();
413
414        for (module_name, module) in &self.modules {
415            for (param_name, param) in module.parameters() {
416                params.insert(format!("{}.{}", module_name, param_name), param);
417            }
418        }
419
420        params
421    }
422
423    fn named_parameters(&self) -> HashMap<String, Parameter> {
424        let mut params = HashMap::new();
425
426        for (module_name, module) in &self.modules {
427            for (param_name, param) in module.named_parameters() {
428                params.insert(format!("{}.{}", module_name, param_name), param);
429            }
430        }
431
432        params
433    }
434
435    fn train(&mut self) {
436        self.base.set_training(true);
437        for module in self.modules.values_mut() {
438            module.train();
439        }
440    }
441
442    fn eval(&mut self) {
443        self.base.set_training(false);
444        for module in self.modules.values_mut() {
445            module.eval();
446        }
447    }
448
449    fn training(&self) -> bool {
450        self.base.training()
451    }
452
453    fn set_training(&mut self, training: bool) {
454        self.base.set_training(training);
455        for module in self.modules.values_mut() {
456            module.set_training(training);
457        }
458    }
459
460    fn to_device(&mut self, device: DeviceType) -> Result<()> {
461        self.base.to_device(device)?;
462        for module in self.modules.values_mut() {
463            module.to_device(device)?;
464        }
465        Ok(())
466    }
467
468    fn children(&self) -> Vec<&dyn Module> {
469        self.modules.values().map(|m| m.as_ref()).collect()
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    // Mock module for testing
478    struct MockModule {
479        base: ModuleBase,
480        _id: i32,
481    }
482
483    impl MockModule {
484        fn new(id: i32) -> Self {
485            Self {
486                base: ModuleBase::new(),
487                _id: id,
488            }
489        }
490    }
491
492    impl Module for MockModule {
493        fn forward(&self, input: &Tensor) -> Result<Tensor> {
494            // Simple identity function for testing
495            Ok(input.clone())
496        }
497
498        fn parameters(&self) -> HashMap<String, Parameter> {
499            HashMap::new()
500        }
501
502        fn named_parameters(&self) -> HashMap<String, Parameter> {
503            HashMap::new()
504        }
505
506        fn train(&mut self) {
507            self.base.set_training(true);
508        }
509
510        fn eval(&mut self) {
511            self.base.set_training(false);
512        }
513
514        fn training(&self) -> bool {
515            self.base.training()
516        }
517
518        fn set_training(&mut self, training: bool) {
519            self.base.set_training(training);
520        }
521
522        fn to_device(&mut self, device: DeviceType) -> Result<()> {
523            self.base.to_device(device)
524        }
525    }
526
527    #[test]
528    fn test_dynamic_graph_creation() {
529        let graph = DynamicGraph::new();
530        assert_eq!(graph.module_count(), 0);
531        assert!(graph.module_names().is_empty());
532        assert!(graph.training());
533    }
534
535    #[test]
536    fn test_module_management() {
537        let mut graph = DynamicGraph::new();
538
539        graph.add_module("mock1".to_string(), MockModule::new(1));
540        graph.add_module("mock2".to_string(), MockModule::new(2));
541
542        assert_eq!(graph.module_count(), 2);
543        assert!(graph.get_module("mock1").is_some());
544        assert!(graph.get_module("nonexistent").is_none());
545
546        let removed = graph.remove_module("mock1");
547        assert!(removed.is_some());
548        assert_eq!(graph.module_count(), 1);
549    }
550
551    #[test]
552    fn test_graph_node_creation() {
553        // Test sequential graph creation
554        let seq_graph =
555            DynamicGraph::sequential(vec!["module1".to_string(), "module2".to_string()]);
556
557        match seq_graph {
558            GraphNode::Sequence(nodes) => {
559                assert_eq!(nodes.len(), 2);
560            }
561            _ => panic!("Expected Sequence node"),
562        }
563
564        // Test conditional graph creation
565        let cond_graph = DynamicGraph::conditional(
566            "test_condition".to_string(),
567            GraphNode::Module("true_module".to_string()),
568            Some(GraphNode::Module("false_module".to_string())),
569        );
570
571        match cond_graph {
572            GraphNode::Conditional { condition, .. } => {
573                assert_eq!(condition, "test_condition");
574            }
575            _ => panic!("Expected Conditional node"),
576        }
577    }
578
579    #[test]
580    fn test_default_combiners() {
581        let graph = DynamicGraph::new();
582
583        // Should have default combiners
584        let combiners = graph.combiner_names();
585        assert!(combiners.iter().any(|&name| name == "add"));
586        assert!(combiners.iter().any(|&name| name == "mean"));
587        assert!(combiners.iter().any(|&name| name == "concat"));
588    }
589
590    #[test]
591    fn test_execution_history() {
592        let graph = DynamicGraph::new();
593
594        assert!(graph.get_execution_history().is_empty());
595
596        // Clear should work even when empty
597        graph.clear_execution_history();
598        assert!(graph.get_execution_history().is_empty());
599    }
600}