Skip to main content

tenflowers_core/graph/optimization/
memory.rs

1//! Memory optimization pass
2//!
3//! This module provides memory optimization capabilities including tensor lifetime
4//! analysis, in-place operation identification, and memory layout optimization.
5
6use super::passes::{get_node_inputs, get_node_outputs, OptimizationPass};
7use crate::graph::{Graph, NodeId};
8use crate::Result;
9use std::collections::{HashMap, HashSet};
10
11/// Memory optimization pass
12/// Minimizes memory usage by reducing tensor lifetime and enabling in-place operations
13pub struct MemoryOptimizationPass;
14
15impl Default for MemoryOptimizationPass {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl MemoryOptimizationPass {
22    pub fn new() -> Self {
23        Self
24    }
25
26    /// Analyzes tensor lifetimes in the graph
27    fn analyze_tensor_lifetimes(&self, graph: &Graph) -> HashMap<NodeId, (usize, usize)> {
28        let mut lifetimes = HashMap::new();
29
30        // Perform topological sort to determine execution order
31        let execution_order = self.topological_sort(graph);
32
33        for (position, &node_id) in execution_order.iter().enumerate() {
34            // Track when this tensor is first created (birth)
35            lifetimes.insert(node_id, (position, position));
36
37            // Update lifetime end for all inputs (last usage)
38            let inputs = get_node_inputs(graph, node_id);
39            for input_id in inputs {
40                if let Some((birth, _)) = lifetimes.get(&input_id) {
41                    lifetimes.insert(input_id, (*birth, position));
42                }
43            }
44        }
45
46        lifetimes
47    }
48
49    /// Performs topological sort on the graph
50    fn topological_sort(&self, graph: &Graph) -> Vec<NodeId> {
51        let mut visited = HashSet::new();
52        let mut result = Vec::new();
53        let mut temp_visited = HashSet::new();
54
55        // Start from nodes with no inputs (source nodes)
56        for node in graph.nodes() {
57            let node_id = node.id;
58            if !visited.contains(&node_id) && get_node_inputs(graph, node_id).is_empty() {
59                self.dfs_visit(graph, node_id, &mut visited, &mut temp_visited, &mut result);
60            }
61        }
62
63        // Handle remaining nodes (in case of cycles or disconnected components)
64        for node in graph.nodes() {
65            let node_id = node.id;
66            if !visited.contains(&node_id) {
67                self.dfs_visit(graph, node_id, &mut visited, &mut temp_visited, &mut result);
68            }
69        }
70
71        result
72    }
73
74    /// DFS visit for topological sort
75    #[allow(clippy::only_used_in_recursion)]
76    fn dfs_visit(
77        &self,
78        graph: &Graph,
79        node_id: NodeId,
80        visited: &mut HashSet<NodeId>,
81        temp_visited: &mut HashSet<NodeId>,
82        result: &mut Vec<NodeId>,
83    ) {
84        if temp_visited.contains(&node_id) {
85            // Cycle detected, skip for now
86            return;
87        }
88
89        if visited.contains(&node_id) {
90            return;
91        }
92
93        temp_visited.insert(node_id);
94
95        let outputs = get_node_outputs(graph, node_id);
96        for output_id in outputs {
97            self.dfs_visit(graph, output_id, visited, temp_visited, result);
98        }
99
100        temp_visited.remove(&node_id);
101        visited.insert(node_id);
102        result.push(node_id);
103    }
104
105    /// Identifies opportunities for in-place operations
106    fn identify_inplace_opportunities(&self, graph: &Graph) -> Vec<(NodeId, NodeId)> {
107        let mut opportunities = Vec::new();
108
109        for node in graph.nodes() {
110            let node_id = node.id;
111            // Check if this operation can be done in-place
112            if let crate::graph::NodeType::Operation(ref op_name) = node.op_type {
113                if self.can_be_inplace(op_name) {
114                    let inputs = get_node_inputs(graph, node_id);
115                    let outputs = get_node_outputs(graph, node_id);
116
117                    // For in-place operation, we need exactly one input and output
118                    // and the input should not be used elsewhere
119                    if inputs.len() == 1 && outputs.len() == 1 {
120                        let input_id = inputs[0];
121
122                        // Check if input is only used by this node
123                        if self.is_only_consumer(graph, input_id, node_id) {
124                            opportunities.push((node_id, input_id));
125                        }
126                    }
127                }
128            }
129        }
130
131        opportunities
132    }
133
134    /// Checks if an operation can be performed in-place
135    fn can_be_inplace(&self, operation: &str) -> bool {
136        matches!(
137            operation,
138            "add"
139                | "sub"
140                | "mul"
141                | "div"
142                | "relu"
143                | "sigmoid"
144                | "tanh"
145                | "abs"
146                | "neg"
147                | "exp"
148                | "log"
149                | "sqrt"
150                | "square"
151        )
152    }
153
154    /// Checks if a node is the only consumer of an input
155    fn is_only_consumer(&self, graph: &Graph, input_id: NodeId, consumer_id: NodeId) -> bool {
156        let outputs = get_node_outputs(graph, input_id);
157        outputs.len() == 1 && outputs[0] == consumer_id
158    }
159
160    /// Optimizes memory layout by grouping operations
161    fn optimize_memory_layout(&self, graph: &mut Graph) -> bool {
162        let mut changed = false;
163
164        // Group operations that can share memory
165        let memory_groups = self.find_memory_sharing_groups(graph);
166
167        for group in memory_groups {
168            if group.len() > 1 {
169                // Mark operations in this group for memory sharing
170                for &node_id in &group {
171                    if let Some(node) = graph.get_node_mut(node_id) {
172                        // Add memory sharing hint to node attributes
173                        node.attributes.insert(
174                            "memory_group".to_string(),
175                            crate::graph::AttributeValue::String(format!("{group:?}")),
176                        );
177                        changed = true;
178                    }
179                }
180            }
181        }
182
183        changed
184    }
185
186    /// Finds groups of operations that can share memory
187    fn find_memory_sharing_groups(&self, graph: &Graph) -> Vec<Vec<NodeId>> {
188        let mut groups = Vec::new();
189        let mut visited = HashSet::new();
190
191        for node in graph.nodes() {
192            let node_id = node.id;
193            if visited.contains(&node_id) {
194                continue;
195            }
196
197            let mut group = Vec::new();
198            self.build_memory_group(graph, node_id, &mut group, &mut visited);
199
200            if group.len() > 1 {
201                groups.push(group);
202            }
203        }
204
205        groups
206    }
207
208    /// Recursively builds a memory sharing group
209    fn build_memory_group(
210        &self,
211        graph: &Graph,
212        node_id: NodeId,
213        group: &mut Vec<NodeId>,
214        visited: &mut HashSet<NodeId>,
215    ) {
216        if visited.contains(&node_id) {
217            return;
218        }
219
220        visited.insert(node_id);
221        group.push(node_id);
222
223        // Only group with immediate successors that can share memory
224        let outputs = get_node_outputs(graph, node_id);
225        for output_id in outputs {
226            if let Some(node) = graph.get_node(output_id) {
227                // Only group operations that can safely share memory
228                if let crate::graph::NodeType::Operation(ref op_name) = node.op_type {
229                    if self.can_share_memory(op_name) {
230                        self.build_memory_group(graph, output_id, group, visited);
231                    }
232                }
233            }
234        }
235    }
236
237    /// Checks if operations can safely share memory
238    fn can_share_memory(&self, operation: &str) -> bool {
239        // Operations that don't change tensor shape can often share memory
240        matches!(
241            operation,
242            "add"
243                | "sub"
244                | "mul"
245                | "div"
246                | "relu"
247                | "sigmoid"
248                | "tanh"
249                | "abs"
250                | "neg"
251                | "exp"
252                | "log"
253                | "sqrt"
254                | "square"
255                | "dropout"
256        )
257    }
258}
259
260impl OptimizationPass for MemoryOptimizationPass {
261    fn apply(&self, graph: &mut Graph) -> Result<bool> {
262        let mut changed = false;
263
264        // Step 1: Analyze tensor lifetimes
265        let lifetimes = self.analyze_tensor_lifetimes(graph);
266
267        // Step 2: Identify in-place operation opportunities
268        let inplace_ops = self.identify_inplace_opportunities(graph);
269
270        // Step 3: Apply in-place optimizations
271        for (node_id, input_id) in inplace_ops {
272            if let Some(node) = graph.get_node_mut(node_id) {
273                // Mark this operation as in-place
274                node.attributes.insert(
275                    "inplace".to_string(),
276                    crate::graph::AttributeValue::Bool(true),
277                );
278                node.attributes.insert(
279                    "inplace_input".to_string(),
280                    crate::graph::AttributeValue::String(input_id.to_string()),
281                );
282                changed = true;
283            }
284        }
285
286        // Step 4: Optimize memory layout
287        if self.optimize_memory_layout(graph) {
288            changed = true;
289        }
290
291        // Step 5: Add memory management hints
292        for (node_id, (birth, death)) in lifetimes {
293            if let Some(node) = graph.get_node_mut(node_id) {
294                node.attributes.insert(
295                    "lifetime_start".to_string(),
296                    crate::graph::AttributeValue::Int(birth as i64),
297                );
298                node.attributes.insert(
299                    "lifetime_end".to_string(),
300                    crate::graph::AttributeValue::Int(death as i64),
301                );
302                changed = true;
303            }
304        }
305
306        Ok(changed)
307    }
308
309    fn name(&self) -> &str {
310        "MemoryOptimization"
311    }
312
313    fn is_applicable(&self, graph: &Graph) -> bool {
314        // Memory optimization is applicable if there are at least 2 nodes
315        graph.nodes().count() >= 2
316    }
317
318    fn priority(&self) -> u32 {
319        80 // Run after CSE and fusion but before dead code elimination
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_memory_optimization_pass() {
329        let pass = MemoryOptimizationPass::new();
330        assert_eq!(pass.name(), "MemoryOptimization");
331        assert_eq!(pass.priority(), 80);
332
333        let graph = Graph::new();
334        assert!(!pass.is_applicable(&graph));
335    }
336
337    #[test]
338    fn test_inplace_operations() {
339        let pass = MemoryOptimizationPass::new();
340
341        // Test that various operations can be in-place
342        assert!(pass.can_be_inplace("add"));
343        assert!(pass.can_be_inplace("relu"));
344        assert!(pass.can_be_inplace("exp"));
345
346        // Test that some operations cannot be in-place
347        assert!(!pass.can_be_inplace("matmul"));
348        assert!(!pass.can_be_inplace("conv2d"));
349    }
350
351    #[test]
352    fn test_memory_sharing() {
353        let pass = MemoryOptimizationPass::new();
354
355        // Test that element-wise operations can share memory
356        assert!(pass.can_share_memory("add"));
357        assert!(pass.can_share_memory("relu"));
358        assert!(pass.can_share_memory("sigmoid"));
359
360        // Test that shape-changing operations cannot share memory
361        assert!(!pass.can_share_memory("matmul"));
362        assert!(!pass.can_share_memory("conv2d"));
363        assert!(!pass.can_share_memory("reshape"));
364    }
365}