1use crate::{FxGraph, Node, TorshResult};
4use petgraph::graph::NodeIndex;
5use petgraph::visit::EdgeRef;
6use std::collections::HashSet;
7use torsh_core::error::TorshError;
8
9pub struct PatternMatcher {
11 pattern: SubgraphPattern,
13}
14
15#[derive(Debug, Clone)]
17pub struct SubgraphPattern {
18 pub name: String,
20 pub operations: Vec<String>,
22 pub replacement: String,
24}
25
26impl SubgraphPattern {
27 pub fn new(name: String, operations: Vec<String>, replacement: String) -> Self {
29 Self {
30 name,
31 operations,
32 replacement,
33 }
34 }
35
36 pub fn linear_relu_fusion() -> Self {
38 Self::new(
39 "linear_relu_fusion".to_string(),
40 vec!["linear".to_string(), "relu".to_string()],
41 "linear_relu".to_string(),
42 )
43 }
44
45 pub fn conv_relu_fusion() -> Self {
47 Self::new(
48 "conv_relu_fusion".to_string(),
49 vec!["conv2d".to_string(), "relu".to_string()],
50 "conv2d_relu".to_string(),
51 )
52 }
53
54 pub fn conv_bn_fusion() -> Self {
56 Self::new(
57 "conv_bn_fusion".to_string(),
58 vec!["conv2d".to_string(), "batch_norm".to_string()],
59 "conv2d_bn".to_string(),
60 )
61 }
62
63 pub fn conv_bn_relu_fusion() -> Self {
65 Self::new(
66 "conv_bn_relu_fusion".to_string(),
67 vec![
68 "conv2d".to_string(),
69 "batch_norm".to_string(),
70 "relu".to_string(),
71 ],
72 "conv2d_bn_relu".to_string(),
73 )
74 }
75}
76
77#[derive(Debug)]
79pub struct PatternMatch {
80 pub nodes: Vec<NodeIndex>,
82 pub pattern: SubgraphPattern,
84}
85
86impl PatternMatcher {
87 pub fn new(pattern: SubgraphPattern) -> Self {
89 Self { pattern }
90 }
91
92 pub fn find_matches(&self, graph: &FxGraph) -> Vec<PatternMatch> {
94 let mut matches = Vec::new();
95
96 for (start_idx, start_node) in graph.nodes() {
98 if let Some(pattern_match) = self.match_pattern_at(graph, start_idx, start_node) {
99 matches.push(pattern_match);
100 }
101 }
102
103 matches
104 }
105
106 fn match_pattern_at(
108 &self,
109 graph: &FxGraph,
110 start_idx: NodeIndex,
111 start_node: &Node,
112 ) -> Option<PatternMatch> {
113 if let Node::Call(op_name, _) = start_node {
115 if self.pattern.operations.is_empty() || &self.pattern.operations[0] != op_name {
116 return None;
117 }
118 } else {
119 return None;
120 }
121
122 if let Some(matched_nodes) = self.match_sequence(graph, start_idx, &self.pattern.operations)
124 {
125 return Some(PatternMatch {
126 nodes: matched_nodes,
127 pattern: self.pattern.clone(),
128 });
129 }
130
131 None
132 }
133
134 fn match_sequence(
136 &self,
137 graph: &FxGraph,
138 start_idx: NodeIndex,
139 operations: &[String],
140 ) -> Option<Vec<NodeIndex>> {
141 if operations.is_empty() {
142 return Some(vec![]);
143 }
144
145 let mut current_nodes = vec![start_idx];
146 let mut matched_nodes = vec![start_idx];
147
148 for expected_op in &operations[1..] {
150 let mut next_nodes = Vec::new();
151
152 for ¤t_idx in ¤t_nodes {
153 let successors: Vec<_> = graph
155 .graph
156 .neighbors_directed(current_idx, petgraph::Direction::Outgoing)
157 .collect();
158
159 for successor_idx in successors {
160 if let Some(Node::Call(op_name, _)) = graph.get_node(successor_idx) {
161 if op_name == expected_op {
162 next_nodes.push(successor_idx);
163 matched_nodes.push(successor_idx);
164 }
165 }
166 }
167 }
168
169 if next_nodes.is_empty() {
170 return None; }
172
173 current_nodes = next_nodes;
174 }
175
176 Some(matched_nodes)
177 }
178}
179
180pub struct SubgraphRewriter {
182 patterns: Vec<SubgraphPattern>,
183}
184
185impl SubgraphRewriter {
186 pub fn new() -> Self {
188 Self {
189 patterns: Vec::new(),
190 }
191 }
192
193 pub fn add_pattern(&mut self, pattern: SubgraphPattern) {
195 self.patterns.push(pattern);
196 }
197
198 pub fn with_common_fusions() -> Self {
200 let mut rewriter = Self::new();
201 rewriter.add_pattern(SubgraphPattern::linear_relu_fusion());
202 rewriter.add_pattern(SubgraphPattern::conv_relu_fusion());
203 rewriter.add_pattern(SubgraphPattern::conv_bn_fusion());
204 rewriter.add_pattern(SubgraphPattern::conv_bn_relu_fusion());
205 rewriter
206 }
207
208 pub fn apply(&self, graph: &mut FxGraph) -> TorshResult<usize> {
210 let mut total_replacements = 0;
211
212 for pattern in &self.patterns {
213 let replacements = self.apply_pattern(graph, pattern)?;
214 total_replacements += replacements;
215 }
216
217 Ok(total_replacements)
218 }
219
220 fn apply_pattern(&self, graph: &mut FxGraph, pattern: &SubgraphPattern) -> TorshResult<usize> {
225 let matcher = PatternMatcher::new(pattern.clone());
226 let mut replacements = 0;
227 let mut budget = graph.node_count() + 1;
230
231 while budget > 0 {
232 budget -= 1;
233
234 let next_match = matcher
235 .find_matches(graph)
236 .into_iter()
237 .find(|candidate| Self::is_legal_match(graph, candidate));
238
239 match next_match {
240 Some(pattern_match) => {
241 self.replace_match(graph, &pattern_match)?;
242 replacements += 1;
243 }
244 None => break,
245 }
246 }
247
248 Ok(replacements)
249 }
250
251 fn is_legal_match(graph: &FxGraph, pattern_match: &PatternMatch) -> bool {
258 let nodes = &pattern_match.nodes;
259 if nodes.len() != pattern_match.pattern.operations.len() || nodes.is_empty() {
260 return false;
261 }
262
263 let unique: HashSet<NodeIndex> = nodes.iter().copied().collect();
264 if unique.len() != nodes.len() {
265 return false;
266 }
267
268 for (position, &node_idx) in nodes.iter().enumerate() {
269 match graph.get_node(node_idx) {
270 Some(Node::Call(op_name, _))
271 if *op_name == pattern_match.pattern.operations[position] => {}
272 _ => return false,
273 }
274
275 if position + 1 < nodes.len() {
276 let next_idx = nodes[position + 1];
277 if graph.graph.find_edge(node_idx, next_idx).is_none() {
278 return false;
279 }
280 let consumers: HashSet<NodeIndex> = graph
282 .graph
283 .neighbors_directed(node_idx, petgraph::Direction::Outgoing)
284 .collect();
285 if consumers.len() != 1 || !consumers.contains(&next_idx) {
286 return false;
287 }
288 }
289 }
290
291 true
292 }
293
294 fn replace_match(&self, graph: &mut FxGraph, pattern_match: &PatternMatch) -> TorshResult<()> {
296 if pattern_match.nodes.is_empty() {
297 return Ok(());
298 }
299
300 let first_node_idx = pattern_match.nodes[0];
301 let matched: HashSet<NodeIndex> = pattern_match.nodes.iter().copied().collect();
302
303 let mut args = if let Some(Node::Call(_, args)) = graph.get_node(first_node_idx) {
305 args.clone()
306 } else {
307 vec![]
308 };
309
310 for &node_idx in &pattern_match.nodes[1..] {
315 let incoming: Vec<(NodeIndex, crate::Edge)> = graph
316 .graph
317 .edges_directed(node_idx, petgraph::Direction::Incoming)
318 .filter(|edge| !matched.contains(&edge.source()))
319 .map(|edge| (edge.source(), edge.weight().clone()))
320 .collect();
321 for (source_idx, weight) in incoming {
322 if !args.contains(&weight.name) {
323 args.push(weight.name.clone());
324 }
325 if graph.graph.find_edge(source_idx, first_node_idx).is_none() {
326 graph.graph.add_edge(source_idx, first_node_idx, weight);
327 }
328 }
329
330 let outgoing: Vec<(NodeIndex, crate::Edge)> = graph
331 .graph
332 .edges_directed(node_idx, petgraph::Direction::Outgoing)
333 .filter(|edge| !matched.contains(&edge.target()))
334 .map(|edge| (edge.target(), edge.weight().clone()))
335 .collect();
336 for (target_idx, weight) in outgoing {
337 if graph.graph.find_edge(first_node_idx, target_idx).is_none() {
338 graph.graph.add_edge(first_node_idx, target_idx, weight);
339 }
340 }
341
342 graph.redirect_boundary_node(node_idx, first_node_idx);
344 }
345
346 graph.graph[first_node_idx] = Node::Call(pattern_match.pattern.replacement.clone(), args);
348
349 let to_remove: HashSet<NodeIndex> = pattern_match.nodes[1..].iter().copied().collect();
352 if !to_remove.is_empty() {
353 graph.remove_nodes(&to_remove);
354 }
355
356 Ok(())
357 }
358}
359
360impl Default for SubgraphRewriter {
361 fn default() -> Self {
362 Self::new()
363 }
364}
365
366pub fn replace_pattern(graph: &mut FxGraph, pattern: &str, _replacement: &str) -> TorshResult<()> {
368 let pattern_obj = match pattern {
369 "linear->relu" => SubgraphPattern::linear_relu_fusion(),
370 "conv2d->relu" => SubgraphPattern::conv_relu_fusion(),
371 "conv2d->batch_norm" => SubgraphPattern::conv_bn_fusion(),
372 "conv2d->batch_norm->relu" => SubgraphPattern::conv_bn_relu_fusion(),
373 _ => {
374 return Err(TorshError::InvalidArgument(format!(
375 "Unknown pattern: {}",
376 pattern
377 )));
378 }
379 };
380
381 let mut rewriter = SubgraphRewriter::new();
382 rewriter.add_pattern(pattern_obj);
383 rewriter.apply(graph)?;
384
385 Ok(())
386}
387
388pub fn apply_fusion_optimizations(graph: &mut FxGraph) -> TorshResult<usize> {
390 let rewriter = SubgraphRewriter::with_common_fusions();
391 rewriter.apply(graph)
392}
393
394pub fn replace_operation_sequence(
396 graph: &mut FxGraph,
397 sequence: &[&str],
398 replacement: &str,
399) -> TorshResult<()> {
400 let operations: Vec<String> = sequence.iter().map(|s| s.to_string()).collect();
401 let pattern = SubgraphPattern::new(
402 "custom_pattern".to_string(),
403 operations,
404 replacement.to_string(),
405 );
406
407 let mut rewriter = SubgraphRewriter::new();
408 rewriter.add_pattern(pattern);
409 rewriter.apply(graph)?;
410
411 Ok(())
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use crate::tracer::ModuleTracer;
418
419 #[test]
420 fn test_pattern_creation() {
421 let pattern = SubgraphPattern::linear_relu_fusion();
422 assert_eq!(pattern.name, "linear_relu_fusion");
423 assert_eq!(pattern.operations, vec!["linear", "relu"]);
424 assert_eq!(pattern.replacement, "linear_relu");
425 }
426
427 #[test]
428 fn test_pattern_matching() {
429 let mut tracer = ModuleTracer::new();
430 tracer.add_input("x");
431 tracer.add_call("linear", vec!["x".to_string()]);
432 tracer.add_call("relu", vec!["node_0".to_string()]);
433 tracer.add_output("node_1");
434 let graph = tracer.finalize();
435
436 let pattern = SubgraphPattern::linear_relu_fusion();
437 let matcher = PatternMatcher::new(pattern);
438 let matches = matcher.find_matches(&graph);
439
440 assert!(!matches.is_empty());
441 }
442
443 #[test]
444 fn test_subgraph_rewriting() {
445 let mut tracer = ModuleTracer::new();
446 tracer.add_input("x");
447 tracer.add_call("linear", vec!["x".to_string()]);
448 tracer.add_call("relu", vec!["node_0".to_string()]);
449 tracer.add_output("node_1");
450 let mut graph = tracer.finalize();
451
452 let original_node_count = graph.node_count();
453
454 let mut rewriter = SubgraphRewriter::new();
455 rewriter.add_pattern(SubgraphPattern::linear_relu_fusion());
456 let replacements = rewriter.apply(&mut graph).unwrap();
457
458 assert!(replacements > 0);
459 assert!(graph.node_count() < original_node_count);
461 }
462
463 #[test]
464 fn test_convenience_functions() {
465 let mut tracer = ModuleTracer::new();
466 tracer.add_input("x");
467 tracer.add_call("linear", vec!["x".to_string()]);
468 tracer.add_call("relu", vec!["node_0".to_string()]);
469 tracer.add_output("node_1");
470 let mut graph = tracer.finalize();
471
472 assert!(replace_pattern(&mut graph, "linear->relu", "linear_relu").is_ok());
474
475 let mut tracer2 = ModuleTracer::new();
477 tracer2.add_input("x");
478 tracer2.add_call("conv2d", vec!["x".to_string()]);
479 tracer2.add_call("batch_norm", vec!["node_0".to_string()]);
480 tracer2.add_call("relu", vec!["node_1".to_string()]);
481 tracer2.add_output("node_2");
482 let mut graph2 = tracer2.finalize();
483
484 assert!(replace_operation_sequence(
485 &mut graph2,
486 &["conv2d", "batch_norm", "relu"],
487 "conv2d_bn_relu"
488 )
489 .is_ok());
490 }
491
492 #[test]
493 fn test_fusion_optimizations() {
494 let mut tracer = ModuleTracer::new();
495 tracer.add_input("x");
496 tracer.add_call("conv2d", vec!["x".to_string()]);
497 tracer.add_call("relu", vec!["node_0".to_string()]);
498 tracer.add_output("node_1");
499 let mut graph = tracer.finalize();
500
501 let _replacements = apply_fusion_optimizations(&mut graph).unwrap();
502 }
504}