tenflowers_core/graph/optimization/
memory.rs1use super::passes::{get_node_inputs, get_node_outputs, OptimizationPass};
7use crate::graph::{Graph, NodeId};
8use crate::Result;
9use std::collections::{HashMap, HashSet};
10
11pub 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 fn analyze_tensor_lifetimes(&self, graph: &Graph) -> HashMap<NodeId, (usize, usize)> {
28 let mut lifetimes = HashMap::new();
29
30 let execution_order = self.topological_sort(graph);
32
33 for (position, &node_id) in execution_order.iter().enumerate() {
34 lifetimes.insert(node_id, (position, position));
36
37 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 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 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 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 #[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 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 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 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 if inputs.len() == 1 && outputs.len() == 1 {
120 let input_id = inputs[0];
121
122 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 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 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 fn optimize_memory_layout(&self, graph: &mut Graph) -> bool {
162 let mut changed = false;
163
164 let memory_groups = self.find_memory_sharing_groups(graph);
166
167 for group in memory_groups {
168 if group.len() > 1 {
169 for &node_id in &group {
171 if let Some(node) = graph.get_node_mut(node_id) {
172 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 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 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 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 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 fn can_share_memory(&self, operation: &str) -> bool {
239 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 let lifetimes = self.analyze_tensor_lifetimes(graph);
266
267 let inplace_ops = self.identify_inplace_opportunities(graph);
269
270 for (node_id, input_id) in inplace_ops {
272 if let Some(node) = graph.get_node_mut(node_id) {
273 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 if self.optimize_memory_layout(graph) {
288 changed = true;
289 }
290
291 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 graph.nodes().count() >= 2
316 }
317
318 fn priority(&self) -> u32 {
319 80 }
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 assert!(pass.can_be_inplace("add"));
343 assert!(pass.can_be_inplace("relu"));
344 assert!(pass.can_be_inplace("exp"));
345
346 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 assert!(pass.can_share_memory("add"));
357 assert!(pass.can_share_memory("relu"));
358 assert!(pass.can_share_memory("sigmoid"));
359
360 assert!(!pass.can_share_memory("matmul"));
362 assert!(!pass.can_share_memory("conv2d"));
363 assert!(!pass.can_share_memory("reshape"));
364 }
365}